import { useMemo, useState, type DragEvent } from "react";
import {
  addEdge,
  applyEdgeChanges,
  applyNodeChanges,
  Background,
  BackgroundVariant,
  ConnectionLineType,
  Controls,
  MiniMap,
  ReactFlow,
  type Connection,
  type Edge,
  type ReactFlowInstance
} from "@xyflow/react";
import { definitionConnectionError } from "@govoplan/core-webui/definition-graph";
import type {
  WorkflowDiagnostic,
  WorkflowGraph,
  WorkflowGraphNode,
  WorkflowNodeType
} from "../../api/workflow";
import { newWorkflowNode } from "./model";
import WorkflowNode, { type WorkflowFlowNode } from "./WorkflowNode";

const nodeTypes = { workflow: WorkflowNode };

export default function WorkflowCanvas({
  graph,
  diagnostics,
  nodeLibrary,
  selectedNodeId,
  readOnly,
  allowsCycles,
  onGraphChange,
  onSelectNode
}: {
  graph: WorkflowGraph;
  diagnostics: WorkflowDiagnostic[];
  nodeLibrary: WorkflowNodeType[];
  selectedNodeId: string | null;
  readOnly: boolean;
  allowsCycles: boolean;
  onGraphChange: (graph: WorkflowGraph) => void;
  onSelectNode: (nodeId: string | null) => void;
}) {
  const [instance, setInstance] = useState<
    ReactFlowInstance<WorkflowFlowNode, Edge> | null
  >(null);
  const definitions = useMemo(
    () => new Map(nodeLibrary.map((item) => [item.type, item])),
    [nodeLibrary]
  );
  const errorNodeIds = useMemo(
    () => new Set(
      diagnostics
        .filter((item) => item.severity === "error" && item.node_id)
        .map((item) => item.node_id as string)
    ),
    [diagnostics]
  );
  const nodes = useMemo<WorkflowFlowNode[]>(
    () => graph.nodes.flatMap((node) => {
      const definition = definitions.get(node.type);
      if (!definition) return [];
      return [{
        id: node.id,
        type: "workflow" as const,
        position: node.position,
        initialWidth: 190,
        initialHeight: 56,
        selected: node.id === selectedNodeId,
        data: {
          label: node.label,
          workflowType: node.type,
          definition,
          hasError: errorNodeIds.has(node.id)
        }
      }];
    }),
    [definitions, errorNodeIds, graph.nodes, selectedNodeId]
  );
  const edges = useMemo<Edge[]>(
    () => graph.edges.map((edge) => ({
      id: edge.id,
      source: edge.source,
      target: edge.target,
      sourceHandle: edge.source_port ?? "output",
      targetHandle: edge.target_port ?? "input",
      type: "smoothstep",
      className: "workflow-edge"
    })),
    [graph.edges]
  );

  const updateNodes = (nextNodes: WorkflowFlowNode[]) => {
    const ids = new Set(nextNodes.map((node) => node.id));
    onGraphChange({
      ...graph,
      nodes: nextNodes.map((flowNode) => {
        const current = graph.nodes.find((node) => node.id === flowNode.id);
        if (!current) throw new Error(`Unknown Workflow node: ${flowNode.id}`);
        return { ...current, position: flowNode.position };
      }),
      edges: graph.edges.filter(
        (edge) => ids.has(edge.source) && ids.has(edge.target)
      )
    });
  };

  const updateEdges = (nextEdges: Edge[]) => {
    onGraphChange({
      ...graph,
      edges: nextEdges.map((edge) => ({
        id: edge.id,
        source: edge.source,
        target: edge.target,
        source_port: edge.sourceHandle ?? "output",
        target_port: edge.targetHandle ?? "input"
      }))
    });
  };

  const isValidConnection = (connection: Connection | Edge): boolean => {
    if (readOnly || !connection.source || !connection.target) return false;
    return definitionConnectionError(
      graph,
      nodeLibrary,
      {
        source: connection.source,
        target: connection.target,
        sourcePort: connection.sourceHandle,
        targetPort: connection.targetHandle
      },
      { allowCycles: allowsCycles }
    ) === null;
  };

  const onDrop = (event: DragEvent<HTMLDivElement>) => {
    event.preventDefault();
    if (readOnly || !instance) return;
    const type = event.dataTransfer.getData(
      "application/x-govoplan-workflow-node"
    );
    if (!type) return;
    const node = newWorkflowNode(
      type,
      instance.screenToFlowPosition({ x: event.clientX, y: event.clientY }),
      nodeLibrary
    );
    onGraphChange({ ...graph, nodes: [...graph.nodes, node] });
    onSelectNode(node.id);
  };

  return (
    <div
      className="workflow-canvas"
      onDragOver={(event) => {
        if (
          event.dataTransfer.types.includes(
            "application/x-govoplan-workflow-node"
          )
        ) {
          event.preventDefault();
          event.dataTransfer.dropEffect = "copy";
        }
      }}
      onDrop={onDrop}
    >
      <ReactFlow<WorkflowFlowNode, Edge>
        nodes={nodes}
        edges={edges}
        nodeTypes={nodeTypes}
        onInit={setInstance}
        onNodesChange={(changes) => {
          if (readOnly) return;
          const graphChanges = changes.filter(
            (change) => change.type !== "dimensions"
          );
          if (graphChanges.length) {
            updateNodes(applyNodeChanges(graphChanges, nodes));
          }
        }}
        onEdgesChange={(changes) => {
          if (!readOnly) updateEdges(applyEdgeChanges(changes, edges));
        }}
        onConnect={(connection) => {
          if (!isValidConnection(connection)) return;
          updateEdges(addEdge({
            ...connection,
            id: `edge-${crypto.randomUUID()}`,
            type: "smoothstep"
          }, edges));
        }}
        isValidConnection={isValidConnection}
        onNodeClick={(_event, node) => onSelectNode(node.id)}
        onPaneClick={() => onSelectNode(null)}
        nodesDraggable={!readOnly}
        nodesConnectable={!readOnly}
        deleteKeyCode={readOnly ? null : ["Backspace", "Delete"]}
        connectionLineType={ConnectionLineType.SmoothStep}
        connectionLineStyle={{ stroke: "var(--accent)", strokeWidth: 3 }}
        connectionRadius={32}
        fitView
        fitViewOptions={{ padding: 0.22, maxZoom: 1.25 }}
        minZoom={0.25}
        maxZoom={1.8}
      >
        <Background variant={BackgroundVariant.Dots} gap={20} size={1.3} />
        <MiniMap
          pannable
          zoomable
          nodeStrokeWidth={2}
          nodeColor={(node) =>
            node.data.hasError ? "var(--danger)" : "var(--accent)"
          }
        />
        <Controls showInteractive={false} />
      </ReactFlow>
      {!graph.nodes.length ? (
        <div className="workflow-canvas-empty">Drop a start node here</div>
      ) : null}
    </div>
  );
}

export function updateWorkflowGraphNode(
  graph: WorkflowGraph,
  updatedNode: WorkflowGraphNode
): WorkflowGraph {
  return {
    ...graph,
    nodes: graph.nodes.map((node) =>
      node.id === updatedNode.id ? updatedNode : node
    )
  };
}
