import { parseArgs } from 'node:util';
import { spawnSync } from 'node:child_process';
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadConfig } from '../config.js';
import { openDb } from '../db/index.js';
import { AuthService } from '../auth/service.js';

// Nom de l'unit systemd (Linux) et label launchd par défaut (macOS, surchargeable via --label).
const SERVICE_NAME = 'arboretum';
const LAUNCHD_LABEL = 'fr.lidge.arboretum';

export type SupportedPlatform = 'linux' | 'darwin';

export interface InstallFlags {
  port?: string | undefined;
  bind?: string | undefined;
  allowOrigin: string[];
  db?: string | undefined;
  vapidContact?: string | undefined;
  claudeHome?: string | undefined;
  binPath?: string | undefined;
  label: string;
  dryRun: boolean;
  noEnable: boolean;
}

// ─── Fonctions pures (génération de contenu / chemins) ────────────────────────────────

/** macOS (launchd) et Linux (systemd) uniquement ; sinon throw avec un message pédagogique. */
export function detectPlatform(platform: NodeJS.Platform = process.platform): SupportedPlatform {
  if (platform === 'linux' || platform === 'darwin') return platform;
  throw new Error(
    `Automatic service installation is supported on Linux (systemd) and macOS (launchd) only.\n` +
      `On ${platform}, run \`arboretum\` manually or set up your own supervisor.`,
  );
}

export function parseInstallArgs(argv: string[]): InstallFlags {
  const { values } = parseArgs({
    args: argv,
    options: {
      port: { type: 'string' },
      bind: { type: 'string' },
      'allow-origin': { type: 'string', multiple: true },
      db: { type: 'string' },
      'vapid-contact': { type: 'string' },
      'claude-home': { type: 'string' },
      'bin-path': { type: 'string' },
      label: { type: 'string' },
      'dry-run': { type: 'boolean', default: false },
      'no-enable': { type: 'boolean', default: false },
    },
    strict: true,
  });
  return {
    port: values.port,
    bind: values.bind,
    allowOrigin: values['allow-origin'] ?? [],
    db: values.db,
    vapidContact: values['vapid-contact'],
    claudeHome: values['claude-home'],
    binPath: values['bin-path'],
    label: values.label ?? LAUNCHD_LABEL,
    dryRun: values['dry-run'] ?? false,
    noEnable: values['no-enable'] ?? false,
  };
}

/**
 * Flags propagés au service : UNIQUEMENT ceux fournis par l'utilisateur (ordre stable,
 * ExecStart déterministe). On n'ajoute JAMAIS --i-know-this-exposes-a-terminal automatiquement
 * (cf. modèle de sécurité : un service exposé doit être un choix conscient et explicite).
 */
export function buildServiceArgs(flags: InstallFlags): string[] {
  const args: string[] = [];
  if (flags.port) args.push('--port', flags.port);
  if (flags.bind) args.push('--bind', flags.bind);
  for (const origin of flags.allowOrigin) args.push('--allow-origin', origin);
  if (flags.db) args.push('--db', flags.db);
  if (flags.vapidContact) args.push('--vapid-contact', flags.vapidContact);
  if (flags.claudeHome) args.push('--claude-home', flags.claudeHome);
  return args;
}

/** Le `dist/index.js` réellement installé (depuis dist/cli/install.js). */
export function resolveScriptPath(): string {
  return fileURLToPath(new URL('../index.js', import.meta.url));
}

/**
 * Cible exécutable du service. Par défaut node + script (immunisé contre un PATH minimal sous
 * systemd/launchd) ; --bin-path force le wrapper `arboretum` global (suit les upgrades npm i -g).
 */
export function resolveBin(flags: Pick<InstallFlags, 'binPath'>): { exec: string; args: string[] } {
  if (flags.binPath) return { exec: flags.binPath, args: [] };
  return { exec: process.execPath, args: [resolveScriptPath()] };
}

export function systemdUnitPath(): string {
  const configHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config');
  return join(configHome, 'systemd', 'user', `${SERVICE_NAME}.service`);
}

