/* Knowledge base — the designers' process notes, split into collapsible
   sections and kept per product (processes differ between products).
   Shared through /api/kb: admins and a product's managers edit, everyone reads. */
(function () {
const { useState, useEffect, useRef, useMemo } = React;

const KBKEY = "nove8_kb_v1";
const uid = () => "kb_" + Date.now().toString(36) + Math.floor(Math.random() * 1e6).toString(36);

/* The first version, split out of the team's existing process document. */
const DEFAULT_SECTIONS = [
  {
    title: "Где находятся задачи",
    body: "Все задачи находятся в Airtable.\n\nПриоритет секторов: 1-й — ASAP, 2-й — Loc, 3-й — All."
  },
  {
    title: "Как взять задачу в работу",
    body: "1. Берём верхнюю карточку задачи из одного из трёх секторов (приоритеты: 1-й — ASAP, 2-й — Loc, 3-й — All).\n"
      + "2. Открываем карточку и в первую очередь меняем исполнительного дизайнера на своё имя и инициалы из списка.\n"
      + "3. Меняем статус задачи на «in progress»."
  },
  {
    title: "Работа над задачей",
    body: "4. Можно приступать к задаче: читать ТЗ, создавать проект в афтере, задавать вопросы тому, кто писал задачу, и т.д.\n"
      + "5. Важно в проекте использовать нейминг, который сгенерировался в названии задачи. В нашем случае:\n"
      + "3283_UGCai_Veo3_rct_dsc_YL_DD_1225\n"
      + "6. Для удобства можно использовать скрипт, который создаёт папку, проект и композицию с одним и тем же неймингом."
  },
  {
    title: "Нейминг вариаций",
    body: "7. Каждый креатив должен иметь последовательный номер вариации, соответственно в каждой композиции нужно прописывать номер вариации вручную:\n"
      + "3283_1_UGCai_Veo3_rct_dsc_YL_DD_1225\n"
      + "3283_2_UGCai_Veo3_rct_dsc_YL_DD_1225\n"
      + "3283_3_UGCai_Veo3_rct_dsc_YL_DD_1225\n"
      + "3283_4_UGCai_Veo3_rct_dsc_YL_DD_1225"
  },
  {
    title: "Согласование первой вариации с СММ",
    body: "8. После выполнения первой вариации меняем статус задачи на «in review». Дальше нужно апрувить первую вариацию у СММ, который указан в задаче. СММ ищем по имени в Slack, присылаем первую вариацию в ЛС со ссылкой на задачу.\n"
      + "9. Доделываем остальные вариации после апрува."
  },
  {
    title: "Загрузка креативов и упаковка проекта",
    body: "10. Загружаем все креативы на гугл диск по ссылке, указанной в задаче.\n"
      + "11. Соответственно чистим и упаковываем проект и загружаем проект по той же ссылке."
  },
  {
    title: "Закрытие задачи",
    body: "12. Как только задача выгружена на гугл диск, можно менять статус на «Done». После чего в Slack в чате Creative_prod должно появиться сообщение о выполненной задаче и сами креативы."
  },
  {
    title: "Оценка в стори-поинтах",
    body: "13. Далее задачу стоит оценить в стори-поинтах через нашу таблицу (в первый раз обязательно напишите Anton Savchenko, чтобы посчитать вместе).\n\n"
      + "После того как задача оценена, сумму стори-поинтов нужно указать в карточке с задачей, а также сделать скриншот таблицы с подсчётами и приложить его в Files к задаче."
  },
  {
    title: "Личная таблица",
    body: "14. Заполняем личную таблицу со стори-поинтами: тип задачи, количество стори-поинтов и ссылка на задачу. Заполняем обязательно после каждой выполненной задачи."
  },
  {
    title: "Готово",
    body: "15. Готово! Возвращаемся к пункту 1."
  },
];

const seedSections = () => DEFAULT_SECTIONS.map((s) => ({ id: uid(), title: s.title, body: s.body }));

function loadLocal() {
  try {
    const raw = localStorage.getItem(KBKEY);
    const doc = raw ? JSON.parse(raw) : null;
    if (doc && doc.byProduct && typeof doc.byProduct === "object") return doc.byProduct;
  } catch (e) {}
  return {};
}

/* Sections are kept per product AND per position, because a motion designer,
   a graphic designer and an innovation manager work differently. Documents
   written before positions existed are a plain array — they are read as the
   first position's sections. */
function sectionsAt(byProduct, pid, pos, firstPos) {
  const entry = byProduct && byProduct[pid];
  if (!entry) return [];
  if (Array.isArray(entry)) return pos === firstPos ? entry : [];
  return Array.isArray(entry[pos]) ? entry[pos] : [];
}
function withSections(byProduct, pid, pos, next, firstPos) {
  const entry = byProduct[pid];
  const base = Array.isArray(entry) ? { [firstPos]: entry } : { ...(entry || {}) };
  base[pos] = next;
  return { ...byProduct, [pid]: base };
}

/* Bodies are stored as a small, whitelisted subset of HTML — what the editor
   produces. Anything else is stripped on read AND on save, so a bad paste (or
   a bad actor) cannot inject markup. Notes written before the visual editor are
   plain text with **bold** / *italic* marks and are converted on the fly. */
const ALLOWED_TAGS = {
  B: [], STRONG: [], I: [], EM: [], U: [], BR: [], DIV: [], P: [],
  UL: [], OL: [], LI: [], A: ["href"],
};

function sanitizeHtml(html) {
  try {
    const doc = new DOMParser().parseFromString('<div id="kbroot">' + String(html == null ? "" : html) + "</div>", "text/html");
    const root = doc.getElementById("kbroot");
    const walk = (node) => {
      for (const child of Array.from(node.childNodes)) {
        if (child.nodeType === 3) continue;              // text stays
        if (child.nodeType !== 1) { child.remove(); continue; }
        const tag = child.tagName;
        if (!ALLOWED_TAGS[tag]) {                        // unwrap, keep the text
          while (child.firstChild) node.insertBefore(child.firstChild, child);
          child.remove();
          continue;
        }
        for (const attr of Array.from(child.attributes)) {
          if (!ALLOWED_TAGS[tag].includes(attr.name.toLowerCase())) child.removeAttribute(attr.name);
        }
        if (tag === "A") {
          const href = child.getAttribute("href") || "";
          if (!/^https?:\/\//i.test(href)) child.removeAttribute("href");
          else { child.setAttribute("target", "_blank"); child.setAttribute("rel", "noopener noreferrer"); }
        }
        walk(child);
      }
    };
    walk(root);
    return root.innerHTML;
  } catch (e) { return ""; }
}

const esc = (t) => String(t == null ? "" : t)
  .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");

/* legacy plain text (with **bold**, *italic*, [label](url), bare links) → HTML */
function legacyToHtml(text) {
  let out = esc(text);
  out = out.replace(/\[([^\]\n]+)\]\((https?:\/\/[^)\s]+)\)/g,
    (m, label, url) => '<a href="' + url + '">' + label + "</a>");
  out = out.replace(/(^|[\s(])(https?:\/\/[^\s<]+)/g,
    (m, pre, url) => pre + '<a href="' + url.replace(/[.,;:)]+$/, "") + '">' + url.replace(/[.,;:)]+$/, "") + "</a>");
  out = out.replace(/\*\*([\s\S]+?)\*\*/g, "<b>$1</b>");
  out = out.replace(/(^|[^*])\*([^*\n]+?)\*/g, "$1<i>$2</i>");
  out = out.replace(/\n/g, "<br>");
  return out;
}

