/* iBD Coliseum — JSON-described pages, edited by clicking them.
 *
 * A page is a document: a hero and a list of blocks, each naming a type the
 * stylesheet already has. The app renders it with its own components, so an
 * uploaded page arrives in the Coliseum's fonts and colours and behaves on a
 * phone — and a block type we do not have is reported rather than rendered.
 * Nothing uploaded becomes code.
 *
 * The editor renders that document at half size and makes every piece of text
 * in it clickable. The people doing this are not coders: asking them to find a
 * string inside JSON is asking them to not do it at all.
 */


/* Which pages can be edited, and how.
 *
 * "doc" pages are described entirely by a document — How to Play is all words,
 * so an uploaded document can own the whole thing.
 *
 * Every other page is mostly live data: the Season page is cards, votes and a
 * leaderboard, and replacing it with a document would throw those away. Those
 * are "slots" — the handful of strings that are actually written by a person
 * rather than computed. The page keeps rendering itself; only the words are
 * ours to change.
 */
const PAGE_REGISTRY = [
  { id: "how-to-play", label: "How to Play", kind: "doc" },
  {
    id: "hall-of-fame", label: "Hall of Fame", kind: "slots", hero: "dark",
    slots: [
      { key: "title", label: "Title", def: "Hall of" },
      { key: "titleAccent", label: "Title · accent word", def: "Fame" },
      { key: "sub", label: "Subtitle", def: "The work that earned recognition — Crowns and Season Champions.", long: true },
    ],
  },
  {
    id: "library", label: "Library", kind: "slots", hero: "dark",
    slots: [
      { key: "title", label: "Title", def: "AI" },
      { key: "titleAccent", label: "Title · accent word", def: "Library" },
      { key: "sub", label: "Subtitle", def: "Useful prompts, workflows, and creations shared across iBD.", long: true },
    ],
  },
  {
    id: "season", label: "Season", kind: "slots", hero: "dark",
    note: "The title is built from the current season, so it is not editable here.",
    slots: [
      { key: "sub", label: "Subtitle", def: "Share what works. Back what helps. Climb the ranks.", long: true },
      { key: "prizeTitle", label: "Prize · title", def: "Season Champion prize" },
      { key: "prizeSub", label: "Prize · detail", def: "Gift card" },
    ],
  },
  {
    id: "arena-closed", label: "Arena · closed messages", kind: "slots", hero: "light",
    note: "What the Arena says once submissions close, and again once voting closes.",
    slots: [
      { key: "soonBadge", label: "Before it opens · badge", def: "Opening soon" },
      { key: "soonTitle", label: "Before it opens · heading", def: "The next Quest is on its way" },
      { key: "soonBody", label: "Before it opens · text", long: true,
        def: "The Arena opens shortly. In the meantime the Season keeps running — share what works and back what helps." },
      { key: "votingBadge", label: "Voting open · badge", def: "Submissions closed · Voting open" },
      { key: "votingTitle", label: "Voting open · heading", def: "Submissions are closed" },
      { key: "votingBody", label: "Voting open · text", long: true,
        def: "Arena voting is still open. Cast your votes below — the Top 5 in each Crown move to the Council. Future Quests will be announced accordingly." },
      { key: "shutBadge", label: "Fully closed · badge", def: "Closed" },
      { key: "shutTitle", label: "Fully closed · heading", def: "The Arena is closed right now" },
      { key: "shutBody", label: "Fully closed · text", long: true,
        def: "Future Quests will be announced accordingly. In the meantime, the Season keeps running — share what works and back what helps." },
    ],
  },
];

function pageEntry(id) { return PAGE_REGISTRY.find((p) => p.id === id); }

// Defaults for a slots page, so the editor always opens on the real wording.
function slotDefaults(id) {
  const entry = pageEntry(id);
  const out = {};
  (entry && entry.slots ? entry.slots : []).forEach((f) => { out[f.key] = f.def; });
  return out;
}

// What a page shows for one slot: the edited value if there is one, the
// built-in wording otherwise. Pages call this instead of holding the string.
function makePageText(overrides) {
  return function pageText(pageId, key) {
    const o = overrides && overrides[pageId];
    const v = o && o.slots ? o.slots[key] : undefined;
    if (v !== undefined && String(v).trim() !== "") return v;
    const d = slotDefaults(pageId);
    return d[key] !== undefined ? d[key] : "";
  };
}

const PAGE_BLOCKS = ["heading", "steps", "cards", "ranks", "meta", "text", "flow"];

