#!/usr/bin/env node

import { createRequire } from 'module';
import { Command } from 'commander';
import { registerBrainCommand } from '@mosaicstack/brain';
import { registerForgeCommand } from '@mosaicstack/forge';
import { registerLogCommand } from '@mosaicstack/log';
import { registerMacpCommand } from '@mosaicstack/macp';
import { registerMemoryCommand } from '@mosaicstack/memory';
import { registerQualityRails } from '@mosaicstack/quality-rails';
import { registerQueueCommand } from '@mosaicstack/queue';
import { registerStorageCommand } from '@mosaicstack/storage';
import { registerTelemetryCommand } from './commands/telemetry.js';
import { registerAgentCommand } from './commands/agent.js';
import { registerInteractionCommand } from './commands/interaction.js';
import { registerConfigCommand } from './commands/config.js';
import { registerFleetCommand } from './commands/fleet.js';
import { registerMissionCommand } from './commands/mission.js';
import { registerUninstallCommand } from './commands/uninstall.js';
// prdy is registered via launch.ts
import { registerLaunchCommands } from './commands/launch.js';
import { registerAuthCommand } from './commands/auth.js';
import { registerFederationCommand } from './commands/federation.js';
import { registerGatewayCommand } from './commands/gateway.js';
import {
  backgroundUpdateCheck,
  checkForAllUpdates,
  formatAllPackagesTable,
  getInstallAllCommand,
  runFrameworkReseed,
  refreshActiveFleetUnits,
  readRosterAgentNames,
  buildRelaunchCommands,
  checkFrameworkDrift,
  FRAMEWORK_RESEED_PACKAGE,
} from './runtime/update-checker.js';
import { runWizard } from './wizard.js';
import { ClackPrompter } from './prompter/clack-prompter.js';
import { HeadlessPrompter } from './prompter/headless-prompter.js';
import { createConfigService } from './config/config-service.js';
import { WizardCancelledError } from './errors.js';
import { DEFAULT_MOSAIC_HOME } from './constants.js';

const _require = createRequire(import.meta.url);
const CLI_VERSION: string = (_require('../package.json') as { version: string }).version;

// Fire-and-forget update check at startup (non-blocking, cached 1h)
try {
  backgroundUpdateCheck();
} catch {
  // Silently ignore — update check is best-effort
}

const program = new Command();

program
  .name('mosaic')
  .description('Mosaic Stack CLI')
  .version(CLI_VERSION)
  .configureHelp({ sortSubcommands: true })
  .addHelpText(
    'after',
    `
Command Groups:

  Runtime:    tui, login, sessions
  Gateway:    gateway
  Framework:  agent, bootstrap, coord, doctor, fleet, init, launch, mission, prdy, seq, sync, upgrade, wizard, yolo
  Platform:   update
  Runtimes:   claude, codex, opencode, pi
`,
  );

// ─── runtime launchers + framework commands ────────────────────────────

registerLaunchCommands(program);

// ─── login ──────────────────────────────────────────────────────────────

program
  .command('login')
  .description('Sign in to a Mosaic gateway')
  .option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
  .option('-e, --email <email>', 'Email address')
  .option('-p, --password <password>', 'Password')
  .action(async (opts: { gateway: string; email?: string; password?: string }) => {
    const { signIn, saveSession } = await import('./auth.js');

    let email = opts.email;
    let password = opts.password;

    if (!email || !password) {
      const readline = await import('node:readline');
      const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
      const ask = (q: string): Promise<string> => new Promise((resolve) => rl.question(q, resolve));

      if (!email) email = await ask('Email: ');
      if (!password) password = await ask('Password: ');
      rl.close();
    }

    try {
      const auth = await signIn(opts.gateway, email, password);
      saveSession(opts.gateway, auth);
      console.log(`Signed in as ${auth.email} (${opts.gateway})`);
    } catch (err) {
      console.error(err instanceof Error ? err.message : String(err));
      process.exit(1);
    }
  });

