#!/usr/bin/env node
import { createHash, randomBytes } from "node:crypto";
import { spawn } from "node:child_process";
import {
  existsSync,
  mkdirSync,
  readFileSync,
  renameSync,
  rmSync,
  writeFileSync,
} from "node:fs";
import { dirname, join, resolve, sep } from "node:path";

const args = process.argv.slice(2);
const configPath = optionValue("--config");
const mode = args.includes("--health")
  ? "health"
  : args.includes("--status")
    ? "status"
    : args.includes("--once")
      ? "once"
      : "run";

if (!configPath) failUsage("--config <path> is required");

const config = readConfig(configPath);
const statePath = join(config.cacheMountPath, config.stateDirectory, "status.json");
const activeChildren = new Set();
let stopping = false;

process.on("SIGTERM", beginShutdown);
process.on("SIGINT", beginShutdown);

if (mode === "health" || mode === "status") {
  const view = await currentStatus(config, statePath);
  process.stdout.write(`${JSON.stringify(view)}\n`);
  process.exit(mode === "health" && view.ready !== true ? 1 : 0);
}

if (mode === "once") {
  const state = await reconcileOnce(config, statePath);
  process.stdout.write(`${JSON.stringify(compactStatus(config, state))}\n`);
  process.exit(state.ready === true ? 0 : 1);
}

log({ event: "managed-repository-reconciler-started", node: config.nodeId, lane: config.lane, desiredHash: config.desiredHash, repositoryCount: config.repositories.length });
while (!stopping) {
  const state = await reconcileOnce(config, statePath);
  log({
    event: "managed-repository-reconcile-completed",
    node: config.nodeId,
    lane: config.lane,
    ready: state.ready,
    lastCycleOk: state.lastCycleOk,
    repositoryCount: state.repositories.length,
    desiredHash: config.desiredHash,
  });
  if (!stopping) await sleep(config.reconcileIntervalMs);
}
log({ event: "managed-repository-reconciler-stopped", node: config.nodeId, lane: config.lane });

function optionValue(name) {
  const index = args.indexOf(name);
  if (index < 0) return null;
  const value = args[index + 1];
  if (!value || value.startsWith("--")) failUsage(`${name} requires a value`);
  return value;
}

function failUsage(message) {
  process.stderr.write(`${JSON.stringify({ ok: false, errorType: "invalid-arguments", message })}\n`);
  process.exit(64);
}

