import { resolveVaultDir, resolveProjectRoot, assertSafeProjectRoot, UnsafeProjectRootError } from '../vault/resolve.js';
import { isProcessAlive } from './shared.js';
import { parseStrictFlags, type ParsedFlags } from './args.js';
import { confirmDestructive } from './confirm.js';
import { loadManifests, resolvePackageRoot } from '../symbionts/detect.js';
import { SymbiontInstaller, removeProjectLaunchers } from '../symbionts/installer.js';
import { resolveMycoHome } from '../grove/paths.js';
import { updateConfig, TierConfigUnreadableError } from '../config/loader.js';
import type { SymbiontManifest } from '../symbionts/manifest-schema.js';
import fs from 'node:fs';
import path from 'node:path';

/** Map uninstall result to human-readable component labels. */
function uninstallLabels(result: ReturnType<SymbiontInstaller['uninstall']>): string[] {
  return [
    result.hooks && 'hooks',
    result.mcp && 'MCP server',
    result.skills && 'skills',
    result.settings && 'settings',
    result.instructions && 'instructions',
  ].filter(Boolean) as string[];
}

const USAGE = `Usage: myco remove [options]

With no flags, removes Myco's global install: unregisters the OS service,
deletes the global launchers, strips Myco's blocks from each detected
agent's user-global config, and unlinks the Myco-shipped skill symlinks.
Captured data under ~/.myco/ is preserved unless --purge is passed.
Prompts for confirmation; pass --yes for non-interactive use.

Passing --project, --symbiont, or --remove-vault switches to project scope.

Options:
  --project [<path>]     Remove Myco's project-local install (defaults to cwd)
  --symbiont <name>      Remove just one symbiont's project-local install
  --purge                Global scope: also delete ~/.myco/ (Grove DB + captured data)
                         Project scope: alias for --remove-vault
  --remove-vault         Project scope: delete the project's .myco/ dir
  --yes                  Skip the confirmation prompt
  -h, --help             Show this help
`;

export async function run(args: string[]): Promise<void> {
  if (args.includes('--help') || args.includes('-h')) {
    process.stdout.write(USAGE);
    return;
  }

  const parsed = parseStrictFlags('myco remove', args, [
    { name: '--project', value: 'optional' },
    { name: '--symbiont', value: 'required' },
    { name: '--purge' },
    { name: '--remove-vault' },
    { name: '--yes' },
    { name: '--help', aliases: ['-h'] },
  ], USAGE);

  // --symbiont removes one agent's registration; the vault-deletion flags
  // don't apply to it. Reject the combination outright — silently
  // dropping a destructive flag is the bug class this command just shed.
  if (parsed.has('--symbiont') && (parsed.has('--remove-vault') || parsed.has('--purge'))) {
    process.stderr.write(`myco remove: --remove-vault/--purge cannot be combined with --symbiont\n\n${USAGE}`);
    process.exit(2);
  }

  // --remove-vault is a project-scope operation: without this routing it
  // would fall through to the machine-wide teardown below (and then be
  // ignored there) — the exact opposite of what the caller asked for.
  if (parsed.has('--symbiont') || parsed.has('--project') || parsed.has('--remove-vault')) {
    return await runProjectRemove(parsed);
  }

  return await runGlobalRemove({
    purge: parsed.has('--purge'),
    assumeYes: parsed.has('--yes'),
  });
}

