import type { UniDeskConfig } from "./config";
import { parseJsonOutput } from "./platform-infra-ops-library";
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";

export interface HostSwapDeclaration {
  configPath: string;
  nodeId: string;
  route: string;
  minDiskFreeGiB: number;
  swap: {
    ensure: boolean;
    size: string;
    path: string;
    expansionPath: string;
    swappiness: number;
    sysctlFile: string;
  };
}

interface HostSwapOptions {
  configPath: string;
  confirm: boolean;
  full: boolean;
  raw: boolean;
  jobId: string | null;
}

type HostSwapDeclarationLoader = (configPath: string) => HostSwapDeclaration;

const defaultConfigPath = "config/platform-db/postgres-pk01.yaml";

export async function runPlatformDbHostSwapCommand(
  config: UniDeskConfig,
  action: string,
  args: string[],
  loadDeclaration: HostSwapDeclarationLoader,
): Promise<Record<string, unknown>> {
  const options = parseOptions(args);
  const declaration = loadDeclaration(options.configPath);
  if (!declaration.swap.ensure) {
    return {
      ok: false,
      error: "host-swap-management-disabled-by-yaml",
      mutation: false,
      configSource: `${declaration.configPath}#node.swap.ensure`,
    };
  }
  if (action === "plan") return await plan(config, declaration, options);
  if (action === "status") return await status(config, declaration, options);
  if (action === "apply") return await apply(config, declaration, options);
  return { ok: false, error: "unsupported-platform-db-swap-command", action, help: swapHelp(declaration.configPath) };
}

export function swapHelp(configPath = defaultConfigPath): Record<string, unknown> {
  return {
    command: "platform-db postgres swap plan|status|apply",
    output: "json",
    usage: [
      `bun scripts/cli.ts platform-db postgres swap plan --config ${configPath}`,
      `bun scripts/cli.ts platform-db postgres swap status --config ${configPath}`,
      `bun scripts/cli.ts platform-db postgres swap apply --config ${configPath} --confirm`,
      `bun scripts/cli.ts platform-db postgres swap status --config ${configPath} --job-id <id>`,
    ],
    safety: {
      sourceOfTruth: `${configPath}#node.swap`,
      asynchronousMutation: true,
      expansion: "keep every active swap area online and add only the YAML-declared missing capacity",
      webProbe: "swap remains excluded from the WebProbe MemAvailable admission threshold",
    },
  };
}

function parseOptions(args: string[]): HostSwapOptions {
  const options: HostSwapOptions = { configPath: defaultConfigPath, confirm: false, full: false, raw: false, jobId: null };
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--config") {
      const value = args[index + 1];
      if (value === undefined || value.trim().length === 0) throw new Error("--config requires a non-empty path");
      options.configPath = value;
      index += 1;
    } else if (arg === "--job-id") {
      const value = args[index + 1];
      if (value === undefined || !/^[A-Za-z0-9_.-]+$/u.test(value)) throw new Error("--job-id requires a safe non-empty identifier");
      options.jobId = value;
      index += 1;
    } else if (arg === "--confirm") {
      options.confirm = true;
    } else if (arg === "--full") {
      options.full = true;
    } else if (arg === "--raw") {
      options.raw = true;
      options.full = true;
    } else {
      throw new Error(`unsupported platform-db postgres swap option: ${arg}`);
    }
  }
  return options;
}

