import { spawnSync } from "node:child_process";
import { closeSync, existsSync, ftruncateSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
import { basename, join, resolve } from "node:path";

import { type UniDeskConfig, repoRoot, rootPath } from "./config";
import { runRemoteGcCommand } from "./gc-remote";

type GcRisk = "low" | "medium" | "high" | "blocked";
type GcItemKind =
  | "file-log-delete"
  | "file-log-compact"
  | "docker-json-log-truncate"
  | "journal-vacuum"
  | "docker-build-cache-prune"
  | "tmp-path-delete"
  | "browser-cache-delete";

interface GcOptions {
  fileLogs: boolean;
  fileLogKeepDays: number;
  fileLogMaxBytes: number;
  fileLogTailBytes: number;
  dockerLogs: boolean;
  dockerLogMaxBytes: number;
  journal: boolean;
  journalTargetBytes: number;
  buildCache: boolean;
  buildCacheUntil: string;
  buildCacheAll: boolean;
  tmp: boolean;
  tmpMinAgeHours: number;
  browserCache: boolean;
  dbSummary: boolean;
  limit: number;
  resultLimit: number;
  full: boolean;
}

interface DbTraceGcOptions {
  beforeDate: string | null;
  types: string[];
  vacuumFull: boolean;
  confirm: boolean;
}

interface GcPolicyOptions {
  dryRun: boolean;
  enableNow: boolean;
}

interface DiskSnapshot {
  filesystem: string;
  sizeBytes: number;
  usedBytes: number;
  availableBytes: number;
  usePercent: number;
  mount: string;
}

interface GcCandidate {
  id: string;
  kind: GcItemKind;
  risk: GcRisk;
  description: string;
  path?: string;
  container?: { id: string; name: string; image: string };
  sizeBytes: number;
  estimatedReclaimBytes: number;
  action: Record<string, unknown>;
}

interface ProtectedGcItem {
  kind: string;
  risk: "blocked";
  ref: string;
  sizeBytes?: number;
  reason: string;
}

interface GcPlan {
  ok: true;
  action: "gc plan";
  dryRun: true;
  mutation: false;
  observedAt: string;
  options: Record<string, unknown>;
  diskBefore: DiskSnapshot | null;
  summary: {
    candidateCount: number;
    returnedCandidateCount: number;
    estimatedReclaimBytes: number;
    estimatedReclaim: string;
    returnedEstimatedReclaimBytes: number;
    returnedEstimatedReclaim: string;
    byKind: Record<string, { count: number; estimatedReclaimBytes: number; estimatedReclaim: string }>;
  };
  candidates: GcCandidate[];
  protected: ProtectedGcItem[];
  databaseSummary: unknown;
  policy: {
    requiresRunConfirm: true;
    runCommand: string;
    neverTouches: string[];
    notes: string[];
  };
}

interface GcRunResult {
  ok: boolean;
  action: "gc run";
  dryRun: false;
  mutation: true;
  observedAt: string;
  options: Record<string, unknown>;
  diskBefore: DiskSnapshot | null;
  diskAfter: DiskSnapshot | null;
  summary: {
    plannedCandidateCount: number;
    attemptedCount: number;
    succeededCount: number;
    failedCount: number;
    estimatedReclaimBytes: number;
    actualDiskReclaimBytes: number | null;
    resultCount: number;
    returnedResultCount: number;
    omittedResultCount: number;
  };
  results: Array<GcCandidate & { status: "succeeded" | "failed"; reclaimedBytes: number | null; error?: string; commandOutput?: unknown }>;
  protected: ProtectedGcItem[];
}

const DEFAULT_OPTIONS: GcOptions = {
  fileLogs: true,
  fileLogKeepDays: 7,
  fileLogMaxBytes: 50 * 1024 * 1024,
  fileLogTailBytes: 20 * 1024 * 1024,
  dockerLogs: true,
  dockerLogMaxBytes: 50 * 1024 * 1024,
  journal: true,
  journalTargetBytes: 512 * 1024 * 1024,
  buildCache: true,
  buildCacheUntil: "24h",
  buildCacheAll: false,
  tmp: true,
  tmpMinAgeHours: 24,
  browserCache: false,
  dbSummary: true,
  limit: 50,
  resultLimit: 50,
  full: false,
};

const DEFAULT_DB_TRACE_TYPES = ["trace-stats-updated", "trace-step-created", "trace-stats-snapshot"];

const TMP_PREFIX_ALLOWLIST = [
  "hwlab-agent-",
  "hwlab-cd-",
  "hwlab-cli-cicd-",
  "hwlab-codeagent-trace",
  "hwlab-desired-state-",
  "hwlab-main-",
  "hwlab-merge-",
  "hwlab-pr",
  "hwlab-refresh-",
  "playwright-artifacts-",
  "playwright_chromiumdev_profile-",
  "unidesk-clean-",
  "unidesk-code-queue",
  "unidesk-hwlab-cd-",
  "unidesk-pr",
  "unidesk-tran-runner",
  "bunx-",
  "codex-app-schema",
  "codex-app-ts",
  "marked-",
  "node-compile-cache",
];

const TMP_EXACT_PROTECT = new Set([
  "/tmp/codex-apply-patch",
  "/tmp/codex-ipc",
  "/tmp/tmux-0",
]);

export async function runGcCommand(config: UniDeskConfig, args: string[]): Promise<unknown> {
  const [action = "plan", ...rest] = args;
  if (action === "remote") {
    const [providerId, subaction = "plan", ...remoteArgs] = rest;
    return await runRemoteGcCommand(config, providerId, subaction, remoteArgs);
  }
  if (action === "policy") {
    const [subaction = "plan", ...policyArgs] = rest;
    const options = parseGcPolicyOptions(policyArgs);
    if (subaction === "plan" || subaction === "render" || subaction === "dry-run") return gcPolicyPlan(options);
    if (subaction === "install") return gcPolicyInstall(options);
    return {
      ok: false,
      error: "unsupported-gc-policy-action",
      action: subaction,
      supportedActions: ["plan", "render", "dry-run", "install"],
    };
  }
  if (action === "db-trace" || action === "db-trace-retention") {
    const [subaction = "plan", ...dbArgs] = rest;
    const options = parseDbTraceGcOptions(dbArgs);
    if (subaction === "plan" || subaction === "dry-run") return gcDbTracePlan(options);
    if (subaction === "run") {
      if (!options.confirm) {
        return {
          ok: false,
          error: "gc-db-trace-run-requires-confirm",
          dryRun: true,
          mutation: false,
          requiredFlags: ["--confirm", "--before-date YYYY-MM-DD", "--vacuum-full"],
          planCommand: "bun scripts/cli.ts gc db-trace plan --before-date YYYY-MM-DD",
          runCommand: "bun scripts/cli.ts gc db-trace run --confirm --before-date YYYY-MM-DD --vacuum-full",
        };
      }
      return gcDbTraceRun(options);
    }
    return {
      ok: false,
      error: "unsupported-gc-db-trace-action",
      action: subaction,
      supportedActions: ["plan", "run"],
    };
  }
  if (action === "plan" || action === "dry-run") {
    return gcPlan(config, parseGcOptions(rest));
  }
  if (action === "run") {
    const confirmIndex = rest.indexOf("--confirm");
    if (confirmIndex === -1) {
      return {
        ok: false,
        error: "gc-run-requires-confirm",
        dryRun: true,
        mutation: false,
        requiredFlag: "--confirm",
        planCommand: "bun scripts/cli.ts gc plan",
        runCommand: "bun scripts/cli.ts gc run --confirm",
      };
    }
    const runArgs = rest.filter((arg, index) => index !== confirmIndex);
    return gcRun(config, parseGcOptions(runArgs));
  }
  return {
    ok: false,
    error: "unsupported-gc-action",
    action,
    supportedActions: ["plan", "run", "db-trace", "policy", "remote"],
  };
}

export function gcPlan(config: UniDeskConfig, options: GcOptions = DEFAULT_OPTIONS): GcPlan {
  const observedAt = new Date().toISOString();
  const candidates: GcCandidate[] = [];
  const protectedItems: ProtectedGcItem[] = [];

  if (options.fileLogs) {
    candidates.push(...collectFileLogCandidates(config, options, observedAt));
  }
  if (options.dockerLogs) {
    candidates.push(...collectDockerLogCandidates(options));
  }
  if (options.journal) {
    const item = collectJournalCandidate(options);
    if (item !== null) candidates.push(item);
  }
  if (options.buildCache) {
    const item = collectBuildCacheCandidate(options);
    if (item !== null) candidates.push(item);
  }
  if (options.tmp) {
    candidates.push(...collectTmpCandidates(options, observedAt));
  }
  if (options.browserCache) {
    const item = collectBrowserCacheCandidate();
    if (item !== null) candidates.push(item);
  } else {
    const path = rootPath(".state", "playwright-browsers");
    if (existsSync(path)) {
      protectedItems.push({
        kind: "browser-cache",
        risk: "blocked",
        ref: path,
        sizeBytes: safePathSize(path),
        reason: "Playwright browser cache is not removed by default; rerun with --include-browser-cache if this cache is approved for one-time cleanup.",
      });
    }
  }

  protectedItems.push(...collectProtectedStorage(config));
  const databaseSummary = options.dbSummary ? collectDatabaseSummary() : { skipped: true, reason: "disabled-by-option" };
  const allCandidates = candidates.sort((left, right) => right.estimatedReclaimBytes - left.estimatedReclaimBytes);
  const visibleCandidates = options.full ? allCandidates : allCandidates.slice(0, options.limit);
  const summary = summarizeCandidates(allCandidates, visibleCandidates);

  return {
    ok: true,
    action: "gc plan",
    dryRun: true,
    mutation: false,
    observedAt,
    options: publicOptions(options),
    diskBefore: rootDiskSnapshot(),
    summary,
    candidates: visibleCandidates,
    protected: protectedItems,
    databaseSummary,
    policy: {
      requiresRunConfirm: true,
      runCommand: "bun scripts/cli.ts gc run --confirm",
      neverTouches: [
        "Docker volumes",
        "PostgreSQL PGDATA",
        "Baidu Netdisk staging/backups",
        "D601 registry storage",
        "Docker images used by containers",
      ],
      notes: [
        "gc run only executes listed one-time cleanup actions after --confirm.",
        options.full ? "Full candidate output requested." : `Default output is capped to ${options.limit} candidates; use --full or --limit N for broader disclosure.`,
        "Database event retention is diagnostic-only in this command; cleanups for oa_events require a backup and a separate schema/retention change.",
        "Docker image cleanup stays under server cleanup plan; gc does not run docker system prune or docker image prune.",
      ],
    },
  };
}

export function gcRun(config: UniDeskConfig, options: GcOptions = DEFAULT_OPTIONS): GcRunResult {
  const plan = gcPlan(config, options);
  const diskBefore = plan.diskBefore;
  const results: GcRunResult["results"] = [];

  for (const candidate of plan.candidates) {
    try {
      const result = executeCandidate(candidate, options);
      results.push({ ...candidate, status: "succeeded", reclaimedBytes: result.reclaimedBytes, commandOutput: result.commandOutput });
    } catch (error) {
      results.push({
        ...candidate,
        status: "failed",
        reclaimedBytes: null,
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  const diskAfter = rootDiskSnapshot();
  const failedCount = results.filter((item) => item.status === "failed").length;
  const estimatedReclaimBytes = plan.summary.returnedEstimatedReclaimBytes;
  const actualDiskReclaimBytes = diskBefore !== null && diskAfter !== null ? diskAfter.availableBytes - diskBefore.availableBytes : null;
  const returnedResults = returnedRunResults(results, options);

  return {
    ok: failedCount === 0,
    action: "gc run",
    dryRun: false,
    mutation: true,
    observedAt: new Date().toISOString(),
    options: publicOptions(options),
    diskBefore,
    diskAfter,
    summary: {
      plannedCandidateCount: plan.candidates.length,
      attemptedCount: results.length,
      succeededCount: results.length - failedCount,
      failedCount,
      estimatedReclaimBytes,
      actualDiskReclaimBytes,
      resultCount: results.length,
      returnedResultCount: returnedResults.length,
      omittedResultCount: Math.max(0, results.length - returnedResults.length),
    },
    results: returnedResults,
    protected: plan.protected,
  };
}

function parseGcOptions(args: string[]): GcOptions {
  const options: GcOptions = { ...DEFAULT_OPTIONS };
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index] ?? "";
    if (arg === "--logs-keep-days" || arg === "--file-log-keep-days") {
      options.fileLogKeepDays = parseNonNegativeNumber(arg, args[++index]);
    } else if (arg === "--file-log-max-bytes") {
      options.fileLogMaxBytes = parseSizeOption(arg, args[++index]);
    } else if (arg === "--file-log-tail-bytes") {
      options.fileLogTailBytes = parseSizeOption(arg, args[++index]);
    } else if (arg === "--docker-log-max-bytes") {
      options.dockerLogMaxBytes = parseSizeOption(arg, args[++index]);
    } else if (arg === "--journal-target-size") {
      options.journalTargetBytes = parseSizeOption(arg, args[++index]);
    } else if (arg === "--build-cache-until") {
      const value = args[++index];
      if (!value || !/^\d+(s|m|h|d)$/u.test(value)) throw new Error(`${arg} must look like 24h, 7d, 30m or 60s`);
      options.buildCacheUntil = value;
    } else if (arg === "--build-cache-all") {
      options.buildCacheAll = true;
    } else if (arg === "--tmp-min-age-hours") {
      options.tmpMinAgeHours = parseNonNegativeNumber(arg, args[++index]);
    } else if (arg === "--include-browser-cache") {
      options.browserCache = true;
    } else if (arg === "--no-browser-cache") {
      options.browserCache = false;
    } else if (arg === "--no-file-logs" || arg === "--no-logs") {
      options.fileLogs = false;
    } else if (arg === "--no-docker-logs") {
      options.dockerLogs = false;
    } else if (arg === "--no-journal") {
      options.journal = false;
    } else if (arg === "--no-build-cache") {
      options.buildCache = false;
    } else if (arg === "--no-tmp") {
      options.tmp = false;
    } else if (arg === "--no-db-summary" || arg === "--no-db") {
      options.dbSummary = false;
    } else if (arg === "--limit") {
      const value = parseNonNegativeNumber(arg, args[++index]);
      if (!Number.isInteger(value) || value <= 0) throw new Error("--limit must be a positive integer");
      options.limit = Math.min(value, 5000);
    } else if (arg === "--result-limit") {
      const value = parseNonNegativeNumber(arg, args[++index]);
      if (!Number.isInteger(value) || value <= 0) throw new Error("--result-limit must be a positive integer");
      options.resultLimit = Math.min(value, 5000);
    } else if (arg === "--full" || arg === "--raw") {
      options.full = true;
    } else if (arg === "--confirm" || arg === "--dry-run") {
      // handled by caller or accepted as a no-op for plan compatibility
    } else {
      throw new Error(`unknown gc option: ${arg}`);
    }
  }
  if (options.fileLogTailBytes >= options.fileLogMaxBytes) {
    throw new Error("--file-log-tail-bytes must be smaller than --file-log-max-bytes");
  }
  return options;
}

function parseDbTraceGcOptions(args: string[]): DbTraceGcOptions {
  const options: DbTraceGcOptions = {
    beforeDate: null,
    types: [...DEFAULT_DB_TRACE_TYPES],
    vacuumFull: false,
    confirm: false,
  };
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index] ?? "";
    if (arg === "--before-date") {
      const value = args[++index];
      if (!value || !/^\d{4}-\d{2}-\d{2}$/u.test(value)) throw new Error("--before-date must be YYYY-MM-DD");
      options.beforeDate = value;
    } else if (arg === "--type") {
      const value = args[++index];
      if (!value || !/^[a-z0-9._:-]+$/iu.test(value)) throw new Error("--type must be a simple event type");
      options.types.push(value);
    } else if (arg === "--only-default-trace-types") {
      options.types = [...DEFAULT_DB_TRACE_TYPES];
    } else if (arg === "--vacuum-full") {
      options.vacuumFull = true;
    } else if (arg === "--confirm") {
      options.confirm = true;
    } else if (arg === "--dry-run") {
      // accepted for plan compatibility
    } else {
      throw new Error(`unknown gc db-trace option: ${arg}`);
    }
  }
  if (options.beforeDate === null) throw new Error("--before-date YYYY-MM-DD is required");
  options.types = [...new Set(options.types)];
  return options;
}

function parseGcPolicyOptions(args: string[]): GcPolicyOptions {
  const options: GcPolicyOptions = {
    dryRun: args.includes("--dry-run"),
    enableNow: !args.includes("--no-enable-now"),
  };
  for (const arg of args) {
    if (arg === "--dry-run" || arg === "--no-enable-now") continue;
    throw new Error(`unknown gc policy option: ${arg}`);
  }
  return options;
}

function parseNonNegativeNumber(name: string, raw: string | undefined): number {
  const value = Number(raw);
  if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative number`);
  return value;
}

function parseSizeOption(name: string, raw: string | undefined): number {
  const value = parseSize(raw ?? "");
  if (value === null || value <= 0) throw new Error(`${name} must be a positive size such as 512M, 1GiB or 50000000`);
  return value;
}

function parseSize(raw: string): number | null {
  const match = raw.trim().match(/^(\d+(?:\.\d+)?)\s*(b|k|kb|kib|m|mb|mib|g|gb|gib)?$/iu);
  if (!match) return null;
  const value = Number(match[1]);
  const unit = (match[2] ?? "b").toLowerCase();
  const multiplier =
    unit === "g" || unit === "gb" || unit === "gib" ? 1024 ** 3
      : unit === "m" || unit === "mb" || unit === "mib" ? 1024 ** 2
        : unit === "k" || unit === "kb" || unit === "kib" ? 1024
          : 1;
  const bytes = Math.floor(value * multiplier);
  return Number.isFinite(bytes) ? bytes : null;
}

function publicOptions(options: GcOptions): Record<string, unknown> {
  return {
    fileLogs: options.fileLogs,
    fileLogKeepDays: options.fileLogKeepDays,
    fileLogMaxBytes: options.fileLogMaxBytes,
    fileLogTailBytes: options.fileLogTailBytes,
    dockerLogs: options.dockerLogs,
    dockerLogMaxBytes: options.dockerLogMaxBytes,
    journal: options.journal,
    journalTargetBytes: options.journalTargetBytes,
    buildCache: options.buildCache,
    buildCacheUntil: options.buildCacheUntil,
    buildCacheAll: options.buildCacheAll,
    tmp: options.tmp,
    tmpMinAgeHours: options.tmpMinAgeHours,
    browserCache: options.browserCache,
    dbSummary: options.dbSummary,
    limit: options.limit,
    resultLimit: options.resultLimit,
    full: options.full,
  };
}

function collectFileLogCandidates(config: UniDeskConfig, options: GcOptions, observedAt: string): GcCandidate[] {
  const logsRoot = resolvePath(config.paths.logsDir);
  if (!existsSync(logsRoot)) return [];
  const cutoffMs = new Date(observedAt).getTime() - options.fileLogKeepDays * 24 * 60 * 60 * 1000;
  const files = collectFiles(logsRoot);
  const candidates: GcCandidate[] = [];
  for (const file of files) {
    if (!/\.(jsonl|log|txt)$/iu.test(file.path)) continue;
    if (file.sizeBytes <= 0) continue;
    const id = `file-log:${file.path}`;
    if (file.mtimeMs < cutoffMs) {
      candidates.push({
        id,
        kind: "file-log-delete",
        risk: "medium",
        description: `Delete UniDesk file log older than ${options.fileLogKeepDays} days`,
        path: file.path,
        sizeBytes: file.sizeBytes,
        estimatedReclaimBytes: file.sizeBytes,
        action: { op: "unlink" },
      });
    } else if (file.sizeBytes > options.fileLogMaxBytes) {
      candidates.push({
        id,
        kind: "file-log-compact",
        risk: "medium",
        description: `Compact large UniDesk file log to tail ${formatBytes(options.fileLogTailBytes)}`,
        path: file.path,
        sizeBytes: file.sizeBytes,
        estimatedReclaimBytes: Math.max(0, file.sizeBytes - options.fileLogTailBytes),
        action: { op: "keep-tail", tailBytes: options.fileLogTailBytes },
      });
    }
  }
  return candidates.sort((left, right) => right.estimatedReclaimBytes - left.estimatedReclaimBytes);
}

function collectDockerLogCandidates(options: GcOptions): GcCandidate[] {
  const containers = dockerContainers();
  const candidates: GcCandidate[] = [];
  for (const container of containers) {
    if (!container.logPath || !existsSync(container.logPath)) continue;
    let stat;
    try {
      stat = statSync(container.logPath);
    } catch {
      continue;
    }
    if (stat.size <= options.dockerLogMaxBytes) continue;
    candidates.push({
      id: `docker-json-log:${container.id}`,
      kind: "docker-json-log-truncate",
      risk: "medium",
      description: `Truncate Docker json-file log larger than ${formatBytes(options.dockerLogMaxBytes)}`,
      path: container.logPath,
      container: { id: container.id.slice(0, 12), name: container.name, image: container.image },
      sizeBytes: stat.size,
      estimatedReclaimBytes: stat.size,
      action: { op: "truncate", targetBytes: 0 },
    });
  }
  return candidates.sort((left, right) => right.estimatedReclaimBytes - left.estimatedReclaimBytes);
}

function collectJournalCandidate(options: GcOptions): GcCandidate | null {
  const result = command(["journalctl", "--disk-usage"], 5000);
  if (result.exitCode !== 0) return null;
  const currentBytes = parseJournalUsage(result.stdout + result.stderr);
  if (currentBytes === null || currentBytes <= options.journalTargetBytes) return null;
  return {
    id: "journalctl:vacuum",
    kind: "journal-vacuum",
    risk: "medium",
    description: `Vacuum systemd journal to ${formatBytes(options.journalTargetBytes)}`,
    sizeBytes: currentBytes,
    estimatedReclaimBytes: Math.max(0, currentBytes - options.journalTargetBytes),
    action: { command: ["journalctl", `--vacuum-size=${options.journalTargetBytes}`] },
  };
}

function collectBuildCacheCandidate(options: GcOptions): GcCandidate | null {
  const result = command(["docker", "system", "df"], 8000);
  if (result.exitCode !== 0) return null;
  const cache = parseDockerSystemDfBuildCache(result.stdout);
  if (cache === null || cache.reclaimableBytes <= 0) return null;
  return {
    id: "docker-builder:prune",
    kind: "docker-build-cache-prune",
    risk: "low",
    description: options.buildCacheAll ? "Prune all Docker BuildKit cache" : `Prune all Docker BuildKit cache unused for ${options.buildCacheUntil}`,
    sizeBytes: cache.sizeBytes,
    estimatedReclaimBytes: cache.reclaimableBytes,
    action: { command: buildCachePruneCommand(options), estimate: "docker-system-df-reclaimable-upper-bound" },
  };
}

function collectTmpCandidates(options: GcOptions, observedAt: string): GcCandidate[] {
  const root = "/tmp";
  if (!existsSync(root)) return [];
  const cutoffMs = new Date(observedAt).getTime() - options.tmpMinAgeHours * 60 * 60 * 1000;
  const result: GcCandidate[] = [];
  for (const entry of readdirSync(root, { withFileTypes: true })) {
    const name = entry.name;
    const path = join(root, name);
    if (TMP_EXACT_PROTECT.has(path)) continue;
    if (!TMP_PREFIX_ALLOWLIST.some((prefix) => name.startsWith(prefix))) continue;
    let stat;
    try {
      stat = lstatSync(path);
    } catch {
      continue;
    }
    if (stat.mtimeMs >= cutoffMs) continue;
    const sizeBytes = safePathSize(path);
    if (sizeBytes <= 0) continue;
    result.push({
      id: `tmp:${path}`,
      kind: "tmp-path-delete",
      risk: "low",
      description: `Delete allowlisted /tmp path older than ${options.tmpMinAgeHours} hours`,
      path,
      sizeBytes,
      estimatedReclaimBytes: sizeBytes,
      action: { op: "rm-recursive", allowlist: "tmp-prefix" },
    });
  }
  return result.sort((left, right) => right.estimatedReclaimBytes - left.estimatedReclaimBytes);
}

function collectBrowserCacheCandidate(): GcCandidate | null {
  const path = rootPath(".state", "playwright-browsers");
  if (!existsSync(path)) return null;
  const sizeBytes = safePathSize(path);
  if (sizeBytes <= 0) return null;
  return {
    id: `browser-cache:${path}`,
    kind: "browser-cache-delete",
    risk: "medium",
    description: "Delete repo-local Playwright browser cache",
    path,
    sizeBytes,
    estimatedReclaimBytes: sizeBytes,
    action: { op: "rm-recursive" },
  };
}

function collectProtectedStorage(config: UniDeskConfig): ProtectedGcItem[] {
  const result: ProtectedGcItem[] = [
    {
      kind: "docker-volume",
      risk: "blocked",
      ref: config.database.volume,
      reason: "PostgreSQL PGDATA is protected; database cleanup requires verified backup and a schema-aware retention plan.",
    },
    {
      kind: "policy",
      risk: "blocked",
      ref: "docker-images-and-volumes",
      reason: "gc does not remove Docker images, containers, volumes or Compose projects.",
    },
  ];
  const baiduStaging = rootPath(".state", "baidu-netdisk", "staging");
  if (existsSync(baiduStaging)) {
    result.push({
      kind: "baidu-netdisk-staging",
      risk: "blocked",
      ref: baiduStaging,
      sizeBytes: safePathSize(baiduStaging),
      reason: "Baidu Netdisk staging may contain backups or transfer state and is not touched by gc.",
    });
  }
  return result;
}

function collectDatabaseSummary(): unknown {
  const dbSize = psql("SELECT pg_size_pretty(pg_database_size(current_database())) AS database_size;");
  const tables = psql(
    "SELECT nspname || '.' || relname AS relation, pg_total_relation_size(c.oid)::text AS bytes, pg_size_pretty(pg_total_relation_size(c.oid)) AS size FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN ('r','m','t') AND n.nspname NOT IN ('pg_catalog','information_schema') ORDER BY pg_total_relation_size(c.oid) DESC LIMIT 10;",
  );
  return {
    mutation: false,
    note: "database is diagnostic-only in gc; oa_events cleanup is intentionally not automated",
    databaseSize: dbSize.ok ? dbSize.lines[0] ?? null : null,
    largestRelations: tables.ok
      ? tables.lines.map((line) => {
        const [relation, bytes, size] = line.split("|");
        return { relation, bytes: Number(bytes), size };
      })
      : [],
    errors: [dbSize, tables].filter((item) => !item.ok).map((item) => item.error),
  };
}

function psql(sql: string, timeoutMs = 6000): { ok: true; lines: string[] } | { ok: false; error: string } {
  const result = psqlCommand(sql, timeoutMs);
  if (result.timedOut) return { ok: false, error: `psql timed out after ${timeoutMs}ms` };
  if (result.exitCode !== 0) return { ok: false, error: result.stderr.trim() || `psql exited ${result.exitCode}` };
  return { ok: true, lines: result.stdout.trim().length === 0 ? [] : result.stdout.trim().split(/\r?\n/u) };
}

function psqlCommand(sql: string, timeoutMs = 6000): { exitCode: number | null; stdout: string; stderr: string; timedOut: boolean } {
  return command([
    "docker",
    "exec",
    "unidesk-database",
    "psql",
    "-U",
    "unidesk",
    "-d",
    "unidesk",
    "-v",
    "ON_ERROR_STOP=1",
    "-Atc",
    sql,
  ], timeoutMs);
}

function gcDbTracePlan(options: DbTraceGcOptions): unknown {
  const beforeDateSql = sqlLiteral(options.beforeDate ?? "");
  const typesSql = sqlStringArray(options.types);
  const matches = psql(
    `SELECT count(*)::text, coalesce(sum(pg_column_size(payload)),0)::text, pg_size_pretty(coalesce(sum(pg_column_size(payload)),0)) FROM public.oa_events WHERE created_at < ${beforeDateSql}::timestamptz AND type = ANY (${typesSql}::text[]);`,
    120000,
  );
  const remaining = psql(
    `SELECT count(*)::text, coalesce(sum(pg_column_size(payload)),0)::text, pg_size_pretty(coalesce(sum(pg_column_size(payload)),0)) FROM public.oa_events WHERE NOT (created_at < ${beforeDateSql}::timestamptz AND type = ANY (${typesSql}::text[]));`,
    120000,
  );
  const tableSize = psql("SELECT pg_total_relation_size('public.oa_events')::text, pg_size_pretty(pg_total_relation_size('public.oa_events'));");
  return {
    ok: matches.ok && remaining.ok && tableSize.ok,
    action: "gc db-trace plan",
    dryRun: true,
    mutation: false,
    observedAt: new Date().toISOString(),
    options: { beforeDate: options.beforeDate, types: options.types, vacuumFull: options.vacuumFull },
    diskBefore: rootDiskSnapshot(),
    target: parseCountBytes(matches),
    remaining: parseCountBytes(remaining),
    oaEventsTable: parseBytesPretty(tableSize),
    policy: {
      requiresRunConfirm: true,
      requiresVacuumFullForDfReclaim: true,
      runCommand: `bun scripts/cli.ts gc db-trace run --confirm --before-date ${options.beforeDate} --vacuum-full`,
      backupRequired: "Verify recent PostgreSQL basebackup before running.",
      scope: "Deletes only selected trace telemetry event types from public.oa_events before beforeDate.",
    },
    errors: [matches, remaining, tableSize].filter((item) => !item.ok).map((item) => (item as { error: string }).error),
  };
}

function gcDbTraceRun(options: DbTraceGcOptions): unknown {
  if (!options.vacuumFull) {
    return {
      ok: false,
      error: "gc-db-trace-run-requires-vacuum-full",
      dryRun: true,
      mutation: false,
      reason: "Plain DELETE frees rows for PostgreSQL reuse but usually does not lower df; --vacuum-full is required for the explicit disk relief path.",
      runCommand: `bun scripts/cli.ts gc db-trace run --confirm --before-date ${options.beforeDate} --vacuum-full`,
    };
  }
  const plan = gcDbTracePlan(options);
  const before = rootDiskSnapshot();
  const beforeDateSql = sqlLiteral(options.beforeDate ?? "");
  const typesSql = sqlStringArray(options.types);
  const deleted = psqlCommand(
    `DELETE FROM public.oa_events WHERE created_at < ${beforeDateSql}::timestamptz AND type = ANY (${typesSql}::text[]);`,
    10 * 60 * 1000,
  );
  if (deleted.timedOut || deleted.exitCode !== 0) {
    return {
      ok: false,
      action: "gc db-trace run",
      dryRun: false,
      mutation: true,
      observedAt: new Date().toISOString(),
      options: { beforeDate: options.beforeDate, types: options.types, vacuumFull: options.vacuumFull },
      diskBefore: before,
      error: deleted.timedOut ? "delete timed out" : deleted.stderr.trim() || `delete exited ${deleted.exitCode}`,
      plan,
    };
  }
  const vacuum = command([
    "docker",
    "exec",
    "unidesk-database",
    "psql",
    "-U",
    "unidesk",
    "-d",
    "unidesk",
    "-c",
    "VACUUM (FULL, ANALYZE) public.oa_events;",
  ], 20 * 60 * 1000);
  const after = rootDiskSnapshot();
  return {
    ok: vacuum.exitCode === 0,
    action: "gc db-trace run",
    dryRun: false,
    mutation: true,
    observedAt: new Date().toISOString(),
    options: { beforeDate: options.beforeDate, types: options.types, vacuumFull: options.vacuumFull },
    diskBefore: before,
    diskAfter: after,
    summary: {
      deletedRows: parseDeleteCount(deleted.stdout),
      actualDiskReclaimBytes: before !== null && after !== null ? after.availableBytes - before.availableBytes : null,
    },
    vacuum: boundedCommandOutput(vacuum),
    plan,
  };
}

function gcPolicyPlan(options: GcPolicyOptions): unknown {
  const files = gcPolicyFiles();
  return {
    ok: true,
    action: "gc policy plan",
    dryRun: true,
    mutation: false,
    observedAt: new Date().toISOString(),
    options,
    files,
    commands: {
      install: [
        ["mkdir", "-p", "/etc/systemd/journald.conf.d", "/etc/systemd/system"],
        ["systemctl", "daemon-reload"],
        ["systemctl", "enable", "--now", "unidesk-gc.timer"],
      ],
      journalRestart: ["systemctl", "restart", "systemd-journald"],
    },
    policy: {
      safeScope: [
        "systemd journal is capped at 512MiB",
        "daily timer runs file-log and allowlisted /tmp low-risk gc only",
        "timer does not touch PostgreSQL PGDATA, Docker images, Docker volumes or Baidu Netdisk staging",
        "timer output is redirected under .state/gc and capped by gc --result-limit",
      ],
      manualDbRetention: "gc db-trace remains explicit maintenance and is not scheduled automatically.",
    },
  };
}

function gcPolicyInstall(options: GcPolicyOptions): unknown {
  const plan = gcPolicyPlan(options);
  if (options.dryRun) return plan;
  const files = gcPolicyFiles();
  mkdirSync("/etc/systemd/journald.conf.d", { recursive: true });
  mkdirSync("/etc/systemd/system", { recursive: true });
  mkdirSync(rootPath(".state", "gc"), { recursive: true });
  writeFileSync(files.journald.path, files.journald.content, "utf8");
  writeFileSync(files.service.path, files.service.content, "utf8");
  writeFileSync(files.timer.path, files.timer.content, "utf8");
  const daemonReload = command(["systemctl", "daemon-reload"], 15000);
  const enableTimer = options.enableNow ? command(["systemctl", "enable", "--now", "unidesk-gc.timer"], 15000) : null;
  const restartJournald = command(["systemctl", "restart", "systemd-journald"], 15000);
  const timerStatus = command(["systemctl", "is-enabled", "unidesk-gc.timer"], 5000);
  const activeStatus = command(["systemctl", "is-active", "unidesk-gc.timer"], 5000);
  const results = {
    daemonReload: boundedCommandOutput(daemonReload),
    enableTimer: enableTimer === null ? { skipped: true } : boundedCommandOutput(enableTimer),
    restartJournald: boundedCommandOutput(restartJournald),
    timerStatus: {
      enabled: timerStatus.stdout.trim(),
      active: activeStatus.stdout.trim(),
    },
  };
  const failed = daemonReload.exitCode !== 0
    || restartJournald.exitCode !== 0
    || (enableTimer !== null && enableTimer.exitCode !== 0);
  return {
    ok: !failed,
    action: "gc policy install",
    dryRun: false,
    mutation: true,
    observedAt: new Date().toISOString(),
    options,
    files,
    results,
    plan,
  };
}

function gcPolicyFiles(): Record<string, { path: string; content: string }> {
  const gcStateDir = rootPath(".state", "gc");
  const bunPath = bunExecutablePath();
  const gcScript = `cd ${shellQuote(repoRoot)} && mkdir -p ${shellQuote(gcStateDir)} && ${shellQuote(bunPath)} scripts/cli.ts gc run --confirm --no-db-summary --no-build-cache --no-docker-logs --no-journal --limit 5000 --result-limit 25 > ${shellQuote(join(gcStateDir, "last-run.json"))} 2> ${shellQuote(join(gcStateDir, "last-run.stderr"))}`;
  return {
    journald: {
      path: "/etc/systemd/journald.conf.d/unidesk-gc.conf",
      content: [
        "[Journal]",
        "SystemMaxUse=512M",
        "RuntimeMaxUse=128M",
        "MaxRetentionSec=7day",
        "",
      ].join("\n"),
    },
    service: {
      path: "/etc/systemd/system/unidesk-gc.service",
      content: [
        "[Unit]",
        "Description=UniDesk low-risk disk garbage collection",
        "Documentation=file:///root/unidesk/docs/reference/cli.md",
        "",
        "[Service]",
        "Type=oneshot",
        `WorkingDirectory=${repoRoot}`,
        `ExecStart=/bin/bash -lc "${systemdDoubleQuoted(gcScript)}"`,
        "",
      ].join("\n"),
    },
    timer: {
      path: "/etc/systemd/system/unidesk-gc.timer",
      content: [
        "[Unit]",
        "Description=Daily UniDesk low-risk disk garbage collection",
        "",
        "[Timer]",
        "OnCalendar=*-*-* 03:20:00",
        "RandomizedDelaySec=20m",
        "Persistent=true",
        "",
        "[Install]",
        "WantedBy=timers.target",
        "",
      ].join("\n"),
    },
  };
}

function bunExecutablePath(): string {
  const candidates = ["/usr/bin/bun", "/root/.bun/bin/bun", process.argv[0] ?? ""];
  for (const candidate of candidates) {
    if (candidate.length > 0 && existsSync(candidate)) return resolve(candidate);
  }
  return "bun";
}

function systemdDoubleQuoted(value: string): string {
  return value
    .replace(/\\/gu, "\\\\")
    .replace(/"/gu, "\\\"")
    .replace(/%/gu, "%%");
}

function shellQuote(value: string): string {
  return `'${value.replace(/'/gu, "'\\''")}'`;
}

