// Découverte auto des dépôts git : marche bornée du système de fichiers à la recherche de `.git`.
// Fonction PURE et tolérante (ne lève jamais) — testable isolément comme parseWorktreePorcelain.
// N'appelle JAMAIS git (détection par présence de `.git`) : la validation réelle (isRepo) et la
// résolution de default_branch se font paresseusement à l'enregistrement, pas par dépôt scanné.
import { readdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';

export interface ScanLimits {
  /** profondeur maximale de descente sous chaque racine (la racine = 0). */
  maxDepth: number;
  /** nombre maximal de repos retournés (garde-fou anti-explosion d'un FS pathologique). */
  maxRepos: number;
  /** noms de dossiers à ne jamais ouvrir (en plus des dotdirs, toujours exclus). */
  excludeDirs?: Set<string>;
}

/** Dossiers jamais explorés : grosses arborescences sans repos racine, ou bruit de build. */
export const DEFAULT_EXCLUDE_DIRS = new Set<string>([
  'node_modules',
  'vendor',
  'target',
  'dist',
  'build',
  '.cache',
  'venv',
  '.venv',
  '__pycache__',
]);

interface Frame {
  dir: string;
  depth: number;
}

/**
 * Parcours itératif (pile explicite, jamais de récursion non bornée) des `roots`.
 * Règles :
 *  - un dossier contenant `.git` (fichier OU dossier → couvre les worktrees liés) est un repo :
 *    on l'enregistre ; en profondeur on NE descend PAS dedans (sous-modules/worktrees imbriqués
 *    ignorés). EXCEPTION : une racine fournie (depth 0) qui est elle-même un repo est aussi un
 *    conteneur — on l'enregistre ET on continue de descendre pour trouver les repos internes ;
 *  - on n'empile que les vrais sous-dossiers (`d.isDirectory()`), donc les symlinks ne sont PAS
 *    suivis (anti-cycle + anti-sortie de racine), et on saute dotdirs + excludeDirs ;
 *  - bornes : `maxDepth`, `maxRepos`, et un éventuel `signal` (timeout global) ;
 *  - tolérance : un `readdir` qui échoue (EACCES/ENOENT) est ignoré, le scan continue ;
 *  - racine inexistante/illisible : ignorée silencieusement.
 * Retourne les chemins absolus dédupliqués des racines de repos, et `truncated` si une borne a coupé.
 */
export async function scanForRepos(
  roots: string[],
  limits: ScanLimits,
  signal?: AbortSignal,
): Promise<{ paths: string[]; truncated: boolean }> {
  const excludes = limits.excludeDirs ?? DEFAULT_EXCLUDE_DIRS;
  const found = new Set<string>();
  const seen = new Set<string>(); // ceinture-bretelles anti-cycle (chemins déjà visités)
  let truncated = false;

  // pile partagée entre toutes les racines : un seul plafond global maxRepos.
  const stack: Frame[] = [];
  for (const root of roots) stack.push({ dir: root, depth: 0 });

  while (stack.length > 0) {
    if (signal?.aborted || found.size >= limits.maxRepos) {
      truncated = true;
      break;
    }
    const { dir, depth } = stack.pop() as Frame;
    if (seen.has(dir)) continue;
    seen.add(dir);

    // Un dossier avec `.git` est un repo. En PROFONDEUR (depth > 0) c'est une feuille : on
    // l'enregistre sans descendre (on n'ouvre pas les sous-modules/worktrees imbriqués). Mais une
    // RACINE fournie explicitement (depth 0) est un CONTENEUR de scan : si elle est elle-même un
    // repo on l'enregistre, puis on CONTINUE de descendre pour découvrir les dépôts qu'elle contient.
    if (existsSync(join(dir, '.git'))) {
      found.add(dir);
      if (depth > 0) continue;
    }
    if (depth >= limits.maxDepth) continue;

    let entries;
    try {
      entries = await readdir(dir, { withFileTypes: true });
    } catch {
      continue; // EACCES/ENOENT/… : dossier ignoré, on poursuit
    }
    for (const e of entries) {
      if (!e.isDirectory()) continue; // symlinks non suivis (isDirectory() est false pour un lien)
      if (e.name.startsWith('.') || excludes.has(e.name)) continue;
      stack.push({ dir: join(dir, e.name), depth: depth + 1 });
    }
  }

  return { paths: [...found], truncated };
}
