import type { Command } from "commander";
import { createLazyCliRuntimeLoader } from "../live-transports/shared/live-transport-cli.js";
import type { MantisDesktopBrowserSmokeOptions } from "./desktop-browser-smoke.runtime.js";
import type { MantisDiscordSmokeOptions } from "./discord-smoke.runtime.js";
import type { MantisBeforeAfterOptions } from "./run.runtime.js";
import type {
  MantisSlackDesktopHydrateMode,
  MantisSlackDesktopSmokeOptions,
} from "./slack-desktop-smoke.runtime.js";
import type {
  MantisTelegramDesktopBuilderOptions,
  MantisTelegramDesktopHydrateMode,
} from "./telegram-desktop-builder.runtime.js";
import type {
  MantisVisualDriverOptions,
  MantisVisualTaskOptions,
  MantisVisualTaskVisionMode,
} from "./visual-task.runtime.js";

type MantisCliRuntime = typeof import("./cli.runtime.js");

const loadMantisCliRuntime = createLazyCliRuntimeLoader<MantisCliRuntime>(
  () => import("./cli.runtime.js"),
);

async function runDiscordSmoke(opts: MantisDiscordSmokeOptions) {
  const runtime = await loadMantisCliRuntime();
  await runtime.runMantisDiscordSmokeCommand(opts);
}

async function runBeforeAfter(opts: MantisBeforeAfterOptions) {
  const runtime = await loadMantisCliRuntime();
  await runtime.runMantisBeforeAfterCommand(opts);
}

async function runDesktopBrowserSmoke(opts: MantisDesktopBrowserSmokeOptions) {
  const runtime = await loadMantisCliRuntime();
  await runtime.runMantisDesktopBrowserSmokeCommand(opts);
}

async function runSlackDesktopSmoke(opts: MantisSlackDesktopSmokeOptions) {
  const runtime = await loadMantisCliRuntime();
  await runtime.runMantisSlackDesktopSmokeCommand(opts);
}

async function runTelegramDesktopBuilder(opts: MantisTelegramDesktopBuilderOptions) {
  const runtime = await loadMantisCliRuntime();
  await runtime.runMantisTelegramDesktopBuilderCommand(opts);
}

async function runVisualDriver(opts: MantisVisualDriverOptions) {
  const runtime = await loadMantisCliRuntime();
  await runtime.runMantisVisualDriverCommand(opts);
}

async function runVisualTask(opts: MantisVisualTaskOptions) {
  const runtime = await loadMantisCliRuntime();
  await runtime.runMantisVisualTaskCommand(opts);
}

type MantisDiscordSmokeCommanderOptions = {
  channelId?: string;
  guildId?: string;
  message?: string;
  outputDir?: string;
  repoRoot?: string;
  skipPost?: boolean;
  tokenFile?: string;
  tokenFileEnv?: string;
  tokenEnv?: string;
};

type MantisBeforeAfterCommanderOptions = {
  baseline?: string;
  candidate?: string;
  credentialRole?: string;
  credentialSource?: string;
  fast?: boolean;
  outputDir?: string;
  providerMode?: string;
  repoRoot?: string;
  scenario?: string;
  skipBuild?: boolean;
  skipInstall?: boolean;
  transport?: string;
};

type MantisDesktopBrowserSmokeCommanderOptions = {
  browserProfileArchiveEnv?: string;
  browserProfileDir?: string;
  browserUrl?: string;
  class?: string;
  crabboxBin?: string;
  htmlFile?: string;
  idleTimeout?: string;
  keepLease?: boolean;
  leaseId?: string;
  machineClass?: string;
  outputDir?: string;
  provider?: string;
  repoRoot?: string;
  ttl?: string;
  videoDuration?: string;
};

type MantisSlackDesktopSmokeCommanderOptions = {
  altModel?: string;
  approvalCheckpoints?: boolean;
  class?: string;
  crabboxBin?: string;
  credentialRole?: string;
  credentialSource?: string;
  fast?: boolean;
  freshPr?: string;
  gatewaySetup?: boolean;
  hydrateMode?: MantisSlackDesktopHydrateMode;
  idleTimeout?: string;
  keepLease?: boolean;
  leaseId?: string;
  machineClass?: string;
  market?: string;
  model?: string;
  outputDir?: string;
  provider?: string;
  providerMode?: string;
  repoRoot?: string;
  scenario?: string[];
  slackChannelId?: string;
  slackUrl?: string;
  ttl?: string;
};

