import { existsSync } from 'node:fs';
import { join } from 'node:path';
import dotenv from 'dotenv';
import type { BaseMidsceneTools } from '../agent-tools/base-tools';
import {
  TOOL_BEHAVIOR_FLAGS,
  stripBehaviorFlags,
} from '../agent-tools/tool-defaults';
import type {
  ToolDefinition,
  ToolResult,
  ToolResultContent,
} from '../agent-tools/types';
import { getDebug } from '../logger';
import {
  canonicalizeCliArgKeys,
  formatCliValidationError,
  getCliOptionDisplay,
  parseCliArgs,
} from './cli-args';
import { CLIError } from './cli-error';
import { writeCliScreenshotFile } from './screenshot-file';
import {
  cliVerboseErrorMessage,
  cliVerboseFlag,
  compactCliVerboseArgs,
  emitCliVerboseEvent,
  stripVerboseFlag,
  withCliVerboseContext,
} from './verbose';

const debug = getDebug('cli-runner');

interface CLICommand {
  name: string;
  def: ToolDefinition;
  hidden?: boolean;
}

export interface CLIExtraCommand {
  name: string;
  def: ToolDefinition;
  aliases?: string[];
  hidden?: boolean;
}

export interface CLIRunnerOptions {
  stripPrefix?: string;
  argv?: string[];
  version?: string;
  extraCommands?: CLIExtraCommand[];
}

export { parseCliArgs, parseValue } from './cli-args';
export { CLIError, reportCLIError } from './cli-error';

function outputContentItem(item: ToolResultContent, isError: boolean): void {
  switch (item.type) {
    case 'text':
      if (isError) {
        console.error(item.text);
      } else {
        console.log(item.text);
      }
      break;

    case 'image': {
      const filepath = writeCliScreenshotFile(item.data, {
        mimeType: item.mimeType,
        filenamePrefix: 'screenshot',
      });
      console.log(`Screenshot saved: ${filepath}`);
      emitCliVerboseEvent({
        event: 'artifact',
        kind: 'screenshot',
        path: filepath,
        mimeType: item.mimeType,
      });
      break;
    }

    default:
      console.log(`[${item.type} content not displayed in CLI]`);
  }
}

function outputResult(result: ToolResult): void {
  for (const item of result.content) {
    outputContentItem(item, result.isError ?? false);
  }
}

export function removePrefix(name: string, prefix?: string): string {
  if (prefix && name.startsWith(prefix)) {
    return name.slice(prefix.length);
  }
  return name;
}

function printCommandHelp(scriptName: string, cmd: CLICommand): void {
  const { def } = cmd;
  const positionalUsage = (def.cli?.positionals ?? [])
    .map((name) => `<${name}>`)
    .join(' ');
  console.log(
    `\nUsage: ${scriptName} ${cmd.name}${positionalUsage ? ` ${positionalUsage}` : ''} [options]\n`,
  );
  console.log(def.description);

  const globalOptionFlags = new Set(
    getGlobalOptions().map((option) => option.flag),
  );
  const schemaEntries = Object.entries(def.schema).filter(([key]) => {
    if (
      def.cli?.positionals?.includes(key) ||
      def.cli?.options?.[key]?.hidden
    ) {
      return false;
    }
    const { label } = getCliOptionDisplay(key, def.cli?.options?.[key]);
    return !globalOptionFlags.has(label);
  });
  if (schemaEntries.length > 0) {
    const optionWidth = Math.max(
      22,
      ...schemaEntries.map(
        ([key]) =>
          getCliOptionDisplay(key, def.cli?.options?.[key]).label.length,
      ),
    );
    console.log('\nOptions:');
    for (const [key, zodType] of schemaEntries) {
      const { label, aliases } = getCliOptionDisplay(
        key,
        def.cli?.options?.[key],
      );
      const desc = zodType.description ?? '';
      const aliasText =
        aliases.length > 0 ? ` (aliases: ${aliases.join(', ')})` : '';
      console.log(`  ${label.padEnd(optionWidth)} ${desc}${aliasText}`);
    }
  }

  printGlobalOptions();
}

function printVersion(scriptName: string, version: string): void {
  console.log(`${scriptName} v${version}`);
}

function printHelp(
  scriptName: string,
  commands: CLICommand[],
  version?: string,
): void {
  if (version) {
    printVersion(scriptName, version);
    console.log('');
  }
  console.log(`\nUsage: ${scriptName} <command> [options]\n`);
  console.log('Commands:');
  for (const { name, def } of commands.filter((command) => !command.hidden)) {
    console.log(`  ${name.padEnd(30)} ${def.description}`);
  }
  console.log(`  ${'version'.padEnd(30)} Show CLI version`);
  printGlobalOptions();
  console.log(`\nRun "${scriptName} <command> --help" for more info.`);
}

function printGlobalOptions(): void {
  const options = getGlobalOptions();
  const optionWidth = Math.max(...options.map((option) => option.flag.length));

  console.log('\nGlobal Options:');
  for (const option of options) {
    console.log(`  ${option.flag.padEnd(optionWidth)} ${option.description}`);
  }
}

function getGlobalOptions(): Array<{ flag: string; description: string }> {
  return [
    {
      flag: `--${cliVerboseFlag}`,
      description:
        'Print progress while the command is running. act and record print readable progress by default. Use --verbose=jsonl for structured events.',
    },
    ...TOOL_BEHAVIOR_FLAGS.map((flag) => ({
      flag: `--${flag.cli}`,
      description: flag.description,
    })),
  ];
}

