/**
 * Axint CLI
 *
 * The command-line interface for the Axint compiler.
 *
 *   axint init [dir]              Scaffold a new Axint project
 *   axint compile <file>          Compile TS intent → Swift App Intent
 *   axint activate                Run a source-free compiler smoke test after install
 *   axint create [dir]            Create a premium Apple-native agent launchpad
 *   axint validate <file>         Validate a compiled intent
 *   axint validate-swift <path...> Validate existing Swift sources against Axint's build-time rules
 *   axint eject <file>            Eject intent to standalone Swift (no vendor lock-in)
 *   axint format <file>           Format a .axint source file in canonical style
 *   axint templates               List bundled intent templates
 *   axint login                   Authenticate with the Axint Registry and unlock fuller repair reports
 *   axint cloud check --source    Run an agent-callable Cloud Check on a file
 *   axint cloud preview --repo    Create an Axint Cloud Preview room for an Apple app
 *   axint cloud status            Show Cloud sign-in and Pro repair-check allowance
 *   axint tokens ingest --source  Convert design tokens into SwiftUI token enums
 *   axint schema compile <file>   Compile compact JSON schemas into Swift
 *   axint suggest <description>   Suggest Apple-native features when MCP is unavailable
 *   axint feature <description>   Generate a multi-file feature package
 *   axint repair <issue>          Plan a project-aware Apple repair loop
 *   axint agent install           Install the local multi-agent project brain
 *   axint agent advice            Ask the local brain for host-specific next moves
 *   axint memory index            Build local project memory from context, proofs, repairs, and feedback
 *   axint feedback create         Create privacy-safe repair feedback packets
 *   axint telemetry status        Inspect source-free adoption telemetry controls
 *   axint publish                 Publish an intent to the Registry
 *   axint add <package>           Install a template from the Registry
 *   axint search [query]          Search the Axint Registry for intent templates
 *   axint watch <file|dir>         Watch intent files and recompile on change
 *   axint status                  Show local package/runtime status and MCP reload steps
 *   axint upgrade                 Check/apply Axint upgrades without losing agent context
 *   axint doctor                  Audit version truth, MCP wiring, and project start files
 *   axint project init            Write Axint project-start files for agent workflows
 *   axint project index           Index the local Apple project into .axint/context
 *   axint project sync-version    Refresh Axint-owned project-pack version hints after an upgrade
 *   axint session start           Start an enforced Axint agent session and refresh context
 *   axint workflow check          Run workflow gates from CLI when MCP is unavailable
 *   axint run                     Run Axint's enforced Apple build/test/runtime loop
 *   axint run status              Show the latest or selected run job state
 *   axint run cancel              Cancel the latest or selected active run job
 *   axint runner once             Execute one BYO-Mac runner Axint job
 *   axint mcp                     Start the MCP server (stdio)
 *   axint mcp status              Show local MCP launch command and reload steps
 *   axint mcp recover             Print same-thread CLI fallback when MCP transport closes
 *   axint xcode install           Install the full Axint Xcode workflow in one pass
 *   axint xcode setup             Configure Axint for Xcode agentic coding
 *   axint xcode guard             Check/write the Axint Xcode drift guard
 *   axint xcode verify            Verify the MCP connection is working
 *   axint xcode fix <path>        Auto-fix mechanical Swift validator errors
 *   axint xcode doctor            Audit environment for Apple-platform agentic coding
 *   axint xcode check             Check a Swift file directly or print the latest Xcode Axint Check summary
 *   axint xcode packet            Print the latest Xcode Fix Packet or AI prompt
 *   axint xcode extension install Install the notarized Axint Source Editor Extension
 *   axint xcode extension status  Report whether the extension is installed
 *   axint --version               Show version
 */

