#!/usr/bin/env bash
# aptly-init — runs once as an initContainer (Helm) or a one-shot service (Compose).
# Produces everything the aptly and nginx containers consume, so neither of them
# has to guess about start-order or generate secrets themselves:
#
#   /run/aptly/aptly.yaml       rendered aptly config (env placeholders resolved)
#   /run/aptly/htpasswd         nginx basic-auth file (hashed here, never in a template)
#   /run/aptly/gnupg/           GNUPGHOME, imported signing key + trusted keys
#   /run/aptly/signing.json     the one source of truth for "how do I sign a publish"
#   /run/aptly/pub/signing-key.asc   armored public key, served by nginx
#
# Inputs (all optional unless noted):
#   APTLY_CONFIG_SRC        path to the un-interpolated aptly config (required)
#   APTLY_CONFIG_DST        path to write the rendered config (required)
#   APTLY_USERS_FILE        plaintext "user:pass" lines, one per line, to hash into htpasswd
#   APTLY_HTPASSWD_SRC      pre-hashed htpasswd file to use as-is (wins over APTLY_USERS_FILE)
#   APTLY_INTERNAL_USER     username always appended to htpasswd (used by the reconcile job)
#   APTLY_INTERNAL_PASSWORD password for APTLY_INTERNAL_USER
#   APTLY_GPG_PRIVATE_KEY_FILE   armored private key
#   APTLY_GPG_SECRET_KEYRING_FILE  binary secring.gpg (alternative to the above)
#   APTLY_GPG_PASSPHRASE_FILE    file containing the passphrase, if any
#   APTLY_GPG_PUBLIC_KEY_FILE    armored public key (derived from the private key if absent)
#   APTLY_GPG_KEYS_DIR       directory of extra *.asc files to import as trusted (mirror) keys
#   APTLY_GPG_ENABLED       "true"/"false" — false disables signing entirely (see below)
#   APTLY_RUN_DIR           defaults to /run/aptly
set -euo pipefail

# Deliberately does NOT source lib/common.sh: that helper requires APTLY_URL
# (it's built for the REST-API scripts), and aptly-init makes no API calls —
# coupling it to that requirement would make aptly-init fail outside a
# context that sets APTLY_URL, which is wrong for an initContainer.
log()  { printf '[aptly-init] %s\n' "$*" >&2; }
die()  { log "ERROR: $*"; exit 1; }
warn() { log "WARN: $*"; }

RUN_DIR="${APTLY_RUN_DIR:-/run/aptly}"
GNUPGHOME="${RUN_DIR}/gnupg"
mkdir -p "${RUN_DIR}" "${RUN_DIR}/pub"
install -d -m 0700 "${GNUPGHOME}"
export GNUPGHOME

# Ensure the publish directory exists before nginx ever tries to mount it.
# On Kubernetes, nginx's subPath mount of data/public happens at container
# creation, after this initContainer completes but with no other guarantee
# the directory has been created yet on a fresh, empty volume — aptly itself
# only creates it lazily on first publish.
if [[ -n "${APTLY_ROOT_DIR:-}" ]]; then
  mkdir -p "${APTLY_ROOT_DIR}/public"
fi

# ---------------------------------------------------------------------------
# 1. Render the aptly config: resolve ${VAR} placeholders against the process
#    environment. Bare `envsubst` (no argument) blanks every variable it does
#    not know about, which would silently wreck a GPG passphrase containing a
#    literal "$". We restrict substitution to a computed SHELL-FORMAT list of
#    currently-exported vars instead.
# ---------------------------------------------------------------------------
: "${APTLY_CONFIG_SRC:?APTLY_CONFIG_SRC must point at the source aptly config}"
: "${APTLY_CONFIG_DST:?APTLY_CONFIG_DST must point at the rendered output path}"

mkdir -p "$(dirname -- "$APTLY_CONFIG_DST")"
shell_format="$(env | cut -d= -f1 | grep -E '^[A-Za-z_][A-Za-z0-9_]*$' | sed 's/^/${/;s/$/}/' | tr '\n' ' ')"
envsubst "${shell_format}" < "$APTLY_CONFIG_SRC" > "$APTLY_CONFIG_DST"
chmod 0640 "$APTLY_CONFIG_DST"

# Strip comment lines first: a source config documenting its own ${VAR}
# syntax in a comment would otherwise trip this check on itself.
# shellcheck disable=SC2016  # single quotes are deliberate: this is a regex, not shell expansion
if unresolved="$(grep -v '^[[:space:]]*#' "$APTLY_CONFIG_DST" \
    | grep -o '\${[A-Za-z_][A-Za-z0-9_]*}' | sort -u)"; then
  while IFS= read -r v; do
    [[ -n "$v" ]] && log "WARN: unresolved placeholder ${v} left in rendered config"
  done <<< "$unresolved"
fi
log "rendered config -> ${APTLY_CONFIG_DST}"

# ---------------------------------------------------------------------------
# 2. htpasswd. Hashing happens here, not in a Helm template: a template using
#    sprig's htpasswd would pick a new random bcrypt salt on every render,
#    changing the Secret on every `helm upgrade` and restart-looping the pod
#    via the checksum/secret annotation. The Secret therefore carries
#    plaintext "user:pass" lines (or a ready-made htpasswd, for the
#    ExternalSecrets path) and this script does the one-time hashing.
# ---------------------------------------------------------------------------
HTPASSWD_OUT="${RUN_DIR}/htpasswd"
: > "$HTPASSWD_OUT"

if [[ -n "${APTLY_HTPASSWD_SRC:-}" && -s "${APTLY_HTPASSWD_SRC}" ]]; then
  cat "${APTLY_HTPASSWD_SRC}" >> "$HTPASSWD_OUT"
  log "using pre-hashed htpasswd from ${APTLY_HTPASSWD_SRC}"
