"use client";

import { useMemo } from "react";
import ReactFlow, {
  Edge,
  Handle,
  MarkerType,
  Node,
  NodeProps,
  Position,
  ReactFlowProvider,
} from "reactflow";
import "reactflow/dist/style.css";
import "./ProgramGraph.css";
import { ReprNode, parseProgramRepr } from "../lib/programRepr";

// Palette tokens (must match tailwind.config.ts).
const C = {
  bg: "#FAFAF7",
  card: "#FCFBF8",
  ink: "#23303A",
  muted: "#6E7F8C",
  border: "#ECEAE4",
  accent: "#3A6B7E",
  highlight: "#BC6B2E",
  matrix: "#F1EFEA",
  score: "#FBF1E6",
};

export type GenePill = { id: string; symbol?: string | null; matched?: boolean };

interface ProgramGraphProps {
  programRepr: string;
  outputLabel?: string;
  /** opaque_id -> { symbol, matched } once revealed. */
  reveal?: Record<string, { symbol: string; matched: boolean }>;
}

// ---------- custom node components ----------------------------------------

function DataPillNode({ data }: NodeProps<{ label: string }>) {
  return (
    <div
      className="pg-node"
      style={{
        background: C.matrix,
        border: `1px solid ${C.border}`,
        color: C.ink,
        borderRadius: 999,
        padding: "8px 14px",
        fontSize: 12,
        whiteSpace: "nowrap",
      }}
    >
      <Handle type="source" position={Position.Right} className="pg-handle" />
      <Handle type="target" position={Position.Left} className="pg-handle" />
      {data.label}
    </div>
  );
}

function ScorePillNode({ data }: NodeProps<{ label: string }>) {
  return (
    <div
      className="pg-node"
      style={{
        background: C.score,
        border: `1px solid ${C.highlight}`,
        color: C.highlight,
        borderRadius: 999,
        padding: "8px 14px",
        fontSize: 12,
        fontWeight: 500,
        whiteSpace: "nowrap",
      }}
    >
      <Handle type="source" position={Position.Right} className="pg-handle" />
      <Handle type="target" position={Position.Left} className="pg-handle" />
      {data.label}
    </div>
  );
}

function VerbNode({
  data,
}: NodeProps<{ label: string; sub?: string; pills?: GenePill[] }>) {
  return (
    <div
      className="pg-node"
      style={{
        background: C.card,
        border: `1px solid ${C.accent}`,
        color: C.ink,
        borderRadius: 8,
        padding: "10px 14px",
        minWidth: 130,
        boxShadow: "0 1px 0 rgba(35,48,58,0.02)",
      }}
    >
      <Handle type="source" position={Position.Right} className="pg-handle" />
      <Handle type="target" position={Position.Left} className="pg-handle" />
      <div style={{ fontSize: 12, fontWeight: 600, color: C.ink }}>
        {data.label}
      </div>
      {data.sub && (
        <div style={{ marginTop: 2, fontSize: 10, color: C.muted }}>
          {data.sub}
        </div>
      )}
      {data.pills && data.pills.length > 0 && (
        <div
          style={{
            marginTop: 6,
            display: "flex",
            flexWrap: "wrap",
            gap: 4,
            maxWidth: 220,
          }}
        >
          {data.pills.map((p) => {
            const display = p.symbol ?? p.id;
            const matched = p.matched;
            return (
              <span
                key={p.id}
                title={p.symbol ? `${p.id} → ${p.symbol}` : p.id}
                style={{
                  fontFamily:
                    'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
                  fontSize: 10,
                  padding: "2px 6px",
                  borderRadius: 4,
                  background: matched ? C.highlight : C.bg,
                  color: matched ? C.card : C.ink,
                  border: `1px solid ${matched ? C.highlight : C.border}`,
                  fontWeight: matched ? 600 : 400,
                  lineHeight: 1.3,
                }}
              >
                {display}
              </span>
            );
          })}
        </div>
      )}
    </div>
  );
}

