import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
import { Loader2 } from "lucide-react";
import { scaleLinear } from "d3-scale";
import { interpolateRgb } from "d3-interpolate";
import { fetchTimeline } from "../api/client.js";
import { getCachedTimeline, setCachedTimeline } from "../utils/investigationCache.js";
import { viewportTooltipPosition } from "../utils/viewportTooltip.js";
import { getNewsTimeframeLabel } from "../constants/timeframes.js";
import "./TimelineView.css";

// Sentiment color: red (negative) → gold (mixed) → teal (positive)
const sentimentColor = scaleLinear()
  .domain([-1, 0, 1])
  .range(["#E05252", "#C8A96E", "#4A9E8A"])
  .interpolate(interpolateRgb)
  .clamp(true);

/** Avoid d3 degenerate domains when all points share the same time (ms). */
function padTimeDomain(minTs, maxTs) {
  const min = Number(minTs);
  const max = Number(maxTs);
  if (!Number.isFinite(min) || !Number.isFinite(max)) {
    const now = Date.now();
    return [now - 7 * 86400000, now];
  }
  if (min >= max) {
    const half = 12 * 3600000;
    return [min - half, max + half];
  }
  const span = max - min;
  const pad = Math.max(span * 0.04, 3600000);
  return [min - pad, max + pad];
}

function safeSentiment(v) {
  const n = Number(v);
  return Number.isFinite(n) ? Math.max(-1, Math.min(1, n)) : 0;
}

/** News scatter height: bounded from width only (do not use full page height). */
function newsChartHeightPx(width) {
  const w = Math.max(320, width);
  return Math.min(340, Math.max(240, Math.round(w * 0.26)));
}