async function runGlobalRemove(opts: { purge: boolean; assumeYes: boolean }): Promise<void> {
  const { purge, assumeYes } = opts;
  const mycoHome = resolveMycoHome();

  if (!assumeYes) {
    let projectCount = 0;
    try {
      const { currentDaemonVariant } = await import('../grove/paths.js');
      const { listGroves, listRegisteredProjects } = await import('../grove/registry.js');
      for (const grove of listGroves(mycoHome, { servedBy: currentDaemonVariant() })) {
        projectCount += listRegisteredProjects(grove.id, mycoHome).length;
      }
    } catch { /* registry unreadable — the summary still describes the rest */ }
    const summary = [
      'myco remove will tear down the machine-wide Myco install:',
      '  - unregister the Myco OS service',
      "  - strip Myco's blocks from every detected agent's global config",
      `  - clean project-local artifacts in ${projectCount} registered project${projectCount === 1 ? '' : 's'}`,
      purge
        ? `  - DELETE ${mycoHome} (Grove DB + all captured data)`
        : `  - preserve captured data at ${mycoHome} (pass --purge to delete it)`,
    ].join('\n');
    if (!await confirmDestructive(summary)) {
      console.error('Aborted — nothing was removed.');
      process.exitCode = 1;
      return;
    }
  }

  console.log(`Removing Myco global install (${mycoHome})\n`);

  // --- Unregister the OS service (do this first so launchd/systemd
  //     doesn't relaunch the daemon mid-uninstall). ---
  try {
    const { getServiceManager } = await import('../service/manager.js');
    const { detectInstallVariant } = await import('./service.js');
    const { serviceLabel } = await import('../service/labels.js');
    const mgr = getServiceManager();
    if (mgr.supported) {
      await mgr.uninstall(serviceLabel(detectInstallVariant()));
      console.log('  ✓ Unregistered OS service');
    }
  } catch (err) {
    console.log(`  ⚠ Service uninstall skipped: ${(err as Error).message}`);
  }

  // --- Remove per-symbiont global config blocks. ---
  const allManifests = loadManifests();
  const pkgRoot = resolvePackageRoot();
  for (const manifest of allManifests) {
    try {
      const installer = new SymbiontInstaller(
        manifest, '/', pkgRoot, false, undefined, null, 'global',
      );
      if (!installer.isAvailableForScope()) continue;
      const removed = uninstallLabels(installer.uninstall());
      if (removed.length > 0) {
        console.log(`  ✓ Removed from ${manifest.displayName}: ${removed.join(', ')}`);
      }
    } catch (err) {
      console.error(`  ✗ Failed to clean ${manifest.displayName}: ${(err as Error).message}`);
    }
  }

  // --- Delete the global launchers. lstat + unlink so we never follow a
  //     symlink and accidentally clobber an unrelated target. ---
  for (const filename of ['launcher.cjs', 'mcp-launcher.cjs']) {
    const target = path.join(mycoHome, filename);
    try {
      fs.lstatSync(target);
      fs.unlinkSync(target);
      console.log(`  ✓ Removed ${target}`);
    } catch { /* not present */ }
  }

  // --- Clean up project-local artifacts in registered projects whose
  //     Grove this binary owns. Scoping to the binary's daemon variant
  //     prevents `myco-dev remove` from touching prod-served projects;
  //     each binary cleans up its own side. ---
  try {
    const { currentDaemonVariant } = await import('../grove/paths.js');
    const { listGroves, listRegisteredProjects } = await import('../grove/registry.js');
    for (const grove of listGroves(mycoHome, { servedBy: currentDaemonVariant() })) {
      for (const project of listRegisteredProjects(grove.id, mycoHome)) {
        await cleanProjectLocalArtifacts(project.root, pkgRoot);
      }
    }
  } catch (err) {
    console.error(`  ⚠ Per-project cleanup skipped: ${(err as Error).message}`);
  }

  // --- Purge (--purge) ---
  if (purge) {
    try {
      fs.rmSync(mycoHome, { recursive: true, force: true });
      console.log(`  ✓ Purged ${mycoHome}`);
    } catch (err) {
      console.error(`  ✗ Failed to purge ${mycoHome}: ${(err as Error).message}`);
    }
  } else {
    console.log(`  – Preserved Grove DB + captured data at ${mycoHome} (use --purge to delete)`);
  }

  console.log('\nMyco global install removed.');
}

async function cleanProjectLocalArtifacts(projectRoot: string, pkgRoot: string): Promise<void> {
  if (!fs.existsSync(projectRoot)) return;
  for (const manifest of loadManifests()) {
    try {
      const installer = new SymbiontInstaller(manifest, projectRoot, pkgRoot);
      const removed = uninstallLabels(installer.uninstall());
      if (removed.length > 0) {
        console.log(`    ✓ ${path.basename(projectRoot)}: cleared ${manifest.displayName}`);
      }
    } catch { /* per-symbiont failures non-fatal */ }
  }
  // `myco remove` is the full project-local teardown — strip every
  // launcher artifact including the dev pin.
  removeProjectLaunchers(projectRoot, { legacy: true, active: true, runtimeCommand: true });
}