function OutputNode({ data }: NodeProps<{ label: string }>) {
  return (
    <div
      className="pg-node"
      style={{
        background: C.highlight,
        color: "#FFFFFF",
        border: `1px solid ${C.highlight}`,
        borderRadius: 999,
        padding: "10px 16px",
        fontSize: 12,
        fontWeight: 600,
        whiteSpace: "nowrap",
        boxShadow: "0 1px 0 rgba(35,48,58,0.04)",
      }}
    >
      <Handle type="target" position={Position.Left} className="pg-handle" />
      {data.label}
    </div>
  );
}

function TierGroupNode({
  data,
}: NodeProps<{ label: string; variant: "tier1" | "tier2"; width: number; height: number }>) {
  const isTier2 = data.variant === "tier2";
  return (
    <div
      style={{
        width: data.width,
        height: data.height,
        borderRadius: 12,
        background: isTier2 ? "transparent" : "#F1F6F7",
        border: isTier2 ? "1px dashed #BC6B2E" : "1px solid #3A6B7E",
        position: "relative",
        pointerEvents: "none",
      }}
    >
      <span
        style={{
          position: "absolute",
          top: 6,
          left: 10,
          padding: "0 6px",
          fontSize: 10,
          letterSpacing: 0.4,
          fontWeight: 600,
          color: isTier2 ? "#BC6B2E" : "#3A6B7E",
          background: isTier2 ? C.bg : "#F1F6F7",
        }}
      >
        {data.label}
      </span>
    </div>
  );
}

const nodeTypes = {
  dataPill: DataPillNode,
  scorePill: ScorePillNode,
  verb: VerbNode,
  output: OutputNode,
  tierGroup: TierGroupNode,
};

// ---------- layout --------------------------------------------------------

const COL_W = 200;     // horizontal spacing between layers
const ROW_H = 95;      // vertical spacing between sibling lanes

interface Built {
  nodes: Node[];
  edges: Edge[];
  width: number;
  height: number;
}

interface Counter {
  id: number;
}

function nid(c: Counter, prefix: string): string {
  c.id += 1;
  return `${prefix}${c.id}`;
}

function pillsFor(
  features: string[],
  reveal?: Record<string, { symbol: string; matched: boolean }>,
): GenePill[] {
  return features.map((id) => {
    const r = reveal?.[id];
    return r ? { id, symbol: r.symbol, matched: r.matched } : { id };
  });
}

/** Recursively build nodes/edges for a subtree rooted at ``tree``.
 * Returns the SubLayout — its own width / height bounding box and the
 * (x,y) of its right-output anchor (where the parent connects). */
type SubLayout = {
  rootId: string;
  width: number;   // span in x
  height: number;  // span in y
  anchorY: number; // vertical centre relative to the layout
};

