#!/usr/bin/env node
import { Command } from 'commander';
import { pathToFileURL } from 'node:url';
import { initCommand } from './commands/init.js';
import { actorCommand } from './commands/actor.js';
import { actorRunCommand, packageRunCommand } from './commands/actor-run.js';
import { elementCommand } from './commands/element.js';
import { actorElementCommand } from './commands/actor-element.js';
import { cqrsCommand } from './commands/cqrs.js';
import { webappCommand } from './commands/webapp.js';
import { installCommand } from './commands/install.js';
import { uninstallCommand } from './commands/uninstall.js';
import { listCommand } from './commands/list.js';
import { infoCommand } from './commands/info.js';
import { validateCommand } from './commands/validate.js';
import { packCommand } from './commands/pack.js';
import { forkCommand } from './commands/fork.js';
import { publishCommand } from './commands/publish.js';
import { loginCommand, logoutCommand, whoamiCommand } from './commands/auth.js';
import {
  configGetCommand,
  configListCommand,
  configResetCommand,
  configSetCommand,
} from './commands/config.js';
import {
  registryDoctorCommand,
  registryLoginCommand,
  registryUseCommand,
} from './commands/registry.js';
import { outdatedCommand } from './commands/outdated.js';
import { updateForkCommand } from './commands/update-fork.js';
import { searchCommand } from './commands/search.js';

function fail(err: unknown): never {
  console.error(err instanceof Error ? err.message : String(err));
  process.exit(1);
}

