#!/usr/bin/env node
// SPEC: PJ2026-01060703 HWLAB runtime GitOps Pipeline guard.
// Responsibility: patch rendered Tekton Pipeline GitOps steps to keep runtime GitOps gates active on no-build paths.
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { createRequire } from "node:module";
import { runtimeGitopsScriptsConfigMap } from "./runtime-gitops-scripts-configmap.mjs";

const requireFromScript = createRequire(import.meta.url);
const requireFromCwd = createRequire(path.join(process.cwd(), "package.json"));
const YAML = requireYaml();
const args = parseArgs(process.argv.slice(2));
const overlay = readOverlay();
const pipelinePath = requiredArg("pipeline");
const scriptsConfigMapPath = args.scriptsConfigMap ?? args.scriptsConfigmap ?? null;
const scriptsDir = args.scriptsDir ?? null;
const namespaceArg = args.namespace ?? null;
const configMapName = args.configMapName ?? args.configmapName ?? `${requiredOverlayString("pipelineName")}-runtime-gitops-scripts`;

if (!existsSync(pipelinePath)) throw new Error(`rendered Pipeline missing: ${pipelinePath}`);

const docs = YAML.parseAllDocuments(readFileSync(pipelinePath, "utf8")).map((document) => document.toJS()).filter((doc) => doc !== null);
let patched = false;
let summary = null;
for (const doc of docs) {
  if (!doc || typeof doc !== "object" || doc.kind !== "Pipeline") continue;
  summary = injectRuntimeGitopsGuard(doc);
  patched = true;
}
if (!patched || summary === null) throw new Error(`rendered Pipeline not found: ${pipelinePath}`);
writeFileSync(pipelinePath, `${docs.map((doc) => YAML.stringify(doc).trimEnd()).join("\n---\n")}\n`, "utf8");

let scriptsConfigMap = null;
if (scriptsConfigMapPath !== null) {
  const namespace = namespaceArg || pipelineNamespace(docs) || "hwlab-ci";
  scriptsConfigMap = writeScriptsConfigMap(scriptsConfigMapPath, namespace);
}

console.error(JSON.stringify({
  event: "unidesk-runtime-gitops-pipeline-guard",
  ok: true,
  pipelinePath,
  runtimeGitopsGuard: summary,
  scriptsConfigMap,
}));

function injectRuntimeGitopsGuard(pipeline) {
  const tasks = Array.isArray(pipeline?.spec?.tasks) ? pipeline.spec.tasks : [];
  const task = tasks.find((item) => item && item.name === "gitops-promote");
  if (!task) throw new Error("runtime GitOps guard injection failed: gitops-promote task missing");
  task.taskSpec = objectOr(task.taskSpec);
  const steps = Array.isArray(task.taskSpec.steps) ? task.taskSpec.steps : [];
  const step = steps.find((item) => typeof item?.script === "string" && item.script.includes("scripts/gitops-render.mjs"));
  if (!step) throw new Error("runtime GitOps guard injection failed: gitops-promote render step missing");

  const originalScript = String(step.script);
  let script = patchNoBuildNoRolloutCondition(originalScript);
  const noBuildConditionPatched = script !== originalScript;
  const beforeSkipPatch = script;
  script = patchNoBuildNoRolloutSkip(script);
  const noBuildSkipPatched = script !== beforeSkipPatch;
  const beforeInjection = script;
  script = injectRuntimeGitopsCommands(script);
  if (hasWillRunGitopsPromoteCoupling(script)) {
    throw new Error("runtime GitOps guard injection failed: no-build-no-rollout marker remains coupled to willRunGitopsPromote");
  }
  if (hasNoBuildNoRolloutEarlyExit(script)) {
    throw new Error("runtime GitOps guard injection failed: no-build-no-rollout-plan early exit remains after patch");
  }
  const postprocessPresent = hasPostprocess(script);
  const verifyPresent = hasVerify(script);
  if (!postprocessPresent || !verifyPresent) throw new Error("runtime GitOps guard injection failed: postprocess/verify commands missing after patch");
  step.script = script;
  ensureRuntimeGitopsScriptsMount(task.taskSpec, step);
  return {
    present: true,
    configMapName,
    stepName: typeof step.name === "string" ? step.name : null,
    noBuildConditionPatched,
    noBuildSkipPatched,
    postprocessInjected: !hasPostprocess(beforeInjection) && postprocessPresent,
    verifyInjected: !hasVerify(beforeInjection) && verifyPresent,
  };
}

function patchNoBuildNoRolloutCondition(script) {
  return String(script).replace(
    /const willRunGitopsPromote = plan\.ciCdPlan && plan\.ciCdPlan\.willRunGitopsPromote === true;\nprocess\.exit\(!willRunGitopsPromote && buildServices\.length === 0 && affectedServices\.length === 0 \? 0 : 1\);/u,
    "process.exit(buildServices.length === 0 && affectedServices.length === 0 ? 0 : 1);",
  );
}

