/* diagram.jsx — clean blueprint flow-diagram engine (SVG).
   Renders one board spec: lanes + nodes + edges, with colour encodings
   (build/buy/adapt · shared primitive · product) and a cross-board
   highlight filter + runtime status playback.
   Exports to window: FlowDiagram, PAL, BUILD, STATUS, PRIM, nodeTags, boardUses */

const PAL = {
  ink:    '#1c2733',   // near-navy charcoal — strokes + titles
  ink2:   '#3b4a5a',
  dim:    '#6b7888',   // secondary labels
  faint:  '#9aa7b4',
  line:   '#aab4c0',   // default edge
  paper:  '#ffffff',
  panel:  '#f4f7fa',   // node neutral fill
  panel2: '#eaeef3',
  grid:   'rgba(28,39,51,0.05)',
  accent: '#2f6df6',   // highlight / live
};

// build buckets — the heart of the "off-the-shelf vs make" answer
const BUILD = {
  shelf: { key:'shelf', label:'Off-the-shelf',  hint:'use as-is',        fill:'#e2f4f0', stroke:'#0f9d8c', text:'#0a5d53' },
  adapt: { key:'adapt', label:'Adapt a model',  hint:'fork a reference', fill:'#ece9fb', stroke:'#6b5bd6', text:'#463a99' },
  make:  { key:'make',  label:'Build it',       hint:'write yourself',   fill:'#fbeedb', stroke:'#d98a1f', text:'#8a560f' },
  infra: { key:'infra', label:'Glue / IO',      hint:'data + control',   fill:'#eef1f5', stroke:'#7d8b9a', text:'#46535f' },
};

const STATUS = {
  unstarted:   { fill:'#dfe5ec', stroke:'#b3bdc9', text:'#8a96a3', label:'idle' },
  in_progress: { fill:'#fdeccb', stroke:'#f5a623', text:'#9a6708', label:'running' },
  green:       { fill:'#d6f3e3', stroke:'#19b36b', text:'#0c7a47', label:'pass' },
  red:         { fill:'#fcdcdc', stroke:'#ef4444', text:'#b21f1f', label:'fail' },
  escalated:   { fill:'#efe2fb', stroke:'#a855f7', text:'#7325b8', label:'2-strikes' },
  waiting:     { fill:'#dbe8fe', stroke:'#2f6df6', text:'#1f49b3', label:'human gate' },
};

const PRIM = {
  interrupt:   { label:'interrupt() + checkpointer', fill:'#dbe8fe', stroke:'#2f6df6', text:'#1f49b3' },
  send:        { label:'Send API (fan-out)',         fill:'#e2f4f0', stroke:'#0f9d8c', text:'#0a5d53' },
  conditional: { label:'conditional edge + counter', fill:'#fbeedb', stroke:'#d98a1f', text:'#8a560f' },
};

// ---- runtime ----------------------------------------------------------
function statusAt(spec, nodeId, t) {
  const evs = (spec.runtime && spec.runtime.events) || [];
  let s = 'unstarted';
  for (const e of evs) { if (e.node === nodeId && e.t <= t) s = e.status; }
  return s;
}

// ---- tag system (for highlight filter) --------------------------------
function nodeTags(n) {
  const t = [];
  if (n.build) t.push(n.build);
  if (n.product) t.push(...[].concat(n.product));
  if (n.prim) t.push(...[].concat(n.prim));
  if (n.comp) t.push(...[].concat(n.comp));
  return t;
}
function boardUses(spec, sel) {
  if (!sel) return true;
  return (spec.nodes || []).some((n) => nodeTags(n).includes(sel));
}

// ---- geometry ---------------------------------------------------------
function clip(n, towards) {
  const hw = n.w / 2, hh = n.h / 2;
  const dx = towards.x - n.x, dy = towards.y - n.y;
  if (dx === 0 && dy === 0) return { x: n.x, y: n.y };
  const tx = dx ? hw / Math.abs(dx) : Infinity;
  const ty = dy ? hh / Math.abs(dy) : Infinity;
  const tt = Math.min(tx, ty);
  return { x: n.x + dx * tt, y: n.y + dy * tt };
}