function buildSubtree(
  tree: ReprNode,
  reveal: ProgramGraphProps["reveal"],
  c: Counter,
  out: { nodes: Node[]; edges: Edge[] },
  xOffset: number,
  yOffset: number,
): SubLayout {
  if (tree.kind === "M") {
    const id = nid(c, "M");
    out.nodes.push({
      id,
      type: "dataPill",
      position: { x: xOffset, y: yOffset },
      data: { label: "Expression matrix" },
      draggable: false,
    });
    return { rootId: id, width: COL_W, height: ROW_H, anchorY: yOffset + 18 };
  }

  if (tree.kind === "Select") {
    const child = buildSubtree(tree.matrix, reveal, c, out, xOffset, yOffset);
    const id = nid(c, "Sel");
    const x = xOffset + child.width;
    const y = child.anchorY - 18;
    out.nodes.push({
      id,
      type: "verb",
      position: { x, y },
      data: {
        label: "Select",
        sub: `${tree.features.length} gene${tree.features.length === 1 ? "" : "s"}`,
        pills: pillsFor(tree.features, reveal),
      },
      draggable: false,
    });
    out.edges.push(edge(`${child.rootId}->${id}`, child.rootId, id));
    const width = child.width + COL_W;
    return {
      rootId: id,
      width,
      height: Math.max(child.height, ROW_H),
      anchorY: child.anchorY,
    };
  }

  if (tree.kind === "Reduce") {
    const child = buildSubtree(tree.matrix, reveal, c, out, xOffset, yOffset);
    const id = nid(c, "Red");
    const x = xOffset + child.width;
    const y = child.anchorY - 18;
    out.nodes.push({
      id,
      type: "verb",
      position: { x, y },
      data: { label: "Reduce", sub: tree.agg },
      draggable: false,
    });
    out.edges.push(edge(`${child.rootId}->${id}`, child.rootId, id));
    return {
      rootId: id,
      width: child.width + COL_W,
      height: Math.max(child.height, ROW_H),
      anchorY: child.anchorY,
    };
  }

  if (tree.kind === "Combine") {
    const left = buildSubtree(tree.left, reveal, c, out, xOffset, yOffset);
    const right = buildSubtree(
      tree.right,
      reveal,
      c,
      out,
      xOffset,
      yOffset + left.height + 20,
    );
    const id = nid(c, "Cmb");
    const maxChildWidth = Math.max(left.width, right.width);
    const x = xOffset + maxChildWidth;
    const centre = (left.anchorY + right.anchorY) / 2;
    out.nodes.push({
      id,
      type: "verb",
      position: { x, y: centre - 18 },
      data: { label: "Combine", sub: tree.op },
      draggable: false,
    });
    out.edges.push(edge(`${left.rootId}->${id}`, left.rootId, id));
    out.edges.push(edge(`${right.rootId}->${id}`, right.rootId, id));
    return {
      rootId: id,
      width: maxChildWidth + COL_W,
      height: left.height + right.height + 20,
      anchorY: centre,
    };
  }

  if (tree.kind === "Search") {
    const child = buildSubtree(tree.matrix, reveal, c, out, xOffset, yOffset);
    const id = nid(c, "Srch");
    const x = xOffset + child.width;
    const y = child.anchorY - 18;
    out.nodes.push({
      id,
      type: "verb",
      position: { x, y },
      data: { label: "Search", sub: `top-${tree.k} (bounded)` },
      draggable: false,
    });
    out.edges.push(edge(`${child.rootId}->${id}`, child.rootId, id));
    return {
      rootId: id,
      width: child.width + COL_W,
      height: Math.max(child.height, ROW_H),
      anchorY: child.anchorY,
    };
  }

  if (tree.kind === "Split") {
    const child = buildSubtree(tree.inner, reveal, c, out, xOffset, yOffset);
    const id = nid(c, "Splt");
    const x = xOffset + child.width;
    const y = child.anchorY - 18;
    out.nodes.push({
      id,
      type: "verb",
      position: { x, y },
      data: { label: "Split", sub: tree.predicate },
      draggable: false,
    });
    out.edges.push(edge(`${child.rootId}->${id}`, child.rootId, id));
    return {
      rootId: id,
      width: child.width + COL_W,
      height: Math.max(child.height, ROW_H),
      anchorY: child.anchorY,
    };
  }

  if (tree.kind === "FitApply") {
    const child = buildSubtree(tree.inner, reveal, c, out, xOffset, yOffset);
    const id = nid(c, "FitA");
    const x = xOffset + child.width;
    const y = child.anchorY - 18;
    out.nodes.push({
      id,
      type: "verb",
      position: { x, y },
      data: { label: "Fit → Apply", sub: `target: ${tree.target}` },
      draggable: false,
    });
    out.edges.push(edge(`${child.rootId}->${id}`, child.rootId, id));
    return {
      rootId: id,
      width: child.width + COL_W,
      height: Math.max(child.height, ROW_H),
      anchorY: child.anchorY,
    };
  }

  if (tree.kind === "Associate") {
    const child = buildSubtree(tree.inner, reveal, c, out, xOffset, yOffset);
    const id = nid(c, "Assoc");
    const x = xOffset + child.width;
    const y = child.anchorY - 18;
    out.nodes.push({
      id,
      type: "verb",
      position: { x, y },
      data: {
        label: "Associate",
        sub: `${tree.assocKind} · target: ${tree.target}`,
      },
      draggable: false,
    });
    out.edges.push(edge(`${child.rootId}->${id}`, child.rootId, id));
    return {
      rootId: id,
      width: child.width + COL_W,
      height: Math.max(child.height, ROW_H),
      anchorY: child.anchorY,
    };
  }

  if (tree.kind === "Effect") {
    const child = buildSubtree(tree.inner, reveal, c, out, xOffset, yOffset);
    const id = nid(c, "Eff");
    const x = xOffset + child.width;
    const y = child.anchorY - 18;
    out.nodes.push({
      id,
      type: "verb",
      position: { x, y },
      data: {
        label: "Effect",
        sub: `${tree.kind} · adjust: stage, age`,
      },
      draggable: false,
    });
    out.edges.push(edge(`${child.rootId}->${id}`, child.rootId, id));
    return {
      rootId: id,
      width: child.width + COL_W,
      height: Math.max(child.height, ROW_H),
      anchorY: child.anchorY,
    };
  }

  if (tree.kind === "Fit") {
    // Legacy v1 fallback: render each child subtree, then a "Fit" verb,
    // then the output node.
    const stacks: SubLayout[] = [];
    let yCursor = yOffset;
    for (const child of tree.children) {
      const lay = buildSubtree(child, reveal, c, out, xOffset, yCursor);
      stacks.push(lay);
      yCursor += lay.height + 20;
    }
    const childMaxWidth = Math.max(...stacks.map((s) => s.width));
    const fitId = nid(c, "Fit");
    const centre =
      stacks.reduce((a, s) => a + s.anchorY, 0) /
      Math.max(stacks.length, 1);
    out.nodes.push({
      id: fitId,
      type: "verb",
      position: { x: xOffset + childMaxWidth, y: centre - 18 },
      data: { label: "Fit", sub: tree.output || "target" },
      draggable: false,
    });
    for (const s of stacks) {
      out.edges.push(edge(`${s.rootId}->${fitId}`, s.rootId, fitId));
    }
    return {
      rootId: fitId,
      width: childMaxWidth + COL_W,
      height: stacks.reduce((a, s) => a + s.height + 20, 0),
      anchorY: centre,
    };
  }

  // Unknown — render as a muted box.
  const id = nid(c, "U");
  out.nodes.push({
    id,
    type: "verb",
    position: { x: xOffset, y: yOffset },
    data: { label: "?", sub: tree.text.slice(0, 32) },
    draggable: false,
  });
  return { rootId: id, width: COL_W, height: ROW_H, anchorY: yOffset + 18 };
}

