import type { ElkNode, ElkExtendedEdge } from "elkjs";
import type { Node, Edge } from "@xyflow/react";

import type { DiagramNode, GraphData } from "../types/graph";
import { getBpmnNodeSize, bareBpmnType, hasExternalLabel } from "../types/bpmn";
import type { ElkLayoutOptions } from "./elk-layout";

// ── BPMN ELK Node Builder ──────────────────────────────────────────────────

interface ElkNodeWithEdges extends ElkNode {
  edges?: ElkExtendedEdge[];
  labels?: Array<{
    text: string;
    width: number;
    height: number;
    layoutOptions?: Record<string, string>;
  }>;
}

function buildBpmnElkNode(
  node: DiagramNode,
): ElkNodeWithEdges {
  const size = getBpmnNodeSize(node.type);
  const elkNode: ElkNodeWithEdges = {
    id: node.id,
    width: node.w ?? size.w,
    height: size.h,
  };

  if (hasExternalLabel(node.type) && node.label) {
    const labelW = Math.min(node.label.length * 6.5 + 16, 160);
    elkNode.labels = [
      {
        text: node.label,
        width: labelW,
        height: size.labelH || 20,
        layoutOptions: {
          "elk.nodeLabels.placement": "OUTSIDE V_BOTTOM H_CENTER",
        },
      },
    ];
  }

  return elkNode;
}

// ── Edge Container Resolution ──────────────────────────────────────────────

function resolveEdgeContainer(
  fromId: string,
  toId: string,
  nodeToContainer: Map<string, { pool: string; lane: string }>,
): string {
  const src = nodeToContainer.get(fromId);
  const tgt = nodeToContainer.get(toId);

  if (!src || !tgt) return "root";
  if (src.pool === tgt.pool && src.lane === tgt.lane) return src.lane;
  if (src.pool === tgt.pool) return src.pool;
  return "root";
}

// ── Build Compound ELK Graph ───────────────────────────────────────────────

