#!/usr/bin/env bash
# Clusev image-source resolver — sourced by install.sh. Defines functions only; sourcing has no
# side effects. Chooses whether to PULL a digest-pinned promoted image or BUILD locally, and ALWAYS
# leaves a coherent pin in CLUSEV_IMAGE / CLUSEV_TERMINAL_IMAGE / CLUSEV_PULL so a stale digest from
# a prior pull can never leak into a later `docker compose build` (which rejects a digest as a tag):
#   env   — CLUSEV_IMAGE already set (staging/CI passed it) → respect it, pull.
#   pull  — a public release-images.lock names ghcr.io digest refs → pull exactly those digests.
#   build — no lock (dev/source tree) or an unusable lock → pin the local default build tags.
# The chosen mode is exposed via the exported global CLUSEV_RELEASE_MODE (read by the caller across
# the sourcing boundary); the caller persists the pin to .env. No file writes here (unit-testable).

# clusev_json_str FILE KEY -> value of a top-level "KEY": "VALUE" string. jq if present, else sed.
# The lock is our own single-object JSON, so the sed fallback is sufficient + dependency-free.
clusev_json_str() {
  local file="$1" key="$2"
  if command -v jq >/dev/null 2>&1; then
    jq -er --arg k "$key" '.[$k] // empty' "$file" 2>/dev/null || true
  else
    sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$file" 2>/dev/null | head -n1
  fi
}

# _clusev_ref_ok REF -> true ONLY for a fully-formed ghcr.io digest ref:
# ghcr.io/<owner>/<name>@sha256:<64 lowercase hex>, where each of <owner>/<name> starts with an
# alphanumeric and otherwise contains only [A-Za-z0-9._-] — so no slash, whitespace, newline, "=" or
# any other shell/dotenv metacharacter can appear ANYWHERE in the ref. Anything looser — a bare tag, a
# non-ghcr host, a short/non-hex digest, an extra or relative path segment (…/../…), or an embedded
# newline in ANY segment — is rejected so the caller falls back to a local build instead of a doomed
# or attacker-directed pull, and no malformed ref can reach force_kv to corrupt .env. The promote CI
# always emits exactly this shape, so a rejected ref means a corrupt/tampered lock.
_clusev_ref_ok() { [[ "$1" =~ ^ghcr\.io/[a-zA-Z0-9][a-zA-Z0-9._-]*/[a-zA-Z0-9][a-zA-Z0-9._-]*@sha256:[0-9a-f]{64}$ ]]; }

# clusev_resolve_release_images [LOCK] -> sets + exports CLUSEV_IMAGE, CLUSEV_TERMINAL_IMAGE,
# CLUSEV_PULL and CLUSEV_RELEASE_MODE (env|pull|build). Always leaves a coherent pin.
clusev_resolve_release_images() {
  local lock="${1:-release-images.lock}" mode appref termref
  # Local build tags — must match the ${CLUSEV_IMAGE:-…}/${CLUSEV_TERMINAL_IMAGE:-…} defaults in
  # docker-compose.prod.yml. Pinned on a build so `docker compose build` never inherits a digest.
  local app_default="clusev-app:prod" term_default="clusev-terminal:prod"
  if [ -n "${CLUSEV_IMAGE:-}" ]; then
    # staging/CI already pinned the image via the environment — respect it, never override.
    export CLUSEV_TERMINAL_IMAGE="${CLUSEV_TERMINAL_IMAGE:-$term_default}"
    export CLUSEV_PULL="${CLUSEV_PULL:-1}"
    # shellcheck disable=SC2209
    mode="env"
  elif [ -f "$lock" ] \
    && appref="$(clusev_json_str "$lock" app)" && _clusev_ref_ok "$appref" \
    && termref="$(clusev_json_str "$lock" terminal)" && _clusev_ref_ok "$termref"; then
    # public tree: pull exactly the promoted digests.
    export CLUSEV_IMAGE="$appref" CLUSEV_TERMINAL_IMAGE="$termref" CLUSEV_PULL=1
    mode="pull"
  else
    # dev/source tree, or an unusable/tampered lock: build the local default tags (never a digest).
    export CLUSEV_IMAGE="$app_default" CLUSEV_TERMINAL_IMAGE="$term_default" CLUSEV_PULL=0
    mode="build"
  fi
  export CLUSEV_RELEASE_MODE="$mode"
}
