import { accessSync, constants, existsSync, readFileSync, statSync } from "node:fs";
import { runCommand } from "./command";
import { repoRoot } from "./config";

const defaultSwapPath = "/swapfile";
const defaultSwapSizeBytes = 2 * 1024 * 1024 * 1024;

export interface SwapArea {
  filename: string;
  type: string;
  sizeBytes: number;
  usedBytes: number;
  priority: number | null;
}

export interface SwapMemoryStatus {
  totalBytes: number;
  availableBytes: number | null;
  swapTotalBytes: number;
  swapFreeBytes: number;
}

export interface SwapStatus {
  memory: SwapMemoryStatus;
  activeSwaps: SwapArea[];
  configuredPath: string;
  configuredPathExists: boolean;
  configuredPathMode: string | null;
  configuredPathSizeBytes: number | null;
  configuredPathActive: boolean;
  fstab: {
    path: string;
    writable: boolean;
    persisted: boolean;
    matchingLine: string | null;
    error: string | null;
  };
  warning: string | null;
}

export interface SwapEnsureResult {
  ok: boolean;
  status: "ok" | "degraded" | "failed";
  requested: {
    path: string;
    sizeBytes: number;
  };
  before: SwapStatus;
  after: SwapStatus;
  actions: Array<{ action: string; ok: boolean; detail?: unknown }>;
  errors: Array<{ action: string; message: string; detail?: unknown }>;
}

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

function parseByteCount(value: string): number {
  const raw = value.trim();
  if (/^\d+$/u.test(raw)) return Number(raw);
  const match = raw.match(/^([0-9]+(?:\.[0-9]+)?)([KMGTPE]?i?B?)$/iu);
  if (!match) return 0;
  const amount = Number(match[1]);
  const unit = match[2].toUpperCase();
  const powers: Record<string, number> = {
    K: 1,
    KB: 1,
    KIB: 1,
    M: 2,
    MB: 2,
    MIB: 2,
    G: 3,
    GB: 3,
    GIB: 3,
    T: 4,
    TB: 4,
    TIB: 4,
    P: 5,
    PB: 5,
    PIB: 5,
    E: 6,
    EB: 6,
    EIB: 6,
  };
  return Math.round(amount * (1024 ** (powers[unit] ?? 0)));
}

function parseMeminfo(): SwapMemoryStatus {
  const raw = readFileSync("/proc/meminfo", "utf8");
  const values = new Map<string, number>();
  for (const line of raw.split("\n")) {
    const match = line.match(/^([^:]+):\s+(\d+)\s+kB/u);
    if (match) values.set(match[1], Number(match[2]) * 1024);
  }
  return {
    totalBytes: values.get("MemTotal") ?? 0,
    availableBytes: values.get("MemAvailable") ?? null,
    swapTotalBytes: values.get("SwapTotal") ?? 0,
    swapFreeBytes: values.get("SwapFree") ?? 0,
  };
}

function parseSwaps(): SwapArea[] {
  if (!existsSync("/proc/swaps")) return [];
  const lines = readFileSync("/proc/swaps", "utf8").trim().split("\n").slice(1);
  return lines.map((line) => line.trim().split(/\s+/u)).filter((parts) => parts.length >= 5).map(([filename, type, sizeKiB, usedKiB, priority]) => ({
    filename,
    type,
    sizeBytes: Number(sizeKiB) * 1024,
    usedBytes: Number(usedKiB) * 1024,
    priority: Number.isFinite(Number(priority)) ? Number(priority) : null,
  }));
}

function fileMode(path: string): string | null {
  if (!existsSync(path)) return null;
  return (statSync(path).mode & 0o777).toString(8).padStart(3, "0");
}

function fstabStatus(path: string): SwapStatus["fstab"] {
  const fstabPath = "/etc/fstab";
  try {
    const raw = existsSync(fstabPath) ? readFileSync(fstabPath, "utf8") : "";
    let writable = false;
    try {
      accessSync(fstabPath, constants.W_OK);
      writable = true;
    } catch {
      writable = false;
    }
    const matchingLine = raw.split("\n").find((line) => {
      const trimmed = line.trim();
      if (trimmed.length === 0 || trimmed.startsWith("#")) return false;
      const parts = trimmed.split(/\s+/u);
      return parts[0] === path && parts[2] === "swap";
    }) ?? null;
    return {
      path: fstabPath,
      writable,
      persisted: matchingLine !== null,
      matchingLine,
      error: null,
    };
  } catch (error) {
    return {
      path: fstabPath,
      writable: false,
      persisted: false,
      matchingLine: null,
      error: error instanceof Error ? error.message : String(error),
    };
  }
}

export function swapStatus(path = defaultSwapPath): SwapStatus {
  const memory = parseMeminfo();
  const activeSwaps = parseSwaps();
  const configuredPathExists = existsSync(path);
  const configuredPathSizeBytes = configuredPathExists ? statSync(path).size : null;
  const configuredPathActive = activeSwaps.some((swap) => swap.filename === path);
  const warning = memory.swapTotalBytes > 0 ? null : "swap is not active; low-memory main servers are at risk of global OOM during builds or diagnostics";
  return {
    memory,
    activeSwaps,
    configuredPath: path,
    configuredPathExists,
    configuredPathMode: fileMode(path),
    configuredPathSizeBytes,
    configuredPathActive,
    fstab: fstabStatus(path),
    warning,
  };
}

