/**
 * Command Registry
 *
 * Singleton class for managing all available commands.
 * Commands can be registered by the canvas package (built-ins)
 * or by consuming apps (custom commands).
 */

import type { CommandDefinition, CommandCategory } from './types';

/**
 * Command Registry - Singleton class for managing all available commands.
 *
 * Usage:
 * ```typescript
 * import { commandRegistry } from '@blinksgg/canvas';
 *
 * // Register a command
 * commandRegistry.register(myCommand);
 *
 * // Get a command by name or alias
 * const cmd = commandRegistry.get('createNode'); // or 'cn'
 *
 * // Search commands
 * const results = commandRegistry.search('node');
 * ```
 */
class CommandRegistry {
	private commands = new Map<string, CommandDefinition>();
	private aliases = new Map<string, string>(); // alias -> command name

	/**
	 * Register a command with the registry.
	 * @param command The command definition to register
	 * @throws Error if command name or alias already exists
	 */
	register(command: CommandDefinition): void {
		if (this.commands.has(command.name)) {
			throw new Error(`Command "${command.name}" is already registered`);
		}

		this.commands.set(command.name, command);

		// Register aliases
		if (command.aliases) {
			for (const alias of command.aliases) {
				if (this.aliases.has(alias)) {
					throw new Error(`Alias "${alias}" is already registered for command "${this.aliases.get(alias)}"`);
				}
				if (this.commands.has(alias)) {
					throw new Error(`Alias "${alias}" conflicts with existing command name`);
				}
				this.aliases.set(alias, command.name);
			}
		}
	}

	/**
	 * Unregister a command by name.
	 * @param name The command name to remove
	 */
	unregister(name: string): void {
		const command = this.commands.get(name);
		if (command) {
			// Remove aliases
			if (command.aliases) {
				for (const alias of command.aliases) {
					this.aliases.delete(alias);
				}
			}
			this.commands.delete(name);
		}
	}

	/**
	 * Get a command by name or alias.
	 * @param nameOrAlias Command name or alias
	 * @returns The command definition or undefined if not found
	 */
	get(nameOrAlias: string): CommandDefinition | undefined {
		// Check if it's a direct command name
		const direct = this.commands.get(nameOrAlias);
		if (direct) return direct;

		// Check if it's an alias
		const commandName = this.aliases.get(nameOrAlias);
		if (commandName) {
			return this.commands.get(commandName);
		}

		return undefined;
	}

	/**
	 * Check if a command exists by name or alias.
	 * @param nameOrAlias Command name or alias
	 */
	has(nameOrAlias: string): boolean {
		return this.commands.has(nameOrAlias) || this.aliases.has(nameOrAlias);
	}

	/**
	 * Search for commands matching a query.
	 * Searches command names, aliases, and descriptions.
	 * @param query Search query (case-insensitive)
	 * @returns Array of matching commands, sorted by relevance
	 */
	search(query: string): CommandDefinition[] {
		if (!query.trim()) {
			return this.all();
		}

		const lowerQuery = query.toLowerCase().trim();
		const results: Array<{ command: CommandDefinition; score: number }> = [];
		const commands = Array.from(this.commands.values());

		for (const command of commands) {
			let score = 0;

			// Exact name match (highest priority)
			if (command.name.toLowerCase() === lowerQuery) {
				score = 100;
			}
			// Name starts with query
			else if (command.name.toLowerCase().startsWith(lowerQuery)) {
				score = 80;
			}
			// Name contains query
			else if (command.name.toLowerCase().includes(lowerQuery)) {
				score = 60;
			}
			// Alias exact match
			else if (command.aliases?.some((a) => a.toLowerCase() === lowerQuery)) {
				score = 90;
			}
			// Alias starts with query
			else if (command.aliases?.some((a) => a.toLowerCase().startsWith(lowerQuery))) {
				score = 70;
			}
			// Alias contains query
			else if (command.aliases?.some((a) => a.toLowerCase().includes(lowerQuery))) {
				score = 50;
			}
			// Description contains query
			else if (command.description.toLowerCase().includes(lowerQuery)) {
				score = 30;
			}

			if (score > 0) {
				results.push({ command, score });
			}
		}

		// Sort by score (descending), then alphabetically
		return results.sort((a, b) => b.score - a.score || a.command.name.localeCompare(b.command.name)).map((r) => r.command);
	}

	/**
	 * Get all registered commands.
	 * @returns Array of all commands, sorted alphabetically by name
	 */
	all(): CommandDefinition[] {
		return Array.from(this.commands.values()).sort((a, b) => a.name.localeCompare(b.name));
	}

	/**
	 * Get commands by category.
	 * @param category The category to filter by
	 * @returns Array of commands in the category
	 */
	byCategory(category: CommandCategory): CommandDefinition[] {
		return this.all().filter((cmd) => cmd.category === category);
	}

	/**
	 * Get all available categories.
	 * @returns Array of unique categories
	 */
	categories(): CommandCategory[] {
		const categories = new Set<CommandCategory>();
		const commands = Array.from(this.commands.values());
		for (const command of commands) {
			categories.add(command.category);
		}
		return Array.from(categories).sort();
	}

	/**
	 * Get the count of registered commands.
	 */
	get size(): number {
		return this.commands.size;
	}

	/**
	 * Clear all registered commands.
	 * Useful for testing.
	 */
	clear(): void {
		this.commands.clear();
		this.aliases.clear();
	}

	/**
	 * Get a serializable list of commands for API responses.
	 */
	toJSON(): Array<{
		name: string;
		aliases: string[];
		description: string;
		category: CommandCategory;
		inputs: Array<{
			name: string;
			type: string;
			prompt: string;
			required: boolean;
		}>;
	}> {
		return this.all().map((cmd) => ({
			name: cmd.name,
			aliases: cmd.aliases || [],
			description: cmd.description,
			category: cmd.category,
			inputs: cmd.inputs.map((input) => ({
				name: input.name,
				type: input.type,
				prompt: input.prompt,
				required: input.required !== false,
			})),
		}));
	}
}

// Singleton instance
export const commandRegistry = new CommandRegistry();

// Default export for convenience
export default commandRegistry;

// Helper function for registering commands
export function registerCommand(command: CommandDefinition): void {
	commandRegistry.register(command);
}