function validatePageDoc(doc) {
  const errors = [];
  if (!doc || typeof doc !== "object") return ["The file is not a JSON object."];
  if (!doc.page) errors.push('Missing "page" — which page this replaces, e.g. "how-to-play".');
  if (doc.hero && typeof doc.hero !== "object") errors.push('"hero" must be an object.');
  if (!Array.isArray(doc.sections)) {
    errors.push('Missing "sections" — a list of blocks.');
    return errors;
  }
  doc.sections.forEach((b, i) => {
    const at = `sections[${i}]`;
    if (!b || typeof b !== "object") { errors.push(`${at} is not an object.`); return; }
    if (!b.type) { errors.push(`${at} has no "type".`); return; }
    if (!PAGE_BLOCKS.includes(b.type)) {
      errors.push(`${at}: "${b.type}" is not a block we render. Available: ${PAGE_BLOCKS.join(", ")}.`);
      return;
    }
    if (["steps", "cards", "ranks"].includes(b.type) && !Array.isArray(b.items)) {
      errors.push(`${at} (${b.type}) needs an "items" list.`);
    }
    if (b.type === "meta" && !Array.isArray(b.columns)) errors.push(`${at} (meta) needs a "columns" list.`);
    if (b.type === "text" && !b.body) errors.push(`${at} (text) needs a "body".`);
  });
  return errors;
}

// Paths are plain dotted strings — "hero.title", "sections.0.items.2.body" —
// so a click can name exactly one field and the editor can write it back
// without knowing anything about the shape around it.
function getAtPath(obj, path) {
  return String(path).split(".").reduce((o, k) => (o == null ? o : o[k]), obj);
}
function setAtPath(obj, path, value) {
  const keys = String(path).split(".");
  const clone = Array.isArray(obj) ? obj.slice() : { ...obj };
  let cur = clone;
  for (let i = 0; i < keys.length - 1; i++) {
    const k = keys[i];
    const next = cur[k];
    cur[k] = Array.isArray(next) ? next.slice() : { ...next };
    cur = cur[k];
  }
  cur[keys[keys.length - 1]] = value;
  return clone;
}

// What the editor calls the thing you clicked. "sections.0.items.2.body" is
// a correct answer to the wrong question.
function labelForPath(doc, path) {
  const parts = String(path).split(".");
  const field = parts[parts.length - 1];
  const nice = { title: "Title", sub: "Subtitle", body: "Text", eyebrow: "Eyebrow",
    flow: "Flow line", kicker: "Kicker", pill: "Badge", name: "Name", pts: "Points",
    label: "Label", value: "Value" }[field] || field;
  if (parts[0] === "hero") return `Page header · ${nice}`;
  const block = doc.sections && doc.sections[Number(parts[1])];
  const kind = block ? block.type : "block";
  const idx = parts.indexOf("items") >= 0 ? Number(parts[parts.indexOf("items") + 1]) : null;
  const which = {
    steps: "Step", cards: "Card", ranks: "Rank", meta: "Column", heading: "Heading",
    text: "Paragraph", flow: "Flow line",
  }[kind] || "Block";
  return idx === null ? `${which} · ${nice}` : `${which} ${idx + 1} · ${nice}`;
}

// One piece of editable text. Outside the editor it is just the words.
function T({ value, path, ed, tag }) {
  if (!value) return null;
  if (!ed) return <React.Fragment>{value}</React.Fragment>;
  const Tag = tag || "span";
  return (
    <Tag className={"pe-hot" + (ed.picked === path ? " on" : "")}
      role="button" tabIndex={0} title="Click to edit"
      onClick={(e) => { e.stopPropagation(); ed.onPick(path); }}
      onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); ed.onPick(path); } }}>
      {value}
    </Tag>);
}

