const fs = require("fs");
const path = require("path");
const { resolveData } = require("./paths");

const CATEGORY_DIRS = {
  models: "models",
  runtime: "runtime",
  logs: "logs",
  metrics: "metrics",
  diagnostics: "diagnostics",
  cache: "cache",
  feedback: "feedback",
  corrections: "corrections",
  evals: "evals",
  exports: "exports",
  tools: "tools",
  tmp: "tmp"
};

function folderSize(dir) {
  if (!fs.existsSync(dir)) return 0;
  return fs.readdirSync(dir, { withFileTypes: true }).reduce((total, entry) => {
    if (entry.name === ".gitkeep") return total;
    const target = path.join(dir, entry.name);
    return total + (entry.isDirectory() ? folderSize(target) : entry.isFile() ? fs.statSync(target).size : 0);
  }, 0);
}

function storageUsage(models = [], selectedModelId = null) {
  const categories = Object.fromEntries(
    Object.entries(CATEGORY_DIRS).map(([key, dir]) => [key, folderSize(resolveData(dir))])
  );
  const installedModels = models.map((model) => {
    const file = modelPath(model);
    return {
      id: model.id,
      label: model.label,
      filename: model.filename,
      selected: model.id === selectedModelId,
      installed: fs.existsSync(file),
      size: fs.existsSync(file) ? fs.statSync(file).size : 0
    };
  });
  const runtimeArchives = listFiles(resolveData("tmp"))
    .filter((file) => /\.(zip|tar\.gz|part)$/i.test(file.name));
  return {
    categories,
    installed_models: installedModels,
    runtime_archives: runtimeArchives,
    total: Object.values(categories).reduce((sum, value) => sum + value, 0)
  };
}

function deleteModel(model, { selectedModelId, gateModelId = null, runtimeRunning, gateRuntimeRunning = false, confirmed }) {
  if (!model) throw new Error("Unknown model.");
  if (!confirmed) throw new Error("Model deletion requires confirmation.");
  if (model.id === selectedModelId && runtimeRunning) {
    throw new Error("Stop the runtime before deleting the selected model.");
  }
  if (model.id === gateModelId && gateRuntimeRunning) {
    throw new Error("Stop the gate runtime before deleting the gate model.");
  }
  const file = modelPath(model);
  if (!fs.existsSync(file)) return { deleted: false, bytes_recovered: 0 };
  const bytes = fs.statSync(file).size;
  fs.rmSync(file, { force: true });
  return { deleted: true, bytes_recovered: bytes };
}

function cleanupStorage(categories, { models, selectedModelId, gateModelId = null, runtimeRunning, activeLogPath = null }) {
  const selected = new Set(Array.isArray(categories) ? categories : []);
  const result = {};
  if (selected.has("unused_models")) {
    let recovered = 0;
    for (const model of models) {
      if (model.id === selectedModelId || model.id === gateModelId) continue;
      const file = modelPath(model);
      if (fs.existsSync(file)) {
        recovered += fs.statSync(file).size;
        fs.rmSync(file, { force: true });
      }
    }
    result.unused_models = recovered;
  }
  if (selected.has("runtime_archives")) {
    result.runtime_archives = removeMatching(resolveData("tmp"), (name) => /\.(zip|tar\.gz|part)$/i.test(name));
  }
  if (selected.has("logs")) {
    result.logs = removeMatching(resolveData("logs"), (_name, _entry, target) =>
      !activeLogPath || path.resolve(target) !== path.resolve(activeLogPath)
    );
  }
  if (selected.has("diagnostics")) result.diagnostics = removeMatching(resolveData("diagnostics"), () => true);
  if (selected.has("cache")) result.cache = clearDirectory(resolveData("cache"));
  if (selected.has("tmp")) result.tmp = clearDirectory(resolveData("tmp"));
  if (selected.has("metrics")) result.metrics = clearDirectory(resolveData("metrics"));
  if (selected.has("runtime")) {
    if (runtimeRunning) throw new Error("Stop the runtime before removing extracted runtime files.");
    result.runtime = clearDirectory(resolveData("runtime"));
  }
  return { recovered_bytes: Object.values(result).reduce((sum, value) => sum + value, 0), categories: result };
}

function listLogs() {
  return listFiles(resolveData("logs"))
    .filter((file) => file.name.endsWith(".log"))
    .sort((a, b) => b.modified_at_ms - a.modified_at_ms);
}

function listLogsPage(pageValue = 1, pageSize = 25) {
  return paginateFileRows(listLogs(), pageValue, pageSize);
}

function paginateFileRows(rows, pageValue = 1, pageSize = 25) {
  const size = Math.max(1, Number.parseInt(pageSize, 10) || 25);
  const pages = Math.max(1, Math.ceil(rows.length / size));
  const page = Math.min(pages, Math.max(1, Number.parseInt(pageValue, 10) || 1));
  const start = (page - 1) * size;
  return { entries: rows.slice(start, start + size), page, pages, page_size: size, total: rows.length };
}

function resolveLog(name) {
  const safeName = path.basename(String(name || ""));
  if (!safeName || safeName !== name || !safeName.endsWith(".log")) throw new Error("Invalid log file.");
  const file = resolveData("logs", safeName);
  if (!fs.existsSync(file) || !fs.statSync(file).isFile()) throw new Error("Log file not found.");
  return file;
}

function readLogTail(name, maxBytes = 262144) {
  const file = resolveLog(name);
  const stat = fs.statSync(file);
  const length = Math.min(stat.size, maxBytes);
  const buffer = Buffer.alloc(length);
  const descriptor = fs.openSync(file, "r");
  try {
    fs.readSync(descriptor, buffer, 0, length, Math.max(0, stat.size - length));
  } finally {
    fs.closeSync(descriptor);
  }
  return {
    name: path.basename(file),
    size: stat.size,
    modified_at: stat.mtime.toISOString(),
    truncated: stat.size > length,
    content: buffer.toString("utf8")
  };
}

function deleteLog(name, activeLogPath = null) {
  const file = resolveLog(name);
  if (activeLogPath && path.resolve(file) === path.resolve(activeLogPath)) {
    throw new Error("The active runtime log cannot be deleted.");
  }
  const bytes = fs.statSync(file).size;
  fs.rmSync(file, { force: true });
  return { deleted: true, bytes_recovered: bytes };
}

function modelPath(model) {
  const file = resolveData("models", model.filename);
  const modelsDir = resolveData("models");
  if (path.dirname(file) !== modelsDir) throw new Error("Invalid model path.");
  return file;
}

function listFiles(dir) {
  if (!fs.existsSync(dir)) return [];
  return fs.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => {
    const file = path.join(dir, entry.name);
    const stat = fs.statSync(file);
    return { name: entry.name, size: stat.size, modified_at: stat.mtime.toISOString(), modified_at_ms: stat.mtimeMs };
  });
}

function removeMatching(dir, predicate) {
  if (!fs.existsSync(dir)) return 0;
  let recovered = 0;
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    const target = path.join(dir, entry.name);
    if (!predicate(entry.name, entry, target)) continue;
    recovered += entry.isDirectory() ? folderSize(target) : entry.isFile() ? fs.statSync(target).size : 0;
    fs.rmSync(target, { recursive: true, force: true });
  }
  return recovered;
}

function clearDirectory(dir) {
  return removeMatching(dir, () => true);
}

module.exports = {
  folderSize,
  storageUsage,
  deleteModel,
  cleanupStorage,
  listLogs,
  listLogsPage,
  paginateFileRows,
  resolveLog,
  readLogTail,
  deleteLog,
  modelPath
};
