"use client";

import { useCallback, useMemo, useRef, useEffect, useState } from "react";
import { DOMAIN_COLORS, DOMAIN_LABELS, type Domain } from "~/components/analytics/computation-graph/domain-colors";
import type { PositionedNode } from "~/components/analytics/computation-graph/graph-data";
import type { ComputationGraphState } from "~/components/analytics/computation-graph/useComputationGraphData";

// ─── Layout constants ────────────────────────────────────────────────────────

const NODE_W = 220;
const NODE_H = 88;
const H_GAP = 50;
const V_GAP = 140;
const PADDING = 60;

const MIN_ZOOM = 0.2;
const MAX_ZOOM = 3;
const ZOOM_STEP = 0.12;

// ─── 2D DAG layout ──────────────────────────────────────────────────────────

interface PlacedNode extends PositionedNode {
  px: number;
  py: number;
}

function build2DLayout(nodes: PositionedNode[]): { placed: PlacedNode[]; width: number; height: number } {
  if (nodes.length === 0) return { placed: [], width: 800, height: 400 };

  const byLevel = new Map<number, PositionedNode[]>();
  for (const n of nodes) {
    const arr = byLevel.get(n.level) ?? [];
    arr.push(n);
    byLevel.set(n.level, arr);
  }

  const levels = [...byLevel.keys()].sort((a, b) => a - b);

  const domainOrder: Domain[] = [
    "EFFORT", "SAH", "ESTIMATE", "INPUT", "ALLOCATION", "COMMERCIAL",
    "RULES", "EXPERIENCE", "CHARGEABILITY", "SPREAD", "BUDGET",
  ];
  for (const [, arr] of byLevel) {
    arr.sort((a, b) => {
      const ai = domainOrder.indexOf(a.domain as Domain);
      const bi = domainOrder.indexOf(b.domain as Domain);
      return ai - bi;
    });
  }

  let maxRowNodes = 0;
  for (const arr of byLevel.values()) {
    if (arr.length > maxRowNodes) maxRowNodes = arr.length;
  }

  const svgW = Math.max(800, maxRowNodes * (NODE_W + H_GAP) - H_GAP + PADDING * 2);
  const svgH = levels.length * (NODE_H + V_GAP) - V_GAP + PADDING * 2;

  const placed: PlacedNode[] = [];
  for (let li = 0; li < levels.length; li++) {
    const level = levels[li]!;
    const row = byLevel.get(level)!;
    const rowWidth = row.length * (NODE_W + H_GAP) - H_GAP;
    const offsetX = (svgW - rowWidth) / 2;

    for (let ni = 0; ni < row.length; ni++) {
      placed.push({
        ...row[ni]!,
        px: offsetX + ni * (NODE_W + H_GAP) + NODE_W / 2,
        py: PADDING + li * (NODE_H + V_GAP) + NODE_H / 2,
      });
    }
  }

  return { placed, width: svgW, height: svgH };
}

// ─── Edge path ───────────────────────────────────────────────────────────────

function edgePath(src: PlacedNode, tgt: PlacedNode): string {
  const sx = src.px;
  const sy = src.py + NODE_H / 2;
  const ex = tgt.px;
  const ey = tgt.py - NODE_H / 2;
  const midY = (sy + ey) / 2;
  return `M ${sx} ${sy} C ${sx} ${midY}, ${ex} ${midY}, ${ex} ${ey}`;
}

// ─── Pre-computed edge label data ────────────────────────────────────────────

interface EdgeLabel {
  x: number;
  y: number;
  anchor: "start" | "middle" | "end";
  offsetX: number;
  text: string;
  pillW: number;
  pillX: number;
}

function computeEdgeLabel(src: PlacedNode, tgt: PlacedNode, formula: string): EdgeLabel {
  const labelX = (src.px + tgt.px) / 2;
  const labelY = (src.py + NODE_H / 2 + tgt.py - NODE_H / 2) / 2;
  const offsetX = src.px === tgt.px ? 14 : 0;
  const anchor = src.px === tgt.px ? "start" : "middle";
  const text = formula.length > 28 ? formula.slice(0, 26) + "..." : formula;
  const pillW = text.length * 7 + 12;
  const pillX = anchor === "middle" ? labelX - pillW / 2 + offsetX : labelX + offsetX - 4;
  return { x: labelX, y: labelY, anchor, offsetX, text, pillW, pillX };
}