// ─── tui ────────────────────────────────────────────────────────────────

program
  .command('tui')
  .description('Launch interactive TUI connected to the gateway')
  .option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
  .option('-c, --conversation <id>', 'Resume a conversation by ID')
  .option('-m, --model <modelId>', 'Model ID to use (e.g. gpt-4o, llama3.2)')
  .option('-p, --provider <provider>', 'Provider to use (e.g. openai, ollama)')
  .option('--agent <idOrName>', 'Connect to a specific agent')
  .option('--project <idOrName>', 'Scope session to project')
  .action(
    async (opts: {
      gateway: string;
      conversation?: string;
      model?: string;
      provider?: string;
      agent?: string;
      project?: string;
    }) => {
      const { loadSession, validateSession, signIn, saveSession } = await import('./auth.js');

      // Try loading saved session
      let session = loadSession(opts.gateway);

      if (session) {
        const valid = await validateSession(opts.gateway, session.cookie);
        if (!valid) {
          console.log('Session expired. Please sign in again.');
          session = null;
        }
      }

      // No valid session — prompt for credentials
      if (!session) {
        const { promptLine, promptSecret } = await import('./commands/gateway/login.js');

        console.log(`Sign in to ${opts.gateway}`);
        const email = await promptLine('Email: ');
        const password = await promptSecret('Password: ');

        try {
          const auth = await signIn(opts.gateway, email, password);
          saveSession(opts.gateway, auth);
          session = auth;
          console.log(`Signed in as ${auth.email}\n`);
        } catch (err) {
          console.error(err instanceof Error ? err.message : String(err));
          process.exit(1);
        }
      }

      // Resolve agent ID if --agent was passed by name
      let agentId: string | undefined;
      let agentName: string | undefined;
      if (opts.agent) {
        try {
          const { fetchAgentConfigs } = await import('./tui/gateway-api.js');
          const agents = await fetchAgentConfigs(opts.gateway, session.cookie);
          const match = agents.find((a) => a.id === opts.agent || a.name === opts.agent);
          if (match) {
            agentId = match.id;
            agentName = match.name;
          } else {
            console.error(`Agent "${opts.agent}" not found.`);
            process.exit(1);
          }
        } catch (err) {
          console.error(
            `Failed to resolve agent: ${err instanceof Error ? err.message : String(err)}`,
          );
          process.exit(1);
        }
      }

      // Resolve project ID if --project was passed by name
      let projectId: string | undefined;
      if (opts.project) {
        try {
          const { fetchProjects } = await import('./tui/gateway-api.js');
          const projects = await fetchProjects(opts.gateway, session.cookie);
          const match = projects.find((p) => p.id === opts.project || p.name === opts.project);
          if (match) {
            projectId = match.id;
          } else {
            console.error(`Project "${opts.project}" not found.`);
            process.exit(1);
          }
        } catch (err) {
          console.error(
            `Failed to resolve project: ${err instanceof Error ? err.message : String(err)}`,
          );
          process.exit(1);
        }
      }

      // Auto-create a conversation if none was specified
      let conversationId = opts.conversation;
      if (!conversationId) {
        try {
          const { createConversation } = await import('./tui/gateway-api.js');
          const conv = await createConversation(opts.gateway, session.cookie, {
            ...(projectId ? { projectId } : {}),
          });
          conversationId = conv.id;
        } catch (err) {
          console.error(
            `Failed to create conversation: ${err instanceof Error ? err.message : String(err)}`,
          );
          process.exit(1);
        }
      }

      // Dynamic import to avoid loading React/Ink for other commands
      const { render } = await import('ink');
      const React = await import('react');
      const { TuiApp } = await import('./tui/app.js');

      render(
        React.createElement(TuiApp, {
          gatewayUrl: opts.gateway,
          conversationId,
          sessionCookie: session.cookie,
          initialModel: opts.model,
          initialProvider: opts.provider,
          agentId,
          agentName: agentName ?? undefined,
          projectId,
          version: CLI_VERSION,
        }),
        { exitOnCtrlC: false },
      );
    },
  );

