/**
 * Hook for managing node drag behavior
 *
 * Pure drag mechanics — always moves nodes. Gesture resolution
 * (which button/source/modifier maps to which action) is handled
 * by the v2 gesture pipeline in useNodeGestures / useCanvasGestures.
 *
 * Type definitions are in ./drag-types.ts
 */

import { useAtom, useAtomValue, useSetAtom, atom } from 'jotai';
import { useGesture, FullGestureState } from '@use-gesture/react';
import { useRef } from 'react';
import type { NodeId, NodePosition, GraphNodeAttributes, UINodeProperties } from '../core/types';
import { graphAtom, preDragNodeAttributesAtom, edgeCreationAtom, currentGraphIdAtom } from '../core/graph-store';
import { nodePositionUpdateCounterAtom } from '../core/graph-position';
import { startNodeDragAtom, endNodeDragAtom, dropTargetNodeIdAtom } from '../core/graph-mutations';
import { panAtom, zoomAtom } from '../core/viewport-store';
import { selectedNodeIdsAtom } from '../core/selection-store';
import { startMutationAtom, completeMutationAtom } from '../core/sync-store';
import { pushHistoryAtom } from '../core/history-store';
import { getNodeDescendants, nestNodesOnDropAtom } from '../core/group-store';
import { getNodeGestureConfig } from '../utils/gesture-configs';
import { primaryInputSourceAtom } from '../core/input-store';
import { getPendingState } from '../utils/mutation-queue';
import { createDebug } from '../utils/debug';
import { hitTestNode } from '../utils/hit-test';
import { buildDragPositions, computeDragUpdates, isDragPrevented } from './useDragStateMachine';

// Re-export extracted modules
export { buildDragPositions, computeDragUpdates, isDragPrevented } from './useDragStateMachine';
export { snapToGrid, clampToBounds, applyDragConstraints } from './useDragConstraints';
export type { DragConstraintOptions } from './useDragConstraints';

// Re-export types for backward compat
export type { DragMemo, UseNodeDragOptions } from './drag-types';
import type { DragMemo, UseNodeDragOptions } from './drag-types';

const debug = createDebug('drag');

/**
 * Hook for node drag behavior with multi-select support.
 * Always treats drag as move-node — no gesture resolution.
 */