function PageBlock({ block, path, ed }) {
  const p = (k) => `${path}.${k}`;
  switch (block.type) {
    case "heading":
      return (
        <div className="how-block-head">
          {block.kicker && <div className="how-kicker"><T value={block.kicker} path={p("kicker")} ed={ed} /></div>}
          {block.title && <h2><T value={block.title} path={p("title")} ed={ed} /></h2>}
          {block.body && <p><T value={block.body} path={p("body")} ed={ed} /></p>}
        </div>);

    case "flow":
      return <div className={"how-flow" + (block.dark ? " dark" : "")}>
        <T value={block.body} path={p("body")} ed={ed} /></div>;

    case "steps":
      return (
        <div className="how-steps">
          {block.items.map((s, i) =>
            <div className="how-card" key={i}>
              <div className="how-num">{s.no || String(i + 1).padStart(2, "0")}</div>
              <div>
                <div className="how-card-top">
                  <h3><T value={s.title} path={p(`items.${i}.title`)} ed={ed} /></h3>
                  {s.pill && <span className="how-pill"><T value={s.pill} path={p(`items.${i}.pill`)} ed={ed} /></span>}
                </div>
                <p><T value={s.body} path={p(`items.${i}.body`)} ed={ed} /></p>
              </div>
            </div>
          )}
        </div>);

    case "cards":
      return (
        <div className="how-crowd-grid">
          {block.items.map((c, i) =>
            <div className="how-card how-card-plain" key={i}>
              <h4><T value={c.title} path={p(`items.${i}.title`)} ed={ed} /></h4>
              <p><T value={c.body} path={p(`items.${i}.body`)} ed={ed} /></p>
            </div>
          )}
        </div>);

    case "ranks":
      return (
        <div className="how-ranks">
          {block.items.map((r, i) =>
            <div className={"how-rank" + (r.current ? " current" : "")} key={i}>
              <span className="how-rank-swatch" style={{ background: r.color || "var(--accent)" }}></span>
              <div className="how-rank-name"><T value={r.name} path={p(`items.${i}.name`)} ed={ed} /></div>
              <div className="how-rank-pts"><T value={r.pts} path={p(`items.${i}.pts`)} ed={ed} /></div>
            </div>
          )}
        </div>);

    case "meta":
      return (
        <div className="how-meta">
          {block.columns.map((col, i) =>
            <div key={i}>
              {col.label && <div className="how-meta-label">
                <T value={col.label} path={p(`columns.${i}.label`)} ed={ed} /></div>}
              {(col.rows || []).map((r, j) =>
                <div className="how-meta-row" key={j}>
                  <span><T value={r.label} path={p(`columns.${i}.rows.${j}.label`)} ed={ed} /></span>
                  <b><T value={r.value} path={p(`columns.${i}.rows.${j}.value`)} ed={ed} /></b>
                </div>)}
              {col.body && <p><T value={col.body} path={p(`columns.${i}.body`)} ed={ed} /></p>}
            </div>
          )}
        </div>);

    case "text":
      return <p className="page-text"><T value={block.body} path={p("body")} ed={ed} /></p>;

    default:
      return null;
  }
}

// The page itself. `ed` is only passed by the editor, so the real page renders
// exactly the same markup without a single click target in it.
function JsonPage({ doc, ed }) {
  const hero = doc.hero || {};
  return (
    <div className="guide json-page">
      <section className="ghero arena">
        <div className="wrap">
          {hero.eyebrow && <div className="eyebrow"><T value={hero.eyebrow} path="hero.eyebrow" ed={ed} /></div>}
          {hero.title && <h1><T value={hero.title} path="hero.title" ed={ed} /></h1>}
          {hero.sub && <p className="sub"><T value={hero.sub} path="hero.sub" ed={ed} /></p>}
          {hero.flow && <div className="how-flow"><T value={hero.flow} path="hero.flow" ed={ed} /></div>}
        </div>
      </section>
      <section className="gbody">
        <div className="wrap" style={{ display: "block" }}>
          {(doc.sections || []).map((b, i) =>
            <PageBlock block={b} path={`sections.${i}`} ed={ed} key={i} />)}
        </div>
      </section>
    </div>
  );
}