async function plan(config: UniDeskConfig, declaration: HostSwapDeclaration, options: HostSwapOptions): Promise<Record<string, unknown>> {
  if (options.jobId !== null || options.confirm) throw new Error("swap plan does not accept --job-id or --confirm");
  const observed = await collect(config, declaration);
  const desiredBytes = parseByteSize(declaration.swap.size);
  const facts = observed.parsed;
  const declaredCapacity = numberValue(facts?.declaredCapacityBytes);
  const diskAvailable = numberValue(facts?.diskAvailableBytes);
  const reserveBytes = declaration.minDiskFreeGiB * 1024 ** 3;
  const additionalDiskBytes = declaredCapacity === null ? desiredBytes : Math.max(0, desiredBytes - declaredCapacity);
  const diskReady = diskAvailable !== null && diskAvailable >= additionalDiskBytes + reserveBytes;
  const inSync = isInSync(facts, declaration, desiredBytes);
  return {
    ok: observed.capture.exitCode === 0 && facts !== null && (inSync || diskReady),
    action: "platform-db-postgres-swap-plan",
    mutation: false,
    target: targetSummary(declaration),
    desired: desiredSummary(declaration, desiredBytes),
    observed: compactFacts(facts),
    plan: {
      action: inSync ? "none" : "add-declared-swap-capacity",
      inSync,
      diskReady,
      additionalDiskBytes,
      reserveBytes,
      swapExcludedFromWebProbeAvailability: true,
      steps: inSync ? [] : [
        "keep-all-current-swap-active",
        "allocate-and-enable-declared-extension",
        "persist-fstab-and-swappiness",
        "verify-declared-capacity",
      ],
    },
    remote: compactCapture(observed.capture, options.full),
    ...(options.raw ? { raw: observed.capture } : {}),
    next: inSync
      ? { status: statusCommand(declaration) }
      : {
          apply: `bun scripts/cli.ts platform-db postgres swap apply --config ${declaration.configPath} --confirm`,
          status: statusCommand(declaration),
        },
  };
}

async function status(config: UniDeskConfig, declaration: HostSwapDeclaration, options: HostSwapOptions): Promise<Record<string, unknown>> {
  if (options.confirm) throw new Error("swap status does not accept --confirm");
  if (options.jobId !== null) return await jobStatus(config, declaration, options.jobId, options.full, options.raw);
  const observed = await collect(config, declaration);
  const desiredBytes = parseByteSize(declaration.swap.size);
  const inSync = isInSync(observed.parsed, declaration, desiredBytes);
  return {
    ok: observed.capture.exitCode === 0 && observed.parsed !== null,
    action: "platform-db-postgres-swap-status",
    mutation: false,
    target: targetSummary(declaration),
    desired: desiredSummary(declaration, desiredBytes),
    observed: compactFacts(observed.parsed),
    policySync: { inSync, swapExcludedFromWebProbeAvailability: true },
    remote: compactCapture(observed.capture, options.full),
    ...(options.raw ? { raw: observed.capture } : {}),
    next: inSync ? {} : { plan: `bun scripts/cli.ts platform-db postgres swap plan --config ${declaration.configPath}` },
  };
}

async function apply(config: UniDeskConfig, declaration: HostSwapDeclaration, options: HostSwapOptions): Promise<Record<string, unknown>> {
  if (options.jobId !== null) throw new Error("swap apply does not accept --job-id");
  if (!options.confirm) return await plan(config, declaration, { ...options, confirm: false });
  const desiredBytes = parseByteSize(declaration.swap.size);
  const payload = {
    path: declaration.swap.path,
    expansionPath: declaration.swap.expansionPath,
    desiredBytes,
    minDiskFreeBytes: declaration.minDiskFreeGiB * 1024 ** 3,
    swappiness: declaration.swap.swappiness,
    sysctlFile: declaration.swap.sysctlFile,
  };
  const capture = await runSshCommandCapture(config, declaration.route, ["sh"], startJobScript(payload));
  const parsed = parseJsonOutput(capture.stdout);
  const jobId = stringValue(parsed?.jobId);
  const ok = capture.exitCode === 0 && parsed?.ok === true && jobId !== null;
  return {
    ok,
    action: "platform-db-postgres-swap-apply",
    mutation: ok,
    target: targetSummary(declaration),
    desired: desiredSummary(declaration, desiredBytes),
    remoteJob: parsed,
    remote: compactCapture(capture, options.full || !ok),
    ...(options.raw ? { raw: capture } : {}),
    next: jobId === null
      ? { plan: `bun scripts/cli.ts platform-db postgres swap plan --config ${declaration.configPath}` }
      : { status: `${statusCommand(declaration)} --job-id ${jobId}` },
  };
}