export function buildBpmnElkGraph(
  graphData: GraphData,
  options: Partial<ElkLayoutOptions> = {},
): ElkNodeWithEdges {
  const direction = options.direction ?? graphData.meta?.layoutDirection ?? "RIGHT";
  const edgeRouting = options.edgeRouting ?? graphData.meta?.edgeRouting ?? "ORTHOGONAL";
  const nodeSpacing = options.nodeSpacing ?? 100;
  const layerSpacing = options.layerSpacing ?? 250;

  // Map nodes to their lane
  const nodesByLane = new Map<string, DiagramNode[]>();
  const freeNodes: DiagramNode[] = [];

  for (const n of graphData.nodes) {
    if (n.lane) {
      const existing = nodesByLane.get(n.lane) ?? [];
      existing.push(n);
      nodesByLane.set(n.lane, existing);
    } else {
      freeNodes.push(n);
    }
  }

  const elkChildren: ElkNodeWithEdges[] = [];

  // Build pool > lane > node hierarchy
  if (graphData.pools) {
    for (const pool of graphData.pools) {
      const laneChildren: ElkNodeWithEdges[] = [];

      for (const lane of pool.lanes) {
        const laneNodes = (nodesByLane.get(lane.id) ?? []).map((n) =>
          buildBpmnElkNode(n),
        );

        laneChildren.push({
          id: lane.id,
          width: 0,
          height: 0,
          layoutOptions: {
            "elk.padding": "[top=55,left=80,bottom=45,right=40]",
          },
          children: laneNodes.length
            ? laneNodes
            : [{ id: `${lane.id}_placeholder`, width: 1, height: 1 }],
        });
      }

      elkChildren.push({
        id: pool.id,
        width: 0,
        height: 0,
        layoutOptions: {
          "elk.padding": "[top=30,left=40,bottom=30,right=30]",
        },
        children: laneChildren,
      });
    }
  }

  // Free-floating nodes (no lane assignment)
  for (const n of freeNodes) {
    elkChildren.push(buildBpmnElkNode(n));
  }

  // Build node-to-container map for edge resolution
  const nodeToContainer = new Map<string, { pool: string; lane: string }>();
  if (graphData.pools) {
    for (const pool of graphData.pools) {
      for (const lane of pool.lanes) {
        for (const n of nodesByLane.get(lane.id) ?? []) {
          nodeToContainer.set(n.id, { pool: pool.id, lane: lane.id });
        }
      }
    }
  }

  // Build all edges with container assignment
  const allEdges: (ElkExtendedEdge & { container: string })[] = [];
  for (let i = 0; i < graphData.edges.length; i++) {
    const e = graphData.edges[i];
    const container = resolveEdgeContainer(e.from, e.to, nodeToContainer);

    const elkEdge: ElkExtendedEdge & { container: string } = {
      id: `e${i}`,
      sources: [e.from],
      targets: [e.to],
      container,
    };

    if (e.label) {
      (elkEdge as ElkNodeWithEdges).labels = [
        {
          text: e.label,
          width: e.label.length * 7 + 12,
          height: 18,
          layoutOptions: {
            "elk.edgeLabels.placement": "CENTER",
          },
        },
      ];
    }

    allEdges.push(elkEdge);
  }

  // Distribute edges to their containers
  function attachEdges(node: ElkNodeWithEdges) {
    const myEdges = allEdges.filter((e) => e.container === node.id);
    if (myEdges.length) {
      node.edges = myEdges;
    }
    if (node.children) {
      for (const child of node.children as ElkNodeWithEdges[]) {
        attachEdges(child);
      }
    }
  }

  const rootGraph: ElkNodeWithEdges = {
    id: "root",
    width: 0,
    height: 0,
    layoutOptions: {
      "elk.algorithm": "layered",
      "elk.direction": direction,
      "elk.edgeRouting": edgeRouting,
      "elk.hierarchyHandling": "INCLUDE_CHILDREN",
      "elk.spacing.nodeNode": String(nodeSpacing),
      "elk.layered.spacing.nodeNodeBetweenLayers": String(layerSpacing),
      "elk.spacing.edgeNode": "70",
      "elk.spacing.edgeEdge": "30",
      "elk.spacing.componentComponent": "80",
      "elk.spacing.portPort": "20",
      "elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
      "elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
      "elk.layered.spacing.edgeNodeBetweenLayers": "70",
      "elk.layered.spacing.edgeEdgeBetweenLayers": "30",
      "elk.nodeLabels.placement": "OUTSIDE V_BOTTOM H_CENTER",
      "elk.edgeLabels.placement": "CENTER",
      "elk.spacing.labelLabel": "8",
      "elk.spacing.edgeLabelSpacing": "6",
      "elk.spacing.labelNode": "10",
      "elk.spacing.labelPort": "6",
      "elk.padding": "[top=30,left=30,bottom=30,right=30]",
    },
    children: elkChildren,
  };

  // Attach root-level edges
  const rootEdges = allEdges.filter((e) => e.container === "root");
  if (rootEdges.length) {
    rootGraph.edges = rootEdges;
  }
  attachEdges(rootGraph);

  return rootGraph;
}

// ── Recursive Position Resolution ──────────────────────────────────────────

export interface BpmnPosition {
  x: number;
  y: number;
  w: number;
  h: number;
}

export function resolveBpmnPositions(
  node: ElkNode,
  offsetX = 0,
  offsetY = 0,
): Map<string, BpmnPosition> {
  const positions = new Map<string, BpmnPosition>();
  const ax = offsetX + (node.x ?? 0);
  const ay = offsetY + (node.y ?? 0);

  positions.set(node.id, {
    x: ax,
    y: ay,
    w: node.width ?? 0,
    h: node.height ?? 0,
  });

  for (const child of node.children ?? []) {
    const childPositions = resolveBpmnPositions(child, ax, ay);
    for (const [k, v] of childPositions) {
      positions.set(k, v);
    }
  }

  return positions;
}

// ── Convert ELK edge section to SVG path ───────────────────────────────────

interface ElkPoint {
  x: number;
  y: number;
}

interface ElkSection {
  startPoint: ElkPoint;
  endPoint: ElkPoint;
  bendPoints?: ElkPoint[];
}