const isHtmlBody = (s) => /<(b|strong|i|em|u|a|br|div|p|ul|ol|li)\b[^>]*>/i.test(String(s || ""));
const bodyToHtml = (body) => sanitizeHtml(isHtmlBody(body) ? body : legacyToHtml(body));

function RichText({ text }) {
  const html = useMemo(() => bodyToHtml(text), [text]);
  return <div className="kb-body" dangerouslySetInnerHTML={{ __html: html }} />;
}

function Section({ section, open, canEdit, first, last, onToggle, onPatch, onRemove, onMove }) {
  const [editing, setEditing] = useState(false);
  const [draftTitle, setDraftTitle] = useState(section.title);
  const edRef = useRef(null);
  useEffect(() => { setDraftTitle(section.title); }, [section.id]);

  // The body is edited in place: bold / italic apply straight away, so what is
  // typed is what the reader sees. Filled once — React must not re-render it,
  // or the caret jumps.
  useEffect(() => {
    if (!editing || !edRef.current) return;
    edRef.current.innerHTML = bodyToHtml(section.body) || "";
    try { document.execCommand("styleWithCSS", false, false); } catch (e) {} // <b>/<i>, not styles
    edRef.current.focus();
  }, [editing, section.id]);

  const exec = (cmd) => {
    const el = edRef.current;
    if (!el) return;
    el.focus();
    try { document.execCommand(cmd, false, null); } catch (e) {}
  };

  /* Links: the selection has to survive the prompt (asking for the address
     blurs the editor and browsers drop the range), so it is saved first and
     restored before the link is applied. */
  const savedRange = useRef(null);
  const saveRange = () => {
    const sel = window.getSelection();
    const el = edRef.current;
    if (!sel || sel.rangeCount === 0 || !el) return;
    const r = sel.getRangeAt(0);
    if (el.contains(r.commonAncestorContainer)) savedRange.current = r.cloneRange();
  };
  const restoreRange = () => {
    const el = edRef.current;
    if (!el) return;
    el.focus();
    const r = savedRange.current;
    if (!r) return;
    const sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(r);
  };
  const applyLink = (url) => {
    const el = edRef.current;
    if (!el || !url) return;
    restoreRange();
    let ok = false;
    try { ok = document.execCommand("createLink", false, url); } catch (e) { ok = false; }
    if (ok) return;
    // fallback for browsers that refuse createLink on a restored range
    const sel = window.getSelection();
    if (!sel || sel.rangeCount === 0) return;
    const range = sel.getRangeAt(0);
    const a = document.createElement("a");
    a.setAttribute("href", url);
    if (range.collapsed) { a.textContent = url; range.insertNode(a); }
    else { a.appendChild(range.extractContents()); range.insertNode(a); }
  };
  const promptLink = () => {
    saveRange();
    const sel = window.getSelection();
    const had = sel && !sel.isCollapsed;
    const url = prompt(had ? "Link address for the selected text" : "Link address", "https://");
    if (!url || !/^https?:\/\//i.test(url.trim())) return;
    applyLink(url.trim());
  };
  const onKeyDown = (e) => {
    if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) { e.preventDefault(); promptLink(); }
  };

  // Pasting a link onto selected text turns that text into the link; anything
  // else is pasted as plain text so foreign formatting never sneaks in.
  const onPaste = (e) => {
    const text = (e.clipboardData || window.clipboardData).getData("text/plain") || "";
    e.preventDefault();
    const sel = window.getSelection();
    const hasSelection = sel && !sel.isCollapsed;
    const isUrl = /^https?:\/\/\S+$/i.test(text.trim());
    try {
      if (isUrl) { saveRange(); applyLink(text.trim()); }
      else document.execCommand("insertText", false, text);
    } catch (err) {}
  };

  const startEdit = (e) => { e.stopPropagation(); if (!open) onToggle(); setEditing(true); };
  const save = () => {
    const html = edRef.current ? sanitizeHtml(edRef.current.innerHTML) : section.body;
    onPatch({ title: draftTitle.trim() || "Untitled", body: html });
    setEditing(false);
  };

  return (
    <div className={"kb-sec" + (open ? " kb-sec-open" : "")}>
      <div className="kb-sec-head" onClick={onToggle}>
        <span className="kb-sec-plus" aria-hidden="true">{open ? "−" : "+"}</span>
        <span className="kb-sec-title">{section.title}</span>
        {canEdit && (
          <span className="kb-sec-tools" onClick={(e) => e.stopPropagation()}>
            <button className="kb-tool" title="Move up" disabled={first} onClick={() => onMove(-1)}>↑</button>
            <button className="kb-tool" title="Move down" disabled={last} onClick={() => onMove(1)}>↓</button>
            <button className="kb-tool" title="Edit this section" onClick={startEdit}>Edit</button>
            <button className="kb-tool kb-tool-del" title="Delete this section"
              onClick={() => { if (confirm(`Delete «${section.title}»?`)) onRemove(); }}>×</button>
          </span>
        )}
      </div>
      {open && (
        <div className="kb-sec-body">
          {editing ? (
            <React.Fragment>
              <input className="kb-input" value={draftTitle} placeholder="Section name"
                onChange={(e) => setDraftTitle(e.target.value)} />
              <div className="kb-tb">
                <button className="kb-tb-btn" title="Bold (Ctrl/⌘ + B)"
                  onMouseDown={(e) => e.preventDefault()} onClick={() => exec("bold")}><b>B</b></button>
                <button className="kb-tb-btn" title="Italic (Ctrl/⌘ + I)"
                  onMouseDown={(e) => e.preventDefault()} onClick={() => exec("italic")}><i>I</i></button>
                <button className="kb-tb-btn" title="Turn the selected text into a link (Ctrl/⌘ + K)"
                  onMouseDown={(e) => { e.preventDefault(); saveRange(); }} onClick={promptLink}>Link</button>
                <span className="kb-tb-hint">select a word → Link, or paste a URL onto it</span>
              </div>
              <div ref={edRef} className="kb-editor" contentEditable suppressContentEditableWarning
                onPaste={onPaste} onKeyDown={onKeyDown} onMouseUp={saveRange} onKeyUp={saveRange} />
              <div className="kb-edit-foot">
                <button className="kb-btn ghost" onClick={() => setEditing(false)}>Cancel</button>
                <button className="kb-btn primary" onClick={save}>Save</button>
              </div>
            </React.Fragment>
          ) : (
            <RichText text={section.body} />
          )}
        </div>
      )}
    </div>
  );
}