export function useNodeDrag(id: NodeId, options: UseNodeDragOptions = {}) {
	const { onPersist, onPersistError, heldKeys } = options;

	const graph = useAtomValue(graphAtom);
	const [pan, setPan] = useAtom(panAtom);
	const startMutation = useSetAtom(startMutationAtom);
	const completeMutation = useSetAtom(completeMutationAtom);
	const setStartDrag = useSetAtom(startNodeDragAtom);
	const setEndDrag = useSetAtom(endNodeDragAtom);
	const getPreDragAttributes = useAtomValue(preDragNodeAttributesAtom);
	const currentZoom = useAtomValue(zoomAtom);
	const getSelectedNodeIds = useAtomValue(selectedNodeIdsAtom);
	const currentGraphId = useAtomValue(currentGraphIdAtom);
	const edgeCreation = useAtomValue(edgeCreationAtom);
	const setGraph = useSetAtom(graphAtom);
	const setNodePositionUpdateCounter = useSetAtom(nodePositionUpdateCounterAtom);
	const pushHistory = useSetAtom(pushHistoryAtom);
	const setDropTarget = useSetAtom(dropTargetNodeIdAtom);
	const nestNodesOnDrop = useSetAtom(nestNodesOnDropAtom);
	const inputSource = useAtomValue(primaryInputSourceAtom);

	// Batch position update atom
	const batchUpdatePositions = useSetAtom(
		atom(null, (get, set, updates: { id: NodeId; pos: NodePosition }[]) => {
			const graph = get(graphAtom);
			updates.forEach((u) => {
				if (graph.hasNode(u.id)) {
					graph.setNodeAttribute(u.id, 'x', u.pos.x);
					graph.setNodeAttribute(u.id, 'y', u.pos.y);
				}
			});
			set(nodePositionUpdateCounterAtom, (c) => c + 1);
		}),
	);

	const updateNodePositions = (updates: { id: NodeId; pos: NodePosition }[]) => {
		batchUpdatePositions(updates);
	};

	const gestureInstanceRef = useRef(0);
	const panStartRef = useRef({ x: 0, y: 0 });

	const isSpaceHeld = Boolean(
		heldKeys?.byCode.Space ||
		heldKeys?.byKey[' '] ||
		heldKeys?.byKey.Spacebar,
	);

	const bind = useGesture(
		{
			onPointerDown: (state) => {
				if (isDragPrevented(state.event.target as HTMLElement)) return;
				gestureInstanceRef.current += 1;
				if (isSpaceHeld) {
					panStartRef.current = pan;
				}
			},
			onDrag: (state: FullGestureState<'drag'>) => {
				if (isDragPrevented(state.event.target as HTMLElement)) return;

				if (edgeCreation.isCreating) return;
				state.event.stopPropagation();

				if (isSpaceHeld) {
					if (!state.tap && state.active) {
						const [mx, my] = state.movement;
						setPan({
							x: panStartRef.current.x + mx,
							y: panStartRef.current.y + my,
						});
					}
					return state.memo as DragMemo | undefined;
				}

				const {
					movement: [mx, my],
					first,
					active,
					down,
					pinching,
					cancel,
					tap,
				} = state;
				let currentMemo = state.memo as DragMemo | undefined;

				if (tap || !active) return currentMemo;

				const currentGestureInstance = gestureInstanceRef.current;

				if (first) {
					const selectionSize = getSelectedNodeIds.size;
					const label = selectionSize > 1 ? `Move ${selectionSize} nodes` : 'Move node';
					pushHistory(label);

					setStartDrag({ nodeId: id });
					const initialPositions = buildDragPositions(graph, getSelectedNodeIds);

					// Start mutations AFTER building full initialPositions (including descendants)
					initialPositions.forEach(() => startMutation());

					currentMemo = {
						initialPositions,
						gestureInstance: currentGestureInstance,
					};
				}

				if (!currentMemo || currentMemo.gestureInstance !== currentGestureInstance || !currentMemo.initialPositions) {
					if (cancel && !pinching && !down && !tap && !active) cancel();
					return currentMemo;
				}

				const updates = computeDragUpdates(currentMemo.initialPositions, mx, my, currentZoom, graph);

				if (updates.length > 0) {
					updateNodePositions(updates);
				}

				// Detect drop target via elementFromPoint
				if (state.event && 'clientX' in state.event) {
					const { nodeId: hoveredId } = hitTestNode(state.event.clientX, state.event.clientY);
					const validTarget = hoveredId && !currentMemo.initialPositions.has(hoveredId)
						? hoveredId : null;
					setDropTarget(validTarget);
				}

				return currentMemo;
			},
			onDragEnd: (state: FullGestureState<'drag'>) => {
				if (isDragPrevented(state.event.target as HTMLElement)) return;

				if (edgeCreation.isCreating) {
					setEndDrag({ nodeId: id });
					return;
				}

				if (isSpaceHeld) {
					return;
				}
				state.event.stopPropagation();
				const memo = state.memo as DragMemo | undefined;

				// Handle drag-to-nest
				setDropTarget(null);
				if (state.event && 'clientX' in state.event && memo?.initialPositions) {
					const { nodeId: hoveredId } = hitTestNode(state.event.clientX, state.event.clientY);
					if (hoveredId && !memo.initialPositions.has(hoveredId)) {
						const draggedNodeIds = Array.from(memo.initialPositions.keys())
							.filter((nid) => getSelectedNodeIds.has(nid));
						if (draggedNodeIds.length > 0) {
							nestNodesOnDrop({ nodeIds: draggedNodeIds, targetId: hoveredId });
						}
					}
				}

				if (!currentGraphId) {
					debug('Cannot update node position: currentGraphId is not set');
					setEndDrag({ nodeId: id });
					return;
				}

				const nodesToUpdate: NodeId[] = memo?.initialPositions ? Array.from(memo.initialPositions.keys()) : [id];

				nodesToUpdate.forEach((draggedNodeId) => {
					if (!graph.hasNode(draggedNodeId)) {
						completeMutation(false);
						return;
					}

					const finalAttrs = graph.getNodeAttributes(draggedNodeId) as GraphNodeAttributes;
					const initialPos = memo?.initialPositions.get(draggedNodeId);

					if (!initialPos) {
						completeMutation(false);
						return;
					}

					const [mx, my] = state.movement;
					const deltaX = mx / currentZoom;
					const deltaY = my / currentZoom;
					const finalPosition = {
						x: initialPos.x + deltaX,
						y: initialPos.y + deltaY,
					};

					updateNodePositions([{ id: draggedNodeId, pos: finalPosition }]);

					if (!onPersist) {
						completeMutation(true);
						setEndDrag({ nodeId: draggedNodeId });
						return;
					}

					const existingDbUiProps: Record<string, unknown> =
						typeof finalAttrs.dbData.ui_properties === 'object' &&
							finalAttrs.dbData.ui_properties !== null &&
							!Array.isArray(finalAttrs.dbData.ui_properties)
							? (finalAttrs.dbData.ui_properties as Record<string, unknown>)
							: {};

					const newUiProperties = {
						...existingDbUiProps,
						x: finalPosition.x,
						y: finalPosition.y,
						zIndex: finalAttrs.zIndex,
					};

					const pendingState = getPendingState(draggedNodeId);

					if (pendingState.inFlight) {
						pendingState.queuedPosition = finalPosition;
						pendingState.queuedUiProperties = newUiProperties;
						pendingState.graphId = currentGraphId;
						return;
					}

					pendingState.inFlight = true;
					pendingState.graphId = currentGraphId;

					const processQueuedUpdate = async (nodeId: NodeId) => {
						const state = getPendingState(nodeId);
						if (state && state.queuedPosition && state.queuedUiProperties && state.graphId) {
							const queuedProps = state.queuedUiProperties;
							const queuedGraphId = state.graphId;

							state.queuedPosition = null;
							state.queuedUiProperties = null;

							startMutation();
							try {
								await onPersist(nodeId, queuedGraphId, queuedProps);
								completeMutation(true);
							} catch (error) {
								completeMutation(false);
								onPersistError?.(nodeId, error as Error);
							}
							state.inFlight = false;
							processQueuedUpdate(nodeId);
						} else if (state) {
							state.inFlight = false;
						}
					};

					onPersist(draggedNodeId, currentGraphId, newUiProperties)
						.then(() => {
							completeMutation(true);
							processQueuedUpdate(draggedNodeId);
						})
						.catch((error) => {
							completeMutation(false);
							const state = getPendingState(draggedNodeId);
							if (state) state.inFlight = false;

							const preDragAttrsForNode = getPreDragAttributes;
							if (preDragAttrsForNode && preDragAttrsForNode.dbData.id === draggedNodeId && graph.hasNode(draggedNodeId)) {
								graph.replaceNodeAttributes(draggedNodeId, preDragAttrsForNode as GraphNodeAttributes);
								setGraph(graph);
							}

							onPersistError?.(draggedNodeId, error);
							processQueuedUpdate(draggedNodeId);
						})
						.finally(() => {
							setEndDrag({ nodeId: draggedNodeId });
						});
				});
			},
		},
		getNodeGestureConfig(inputSource),
	);

	return { bind, updateNodePositions };
}
