/**
 * SiCierto Forecast — main React app.
 *
 * Renders:
 *  - Header strip: asset, regime, prob-higher across horizons, next-refresh countdown
 *  - Main chart: recent actuals + upcoming forecast (line + 80% confidence band)
 *  - Track record panel: band hit rate, direction Brier score, MAE at 1/6/24h
 *  - Footer: last successful run, worker status dot, disclaimer
 */

const { useEffect, useState, useMemo } = React;
const {
  ResponsiveContainer, ComposedChart, Line, Area, XAxis, YAxis,
  Tooltip, ReferenceLine, CartesianGrid, Label,
} = Recharts;

const CFG = window.SICIERTO_CONFIG;
const supabase = window.supabase.createClient(
  CFG.SUPABASE_URL,
  CFG.SUPABASE_PUBLISHABLE_KEY
);

// ---------- data fetchers ----------

async function fetchLatestRun() {
  const { data, error } = await supabase
    .from("forecasts")
    .select("run_at")
    .order("run_at", { ascending: false })
    .limit(1);
  if (error) throw error;
  return data?.[0]?.run_at ?? null;
}

async function fetchForecastsForRun(runAt) {
  const { data, error } = await supabase
    .from("forecasts")
    .select("*")
    .eq("run_at", runAt)
    .order("horizon_hours", { ascending: true });
  if (error) throw error;
  return data ?? [];
}

async function fetchRegimeForRun(runAt) {
  const { data, error } = await supabase
    .from("regimes")
    .select("*")
    .eq("run_at", runAt)
    .limit(1);
  if (error) throw error;
  return data?.[0] ?? null;
}

async function fetchRecentActuals(hoursBack = 48) {
  const since = new Date(Date.now() - hoursBack * 3600 * 1000).toISOString();
  const { data, error } = await supabase
    .from("forecasts")
    .select("target_at, actual, price_at_run, horizon_hours")
    .eq("horizon_hours", 1)
    .gte("target_at", since)
    .not("actual", "is", null)
    .order("target_at", { ascending: true });
  if (error) throw error;
  return data ?? [];
}

async function fetchStatsRows(daysBack = 30) {
  const since = new Date(Date.now() - daysBack * 86400 * 1000).toISOString();
  const { data, error } = await supabase
    .from("forecasts")
    .select("horizon_hours, predicted, lower_80, upper_80, prob_higher, actual, actual_higher, target_at")
    .gte("target_at", since)
    .not("actual", "is", null);
  if (error) throw error;
  return data ?? [];
}

// ---------- stats helpers ----------

function bandHitRate(rows) {
  if (!rows.length) return null;
  const hits = rows.filter(r =>
    Number(r.actual) >= Number(r.lower_80) &&
    Number(r.actual) <= Number(r.upper_80)
  ).length;
  return hits / rows.length;
}

function brierScore(rows) {
  if (!rows.length) return null;
  const total = rows.reduce((sum, r) => {
    const p = Number(r.prob_higher);
    const o = r.actual_higher ? 1 : 0;
    return sum + (p - o) ** 2;
  }, 0);
  return total / rows.length;
}

function maeByHorizon(rows, horizon) {
  const subset = rows.filter(r => r.horizon_hours === horizon);
  if (!subset.length) return null;
  const total = subset.reduce(
    (sum, r) => sum + Math.abs(Number(r.actual) - Number(r.predicted)),
    0
  );
  return total / subset.length;
}

function pct(x, digits = 1) {
  if (x === null || x === undefined || Number.isNaN(x)) return "—";
  return (x * 100).toFixed(digits) + "%";
}

function dollars(x, digits = 2) {
  if (x === null || x === undefined || Number.isNaN(x)) return "—";
  return "$" + Number(x).toFixed(digits);
}

function ago(iso) {
  if (!iso) return "—";
  const diff = Date.now() - new Date(iso).getTime();
  const mins = Math.floor(diff / 60000);
  if (mins < 1) return "just now";
  if (mins < 60) return `${mins}m ago`;
  const hrs = Math.floor(mins / 60);
  if (hrs < 24) return `${hrs}h ago`;
  return `${Math.floor(hrs / 24)}d ago`;
}

function nextRefreshCountdown() {
  // Worker cron: 5 past every UTC hour.
  const nextRun = new Date();
  nextRun.setUTCMinutes(5, 0, 0);
  if (nextRun <= new Date()) nextRun.setUTCHours(nextRun.getUTCHours() + 1);
  const diff = nextRun - new Date();
  const mins = Math.floor(diff / 60000);
  const secs = Math.floor((diff % 60000) / 1000);
  return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
}

// ---------- date/time formatters ----------

// Compact date+time label for X-axis ticks. Includes date so tomorrow is obvious.
function formatChartTick(ms) {
  const d = new Date(ms);
  const monthDay = d.toLocaleDateString([], { month: "short", day: "numeric" });
  const time = d.toLocaleTimeString([], { hour: "numeric" });
  return `${monthDay} · ${time}`;
}

