#!/usr/bin/env bash
#
# A wizard — walks a human through a manual procedure step by step.
# Generated by the /wizard skill.
#
# Everything above the "STAGES" marker is the wizard library: do not hand-edit
# it. Author the per-step stages below the marker.

set -euo pipefail

# ──────────────────────────────────────────────────────────────────────────
# Wizard library — delightful, consistent UX. Identical across every wizard.
# ──────────────────────────────────────────────────────────────────────────

if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then
  BOLD=$(tput bold); DIM=$(tput dim); RESET=$(tput sgr0)
  BLUE=$(tput setaf 4); GREEN=$(tput setaf 2); YELLOW=$(tput setaf 3); RED=$(tput setaf 1)
else
  BOLD=""; DIM=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED=""
fi

# Author sets this at the top of the stages section.
TOTAL_STAGES=0

_STAGE_INDEX=0
ENV_FILE="${ENV_FILE:-.env}"
WRITTEN_ENV=()    # KEYs written to ENV_FILE this run
WRITTEN_SECRET=() # secret NAMEs set this run
SKIPPED=()        # things we couldn't do (e.g. gh missing)

# _clear — wipe the terminal so only the current step is on screen. No-op when
# output isn't a terminal, so piped logs stay readable.
_clear() {
  [[ -t 1 ]] || return 0
  if command -v tput >/dev/null 2>&1; then tput clear; else printf '\033[2J\033[3J\033[H'; fi
}

# banner "Title" — opening frame: what this wizard does.
banner() {
  _clear
  printf '\n%s%s  %s%s\n' "$BOLD" "$BLUE" "$1" "$RESET"
  printf '%s  %s stages%s\n\n' "$DIM" "$TOTAL_STAGES" "$RESET"
  printf '%s  You drive the browser; this wizard tells you exactly what to do and\n' "$DIM"
  printf '  captures the values you copy back. Stop any time with Ctrl-C and re-run\n'
  printf '  later — it remembers values already saved.%s\n' "$RESET"
  pause "Ready to start?"
}

# stage "Name" — clear the screen, then announce a stage and show progress.
# Clearing keeps only the current step on screen.
stage() {
  _clear
  _STAGE_INDEX=$((_STAGE_INDEX + 1))
  printf '\n%s%s▸ Stage %s/%s · %s%s\n' \
    "$BOLD" "$BLUE" "$_STAGE_INDEX" "$TOTAL_STAGES" "$1" "$RESET"
}

# say "..." — a plain instruction line.
say()  { printf '  %s\n' "$1"; }
# step "..." — a numbered-feeling action the human takes in the browser.
step() { printf '  %s•%s %s\n' "$BLUE" "$RESET" "$1"; }
note() { printf '  %s%s%s\n' "$DIM" "$1" "$RESET"; }
warn() { printf '  %s⚠ %s%s\n' "$YELLOW" "$1" "$RESET"; }

# open_url URL — open in the human's browser, cross-platform incl. WSL.
open_url() {
  local url="$1"
  printf '  %s↗ opening%s %s\n' "$GREEN" "$RESET" "$url"
  { if   command -v wslview     >/dev/null 2>&1; then wslview "$url"
    elif command -v explorer.exe >/dev/null 2>&1; then explorer.exe "$url"
    elif command -v xdg-open    >/dev/null 2>&1; then xdg-open "$url"
    elif command -v open        >/dev/null 2>&1; then open "$url"
    else warn "couldn't open a browser — visit it manually: $url"; fi
  } >/dev/null 2>&1 || warn "couldn't open a browser — visit it manually: $url"
}

# pause "msg" — wait for the human to confirm they've done the manual part.
pause() {
  printf '  %s%s%s ' "$DIM" "${1:-Press Enter to continue}" "$RESET"
  read -r _ || true
}

# confirm "question" — y/N gate; returns success on yes.
confirm() {
  local reply=""
  printf '  %s? %s [y/N] ' "$YELLOW" "$1"
  read -r reply || true
  [[ "$reply" =~ ^[Yy] ]]
}

# _existing KEY — current value of KEY in ENV_FILE, if any.
_existing() {
  [[ -f "$ENV_FILE" ]] || return 1
  local line; line=$(grep -E "^${1}=" "$ENV_FILE" | tail -n1) || return 1
  printf '%s' "${line#*=}"
}

