/**
 * Edge Overlay Component
 *
 * Wraps a node and provides an edge creation handle.
 * Drag from the handle to another node to create an edge.
 */

import React, { useRef } from 'react';
import { useSetAtom, useAtomValue } from 'jotai';
import { useGesture } from '@use-gesture/react';
import { graphAtom, edgeCreationAtom, currentGraphIdAtom } from '../core/graph-store';
import { nodePositionAtomFamily } from '../core/graph-position';
import { swapEdgeAtomicAtom } from '../core/graph-mutations';
import { addEdgeToLocalGraphAtom, removeEdgeFromLocalGraphAtom } from '../core/graph-mutations-edges';
import {
	addNodesToSelectionAtom,
	removeNodesFromSelectionAtom,
} from '../core/selection-store';
import type { EdgeCreationState, GraphNodeAttributes, DBGraphEdge, Json } from '../core/types';
import { hitTestNode, hitTestPort } from '../utils/hit-test';
import { EdgePath } from './EdgePath';

// Re-export sub-components
export { EdgePath } from './EdgePath';
export type { EdgePathProps } from './EdgePath';
export { EdgeLabel } from './EdgeLabel';
export type { EdgeLabelProps } from './EdgeLabel';

export interface EdgeOverlayProps {
	/** The node ID this overlay wraps */
	nodeId: string;
	/** The node content to render */
	children: React.ReactNode;
	/**
	 * Generate a source handle identifier for edge creation.
	 * Default: returns `widget:${nodeId}:output:data`
	 */
	getSourceHandle?: (nodeId: string) => string;
	/**
	 * Generate a target handle identifier for edge creation.
	 * Default: returns `widget:${targetNodeId}:input:default`
	 */
	getTargetHandle?: (targetNodeId: string) => string;
	/**
	 * Callback to persist the edge to database.
	 * Called after drag ends on a valid target node.
	 * Should return the created edge or throw on error.
	 */
	onEdgeCreate?: (params: {
		graphId: string;
		sourceNodeId: string;
		targetNodeId: string;
		sourceHandle: string;
		targetHandle: string;
		tempEdge: DBGraphEdge;
	}) => Promise<DBGraphEdge>;
	/**
	 * Callback when edge creation fails.
	 */
	onEdgeCreateError?: (error: Error, tempEdgeId: string) => void;
	/**
	 * Callback after edge is successfully created.
	 * Useful for app-specific actions like opening a config panel.
	 */
	onEdgeCreated?: (params: {
		edge: DBGraphEdge;
		sourceNodeId: string;
		targetNodeId: string;
		sourceHandle: string;
		targetHandle: string;
	}) => void;
	/** Custom styles for the edge handle */
	handleStyle?: React.CSSProperties;
	/** Custom class name for the edge handle */
	handleClassName?: string;
	/**
	 * Validation callback — return false to prevent edge creation.
	 * Called with source and target port connections before creating the edge.
	 */
	canConnect?: (source: { nodeId: string; portId: string | null }, target: { nodeId: string; portId: string | null }) => boolean;
}

/**
 * Default handle format functions
 */
const defaultGetSourceHandle = (nodeId: string) => `widget:${nodeId}:output:data`;
const defaultGetTargetHandle = (targetNodeId: string) => `widget:${targetNodeId}:input:default`;

/**
 * Edge overlay component that enables edge creation via drag
 */
