#!/usr/bin/env node
"use strict";

// The receiver is deliberately separate from the public authorization process.
// It owns the private Forgejo transport configuration and accepts only a bundle
// already bound to a workorder/session by lake-lamp-authz.
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { execFile } = require("node:child_process");

function loadRegistry(file) {
  const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
  if (!parsed || !parsed.repos || typeof parsed.repos !== "object") throw new Error("invalid_repo_push_registry");
  return parsed.repos;
}

async function receiveBundle(request, options = {}) {
  const registry = options.registry || loadRegistry(options.registryFile || process.env.LAKE_LAMP_REPO_PUSH_REGISTRY || "/etc/guanghu/lake-lamp/repo-push-registry.json");
  const run = options.run || runGit;
  const repo = String(request.repo || "").toLowerCase();
  const branch = String(request.branch || "main");
  const expectedHead = String(request.expected_head || "").toLowerCase();
  const entry = registry[repo];
  if (!entry) return blocked("repo_not_allowlisted");
  if (!/^[a-z0-9][a-z0-9._/-]{0,199}$/.test(branch) || branch !== String(entry.branch || "main")) return blocked("branch_not_allowlisted");
  if (!/^[0-9a-f]{40}$/.test(expectedHead)) return blocked("expected_head_required");
  const bundlePath = checkedBundlePath(request.bundle_path, options.uploadDir || process.env.LAKE_LAMP_REPO_UPLOAD_DIR || "/var/lib/guanghu/repo-push-uploads");
  if (!bundlePath) return blocked("bundle_path_invalid");

  const temporary = fs.mkdtempSync(path.join(options.tempDir || os.tmpdir(), "lake-lamp-receive-"));
  const bare = path.join(temporary, "quarantine.git");
  try {
    await run(["init", "--bare", bare]);
    await run(["-C", bare, "bundle", "verify", bundlePath]);
    await run(["-C", bare, "fetch", bundlePath, `refs/heads/${branch}:refs/heads/incoming`]);
    const incoming = (await run(["-C", bare, "rev-parse", "refs/heads/incoming"])).stdout.trim().toLowerCase();
    if (!/^[0-9a-f]{40}$/.test(incoming)) return blocked("bundle_branch_missing");
    const remoteHead = parseRemoteHead((await run(["ls-remote", "--exit-code", "--heads", entry.remote, `refs/heads/${branch}`])).stdout);
    if (remoteHead !== expectedHead) return blocked("expected_head_mismatch", { expected_head: expectedHead, actual_head: remoteHead || "" });
    const ancestry = await run(["-C", bare, "merge-base", "--is-ancestor", expectedHead, incoming], { allowFailure: true });
    if (!ancestry.ok) return blocked("non_fast_forward_bundle", { expected_head: expectedHead, incoming_commit: incoming });
    await run(["-C", bare, "push", entry.remote, `refs/heads/incoming:refs/heads/${branch}`]);
    return { ok: true, repo, branch, commit_sha: incoming, verification_url: String(entry.verification_url || ""), diagnostic_code: "repo_push_succeeded", deployment: { state: "not_requested", next_step: "仅当冰朔与语言层确认该提交需要部署时，再创建绑定此 SHA 的部署工单并显式派发。" } };
  } catch (error) {
    return blocked("repo_push_receiver_failed", { evidence: String(error && error.message || "receiver failure").slice(0, 600) });
  } finally {
    fs.rmSync(temporary, { recursive: true, force: true });
  }
}

function checkedBundlePath(value, uploadDir) {
  try {
    const root = fs.realpathSync(uploadDir);
    const candidate = fs.realpathSync(String(value || ""));
    return candidate.startsWith(`${root}${path.sep}`) && fs.statSync(candidate).isFile() ? candidate : "";
  } catch { return ""; }
}
function parseRemoteHead(output) {
  const match = String(output || "").match(/^([0-9a-f]{40})\s+refs\/heads\//m);
  return match ? match[1].toLowerCase() : "";
}
function blocked(diagnostic_code, extra = {}) { return { ok: false, diagnostic_code, ...extra }; }
function runGit(args, options = {}) {
  return new Promise((resolve, reject) => execFile("git", args, { timeout: 120000, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
    const result = { ok: !error, stdout: String(stdout || ""), stderr: String(stderr || "") };
    if (error && !options.allowFailure) return reject(new Error(result.stderr || error.message));
    resolve(result);
  }));
}

module.exports = { receiveBundle, checkedBundlePath, parseRemoteHead };
