/**
 * Plugin Registry
 *
 * Central registry for canvas plugins. Handles:
 * - Registration with dependency resolution
 * - Conflict detection across all subsystems
 * - Atomic registration (all-or-nothing)
 * - Cleanup on unregister
 */

import type {
	CanvasPlugin,
	PluginContext,
	PluginRegistration,
} from './plugin-types';
import { PluginError } from './plugin-types';
import { registerNodeType, registerNodeTypes, unregisterNodeType } from './node-type-registry';
import { registerAction as registerEventAction, unregisterAction as unregisterEventAction } from './action-registry';
import { registerAction as registerGestureAction, unregisterAction as unregisterGestureAction } from '../gestures/dispatcher';
import { commandRegistry } from '../commands/registry';
import { registerEdgePathCalculator, unregisterEdgePathCalculator } from '../utils/edge-path-registry';
import { createDebug } from '../utils/debug';

const debug = createDebug('plugins');

// =============================================================================
// Registry
// =============================================================================

const plugins = new Map<string, PluginRegistration>();

/**
 * Register a plugin. All capabilities are registered atomically.
 *
 * @throws {PluginError} ALREADY_REGISTERED — plugin ID already in use
 * @throws {PluginError} MISSING_DEPENDENCY — required plugin not registered
 * @throws {PluginError} CONFLICT — duplicate node type, action, command, or calculator
 *
 * @example
 * ```ts
 * registerPlugin({
 *   id: 'my-plugin',
 *   name: 'My Plugin',
 *   nodeTypes: { 'custom': CustomNode },
 *   commands: [myCommand],
 * });
 * ```
 */
export function registerPlugin(plugin: CanvasPlugin): void {
	debug('Registering plugin: %s', plugin.id);

	// 1. Check not already registered
	if (plugins.has(plugin.id)) {
		throw new PluginError(
			'Plugin is already registered',
			plugin.id,
			'ALREADY_REGISTERED',
		);
	}

	// 2. Check dependencies
	if (plugin.dependencies) {
		for (const depId of plugin.dependencies) {
			if (!plugins.has(depId)) {
				throw new PluginError(
					`Missing dependency: "${depId}"`,
					plugin.id,
					'MISSING_DEPENDENCY',
				);
			}
		}
	}

	// 3. Check for conflicts before registering anything
	detectConflicts(plugin);

	// 4. Register all capabilities
	const cleanups: (() => void)[] = [];

	try {
		// Node types
		if (plugin.nodeTypes) {
			const nodeTypeNames = Object.keys(plugin.nodeTypes);
			registerNodeTypes(plugin.nodeTypes);
			cleanups.push(() => {
				for (const name of nodeTypeNames) {
					unregisterNodeType(name);
				}
			});
		}

		// Edge path calculators
		if (plugin.edgePathCalculators) {
			for (const [name, calc] of Object.entries(plugin.edgePathCalculators)) {
				registerEdgePathCalculator(name, calc);
				cleanups.push(() => unregisterEdgePathCalculator(name));
			}
		}

		// Gesture action handlers
		if (plugin.actionHandlers) {
			for (const [actionId, handler] of Object.entries(plugin.actionHandlers)) {
				registerGestureAction(actionId, handler);
				cleanups.push(() => unregisterGestureAction(actionId));
			}
		}

		// Gesture contexts are stored on the plugin — consumers read them
		// via getPluginGestureContexts() and merge them into the mapping index.

		// Commands
		if (plugin.commands) {
			for (const cmd of plugin.commands) {
				commandRegistry.register(cmd);
				cleanups.push(() => commandRegistry.unregister(cmd.name));
			}
		}

		// Event-action definitions
		if (plugin.actions) {
			for (const action of plugin.actions) {
				registerEventAction(action);
				cleanups.push(() => unregisterEventAction(action.id));
			}
		}

		// 5. Lifecycle hook
		let lifecycleCleanup: (() => void) | null = null;
		if (plugin.onRegister) {
			const ctx = makePluginContext(plugin.id);
			try {
				const result = plugin.onRegister(ctx);
				if (typeof result === 'function') {
					lifecycleCleanup = result;
				}
			} catch (err) {
				// Rollback all registrations on lifecycle error
				for (const cleanup of cleanups.reverse()) {
					try { cleanup(); } catch { /* swallow cleanup errors */ }
				}
				throw new PluginError(
					`onRegister failed: ${err instanceof Error ? err.message : String(err)}`,
					plugin.id,
					'LIFECYCLE_ERROR',
				);
			}
		}

		// 6. Store registration
		plugins.set(plugin.id, {
			plugin,
			cleanup: () => {
				for (const cleanup of cleanups.reverse()) {
					try { cleanup(); } catch { /* swallow cleanup errors */ }
				}
				if (lifecycleCleanup) {
					try { lifecycleCleanup(); } catch { /* swallow */ }
				}
			},
			registeredAt: Date.now(),
		});

		debug('Plugin registered: %s (%d node types, %d commands, %d actions)',
			plugin.id,
			Object.keys(plugin.nodeTypes ?? {}).length,
			plugin.commands?.length ?? 0,
			plugin.actions?.length ?? 0,
		);
	} catch (err) {
		if (err instanceof PluginError) throw err;
		// Rollback on unexpected error
		for (const cleanup of cleanups.reverse()) {
			try { cleanup(); } catch { /* swallow */ }
		}
		throw err;
	}
}

