/**
 * `forge worktree` — list / clean leftover worktrees created by Forge pipelines.
 *
 * The mantis-bug-fix-and-mr and gitlab-issue-fix-and-review builtin pipelines
 * stage their work in `<project>/.forge/worktrees/<id>`. The pipeline auto-
 * cleans on success, but failed runs leave the worktree on disk for
 * inspection — those accumulate over time and bloat the project directory
 * (one full project checkout per failed bug). This command sweeps them.
 *
 * Scope: ONLY `.forge/worktrees/*` directories under each project root.
 * User-created git worktrees elsewhere are never touched.
 */

import { execSync, spawnSync } from 'node:child_process';
import { existsSync, readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';

interface Project { name: string; path: string; language?: string; }

const isTty = !!process.stdout.isTTY;
const dim  = (s: string) => isTty ? `\x1b[2m${s}\x1b[0m` : s;
const red  = (s: string) => isTty ? `\x1b[31m${s}\x1b[0m` : s;
const grn  = (s: string) => isTty ? `\x1b[32m${s}\x1b[0m` : s;
const yel  = (s: string) => isTty ? `\x1b[33m${s}\x1b[0m` : s;
const cyan = (s: string) => isTty ? `\x1b[36m${s}\x1b[0m` : s;

function getBase(): string {
  const portArgIdx = process.argv.findIndex(a => a === '--port');
  const port = portArgIdx >= 0 ? process.argv[portArgIdx + 1] : undefined;
  return process.env.MW_URL || `http://localhost:${port || '8403'}`;
}

async function fetchProjects(): Promise<Project[]> {
  const base = getBase();
  try {
    const res = await fetch(base + '/api/projects');
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (e) {
    throw new Error(`forge unreachable on ${base}: ${(e as Error).message}`);
  }
}

/** Bytes-on-disk for a directory tree. `du -sk` returns kilobytes; we convert. */
function dirSizeBytes(p: string): number {
  try {
    const out = execSync(`du -sk "${p}" 2>/dev/null | awk '{print $1}'`, { encoding: 'utf8' }).trim();
    return Number(out || 0) * 1024;
  } catch { return 0; }
}

function fmtSize(bytes: number): string {
  if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)}G`;
  if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(1)}M`;
  if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)}K`;
  return `${bytes}B`;
}

interface Entry {
  project: string;
  projectPath: string;
  worktreeDir: string;     // absolute
  name: string;            // basename under .forge/worktrees/
  branch: string | null;   // git's reported branch, if any
  registered: boolean;     // appears in `git worktree list`
  bytes: number;
}

function scanProject(p: Project): Entry[] {
  const root = p.path;
  const wtRoot = join(root, '.forge', 'worktrees');
  if (!existsSync(wtRoot)) return [];

  // Map of absolute path → branch (from git's view of registered worktrees)
  const registered = new Map<string, string | null>();
  try {
    const porc = execSync('git worktree list --porcelain', { cwd: root, encoding: 'utf8' });
    let cur: { path?: string; branch?: string } = {};
    for (const line of porc.split('\n')) {
      if (line.startsWith('worktree ')) {
        if (cur.path) registered.set(cur.path, cur.branch ?? null);
        cur = { path: line.slice('worktree '.length).trim() };
      } else if (line.startsWith('branch ')) {
        cur.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '').trim();
      } else if (line === '' && cur.path) {
        registered.set(cur.path, cur.branch ?? null);
        cur = {};
      }
    }
    if (cur.path) registered.set(cur.path, cur.branch ?? null);
  } catch { /* not a git repo; just walk the dir */ }

  const out: Entry[] = [];
  let names: string[];
  try { names = readdirSync(wtRoot); } catch { return []; }
  for (const name of names) {
    const wt = join(wtRoot, name);
    let isDir = false;
    try { isDir = statSync(wt).isDirectory(); } catch { continue; }
    if (!isDir) continue;
    out.push({
      project: p.name, projectPath: root,
      worktreeDir: wt, name,
      branch: registered.get(wt) ?? null,
      registered: registered.has(wt),
      bytes: dirSizeBytes(wt),
    });
  }
  return out;
}

function removeOne(e: Entry): { ok: boolean; msg: string } {
  // Try git worktree remove first — it untracks + deletes the dir together.
  const r = spawnSync('git', ['worktree', 'remove', '--force', e.worktreeDir],
    { cwd: e.projectPath, encoding: 'utf8' });
  if (r.status === 0) {
    // Best-effort drop the local branch the worktree was pinning.
    if (e.branch) {
      spawnSync('git', ['branch', '-D', e.branch], { cwd: e.projectPath, encoding: 'utf8' });
    }
    return { ok: true, msg: 'worktree remove' };
  }

  // Stale registration or never-tracked dir → rm -rf + prune + drop branch.
  const rm = spawnSync('rm', ['-rf', e.worktreeDir], { encoding: 'utf8' });
  if (rm.status !== 0) {
    return { ok: false, msg: `rm -rf failed: ${rm.stderr.trim().slice(0, 200)}` };
  }
  spawnSync('git', ['worktree', 'prune'], { cwd: e.projectPath, encoding: 'utf8' });
  if (e.branch) {
    spawnSync('git', ['branch', '-D', e.branch], { cwd: e.projectPath, encoding: 'utf8' });
  }
  return { ok: true, msg: 'rm -rf + prune' };
}

export async function worktreeCommand(args: string[]): Promise<void> {
  const sub = args[0] || 'list';
  const rest = args.slice(1);
  switch (sub) {
    case 'list':
    case 'ls':
      return cmdList(rest);
    case 'clean':
    case 'rm':
      return cmdClean(rest);
    case '--help':
    case '-h':
    default:
      return printHelp();
  }
}

async function cmdList(args: string[]): Promise<void> {
  const projectFilter = args.find(a => !a.startsWith('-'));
  const projects = await fetchProjects();
  const targets = projectFilter ? projects.filter(p => p.name === projectFilter || p.path === projectFilter) : projects;
  if (projectFilter && targets.length === 0) {
    console.error(red(`project not found: ${projectFilter}`));
    process.exit(1);
  }

  let total = 0, totalBytes = 0;
  for (const p of targets) {
    const entries = scanProject(p);
    if (!entries.length) continue;
    const subBytes = entries.reduce((a, e) => a + e.bytes, 0);
    console.log(`${cyan(p.name)} ${dim(p.path)} ${dim(`(${entries.length}, ${fmtSize(subBytes)})`)}`);
    for (const e of entries) {
      const flag = e.registered ? grn('●') : yel('○');
      const br = e.branch ? dim(e.branch) : dim('(unregistered)');
      console.log(`  ${flag} ${e.name.padEnd(30)} ${fmtSize(e.bytes).padStart(7)}  ${br}`);
    }
    total += entries.length;
    totalBytes += subBytes;
  }
  if (total === 0) console.log(dim('(no Forge worktrees)'));
  else console.log(`\n${total} worktree(s), ${fmtSize(totalBytes)}`);
}

async function cmdClean(args: string[]): Promise<void> {
  const dryRun = args.includes('--dry-run') || args.includes('-n');
  const yes = args.includes('--yes') || args.includes('-y');
  const projectFilter = args.find(a => !a.startsWith('-'));

  const projects = await fetchProjects();
  const targets = projectFilter ? projects.filter(p => p.name === projectFilter || p.path === projectFilter) : projects;
  if (projectFilter && targets.length === 0) {
    console.error(red(`project not found: ${projectFilter}`));
    process.exit(1);
  }

  const all: Entry[] = [];
  for (const p of targets) all.push(...scanProject(p));
  if (all.length === 0) { console.log(dim('(no Forge worktrees to clean)')); return; }

  const totalBytes = all.reduce((a, e) => a + e.bytes, 0);
  console.log(`Found ${all.length} worktree(s), ${fmtSize(totalBytes)} total:`);
  for (const e of all) console.log(`  ${dim(e.project + ':')} ${e.name} ${dim(`(${fmtSize(e.bytes)})`)}`);

  if (dryRun) { console.log(yel('\n--dry-run — nothing removed.')); return; }

  if (!yes && isTty) {
    process.stdout.write(`\nRemove these ${all.length} worktree(s)? [y/N] `);
    const ans = await readOneLine();
    if (!/^y(es)?$/i.test(ans.trim())) { console.log('aborted'); return; }
  }

  let ok = 0, fail = 0;
  for (const e of all) {
    const r = removeOne(e);
    if (r.ok) { console.log(`  ${grn('✓')} ${e.project}/${e.name} ${dim(r.msg)}`); ok++; }
    else      { console.log(`  ${red('✗')} ${e.project}/${e.name} ${red(r.msg)}`); fail++; }
  }
  console.log(`\n${grn(`removed ${ok}`)}${fail ? `, ${red(`${fail} failed`)}` : ''} (${fmtSize(totalBytes)} freed)`);
}

function readOneLine(): Promise<string> {
  return new Promise((resolve) => {
    let buf = '';
    process.stdin.setEncoding('utf8');
    process.stdin.on('data', (chunk) => {
      buf += chunk;
      const i = buf.indexOf('\n');
      if (i >= 0) { process.stdin.pause(); resolve(buf.slice(0, i)); }
    });
    process.stdin.resume();
  });
}

function printHelp(): void {
  console.log(`forge worktree — manage pipeline worktrees under <project>/.forge/worktrees/

Usage:
  forge worktree                   List all (alias of 'list')
  forge worktree list [project]    List worktrees, optionally filtered by project
  forge worktree clean [project]   Remove all worktrees (prompts unless --yes)
    --dry-run, -n                  Show what would be removed
    --yes,     -y                  Don't prompt

Worktrees are only created by Forge builtin pipelines (mantis-bug-fix-and-mr,
gitlab-issue-fix-and-review). Successful runs auto-clean; failed runs leave
the worktree for inspection. This command sweeps the failed leftovers.

Only paths under <project>/.forge/worktrees/ are ever touched.`);
}
