// Couche git sûre : tout passe par execFile (JAMAIS de shell), arguments en tableau, `--` avant
// les chemins/refs utilisateur. Fonctions pures sans état, prenant un cwd déjà validé par l'appelant.
import { execFile } from 'node:child_process';
import { resolve, sep } from 'node:path';
import type { WorktreeGitStatus } from '@arboretum/shared';

const GIT_TIMEOUT_MS = 10_000;
const GIT_MAX_BUFFER = 8 * 1024 * 1024;

interface GitError extends Error {
  stderr?: string;
  code?: number | string;
}

function git(cwd: string, args: string[]): Promise<string> {
  return new Promise((resolveP, reject) => {
    execFile(
      'git',
      args,
      {
        cwd,
        timeout: GIT_TIMEOUT_MS,
        maxBuffer: GIT_MAX_BUFFER,
        // GIT_OPTIONAL_LOCKS=0 : pas de prise de verrou par `status` (perf + concurrence avec une
        // session claude active) ; LC_ALL=C : sortie stable pour le parsing.
        env: { ...process.env, GIT_OPTIONAL_LOCKS: '0', LC_ALL: 'C' },
      },
      (err, stdout, stderr) => {
        if (err) {
          (err as GitError).stderr = String(stderr);
          reject(err);
        } else {
          resolveP(stdout.toString());
        }
      },
    );
  });
}

export interface ParsedWorktree {
  path: string;
  head: string | null;
  branch: string | null;
  detached: boolean;
  locked: boolean;
  prunable: boolean;
  bare: boolean;
}

/** Parse la sortie `git worktree list --porcelain` (testable isolément, sans repo réel). */
export function parseWorktreePorcelain(stdout: string): ParsedWorktree[] {
  const out: ParsedWorktree[] = [];
  let cur: Partial<ParsedWorktree> | null = null;
  const flush = (): void => {
    if (cur?.path) {
      out.push({
        path: cur.path,
        head: cur.head ?? null,
        branch: cur.branch ?? null,
        detached: cur.detached ?? false,
        locked: cur.locked ?? false,
        prunable: cur.prunable ?? false,
        bare: cur.bare ?? false,
      });
    }
    cur = null;
  };
  for (const line of stdout.split('\n')) {
    if (line === '') {
      flush();
      continue;
    }
    const sp = line.indexOf(' ');
    const key = sp === -1 ? line : line.slice(0, sp);
    const val = sp === -1 ? '' : line.slice(sp + 1);
    switch (key) {
      case 'worktree':
        flush();
        cur = { path: val };
        break;
      case 'HEAD':
        if (cur) cur.head = val;
        break;
      case 'branch':
        if (cur) cur.branch = val.replace(/^refs\/heads\//, '');
        break;
      case 'detached':
        if (cur) cur.detached = true;
        break;
      case 'bare':
        if (cur) cur.bare = true;
        break;
      case 'locked':
        if (cur) cur.locked = true;
        break;
      case 'prunable':
        if (cur) cur.prunable = true;
        break;
    }
  }
  flush();
  return out;
}

/** Refuse les noms de branche dangereux (défense en profondeur ; git valide déjà côté lui). */
export function isValidBranchName(name: string): boolean {
  return (
    /^[A-Za-z0-9._/-]+$/.test(name) &&
    !name.startsWith('-') &&
    !name.startsWith('/') &&
    !name.endsWith('/') &&
    !name.endsWith('.lock') &&
    !name.includes('..') &&
    !name.includes('//')
  );
}

/** Chemin absolu sans segment `..` après résolution (évite l'échappement d'arborescence). */
export function isSafeAbsolutePath(p: string): boolean {
  return p.startsWith('/') && resolve(p) === p && !p.split(sep).includes('..');
}

/** true si `path` est la racine d'un dépôt git (main worktree) accessible. */
export async function isRepo(path: string): Promise<boolean> {
  try {
    const top = (await git(path, ['rev-parse', '--show-toplevel'])).trim();
    return resolve(top) === resolve(path);
  } catch {
    return false;
  }
}

/** Branche par défaut (origin/HEAD) en nom court, ou null si indéterminée. */
export async function defaultBranch(repoPath: string): Promise<string | null> {
  try {
    const ref = (await git(repoPath, ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'])).trim();
    return ref.replace(/^refs\/remotes\/origin\//, '') || null;
  } catch {
    return null;
  }
}

export async function listWorktrees(repoPath: string): Promise<ParsedWorktree[]> {
  return parseWorktreePorcelain(await git(repoPath, ['worktree', 'list', '--porcelain']));
}

/** État git d'un worktree : ahead/behind vs upstream + nombre de fichiers modifiés. */
export async function worktreeStatus(worktreePath: string): Promise<WorktreeGitStatus> {
  let upstream: string | null = null;
  let ahead = 0;
  let behind = 0;
  try {
    upstream = (await git(worktreePath, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'])).trim() || null;
  } catch {
    upstream = null;
  }
  if (upstream) {
    try {
      // left = commits de l'upstream absents de HEAD (behind) ; right = HEAD non poussés (ahead).
      const out = (await git(worktreePath, ['rev-list', '--left-right', '--count', `${upstream}...HEAD`])).trim();
      const [left, right] = out.split(/\s+/);
      behind = Number(left) || 0;
      ahead = Number(right) || 0;
    } catch {
      /* refs inaccessibles : on laisse 0/0 */
    }
  }
  let dirtyCount = 0;
  try {
    const status = await git(worktreePath, ['status', '--porcelain=v1', '--untracked-files=all']);
    dirtyCount = status.split('\n').filter((l) => l.trim() !== '').length;
  } catch {
    /* ignore */
  }
  return { ahead, behind, dirtyCount, upstream };
}

export async function addWorktree(
  repoPath: string,
  opts: { path: string; branch: string; newBranch: boolean; baseRef?: string },
): Promise<void> {
  const args = ['worktree', 'add'];
  if (opts.newBranch) args.push('-b', opts.branch);
  args.push('--', opts.path);
  if (opts.newBranch) {
    if (opts.baseRef) args.push(opts.baseRef);
  } else {
    args.push(opts.branch);
  }
  await git(repoPath, args);
}

export async function removeWorktree(repoPath: string, worktreePath: string, force: boolean): Promise<void> {
  await git(repoPath, ['worktree', 'remove', ...(force ? ['--force'] : []), '--', worktreePath]);
}

export async function pruneWorktrees(repoPath: string): Promise<void> {
  await git(repoPath, ['worktree', 'prune']);
}

/** true si l'erreur git d'un `worktree remove` est due à des changements non sauvegardés. */
export function isDirtyWorktreeError(err: unknown): boolean {
  const msg = `${(err as GitError)?.stderr ?? ''} ${(err as Error)?.message ?? ''}`;
  return /contains modified or untracked files|is dirty|use --force/i.test(msg);
}
