#!/bin/sh
set -eu
# __HWLAB_CI_TIMING_SHELL__

export HWLAB_TEKTON_PIPELINERUN="$(context.pipelineRun.name)"
export HWLAB_TEKTON_TASKRUN="$(context.taskRun.name)"
export HWLAB_TEKTON_TASK="gitops-promote"
export HWLAB_SOURCE_REVISION="$(params.revision)"
echo '{"event":"ci-base-image","phase":"gitops-promote","image":"__HWLAB_CI_TOOLS_IMAGE__","policy":"no-runtime-apt"}'
for tool in node git timeout; do command -v "$tool" >/dev/null 2>&1 || { echo '{"event":"ci-base-image","phase":"gitops-promote","ok":false,"reason":"missing-tool","tool":"'"$tool"'"}'; exit 31; }; done
# __HWLAB_GIT_SSH_SHELL__

git_url_requires_ssh() { case "$1" in git@*|ssh://*) return 0 ;; *) return 1 ;; esac; }
lane="$(params.lane)"
case "$lane" in
  node) runtime_lane=false ;;
  *) runtime_lane=true ;;
esac
if [ "$runtime_lane" = "true" ]; then
  printf 'false' > /tekton/results/runtime-ready-required
else
  printf 'true' > /tekton/results/runtime-ready-required
fi
source_head_url_for_setup="$(params.git-url)"
gitops_read_url_for_setup="$(params.git-url)"
if [ "$runtime_lane" = "true" ]; then
  source_head_url_for_setup="$(params.git-read-url)"
  gitops_read_url_for_setup="$(params.git-read-url)"
fi
gitops_write_url_for_setup="$(params.git-write-url)"
if git_url_requires_ssh "$source_head_url_for_setup" || git_url_requires_ssh "$gitops_read_url_for_setup" || git_url_requires_ssh "$gitops_write_url_for_setup"; then
  for tool in ssh ssh-keyscan; do command -v "$tool" >/dev/null 2>&1 || { echo '{"event":"ci-base-image","phase":"gitops-promote","ok":false,"reason":"missing-tool","tool":"'"$tool"'"}'; exit 31; }; done
  git_ssh_setup
else
  echo '{"event":"git-ssh-setup","phase":"gitops-promote","status":"skipped","reason":"non-ssh-git-url"}'
fi
cd /workspace/source/repo
test "$(git rev-parse HEAD)" = "$(params.revision)"

check_source_head() {
  phase="$1"
  expected="${2:-$(params.revision)}"
  source_head_url="$(params.git-url)"
  if [ "$runtime_lane" = "true" ]; then
    source_head_url="$(params.git-read-url)"
  fi
  latest_file="$(mktemp)"
  if ! git_timed "source-head-$phase" 45 git ls-remote "$source_head_url" "refs/heads/$(params.source-branch)" > "$latest_file"; then
    echo '{"status":"failed","reason":"source-head-check-failed","phase":"'"$phase"'","sourceBranch":"'"$(params.source-branch)"'","sourceRevision":"'"$(params.revision)"'"}'
    exit 1
  fi
  latest="$(cut -f1 "$latest_file" | head -n 1)"
  if [ -z "$latest" ]; then
    echo '{"status":"failed","reason":"source-head-unresolved","phase":"'"$phase"'","sourceBranch":"'"$(params.source-branch)"'","sourceRevision":"'"$(params.revision)"'"}'
    exit 1
  fi
  if [ "$latest" != "$expected" ]; then
    printf 'false' > /tekton/results/runtime-ready-required || true
    echo '{"status":"skipped-stale-source","verdict":"superseded","phase":"'"$phase"'","sourceBranch":"'"$(params.source-branch)"'","sourceRevision":"'"$(params.revision)"'","expectedRevision":"'"$expected"'","latestRevision":"'"$latest"'"}'
    exit 0
  fi
}

