import { type Dispatch, type MouseEvent, type MutableRefObject, type SetStateAction, useEffect, useState } from "react";
import { canvasNodeClickMoveThreshold } from "./canvasConstants";
import type {
  CanvasAlignGuide,
  CanvasImageNode,
  CanvasImageNodeDrag,
  CanvasNodeKind,
  CanvasNodePackage,
  CanvasNodePackageDrag,
  CanvasNodeResizeDrag,
  CanvasNodeSize,
  CanvasPanDrag,
  CanvasPoint,
  CanvasSelectedNode,
  CanvasSelectionDrag,
  CanvasTextNode,
  CanvasTextNodeDrag,
  CanvasVideoNode,
  CanvasVideoNodeDrag,
  CanvasViewport,
} from "./canvasTypes";
import { clampCanvasNodeSize, moveCanvasNodesForPackageDrag, normalizeCanvasSelectionRect } from "./canvasUtils";

export interface CanvasNodeDragCallbacks {
  pushHistorySnapshot: () => void;
  clearCanvasSelection: () => void;
  selectCanvasNode: (kind: CanvasNodeKind, id: string, addToSelection?: boolean) => void;
  applyCanvasSelection: (nodes: CanvasSelectedNode[]) => void;
  getCanvasPointFromClient: (clientX: number, clientY: number) => CanvasPoint;
  getNodesInSelectionRect: (rect: { left: number; top: number; width: number; height: number }) => CanvasSelectedNode[];
  expandCanvasNodePackage: (pkg: CanvasNodePackage) => void;
  onBeforeResize?: () => void;
}

export interface UseCanvasNodeDragParams {
  zoomRef: MutableRefObject<number>;
  textNodesRef: MutableRefObject<CanvasTextNode[]>;
  imageNodesRef: MutableRefObject<CanvasImageNode[]>;
  videoNodesRef: MutableRefObject<CanvasVideoNode[]>;
  nodePackagesRef: MutableRefObject<CanvasNodePackage[]>;
  setTextNodes: Dispatch<SetStateAction<CanvasTextNode[]>>;
  setImageNodes: Dispatch<SetStateAction<CanvasImageNode[]>>;
  setVideoNodes: Dispatch<SetStateAction<CanvasVideoNode[]>>;
  setNodePackages: Dispatch<SetStateAction<CanvasNodePackage[]>>;
  setCanvasViewport: Dispatch<SetStateAction<CanvasViewport>>;
  callbacksRef: MutableRefObject<CanvasNodeDragCallbacks>;
  suppressNextPaneClickRef: MutableRefObject<boolean>;
}