type MantisTelegramDesktopBuilderCommanderOptions = {
  class?: string;
  crabboxBin?: string;
  credentialRole?: string;
  credentialSource?: string;
  gatewaySetup?: boolean;
  hydrateMode?: MantisTelegramDesktopHydrateMode;
  idleTimeout?: string;
  keepLease?: boolean;
  leaseId?: string;
  machineClass?: string;
  outputDir?: string;
  provider?: string;
  repoRoot?: string;
  telegramProfileArchiveEnv?: string;
  telegramProfileDir?: string;
  ttl?: string;
};

type MantisVisualTaskCommanderOptions = {
  browserUrl?: string;
  class?: string;
  crabboxBin?: string;
  duration?: string;
  expectText?: string;
  idleTimeout?: string;
  keepLease?: boolean;
  leaseId?: string;
  machineClass?: string;
  outputDir?: string;
  provider?: string;
  repoRoot?: string;
  settleMs?: string;
  ttl?: string;
  visionMode?: MantisVisualTaskVisionMode;
  visionModel?: string;
  visionPrompt?: string;
  visionTimeoutMs?: string;
};

type MantisVisualDriverCommanderOptions = {
  browserUrl?: string;
  crabboxBin?: string;
  expectText?: string;
  leaseId?: string;
  outputDir?: string;
  provider?: string;
  repoRoot?: string;
  settleMs?: string;
  visionMode?: MantisVisualTaskVisionMode;
  visionModel?: string;
  visionPrompt?: string;
  visionTimeoutMs?: string;
};

function collectString(value: string, previous: string[] = []) {
  return [...previous, value];
}

function parseOptionalInteger(value: string | undefined, label: string) {
  if (value === undefined) {
    return undefined;
  }
  const parsed = Number.parseInt(value, 10);
  if (!Number.isFinite(parsed) || String(parsed) !== value || parsed < 0) {
    throw new Error(`${label} must be a non-negative integer`);
  }
  return parsed;
}