import { Command, InvalidArgumentError } from "commander";
import { readFileSync, existsSync } from "node:fs";
import { resolve, dirname, basename } from "node:path";
import { fileURLToPath } from "node:url";
import type { XcodeCheckOutput } from "./xcode-check.js";
import type { XcodePacketKind, XcodePacketOutput } from "./xcode-packet.js";
import { scaffoldProject } from "./scaffold.js";
import { registerCreate } from "./create.js";
import { registerActivate } from "./activate.js";
import { registerCompile } from "./compile.js";
import { registerValidate } from "./validate.js";
import { registerValidateSwift } from "./validate-swift.js";
import { registerEject } from "./eject.js";
import { registerFormat } from "./format.js";
import { registerTemplates } from "./templates.js";
import { registerLogin } from "./login.js";
import { registerCloud } from "./cloud.js";
import { registerTokens } from "./tokens.js";
import { registerSchema } from "./schema.js";
import { registerSuggest } from "./suggest.js";
import { registerFeature } from "./feature.js";
import { registerRepair } from "./repair.js";
import { registerAgent } from "./agent.js";
import { registerMemory } from "./memory.js";
import { registerFeedback } from "./feedback.js";
import { registerTelemetry } from "./telemetry.js";
import { registerPublish } from "./publish.js";
import { registerAdd } from "./add.js";
import { registerSearch } from "./search.js";
import { registerWatch } from "./watch.js";
import { registerStatus, renderCliStatus, renderMcpRecoveryPacket } from "./status.js";
import { registerUpgrade } from "./upgrade.js";
import { registerDoctor } from "./doctor.js";
import { registerProject } from "./project.js";
import { registerSession } from "./session.js";
import { registerWorkflow } from "./workflow.js";
import { registerRun } from "./run.js";
import { registerPaintTest } from "./paint-test.js";
import { registerXcodeGuard } from "./xcode-guard.js";
import {
  renderProjectStartPack,
  writeProjectStartPack,
  type ProjectAgent,
  type ProjectMcpMode,
} from "../project/start-pack.js";
import {
  installAxintLocalAgent,
  renderAxintAgentInstallReport,
} from "../project/local-agent.js";
import {
  commandPathFor,
  inferAxintHost,
  recordAdoptionEvent,
} from "../telemetry/adoption.js";

const __dirname = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync(resolve(__dirname, "../../package.json"), "utf-8"));
const VERSION = pkg.version as string;

const program = new Command();
const XCODE_PACKET_KINDS = ["any", "compile", "validate"] as const;
const XCODE_CHECK_FORMATS = ["markdown", "json", "prompt", "path"] as const;
const XCODE_PACKET_FORMATS = ["markdown", "prompt", "json", "path"] as const;
const INIT_AGENT_CHOICES = [
  "all",
  "claude",
  "codex",
  "cowork",
  "cursor",
  "xcode",
] as const;
const INIT_MCP_MODE_CHOICES = ["local", "remote"] as const;
const activeTelemetryCommand: {
  current: { command: string; host: string } | null;
} = { current: null };

if (
  basename(process.argv[1] ?? "") === "create-axint-app" &&
  process.argv[2] !== "create"
) {
  process.argv.splice(2, 0, "create");
}

function parseChoice<T extends string>(
  label: string,
  value: string,
  choices: readonly T[]
): T {
  const normalized = value.trim().toLowerCase();
  if ((choices as readonly string[]).includes(normalized)) {
    return normalized as T;
  }

  throw new InvalidArgumentError(
    `invalid ${label}: ${value} (expected one of ${choices.join(", ")})`
  );
}

program
  .name("axint")
  .description(
    "The open-source compiler that transforms AI agent definitions into native Apple App Intents."
  )
  .enablePositionalOptions()
  .version(VERSION)
  .addHelpText(
    "after",
    `
Linux sandbox / Cowork lane:
  Hosts without a Swift toolchain (Cowork, Linux CI, restricted containers)
  can still run validate-swift, workflow check, cloud check, suggest, and
  feature directly with:

    npx -y -p @axint/compiler@${VERSION} axint <command>

  The build/test proof step still needs a Mac with xcodebuild.
`
  );

program.hook("preAction", async (_thisCommand, actionCommand) => {
  const command = commandPathFor(actionCommand);
  if (!command || command === "telemetry" || command.startsWith("telemetry ")) {
    return;
  }
  const host = inferAxintHost(actionCommand.opts?.() as Record<string, unknown>);
  activeTelemetryCommand.current = { command, host };

  await recordAdoptionEvent({
    source: "cli",
    eventName: "cli_command_started",
    version: VERSION,
    command,
    host,
  });
});