function pushAction(
  actions: SwapEnsureResult["actions"],
  errors: SwapEnsureResult["errors"],
  action: string,
  command: string[],
): boolean {
  const result = runCommand(command, repoRoot, { timeoutMs: 120_000 });
  const ok = result.exitCode === 0;
  const detail = {
    command,
    exitCode: result.exitCode,
    stdoutTail: result.stdout.slice(-1200),
    stderrTail: result.stderr.slice(-1200),
    timedOut: result.timedOut,
  };
  actions.push({ action, ok, detail });
  if (!ok) {
    errors.push({
      action,
      message: result.stderr.trim() || result.stdout.trim() || `command failed with exit code ${result.exitCode}`,
      detail,
    });
  }
  return ok;
}

function ensureFstabLine(path: string): { ok: boolean; action: string; detail: unknown } {
  const line = `${path} none swap sw 0 0`;
  const script = [
    "set -euo pipefail",
    "touch /etc/fstab",
    `grep -Eq '^${path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[[:space:]]+[^[:space:]]+[[:space:]]+swap[[:space:]]' /etc/fstab || printf '%s\\n' ${shellQuote(line)} >> /etc/fstab`,
  ].join("\n");
  const result = runCommand(["bash", "-lc", script], repoRoot, { timeoutMs: 30_000 });
  return {
    ok: result.exitCode === 0,
    action: "persist-fstab",
    detail: {
      command: ["bash", "-lc", script],
      exitCode: result.exitCode,
      stdoutTail: result.stdout.slice(-1200),
      stderrTail: result.stderr.slice(-1200),
      timedOut: result.timedOut,
    },
  };
}

function parseSizeOption(args: string[], defaultBytes: number): number {
  const index = args.indexOf("--size");
  const raw = index === -1 ? undefined : args[index + 1];
  if (raw === undefined) return defaultBytes;
  const bytes = parseByteCount(raw);
  if (!Number.isFinite(bytes) || bytes <= 0) throw new Error("--size must be a positive byte count such as 2GiB or 4096M");
  return bytes;
}

function parsePathOption(args: string[], defaultPath: string): string {
  const index = args.indexOf("--path");
  if (index === -1) return defaultPath;
  const raw = args[index + 1];
  if (raw === undefined || !raw.startsWith("/")) throw new Error("--path must be an absolute path");
  return raw;
}

function hasFlag(args: string[], name: string): boolean {
  return args.includes(name);
}

export function runSwapCommand(args: string[]): unknown {
  const [action = "status"] = args;
  const path = parsePathOption(args, defaultSwapPath);
  if (action === "status") return swapStatus(path);
  if (action === "ensure") {
    const sizeBytes = parseSizeOption(args, defaultSwapSizeBytes);
    const dryRun = hasFlag(args, "--dry-run");
    const before = swapStatus(path);
    const actions: SwapEnsureResult["actions"] = [];
    const errors: SwapEnsureResult["errors"] = [];
    if (before.memory.swapTotalBytes > 0) {
      actions.push({ action: "noop-existing-swap", ok: true, detail: { activeSwaps: before.activeSwaps } });
      const after = swapStatus(path);
      return { ok: true, status: "ok", requested: { path, sizeBytes }, before, after, actions, errors } satisfies SwapEnsureResult;
    }
    if (dryRun) {
      actions.push({ action: "dry-run", ok: true, detail: { wouldCreate: path, sizeBytes, wouldPersistFstab: true } });
      const after = swapStatus(path);
      return { ok: true, status: "degraded", requested: { path, sizeBytes }, before, after, actions, errors } satisfies SwapEnsureResult;
    }
    if (!existsSync(path)) {
      const sizeMiB = Math.ceil(sizeBytes / 1024 / 1024);
      const allocated = pushAction(actions, errors, "allocate-swapfile", ["fallocate", "-l", `${sizeMiB}M`, path]);
      if (!allocated) pushAction(actions, errors, "allocate-swapfile-dd-fallback", ["dd", "if=/dev/zero", `of=${path}`, "bs=1M", `count=${sizeMiB}`, "status=none"]);
    } else {
      const existingBytes = statSync(path).size;
      if (existingBytes < sizeBytes) {
        const sizeMiB = Math.ceil(sizeBytes / 1024 / 1024);
        const resized = pushAction(actions, errors, "resize-existing-swapfile", ["fallocate", "-l", `${sizeMiB}M`, path]);
        if (!resized) pushAction(actions, errors, "resize-existing-swapfile-dd-fallback", ["dd", "if=/dev/zero", `of=${path}`, "bs=1M", `count=${sizeMiB}`, "status=none"]);
      } else {
        actions.push({ action: "reuse-existing-swapfile-path", ok: true, detail: { path, sizeBytes: existingBytes } });
      }
    }
    pushAction(actions, errors, "chmod-600", ["chmod", "600", path]);
    pushAction(actions, errors, "mkswap", ["mkswap", path]);
    pushAction(actions, errors, "swapon", ["swapon", path]);
    const persist = ensureFstabLine(path);
    actions.push({ action: persist.action, ok: persist.ok, detail: persist.detail });
    if (!persist.ok) {
      errors.push({
        action: persist.action,
        message: "swap is active but /etc/fstab could not be updated; rerun ensure as root or add the returned fstab line manually",
        detail: persist.detail,
      });
    }
    const after = swapStatus(path);
    const swapActive = after.memory.swapTotalBytes > 0;
    const status = swapActive && after.fstab.persisted ? "ok" : swapActive ? "degraded" : "failed";
    return {
      ok: status !== "failed",
      status,
      requested: { path, sizeBytes },
      before,
      after,
      actions,
      errors,
    } satisfies SwapEnsureResult;
  }
  throw new Error("server swap command must be one of: status, ensure");
}