function SlotsPreview({ entry, values, ed }) {
  const v = (k) => values[k];
  if (entry.id === "arena-closed") {
    const panel = (badge, title, body, keys) =>
      <div className="quest-wrap"><div className="wrap"><div className="arena-panel">
        <span className="ap-badge"><T value={badge} path={keys[0]} ed={ed} /></span>
        <h2><T value={title} path={keys[1]} ed={ed} /></h2>
        <p><T value={body} path={keys[2]} ed={ed} /></p>
      </div></div></div>;
    return (
      <div className="pe-arena-states">
        <div className="pe-state-label">Before it opens</div>
        {panel(v("soonBadge"), v("soonTitle"), v("soonBody"),
          ["soonBadge", "soonTitle", "soonBody"])}
        <div className="pe-state-label">After submissions close</div>
        {panel(v("votingBadge"), v("votingTitle"), v("votingBody"),
          ["votingBadge", "votingTitle", "votingBody"])}
        <div className="pe-state-label">After voting closes</div>
        {panel(v("shutBadge"), v("shutTitle"), v("shutBody"),
          ["shutBadge", "shutTitle", "shutBody"])}
      </div>);
  }

  if (entry.id === "season") {
    return (
      <div className="guide json-page">
        <section className="ghero arena"><div className="wrap">
          <h1>2026 Season <em>4</em></h1>
          <p className="sub"><T value={v("sub")} path="sub" ed={ed} /></p>
          <div className="sprog-card on-hero">
            <div className="sprog-prize">
              <span className="sprog-prize-mark">1st</span>
              <div>
                <div className="sprog-prize-title"><T value={v("prizeTitle")} path="prizeTitle" ed={ed} /></div>
                <div className="sprog-prize-sub"><T value={v("prizeSub")} path="prizeSub" ed={ed} /></div>
              </div>
            </div>
          </div>
        </div></section>
      </div>);
  }

  return (
    <div className="guide json-page">
      <section className={"ghero arena" + (entry.id === "hall-of-fame" ? " hof-hero" : "")}>
        <div className="wrap">
          <h1><T value={v("title")} path="title" ed={ed} />{" "}
            <em><T value={v("titleAccent")} path="titleAccent" ed={ed} /></em></h1>
          <p className="sub"><T value={v("sub")} path="sub" ed={ed} /></p>
        </div>
      </section>
    </div>);
}

