/* Animated delivery spine. One dot per programme on a true year axis, an
   accumulation band that only ever grows, and a 30-second playhead that names
   the flagship it is crossing. Static and complete under reduced motion. */
const SP = window.SX_DATA;
const SP_FROM = 1999, SP_TO = 2026;
const LAYERS_OF = {
  "ERP & billing": ["core", "data", "apps"],
  "Data platforms": ["data", "apps"],
  "Cloud & DevOps": ["apps"],
  "Industrial IoT & edge": ["edge", "data", "apps"],
  "Energy-tech": ["edge", "data", "core"],
  "Agri-tech": ["edge", "data", "apps"],
  "Applied ML": ["ml", "data"],
  "Computer vision": ["ml", "edge", "apps"],
  "Document intelligence": ["agents", "data", "apps"],
  "AI agents": ["agents", "data", "apps"],
};
const SP_TIER_R = { flagship: 11, deployment: 6.5, delivery: 4.2 };
const SP_TIER_W = { flagship: 3, deployment: 2, delivery: 1 };
const NODE_CAP = 4;
const spYear = (p) => +String(p.year || "").slice(0, 4) || null;

function buildSpine(rows, W) {
  const left = 132, right = W - 28;
  const years = [];
  for (let y = SP_FROM; y <= SP_TO; y++) years.push(y);
  const step = (right - left) / years.length;
  const x = (y) => left + (y - SP_FROM) * step + step / 2;
  const nodeFloor = 196;
  const nodes = [], chips = [];
  years.forEach((y) => {
    const inYear = rows.filter((p) => spYear(p) === y).sort((a, b) => SP_TIER_R[b.tier] - SP_TIER_R[a.tier]);
    let cursor = nodeFloor;
    inYear.slice(0, NODE_CAP).forEach((p) => {
      const r = SP_TIER_R[p.tier];
      cursor -= r + 3;
      nodes.push({ p, year: y, x: x(y), y: cursor, r });
      cursor -= r + 4;
    });
    if (inYear.length > NODE_CAP) chips.push({ year: y, x: x(y), y: cursor - 6, count: inYear.length - NODE_CAP, all: inYear });
  });
  const axisY = 226, bandTop = 262, rowH = 16, gap = 2;
  const rowY = {};
  SP.layers.forEach((l, i) => { rowY[l.id] = bandTop + (SP.layers.length - 1 - i) * (rowH + gap); });
  const cells = [], ghosts = [];
  const reached = {};
  years.forEach((y) => {
    const active = new Set();
    rows.forEach((p) => {
      const py = spYear(p); if (!py) return;
      if (py <= y && y <= py + 2) (LAYERS_OF[p.practice] || []).forEach((t) => active.add(t));
      if (py <= y) (LAYERS_OF[p.practice] || []).forEach((t) => { if (reached[t] === undefined) reached[t] = y; });
    });
    SP.layers.forEach((l) => {
      const cx = x(y) - step / 2 + 1, w = step - 2;
      if (active.has(l.id)) cells.push({ key: y + l.id, x: cx, y: rowY[l.id], w, h: rowH, year: y });
      else if (reached[l.id] !== undefined && reached[l.id] <= y) ghosts.push({ key: y + l.id, x: cx, y: rowY[l.id], w, h: rowH, year: y });
    });
  });
  const wTop = 34, wBottom = 74, wMax = 5;
  let wPath = "";
  years.forEach((y, i) => {
    const v = rows.filter((p) => spYear(p) === y).reduce((s, p) => s + SP_TIER_W[p.tier], 0);
    const yy = Math.round((wBottom - (Math.min(v, wMax) / wMax) * (wBottom - wTop)) * 10) / 10;
    wPath += (i === 0 ? `M${Math.round(x(y) - step / 2)},${yy}` : `L${Math.round(x(y) - step / 2)},${yy}`) + `L${Math.round(x(y) + step / 2)},${yy}`;
  });
  const wArea = `M${left},${wBottom}` + wPath.replace(/^M/, "L") + `L${Math.round(right)},${wBottom}z`;
  return { years, step, x, left, right, nodes, chips, cells, ghosts, rowY, axisY, bandTop, wPath, wArea, wBottom, wTop,
    bandBottom: bandTop + SP.layers.length * (rowH + gap) - gap };
}