program.hook("postAction", async (_thisCommand, actionCommand) => {
  const command = commandPathFor(actionCommand);
  if (!command || command === "telemetry" || command.startsWith("telemetry ")) {
    return;
  }
  const host = inferAxintHost(actionCommand.opts?.() as Record<string, unknown>);

  await recordAdoptionEvent({
    source: "cli",
    eventName: "cli_command_completed",
    version: VERSION,
    command,
    host,
    result:
      typeof process.exitCode === "number" && process.exitCode !== 0
        ? "nonzero_exit"
        : "ok",
  });
  activeTelemetryCommand.current = null;
});

// ─── init ────────────────────────────────────────────────────────────

program
  .command("init")
  .description(
    "Scaffold a new Axint project, or initialize Axint inside an existing Apple/Xcode project"
  )
  .argument("[dir]", "Project directory (defaults to current dir)", ".")
  .option(
    "-t, --template <name>",
    "Starter template (send-message, create-event, book-ride, ...)",
    "create-event"
  )
  .option("--no-install", "Skip running `npm install`")
  .option("--name <name>", "Project name (defaults to the directory name)")
  .option(
    "--apple-project",
    "Initialize Axint in an existing Apple/Xcode project instead of scaffolding a new compiler package"
  )
  .option(
    "--agent <agent>",
    "Agent host lane for existing Apple projects: all, claude, codex, cowork, cursor, or xcode",
    (value) => parseChoice("agent", value, INIT_AGENT_CHOICES),
    "all" as ProjectAgent
  )
  .option(
    "--mode <mode>",
    "MCP mode for existing Apple projects: local or remote",
    (value) => parseChoice("mode", value, INIT_MCP_MODE_CHOICES),
    "local" as ProjectMcpMode
  )
  .option(
    "--force",
    "Overwrite existing Axint project-start files when using --apple-project"
  )
  .action(
    async (
      dir: string,
      options: {
        template: string;
        install: boolean;
        name?: string;
        appleProject?: boolean;
        agent: ProjectAgent;
        mode: ProjectMcpMode;
        force?: boolean;
      }
    ) => {
      const targetDir = resolve(dir);
      const projectName = options.name ?? basename(targetDir);

      try {
        if (options.appleProject) {
          const pack = writeProjectStartPack({
            targetDir,
            projectName,
            agent: options.agent,
            mode: options.mode,
            version: VERSION,
            force: options.force ?? false,
          });
          const localAgent = installAxintLocalAgent({
            cwd: targetDir,
            projectName,
            agent: options.agent,
            force: options.force ?? false,
          });

          console.log(renderProjectStartPack(pack));
          console.log();
          console.log(renderAxintAgentInstallReport(localAgent));
          console.log(
            `  \x1b[38;5;208m◆\x1b[0m \x1b[1mAxint\x1b[0m · existing Apple project ready`
          );
          console.log();
          console.log(`  \x1b[1mNext:\x1b[0m`);
          console.log(
            `    axint agent advice --dir ${targetDir} --agent ${options.agent}`
          );
          console.log(
            `    axint run --dir ${targetDir} --agent ${options.agent} --dry-run`
          );
          console.log();
          return;
        }

        const result = await scaffoldProject({
          targetDir,
          projectName,
          template: options.template,
          version: VERSION,
          install: options.install,
        });

        console.log();
        console.log(`  \x1b[38;5;208m◆\x1b[0m \x1b[1mAxint\x1b[0m · project ready`);
        console.log();
        console.log(
          `    \x1b[2m${result.files.length} files written to ${targetDir}\x1b[0m`
        );
        console.log();
        console.log(`  \x1b[1mNext:\x1b[0m`);
        if (dir !== ".") console.log(`    cd ${dir}`);
        if (options.install) {
          console.log(`    npm run compile`);
        } else {
          console.log(`    npm install`);
          console.log(`    npm run compile`);
        }
        console.log();
        console.log(
          `  \x1b[2mDocs:   https://github.com/agenticempire/axint#readme\x1b[0m`
        );
        console.log(
          `  \x1b[2mMCP:    .vscode/mcp.json is ready for Cursor, Claude Code, and Windsurf\x1b[0m`
        );
        console.log();
      } catch (err: unknown) {
        console.error(`\x1b[31merror:\x1b[0m ${(err as Error).message ?? err}`);
        process.exit(1);
      }
    }
  );

