// Mosaic site — shared UI components
// Exports: framingColor, SFav, SFavRow, STicks, SCollage, SMast, tickLabel

// Resolve a framing's colour from the active colour mode
const framingColor = (framing, idx, mode) =>
  mode === "neutral" ? NEUTRAL_PALETTE[idx % NEUTRAL_PALETTE.length] : SENT_COLORS[framing.lean];

const SFav = ({ id, size = 20 }) => {
  const f = FAVS[id] || { l: "?", bg: "#666" };
  // YouTube voices → round creator face; outlets → real favicon (DDG→Google→monogram); else monogram
  if (f.avatar) {
    return <img className="fav favimg faceimg" src={f.avatar} alt={f.n} title={f.n}
      style={{ width: size, height: size }}
      onError={(e) => { const s = e.target; s.style.display = "none"; if (s.nextSibling) s.nextSibling.style.display = "inline-flex"; }} />;
  }
  if (f.domain) {
    const onErr = (e) => {
      const t = e.target;
      if (!t.dataset.fb) { t.dataset.fb = "1"; t.src = "https://www.google.com/s2/favicons?domain=" + f.domain + "&sz=64"; }
      else { t.style.display = "none"; if (t.nextSibling) t.nextSibling.style.display = "inline-flex"; }
    };
    return (
      <React.Fragment>
        <img className="fav favimg" src={"https://icons.duckduckgo.com/ip3/" + f.domain + ".ico"} alt={f.n} title={f.n}
          style={{ width: size, height: size }} onError={onErr} />
        <span className="fav" style={{ width: size, height: size, background: f.bg, fontSize: Math.round(size * 0.52), display: "none" }}>{f.l}</span>
      </React.Fragment>
    );
  }
  return <span className="fav" style={{ width: size, height: size, background: f.bg, fontSize: Math.round(size * 0.52) }}>{f.l}</span>;
};

const SFavRow = ({ ids, size = 17, max = 6, gap = null }) => (
  <span style={{ display: "inline-flex", alignItems: "center" }}>
    {ids.slice(0, max).map((id, i) => (
      <span key={i} style={gap == null
        ? { marginLeft: i === 0 ? 0 : -6, borderRadius: 6, boxShadow: "0 0 0 2px var(--bg-primary)", display: "inline-flex" }
        : { marginLeft: i === 0 ? 0 : gap, display: "inline-flex" }}>
        <SFav id={id} size={size} />
      </span>
    ))}
  </span>
);

// Segment ticks — one tick per source, coloured by the framing it sits under
const STicks = ({ story, mode, tw = 9, th = 16, gap = 3, max = 60 }) => {
  const ticks = [];
  story.framings.forEach((f, fi) => {
    const c = framingColor(f, fi, mode);
    for (let i = 0; i < f.n && ticks.length < max; i++) ticks.push(c);
  });
  return (
    <span className="ticks" style={{ gap }}>
      {ticks.map((c, i) => <i key={i} style={{ width: tw, height: th, background: c }}></i>)}
    </span>
  );
};

const tickLabel = (story, mode) => {
  const total = story.srcCount || story.framings.reduce((s, f) => s + f.n, 0);
  if (mode === "neutral") return `${story.framings.length} framings · ${total} sources`;
  const sums = { pos: 0, mix: 0, neg: 0 };
  story.framings.forEach((f) => { sums[f.lean] += f.n; });
  const top = Object.entries(sums).sort((a, b) => b[1] - a[1])[0];
  const word = { pos: "Positive", mix: "Neutral", neg: "Negative" }[top[0]];
  return `${Math.round((top[1] / total) * 100)}% ${word} · ${total} sources`;
};