/** Phone layout: the same record as year rows, newest first, with era headers
    and a per-year band strip. The playhead highlights the year it is on. */
function VerticalSpine({ rows, pyInt }) {
  const years = [];
  for (let y = SP_TO; y >= SP_FROM; y--) years.push(y);
  return (
    <div style={{ marginTop: 18 }}>
      {years.map((y) => {
        const era = SP.eras.find((e) => { const [a, b] = e.label.split("–").map(Number); return y >= a && y <= b; });
        const [, eraEnd] = era ? era.label.split("–").map(Number) : [];
        const inYear = rows.filter((p) => spYear(p) === y);
        const active = new Set(), reach = new Set();
        rows.forEach((p) => {
          const py2 = spYear(p);
          if (py2 <= y && y <= py2 + 2) (LAYERS_OF[p.practice] || []).forEach((t) => active.add(t));
          if (py2 <= y) (LAYERS_OF[p.practice] || []).forEach((t) => reach.add(t));
        });
        return (
          <div key={y}>
            {y === SP_TO || y === eraEnd ? (
              <div style={{ position: "sticky", top: 0, zIndex: 2, background: "var(--bg-page)", padding: "12px 0 8px", borderBottom: "1px solid var(--border-default)", marginTop: 16 }}>
                <p className="sx-caption" style={{ color: "var(--sx-accent)" }}>{era.title}</p>
                <p className="sx-caption" style={{ marginTop: 4, color: "var(--text-quiet)", textTransform: "none" }}>{era.label}</p>
              </div>
            ) : null}
            <div style={{ display: "grid", gridTemplateColumns: "16px 38px 1fr", gap: 10, alignItems: "start", padding: "8px 0" }}>
              <span style={{ display: "flex", flexDirection: "column", gap: 1, paddingTop: 4 }}>
                {SP.layers.slice().reverse().map((l) => (
                  <span key={l.id} style={{ height: 4, background: active.has(l.id) ? "color-mix(in oklab, var(--sx-accent) 52%, var(--surface-card))" : "transparent", borderTop: !active.has(l.id) && reach.has(l.id) ? "1px solid var(--border-default)" : "none" }} />
                ))}
              </span>
              <span className="sx-caption sx-num" style={{ color: y === pyInt ? "var(--sx-accent)" : "var(--text-quiet)" }}>{y}</span>
              <div style={{ borderLeft: "1px solid var(--border-default)", paddingLeft: 12, display: "grid", gap: 8, minHeight: 14 }}>
                {inYear.map((p) => (
                  <span key={p.slug} style={{ display: "flex", gap: 8, alignItems: "flex-start" }}>
                    <svg width={p.tier === "flagship" ? 13 : 9} height={13} aria-hidden style={{ flex: "none", marginTop: 3 }}>
                      <circle cx={p.tier === "flagship" ? 6.5 : 4.5} cy="6" r={p.tier === "flagship" ? 6 : p.tier === "deployment" ? 4 : 3} fill="var(--sx-accent)" />
                    </svg>
                    <span>
                      <span className="sx-small" style={{ display: "block", color: "var(--text-strong)" }}>{p.short}</span>
                      <span className="sx-caption" style={{ display: "block", marginTop: 2, color: "var(--text-quiet)", textTransform: "none" }}>{p.title}</span>
                    </span>
                  </span>
                ))}
              </div>
            </div>
          </div>
        );
      })}
    </div>
  );
}

