/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { CommandKind } from '../ui/commands/types.js';
/**
 * Internal registry to track commands and conflicts during resolution.
 */
class CommandRegistry {
    commandMap = new Map();
    conflictsMap = new Map();
    firstEncounters = new Map();
    get finalCommands() {
        return Array.from(this.commandMap.values());
    }
    get conflicts() {
        return Array.from(this.conflictsMap.values());
    }
}
/**
 * Resolves name conflicts among slash commands.
 *
 * Rules:
 * 1. Built-in commands always keep the original name.
 * 2. All other types are prefixed with their source name (e.g. user.name).
 * 3. If multiple non-built-in commands conflict, all of them are renamed.
 */
export class SlashCommandResolver {
    /**
     * Orchestrates conflict resolution by applying renaming rules to ensures
     * every command has a unique name.
     */
    static resolve(allCommands) {
        const registry = new CommandRegistry();
        for (const cmd of allCommands) {
            const originalName = cmd.name;
            let finalName = originalName;
            if (registry.firstEncounters.has(originalName)) {
                // We've already seen a command with this name, so resolve the conflict.
                finalName = this.handleConflict(cmd, registry);
            }
            else {
                // Track the first claimant to report them as the conflict reason later.
                registry.firstEncounters.set(originalName, cmd);
            }
            // Store under final name, ensuring the command object reflects it.
            registry.commandMap.set(finalName, {
                ...cmd,
                name: finalName,
            });
        }
        return {
            finalCommands: registry.finalCommands,
            conflicts: registry.conflicts,
        };
    }
    /**
     * Resolves a name collision by deciding which command keeps the name and which is renamed.
     *
     * @param incoming The command currently being processed that has a name collision.
     * @param registry The internal state of the resolution process.
     * @returns The final name to be assigned to the `incoming` command.
     */
    static handleConflict(incoming, registry) {
        const collidingName = incoming.name;
        const originalClaimant = registry.firstEncounters.get(collidingName);
        // Incoming built-in takes priority. Prefix any existing owner.
        if (incoming.kind === CommandKind.BUILT_IN) {
            this.prefixExistingCommand(collidingName, incoming, registry);
            return collidingName;
        }
        // Incoming non-built-in is renamed to its source-prefixed version.
        const renamedName = this.getRenamedName(incoming.name, this.getPrefix(incoming), registry.commandMap);
        this.trackConflict(registry.conflictsMap, collidingName, originalClaimant, incoming, renamedName);
        // Prefix current owner as well if it isn't a built-in.
        this.prefixExistingCommand(collidingName, incoming, registry);
        return renamedName;
    }
    /**
     * Safely renames the command currently occupying a name in the registry.
     *
     * @param name The name of the command to prefix.
     * @param reason The incoming command that is causing the prefixing.
     * @param registry The internal state of the resolution process.
     */
    static prefixExistingCommand(name, reason, registry) {
        const currentOwner = registry.commandMap.get(name);
        // Only non-built-in commands can be prefixed.
        if (!currentOwner || currentOwner.kind === CommandKind.BUILT_IN) {
            return;
        }
        // Determine the new name for the owner using its source prefix.
        const renamedName = this.getRenamedName(currentOwner.name, this.getPrefix(currentOwner), registry.commandMap);
        // Update the registry: remove the old name and add the owner under the new name.
        registry.commandMap.delete(name);
        const renamedOwner = { ...currentOwner, name: renamedName };
        registry.commandMap.set(renamedName, renamedOwner);
        // Record the conflict so the user can be notified of the prefixing.
        this.trackConflict(registry.conflictsMap, name, reason, currentOwner, renamedName);
    }
    /**
     * Generates a unique name using numeric suffixes if needed.
     */
    static getRenamedName(name, prefix, commandMap) {
        const base = prefix ? `${prefix}.${name}` : name;
        let renamedName = base;
        let suffix = 1;
        while (commandMap.has(renamedName)) {
            renamedName = `${base}${suffix}`;
            suffix++;
        }
        return renamedName;
    }
    /**
     * Returns a suitable prefix for a conflicting command.
     */
    static getPrefix(cmd) {
        switch (cmd.kind) {
            case CommandKind.EXTENSION_FILE:
            case CommandKind.SKILL:
                return cmd.extensionName;
            case CommandKind.MCP_PROMPT:
                return cmd.mcpServerName;
            case CommandKind.USER_FILE:
                return 'user';
            case CommandKind.WORKSPACE_FILE:
                return 'workspace';
            default:
                return undefined;
        }
    }
    /**
     * Logs a conflict event.
     */
    static trackConflict(conflictsMap, originalName, reason, displacedCommand, renamedTo) {
        if (!conflictsMap.has(originalName)) {
            conflictsMap.set(originalName, {
                name: originalName,
                losers: [],
            });
        }
        conflictsMap.get(originalName).losers.push({
            command: displacedCommand,
            renamedTo,
            reason,
        });
    }
}
//# sourceMappingURL=SlashCommandResolver.js.map