// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { ActionResult } from "../action.js";
import { ActionContext, SessionContext } from "../agentInterface.js";
import {
    AppAgentCommandInterface,
    CommandDescriptor,
    CommandDescriptors,
    CommandDescriptorTable,
    CompletionDirection,
    CompletionGroups,
} from "../command.js";

import {
    ParameterDefinitions,
    ParsedCommandParams,
    PartialParsedCommandParams,
} from "../parameters.js";

export {
    resolveFlag,
    getFlagMultiple,
    getFlagType,
} from "./parameterHelpers.js";

// Command handlers MAY return an ActionResult — when they do, the
// dispatcher's command pipeline runs the same post-execution processing it
// uses for actions (display content, pendingChoice / yes-no card,
// dynamicDisplayId). Returning void / undefined keeps the legacy "use
// actionIO directly" pattern. Both are supported.
export type CommandHandlerNoParams = CommandDescriptor & {
    parameters?: undefined | false;
    run(
        context: ActionContext<unknown>,
        params: undefined,
        attachments?: string[],
    ): Promise<ActionResult | undefined | void>;
    getCompletion?: undefined;
};

export type CommandHandler = CommandDescriptor & {
    parameters: ParameterDefinitions;
    run(
        context: ActionContext<unknown>,
        params: ParsedCommandParams<ParameterDefinitions>,
        attachments?: string[],
    ): Promise<ActionResult | undefined | void>;
    getCompletion?(
        context: SessionContext<unknown>,
        params: PartialParsedCommandParams<ParameterDefinitions>,
        names: string[],
        direction?: CompletionDirection,
    ): Promise<CompletionGroups>;
};

type CommandHandlerTypes = CommandHandlerNoParams | CommandHandler;

function isCommandHandlerNoParams(
    handler: CommandHandlerTypes,
): handler is CommandHandlerNoParams {
    return handler.parameters === undefined || handler.parameters === false;
}

type CommandDefinitions = CommandHandlerTypes | CommandHandlerTable;

export interface CommandHandlerTable extends CommandDescriptorTable {
    description: string;
    commands: Record<string, CommandDefinitions>;
    defaultSubCommand?: CommandHandlerTypes | string | undefined;
}

function getDefaultSubCommand(
    table: CommandHandlerTable,
): CommandHandlerTypes | undefined {
    if (typeof table.defaultSubCommand === "string") {
        const defaultSubCommand = table.commands[table.defaultSubCommand];
        if (
            defaultSubCommand !== undefined &&
            isCommandDescriptorTable(defaultSubCommand)
        ) {
            return undefined;
        }
        return defaultSubCommand;
    }
    return table.defaultSubCommand;
}

function hasCompletion(handlers: CommandDefinitions): boolean {
    if (isCommandDescriptorTable(handlers)) {
        const defaultSubCommand = getDefaultSubCommand(handlers);
        return (
            (defaultSubCommand !== undefined &&
                hasCompletion(defaultSubCommand)) ||
            Object.values(handlers.commands).some(hasCompletion)
        );
    }
    return handlers.getCompletion !== undefined;
}

export function isCommandDescriptorTable(
    entry: CommandDescriptors,
): entry is CommandDescriptorTable {
    return (entry as CommandDescriptorTable).commands !== undefined;
}

function getCommandHandler(
    handlers: CommandDefinitions,
    commands: string[],
): CommandHandlerTypes {
    let curr: CommandDefinitions = handlers;
    const commandPrefix: string[] = [];

    for (const command of commands) {
        commandPrefix.push(command);
        if (!isCommandDescriptorTable(curr)) {
            throw new Error(
                `Unknown subcommand '${commands.join(" ")}' in '@${commandPrefix.join(" ")}'`,
            );
        }
        const next: CommandDefinitions | undefined = curr.commands[command];
        if (next === undefined) {
            throw new Error(
                `Unknown command '${command}' in '@${commandPrefix.join(" ")}'`,
            );
        }
        curr = next;
    }

    if (!isCommandDescriptorTable(curr)) {
        return curr;
    }

    const defaultSubCommand = getDefaultSubCommand(curr);
    if (defaultSubCommand === undefined) {
        throw new Error(
            `Command '@${commandPrefix.join(" ")}' requires a subcommand`,
        );
    }
    return defaultSubCommand;
}

export function getCommandInterface(
    handlers: CommandDefinitions,
): AppAgentCommandInterface {
    const commandInterface: AppAgentCommandInterface = {
        getCommands: async () => handlers,
        executeCommand: async (
            commands: string[],
            params: ParsedCommandParams<ParameterDefinitions> | undefined,
            context: ActionContext<unknown>,
            attachments?: string[],
        ): Promise<ActionResult | undefined> => {
            const handler = getCommandHandler(handlers, commands);

            // Pass through the handler's return value so the dispatcher's
            // command pipeline can post-process ActionResult fields. void is
            // coerced to undefined.
            if (isCommandHandlerNoParams(handler)) {
                if (params !== undefined) {
                    throw new Error(
                        `Command '@${commands.join(" ")}' does not accept parameters`,
                    );
                }
                const result = await handler.run(
                    context,
                    undefined,
                    attachments,
                );
                return result ?? undefined;
            } else {
                if (params === undefined) {
                    throw new Error(
                        `Command '@${commands.join(" ")}' expects parameters`,
                    );
                }
                const result = await handler.run(context, params, attachments);
                return result ?? undefined;
            }
        },
    };

    if (hasCompletion(handlers)) {
        commandInterface.getCommandCompletion = async (
            commands: string[],
            params: ParsedCommandParams<ParameterDefinitions>,
            names: string[],
            context: SessionContext<unknown>,
            direction?: CompletionDirection,
        ) => {
            const handler = getCommandHandler(handlers, commands);
            return (
                handler.getCompletion?.(context, params, names, direction) ?? {
                    groups: [],
                }
            );
        };
    }
    return commandInterface;
}