function AnimatedSpine({ mobile }) {
  const rows = React.useMemo(() => SP.projects.filter(spYear), []);
  const wrapRef = React.useRef(null);
  const [W, setW] = React.useState(mobile ? 700 : 1180);
  const [playing, setPlaying] = React.useState(false);
  const [year, setYear] = React.useState(SP_TO);
  const [hover, setHover] = React.useState(null);
  const [chip, setChip] = React.useState(null);
  const raf = React.useRef(0), t0 = React.useRef(0);

  React.useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    const ro = new ResizeObserver(() => setW(Math.max(680, el.clientWidth)));
    ro.observe(el); setW(Math.max(680, el.clientWidth));
    return () => ro.disconnect();
  }, []);

  React.useEffect(() => {
    if (!playing) { cancelAnimationFrame(raf.current); return; }
    t0.current = 0;
    const tick = (ts) => {
      if (!t0.current) t0.current = ts;
      const f = ((ts - t0.current) / 30000) % 1;
      setYear(SP_FROM + f * (SP_TO - SP_FROM));
      raf.current = requestAnimationFrame(tick);
    };
    raf.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf.current);
  }, [playing]);

  const g = buildSpine(rows, W);
  const H = g.bandBottom + 56;
  const py = year, pyInt = Math.round(year);
  const era = SP.eras.find((e) => { const [a, b] = e.label.split("–").map(Number); return pyInt >= a && pyInt <= b; }) || SP.eras[SP.eras.length - 1];
  const landed = rows.filter((p) => spYear(p) === pyInt);
  const activeLayers = new Set(), reachedLayers = new Set();
  rows.forEach((p) => {
    const y = spYear(p);
    if (y <= pyInt && pyInt <= y + 2) (LAYERS_OF[p.practice] || []).forEach((t) => activeLayers.add(t));
    if (y <= pyInt) (LAYERS_OF[p.practice] || []).forEach((t) => reachedLayers.add(t));
  });
  const flagLabels = playing
    ? g.nodes.filter((n) => n.p.tier === "flagship" && py >= n.year && py < n.year + 2.2)
    : hover ? [] : g.nodes.filter((n) => n.p.slug === "lesco");

  const setFromX = (clientX, el) => {
    const box = el.ownerSVGElement.getBoundingClientRect();
    const sx = (clientX - box.left) * (W / box.width);
    const f = (sx - g.left) / (g.right - g.left);
    setPlaying(false);
    setYear(Math.round(SP_FROM + Math.min(1, Math.max(0, f)) * (SP_TO - SP_FROM)));
  };

  return (
    <div ref={wrapRef}>
      <div style={{ display: "flex", alignItems: "center", gap: 16, flexWrap: "wrap", paddingBottom: 16, borderBottom: "1px solid var(--border-default)" }}>
        <button type="button" onClick={() => setPlaying(!playing)} aria-label={playing ? "Pause the timeline" : "Play the timeline"}
          style={{ display: "inline-flex", alignItems: "center", gap: 9, padding: "6px 14px 6px 6px", border: "1px solid var(--border-default)", borderRadius: 999, background: "var(--surface-card)", cursor: "pointer", fontFamily: "var(--font-body)", fontSize: 13, fontWeight: 500, color: "var(--text-strong)" }}>
          <svg width="22" height="22" viewBox="0 0 22 22" aria-hidden><circle cx="11" cy="11" r="10.5" fill="var(--sx-accent)" />
            {playing ? <g fill="var(--sx-accent-ink)"><rect x="8" y="7" width="2.4" height="8" /><rect x="11.6" y="7" width="2.4" height="8" /></g>
              : <path d="M9 7.4l6 3.6-6 3.6z" fill="var(--sx-accent-ink)" />}
          </svg>
          {playing ? "Pause" : "Play"}
        </button>
        <span className="sx-caption" style={{ color: "var(--text-quiet)", textTransform: "none" }}>{mobile ? "or scroll the years below" : "or drag across the plot"}</span>
        <span className="sx-micro" style={{ marginLeft: mobile ? 0 : "auto", color: "var(--text-body)" }}>{rows.length} programmes · {SP_FROM}–{SP_TO}</span>
      </div>

      {mobile ? (
        <VerticalSpine rows={rows} pyInt={pyInt} />
      ) : (
      <div style={{ position: "relative", marginTop: 20, overflowX: "auto" }}>
        <svg viewBox={`0 0 ${W} ${H}`} width="100%" height={mobile ? 420 : H} role="img" style={{ display: "block", touchAction: "none", minWidth: 680 }}
          aria-label={`Delivery timeline ${SP_FROM} to ${SP_TO}, ${rows.length} programmes. A table of the same record follows.`}>
          {SP.eras.map((e, i) => {
            const [a, b] = e.label.split("–").map(Number);
            const x0 = g.x(a) - g.step / 2, x1 = g.x(Math.min(b, SP_TO)) + g.step / 2;
            const lit = pyInt >= a && pyInt <= b;
            const maxChars = Math.floor((x1 - x0 - 12) / 6.2);
            const short = e.short || e.title.toLowerCase();
            const label = short.length > maxChars ? short.split(/[ ,]/)[0].slice(0, Math.max(3, maxChars)) : short;
            return (
              <g key={e.id}>
                <rect x={x0} y={18} width={x1 - x0} height={g.bandBottom - 18} fill={i % 2 ? "rgba(127,146,196,0.012)" : "rgba(127,146,196,0.035)"} />
                <line x1={x0} y1={18} x2={x0} y2={g.bandBottom} stroke="var(--border-default)" />
                <text x={x0 + 7} y={30} fill={lit ? "var(--sx-accent)" : "var(--text-quiet)"} style={{ font: "400 11px var(--font-mono)" }}>{label}</text>
              </g>
            );
          })}
          <rect x={g.x(pyInt) - g.step / 2} y={18} width={g.step} height={g.bandBottom - 18} fill="var(--sx-accent-soft)" />
          {g.years.map((y) => (
            <g key={y}>
              <line x1={g.x(y)} y1={18} x2={g.x(y)} y2={y % 10 === 0 ? g.bandBottom : g.axisY + (y % 5 === 0 ? 8 : 4)} stroke={y % 10 === 0 ? "var(--border-strong)" : "var(--border-default)"} />
              {y % 5 === 0 && <text x={g.x(y)} y={g.axisY + 22} textAnchor="middle" fill="var(--text-quiet)" style={{ font: "400 12px var(--font-mono)" }}>{y}</text>}
            </g>
          ))}
          <path d={g.wArea} fill="var(--sx-accent-soft)" />
          <path d={g.wPath} fill="none" stroke="var(--sx-accent-line)" strokeWidth="1.5" />
          <line x1={g.left} y1={g.wBottom} x2={g.right} y2={g.wBottom} stroke="var(--border-default)" />
          <text x={g.left - 10} y={g.wTop + 12} textAnchor="end" fill="var(--text-quiet)" style={{ font: "400 11px var(--font-body)" }}>Weight</text>

          {g.nodes.map((n) => {
            const fut = playing && n.year > py;
            const hov = hover && hover.p.slug === n.p.slug;
            const r = hov ? n.r * 1.3 : n.r;
            return (
              <g key={n.p.slug} opacity={fut ? 0 : 1} style={{ transition: "opacity 200ms linear", cursor: "pointer" }}
                onPointerEnter={() => setHover(n)} onPointerLeave={() => setHover(null)}>
                <circle cx={n.x} cy={n.y} r={r + 5} fill="transparent" />
                <circle cx={n.x} cy={n.y} r={r} fill={n.p.tier === "flagship" ? "var(--sx-accent)" : n.p.tier === "deployment" ? "color-mix(in oklab, var(--sx-accent) 70%, var(--surface-raised))" : "color-mix(in oklab, var(--sx-accent) 45%, var(--surface-raised))"}
                  stroke={hov ? "var(--sx-accent)" : "var(--bg-page)"} strokeWidth={hov ? 1.5 : 1} style={{ pointerEvents: "none" }} />
              </g>
            );
          })}
          {g.chips.map((c) => (
            <g key={c.year} onPointerEnter={() => setChip(c.year)} onPointerLeave={() => setChip(null)} style={{ cursor: "pointer" }}>
              <rect x={c.x - 13} y={c.y - 11} width={26} height={15} rx={2} fill="var(--surface-raised)" stroke={chip === c.year ? "var(--sx-accent)" : "var(--border-strong)"} />
              <text x={c.x} y={c.y} textAnchor="middle" fill="var(--text-body)" style={{ font: "400 10px var(--font-mono)", pointerEvents: "none" }}>+{c.count}</text>
            </g>
          ))}
          {flagLabels.map((n, i) => {
            const flip = n.x > g.right - 200;
            const ly = n.y - n.r - 14 - i * 15;
            return (
              <g key={"l" + n.p.slug}>
                <line x1={n.x} y1={n.y - n.r - 2} x2={n.x} y2={ly + 2} stroke="var(--sx-accent-line)" />
                <text x={n.x + (flip ? -6 : 6)} y={ly} textAnchor={flip ? "end" : "start"} fill="var(--text-strong)" style={{ font: "400 12px var(--font-mono)" }}>{n.p.short.toLowerCase()}</text>
              </g>
            );
          })}
          <line x1={g.left} y1={g.axisY} x2={g.right} y2={g.axisY} stroke="var(--border-strong)" />
          {g.ghosts.map((c) => <rect key={c.key} x={c.x} y={c.y} width={c.w} height={c.h} fill="none" stroke="var(--border-default)" />)}
          {g.cells.filter((c) => !(playing && c.year > py)).map((c) => (
            <rect key={c.key} x={c.x} y={c.y} width={c.w} height={c.h} fill="color-mix(in oklab, var(--sx-accent) 52%, var(--surface-card))" />
          ))}
          {SP.layers.map((l) => (
            <text key={l.id} x={g.left - 10} y={g.rowY[l.id] + 12} textAnchor="end" fill="var(--text-quiet)" style={{ font: "400 11px var(--font-body)" }}>{l.label}</text>
          ))}
          <line x1={g.x(py)} y1={18} x2={g.x(py)} y2={g.bandBottom} stroke="var(--sx-accent)" strokeWidth="1" />
          <g transform={`translate(${g.x(py)},${g.axisY - 44})`}>
            <rect x={-19} y={-12} width={38} height={16} rx={2} fill="var(--sx-accent)" />
            <text x={0} y={0} textAnchor="middle" fill="var(--sx-accent-ink)" style={{ font: "500 11px var(--font-mono)" }}>{pyInt}</text>
          </g>
          <rect x={g.left} y={18} width={g.right - g.left} height={g.bandBottom - 18} fill="transparent" role="slider" tabIndex={0}
            aria-label="Timeline year" aria-valuemin={SP_FROM} aria-valuemax={SP_TO} aria-valuenow={pyInt}
            onPointerDown={(e) => { e.currentTarget.setPointerCapture(e.pointerId); setFromX(e.clientX, e.currentTarget); }}
            onPointerMove={(e) => { if (e.buttons) setFromX(e.clientX, e.currentTarget); }}
            onKeyDown={(e) => { if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault(); setPlaying(false); setYear(Math.min(SP_TO, Math.max(SP_FROM, pyInt + (e.key === "ArrowRight" ? 1 : -1)))); } }}
            style={{ cursor: "ew-resize" }} />
        </svg>

        {hover && !mobile && (
          <div style={{ position: "absolute", left: `${Math.min(Math.max((hover.x / W) * 100, 2), 78)}%`, top: Math.max(hover.y - 130, 4), width: 250, padding: "13px 15px", background: "var(--surface-overlay)", border: "1px solid var(--border-strong)", borderRadius: "var(--r-card)", boxShadow: "var(--shadow-overlay)", pointerEvents: "none" }}>
            <p className="sx-micro" style={{ color: "var(--text-accent)" }}>{hover.p.practice.toLowerCase()} · {hover.p.tier}</p>
            <p className="sx-small" style={{ marginTop: 8, color: "var(--text-strong)" }}>{hover.p.client}</p>
            <p className="sx-caption" style={{ marginTop: 6, color: "var(--text-body)", textTransform: "none" }}>{hover.p.title}</p>
            <p className="sx-caption" style={{ marginTop: 8, color: "var(--text-quiet)", textTransform: "none" }}>{hover.p.year}{hover.p.country ? " · " + hover.p.country.toLowerCase() : ""}</p>
          </div>
        )}
        {chip && (
          <div style={{ position: "absolute", left: `${Math.min(Math.max((g.x(chip) / W) * 100, 2), 74)}%`, top: 8, width: 260, padding: "12px 14px", background: "var(--surface-overlay)", border: "1px solid var(--border-strong)", borderRadius: "var(--r-card)", boxShadow: "var(--shadow-overlay)", pointerEvents: "none" }}>
            <p className="sx-caption" style={{ color: "var(--text-quiet)", textTransform: "none" }}>{chip} · {g.chips.find((c) => c.year === chip).all.length} programmes</p>
            <ul style={{ listStyle: "none", margin: "8px 0 0", padding: 0, display: "grid", gap: 4 }}>
              {g.chips.find((c) => c.year === chip).all.map((p) => (
                <li key={p.slug} className="sx-caption" style={{ color: "var(--text-body)", textTransform: "none" }}>{p.short} — {p.title.toLowerCase()}</li>
              ))}
            </ul>
          </div>
        )}
      </div>
      )}

      <div style={{ display: "grid", gap: 1, gridTemplateColumns: mobile ? "1fr" : "repeat(3, minmax(0,1fr))", marginTop: 24, background: "var(--border-default)", border: "1px solid var(--border-default)", borderRadius: "var(--r-card)", overflow: "hidden" }}>
        <div style={{ background: "var(--surface-card)", padding: "18px 20px" }}>
          <p className="sx-caption" style={{ color: "var(--text-quiet)" }}>at the playhead</p>
          <p className="sx-h2 sx-num" style={{ marginTop: 8 }}>{pyInt}</p>
          <p className="sx-caption" style={{ marginTop: 8, color: "var(--text-body)", textTransform: "none" }}>{era.title} · {era.label}</p>
        </div>
        <div style={{ background: "var(--surface-card)", padding: "18px 20px" }}>
          <p className="sx-caption" style={{ color: "var(--text-quiet)" }}>landed that year</p>
          <ul style={{ listStyle: "none", margin: "10px 0 0", padding: 0, display: "grid", gap: 6 }}>
            {landed.length ? landed.map((p) => (
              <li key={p.slug} className="sx-small" style={{ color: "var(--text-strong)" }}>{p.short} <span style={{ color: "var(--text-quiet)" }}>— {p.title.toLowerCase()}</span></li>
            )) : <li className="sx-caption" style={{ color: "var(--text-quiet)", textTransform: "none" }}>nothing new shipped this year — the programmes from the years either side were still running</li>}
          </ul>
        </div>
        <div style={{ background: "var(--surface-card)", padding: "18px 20px" }}>
          <p className="sx-caption" style={{ color: "var(--text-quiet)" }}>capability layers</p>
          <ul style={{ listStyle: "none", margin: "10px 0 0", padding: 0, display: "grid", gap: 4 }}>
            {SP.layers.slice().reverse().map((l) => {
              const on = activeLayers.has(l.id), reach = reachedLayers.has(l.id);
              return (
                <li key={l.id} style={{ display: "flex", alignItems: "center", gap: 9 }}>
                  <span style={{ width: 22, height: 8, background: on ? "color-mix(in oklab, var(--sx-accent) 52%, var(--surface-card))" : "transparent", border: on ? "none" : `1px solid ${reach ? "var(--border-strong)" : "var(--border-default)"}` }} />
                  <span className="sx-caption" style={{ color: on ? "var(--text-strong)" : "var(--text-quiet)", opacity: on || reach ? 1 : 0.5, textTransform: "none" }}>{l.label}</span>
                </li>
              );
            })}
          </ul>
        </div>
      </div>
      <p className="sx-caption" style={{ marginTop: 14, color: "var(--text-quiet)", textTransform: "none", textWrap: "pretty" }}>
        Dot size is the weight of the programme. The band underneath fills a layer in the years work touching it landed, and keeps a ghost outline of every layer ever reached — that outline never falls. Delivery years for the enterprise record are being confirmed against contracts.
      </p>
    </div>
  );
}

Object.assign(window, { AnimatedSpine, VerticalSpine, LAYERS_OF });