// ─── subcommands ──────────────────────────────────────────────────────

registerCompile(program);
registerActivate(program);
registerValidate(program);
registerValidateSwift(program);
registerEject(program);
registerFormat(program);
registerTemplates(program);
registerCreate(program, VERSION);
registerLogin(program);
registerCloud(program);
registerTokens(program);
registerSchema(program);
registerSuggest(program);
registerFeature(program);
registerRepair(program);
registerAgent(program);
registerMemory(program);
registerFeedback(program);
registerTelemetry(program);
registerPublish(program, VERSION);
registerAdd(program, VERSION);
registerSearch(program, VERSION);
registerWatch(program);
registerStatus(program, VERSION);
registerUpgrade(program, VERSION);
registerDoctor(program, VERSION);
registerProject(program, VERSION);
registerSession(program, VERSION);
registerWorkflow(program);
registerRun(program, VERSION);
registerPaintTest(program);

// ─── mcp ─────────────────────────────────────────────────────────────

const mcp = program
  .command("mcp")
  .description(
    "Start the Axint MCP server (stdio) for Claude Code, Cursor, Windsurf, Zed, or any MCP client"
  )
  .action(async () => {
    const { startMCPServer } = await import("../mcp/server.js");
    await startMCPServer();
  });

mcp
  .command("status")
  .description("Show local Axint MCP launch details and reload steps")
  .option("--format <format>", "Output format: markdown, json, or prompt", "markdown")
  .action((options: { format?: "markdown" | "json" | "prompt" }) => {
    console.log(renderCliStatus(VERSION, options.format ?? "markdown"));
  });

mcp
  .command("install")
  .description(
    "Install Axint into a host-level MCP config so agents do not fall back to CLI"
  )
  .option("--agent <agent>", "Agent host to configure: codex, claude, or all", "codex")
  .option("--remote", "Use the hosted remote MCP endpoint instead of local stdio")
  .option("--force", "Replace an existing axint MCP server entry for the selected host")
  .option("--dry-run", "Print the install plan without changing host config")
  .option("--format <format>", "Output format: markdown or json", "markdown")
  .action(
    async (options: {
      agent?: string;
      remote?: boolean;
      force?: boolean;
      dryRun?: boolean;
      format?: string;
    }) => {
      const { installMcpHosts, parseMcpInstallFormat, renderMcpInstallReport } =
        await import("./mcp-install.js");
      const format = parseMcpInstallFormat(options.format ?? "markdown");
      const report = installMcpHosts({
        agent: options.agent,
        remote: options.remote ?? false,
        force: options.force ?? false,
        dryRun: options.dryRun ?? false,
      });
      console.log(renderMcpInstallReport(report, format));
      if (report.status === "fail") process.exitCode = 1;
    }
  );

mcp
  .command("recover")
  .description("Print a same-thread CLI recovery packet when MCP transport closes")
  .option("--dir <dir>", "Project directory", ".")
  .option("--agent <agent>", "Agent host lane, e.g. codex, claude, cursor, xcode", "all")
  .option("--session-token <token>", "Existing Axint session token if one is known")
  .action((options: { dir?: string; agent?: string; sessionToken?: string }) => {
    console.log(
      renderMcpRecoveryPacket(VERSION, {
        dir: options.dir,
        agent: options.agent,
        sessionToken: options.sessionToken,
      })
    );
  });

// ─── xcode ──────────────────────────────────────────────────────────

const xcode = program
  .command("xcode")
  .description("Xcode integration setup and management");

xcode
  .command("install")
  .description(
    "Install the full Axint Xcode workflow: MCP setup, guarded project files, context index, and verification"
  )
  .option("--agent <agent>", "Which agent to configure (claude, codex, all)", "claude")
  .option("--remote", "Use the hosted remote MCP endpoint instead of local stdio")
  .option(
    "--local-build",
    "Use this checkout's built dist/mcp/register.js instead of the npm package"
  )
  .option("--project <dir>", "Project directory for guarded setup", ".")
  .option("--name <name>", "Project name for guarded setup")
  .option("--no-verify", "Skip the final MCP verification pass")
  .action(
    async (options: {
      agent: string;
      remote: boolean;
      localBuild?: boolean;
      project?: string;
      name?: string;
      verify?: boolean;
    }) => {
      const { installXcodeWorkflow } = await import("./xcode-setup.js");
      await installXcodeWorkflow(options);
    }
  );

