// Sankey.jsx — receipt-aesthetic Sankey diagram with breakdown selector
// Self-contained layout (no d3-sankey). Operates on TM data.

const { useMemo, useState } = React;

// ---------- helpers ----------
const fmtM = (n) => (n >= 100 ? n.toFixed(0) : n.toFixed(1)) + "M";
const fmtPct = (n, total) => ((n / total) * 100).toFixed(1) + "%";

// Hash a string to a hue
function hashHue(s) {
  let h = 0;
  for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;
  return Math.abs(h) % 360;
}
function nodeColor(node) {
  if (node.shadow) return "var(--danger)";
  return `hsl(${hashHue(node.id)}, 18%, 58%)`;
}

// Build the multi-column flow graph from a chosen pipeline
// pipeline: array of column kinds, e.g. ["team","tech","project","artifact"]
function buildFlow(pipeline, compact = false) {
  const TM = window.TM;
  const artLabel = (a) => compact ? a.id : `${a.id} · ${a.title}`;

  // Project -> tech mix (deterministic distribution)
  const projTechMix = {
    billing:  { go: 0.55, sql: 0.35, tf: 0.10 },
    e2e:      { ts: 0.70, shell: 0.30 },
    rfc:      { ts: 0.60, py: 0.40 },
    auth:     { go: 0.60, ts: 0.40 },
    sdk:      { ts: 0.55, go: 0.45 },
    p2sweep:  { go: 0.50, ts: 0.50 },
    checkout: { ts: 0.55, react: 0.45 },
    lp:       { react: 0.65, ts: 0.35 },
    perf:     { react: 0.55, ts: 0.45 },
    settings: { react: 0.60, ts: 0.40 },
    a11y:     { react: 0.70, ts: 0.30 },
    tokens:   { react: 0.55, ts: 0.45 },
    dbt:      { sql: 0.85, py: 0.15 },
    lint:     { sql: 0.60, py: 0.40 },
    feature:  { py: 0.65, rust: 0.20, sql: 0.15 },
    ds:       { react: 0.70, ts: 0.30 },
    secscan:  { go: 0.45, py: 0.35, tf: 0.20 },
    iam:      { tf: 0.70, py: 0.30 },
    hpa:      { tf: 0.55, go: 0.45 },
    runbook:  { ts: 0.40, shell: 0.60 },
    shadow1:  { py: 0.50, ts: 0.50 },
    shadow2:  { ts: 1.0 },
  };
  // Project -> model mix
  const projModelMix = {
    billing:  { sonnet: 0.6, gpt4o: 0.25, haiku: 0.15 },
    e2e:      { haiku: 0.65, sonnet: 0.35 },
    rfc:      { o1: 0.7, sonnet: 0.3 },
    auth:     { sonnet: 0.55, gpt4o: 0.35, haiku: 0.10 },
    sdk:      { sonnet: 0.5, gpt4o: 0.3, haiku: 0.2 },
    p2sweep:  { gpt4o: 0.5, sonnet: 0.4, haiku: 0.1 },
    checkout: { sonnet: 0.55, gpt4o: 0.45 },
    lp:       { gpt4o: 0.7, sonnet: 0.3 },
    perf:     { haiku: 0.6, sonnet: 0.4 },
    settings: { sonnet: 0.5, gpt4o: 0.4, haiku: 0.1 },
    a11y:     { haiku: 0.6, gpt4o: 0.4 },
    tokens:   { sonnet: 0.7, gpt4o: 0.3 },
    dbt:      { sonnet: 0.5, gpt4o: 0.4, haiku: 0.1 },
    lint:     { haiku: 0.7, sonnet: 0.3 },
    feature:  { o1: 0.55, sonnet: 0.45 },
    ds:       { sonnet: 0.6, gpt4o: 0.4 },
    secscan:  { o1: 0.55, gpt4o: 0.3, sonnet: 0.15 },
    iam:      { sonnet: 0.6, gpt4o: 0.4 },
    hpa:      { sonnet: 0.6, gpt4o: 0.4 },
    runbook:  { gpt4o: 0.7, haiku: 0.3 },
    shadow1:  { gpt4o: 1.0 },
    shadow2:  { haiku: 1.0 },
  };

  // Build per-column nodes & links
  const cols = pipeline.map((k) => ({ kind: k, nodes: [] }));
  const links = [];
  const nodesById = {};

  function addNode(colIdx, id, label, value, meta = {}) {
    const key = `${colIdx}::${id}`;
    if (!nodesById[key]) {
      nodesById[key] = { col: colIdx, id, label, value: 0, meta, key };
      cols[colIdx].nodes.push(nodesById[key]);
    }
    nodesById[key].value += value;
    return nodesById[key];
  }

  // Iterate every PR (each PR has a paired ticket via art.ticket).
  // Iterate shadow rows separately so they still appear when shadow is enabled.
  // Standalone tickets that aren't paired with any PR aren't iterated — but every
  // ticket in the data is paired with at least one PR by design, so nothing is lost.
  const iterate = TM.artifacts.filter((a) => a.kind === "pr" || a.kind === "shadow");
  iterate.forEach((art) => {
    const proj = TM.projects.find((p) => p.id === art.project);
    if (!proj) return;
    const team = TM.teams.find((t) => t.id === proj.team) || { id: "shadow", name: "unattributed", dept: "shadow", shadow: true };
    const dept = TM.departments.find((d) => d.id === team.dept) || { id: "shadow", name: "unattributed", shadow: true };
    const model = TM.models.find((m) => m.id === art.model);

    // Find paired ticket for this PR (or null for shadow)
    const ticket = art.ticket ? TM.artifacts.find((a) => a.id === art.ticket && a.kind === "jira") : null;

    // For tech, split the artifact across the project's tech mix
    const techMix = projTechMix[art.project] || { ts: 1 };

    // Build path for each tech share
    Object.entries(techMix).forEach(([techId, share]) => {
      const tech = TM.techs.find((t) => t.id === techId);
      if (!tech) return;
      const value = art.tokens * share;
      const path = [];
      pipeline.forEach((kind, ci) => {
        let n;
        if (kind === "dept")     n = addNode(ci, dept.id, dept.name, value, { shadow: dept.shadow });
        if (kind === "team")     n = addNode(ci, team.id, team.name, value, { shadow: team.shadow, dept: team.dept });
        if (kind === "tech")     n = addNode(ci, tech.id, tech.name, value);
        if (kind === "model")    n = addNode(ci, model.id, model.name, value);
        if (kind === "ticket") {
          if (ticket) {
            n = addNode(ci, ticket.id, artLabel(ticket), value, { artifact: ticket });
          } else if (art.kind === "shadow") {
            n = addNode(ci, "shadow·" + art.project, compact ? art.id : art.title, value, { artifact: art, shadow: true });
          }
        }
        if (kind === "pr") {
          n = addNode(ci, art.id, artLabel(art), value, { artifact: art, shadow: art.kind === "shadow" });
        }
        if (kind === "artifact") n = addNode(ci, art.id + "·" + art.project, artLabel(art), value, { artifact: art });
        if (n) path.push(n);
      });
      // Add links between adjacent columns
      for (let i = 0; i < path.length - 1; i++) {
        links.push({ source: path[i], target: path[i + 1], value, shadow: art.kind === "shadow" });
      }
    });
  });

  // Sort nodes within each column by value desc and assign vertical positions
  return { cols, links };
}

