/**
 * Pipeline scratch-dir garbage collector.
 *
 * Scans every project's `.forge/worktrees/` for `pipeline-<id>/` dirs
 * created by the `{{run.tmp_dir}}` mechanism in lib/pipeline.ts. Compares
 * each dir's pipeline state (status + completedAt) against the retention
 * settings and rm -rf's expired ones.
 *
 * Called from:
 *  - lib/init.ts setInterval (default every 6h, settings.pipelineTmpGcIntervalHours)
 *  - cli/mw.ts `forge pipeline gc` (manual / dry-run)
 *
 * Retention rules (settings):
 *  - done   → wiped immediately when pipeline.status flips (in pipeline.ts checkPipelineCompletion).
 *             GC here only catches done dirs left over from older builds.
 *  - failed → kept pipelineTmpKeepFailedDays days, then swept.
 *  - cancelled → kept pipelineTmpKeepCancelledDays days, then swept.
 *  - running / started → never touched.
 *  - orphan (no pipeline state file) → swept after 7d based on mtime.
 */

import { readdirSync, statSync, rmSync, existsSync } from 'node:fs';
import { scanProjects, getProjectWorktreeRoot } from './projects';
import { join } from 'node:path';
import { getPipeline } from './pipeline';
import { loadSettings } from './settings';

export interface GcResult {
  scanned: number;
  removed: { path: string; reason: string }[];
  kept: { path: string; reason: string }[];
}

const ORPHAN_KEEP_MS = 7 * 86400_000;

export function gcPipelineTmp(opts: { dryRun?: boolean } = {}): GcResult {
  const settings = loadSettings();
  const failedKeepMs = Math.max(0, (settings.pipelineTmpKeepFailedDays ?? 3)) * 86400_000;
  const cancelledKeepMs = Math.max(0, (settings.pipelineTmpKeepCancelledDays ?? 3)) * 86400_000;
  const cleanDoneNow = settings.pipelineTmpCleanDoneImmediate !== false;
  const now = Date.now();

  const removed: GcResult['removed'] = [];
  const kept: GcResult['kept'] = [];
  let scanned = 0;

  for (const proj of scanProjects()) {
    // Scratch uses a flatter <dataDir>/scratch/worktrees/ layout — see
    // getProjectWorktreeRoot for the rationale. Every other project sticks
    // to the regular <project>/.forge/worktrees/ convention.
    const wtDir = getProjectWorktreeRoot(proj);
    if (!existsSync(wtDir)) continue;
    let entries: string[];
    try { entries = readdirSync(wtDir); } catch { continue; }

    for (const entry of entries) {
      if (!entry.startsWith('pipeline-')) continue;
      const pipeId = entry.slice('pipeline-'.length);
      const fullPath = join(wtDir, entry);
      scanned++;

      const pipeline = getPipeline(pipeId);

      // Orphan: pipeline state gone. Use mtime as best signal.
      if (!pipeline) {
        let mtimeMs: number;
        try { mtimeMs = statSync(fullPath).mtimeMs; } catch { continue; }
        if (now - mtimeMs > ORPHAN_KEEP_MS) {
          if (!opts.dryRun) {
            try { rmSync(fullPath, { recursive: true, force: true }); } catch {}
          }
          removed.push({ path: fullPath, reason: `orphan (>${Math.round(ORPHAN_KEEP_MS / 86400_000)}d)` });
        } else {
          kept.push({ path: fullPath, reason: 'orphan, still fresh' });
        }
        continue;
      }

      const completedAt = pipeline.completedAt ? Date.parse(pipeline.completedAt) : null;
      let shouldRemove = false;
      let reason = '';

      if (pipeline.status === 'done' && cleanDoneNow) {
        shouldRemove = true;
        reason = 'done (immediate cleanup enabled)';
      } else if (pipeline.status === 'failed' && completedAt && now - completedAt > failedKeepMs) {
        shouldRemove = true;
        reason = `failed > ${settings.pipelineTmpKeepFailedDays}d`;
      } else if (pipeline.status === 'cancelled' && completedAt && now - completedAt > cancelledKeepMs) {
        shouldRemove = true;
        reason = `cancelled > ${settings.pipelineTmpKeepCancelledDays}d`;
      }

      if (shouldRemove) {
        if (!opts.dryRun) {
          try { rmSync(fullPath, { recursive: true, force: true }); } catch (e) {
            console.warn(`[pipeline-gc] rm ${fullPath} failed: ${(e as Error).message}`);
          }
        }
        removed.push({ path: fullPath, reason });
      } else {
        kept.push({ path: fullPath, reason: `status=${pipeline.status}` });
      }
    }
  }

  return { scanned, removed, kept };
}
