/**
 * 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 */
    }
  }
}

// --- review & merge (human-triggered) --------------------------------------

/** Whether a worktree directory still exists on disk. */
export function isWorktreePresent(wtPath: string): boolean {
  return fs.existsSync(wtPath);
}

/** Current checked-out branch at the main worktree (REPO_ROOT). */
export function currentBranch(): string {
  return git(['rev-parse', '--abbrev-ref', 'HEAD']);
}

/** Empty when the main worktree is clean, else a short summary of changes. */
export function mainDirtySummary(): string {
  const out = git(['status', '--porcelain']);
  if (!out) return '';
  const files = out.split('\n');
  return `${files.length} uncommitted change(s) in the main worktree:\n${files.slice(0, 12).join('\n')}${files.length > 12 ? '\n…' : ''}`;
}

/** True if `branch` is already contained in HEAD (nothing to merge). */
export function isMergedIntoHead(branch: string): boolean {
  try {
    git(['merge-base', '--is-ancestor', branch, 'HEAD']);
    return true;
  } catch {
    return false;
  }
}

export interface CommitLog {
  sha: string;
  subject: string;
}

/** Commits on `branch` that are not yet on HEAD (what a merge would bring in). */
export function commitsAheadOfHead(branch: string): CommitLog[] {
  let out: string;
  try {
    out = git(['log', '--no-decorate', '--pretty=format:%h%x09%s', `HEAD..${branch}`]);
  } catch {
    return [];
  }
  if (!out) return [];
  return out.split('\n').map((line) => {
    const idx = line.indexOf('\t');
    return idx >= 0
      ? { sha: line.slice(0, idx), subject: line.slice(idx + 1) }
      : { sha: line, subject: '' };
  });
}

/** Compact `--stat` summary of what `branch` changed since it forked from HEAD. */
export function diffStat(branch: string): string {
  try {
    return git(['diff', '--stat', `HEAD...${branch}`]);
  } catch {
    return '';
  }
}

/** Full diff of what `branch` changed since it forked from HEAD, size-capped. */
export function diffPatch(branch: string, maxBytes = 60_000): { patch: string; truncated: boolean } {
  let raw = '';
  try {
    raw = execSync(`git diff HEAD...${branch}`, {
      encoding: 'utf8',
      cwd: REPO_ROOT,
      stdio: ['ignore', 'pipe', 'ignore'],
      maxBuffer: 10 * 1024 * 1024,
    });
  } catch {
    return { patch: '', truncated: false };
  }
  if (raw.length <= maxBytes) return { patch: raw, truncated: false };
  return { patch: raw.slice(0, maxBytes), truncated: true };
}

export interface MergeResult {
  ok: boolean;
  /** Branch was already an ancestor of HEAD — merge was a no-op. */
  alreadyMerged: boolean;
  /** Branch the merge targeted (the main worktree's checked-out branch). */
  target: string;
  branch: string;
  output: string;
}

/** Outcome of a single branch in a batch merge. */
export interface BatchMergeItem extends MergeResult {
  /** The run this branch belonged to (for UI correlation). */
  runId: string;
  /** True if this branch conflicted (whether or not resolution succeeded). */
  conflict: boolean;
  /** When an agent resolved a conflict for this branch, its run id; else null. */
  resolutionRunId: string | null;
  /** True if the branch couldn't be merged because it no longer resolves to a
   * commit (worktree/branch cleaned up but the run row lingered). Such branches
   * are skipped — the batch continues — rather than halting the whole run. */
  skipped: boolean;
}

/** Result of merging several branches sequentially into the main branch. */
export interface BatchMergeResult {
  target: string;
  /** One entry per candidate branch, in the order processed. */
  items: BatchMergeItem[];
  /** The branch that conflicted and stopped the batch, if any. Branches after
   * it were not attempted. */
  haltedOn: string | null;
  /** True if the main worktree was dirty and nothing was attempted. */
  aborted: boolean;
  /** Human-readable reason when `aborted` or `haltedOn`. */
  reason: string | null;
  /** Branch ref snapshotting HEAD before the batch, so the operator can reset
   * the whole batch (`git reset --hard <recoveryRef>`) if a resolution misfires. */
  recoveryRef: string | null;
}

/** Whether a merge is currently in progress in the main worktree (MERGE_HEAD). */
export function hasMergeInProgress(): boolean {
  try {
    execSync('git rev-parse -q --verify MERGE_HEAD', { cwd: REPO_ROOT, stdio: 'ignore' });
    return true;
  } catch {
    return false;
  }
}

/** Files with unresolved merge conflicts in the main worktree. */
export function unmergedPaths(): string[] {
  try {
    const out = git(['diff', '--name-only', '--diff-filter=U']);
    return out ? out.split('\n').filter(Boolean) : [];
  } catch {
    return [];
  }
}