async function collect(config: UniDeskConfig, declaration: HostSwapDeclaration): Promise<{ capture: SshCaptureResult; parsed: Record<string, unknown> | null }> {
  const encoded = Buffer.from(JSON.stringify({
    path: declaration.swap.path,
    expansionPath: declaration.swap.expansionPath,
    sysctlFile: declaration.swap.sysctlFile,
  }), "utf8").toString("base64");
  const capture = await runSshCommandCapture(config, declaration.route, ["sh"], factsScript(encoded));
  return { capture, parsed: parseJsonOutput(capture.stdout) };
}

async function jobStatus(
  config: UniDeskConfig,
  declaration: HostSwapDeclaration,
  jobId: string,
  full: boolean,
  raw: boolean,
): Promise<Record<string, unknown>> {
  const capture = await runSshCommandCapture(config, declaration.route, ["sh"], `
set -eu
sudo -n sh <<'ROOT'
job_dir="/root/.unidesk/platform-db/swap-jobs/${jobId}"
if [ ! -d "$job_dir" ]; then
  printf '{"ok":false,"status":"missing","jobId":"${jobId}"}\\n'
  exit 0
fi
state='{}'
result='null'
[ ! -f "$job_dir/state.json" ] || state="$(cat "$job_dir/state.json")"
[ ! -f "$job_dir/result.json" ] || result="$(cat "$job_dir/result.json")"
python3 - "$state" "$result" <<'PY'
import json, sys
state=json.loads(sys.argv[1])
result=json.loads(sys.argv[2])
print(json.dumps({"ok": True, "job": state, "result": result}, separators=(",", ":")))
PY
ROOT
`);
  const parsed = parseJsonOutput(capture.stdout);
  const statusValue = stringValue(recordValue(parsed?.job)?.status);
  return {
    ok: capture.exitCode === 0 && parsed?.ok === true,
    action: "platform-db-postgres-swap-job-status",
    mutation: false,
    target: targetSummary(declaration),
    jobId,
    job: parsed?.job ?? null,
    result: parsed?.result ?? null,
    terminal: statusValue === "succeeded" || statusValue === "failed",
    remote: compactCapture(capture, full || capture.exitCode !== 0),
    ...(raw ? { raw: capture } : {}),
    next: statusValue === "succeeded"
      ? { status: statusCommand(declaration) }
      : statusValue === "failed"
        ? { plan: `bun scripts/cli.ts platform-db postgres swap plan --config ${declaration.configPath}` }
        : { status: `${statusCommand(declaration)} --job-id ${jobId}` },
  };
}

function startJobScript(payload: Record<string, unknown>): string {
  const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
  return `
set -eu
if ! command -v sudo >/dev/null 2>&1 || ! sudo -n true >/dev/null 2>&1; then
  printf '{"ok":false,"error":"sudo-noninteractive-unavailable"}\\n'
  exit 1
fi
sudo -n sh <<'ROOT'
set -eu
umask 077
base="/root/.unidesk/platform-db/swap-jobs"
mkdir -p "$base"
job_id="swap-$(date +%Y%m%d%H%M%S)-$$"
job_dir="$base/$job_id"
mkdir "$job_dir"
printf '%s' '${encoded}' | base64 -d > "$job_dir/payload.json"
cat > "$job_dir/run.sh" <<'RUNNER'
${remoteRunner()}
RUNNER
chmod 700 "$job_dir/run.sh"
printf '{"status":"queued","stage":"created","jobId":"%s"}\\n' "$job_id" > "$job_dir/state.json"
nohup "$job_dir/run.sh" "$job_dir" > "$job_dir/run.log" 2>&1 &
printf '{"ok":true,"jobId":"%s","status":"queued"}\\n' "$job_id"
ROOT
`;
}