check_source_head before-render
if [ -s /workspace/source/affected-services.json ] && node - /workspace/source/affected-services.json <<'NODE'
const fs = require("node:fs");
const plan = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const buildServices = Array.isArray(plan.buildServices) ? plan.buildServices : [];
const affectedServices = Array.isArray(plan.affectedServices) ? plan.affectedServices : [];
const willRunGitopsPromote = plan.ciCdPlan && plan.ciCdPlan.willRunGitopsPromote === true;
process.exit(!willRunGitopsPromote && buildServices.length === 0 && affectedServices.length === 0 ? 0 : 1);
NODE
then
  printf 'false' > /tekton/results/runtime-ready-required || true
  echo '{"event":"gitops-promote","status":"skipped","reason":"no-build-no-rollout-plan"}'
  exit 0
fi
git config --global user.name "HWLAB node GitOps Bot"
git config --global user.email "hwlab-node-gitops-bot@users.noreply.github.com"
catalog_path="$(params.catalog-path)"
runtime_path="$(params.runtime-path)"
gitops_root_from_runtime_path() {
  path="$1"
  case "$path" in
    */*) printf '%s
' "${path%/*}" ;;
    *) printf '.
' ;;
  esac
}
gitops_root="$(gitops_root_from_runtime_path "$runtime_path")"

argo_hard_refresh_runtime_lane() {
  if [ "$runtime_lane" != "true" ]; then return 0; fi
  ARGO_APPLICATION="hwlab-node-$lane" ARGO_FIELD_MANAGER="hwlab-$lane-gitops-promote" node <<'NODE'
const fs = require("node:fs");
const https = require("node:https");

const host = process.env.KUBERNETES_SERVICE_HOST;
const port = process.env.KUBERNETES_SERVICE_PORT || "443";
const startedAt = Date.now();
const application = process.env.ARGO_APPLICATION || "hwlab-node-v02";
const fieldManager = process.env.ARGO_FIELD_MANAGER || "hwlab-v02-gitops-promote";
const pipelineRun = process.env.HWLAB_TEKTON_PIPELINERUN || null;
const taskRun = process.env.HWLAB_TEKTON_TASKRUN || null;
const task = process.env.HWLAB_TEKTON_TASK || null;
const revision = process.env.HWLAB_SOURCE_REVISION || null;

function emit(payload) {
  console.log(JSON.stringify({
    event: "node-cicd-timing",
    schemaVersion: "v1",
    stage: "argo-hard-refresh",
    pipelineRun,
    taskRun,
    task,
    revision,
    application,
    source: "scripts/gitops-render.mjs",
    at: new Date().toISOString(),
    ...payload
  }));
}

function safeJson(text) {
  try {
    return JSON.parse(text);
  } catch {
    return null;
  }
}

function request(method, path, body, contentType = "application/merge-patch+json") {
  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 payload = body ? JSON.stringify(body) : "";
  const headers = { Authorization: "Bearer " + token };
  if (payload) {
    headers["Content-Type"] = contentType;
    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, json: safeJson(data) }));
    });
    req.on("error", reject);
    if (payload) req.write(payload);
    req.end();
  });
}

(async () => {
  if (!host) {
    emit({ status: "skipped", reason: "kubernetes-service-host-missing", durationMs: Date.now() - startedAt });
    return;
  }
  const response = await request(
    "PATCH",
    "/apis/argoproj.io/v1alpha1/namespaces/argocd/applications/" + encodeURIComponent(application) + "?fieldManager=" + encodeURIComponent(fieldManager),
    { metadata: { annotations: { "argocd.argoproj.io/refresh": "hard" } } }
  );
  if (response.statusCode >= 200 && response.statusCode < 300) {
    emit({ status: "succeeded", durationMs: Date.now() - startedAt, statusCode: response.statusCode });
    return;
  }
  emit({ status: "degraded", reason: "argo-refresh-patch-failed", durationMs: Date.now() - startedAt, statusCode: response.statusCode, body: response.body.slice(0, 1000) });
})().catch((error) => {
  emit({ status: "degraded", reason: "argo-refresh-patch-error", durationMs: Date.now() - startedAt, error: error.message });
});
NODE
}

if [ -s /workspace/source/dev-artifacts.json ]; then
  node scripts/refresh-artifact-catalog.mjs --lane "$lane" --catalog-path "$catalog_path" --deploy-config deploy/deploy.yaml --image-tag-mode "$(params.image-tag-mode)" --target-ref "$(params.revision)" --publish-report /workspace/source/dev-artifacts.json --write
fi
gitops_render_started_ms="$(ci_now_ms)"
node scripts/run-bun.mjs scripts/gitops-render.mjs --lane "$lane" --catalog-path "$catalog_path" --image-tag-mode "$(params.image-tag-mode)" --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --gitops-root "$gitops_root" --out "$gitops_root" --registry-prefix "$(params.registry-prefix)" --use-deploy-images
node scripts/run-bun.mjs scripts/gitops-render.mjs --lane "$lane" --catalog-path "$catalog_path" --image-tag-mode "$(params.image-tag-mode)" --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --gitops-root "$gitops_root" --out "$gitops_root" --registry-prefix "$(params.registry-prefix)" --use-deploy-images --check
ci_timing_emit gitops-render succeeded "$gitops_render_started_ms"
check_source_head before-push
workdir="$(mktemp -d)"
gitops_read_url="$(params.git-url)"
gitops_write_url="$(params.git-write-url)"
if [ "$runtime_lane" = "true" ]; then
  gitops_read_url="$(params.git-read-url)"
fi
gitops_clone_started_ms="$(ci_now_ms)"
git_timed gitops-clone 180 git clone --no-checkout "$gitops_read_url" "$workdir/gitops"
cd "$workdir/gitops"
git remote set-url origin "$gitops_write_url"
old_runtime_snapshot="$workdir/old-runtime-snapshot"
if git_timed gitops-branch-ls 45 git ls-remote --exit-code --heads origin "$(params.gitops-branch)" >/dev/null; then
  git_timed gitops-fetch 90 git fetch origin "$(params.gitops-branch)"
  git checkout -B "$(params.gitops-branch)" "origin/$(params.gitops-branch)"
  if [ "$runtime_lane" = "true" ] && [ -d "$runtime_path" ]; then
    mkdir -p "$old_runtime_snapshot"
    cp -a "$runtime_path"/. "$old_runtime_snapshot"/
  fi
  if [ "$runtime_lane" = "true" ]; then rm -rf "$runtime_path" "$catalog_path"; else rm -rf deploy/gitops/node "$catalog_path"; fi
else
  git checkout --orphan "$(params.gitops-branch)"
  git rm -rf . >/dev/null 2>&1 || true
fi
ci_timing_emit gitops-clone succeeded "$gitops_clone_started_ms"
mkdir -p "$(dirname "$runtime_path")" "$(dirname "$catalog_path")"
if [ "$runtime_lane" = "true" ]; then
  cp -a "/workspace/source/repo/$runtime_path" "$runtime_path"
  cp "/workspace/source/repo/$catalog_path" "$catalog_path"
  git add "$catalog_path" "$runtime_path"
else
  mkdir -p deploy/gitops
  cp -a /workspace/source/repo/deploy/gitops/node deploy/gitops/node
  cp "/workspace/source/repo/$catalog_path" "$catalog_path"
  git add "$catalog_path" deploy/gitops/node
fi
if [ "$runtime_lane" = "true" ] && [ -s /workspace/source/affected-services.json ]; then
  runtime_noop_decision="$(node - "$runtime_path" "$old_runtime_snapshot" /workspace/source/affected-services.json <<'NODE'
const fs = require("node:fs");
const path = require("node:path");

const [runtimePath, oldRuntimePath, planPath] = process.argv.slice(2);

function readJson(filePath) {
  return JSON.parse(fs.readFileSync(filePath, "utf8"));
}

function stable(value) {
  if (Array.isArray(value)) return "[" + value.map(stable).join(",") + "]";
  if (value && typeof value === "object") {
    return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + stable(value[key])).join(",") + "}";
  }
  return JSON.stringify(value);
}

function scrub(value) {
  if (Array.isArray(value)) return value.map(scrub);
  if (!value || typeof value !== "object") return value;
  const result = {};
  for (const [key, child] of Object.entries(value)) {
    if (key === "hwlab.pikastech.local/source-commit" ||
        key === "hwlab.pikastech.local/artifact-source-commit" ||
        key === "hwlab.pikastech.local/boot-commit" ||
        key === "sourceCommitId" ||
        key === "bootCommit") {
      result[key] = "<runtime-source-identity>";
      continue;
    }
    const envName = String(value.name || "");
    if (key === "value" && /^HWLAB_.*COMMIT/.test(envName)) {
      result[key] = "<runtime-source-identity>";
      continue;
    }
    result[key] = scrub(child);
  }
  return result;
}

function listFiles(rootDir) {
  if (!rootDir || !fs.existsSync(rootDir)) return null;
  const files = [];
  function walk(current) {
    for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
      const fullPath = path.join(current, entry.name);
      if (entry.isDirectory()) walk(fullPath);
      else if (entry.isFile()) files.push(path.relative(rootDir, fullPath).replaceAll(path.sep, "/"));
    }
  }
  walk(rootDir);
  return files.sort();
}

function normalizedTree(rootDir) {
  const files = listFiles(rootDir);
  if (!files) return null;
  const entries = {};
  for (const relativePath of files) {
    const filePath = path.join(rootDir, relativePath);
    const text = fs.readFileSync(filePath, "utf8");
    try {
      entries[relativePath] = stable(scrub(JSON.parse(text)));
    } catch {
      entries[relativePath] = text.replace(/[a-f0-9]{40}/gu, "<runtime-source-identity>");
    }
  }
  return stable(entries);
}

const plan = readJson(planPath);
const buildServices = Array.isArray(plan.buildServices) ? plan.buildServices : [];
const rolloutServices = Array.isArray(plan.rolloutServices) ? plan.rolloutServices : [];
if (buildServices.length > 0 || rolloutServices.length > 0) {
  process.stdout.write("runtime-required");
  process.exit(0);
}

const oldTree = normalizedTree(oldRuntimePath);
const newTree = normalizedTree(runtimePath);
process.stdout.write(oldTree && newTree && oldTree === newTree ? "runtime-identity-only" : "runtime-required");
NODE
)"
  if [ "$runtime_noop_decision" = "runtime-identity-only" ]; then
    printf 'false' > /tekton/results/runtime-ready-required
    echo '{"status":"skipped-runtime-unchanged","reason":"runtime-identity-only","gitopsBranch":"'"$(params.gitops-branch)"'","sourceRevision":"'"$(params.revision)"'"}'
    ci_timing_emit gitops-commit skipped "$(ci_now_ms)"
    ci_timing_emit gitops-push skipped "$(ci_now_ms)"
    exit 0
  fi
fi
if git diff --cached --quiet; then
  printf 'false' > /tekton/results/runtime-ready-required
  echo '{"status":"unchanged","gitopsBranch":"'"$(params.gitops-branch)"'","sourceRevision":"'"$(params.revision)"'"}'
  ci_timing_emit gitops-commit unchanged "$(ci_now_ms)"
  ci_timing_emit gitops-push unchanged "$(ci_now_ms)"
  exit 0
fi
short="$(printf '%.7s' "$(params.revision)")"
gitops_commit_started_ms="$(ci_now_ms)"
git commit -m "chore: promote node GitOps source $short"
ci_timing_emit gitops-commit succeeded "$gitops_commit_started_ms"
gitops_push_started_ms="$(ci_now_ms)"
git_timed gitops-push 120 git push origin "HEAD:$(params.gitops-branch)"
ci_timing_emit gitops-push succeeded "$gitops_push_started_ms"
if [ "$runtime_lane" = "true" ]; then
  printf 'false' > /tekton/results/runtime-ready-required
  echo '{"event":"runtime-ready","phase":"gitops-promote","status":"delegated-post-flush-closeout","gitopsBranch":"'"$(params.gitops-branch)"'","sourceRevision":"'"$(params.revision)"'"}'
fi
argo_hard_refresh_runtime_lane
echo '{"status":"pushed","gitopsBranch":"'"$(params.gitops-branch)"'","sourceRevision":"'"$(params.revision)"'","gitopsWriteUrl":"'"$gitops_write_url"'"}'