export default function TimelineView({ query }) {
  const plotClipId = useId().replace(/:/g, "");
  const newsClipUrl = `url(#tl-${plotClipId}-news)`;
  const interestClipUrl = `url(#tl-${plotClipId}-interest)`;

  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [size, setSize] = useState({ w: 1100, newsH: newsChartHeightPx(1100) });
  const [hover, setHover] = useState(null);
  const containerRef = useRef(null);
  const timelineTooltipRef = useRef(null);

  const topic = query?.topic?.trim() ?? "";
  const newsTimeframe = query?.newsTimeframe ?? "6m";

  useEffect(() => {
    let cancelled = false;
    if (!topic) {
      setData(null);
      setLoading(false);
      return () => {
        cancelled = true;
      };
    }
    const cached = getCachedTimeline(topic, newsTimeframe);
    if (cached) {
      setData(cached);
      setLoading(false);
      return () => {
        cancelled = true;
      };
    }
    setLoading(true);
    fetchTimeline({ topic, newsTimeframe })
      .then((d) => {
        if (cancelled) return;
        setData(d);
        setCachedTimeline(topic, newsTimeframe, d);
      })
      .finally(() => !cancelled && setLoading(false));
    return () => {
      cancelled = true;
    };
  }, [topic, newsTimeframe]);

  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    let debounceT = 0;
    let raf = 0;
    const applyWidth = (w) => {
      const nextW = Math.max(320, Math.floor(w) || 1100);
      setSize({ w: nextW, newsH: newsChartHeightPx(nextW) });
    };
    /** Debounce: mobile chrome + layout thrash fires ResizeObserver in bursts. */
    const schedule = (w) => {
      window.clearTimeout(debounceT);
      debounceT = window.setTimeout(() => {
        if (raf) cancelAnimationFrame(raf);
        raf = requestAnimationFrame(() => {
          raf = 0;
          applyWidth(w);
        });
      }, 100);
    };
    const updateFromRect = () => {
      schedule(el.getBoundingClientRect().width);
    };

    updateFromRect();

    if (typeof ResizeObserver === "undefined") {
      window.addEventListener("resize", updateFromRect);
      return () => {
        window.removeEventListener("resize", updateFromRect);
        window.clearTimeout(debounceT);
        if (raf) cancelAnimationFrame(raf);
      };
    }

    const ro = new ResizeObserver((entries) => {
      schedule(entries[0].contentRect.width);
    });
    ro.observe(el);
    return () => {
      ro.disconnect();
      window.clearTimeout(debounceT);
      if (raf) cancelAnimationFrame(raf);
    };
  }, []);

  const normalizedData = useMemo(() => normalizeTimelineData(data, query), [data, query]);

  const { xScale, yScale, rScale, ticksX, ticksY, padding, isCompact, hasPoints } = useMemo(() => {
    if (!normalizedData?.newsDistribution) return {};
    const isCompact = size.w < 760;
    const padding = isCompact
      ? { top: 46, right: 20, bottom: 56, left: 42 }
      : { top: 60, right: 60, bottom: 70, left: 60 };
    const points = normalizedData.newsDistribution.points ?? [];
    if (!points.length) {
      return {
        xScale: null,
        yScale: null,
        rScale: null,
        ticksX: [],
        ticksY: [],
        padding,
        isCompact,
        hasPoints: false,
      };
    }
    const xMin = Math.min(...points.map((p) => p.timestamp));
    const xMax = Math.max(...points.map((p) => p.timestamp));
    const winS = normalizedData.newsDistribution.windowStartMs;
    const winE = normalizedData.newsDistribution.windowEndMs;
    const hasWin =
      Number.isFinite(winS) && Number.isFinite(winE) && Number(winE) > Number(winS);
    let xd0;
    let xd1;
    if (hasWin) {
      xd0 = Number(winS);
      xd1 = Number(winE);
    } else {
      [xd0, xd1] = padTimeDomain(xMin, xMax);
    }
    const yMax = Math.max(1, ...points.map((p) => Number(p.articleCount) || 0));
    const innerW = size.w - padding.left - padding.right;
    const innerH = size.newsH - padding.top - padding.bottom;

    const x = scaleLinear().domain([xd0, xd1]).range([padding.left, padding.left + innerW]);
    const y = scaleLinear().domain([0, yMax * 1.1]).range([padding.top + innerH, padding.top]);
    const r = scaleLinear().domain([0, yMax]).range([2, 14]);

    const ticksX = x.ticks(6);
    const ticksY = y.ticks(5);
    return { xScale: x, yScale: y, rScale: r, ticksX, ticksY, padding, isCompact, hasPoints: true };
  }, [normalizedData, size.w, size.newsH]);

  const newsWindowLabel = getNewsTimeframeLabel(query.newsTimeframe);
  const axisTimeframe =
    normalizedData?.newsDistribution?.selectedWindow ?? query?.newsTimeframe ?? "6m";

  const interestSource = normalizedData?.interestOverTime?.source ?? "unknown";
  const trendKeywords = normalizedData?.interestOverTime?.keywords ?? [];
  const trendsTimeframe = normalizedData?.interestOverTime?.trendsTimeframe ?? null;
  const trendsFetchError = normalizedData?.interestOverTime?.trendsFetchError ?? null;

  const interestPanel = useMemo(() => {
    const windowNote = `Uses the same range as the news chart (${newsWindowLabel}).`;
    if (interestSource === "google-trends") {
      return {
        title: "Google Trends",
        lede: `Relative search interest (Google’s 0–100 scale) for the terms below. ${windowNote}`,
      };
    }
    if (interestSource === "google-trends-empty") {
      return {
        title: "Google Trends",
        lede: trendsTimeframe
          ? `No daily series from Google for ${trendsTimeframe}. ${windowNote} You can change the investigation timeframe when editing the topic.`
          : `No daily series for this range. ${windowNote} You can change the investigation timeframe when editing the topic.`,
      };
    }
    if (interestSource === "google-trends-unavailable") {
      const detail =
        trendsFetchError === "disabled_by_config"
          ? "Google Trends is turned off on the server (set GOOGLE_TRENDS_ENABLED=true to enable)."
          : trendsFetchError === "missing_dependency"
            ? "The API server is missing the `pytrends-modern` or `pytrends` package (install dependencies and restart)."
            : trendsFetchError === "rate_limited_or_blocked"
              ? "Google blocked or rate-limited the request. This unofficial Trends endpoint is fragile from datacenters and under load."
              : trendsFetchError === "empty_keywords_or_series"
                ? "No usable keywords or time series came back for this topic and date span."
                : trendsFetchError === "timeout"
                  ? "Google Trends exceeded the server wait limit."
                  : trendsFetchError
                    ? `Request failed (${trendsFetchError}). Check server logs for the full traceback.`
                    : "Try again later, or inspect server logs if you operate this deployment.";
      return {
        title: "Google Trends",
        lede: `Search-interest data could not be loaded. ${detail}`,
      };
    }
    if (interestSource === "derived-from-news-volume") {
      return {
        title: "Article volume",
        lede: `Fallback curve from indexed article dates. ${windowNote}`,
      };
    }
    return {
      title: "Search interest",
      lede: `External attention signal. ${windowNote}`,
    };
  }, [interestSource, trendsTimeframe, newsWindowLabel, trendsFetchError]);

  const interestSeries = useMemo(() => {
    const all = normalizedData?.interestOverTime?.points ?? [];
    if (!all.length) return [];

    let filtered = all.filter((p) => Number.isFinite(Number(p?.timestamp)));
    const winS = normalizedData?.newsDistribution?.windowStartMs;
    const winE = normalizedData?.newsDistribution?.windowEndMs;
    const hasWin =
      Number.isFinite(winS) && Number.isFinite(winE) && Number(winE) > Number(winS);
    if (hasWin) {
      const ws = Number(winS);
      const we = Number(winE);
      filtered = filtered.filter((p) => {
        const t = Number(p.timestamp);
        return t >= ws && t <= we;
      });
    }
    return filtered;
  }, [normalizedData]);

  const interestChart = useMemo(() => {
    if (!interestSeries.length) return null;
    const isCompact = size.w < 760;
    const cHeight = isCompact ? 260 : 300;
    const chartPadding = isCompact
      ? { top: 24, right: 20, bottom: 44, left: 42 }
      : { top: 28, right: 42, bottom: 48, left: 56 };

    const winS = normalizedData?.newsDistribution?.windowStartMs;
    const winE = normalizedData?.newsDistribution?.windowEndMs;
    const hasWin =
      Number.isFinite(winS) && Number.isFinite(winE) && Number(winE) > Number(winS);
    let ix0;
    let ix1;
    if (hasWin) {
      ix0 = Number(winS);
      ix1 = Number(winE);
    } else {
      const xMin = Math.min(...interestSeries.map((p) => p.timestamp));
      const xMax = Math.max(...interestSeries.map((p) => p.timestamp));
      [ix0, ix1] = padTimeDomain(xMin, xMax);
    }
    const yMax = Math.max(1, ...interestSeries.map((p) => Number(p.value) || 0));
    const innerW = size.w - chartPadding.left - chartPadding.right;
    const innerH = cHeight - chartPadding.top - chartPadding.bottom;

    const xScale = scaleLinear()
      .domain([ix0, ix1])
      .range([chartPadding.left, chartPadding.left + innerW]);
    const yScale = scaleLinear()
      .domain([0, yMax * 1.15])
      .range([chartPadding.top + innerH, chartPadding.top]);

    const ticksX = xScale.ticks(6);
    const ticksY = yScale.ticks(4);
    const linePath = interestSeries
      .map((p, i) => `${i === 0 ? "M" : "L"}${xScale(p.timestamp)} ${yScale(Number(p.value) || 0)}`)
      .join(" ");

    const singleInterest = interestSeries.length === 1;
    const lone = singleInterest ? interestSeries[0] : null;

    return {
      cHeight,
      chartPadding,
      xScale,
      yScale,
      ticksX,
      ticksY,
      linePath,
      singleInterest,
      lonePoint: lone,
    };
  }, [interestSeries, size.w, normalizedData?.newsDistribution]);

  useLayoutEffect(() => {
    if (!hover) return;
    const el = timelineTooltipRef.current;
    if (!el || hover.clientX == null || hover.clientY == null) return;
    const { width, height } = el.getBoundingClientRect();
    const estWidth = width > 0 ? width : 236;
    const estHeight = height > 0 ? height : 92;
    const { left, top } = viewportTooltipPosition(hover.clientX, hover.clientY, {
      estWidth,
      estHeight,
    });
    el.style.left = `${left}px`;
    el.style.top = `${top}px`;
  }, [hover]);

  return (
    <div className="timeline-view" ref={containerRef}>
      <header className="timeline-view__header">
        <div>
          <div className="label">Timeline</div>
          <h1 className="timeline-view__title">Narrative and attention over time</h1>
        </div>
      </header>

      <section className="timeline-card">
        <div className="timeline-card__head">
          <div>
            <div className="label">News distribution</div>
            <h2 className="timeline-card__title">{newsWindowLabel}</h2>
          </div>
          <p className="timeline-card__subtle">
            Controlled by the main investigation timeframe.
          </p>
        </div>

        <TimelineLegend />

        {loading || !data ? (
          <div className="timeline-view__loading">
            <Loader2 size={14} className="spin" />
            <span>plotting article volume</span>
          </div>
        ) : !hasPoints ? (
          <div className="timeline-view__loading">
            <span>no timeline data for this query</span>
          </div>
        ) : (
          <svg
            viewBox={`0 0 ${size.w} ${size.newsH}`}
            width="100%"
            preserveAspectRatio="xMidYMid meet"
            className="timeline-view__svg"
            onMouseLeave={() => setHover(null)}
          >
          {/* Y axis */}
          {ticksY.map((t) => (
            <g key={t} transform={`translate(0,${yScale(t)})`}>
              <line
                x1={padding.left}
                x2={size.w - padding.right}
                stroke="var(--border)"
                strokeDasharray="2 4"
                opacity={t === 0 ? 0.6 : 0.25}
              />
              <text
                x={padding.left - 12}
                y={4}
                textAnchor="end"
                className="timeline-view__tick"
              >
                {t}
              </text>
            </g>
          ))}

          {/* X axis */}
          {ticksX.map((t) => (
            <g key={t} transform={`translate(${xScale(t)},0)`}>
              <line
                y1={padding.top}
                y2={size.newsH - padding.bottom}
                stroke="var(--border)"
                strokeDasharray="2 4"
                opacity={0.25}
              />
              <text
                y={size.newsH - padding.bottom + 22}
                textAnchor="middle"
                className="timeline-view__tick"
              >
                {formatDateTick(t, axisTimeframe)}
              </text>
            </g>
          ))}

          {/* Axis labels */}
          <text
            x={padding.left}
            y={padding.top - 18}
            className="timeline-view__axis-label"
          >
            ARTICLE VOLUME
          </text>
          <text
            x={size.w - padding.right}
            y={size.newsH - padding.bottom + (isCompact ? 36 : 46)}
            textAnchor="end"
            className="timeline-view__axis-label"
          >
            COLOR · SENTIMENT (RED ← → TEAL)
          </text>

          <defs>
            <clipPath id={`tl-${plotClipId}-news`}>
              <rect
                x={padding.left}
                y={padding.top}
                width={Math.max(0, size.w - padding.left - padding.right)}
                height={Math.max(0, size.newsH - padding.top - padding.bottom)}
              />
            </clipPath>
          </defs>

          {/* Plot region only (stops scatter sitting on the y-axis when x is slightly out of domain). */}
          <g clipPath={newsClipUrl}>
            {(normalizedData.newsDistribution.events ?? []).map((e) => (
              <g key={e.id ?? e.timestamp} transform={`translate(${xScale(e.timestamp)},0)`}>
                <line
                  y1={padding.top}
                  y2={size.newsH - padding.bottom}
                  stroke="var(--accent)"
                  strokeOpacity="0.55"
                  strokeDasharray="3 3"
                />
                <g transform={`translate(8, ${padding.top + 6})`}>
                  <text className="timeline-view__event">{e.label}</text>
                </g>
              </g>
            ))}

            {normalizedData.newsDistribution.points.map((p, i) => {
              const cx = xScale(p.timestamp);
              const cy = yScale(p.articleCount);
              const r = rScale(p.articleCount);
              const fill = sentimentColor(safeSentiment(p.sentiment));
              return (
                <circle
                  key={`${p.timestamp}-${i}`}
                  cx={cx}
                  cy={cy}
                  r={r}
                  fill={fill}
                  fillOpacity={0.85}
                  stroke={fill}
                  strokeOpacity={0.4}
                  strokeWidth={r * 0.4}
                  onMouseEnter={(ev) => {
                    setHover({
                      ...p,
                      sentiment: safeSentiment(p.sentiment),
                      cx,
                      cy,
                      fill,
                      clientX: ev.clientX,
                      clientY: ev.clientY,
                    });
                  }}
                  onMouseLeave={() => setHover(null)}
                  style={{ cursor: "pointer" }}
                />
              );
            })}
          </g>
          </svg>
        )}
      </section>

      <section className="timeline-card timeline-card--interest">
        <div className="timeline-card__head">
          <div>
            <div className="label">Interest over time</div>
            <h2 className="timeline-card__title">{interestPanel.title}</h2>
            <p className="timeline-card__subtle timeline-card__subtle--interest">
              {interestPanel.lede}
            </p>
            {trendKeywords.length > 0 && (
              <p className="timeline-card__subtle timeline-card__subtle--keywords">
                <span className="label">Terms</span> {trendKeywords.join(" · ")}
              </p>
            )}
          </div>
        </div>

        {!interestChart ? (
          <div
            className="timeline-view__interest-chart-placeholder"
            role="presentation"
            aria-hidden="true"
          />
        ) : (
          <svg
            viewBox={`0 0 ${size.w} ${interestChart.cHeight}`}
            width="100%"
            preserveAspectRatio="xMidYMid meet"
            className="timeline-view__svg"
          >
            <text
              x={interestChart.chartPadding.left}
              y={interestChart.chartPadding.top - 8}
              textAnchor="start"
              className="timeline-view__axis-label"
            >
              {interestSource === "google-trends" || interestSource === "google-trends-empty"
                ? "SEARCH INTEREST (0–100)"
                : interestSource === "derived-from-news-volume"
                  ? "RELATIVE VOLUME"
                  : "RELATIVE SCORE"}
            </text>
            <text
              x={size.w - interestChart.chartPadding.right}
              y={
                interestChart.cHeight -
                interestChart.chartPadding.bottom +
                (size.w < 760 ? 36 : 44)
              }
              textAnchor="end"
              className="timeline-view__axis-label"
            >
              DATE
            </text>
            {interestChart.ticksY.map((t) => (
              <g key={`iy-${t}`} transform={`translate(0,${interestChart.yScale(t)})`}>
                <line
                  x1={interestChart.chartPadding.left}
                  x2={size.w - interestChart.chartPadding.right}
                  stroke="var(--border)"
                  strokeDasharray="2 4"
                  opacity={t === 0 ? 0.55 : 0.25}
                />
                <text
                  x={interestChart.chartPadding.left - 12}
                  y={4}
                  textAnchor="end"
                  className="timeline-view__tick"
                >
                  {Math.round(t)}
                </text>
              </g>
            ))}

            {interestChart.ticksX.map((t) => (
              <g key={`ix-${t}`} transform={`translate(${interestChart.xScale(t)},0)`}>
                <line
                  y1={interestChart.chartPadding.top}
                  y2={interestChart.cHeight - interestChart.chartPadding.bottom}
                  stroke="var(--border)"
                  strokeDasharray="2 4"
                  opacity={0.25}
                />
                <text
                  y={interestChart.cHeight - interestChart.chartPadding.bottom + 20}
                  textAnchor="middle"
                  className="timeline-view__tick"
                >
                  {new Date(t).toLocaleDateString("en-GB", {
                    month: "short",
                    year: "2-digit",
                  })}
                </text>
              </g>
            ))}

            <defs>
              <clipPath id={`tl-${plotClipId}-interest`}>
                <rect
                  x={interestChart.chartPadding.left}
                  y={interestChart.chartPadding.top}
                  width={Math.max(
                    0,
                    size.w - interestChart.chartPadding.left - interestChart.chartPadding.right
                  )}
                  height={Math.max(
                    0,
                    interestChart.cHeight -
                      interestChart.chartPadding.top -
                      interestChart.chartPadding.bottom
                  )}
                />
              </clipPath>
            </defs>
            <g clipPath={interestClipUrl}>
              {interestChart.singleInterest && interestChart.lonePoint ? (
                <circle
                  cx={interestChart.xScale(interestChart.lonePoint.timestamp)}
                  cy={interestChart.yScale(Number(interestChart.lonePoint.value) || 0)}
                  r={5}
                  fill="var(--accent)"
                  fillOpacity={0.95}
                  stroke="var(--accent)"
                  strokeOpacity={0.35}
                  strokeWidth={10}
                />
              ) : (
                <path
                  d={interestChart.linePath}
                  fill="none"
                  stroke="var(--accent)"
                  strokeWidth="2.5"
                  strokeLinejoin="round"
                  strokeLinecap="round"
                />
              )}
            </g>
          </svg>
        )}
      </section>

      {hover && (
        <div
          ref={timelineTooltipRef}
          className="map-tooltip"
          style={{
            position: "fixed",
            left: 0,
            top: 0,
            transform: "none",
          }}
        >
          <div className="map-tooltip__name">
            {new Date(hover.timestamp).toLocaleDateString("en-GB", {
              day: "2-digit",
              month: "short",
              year: "numeric",
            })}
          </div>
          <div className="map-tooltip__row">
            <span className="label">Articles</span>
            <span className="mono">{hover.articleCount}</span>
          </div>
          <div className="map-tooltip__row">
            <span className="label">Sentiment</span>
            <span className="mono" style={{ color: hover.fill }}>
              {safeSentiment(hover.sentiment).toFixed(2)}
            </span>
          </div>
        </div>
      )}
    </div>
  );
}