// Lay out nodes vertically in each column
function layoutFlow(flow, { width, height, padX = 24, gapY = 4, nodeWidth = 14 }) {
  const cols = flow.cols;
  const nCols = cols.length;
  const colX = (i) => padX + (i * (width - 2 * padX - nodeWidth)) / Math.max(1, nCols - 1);

  // Sum per column for normalization
  const colTotals = cols.map((c) => c.nodes.reduce((s, n) => s + n.value, 0));
  const maxTotal = Math.max(...colTotals);

  cols.forEach((c, ci) => {
    // sort: shadow last for visibility
    c.nodes.sort((a, b) => {
      if (a.meta.shadow !== b.meta.shadow) return a.meta.shadow ? 1 : -1;
      return b.value - a.value;
    });
    const total = colTotals[ci];
    const usable = height - (c.nodes.length - 1) * gapY;
    const scale = (usable * (total / maxTotal)) / total; // px per token
    let y = (height - (total * scale + (c.nodes.length - 1) * gapY)) / 2;
    c.nodes.forEach((n) => {
      n.h = Math.max(2, n.value * scale);
      n.x = colX(ci);
      n.y = y;
      n.w = nodeWidth;
      n.scale = scale;
      y += n.h + gapY;
    });
  });

  // For each node, lay out incoming and outgoing link "stacks"
  const linksBySource = new Map();
  const linksByTarget = new Map();
  flow.links.forEach((l) => {
    if (!linksBySource.has(l.source.key)) linksBySource.set(l.source.key, []);
    if (!linksByTarget.has(l.target.key)) linksByTarget.set(l.target.key, []);
    linksBySource.get(l.source.key).push(l);
    linksByTarget.get(l.target.key).push(l);
  });

  // Sort outgoing/incoming by partner y
  linksBySource.forEach((arr) => arr.sort((a, b) => a.target.y - b.target.y));
  linksByTarget.forEach((arr) => arr.sort((a, b) => a.source.y - b.source.y));

  // Assign per-link y offsets at source/target
  cols.forEach((c) => {
    c.nodes.forEach((n) => {
      const out = linksBySource.get(n.key) || [];
      let yo = n.y;
      out.forEach((l) => {
        const lh = l.value * n.scale;
        l.y0 = yo + lh / 2;
        l.h0 = lh;
        yo += lh;
      });
      const ins = linksByTarget.get(n.key) || [];
      let yi = n.y;
      ins.forEach((l) => {
        const lh = l.value * n.scale;
        l.y1 = yi + lh / 2;
        l.h1 = lh;
        yi += lh;
      });
    });
  });

  return { width, height };
}