function sqlLiteral(value: string): string {
  return `'${value.replace(/'/gu, "''")}'`;
}

function sqlStringArray(values: string[]): string {
  return `ARRAY[${values.map(sqlLiteral).join(",")}]`;
}

function parseCountBytes(result: ReturnType<typeof psql>): unknown {
  if (!result.ok) return null;
  const [count, bytes, pretty] = (result.lines[0] ?? "0|0|0 B").split("|");
  return { count: Number(count), payloadBytes: Number(bytes), payloadSize: pretty };
}

function parseBytesPretty(result: ReturnType<typeof psql>): unknown {
  if (!result.ok) return null;
  const [bytes, pretty] = (result.lines[0] ?? "0|0 B").split("|");
  return { bytes: Number(bytes), size: pretty };
}

function parseDeleteCount(output: string): number | null {
  const match = output.match(/DELETE\s+(\d+)/u);
  return match ? Number(match[1]) : null;
}

function executeCandidate(candidate: GcCandidate, options: GcOptions): { reclaimedBytes: number | null; commandOutput?: unknown } {
  if (candidate.kind === "file-log-delete" && candidate.path !== undefined) {
    const before = safeFileSize(candidate.path);
    unlinkSync(candidate.path);
    return { reclaimedBytes: before };
  }
  if (candidate.kind === "file-log-compact" && candidate.path !== undefined) {
    return { reclaimedBytes: keepFileTail(candidate.path, options.fileLogTailBytes) };
  }
  if (candidate.kind === "docker-json-log-truncate" && candidate.path !== undefined) {
    const before = safeFileSize(candidate.path);
    ftruncateFile(candidate.path, 0);
    return { reclaimedBytes: before };
  }
  if (candidate.kind === "tmp-path-delete" && candidate.path !== undefined) {
    assertTmpCandidatePath(candidate.path);
    const before = safePathSize(candidate.path);
    rmSync(candidate.path, { recursive: true, force: true });
    return { reclaimedBytes: before };
  }
  if (candidate.kind === "browser-cache-delete" && candidate.path !== undefined) {
    const expected = rootPath(".state", "playwright-browsers");
    if (resolve(candidate.path) !== resolve(expected)) throw new Error(`refusing to remove unexpected browser cache path: ${candidate.path}`);
    const before = safePathSize(candidate.path);
    rmSync(candidate.path, { recursive: true, force: true });
    return { reclaimedBytes: before };
  }
  if (candidate.kind === "journal-vacuum") {
    const result = command(["journalctl", `--vacuum-size=${options.journalTargetBytes}`], 30000);
    if (result.exitCode !== 0) throw new Error(result.stderr.trim() || "journalctl vacuum failed");
    return { reclaimedBytes: null, commandOutput: boundedCommandOutput(result) };
  }
  if (candidate.kind === "docker-build-cache-prune") {
    const result = command(buildCachePruneCommand(options), 45000);
    if (result.exitCode !== 0) throw new Error(result.stderr.trim() || "docker builder prune failed");
    return { reclaimedBytes: null, commandOutput: boundedCommandOutput(result) };
  }
  throw new Error(`unsupported gc candidate kind: ${candidate.kind}`);
}