export function launchAgentPlistPath(label: string): string {
  return join(homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
}

export function launchdLogPaths(): { dir: string; out: string; err: string } {
  const dir = join(homedir(), 'Library', 'Logs', 'arboretum');
  return { dir, out: join(dir, 'out.log'), err: join(dir, 'err.log') };
}

export function xmlEscape(s: string): string {
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

// systemd accepte les doubles quotes dans ExecStart ; on ne quote que les tokens à espaces.
function quoteIfNeeded(token: string): string {
  return /\s/.test(token) ? `"${token}"` : token;
}

export function renderSystemdUnit(input: { exec: string; scriptArgs: string[] }): string {
  const execStart = [input.exec, ...input.scriptArgs].map(quoteIfNeeded).join(' ');
  // KillSignal=SIGTERM + TimeoutStopSec=10 collent au drain de runDaemon (SIGTERM → drain 1s → close).
  return `[Unit]
Description=Arboretum — git worktree & Claude Code dashboard
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=${execStart}
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=10
Environment=NODE_ENV=production

[Install]
WantedBy=default.target
`;
}

export function renderLaunchAgentPlist(input: {
  label: string;
  programArguments: string[];
  stdoutPath: string;
  stderrPath: string;
}): string {
  const args = input.programArguments.map((a) => `    <string>${xmlEscape(a)}</string>`).join('\n');
  // KeepAlive/SuccessfulExit=false ≈ Restart=on-failure (ne relance pas après un drain volontaire).
  return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>${xmlEscape(input.label)}</string>
  <key>ProgramArguments</key>
  <array>
${args}
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <dict>
    <key>SuccessfulExit</key>
    <false/>
  </dict>
  <key>StandardOutPath</key>
  <string>${xmlEscape(input.stdoutPath)}</string>
  <key>StandardErrorPath</key>
  <string>${xmlEscape(input.stderrPath)}</string>
  <key>EnvironmentVariables</key>
  <dict>
    <key>NODE_ENV</key>
    <string>production</string>
  </dict>
</dict>
</plist>
`;
}

export function printTokenBanner(token: string, url: string): void {
  console.log('\n┌──────────────────────────────────────────────────────────────────┐');
  console.log('│  First start — your access token (shown once, store it safely):  │');
  console.log('└──────────────────────────────────────────────────────────────────┘');
  console.log(`\n  ${token}\n`);
  console.log(`  Login at: ${url}/\n`);
}

export function printUsage(version: string): void {
  console.log(`Arboretum v${version} — git worktree & Claude Code dashboard

Usage:
  arboretum [flags]            Start the daemon (default)
  arboretum serve [flags]      Start the daemon (explicit alias)
  arboretum install [flags]    Install & start a user service (systemd on Linux, launchd on macOS)
  arboretum uninstall          Stop & remove the user service
  arboretum status             Show the service status
  arboretum help               Show this help

Daemon flags:
  --port <n>                         Port to listen on (default 7317)
  --bind <addr>                      Bind address (default 127.0.0.1)
  --allow-origin <url>               Additional allowed Origin (repeatable)
  --db <path>                        SQLite database path
  --vapid-contact <mailto|url>       VAPID contact subject for Web Push
  --i-know-this-exposes-a-terminal   Acknowledge a non-loopback bind (avoid — prefer Tailscale Serve)

Install flags (daemon flags above are propagated to the service):
  --bin-path <path>                  Use this binary in the service instead of node + script
  --label <id>                       launchd label (macOS only, default ${LAUNCHD_LABEL})
  --dry-run                          Print the unit/plist and commands without applying anything
  --no-enable                        Write the service file but do not enable/start it
`);
}

// ─── Effets de bord (fs + exec) ───────────────────────────────────────────────────────

function run(cmd: string, args: string[], opts?: { check?: boolean }): number {
  const res = spawnSync(cmd, args, { stdio: 'inherit' });
  if (res.error) {
    if ((res.error as NodeJS.ErrnoException).code === 'ENOENT') {
      throw new Error(`Command not found: ${cmd}. Is it installed and on your PATH?`);
    }
    throw res.error;
  }
  const code = res.status ?? 0;
  if (opts?.check && code !== 0) {
    throw new Error(`Command failed (exit ${code}): ${cmd} ${args.join(' ')}`);
  }
  return code;
}

/**
 * Bootstrap du token avec EXACTEMENT les flags du service (même db). Valide aussi le bind
 * (garde-fou de loadConfig). Affiche le token une fois, puis ferme la db avant que le service
 * ne l'ouvre. Skippé en --dry-run par l'appelant.
 */
function bootstrapToken(serviceArgs: string[]): void {
  const config = loadConfig(serviceArgs);
  const url = `http://${config.bind === '0.0.0.0' ? '127.0.0.1' : config.bind}:${config.port}`;
  const db = openDb(config.dbPath);
  try {
    const token = new AuthService(db).ensureBootstrapToken();
    if (token) printTokenBanner(token, url);
    else console.log('\nAn access token already exists in this database — manage tokens from Settings.\n');
  } finally {
    db.close();
  }
}

export async function runInstall(argv: string[]): Promise<void> {
  const platform = detectPlatform();
  const flags = parseInstallArgs(argv);
  const serviceArgs = buildServiceArgs(flags);
  const { exec, args: binArgs } = resolveBin(flags);
  const scriptArgs = [...binArgs, ...serviceArgs];

  if (platform === 'linux') {
    const unit = renderSystemdUnit({ exec, scriptArgs });
    const unitPath = systemdUnitPath();
    if (flags.dryRun) {
      console.log(`# ${unitPath}\n${unit}\n# commands:`);
      console.log('systemctl --user daemon-reload');
      if (!flags.noEnable) {
        console.log(`systemctl --user enable --now ${SERVICE_NAME}`);
        console.log(`loginctl enable-linger ${process.env.USER ?? '$USER'}`);
      }
      return;
    }
    bootstrapToken(serviceArgs);
    mkdirSync(dirname(unitPath), { recursive: true });
    writeFileSync(unitPath, unit);
    console.log(`Wrote ${unitPath}`);
    run('systemctl', ['--user', 'daemon-reload'], { check: true });
    if (!flags.noEnable) {
      run('systemctl', ['--user', 'enable', '--now', SERVICE_NAME], { check: true });
      // enable-linger best-effort : absent en CI / sans session loginctl, non bloquant.
      if (run('loginctl', ['enable-linger', process.env.USER ?? '']) !== 0) {
        console.warn('Warning: could not enable linger — the service may not start at boot.');
      }
    }
    console.log(`\nArboretum service installed. Logs: journalctl --user -u ${SERVICE_NAME} -f`);
    return;
  }

  // macOS (launchd)
  const logs = launchdLogPaths();
  const programArguments = [exec, ...scriptArgs];
  const plist = renderLaunchAgentPlist({
    label: flags.label,
    programArguments,
    stdoutPath: logs.out,
    stderrPath: logs.err,
  });
  const plistPath = launchAgentPlistPath(flags.label);
  const uid = process.getuid?.() ?? 0;
  const target = `gui/${uid}/${flags.label}`;
  if (flags.dryRun) {
    console.log(`# ${plistPath}\n${plist}\n# commands:`);
    console.log(`launchctl bootout ${target}   # best-effort`);
    if (!flags.noEnable) {
      console.log(`launchctl bootstrap gui/${uid} ${plistPath}`);
      console.log(`launchctl enable ${target}`);
      console.log(`launchctl kickstart -k ${target}`);
    }
    return;
  }
  bootstrapToken(serviceArgs);
  mkdirSync(dirname(plistPath), { recursive: true });
  mkdirSync(logs.dir, { recursive: true });
  writeFileSync(plistPath, plist);
  console.log(`Wrote ${plistPath}`);
  if (!flags.noEnable) {
    run('launchctl', ['bootout', target]); // best-effort : ignore "not loaded" (rend bootstrap idempotent)
    run('launchctl', ['bootstrap', `gui/${uid}`, plistPath], { check: true });
    run('launchctl', ['enable', target]);
    run('launchctl', ['kickstart', '-k', target]);
  }
  console.log(`\nArboretum service installed. Logs: ${logs.out}`);
}

export async function runUninstall(argv: string[]): Promise<void> {
  const platform = detectPlatform();
  const flags = parseInstallArgs(argv);
  if (platform === 'linux') {
    const unitPath = systemdUnitPath();
    run('systemctl', ['--user', 'disable', '--now', SERVICE_NAME]); // best-effort
    if (existsSync(unitPath)) {
      rmSync(unitPath);
      console.log(`Removed ${unitPath}`);
    }
    run('systemctl', ['--user', 'daemon-reload']);
    console.log('Arboretum service removed.');
    return;
  }
  const plistPath = launchAgentPlistPath(flags.label);
  const uid = process.getuid?.() ?? 0;
  run('launchctl', ['bootout', `gui/${uid}/${flags.label}`]); // best-effort
  if (existsSync(plistPath)) {
    rmSync(plistPath);
    console.log(`Removed ${plistPath}`);
  }
  console.log('Arboretum service removed.');
}

export async function runStatus(argv: string[]): Promise<void> {
  const platform = detectPlatform();
  const flags = parseInstallArgs(argv);
  if (platform === 'linux') {
    const code = run('systemctl', ['--user', 'status', SERVICE_NAME, '--no-pager']);
    console.log(`\nLogs: journalctl --user -u ${SERVICE_NAME} -f`);
    process.exitCode = code;
    return;
  }
  const uid = process.getuid?.() ?? 0;
  const code = run('launchctl', ['print', `gui/${uid}/${flags.label}`]);
  console.log(`\nLogs: ${launchdLogPaths().out}`);
  process.exitCode = code;
}