# ask KEY "Prompt" — read a value into $KEY. Offers the existing .env value as
# a default on re-runs (Enter keeps it). Visible input (non-secret).
ask() {
  local key="$1" prompt="$2" current input
  current=$(_existing "$key" || true)
  if [[ -n "$current" ]]; then
    printf '  %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET"
  else
    printf '  %s%s%s ' "$BOLD" "$prompt" "$RESET"
  fi
  read -r input || true
  [[ -z "$input" && -n "$current" ]] && input="$current"
  printf -v "$key" '%s' "$input"
}

# ask_secret KEY "Prompt" — like ask, but input is hidden.
ask_secret() {
  local key="$1" prompt="$2" current input
  current=$(_existing "$key" || true)
  if [[ -n "$current" ]]; then
    printf '  %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET"
  else
    printf '  %s%s%s ' "$BOLD" "$prompt" "$RESET"
  fi
  read -rs input || true
  printf '\n'
  [[ -z "$input" && -n "$current" ]] && input="$current"
  printf -v "$key" '%s' "$input"
}

# write_env KEY VALUE — upsert KEY=VALUE into ENV_FILE (creates it; replaces
# any existing line). Idempotent.
write_env() {
  local key="$1" value="$2" tmp
  touch "$ENV_FILE"
  tmp=$(mktemp)
  grep -vE "^${key}=" "$ENV_FILE" > "$tmp" || true
  printf '%s=%s\n' "$key" "$value" >> "$tmp"
  mv "$tmp" "$ENV_FILE"
  WRITTEN_ENV+=("$key")
  printf '  %s✓ wrote%s %s → %s\n' "$GREEN" "$RESET" "$key" "$ENV_FILE"
}

# set_secret NAME VALUE — set a GitHub Actions repo secret via gh. Falls back
# to a warning (and records it) if gh is unavailable or unauthenticated.
set_secret() {
  local name="$1" value="$2"
  if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
    if printf '%s' "$value" | gh secret set "$name" >/dev/null 2>&1; then
      WRITTEN_SECRET+=("$name")
      printf '  %s✓ set%s GitHub secret %s\n' "$GREEN" "$RESET" "$name"
      return
    fi
  fi
  SKIPPED+=("GitHub secret $name (set it manually: gh secret set $name)")
  warn "skipped GitHub secret $name — gh not ready; set it later"
}

# set_var NAME VALUE — set a GitHub Actions repo variable (non-secret).
set_var() {
  local name="$1" value="$2"
  if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
    if gh variable set "$name" --body "$value" >/dev/null 2>&1; then
      printf '  %s✓ set%s GitHub variable %s\n' "$GREEN" "$RESET" "$name"
      return
    fi
  fi
  SKIPPED+=("GitHub variable $name")
  warn "skipped GitHub variable $name — gh not ready; set it later"
}

