#!/usr/bin/env bash
#MISE description="Statically validate the k8s surface (charts + Flux tree)"
#MISE dir="{{config_root}}"
# Renders every chart (helm template + kubeconform) + builds the Flux tree as
# Flux would (flux-local --enable-helm). No cluster; the CI gate for kubernetes/
# + charts/. NB: don't run while Tilt is converging — its helm-deps resource
# rebuilds charts/*/charts/ and races the renders.
set -euo pipefail

# Charts are role-grouped: apps/* (services), platform/* (operators/CRs),
# lib/yucca-common (shared library), dev/mock-oidc (dev-only). Paths below are
# relative to charts/.
LIB_CONSUMERS=(apps/yucca-api apps/yucca-admin-api apps/yucca-metrics-worker apps/web apps/meta apps/michael dev/mock-oidc)
ALL_CHARTS=(apps/yucca-api apps/yucca-admin-api apps/yucca-metrics-worker apps/web apps/meta apps/michael dev/mock-oidc platform/cnpg-cluster platform/ceph-objectuser platform/rook-ceph-cluster)

echo "==> helm dependency build (yucca-common consumers)"
for c in "${LIB_CONSUMERS[@]}"; do
  (cd "charts/$c" && helm dependency build >/dev/null)
done

echo "==> helm template + kubeconform"
for c in "${ALL_CHARTS[@]}"; do
  ns=yucca
  [ "$c" = "platform/rook-ceph-cluster" ] && ns=rook-ceph
  # NB: release name must not be YAML-boolean-ish ("y"/"on"/...): it lands in
  # labels and kubeconform reads it back as a bool.
  helm template yucca "charts/$c" -n "$ns" \
    | kubeconform -strict -ignore-missing-schemas - \
    && echo "    OK  $c"
done

echo "==> kustomize build (Flux entrypoints)"
# Entry points clusters/<p>/<r> + overlays apps/<p>/<r> (composing
# components/{infra,roles/<role>}). LoadRestrictionsNone mirrors Flux's
# kustomize-controller within one git artifact: role Components reference
# ../../apps/<app>.yaml, which the CLI's default RootOnly restrictor rejects.
kb() { kustomize build --load-restrictor=LoadRestrictionsNone "$@"; }
CLUSTERS=(staging/austin prod/htz-fsn1 dev/local)

echo "==> topology substitution safety"
# The real-cluster topology is a JSON document after Flux postBuild
# substitution. Reject characters that would escape its JSON string literals,
# and enforce the immutable internal-code convention before Flux sees them.
node <<'NODE'
const fs = require('node:fs');
const environments = [
  'kubernetes/clusters/staging/austin',
  'kubernetes/clusters/prod/htz-fsn1',
];
for (const directory of environments) {
  const files = [`${directory}/cluster-settings.yaml`, `${directory}/cluster-settings.generated.yaml`];
  const text = files.map((file) => fs.readFileSync(file, 'utf8')).join('\n');
  const value = (key) => {
    const match = text.match(new RegExp(`^\\s*${key}:\\s*(.+)$`, 'm'));
    if (!match) throw new Error(`${directory}: missing ${key}`);
    const raw = match[1].trim().replace(/\s+#.*$/, '');
    return raw.startsWith('"') ? JSON.parse(raw) : raw;
  };
  const site = value('SITE_CODE');
  const cluster = value('STORAGE_CLUSTER_CODE');
  const legacySite = value('LEGACY_SITE_CODE');
  const legacyCluster = value('LEGACY_STORAGE_CLUSTER_CODE');
  if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(site)) throw new Error(`${directory}: invalid SITE_CODE ${site}`);
  if (!cluster.startsWith(`${site}-`) || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(cluster)) {
    throw new Error(`${directory}: STORAGE_CLUSTER_CODE ${cluster} must start with ${site}-`);
  }
  if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(legacySite)) {
    throw new Error(`${directory}: invalid LEGACY_SITE_CODE ${legacySite}`);
  }
  if (
    !legacyCluster.startsWith(`${legacySite}-`) ||
    !/^[a-z0-9][a-z0-9-]{0,63}$/.test(legacyCluster)
  ) {
    throw new Error(`${directory}: LEGACY_STORAGE_CLUSTER_CODE ${legacyCluster} must start with ${legacySite}-`);
  }
  for (const key of [
    'SITE_DISPLAY_NAME',
    'SITE_DESCRIPTION',
    'STORAGE_CLUSTER_DISPLAY_NAME',
    'GW_HOST',
    'S3_ENDPOINT',
    'S3_HOST',
  ]) {
    const label = value(key);
    if (/["\\\u0000-\u001f]/.test(label)) {
      throw new Error(`${directory}: ${key} contains a character that is unsafe for topology JSON substitution`);
    }
  }
  for (const key of ['GW_HOST', 'S3_HOST']) {
    const host = value(key);
    if (!/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(host)) {
      throw new Error(`${directory}: invalid ${key} ${host}`);
    }
  }
  const s3Url = new URL(value('S3_ENDPOINT'));
  if (!['http:', 'https:'].includes(s3Url.protocol)) {
    throw new Error(`${directory}: S3_ENDPOINT must use HTTP(S)`);
  }
}
NODE
echo "    OK  topology identifiers and labels"

for c in "${CLUSTERS[@]}"; do
  kb "kubernetes/clusters/$c" >/dev/null && echo "    OK  kubernetes/clusters/$c"
  kb "kubernetes/apps/$c" >/dev/null && echo "    OK  kubernetes/apps/$c"
done
# Dev-mirror HelmRepository sources (consumed by Tilt + the dev cluster-repos
# Kustomization; kept validated too).
kb kubernetes/apps/dev/local/repos >/dev/null && echo "    OK  kubernetes/apps/dev/local/repos"

echo "==> prod guardrails (no dev IdP behind the primary app set)"
# Prod cluster-settings carry a placeholder dev OIDC issuer until the real IdP
# exists; make enabling roles/primary against it mechanically impossible.
# Staging legitimately uses the dev issuer — only prod overlays are checked.
for kz in kubernetes/apps/prod/*/kustomization.yaml; do
  region_dir=$(basename "$(dirname "$kz")")
  settings="kubernetes/clusters/prod/$region_dir/cluster-settings.yaml"
  # primary role enabled = an UNcommented components entry referencing roles/primary
  if grep -Eq '^[[:space:]]*-[[:space:]].*components/roles/primary' "$kz"; then
    if grep -Eq '^[[:space:]]*OIDC_ISSUER:.*(external-dev-|mock-oidc)' "$settings"; then
      echo "FAIL: prod/$region_dir enables roles/primary but $settings still points at a dev OIDC issuer" >&2
      exit 1
    fi
  fi
done
echo "    OK  prod OIDC guard"

echo "==> flux-local build (full tree, helm rendering as Flux would)"
# Via uvx so uv supplies a Python matching flux-local's requires-python
# (>=3.13) regardless of the host. One retry: flux-local fans out `flux build
# ks` subprocesses which very rarely segfault under load.
flux_local() { uvx --from "flux-local==8.2.0" flux-local "$@"; }
# Per-cluster path: clusters reuse the same Kustomization names (cluster-apps,
# flux-system ns), so flux-local must scope to one cluster at a time.
for c in "${CLUSTERS[@]}"; do
  flux_local build all "kubernetes/clusters/$c" --enable-helm --no-enable-dns >/dev/null \
    || flux_local build all "kubernetes/clusters/$c" --enable-helm --no-enable-dns >/dev/null
  echo "    OK  flux-local ($c)"
done

echo "k8s surface: ALL VALID"