// Rich date+time for tooltip labels.
function formatTooltipLabel(ms) {
  return new Date(ms).toLocaleString([], {
    weekday: "short",
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
  });
}

// ---------- chart data prep ----------

function buildChartRows(actuals, forecasts, runAt) {
  const past = actuals.map(r => ({
    t: new Date(r.target_at).getTime(),
    actual: Number(r.actual),
  }));

  const future = forecasts.map(r => ({
    t: new Date(r.target_at).getTime(),
    predicted: Number(r.predicted),
    // Recharts Area with array dataKey needs [lower, upper] tuple:
    band: [Number(r.lower_80), Number(r.upper_80)],
  }));

  // Anchor point at run_at with the spot price so the forecast + band
  // start from "now" rather than jumping from last actual to the 1h horizon.
  if (forecasts.length && runAt) {
    const spot = Number(forecasts[0].price_at_run);
    const anchorT = new Date(runAt).getTime();
    future.unshift({
      t: anchorT,
      predicted: spot,
      band: [spot, spot],
      // Also stitch the actual series into the anchor so the solid line
      // meets the dashed forecast line at "now" instead of leaving a gap.
      actual: spot,
    });
  }

  return [...past, ...future].sort((a, b) => a.t - b.t);
}

// ---------- components ----------

function Header({ regime, forecasts, countdown }) {
  const byHorizon = Object.fromEntries(forecasts.map(f => [f.horizon_hours, f]));
  const p1 = byHorizon[1]?.prob_higher;
  const p6 = byHorizon[6]?.prob_higher;
  const p24 = byHorizon[24]?.prob_higher;

  return (
    <div className="border-b border-neutral-800 pb-4 mb-6">
      <div className="flex flex-wrap items-baseline gap-x-6 gap-y-2">
        <div className="text-2xl font-semibold tracking-tight">
          SOL <span className="text-neutral-500 font-normal">·</span>{" "}
          <span className="text-emerald-400">{regime?.label_display ?? "—"}</span>
        </div>
        <div className="text-sm text-neutral-400 font-mono">
          Model: {pct(p1)} higher in 1h · {pct(p6)} in 6h · {pct(p24)} in 24h
        </div>
        <div className="ml-auto text-xs text-neutral-500 font-mono">
          Next refresh in {countdown}
        </div>
      </div>
      {regime?.notes && (
        <div className="text-xs text-neutral-500 mt-2">{regime.notes}</div>
      )}
    </div>
  );
}

function MainChart({ rows, runAt }) {
  if (!rows.length) {
    return (
      <div className="h-80 flex items-center justify-center text-neutral-500 text-sm">
        Waiting for data…
      </div>
    );
  }

  const runAtMs = runAt ? new Date(runAt).getTime() : null;

  return (
    <div className="h-96 w-full">
      <ResponsiveContainer width="100%" height="100%">
        <ComposedChart data={rows} margin={{ top: 24, right: 24, bottom: 8, left: 0 }}>
          <CartesianGrid stroke="#1f1f27" vertical={false} />
          <XAxis
            dataKey="t"
            type="number"
            domain={["dataMin", "dataMax"]}
            tickFormatter={formatChartTick}
            stroke="#4b5563"
            tick={{ fontSize: 11 }}
            scale="time"
            minTickGap={40}
          />
          <YAxis
            stroke="#4b5563"
            tick={{ fontSize: 11 }}
            domain={["auto", "auto"]}
            tickFormatter={v => `$${Number(v).toFixed(0)}`}
            width={64}
          />
          <Tooltip
            contentStyle={{
              background: "#0f0f14",
              border: "1px solid #27272a",
              borderRadius: 4,
              fontSize: 12,
            }}
            labelStyle={{ color: "#9ca3af" }}
            labelFormatter={formatTooltipLabel}
            formatter={(value, name) => {
              if (name === "80% band") {
                // value is [lower, upper]
                if (Array.isArray(value)) {
                  return [`$${value[0].toFixed(2)} — $${value[1].toFixed(2)}`, name];
                }
              }
              return [`$${Number(value).toFixed(2)}`, name];
            }}
          />
          {/* 80% confidence band as a single Area with [lower, upper] tuple */}
          <Area
            type="monotone"
            dataKey="band"
            stroke="none"
            fill="#10b981"
            fillOpacity={0.12}
            isAnimationActive={false}
            name="80% band"
            connectNulls
          />
          {/* Actual line (solid, past) */}
          <Line
            type="monotone"
            dataKey="actual"
            stroke="#e5e5e5"
            strokeWidth={2}
            dot={false}
            connectNulls
            isAnimationActive={false}
            name="Actual"
          />
          {/* Predicted line (dashed, forward-looking) */}
          <Line
            type="monotone"
            dataKey="predicted"
            stroke="#10b981"
            strokeWidth={2}
            strokeDasharray="4 3"
            dot={false}
            connectNulls
            isAnimationActive={false}
            name="Forecast"
          />
          {/* "Now" reference line — pill label, prominent stroke */}
          {runAtMs && (
            <ReferenceLine
              x={runAtMs}
              stroke="#a78bfa"
              strokeWidth={1.5}
              strokeDasharray="3 3"
            >
              <Label
                value="NOW"
                position="top"
                offset={8}
                fill="#e9d5ff"
                fontSize={10}
                fontWeight={700}
                style={{
                  paintOrder: "stroke",
                  stroke: "#1e1b4b",
                  strokeWidth: 4,
                  strokeLinejoin: "round",
                }}
              />
            </ReferenceLine>
          )}
        </ComposedChart>
      </ResponsiveContainer>
    </div>
  );
}