// ─── sessions ───────────────────────────────────────────────────────────

const sessionsCmd = program
  .command('sessions')
  .description('Manage active agent sessions')
  .configureHelp({ sortSubcommands: true });

sessionsCmd
  .command('list')
  .description('List active agent sessions')
  .option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
  .action(async (opts: { gateway: string }) => {
    const { withAuth } = await import('./commands/with-auth.js');
    const auth = await withAuth(opts.gateway);
    const { fetchSessions } = await import('./tui/gateway-api.js');

    try {
      const result = await fetchSessions(auth.gateway, auth.cookie);
      if (result.total === 0) {
        console.log('No active sessions.');
        return;
      }
      console.log(`Active sessions (${result.total}):\n`);
      for (const s of result.sessions) {
        const created = new Date(s.createdAt).toLocaleString();
        const durationSec = Math.round(s.durationMs / 1000);
        console.log(`  ID:       ${s.id}`);
        console.log(`  Model:    ${s.provider}/${s.modelId}`);
        console.log(`  Created:  ${created}`);
        console.log(`  Prompts:  ${s.promptCount}`);
        console.log(`  Duration: ${durationSec}s`);
        if (s.channels.length > 0) {
          console.log(`  Channels: ${s.channels.join(', ')}`);
        }
        console.log('');
      }
    } catch (err) {
      console.error(err instanceof Error ? err.message : String(err));
      process.exit(1);
    }
  });

sessionsCmd
  .command('resume <id>')
  .description('Resume an existing agent session in the TUI')
  .option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
  .action(async (id: string, opts: { gateway: string }) => {
    const { loadSession, validateSession } = await import('./auth.js');

    const session = loadSession(opts.gateway);
    if (!session) {
      console.error('Not signed in. Run `mosaic login` first.');
      process.exit(1);
    }

    const valid = await validateSession(opts.gateway, session.cookie);
    if (!valid) {
      console.error('Session expired. Run `mosaic login` again.');
      process.exit(1);
    }

    const { render } = await import('ink');
    const React = await import('react');
    const { TuiApp } = await import('./tui/app.js');

    render(
      React.createElement(TuiApp, {
        gatewayUrl: opts.gateway,
        conversationId: id,
        sessionCookie: session.cookie,
        version: CLI_VERSION,
      }),
    );
  });

sessionsCmd
  .command('destroy <id>')
  .description('Terminate an active agent session')
  .option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
  .action(async (id: string, opts: { gateway: string }) => {
    const { withAuth } = await import('./commands/with-auth.js');
    const auth = await withAuth(opts.gateway);
    const { deleteSession } = await import('./tui/gateway-api.js');

    try {
      await deleteSession(auth.gateway, auth.cookie, id);
      console.log(`Session ${id} destroyed.`);
    } catch (err) {
      console.error(err instanceof Error ? err.message : String(err));
      process.exit(1);
    }
  });

// ─── auth ────────────────────────────────────────────────────────────────

registerAuthCommand(program);

// ─── gateway ──────────────────────────────────────────────────────────

registerGatewayCommand(program);

// ─── federation ───────────────────────────────────────────────────────

registerFederationCommand(program);

// ─── agent ─────────────────────────────────────────────────────────────

registerAgentCommand(program);

// ─── interaction ───────────────────────────────────────────────────────

registerInteractionCommand(program);

// ─── fleet ─────────────────────────────────────────────────────────────

registerFleetCommand(program);

// ─── config ────────────────────────────────────────────────────────────

registerConfigCommand(program);

// ─── mission ───────────────────────────────────────────────────────────

registerMissionCommand(program);

// ─── brain ──────────────────────────────────────────────────────────────

registerBrainCommand(program);

// ─── forge ───────────────────────────────────────────────────────────────

registerForgeCommand(program);

// ─── macp ────────────────────────────────────────────────────────────────

registerMacpCommand(program);

// ─── quality-rails ──────────────────────────────────────────────────────