function edgePath(a, b, edge) {
  const route = edge.route || 'auto';
  if (route === 'below' || route === 'above') {
    const dip = edge.dip || 64;
    const sy = route === 'below' ? Math.max(a.y, b.y) + dip : Math.min(a.y, b.y) - dip;
    const p0 = { x: a.x, y: route === 'below' ? a.y + a.h / 2 : a.y - a.h / 2 };
    const p3 = { x: b.x, y: route === 'below' ? b.y + b.h / 2 : b.y - b.h / 2 };
    return {
      d: `M ${p0.x} ${p0.y} C ${p0.x} ${sy}, ${p3.x} ${sy}, ${p3.x} ${p3.y}`,
      end: p3, tan: { x: 0, y: route === 'below' ? -1 : 1 },
    };
  }
  const s = clip(a, b), e = clip(b, a);
  if (Math.abs(s.y - e.y) < 3) return { d: `M ${s.x} ${s.y} L ${e.x} ${e.y}`, end: e, tan: { x: Math.sign(e.x - s.x) || 1, y: 0 } };
  const mx = (s.x + e.x) / 2;
  return { d: `M ${s.x} ${s.y} C ${mx} ${s.y}, ${mx} ${e.y}, ${e.x} ${e.y}`, end: e, tan: { x: Math.sign(e.x - s.x) || 1, y: 0 } };
}

const EK = {
  seq:   { stroke: PAL.line, dash: null,    label: PAL.dim },
  fan:   { stroke: '#0f9d8c', dash: null,   label: '#0a5d53' },
  cond:  { stroke: '#d98a1f', dash: '5 4',  label: '#8a560f' },
  human: { stroke: '#2f6df6', dash: '5 4',  label: '#1f49b3' },
  loop:  { stroke: '#d98a1f', dash: '5 4',  label: '#8a560f' },
  feed:  { stroke: '#6b5bd6', dash: '6 4',  label: '#463a99' },
  emit:  { stroke: '#7d8b9a', dash: '2 4',  label: '#46535f' },
};

// ---- shapes -----------------------------------------------------------
function shapePath(n) {
  const x = n.x - n.w / 2, y = n.y - n.h / 2, w = n.w, h = n.h;
  switch (n.shape) {
    case 'store': { // cylinder
      const r = Math.min(10, h * 0.16);
      return `M ${x} ${y + r} C ${x} ${y - r * 0.5}, ${x + w} ${y - r * 0.5}, ${x + w} ${y + r} `
           + `L ${x + w} ${y + h - r} C ${x + w} ${y + h + r * 0.5}, ${x} ${y + h + r * 0.5}, ${x} ${y + h - r} Z`;
    }
    case 'decision': // diamond
      return `M ${n.x} ${y} L ${x + w} ${n.y} L ${n.x} ${y + h} L ${x} ${n.y} Z`;
    case 'io': { // parallelogram
      const sk = 14;
      return `M ${x + sk} ${y} L ${x + w} ${y} L ${x + w - sk} ${y + h} L ${x} ${y + h} Z`;
    }
    default: { // rounded rect
      const r = n.shape === 'tool' ? 3 : 8;
      return `M ${x + r} ${y} L ${x + w - r} ${y} Q ${x + w} ${y} ${x + w} ${y + r} `
           + `L ${x + w} ${y + h - r} Q ${x + w} ${y + h} ${x + w - r} ${y + h} `
           + `L ${x + r} ${y + h} Q ${x} ${y + h} ${x} ${y + h - r} L ${x} ${y + r} Q ${x} ${y} ${x + r} ${y} Z`;
    }
  }
}

function nodeColors(n, ctx) {
  // runtime overrides everything when the board is "running"
  if (ctx.running && ctx.spec.runtime) {
    const st = STATUS[statusAt(ctx.spec, n.id, ctx.t)];
    return { fill: st.fill, stroke: st.stroke, text: PAL.ink };
  }
  if (ctx.mode === 'primitive') {
    const p = [].concat(n.prim || [])[0];
    if (p && PRIM[p]) return { fill: PRIM[p].fill, stroke: PRIM[p].stroke, text: PAL.ink };
    return { fill: PAL.panel, stroke: PAL.faint, text: PAL.ink };
  }
  if (ctx.mode === 'product') {
    if (n.build && BUILD[n.build]) return { fill: PAL.paper, stroke: BUILD[n.build].stroke, text: PAL.ink };
    return { fill: PAL.panel, stroke: PAL.faint, text: PAL.ink };
  }
  // default: build/buy/make
  const b = BUILD[n.build] || BUILD.infra;
  return { fill: b.fill, stroke: b.stroke, text: PAL.ink };
}

