#!/bin/sh
set -eu
# __HWLAB_DEPENDENCY_PROXY_PROBE__
echo '{"event":"ci-base-image","phase":"control-plane-reconciler","image":"__HWLAB_CI_TOOLS_IMAGE__","policy":"no-runtime-apk"}'
for tool in node npm git python3 ssh curl timeout; do command -v "$tool" >/dev/null 2>&1 || { echo '{"event":"ci-base-image","phase":"control-plane-reconciler","ok":false,"reason":"missing-tool","tool":"'"$tool"'"}'; exit 31; }; done
# __HWLAB_GIT_SSH_SHELL__

git_ssh_setup
workdir="$(mktemp -d)"
git_timed control-plane-clone 180 git clone --depth 1 --branch "$SOURCE_BRANCH" "$GIT_READ_URL" "$workdir/repo"
cd "$workdir/repo"
git remote set-url origin "$GIT_READ_URL"
revision="$(git rev-parse HEAD)"
: "${LANE:?LANE is required}"
: "${CATALOG_PATH:?CATALOG_PATH is required}"
: "${IMAGE_TAG_MODE:?IMAGE_TAG_MODE is required}"
: "${RUNTIME_PATH:?RUNTIME_PATH is required}"
gitops_root="${RUNTIME_PATH%/*}"
if [ "$gitops_root" = "$RUNTIME_PATH" ]; then gitops_root="."; fi
node scripts/run-bun.mjs scripts/gitops-render.mjs --lane "$LANE" --catalog-path "$CATALOG_PATH" --image-tag-mode "$IMAGE_TAG_MODE" --source-revision "$revision" --source-repo "$GIT_URL" --source-branch "$SOURCE_BRANCH" --gitops-branch "$GITOPS_BRANCH" --gitops-root "$gitops_root" --out "$gitops_root" --registry-prefix "$REGISTRY_PREFIX"
node scripts/run-bun.mjs scripts/gitops-render.mjs --lane "$LANE" --catalog-path "$CATALOG_PATH" --image-tag-mode "$IMAGE_TAG_MODE" --source-revision "$revision" --source-repo "$GIT_URL" --source-branch "$SOURCE_BRANCH" --gitops-branch "$GITOPS_BRANCH" --gitops-root "$gitops_root" --out "$gitops_root" --registry-prefix "$REGISTRY_PREFIX" --check
node <<'NODE'
const fs = require("node:fs");
const https = require("node:https");

function requiredEnv(name) {
  const value = process.env[name];
  if (!value) throw new Error(name + " is required");
  return value;
}

const namespace = requiredEnv("POD_NAMESPACE");
const host = process.env.KUBERNETES_SERVICE_HOST;
const port = process.env.KUBERNETES_SERVICE_PORT || "443";
if (!host) throw new Error("KUBERNETES_SERVICE_HOST is not available");

const token = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8");
const ca = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");
const tektonDir = requiredEnv("TEKTON_DIR");
const argoFiles = String(process.env.ARGO_FILES || "").split(",").map((item) => item.trim()).filter(Boolean);
const fieldManager = requiredEnv("FIELD_MANAGER");
const eventName = requiredEnv("RECONCILE_EVENT");
const manifestFiles = [
  "deploy/gitops/node/" + tektonDir + "/rbac.yaml",
  "deploy/gitops/node/" + tektonDir + "/tasks.yaml",
  "deploy/gitops/node/" + tektonDir + "/pipeline.yaml",
  ...(process.env.LANE === "v02" ? [] : [
    "deploy/gitops/node/" + tektonDir + "/poller.yaml",
    "deploy/gitops/node/" + tektonDir + "/control-plane-reconciler.yaml"
  ]),
  ...argoFiles
];
const plurals = new Map([
  ["ServiceAccount", "serviceaccounts"],
  ["Role", "roles"],
  ["RoleBinding", "rolebindings"],
  ["Pipeline", "pipelines"],
  ["Task", "tasks"],
  ["CronJob", "cronjobs"],
  ["Application", "applications"],
  ["AppProject", "appprojects"]
]);

function encode(value) {
  return encodeURIComponent(String(value));
}

function itemsFrom(file) {
  const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
  return parsed.kind === "List" ? parsed.items || [] : [parsed];
}

function apiPath(item) {
  if (item.kind === "Namespace") return null;
  const name = item.metadata?.name;
  if (!name) throw new Error("manifest item is missing metadata.name");
  const ns = item.metadata?.namespace || namespace;
  const plural = plurals.get(item.kind);
  if (!plural) throw new Error("unsupported reconciler manifest kind " + item.kind);
  if (item.apiVersion === "v1") return "/api/v1/namespaces/" + encode(ns) + "/" + plural + "/" + encode(name);
  const parts = String(item.apiVersion || "").split("/");
  if (parts.length !== 2) throw new Error("unsupported apiVersion " + item.apiVersion + " for " + item.kind);
  return "/apis/" + encode(parts[0]) + "/" + encode(parts[1]) + "/namespaces/" + encode(ns) + "/" + plural + "/" + encode(name);
}

function reconcilePolicy(item) {
  const ns = item.metadata?.namespace || namespace;
  const crossNamespaceRbac = (item.kind === "Role" || item.kind === "RoleBinding") && ns !== namespace;
  if (crossNamespaceRbac && process.env.HWLAB_RECONCILE_CROSS_NAMESPACE_RBAC !== "1") {
    return { action: "skip", reason: "cross-namespace-rbac-bootstrap-managed", namespace: ns };
  }
  return { action: "apply", namespace: ns };
}

function request(method, path, body) {
  const payload = body ? JSON.stringify(body) : "";
  const headers = { Authorization: "Bearer " + token };
  if (payload) {
    headers["Content-Type"] = "application/apply-patch+yaml";
    headers["Content-Length"] = Buffer.byteLength(payload);
  }
  return new Promise((resolve, reject) => {
    const req = https.request({ host, port, method, path, ca, headers }, (res) => {
      let data = "";
      res.setEncoding("utf8");
      res.on("data", (chunk) => { data += chunk; });
      res.on("end", () => resolve({ statusCode: res.statusCode || 0, body: data }));
    });
    req.on("error", reject);
    if (payload) req.write(payload);
    req.end();
  });
}

(async () => {
  const results = [];
  for (const file of manifestFiles) {
    for (const item of itemsFrom(file)) {
      const policy = reconcilePolicy(item);
      if (policy.action === "skip") {
        results.push({ file, kind: item.kind, name: item.metadata?.name || null, namespace: policy.namespace, status: "skipped", reason: policy.reason });
        continue;
      }
      const path = apiPath(item);
      if (!path) {
        results.push({ file, kind: item.kind, name: item.metadata?.name || null, status: "skipped-cluster-scoped" });
        continue;
      }
      const response = await request("PATCH", path + "?fieldManager=" + encode(fieldManager) + "&force=true", item);
      if (response.statusCode < 200 || response.statusCode > 299) {
        throw new Error("apply failed for " + item.kind + "/" + item.metadata.name + " status=" + response.statusCode + " body=" + response.body.slice(0, 1000));
      }
      results.push({ file, kind: item.kind, name: item.metadata.name, status: "applied", statusCode: response.statusCode });
    }
  }
  console.log(JSON.stringify({ event: eventName, sourceRevision: process.env.RECONCILE_REVISION || null, results }, null, 2));
})().catch((error) => {
  console.error(error.stack || error.message);
  process.exitCode = 1;
});
NODE