function readConfig(path) {
  let raw;
  try {
    raw = JSON.parse(readFileSync(path, "utf8"));
  } catch {
    throw new Error("managed repository config is not readable JSON");
  }
  requireObject(raw, "config");
  const desiredHash = requireString(raw.desiredHash, "desiredHash");
  const { desiredHash: _omitted, ...desired } = raw;
  const computedHash = sha256(JSON.stringify(desired));
  if (computedHash !== desiredHash) throw new Error("managed repository config desiredHash does not match rendered content");
  if (raw.schemaVersion !== 1 || raw.kind !== "AgentRunManagedRepositoryReconciler") throw new Error("unsupported managed repository config envelope");
  const cacheMountPath = requireAbsolutePath(raw.cacheMountPath, "cacheMountPath");
  const stateDirectory = requireRelativePath(raw.stateDirectory, "stateDirectory");
  requireObject(raw.remoteAuth, "remoteAuth");
  requireObject(raw.remoteAuth.proxy, "remoteAuth.proxy");
  const remoteAuth = {
    configRef: requireString(raw.remoteAuth.configRef, "remoteAuth.configRef"),
    capabilitySha256: requireHex(raw.remoteAuth.capabilitySha256, "remoteAuth.capabilitySha256"),
    authMode: requireLiteral(raw.remoteAuth.authMode, "remoteAuth.authMode", "github-https-token"),
    host: requireHost(raw.remoteAuth.host, "remoteAuth.host"),
    tokenFile: requireAbsolutePath(raw.remoteAuth.tokenFile, "remoteAuth.tokenFile"),
    askPassFile: requireAbsolutePath(raw.remoteAuth.askPassFile, "remoteAuth.askPassFile"),
    proxy: {
      host: requireHost(raw.remoteAuth.proxy.host, "remoteAuth.proxy.host"),
      port: requirePort(raw.remoteAuth.proxy.port, "remoteAuth.proxy.port"),
    },
  };
  const repositories = requireArray(raw.repositories, "repositories").map((item, index) => {
    requireObject(item, `repositories[${index}]`);
    const key = requireSimpleId(item.key, `repositories[${index}].key`);
    const repository = requireRepository(item.repository, `repositories[${index}].repository`);
    const remote = requireString(item.remote, `repositories[${index}].remote`);
    const sourceBranch = requireGitRef(item.sourceBranch, `repositories[${index}].sourceBranch`);
    const additionalSourceBranches = item.additionalSourceBranches === undefined
      ? []
      : requireArray(item.additionalSourceBranches, `repositories[${index}].additionalSourceBranches`)
        .map((branch, branchIndex) => requireGitRef(branch, `repositories[${index}].additionalSourceBranches[${branchIndex}]`));
    ensureUnique([sourceBranch, ...additionalSourceBranches], `repositories[${index}] source branches`);
    const desiredFingerprint = requireHex(item.desiredFingerprint, `repositories[${index}].desiredFingerprint`);
    const expectedFingerprint = sha256([key, repository, remote, sourceBranch, ...additionalSourceBranches].join("\0"));
    if (desiredFingerprint !== expectedFingerprint) throw new Error(`repositories[${index}].desiredFingerprint does not match repository desired state`);
    requireCanonicalRemote(remote, repository, remoteAuth.host, `repositories[${index}].remote`);
    return { key, repository, remote, sourceBranch, additionalSourceBranches, desiredFingerprint };
  });
  if (repositories.length === 0) throw new Error("repositories must not be empty");
  ensureUnique(repositories.map((item) => item.key), "repository keys");
  ensureUnique(repositories.map((item) => item.repository), "repository paths");
  ensureUnique(repositories.map((item) => item.remote), "repository remotes");
  requireObject(raw.retry, "retry");
  requireObject(raw.freshness, "freshness");
  requireObject(raw.lifecycle, "lifecycle");
  if (raw.lifecycle.undeclaredRepositoryPolicy !== "retain" || raw.lifecycle.managedRefs !== "source-branch-only") {
    throw new Error("unsupported managed repository lifecycle policy");
  }
  return {
    schemaVersion: 1,
    kind: raw.kind,
    desiredHash,
    nodeId: requireString(raw.nodeId, "nodeId"),
    lane: requireString(raw.lane, "lane"),
    cacheMountPath,
    stateDirectory,
    reconcileIntervalMs: requirePositiveInteger(raw.reconcileIntervalMs, "reconcileIntervalMs"),
    fetchTimeoutMs: requirePositiveInteger(raw.fetchTimeoutMs, "fetchTimeoutMs"),
    shutdownGraceMs: requirePositiveInteger(raw.shutdownGraceMs, "shutdownGraceMs"),
    maxConcurrentRepositories: requirePositiveInteger(raw.maxConcurrentRepositories, "maxConcurrentRepositories"),
    retry: {
      maxAttempts: requirePositiveInteger(raw.retry.maxAttempts, "retry.maxAttempts"),
      initialDelayMs: requirePositiveInteger(raw.retry.initialDelayMs, "retry.initialDelayMs"),
      maxDelayMs: requirePositiveInteger(raw.retry.maxDelayMs, "retry.maxDelayMs"),
    },
    freshness: {
      maxAgeMs: requirePositiveInteger(raw.freshness.maxAgeMs, "freshness.maxAgeMs"),
    },
    lifecycle: {
      undeclaredRepositoryPolicy: "retain",
      managedRefs: "source-branch-only",
    },
    repositories,
    remoteAuth,
  };
}