registerQualityRails(program);

// ─── log ─────────────────────────────────────────────────────────────────

registerLogCommand(program);

// ─── memory ──────────────────────────────────────────────────────────────

registerMemoryCommand(program);

// ─── queue ───────────────────────────────────────────────────────────────

registerQueueCommand(program);

// ─── storage ─────────────────────────────────────────────────────────────

registerStorageCommand(program);

// ─── uninstall ───────────────────────────────────────────────────────────────

registerUninstallCommand(program);

// ─── telemetry ───────────────────────────────────────────────────────────────

registerTelemetryCommand(program);

// ─── update ─────────────────────────────────────────────────────────────

program
  .command('update')
  .description('Check for and install Mosaic CLI updates')
  .option('--check', 'Check only, do not install')
  .option(
    '--no-reseed',
    'Skip re-seeding framework files into ~/.config/mosaic after the CLI update',
  )
  .option('--relaunch', 'Restart durable fleet agents so the new launcher/runtime takes effect')
  .action(async (opts: { check?: boolean; reseed?: boolean; relaunch?: boolean }) => {
    // checkForAllUpdates imported statically above
    const { execSync } = await import('node:child_process');

    // Re-seed the framework from the freshly-installed package, propagate shipped
    // systemd unit fixes to the active units, and (opt-in) relaunch durable
    // agents. Shared by the "packages updated" and the "framework drift" paths.
    const reseedFramework = (reason: string): void => {
      console.log(reason);
      const reseed = runFrameworkReseed();
      if (!reseed.ok) {
        console.error(
          `\n⚠ Framework re-seed skipped: ${reseed.reason ?? 'unknown'}.\n` +
            '  Activate manually: bash "$(npm root -g)/@mosaicstack/mosaic/framework/install.sh" ' +
            '(MOSAIC_SYNC_ONLY=1 MOSAIC_INSTALL_MODE=keep)',
        );
        return;
      }
      console.log('✔ Framework re-seeded.');
      // Propagate shipped systemd unit fixes to the ACTIVE units (re-seed only
      // touches ~/.config/mosaic/systemd/user; systemd runs ~/.config/systemd/user).
      const units = refreshActiveFleetUnits();
      if (units.refreshed.length > 0) {
        console.log(`✔ Refreshed ${units.refreshed.length} active systemd unit(s).`);
      }
      const agents = readRosterAgentNames();
      if (agents.length === 0) return;
      if (opts.relaunch) {
        console.log(`\nRelaunching ${agents.length} fleet agent(s) to pick up the new runtime…`);
        for (const restart of buildRelaunchCommands(agents)) {
          try {
            execSync(restart.join(' '), { stdio: 'inherit', timeout: 30_000 });
          } catch {
            console.error(`  ⚠ failed to restart agent — run: ${restart.join(' ')}`);
          }
        }
        console.log('✔ Agents relaunched.');
      } else {
        console.log(
          `\nℹ ${agents.length} fleet agent(s) are still running the previous runtime. ` +
            'Restart them to activate the update:\n    mosaic update --relaunch   ' +
            '(or: mosaic fleet restart <agent>)',
        );
      }
    };

    console.log('Checking for updates…');
    const results = checkForAllUpdates({ skipCache: true });

    console.log('');
    console.log(formatAllPackagesTable(results));

    const outdated = results.filter((r: { updateAvailable: boolean }) => r.updateAvailable);
    if (outdated.length === 0) {
      const anyInstalled = results.some((r: { current: string }) => r.current);
      if (!anyInstalled) {
        console.error('No @mosaicstack/* packages are installed.');
        process.exit(1);
      }
      console.log('\n✔ All packages up to date.');
      // #642: the CLI may have been upgraded outside `mosaic update` (e.g. a
      // direct `npm i -g`), leaving the framework files stale even though no
      // package is reported outdated. Detect that via the framework version and
      // re-seed so shipped launcher/runtime fixes still activate.
      const drift = checkFrameworkDrift();
      if (drift.drifted && opts.reseed !== false) {
        reseedFramework(
          `\nFramework drift detected (on-disk v${drift.installed} < bundled v${drift.bundled}) — ` +
            'the CLI was updated outside `mosaic update`. Re-seeding framework files into ' +
            '~/.config/mosaic (data-safe; keeps your edits)…',
        );
      }
      return;
    }

    if (opts.check) {
      process.exit(2); // Signal to callers that an update exists
    }

    console.log(`\nInstalling ${outdated.length} update(s)…`);
    try {
      // Relies on @mosaicstack:registry in ~/.npmrc
      const cmd = getInstallAllCommand(outdated);
      execSync(cmd, {
        stdio: 'inherit',
        timeout: 60_000,
      });
      console.log('\n✔ Updated successfully.');
    } catch {
      console.error('\nUpdate failed. Try manually: bash tools/install.sh');
      process.exit(1);
    }

    // F3-m3 / R13: the CLI is updated, but the framework files in
    // ~/.config/mosaic/ are still the previous version. Re-seed them from the
    // freshly-installed package so shipped launcher/runtime changes ACTIVATE.
    // Re-seed when the framework-bearing package itself updated OR the on-disk
    // framework is older than the freshly-installed one (#642 — e.g. only
    // sibling packages were outdated but the CLI was already ahead).
    const mosaicUpdated = outdated.some(
      (r: { package: string }) => r.package === FRAMEWORK_RESEED_PACKAGE,
    );
    const drift = checkFrameworkDrift();
    if ((mosaicUpdated || drift.drifted) && opts.reseed !== false) {
      reseedFramework(
        '\nRe-seeding framework files into ~/.config/mosaic (data-safe; keeps your edits)…',
      );
    }
  });

