import { createHash, randomUUID } from "node:crypto";
import {
  chmodSync,
  closeSync,
  constants,
  existsSync,
  fsyncSync,
  lstatSync,
  mkdirSync,
  openSync,
  readFileSync,
  readlinkSync,
  renameSync,
  rmSync,
  symlinkSync,
  unlinkSync,
  writeFileSync,
} from "node:fs";
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
import { readRuntimeReleaseDescriptor, verifyRuntimeReleaseArtifact } from "./runtime-release.mjs";

const SHA256_RE = /^[0-9a-f]{64}$/;
const POINTERS = ["current", "previous"];
const LOCK_NAME = ".apply.lock";
const TRANSACTION_NAME = ".apply-transaction.json";
const RELEASES_NAME = "releases";
const TAR_BLOCK = 512;

function fail(message) {
  throw new Error(`runtime apply: ${message}`);
}

function assertAbsoluteDirectoryPath(path, label) {
  if (!isAbsolute(path)) fail(`${label} must be absolute: ${path}`);
  return resolve(path);
}

function assertSha256(value, label) {
  if (!SHA256_RE.test(String(value))) fail(`invalid ${label}: ${value}`);
  return String(value);
}

function sha256(content) {
  return createHash("sha256").update(content).digest("hex");
}

function fsyncDirectory(path) {
  const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY);
  try {
    fsyncSync(fd);
  } finally {
    closeSync(fd);
  }
}

function atomicWrite(path, content, mode = 0o600) {
  const parent = dirname(path);
  const temporary = join(parent, `.${randomUUID()}.tmp`);
  const fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, mode);
  try {
    writeFileSync(fd, content);
    fsyncSync(fd);
  } finally {
    closeSync(fd);
  }
  renameSync(temporary, path);
  fsyncDirectory(parent);
}

function parseJsonFile(path, label) {
  let parsed;
  try {
    parsed = JSON.parse(readFileSync(path, "utf8"));
  } catch (error) {
    fail(`${label} is unreadable: ${path}: ${error.message}`);
  }
  return parsed;
}

function processStartTime(pid) {
  const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
  const close = stat.lastIndexOf(")");
  if (close < 0) fail(`cannot parse process state for pid ${pid}`);
  const fields = stat.slice(close + 2).split(" ");
  return fields[19];
}

function acquireLock(root) {
  const lockPath = join(root, LOCK_NAME);
  try {
    mkdirSync(lockPath, { mode: 0o700 });
  } catch (error) {
    if (error.code !== "EEXIST") throw error;
    const ownerPath = join(lockPath, "owner.json");
    if (!lstatSync(lockPath).isDirectory() || !existsSync(ownerPath) || !lstatSync(ownerPath).isFile()) {
      fail(`unknown apply lock; preserved: ${lockPath}`);
    }
    const owner = parseJsonFile(ownerPath, "apply lock owner");
    if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0 || typeof owner.startTime !== "string") {
      fail(`invalid apply lock; preserved: ${lockPath}`);
    }
    let live = false;
    try {
      live = processStartTime(owner.pid) === owner.startTime;
    } catch (readError) {
      if (readError.code !== "ENOENT" && readError.code !== "ESRCH") throw readError;
    }
    if (live) fail(`apply already in progress: pid ${owner.pid}`);
    rmSync(lockPath, { recursive: true });
    fsyncDirectory(root);
    mkdirSync(lockPath, { mode: 0o700 });
  }
  atomicWrite(join(lockPath, "owner.json"), `${JSON.stringify({ pid: process.pid, startTime: processStartTime(process.pid) })}\n`);
  return () => {
    rmSync(lockPath, { recursive: true });
    fsyncDirectory(root);
  };
}

function pathExists(path) {
  try {
    lstatSync(path);
    return true;
  } catch (error) {
    if (error.code === "ENOENT") return false;
    throw error;
  }
}

function expectedPointerTarget(digest) {
  return `${RELEASES_NAME}/${digest}/runtime`;
}

function readPointer(root, name) {
  const path = join(root, name);
  if (!pathExists(path)) return null;
  const stat = lstatSync(path);
  if (!stat.isSymbolicLink()) fail(`unknown ${name} path; preserved: ${path}`);
  const target = readlinkSync(path);
  if (!new RegExp(`^${RELEASES_NAME}/[0-9a-f]{64}/runtime$`).test(target)) {
    fail(`unknown ${name} target; preserved: ${path} -> ${target}`);
  }
  return target;
}