function edge(id: string, source: string, target: string): Edge {
  return {
    id,
    source,
    target,
    type: "smoothstep",
    style: { stroke: "#B9B6AE", strokeWidth: 1.4 },
    markerEnd: {
      type: MarkerType.ArrowClosed,
      color: "#B9B6AE",
      width: 14,
      height: 14,
    },
  };
}

function buildGraph(props: ProgramGraphProps): Built {
  const out = { nodes: [] as Node[], edges: [] as Edge[] };
  const c: Counter = { id: 0 };
  let tree: ReprNode;
  try {
    tree = parseProgramRepr(props.programRepr);
  } catch (e) {
    out.nodes.push({
      id: "err",
      type: "verb",
      position: { x: 20, y: 20 },
      data: { label: "couldn't parse program", sub: String(e) },
      draggable: false,
    });
    return { ...out, width: 320, height: 120 };
  }

  if (tree.kind === "Combine") {
    return buildCombineRoot(tree, props, c, out);
  }

  const lay = buildSubtree(tree, props.reveal, c, out, 20, 20);
  // Append a terminal output pill for typed (engine_v2) trees that end in
  // a Vector but have no explicit output node. The legacy Fit root path
  // already adds a "Fit" verb labelled with the output, so we skip there.
  if (tree.kind !== "Fit") {
    const id = nid(c, "Out");
    const x = lay.width + 40;
    const y = lay.anchorY - 14;
    out.nodes.push({
      id,
      type: "output",
      position: { x, y },
      data: { label: props.outputLabel ?? "score" },
      draggable: false,
    });
    out.edges.push(edge(`${lay.rootId}->${id}`, lay.rootId, id));
    return { ...out, width: lay.width + COL_W + 40, height: lay.height + 60 };
  }
  return { ...out, width: lay.width + 40, height: lay.height + 60 };
}