async function reconcileOnce(cfg, path) {
  mkdirSync(dirname(path), { recursive: true });
  const credential = credentialStatus(cfg);
  const previous = readState(path);
  const previousByFingerprint = new Map(
    Array.isArray(previous?.repositories)
      ? previous.repositories.map((item) => [item?.desiredFingerprint, item])
      : [],
  );
  const results = await mapConcurrent(cfg.repositories, cfg.maxConcurrentRepositories, async (repository) => {
    const prior = previousByFingerprint.get(repository.desiredFingerprint) ?? null;
    return reconcileWithRetry(cfg, repository, prior);
  });
  const now = new Date().toISOString();
  const repositories = results.map((item) => withFreshness(item, cfg.freshness.maxAgeMs));
  const state = {
    schemaVersion: 1,
    kind: "AgentRunManagedRepositoryReconcilerStatus",
    desiredHash: cfg.desiredHash,
    nodeId: cfg.nodeId,
    lane: cfg.lane,
    observedAt: now,
    heartbeatAt: now,
    ready: credential.ready === true && repositories.every((item) => item.fresh === true && item.refPresent === true),
    lastCycleOk: repositories.every((item) => item.lastAttemptOk === true),
    credential,
    lifecycle: cfg.lifecycle,
    repositories,
    valuesPrinted: false,
  };
  writeState(path, state);
  return state;
}

async function reconcileWithRetry(cfg, repository, prior) {
  let failure = null;
  for (let attempt = 1; attempt <= cfg.retry.maxAttempts && !stopping; attempt += 1) {
    try {
      const result = await materializeRepository(cfg, repository);
      return {
        ...repositoryIdentity(repository),
        refPresent: true,
        commit: result.commit,
        lastAttemptAt: new Date().toISOString(),
        lastAttemptOk: true,
        lastSuccessAt: new Date().toISOString(),
        attempts: attempt,
        errorCode: null,
        errorFingerprint: null,
      };
    } catch (error) {
      failure = safeError(error);
      log({ event: "managed-repository-reconcile-attempt-failed", repository: repository.repository, sourceBranch: repository.sourceBranch, attempt, errorCode: failure.code, errorFingerprint: failure.fingerprint });
      if (attempt < cfg.retry.maxAttempts && !stopping) {
        const delayMs = Math.min(cfg.retry.maxDelayMs, cfg.retry.initialDelayMs * (2 ** (attempt - 1)));
        await sleep(delayMs);
      }
    }
  }
  const inspected = await inspectRepository(cfg, repository);
  const preserveSuccess = prior
    && prior.desiredFingerprint === repository.desiredFingerprint
    && prior.commit === inspected.commit
    && typeof prior.lastSuccessAt === "string";
  return {
    ...repositoryIdentity(repository),
    refPresent: inspected.refPresent,
    commit: inspected.commit,
    lastAttemptAt: new Date().toISOString(),
    lastAttemptOk: false,
    lastSuccessAt: preserveSuccess ? prior.lastSuccessAt : null,
    attempts: cfg.retry.maxAttempts,
    errorCode: failure?.code ?? (stopping ? "shutdown" : "reconcile-failed"),
    errorFingerprint: failure?.fingerprint ?? null,
  };
}

