/**
 * GET  /api/cache    → cache contents + total size.
 * DELETE /api/cache  → wipe everything cacheable.
 * DELETE /api/cache?name=<entry>&category=<cat>  → wipe a single entry.
 *
 * Backs the "Cache" item in the user dropdown. Three categories:
 *   • cloned-projects — git clones Forge made for pipeline runs
 *   • tmp             — chat-saved files (save_tmp_file, new default)
 *   • scratch         — top-level files in <dataDir>/scratch/. Includes
 *                       legacy save_scratch_file output (pre-rename) and
 *                       anything else dropped at scratch root. Subdirs
 *                       (task workdirs) are NOT listed — they belong to
 *                       live workspaces, not cache. CLAUDE.md (scratch-
 *                       project marker) is skipped.
 *
 * The `entries` array is flat (categories mixed) so existing Dashboard
 * UI keeps working without changes — each entry carries a `category`
 * field for display / per-category deletion. Janitor scope matches:
 * lib/scratch-cleanup.ts sweeps tmp/ + scratch/ top-level files.
 */

import { NextResponse } from 'next/server';
import { existsSync, readdirSync, rmSync, statSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import { getDataDir } from '@/lib/dirs';

type Category = 'cloned-projects' | 'tmp' | 'scratch';
interface CacheEntry { name: string; bytes: number; category: Category }

function dirSizeBytes(p: string): number {
  let total = 0;
  try {
    for (const ent of readdirSync(p, { withFileTypes: true })) {
      const child = join(p, ent.name);
      try {
        if (ent.isDirectory()) total += dirSizeBytes(child);
        else total += statSync(child).size;
      } catch { /* skip unreadable */ }
    }
  } catch { /* skip unreadable */ }
  return total;
}

function listCache(): { entries: CacheEntry[]; total_bytes: number; roots: Record<Category, string> } {
  const dataDir = getDataDir();
  const clonedRoot = join(dataDir, 'cloned-projects');
  const tmpRoot = join(dataDir, 'tmp');
  const scratchRoot = join(dataDir, 'scratch');
  const entries: CacheEntry[] = [];

  if (existsSync(clonedRoot)) {
    for (const ent of readdirSync(clonedRoot, { withFileTypes: true })) {
      if (!ent.isDirectory()) continue;
      entries.push({ name: ent.name, bytes: dirSizeBytes(join(clonedRoot, ent.name)), category: 'cloned-projects' });
    }
  }
  if (existsSync(tmpRoot)) {
    for (const ent of readdirSync(tmpRoot, { withFileTypes: true })) {
      // tmp/ holds bare files written by save_tmp_file (the tool rejects
      // any filename containing `/`). Subdirs aren't expected — if one
      // shows up (manual touch, older code) measure recursively so the
      // entry is still wipable from the UI rather than left dangling.
      if (ent.name.startsWith('.')) continue;
      const p = join(tmpRoot, ent.name);
      try {
        const st = statSync(p);
        const bytes = st.isDirectory() ? dirSizeBytes(p) : st.size;
        entries.push({ name: ent.name, bytes, category: 'tmp' });
      } catch { /* skip unreadable */ }
    }
  }
  if (existsSync(scratchRoot)) {
    // scratch/ has two coexisting kinds of content:
    //   1. Top-level files from legacy save_scratch_file (now renamed
    //      save_tmp_file → tmp/). Janitor sweeps these at 7d. Listed.
    //   2. Subdirs that are LIVE workdirs for dispatched tasks (every
    //      dispatch_task into the scratch project gets one). Deleting
    //      these mid-run breaks the running task — NOT listed.
    // CLAUDE.md is the scratch-project marker; preserved by janitor and
    // hidden here so users can't nuke it from the UI.
    for (const ent of readdirSync(scratchRoot, { withFileTypes: true })) {
      if (ent.name.startsWith('.') || ent.name === 'CLAUDE.md') continue;
      if (!ent.isFile()) continue;
      const p = join(scratchRoot, ent.name);
      try {
        const st = statSync(p);
        entries.push({ name: ent.name, bytes: st.size, category: 'scratch' });
      } catch { /* skip unreadable */ }
    }
  }

  entries.sort((a, b) => b.bytes - a.bytes);
  return {
    entries,
    total_bytes: entries.reduce((s, e) => s + e.bytes, 0),
    roots: { 'cloned-projects': clonedRoot, tmp: tmpRoot, scratch: scratchRoot },
  };
}

function isValidName(name: string): boolean {
  return !!name && !name.includes('/') && !name.includes('\\') && !name.includes('..');
}

function rmEntry(category: Category, name: string, dataDir: string): { ok: boolean; bytes: number; error?: string } {
  // scratch/CLAUDE.md is the project marker — refuse to delete even if
  // a caller asks for it by name. Same instinct as the listCache filter.
  if (category === 'scratch' && (name === 'CLAUDE.md' || name.startsWith('.'))) {
    return { ok: false, bytes: 0, error: 'refused: scratch marker / dotfile' };
  }
  const root = join(dataDir, category);
  const target = join(root, name);
  if (!existsSync(target)) return { ok: false, bytes: 0, error: 'not found' };
  let bytes = 0;
  try {
    const st = statSync(target);
    // scratch/ category only deletes top-level files — never the dir
    // contents of a task workdir subdir. listCache already hides subdirs;
    // this is belt-and-suspenders for direct API callers.
    if (category === 'scratch' && st.isDirectory()) {
      return { ok: false, bytes: 0, error: 'refused: scratch subdir is a live task workdir, not cache' };
    }
    bytes = st.isDirectory() ? dirSizeBytes(target) : st.size;
    if (st.isDirectory()) rmSync(target, { recursive: true, force: true });
    else unlinkSync(target);
  } catch (e) { return { ok: false, bytes: 0, error: (e as Error).message }; }
  return { ok: true, bytes };
}

export async function GET() {
  return NextResponse.json(listCache());
}

export async function DELETE(req: Request) {
  const url = new URL(req.url);
  const name = url.searchParams.get('name');
  const categoryParam = url.searchParams.get('category') as Category | null;
  const dataDir = getDataDir();

  if (name && !isValidName(name)) {
    return NextResponse.json({ ok: false, error: 'invalid name' }, { status: 400 });
  }

  // Single-entry delete: scan all categories if category not specified,
  // so the existing Dashboard "delete by name" call (no category) keeps
  // working — finds the entry in whichever category owns it.
  if (name) {
    const cats: Category[] = categoryParam ? [categoryParam] : ['cloned-projects', 'tmp', 'scratch'];
    for (const cat of cats) {
      const r = rmEntry(cat, name, dataDir);
      if (r.ok) return NextResponse.json({ ok: true, deleted: 1, freed_bytes: r.bytes, category: cat });
      if (r.error && r.error !== 'not found') {
        return NextResponse.json({ ok: false, error: r.error }, { status: 500 });
      }
    }
    return NextResponse.json({ ok: false, error: 'not found' }, { status: 404 });
  }

  // Wipe-all (optionally scoped to one category).
  const before = listCache();
  const toDelete = categoryParam ? before.entries.filter((e) => e.category === categoryParam) : before.entries;
  let deleted = 0, freed = 0;
  for (const e of toDelete) {
    const r = rmEntry(e.category, e.name, dataDir);
    if (r.ok) { deleted++; freed += r.bytes; }
  }
  return NextResponse.json({ ok: true, deleted, freed_bytes: freed });
}