// ---- node component ---------------------------------------------------
function DNode({ n, ctx }) {
  const dim = ctx.density;
  const col = nodeColors(n, ctx);
  const matched = !ctx.sel || nodeTags(n).includes(ctx.sel);
  const op = matched ? 1 : 0.16;
  const x = n.x - n.w / 2, y = n.y - n.h / 2;
  const running = ctx.running && ctx.spec.runtime;
  const st = running ? statusAt(ctx.spec, n.id, ctx.t) : null;
  const titleY = n.sub && dim !== 'min' ? n.y - 4 : n.y + 5;

  const tags = nodeTags(n);
  const showBadge = dim === 'heavy' && (n.product || (n.build && ctx.mode !== 'build'));
  const prod = [].concat(n.product || [])[0];

  return (
    <g style={{ opacity: op, transition: 'opacity .25s' }}>
      {matched && ctx.sel && (
        <path d={shapePath({ ...n, w: n.w + 8, h: n.h + 8 })} fill="none"
          stroke={PAL.accent} strokeWidth="2" strokeDasharray="4 3" opacity="0.9" />
      )}
      <path d={shapePath(n)} fill={col.fill} stroke={col.stroke}
        strokeWidth={n.strong ? 2 : 1.4}
        strokeDasharray={n.build === 'make' && !running ? '6 3' : null}
        style={{ transition: 'fill .3s, stroke .3s' }} />
      {/* status dot in runtime */}
      {running && st && st !== 'unstarted' && (
        <circle cx={x + 11} cy={y + 11} r="4" fill={STATUS[st].stroke}
          style={st === 'in_progress' ? { animation: 'dpulse 1.1s ease-in-out infinite' } : null} />
      )}
      {n.shape !== 'decision' && (
        <text x={n.x} y={titleY} textAnchor="middle"
          style={{ font: `600 ${n.fs || 14}px "Space Grotesk", sans-serif`, fill: col.text || PAL.ink }}>
          {n.label}
        </text>
      )}
      {n.shape === 'decision' && (
        <text x={n.x} y={n.y + 4} textAnchor="middle"
          style={{ font: '600 12px "Space Grotesk", sans-serif', fill: PAL.ink }}>{n.label}</text>
      )}
      {n.sub && dim !== 'min' && n.shape !== 'decision' && (
        <text x={n.x} y={n.y + 13} textAnchor="middle"
          style={{ font: '500 9.5px "JetBrains Mono", monospace', fill: PAL.dim, letterSpacing: '.06em' }}>
          {n.sub.toUpperCase()}
        </text>
      )}
      {/* product / tool badge */}
      {showBadge && prod && (
        <text x={n.x} y={y + n.h + 13} textAnchor="middle"
          style={{ font: '500 9px "JetBrains Mono", monospace', fill: PAL.faint, letterSpacing: '.04em' }}>
          {prod}
        </text>
      )}
    </g>
  );
}

// ---- edge component ---------------------------------------------------
function DEdge({ e, byId, ctx }) {
  const a = byId[e.from], b = byId[e.to];
  if (!a || !b) return null;
  const sk = EK[e.kind] || EK.seq;
  const { d, end, tan } = edgePath(a, b, e);
  const live = ctx.running && ctx.spec.runtime &&
    statusAt(ctx.spec, e.from, ctx.t) === 'green' && statusAt(ctx.spec, e.to, ctx.t) !== 'unstarted';
  const stroke = live ? '#19b36b' : sk.stroke;
  // arrowhead
  const ang = Math.atan2(tan.y, tan.x);
  const ah = 7;
  const p1 = { x: end.x - ah * Math.cos(ang - 0.45), y: end.y - ah * Math.sin(ang - 0.45) };
  const p2 = { x: end.x - ah * Math.cos(ang + 0.45), y: end.y - ah * Math.sin(ang + 0.45) };
  const mid = midpoint(d);
  return (
    <g style={{ opacity: ctx.sel ? 0.5 : 1, transition: 'opacity .25s' }}>
      <path d={d} fill="none" stroke={stroke} strokeWidth={live ? 2.4 : 1.6}
        strokeDasharray={sk.dash} style={{ transition: 'stroke .3s' }} />
      <path d={`M ${end.x} ${end.y} L ${p1.x} ${p1.y} L ${p2.x} ${p2.y} Z`} fill={stroke} />
      {e.label && ctx.density !== 'min' && (
        <g transform={`translate(${e.lx ?? mid.x} ${e.ly ?? mid.y})`}>
          <rect x={-(e.label.length * 3.3 + 6)} y="-9" width={e.label.length * 6.6 + 12} height="17" rx="3"
            fill={PAL.paper} stroke={sk.stroke} strokeWidth="0.8" opacity="0.96" />
          <text x="0" y="3" textAnchor="middle"
            style={{ font: '600 9.5px "JetBrains Mono", monospace', fill: sk.label, letterSpacing: '.02em' }}>
            {e.label}
          </text>
        </g>
      )}
    </g>
  );
}
function midpoint(d) {
  // crude: parse the M and last coord for a label anchor near visual middle
  const nums = d.match(/-?\d+(\.\d+)?/g).map(Number);
  const x0 = nums[0], y0 = nums[1], xn = nums[nums.length - 2], yn = nums[nums.length - 1];
  return { x: (x0 + xn) / 2, y: (y0 + yn) / 2 };
}