function remoteRunner(): string {
  return String.raw`#!/bin/sh
set -eu
job_dir="$1"
payload="$job_dir/payload.json"
state="$job_dir/state.json"
result="$job_dir/result.json"

json_get() {
  python3 - "$payload" "$1" <<'PY'
import json, sys
value=json.load(open(sys.argv[1], encoding="utf-8"))
for part in sys.argv[2].split("."):
    value=value[part]
print(value)
PY
}

write_state() {
  status="$1"
  stage="$2"
  exit_code=""
  if [ "$#" -ge 3 ]; then exit_code="$3"; fi
  python3 - "$state" "$status" "$stage" "$exit_code" <<'PY'
import datetime, json, os, sys
path,status,stage,exit_code=sys.argv[1:5]
try:
    current=json.load(open(path, encoding="utf-8"))
except Exception:
    current={}
current.update({"status":status,"stage":stage,"updatedAt":datetime.datetime.now(datetime.timezone.utc).isoformat()})
if status == "running" and not current.get("startedAt"):
    current["startedAt"]=current["updatedAt"]
if status in {"succeeded","failed"}:
    current["finishedAt"]=current["updatedAt"]
    current["exitCode"]=int(exit_code or ("0" if status == "succeeded" else "1"))
tmp=path+".tmp"
json.dump(current, open(tmp,"w"), separators=(",",":"))
os.replace(tmp,path)
PY
}

fail() {
  rc="$1"
  stage="$2"
  write_state failed "$stage" "$rc"
  exit "$rc"
}

run_stage() {
  stage="$1"
  shift
  write_state running "$stage"
  "$@" || fail "$?" "$stage"
}

PATH_DECLARED="$(json_get path)"
EXPANSION_PATH="$(json_get expansionPath)"
DESIRED_BYTES="$(json_get desiredBytes)"
MIN_DISK_FREE_BYTES="$(json_get minDiskFreeBytes)"
SWAPPINESS="$(json_get swappiness)"
SYSCTL_FILE="$(json_get sysctlFile)"
write_state running preflight
case "$PATH_DECLARED" in /*) ;; *) fail 2 invalid-swap-path ;; esac
case "$EXPANSION_PATH" in /*) ;; *) fail 2 invalid-expansion-path ;; esac
case "$SYSCTL_FILE" in /etc/sysctl.d/*.conf) ;; *) fail 2 invalid-sysctl-path ;; esac
if [ -L "$PATH_DECLARED" ] || [ -L "$EXPANSION_PATH" ]; then fail 2 swap-path-symlink; fi
available_bytes="$(df -B1 --output=avail "$PATH_DECLARED" 2>/dev/null | tail -1 | tr -d ' ' || df -B1 --output=avail / | tail -1 | tr -d ' ')"
declared_capacity_bytes=0
if awk -v path="$PATH_DECLARED" 'NR>1 && $1==path {found=1} END {exit found?0:1}' /proc/swaps && [ -f "$PATH_DECLARED" ]; then
  declared_capacity_bytes="$((declared_capacity_bytes + $(stat -c %s "$PATH_DECLARED")))"
fi
if awk -v path="$EXPANSION_PATH" 'NR>1 && $1==path {found=1} END {exit found?0:1}' /proc/swaps && [ -f "$EXPANSION_PATH" ]; then
  declared_capacity_bytes="$((declared_capacity_bytes + $(stat -c %s "$EXPANSION_PATH")))"
fi
missing_bytes="$((DESIRED_BYTES - declared_capacity_bytes))"

if [ "$missing_bytes" -le 0 ]; then
  write_state running persist-policy
else
  required_bytes="$((missing_bytes + MIN_DISK_FREE_BYTES))"
  if [ "$available_bytes" -lt "$required_bytes" ]; then fail 4 insufficient-disk-for-safe-expansion; fi
  if [ -e "$EXPANSION_PATH" ]; then fail 3 expansion-path-already-exists; fi
  write_state running allocate-extension
  if ! fallocate -l "$missing_bytes" "$EXPANSION_PATH"; then
    rm -f "$EXPANSION_PATH"
    fail 5 allocate-extension
  fi
  chmod 600 "$EXPANSION_PATH"
  if ! mkswap "$EXPANSION_PATH"; then
    rm -f "$EXPANSION_PATH"
    fail 6 format-extension
  fi
  if ! swapon "$EXPANSION_PATH"; then
    rm -f "$EXPANSION_PATH"
    fail 7 enable-extension
  fi
fi

write_state running persist-policy
touch /etc/fstab
awk -v path="$PATH_DECLARED" '$1==path && $3=="swap" {found=1} END {exit found?0:1}' /etc/fstab || printf '%s none swap sw 0 0\n' "$PATH_DECLARED" >> /etc/fstab
if awk -v path="$EXPANSION_PATH" 'NR>1 && $1==path {found=1} END {exit found?0:1}' /proc/swaps; then
  awk -v path="$EXPANSION_PATH" '$1==path && $3=="swap" {found=1} END {exit found?0:1}' /etc/fstab || printf '%s none swap sw 0 0\n' "$EXPANSION_PATH" >> /etc/fstab
fi
tmp_sysctl="$SYSCTL_FILE.tmp"
printf 'vm.swappiness = %s\n' "$SWAPPINESS" > "$tmp_sysctl"
chmod 644 "$tmp_sysctl"
mv "$tmp_sysctl" "$SYSCTL_FILE"
sysctl -q -w "vm.swappiness=$SWAPPINESS"

write_state running verify
if ! python3 - "$PATH_DECLARED" "$EXPANSION_PATH" "$DESIRED_BYTES" "$SWAPPINESS" "$SYSCTL_FILE" > "$result" <<'PY'
import json, os, stat, sys
path,expansion_path,desired_raw,swappiness_raw,sysctl_file=sys.argv[1:6]
desired=int(desired_raw)
swappiness=int(swappiness_raw)
swaps=[]
with open("/proc/swaps", encoding="utf-8") as handle:
    for line in list(handle)[1:]:
        fields=line.split()
        if len(fields) >= 5:
            swaps.append({"path":fields[0],"sizeBytes":int(fields[2])*1024,"usedBytes":int(fields[3])*1024,"priority":int(fields[4])})
configured=next((item for item in swaps if item["path"] == path), None)
extension=next((item for item in swaps if item["path"] == expansion_path), None)
runtime_swappiness=int(open("/proc/sys/vm/swappiness", encoding="utf-8").read().strip())
declared_capacity=sum(os.stat(item).st_size for item in (path, expansion_path) if os.path.isfile(item) and any(swap["path"] == item for swap in swaps))
result={
    "ok": configured is not None and declared_capacity >= desired and runtime_swappiness == swappiness,
    "path": path,
    "desiredBytes": desired,
    "configured": configured,
    "extension": extension,
    "totalSwapBytes": sum(item["sizeBytes"] for item in swaps),
    "declaredCapacityBytes": declared_capacity,
    "runtimeSwappiness": runtime_swappiness,
    "sysctlFile": sysctl_file,
    "sysctlFileExists": os.path.isfile(sysctl_file),
    "mode": oct(stat.S_IMODE(os.stat(path).st_mode))[2:] if os.path.isfile(path) else None,
}
print(json.dumps(result, separators=(",",":")))
raise SystemExit(0 if result["ok"] else 8)
PY
then
  fail 8 verify
fi
write_state succeeded complete 0
`;
}

