/* global React */
// Feed search: grounded Q&A over the loaded News / Tweets / Podcasts feed.
// The browser does lightweight keyword retrieval over the already-loaded deck,
// posts the candidate items to /api/ask, which calls OpenRouter server-side with
// the paid key (the key never touches the browser) and returns a grounded,
// cited answer. Ported from the ldp-os ask-bar: same instructions, same key.
(() => {
  const e = React.createElement;
  const { useState, useEffect, useRef, useMemo } = React;

  const ASK_STOP = new Set(
    ("the a an of to in on for and or is are was were be as at by with from about into over after this "
     + "that these those it its their his her our your what which who whom when where why how did do does "
     + "say said says tell told me my we you they i").split(" ")
  );

  // --- normalize a tweet / news / podcast row into one retrieval record ----
  // Each record is { ts (unix, for recency sort), slim (the fields /api/ask
  // renders + the source chip shows), blob (lowercased keyword-match text) }.
  function symText(symbols) {
    if (!Array.isArray(symbols)) return "";
    return symbols
      .map((s) => (typeof s === "string" ? s : (s && (s.text || s.symbol || s.tag)) || ""))
      .filter(Boolean).join(" ");
  }
  function isoDay(ts) {
    if (!ts) return "";
    const d = new Date(ts * 1000);
    return isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
  }

  function tweetRec(t) {
    const syms = symText(t.symbols);
    const handle = t.handle || "";
    const ts = t.created_unix || 0;
    return {
      ts,
      slim: {
        date: isoDay(ts), name: t.author_name || (handle ? "@" + handle : ""),
        ticker: syms.split(" ")[0] || "", type: "tweet",
        source_label: handle ? "@" + handle : "tweet", url: t.url || "",
        quote: t.text || "", impact_note: "", episode_title: "", takeaways: [],
      },
      blob: [t.author_name, handle, t.text, syms,
             t.quoted && t.quoted.text, t.link_card && t.link_card.title]
        .filter(Boolean).join(" ").toLowerCase(),
    };
  }

  function newsRec(n) {
    const ts = n.published_unix || 0;
    return {
      ts,
      slim: {
        date: isoDay(ts), name: "", ticker: "", type: "news",
        source_label: n.source || n.feed || "news", url: n.url || "",
        quote: n.title || "", impact_note: n.summary || "", episode_title: "", takeaways: [],
      },
      blob: [n.title, n.summary, n.source, n.feed].filter(Boolean).join(" ").toLowerCase(),
    };
  }

  function podRec(p) {
    const parsed = Date.parse((p.published_at || "") + "T00:00:00Z");
    const ts = isNaN(parsed) ? 0 : Math.floor(parsed / 1000);
    const bullets = Array.isArray(p.bullet_summary) ? p.bullet_summary : [];
    return {
      ts,
      slim: {
        date: p.published_at || isoDay(ts), name: p.channel || "", ticker: "", type: "podcast",
        source_label: p.channel || "podcast", url: p.url || "",
        quote: "", impact_note: p.interest_reasoning || "", episode_title: p.title || "",
        takeaways: bullets.slice(0, 6),
      },
      blob: [p.channel, p.title, bullets.join(" "), p.interest_reasoning]
        .filter(Boolean).join(" ").toLowerCase(),
    };
  }

  function buildAskIndex(data) {
    if (!data) return [];
    const out = [];
    (data.tweets || []).forEach((t) => out.push(tweetRec(t)));
    (data.news || []).forEach((n) => out.push(newsRec(n)));
    (data.podcasts || []).forEach((p) => out.push(podRec(p)));
    return out;
  }

  function recKey(rec) {
    return rec.ts + "|" + (rec.slim.quote || rec.slim.episode_title || rec.slim.source_label);
  }

  function retrieveCandidates(index, question, n) {
    if (!index.length) return [];
    const toks = (question.toLowerCase().match(/[a-z0-9$]+/g) || [])
      .filter((t) => t.length > 2 && !ASK_STOP.has(t));
    const scored = index
      .map((rec) => {
        let score = 0;
        for (const t of toks) if (rec.blob.indexOf(t) !== -1) score += 1;
        return { rec, score };
      })
      .filter((s) => s.score > 0)
      .sort((a, b) => b.score - a.score || b.rec.ts - a.rec.ts)
      .slice(0, n);
    const hits = scored.map((s) => s.rec.slim);
    // Always give the model a floor of recent context, even for a no-keyword-match
    // question, so it can answer "the feed does not cover that" from real items.
    if (hits.length >= Math.min(n, 12)) return hits;
    const seen = new Set(scored.map((s) => recKey(s.rec)));
    const recent = index.slice().sort((a, b) => b.ts - a.ts);
    for (const r of recent) {
      if (hits.length >= n) break;
      const k = recKey(r);
      if (!seen.has(k)) { hits.push(r.slim); seen.add(k); }
    }
    return hits;
  }

  function searchIcon(cls, size) {
    return e("svg", {
      className: cls, width: size, height: size, viewBox: "0 0 24 24", fill: "none",
      stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round",
      "aria-hidden": "true",
    }, e("circle", { cx: 11, cy: 11, r: 7 }), e("path", { d: "M21 21l-4.3-4.3" }));
  }

  // A command-palette-style search: a wide "Search the feed" trigger in the top
  // bar (collapses to an icon on phones), opening a popover with the input, the
  // grounded answer, and the exact sources the model cited.
  function AskBar({ data }) {
    const [q, setQ] = useState("");
    const [open, setOpen] = useState(false);
    const [loading, setLoading] = useState(false);
    const [result, setResult] = useState(null); // { answer, sources, model } | { error }
    const boxRef = useRef(null);
    const inputRef = useRef(null);
    const index = useMemo(() => buildAskIndex(data), [data]);

    // "/" from anywhere opens the palette (unless already typing in a field).
    useEffect(() => {
      const onSlash = (ev) => {
        if (ev.key !== "/" || ev.metaKey || ev.ctrlKey || ev.altKey) return;
        const el = document.activeElement;
        if (el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable)) return;
        ev.preventDefault();
        setOpen(true);
      };
      document.addEventListener("keydown", onSlash);
      return () => document.removeEventListener("keydown", onSlash);
    }, []);

    useEffect(() => {
      if (!open) return;
      if (inputRef.current) inputRef.current.focus();
      const onDoc = (ev) => { if (boxRef.current && !boxRef.current.contains(ev.target)) setOpen(false); };
      const onKey = (ev) => { if (ev.key === "Escape") setOpen(false); };
      document.addEventListener("mousedown", onDoc);
      document.addEventListener("keydown", onKey);
      return () => {
        document.removeEventListener("mousedown", onDoc);
        document.removeEventListener("keydown", onKey);
      };
    }, [open]);

    // The model cites items inline as [n]; show only the sources it actually used.
    const cited = useMemo(() => {
      if (!result || !result.answer || !result.sources) return [];
      const nums = new Set();
      const re = /\[(\d+)\]/g; let m;
      while ((m = re.exec(result.answer))) {
        const nn = parseInt(m[1], 10);
        if (nn >= 1 && nn <= result.sources.length) nums.add(nn);
      }
      return Array.from(nums).sort((a, b) => a - b)
        .map((nn) => Object.assign({ n: nn }, result.sources[nn - 1]));
    }, [result]);

    async function ask() {
      const question = q.trim();
      if (!question || loading) return;
      const candidates = retrieveCandidates(index, question, 50);
      setOpen(true); setResult(null);
      if (!candidates.length) {
        setResult({ error: "The feed has not loaded yet. Try again in a moment." });
        return;
      }
      setLoading(true);
      try {
        const resp = await fetch("/api/ask", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ question, items: candidates }),
        });
        const d = await resp.json().catch(() => ({}));
        setResult(resp.ok ? d : { error: d.error || ("Request failed (" + resp.status + ")") });
      } catch (_) {
        setResult({ error: "Could not reach the search backend." });
      } finally {
        setLoading(false);
      }
    }

    let panelBody;
    if (loading) {
      panelBody = e("div", { className: "askbar-status" }, "Searching the feed and asking the model...");
    } else if (result && result.error) {
      panelBody = e("div", { className: "askbar-status askbar-err" }, result.error);
    } else if (result && result.answer) {
      panelBody = e(React.Fragment, null,
        e("div", { className: "askbar-answer" }, result.answer),
        cited.length ? e("div", { className: "askbar-sources" },
          e("div", { className: "askbar-sources-h" }, "Sources"),
          cited.map((s) => {
            const meta = `${s.date}${s.source_label ? " · " + s.source_label : ""}${s.ticker ? " · " + s.ticker : ""}`;
            const inner = [
              e("span", { className: "askbar-src-n", key: "n" }, `[${s.n}]`),
              e("span", { className: "askbar-src-meta", key: "m" }, meta),
              s.url ? e("span", { className: "askbar-src-arrow", key: "a", "aria-hidden": "true" }, "↗") : null,
            ];
            return s.url
              ? e("a", { className: "askbar-src", key: s.n, href: s.url, target: "_blank", rel: "noopener noreferrer" }, inner)
              : e("div", { className: "askbar-src askbar-src-plain", key: s.n }, inner);
          })
        ) : null,
        e("div", { className: "askbar-model" },
          `via ${result.model || "model"}. Grounded in the feed; verify before relying.`)
      );
    } else {
      panelBody = e("div", { className: "askbar-status" }, "Type a question and press Enter.");
    }

    return e("div", { className: "askbar", ref: boxRef },
      e("button", {
        className: "ask-trigger", type: "button", onClick: () => setOpen((v) => !v),
        "aria-expanded": open, "aria-haspopup": "dialog", "aria-label": "Search the feed",
      },
        searchIcon("ask-trigger-ic", 13),
        e("span", { className: "ask-trigger-label" }, "Search the feed"),
        e("span", { className: "ask-kbd", "aria-hidden": "true" }, "/")),
      open ? e("div", { className: "askbar-panel", role: "dialog", "aria-label": "Search the feed" },
        e("div", { className: "askbar-field" },
          searchIcon("askbar-ico", 15),
          e("input", {
            ref: inputRef, className: "askbar-input", type: "text", value: q,
            placeholder: "Ask the feed a question",
            onChange: (ev) => setQ(ev.target.value),
            onKeyDown: (ev) => { if (ev.key === "Enter") ask(); },
            "aria-label": "Ask a question about the feed",
          }),
          loading ? e("span", { className: "askbar-spin", "aria-hidden": "true" }) : null),
        panelBody) : null);
  }

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