// ---- lane / group background -----------------------------------------
function DLane({ l }) {
  const toneFill = l.tone === 'agentic' ? 'rgba(107,91,214,0.06)'
    : l.tone === 'deterministic' ? 'rgba(15,157,140,0.06)'
    : l.tone === 'accent' ? 'rgba(47,109,246,0.05)'
    : 'rgba(28,39,51,0.035)';
  const toneStroke = l.tone === 'agentic' ? 'rgba(107,91,214,0.4)'
    : l.tone === 'deterministic' ? 'rgba(15,157,140,0.4)'
    : l.tone === 'accent' ? 'rgba(47,109,246,0.4)'
    : 'rgba(28,39,51,0.22)';
  return (
    <g>
      <rect x={l.x} y={l.y} width={l.w} height={l.h} rx="10" fill={toneFill}
        stroke={toneStroke} strokeWidth="1.2" strokeDasharray={l.dash === false ? null : '7 5'} />
      {l.label && (
        <text x={l.x + 14} y={l.y + 20}
          style={{ font: '700 11px "JetBrains Mono", monospace', fill: toneStroke.replace(/0\.\d+\)/, '1)'), letterSpacing: '.1em' }}>
          {l.label.toUpperCase()}
        </text>
      )}
      {l.note && (
        <text x={l.x + 14} y={l.y + 36}
          style={{ font: '400 10px "JetBrains Mono", monospace', fill: PAL.dim, letterSpacing: '.02em' }}>
          {l.note}
        </text>
      )}
    </g>
  );
}

// ---- free annotation --------------------------------------------------
function DNote({ a }) {
  const lines = [].concat(a.text);
  return (
    <text x={a.x} y={a.y} textAnchor={a.align || 'start'}>
      {lines.map((ln, i) => (
        <tspan key={i} x={a.x} dy={i === 0 ? 0 : (a.lh || 15)}
          style={{ font: `${a.bold ? '700' : '400'} ${a.fs || 11}px ${a.mono === false ? '"Space Grotesk", sans-serif' : '"JetBrains Mono", monospace'}`,
            fill: a.color || PAL.dim, letterSpacing: '.02em' }}>
          {ln}
        </tspan>
      ))}
    </text>
  );
}

// ---- the diagram ------------------------------------------------------
function FlowDiagram({ spec, mode = 'build', sel = null, density = 'heavy', running = false, t = 0 }) {
  const byId = {};
  (spec.nodes || []).forEach((n) => {
    byId[n.id] = { ...n, w: n.w || 150, h: n.h || 56 };
  });
  const ctx = { spec, mode, sel, density, running, t };
  const vb = spec.vb || [0, 0, 1000, 600];
  return (
    <svg viewBox={vb.join(' ')} width="100%" height="100%"
      preserveAspectRatio="xMidYMid meet" style={{ display: 'block' }}>
      {(spec.lanes || []).map((l, i) => <DLane key={'l' + i} l={l} />)}
      {(spec.edges || []).map((e, i) => <DEdge key={'e' + i} e={e} byId={byId} ctx={ctx} />)}
      {Object.values(byId).map((n) => <DNode key={n.id} n={n} ctx={ctx} />)}
      {(spec.notes || []).map((a, i) => <DNote key={'n' + i} a={a} />)}
    </svg>
  );
}

Object.assign(window, { FlowDiagram, PAL, BUILD, STATUS, PRIM, nodeTags, boardUses, statusAt });
