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

import type { DiagramNode, DiagramEdge, GraphData } from "../types/graph";
import type { ElkWorkerResponse } from "./elk-worker";
import {
  buildBpmnElkGraph,
  resolveBpmnPositions,
  applyBpmnPositions,
} from "./bpmn-layout";
import { getErEntityHeight } from "../types/er/constants";
import { OC_SIZES } from "../types/orgchart/constants";
import { getArchNodeSize } from "../types/architecture/constants";
import { getSeqNodeSize } from "../types/sequence/constants";
import { getFlowNodeSize } from "../types/flowchart/constants";
import { computeSequenceLayout } from "./sequence-layout";

// ── Layout Options ──────────────────────────────────────────────────────────

export type LayoutDirection = "DOWN" | "RIGHT" | "LEFT" | "UP";
export type EdgeRouting = "ORTHOGONAL" | "SPLINES" | "POLYLINE";

export interface ElkLayoutOptions {
  direction: LayoutDirection;
  edgeRouting: EdgeRouting;
  nodeSpacing?: number;
  layerSpacing?: number;
}

const DEFAULT_OPTIONS: ElkLayoutOptions = {
  direction: "DOWN",
  edgeRouting: "ORTHOGONAL",
  nodeSpacing: 80,
  layerSpacing: 100,
};

const DEFAULT_NODE_WIDTH = 150;
const DEFAULT_NODE_HEIGHT = 50;
export const SOFT_CAP_NODE_COUNT = 200;

// ── Node Dimension Resolution ─────────────────────────────────────────────

function getNodeDimensions(node: Node): { w: number; h: number } {
  const data = node.data as unknown as DiagramNode;

  // E-R entities: height computed from column count
  if (node.type === "erEntity" && data.columns) {
    return { w: data.w ?? node.measured?.width ?? DEFAULT_NODE_WIDTH, h: getErEntityHeight(data.columns) };
  }

  // Org chart persons: fixed dimensions
  if (node.type === "orgchartPerson") {
    return { w: data.w ?? OC_SIZES.person.w, h: OC_SIZES.person.h };
  }

  // Architecture nodes: per-subtype dimensions
  const archSize = getArchNodeSize(node.type);
  if (archSize) {
    return { w: data.w ?? archSize.w, h: archSize.h };
  }

  // Sequence nodes: per-subtype dimensions
  const seqSize = getSeqNodeSize(node.type);
  if (seqSize) {
    return { w: data.w ?? seqSize.w, h: seqSize.h };
  }

  // Flowchart nodes: per-subtype dimensions
  const flowSize = getFlowNodeSize(node.type);
  if (flowSize) {
    return { w: data.w ?? flowSize.w, h: flowSize.h };
  }

  // Default: measured or fallback
  return {
    w: data.w ?? node.measured?.width ?? DEFAULT_NODE_WIDTH,
    h: node.measured?.height ?? DEFAULT_NODE_HEIGHT,
  };
}

// ── ELK Graph Building ─────────────────────────────────────────────────────

export function buildElkGraph(
  nodes: Node[],
  edges: Edge[],
  options: Partial<ElkLayoutOptions> = {},
): ElkNode {
  const opts = { ...DEFAULT_OPTIONS, ...options };

  return {
    id: "root",
    layoutOptions: {
      "elk.algorithm": "layered",
      "elk.direction": opts.direction,
      "elk.edgeRouting": opts.edgeRouting,
      "elk.spacing.nodeNode": String(opts.nodeSpacing ?? 80),
      "elk.layered.spacing.nodeNodeBetweenLayers": String(
        opts.layerSpacing ?? 100,
      ),
      "elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
      "elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
    },
    children: nodes.map((node) => {
      const { w, h } = getNodeDimensions(node);
      return { id: node.id, width: w, height: h };
    }),
    edges: edges.map(
      (edge) =>
        ({
          id: edge.id,
          sources: [edge.source],
          targets: [edge.target],
        }) as ElkExtendedEdge,
    ),
  };
}

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

export function resolvePositions(
  elkGraph: ElkNode,
  originalNodes: Node[],
): Node[] {
  const positionMap = new Map<string, { x: number; y: number }>();
  for (const child of elkGraph.children ?? []) {
    if (child.x !== undefined && child.y !== undefined) {
      positionMap.set(child.id, { x: child.x, y: child.y });
    }
  }

  return originalNodes.map((node) => {
    const data = node.data as unknown as DiagramNode;
    // Skip nodes explicitly marked as manually positioned (set by user drag in Story 2.9)
    if (data.manuallyPositioned) return node;

    const elkPos = positionMap.get(node.id);
    if (!elkPos) return node;

    return {
      ...node,
      position: elkPos,
    };
  });
}