function buildCachePruneCommand(options: GcOptions): string[] {
  const commandArgs = ["docker", "builder", "prune", "--all", "--force"];
  if (!options.buildCacheAll) commandArgs.push("--filter", `until=${options.buildCacheUntil}`);
  return commandArgs;
}

function keepFileTail(path: string, tailBytes: number): number {
  const before = safeFileSize(path);
  if (before <= tailBytes) return 0;
  const bytesToRead = Math.min(before, tailBytes);
  const fd = openSync(path, "r+");
  const buffer = Buffer.alloc(bytesToRead);
  try {
    readSync(fd, buffer, 0, bytesToRead, before - bytesToRead);
    ftruncateSync(fd, 0);
    writeSync(fd, buffer, 0, bytesToRead, 0);
    ftruncateSync(fd, bytesToRead);
  } finally {
    closeSync(fd);
  }
  return Math.max(0, before - bytesToRead);
}

function ftruncateFile(path: string, size: number): void {
  const fd = openSync(path, "r+");
  try {
    ftruncateSync(fd, size);
  } finally {
    closeSync(fd);
  }
}

function assertTmpCandidatePath(path: string): void {
  const resolved = resolve(path);
  if (!resolved.startsWith("/tmp/")) throw new Error(`refusing to remove non-/tmp path: ${path}`);
  if (TMP_EXACT_PROTECT.has(resolved)) throw new Error(`refusing to remove protected tmp path: ${path}`);
  const name = basename(resolved);
  if (!TMP_PREFIX_ALLOWLIST.some((prefix) => name.startsWith(prefix))) {
    throw new Error(`refusing to remove tmp path outside allowlist: ${path}`);
  }
}

