/**
 * Force-directed layout hook.
 *
 * Uses D3 force simulation to arrange nodes without overlap.
 * Storage-agnostic: accepts optional persistence callback.
 */

import * as d3 from 'd3-force';
import { useAtomValue, useSetAtom } from 'jotai';
import { useRef } from 'react';
import { graphAtom } from '../core/graph-store';
import { updateNodePositionAtom } from '../core/graph-position';
import { uiNodesAtom } from '../core/graph-derived';
import { checkNodesOverlap, type NodeWithDimensions } from '../utils/layout';
import type { NodePosition } from '../core/types';
import { createDebug } from '../utils/debug';

const debug = createDebug('force-layout');

/**
 * Position update for a single node (used in persistence callback)
 */
export interface NodePositionUpdate {
	nodeId: string;
	position: NodePosition;
}

/**
 * Options for the force layout
 */
export interface UseForceLayoutOptions {
	/**
	 * Optional callback to persist positions after layout.
	 * Called with all updated positions when layout completes.
	 */
	onPositionsChanged?: (updates: NodePositionUpdate[]) => void | Promise<void>;

	/**
	 * Maximum iterations before stopping (default: 1000)
	 */
	maxIterations?: number;

	/**
	 * Charge strength for node repulsion (default: -6000)
	 * More negative = stronger repulsion
	 */
	chargeStrength?: number;

	/**
	 * Link strength for mesh connections (default: 0.03)
	 */
	linkStrength?: number;
}

interface SimulationNode extends NodeWithDimensions {
	id: string;
	radius: number;
}

/**
 * Hook to apply force-directed layout to canvas nodes.
 *
 * @param options - Configuration options including persistence callback
 * @returns Object with applyForceLayout function and isRunning state
 *
 * @example
 * // Basic usage (no persistence)
 * const { applyForceLayout } = useForceLayout();
 * await applyForceLayout();
 *
 * @example
 * // With database persistence
 * const { applyForceLayout } = useForceLayout({
 *   onPositionsChanged: async (updates) => {
 *     for (const { nodeId, position } of updates) {
 *       await saveNodePosition(nodeId, position);
 *     }
 *   }
 * });
 */
export const useForceLayout = (options: UseForceLayoutOptions = {}) => {
	const { onPositionsChanged, maxIterations = 1000, chargeStrength = -6000, linkStrength = 0.03 } = options;

	const nodes = useAtomValue(uiNodesAtom);
	const graph = useAtomValue(graphAtom);
	const updateNodePosition = useSetAtom(updateNodePositionAtom);
	const isRunningRef = useRef(false);

	// Create virtual links in a mesh pattern for better spreading
	const createVirtualLinks = () => {
		const links: Array<{ source: string; target: string; strength: number }> = [];

		for (let i = 0; i < nodes.length; i++) {
			// Connect to next few nodes (wrapping around)
			for (let j = 1; j <= 3; j++) {
				const targetIndex = (i + j) % nodes.length;
				links.push({
					source: nodes[i].id,
					target: nodes[targetIndex].id,
					strength: 0.05, // Very weak connection
				});
			}
		}

		return links;
	};

	const applyForceLayout = async (): Promise<void> => {
		if (isRunningRef.current) {
			debug.warn('Layout already running, ignoring request.');
			return;
		}

		if (nodes.length === 0) {
			debug.warn('No nodes to layout.');
			return;
		}

		isRunningRef.current = true;

		// Create simulation nodes with size information
		const simulationNodes: SimulationNode[] = nodes.map((node) => {
			const width = node.width || 80;
			const height = node.height || 80;

			return {
				id: node.id,
				x: node.position?.x || 0,
				y: node.position?.y || 0,
				width,
				height,
				radius: Math.max(width, height) + 80,
			};
		});

		// Create force simulation
		const simulation = d3
			.forceSimulation(simulationNodes)
			.force('charge', d3.forceManyBody().strength(chargeStrength).distanceMax(900))
			.force(
				'collide',
				d3
					.forceCollide<SimulationNode>()
					.radius((d) => d.radius)
					.strength(2)
					.iterations(8),
			)
			.force(
				'link',
				d3
					.forceLink(createVirtualLinks())
					.id((d) => (d as SimulationNode).id)
					.strength(linkStrength),
			)
			.force('center', d3.forceCenter(0, 0))
			.stop(); // Stop initially, tick manually

		debug('Starting simulation...');

		return new Promise((resolve) => {
			let iterations = 0;

			function runSimulationStep() {
				if (iterations >= maxIterations) {
					debug('Reached max iterations (%d), finalizing.', maxIterations);
					finalizeLayout();
					return;
				}

				simulation.tick();
				iterations++;

				// Check for overlaps
				let hasOverlaps = false;

				for (let i = 0; i < simulationNodes.length; i++) {
					for (let j = i + 1; j < simulationNodes.length; j++) {
						if (checkNodesOverlap(simulationNodes[i], simulationNodes[j])) {
							hasOverlaps = true;
							break;
						}
					}
					if (hasOverlaps) break;
				}

				if (!hasOverlaps) {
					debug('No overlaps after %d iterations.', iterations);
					finalizeLayout();
					return;
				}

				// Schedule next step
				requestAnimationFrame(runSimulationStep);
			}

			function finalizeLayout() {
				// Collect all position updates
				const positionUpdates: NodePositionUpdate[] = [];

				simulationNodes.forEach((simNode) => {
					if (graph.hasNode(simNode.id)) {
						const newPosition: NodePosition = {
							x: Math.round(simNode.x),
							y: Math.round(simNode.y),
						};

						// Update local position immediately
						updateNodePosition({ nodeId: simNode.id, position: newPosition });

						positionUpdates.push({
							nodeId: simNode.id,
							position: newPosition,
						});
					}
				});

				// Call persistence callback if provided
				if (onPositionsChanged && positionUpdates.length > 0) {
					debug('Saving %d positions via callback...', positionUpdates.length);
					Promise.resolve(onPositionsChanged(positionUpdates))
						.then(() => debug('Positions saved successfully.'))
						.catch((err) => debug.error('Error saving positions: %O', err));
				}

				debug('Layout complete.');
				isRunningRef.current = false;
				resolve();
			}

			// Start the simulation
			requestAnimationFrame(runSimulationStep);
		});
	};

	return {
		applyForceLayout,
		isRunning: isRunningRef.current,
	};
};
