import { ToolcraftBugError, defineCommand, defineGroup, UserError } from "toolcraft";
import { toMcpPrefix } from "./naming.js";
const CLI_SCOPE = ["cli"];
export function defineClient(options) {
    validateClientName(options.name);
    if (options.auth === undefined) {
        throw new UserError("defineClient requires an auth provider.");
    }
    const mergedChildren = mergeChildren([
        { nodes: options.commands, source: "generated" },
        { nodes: options.handwrittenCommands ?? [], source: "handwritten" },
        { nodes: options.auth.commands.map((command) => cloneNode(command, CLI_SCOPE)), source: "auth" }
    ]);
    return {
        name: options.name,
        mcpPrefix: toMcpPrefix(options.name),
        root: defineGroup({
            name: options.name,
            children: mergedChildren
        }),
        services: {
            baseUrl: options.baseUrl,
            tokenSource: options.auth
        }
    };
}
function mergeChildren(entries) {
    const nodeSources = new Map();
    const merged = [];
    for (const entry of entries) {
        mergeInto(merged, entry.nodes, [], entry.source, nodeSources);
    }
    // defineGroup snapshots its children at construction time, while mergeInto mutates an
    // existing group's children after that snapshot. Re-cloning here re-materializes each
    // merged group so nesting client.root under another group preserves the merged children.
    return merged.map((node) => cloneNode(node));
}
function mergeInto(target, incoming, path, source, nodeSources) {
    for (const candidate of incoming) {
        const nextNode = cloneNode(candidate);
        registerSource(nextNode, source, nodeSources);
        const existing = target.find((node) => node.name === nextNode.name);
        if (existing === undefined) {
            target.push(nextNode);
            continue;
        }
        if (existing.kind !== "group" || nextNode.kind !== "group") {
            throw createCollisionError([...path, nextNode.name], getRegisteredSource(nodeSources, existing), source);
        }
        mergeInto(existing.children, nextNode.children, [...path, nextNode.name], source, nodeSources);
    }
}
function registerSource(node, source, nodeSources) {
    nodeSources.set(node, source);
    if (node.kind === "group") {
        for (const child of node.children) {
            registerSource(child, source, nodeSources);
        }
    }
}
function getRegisteredSource(nodeSources, node) {
    const source = nodeSources.get(node);
    if (source === undefined) {
        throw new ToolcraftBugError("merged command node is missing source metadata.");
    }
    return source;
}
function createCollisionError(path, left, right) {
    return new UserError(`Command path ${JSON.stringify(path.join(" "))} is defined more than once (${left} and ${right}).`);
}
function cloneNode(node, scopeOverride) {
    if (node.kind === "command") {
        return cloneCommand(node, scopeOverride);
    }
    return cloneGroup(node, scopeOverride);
}
function cloneCommand(command, scopeOverride) {
    return defineCommand({
        name: command.name,
        description: command.description,
        examples: command.examples.map((example) => ({
            title: example.title,
            params: { ...example.params }
        })),
        aliases: [...command.aliases],
        positional: [...command.positional],
        params: command.params,
        secrets: { ...command.secrets },
        scope: [...(scopeOverride ?? command.scope)],
        confirm: command.confirm,
        requires: command.requires,
        handler: command.handler,
        render: command.render
    });
}
function cloneGroup(group, scopeOverride) {
    const children = group.children.map((child) => cloneNode(child, scopeOverride));
    const defaultCommand = findCommand(children, group.default?.name);
    return defineGroup({
        name: group.name,
        description: group.description,
        aliases: [...group.aliases],
        scope: cloneScope(group.scope, scopeOverride),
        secrets: { ...group.secrets },
        requires: group.requires,
        children,
        default: defaultCommand
    });
}
function validateClientName(name) {
    if (!isValidClientName(name)) {
        throw new UserError(`Client name ${JSON.stringify(name)} must use lowercase letters, numbers, and hyphens only.`);
    }
}
function isValidClientName(name) {
    if (name.length === 0 || name.startsWith("-") || name.endsWith("-")) {
        return false;
    }
    for (const character of name) {
        if (character === "-") {
            continue;
        }
        if (character >= "a" && character <= "z") {
            continue;
        }
        if (character >= "0" && character <= "9") {
            continue;
        }
        return false;
    }
    return true;
}
function cloneScope(scope, scopeOverride) {
    if (scopeOverride !== undefined) {
        return [...scopeOverride];
    }
    if (scope === undefined) {
        return undefined;
    }
    return [...scope];
}
function findCommand(nodes, name) {
    if (name === undefined) {
        return undefined;
    }
    for (const node of nodes) {
        if (node.kind === "command" && node.name === name) {
            return node;
        }
    }
    return undefined;
}
