import fs from 'node:fs/promises';
import path from 'node:path';
import { execFile } from 'node:child_process';

interface MtimeSnap {
  root: number;
  gitHead: number | null;
  gitIndex: number | null;
}

interface CacheEntry {
  files: string[];
  mtimes: MtimeSnap;
}

const cache = new Map<string, CacheEntry>(); // keyed by projectId

// Concurrent calls with a cold/stale cache may both spawn rg. The result is
// deterministic so they overwrite identically — no data corruption, just a
// rare extra subprocess. Acceptable for single-user mode.
export async function getProjectFiles(projectId: string, projectRoot: string): Promise<string[]> {
  const current = await snapMtimes(projectRoot);
  const cached = cache.get(projectId);
  if (cached && eqMtimes(cached.mtimes, current)) {
    return cached.files;
  }
  const files = await runRgFiles(projectRoot);
  cache.set(projectId, { files, mtimes: current });
  return files;
}

async function snapMtimes(root: string): Promise<MtimeSnap> {
  const rootStat = await fs.stat(root);
  let gitHead: number | null = null;
  let gitIndex: number | null = null;
  // best-effort; ignore failure because the project may not be a git repo
  try { gitHead = (await fs.stat(path.join(root, '.git', 'HEAD'))).mtimeMs; } catch {}
  try { gitIndex = (await fs.stat(path.join(root, '.git', 'index'))).mtimeMs; } catch {}
  return { root: rootStat.mtimeMs, gitHead, gitIndex };
}

function eqMtimes(a: MtimeSnap, b: MtimeSnap): boolean {
  return a.root === b.root && a.gitHead === b.gitHead && a.gitIndex === b.gitIndex;
}

function runRgFiles(root: string): Promise<string[]> {
  return new Promise((resolve, reject) => {
    execFile('rg', ['--files'], { cwd: root, maxBuffer: 32 * 1024 * 1024 }, (err, stdout) => {
      if (err) return reject(err);
      resolve(stdout.split('\n').filter(Boolean));
    });
  });
}
