/**
 * CommandProvider Component
 *
 * Provides command execution context with callback-based mutations.
 * Apps provide their own mutation callbacks (e.g., from React Query)
 * instead of the canvas package hardcoding them.
 */

'use client';

import React, { createContext, useContext, useEffect, useRef } from 'react';
import { useAtomValue, useSetAtom, useAtom } from 'jotai';
import { useStore } from 'jotai';

// Canvas core state
import { currentGraphIdAtom } from '../core/graph-store';
import { selectedNodeIdsAtom } from '../core/selection-store';
import { zoomAtom, panAtom } from '../core/viewport-store';
import { undoAtom, redoAtom } from '../core/history-store';

// Command system
import type { CommandContext, CreateNodePayload, CreateEdgePayload } from './types';
import {
	commandLineStateAtom,
	inputModeAtom,
	commandFeedbackAtom,
	closeCommandLineAtom,
	setCommandErrorAtom,
} from './store';
import { commandRegistry } from './registry';

// Layout hooks
import { useFitToBounds, FitToBoundsMode } from '../hooks/useLayout';
import { useForceLayout } from '../hooks/useForceLayout';
import { useTreeLayout } from '../hooks/useTreeLayout';
import { useGridLayout } from '../hooks/useGridLayout';

// =============================================================================
// Props Types
// =============================================================================

export interface CommandProviderProps {
	children: React.ReactNode;

	// Node mutations (app provides implementations)
	onCreateNode?: (payload: CreateNodePayload) => Promise<unknown>;
	onUpdateNode?: (nodeId: string, updates: Record<string, unknown>) => Promise<void>;
	onDeleteNode?: (nodeId: string) => Promise<void>;

	// Edge mutations (app provides implementations)
	onCreateEdge?: (payload: CreateEdgePayload) => Promise<unknown>;
	onDeleteEdge?: (edgeId: string) => Promise<void>;

	// Force layout persistence (optional)
	onForceLayoutPersist?: (positions: Array<{ nodeId: string; x: number; y: number }>) => Promise<void>;
}

// =============================================================================
// Context Type
// =============================================================================

type CommandContextValue = {
	/**
	 * Execute a command by name with given inputs.
	 * @param commandName Name or alias of the command
	 * @param inputs Pre-collected inputs (for programmatic execution)
	 */
	executeCommand: (commandName: string, inputs: Record<string, unknown>) => Promise<void>;

	/**
	 * Get the current command context (for use in command execute functions).
	 */
	getContext: () => CommandContext;

	/**
	 * Check if a command exists.
	 */
	hasCommand: (name: string) => boolean;
};

const CommandContextContext = createContext<CommandContextValue | null>(null);

// =============================================================================
// Provider Component
// =============================================================================