function linkPath(l) {
  const x0 = l.source.x + l.source.w;
  const x1 = l.target.x;
  const mx = (x0 + x1) / 2;
  return `M${x0},${l.y0} C${mx},${l.y0} ${mx},${l.y1} ${x1},${l.y1}`;
}

// ---------- column kind options ----------
const KINDS = {
  dept:     { label: "department",  hint: "// dept" },
  team:     { label: "team",        hint: "// team" },
  tech:     { label: "technology",  hint: "// tech" },
  model:    { label: "llm model",   hint: "// model" },
  ticket:   { label: "ticket",      hint: "// jira" },
  pr:       { label: "pull request", hint: "// github" },
};

// Preset breakdowns
const PRESETS = [
  { id: "org",     name: "by org",        pipeline: ["dept", "team", "ticket", "pr"] },
  { id: "tech",    name: "by technology", pipeline: ["tech", "ticket", "pr"] },
  { id: "model",   name: "by llm model",  pipeline: ["model", "ticket", "pr"] },
];

// ---------- main component ----------
function SankeyView({ width = 1100, height = 520, presetId = "org", windowKey = "7d", onOpenArtifact }) {
  const [hover, setHover] = useState(null);
  const [pinned, setPinned] = useState(null);

  const preset = PRESETS.find((p) => p.id === presetId) || PRESETS[0];
  const compact = width < 900;
  const flow = useMemo(() => buildFlow(preset.pipeline, compact), [presetId, compact]);
  const padX = 110;
  const nodeWidth = 12;
  layoutFlow(flow, { width, height: height - 40, padX, nodeWidth, gapY: 3 });

  const total = flow.cols[0].nodes.reduce((s, n) => s + n.value, 0);

  // Hovered set: highlight links from/to a hovered node
  const focus = pinned || hover;
  const isLinkFocused = (l) => {
    if (!focus) return false;
    return l.source.key === focus.key || l.target.key === focus.key;
  };
  const isNodeFocused = (n) => {
    if (!focus) return false;
    if (n.key === focus.key) return true;
    return flow.links.some((l) =>
      (l.source.key === focus.key && l.target.key === n.key) ||
      (l.target.key === focus.key && l.source.key === n.key) ||
      (l.source.key === n.key && l.target.key === focus.key) ||
      (l.target.key === n.key && l.source.key === focus.key)
    );
  };

  return (
    <div style={skStyles.wrap}>

      <svg width={width} height={height - 40} style={{ display: "block" }}>
        <defs>
          <pattern id="shadowPattern" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(45)">
            <rect width="6" height="6" fill="var(--danger-faint)" />
            <line x1="0" y1="0" x2="0" y2="6" stroke="var(--danger)" strokeWidth="1.2" />
          </pattern>
        </defs>

        {/* dashed verticals between columns */}
        {flow.cols.slice(0, -1).map((c, i) => {
          const x = (c.nodes[0]?.x ?? 0) + 12 + ((flow.cols[i + 1].nodes[0]?.x ?? 0) - (c.nodes[0]?.x ?? 0)) / 2;
          return <line key={i} x1={x} x2={x} y1={6} y2={height - 46} stroke="var(--fg-4)" strokeDasharray="2 4" />;
        })}

        {/* links */}
        <g>
          {flow.links.map((l, i) => {
            const focused = isLinkFocused(l);
            const dim = focus && !focused;
            return (
              <path
                key={i}
                d={linkPath(l)}
                fill="none"
                stroke={l.shadow ? "url(#shadowPattern)" : nodeColor(l.source)}
                strokeOpacity={dim ? 0.06 : focused ? 0.55 : 0.22}
                strokeWidth={Math.max(1, l.h0)}
                style={{ transition: "stroke-opacity var(--dur-fast) var(--ease)" }}
              />
            );
          })}
        </g>

        {/* nodes */}
        <g>
          {flow.cols.map((c, ci) =>
            c.nodes.map((n) => {
              const focused = isNodeFocused(n);
              const dim = focus && !focused;
              const isLast = ci === flow.cols.length - 1;
              const labelX = ci === 0 ? n.x - 8 : n.x + n.w + 8;
              const anchor = ci === 0 ? "end" : "start";
              const isArtifact = preset.pipeline[ci] === "ticket" || preset.pipeline[ci] === "pr";
              const passthrough = n.meta.passthrough;
              return (
                <g
                  key={n.key}
                  style={{ cursor: isArtifact && !passthrough ? "pointer" : "default", opacity: dim ? 0.35 : passthrough ? 0.5 : 1, transition: "opacity var(--dur-fast) var(--ease)" }}
                  onMouseEnter={() => setHover(n)}
                  onMouseLeave={() => setHover(null)}
                  onClick={() => {
                    if (isArtifact && n.meta.artifact && onOpenArtifact) onOpenArtifact(n.meta.artifact);
                    else setPinned(pinned?.key === n.key ? null : n);
                  }}
                >
                  <rect
                    x={n.x} y={n.y} width={n.w} height={n.h}
                    fill={n.meta.shadow ? "url(#shadowPattern)" : nodeColor(n)}
                    stroke={focused ? "var(--fg-0)" : "transparent"}
                    strokeWidth={1}
                  />
                  {n.h >= 8 && (
                    <text
                      x={labelX}
                      y={n.y + n.h / 2}
                      textAnchor={anchor}
                      dominantBaseline="middle"
                      fontFamily="var(--font-mono)"
                      fontSize="11"
                      fill={n.meta.shadow ? "var(--danger)" : "var(--fg-1)"}
                      style={{ pointerEvents: "none" }}
                    >
                      {truncate(n.label, isArtifact ? 28 : 18)}
                    </text>
                  )}
                  {n.h >= 8 && (
                    <text
                      x={labelX}
                      y={n.y + n.h / 2 + 12}
                      textAnchor={anchor}
                      dominantBaseline="middle"
                      fontFamily="var(--font-mono)"
                      fontSize="9"
                      fill="var(--fg-3)"
                      style={{ pointerEvents: "none" }}
                    >
                      {fmtM(n.value)} · {fmtPct(n.value, total)}
                    </text>
                  )}
                </g>
              );
            })
          )}
        </g>
      </svg>

      {/* footer / hover readout */}
      <div style={skStyles.foot}>
        {focus ? (
          <span>
            <span style={{ color: "var(--fg-3)" }}>focus </span>
            <span style={{ color: focus.meta.shadow ? "var(--danger)" : "var(--fg-0)" }}>{focus.label}</span>
            <span style={{ color: "var(--fg-3)" }}>  ·  </span>
            <span style={{ color: "var(--fg-0)" }}>{fmtM(focus.value)}</span>
            <span style={{ color: "var(--fg-3)" }}>  ·  </span>
            <span style={{ color: "var(--fg-2)" }}>{fmtPct(focus.value, total)} of total</span>
            {pinned && <span style={{ marginLeft: 16, color: "var(--accent)" }}>● pinned · click again to release</span>}
          </span>
        ) : (
          <span style={{ color: "var(--fg-3)" }}>// hover a node to focus its flows · click an artifact node to open the PR or jira ticket</span>
        )}
      </div>
    </div>
  );
}

function truncate(s, n) {
  return s.length > n ? s.slice(0, n - 1) + "…" : s;
}

const skStyles = {
  wrap: { background: "var(--bg-1)", border: "1px solid var(--border)", borderRadius: "var(--radius-md)", padding: 16 },
  colHeads: { position: "relative", marginBottom: 8, borderBottom: "var(--rule-dashed)" },
  colHead: { display: "flex", flexDirection: "column", gap: 2, alignItems: "center", textAlign: "center" },
  colHeadHint: { fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--fg-3)", textTransform: "uppercase", letterSpacing: "0.06em" },
  colHeadLabel: { fontFamily: "var(--font-display)", fontSize: 14, fontWeight: 600, color: "var(--fg-0)" },
  foot: { paddingTop: 10, borderTop: "var(--rule-dashed)", marginTop: 8, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--fg-2)" },
};

window.SankeyView = SankeyView;
window.SANKEY_PRESETS = PRESETS;