function summarizeCandidates(candidates: GcCandidate[], returnedCandidates: GcCandidate[]): GcPlan["summary"] {
  const byKind: GcPlan["summary"]["byKind"] = {};
  let estimatedReclaimBytes = 0;
  for (const candidate of candidates) {
    estimatedReclaimBytes += candidate.estimatedReclaimBytes;
    const current = byKind[candidate.kind] ?? { count: 0, estimatedReclaimBytes: 0, estimatedReclaim: "0 B" };
    current.count += 1;
    current.estimatedReclaimBytes += candidate.estimatedReclaimBytes;
    current.estimatedReclaim = formatBytes(current.estimatedReclaimBytes);
    byKind[candidate.kind] = current;
  }
  const returnedEstimatedReclaimBytes = returnedCandidates.reduce((sum, candidate) => sum + candidate.estimatedReclaimBytes, 0);
  return {
    candidateCount: candidates.length,
    returnedCandidateCount: returnedCandidates.length,
    estimatedReclaimBytes,
    estimatedReclaim: formatBytes(estimatedReclaimBytes),
    returnedEstimatedReclaimBytes,
    returnedEstimatedReclaim: formatBytes(returnedEstimatedReclaimBytes),
    byKind,
  };
}

function returnedRunResults(results: GcRunResult["results"], options: GcOptions): GcRunResult["results"] {
  if (options.full) return results;
  const failed = results.filter((item) => item.status === "failed");
  const succeeded = results.filter((item) => item.status === "succeeded");
  return [...failed, ...succeeded].slice(0, options.resultLimit);
}