function setPointer(root, name, target) {
  if (!POINTERS.includes(name)) fail(`invalid pointer name: ${name}`);
  if (target !== null && !new RegExp(`^${RELEASES_NAME}/[0-9a-f]{64}/runtime$`).test(target)) {
    fail(`invalid ${name} target: ${target}`);
  }
  const path = join(root, name);
  if (target === null) {
    if (pathExists(path)) unlinkSync(path);
    fsyncDirectory(root);
    return;
  }
  const temporary = join(root, `.${name}.${randomUUID()}.tmp`);
  symlinkSync(target, temporary);
  renameSync(temporary, path);
  fsyncDirectory(root);
}

function restorePointers(root, transaction) {
  setPointer(root, "current", transaction.oldCurrent);
  setPointer(root, "previous", transaction.oldPrevious);
}

function validateTransaction(root, transaction) {
  const allowedPhases = new Set(["installing", "installed", "switching", "switched", "committed"]);
  if (!transaction || transaction.version !== 1 || !allowedPhases.has(transaction.phase)) {
    fail(`invalid interrupted transaction; preserved: ${join(root, TRANSACTION_NAME)}`);
  }
  assertSha256(transaction.digest, "transaction digest");
  for (const name of ["oldCurrent", "oldPrevious"]) {
    const value = transaction[name];
    if (value !== null && !new RegExp(`^${RELEASES_NAME}/[0-9a-f]{64}/runtime$`).test(value)) {
      fail(`invalid interrupted transaction ${name}; preserved`);
    }
  }
  const wantedStaging = `.staging-${transaction.digest}-${transaction.token}`;
  if (!/^[0-9a-f-]{36}$/.test(String(transaction.token)) || transaction.stagingName !== wantedStaging) {
    fail("invalid interrupted transaction staging path; preserved");
  }
  return transaction;
}

function writeTransaction(root, transaction) {
  atomicWrite(join(root, TRANSACTION_NAME), `${JSON.stringify(transaction)}\n`);
}

function removeKnownStaging(root, transaction) {
  const staging = join(root, transaction.stagingName);
  if (!pathExists(staging)) return;
  const stat = lstatSync(staging);
  if (!stat.isDirectory() || stat.isSymbolicLink()) fail(`unknown staging path; preserved: ${staging}`);
  rmSync(staging, { recursive: true });
  fsyncDirectory(root);
}

export function recoverInterruptedApply(releaseRoot) {
  const root = assertAbsoluteDirectoryPath(releaseRoot, "release root");
  const path = join(root, TRANSACTION_NAME);
  if (!pathExists(path)) return false;
  if (!lstatSync(path).isFile()) fail(`unknown transaction path; preserved: ${path}`);
  const transaction = validateTransaction(root, parseJsonFile(path, "interrupted transaction"));
  if (transaction.phase !== "committed") restorePointers(root, transaction);
  removeKnownStaging(root, transaction);
  unlinkSync(path);
  fsyncDirectory(root);
  return true;
}

function parseTarString(header, offset, length) {
  const end = header.indexOf(0, offset);
  return header.subarray(offset, end >= offset && end < offset + length ? end : offset + length).toString("utf8");
}

function parseTarOctal(header, offset, length, label) {
  const raw = parseTarString(header, offset, length).trim();
  if (!/^[0-7]+$/.test(raw)) fail(`invalid tar ${label}`);
  const value = Number.parseInt(raw, 8);
  if (!Number.isSafeInteger(value) || value < 0) fail(`invalid tar ${label}`);
  return value;
}

function safeRuntimeRelativePath(name) {
  if (name === "runtime" || name === "runtime/") return "";
  if (!name.startsWith("runtime/") || name.includes("\0") || name.includes("\\")) {
    fail(`artifact entry outside runtime/: ${JSON.stringify(name)}`);
  }
  const rel = name.slice("runtime/".length).replace(/\/$/, "");
  if (!rel || rel.split("/").some((part) => !part || part === "." || part === "..")) {
    fail(`unsafe artifact entry: ${JSON.stringify(name)}`);
  }
  return rel;
}

function safeDestination(runtimeRoot, rel) {
  const destination = resolve(runtimeRoot, rel);
  if (destination !== runtimeRoot && !destination.startsWith(`${runtimeRoot}${sep}`)) {
    fail(`artifact path escapes runtime root: ${rel}`);
  }
  return destination;
}

function writeExclusive(path, content, mode = 0o600) {
  const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, mode);
  try {
    writeFileSync(fd, content);
    fsyncSync(fd);
  } finally {
    closeSync(fd);
  }
}