function formatDateTick(timestamp, windowValue) {
  const date = new Date(timestamp);
  if (windowValue === "48h") {
    return date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
  }
  if (windowValue === "1w" || windowValue === "2w") {
    return date.toLocaleDateString("en-GB", { day: "2-digit", month: "short" });
  }
  return date.toLocaleDateString("en-GB", { month: "short", year: "2-digit" });
}

function TimelineLegend() {
  const stops = [-1, -0.5, 0, 0.5, 1];
  return (
    <div className="timeline-legend">
      <div className="label timeline-legend__title">Sentiment</div>
      <div className="timeline-legend__bar">
        {stops.map((s, i) => (
          <div
            key={i}
            className="timeline-legend__cell"
            style={{ background: sentimentColor(s) }}
          />
        ))}
      </div>
      <div className="timeline-legend__scale">
        <span>negative</span>
        <span>positive</span>
      </div>
    </div>
  );
}

function defaultAvailableYears() {
  const currentYear = new Date().getFullYear();
  const years = [];
  for (let yr = currentYear; yr >= 2019; yr -= 1) years.push(yr);
  return years;
}

function normalizeTimelineData(raw, query) {
  if (!raw) return null;

  // News chart must work even when interestOverTime is missing or Trends failed server-side.
  if (raw.newsDistribution) {
    const rawIo = raw.interestOverTime;
    const hasIo = rawIo && typeof rawIo === "object";
    const interestOverTime = hasIo
      ? {
          source: "google-trends-unavailable",
          keywords: [],
          trendsTimeframe: null,
          trendsFetchError: null,
          defaultWindow: "12m",
          availableYears: defaultAvailableYears(),
          points: [],
          ...rawIo,
          points: rawIo.points ?? [],
          availableYears:
            Array.isArray(rawIo.availableYears) && rawIo.availableYears.length
              ? rawIo.availableYears
              : defaultAvailableYears(),
          keywords: rawIo.keywords ?? [],
        }
      : {
          source: "google-trends-unavailable",
          keywords: [],
          trendsTimeframe: null,
          trendsFetchError: null,
          defaultWindow: "12m",
          availableYears: defaultAvailableYears(),
          points: [],
        };

    return {
      ...raw,
      newsDistribution: {
        ...raw.newsDistribution,
        points: raw.newsDistribution.points ?? [],
        events: raw.newsDistribution.events ?? [],
        windowStartMs: raw.newsDistribution.windowStartMs,
        windowEndMs: raw.newsDistribution.windowEndMs,
      },
      interestOverTime,
    };
  }

  const points = raw.points ?? [];
  const events = raw.events ?? [];
  const availableYears = defaultAvailableYears();

  const byMonth = new Map();
  points.forEach((point) => {
    const d = new Date(point.timestamp);
    const key = `${d.getFullYear()}-${d.getMonth()}`;
    const current = byMonth.get(key);
    if (!current) {
      byMonth.set(key, {
        timestamp: new Date(d.getFullYear(), d.getMonth(), 1).getTime(),
        value: Math.max(0, Math.round(point.articleCount || 0)),
      });
      return;
    }
    current.value += Math.max(0, Math.round(point.articleCount || 0));
  });

  const trendPoints = Array.from(byMonth.values()).sort(
    (a, b) => a.timestamp - b.timestamp
  );

  return {
    newsDistribution: {
      selectedWindow: query?.newsTimeframe ?? "6m",
      points,
      events: events ?? [],
    },
    interestOverTime: {
      source: "derived-from-news-volume",
      keywords: [],
      trendsTimeframe: null,
      trendsFetchError: null,
      defaultWindow: "12m",
      availableYears,
      points: trendPoints,
    },
  };
}