async function materializeRepository(cfg, repository) {
  assertCredentialReady(cfg);
  const finalPath = repositoryPath(cfg, repository);
  mkdirSync(dirname(finalPath), { recursive: true });
  const existing = existsSync(finalPath);
  if (existing && !(await isBareRepository(finalPath, cfg.fetchTimeoutMs))) {
    throw codedError("invalid-repository-path", "declared repository path exists but is not a bare Git repository");
  }
  const workPath = existing
    ? finalPath
    : join(dirname(finalPath), `.${repository.key}.managed-tmp-${process.pid}-${randomBytes(6).toString("hex")}`);
  const branches = [repository.sourceBranch, ...repository.additionalSourceBranches];
  try {
    if (!existing) await git(cfg, ["init", "--bare", workPath]);
    await git(cfg, ["--git-dir", workPath, "config", "uploadpack.allowReachableSHA1InWant", "true"]);
    await git(cfg, ["--git-dir", workPath, "config", "uploadpack.allowAnySHA1InWant", "true"]);
    await git(cfg, ["--git-dir", workPath, "config", "http.uploadpack", "true"]);
    const commits = new Map();
    for (const [index, branch] of branches.entries()) {
      const stageRef = `refs/agentrun-managed/stage/${repository.key}/${index}`;
      await git(cfg, ["--git-dir", workPath, "fetch", "--no-tags", repository.remote, `+refs/heads/${branch}:${stageRef}`]);
      const commit = (await git(cfg, ["--git-dir", workPath, "rev-parse", "--verify", `${stageRef}^{commit}`])).stdout.trim();
      if (!/^[0-9a-f]{40}$/u.test(commit)) throw codedError("invalid-source-commit", "fetched source ref did not resolve to a commit");
      await git(cfg, ["--git-dir", workPath, "update-ref", `refs/heads/${branch}`, commit]);
      await git(cfg, ["--git-dir", workPath, "update-ref", "-d", stageRef]);
      commits.set(branch, commit);
    }
    await git(cfg, ["--git-dir", workPath, "update-server-info"]);
    if (!existing) {
      await git(cfg, ["--git-dir", workPath, "symbolic-ref", "HEAD", `refs/heads/${repository.sourceBranch}`]);
      renameSync(workPath, finalPath);
    }
    return { commit: commits.get(repository.sourceBranch) };
  } catch (error) {
    if (existing) {
      for (const index of branches.keys()) {
        try { await git(cfg, ["--git-dir", workPath, "update-ref", "-d", `refs/agentrun-managed/stage/${repository.key}/${index}`]); } catch {}
      }
    } else {
      rmSync(workPath, { recursive: true, force: true });
    }
    throw error;
  }
}

async function inspectRepository(cfg, repository) {
  const path = repositoryPath(cfg, repository);
  if (!existsSync(path) || !(await isBareRepository(path, Math.min(cfg.fetchTimeoutMs, 10000)))) return { refPresent: false, commit: null };
  try {
    const result = await run("git", ["--git-dir", path, "rev-parse", "--verify", `refs/heads/${repository.sourceBranch}^{commit}`], { timeoutMs: Math.min(cfg.fetchTimeoutMs, 10000), env: process.env });
    const commit = result.stdout.trim();
    return { refPresent: /^[0-9a-f]{40}$/u.test(commit), commit: /^[0-9a-f]{40}$/u.test(commit) ? commit : null };
  } catch {
    return { refPresent: false, commit: null };
  }
}

async function isBareRepository(path, timeoutMs) {
  try {
    const result = await run("git", ["--git-dir", path, "rev-parse", "--is-bare-repository"], { timeoutMs, env: process.env });
    return result.stdout.trim() === "true";
  } catch {
    return false;
  }
}

async function git(cfg, gitArgs) {
  const proxyUrl = `http://${cfg.remoteAuth.proxy.host}:${cfg.remoteAuth.proxy.port}`;
  const env = {
    PATH: process.env.PATH ?? "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
    LANG: process.env.LANG ?? "C.UTF-8",
    LC_ALL: process.env.LC_ALL ?? "C.UTF-8",
    GIT_TERMINAL_PROMPT: "0",
    GIT_ASKPASS: cfg.remoteAuth.askPassFile,
    GIT_ASKPASS_REQUIRE: "force",
    GIT_CONFIG_NOSYSTEM: "1",
    GIT_CONFIG_GLOBAL: "/dev/null",
    GIT_SSL_NO_VERIFY: "0",
    HTTPS_PROXY: proxyUrl,
    HTTP_PROXY: proxyUrl,
    https_proxy: proxyUrl,
    http_proxy: proxyUrl,
    NO_PROXY: "",
    no_proxy: "",
  };
  return run("git", [
    "-c", "credential.helper=",
    "-c", "http.extraHeader=",
    "-c", "http.sslVerify=true",
    ...gitArgs,
  ], { timeoutMs: cfg.fetchTimeoutMs, env });
}

