/* global React, Icons, Empty, ageChip, relTime, podDate, TweetCard, NewsCard, PodcastCard */
// The Debates view (ADR-0020, sixth amendment): one ledger of the week's
// two-sided questions on the left, ordered by WHEN each was last argued (the
// freshest item either side cites), with the source count as the tiebreak, and
// a self-contained reading pane on the right showing the chosen
// debate as two side-by-side panels, the case and the counter, each with its
// named voices and its citations listed as rows that unfold the cited card. Every side cites
// evidence, and the two sides together span at least two source kinds (the
// gate lives in scripts/apply_debates.py). Nothing here uses green, red, or
// violet: the case speaks cyan, the counter magenta.
//
// On a phone the ledger is the screen; choosing a question pushes the pane in
// full width with a back chevron and one history entry, so swipe-back works.
//
// Reads payload.debates from the synthesis the app already fetched. Cards
// resolve their rows from data.cited first (every cited id, shipped by
// /api/deck) and fall back to the window arrays.
(() => {
  const e = React.createElement;
  const { useState, useMemo, useEffect, Fragment } = React;

  const BEATS = [
    { id: "macro", title: "Macro and markets" },
    { id: "ai", title: "AI and chips" },
    { id: "deals", title: "Deals and private markets" },
  ];
  const KINDS = [
    { id: "news", label: "News", Icon: Icons.News },
    { id: "tweet", label: "X", Icon: Icons.Tweet },
    { id: "podcast", label: "Podcasts", Icon: Icons.Mic },
  ];
  const SIDES = [
    { id: "case", label: "The case" },
    { id: "counter", label: "The counter" },
  ];
  const byRecency = (a, b) => (a.first_seen < b.first_seen ? 1 : a.first_seen > b.first_seen ? -1 : 0);
  // How much a question is being argued: distinct sources across both sides
  // first (the number the row shows, so the order never contradicts it), then
  // how many source kinds, then named voices, then recency. Deterministic.
  function heatOf(d, items) {
    const sources = new Set([...sideSources(d.case, items), ...sideSources(d.counter, items)]);
    const kinds = kindsOf(d).size;
    const voices = new Set([...(d.case.voices || []), ...(d.counter.voices || [])]).size;
    return { sources: sources.size, kinds, voices };
  }
  const byHeat = (a, b) => (b.heat.sources - a.heat.sources) || (b.heat.kinds - a.heat.kinds)
    || (b.heat.voices - a.heat.voices) || byRecency(a.d, b.d);

  // When a question was last argued: the newest item either side cites. This is
  // what orders the ledger (the site is called dujour), while first_seen still
  // says how long the argument has been running. Future-dated rows exist in the
  // news table, so the value is clamped to now; a debate whose items all fall
  // outside the shipped window sorts last rather than first.
  // The stamp behind ONE citation, 0 when the cited item is not in the shipped
  // window or is future dated, so unknowns sort last wherever evidence is
  // ordered by it. An external post carries no stamp and is always 0.
  function evTime(ev, items) {
    const now = Math.floor(Date.now() / 1000);
    const id = String((ev && ev.id) || "");
    let u = 0;
    if (ev.kind === "tweet") { const t = items.tweet.get(id); u = (t && t.created_unix) || 0; }
    else if (ev.kind === "news") { const n = items.news.get(id); u = (n && n.published_unix) || 0; }
    else if (ev.kind === "podcast") {
      const p = items.podcast.get(id);
      u = p && p.published_at ? Math.floor(Date.parse(`${p.published_at}T12:00:00Z`) / 1000) : 0;
    }
    return u && u <= now ? u : 0;
  }

  // A side's evidence newest first, the model's own order breaking ties, so the
  // citations read the way the rest of the site does.
  function sortedEvidence(side, items) {
    return ((side && side.evidence) || [])
      .map((ev, i) => ({ ev, i, t: evTime(ev, items) }))
      .sort((a, b) => (b.t - a.t) || (a.i - b.i))
      .map((r) => r.ev);
  }

  function lastArguedAt(d, items) {
    let max = 0;
    for (const side of ["case", "counter"]) {
      for (const ev of (d[side] && d[side].evidence) || []) {
        const u = evTime(ev, items);
        if (u > max) max = u;
      }
    }
    return max;
  }
  const byFreshness = (a, b) => (b.last - a.last) || byHeat(a, b);

  // "Today" / "Yesterday" / "Fri" / "Aug 26" for a unix stamp, the ledger's
  // freshness mark. Distinct from ageChip, which reads a first_seen date.
  function dayChip(unix) {
    if (!unix) return null;
    const d = new Date(unix * 1000), t = new Date();
    const day = new Date(d.getFullYear(), d.getMonth(), d.getDate());
    const today = new Date(t.getFullYear(), t.getMonth(), t.getDate());
    const n = Math.round((today - day) / 86400000);
    if (n <= 0) return "Today";
    if (n === 1) return "Yesterday";
    if (n <= 6) return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()];
    return `${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][d.getMonth()]} ${d.getDate()}`;
  }
  const PHONE = () => !!(window.matchMedia && window.matchMedia("(max-width: 768px)").matches);

  // An external citation on x.com counts as X for the glyph row.
  function sourceKind(ev) {
    if (ev.kind === "external") return /(^|\.)x\.com\//.test(ev.url || "") ? "tweet" : null;
    return ev.kind;
  }
  function kindsOf(d) {
    const s = new Set();
    [...(d.case.evidence || []), ...(d.counter.evidence || [])].forEach((ev) => { const k = sourceKind(ev); if (k) s.add(k); });
    return s;
  }

  function useItems(data) {
    return useMemo(() => {
      const maps = { tweet: new Map(), news: new Map(), podcast: new Map() };
      const add = (kind, rows, idKey) => (rows || []).forEach((r) => maps[kind].set(String(r[idKey]), r));
      add("tweet", data && data.tweets, "id");
      add("news", data && data.news, "id");
      add("podcast", data && data.podcasts, "video_id");
      const cited = (data && data.cited) || {};
      add("tweet", cited.tweets, "id");
      add("news", cited.news, "id");
      add("podcast", cited.podcasts, "video_id");
      return maps;
    }, [data]);
  }

  // What a side actually CITES (outlet, @handle, show), derived from the stamped
  // evidence, never from the model's prose; the model's named voices stay
  // inside the panel as the people speaking.
  function sideSources(side, items) {
    const out = [];
    const push = (s) => { if (s && !out.includes(s)) out.push(s); };
    for (const ev of sortedEvidence(side, items)) {
      if (ev.kind === "news") push(ev.source || (items.news.get(String(ev.id)) || {}).source);
      else if (ev.kind === "tweet") { const h = ev.handle || (items.tweet.get(String(ev.id)) || {}).handle; push(h ? `@${h}` : null); }
      else if (ev.kind === "podcast") push(ev.channel || (items.podcast.get(String(ev.id)) || {}).channel);
      else if (ev.kind === "external") push(ev.label);
    }
    return out;
  }
  const sourcesLabel = (list, max = 3) => !list.length ? null
    : list.length > max ? `${list.slice(0, max).join(", ")} +${list.length - max}` : list.join(", ");

  const Glyphs = ({ d }) => {
    const ks = kindsOf(d);
    return e("span", { className: "dbx-glyphs", title: "which sources weighed in" },
      KINDS.map((k) => e(k.Icon, { key: k.id, size: 12, className: ks.has(k.id) ? `has k-${k.id}` : "" })));
  };

  // One cited item in its own card anatomy; an external post as a headline row.
  function ItemCard({ ev, items }) {
    if (ev.kind === "tweet") { const t = items.tweet.get(String(ev.id)); return t ? e(TweetCard, { t }) : null; }
    if (ev.kind === "news") { const n = items.news.get(String(ev.id)); return n ? e(NewsCard, { n }) : null; }
    if (ev.kind === "podcast") { const p = items.podcast.get(String(ev.id)); return p ? e(PodcastCard, { p }) : null; }
    if (ev.kind === "external" && ev.url) {
      return e("article", { className: "news fade-in" },
        e("div", { className: "news-top" },
          e("span", { className: "news-src trade" }, e("span", { className: "sd" }), ev.label || "source"),
          e("span", { className: "news-feed" }, "originating post, outside the corpus")),
        e("a", { className: "news-title", href: ev.url, target: "_blank", rel: "noopener" },
          ev.label ? `${ev.label} on ${sourceKind(ev) === "tweet" ? "X" : "the web"}` : ev.url.replace(/^https?:\/\//, "")));
    }
    return null;
  }

  // A citation as a readable row: kind glyph, source, a fragment of the item,
  // and its date. Tapping unfolds the full card beneath the row.
  function citeOf(ev, items) {
    const clip = (s, n) => { const t = (s || "").replace(/\s+/g, " ").trim(); return t.length > n ? `${t.slice(0, n - 1).trim()}...` : t; };
    if (ev.kind === "news") { const n = items.news.get(String(ev.id)) || {}; return { Icon: Icons.News, who: ev.source || n.source || "news", what: clip(n.title, 110), when: n.published_unix ? relTime(n.published_unix) : "" }; }
    if (ev.kind === "tweet") { const t = items.tweet.get(String(ev.id)) || {}; return { Icon: Icons.Tweet, who: `@${ev.handle || t.handle || "x"}`, what: clip(t.text, 110), when: t.created_unix ? relTime(t.created_unix) : "" }; }
    if (ev.kind === "podcast") { const p = items.podcast.get(String(ev.id)) || {}; return { Icon: Icons.Mic, who: ev.channel || p.channel || "podcast", what: clip(p.title, 110), when: p.published_at ? podDate(p.published_at) : "" }; }
    return { Icon: Icons.External, who: ev.label || "source", what: "originating post, outside the corpus", when: "" };
  }
  function CiteRow({ ev, items, open, onToggle }) {
    const c = citeOf(ev, items);
    return e(Fragment, null,
      e("button", { className: `dbx-cite${open ? " on" : ""}`, onClick: onToggle, "aria-expanded": open },
        e(c.Icon, { size: 12, className: `dbx-cite-ic k-${ev.kind}` }),
        e("span", { className: "dbx-cite-body" },
          e("span", { className: "dbx-cite-who" }, c.who),
          c.what ? e("span", { className: "dbx-cite-what" }, c.what) : null),
        c.when ? e("span", { className: "dbx-cite-when tabnum" }, c.when) : null),
      open ? e("div", { className: "dbx-cards fade-in" }, e(ItemCard, { ev, items })) : null);
  }

  // One side of the argument: eyebrow, the position, who says it, and its
  // citations as rows that unfold the cited card inside the panel.
  function SidePanel({ side, x, items }) {
    const list = useMemo(() => sortedEvidence(x, items), [x, items]);
    // Every citation is unfolded on arrival: the receipts are the point, so the
    // cards are there to read without a tap. A row still folds away on click.
    const allOpen = (ev) => new Set((ev || []).map((_, i) => i));
    const [open, setOpen] = useState(() => allOpen(list));
    useEffect(() => { setOpen(allOpen(list)); }, [list]);
    const toggle = (i) => setOpen((prev) => {
      const next = new Set(prev);
      if (next.has(i)) next.delete(i); else next.add(i);
      return next;
    });
    // Four slots, always rendered: the two panels share a subgrid (dujour.css
    // .dbx-sides), so eyebrow, position, voices, and citations line up across
    // the case and the counter and both panels stand the same height.
    return e("section", { className: `dbx-side ${side.id}`, "aria-label": side.label },
      e("div", { className: "dbx-side-eyebrow" }, side.label),
      e("div", { className: "dbx-side-text" }, x.text),
      e("div", { className: "dbx-side-voices" }, x.voices && x.voices.length ? x.voices.join(" · ") : null),
      e("div", { className: "dbx-cites" },
        list.map((ev, i) => e(CiteRow, { key: i, ev, items, open: open.has(i), onToggle: () => toggle(i) }))));
  }

  function Pane({ d, items, onBack, phone, isNew }) {
    if (!d) return e("div", { className: "dbx-pane" }, e(Empty, null, "// pick a question"));
    const beat = BEATS.find((b) => b.id === d.beat) || BEATS[1];
    const last = dayChip(lastArguedAt(d, items));
    const since = ageChip(d.first_seen);
    const caseSrc = sourcesLabel(sideSources(d.case, items));
    const counterSrc = sourcesLabel(sideSources(d.counter, items));
    // On a phone the pane is a pushed screen, so the way back is a labeled
    // control someone new will actually see: a cyan pill at the top naming the
    // list it returns to, and the same again under the argument so a long read
    // never ends with a scroll back up. Swipe-back works too (history entry).
    const backBtn = phone ? e("button", { className: "dbx-back", onClick: onBack },
      e(Icons.Back, { size: 16 }), e("span", null, "Open questions")) : null;
    return e("div", { className: "dbx-pane", key: d.key },
      e("div", { className: "dbx-pane-inner" },
        backBtn ? e("div", { className: "dbx-backbar" }, backBtn) : null,
        e("div", { className: "dbx-pane-head" },
          e("span", { className: "dbx-eyebrow" }, beat.title),
          isNew ? e("span", { className: "trend new" }, "New") : null,
          e("span", { className: "dbx-when" },
            last ? `Last argued ${last.toLowerCase()}` : null,
            !isNew && since && last ? " · " : null,
            !isNew && since ? `open since ${since.label === "New" ? "today" : since.label}` : null),
          e(Glyphs, { d })),
        e("h3", { className: "dbx-q" }, d.question),
        e("div", { className: "dbx-attrib" },
          e("span", { className: "case" }, caseSrc || "the case"),
          e("span", { className: "dbx-vs" }, "vs"),
          e("span", { className: "counter" }, counterSrc || "the counter")),
        e("div", { className: "dbx-sides" },
          SIDES.map((s) => d[s.id] ? e(SidePanel, { key: s.id, side: s, x: d[s.id], items }) : null)),
        backBtn ? e("div", { className: "dbx-backbar bottom" }, backBtn) : null));
  }

  function Ledger({ ordered, items, selected, onPick }) {
    return e("div", { className: "dbx-list" },
      e("div", { className: "day-div dbx-group" }, "Open questions", e("span", { className: "dbx-group-n tabnum" }, ordered.length)),
      ordered.map(({ d, heat, last, isNew }) => e("button", {
        key: d.key, className: `dbx-row${selected === d.key ? " on" : ""}`, onClick: () => onPick(d.key), "aria-pressed": selected === d.key,
      },
        e("div", { className: "dbx-row-meta" },
          isNew ? e("span", { className: "trend new" }, "New") : null,
          e("span", { className: "dbx-when" }, dayChip(last) || "older"),
          e(Glyphs, { d }),
          e("span", { className: "dbx-row-n tabnum" }, `${heat.sources} sources`)),
        e("div", { className: "dbx-row-q" }, d.question),
        e("div", { className: "dbx-row-who" },
          e("span", null, sourcesLabel(sideSources(d.case, items), 2) || "the case"),
          e("span", { className: "dbx-vs" }, "vs"),
          e("span", null, sourcesLabel(sideSources(d.counter, items), 2) || "the counter")),
        e(Icons.Chevron, { size: 14, className: "dbx-row-chev" }))));
  }

  function DebatesView({ data, loading }) {
    const items = useItems(data);
    const p = data && data.synthesis && data.synthesis.payload;
    const debates = useMemo(() => ((p && p.debates) || []).filter((d) => d && d.case && d.counter), [p]);
    const issueDay = ((data && data.synthesis && data.synthesis.generated_at) || "").slice(0, 10);
    const ordered = useMemo(() => debates
      .map((d) => ({ d, heat: heatOf(d, items), last: lastArguedAt(d, items), isNew: d.first_seen === issueDay }))
      .sort(byFreshness), [debates, items, issueDay]);
    // The most recently argued question is open at first paint (a ?debate=<key>
    // link wins), so the pane is never empty; on a phone nothing is pushed until a tap.
    const [selected, setSelected] = useState(() => {
      try { return new URLSearchParams(location.search).get("debate"); } catch { return null; }
    });
    const [pushed, setPushed] = useState(false);
    const [phone, setPhone] = useState(PHONE);
    useEffect(() => {
      const mq = window.matchMedia("(max-width: 768px)");
      const on = () => setPhone(mq.matches);
      if (mq.addEventListener) mq.addEventListener("change", on); else mq.addListener(on);
      const onPop = () => setPushed(false);
      window.addEventListener("popstate", onPop);
      return () => {
        if (mq.removeEventListener) mq.removeEventListener("change", on); else mq.removeListener(on);
        window.removeEventListener("popstate", onPop);
      };
    }, []);
    const first = ordered.length ? ordered[0].d : null;
    const current = debates.find((d) => d.key === selected) || (phone ? null : first);
    const pick = (key) => {
      setSelected(key);
      if (phone && !pushed) { try { history.pushState({ debate: key }, ""); } catch (_) { /* ignore */ } setPushed(true); }
    };
    const back = () => { try { history.back(); } catch (_) { setPushed(false); } };

    if (loading) {
      return e("div", { className: "dbx" },
        e("div", { className: "dbx-list" }, [1, 2, 3, 4, 5, 6].map((i) => e("div", { className: "skel news-skel", key: i },
          e("div", null, e("div", { className: "b l1" }), e("div", { className: "b l2" }))))),
        e("div", { className: "dbx-pane" }, e("div", { className: "theme-skel", style: { margin: 18 } })));
    }
    if (!debates.length) {
      return e("div", { className: "brief" }, e("div", { className: "brief-inner" },
        e("div", { className: "empty" }, "// no debates yet", e("br"), "the daily routine writes them")));
    }
    const showPane = !phone || (pushed && !!current);
    return e("div", { className: `dbx${phone ? " phone" : ""}${showPane && phone ? " pushed" : ""}` },
      (!phone || !showPane) ? e(Ledger, { ordered, items, selected: current && current.key, onPick: pick }) : null,
      showPane ? e(Pane, { d: current, items, onBack: back, phone,
        isNew: !!(current && current.first_seen === issueDay) }) : null);
  }

  Object.assign(window, { DebatesView });
})();