// ---------- Combine root: Tier-1 boxes per side + Tier-2 dashed wrap -----

const SUBTREE_X = 40;        // left padding inside Tier-2 wrapper
const SUBTREE_Y0 = 44;       // top padding (room for Tier-2 label)
const TIER_GAP = 56;         // vertical gap between Tier-1 groups
const TIER_PAD = 18;         // padding around node bboxes inside Tier-1
const TIER_LABEL_H = 22;     // visual room for "Tier-1" label

function buildCombineRoot(
  tree: ReprNode & { kind: "Combine" },
  props: ProgramGraphProps,
  c: Counter,
  out: { nodes: Node[]; edges: Edge[] },
): Built {
  const leftStart = out.nodes.length;
  const left = buildSubtree(tree.left, props.reveal, c, out, SUBTREE_X, SUBTREE_Y0);
  const leftRange: [number, number] = [leftStart, out.nodes.length];

  const rightStart = out.nodes.length;
  const rightY = SUBTREE_Y0 + left.height + TIER_GAP;
  const right = buildSubtree(tree.right, props.reveal, c, out, SUBTREE_X, rightY);
  const rightRange: [number, number] = [rightStart, out.nodes.length];

  const leftBbox = bboxFromNodes(out.nodes, leftRange);
  const rightBbox = bboxFromNodes(out.nodes, rightRange);

  // Tier-1 boxes are positioned to contain each side's bbox.
  const leftTier = boxForBbox(leftBbox, "Tier-1", "tier1");
  const rightTier = boxForBbox(rightBbox, "Tier-1", "tier1");

  // Place Combine + Output to the right of the wider subtree.
  const subtreeMaxRight = Math.max(leftTier.maxX, rightTier.maxX);
  const cmbX = subtreeMaxRight + 30;
  const centreY = (left.anchorY + right.anchorY) / 2;
  const cmbId = nid(c, "Cmb");
  out.nodes.push({
    id: cmbId,
    type: "verb",
    position: { x: cmbX, y: centreY - 18 },
    data: { label: "Combine", sub: tree.op },
    draggable: false,
  });
  out.edges.push(edge(`${left.rootId}->${cmbId}`, left.rootId, cmbId));
  out.edges.push(edge(`${right.rootId}->${cmbId}`, right.rootId, cmbId));

  const outX = cmbX + COL_W;
  const outId = nid(c, "Out");
  out.nodes.push({
    id: outId,
    type: "output",
    position: { x: outX, y: centreY - 14 },
    data: { label: props.outputLabel ?? "score" },
    draggable: false,
  });
  out.edges.push(edge(`${cmbId}->${outId}`, cmbId, outId));

  // Tier-2 dashed wrap around EVERYTHING (left + right + Combine + Output).
  const allBbox = unionBboxes([
    leftTier,
    rightTier,
    { minX: cmbX, minY: centreY - 22, maxX: outX + 170, maxY: centreY + 22 },
  ]);
  const tier2 = boxForBbox(allBbox, "Tier-2 program", "tier2");

  // Insert tier-group nodes at the START so they render BEHIND everything.
  out.nodes.unshift(tierGroupNode(c, leftTier, "Tier-1", "tier1"));
  out.nodes.unshift(tierGroupNode(c, rightTier, "Tier-1", "tier1"));
  out.nodes.unshift(tierGroupNode(c, tier2, "Tier-2 program", "tier2"));

  const totalWidth = tier2.maxX + 24;
  const totalHeight = tier2.maxY + 24;
  return { ...out, width: totalWidth, height: totalHeight };
}

