import { execFile } from 'node:child_process';
import { promisify } from 'node:util';

const execFileAsync = promisify(execFile);

const CACHE_TTL_MS = 30_000;
const GIT_TIMEOUT_MS = 2_000;
// Cap stdout size so a pathological repo can't blow the buffer. Branch + status
// porcelain + diverge counts never approach this on a real repo.
const GIT_MAX_BUFFER = 1024 * 1024;

export interface GitMeta {
  branch: string | null;
  is_dirty: boolean;
  ahead: number;
  behind: number;
}

interface CacheEntry {
  at: number;
  value: GitMeta | null;
}

const cache = new Map<string, CacheEntry>();

// Runs a single git invocation with a hard 2s timeout. Returns null on any
// failure (non-zero exit, timeout, git not installed) so callers can decide
// how to degrade. Stderr is intentionally swallowed; we don't surface git's
// error text to the model or UI.
async function runGit(args: string[], cwd: string): Promise<string | null> {
  try {
    const { stdout } = await execFileAsync('git', args, {
      cwd,
      timeout: GIT_TIMEOUT_MS,
      windowsHide: true,
      maxBuffer: GIT_MAX_BUFFER,
    });
    return stdout.toString();
  } catch {
    return null;
  }
}

export async function getGitMeta(rootPath: string): Promise<GitMeta | null> {
  const cached = cache.get(rootPath);
  const now = Date.now();
  if (cached && now - cached.at < CACHE_TTL_MS) {
    return cached.value;
  }

  // Three calls in parallel. rev-parse establishes repo + branch name;
  // status --porcelain detects dirtiness with no false-positives from formatting;
  // rev-list --left-right --count compares HEAD to upstream and is allowed to
  // fail silently (returns null → ahead/behind = 0) when no upstream is set.
  const [branchOut, statusOut, divergedOut] = await Promise.all([
    runGit(['rev-parse', '--abbrev-ref', 'HEAD'], rootPath),
    runGit(['status', '--porcelain'], rootPath),
    runGit(['rev-list', '--left-right', '--count', 'HEAD...@{upstream}'], rootPath),
  ]);

  // If rev-parse fails, this isn't a git repo (or git isn't installed). Cache
  // the null result so the next 30s of requests don't re-probe.
  if (branchOut === null) {
    cache.set(rootPath, { at: now, value: null });
    return null;
  }

  const branch = branchOut.trim() || null;
  const is_dirty = statusOut !== null && statusOut.trim().length > 0;

  let ahead = 0;
  let behind = 0;
  if (divergedOut !== null) {
    const match = divergedOut.trim().match(/^(\d+)\s+(\d+)/);
    if (match) {
      ahead = Number(match[1]);
      behind = Number(match[2]);
    }
  }

  const value: GitMeta = { branch, is_dirty, ahead, behind };
  cache.set(rootPath, { at: now, value });
  return value;
}