function KnowledgeBase({ products, positions, posStyle, role, me }) {
  const [byProduct, setByProduct] = useState(loadLocal);
  const [pid, setPid] = useState(() => {
    try { return localStorage.getItem("nove8_kb_product") || ""; } catch (e) { return ""; }
  });
  const posList = (positions && positions.length) ? positions : ["Motion Designer"];
  const firstPos = posList[0];
  const [pos, setPos] = useState(() => {
    try {
      const saved = localStorage.getItem("nove8_kb_position") || "";
      return saved && posList.includes(saved) ? saved : firstPos;
    } catch (e) { return firstPos; }
  });
  useEffect(() => { if (!posList.includes(pos)) setPos(firstPos); }, [positions]);
  useEffect(() => { try { localStorage.setItem("nove8_kb_position", pos || ""); } catch (e) {} }, [pos]);
  const [open, setOpen] = useState(() => new Set());
  const dirty = useRef(false);
  const loaded = useRef(false);

  // pick a product once the roster is known
  useEffect(() => {
    if (!products.length) return;
    if (!pid || !products.some((p) => p.id === pid)) setPid(products[0].id);
  }, [products, pid]);
  useEffect(() => { try { localStorage.setItem("nove8_kb_product", pid || ""); } catch (e) {} }, [pid]);

  // who may edit: admins everywhere, a manager for the products they run
  const canEdit = useMemo(() => {
    if (role === "admin") return true;
    if (role !== "manager") return false;
    return Array.isArray(me && me.products) ? me.products.includes(pid) : false;
  }, [role, me, pid]);

  // local cache + publish edits
  useEffect(() => {
    try { localStorage.setItem(KBKEY, JSON.stringify({ byProduct })); } catch (e) {}
    if (!dirty.current) return;
    dirty.current = false;
    try { localStorage.setItem("nove8_kb_edit_ts", String(Date.now())); } catch (e) {}
    fetch("/api/kb", {
      method: "PUT",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ byProduct }),
    }).catch(() => {});
  }, [byProduct]);

  // server wins on load, then keep fresh in the background
  useEffect(() => {
    let alive = true;
    const pull = () => {
      fetch("/api/kb", { cache: "no-store" })
        .then((r) => (r.ok ? r.json() : null))
        .then((j) => {
          if (!alive) return;
          loaded.current = true;
          const srv = j && j.state && j.state.byProduct;
          if (!srv || typeof srv !== "object") return;
          try {
            const ts = Number(localStorage.getItem("nove8_kb_edit_ts") || 0);
            if (Date.now() - ts < 45000) return; // don't stomp an in-flight edit
          } catch (e) {}
          setByProduct((cur) => (JSON.stringify(cur) === JSON.stringify(srv) ? cur : srv));
        })
        .catch(() => {});
    };
    pull();
    const id = setInterval(pull, 30000);
    return () => { alive = false; clearInterval(id); };
  }, []);

  const sections = pid ? sectionsAt(byProduct, pid, pos, firstPos) : [];
  const setSections = (next) => {
    dirty.current = true;
    setByProduct((cur) => withSections(cur, pid, pos, next, firstPos));
  };

  const addSection = () => {
    setSections([...sections, { id: uid(), title: "New section", body: "" }]);
    setOpen((o) => new Set(o));
  };
  const patchSection = (id, patch) => setSections(sections.map((s) => s.id === id ? { ...s, ...patch } : s));
  const removeSection = (id) => setSections(sections.filter((s) => s.id !== id));
  const moveSection = (id, dir) => {
    const i = sections.findIndex((s) => s.id === id);
    const j = i + dir;
    if (i < 0 || j < 0 || j >= sections.length) return;
    const next = sections.slice();
    const tmp = next[i]; next[i] = next[j]; next[j] = tmp;
    setSections(next);
  };
  const seedFromDefault = () => setSections(seedSections());
  const copyFrom = (fromPid, fromPos) => {
    const src = sectionsAt(byProduct, fromPid, fromPos, firstPos);
    if (!src.length) return;
    if (sections.length && !confirm("Replace the sections of this product and position with a copy?")) return;
    setSections(src.map((x) => ({ id: uid(), title: x.title, body: x.body })));
  };

  const toggle = (id) => setOpen((o) => { const n = new Set(o); n.has(id) ? n.delete(id) : n.add(id); return n; });
  const allOpen = sections.length > 0 && sections.every((s) => open.has(s.id));
  const toggleAll = () => setOpen(allOpen ? new Set() : new Set(sections.map((s) => s.id)));

  // every other product × position combination that already has content
  const others = [];
  for (const p of products) {
    for (const q of posList) {
      if (p.id === pid && q === pos) continue;
      if (sectionsAt(byProduct, p.id, q, firstPos).length === 0) continue;
      others.push({ pid: p.id, pos: q, label: p.name + " · " + ((posStyle && posStyle[q] && posStyle[q].short) || q) });
    }
  }

  return (
    <div className="kb">
      <div className="kb-toolbar">
        <h1>Knowledge base</h1>
        <p>How we work, step by step · the process can differ per product</p>
      </div>

      <div className="kb-scroll">
      <div className="kb-products">
        {products.map((p) => (
          <button key={p.id} className={"kb-prod" + (p.id === pid ? " on" : "")}
            style={p.id === pid ? { background: p.color, borderColor: p.color } : undefined}
            onClick={() => { setPid(p.id); setOpen(new Set()); }}>{p.name}</button>
        ))}
        {products.length === 0 && <span className="kb-empty">No products yet</span>}
      </div>

      <div className="kb-positions">
        {posList.map((q) => (
          <button key={q} className={"kb-pos" + (q === pos ? " on" : "")}
            onClick={() => { setPos(q); setOpen(new Set()); }}>
            <span className="kb-pos-dot" style={{ background: (posStyle && posStyle[q] && posStyle[q].dot) || "#c2c8d0" }}></span>
            {(posStyle && posStyle[q] && posStyle[q].short) || q}
          </button>
        ))}
        <div className="kb-spacer"></div>
        {sections.length > 0 && (
          <button className="kb-btn ghost" onClick={toggleAll}>{allOpen ? "Collapse all" : "Expand all"}</button>
        )}
      </div>

      <div className="kb-list">
        {sections.map((s, i) => (
          <Section key={s.id} section={s} open={open.has(s.id)} canEdit={canEdit}
            first={i === 0} last={i === sections.length - 1}
            onToggle={() => toggle(s.id)}
            onPatch={(patch) => patchSection(s.id, patch)}
            onRemove={() => removeSection(s.id)}
            onMove={(dir) => moveSection(s.id, dir)} />
        ))}

        {sections.length === 0 && (
          <div className="kb-blank">
            <b>Nothing here yet</b>
            {canEdit
              ? <span>Start from the standard creative process, copy another product’s, or add sections yourself.</span>
              : <span>The process for this product and position hasn’t been written up yet.</span>}
            {canEdit && (
              <div className="kb-blank-actions">
                <button className="kb-btn primary" onClick={seedFromDefault}>Use the standard process</button>
                {others.map((o) => (
                  <button key={o.pid + o.pos} className="kb-btn ghost" onClick={() => copyFrom(o.pid, o.pos)}>Copy from {o.label}</button>
                ))}
              </div>
            )}
          </div>
        )}

        {canEdit && sections.length > 0 && (
          <div className="kb-foot">
            <button className="kb-btn ghost" onClick={addSection}>+ Add section</button>
            {others.map((o) => (
              <button key={o.pid + o.pos} className="kb-btn ghost" onClick={() => copyFrom(o.pid, o.pos)}>Copy from {o.label}</button>
            ))}
          </div>
        )}
      </div>
      </div>
    </div>
  );
}

window.KnowledgeBase = KnowledgeBase;
})();
