/* Overview "Next 24 hours" (W_NextDay): the expected size of the next BTC move and the US-demand lean, with the
 * graded track record of every past prediction. Data: /api/market-signals (src/signals_store.js);
 * the models are documented in src/market_signals.js. */

const { useState: useStateSG, useEffect: useEffectSG, useRef: useRefSG } = React;

const SG_REFRESH_MS = 10 * 60 * 1000;
const SG_REGIME = {
  quiet: { label: 'Quiet', chip: 'neutral', text: 'smaller than usual' },
  normal: { label: 'Normal', chip: 'info', text: 'about usual' },
  elevated: { label: 'Elevated', chip: 'gold', text: 'larger than usual' },
  high: { label: 'High', chip: 'bear', text: 'much larger than usual' },
};
const SG_LEAN = {
  up: { label: 'Strong US demand', chip: 'bull' },
  down: { label: 'Weak US demand', chip: 'bear' },
  none: { label: 'Normal US demand', chip: 'neutral' },
};

const sgUSD = (n) => '$' + Math.round(n).toLocaleString('en-US');
const sgPct = (v, d = 2) => (v == null ? '–' : v.toFixed(d) + '%');
const sgSigned = (v, d = 2) => (v == null ? '–' : (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v).toFixed(d));
// Graded returns are stored as log returns (100 * ln(end/start)); show them as simple % moves.
const sgSimple = (logPct) => window.OverviewFormat.simpleReturnPct(logPct);

