// Probability console: one monitored event over the last four hours, the rule
// that fired, and the signals that compose the move.
//
// Deliberately static. An earlier version streamed sub-second WebSocket frames
// and re-drew the number several times a second, which read as restless noise
// rather than as evidence. Nothing here animates, and nothing is clickable.
//
// The chart is instrument, not decoration: a scale, the line, and the moment
// the alert fired, marked on the line and dropped to the time axis. A reader should be able to see why the alert fired without being told,
// which is the whole claim the section makes.

const BASELINE = 17.3;
const CURRENT = 83.1;

// The alert rule the chart draws: a move of at least THRESHOLD_PP arms it.
const THRESHOLD_PP = 15;
// The chart covers the last four hours of trading, 10:30 to 14:30 UTC, marked
// every half hour. At two minutes a sample, the spike takes about half an hour.
const WINDOW_START_MIN = 10 * 60 + 30;
const WINDOW_MIN = 240;
const TICK_MIN = 30;
const clock = (m) => `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;

// How each signal shared the move. Shares sum to 1; the residual left by
// rounding lands on the largest one, so the rows always add up to CURRENT
// minus BASELINE exactly.
const DRIVERS = [
  { key: 'market', share:  0.451 },
  { key: 'osint',  share:  0.289 },
  { key: 'agency', share:  0.183 },
  { key: 'expert', share:  0.134 },
  { key: 'social', share: -0.057 },
];


const SERIES_N = 120;
const SPIKE_AT = 72;
const SPIKE_RISE = 16;

function useConsoleT() {
  const hook = typeof useT === 'function' ? useT : null;
  const fromCtx = hook ? hook() : null;
  return (key) => {
    const value = fromCtx ? fromCtx(key) : null;
    return value && value !== key ? value : key;
  };
}

function contributions() {
  const move = Math.round((CURRENT - BASELINE) * 10) / 10;
  const rows = DRIVERS.map((d) => ({
    key: d.key,
    pp: Math.round(d.share * move * 10) / 10,
  }));
  const drift = Math.round((move - rows.reduce((sum, r) => sum + r.pp, 0)) * 10) / 10;
  if (drift) {
    const biggest = rows.reduce((a, b) => (Math.abs(b.pp) > Math.abs(a.pp) ? b : a));
    biggest.pp = Math.round((biggest.pp + drift) * 10) / 10;
  }
  return { rows, move };
}

// A market that breathes: small, uneven swings around the baseline, a steep
// rise that still wobbles on the way up, then a plateau that keeps moving and
// ends exactly on the current value. A sum of sines stands in for noise, so
// the chart is deterministic and never redraws itself.
const wobble = (i) =>
  Math.sin(i * 0.42) * 1.2 + Math.sin(i * 0.19 + 1.3) * 1.5 + Math.sin(i * 1.05 + 0.4) * 0.35 + Math.sin(i * 0.07 + 2.1) * 1.0;

function series() {
  const out = [];
  for (let i = 0; i < SERIES_N; i++) {
    if (i <= SPIKE_AT) {
      out.push(BASELINE + wobble(i) * 0.9);
    } else if (i <= SPIKE_AT + SPIKE_RISE) {
      const t = (i - SPIKE_AT) / SPIKE_RISE;
      out.push(BASELINE + (CURRENT - BASELINE) * (t * t * (3 - 2 * t)) + wobble(i) * 1.2 * Math.sin(Math.PI * t));
    } else {
      out.push(CURRENT + wobble(i) * 1.1);
    }
  }
  // Land the last few samples on CURRENT so the head dot reads the headline.
  const tail = 8;
  const miss = CURRENT - out[SERIES_N - 1];
  for (let k = 0; k < tail; k++) out[SERIES_N - tail + k] += miss * ((k + 1) / tail);
  return out;
}

// Catmull-Rom through the samples, as cubic Béziers, so the line reads as a
// curve rather than a string of straight segments.
function smoothPath(pts) {
  let d = `M ${pts[0][0].toFixed(2)} ${pts[0][1].toFixed(2)}`;
  for (let i = 0; i < pts.length - 1; i++) {
    const p0 = pts[i - 1] || pts[i], p1 = pts[i], p2 = pts[i + 1], p3 = pts[i + 2] || p2;
    const c1 = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6];
    const c2 = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6];
    d += ` C ${c1[0].toFixed(2)} ${c1[1].toFixed(2)} ${c2[0].toFixed(2)} ${c2[1].toFixed(2)} ${p2[0].toFixed(2)} ${p2[1].toFixed(2)}`;
  }
  return d;
}

function SpikeChart({ t }) {
  const points = series();
  const n = points.length - 1;
  // Positions are percentages of the plot box. The SVG stretches to the box
  // (strokes stay hairline through non-scaling-stroke) and every label, dot and
  // flag is HTML on top of it, so type matches the explainability column at any
  // width instead of growing and shrinking with the viewBox.
  const fx = (i) => (i / n) * 100;
  const fy = (v) => (1 - v / 100) * 100;

  // The alert fires on the first sample that clears the rule, which is where the
  // line changes colour: calm before, amber from the alert onwards.
  const fired = points.findIndex((v, i) => i > SPIKE_AT && v - BASELINE >= THRESHOLD_PP);
  const trace = (from, to) => smoothPath(points.slice(from, to + 1).map((v, k) => [fx(from + k), fy(v)]));
  const calm = trace(0, fired);
  const alert = trace(fired, n);

  const at = (i, v) => ({ left: `${fx(i)}%`, top: `${fy(v)}%` });

  return (
    <figure className="pc-chart" role="img" aria-label={t('platChartAlt')}>
      <div className="pc-plot">
        <svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
          {[0, 50, 100].map((v) => (
            <line key={v} x1="0" y1={fy(v)} x2="100" y2={fy(v)} className="pc-grid-line" vectorEffect="non-scaling-stroke" />
          ))}
          <line x1={fx(fired)} y1={fy(points[fired])} x2={fx(fired)} y2="100" className="pc-alert-line" vectorEffect="non-scaling-stroke" />
          <path d={calm} className="pc-line-calm" vectorEffect="non-scaling-stroke" />
          <path d={alert} className="pc-line-alert" vectorEffect="non-scaling-stroke" />
        </svg>

        {[0, 50, 100].map((v) => (
          <span key={v} className="pc-axis" style={{ top: `${fy(v)}%` }}>{v}</span>
        ))}

        <span className="pc-mark pc-mark-fire" style={at(fired, points[fired])} />
        <span className="pc-fire-label" style={at(fired, points[fired])}>{t('platAlert')}</span>
        <span className="pc-mark pc-mark-head" style={at(n, points[n])} />
      </div>
      <div className="pc-xaxis" aria-hidden="true">
        {Array.from({ length: WINDOW_MIN / TICK_MIN + 1 }, (_, k) => k * TICK_MIN).map((m) => (
          <i key={`t${m}`} className="pc-tick" style={{ left: `${(m / WINDOW_MIN) * 100}%` }} />
        ))}
        {/* Full hours are labelled, the last half hour ends on "now". */}
        {[30, 90, 150].map((m) => (
          <span key={m} style={{ left: `${(m / WINDOW_MIN) * 100}%` }}>{clock(WINDOW_START_MIN + m)}</span>
        ))}
        <span className="pc-now" style={{ left: '100%' }}>{t('platNow')}</span>
        <em className="pc-xaxis-unit">UTC</em>
      </div>
    </figure>
  );
}

// One bar, one segment per group: the move is not four separate facts, it is
// one number these groups add up to, in the proportion each of them carried.
function Composition({ rows, move, t }) {
  const positive = rows.filter((r) => r.pp > 0);
  const negative = rows.filter((r) => r.pp < 0);
  const total = positive.reduce((sum, r) => sum + r.pp, 0)
    + negative.reduce((sum, r) => sum + Math.abs(r.pp), 0);

  return (
    <div className="pc-comp">
      <div className="pc-comp-head">
        <span className="pc-comp-label">{t('platWhyMeta')}</span>
        <span className="pc-comp-total">+{move.toFixed(1)} pp</span>
      </div>
      <div className="pc-comp-bar" aria-hidden="true">
        {positive.map((r, i) => (
          <span key={r.key} className={`pc-seg pc-seg-${i}`} style={{ width: `${(r.pp / total) * 100}%` }} />
        ))}
        {negative.map((r) => (
          <span key={r.key} className="pc-seg pc-seg-neg" style={{ width: `${(Math.abs(r.pp) / total) * 100}%` }} />
        ))}
      </div>
    </div>
  );
}

function ProbabilityConsole() {
  const t = useConsoleT();
  const { rows, move } = contributions();
  // One shared scale across the rows, so bar lengths compare to each other.
  const widest = rows.reduce((n, r) => Math.max(n, Math.abs(r.pp)), 0) || 1;

  return (
    <div className="pc">
      <div className="pc-grid">
        <section className="pc-cell pc-event">
          <div className="pc-cell-head">
            <h4>{t('platEvent')}</h4>
          </div>

          <div className="pc-headline">
            <p className="pc-question">{t('platQuestion')}</p>
            <div className="pc-prob">
              <span className="pc-prob-k">{t('platProbLabel')}</span>
              <span className="pc-prob-v">{CURRENT.toFixed(1)}<span>%</span></span>
            </div>
          </div>

          <SpikeChart t={t} />
          <Composition rows={rows} move={move} t={t} />
        </section>

        <section className="pc-cell pc-why">
          <div className="pc-cell-head">
            <h4>{t('platWhy')}</h4>
          </div>

          {/* Contribution first, because that is what the reader came for, then who
              produced it, then why, then the bar that puts the size of the move
              next to the other four. All five bars share one scale, so their
              lengths compare to each other instead of each filling its own track. */}
          <ul className="pc-drivers">
            {rows.map((r, i) => (
              <li key={r.key} className={r.pp >= 0 ? 'pos' : 'neg'}>
                <span className="pc-driver-pp">{r.pp >= 0 ? '+' : '−'}{Math.abs(r.pp).toFixed(1)} pp</span>
                <span className="pc-driver-src">{t(`platDrv_${r.key}`)}</span>
                <p className="pc-driver-txt">{t(`platDrv_${r.key}_t`)}</p>
                <span className="pc-driver-bar" aria-hidden="true">
                  <span
                    className={r.pp >= 0 ? `pc-seg-${i}` : 'pc-seg-neg'}
                    style={{ width: `${(Math.abs(r.pp) / widest) * 100}%` }}
                  />
                </span>
              </li>
            ))}
          </ul>
        </section>
      </div>
    </div>
  );
}

Object.assign(window, { ProbabilityConsole });
