// ————————————————————————————————————————————————————————————————
//  Firehose — chronological news view (Bloomberg-dense)
// ————————————————————————————————————————————————————————————————

const { useState: NUS, useMemo: NUM, useEffect: NUE, useRef: NUR } = React;

// ---- tiny helpers
function tsParse(s) {
  if (!s) return new Date(0);
  return new Date(s.replace(" ", "T") + ":00+09:00");
}
function tsDay(s) {
  return (s || "").split(" ")[0];
}
function tsTime(s) {
  return (s || "").split(" ")[1] || "";
}
function humanDay(d, today) {
  // d and today are "YYYY-MM-DD"
  if (d === today) return "TODAY";
  const dd = new Date(d + "T00:00:00+09:00"),
    tt = new Date(today + "T00:00:00+09:00");
  const days = Math.round((tt - dd) / (1000 * 60 * 60 * 24));
  if (days === 1) return "YESTERDAY";
  const dt = new Date(d + "T00:00:00+09:00");
  return dt
    .toLocaleDateString("en-US", {
      weekday: "short",
      month: "short",
      day: "numeric",
    })
    .toUpperCase();
}

const IMPACT_COLOR = {
  high: "oklch(68% 0.18 25)",
  med: "oklch(76% 0.14 70)",
  low: "oklch(72% 0.05 260)",
};

const SOURCE_COLOR = {
  "Nikkei Asia": "oklch(72% 0.16 25)",
  Nikkei: "oklch(72% 0.16 25)",
  Reuters: "oklch(66% 0.18 30)",
  Bloomberg: "oklch(72% 0.14 260)",
};

function sentimentClass(s) {
  if (s >= 0.4) return "pos";
  if (s >= 0.1) return "posw";
  if (s <= -0.4) return "neg";
  if (s <= -0.1) return "negw";
  return "neu";
}