function factsScript(encoded: string): string {
  return String.raw`
set -eu
payload="$(mktemp)"
trap 'rm -f "$payload"' EXIT
printf '%s' '${encoded}' | base64 -d > "$payload"
python3 - "$payload" <<'PY'
import json, os, stat, sys
spec=json.load(open(sys.argv[1], encoding="utf-8"))
path=spec["path"]
expansion_path=spec["expansionPath"]
sysctl_file=spec["sysctlFile"]
swaps=[]
with open("/proc/swaps", encoding="utf-8") as handle:
    for line in list(handle)[1:]:
        fields=line.split()
        if len(fields) >= 5:
            swaps.append({"path":fields[0],"sizeBytes":int(fields[2])*1024,"usedBytes":int(fields[3])*1024,"priority":int(fields[4])})
mem={}
with open("/proc/meminfo", encoding="utf-8") as handle:
    for line in handle:
        if ":" not in line: continue
        key,value=line.split(":",1)
        fields=value.split()
        if fields and fields[0].isdigit(): mem[key]=int(fields[0])*1024
disk=os.statvfs(path if os.path.exists(path) else os.path.dirname(path) or "/")
fstab=False
expansion_fstab=False
try:
    with open("/etc/fstab", encoding="utf-8") as handle:
        for line in handle:
            fields=line.split()
            if len(fields) >= 3 and fields[0] == path and fields[2] == "swap": fstab=True
            if len(fields) >= 3 and fields[0] == expansion_path and fields[2] == "swap": expansion_fstab=True
except OSError:
    pass
sysctl_persisted=None
try:
    for line in open(sysctl_file, encoding="utf-8"):
        if line.strip().startswith("vm.swappiness"):
            sysctl_persisted=int(line.split("=",1)[1].strip())
except OSError:
    pass
result={
    "ok": True,
    "path": path,
    "configuredPathExists": os.path.isfile(path),
    "configuredPathSizeBytes": os.stat(path).st_size if os.path.isfile(path) else None,
    "configuredPathMode": oct(stat.S_IMODE(os.stat(path).st_mode))[2:] if os.path.isfile(path) else None,
    "configuredPathActive": any(item["path"] == path for item in swaps),
    "expansionPath": expansion_path,
    "expansionPathExists": os.path.isfile(expansion_path),
    "expansionPathSizeBytes": os.stat(expansion_path).st_size if os.path.isfile(expansion_path) else None,
    "expansionPathMode": oct(stat.S_IMODE(os.stat(expansion_path).st_mode))[2:] if os.path.isfile(expansion_path) else None,
    "expansionPathActive": any(item["path"] == expansion_path for item in swaps),
    "activeSwaps": swaps,
    "declaredCapacityBytes": sum(os.stat(item).st_size for item in (path, expansion_path) if os.path.isfile(item) and any(swap["path"] == item for swap in swaps)),
    "swapTotalBytes": mem.get("SwapTotal",0),
    "swapFreeBytes": mem.get("SwapFree",0),
    "memAvailableBytes": mem.get("MemAvailable"),
    "diskAvailableBytes": disk.f_bavail * disk.f_frsize,
    "fstabPersisted": fstab,
    "expansionFstabPersisted": expansion_fstab,
    "runtimeSwappiness": int(open("/proc/sys/vm/swappiness", encoding="utf-8").read().strip()),
    "persistedSwappiness": sysctl_persisted,
    "sysctlFile": sysctl_file,
    "swapExcludedFromWebProbeAvailability": True,
}
print(json.dumps(result, separators=(",",":")))
PY
`;
}