export function useCanvasNodeDrag(params: UseCanvasNodeDragParams) {
  const {
    zoomRef,
    textNodesRef,
    imageNodesRef,
    videoNodesRef,
    nodePackagesRef,
    setTextNodes,
    setImageNodes,
    setVideoNodes,
    setNodePackages,
    setCanvasViewport,
    callbacksRef,
    suppressNextPaneClickRef,
  } = params;

  const [textNodeDrag, setTextNodeDrag] = useState<CanvasTextNodeDrag | null>(null);
  const [imageNodeDrag, setImageNodeDrag] = useState<CanvasImageNodeDrag | null>(null);
  const [videoNodeDrag, setVideoNodeDrag] = useState<CanvasVideoNodeDrag | null>(null);
  const [packageDrag, setPackageDrag] = useState<CanvasNodePackageDrag | null>(null);
  const [selectionDrag, setSelectionDrag] = useState<CanvasSelectionDrag | null>(null);
  const [nodeResizeDrag, setNodeResizeDrag] = useState<CanvasNodeResizeDrag | null>(null);
  const [canvasPanDrag, setCanvasPanDrag] = useState<CanvasPanDrag | null>(null);
  const [alignGuides, setAlignGuides] = useState<CanvasAlignGuide[]>([]);

  const didNodeDragStayClick = (event: globalThis.MouseEvent, drag: { startX: number; startY: number }) =>
    Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) <= canvasNodeClickMoveThreshold;

  const computeAlignGuides = (draggedId: string, pos: { x: number; y: number }, size: { width: number; height: number }): CanvasAlignGuide[] => {
    const threshold = 6;
    const guides: CanvasAlignGuide[] = [];
    const cx = pos.x + size.width / 2;
    const cy = pos.y + size.height / 2;
    const right = pos.x + size.width;
    const bottom = pos.y + size.height;
    const others: Array<{ pos: CanvasPoint; size: CanvasNodeSize }> = [];
    for (const node of textNodesRef.current) {
      if (node.id !== draggedId) others.push({ pos: node.position, size: node.size });
    }
    for (const node of imageNodesRef.current) {
      if (node.id !== draggedId) others.push({ pos: node.position, size: node.size });
    }
    for (const node of videoNodesRef.current) {
      if (node.id !== draggedId) others.push({ pos: node.position, size: node.size });
    }
    for (const other of others) {
      const ocx = other.pos.x + other.size.width / 2;
      const ocy = other.pos.y + other.size.height / 2;
      const oRight = other.pos.x + other.size.width;
      const oBottom = other.pos.y + other.size.height;
      if (Math.abs(cx - ocx) < threshold) guides.push({ axis: "x", position: ocx });
      if (Math.abs(pos.x - other.pos.x) < threshold) guides.push({ axis: "x", position: other.pos.x });
      if (Math.abs(right - oRight) < threshold) guides.push({ axis: "x", position: oRight });
      if (Math.abs(cy - ocy) < threshold) guides.push({ axis: "y", position: ocy });
      if (Math.abs(pos.y - other.pos.y) < threshold) guides.push({ axis: "y", position: other.pos.y });
      if (Math.abs(bottom - oBottom) < threshold) guides.push({ axis: "y", position: oBottom });
    }
    const seen = new Set<string>();
    return guides.filter((g) => {
      const key = `${g.axis}:${Math.round(g.position)}`;
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  };

  useEffect(() => {
    if (!textNodeDrag) return;
    const draggedNode = textNodesRef.current.find((n) => n.id === textNodeDrag.nodeId);
    const draggedSize = draggedNode?.size || { width: 320, height: 200 };
    const handleMove = (event: globalThis.MouseEvent) => {
      const hasMoved = !didNodeDragStayClick(event, textNodeDrag);
      if (hasMoved && !textNodeDrag.hasMoved) {
        callbacksRef.current.pushHistorySnapshot();
        callbacksRef.current.clearCanvasSelection();
        setTextNodeDrag((d) => d?.nodeId === textNodeDrag.nodeId ? { ...d, hasMoved: true } : d);
      }
      if (!hasMoved && !textNodeDrag.hasMoved) return;
      const newPos = {
        x: textNodeDrag.originX + (event.clientX - textNodeDrag.startX) / zoomRef.current,
        y: textNodeDrag.originY + (event.clientY - textNodeDrag.startY) / zoomRef.current,
      };
      setTextNodes((nodes) => nodes.map((n) => n.id === textNodeDrag.nodeId ? { ...n, position: newPos } : n));
      setAlignGuides(computeAlignGuides(textNodeDrag.nodeId, newPos, draggedSize));
    };
    const handleUp = (event: globalThis.MouseEvent) => {
      if (didNodeDragStayClick(event, textNodeDrag)) callbacksRef.current.selectCanvasNode("text", textNodeDrag.nodeId, event.shiftKey);
      setTextNodeDrag(null);
      setAlignGuides([]);
    };
    window.addEventListener("mousemove", handleMove);
    window.addEventListener("mouseup", handleUp);
    return () => { window.removeEventListener("mousemove", handleMove); window.removeEventListener("mouseup", handleUp); };
  }, [textNodeDrag]);

  useEffect(() => {
    if (!imageNodeDrag) return;
    const draggedNode = imageNodesRef.current.find((n) => n.id === imageNodeDrag.nodeId);
    const draggedSize = draggedNode?.size || { width: 320, height: 320 };
    const handleMove = (event: globalThis.MouseEvent) => {
      const hasMoved = !didNodeDragStayClick(event, imageNodeDrag);
      if (hasMoved && !imageNodeDrag.hasMoved) {
        callbacksRef.current.pushHistorySnapshot();
        callbacksRef.current.clearCanvasSelection();
        setImageNodeDrag((d) => d?.nodeId === imageNodeDrag.nodeId ? { ...d, hasMoved: true } : d);
      }
      if (!hasMoved && !imageNodeDrag.hasMoved) return;
      const newPos = {
        x: imageNodeDrag.originX + (event.clientX - imageNodeDrag.startX) / zoomRef.current,
        y: imageNodeDrag.originY + (event.clientY - imageNodeDrag.startY) / zoomRef.current,
      };
      setImageNodes((nodes) => nodes.map((n) => n.id === imageNodeDrag.nodeId ? { ...n, position: newPos } : n));
      setAlignGuides(computeAlignGuides(imageNodeDrag.nodeId, newPos, draggedSize));
    };
    const handleUp = (event: globalThis.MouseEvent) => {
      if (didNodeDragStayClick(event, imageNodeDrag)) callbacksRef.current.selectCanvasNode("image", imageNodeDrag.nodeId, event.shiftKey);
      setImageNodeDrag(null);
      setAlignGuides([]);
    };
    window.addEventListener("mousemove", handleMove);
    window.addEventListener("mouseup", handleUp);
    return () => { window.removeEventListener("mousemove", handleMove); window.removeEventListener("mouseup", handleUp); };
  }, [imageNodeDrag]);

  useEffect(() => {
    if (!videoNodeDrag) return;
    const draggedNode = videoNodesRef.current.find((n) => n.id === videoNodeDrag.nodeId);
    const draggedSize = draggedNode?.size || { width: 320, height: 240 };
    const handleMove = (event: globalThis.MouseEvent) => {
      const hasMoved = !didNodeDragStayClick(event, videoNodeDrag);
      if (hasMoved && !videoNodeDrag.hasMoved) {
        callbacksRef.current.pushHistorySnapshot();
        callbacksRef.current.clearCanvasSelection();
        setVideoNodeDrag((d) => d?.nodeId === videoNodeDrag.nodeId ? { ...d, hasMoved: true } : d);
      }
      if (!hasMoved && !videoNodeDrag.hasMoved) return;
      const newPos = {
        x: videoNodeDrag.originX + (event.clientX - videoNodeDrag.startX) / zoomRef.current,
        y: videoNodeDrag.originY + (event.clientY - videoNodeDrag.startY) / zoomRef.current,
      };
      setVideoNodes((nodes) => nodes.map((n) => n.id === videoNodeDrag.nodeId ? { ...n, position: newPos } : n));
      setAlignGuides(computeAlignGuides(videoNodeDrag.nodeId, newPos, draggedSize));
    };
    const handleUp = (event: globalThis.MouseEvent) => {
      if (didNodeDragStayClick(event, videoNodeDrag)) callbacksRef.current.selectCanvasNode("video", videoNodeDrag.nodeId, event.shiftKey);
      setVideoNodeDrag(null);
      setAlignGuides([]);
    };
    window.addEventListener("mousemove", handleMove);
    window.addEventListener("mouseup", handleUp);
    return () => { window.removeEventListener("mousemove", handleMove); window.removeEventListener("mouseup", handleUp); };
  }, [videoNodeDrag]);

  useEffect(() => {
    if (!packageDrag) return;
    const handleMove = (event: globalThis.MouseEvent) => {
      const deltaX = (event.clientX - packageDrag.startX) / zoomRef.current;
      const deltaY = (event.clientY - packageDrag.startY) / zoomRef.current;
      const hasMoved = Math.abs(event.clientX - packageDrag.startX) > canvasNodeClickMoveThreshold ||
        Math.abs(event.clientY - packageDrag.startY) > canvasNodeClickMoveThreshold;
      if (hasMoved && !packageDrag.hasMoved) {
        callbacksRef.current.pushHistorySnapshot();
        setPackageDrag((c) => c?.packageId === packageDrag.packageId ? { ...c, hasMoved: true } : c);
      }
      if (!hasMoved && !packageDrag.hasMoved) return;
      const delta = { x: deltaX, y: deltaY };
      if (packageDrag.collapsed && packageDrag.collapsedBounds) {
        setNodePackages((current) => current.map((pkg) =>
          pkg.id === packageDrag.packageId ? { ...pkg, collapsedBounds: { ...packageDrag.collapsedBounds!, left: packageDrag.collapsedBounds!.left + deltaX, top: packageDrag.collapsedBounds!.top + deltaY } } : pkg
        ));
      } else {
        setTextNodes((nodes) => moveCanvasNodesForPackageDrag(nodes, packageDrag.textOrigins, delta));
        setImageNodes((nodes) => moveCanvasNodesForPackageDrag(nodes, packageDrag.imageOrigins, delta));
        setVideoNodes((nodes) => moveCanvasNodesForPackageDrag(nodes, packageDrag.videoOrigins, delta));
      }
    };
    const handleUp = () => {
      if (!packageDrag.hasMoved && packageDrag.collapsed) {
        const pkg = nodePackagesRef.current.find((p) => p.id === packageDrag.packageId);
        if (pkg) callbacksRef.current.expandCanvasNodePackage(pkg);
      }
      setPackageDrag(null);
    };
    window.addEventListener("mousemove", handleMove);
    window.addEventListener("mouseup", handleUp);
    return () => { window.removeEventListener("mousemove", handleMove); window.removeEventListener("mouseup", handleUp); };
  }, [packageDrag]);

  useEffect(() => {
    if (!nodeResizeDrag) return;
    callbacksRef.current.pushHistorySnapshot();
    const handleMove = (event: globalThis.MouseEvent) => {
      const nextSize = clampCanvasNodeSize(
        nodeResizeDrag.kind,
        nodeResizeDrag.originWidth + (event.clientX - nodeResizeDrag.startX) / zoomRef.current,
        nodeResizeDrag.originHeight + (event.clientY - nodeResizeDrag.startY) / zoomRef.current
      );
      if (nodeResizeDrag.kind === "text") {
        setTextNodes((nodes) => nodes.map((n) => n.id === nodeResizeDrag.nodeId ? { ...n, size: nextSize } : n));
      } else if (nodeResizeDrag.kind === "image") {
        setImageNodes((nodes) => nodes.map((n) => n.id === nodeResizeDrag.nodeId ? { ...n, size: nextSize } : n));
      } else {
        setVideoNodes((nodes) => nodes.map((n) => n.id === nodeResizeDrag.nodeId ? { ...n, size: nextSize } : n));
      }
    };
    const handleUp = () => setNodeResizeDrag(null);
    window.addEventListener("mousemove", handleMove);
    window.addEventListener("mouseup", handleUp);
    return () => { window.removeEventListener("mousemove", handleMove); window.removeEventListener("mouseup", handleUp); };
  }, [nodeResizeDrag]);

  useEffect(() => {
    if (!selectionDrag) return;
    const handleMove = (event: globalThis.MouseEvent) => {
      const current = callbacksRef.current.getCanvasPointFromClient(event.clientX, event.clientY);
      setSelectionDrag((drag) => drag ? { ...drag, current, hasMoved: drag.hasMoved || Math.hypot(current.x - drag.start.x, current.y - drag.start.y) > canvasNodeClickMoveThreshold } : drag);
    };
    const handleUp = (event: globalThis.MouseEvent) => {
      const current = callbacksRef.current.getCanvasPointFromClient(event.clientX, event.clientY);
      const hasMoved = selectionDrag.hasMoved || Math.hypot(current.x - selectionDrag.start.x, current.y - selectionDrag.start.y) > canvasNodeClickMoveThreshold;
      if (hasMoved) {
        const selectionRect = normalizeCanvasSelectionRect(selectionDrag.start, current);
        callbacksRef.current.applyCanvasSelection(callbacksRef.current.getNodesInSelectionRect(selectionRect));
        suppressNextPaneClickRef.current = true;
      } else {
        callbacksRef.current.clearCanvasSelection();
      }
      setSelectionDrag(null);
    };
    window.addEventListener("mousemove", handleMove);
    window.addEventListener("mouseup", handleUp);
    return () => { window.removeEventListener("mousemove", handleMove); window.removeEventListener("mouseup", handleUp); };
  }, [selectionDrag]);

  useEffect(() => {
    if (!canvasPanDrag) return;
    const handleMove = (event: globalThis.MouseEvent) => {
      setCanvasViewport((vp) => ({ ...vp, x: canvasPanDrag.originX + event.clientX - canvasPanDrag.startX, y: canvasPanDrag.originY + event.clientY - canvasPanDrag.startY }));
    };
    const handleUp = () => setCanvasPanDrag(null);
    window.addEventListener("mousemove", handleMove);
    window.addEventListener("mouseup", handleUp);
    return () => { window.removeEventListener("mousemove", handleMove); window.removeEventListener("mouseup", handleUp); };
  }, [canvasPanDrag]);

  const handleNodeResizeStart = (
    event: MouseEvent<HTMLButtonElement>,
    kind: CanvasNodeKind,
    nodeId: string,
    size: CanvasNodeSize
  ) => {
    if (event.button !== 0) return;
    event.preventDefault();
    event.stopPropagation();
    callbacksRef.current.selectCanvasNode(kind, nodeId);
    setSelectionDrag(null);
    setTextNodeDrag(null);
    setImageNodeDrag(null);
    setVideoNodeDrag(null);
    callbacksRef.current.onBeforeResize?.();
    setNodeResizeDrag({
      kind,
      nodeId,
      startX: event.clientX,
      startY: event.clientY,
      originWidth: size.width,
      originHeight: size.height,
    });
  };

  return {
    textNodeDrag, setTextNodeDrag,
    imageNodeDrag, setImageNodeDrag,
    videoNodeDrag, setVideoNodeDrag,
    packageDrag, setPackageDrag,
    selectionDrag, setSelectionDrag,
    nodeResizeDrag, setNodeResizeDrag,
    canvasPanDrag, setCanvasPanDrag,
    alignGuides, setAlignGuides,
    handleNodeResizeStart,
  };
}