type AnyMidsceneTools = BaseMidsceneTools<any, any>;

export async function runToolsCLI(
  tools: AnyMidsceneTools,
  scriptName: string,
  options?: CLIRunnerOptions,
): Promise<void> {
  const inputArgs = options?.argv ?? process.argv.slice(2);
  debug('CLI invoked: %s %s', scriptName, inputArgs.join(' '));

  const {
    rawArgs: argsWithoutVerbose,
    verbose,
    format: verboseFormat,
  } = stripVerboseFlag(inputArgs);

  // Global behavior flags (e.g. `--deep-locate` / `--deep-think`) apply
  // regardless of which command runs. `stripBehaviorFlags` is the single place
  // that knows how they look on the command line: it resolves their defaults
  // and returns the remaining args so the per-command parser never sees them.
  // See https://github.com/web-infra-dev/midscene/issues/2446.
  const { rawArgs, toolDefaults } = stripBehaviorFlags(argsWithoutVerbose);
  if (Object.keys(toolDefaults).length > 0) {
    tools.setToolDefaults?.(toolDefaults);
  }

  // Load .env from cwd before any tool initialization
  const envFile = join(process.cwd(), '.env');
  if (existsSync(envFile)) {
    dotenv.config({ path: envFile });
  }

  await tools.initTools();

  const toolDefinitions = [
    ...tools.getToolDefinitions(),
    ...(tools.getCliToolDefinitions?.() ?? []),
  ];
  const commands: CLICommand[] = toolDefinitions.map((def) => ({
    name: removePrefix(def.name, options?.stripPrefix).toLowerCase(),
    def,
  }));
  if (options?.extraCommands?.length) {
    commands.push(
      ...options.extraCommands.flatMap((cmd) => [
        {
          name: cmd.name.toLowerCase(),
          def: cmd.def,
          hidden: cmd.hidden,
        },
        ...(cmd.aliases ?? []).map((alias) => ({
          name: alias.toLowerCase(),
          def: cmd.def,
          hidden: true,
        })),
      ]),
    );
  }
  const cliVersion = options?.version;

  const [commandName, ...restArgs] = rawArgs;

  if (!commandName || commandName === '--help' || commandName === '-h') {
    debug('showing help (no command or --help flag)');
    printHelp(scriptName, commands, cliVersion);
    return;
  }

  if (
    commandName === '--version' ||
    commandName === '-v' ||
    commandName.toLowerCase() === 'version'
  ) {
    if (!cliVersion) {
      throw new CLIError('Failed to determine CLI version');
    }
    printVersion(scriptName, cliVersion);
    return;
  }

  const match = commands.find(
    (c) => c.name.toLowerCase() === commandName.toLowerCase(),
  );
  if (!match) {
    debug('unknown command: %s', commandName);
    console.error(`Unknown command: ${commandName}`);
    printHelp(scriptName, commands, cliVersion);
    throw new CLIError(`Unknown command: ${commandName}`);
  }

  const positionalNames = match.def.cli?.positionals ?? [];
  const positionalArgs: Record<string, unknown> = {};
  let optionStartIndex = 0;
  while (
    optionStartIndex < restArgs.length &&
    optionStartIndex < positionalNames.length &&
    !restArgs[optionStartIndex].startsWith('-')
  ) {
    positionalArgs[positionalNames[optionStartIndex]] =
      restArgs[optionStartIndex];
    optionStartIndex += 1;
  }
  const parsedArgs = {
    ...positionalArgs,
    ...parseCliArgs(restArgs.slice(optionStartIndex)),
  };
  if (parsedArgs.help === true) {
    debug('showing command help for: %s', match.name);
    printCommandHelp(scriptName, match);
    return;
  }

  const cliValidationError = formatCliValidationError(
    scriptName,
    match.name,
    match.def,
    parsedArgs,
  );
  if (cliValidationError) {
    throw new CLIError(cliValidationError);
  }

  const handlerArgs = canonicalizeCliArgKeys(
    scriptName,
    match.name,
    match.def,
    parsedArgs,
  );

  debug('command: %s, args: %s', match.name, JSON.stringify(handlerArgs));

  const verboseEnabled =
    verbose || match.name === 'act' || match.name === 'record';

  await withCliVerboseContext(
    {
      enabled: verboseEnabled,
      format: verboseFormat,
      scriptName,
      commandName: match.name,
      startedAt: Date.now(),
    },
    async () => {
      const startedAt = Date.now();
      emitCliVerboseEvent({
        event: 'command_start',
        args: compactCliVerboseArgs(handlerArgs),
      });

      try {
        const result = await match.def.handler(handlerArgs);
        debug(
          'command %s completed, isError: %s',
          match.name,
          result.isError ?? false,
        );
        outputResult(result);
        emitCliVerboseEvent({
          event: 'command_done',
          status: result.isError ? 'error' : 'ok',
          durationMs: Date.now() - startedAt,
        });
        if (result.isError) {
          throw new CLIError('Command failed', 1);
        }
      } catch (error) {
        if (
          !(error instanceof CLIError && error.message === 'Command failed')
        ) {
          emitCliVerboseEvent({
            event: 'command_done',
            status: 'error',
            durationMs: Date.now() - startedAt,
            error: cliVerboseErrorMessage(error),
          });
        }
        throw error;
      } finally {
        await tools.destroy();
      }
    },
  );
}