function extractArtifact(artifact, staging) {
  const runtimeRoot = join(staging, "runtime");
  mkdirSync(runtimeRoot, { mode: 0o755 });
  let offset = 0;
  let entries = 0;
  while (offset + TAR_BLOCK <= artifact.length) {
    const header = artifact.subarray(offset, offset + TAR_BLOCK);
    if (header.every((byte) => byte === 0)) break;
    const storedChecksum = parseTarOctal(header, 148, 8, "checksum");
    let checksum = 0;
    for (let index = 0; index < TAR_BLOCK; index += 1) checksum += index >= 148 && index < 156 ? 32 : header[index];
    if (checksum !== storedChecksum) fail("artifact tar checksum mismatch");
    const name = parseTarString(header, 0, 100);
    const prefix = parseTarString(header, 345, 155);
    const fullName = prefix ? `${prefix}/${name}` : name;
    const rel = safeRuntimeRelativePath(fullName);
    const type = String.fromCharCode(header[156] || 48);
    const size = parseTarOctal(header, 124, 12, "size");
    const mode = parseTarOctal(header, 100, 8, "mode") & 0o777;
    const payloadStart = offset + TAR_BLOCK;
    const payloadEnd = payloadStart + size;
    if (payloadEnd > artifact.length) fail(`truncated artifact entry: ${fullName}`);
    const destination = safeDestination(runtimeRoot, rel);
    if (rel === "" && type !== "5") fail("runtime artifact root must be a directory");
    if (type === "5") {
      mkdirSync(destination, { recursive: true, mode: mode || 0o755 });
      chmodSync(destination, mode || 0o755);
    } else if (type === "0") {
      mkdirSync(dirname(destination), { recursive: true, mode: 0o755 });
      const fd = openSync(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, mode || 0o644);
      try {
        writeFileSync(fd, artifact.subarray(payloadStart, payloadEnd));
        fsyncSync(fd);
      } finally {
        closeSync(fd);
      }
      chmodSync(destination, mode || 0o644);
    } else if (type === "2") {
      const link = parseTarString(header, 157, 100);
      if (!link || isAbsolute(link)) fail(`unsafe artifact symlink: ${fullName} -> ${link}`);
      const resolvedLink = resolve(dirname(destination), link);
      if (resolvedLink !== runtimeRoot && !resolvedLink.startsWith(`${runtimeRoot}${sep}`)) {
        fail(`artifact symlink escapes runtime root: ${fullName} -> ${link}`);
      }
      mkdirSync(dirname(destination), { recursive: true, mode: 0o755 });
      symlinkSync(link, destination);
    } else {
      fail(`unsupported artifact entry type ${JSON.stringify(type)}: ${fullName}`);
    }
    entries += 1;
    offset = payloadStart + Math.ceil(size / TAR_BLOCK) * TAR_BLOCK;
  }
  if (entries === 0) fail("artifact contains no runtime entries");
  fsyncDirectory(runtimeRoot);
}

function ensureManagedDirectory(path, label) {
  if (!pathExists(path)) {
    mkdirSync(path, { mode: 0o755 });
    return;
  }
  const stat = lstatSync(path);
  if (!stat.isDirectory() || stat.isSymbolicLink()) fail(`unknown ${label} path; preserved: ${path}`);
}

async function validateInstalledRelease(releasePath, digest) {
  const descriptorPath = join(releasePath, "descriptor.json");
  const artifactPath = join(releasePath, `runtime-release-${digest}.tar`);
  const runtimePath = join(releasePath, "runtime");
  if (!existsSync(descriptorPath) || !lstatSync(descriptorPath).isFile()) return false;
  if (!existsSync(artifactPath) || !lstatSync(artifactPath).isFile()) return false;
  if (!existsSync(runtimePath) || !lstatSync(runtimePath).isDirectory() || lstatSync(runtimePath).isSymbolicLink()) return false;
  const descriptor = await readRuntimeReleaseDescriptor(descriptorPath);
  await verifyRuntimeReleaseArtifact({ descriptor, artifactPath });
  return true;
}

function initializeRoot(root) {
  if (!pathExists(root)) {
    mkdirSync(root, { mode: 0o755 });
  } else {
    const stat = lstatSync(root);
    if (!stat.isDirectory() || stat.isSymbolicLink()) fail(`unknown release root path; preserved: ${root}`);
    for (const pointer of POINTERS) readPointer(root, pointer);
  }
  ensureManagedDirectory(join(root, RELEASES_NAME), "releases");
}