xcode
  .command("setup")
  .description("Configure Axint as an MCP server for Xcode's agentic coding workflow")
  .option("--agent <agent>", "Which agent to configure (claude, codex, all)", "all")
  .option("--remote", "Use the hosted remote MCP endpoint instead of local stdio")
  .option(
    "--guarded",
    "Also write project Axint memory and an Xcode guard proof file for this project"
  )
  .option(
    "--local-build",
    "Use this checkout's built dist/mcp/register.js instead of the npm package"
  )
  .option("--project <dir>", "Project directory for guarded setup", ".")
  .option("--name <name>", "Project name for guarded setup")
  .action(
    async (options: {
      agent: string;
      remote: boolean;
      guarded?: boolean;
      localBuild?: boolean;
      project?: string;
      name?: string;
    }) => {
      const { setupXcode } = await import("./xcode-setup.js");
      await setupXcode(options);
    }
  );

registerXcodeGuard(xcode);

xcode
  .command("verify")
  .description("Verify Axint MCP connection is working in Xcode")
  .action(async () => {
    const { verifyXcode } = await import("./xcode-setup.js");
    await verifyXcode();
  });

xcode
  .command("fix")
  .description("Auto-fix mechanical Swift validator errors (dry-run by default)")
  .argument("<path>", "Swift file or directory to fix")
  .option("--apply", "Write changes to disk (omit for a dry-run preview)")
  .action(async (pathArg: string, options: { apply?: boolean }) => {
    const { runXcodeFix } = await import("./xcode-fix.js");
    await runXcodeFix(pathArg, { apply: options.apply ?? false });
  });

xcode
  .command("doctor")
  .description("Audit your environment for Apple-platform agentic coding")
  .action(async () => {
    const { runXcodeDoctor } = await import("./xcode-doctor.js");
    await runXcodeDoctor();
  });

xcode
  .command("check")
  .description(
    "Check a current Swift file directly, or read the latest emitted Xcode Axint Check summary"
  )
  .argument(
    "[file]",
    "Swift file to check directly. Omit to read the latest emitted Xcode packet summary."
  )
  .option(
    "--root <dir>",
    "DerivedData root, plugin work directory, or exact latest.json packet path"
  )
  .option("--source <file>", "Swift file to check directly")
  .option("--project <dir>", "Project root for direct file checks")
  .option(
    "--platform <platform>",
    "Target platform for direct file checks (iOS, macOS, watchOS, visionOS, all)"
  )
  .option("--build-log <text>", "Inline Xcode build log evidence for direct file checks")
  .option("--test-failure <text>", "Inline failing test evidence for direct file checks")
  .option(
    "--runtime-failure <text>",
    "Inline runtime or interaction failure evidence for direct file checks"
  )
  .option("--expected <text>", "Expected behavior for direct file checks")
  .option("--actual <text>", "Actual behavior for direct file checks")
  .option(
    "--changed <files...>",
    "Changed files to pin into the refreshed project context"
  )
  .option(
    "--no-refresh-context",
    "Skip refreshing .axint/context before direct file checks"
  )
  .option(
    "--kind <kind>",
    "Check type to read (any, compile, validate)",
    (value) => parseChoice("kind", value, XCODE_PACKET_KINDS),
    "any"
  )
  .option(
    "--format <format>",
    "Output format (markdown, json, prompt, path)",
    (value) => parseChoice("format", value, XCODE_CHECK_FORMATS),
    "markdown"
  )
  .action(
    async (
      file: string | undefined,
      options: {
        root?: string;
        source?: string;
        project?: string;
        platform?: "iOS" | "macOS" | "watchOS" | "visionOS" | "all";
        buildLog?: string;
        testFailure?: string;
        runtimeFailure?: string;
        expected?: string;
        actual?: string;
        changed?: string[];
        refreshContext?: boolean;
        kind: XcodePacketKind;
        format: XcodeCheckOutput;
      }
    ) => {
      const { runXcodeCheck } = await import("./xcode-check.js");
      await runXcodeCheck({
        root: options.root,
        sourcePath: options.source ?? file,
        project: options.project,
        platform: options.platform,
        xcodeBuildLog: options.buildLog,
        testFailure: options.testFailure,
        runtimeFailure: options.runtimeFailure,
        expectedBehavior: options.expected,
        actualBehavior: options.actual,
        changedFiles: options.changed,
        refreshContext: options.refreshContext,
        kind: options.kind,
        format: options.format,
      });
    }
  );