async function currentStatus(cfg, path) {
  const credential = credentialStatus(cfg);
  const state = readState(path);
  if (!state || state.desiredHash !== cfg.desiredHash || !Array.isArray(state.repositories)) {
    return {
      ok: false,
      ready: false,
      desiredHash: cfg.desiredHash,
      observedDesiredHash: state?.desiredHash ?? null,
      reason: state ? "desired-state-mismatch" : "status-missing",
      repositoryCount: cfg.repositories.length,
      credential,
      valuesPrinted: false,
    };
  }
  const stateByFingerprint = new Map(state.repositories.map((item) => [item?.desiredFingerprint, item]));
  const repositories = [];
  for (const desired of cfg.repositories) {
    const observed = stateByFingerprint.get(desired.desiredFingerprint) ?? null;
    const inspected = await inspectRepository(cfg, desired);
    const desiredIdentity = repositoryIdentity(desired);
    const item = withFreshness({
      ...desiredIdentity,
      remoteFingerprint: observed?.remoteFingerprint ?? null,
      remoteFingerprintAligned: observed?.remoteFingerprint === desiredIdentity.remoteFingerprint,
      refPresent: inspected.refPresent,
      commit: inspected.commit,
      lastAttemptAt: observed?.lastAttemptAt ?? null,
      lastAttemptOk: observed?.lastAttemptOk === true,
      lastSuccessAt: observed?.commit === inspected.commit ? observed?.lastSuccessAt ?? null : null,
      attempts: Number.isInteger(observed?.attempts) ? observed.attempts : null,
      errorCode: observed?.errorCode ?? null,
      errorFingerprint: observed?.errorFingerprint ?? null,
    }, cfg.freshness.maxAgeMs);
    repositories.push(item);
  }
  const heartbeatAgeMs = isoAgeMs(state.heartbeatAt);
  const heartbeatFresh = heartbeatAgeMs !== null && heartbeatAgeMs <= cfg.freshness.maxAgeMs;
  const ready = credential.ready === true && heartbeatFresh && repositories.every((item) => item.refPresent === true && item.fresh === true && item.remoteFingerprintAligned === true);
  return {
    ok: ready,
    ready,
    desiredHash: cfg.desiredHash,
    observedDesiredHash: state.desiredHash,
    nodeId: cfg.nodeId,
    lane: cfg.lane,
    heartbeatAt: state.heartbeatAt ?? null,
    heartbeatAgeMs,
    heartbeatFresh,
    lifecycle: cfg.lifecycle,
    credential,
    repositories,
    valuesPrinted: false,
  };
}

function compactStatus(cfg, state) {
  return {
    ok: state.ready === true,
    ready: state.ready === true,
    desiredHash: cfg.desiredHash,
    nodeId: cfg.nodeId,
    lane: cfg.lane,
    heartbeatAt: state.heartbeatAt,
    lifecycle: cfg.lifecycle,
    credential: state.credential,
    repositories: state.repositories,
    valuesPrinted: false,
  };
}

function repositoryIdentity(repository) {
  return {
    key: repository.key,
    repository: repository.repository,
    sourceBranch: repository.sourceBranch,
    additionalSourceBranches: repository.additionalSourceBranches,
    desiredFingerprint: repository.desiredFingerprint,
    remoteFingerprint: `sha256:${sha256(repository.remote)}`,
  };
}

function withFreshness(item, maxAgeMs) {
  const ageMs = isoAgeMs(item.lastSuccessAt);
  return {
    ...item,
    lastSuccessAgeMs: ageMs,
    fresh: item.refPresent === true && ageMs !== null && ageMs <= maxAgeMs,
  };
}

function repositoryPath(cfg, repository) {
  const root = resolve(cfg.cacheMountPath);
  const path = resolve(root, `${repository.repository}.git`);
  if (path !== root && !path.startsWith(`${root}${sep}`)) throw codedError("unsafe-repository-path", "repository path escapes cache root");
  return path;
}

function readState(path) {
  try {
    return JSON.parse(readFileSync(path, "utf8"));
  } catch {
    return null;
  }
}