export function createProgram(): Command {
  const program = new Command();
  program
    .name('matrix')
    .description('Matrix SDK CLI for package authors')
    .version('0.1.0');

  program
    .command('init')
    .description('Initialize a new Matrix package')
    .option('--name <name>', 'Package name override')
    .option('--version <version>', 'Package version override')
    .option('--description <description>', 'Package description override')
    .option('-y, --yes', 'Skip prompts and use defaults/options')
    .action(async (options: {
      name?: string;
      version?: string;
      description?: string;
      yes?: boolean;
    }) => {
      await initCommand({
        name: options.name,
        version: options.version,
        description: options.description,
        yes: options.yes,
      });
    });

  const actor = program
    .command('actor [name]')
    .description('Scaffold a new actor package')
    .option('--out <path>', 'Output directory (default: ./<name>)')
    .action(async (name: string | undefined, options: { out?: string }) => {
      if (!name) {
        fail(new Error('actor requires a name. Use `matrix actor run <entry>` for direct actor execution.'));
      }
      await actorCommand(name, { outputDir: options.out });
    });

  actor
    .command('run <entry>')
    .description('Run one actor directly on the selected Space bus without managed host inventory')
    .option('--export <name>', 'Actor export name (default: default)')
    .requiredOption('--mount <mount>', 'Logical actor mount under the selected Space authority root')
    .option('--nats-url <url>', 'NATS broker URL, e.g. nats://127.0.0.1:4222')
    .option('--root <root>', 'Matrix/NATS subject root')
    .option('--space <root>', 'Alias for --root')
    .option('--jwt <jwt>', 'NATS user JWT')
    .option('--seed <seed>', 'NATS user seed')
    .option('--home <path>', 'Credential/config home (also checks ./.matrix and ~/.matrix)')
    .option('--props <json>', 'JSON object assigned as actor bootstrap props')
    .option('--check-op <op>', 'Invoke one actor operation after mount, print the result, and exit')
    .option('--check-payload <json>', 'JSON object passed to --check-op')
    .option('--json', 'Output mount details as JSON')
    .action(async (entry: string, options: {
      export?: string;
      mount: string;
      natsUrl?: string;
      root?: string;
      space?: string;
      jwt?: string;
      seed?: string;
      home?: string;
      props?: string;
      checkOp?: string;
      checkPayload?: string;
      json?: boolean;
    }) => {
      try {
        await actorRunCommand(entry, options);
      } catch (err) {
        fail(err);
      }
    });

  const packageCommand = program
    .command('package')
    .description('Direct package execution commands');

  packageCommand
    .command('run <packageDir>')
    .description('Run one package directly on the selected Space bus without managed host inventory')
    .option('--entry <path>', 'Package entry override')
    .option('--export <name>', 'Package export override')
    .option('--mount <mount>', 'Logical actor mount under the selected Space authority root')
    .option('--nats-url <url>', 'NATS broker URL, e.g. nats://127.0.0.1:4222')
    .option('--root <root>', 'Matrix/NATS subject root')
    .option('--space <root>', 'Alias for --root')
    .option('--jwt <jwt>', 'NATS user JWT')
    .option('--seed <seed>', 'NATS user seed')
    .option('--home <path>', 'Credential/config home (also checks ./.matrix and ~/.matrix)')
    .option('--overrides <json>', 'JSON object passed as package bootstrap overrides')
    .option('--check-op <op>', 'Invoke one actor operation after package mount, print the result, and exit')
    .option('--check-payload <json>', 'JSON object passed to --check-op')
    .option('--json', 'Output mount details as JSON')
    .action(async (packageDir: string, options: {
      entry?: string;
      export?: string;
      mount?: string;
      natsUrl?: string;
      root?: string;
      space?: string;
      jwt?: string;
      seed?: string;
      home?: string;
      overrides?: string;
      checkOp?: string;
      checkPayload?: string;
      json?: boolean;
    }) => {
      try {
        await packageRunCommand(packageDir, options);
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('element <name>')
    .description('Scaffold a new element package')
    .option('--out <path>', 'Output directory (default: ./<name>)')
    .action(async (name: string, options: { out?: string }) => {
      await elementCommand(name, { outputDir: options.out });
    });

  program
    .command('actor-element <name>')
    .description('Scaffold a package containing paired actor + element components')
    .option('--out <path>', 'Output directory (default: ./<name>)')
    .action(async (name: string, options: { out?: string }) => {
      await actorElementCommand(name, { outputDir: options.out });
    });

  program
    .command('cqrs <name>')
    .description('Scaffold a package with CQRS command/query actors')
    .option('--out <path>', 'Output directory (default: ./<name>)')
    .action(async (name: string, options: { out?: string }) => {
      await cqrsCommand(name, { outputDir: options.out });
    });

  program
    .command('webapp <name>')
    .description('Scaffold a browser web app with Shell pattern, federation, and Vite')
    .option('--out <path>', 'Output directory (default: ./<name>)')
    .action(async (name: string, options: { out?: string }) => {
      await webappCommand(name, { outputDir: options.out });
    });

  program
    .command('pack')
    .description('Create distributable tarball for current package')
    .option('--out-dir <path>', 'Output directory for tarball')
    .option('--build-first', 'Run npm build before packing')
    .option('--json', 'Output as JSON')
    .action(async (options: { outDir?: string; buildFirst?: boolean; json?: boolean }) => {
      try {
        const result = await packCommand({
          outDir: options.outDir,
          buildFirst: options.buildFirst,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(`Packed ${result.packageName}@${result.version} -> ${result.tarballPath}`);
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('install <source>')
    .description('Install a Matrix package from directory, tarball, or registry name')
    .option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
    .option('--registry-dir <path>', 'Override local registry root')
    .option('--registry <url>', 'NPM-compatible registry URL')
    .option('--credentials-file <path>', 'Override registry credentials file path')
    .option('-f, --force', 'Overwrite existing installed package')
    .option('--json', 'Output as JSON')
    .action(async (source: string, options: {
      packagesDir?: string;
      registryDir?: string;
      registry?: string;
      credentialsFile?: string;
      force?: boolean;
      json?: boolean;
    }) => {
      try {
        const result = await installCommand(source, {
          packagesDir: options.packagesDir,
          registryDir: options.registryDir,
          registry: options.registry,
          credentialsPath: options.credentialsFile,
          force: options.force,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(`Installed ${result.packageName}@${result.version} -> ${result.targetDir}`);
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('uninstall <packageName>')
    .description('Uninstall a Matrix package from package root')
    .option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
    .option('--json', 'Output as JSON')
    .action(async (packageName: string, options: { packagesDir?: string; json?: boolean }) => {
      try {
        const result = await uninstallCommand(packageName, {
          packagesDir: options.packagesDir,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(`Uninstalled ${result.packageName}`);
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('fork <packageName>')
    .description('Fork an installed package for modification')
    .option('--name <newName>', 'Forked package name')
    .option('--replace-origin', 'Do not rename component type/mount/export fields')
    .option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
    .option('--forked-by <name>', 'Fork author metadata override')
    .option('--json', 'Output as JSON')
    .action(async (packageName: string, options: {
      name?: string;
      replaceOrigin?: boolean;
      packagesDir?: string;
      forkedBy?: string;
      json?: boolean;
    }) => {
      try {
        const result = await forkCommand(packageName, {
          name: options.name,
          replaceOrigin: options.replaceOrigin,
          packagesDir: options.packagesDir,
          forkedBy: options.forkedBy,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(`Forked ${result.sourcePackageName} -> ${result.forkedPackageName}`);
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('publish')
    .description('Publish current package to registry')
    .option('--registry-dir <path>', 'Override local registry root')
    .option('--registry <url>', 'NPM-compatible registry URL')
    .option('--credentials-file <path>', 'Override credentials file path')
    .option('--dry-run', 'Run publish preflight without writing registry artifacts')
    .option('--json', 'Output as JSON')
    .action(async (options: {
      registryDir?: string;
      registry?: string;
      credentialsFile?: string;
      dryRun?: boolean;
      json?: boolean;
    }) => {
      try {
        const result = await publishCommand({
          registryDir: options.registryDir,
          registry: options.registry,
          credentialsPath: options.credentialsFile,
          dryRun: options.dryRun,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(`Published ${result.packageName}@${result.version}`);
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('search <query>')
    .description('Search the configured npm-compatible Matrix package registry')
    .option('--registry-dir <path>', 'Search local Matrix discovery index under this registry root')
    .option('--registry <url>', 'NPM-compatible registry URL')
    .option('--credentials-file <path>', 'Override registry credentials file path')
    .option('--limit <count>', 'Maximum number of results (default: 20)', parseInt)
    .option('--json', 'Output as JSON')
    .action(async (query: string, options: {
      registryDir?: string;
      registry?: string;
      credentialsFile?: string;
      limit?: number;
      json?: boolean;
    }) => {
      try {
        const result = await searchCommand(query, {
          registryDir: options.registryDir,
          registry: options.registry,
          credentialsPath: options.credentialsFile,
          limit: options.limit,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        if (result.results.length === 0) {
          console.log('(no packages found)');
          return;
        }
        for (const entry of result.results) {
          const description = entry.description ? ` - ${entry.description}` : '';
          console.log(`${entry.packageName}@${entry.version}${description}`);
        }
      } catch (err) {
        fail(err);
      }
    });

  const config = program
    .command('config')
    .description('Read and write Matrix CLI configuration');

  config
    .command('get <key>')
    .description('Read a Matrix CLI config value')
    .option('--config-file <path>', 'Override CLI config file path')
    .option('--json', 'Output as JSON')
    .action(async (key: string, options: { configFile?: string; json?: boolean }) => {
      try {
        const result = await configGetCommand(key, {
          configPath: options.configFile,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(result.value ?? '');
      } catch (err) {
        fail(err);
      }
    });

  config
    .command('set <key> <value>')
    .description('Set a Matrix CLI config value')
    .option('--config-file <path>', 'Override CLI config file path')
    .option('--json', 'Output as JSON')
    .action(async (key: string, value: string, options: { configFile?: string; json?: boolean }) => {
      try {
        const result = await configSetCommand(key, value, {
          configPath: options.configFile,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(`${result.key}=${result.value}`);
      } catch (err) {
        fail(err);
      }
    });

  config
    .command('list')
    .description('List Matrix CLI config values')
    .option('--config-file <path>', 'Override CLI config file path')
    .option('--json', 'Output as JSON')
    .action(async (options: { configFile?: string; json?: boolean }) => {
      try {
        const result = await configListCommand({
          configPath: options.configFile,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(`registry=${result.effective.registry}`);
      } catch (err) {
        fail(err);
      }
    });

  config
    .command('reset')
    .description('Reset Matrix CLI config values')
    .option('--config-file <path>', 'Override CLI config file path')
    .option('--json', 'Output as JSON')
    .action(async (options: { configFile?: string; json?: boolean }) => {
      try {
        const result = await configResetCommand({
          configPath: options.configFile,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(`Reset ${result.configPath}`);
      } catch (err) {
        fail(err);
      }
    });

  const registry = program
    .command('registry')
    .description('Inspect and configure the Matrix package registry');

  registry
    .command('doctor')
    .description('Diagnose npm registry and Matrix scope resolution')
    .option('--registry <url>', 'Registry URL to diagnose')
    .option('--credentials-file <path>', 'Override registry credentials file path')
    .option('--config-file <path>', 'Override CLI config file path')
    .option('--timeout-ms <ms>', 'Network/command timeout in milliseconds', (value) => Number.parseInt(value, 10))
    .option('--json', 'Output as JSON')
    .action(async (options: {
      registry?: string;
      credentialsFile?: string;
      configFile?: string;
      timeoutMs?: number;
      json?: boolean;
    }) => {
      try {
        const result = await registryDoctorCommand({
          registry: options.registry,
          credentialsPath: options.credentialsFile,
          configPath: options.configFile,
          timeoutMs: options.timeoutMs,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(`registry=${result.registry.url}`);
        console.log(`registryReachable=${result.registry.reachable}`);
        console.log(`credentials=${result.credentials.found ? result.credentials.username ?? 'found' : 'missing'}`);
        console.log(`scopeResolution=${result.npm.allScopesMatch ? 'ok' : 'mismatch'}`);
      } catch (err) {
        fail(err);
      }
    });

  registry
    .command('login')
    .description('Store credentials for a Matrix npm registry')
    .requiredOption('--username <name>', 'Registry username')
    .requiredOption('--token <token>', 'Registry token')
    .option('--email <email>', 'Optional account email')
    .option('--expires-at <iso>', 'Optional token expiry timestamp')
    .option('--registry <url>', 'Registry URL')
    .option('--credentials-file <path>', 'Override registry credentials file path')
    .option('--json', 'Output as JSON')
    .action(async (options: {
      username: string;
      token: string;
      email?: string;
      expiresAt?: string;
      registry?: string;
      credentialsFile?: string;
      json?: boolean;
    }) => {
      try {
        const result = await registryLoginCommand({
          username: options.username,
          token: options.token,
          email: options.email,
          expiresAt: options.expiresAt,
          registry: options.registry,
          credentialsPath: options.credentialsFile,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(`Logged in as ${result.username} (${result.registry})`);
      } catch (err) {
        fail(err);
      }
    });

  registry
    .command('use <profile>')
    .description('Set the Matrix package registry profile')
    .option('--registry <url>', 'Registry URL override')
    .option('--config-file <path>', 'Override CLI config file path')
    .option('--json', 'Output as JSON')
    .action(async (profile: string, options: {
      registry?: string;
      configFile?: string;
      json?: boolean;
    }) => {
      try {
        const result = await registryUseCommand(profile, {
          registry: options.registry,
          configPath: options.configFile,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(`registry=${result.registry}`);
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('login')
    .description('Authenticate with a Matrix package registry')
    .option('--username <name>', 'Registry username')
    .option('--token <token>', 'Registry token')
    .option('--email <email>', 'Optional account email')
    .option('--expires-at <iso>', 'Optional token expiry timestamp')
    .option('--registry <url>', 'Registry identifier')
    .option('--credentials-file <path>', 'Override credentials file path')
    .option('--json', 'Output as JSON')
    .action(async (options: {
      username: string;
      token: string;
      email?: string;
      expiresAt?: string;
      registry?: string;
      credentialsFile?: string;
      json?: boolean;
    }) => {
      try {
        if (!options.username || !options.token) {
          throw new Error('MX_AUTH_REQUIRED: username and token are required for registry login');
        }
        const result = await loginCommand({
          username: options.username,
          token: options.token,
          email: options.email,
          expiresAt: options.expiresAt,
          registry: options.registry,
          credentialsPath: options.credentialsFile,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(`Logged in as ${result.username} (${result.registry})`);
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('logout')
    .description('Clear stored registry credentials')
    .option('--registry <url>', 'Registry identifier')
    .option('--credentials-file <path>', 'Override credentials file path')
    .option('--json', 'Output as JSON')
    .action(async (options: {
      registry?: string;
      credentialsFile?: string;
      json?: boolean;
    }) => {
      try {
        const result = await logoutCommand({
          registry: options.registry,
          credentialsPath: options.credentialsFile,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(result.removed ? `Logged out of ${result.registry}` : `No session for ${result.registry}`);
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('whoami')
    .description('Show current registry identity')
    .option('--registry <url>', 'Registry identifier')
    .option('--credentials-file <path>', 'Override credentials file path')
    .option('--json', 'Output as JSON')
    .action(async (options: {
      registry?: string;
      credentialsFile?: string;
      json?: boolean;
    }) => {
      try {
        const result = await whoamiCommand({
          registry: options.registry,
          credentialsPath: options.credentialsFile,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        if (!result.authenticated) {
          console.log(`Not logged in (${result.registry})`);
          return;
        }
        console.log(`${result.username} (${result.registry})`);
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('outdated')
    .description('Check if forked packages have upstream updates')
    .option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
    .option('--registry-dir <path>', 'Override local registry root')
    .option('--json', 'Output as JSON')
    .action(async (options: { packagesDir?: string; registryDir?: string; json?: boolean }) => {
      try {
        const result = await outdatedCommand({
          packagesDir: options.packagesDir,
          registryDir: options.registryDir,
        });
        if (options.json) {
          console.log(JSON.stringify(result.entries, null, 2));
          return;
        }
        if (result.entries.length === 0) {
          console.log('(no forked packages)');
          return;
        }
        for (const entry of result.entries) {
          console.log(`${entry.packageName} ${entry.localOriginVersion} -> ${entry.latestOriginVersion ?? 'n/a'} (${entry.status})`);
        }
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('update-fork <packageName>')
    .description('Update fork metadata to latest upstream version')
    .option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
    .option('--registry-dir <path>', 'Override local registry root')
    .option('--version <version>', 'Target upstream version')
    .option('--json', 'Output as JSON')
    .action(async (packageName: string, options: {
      packagesDir?: string;
      registryDir?: string;
      version?: string;
      json?: boolean;
    }) => {
      try {
        const result = await updateForkCommand(packageName, {
          packagesDir: options.packagesDir,
          registryDir: options.registryDir,
          version: options.version,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        if (result.updated) {
          console.log(`Updated ${result.packageName}: ${result.previousOriginVersion} -> ${result.nextOriginVersion}`);
        } else {
          console.log(`${result.packageName} already up to date (${result.nextOriginVersion})`);
        }
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('list')
    .description('List installed Matrix packages')
    .option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
    .option('--json', 'Output as JSON')
    .action(async (options: { packagesDir?: string; json?: boolean }) => {
      try {
        const result = await listCommand({
          packagesDir: options.packagesDir,
        });
        if (options.json) {
          console.log(JSON.stringify(result.packages, null, 2));
          return;
        }
        if (result.packages.length === 0) {
          console.log('(no installed packages)');
          return;
        }
        for (const packageName of result.packages) {
          console.log(packageName);
        }
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('info <packageName>')
    .description('Show installed package metadata')
    .option('--packages-dir <path>', 'Override packages root (default: ~/.matrix/packages)')
    .option('--json', 'Output as JSON')
    .action(async (packageName: string, options: { packagesDir?: string; json?: boolean }) => {
      try {
        const result = await infoCommand(packageName, {
          packagesDir: options.packagesDir,
        });
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
        console.log(`${result.packageName}`);
        console.log(`path: ${result.installPath}`);
        console.log(`manifest: ${result.hasMatrixJson ? 'matrix.json' : 'package.json.matrix'}`);
      } catch (err) {
        fail(err);
      }
    });

  program
    .command('validate [target]')
    .description('Validate matrix manifest at directory, matrix.json, or package.json')
    .option('--json', 'Output as JSON')
    .action(async (target: string | undefined, options: { json?: boolean }) => {
      try {
        const result = await validateCommand(target);
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
        } else {
          console.log(`package: ${result.packageName}`);
          console.log(`valid: ${result.valid ? 'yes' : 'no'}`);
          if (!result.valid) {
            for (const diagnostic of result.diagnostics) {
              console.log(`- ${diagnostic.code}: ${diagnostic.message}`);
            }
          }
        }
        if (!result.valid) {
          process.exit(1);
        }
      } catch (err) {
        fail(err);
      }
    });

  return program;
}

async function main(): Promise<void> {
  const program = createProgram();
  await program.parseAsync(process.argv);
}

const invokedPath = process.argv[1];
const isDirectExecution = Boolean(invokedPath)
  && pathToFileURL(invokedPath).href === import.meta.url;

if (isDirectExecution) {
  main().catch((err) => {
    console.error('Fatal error:', err);
    process.exit(1);
  });
}