elif [[ -n "${APTLY_USERS_FILE:-}" && -s "${APTLY_USERS_FILE}" ]]; then
  while IFS=: read -r user pass; do
    [[ -z "$user" || "$user" == \#* || -z "${pass:-}" ]] && continue
    printf '%s:%s\n' "$user" "$(openssl passwd -apr1 -- "$pass")" >> "$HTPASSWD_OUT"
  done < "${APTLY_USERS_FILE}"
  log "hashed $(wc -l < "$HTPASSWD_OUT") user(s) from ${APTLY_USERS_FILE}"
fi

if [[ -n "${APTLY_INTERNAL_USER:-}" && -n "${APTLY_INTERNAL_PASSWORD:-}" ]]; then
  printf '%s:%s\n' "$APTLY_INTERNAL_USER" "$(openssl passwd -apr1 -- "$APTLY_INTERNAL_PASSWORD")" >> "$HTPASSWD_OUT"
  log "appended internal user '${APTLY_INTERNAL_USER}' (used by the reconcile job / cron)"
fi
# World-readable, not 0640: the nginx container reads this file as its own
# UID (101 upstream, or whatever securityContext.runAsUser is set to in the
# Helm chart), which has no relation to the aptly UID that wrote it. In
# Kubernetes, pod-level fsGroup would put both UIDs in a shared supplementary
# group instead — Compose has no equivalent, so this file has to be
# world-readable. It contains only apr1-hashed passwords, not plaintext.
chmod 0644 "$HTPASSWD_OUT"

# ---------------------------------------------------------------------------
# 3. GPG. GNUPGHOME lives on an in-memory emptyDir: the private key never
#    touches a PersistentVolume, and the keyring is re-derived from Secrets
#    on every start, so key rotation is just "restart the pod".
# ---------------------------------------------------------------------------
gpg_enabled="${APTLY_GPG_ENABLED:-true}"
signing_json="${RUN_DIR}/signing.json"

if [[ "$gpg_enabled" != "true" ]]; then
  printf '{"skip": true}\n' > "$signing_json"
  log "signing disabled (APTLY_GPG_ENABLED=false) -> ${signing_json}"
else
  imported_any=false

  if [[ -n "${APTLY_GPG_PRIVATE_KEY_FILE:-}" && -s "${APTLY_GPG_PRIVATE_KEY_FILE}" ]]; then
    gpg --batch --import "${APTLY_GPG_PRIVATE_KEY_FILE}" 2>&1 | while read -r l; do log "gpg: $l"; done || true
    imported_any=true
  fi
  if [[ -n "${APTLY_GPG_SECRET_KEYRING_FILE:-}" && -s "${APTLY_GPG_SECRET_KEYRING_FILE}" ]]; then
    gpg --batch --import "${APTLY_GPG_SECRET_KEYRING_FILE}" 2>&1 | while read -r l; do log "gpg: $l"; done || true
    imported_any=true
  fi

  if [[ "$imported_any" != "true" ]]; then
    warn_msg="APTLY_GPG_ENABLED=true but no private key was provided"
    log "WARN: ${warn_msg} — publishing will fail signature checks unless aptly.gpg.enabled is also false"
    printf '{"skip": true}\n' > "$signing_json"
  else
    # Auto-detect the key id so nothing downstream has to guess it.
    key_id="$(gpg --batch --list-secret-keys --with-colons 2>/dev/null | awk -F: '$1=="sec"{print $5; exit}')"
    [[ -z "$key_id" ]] && die "GPG key(s) imported but no secret key id could be detected"

    passphrase_file=""
    if [[ -n "${APTLY_GPG_PASSPHRASE_FILE:-}" && -s "${APTLY_GPG_PASSPHRASE_FILE}" ]]; then
      passphrase_file="${APTLY_GPG_PASSPHRASE_FILE}"
    fi

    jq -n --arg keyId "$key_id" --arg keyring "" --arg secretKeyring "" \
      --arg passphraseFile "$passphrase_file" \
      '{skip: false, batch: true, gpgKey: $keyId} + (if $passphraseFile != "" then {passphraseFile: $passphraseFile} else {} end)' \
      > "$signing_json"
    log "signing key detected: ${key_id} -> ${signing_json}"

    # Export the public key so nginx can serve it, and clients can `signed-by=` it.
    pub_out="${RUN_DIR}/pub/signing-key.asc"
    if [[ -n "${APTLY_GPG_PUBLIC_KEY_FILE:-}" && -s "${APTLY_GPG_PUBLIC_KEY_FILE}" ]]; then
      cp "${APTLY_GPG_PUBLIC_KEY_FILE}" "$pub_out"
    else
      gpg --batch --armor --export "$key_id" > "$pub_out"
    fi
    log "public key exported -> ${pub_out}"
  fi

  # Trusted keys for mirror verification (aptly.gpgKeys[] in values / mirrors.gpgKeys in state.yaml)
  if [[ -n "${APTLY_GPG_KEYS_DIR:-}" && -d "${APTLY_GPG_KEYS_DIR}" ]]; then
    shopt -s nullglob
    for f in "${APTLY_GPG_KEYS_DIR}"/*.asc "${APTLY_GPG_KEYS_DIR}"/*.gpg; do
      [[ -e "$f" ]] || continue
      if gpg --batch --import "$f" 2>&1 | while read -r l; do log "gpg: $l"; done; then
        log "imported trusted key from ${f}"
      else
        warn "failed to import trusted key ${f} (continuing — mirrors referencing it will fail signature checks)"
      fi
    done
    shopt -u nullglob
  fi
fi

chmod -R go-rwx "${GNUPGHOME}" 2>/dev/null || true
log "done"