function StatCard({ title, value, hint }) {
  return (
    <div className="rounded-lg border border-neutral-800 bg-neutral-950/60 p-4">
      <div className="text-xs text-neutral-500 uppercase tracking-wider">{title}</div>
      <div className="text-2xl font-mono text-neutral-100 mt-1">{value}</div>
      {hint && <div className="text-xs text-neutral-500 mt-2 leading-snug">{hint}</div>}
    </div>
  );
}

function TrackRecord({ statsRows }) {
  const hit = bandHitRate(statsRows);
  const brier = brierScore(statsRows);
  const mae1 = maeByHorizon(statsRows, 1);
  const mae6 = maeByHorizon(statsRows, 6);
  const mae24 = maeByHorizon(statsRows, 24);

  return (
    <div className="mt-8">
      <div className="text-xs text-neutral-500 uppercase tracking-wider mb-3">
        Track record — last 30 days
      </div>
      <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
        <StatCard
          title="Band hit rate"
          value={pct(hit)}
          hint="Share of actuals landing inside the 80% band. Well-calibrated shows ~80%."
        />
        <StatCard
          title="Direction Brier"
          value={brier === null ? "—" : brier.toFixed(3)}
          hint="Lower is better. 0.25 = coin flip, 0.00 = perfect calls."
        />
        <StatCard
          title="MAE — 1h / 6h / 24h"
          value={`${dollars(mae1)} / ${dollars(mae6)} / ${dollars(mae24)}`}
          hint="Average absolute error in USD per horizon."
        />
      </div>
    </div>
  );
}

function Footer({ lastRunIso, workerHealthy }) {
  return (
    <footer className="mt-12 pt-6 border-t border-neutral-800 text-xs text-neutral-500">
      <div className="flex flex-wrap items-center gap-x-4 gap-y-2">
        <div className="flex items-center gap-2">
          <span
            className={`inline-block w-2 h-2 rounded-full ${
              workerHealthy ? "bg-emerald-500" : "bg-red-500"
            }`}
          />
          Worker {workerHealthy ? "healthy" : "stale"} · last run {ago(lastRunIso)}
        </div>
        <div className="ml-auto max-w-lg text-right">
          SiCierto forecasts are analytical output, not investment advice.
        </div>
      </div>
    </footer>
  );
}

function App() {
  const [runAt, setRunAt] = useState(null);
  const [forecasts, setForecasts] = useState([]);
  const [regime, setRegime] = useState(null);
  const [actuals, setActuals] = useState([]);
  const [statsRows, setStatsRows] = useState([]);
  const [error, setError] = useState(null);
  const [countdown, setCountdown] = useState("--:--");

  async function loadAll() {
    try {
      const latest = await fetchLatestRun();
      if (!latest) {
        setError("No forecast data yet — the worker hasn't produced its first run.");
        return;
      }
      const [f, r, a, s] = await Promise.all([
        fetchForecastsForRun(latest),
        fetchRegimeForRun(latest),
        fetchRecentActuals(48),
        fetchStatsRows(30),
      ]);
      setRunAt(latest);
      setForecasts(f);
      setRegime(r);
      setActuals(a);
      setStatsRows(s);
      setError(null);
    } catch (err) {
      console.error(err);
      setError(err.message || String(err));
    }
  }

  useEffect(() => {
    loadAll();
    const dataInterval = setInterval(loadAll, 60_000);
    return () => clearInterval(dataInterval);
  }, []);

  useEffect(() => {
    const tick = () => setCountdown(nextRefreshCountdown());
    tick();
    const t = setInterval(tick, 1000);
    return () => clearInterval(t);
  }, []);

  const chartRows = useMemo(
    () => buildChartRows(actuals, forecasts, runAt),
    [actuals, forecasts, runAt]
  );

  const workerHealthy = useMemo(() => {
    if (!runAt) return false;
    return Date.now() - new Date(runAt).getTime() < 90 * 60 * 1000;
  }, [runAt]);

  if (error) {
    return (
      <div className="max-w-4xl mx-auto p-6 text-red-400 font-mono text-sm">
        Error: {error}
      </div>
    );
  }

  return (
    <div className="max-w-5xl mx-auto p-6">
      <Header regime={regime} forecasts={forecasts} countdown={countdown} />
      <MainChart rows={chartRows} runAt={runAt} />
      <TrackRecord statsRows={statsRows} />
      <Footer lastRunIso={runAt} workerHealthy={workerHealthy} />
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