// ─── Component ───────────────────────────────────────────────────────────────

interface Props {
  state: ComputationGraphState;
}

export default function ComputationGraph2D({ state }: Props) {
  const { graphData, highlightedNodes, handleNodeClick } = state;
  const containerRef = useRef<HTMLDivElement>(null);
  const transformRef = useRef<HTMLDivElement>(null);
  const [isDark, setIsDark] = useState(false);

  // Pan & zoom via refs — zero React re-renders during interaction
  const viewState = useRef({ zoom: 1, panX: 0, panY: 0 });
  const zoomLabelRef = useRef<HTMLSpanElement>(null);
  const isPanning = useRef(false);
  const panStart = useRef({ x: 0, y: 0, panX: 0, panY: 0 });
  const mousePosRef = useRef({ x: 0, y: 0 });
  const tooltipRef = useRef<HTMLDivElement>(null);
  const hoveredNodeRef = useRef<PositionedNode | null>(null);

  useEffect(() => {
    const mq = window.matchMedia("(prefers-color-scheme: dark)");
    setIsDark(mq.matches || document.documentElement.classList.contains("dark"));
    const handler = () => setIsDark(mq.matches || document.documentElement.classList.contains("dark"));
    mq.addEventListener("change", handler);
    return () => mq.removeEventListener("change", handler);
  }, []);

  const { placed, width, height } = useMemo(
    () => build2DLayout(graphData.nodes),
    [graphData.nodes],
  );

  const nodeMap = useMemo(() => {
    const m = new Map<string, PlacedNode>();
    for (const n of placed) m.set(n.id, n);
    return m;
  }, [placed]);

  // ── Apply CSS transform to the wrapper div (GPU-composited, no SVG repaint) ──
  const applyTransform = useCallback(() => {
    const el = transformRef.current;
    if (!el) return;
    const { zoom, panX, panY } = viewState.current;
    el.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`;
  }, []);

  const updateZoomLabel = useCallback((zoom: number) => {
    const el = zoomLabelRef.current;
    if (el) el.textContent = `${Math.round(zoom * 100)}%`;
  }, []);

  // Update tooltip content + position via DOM (no React re-render)
  const updateTooltip = useCallback((node: PositionedNode | null) => {
    const el = tooltipRef.current;
    if (!el) return;
    if (!node) {
      el.style.display = "none";
      return;
    }
    el.style.display = "block";
    el.style.top = `${mousePosRef.current.y + 16}px`;
    el.style.left = `${mousePosRef.current.x + 16}px`;
    const label = el.querySelector("[data-tt-label]");
    const dot = el.querySelector("[data-tt-dot]") as HTMLElement | null;
    const domain = el.querySelector("[data-tt-domain]");
    const value = el.querySelector("[data-tt-value]");
    const unit = el.querySelector("[data-tt-unit]");
    const desc = el.querySelector("[data-tt-desc]");
    const formula = el.querySelector("[data-tt-formula]");
    if (label) label.textContent = node.label;
    if (dot) dot.style.backgroundColor = node.color;
    if (domain) domain.textContent = DOMAIN_LABELS[node.domain];
    if (value) value.textContent = String(node.value);
    if (unit) unit.textContent = node.unit;
    if (desc) desc.textContent = node.description;
    if (formula) {
      formula.textContent = node.formula ?? "";
      (formula as HTMLElement).style.display = node.formula ? "block" : "none";
    }
  }, []);

  // Fit to view
  const fitToView = useCallback(() => {
    const el = containerRef.current;
    if (!el || placed.length === 0) return;
    const cw = el.clientWidth;
    const ch = el.clientHeight;
    const scaleX = cw / width;
    const scaleY = ch / height;
    const newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, Math.min(scaleX, scaleY) * 0.92));
    viewState.current = {
      zoom: newZoom,
      panX: (cw - width * newZoom) / 2,
      panY: (ch - height * newZoom) / 2,
    };
    applyTransform();
    updateZoomLabel(newZoom);
  }, [width, height, placed.length, applyTransform, updateZoomLabel]);

  useEffect(() => {
    if (placed.length > 0) fitToView();
  }, [placed.length, fitToView]);

  // Focus on a node
  const focusNode = useCallback((node: PlacedNode) => {
    const el = containerRef.current;
    if (!el) return;
    const cw = el.clientWidth;
    const ch = el.clientHeight;
    const targetZoom = 1.2;
    viewState.current = {
      zoom: targetZoom,
      panX: cw / 2 - node.px * targetZoom,
      panY: ch / 2 - node.py * targetZoom,
    };
    applyTransform();
    updateZoomLabel(targetZoom);
  }, [applyTransform, updateZoomLabel]);

  // Wheel zoom — native event, direct DOM mutation
  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    const handler = (e: WheelEvent) => {
      e.preventDefault();
      const rect = el.getBoundingClientRect();
      const cx = e.clientX - rect.left;
      const cy = e.clientY - rect.top;
      const vs = viewState.current;
      const direction = e.deltaY < 0 ? 1 : -1;
      const newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, vs.zoom * (1 + direction * ZOOM_STEP)));
      const scale = newZoom / vs.zoom;
      vs.panX = cx - scale * (cx - vs.panX);
      vs.panY = cy - scale * (cy - vs.panY);
      vs.zoom = newZoom;
      applyTransform();
      updateZoomLabel(newZoom);
    };
    el.addEventListener("wheel", handler, { passive: false });
    return () => el.removeEventListener("wheel", handler);
  }, [applyTransform, updateZoomLabel]);

  // Pointer handlers — native events, direct DOM mutation, no React state
  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;

    const onDown = (e: PointerEvent) => {
      const tag = (e.target as Element).tagName;
      const isBg = tag === "svg" || tag === "DIV" || !!(e.target as Element).closest?.("[data-bg]");
      if (e.button === 1 || isBg) {
        isPanning.current = true;
        const vs = viewState.current;
        panStart.current = { x: e.clientX, y: e.clientY, panX: vs.panX, panY: vs.panY };
        (e.target as Element).setPointerCapture?.(e.pointerId);
        e.preventDefault();
        el.style.cursor = "grabbing";
      }
    };

    const onMove = (e: PointerEvent) => {
      mousePosRef.current.x = e.clientX;
      mousePosRef.current.y = e.clientY;
      if (hoveredNodeRef.current) {
        const tip = tooltipRef.current;
        if (tip) {
          tip.style.top = `${e.clientY + 16}px`;
          tip.style.left = `${e.clientX + 16}px`;
        }
      }
      if (!isPanning.current) return;
      viewState.current.panX = panStart.current.panX + (e.clientX - panStart.current.x);
      viewState.current.panY = panStart.current.panY + (e.clientY - panStart.current.y);
      applyTransform();
    };

    const onUp = () => {
      isPanning.current = false;
      el.style.cursor = "grab";
    };

    el.addEventListener("pointerdown", onDown);
    el.addEventListener("pointermove", onMove);
    el.addEventListener("pointerup", onUp);
    el.addEventListener("pointerleave", onUp);
    return () => {
      el.removeEventListener("pointerdown", onDown);
      el.removeEventListener("pointermove", onMove);
      el.removeEventListener("pointerup", onUp);
      el.removeEventListener("pointerleave", onUp);
    };
  }, [applyTransform, updateTooltip]);

  // Zoom controls
  const zoomBy = useCallback((direction: 1 | -1) => {
    const el = containerRef.current;
    if (!el) return;
    const cw = el.clientWidth / 2;
    const ch = el.clientHeight / 2;
    const vs = viewState.current;
    const next = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, vs.zoom * (1 + direction * ZOOM_STEP)));
    const scale = next / vs.zoom;
    vs.panX = cw - scale * (cw - vs.panX);
    vs.panY = ch - scale * (ch - vs.panY);
    vs.zoom = next;
    applyTransform();
    updateZoomLabel(next);
  }, [applyTransform, updateZoomLabel]);

  if (graphData.nodes.length === 0) {
    return (
      <div className="flex h-full items-center justify-center text-zinc-500">
        {state.viewMode === "resource" ? "Select a resource and month" : "Select a project"}
      </div>
    );
  }

  const cardBg = isDark ? "#1e293b" : "#ffffff";
  const cardTextPrimary = isDark ? "#f1f5f9" : "#1e293b";
  const cardTextSecondary = isDark ? "#94a3b8" : "#64748b";
  const canvasBg = isDark ? "#0f172a" : "#f8fafc";
  const gridLine = isDark ? "#1e293b" : "#e2e8f0";
  const pillBg = isDark ? "#1e293b" : "#ffffff";
  const pillStroke = isDark ? "#334155" : "#e2e8f0";
  const pillText = isDark ? "#94a3b8" : "#475569";

  return (
    <div
      ref={containerRef}
      className="relative h-full w-full overflow-hidden"
      style={{ backgroundColor: canvasBg, cursor: "grab" }}
    >
      {/* Wrapper div for CSS transform — GPU-composited, no SVG repaint */}
      <div
        ref={transformRef}
        style={{
          position: "absolute",
          width: `${width}px`,
          height: `${height}px`,
          transformOrigin: "0 0",
          willChange: "transform",
        }}
      >
        <svg
          width={width}
          height={height}
          style={{ position: "absolute", top: 0, left: 0 }}
        >
          <defs>
            <marker id="arrow-default" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
              <path d="M 0 0 L 10 3.5 L 0 7 Z" fill={isDark ? "#475569" : "#94a3b8"} />
            </marker>
            <marker id="arrow-highlight" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
              <path d="M 0 0 L 10 3.5 L 0 7 Z" fill="#3b82f6" />
            </marker>
          </defs>

          {/* Grid lines */}
          {[...new Set(placed.map((n) => n.py))].map((y) => (
            <line
              key={`grid-${y}`}
              x1="0" y1={y} x2={width} y2={y}
              stroke={gridLine} strokeWidth="1" strokeDasharray="4 4" opacity="0.4"
            />
          ))}

          {/* ── Links ── */}
          {graphData.links.map((link) => {
            const sourceId = typeof link.source === "object" ? (link.source as { id: string }).id : link.source;
            const targetId = typeof link.target === "object" ? (link.target as { id: string }).id : link.target;
            const src = nodeMap.get(sourceId);
            const tgt = nodeMap.get(targetId);
            if (!src || !tgt) return null;

            const isHighlighted = highlightedNodes
              ? highlightedNodes.has(sourceId) && highlightedNodes.has(targetId)
              : false;
            const isNeutral = !highlightedNodes;
            const isDimmed = highlightedNodes && !isHighlighted;
            const showLabel = (isNeutral || isHighlighted) && link.formula;

            return (
              <g key={`${sourceId}-${targetId}`}>
                <path
                  d={edgePath(src, tgt)}
                  fill="none"
                  stroke={isDimmed ? (isDark ? "#334155" : "#d1d5db") : src.color}
                  strokeWidth={isDimmed ? 1 : isHighlighted ? (link.weight ?? 1) * 2.5 : (link.weight ?? 1) * 1.5}
                  strokeOpacity={isDimmed ? 0.2 : isHighlighted ? 0.8 : 0.4}
                  markerEnd={isDimmed ? undefined : (isHighlighted ? "url(#arrow-highlight)" : "url(#arrow-default)")}
                />
                {showLabel && (() => {
                  const lbl = computeEdgeLabel(src, tgt, link.formula);
                  return (
                    <>
                      <rect
                        x={lbl.pillX} y={lbl.y - 10}
                        width={lbl.pillW} height={20} rx="4"
                        fill={pillBg} stroke={pillStroke} strokeWidth="1" opacity="0.95"
                      />
                      <text
                        x={lbl.x + lbl.offsetX} y={lbl.y + 1}
                        fontSize="12" fill={pillText}
                        textAnchor={lbl.anchor} dominantBaseline="central"
                        className="pointer-events-none select-none"
                        fontFamily="ui-monospace, monospace"
                      >
                        {lbl.text}
                      </text>
                    </>
                  );
                })()}
              </g>
            );
          })}

          {/* ── Nodes ── */}
          {placed.map((node) => {
            const isActive = !highlightedNodes || highlightedNodes.has(node.id);
            const isDimmed = highlightedNodes && !isActive;
            const color = DOMAIN_COLORS[node.domain as Domain] ?? "#6b7280";

            const valueStr = typeof node.value === "number"
              ? node.value.toFixed(1)
              : String(node.value);
            const displayValue = valueStr.length > 16 ? valueStr.slice(0, 14) + "..." : valueStr;
            const displayLabel = node.label.length > 22 ? node.label.slice(0, 20) + "..." : node.label;

            return (
              <g
                key={node.id}
                transform={`translate(${node.px - NODE_W / 2}, ${node.py - NODE_H / 2})`}
                onClick={(e) => {
                  e.stopPropagation();
                  if (e.detail === 2) {
                    focusNode(node);
                  } else {
                    handleNodeClick(node.id);
                  }
                }}
                onMouseEnter={() => { hoveredNodeRef.current = node; updateTooltip(node); }}
                onMouseLeave={() => { hoveredNodeRef.current = null; updateTooltip(null); }}
                className="cursor-pointer"
                opacity={isDimmed ? 0.2 : 1}
              >
                <rect
                  width={NODE_W} height={NODE_H} rx="10"
                  fill={cardBg}
                  stroke={isDark ? "#334155" : "#e2e8f0"}
                  strokeWidth="1"
                />
                <path
                  d={`M 10 0 H ${NODE_W - 10} Q ${NODE_W} 0 ${NODE_W} 10 V 4 H 0 V 10 Q 0 0 10 0 Z`}
                  fill={color}
                />
                <text x="12" y="22" fontSize="11" fontWeight="500" fill={cardTextSecondary} fontFamily="system-ui, sans-serif">
                  {DOMAIN_LABELS[node.domain as Domain]}
                </text>
                <text x="12" y="44" fontSize="14" fontWeight="600" fill={cardTextPrimary} fontFamily="system-ui, sans-serif">
                  {displayLabel}
                </text>
                <text x="12" y="68" fontSize="18" fontWeight="700" fill={color} fontFamily="ui-monospace, monospace">
                  {displayValue}
                </text>
                <text x={NODE_W - 12} y="68" fontSize="12" fontWeight="400" fill={cardTextSecondary} textAnchor="end" fontFamily="system-ui, sans-serif">
                  {node.unit}
                </text>
              </g>
            );
          })}
        </svg>
      </div>

      {/* Background click target for pan (covers viewport) */}
      <div data-bg="true" className="absolute inset-0" style={{ zIndex: -1 }} />

      {/* ── Zoom controls (bottom-right) ── */}
      <div className="absolute bottom-4 right-4 flex flex-col gap-1">
        <button
          onClick={() => zoomBy(1)}
          className="flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-300 bg-white text-sm font-bold text-zinc-700 shadow-sm hover:bg-zinc-100 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700"
          title="Zoom in"
        >
          +
        </button>
        <button
          onClick={() => zoomBy(-1)}
          className="flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-300 bg-white text-sm font-bold text-zinc-700 shadow-sm hover:bg-zinc-100 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700"
          title="Zoom out"
        >
          -
        </button>
        <button
          onClick={fitToView}
          className="flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-300 bg-white text-xs font-medium text-zinc-700 shadow-sm hover:bg-zinc-100 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700"
          title="Fit to view"
        >
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5">
            <rect x="1" y="1" width="12" height="12" rx="2" />
            <path d="M 4 7 L 1 7 M 10 7 L 13 7 M 7 4 L 7 1 M 7 10 L 7 13" />
          </svg>
        </button>
        <span ref={zoomLabelRef} className="mt-1 text-center text-[10px] text-zinc-400">
          100%
        </span>
      </div>

      {/* Hover Tooltip — always rendered, shown/hidden via DOM */}
      <div
        ref={tooltipRef}
        className="pointer-events-none fixed z-50 max-w-sm rounded-lg border border-zinc-300 bg-white px-4 py-3 text-sm text-zinc-800 shadow-xl dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-200"
        style={{ display: "none", top: 0, left: 0 }}
      >
        <div className="flex items-center gap-2">
          <span data-tt-dot className="inline-block h-2.5 w-2.5 rounded-full" />
          <span data-tt-label className="font-semibold" />
          <span data-tt-domain className="ml-auto text-xs text-zinc-500" />
        </div>
        <div className="mt-1 text-lg font-bold">
          <span data-tt-value /> <span data-tt-unit className="text-sm font-normal text-zinc-400" />
        </div>
        <div data-tt-desc className="mt-1 text-xs text-zinc-500 dark:text-zinc-400" />
        <div data-tt-formula className="mt-1 font-mono text-xs text-blue-500" />
      </div>
    </div>
  );
}