/**
 * Unregister a plugin and clean up all its registrations.
 *
 * @throws {PluginError} NOT_FOUND — plugin not registered
 * @throws {PluginError} CONFLICT — other plugins depend on this one
 */
export function unregisterPlugin(pluginId: string): void {
	const registration = plugins.get(pluginId);
	if (!registration) {
		throw new PluginError('Plugin is not registered', pluginId, 'NOT_FOUND');
	}

	// Check no other plugin depends on this one
	for (const [otherId, other] of plugins) {
		if (other.plugin.dependencies?.includes(pluginId)) {
			throw new PluginError(
				`Cannot unregister: plugin "${otherId}" depends on it`,
				pluginId,
				'CONFLICT',
			);
		}
	}

	// Run cleanup
	if (registration.cleanup) {
		registration.cleanup();
	}

	plugins.delete(pluginId);
	debug('Plugin unregistered: %s', pluginId);
}

// =============================================================================
// Queries
// =============================================================================

/** Get a registered plugin by ID */
export function getPlugin(id: string): CanvasPlugin | undefined {
	return plugins.get(id)?.plugin;
}

/** Check if a plugin is registered */
export function hasPlugin(id: string): boolean {
	return plugins.has(id);
}

/** Get all registered plugins */
export function getAllPlugins(): CanvasPlugin[] {
	return Array.from(plugins.values()).map((r) => r.plugin);
}

/** Get all registered plugin IDs */
export function getPluginIds(): string[] {
	return Array.from(plugins.keys());
}

/**
 * Get all gesture contexts contributed by plugins.
 * Used by GestureProvider to merge plugin contexts into the mapping index.
 */
export function getPluginGestureContexts(): import('../gestures/types').MappingContext[] {
	const contexts: import('../gestures/types').MappingContext[] = [];
	for (const registration of plugins.values()) {
		if (registration.plugin.gestureContexts) {
			contexts.push(...registration.plugin.gestureContexts);
		}
	}
	return contexts;
}

/** Clear all plugins. Mainly for testing. */
export function clearPlugins(): void {
	// Unregister in reverse order (respect dependencies)
	const ids = Array.from(plugins.keys()).reverse();
	for (const id of ids) {
		const reg = plugins.get(id);
		if (reg?.cleanup) {
			try { reg.cleanup(); } catch { /* swallow */ }
		}
		plugins.delete(id);
	}
	debug('All plugins cleared');
}

// =============================================================================
// Conflict Detection
// =============================================================================

function detectConflicts(plugin: CanvasPlugin): void {
	// Check command name conflicts
	if (plugin.commands) {
		for (const cmd of plugin.commands) {
			if (commandRegistry.has(cmd.name)) {
				throw new PluginError(
					`Command "${cmd.name}" is already registered`,
					plugin.id,
					'CONFLICT',
				);
			}
		}
	}

	// Check edge path calculator conflicts
	if (plugin.edgePathCalculators) {
		for (const name of Object.keys(plugin.edgePathCalculators)) {
			// Built-in types are fine to shadow (plugin overrides)
			// Only conflict with other plugins
			for (const [otherId, other] of plugins) {
				if (other.plugin.edgePathCalculators?.[name]) {
					throw new PluginError(
						`Edge path calculator "${name}" already registered by plugin "${otherId}"`,
						plugin.id,
						'CONFLICT',
					);
				}
			}
		}
	}

	// Check node type conflicts between plugins
	if (plugin.nodeTypes) {
		for (const nodeType of Object.keys(plugin.nodeTypes)) {
			for (const [otherId, other] of plugins) {
				if (other.plugin.nodeTypes?.[nodeType]) {
					throw new PluginError(
						`Node type "${nodeType}" already registered by plugin "${otherId}"`,
						plugin.id,
						'CONFLICT',
					);
				}
			}
		}
	}

	// Check action handler conflicts between plugins
	if (plugin.actionHandlers) {
		for (const actionId of Object.keys(plugin.actionHandlers)) {
			for (const [otherId, other] of plugins) {
				if (other.plugin.actionHandlers?.[actionId]) {
					throw new PluginError(
						`Action handler "${actionId}" already registered by plugin "${otherId}"`,
						plugin.id,
						'CONFLICT',
					);
				}
			}
		}
	}
}

// =============================================================================
// Internal Helpers
// =============================================================================

function makePluginContext(pluginId: string): PluginContext {
	return {
		pluginId,
		getPlugin,
		hasPlugin,
	};
}