export function CommandProvider({
	children,
	onCreateNode,
	onUpdateNode,
	onDeleteNode,
	onCreateEdge,
	onDeleteEdge,
	onForceLayoutPersist,
}: CommandProviderProps) {
	// Get the Jotai store for direct get/set access
	const store = useStore();

	// Current state from canvas atoms
	const currentGraphId = useAtomValue(currentGraphIdAtom);
	const selectedNodeIds = useAtomValue(selectedNodeIdsAtom);
	const zoom = useAtomValue(zoomAtom);
	const pan = useAtomValue(panAtom);

	// History actions
	const undo = useSetAtom(undoAtom);
	const redo = useSetAtom(redoAtom);

	// Layout functions from canvas hooks
	const { fitToBounds } = useFitToBounds();
	const persistCallback = onForceLayoutPersist
		? async (updates: Array<{ nodeId: string; position: { x: number; y: number } }>) => {
				await onForceLayoutPersist(updates.map((u) => ({ nodeId: u.nodeId, x: u.position.x, y: u.position.y })));
			}
		: undefined;
	const { applyForceLayout } = useForceLayout({ onPositionsChanged: persistCallback });
	const { applyLayout: applyTreeLayoutTopDown } = useTreeLayout({ onPositionsChanged: persistCallback });
	const { applyLayout: applyTreeLayoutLeftRight } = useTreeLayout({ direction: 'left-right', onPositionsChanged: persistCallback });
	const { applyLayout: applyGridLayoutDefault } = useGridLayout({ onPositionsChanged: persistCallback });

	// Action atoms
	const closeCommandLine = useSetAtom(closeCommandLineAtom);
	const setCommandError = useSetAtom(setCommandErrorAtom);

	// Build the command context with callback-based mutations
	const getContext = (): CommandContext => {
		return {
			get: store.get,
			set: store.set,
			currentGraphId,
			selectedNodeIds,
			viewport: { zoom, pan },
			mutations: {
				createNode: async (payload: CreateNodePayload) => {
					if (!onCreateNode) {
						throw new Error('onCreateNode callback not provided to CommandProvider');
					}
					return onCreateNode(payload);
				},
				updateNode: async (nodeId: string, updates: Record<string, unknown>) => {
					if (!onUpdateNode) {
						throw new Error('onUpdateNode callback not provided to CommandProvider');
					}
					return onUpdateNode(nodeId, updates);
				},
				deleteNode: async (nodeId: string) => {
					if (!onDeleteNode) {
						throw new Error('onDeleteNode callback not provided to CommandProvider');
					}
					return onDeleteNode(nodeId);
				},
				createEdge: async (payload: CreateEdgePayload) => {
					if (!onCreateEdge) {
						throw new Error('onCreateEdge callback not provided to CommandProvider');
					}
					return onCreateEdge(payload);
				},
				deleteEdge: async (edgeId: string) => {
					if (!onDeleteEdge) {
						throw new Error('onDeleteEdge callback not provided to CommandProvider');
					}
					return onDeleteEdge(edgeId);
				},
			},
			layout: {
				fitToBounds: (mode: 'graph' | 'selection', padding?: number) => {
					const fitMode = mode === 'graph' ? FitToBoundsMode.Graph : FitToBoundsMode.Selection;
					fitToBounds(fitMode, padding);
				},
				applyForceLayout,
				applyTreeLayout: async (opts) => {
					if (opts?.direction === 'left-right') {
						await applyTreeLayoutLeftRight();
					} else {
						await applyTreeLayoutTopDown();
					}
				},
				applyGridLayout: async () => {
					await applyGridLayoutDefault();
				},
			},
			history: {
				undo,
				redo,
			},
		};
	};

	// Execute a command
	const executeCommand = async (commandName: string, inputs: Record<string, unknown>) => {
		const command = commandRegistry.get(commandName);
		if (!command) {
			throw new Error(`Unknown command: ${commandName}`);
		}

		// Validate required inputs
		for (const inputDef of command.inputs) {
			if (inputDef.required !== false && inputs[inputDef.name] === undefined) {
				throw new Error(`Missing required input: ${inputDef.name}`);
			}
		}

		// Execute the command
		const ctx = getContext();
		try {
			await command.execute(inputs, ctx);
			closeCommandLine();
		} catch (error) {
			const message = error instanceof Error ? error.message : 'Command execution failed';
			setCommandError(message);
			throw error;
		}
	};

	// Check if command exists
	const hasCommand = (name: string) => {
		return commandRegistry.has(name);
	};

	// Watch for when all inputs are collected and execute the command
	const [commandState, setCommandState] = useAtom(commandLineStateAtom);
	const isExecutingRef = useRef(false);

	useEffect(() => {
		// Handle no-input commands (phase === 'executing' with no inputs)
		if (commandState.phase === 'executing' && !isExecutingRef.current) {
			const { command } = commandState;
			if (command.inputs.length === 0) {
				isExecutingRef.current = true;
				const ctx = getContext();
				command
					.execute({}, ctx)
					.then(() => {
						setCommandState({ phase: 'searching', query: '', suggestions: commandRegistry.all() });
					})
					.catch((error) => {
						const message = error instanceof Error ? error.message : 'Command execution failed';
						setCommandError(message);
					})
					.finally(() => {
						isExecutingRef.current = false;
					});
			}
			return;
		}

		// Check if we're in collecting phase with all inputs collected
		if (commandState.phase !== 'collecting') {
			isExecutingRef.current = false;
			return;
		}

		const { command, inputIndex, collected } = commandState;
		const lastInputIndex = command.inputs.length - 1;
		const lastInput = command.inputs[lastInputIndex];

		// Check if we have the last input
		if (inputIndex === lastInputIndex && collected[lastInput.name] !== undefined && !isExecutingRef.current) {
			isExecutingRef.current = true;

			// Execute the command
			setCommandState({ phase: 'executing', command });

			const ctx = getContext();
			command
				.execute(collected, ctx)
				.then(() => {
					// Reset to search mode instead of closing - keep command line open
					setCommandState({ phase: 'searching', query: '', suggestions: commandRegistry.all() });
					store.set(inputModeAtom, { type: 'normal' });
					store.set(commandFeedbackAtom, null);
				})
				.catch((error) => {
					const message = error instanceof Error ? error.message : 'Command execution failed';
					setCommandError(message);
				})
				.finally(() => {
					isExecutingRef.current = false;
				});
		}
	}, [commandState, getContext, closeCommandLine, setCommandError, setCommandState, store]);

	const value: CommandContextValue = {
		executeCommand,
		getContext,
		hasCommand,
	};

	return <CommandContextContext value={value}>{children}</CommandContextContext>;
}

// =============================================================================
// Hooks
// =============================================================================

/**
 * Hook to access the command context.
 * Must be used within a CommandProvider.
 */
export function useCommandContext(): CommandContextValue {
	const context = useContext(CommandContextContext);
	if (!context) {
		throw new Error('useCommandContext must be used within a CommandProvider');
	}
	return context;
}

/**
 * Hook to execute commands.
 * Returns a function that can be called with command name and inputs.
 */
export function useExecuteCommand() {
	const { executeCommand } = useCommandContext();
	return executeCommand;
}