// ── Web Worker Management ───────────────────────────────────────────────────

let worker: Worker | null = null;
let pendingReject: ((reason: Error) => void) | null = null;

const LAYOUT_TIMEOUT_MS = 10_000;

function getWorker(): Worker {
  if (!worker) {
    worker = new Worker(new URL("./elk-worker.ts", import.meta.url), {
      type: "module",
    });
  }
  return worker;
}

export function terminateWorker(): void {
  if (pendingReject) {
    pendingReject(new Error("Layout cancelled"));
    pendingReject = null;
  }
  if (worker) {
    worker.terminate();
    worker = null;
  }
}

// ── BPMN Compound Layout Detection ──────────────────────────────────────────

/** Reconstruct GraphData from @xyflow nodes for compound BPMN layout. */
function flowToBpmnGraphData(
  nodes: Node[],
  edges: Edge[],
): GraphData {
  const poolNodes = nodes.filter((n) => n.type === "bpmnPool");
  const laneNodes = nodes.filter((n) => n.type === "bpmnLane");
  const contentNodes = nodes.filter(
    (n) =>
      n.type !== "bpmnPool" &&
      n.type !== "bpmnLane" &&
      n.type !== "bpmnGroup",
  );

  return {
    pools: poolNodes.map((pool) => ({
      id: pool.id,
      label: String((pool.data as Record<string, unknown>).label ?? ""),
      lanes: laneNodes
        .filter((lane) => lane.parentId === pool.id)
        .map((lane) => ({
          id: lane.id,
          label: String(
            (lane.data as Record<string, unknown>).label ?? "",
          ),
        })),
    })),
    nodes: contentNodes.map((n) => {
      const d = n.data as unknown as DiagramNode;
      return {
        ...d,
        id: n.id,
        lane: d.lane ?? n.parentId,
      } as DiagramNode;
    }),
    edges: edges.map((e) => ({
      id: e.id,
      from: e.source,
      to: e.target,
      label: typeof e.label === "string" ? e.label : undefined,
      type: (e.data as Record<string, unknown> | undefined)?.type as
        | string
        | undefined,
    })),
  };
}

// ── Main Layout Function ────────────────────────────────────────────────────

export interface LayoutResult {
  nodes: Node[];
  edges?: Edge[];
}

export function computeLayout(
  nodes: Node[],
  edges: Edge[],
  options: Partial<ElkLayoutOptions> = {},
): Promise<LayoutResult> {
  return new Promise((resolve, reject) => {
    if (nodes.length === 0) {
      resolve({ nodes });
      return;
    }

    // Cancel any in-flight layout (single-flight pattern)
    if (pendingReject) {
      pendingReject(new Error("Layout superseded"));
      pendingReject = null;
    }

    // Detect sequence diagram — uses custom synchronous layout, not ELK
    const isSequence = nodes.some((n) => n.type === "seqParticipant");
    if (isSequence) {
      const result = computeSequenceLayout(nodes, edges);
      resolve({ nodes: result.nodes, edges: result.edges });
      return;
    }

    // Detect BPMN compound layout (pools present in @xyflow nodes)
    const isCompound = nodes.some((n) => n.type === "bpmnPool");

    let elkGraph: ElkNode;
    if (isCompound) {
      const graphData = flowToBpmnGraphData(nodes, edges);
      elkGraph = buildBpmnElkGraph(graphData, options);
    } else {
      elkGraph = buildElkGraph(nodes, edges, options);
    }

    const w = getWorker();

    let settled = false;
    const timeoutId = setTimeout(() => {
      if (!settled) {
        settled = true;
        pendingReject = null;
        w.removeEventListener("message", handler);
        reject(new Error("ELK layout timed out"));
      }
    }, LAYOUT_TIMEOUT_MS);

    const handler = (event: MessageEvent<ElkWorkerResponse>) => {
      if (settled) return;
      settled = true;
      clearTimeout(timeoutId);
      pendingReject = null;
      w.removeEventListener("message", handler);

      if (event.data.type === "result" && event.data.graph) {
        if (isCompound) {
          const positions = resolveBpmnPositions(event.data.graph);
          resolve({ nodes: applyBpmnPositions(positions, nodes) });
        } else {
          resolve({ nodes: resolvePositions(event.data.graph, nodes) });
        }
      } else {
        reject(new Error(event.data.message ?? "ELK layout failed"));
      }
    };

    pendingReject = reject;
    w.addEventListener("message", handler);
    w.postMessage({ type: "layout", graph: elkGraph });
  });
}