async function defaultReadiness({ runtimePath }) {
  return existsSync(runtimePath) && lstatSync(runtimePath).isDirectory() && !lstatSync(runtimePath).isSymbolicLink();
}

export async function applyRuntimeRelease({ descriptorPath, artifactPath, releaseRoot, readiness = defaultReadiness }) {
  const root = assertAbsoluteDirectoryPath(releaseRoot, "release root");
  if (!isAbsolute(descriptorPath)) fail(`descriptor path must be absolute: ${descriptorPath}`);
  if (!isAbsolute(artifactPath)) fail(`artifact path must be absolute: ${artifactPath}`);
  if (typeof readiness !== "function") fail("readiness must be a function");

  const descriptorBytes = readFileSync(descriptorPath);
  const descriptor = await readRuntimeReleaseDescriptor(descriptorPath);
  await verifyRuntimeReleaseArtifact({ descriptor, artifactPath });
  if (!readFileSync(descriptorPath).equals(descriptorBytes)) fail(`descriptor changed during validation: ${descriptorPath}`);
  const digest = assertSha256(descriptor.artifactSha256, "artifact digest");
  const artifact = readFileSync(artifactPath);
  if (sha256(artifact) !== digest) fail(`artifact changed after validation: ${artifactPath}`);

  initializeRoot(root);
  const releasePath = join(root, RELEASES_NAME, digest);
  const runtimePath = join(releasePath, "runtime");
  const desiredTarget = expectedPointerTarget(digest);
  const releaseExistedBeforeLock = pathExists(releasePath);
  if (releaseExistedBeforeLock && (!lstatSync(releasePath).isDirectory() || lstatSync(releasePath).isSymbolicLink())) {
    fail(`unknown release path; preserved: ${releasePath}`);
  }

  const releaseLock = acquireLock(root);
  try {
    recoverInterruptedApply(root);
    const current = readPointer(root, "current");
    if (pathExists(releasePath)) {
      if (!(await validateInstalledRelease(releasePath, digest))) fail(`existing release is incomplete; preserved: ${releasePath}`);
      if (current === desiredTarget) {
        return { status: "unchanged", artifactSha256: digest, runtimePath };
      }
    } else if (releaseExistedBeforeLock) {
      fail(`release path disappeared during apply: ${releasePath}`);
    }

    const token = randomUUID();
    const transaction = {
      version: 1,
      phase: "installing",
      digest,
      token,
      stagingName: `.staging-${digest}-${token}`,
      oldCurrent: current,
      oldPrevious: readPointer(root, "previous"),
    };
    writeTransaction(root, transaction);
    const staging = join(root, transaction.stagingName);
    try {
      if (!pathExists(releasePath)) {
        mkdirSync(staging, { mode: 0o755 });
        extractArtifact(artifact, staging);
        writeExclusive(join(staging, `runtime-release-${digest}.tar`), artifact);
        writeExclusive(join(staging, "descriptor.json"), descriptorBytes);
        if (sha256(readFileSync(join(staging, `runtime-release-${digest}.tar`))) !== digest) fail("staged artifact digest mismatch");
        fsyncDirectory(staging);
        renameSync(staging, releasePath);
        fsyncDirectory(join(root, RELEASES_NAME));
      }
      transaction.phase = "installed";
      writeTransaction(root, transaction);
      transaction.phase = "switching";
      writeTransaction(root, transaction);
      setPointer(root, "previous", transaction.oldCurrent);
      setPointer(root, "current", desiredTarget);
      transaction.phase = "switched";
      writeTransaction(root, transaction);
      let ready;
      try {
        ready = await readiness({ descriptor, artifactSha256: digest, releasePath, runtimePath });
      } catch (error) {
        throw new Error(`readiness failed: ${error.message}`, { cause: error });
      }
      if (ready === false) throw new Error("readiness failed: callback returned false");
      transaction.phase = "committed";
      writeTransaction(root, transaction);
      unlinkSync(join(root, TRANSACTION_NAME));
      fsyncDirectory(root);
      return { status: "applied", artifactSha256: digest, runtimePath };
    } catch (error) {
      let rollbackError;
      try {
        restorePointers(root, transaction);
        removeKnownStaging(root, transaction);
        if (pathExists(join(root, TRANSACTION_NAME))) unlinkSync(join(root, TRANSACTION_NAME));
        fsyncDirectory(root);
      } catch (caught) {
        rollbackError = caught;
      }
      if (rollbackError) fail(`${error.message}; rollback failed: ${rollbackError.message}`);
      throw error;
    }
  } finally {
    releaseLock();
  }
}