export function EdgeOverlay({
	nodeId,
	children,
	getSourceHandle = defaultGetSourceHandle,
	getTargetHandle = defaultGetTargetHandle,
	onEdgeCreate,
	onEdgeCreateError,
	onEdgeCreated,
	handleStyle,
	handleClassName,
	canConnect,
}: EdgeOverlayProps) {
	const setEdgeCreationState = useSetAtom(edgeCreationAtom);
	const graph = useAtomValue(graphAtom);
	const addEdgeToLocalGraph = useSetAtom(addEdgeToLocalGraphAtom);
	const removeEdgeFromLocalGraph = useSetAtom(removeEdgeFromLocalGraphAtom);
	const swapEdgeAtomic = useSetAtom(swapEdgeAtomicAtom);
	const addNodeToSelection = useSetAtom(addNodesToSelectionAtom);
	const removeNodeFromSelection = useSetAtom(removeNodesFromSelectionAtom);
	const currentGraphId = useAtomValue(currentGraphIdAtom);

	const handleRef = useRef<HTMLDivElement>(null);
	const wrapperRef = useRef<HTMLDivElement>(null);

	const position = useAtomValue(nodePositionAtomFamily(nodeId));
	const nodeAttributes = graph.hasNode(nodeId)
		? (graph.getNodeAttributes(nodeId) as GraphNodeAttributes | undefined)
		: undefined;
	const width = nodeAttributes?.width ?? 500;
	const height = nodeAttributes?.height ?? 500;

	const handleDragStart = (x: number, y: number) => {
		if (!graph.hasNode(nodeId)) return;

		const sourceNodeAttributes = graph.getNodeAttributes(nodeId);
		if (!sourceNodeAttributes) return;

		const nodeRect = wrapperRef.current?.querySelector('[data-draggable-node="true"]')?.getBoundingClientRect();
		if (!nodeRect) return;

		const sourceNodePosition = {
			x: (sourceNodeAttributes as GraphNodeAttributes).x + nodeRect.width / 2,
			y: (sourceNodeAttributes as GraphNodeAttributes).y + nodeRect.height / 2,
		};

		const sourceHandle = getSourceHandle(nodeId);

		setEdgeCreationState({
			isCreating: true,
			sourceNodeId: nodeId,
			sourceNodePosition,
			targetPosition: { x, y },
			hoveredTargetNodeId: null,
			sourceHandle,
			targetHandle: null,
			sourcePort: null,
			targetPort: null,
			snappedTargetPosition: null,
		});
	};

	const handleDrag = (x: number, y: number) => {
		setEdgeCreationState((prev: EdgeCreationState) => {
			if (!prev.isCreating) return prev;

			const { nodeId: hitNodeId } = hitTestNode(x, y);
			let hoveredNodeId: string | null = null;
			let targetPort: string | null = null;

			// Check for port under cursor first
			const portInfo = hitTestPort(x, y);
			if (portInfo && portInfo.nodeId !== nodeId) {
				hoveredNodeId = portInfo.nodeId;
				targetPort = portInfo.portId;
			} else if (hitNodeId) {
				hoveredNodeId = hitNodeId;
			}

			// Update selection highlighting
			if (hoveredNodeId && hoveredNodeId !== nodeId && hoveredNodeId !== prev.hoveredTargetNodeId) {
				if (prev.hoveredTargetNodeId) {
					removeNodeFromSelection([prev.hoveredTargetNodeId]);
				}
				addNodeToSelection([hoveredNodeId]);
			}

			return {
				...prev,
				targetPosition: { x, y },
				hoveredTargetNodeId: hoveredNodeId && hoveredNodeId !== nodeId ? hoveredNodeId : null,
				targetPort,
			};
		});
	};

	const handleDragEnd = async (x: number, y: number) => {
		let targetNodeId: string | null = null;
		const { nodeId: hitNodeId } = hitTestNode(x, y);
		if (hitNodeId) {
			targetNodeId = hitNodeId;
		}

		// Clear edge creation state
		setEdgeCreationState((prev: EdgeCreationState) => {
			if (prev.hoveredTargetNodeId) {
				removeNodeFromSelection([prev.hoveredTargetNodeId]);
			}
			return {
				isCreating: false,
				sourceNodeId: null,
				sourceNodePosition: null,
				targetPosition: null,
				hoveredTargetNodeId: null,
				sourceHandle: null,
				targetHandle: null,
				sourcePort: null,
				targetPort: null,
				snappedTargetPosition: null,
			};
		});

		// Create edge if valid target
		if (targetNodeId && targetNodeId !== nodeId && currentGraphId) {
			// Check canConnect validation
			if (canConnect) {
				const allowed = canConnect(
					{ nodeId, portId: null },
					{ nodeId: targetNodeId, portId: null },
				);
				if (!allowed) return;
			}
			const sourceHandle = getSourceHandle(nodeId);
			const targetHandle = getTargetHandle(targetNodeId);

			const tempEdgeId = crypto.randomUUID();
			const tempEdge: DBGraphEdge = {
				id: tempEdgeId,
				graph_id: currentGraphId,
				source_node_id: nodeId,
				target_node_id: targetNodeId,
				edge_type: 'data_connection',
				filter_condition: null,
				ui_properties: {
					style: 'solid',
					color: '#999',
					sourceHandle,
					targetHandle,
				} as { [key: string]: Json | undefined },
				created_at: new Date().toISOString(),
				updated_at: new Date().toISOString(),
			};

			// Add temp edge to local graph
			addEdgeToLocalGraph(tempEdge);

			// If no persistence callback, just keep the local edge
			if (!onEdgeCreate) {
				return;
			}

			try {
				const newEdge = await onEdgeCreate({
					graphId: currentGraphId,
					sourceNodeId: nodeId,
					targetNodeId,
					sourceHandle,
					targetHandle,
					tempEdge,
				});

				// Swap temp edge with real edge
				swapEdgeAtomic({ tempEdgeId, newEdge });

				// Notify app of successful creation
				onEdgeCreated?.({
					edge: newEdge,
					sourceNodeId: nodeId,
					targetNodeId,
					sourceHandle,
					targetHandle,
				});
			} catch (error) {
				// Remove temp edge on error
				removeEdgeFromLocalGraph(tempEdgeId);
				onEdgeCreateError?.(error as Error, tempEdgeId);
			}
		}
	};

	useGesture(
		{
			onDragStart: ({ event, xy: [x, y] }) => {
				event.stopPropagation();
				handleDragStart(x, y);
			},
			onDrag: ({ xy: [x, y] }) => {
				handleDrag(x, y);
			},
			onDragEnd: ({ xy: [x, y] }) => {
				handleDragEnd(x, y);
			},
		},
		{
			target: handleRef,
			eventOptions: { passive: false },
		},
	);

	if (!position || !nodeAttributes) {
		return null;
	}

	return (
		<div
			ref={wrapperRef}
			style={{
				position: 'absolute',
				top: position.y,
				left: position.x,
				width: `${width}px`,
				height: `${height}px`,
			}}
		>
			{children}
			<EdgePath
				handleRef={handleRef}
				className={handleClassName}
				style={handleStyle}
			/>
		</div>
	);
}