function elkSectionToPath(section: ElkSection, routing: string): string {
  const pts: ElkPoint[] = [
    section.startPoint,
    ...(section.bendPoints ?? []),
    section.endPoint,
  ];

  if (routing !== "SPLINES") {
    return `M ${pts.map((p) => `${p.x} ${p.y}`).join(" L ")}`;
  }

  let d = `M ${pts[0].x} ${pts[0].y}`;
  let i = 1;
  while (i < pts.length) {
    const rem = pts.length - i;
    if (rem >= 3) {
      d += ` C ${pts[i].x} ${pts[i].y}, ${pts[i + 1].x} ${pts[i + 1].y}, ${pts[i + 2].x} ${pts[i + 2].y}`;
      i += 3;
    } else if (rem === 2) {
      d += ` Q ${pts[i].x} ${pts[i].y}, ${pts[i + 1].x} ${pts[i + 1].y}`;
      i += 2;
    } else {
      d += ` L ${pts[i].x} ${pts[i].y}`;
      i += 1;
    }
  }
  return d;
}

// ── Resolve Edge Paths ─────────────────────────────────────────────────────

export interface ResolvedEdge {
  id: string;
  d: string;
  midX: number;
  midY: number;
}

export function resolveBpmnEdges(
  node: ElkNode & { edges?: Array<ElkExtendedEdge & { sections?: ElkSection[] }> },
  positions: Map<string, BpmnPosition>,
  routing: string,
): ResolvedEdge[] {
  const edges: ResolvedEdge[] = [];
  const nodePos = positions.get(node.id);
  const ox = nodePos?.x ?? 0;
  const oy = nodePos?.y ?? 0;

  if (node.edges) {
    for (const e of node.edges) {
      const sections = (e as unknown as { sections?: ElkSection[] }).sections;
      if (!sections?.length) continue;

      const pathParts = sections.map((sec) => {
        const shifted: ElkSection = {
          startPoint: { x: sec.startPoint.x + ox, y: sec.startPoint.y + oy },
          endPoint: { x: sec.endPoint.x + ox, y: sec.endPoint.y + oy },
          bendPoints: (sec.bendPoints ?? []).map((p) => ({
            x: p.x + ox,
            y: p.y + oy,
          })),
        };
        return elkSectionToPath(shifted, routing);
      });

      const allPts: ElkPoint[] = [];
      for (const sec of sections) {
        allPts.push({ x: sec.startPoint.x + ox, y: sec.startPoint.y + oy });
        for (const bp of sec.bendPoints ?? []) {
          allPts.push({ x: bp.x + ox, y: bp.y + oy });
        }
        allPts.push({ x: sec.endPoint.x + ox, y: sec.endPoint.y + oy });
      }
      const mid = allPts[Math.floor(allPts.length / 2)];

      // Use ELK-computed label position if available
      let labelX = mid.x;
      let labelY = mid.y;
      const labels = (e as unknown as { labels?: Array<{ x?: number; y?: number; width?: number; height?: number }> }).labels;
      if (labels?.length && labels[0].x != null) {
        const lbl = labels[0];
        labelX = (lbl.x ?? 0) + ox + (lbl.width ?? 0) / 2;
        labelY = (lbl.y ?? 0) + oy + (lbl.height ?? 0) / 2;
      }

      edges.push({
        id: e.id,
        d: pathParts.join(" "),
        midX: labelX,
        midY: labelY,
      });
    }
  }

  if (node.children) {
    for (const child of node.children) {
      edges.push(
        ...resolveBpmnEdges(
          child as ElkNode & { edges?: Array<ElkExtendedEdge & { sections?: ElkSection[] }> },
          positions,
          routing,
        ),
      );
    }
  }

  return edges;
}

// ── Apply BPMN Layout to @xyflow/react Nodes ──────────────────────────────

export function applyBpmnPositions(
  positions: Map<string, BpmnPosition>,
  originalNodes: Node[],
): Node[] {
  return originalNodes.map((node) => {
    const data = node.data as unknown as DiagramNode;
    if (data.manuallyPositioned) return node;

    const pos = positions.get(node.id);
    if (!pos) return node;

    // For pool/lane group nodes, also set the computed width/height
    if (node.type === "bpmnPool" || node.type === "bpmnLane") {
      return {
        ...node,
        position: { x: pos.x, y: pos.y },
        style: { ...node.style, width: pos.w, height: pos.h },
      };
    }

    return {
      ...node,
      position: { x: pos.x, y: pos.y },
    };
  });
}