xcode
  .command("packet")
  .description("Read the latest Xcode Fix Packet emitted by Axint build plugins")
  .option(
    "--root <dir>",
    "DerivedData root, plugin work directory, or exact latest.json packet path"
  )
  .option(
    "--kind <kind>",
    "Packet type to read (any, compile, validate)",
    (value) => parseChoice("kind", value, XCODE_PACKET_KINDS),
    "any"
  )
  .option(
    "--format <format>",
    "Output format (markdown, prompt, json, path)",
    (value) => parseChoice("format", value, XCODE_PACKET_FORMATS),
    "markdown"
  )
  .action(
    async (options: {
      root?: string;
      kind: XcodePacketKind;
      format: XcodePacketOutput;
    }) => {
      const { runXcodePacket } = await import("./xcode-packet.js");
      await runXcodePacket(options);
    }
  );

const xcodeExtension = xcode
  .command("extension")
  .description("Manage the Axint Xcode Source Editor Extension");

xcodeExtension
  .command("install")
  .description("Download and install the latest notarized Axint extension")
  .option("--force", "Replace an existing install with the latest release")
  .option("--dir <dir>", "Install directory (defaults to ~/Applications)")
  .action(async (options: { force?: boolean; dir?: string }) => {
    const { installXcodeExtension } = await import("./xcode-extension.js");
    await installXcodeExtension(options);
  });

xcodeExtension
  .command("status")
  .description("Report whether the Axint extension is installed and its version")
  .action(async () => {
    const { xcodeExtensionStatus } = await import("./xcode-extension.js");
    await xcodeExtensionStatus();
  });

// ─── help layout ─────────────────────────────────────────────────────
// A first-time user should see the seven hero commands before the long
// tail. Everything stays registered and functional — the sections only
// shape `axint --help`.

const HELP_SECTIONS: ReadonlyArray<readonly [string, ReadonlyArray<string>]> = [
  [
    "Core:",
    ["init", "compile", "validate", "validate-swift", "templates", "suggest", "mcp"],
  ],
  [
    "Authoring:",
    [
      "create",
      "activate",
      "eject",
      "format",
      "tokens",
      "schema",
      "feature",
      "watch",
      "paint-test",
    ],
  ],
  [
    "Registry & cloud:",
    ["login", "cloud", "publish", "add", "search", "feedback", "telemetry"],
  ],
  [
    "Project & agents:",
    [
      "repair",
      "agent",
      "memory",
      "project",
      "session",
      "workflow",
      "run",
      "runner",
      "status",
      "upgrade",
      "doctor",
      "xcode",
    ],
  ],
];

const helpSectionByCommand = new Map(
  HELP_SECTIONS.flatMap(([heading, names]) =>
    names.map((name) => [name, heading] as const)
  )
);

for (const command of program.commands) {
  command.helpGroup(helpSectionByCommand.get(command.name()) ?? "Project & agents:");
}

// The implicit `help` command belongs with the hero commands.
program.commandsGroup("Core:");
program.helpCommand(true);

// Helper used by scaffold to avoid a circular import
export function __axintExistsSync(p: string) {
  return existsSync(p);
}

try {
  await program.parseAsync();
} catch (error) {
  const failedCommand = activeTelemetryCommand.current;
  if (failedCommand) {
    await recordAdoptionEvent({
      source: "cli",
      eventName: "cli_command_failed",
      version: VERSION,
      command: failedCommand.command,
      host: failedCommand.host,
      result: "exception",
    });
  }
  throw error;
}