function writeState(path, state) {
  mkdirSync(dirname(path), { recursive: true });
  const temporary = `${path}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
  writeFileSync(temporary, `${JSON.stringify(state)}\n`, { mode: 0o600 });
  renameSync(temporary, path);
}

async function mapConcurrent(items, concurrency, worker) {
  const results = new Array(items.length);
  let index = 0;
  async function consume() {
    while (!stopping) {
      const current = index;
      index += 1;
      if (current >= items.length) return;
      results[current] = await worker(items[current], current);
    }
  }
  await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => consume()));
  for (let current = 0; current < results.length; current += 1) {
    if (results[current] === undefined) {
      const item = items[current];
      results[current] = {
        ...repositoryIdentity(item),
        refPresent: false,
        commit: null,
        lastAttemptAt: new Date().toISOString(),
        lastAttemptOk: false,
        lastSuccessAt: null,
        attempts: 0,
        errorCode: "shutdown",
        errorFingerprint: null,
      };
    }
  }
  return results;
}

function run(command, commandArgs, options) {
  return new Promise((resolvePromise, rejectPromise) => {
    const child = spawn(command, commandArgs, { env: options.env, stdio: ["ignore", "pipe", "pipe"] });
    activeChildren.add(child);
    let stdout = "";
    let stderr = "";
    let timedOut = false;
    const timer = setTimeout(() => {
      timedOut = true;
      child.kill("SIGKILL");
    }, options.timeoutMs);
    child.stdout.on("data", (chunk) => { if (stdout.length < 32768) stdout += chunk.toString("utf8"); });
    child.stderr.on("data", (chunk) => { if (stderr.length < 32768) stderr += chunk.toString("utf8"); });
    child.on("error", (error) => {
      clearTimeout(timer);
      activeChildren.delete(child);
      rejectPromise(codedError("command-start-failed", error.message));
    });
    child.on("close", (code, signal) => {
      clearTimeout(timer);
      activeChildren.delete(child);
      if (timedOut) return rejectPromise(codedError("fetch-timeout", `${command} exceeded timeout`));
      if (code !== 0) return rejectPromise(codedError("command-failed", `${command} exited ${code ?? signal}: ${stderr || stdout}`));
      resolvePromise({ stdout, stderr });
    });
  });
}

function beginShutdown() {
  if (stopping) return;
  stopping = true;
  const timer = setTimeout(() => {
    for (const child of activeChildren) child.kill("SIGKILL");
  }, config.shutdownGraceMs);
  timer.unref();
}

function sleep(ms) {
  if (stopping) return Promise.resolve();
  return new Promise((resolvePromise) => {
    let settled = false;
    const finish = () => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      clearInterval(check);
      resolvePromise();
    };
    const timer = setTimeout(finish, ms);
    const check = setInterval(() => {
      if (stopping) finish();
    }, Math.min(ms, 100));
  });
}

function safeError(error) {
  const message = error instanceof Error ? error.message : String(error);
  const lower = message.toLowerCase();
  const code = error?.code && typeof error.code === "string" && error.code !== "command-failed"
    ? error.code
    : lower.includes("couldn't find remote ref") || lower.includes("remote ref does not exist")
      ? "source-ref-missing"
      : lower.includes("repository not found") || lower.includes("not found")
        ? "repository-not-found"
        : lower.includes("permission denied") || lower.includes("publickey") || lower.includes("authentication")
          ? "authentication-failed"
          : lower.includes("timeout") || lower.includes("timed out")
            ? "fetch-timeout"
            : "fetch-failed";
  return { code, fingerprint: sha256(message).slice(0, 16) };
}

function codedError(code, message) {
  const error = new Error(message);
  error.code = code;
  return error;
}

function isoAgeMs(value) {
  if (typeof value !== "string") return null;
  const time = Date.parse(value);
  return Number.isFinite(time) ? Math.max(0, Date.now() - time) : null;
}

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

function log(value) {
  process.stdout.write(`${JSON.stringify({ ...value, valuesPrinted: false })}\n`);
}

function requireObject(value, path) {
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} must be an object`);
  return value;
}

function requireArray(value, path) {
  if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
  return value;
}