# finish — clear, then a closing summary of everything configured.
finish() {
  _clear
  printf '\n%s%s  ✓ Setup complete%s\n' "$BOLD" "$GREEN" "$RESET"
  (( ${#WRITTEN_ENV[@]} ))    && note "wrote ${#WRITTEN_ENV[@]} value(s) to $ENV_FILE: ${WRITTEN_ENV[*]}"
  (( ${#WRITTEN_SECRET[@]} )) && note "set ${#WRITTEN_SECRET[@]} GitHub secret(s): ${WRITTEN_SECRET[*]}"
  if (( ${#SKIPPED[@]} )); then
    printf '\n'; warn "still to do by hand:"
    for s in "${SKIPPED[@]}"; do note "  - $s"; done
  fi
  printf '\n'
}

# ──────────────────────────────────────────────────────────────────────────
# STAGES — bring the punktfunk Nix binary cache at https://nix.unom.io live.
#
# Everything here is a step only a human can take: merging an infra PR, running an apply,
# dispatching a deploy. The wizard opens each page, says exactly what to do, and then
# VERIFIES the result before moving on — the failure signatures are easy to confuse:
#
#   TLS handshake failure  -> the vhost is not applied (Caddy has no cert for that name)
#   502 / 503              -> vhost fine, the container behind :3250 is not running
#   404                    -> healthy, the cache is simply empty
#   200                    -> serving content
#
# Safe to re-run: every stage detects work already done and skips it.
# Full context: packaging/nix/README.md § "Cache infrastructure (maintainers)".
# ──────────────────────────────────────────────────────────────────────────

TOTAL_STAGES=4

CACHE_HOST="nix.unom.io"
CACHE_PORT=3250
KEY_NAME="punktfunk-cache-1"
GITEA="https://git.unom.io"
GITEA_REPO="$GITEA/unom/punktfunk"
INFRA_REPO="$GITEA/unom/infra"

# probe PATH — HTTP status for a path on the cache, or 000 if it cannot be reached at all.
probe() { curl -sS -o /dev/null -w '%{http_code}' -m 15 "https://$CACHE_HOST$1" 2>/dev/null || echo 000; }

# nix_key ARGS… — run `nix key …` from a local nix if there is one, otherwise from the
# official image. Keeps this usable on a machine with no nix (the maintainer box is macOS).
nix_key() {
  if command -v nix >/dev/null 2>&1; then
    nix --extra-experimental-features nix-command key "$@"
  elif command -v docker >/dev/null 2>&1; then
    docker run --rm -i nixos/nix nix --extra-experimental-features nix-command key "$@"
  else
    return 127
  fi
}

banner "punktfunk Nix binary cache — bring-up"

# ── 1 ─────────────────────────────────────────────────────────────────────
stage "Ingress — DNS + the Caddy vhost (unom/infra)"

say "Both halves live in unom/infra and must move together: terraform/cloudflare/records.tf"
say "owns the DNS record, caddy/Caddyfile owns the vhost. That file's own rule:"
note "  \"a name here with no vhost 404s, a vhost with no name here never cuts over.\""
printf '\n'
warn "Neither is a click."
note "  A record added in the Cloudflare dashboard is out-of-band and risks the duplicate-record"
note "  round-robin records.tf documents. ~/caddy/Caddyfile on unom-1 looks like the config but is"
note "  a copy deploy-all.sh rsyncs from the repo — a vhost added there lasts until the next deploy."
printf '\n'

TARGET_IP="$(dig +short flatpak.unom.io | tail -n1)"
[ -n "$TARGET_IP" ] || TARGET_IP="167.233.145.172"
CURRENT="$(dig +short "$CACHE_HOST" | tail -n1)"

if [ -n "$CURRENT" ]; then
  printf '  %s✓%s %s already resolves to %s\n' "$GREEN" "$RESET" "$CACHE_HOST" "$CURRENT"
  [ "$CURRENT" = "$TARGET_IP" ] || warn "expected $TARGET_IP (where flatpak.unom.io points) — check for a stale duplicate record"
else
  open_url "$INFRA_REPO/pulls"
  step "Merge the 'Serve nix.unom.io' PR (adds \"nix\" to local.hostnames + the vhost)."
  step "Run dns-cutover.yml with target=hcloud, action=plan."
  step "The plan must show exactly ONE added record: cloudflare_record.a[\"nix\"]."
  warn "If it shows anything else, stop — that zone config is shared with every unom site."
  step "Re-run it with action=apply."
  step "Then run deploy-all so the box picks up the new Caddyfile."
  pause "Applied? Press Enter to verify DNS"

  i=0
  while [ "$i" -lt 10 ]; do
    CURRENT="$(dig +short "$CACHE_HOST" | tail -n1)"
    [ -n "$CURRENT" ] && break
    printf '  %swaiting for DNS (TTL is 300s)…%s\n' "$DIM" "$RESET"
    sleep 10
    i=$((i + 1))
  done
  if [ -n "$CURRENT" ]; then
    printf '  %s✓%s %s -> %s\n' "$GREEN" "$RESET" "$CACHE_HOST" "$CURRENT"
  else
    warn "$CACHE_HOST still does not resolve."
    SKIPPED+=("DNS record for $CACHE_HOST (unom/infra records.tf + dns-cutover apply)")
    confirm "Continue anyway?" || exit 1
  fi
fi

# The certificate is the proof the vhost half landed. Diagnose by SNI: Caddy 308s EVERY Host
# on :80 to https, including names it has never heard of, so probing port 80 proves nothing.
printf '  %schecking for a certificate…%s\n' "$DIM" "$RESET"
TLS_OUT="$(openssl s_client -connect "${CACHE_HOST}:443" -servername "$CACHE_HOST" \
            </dev/null 2>&1 | grep -E '^subject=|alert' | head -n3 || true)"
if printf '%s' "$TLS_OUT" | grep -q '^subject='; then
  printf '  %s✓%s Caddy is serving a certificate for %s\n' "$GREEN" "$RESET" "$CACHE_HOST"
else
  warn "No certificate for $CACHE_HOST yet:"
  printf '      %s%s%s\n' "$DIM" "${TLS_OUT:-(no response)}" "$RESET"
  note "  Caddy issues one automatically once the name resolves AND the vhost is deployed."
  note "  If DNS is good, the Caddyfile half has not reached the box — re-run deploy-all."
  SKIPPED+=("Caddy vhost for $CACHE_HOST")
  confirm "Continue anyway?" || exit 1
fi

# ── 2 ─────────────────────────────────────────────────────────────────────
stage "Start the cache container on unom-1"

CODE="$(probe /nix-cache-info)"
if [ "$CODE" = 404 ] || [ "$CODE" = 200 ]; then
  printf '  %s✓%s Container already answering (HTTP %s)\n' "$GREEN" "$RESET" "$CODE"
else
  say "deploy-services.yml ships the compose file + Caddyfile + prune.sh and starts the"
  say "container on port $CACHE_PORT. It serves an EMPTY cache until the first publish."
  open_url "$GITEA_REPO/actions?workflow=deploy-services.yml"
  step "Run workflow -> leave the input blank -> Run."
  step "Wait for the nix-cache job to go green."
  pause "Green? Press Enter to verify"

  CODE="$(probe /nix-cache-info)"
  case "$CODE" in
    404) printf '  %s✓%s Up and empty — 404 on every path, exactly right for an empty cache\n' "$GREEN" "$RESET" ;;
    200) printf '  %s✓%s Up and already holding content\n' "$GREEN" "$RESET" ;;
    502|503)
      warn "Caddy answered $CODE — the vhost is live but nothing is listening on :$CACHE_PORT."
      note "  Check the nix-cache job, or docker compose ps on unom-1."
      SKIPPED+=("cache container on unom-1:$CACHE_PORT")
      confirm "Continue anyway?" || exit 1 ;;
    *)
      warn "Unexpected response ($CODE) from https://$CACHE_HOST/nix-cache-info"
      SKIPPED+=("cache container on unom-1:$CACHE_PORT")
      confirm "Continue anyway?" || exit 1 ;;
  esac
fi

# ── 3 ─────────────────────────────────────────────────────────────────────
stage "Signing key"

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
README_MD="$REPO_ROOT/packaging/nix/README.md"
INSTALL_MD="$REPO_ROOT/docs-site/content/docs/install.md"

if grep -q "$KEY_NAME:<" "$README_MD" 2>/dev/null; then
  say "The docs still carry a placeholder, so no key has been installed yet."
  say "Generating an ed25519 key pair…"
  SECRET_KEY="$(nix_key generate-secret --key-name "$KEY_NAME" 2>/dev/null || true)"
  if [ -z "$SECRET_KEY" ]; then
    warn "Could not run nix here (no local nix, and no docker to fall back to)."
    note "  nix key generate-secret --key-name $KEY_NAME"
    ask_secret SECRET_KEY "Paste the secret key line:"
  fi

  if [ -z "$SECRET_KEY" ]; then
    SKIPPED+=("NIX_CACHE_SIGNING_KEY + the public key in the docs")
  else
    PUBLIC_KEY="$(printf '%s' "$SECRET_KEY" | nix_key convert-secret-to-public 2>/dev/null || true)"
    printf '\n  %sSecret key — paste into Gitea now; nothing here keeps a copy:%s\n\n' "$BOLD" "$RESET"
    printf '      %s\n\n' "$SECRET_KEY"
    open_url "$GITEA_REPO/settings/actions/secrets"
    step "Add Secret -> Name: NIX_CACHE_SIGNING_KEY"
    step "Value: the whole line above, including the '$KEY_NAME:' prefix."
    pause "Stored? Press Enter"
    WRITTEN_SECRET+=("NIX_CACHE_SIGNING_KEY (Gitea)")
    SECRET_KEY=""

    if [ -n "$PUBLIC_KEY" ]; then
      printf '\n  %sPublic key%s — what users pin:\n\n      %s\n\n' "$BOLD" "$RESET" "$PUBLIC_KEY"
      for f in "$README_MD" "$INSTALL_MD"; do
        [ -f "$f" ] || continue
        tmp="$(mktemp)"
        sed "s|${KEY_NAME}:<[^>]*>|${PUBLIC_KEY}|g" "$f" > "$tmp" && mv "$tmp" "$f"
        printf '  %s✓ pinned in%s %s\n' "$GREEN" "$RESET" "${f#"$REPO_ROOT"/}"
      done
      say "Commit those two files — without the key nobody can trust the cache."
    fi
  fi
else
  PUBLIC_KEY="$(grep -om1 "$KEY_NAME:[A-Za-z0-9+/=]*" "$README_MD" 2>/dev/null || true)"
  printf '  %s✓%s A key is already installed and pinned in the docs\n' "$GREEN" "$RESET"
  [ -n "$PUBLIC_KEY" ] && printf '      %s\n' "$PUBLIC_KEY"
  printf '\n'
  warn "Do not regenerate it casually."
  note "  A new key invalidates every signature already published, and every user pinning the"
  note "  old one starts failing. Rotating means updating the docs and telling users."
  pause "Press Enter for the last stage"
fi

# ── 4 ─────────────────────────────────────────────────────────────────────
stage "Publish — land the flake on main and verify"

say "The publish tier runs on a push to main touching the flake, Cargo.*, or packaging/nix."
printf '\n'
note "  It builds the whole Rust workspace AND gamescope inside the nix sandbox — sccache"
note "  cannot reach in there, so budget roughly an hour for the first run."
note "  If it reddens on 'Build the bun packages', that is the known intermittent OOM"
note "  (exit 137) rather than a real break — re-run the job."
printf '\n'
open_url "$GITEA_REPO/actions?workflow=nix.yml"
step "Merge any outstanding cache PR, or push a flake-touching commit to main."
step "Watch the nix workflow's 'Sign + publish to nix.unom.io' step."
pause "Published? Press Enter to verify the live cache"

CODE="$(probe /nix-cache-info)"
if [ "$CODE" = 200 ]; then
  printf '  %s✓%s nix-cache-info is being served\n' "$GREEN" "$RESET"
  LIVE_PUB="$(curl -sS -m 15 "https://$CACHE_HOST/punktfunk-cache.pub" 2>/dev/null || true)"
  if [ -n "$LIVE_PUB" ]; then
    printf '  %s✓%s published key: %s\n' "$GREEN" "$RESET" "$LIVE_PUB"
    if [ -n "${PUBLIC_KEY:-}" ] && [ "$LIVE_PUB" != "$PUBLIC_KEY" ]; then
      warn "That does NOT match the key pinned in the docs:"
      note "    docs:  ${PUBLIC_KEY}"
      note "    cache: ${LIVE_PUB}"
      note "  Users following the docs would reject everything this cache serves."
      SKIPPED+=("public key mismatch between the docs and $CACHE_HOST")
    fi
  fi
  # The one failure mode that breaks USERS rather than us: nix reads any non-404 as a hard
  # error, not as a cache miss, so a miss MUST 404.
  MISS="$(probe /0000000000000000000000000000000000.narinfo)"
  if [ "$MISS" = 404 ]; then
    printf '  %s✓%s a miss returns 404 — nix falls through to cache.nixos.org correctly\n' "$GREEN" "$RESET"
  else
    warn "a miss returns $MISS, not 404 — every user build would fail on any package this"
    warn "cache does not hold. Check for a proxy or auth layer in front of Caddy."
    SKIPPED+=("404-on-miss behaviour at $CACHE_HOST")
  fi
else
  warn "https://$CACHE_HOST/nix-cache-info returned $CODE — nothing published yet."
  SKIPPED+=("first publish to $CACHE_HOST")
fi

finish

printf '  %sUsers now add, on NixOS:%s\n\n' "$BOLD" "$RESET"
printf '      nix.settings = {\n'
printf '        substituters = [ "https://%s" ];\n' "$CACHE_HOST"
printf '        trusted-public-keys = [ "%s" ];\n' "${PUBLIC_KEY:-$KEY_NAME:…}"
printf '      };\n\n'
note "  Full instructions: packaging/nix/README.md § Binary cache"
printf '\n'
