/**
 * Git worktree management for isolated agent runs.
 *
 * Each run optionally gets its own worktree on a dedicated branch off the
 * current `HEAD`. This keeps `main` clean, isolates heavy per-task build
 * artifacts (e.g. the Rust `target/` dir), and lets multiple runs proceed in
 * parallel without clobbering each other's working tree.
 */

import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { BRANCH_PREFIX, REPO_ROOT, WORKTREE_ROOT } from './config.js';

export interface Worktree {
  path: string;
  branch: string;
}

/** Run a git command, returning trimmed stdout. */
function git(args: string[], opts?: { cwd?: string }): string {
  return execSync(['git', ...args].join(' '), {
    encoding: 'utf8',
    cwd: opts?.cwd ?? REPO_ROOT,
    stdio: ['ignore', 'pipe', 'pipe'],
  }).trim();
}

/** Filesystem path for a run's worktree. */
export function worktreePathFor(runId: string): string {
  return path.join(WORKTREE_ROOT, runId);
}

/** Stable branch name derived from the card id + run id. */
export function branchNameFor(cardId: string, runId: string): string {
  const safe = cardId.replace(/[^a-z0-9-]+/gi, '-').toLowerCase().replace(/^-+|-+$/g, '').slice(0, 24);
  return `${BRANCH_PREFIX}/${safe || 'card'}-${runId.slice(0, 8)}`;
}

/**
 * Create a new worktree on a fresh branch off `HEAD`.
 *
 * Throws if the branch already exists or the worktree path is taken; callers
 * are expected to use a unique run id.
 */
export function createWorktree(runId: string, cardId: string): Worktree {
  fs.mkdirSync(WORKTREE_ROOT, { recursive: true });
  const wtPath = worktreePathFor(runId);
  const branch = branchNameFor(cardId, runId);
  git(['worktree', 'add', '-b', branch, JSON.stringify(wtPath)]);
  return { path: wtPath, branch };
}

/** Short SHA at the head of a worktree (best-effort). */
export function headSha(wtPath: string): string | null {
  try {
    return git(['rev-parse', '--short', 'HEAD'], { cwd: wtPath });
  } catch {
    return null;
  }
}

/** Best-effort uncommitted change summary in a worktree (`git status --short`). */
export function dirtySummary(wtPath: string): string {
  try {
    const out = git(['status', '--short'], { cwd: wtPath });
    if (!out) return '';
    const files = out.split('\n');
    return `${files.length} changed file(s):\n${files.slice(0, 12).join('\n')}${files.length > 12 ? '\n…' : ''}`;
  } catch {
    return '';
  }
}

/** Remove a worktree and delete its branch (best-effort, never throws). */
export function removeWorktree(runId: string, branch: string | null): void {
  const wtPath = worktreePathFor(runId);
  try {
    git(['worktree', 'remove', '--force', JSON.stringify(wtPath)]);
  } catch {
    /* already gone */
  }
  if (branch) {
    try {
      git(['branch', '-D', branch]);
    } catch {
      /* branch may be checked out elsewhere or already deleted */
    }
  }
}