function collectFiles(root: string): Array<{ path: string; sizeBytes: number; mtimeMs: number }> {
  const result: Array<{ path: string; sizeBytes: number; mtimeMs: number }> = [];
  const visit = (dir: string): void => {
    let entries;
    try {
      entries = readdirSync(dir, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries) {
      const path = join(dir, entry.name);
      try {
        const stat = lstatSync(path);
        if (entry.isDirectory()) {
          visit(path);
        } else if (entry.isFile()) {
          result.push({ path, sizeBytes: stat.size, mtimeMs: stat.mtimeMs });
        }
      } catch {
        // Ignore paths that disappear while gc is planning.
      }
    }
  };
  visit(root);
  return result;
}

function safePathSize(path: string): number {
  try {
    const stat = lstatSync(path);
    if (stat.isFile() || stat.isSymbolicLink()) return stat.size;
    if (!stat.isDirectory()) return 0;
    let total = 0;
    for (const entry of readdirSync(path)) {
      total += safePathSize(join(path, entry));
    }
    return total;
  } catch {
    return 0;
  }
}

function safeFileSize(path: string): number {
  try {
    return statSync(path).size;
  } catch {
    return 0;
  }
}

function resolvePath(path: string): string {
  return path.startsWith("/") ? path : rootPath(path);
}

function rootDiskSnapshot(): DiskSnapshot | null {
  const result = command(["df", "-B1", "-P", "/"], 5000);
  if (result.exitCode !== 0) return null;
  const line = result.stdout.trim().split(/\r?\n/u)[1];
  if (!line) return null;
  const parts = line.trim().split(/\s+/u);
  if (parts.length < 6) return null;
  return {
    filesystem: parts[0] ?? "",
    sizeBytes: Number(parts[1]),
    usedBytes: Number(parts[2]),
    availableBytes: Number(parts[3]),
    usePercent: Number((parts[4] ?? "0").replace("%", "")),
    mount: parts[5] ?? "/",
  };
}

function dockerContainers(): Array<{ id: string; name: string; image: string; logPath: string }> {
  const ps = command(["docker", "ps", "-qa", "--no-trunc"], 5000);
  if (ps.exitCode !== 0 || ps.stdout.trim().length === 0) return [];
  const ids = ps.stdout.trim().split(/\s+/u);
  const inspect = command(["docker", "inspect", ...ids], 10000);
  if (inspect.exitCode !== 0 || inspect.stdout.trim().length === 0) return [];
  try {
    const parsed = JSON.parse(inspect.stdout) as unknown;
    if (!Array.isArray(parsed)) return [];
    return parsed.map((item) => {
      const record = item as Record<string, unknown>;
      const config = typeof record.Config === "object" && record.Config !== null ? record.Config as Record<string, unknown> : {};
      return {
        id: String(record.Id ?? ""),
        name: String(record.Name ?? "").replace(/^\//u, ""),
        image: String(config.Image ?? record.Image ?? ""),
        logPath: String(record.LogPath ?? ""),
      };
    }).filter((item) => item.id.length > 0);
  } catch {
    return [];
  }
}

function parseJournalUsage(text: string): number | null {
  const match = text.match(/take up\s+([0-9.]+)\s*([KMGT]?)(?:i?B|B)?/iu);
  if (!match) return null;
  const value = Number(match[1]);
  const unit = (match[2] ?? "").toUpperCase();
  const multiplier = unit === "T" ? 1024 ** 4 : unit === "G" ? 1024 ** 3 : unit === "M" ? 1024 ** 2 : unit === "K" ? 1024 : 1;
  return Math.floor(value * multiplier);
}

function parseDockerSystemDfBuildCache(text: string): { sizeBytes: number; reclaimableBytes: number } | null {
  for (const line of text.split(/\r?\n/u)) {
    if (!line.startsWith("Build Cache")) continue;
    const match = line.trim().match(/^Build Cache\s+\S+\s+\S+\s+(\S+)\s+(\S+)/u);
    if (!match) continue;
    const sizeBytes = parseDockerHumanSize(match[1] ?? "");
    const reclaimableBytes = parseDockerHumanSize(match[2] ?? "");
    if (sizeBytes === null || reclaimableBytes === null) return null;
    return { sizeBytes, reclaimableBytes };
  }
  return null;
}

function parseDockerHumanSize(raw: string): number | null {
  const cleaned = raw.replace(/\(.+$/u, "");
  const match = cleaned.match(/^([0-9.]+)\s*([KMGT]?B)$/iu);
  if (!match) return null;
  const value = Number(match[1]);
  const unit = match[2]?.toUpperCase();
  const multiplier = unit === "TB" ? 1000 ** 4 : unit === "GB" ? 1000 ** 3 : unit === "MB" ? 1000 ** 2 : unit === "KB" ? 1000 : 1;
  return Math.floor(value * multiplier);
}

function command(commandArgs: string[], timeoutMs: number): { exitCode: number | null; stdout: string; stderr: string; timedOut: boolean } {
  const result = spawnSync(commandArgs[0], commandArgs.slice(1), {
    cwd: repoRoot,
    encoding: "utf8",
    maxBuffer: 1024 * 1024 * 8,
    timeout: timeoutMs,
  });
  const error = result.error as (Error & { code?: string }) | undefined;
  return {
    exitCode: result.status,
    stdout: result.stdout ?? "",
    stderr: result.stderr ?? error?.message ?? "",
    timedOut: error?.code === "ETIMEDOUT",
  };
}

function boundedCommandOutput(result: { stdout: string; stderr: string; exitCode: number | null; timedOut: boolean }): unknown {
  return {
    exitCode: result.exitCode,
    timedOut: result.timedOut,
    stdoutTail: result.stdout.slice(-2000),
    stderrTail: result.stderr.slice(-2000),
  };
}

function formatBytes(bytes: number): string {
  const units = ["B", "KiB", "MiB", "GiB", "TiB"];
  let value = Math.max(0, bytes);
  let unitIndex = 0;
  while (value >= 1024 && unitIndex < units.length - 1) {
    value /= 1024;
    unitIndex += 1;
  }
  return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`;
}