/**
 * Resolve a ref to its commit SHA, or null if it doesn't resolve (branch deleted,
 * never created, dangling). Used to skip stale run records whose worktree+branch
 * were cleaned up out-of-band but whose row still carries a `branch` value.
 */
export function resolveRef(ref: string): string | null {
  try {
    return git(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]);
  } catch {
    return null;
  }
}

/** Abort an in-progress merge in the main worktree (best-effort). */
export function abortMerge(): void {
  try {
    execSync('git merge --abort', { cwd: REPO_ROOT, stdio: 'ignore' });
  } catch {
    /* not mid-merge */
  }
}

/** Outcome of a non-aborting merge attempt. */
export interface AttemptMergeResult {
  /** Clean merge committed, or branch was already in HEAD. */
  ok: boolean;
  alreadyMerged: boolean;
  /** True when the merge stopped with conflicts (tree left mid-merge). */
  conflict: boolean;
  /** Conflicted file paths when `conflict`. */
  conflictedPaths: string[];
  output: string;
}

/**
 * Attempt to merge `branch` into the main worktree WITHOUT aborting on conflict.
 *
 * On conflict the main tree is left mid-merge (MERGE_HEAD present, conflict
 * markers in files) so a conflict-resolution agent can work on it; the caller
 * must then either complete it or call `abortMerge()`. Returns the conflicted
 * paths so the resolver can be pointed at them. Non-conflict failures are
 * aborted so the tree isn't left half-merged.
 */
export function attemptMerge(branch: string): AttemptMergeResult {
  const target = currentBranch();
  if (isMergedIntoHead(branch)) {
    return { ok: true, alreadyMerged: true, conflict: false, conflictedPaths: [], output: `${branch} is already in ${target}.` };
  }
  try {
    const out = execSync(
      `git merge --no-ff --no-edit -m ${JSON.stringify(`Merge agent branch ${branch} into ${target}`)} ${branch}`,
      { encoding: 'utf8', cwd: REPO_ROOT, stdio: ['ignore', 'pipe', 'pipe'] },
    );
    return { ok: true, alreadyMerged: false, conflict: false, conflictedPaths: [], output: out.trim() || `Merged ${branch} into ${target}.` };
  } catch (err) {
    const stderr = (err as { stderr?: Buffer }).stderr?.toString().trim() ?? '';
    const conflictedPaths = unmergedPaths();
    // A genuine conflict leaves the tree mid-merge for the resolver; anything
    // else is an outright failure, so clean up so the tree isn't half-merged.
    if (conflictedPaths.length === 0) {
      abortMerge();
      return { ok: false, alreadyMerged: false, conflict: false, conflictedPaths: [], output: stderr || `Merge of ${branch} failed (aborted; ${target} left clean).` };
    }
    return {
      ok: false,
      alreadyMerged: false,
      conflict: true,
      conflictedPaths,
      output: stderr || `Merge of ${branch} conflicted in ${conflictedPaths.length} file(s).`,
    };
  }
}

/** Snapshot HEAD into a branch ref as a recovery checkpoint. */
export function createRecoveryBranch(name: string): void {
  execSync(`git update-ref refs/heads/${name} HEAD`, { cwd: REPO_ROOT, stdio: 'ignore' });
}

// Batch merge orchestration lives in the run manager (`runs.ts`), which can
// spawn conflict-resolution agents between attempts. See `attemptMerge` above
// for the non-aborting primitive it builds on.

/**
 * Merge an agent branch into the main worktree's checked-out branch.
 *
 * Refuses (returns `ok:false`) on conflict, aborting the merge so the main tree
 * is left clean for a manual resolution. Callers should check `mainDirtySummary`
 * first — git will refuse to merge a dirty tree and this returns that as a
 * failure too.
 */
export function mergeBranch(branch: string): MergeResult {
  const target = currentBranch();
  if (isMergedIntoHead(branch)) {
    return { ok: true, alreadyMerged: true, target, branch, output: `${branch} is already in ${target}.` };
  }
  try {
    const out = execSync(
      `git merge --no-ff --no-edit -m ${JSON.stringify(`Merge agent branch ${branch} into ${target}`)} ${branch}`,
      { encoding: 'utf8', cwd: REPO_ROOT, stdio: ['ignore', 'pipe', 'pipe'] },
    );
    return { ok: true, alreadyMerged: false, target, branch, output: out.trim() || `Merged ${branch} into ${target}.` };
  } catch (err) {
    // Abort any in-progress merge so the main worktree stays clean.
    try {
      execSync('git merge --abort', { cwd: REPO_ROOT, stdio: 'ignore' });
    } catch {
      /* not mid-merge */
    }
    const stderr = (err as { stderr?: Buffer }).stderr?.toString().trim() ?? '';
    const message = err instanceof Error ? err.message : String(err);
    return {
      ok: false,
      alreadyMerged: false,
      target,
      branch,
      output: stderr || `Merge of ${branch} failed (aborted; ${target} left clean):
${message}`,
    };
  }
}