// ─── wizard ─────────────────────────────────────────────────────────────

program
  .command('wizard')
  .description('Run the Mosaic installation wizard')
  .option('--non-interactive', 'Run without prompts (uses defaults + flags)')
  .option('--source-dir <path>', 'Source directory for framework files')
  .option('--mosaic-home <path>', 'Target config directory')
  .option('--name <name>', 'Agent name')
  .option('--role <description>', 'Agent role description')
  .option('--style <style>', 'Communication style: direct|friendly|formal')
  .option('--accessibility <prefs>', 'Accessibility preferences')
  .option('--guardrails <rules>', 'Custom guardrails')
  .option('--user-name <name>', 'Your name')
  .option('--pronouns <pronouns>', 'Your pronouns')
  .option('--timezone <tz>', 'Your timezone')
  .action(async (opts: Record<string, string | boolean | undefined>) => {
    // All wizard imports are now static (see top of file)

    try {
      const mosaicHome = (opts['mosaicHome'] as string | undefined) ?? DEFAULT_MOSAIC_HOME;
      const sourceDir = (opts['sourceDir'] as string | undefined) ?? mosaicHome;

      const prompter = opts['nonInteractive'] ? new HeadlessPrompter() : new ClackPrompter();

      const configService = createConfigService(mosaicHome, sourceDir);

      await runWizard({
        mosaicHome,
        sourceDir,
        prompter,
        configService,
        cliOverrides: {
          soul: {
            agentName: opts['name'] as string | undefined,
            roleDescription: opts['role'] as string | undefined,
            communicationStyle: opts['style'] as 'direct' | 'friendly' | 'formal' | undefined,
            accessibility: opts['accessibility'] as string | undefined,
            customGuardrails: opts['guardrails'] as string | undefined,
          },
          user: {
            userName: opts['userName'] as string | undefined,
            pronouns: opts['pronouns'] as string | undefined,
            timezone: opts['timezone'] as string | undefined,
          },
        },
      });
    } catch (err) {
      if (err instanceof WizardCancelledError) {
        console.log('\nWizard cancelled.');
        process.exit(0);
      }
      console.error('Wizard failed:', err);
      process.exit(1);
    }
  });

program.parse();