function parseByteSize(value: string): number {
  const match = value.trim().match(/^([1-9][0-9]*)([KMGT]i?B?|B)$/iu);
  if (match === null) throw new Error(`node.swap.size must use a positive size such as 64GiB; received ${value}`);
  const unit = match[2].toUpperCase().replace("IB", "").replace("B", "");
  const powers: Record<string, number> = { "": 0, K: 1, M: 2, G: 3, T: 4 };
  return Number(match[1]) * 1024 ** powers[unit];
}

function isInSync(facts: Record<string, unknown> | null, declaration: HostSwapDeclaration, desiredBytes: number): boolean {
  return facts !== null
    && facts.configuredPathActive === true
    && numberValue(facts.declaredCapacityBytes) !== null
    && (numberValue(facts.declaredCapacityBytes) ?? 0) >= desiredBytes
    && facts.configuredPathMode === "600"
    && facts.fstabPersisted === true
    && (facts.expansionPathActive !== true || (facts.expansionPathMode === "600" && facts.expansionFstabPersisted === true))
    && numberValue(facts.runtimeSwappiness) === declaration.swap.swappiness
    && numberValue(facts.persistedSwappiness) === declaration.swap.swappiness;
}

function desiredSummary(declaration: HostSwapDeclaration, desiredBytes: number): Record<string, unknown> {
  return {
    configSource: `${declaration.configPath}#node.swap`,
    ensure: declaration.swap.ensure,
    path: declaration.swap.path,
    expansionPath: declaration.swap.expansionPath,
    size: declaration.swap.size,
    sizeBytes: desiredBytes,
    swappiness: declaration.swap.swappiness,
    sysctlFile: declaration.swap.sysctlFile,
  };
}