function useSGWidth(def = 640) {
  const ref = useRefSG(null);
  const [w, setW] = useStateSG(def);
  useEffectSG(() => {
    const el = ref.current;
    if (!el) return undefined;
    const set = () => setW(Math.max(260, Math.floor(el.getBoundingClientRect().width)));
    set();
    if (typeof ResizeObserver === 'undefined') return undefined;
    const ro = new ResizeObserver(set);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);
  return [ref, w];
}

const SG_Label = ({ children }) => (
  <div style={{ fontSize: 10.5, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-3)', marginBottom: 6 }}>{children}</div>
);

// === Forecast vs actual chart: bar = actual |24h move|, line = forecast ±1σ, dashed = ±2σ ===
function SG_MoveChart({ rows }) {
  const [ref, W] = useSGWidth();
  const [hover, setHover] = useStateSG(null);
  const n = rows.length;
  if (n < 2) return <div ref={ref} style={{ padding: '24px 0', color: 'var(--text-3)', fontSize: 12.5, textAlign: 'center' }}>The first predictions are graded 24 hours after they are made.</div>;
  const padL = 6, padR = 40, H = 150, axisH = 18, plotH = H - axisH;
  const plotW = W - padL - padR;
  const x = (i) => padL + (i / (n - 1)) * plotW;
  const top = Math.max(...rows.map((r) => Math.max(r.absPct, 2 * r.sigmaPct))) * 1.05;
  const y = (v) => plotH - (v / top) * (plotH - 8);
  const bw = Math.max(1, plotW / n - (n > 200 ? 0 : 1));
  const line = (k) => rows.map((r, i) => (i ? 'L' : 'M') + x(i).toFixed(1) + ' ' + y(k * r.sigmaPct).toFixed(1)).join(' ');
  const ticks = [0, 0.5, 1].map((f) => Math.round(f * (n - 1)));
  const onMove = (e) => {
    const b = e.currentTarget.getBoundingClientRect();
    setHover(Math.max(0, Math.min(n - 1, Math.round(((e.clientX - b.left - padL) / plotW) * (n - 1)))));
  };
  const hv = hover != null ? rows[hover] : null;
  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <svg width={W} height={H} style={{ display: 'block' }} onMouseMove={onMove} onMouseLeave={() => setHover(null)} role="img" aria-label="Forecast versus actual 24 hour moves">
        {[0, top / 2].map((v) => (
          <g key={v}>
            <line x1={padL} x2={W - padR} y1={y(v)} y2={y(v)} stroke="var(--border-soft)" strokeWidth="0.8" />
            <text x={W - padR + 6} y={y(v) + 3} fontSize="10" fontFamily="var(--font-mono)" fill="var(--text-4)">{v.toFixed(1)}%</text>
          </g>
        ))}
        {rows.map((r, i) => (
          <rect key={i} x={x(i) - bw / 2} y={y(r.absPct)} width={bw} height={Math.max(0.5, plotH - y(r.absPct))}
                fill={r.within1 ? 'var(--bull)' : r.absPct <= 2 * r.sigmaPct ? 'var(--gold)' : 'var(--bear)'} opacity={r.live ? 0.85 : 0.4} />
        ))}
        <path d={line(1)} fill="none" stroke="var(--text)" strokeWidth="1.4" strokeLinejoin="round" />
        <path d={line(2)} fill="none" stroke="var(--text-3)" strokeWidth="1" strokeDasharray="4 3" />
        {ticks.map((i, k) => (
          <text key={k} x={Math.min(Math.max(x(i), 24), W - padR - 24)} y={H - 4} textAnchor="middle" fontSize="9.5" fill="var(--text-4)">
            {new Date(rows[i].madeAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
          </text>
        ))}
        {hover != null && <line x1={x(hover)} x2={x(hover)} y1="2" y2={plotH} stroke="var(--text-3)" strokeWidth="0.8" strokeDasharray="3 3" pointerEvents="none" />}
      </svg>
      {hv && (
        <div style={{ position: 'absolute', top: 4, left: Math.min(Math.max(x(hover) - 80, 4), W - 168), width: 164, pointerEvents: 'none', background: 'var(--bg-elev)',
                      border: '1px solid var(--border)', borderRadius: 7, padding: '6px 9px', fontSize: 11, lineHeight: 1.55, boxShadow: '0 4px 14px rgba(0,0,0,0.35)' }}>
          <div style={{ color: 'var(--text-3)' }}>{new Date(hv.madeAt).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}{hv.live ? '' : ' · backfilled'}</div>
          <div>Forecast <b className="mono">±{hv.sigmaPct.toFixed(2)}%</b></div>
          <div>Actual <b className="mono" style={{ color: hv.returnPct >= 0 ? 'var(--bull)' : 'var(--bear)' }}>{sgSigned(sgSimple(hv.returnPct))}%</b> {hv.within1 ? 'inside' : 'outside'}</div>
        </div>
      )}
    </div>
  );
}

// === One row of the scorecard tables ======================================
function SG_Row({ label, live, liveSub, backfill, target, hint }) {
  const cell = { padding: '6px 8px', textAlign: 'right', fontFamily: 'var(--font-mono)', whiteSpace: 'nowrap' };
  return (
    <tr style={{ borderTop: '1px solid var(--border-soft)' }}>
      <td style={{ padding: '6px 8px 6px 0', color: 'var(--text-2)' }} title={hint}>{label}</td>
      <td style={cell}>{live}{liveSub ? <div style={{ fontSize: 9.5, color: 'var(--text-4)', fontFamily: 'var(--font-sans)', marginTop: 1 }}>{liveSub}</div> : null}</td>
      <td style={{ ...cell, color: 'var(--text-3)' }}>{backfill}</td>
      <td style={{ ...cell, color: 'var(--text-4)' }}>{target}</td>
    </tr>
  );
}

function SG_Table({ children }) {
  const th = { padding: '4px 8px', textAlign: 'right', fontWeight: 500, fontSize: 10, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--text-4)' };
  return (
    <div style={{ overflowX: 'auto' }}>
      <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
        <thead><tr><th style={{ ...th, textAlign: 'left', paddingLeft: 0 }} /><th style={th}>Live</th><th style={th}>Backfilled</th><th style={th}>Backtest</th></tr></thead>
        <tbody>{children}</tbody>
      </table>
    </div>
  );
}

// === The section ===========================================================
function W_NextDay() {
  const [state, setState] = useStateSG({ data: null, error: null, loading: true });
  useEffectSG(() => {
    let cancelled = false, timer;
    const load = () => {
      fetch('/api/market-signals')
        .then((r) => r.json())
        .then((j) => { if (!cancelled) setState({ data: j.ok ? j.data : null, error: j.ok ? null : (j.error || 'unavailable'), loading: false }); })
        .catch(() => { if (!cancelled) setState((s) => ({ ...s, error: 'unavailable', loading: false })); })
        .finally(() => { if (!cancelled) timer = setTimeout(load, SG_REFRESH_MS); });
    };
    load();
    return () => { cancelled = true; clearTimeout(timer); };
  }, []);

  return <SG_View data={state.data} loading={state.loading} error={state.error} />;
}

/** The card itself, from the /api/market-signals payload (no fetching, so it can be rendered in tests). */
function SG_View({ data: d, loading, error }) {
  const mv = d && d.move, dm = d && d.demand;
  const sc = d && d.scorecard;
  const mBt = d && d.models.move.backtest, dBt = d && d.models.demand.backtest;
  const nOf = (k) => (k ? ` (${k.toLocaleString()})` : '');
  // Mean of the simple 24h returns (sent by the server); older payloads only carry the log mean.
  const avgSimple = (dmd) => (dmd.allAvgSimpleReturnPct != null ? dmd.allAvgSimpleReturnPct : sgSimple(dmd.allAvgReturnPct));
  const edge = (s, lean) => (s.demand[lean].n && s.demand.allAvgReturnPct != null ? sgSigned(s.demand[lean].avgReturnPct - s.demand.allAvgReturnPct, 2) : '–');
  // The same edge with its 95% interval (overlap-aware, from the server) and how many independent days stand behind it.
  const edgeSub = (s, lean) => {
    const L = s.demand[lean], OF = window.OverviewFormat;
    if (!L.n) return '';
    const ci = L.avgReturnCi95 && s.demand.allAvgReturnPct != null ? L.avgReturnCi95.map((v) => v - s.demand.allAvgReturnPct) : null;
    return OF.sampleText(L.n, L.nIndependent) + (ci ? ' · 95% ' + OF.interval(ci, 2) : '');
  };
  const sizeRatio = (set) => (set.move.avgSizeRatio == null ? '–' : Math.round(set.move.avgSizeRatio * 100) + '%');
  const sizeSub = (set) => {
    const OF = window.OverviewFormat;
    return set.move.n ? OF.sampleText(set.move.n, set.move.nIndependent) + (set.move.avgSizeRatioCi95 ? ' · 95% ' + OF.range(set.move.avgSizeRatioCi95.map((v) => v * 100), 0, '%') : '') : '';
  };

  return (
    <div className="card" style={{ gridColumn: 'span 12' }}>
      <div className="card-h" style={{ flexWrap: 'wrap', gap: 10 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
          <h3 style={{ margin: 0 }}>Next 24 hours · BTC</h3>
          <span className="chip neutral" title="Every forecast is stored when it is made and graded 24 hours later">tracked &amp; graded</span>
          {mv && mv.stale && <span className="chip bear"><span className="chip-dot" /> data is stale</span>}
        </div>
        {mv && <span style={{ fontSize: 10.5, color: 'var(--text-4)' }}>made {new Date(mv.madeAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} · updates hourly</span>}
      </div>

      <div className="card-b">
        {!mv && (
          <div style={{ padding: '28px 0', textAlign: 'center', color: 'var(--text-3)', fontSize: 12.5 }}>
            {loading ? 'Loading…' : error ? 'Signals are unavailable right now. They will retry shortly.' : 'Collecting data: the first forecasts appear shortly after the server starts.'}
          </div>
        )}

        {mv && (
          <>
            <div className="rg-split" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24 }}>
              {/* expected move */}
              <div>
                <SG_Label>Expected move · size, not direction</SG_Label>
                <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
                  <span className="mono" style={{ fontSize: 44, lineHeight: 1, fontWeight: 600, letterSpacing: '-0.02em' }}>±{mv.prediction.sigmaPct.toFixed(2)}%</span>
                  <span className={'chip ' + SG_REGIME[mv.prediction.regime].chip}><span className="chip-dot" /> {SG_REGIME[mv.prediction.regime].label}</span>
                </div>
                <div style={{ fontSize: 12.5, color: 'var(--text-2)', marginTop: 10, lineHeight: 1.6 }}>
                  Likely range <b className="mono">{sgUSD(mv.refPrice * Math.exp(-mv.prediction.sigmaPct / 100))} – {sgUSD(mv.refPrice * Math.exp(mv.prediction.sigmaPct / 100))}</b>
                  <span style={{ color: 'var(--text-3)' }}> (about 3 days in 4 end inside)</span><br />
                  Wide range <span className="mono">±{(2 * mv.prediction.sigmaPct).toFixed(2)}%</span>
                  <span style={{ color: 'var(--text-3)' }}> · {sgUSD(mv.refPrice * Math.exp(-2 * mv.prediction.sigmaPct / 100))} – {sgUSD(mv.refPrice * Math.exp(2 * mv.prediction.sigmaPct / 100))} (19 in 20)</span>
                </div>
                <div style={{ fontSize: 11.5, color: 'var(--text-3)', marginTop: 8 }}>
                  A {SG_REGIME[mv.prediction.regime].text} move for BTC{mv.prediction.weekend ? ', and a weekend, which is usually calmer' : ''}.
                  Recent volatility: last day <span className="mono">{mv.prediction.v1dPct.toFixed(1)}%</span>, 7 days <span className="mono">{mv.prediction.v7dPct.toFixed(1)}%</span>, 30 days <span className="mono">{mv.prediction.v30dPct.toFixed(1)}%</span> a day.
                </div>
              </div>

              {/* US demand */}
              <div>
                <SG_Label>US demand · Coinbase premium</SG_Label>
                {dm ? (
                  <>
                    <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
                      <span className="mono" style={{ fontSize: 44, lineHeight: 1, fontWeight: 600, letterSpacing: '-0.02em', color: dm.prediction.avg6hBp >= 0 ? 'var(--bull)' : 'var(--bear)' }}>{sgSigned(dm.prediction.avg6hBp, 1)}</span>
                      <span style={{ fontSize: 12, color: 'var(--text-3)' }}>bp, 6h avg</span>
                      <span className={'chip ' + SG_LEAN[dm.prediction.lean].chip}><span className="chip-dot" /> {SG_LEAN[dm.prediction.lean].label}</span>
                    </div>
                    <div style={{ fontSize: 12.5, color: 'var(--text-2)', marginTop: 10, lineHeight: 1.6 }}>
                      Coinbase is {dm.prediction.avg6hBp >= 0 ? 'above' : 'below'} Binance and higher than <b className="mono">{dm.prediction.rank.toFixed(0)}%</b> of the last 30 days.
                    </div>
                    <div style={{ fontSize: 11.5, color: 'var(--text-3)', marginTop: 8, lineHeight: 1.55 }}>
                      A weak tilt, not a call: after a strong reading BTC did slightly better than average over the next 24h in the 2 fit years and only marginally in the test year. Top 20% of the month = strong, bottom 20% = weak.
                    </div>
                  </>
                ) : <div style={{ fontSize: 12.5, color: 'var(--text-3)' }}>Needs 15 days of Coinbase history before the first reading.</div>}
              </div>
            </div>

            {/* track record */}
            <div style={{ marginTop: 22 }}>
              <SG_Label>Track record · forecast vs what happened (last 30 days)</SG_Label>
              <SG_MoveChart rows={d.history.move} />
              <div style={{ display: 'flex', gap: 14, flexWrap: 'wrap', fontSize: 10.5, color: 'var(--text-3)', marginTop: 4 }}>
                <span><span style={{ color: 'var(--text)' }}>━</span> forecast ±1σ</span>
                <span><span style={{ color: 'var(--text-3)' }}>┅</span> ±2σ</span>
                <span><span style={{ color: 'var(--bull)' }}>■</span> actual inside ±1σ</span>
                <span><span style={{ color: 'var(--gold)' }}>■</span> inside ±2σ</span>
                <span><span style={{ color: 'var(--bear)' }}>■</span> outside</span>
                <span>faded = backfilled</span>
              </div>
            </div>

            <div className="rg-split" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, marginTop: 16 }}>
              <div>
                <SG_Label>Expected move · graded</SG_Label>
                <SG_Table>
                  <SG_Row label="Ended inside ±1σ" live={sgPct(sc.live.move.within1Pct, 0) + nOf(sc.live.move.n)} backfill={sgPct(sc.backfill.move.within1Pct, 0) + nOf(sc.backfill.move.n)} target={mBt.within1Pct + '%'} />
                  <SG_Row label="Ended inside ±2σ" live={sgPct(sc.live.move.within2Pct, 0)} backfill={sgPct(sc.backfill.move.within2Pct, 0)} target={mBt.within2Pct + '%'} />
                  <SG_Row label="Realised move ÷ forecast size" hint="Average of |actual 24h move| ÷ forecast size. Below 100% means the forecast range was wider than what happened (conservative); above 100% means it was too narrow. Moves are about 80% of the forecast size on average if the forecast were exactly right."
                          live={sizeRatio(sc.live)} liveSub={sizeSub(sc.live)} backfill={sizeRatio(sc.backfill)} target="" />
                  <SG_Row label="Forecast ↔ actual size" hint="Correlation between the forecast and the actual size of the move: 0 = useless, higher = better"
                          live={sc.live.move.corr == null ? '–' : sc.live.move.corr.toFixed(2)} backfill={sc.backfill.move.corr == null ? '–' : sc.backfill.move.corr.toFixed(2)} target={mBt.corr.toFixed(2)} />
                </SG_Table>
              </div>
              <div>
                <SG_Label>US demand · graded (24h return vs all hours, pts)</SG_Label>
                <SG_Table>
                  <SG_Row label="After strong demand" live={edge(sc.live, 'up')} liveSub={edgeSub(sc.live, 'up')} backfill={edge(sc.backfill, 'up') + nOf(sc.backfill.demand.up.n)} target={sgSigned(dBt.test.upEdge)} />
                  <SG_Row label="After weak demand" hint="Negative is right for a weak reading" live={edge(sc.live, 'down')} liveSub={edgeSub(sc.live, 'down')} backfill={edge(sc.backfill, 'down') + nOf(sc.backfill.demand.down.n)} target={sgSigned(dBt.test.downEdge)} />
                  <SG_Row label="All hours, avg 24h return" live={sc.live.demand.n ? sgSigned(avgSimple(sc.live.demand)) + '%' : '–'} backfill={sc.backfill.demand.n ? sgSigned(avgSimple(sc.backfill.demand)) + '%' : '–'} target="" />
                </SG_Table>
              </div>
            </div>

            <div style={{ marginTop: 12, fontSize: 10.5, color: 'var(--text-4)', lineHeight: 1.5 }}>
              Live = made in real time; backfilled = recomputed from the 30 days before launch (still out of sample, but not a live record); backtest = the year after the model was fitted. Forecasts are made hourly, so consecutive 24h windows overlap and the counts are not independent. Not financial advice.
            </div>
          </>
        )}
      </div>
    </div>
  );
}

Object.assign(window, { W_NextDay, SG_View });