function requireString(value, path) {
  if (typeof value !== "string" || value.length === 0) throw new Error(`${path} must be a non-empty string`);
  return value;
}

function requirePositiveInteger(value, path) {
  if (!Number.isInteger(value) || value <= 0) throw new Error(`${path} must be a positive integer`);
  return value;
}

function requirePort(value, path) {
  const port = requirePositiveInteger(value, path);
  if (port > 65535) throw new Error(`${path} must be at most 65535`);
  return port;
}

function requireLiteral(value, path, expected) {
  if (value !== expected) throw new Error(`${path} must be ${expected}`);
  return expected;
}

function requireHost(value, path) {
  const host = requireString(value, path);
  if (host !== host.toLowerCase() || !/^(?:[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?|\d{1,3}(?:\.\d{1,3}){3})$/u.test(host) || host.includes("..")) {
    throw new Error(`${path} must be a lowercase hostname or IPv4 address without a port`);
  }
  return host;
}

function requireCanonicalRemote(value, repository, host, path) {
  let parsed;
  try {
    parsed = new URL(value);
  } catch {
    throw new Error(`${path} must be a canonical HTTPS URL`);
  }
  const expected = `https://${host}/${repository}.git`;
  if (
    value !== expected
    || parsed.protocol !== "https:"
    || parsed.hostname !== host
    || parsed.port !== ""
    || parsed.username !== ""
    || parsed.password !== ""
    || parsed.search !== ""
    || parsed.hash !== ""
  ) throw new Error(`${path} must exactly match ${expected} without userinfo, port, query, or fragment`);
}

function credentialStatus(cfg) {
  try {
    assertCredentialReady(cfg);
    return { ready: true, errorCode: null, errorFingerprint: null, valuesPrinted: false };
  } catch (error) {
    const failure = safeError(error);
    return { ready: false, errorCode: failure.code, errorFingerprint: failure.fingerprint, valuesPrinted: false };
  }
}

function assertCredentialReady(cfg) {
  if (!existsSync(cfg.remoteAuth.askPassFile)) throw codedError("credential-helper-missing", "rendered credential helper is missing");
  let token;
  try {
    token = readFileSync(cfg.remoteAuth.tokenFile, "utf8").trim();
  } catch {
    throw codedError("credential-file-missing", "projected credential file is missing or unreadable");
  }
  if (token.length === 0) throw codedError("credential-empty", "projected credential file is empty");
  if (/[\u0000\r\n]/u.test(token)) throw codedError("credential-invalid", "projected credential file contains unsupported control characters");
}

function requireAbsolutePath(value, path) {
  const text = requireString(value, path);
  if (!text.startsWith("/") || text.includes("..")) throw new Error(`${path} must be an absolute path without ..`);
  return text;
}

function requireRelativePath(value, path) {
  const text = requireString(value, path);
  if (text.startsWith("/") || text.includes("..") || text === ".") throw new Error(`${path} must be a dedicated relative path without ..`);
  return text;
}

function requireSimpleId(value, path) {
  const text = requireString(value, path);
  if (!/^[A-Za-z0-9._-]+$/u.test(text)) throw new Error(`${path} must use a simple id`);
  return text;
}

function requireRepository(value, path) {
  const text = requireString(value, path);
  if (!/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/u.test(text) || text.includes("..")) throw new Error(`${path} must be owner/name without ..`);
  return text;
}

function requireGitRef(value, path) {
  const text = requireString(value, path);
  if (text.startsWith("-") || text.startsWith("/") || text.endsWith("/") || text.endsWith(".") || text.includes("..") || text.includes("@{") || /[\u0000-\u0020~^:?*[\\]/u.test(text)) {
    throw new Error(`${path} must be a safe Git ref name`);
  }
  return text;
}

function requireHex(value, path) {
  const text = requireString(value, path);
  if (!/^[0-9a-f]{64}$/u.test(text)) throw new Error(`${path} must be a sha256 fingerprint`);
  return text;
}

function ensureUnique(values, path) {
  if (new Set(values).size !== values.length) throw new Error(`${path} must be unique`);
}