async function runProjectRemove(parsed: ParsedFlags): Promise<void> {
  const projectArg = parsed.value('--project');
  const explicitRoot = projectArg ? path.resolve(projectArg) : null;
  const vaultDir = explicitRoot ? path.join(explicitRoot, '.myco') : resolveVaultDir();
  const projectRoot = explicitRoot ?? resolveProjectRoot(vaultDir);
  const hasConfig = fs.existsSync(path.join(vaultDir, 'myco.yaml'));
  const symbiontName = parsed.value('--symbiont');
  const assumeYes = parsed.has('--yes');
  const removeVault = parsed.has('--remove-vault') || parsed.has('--purge');

  // Safety gate BEFORE any cleanup. Project-scope uninstall resolves
  // config dirs relative to the root, so `--project ~` would strip the
  // user's GLOBAL agent configs (~/.claude, ~/.cursor, …) and
  // `--remove-vault` would rmSync ~/.myco itself. Same guard class as
  // the hook hot path: reject /, $HOME, and home-shaped directories.
  try {
    assertSafeProjectRoot(projectRoot);
  } catch (err) {
    if (err instanceof UnsafeProjectRootError) {
      console.error(`Refusing project-scope removal at ${err.projectRoot}: ${err.reason}.`);
      console.error('Pass the project repository root via --project <path>.');
      process.exit(1);
    }
    throw err;
  }

  if (symbiontName) {
    if (!hasConfig) {
      console.error(`No myco.yaml found in ${vaultDir}. Nothing to remove.`);
      process.exit(1);
    }
    const allManifests = loadManifests();
    const manifest = allManifests.find((m) => m.name === symbiontName);
    if (!manifest) {
      console.error(`Unknown symbiont: ${symbiontName}. Available: ${allManifests.map((m) => m.name).join(', ')}`);
      process.exit(1);
    }

    const pkgRoot = resolvePackageRoot();
    const installer = new SymbiontInstaller(manifest, projectRoot, pkgRoot);
    const removed = uninstallLabels(installer.uninstall());

    if (removed.length > 0) {
      console.log(`  ✓ Removed ${manifest.displayName}: ${removed.join(', ')}`);
    } else {
      console.log(`  – ${manifest.displayName}: nothing to remove`);
    }

    try {
      updateConfig(vaultDir, (c) => {
        if (!c.symbionts?.[symbiontName]) return c;
        const { [symbiontName]: _, ...rest } = c.symbionts;
        return { ...c, symbionts: Object.keys(rest).length > 0 ? rest : undefined };
      });
      console.log(`  ✓ Removed ${symbiontName} from myco.yaml`);
    } catch (err) {
      if (!(err instanceof TierConfigUnreadableError)) throw err;
      // Teardown proceeds: an unparseable myco.yaml has no reachable
      // symbionts block to clean, and remove must not clobber a file the
      // user may still want to repair.
      console.log(`  !! Skipped myco.yaml cleanup — file is unparseable (${err.filePath})`);
    }
    return;
  }

  const allManifests = loadManifests();
  const pkgRoot = resolvePackageRoot();

  // Confirm the vault deletion UP FRONT — before any teardown — so a
  // decline leaves the project fully intact instead of half-removed
  // (hooks stripped, daemon stopped, vault still present).
  if (removeVault && !assumeYes && fs.existsSync(vaultDir)) {
    const confirmed = await confirmDestructive(
      `This removes Myco's project-local install from ${projectRoot} and permanently deletes the vault at ${vaultDir} (captured sessions, spores, and project config).`,
    );
    if (!confirmed) {
      console.error('Aborted — nothing was removed. Re-run with --yes to skip confirmation.');
      process.exitCode = 1;
      return;
    }
  }

  // Orphan remedy: launcher artifacts without a myco.yaml — the state
  // `myco doctor` reports as "orphan project-local launcher stubs" and
  // routes here. There is no config or daemon to act on, but the
  // launcher/per-symbiont artifact cleanup still applies.
  if (!hasConfig) {
    console.log(`No myco.yaml in ${vaultDir} — cleaning orphan launcher artifacts in ${projectRoot}\n`);
    await cleanProjectLocalArtifacts(projectRoot, pkgRoot);
    if (removeVault && fs.existsSync(vaultDir)) {
      fs.rmSync(vaultDir, { recursive: true, force: true });
      console.log(`  ✓ Removed vault at ${vaultDir}`);
    }
    console.log('\nOrphan project-local artifacts cleaned.');
    return;
  }

  console.log(`Removing Myco project-local install from ${projectRoot}\n`);

  const daemonPath = path.join(vaultDir, 'daemon.json');
  try {
    const daemon = JSON.parse(fs.readFileSync(daemonPath, 'utf-8'));
    if (isProcessAlive(daemon.pid)) {
      process.kill(daemon.pid, 'SIGTERM');
      console.log(`  ✓ Stopped daemon (pid ${daemon.pid})`);
    }
    fs.unlinkSync(daemonPath);
  } catch { /* no daemon running */ }

  const configured = allManifests.filter((m) =>
    fs.existsSync(path.join(projectRoot, m.configDir)),
  );

  for (const manifest of configured) {
    try {
      const installer = new SymbiontInstaller(manifest, projectRoot, pkgRoot);
      const removed = uninstallLabels(installer.uninstall());

      if (removed.length > 0) {
        console.log(`  ✓ Removed from ${manifest.displayName}: ${removed.join(', ')}`);
      }
    } catch (err) {
      console.error(`  ✗ Failed to clean ${manifest.displayName}: ${(err as Error).message}`);
    }
  }

  const mcpJsonPath = path.join(projectRoot, '.mcp.json');
  try {
    const config = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'));
    if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) {
      fs.unlinkSync(mcpJsonPath);
      console.log('  ✓ Removed empty .mcp.json');
    }
  } catch { /* doesn't exist or already clean */ }

  if (removeVault) {
    fs.rmSync(vaultDir, { recursive: true, force: true });
    console.log(`  ✓ Removed vault at ${vaultDir}`);
  } else {
    console.log(`  – Vault preserved at ${vaultDir} (use --remove-vault to delete)`);
  }

  console.log('\nMyco project-local install removed.');
}