export function registerMantisCli(qa: Command) {
  const mantis = qa
    .command("mantis")
    .description("Run Mantis before/after and live-smoke verification flows");

  mantis
    .command("run")
    .description("Run a Mantis before/after scenario against baseline and candidate refs")
    .requiredOption("--transport <transport>", "Transport to verify; currently only discord")
    .requiredOption("--scenario <id>", "Mantis scenario id to run")
    .requiredOption("--baseline <ref>", "Ref expected to reproduce the bug")
    .requiredOption("--candidate <ref>", "Ref expected to contain the fix")
    .option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
    .option("--output-dir <path>", "Mantis before/after artifact directory")
    .option("--provider-mode <mode>", "QA provider mode", "live-frontier")
    .option("--credential-source <source>", "QA credential source", "convex")
    .option("--credential-role <role>", "QA credential role", "ci")
    .option("--fast", "Enable fast provider mode where supported", true)
    .option("--skip-install", "Skip pnpm install in baseline/candidate worktrees", false)
    .option("--skip-build", "Skip pnpm build in baseline/candidate worktrees", false)
    .action(async (opts: MantisBeforeAfterCommanderOptions) => {
      await runBeforeAfter({
        baseline: opts.baseline,
        candidate: opts.candidate,
        credentialRole: opts.credentialRole,
        credentialSource: opts.credentialSource,
        fastMode: opts.fast,
        outputDir: opts.outputDir,
        providerMode: opts.providerMode,
        repoRoot: opts.repoRoot,
        scenario: opts.scenario,
        skipBuild: opts.skipBuild,
        skipInstall: opts.skipInstall,
        transport: opts.transport,
      });
    });

  mantis
    .command("discord-smoke")
    .description("Verify the Mantis Discord bot can see the guild/channel, post, and react")
    .option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
    .option("--output-dir <path>", "Mantis Discord smoke artifact directory")
    .option("--guild-id <id>", "Override OPENCLAW_QA_DISCORD_GUILD_ID")
    .option("--channel-id <id>", "Override OPENCLAW_QA_DISCORD_CHANNEL_ID")
    .option("--token-env <name>", "Env var containing the Mantis Discord bot token")
    .option("--token-file <path>", "File containing the Mantis Discord bot token")
    .option("--token-file-env <name>", "Env var containing the Mantis Discord bot token file path")
    .option("--message <text>", "Smoke message to post")
    .option("--skip-post", "Only check Discord API visibility; do not post or react", false)
    .action(async (opts: MantisDiscordSmokeCommanderOptions) => {
      await runDiscordSmoke({
        channelId: opts.channelId,
        guildId: opts.guildId,
        message: opts.message,
        outputDir: opts.outputDir,
        repoRoot: opts.repoRoot,
        skipPost: opts.skipPost,
        tokenFile: opts.tokenFile,
        tokenFileEnv: opts.tokenFileEnv,
        tokenEnv: opts.tokenEnv,
      });
    });

  mantis
    .command("desktop-browser-smoke")
    .description(
      "Lease or reuse a Crabbox desktop, open a visible browser, and capture VNC desktop screenshot/video artifacts",
    )
    .option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
    .option("--output-dir <path>", "Mantis desktop browser artifact directory")
    .option("--browser-url <url>", "URL to open in the visible browser")
    .option(
      "--browser-profile-archive-env <name>",
      "Env var containing a base64 .tgz Chrome profile archive to restore before launch",
    )
    .option(
      "--browser-profile-dir <remote-path>",
      "Remote Chrome user-data-dir path to reuse for browser login state",
    )
    .option("--html-file <path>", "Repo-local HTML file to render in the visible browser")
    .option("--crabbox-bin <path>", "Crabbox binary path")
    .option("--provider <provider>", "Crabbox provider")
    .option("--machine-class <class>", "Crabbox machine class")
    .option("--class <class>", "Alias for --machine-class")
    .option("--lease-id <id>", "Reuse an existing Crabbox lease")
    .option("--idle-timeout <duration>", "Crabbox idle timeout")
    .option("--ttl <duration>", "Crabbox maximum lease lifetime")
    .option("--video-duration <seconds>", "Visible desktop recording duration in seconds")
    .option("--keep-lease", "Keep a lease created by this run after a passing smoke")
    .action(async (opts: MantisDesktopBrowserSmokeCommanderOptions) => {
      await runDesktopBrowserSmoke({
        browserProfileArchiveEnv: opts.browserProfileArchiveEnv,
        browserProfileDir: opts.browserProfileDir,
        browserUrl: opts.browserUrl,
        crabboxBin: opts.crabboxBin,
        htmlFile: opts.htmlFile,
        idleTimeout: opts.idleTimeout,
        keepLease: opts.keepLease,
        leaseId: opts.leaseId,
        machineClass: opts.machineClass ?? opts.class,
        outputDir: opts.outputDir,
        provider: opts.provider,
        repoRoot: opts.repoRoot,
        ttl: opts.ttl,
        videoDurationSeconds: parseOptionalInteger(opts.videoDuration, "--video-duration"),
      });
    });

  mantis
    .command("slack-desktop-smoke")
    .description(
      "Lease or reuse a Crabbox VNC desktop, run Slack QA inside it, open Slack in the browser, and capture screenshot/video artifacts",
    )
    .option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
    .option("--output-dir <path>", "Mantis Slack desktop artifact directory")
    .option("--crabbox-bin <path>", "Crabbox binary path")
    .option("--provider <provider>", "Crabbox provider")
    .option("--machine-class <class>", "Crabbox machine class")
    .option("--class <class>", "Alias for --machine-class")
    .option("--market <market>", "Crabbox capacity market: spot or on-demand")
    .option("--lease-id <id>", "Reuse an existing Crabbox lease")
    .option("--fresh-pr <spec>", "Use Crabbox fresh PR checkout instead of syncing the local tree")
    .option("--idle-timeout <duration>", "Crabbox idle timeout")
    .option("--ttl <duration>", "Crabbox maximum lease lifetime")
    .option("--keep-lease", "Keep a lease created by this run after a passing smoke")
    .option("--no-keep-lease", "Stop a lease created by this run after a passing smoke")
    .option("--gateway-setup", "Start a persistent OpenClaw Slack gateway inside the VNC VM")
    .option(
      "--approval-checkpoints",
      "Run Slack approval scenarios with visual checkpoint screenshot acknowledgements",
    )
    .option("--slack-url <url>", "Slack web URL to open in the visible browser")
    .option("--slack-channel-id <id>", "Slack channel id for gateway setup allowlist")
    .option("--provider-mode <mode>", "QA provider mode")
    .option("--hydrate-mode <mode>", "Remote hydrate mode: source or prehydrated")
    .option("--model <ref>", "Primary provider/model ref")
    .option("--alt-model <ref>", "Alternate provider/model ref")
    .option(
      "--scenario <id>",
      "Run only the named Slack QA scenario (repeatable)",
      collectString,
      [],
    )
    .option("--credential-source <source>", "Credential source for Slack QA: env or convex")
    .option("--credential-role <role>", "Credential role for convex auth")
    .option("--fast", "Enable provider fast mode where supported")
    .action(async (opts: MantisSlackDesktopSmokeCommanderOptions) => {
      if (opts.approvalCheckpoints && opts.gatewaySetup) {
        throw new Error("--approval-checkpoints cannot be used with --gateway-setup.");
      }
      await runSlackDesktopSmoke({
        alternateModel: opts.altModel,
        approvalCheckpoints: opts.approvalCheckpoints,
        crabboxBin: opts.crabboxBin,
        credentialRole: opts.credentialRole,
        credentialSource: opts.credentialSource,
        fastMode: opts.fast,
        freshPr: opts.freshPr,
        gatewaySetup: opts.gatewaySetup,
        hydrateMode: opts.hydrateMode,
        idleTimeout: opts.idleTimeout,
        keepLease: opts.keepLease,
        leaseId: opts.leaseId,
        machineClass: opts.machineClass ?? opts.class,
        market: opts.market,
        outputDir: opts.outputDir,
        primaryModel: opts.model,
        provider: opts.provider,
        providerMode: opts.providerMode,
        repoRoot: opts.repoRoot,
        scenarioIds: opts.scenario,
        slackChannelId: opts.slackChannelId,
        slackUrl: opts.slackUrl,
        ttl: opts.ttl,
      });
    });

  mantis
    .command("telegram-desktop-builder")
    .description(
      "Lease or reuse a Crabbox VNC desktop, install Telegram Desktop, configure OpenClaw Telegram with a bot token, and capture screenshot/video artifacts",
    )
    .option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
    .option("--output-dir <path>", "Mantis Telegram desktop builder artifact directory")
    .option("--crabbox-bin <path>", "Crabbox binary path")
    .option("--provider <provider>", "Crabbox provider")
    .option("--machine-class <class>", "Crabbox machine class")
    .option("--class <class>", "Alias for --machine-class")
    .option("--lease-id <id>", "Reuse an existing Crabbox lease")
    .option("--idle-timeout <duration>", "Crabbox idle timeout")
    .option("--ttl <duration>", "Crabbox maximum lease lifetime")
    .option("--keep-lease", "Keep a lease created by this run after a passing builder run")
    .option("--no-keep-lease", "Stop a lease created by this run after a passing builder run")
    .option("--no-gateway-setup", "Install Telegram Desktop only; do not configure OpenClaw")
    .option("--credential-source <source>", "Credential source for Telegram setup: env or convex")
    .option("--credential-role <role>", "Credential role for convex auth")
    .option("--hydrate-mode <mode>", "Remote hydrate mode: source or prehydrated")
    .option(
      "--telegram-profile-archive-env <name>",
      "Env var containing a base64 .tgz Telegram Desktop profile archive",
    )
    .option(
      "--telegram-profile-dir <remote-path>",
      "Remote Telegram Desktop profile dir restored before app launch",
    )
    .action(async (opts: MantisTelegramDesktopBuilderCommanderOptions) => {
      await runTelegramDesktopBuilder({
        crabboxBin: opts.crabboxBin,
        credentialRole: opts.credentialRole,
        credentialSource: opts.credentialSource,
        gatewaySetup: opts.gatewaySetup,
        hydrateMode: opts.hydrateMode,
        idleTimeout: opts.idleTimeout,
        keepLease: opts.keepLease,
        leaseId: opts.leaseId,
        machineClass: opts.machineClass ?? opts.class,
        outputDir: opts.outputDir,
        provider: opts.provider,
        repoRoot: opts.repoRoot,
        telegramProfileArchiveEnv: opts.telegramProfileArchiveEnv,
        telegramProfileDir: opts.telegramProfileDir,
        ttl: opts.ttl,
      });
    });

  mantis
    .command("visual-task")
    .description(
      "Lease or reuse a Crabbox desktop, drive visible browser UI, record MP4, screenshot it, and optionally run image-understanding assertions",
    )
    .option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
    .option("--output-dir <path>", "Mantis visual-task artifact directory")
    .option("--crabbox-bin <path>", "Crabbox binary path")
    .option("--provider <provider>", "Crabbox provider")
    .option("--machine-class <class>", "Crabbox machine class")
    .option("--class <class>", "Alias for --machine-class")
    .option("--lease-id <id>", "Reuse an existing Crabbox lease")
    .option("--idle-timeout <duration>", "Crabbox idle timeout")
    .option("--ttl <duration>", "Crabbox maximum lease lifetime")
    .option("--keep-lease", "Keep a lease created by this run after a passing task")
    .option("--browser-url <url>", "URL to open in the visible browser")
    .option("--duration <duration>", "Desktop recording duration")
    .option("--settle-ms <ms>", "Milliseconds to wait after launch before screenshot")
    .option("--vision-mode <mode>", "Vision mode: image-describe or metadata")
    .option("--vision-prompt <text>", "Prompt for image understanding")
    .option("--vision-model <provider/model>", "Image-capable provider/model ref")
    .option("--vision-timeout-ms <ms>", "Image understanding timeout in milliseconds")
    .option("--expect-text <text>", "Case-insensitive text expected in the vision output")
    .action(async (opts: MantisVisualTaskCommanderOptions) => {
      await runVisualTask({
        browserUrl: opts.browserUrl,
        crabboxBin: opts.crabboxBin,
        duration: opts.duration,
        expectText: opts.expectText,
        idleTimeout: opts.idleTimeout,
        keepLease: opts.keepLease,
        leaseId: opts.leaseId,
        machineClass: opts.machineClass ?? opts.class,
        outputDir: opts.outputDir,
        provider: opts.provider,
        repoRoot: opts.repoRoot,
        settleMs: parseOptionalInteger(opts.settleMs, "--settle-ms"),
        ttl: opts.ttl,
        visionMode: opts.visionMode,
        visionModel: opts.visionModel,
        visionPrompt: opts.visionPrompt,
        visionTimeoutMs: parseOptionalInteger(opts.visionTimeoutMs, "--vision-timeout-ms"),
      });
    });

  mantis
    .command("visual-driver")
    .description(
      "Driver half for Mantis visual-task; launched by Crabbox record --while, then opens browser, screenshots, and runs vision",
    )
    .option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
    .option("--output-dir <path>", "Mantis visual-task artifact directory")
    .option("--crabbox-bin <path>", "Crabbox binary path")
    .option("--provider <provider>", "Crabbox provider")
    .option("--lease-id <id>", "Crabbox lease id")
    .option("--browser-url <url>", "URL to open in the visible browser")
    .option("--settle-ms <ms>", "Milliseconds to wait after launch before screenshot")
    .option("--vision-mode <mode>", "Vision mode: image-describe or metadata")
    .option("--vision-prompt <text>", "Prompt for image understanding")
    .option("--vision-model <provider/model>", "Image-capable provider/model ref")
    .option("--vision-timeout-ms <ms>", "Image understanding timeout in milliseconds")
    .option("--expect-text <text>", "Case-insensitive text expected in the vision output")
    .action(async (opts: MantisVisualDriverCommanderOptions) => {
      await runVisualDriver({
        browserUrl: opts.browserUrl,
        crabboxBin: opts.crabboxBin,
        expectText: opts.expectText,
        leaseId: opts.leaseId,
        outputDir: opts.outputDir,
        provider: opts.provider,
        repoRoot: opts.repoRoot,
        settleMs: parseOptionalInteger(opts.settleMs, "--settle-ms"),
        visionMode: opts.visionMode,
        visionModel: opts.visionModel,
        visionPrompt: opts.visionPrompt,
        visionTimeoutMs: parseOptionalInteger(opts.visionTimeoutMs, "--vision-timeout-ms"),
      });
    });
}