function compactFacts(facts: Record<string, unknown> | null): Record<string, unknown> | null {
  if (facts === null) return null;
  return {
    configuredPathExists: facts.configuredPathExists ?? null,
    configuredPathSizeBytes: facts.configuredPathSizeBytes ?? null,
    configuredPathMode: facts.configuredPathMode ?? null,
    configuredPathActive: facts.configuredPathActive ?? null,
    expansionPath: facts.expansionPath ?? null,
    expansionPathExists: facts.expansionPathExists ?? null,
    expansionPathSizeBytes: facts.expansionPathSizeBytes ?? null,
    expansionPathMode: facts.expansionPathMode ?? null,
    expansionPathActive: facts.expansionPathActive ?? null,
    activeSwaps: facts.activeSwaps ?? [],
    declaredCapacityBytes: facts.declaredCapacityBytes ?? null,
    swapTotalBytes: facts.swapTotalBytes ?? null,
    swapFreeBytes: facts.swapFreeBytes ?? null,
    memAvailableBytes: facts.memAvailableBytes ?? null,
    diskAvailableBytes: facts.diskAvailableBytes ?? null,
    fstabPersisted: facts.fstabPersisted ?? null,
    expansionFstabPersisted: facts.expansionFstabPersisted ?? null,
    runtimeSwappiness: facts.runtimeSwappiness ?? null,
    persistedSwappiness: facts.persistedSwappiness ?? null,
    sysctlFile: facts.sysctlFile ?? null,
    swapExcludedFromWebProbeAvailability: true,
  };
}

function targetSummary(declaration: HostSwapDeclaration): Record<string, unknown> {
  return { node: declaration.nodeId, route: declaration.route, config: declaration.configPath };
}

function statusCommand(declaration: HostSwapDeclaration): string {
  return `bun scripts/cli.ts platform-db postgres swap status --config ${declaration.configPath}`;
}

function compactCapture(capture: SshCaptureResult, full: boolean): Record<string, unknown> {
  return {
    exitCode: capture.exitCode,
    stdoutBytes: Buffer.byteLength(capture.stdout, "utf8"),
    stderrBytes: Buffer.byteLength(capture.stderr, "utf8"),
    stdoutTail: full || capture.exitCode !== 0 ? capture.stdout.slice(-8000) : "",
    stderrTail: full || capture.exitCode !== 0 ? capture.stderr.slice(-4000) : "",
  };
}

function numberValue(value: unknown): number | null {
  return typeof value === "number" && Number.isFinite(value) ? value : null;
}

function stringValue(value: unknown): string | null {
  return typeof value === "string" && value.length > 0 ? value : null;
}

function recordValue(value: unknown): Record<string, unknown> | null {
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