interface Bbox {
  minX: number;
  minY: number;
  maxX: number;
  maxY: number;
}

function bboxFromNodes(nodes: Node[], [start, end]: [number, number]): Bbox {
  let minX = Infinity;
  let minY = Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;
  for (let i = start; i < end; i++) {
    const n = nodes[i];
    const { x, y } = n.position;
    const { w, h } = approxSize(n);
    if (x < minX) minX = x;
    if (y < minY) minY = y;
    if (x + w > maxX) maxX = x + w;
    if (y + h > maxY) maxY = y + h;
  }
  return { minX, minY, maxX, maxY };
}

function approxSize(n: Node): { w: number; h: number } {
  // Conservative bounds for each node type. Verb nodes with many gene
  // pills get taller — estimate from the data payload.
  const t = n.type;
  if (t === "dataPill" || t === "scorePill" || t === "output") {
    return { w: 170, h: 38 };
  }
  if (t === "verb") {
    const pills: unknown = (n.data as { pills?: unknown })?.pills;
    const pillCount = Array.isArray(pills) ? pills.length : 0;
    const pillRows = pillCount > 0 ? Math.ceil(pillCount / 4) : 0;
    return { w: 220, h: 56 + pillRows * 22 };
  }
  return { w: 170, h: 56 };
}

function unionBboxes(list: Bbox[]): Bbox {
  return {
    minX: Math.min(...list.map((b) => b.minX)),
    minY: Math.min(...list.map((b) => b.minY)),
    maxX: Math.max(...list.map((b) => b.maxX)),
    maxY: Math.max(...list.map((b) => b.maxY)),
  };
}

function boxForBbox(b: Bbox, _label: string, _variant: "tier1" | "tier2"): Bbox {
  // Same shape; bbox-with-padding used both for the visual tier rect and
  // for downstream layout.
  return {
    minX: b.minX - TIER_PAD,
    minY: b.minY - TIER_LABEL_H - 6,
    maxX: b.maxX + TIER_PAD,
    maxY: b.maxY + TIER_PAD,
  };
}

function tierGroupNode(
  c: Counter,
  b: Bbox,
  label: string,
  variant: "tier1" | "tier2",
): Node {
  return {
    id: nid(c, "tier-"),
    type: "tierGroup",
    position: { x: b.minX, y: b.minY },
    data: {
      label,
      variant,
      width: b.maxX - b.minX,
      height: b.maxY - b.minY,
    },
    draggable: false,
    selectable: false,
    style: { zIndex: 0 },
  };
}

export default function ProgramGraph(props: ProgramGraphProps) {
  const { nodes, edges, width, height } = useMemo(
    () => buildGraph(props),
    [props],
  );
  const containerHeight = Math.max(220, height);
  // Remount-key so ReactFlow's one-shot `fitView` runs again every
  // time the laid-out program changes (winner ↔ candidate, pasted,
  // dataset swap). Without this a larger Tier-2 graph stays at the
  // previous zoom and clips on the right/bottom.
  const fitKey = `${width}x${height}:${nodes.length}:${edges.length}`;

  return (
    <div
      style={{ height: containerHeight, background: "transparent", borderRadius: 8 }}
      className="pg-canvas"
    >
      <ReactFlowProvider>
        <ReactFlow
          key={fitKey}
          nodes={nodes}
          edges={edges}
          nodeTypes={nodeTypes}
          fitView
          fitViewOptions={{ padding: 0.18 }}
          nodesDraggable={false}
          nodesConnectable={false}
          edgesFocusable={false}
          panOnDrag={false}
          panOnScroll={false}
          zoomOnScroll={false}
          zoomOnPinch={false}
          zoomOnDoubleClick={false}
          preventScrolling={false}
          proOptions={{ hideAttribution: true }}
          // A floor of 0.4 was too strict for full 2-tier programs;
          // 0.2 lets fitView always scale the whole tree to fit the
          // card width without horizontal overflow.
          minZoom={0.2}
          maxZoom={1.2}
        />
      </ReactFlowProvider>
    </div>
  );
}