function PageEditor({ overrides, onApply, onRevert, starterFor }) {
  const PAGES = PAGE_REGISTRY;
  const [page, setPage] = React.useState(PAGES[0].id);
  // The working copy. A doc page holds the whole document; a slots page holds
  // { slots: {...} }. Either way it starts from what the page shows today, so
  // opening the tab never presents an empty state.
  const [doc, setDoc] = React.useState(() => overrides[PAGES[0].id] || starterFor(PAGES[0].id));
  const [history, setHistory] = React.useState([]);
  const [picked, setPicked] = React.useState(null);
  const [draft, setDraft] = React.useState("");
  const [narrow, setNarrow] = React.useState(false);
  const [showJson, setShowJson] = React.useState(false);
  const [jsonText, setJsonText] = React.useState("");
  const [jsonErrors, setJsonErrors] = React.useState([]);
  const [applied, setApplied] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const [saveError, setSaveError] = React.useState("");

  // The write goes first. If the database refuses it, the page on screen must
  // keep saying what is actually live rather than what was hoped for.
  const publish = async () => {
    setBusy(true); setSaveError("");
    try {
      await onApply(page, doc);
      setApplied(true);
      setTimeout(() => setApplied(false), 2500);
    } catch (err) {
      setSaveError(err.message || "Could not publish that page.");
    } finally { setBusy(false); }
  };
  const revert = async () => {
    setBusy(true); setSaveError("");
    try { await onRevert(page); }
    catch (err) { setSaveError(err.message || "Could not revert that page."); }
    finally { setBusy(false); }
  };

  const entry = pageEntry(page) || PAGES[0];
  const isDoc = entry.kind === "doc";

  React.useEffect(() => {
    const e = pageEntry(page);
    setDoc(overrides[page] || (e && e.kind === "doc"
      ? starterFor(page)
      : { page, slots: slotDefaults(page) }));
    setPicked(null);
    setHistory([]);
  }, [page]);

  const pathRoot = (path) => (isDoc ? path : `slots.${path}`);
  const pick = (path) => {
    setPicked(path);
    setDraft(String(getAtPath(doc, pathRoot(path)) || ""));
  };

  const update = () => {
    if (!picked) return;
    setHistory((h) => [...h, doc]);
    setDoc(setAtPath(doc, pathRoot(picked), draft));
    setPicked(null);
  };

  const undo = () => {
    if (history.length === 0) return;
    setDoc(history[history.length - 1]);
    setHistory(history.slice(0, -1));
    setPicked(null);
  };

  const loadJson = (raw) => {
    setJsonText(raw);
    try {
      const parsed = JSON.parse(raw);
      const errs = validatePageDoc(parsed);
      setJsonErrors(errs);
      if (errs.length === 0) { setHistory((h) => [...h, doc]); setDoc(parsed); }
    } catch (e) {
      setJsonErrors(["That is not valid JSON — " + e.message]);
    }
  };

  const ed = { picked, onPick: pick };
  const live = overrides[page];
  const baseline = isDoc ? starterFor(page) : { page, slots: slotDefaults(page) };
  const changed = JSON.stringify(doc) !== JSON.stringify(live || baseline);
  const slotLabel = () => {
    if (!picked) return "";
    if (isDoc) return labelForPath(doc, picked);
    const f = (entry.slots || []).find((x) => x.key === picked);
    return f ? f.label : picked;
  };

  return (
    <div className="pages-page">
      <section className="ghero arena">
        <div className="wrap">
          <div className="eyebrow">iBD Coliseum · Pages</div>
          <h1>Edit a <em>page</em></h1>
          <p className="sub">
            Pick a page, click any words in it, and change them. Nothing is live until you press
            Publish, and Revert puts the original back.
          </p>
        </div>
      </section>

      <div className="wrap pages-body">
        <div className="pe-bar">
          <select className="ss-input" value={page} onChange={(e) => setPage(e.target.value)} aria-label="Which page">
            {PAGES.map((p) => <option key={p.id} value={p.id}>{p.label}</option>)}
          </select>
          <span className="pe-state">{live ? "Custom version is live" : "Showing the built-in page"}</span>
          <span className="pe-bar-grow"></span>
          <button className="quest-btn ghost sm" onClick={() => setNarrow((n) => !n)}>
            {narrow ? "Desktop" : "Phone"}
          </button>
          <button className="quest-btn ghost sm" disabled={history.length === 0} onClick={undo}>
            Undo{history.length > 0 ? ` (${history.length})` : ""}
          </button>
          <button className="quest-btn" disabled={!changed || busy} onClick={publish}>
            {busy ? "Publishing\u2026" : applied ? "Published" : "Publish"}
          </button>
          {live && <button className="quest-btn ghost sm" disabled={busy} onClick={revert}>Revert</button>}
        </div>

        {saveError && <p className="pe-save-error" role="alert">{saveError}</p>}

        <div className="pe-split">
          <div className="pe-stage">
            <div className="pe-stage-head">
              Click any text to change it
            </div>
            <div className={"pe-preview" + (narrow ? " narrow" : "")}>
              {isDoc
                ? <JsonPage doc={doc} ed={ed} />
                : <SlotsPreview entry={entry} values={doc.slots || {}} ed={ed} />}
            </div>
            {entry.note && <p className="pe-note">{entry.note}</p>}
          </div>

          <div className="pe-side">
            {picked
              ? <div className="dcard pe-edit">
                  <div className="dh"><h3>{slotLabel()}</h3></div>
                  <textarea className="pe-field" value={draft} autoFocus
                    onChange={(e) => setDraft(e.target.value)} />
                  <div className="pe-edit-actions">
                    <button className="quest-btn" onClick={update}>Update</button>
                    <button className="quest-btn ghost sm" onClick={() => setPicked(null)}>Cancel</button>
                  </div>
                </div>
              : <div className="dcard pe-empty-note">
                  <p><b>Nothing selected.</b></p>
                  <p>Click a heading, a step, or any sentence in the page on the left. A box will open here
                     with the current wording — change it and press Update.</p>
                  <p className="pe-dim">Your changes show on the left as you make them. They only reach
                     the real page when you press Publish.</p>
                </div>}
          </div>
        </div>

        {isDoc &&
        <div className="dcard pe-adv">
          <button className="pe-adv-toggle" onClick={() => { setShowJson((v) => !v); setJsonText(JSON.stringify(doc, null, 2)); }}>
            {showJson ? "Hide" : "Show"} the JSON behind this page
          </button>
          {showJson &&
            <React.Fragment>
              <textarea className="pe-text" spellCheck="false" value={jsonText}
                onChange={(e) => loadJson(e.target.value)} />
              {jsonErrors.length > 0 &&
                <div className="pe-errors" role="alert">
                  <b>{jsonErrors.length} {jsonErrors.length === 1 ? "problem" : "problems"}</b>
                  <ul>{jsonErrors.map((e, i) => <li key={i}>{e}</li>)}</ul>
                </div>}
              <p className="pe-dim">Paste a document here to replace the whole page — that is how a file
                 from Claude would come in.</p>
            </React.Fragment>}
        </div>}
      </div>
    </div>
  );
}

window.JsonPage = JsonPage;
window.PageEditor = PageEditor;
window.validatePageDoc = validatePageDoc;
window.makePageText = makePageText;
window.PAGE_REGISTRY = PAGE_REGISTRY;