function patchNoBuildNoRolloutSkip(script) {
  return String(script).replace(
    /(^|\n)([ \t]*)echo '\{"event":"gitops-promote","status":"skipped","reason":"no-build-no-rollout-plan"\}'(?:\s*>\s*&2)?\n[ \t]*exit 0(?=\n|$)/u,
    (_match, prefix, indent) => `${prefix}${indent}echo '{"event":"gitops-promote","status":"continuing","reason":"no-build-no-rollout-plan-gitops-verify"}' >&2`,
  );
}

function hasWillRunGitopsPromoteCoupling(script) {
  return /process\.exit\(!willRunGitopsPromote && buildServices\.length === 0 && affectedServices\.length === 0 \? 0 : 1\);/u.test(String(script));
}

function hasNoBuildNoRolloutEarlyExit(script) {
  return /echo '\{"event":"gitops-promote","status":"skipped","reason":"no-build-no-rollout-plan"\}'(?:\s*>\s*&2)?\n[ \t]*exit 0(?=\n|$)/u.test(String(script));
}

function injectRuntimeGitopsCommands(script) {
  const overlayEnv = "UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE=/etc/unidesk-cicd-runtime-gitops/runtime-gitops-overlay.json";
  const migratedScript = String(script).replace(/UNIDESK_RUNTIME_GITOPS_OVERLAY_B64='[^'\n]*'/gu, overlayEnv);
  if (hasPostprocess(migratedScript) && hasVerify(migratedScript)) return migratedScript;
  const postprocess = `${overlayEnv} node /etc/unidesk-cicd-runtime-gitops/runtime-gitops-postprocess.mjs`;
  const verify = `${overlayEnv} node /etc/unidesk-cicd-runtime-gitops/runtime-gitops-verify.mjs`;
  return migratedScript.replace(
    /(node scripts\/run-bun\.mjs scripts\/gitops-render\.mjs[^\n]*--use-deploy-images[^\n]*)/g,
    (match) => {
      if (match.includes("--check")) return hasVerify(script) ? match : verify;
      return hasPostprocess(script) ? match : `${match}\n${postprocess}`;
    },
  );
}

function hasPostprocess(script) {
  const value = String(script);
  return value.includes("runtime-gitops-postprocess.mjs") || value.includes("unidesk-runtime-gitops-postprocess");
}

function hasVerify(script) {
  const value = String(script);
  return value.includes("runtime-gitops-verify.mjs") || value.includes("unidesk-runtime-gitops-verify");
}

function ensureRuntimeGitopsScriptsMount(taskSpec, step) {
  const volumeName = "unidesk-runtime-gitops-scripts";
  taskSpec.volumes = Array.isArray(taskSpec.volumes) ? taskSpec.volumes : [];
  if (!taskSpec.volumes.some((item) => item && item.name === volumeName)) {
    taskSpec.volumes.push({ name: volumeName, configMap: { name: configMapName, defaultMode: 0o755 } });
  }
  step.volumeMounts = Array.isArray(step.volumeMounts) ? step.volumeMounts : [];
  if (!step.volumeMounts.some((item) => item && item.name === volumeName)) {
    step.volumeMounts.push({ name: volumeName, mountPath: "/etc/unidesk-cicd-runtime-gitops", readOnly: true });
  }
}

function writeScriptsConfigMap(targetPath, namespace) {
  if (scriptsDir === null) throw new Error("--scripts-dir is required with --scripts-configmap");
  const configMap = runtimeGitopsScriptsConfigMap({ overlay, scriptsDir, namespace, configMapName });
  writeFileSync(targetPath, `${YAML.stringify(configMap).trimEnd()}\n`, "utf8");
  return { name: configMapName, namespace, keyCount: Object.keys(configMap.data).length, path: targetPath };
}

function pipelineNamespace(docs) {
  for (const doc of docs) {
    const namespace = doc?.metadata?.namespace;
    if (typeof namespace === "string" && namespace.length > 0) return namespace;
  }
  return null;
}

function readOverlay() {
  const encoded = process.env.UNIDESK_RUNTIME_GITOPS_OVERLAY_B64;
  if (!encoded) throw new Error("UNIDESK_RUNTIME_GITOPS_OVERLAY_B64 is required");
  return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
}

function parseArgs(values) {
  const out = {};
  for (let index = 0; index < values.length; index += 1) {
    const value = values[index];
    if (!value.startsWith("--")) throw new Error(`unexpected argument: ${value}`);
    const key = value.slice(2).replace(/-([a-z])/gu, (_match, char) => char.toUpperCase());
    const next = values[index + 1];
    if (next === undefined || next.startsWith("--")) throw new Error(`${value} requires a value`);
    out[key] = next;
    index += 1;
  }
  return out;
}

function requiredArg(name) {
  const value = args[name];
  if (typeof value !== "string" || value.length === 0) throw new Error(`--${name} is required`);
  return value;
}

function requiredOverlayString(name) {
  const value = overlay[name];
  if (typeof value !== "string" || value.length === 0) throw new Error(`overlay.${name} is required`);
  return value;
}

function objectOr(value) {
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}

function requireYaml() {
  try {
    return requireFromScript("yaml");
  } catch {
    return requireFromCwd("yaml");
  }
}