// Mini source sentiment heartbeat — sparkline of last 20 news items' sentiment
function SourceHeartbeat({ source, news }) {
  const seq = NUM(
    () =>
      news
        .filter((n) => n.source === source)
        .slice(0, 20)
        .reverse(),
    [source, news],
  );
  const W = 60,
    H = 18;
  if (!seq.length) return null;
  const pts = seq.map((n, i) => {
    const x = (i / Math.max(1, seq.length - 1)) * W;
    const y = H / 2 - n.sentiment * (H / 2 - 2);
    return `${x.toFixed(1)},${y.toFixed(1)}`;
  });
  const avg = seq.reduce((a, b) => a + b.sentiment, 0) / seq.length;
  const col = avg >= 0 ? "oklch(74% 0.16 155)" : "oklch(68% 0.18 25)";
  return (
    <svg width={W} height={H} className="src-heartbeat">
      <line
        x1="0"
        x2={W}
        y1={H / 2}
        y2={H / 2}
        stroke="var(--line-faint)"
        strokeDasharray="2 3"
      />
      <polyline
        points={pts.join(" ")}
        fill="none"
        stroke={col}
        strokeWidth="1.2"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function ThemePill({ themeId, active, onToggle, count }) {
  const th = window.THEMES.find((t) => t.id === themeId);
  if (!th) return null;
  return (
    <button
      className={"nv-pill" + (active ? " on" : "")}
      style={
        active
          ? {
              borderColor: th.color,
              background: `color-mix(in oklch, ${th.color} 14%, var(--bg-1))`,
              color: "var(--fg-0)",
            }
          : {}
      }
      onClick={onToggle}
    >
      <span className="tc-dot" style={{ background: th.color }} />
      {th.name}
      {count !== undefined && <span className="nv-pill-n">{count}</span>}
    </button>
  );
}

function TickerChip({ ticker, onOpen, lang }) {
  const s = window.STOCKS[ticker];
  if (!s) return null;
  const up = s.chg >= 0;
  const name = lang === "jp" ? s.jp || s.name : s.name;
  return (
    <button
      className="nv-tchip"
      onClick={(e) => {
        e.stopPropagation();
        onOpen && onOpen(ticker);
      }}
      title={`${ticker} · ${name}`}
    >
      <span className="nv-tchip-t">{ticker.replace(".T", "")}</span>
      <span className="nv-tchip-n">{name}</span>
      <span className={"nv-tchip-c " + (up ? "up" : "down")}>
        {window.fmtPct(s.chg)}
      </span>
    </button>
  );
}

function NewsRow({ n, expanded, onToggle, onOpenStock, onOpenTheme, lang }) {
  const impactCol = IMPACT_COLOR[n.impact];
  const srcCol = SOURCE_COLOR[n.source] || "oklch(66% 0.04 260)";
  const sentCls = sentimentClass(n.sentiment);
  return (
    <div className={"nv-row" + (expanded ? " ex" : "")}>
      <button className="nv-row-main" onClick={onToggle}>
        <span className="nv-row-t">{tsTime(n.date)}</span>
        <span
          className="nv-row-src"
          style={{
            color: srcCol,
            borderColor: `color-mix(in oklch, ${srcCol} 40%, transparent)`,
          }}
        >
          {n.source}
        </span>
        <span
          className="nv-row-imp"
          title={`impact: ${n.impact}`}
          style={{ background: impactCol }}
        />
        <span className="nv-row-h">{n.headline}</span>
        <span
          className={"nv-row-s " + sentCls}
          title={`sentiment ${(n.sentiment || 0) >= 0 ? "+" : ""}${(n.sentiment || 0).toFixed(2)}`}
        >
          <span
            className="nv-row-s-bar"
            style={{
              width: `${Math.abs(n.sentiment) * 100}%`,
              marginLeft: n.sentiment < 0 ? `${(1 + n.sentiment) * 100}%` : 0,
            }}
          />
        </span>
      </button>
      <div className="nv-row-tags">
        {(n.tickers || []).slice(0, 5).map((tk) => (
          <TickerChip key={tk} ticker={tk} onOpen={onOpenStock} lang={lang} />
        ))}
        {(n.themes || []).slice(0, 2).map((th) => {
          const theme = window.THEMES.find((t) => t.id === th);
          if (!theme) return null;
          return (
            <button
              key={th}
              className="nv-thchip"
              onClick={(e) => {
                e.stopPropagation();
                onOpenTheme && onOpenTheme(th);
              }}
              style={{
                borderColor: `color-mix(in oklch, ${theme.color} 50%, transparent)`,
                color: theme.color,
              }}
            >
              <span className="tc-dot" style={{ background: theme.color }} />
              {theme.name}
            </button>
          );
        })}
      </div>
      {expanded && n.summary && (
        <div className="nv-row-ex">
          <div className="nv-row-ex-sum">{n.summary}</div>
          {(n.tickers || []).length > 0 && (
            <div className="nv-row-ex-mov">
              <div className="nv-row-ex-l">
                {lang === "jp" ? "関連銘柄" : "Related movers"}
              </div>
              <div className="nv-row-ex-ticks">
                {(n.tickers || []).map((tk) => {
                  const s = window.STOCKS[tk];
                  if (!s) return null;
                  const up = s.chg >= 0;
                  return (
                    <button
                      key={tk}
                      className="nv-row-ex-tick"
                      onClick={() => onOpenStock && onOpenStock(tk)}
                    >
                      <div className="nv-row-ex-tick-l">
                        <div className="nv-row-ex-tick-t">
                          {tk.replace(".T", "")}
                        </div>
                        <div className="nv-row-ex-tick-n">
                          {lang === "jp" ? s.jp : s.name}
                        </div>
                      </div>
                      <div
                        className={"nv-row-ex-tick-c " + (up ? "up" : "down")}
                      >
                        {window.fmtPct(s.chg)}
                      </div>
                    </button>
                  );
                })}
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function TopMovers({ news, onOpenStock, lang }) {
  // Rank tickers by (abs(chg) * mention count) to surface "news-moved" names
  const ranked = NUM(() => {
    const count = {};
    news.forEach((n) =>
      (n.tickers || []).forEach((t) => {
        count[t] = (count[t] || 0) + 1;
      }),
    );
    return Object.entries(count)
      .map(([t, c]) => {
        const s = window.STOCKS[t];
        if (!s) return null;
        return {
          t,
          c,
          chg: s.chg,
          score: Math.abs(s.chg) * (1 + Math.log(c + 1)),
        };
      })
      .filter(Boolean)
      .sort((a, b) => b.score - a.score)
      .slice(0, 8);
  }, [news]);
  return (
    <div className="nv-movers">
      <div className="nv-movers-h">
        {lang === "jp" ? "ニュース連動上位" : "Top movers tied to news"}
      </div>
      <div className="nv-movers-list">
        {ranked.map((r) => {
          const s = window.STOCKS[r.t];
          const up = s.chg >= 0;
          return (
            <button
              key={r.t}
              className="nv-mover"
              onClick={() => onOpenStock(r.t)}
            >
              <div className="nv-mover-l">
                <div className="nv-mover-t">{r.t.replace(".T", "")}</div>
                <div className="nv-mover-n">
                  {lang === "jp" ? s.jp : s.name}
                </div>
              </div>
              <div className="nv-mover-r">
                <div className={"nv-mover-c " + (up ? "up" : "down")}>
                  {window.fmtPct(s.chg)}
                </div>
                <div className="nv-mover-m">
                  {r.c} {lang === "jp" ? "本" : "items"}
                </div>
              </div>
            </button>
          );
        })}
      </div>
    </div>
  );
}

function SourceSentiment({ news, lang }) {
  const sources = NUM(() => {
    const map = {};
    news.forEach((n) => {
      if (!map[n.source]) map[n.source] = { sum: 0, n: 0 };
      map[n.source].sum += n.sentiment;
      map[n.source].n += 1;
    });
    return Object.entries(map)
      .map(([s, v]) => ({ s, avg: v.sum / v.n, n: v.n }))
      .sort((a, b) => b.n - a.n);
  }, [news]);
  return (
    <div className="nv-sources">
      <div className="nv-sources-h">
        {lang === "jp" ? "ソース別センチメント" : "Source sentiment"}
      </div>
      <div className="nv-sources-list">
        {sources.map((src) => (
          <div key={src.s} className="nv-source">
            <div className="nv-source-l">
              <div
                className="nv-source-t"
                style={{ color: SOURCE_COLOR[src.s] || "var(--fg-1)" }}
              >
                {src.s}
              </div>
              <div className="nv-source-n">
                {src.n} {lang === "jp" ? "本" : "items"}
              </div>
            </div>
            <SourceHeartbeat source={src.s} news={news} />
            <div className={"nv-source-avg " + (src.avg >= 0 ? "pos" : "neg")}>
              {src.avg >= 0 ? "+" : ""}
              {src.avg.toFixed(2)}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

function NewsView({ lang, onOpenStock, onOpenTheme }) {
  const [expanded, setExpanded] = NUS(new Set());
  const [themeFilters, setThemeFilters] = NUS(new Set());
  const [sourceFilter, setSourceFilter] = NUS("all");
  const [sentimentFilter, setSentimentFilter] = NUS("all"); // all | pos | neg
  const [impactFilter, setImpactFilter] = NUS("all");

  const allNews = NUM(
    () => [...window.NEWS].sort((a, b) => tsParse(b.date) - tsParse(a.date)),
    [],
  );

  const filtered = NUM(() => {
    return allNews.filter((n) => {
      if (
        themeFilters.size > 0 &&
        !(n.themes || []).some((t) => themeFilters.has(t))
      )
        return false;
      if (sourceFilter !== "all" && n.source !== sourceFilter) return false;
      if (sentimentFilter === "pos" && n.sentiment <= 0) return false;
      if (sentimentFilter === "neg" && n.sentiment >= 0) return false;
      if (impactFilter !== "all" && n.impact !== impactFilter) return false;
      return true;
    });
  }, [allNews, themeFilters, sourceFilter, sentimentFilter, impactFilter]);

  // Group by day
  const grouped = NUM(() => {
    const groups = {};
    filtered.forEach((n) => {
      const d = tsDay(n.date);
      if (!groups[d]) groups[d] = [];
      groups[d].push(n);
    });
    return Object.entries(groups).sort((a, b) => b[0].localeCompare(a[0]));
  }, [filtered]);

  const today = allNews.length ? tsDay(allNews[0].date) : "2026-04-17";

  // Theme counts for pills
  const themeCounts = NUM(() => {
    const c = {};
    window.THEMES.forEach((th) => {
      c[th.id] = 0;
    });
    allNews.forEach((n) =>
      (n.themes || []).forEach((th) => {
        if (c[th] !== undefined) c[th]++;
      }),
    );
    return c;
  }, [allNews]);

  const sources = NUM(
    () => Array.from(new Set(allNews.map((n) => n.source))),
    [allNews],
  );

  function toggleExpand(id) {
    const s = new Set(expanded);
    s.has(id) ? s.delete(id) : s.add(id);
    setExpanded(s);
  }
  function toggleTheme(id) {
    const s = new Set(themeFilters);
    s.has(id) ? s.delete(id) : s.add(id);
    setThemeFilters(s);
  }

  // Tick a "live" marker every second for the LIVE indicator dot
  const [tick, setTick] = NUS(0);
  NUE(() => {
    const t = setInterval(() => setTick((x) => x + 1), 1100);
    return () => clearInterval(t);
  }, []);

  return (
    <div className="nv-view">
      {/* Header bar */}
      <div className="nv-header">
        <div className="nv-header-l">
          <div className="nv-title">
            <span className="nv-live">
              <span className="nv-live-dot" />
              {lang === "jp" ? "ライブ" : "LIVE"}
            </span>
            <span className="nv-title-t">
              {lang === "jp" ? "ファイアホース" : "Firehose"}
            </span>
            <span className="nv-title-s">
              {filtered.length} / {allNews.length}{" "}
              {lang === "jp" ? "件" : "items"}
            </span>
          </div>
        </div>
        <div className="nv-header-r">
          <div className="seg small">
            <button
              className={impactFilter === "all" ? "on" : ""}
              onClick={() => setImpactFilter("all")}
            >
              {lang === "jp" ? "全て" : "All impact"}
            </button>
            <button
              className={impactFilter === "high" ? "on" : ""}
              onClick={() => setImpactFilter("high")}
            >
              High
            </button>
            <button
              className={impactFilter === "med" ? "on" : ""}
              onClick={() => setImpactFilter("med")}
            >
              Med
            </button>
            <button
              className={impactFilter === "low" ? "on" : ""}
              onClick={() => setImpactFilter("low")}
            >
              Low
            </button>
          </div>
          <div className="seg small">
            <button
              className={sentimentFilter === "all" ? "on" : ""}
              onClick={() => setSentimentFilter("all")}
            >
              {lang === "jp" ? "全感情" : "All"}
            </button>
            <button
              className={sentimentFilter === "pos" ? "on" : ""}
              onClick={() => setSentimentFilter("pos")}
            >
              + pos
            </button>
            <button
              className={sentimentFilter === "neg" ? "on" : ""}
              onClick={() => setSentimentFilter("neg")}
            >
              − neg
            </button>
          </div>
          <select
            className="nv-src-sel"
            value={sourceFilter}
            onChange={(e) => setSourceFilter(e.target.value)}
          >
            <option value="all">
              {lang === "jp" ? "全ソース" : "All sources"}
            </option>
            {sources.map((s) => (
              <option key={s} value={s}>
                {s}
              </option>
            ))}
          </select>
        </div>
      </div>

      {/* Theme pill rail */}
      <div className="nv-pills">
        {window.THEMES.filter((th) => themeCounts[th.id] > 0).map((th) => (
          <ThemePill
            key={th.id}
            themeId={th.id}
            active={themeFilters.has(th.id)}
            onToggle={() => toggleTheme(th.id)}
            count={themeCounts[th.id]}
          />
        ))}
        {themeFilters.size > 0 && (
          <button
            className="nv-pill-clear"
            onClick={() => setThemeFilters(new Set())}
          >
            {lang === "jp" ? "クリア" : "clear"}
          </button>
        )}
      </div>

      {/* Main grid: feed + rail */}
      <div className="nv-grid">
        <div className="nv-feed">
          {grouped.length === 0 && (
            <div className="nv-empty">
              {lang === "jp"
                ? "一致するニュースはありません"
                : "No news match these filters."}
            </div>
          )}
          {grouped.map(([day, items]) => (
            <div key={day} className="nv-day">
              <div className="nv-day-h">
                <span className="nv-day-l">{humanDay(day, today)}</span>
                <span className="nv-day-d">{day}</span>
                <span className="nv-day-n">
                  {items.length} {lang === "jp" ? "本" : "items"}
                </span>
                <div className="nv-day-line" />
              </div>
              <div className="nv-day-list">
                {items.map((n) => (
                  <NewsRow
                    key={n.id}
                    n={n}
                    expanded={expanded.has(n.id)}
                    onToggle={() => toggleExpand(n.id)}
                    onOpenStock={onOpenStock}
                    onOpenTheme={onOpenTheme}
                    lang={lang}
                  />
                ))}
              </div>
            </div>
          ))}
        </div>

        <aside className="nv-rail">
          <TopMovers news={filtered} onOpenStock={onOpenStock} lang={lang} />
          <SourceSentiment news={allNews} lang={lang} />
        </aside>
      </div>
    </div>
  );
}

window.NewsView = NewsView;