// Full record grouped by framing — explicit data if present, else derived from each framing's sources
const recordOf = (story) => story.record || story.framings.map((f, i) => ({
  f: i,
  // No per-article titles for derived stories — reuse the framing's real headline (never fabricate one)
  items: f.srcs.map((s) => ({ s, d: story.upd, h: (f.q || "").replace(/[“”"]/g, "") })),
}));

// Abstract paper-collage placeholder (stands in for Mosaic's editorial collage art)
const SCOLLAGE_SEEDS = {
  a: [
    { x: "-6%", y: "-10%", w: "48%", h: "70%", bg: "#C8602B", rot: -7 },
    { x: "36%", y: "18%", w: "44%", h: "72%", bg: "#3E4A38", rot: 4 },
    { x: "62%", y: "-12%", w: "42%", h: "52%", bg: "#EFE3CB", rot: -3 },
    { x: "16%", y: "52%", w: "34%", h: "56%", bg: "#27292D", rot: 6 },
    { x: "52%", y: "60%", w: "26%", h: "30%", bg: "#D7C5A1", rot: -10 },
  ],
  b: [
    { x: "-8%", y: "30%", w: "56%", h: "66%", bg: "#3E4A38", rot: 5 },
    { x: "30%", y: "-14%", w: "46%", h: "62%", bg: "#EFE3CB", rot: -5 },
    { x: "58%", y: "34%", w: "48%", h: "66%", bg: "#C8602B", rot: 3 },
    { x: "10%", y: "-8%", w: "26%", h: "42%", bg: "#27292D", rot: -9 },
  ],
  c: [
    { x: "8%", y: "-12%", w: "50%", h: "74%", bg: "#EFE3CB", rot: 4 },
    { x: "-10%", y: "42%", w: "44%", h: "62%", bg: "#C8602B", rot: -6 },
    { x: "50%", y: "20%", w: "54%", h: "70%", bg: "#27292D", rot: 7 },
    { x: "36%", y: "64%", w: "30%", h: "40%", bg: "#3E4A38", rot: -4 },
  ],
};

const SCollage = ({ seed = "a", radius = 8, style }) => (
  <span className="collage" style={{ borderRadius: radius, ...style }}>
    {(SCOLLAGE_SEEDS[seed] || SCOLLAGE_SEEDS.a).map((p, i) => (
      <i key={i} style={{ left: p.x, top: p.y, width: p.w, height: p.h, background: p.bg, transform: `rotate(${p.rot}deg)` }}></i>
    ))}
  </span>
);

// Generated zine-collage topic heroes (the gen_hero.py house style). Falls back to the
// abstract SCollage for any story without a generated hero.
const HERO_MAP = { gilgit: "gb", budget: "bud", fixedtax: "ftx", azad: "ajk", waziristan: "nwz", nec: "nec", textiles: "tex",
  // v1 bounded set — each topic has its own coloured hero (gen_hero.py, per-topic palette)
  "gb-election": "gb-election", "india-water": "india-water", "baldia-acquittal": "baldia-acquittal",
  "afghan-border-strikes": "afghan-border-strikes", "ajk-jaac": "ajk-jaac", "imran-health": "imran-health",
  "women-safety": "women-safety", "budget-fy27": "budget-fy27", "remittances-record": "remittances-record",
  "austerity-hours": "austerity-hours" };
const SArt = ({ id, seed = "a", radius = 8, style }) => {
  const file = HERO_MAP[id];
  if (!file) return <SCollage seed={seed} radius={radius} style={style} />;
  return (
    <span className="collage" style={{ borderRadius: radius, ...style }}>
      <img src={"assets/heroes/" + file + ".png"} alt=""
        style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
    </span>
  );
};

// Flexible markdown renderer — styled to the design so a dossier's prose sections (and any future
// sections) display without UI code changes. Handles ## headings, **bold**, *italic*, > quotes, - lists.
const mdInline = (t) => {
  const parts = [];
  let rest = String(t), key = 0;
  // [text](url) link · **bold** · *italic* · _italic_
  const re = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)|\*\*(.+?)\*\*|\*(.+?)\*|_(.+?)_/;
  let m;
  while ((m = re.exec(rest))) {
    if (m.index > 0) parts.push(rest.slice(0, m.index));
    if (m[1]) {
      let host = ""; try { host = new URL(m[2]).hostname.replace(/^www\./, ""); } catch (e) {}
      const onErr = (e) => {
        const t = e.target;
        if (!t.dataset.fb) { t.dataset.fb = "1"; t.src = "https://www.google.com/s2/favicons?domain=" + host + "&sz=32"; }
        else { t.style.display = "none"; }
      };
      parts.push(
        <a key={key++} href={m[2]} target="_blank" rel="noopener noreferrer" className="md-a">
          {host && <img className="md-fav" src={"https://icons.duckduckgo.com/ip3/" + host + ".ico"} onError={onErr} alt="" />}
          {m[1]}
        </a>
      );
    }
    else if (m[3]) parts.push(<strong key={key++}>{m[3]}</strong>);
    else parts.push(<em key={key++}>{m[4] || m[5]}</em>);
    rest = rest.slice(m.index + m[0].length);
  }
  if (rest) parts.push(rest);
  return parts;
};
// parse `:::type{k="v" k2="v2"}` attrs
const mdAttrs = (s) => {
  const a = {}; const re = /(\w+)="([^"]*)"/g; let m;
  while ((m = re.exec(s || ""))) a[m[1]] = m[2];
  return a;
};
// render one `:::component` directive as a neutral-mode React block (the live design language)
const MDComp = ({ type, attrs, body }) => {
  const lbl = (t) => <div className="kicker md-h">{t}</div>;
  if (type === "tldr") return <section className="md-comp">{lbl("The 30-second read")}<MD md={body} /></section>;
  if (type === "matters") return <div className="md-matters"><span className="md-mtag">why this matters</span><span>{mdInline(body)}</span></div>;
  if (type === "snapshot") return <div className="md-snapshot">{mdInline(body)}</div>;
  if (type === "keyfacts") return <section className="md-comp">{lbl("What everyone agrees on")}<MD md={body} /></section>;
  if (type === "faultline") return <section className="md-comp md-fault">{lbl("The fault line")}<div className="md-faultq">{mdInline(body)}</div></section>;
  if (type === "loudness") return <div className="md-loud"><span className="md-loudlbl">loudest: {mdInline(attrs.leader || "")}</span><p>{mdInline(body)}</p></div>;
  if (type === "spectrum") return <section className="md-comp">{lbl("The spread of views")}<p className="md-p">{mdInline(body)}</p></section>;
  if (type === "intl") return <section className="md-comp">{lbl("The foreign vantage")}<MD md={body} /></section>;
  if (type === "blindspot") return <section className="md-comp md-blind">{lbl("The blind spot")}<MD md={body} /></section>;
  if (type === "watch") return <section className="md-comp">{lbl("What to watch next")}<MD md={body} /></section>;
  if (type === "record") return <section className="md-comp md-record">{lbl("The full record")}<MD md={body} /></section>;
  if (type === "methodology") return <footer className="md-method">{mdInline(body)}</footer>;
  if (type === "quote") return (
    <blockquote className="md-qcard"><p>“{mdInline(body)}”</p>
      <footer>{attrs.url ? <a href={attrs.url} target="_blank" rel="noreferrer">{attrs.voice}</a> : <span>{attrs.voice}</span>}
        {attrs.view && <span className="md-qview">{attrs.view}</span>}
        {attrs.engagement && <span className="md-qeng">{attrs.engagement}</span>}</footer></blockquote>);
  if (type === "tweet") {
    const plat = (attrs.platform || "x").toLowerCase();
    const icon = plat === "reddit" ? "ph-reddit-logo" : plat === "youtube" ? "ph-youtube-logo" : "ph-x-logo";
    return (
      <blockquote className={"md-tweet md-tweet-" + plat}>
        <div className="md-tweethead">
          <i className={"ph " + icon}></i>
          {attrs.url ? <a href={attrs.url} target="_blank" rel="noreferrer">{attrs.handle}</a> : <span>{attrs.handle}</span>}
          {attrs.engagement && <span className="md-tweeteng">{attrs.engagement}</span>}
        </div>
        <p>{mdInline(body)}</p>
      </blockquote>);
  }
  if (type === "chart") {
    const rows = body.split("\n").filter((r) => r.includes("|")).map((r) => {
      const k = r.lastIndexOf("|"); return [r.slice(0, k).trim(), parseFloat(r.slice(k + 1).replace(/[^0-9.]/g, "")) || 0];
    });
    const mx = Math.max(...rows.map((r) => r[1]), 1);
    return (
      <figure className="md-chart">
        <figcaption>{mdInline(attrs.title || "")}</figcaption>
        {rows.map(([l, v], j) => (
          <div className="md-barrow" key={j}>
            <span className="md-barlbl">{l}</span>
            <div className="md-bartrack"><i style={{ width: (v / mx * 100) + "%", background: NEUTRAL_PALETTE[j % NEUTRAL_PALETTE.length] }}></i></div>
            <span className="md-barval">{v}</span>
          </div>
        ))}
        {attrs.source && <div className="md-chartsrc">source: {mdInline(attrs.source)}{attrs.unit ? " · " + attrs.unit : ""}</div>}
        {attrs.note && <div className="md-chartnote">{mdInline(attrs.note)}</div>}
      </figure>);
  }
  return <div className="md-comp"><MD md={body} /></div>;
};
const MD = ({ md }) => {
  if (!md) return null;
  const lines = String(md).replace(/\r/g, "").split("\n");
  const blocks = [];
  let i = 0;
  while (i < lines.length) {
    let line = lines[i];
    const dir = line.match(/^:::(\w+)(?:\{(.*)\})?\s*$/);
    if (dir) {
      const buf = []; i++;
      while (i < lines.length && lines[i].trim() !== ":::") { buf.push(lines[i]); i++; }
      i++;
      blocks.push(<MDComp key={i} type={dir[1]} attrs={mdAttrs(dir[2] || "")} body={buf.join("\n")} />);
    } else if (/^#{1,6}\s/.test(line)) {
      const text = line.replace(/^#+\s/, "").replace(/\s*\{#[^}]+\}\s*$/, "");
      blocks.push(<div key={i} className="kicker md-h">{text}</div>);
      i++;
    } else if (/^>\s?/.test(line)) {
      const buf = [];
      while (i < lines.length && /^>\s?/.test(lines[i])) { buf.push(lines[i].replace(/^>\s?/, "")); i++; }
      blocks.push(<blockquote key={i} className="md-q">{mdInline(buf.join(" "))}</blockquote>);
    } else if (/^[-*]\s/.test(line)) {
      const items = [];
      while (i < lines.length && /^[-*]\s/.test(lines[i])) { items.push(lines[i].replace(/^[-*]\s/, "")); i++; }
      blocks.push(<ul key={i} className="md-ul">{items.map((it, j) => <li key={j}>{mdInline(it)}</li>)}</ul>);
    } else if (line.trim() === "") {
      i++;
    } else {
      const buf = [];
      while (i < lines.length && lines[i].trim() !== "" && !/^(#{1,6}\s|>\s?|[-*]\s)/.test(lines[i])) { buf.push(lines[i]); i++; }
      blocks.push(<p key={i} className="md-p">{mdInline(buf.join(" "))}</p>);
    }
  }
  return <div className="mdbody">{blocks}</div>;
};

// Entrance fade — transition-based so the resting state is fully visible (capture/print safe)
const FadeIn = ({ children, style }) => {
  const [pre, setPre] = React.useState(true);
  React.useEffect(() => {
    const tm = setTimeout(() => setPre(false), 30);
    return () => clearTimeout(tm);
  }, []);
  return <div className={"pagefade" + (pre ? " pre" : "")} style={style}>{children}</div>;
};

// Masthead — nav drives page + category filter
const SMast = ({ page, cat, theme, onNav, onTheme }) => {
  const navs = [
    { k: "today", label: "Today" },
    { k: "politics", label: "Politics" },
    { k: "economy", label: "Economy" },
    { k: "sources", label: "Sources" },
  ];
  const active = page === "sources" ? "sources" : (cat === "Politics" ? "politics" : cat === "Economy" ? "economy" : "today");
  return (
    <header className="mast">
      <div className="left">
        <img src={theme === "dark" ? "assets/logo-dark.png" : "assets/logo-light.png"} alt="Mosaic" style={{ height: 28, cursor: "pointer" }} onClick={() => onNav("today")} />
        <nav>
          {navs.map((n) => (
            <button key={n.k} className={active === n.k ? "on" : ""} onClick={() => onNav(n.k)}>{n.label}</button>
          ))}
        </nav>
      </div>
      <div className="right">
        <span className="micro">{new Date().toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "long", year: "numeric" })}</span>
        <button className="themeBtn" title="Toggle theme" onClick={onTheme}><i></i></button>
      </div>
    </header>
  );
};

Object.assign(window, { framingColor, SFav, SFavRow, STicks, SCollage, SArt, SMast, tickLabel, recordOf, MD, FadeIn });
