#!/usr/bin/env bash
#
# scripts/release.sh — scripted release for vernonkeenan/lib (ADR-KV-014).
#
# Replaces the manual "Update Procedure" in README.md (three web UIs + a
# go-swagger CLI installed nowhere) with a single, repeatable script driven
# through `make`. See README.md's "Release Procedure" section for the
# operator-facing walkthrough.
#
# Subcommands:
#   install-swagger          Install the pinned go-swagger CLI and verify its version.
#   regen                    Regenerate api/ clients from the sibling service specs.
#   release <VERSION> [--dry-run]
#                             Validate, tag, push, publish the GitHub release, and
#                             trigger the Gitea mirror sync. With --dry-run, every
#                             step up to (but not including) push/tag/release/mirror
#                             is performed and the remaining steps are only printed.
#
# Env:
#   SWAGGER_VERSION   Pinned go-swagger version (default set by Makefile; can override).
#   GITHUB_REPO        owner/repo on GitHub (default vernonkeenan/lib).
#   GITEA_BASE_URL     Gitea API base (default https://code.tnxs.net/api/v1).
#   GITEA_TOKEN        Gitea API token with repo mirror-sync rights. If unset, the
#                       mirror-sync step is skipped with a loud warning — it is
#                       never required to fail the release. There is no
#                       Gitea/tnxs API token variable defined in this workstation's
#                       env files today (checked kv.env, taxnexus.env, telnexus.env,
#                       rosetta.env, wdwai.env); GITEA_TOKEN is a new variable name
#                       introduced by this script, not a rename of an existing one.
#   GITEA_MIRRORS       Space-separated "owner/repo" mirror list (default
#                       "vernonkeenan/lib").
#
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"

SWAGGER_VERSION="${SWAGGER_VERSION:-v0.35.0}"
GITHUB_REPO="${GITHUB_REPO:-vernonkeenan/lib}"
GITEA_BASE_URL="${GITEA_BASE_URL:-https://code.tnxs.net/api/v1}"
GITEA_MIRRORS="${GITEA_MIRRORS:-vernonkeenan/lib}"

# Sibling service repos this ritual pulls specs from, and the go-swagger
# generate flags the README's `make swagger` target used to run inline.
# Format: "dir:spec_basename:client_name:target_subdir:client_pkg:model_pkg"
SERVICES=(
  "auth:auth-vernonkeenan:auth:api/auth:auth-client:auth-models"
  "crm:crm-vernonkeenan:crm:api/crm:crm-client:crm-models"
  "stash:stash-vernonkeenan:stash:api/stash:stash-client:stash-models"
  "sf-gate:sf-gate-vernonkeenan:sfgate:api/sfgate:sfgate-client:sfgate-models"
  "research:research-vernonkeenan:research:api/research:research-client:research-models"
  "members:members-vernonkeenan:members:api/members:members-client:members-models"
  "plex:plex-vernonkeenan:plex:api/plex:plex-client:plex-models"
)

# Host/path substitutions the ritual applied to swagger/external/*.yaml,
# keyed by service dir (same mapping the Makefile hardcoded per client).
external_host_for() {
  case "$1" in
    auth) echo "auth.vernonkeenan.com:8080" ;;
    crm) echo "crm.vernonkeenan.com:8080" ;;
    stash) echo "stash.vernonkeenan.com:8080" ;;
    sf-gate) echo "sf-gate.vernonkeenan.com:8080" ;;
    research) echo "research.vernonkeenan.com:8080" ;;
    members) echo "members.vernonkeenan.com:8080" ;;
    plex) echo "plex.vernonkeenan.com:8080" ;;
  esac
}

external_path_for() {
  case "$1" in
    auth) echo "/vk/auth/v1" ;;
    crm) echo "/vk/crm/v1" ;;
    stash) echo "/vk/stash/v1" ;;
    sf-gate) echo "/vk/sf-gate/v1" ;;
    research) echo "/vk/research/v1" ;;
    members) echo "/vk/members/v1" ;;
    plex) echo "/vk/plex/v1" ;;
  esac
}

log() { printf '[release] %s\n' "$*" >&2; }
warn() { printf '\033[33m[release] WARNING: %s\033[0m\n' "$*" >&2; }
die() { printf '\033[31m[release] ERROR: %s\033[0m\n' "$*" >&2; exit 1; }

usage() {
  cat >&2 <<'EOF'
Usage:
  scripts/release.sh install-swagger
  scripts/release.sh regen
  scripts/release.sh release <VERSION> [--dry-run]

See the top of this file for env vars.
EOF
}

# ---------------------------------------------------------------------------
# install-swagger
# ---------------------------------------------------------------------------

swagger_bin() {
  local gobin
  gobin="$(go env GOBIN)"
  if [[ -z "$gobin" ]]; then
    gobin="$(go env GOPATH)/bin"
  fi
  echo "${gobin}/swagger"
}

cmd_install_swagger() {
  log "Installing go-swagger ${SWAGGER_VERSION} (pinned; see README.md and Makefile SWAGGER_VERSION)"
  GOFLAGS=-mod=mod go install "github.com/go-swagger/go-swagger/cmd/swagger@${SWAGGER_VERSION}"

  local bin
  bin="$(swagger_bin)"
  [[ -x "$bin" ]] || die "go install reported success but ${bin} is not executable"

  local got version_output
  version_output="$("$bin" version 2>&1)"
  got="${version_output%%$'\n'*}"
  log "swagger version: ${got}"
  case "$got" in
    *"${SWAGGER_VERSION#v}"*) ;;
    *) die "installed swagger version does not match pinned ${SWAGGER_VERSION} (got: ${got})" ;;
  esac
  log "go-swagger ${SWAGGER_VERSION} verified at ${bin}"
}

# ---------------------------------------------------------------------------
# regen
# ---------------------------------------------------------------------------

check_sibling_specs() {
  local missing=()
  local svc dir spec_base rest
  for svc in "${SERVICES[@]}"; do
    IFS=':' read -r dir spec_base rest <<<"$svc"
    local spec_path="${REPO_ROOT}/../${dir}/swagger/${spec_base}.yaml"
    if [[ ! -f "$spec_path" ]]; then
      missing+=("$spec_path")
    fi
  done
  if [[ ${#missing[@]} -gt 0 ]]; then
    die "missing sibling spec(s), cannot regen (need all sibling repos cloned adjacently per README prerequisite):
$(printf '  - %s\n' "${missing[@]}")"
  fi
  log "all ${#SERVICES[@]} sibling specs present"
}

cmd_regen() {
  check_sibling_specs

  local bin
  bin="$(swagger_bin)"
  [[ -x "$bin" ]] || die "swagger CLI not found at ${bin} — run 'make install-swagger' first"

  cd "$REPO_ROOT"
  mkdir -p swagger/logs

  log "copying sibling specs into ./swagger and ./swagger/external"
  local svc dir spec_base name target pkg model
  for svc in "${SERVICES[@]}"; do
    IFS=':' read -r dir spec_base name target pkg model <<<"$svc"
    cp "../${dir}/swagger/${spec_base}.yaml" "./swagger/"
    cp "../${dir}/swagger/${spec_base}.yaml" "./swagger/external/"
  done

  log "removing generated api/ tree"
  rm -rf api

  for svc in "${SERVICES[@]}"; do
    IFS=':' read -r dir spec_base name target pkg model <<<"$svc"
    log "generating ${name} client -> ${target}"
    mkdir -p "$target"
    local generation_spec="./swagger/${spec_base}.yaml"
    local -a generation_filter=()
    if [[ "$dir" == "stash" ]]; then
      # Stash's provider spec retains a deprecated, fail-closed POST so old
      # callers receive a governed 403. Lib must not turn that compatibility
      # route back into a mutation client. Generate only the two metadata
      # reads from the narrower consumer projection, which also excludes
      # document bytes and legacy ownership fields from the response model.
      generation_spec="./swagger/client/stash-pdf-metadata.yaml"
      generation_filter=(
        --operation=listPdfs
        --operation=getPdfs
        --model=Document
        --model=DocumentResponse
        --model=Error
        --model=ResponseMeta
      )
    fi
    "$bin" generate client \
      --log-output="./swagger/logs/generate-${name}-client.log" \
      --copyright-file=./build/COPYRIGHT \
      --name="${name}" \
      --spec="${generation_spec}" \
      --target="${target}" \
      --client-package="${pkg}" \
      --model-package="${model}" \
      --principal=app.User \
      "${generation_filter[@]}"

    local host path ext="./swagger/external/${spec_base}.yaml"
    host="$(external_host_for "$dir")"
    path="$(external_path_for "$dir")"
    sed -i 's|"http"|"https"|g' "$ext"
    sed -i "s|${host}|gw.tnxs.net|g" "$ext"
    sed -i "s|\"/v1\"|\"${path}\"|g" "$ext"
  done

  log "regen complete for: $(printf '%s ' "${SERVICES[@]%%:*}")"

  # Best-effort publish of the external swagger docs, exactly as the manual
  # ritual's last step did. Never fails the regen — CI/dev boxes typically
  # have no SSH access to whm.noc.tnxs.net.
  if command -v scp >/dev/null 2>&1; then
    log "attempting best-effort scp of swagger/external/*.yaml to whm.noc.tnxs.net (skip on failure)"
    if ! scp -o ConnectTimeout=5 -o BatchMode=yes \
        ./swagger/external/*.yaml root@whm.noc.tnxs.net:/home/taxassets/www/swagger/external 2>/dev/null; then
      warn "scp to whm.noc.tnxs.net failed or is unreachable from this host — skipped (not fatal)."
      warn "If external swagger docs need publishing, run the scp manually from a host with access."
    fi
  fi

  log "sanity build (non-fatal; real gate is CI on the regen PR)"
  if ! go build ./... 2>&1; then
    warn "generated code does not build. Known cause: any sibling spec operation missing a top-level"
    warn "'tags:' entry lands in a fallback 'operations' package whose import path in"
    warn "<name>_client/<name>_client.go does not respect --client-package across at least go-swagger"
    warn "v0.33.2-v0.35.0 (reproduced on all four during this tooling's own verification). As of this"
    warn "writing that's sf-gate-vernonkeenan.yaml's 'PUT /researchprojectservices' (no tags:). Fix at"
    warn "the source (add a tags: entry in the sf-gate repo's spec) rather than hand-patching generated"
    warn "code, since the next regen would just reintroduce it."
  fi

  log "review 'git status' / 'git diff' and commit the regen output as its own PR before running 'make release'."
}

# ---------------------------------------------------------------------------
# release
# ---------------------------------------------------------------------------

validate_semver() {
  local v="$1"
  [[ "$v" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || \
    die "VERSION '${v}' is not a valid semver tag (expected vMAJOR.MINOR.PATCH, e.g. v0.7.5)"
}

latest_tag() {
  git -C "$REPO_ROOT" tag --list 'v*' | sort -V | tail -1
}

check_monotonic() {
  local new="$1" latest="$2"
  [[ -n "$latest" ]] || { log "no existing v* tags found; ${new} will be the first"; return; }
  local top
  top="$(printf '%s\n%s\n' "$latest" "$new" | sort -V | tail -1)"
  if [[ "$new" == "$latest" ]]; then
    die "VERSION ${new} already exists as a tag"
  fi
  if [[ "$top" != "$new" ]]; then
    die "VERSION ${new} is not greater than latest tag ${latest} (releases must be monotonically increasing)"
  fi
  log "monotonicity OK: ${latest} -> ${new}"
}

cmd_release() {
  local version="${1:-}"
  local dry_run="false"
  shift || true
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --dry-run) dry_run="true" ;;
      *) die "unknown release argument: $1" ;;
    esac
    shift
  done

  [[ -n "$version" ]] || die "usage: scripts/release.sh release <VERSION> [--dry-run] (or: make release VERSION=v0.7.5)"
  validate_semver "$version"

  cd "$REPO_ROOT"

  log "fetching tags from origin"
  git fetch --tags --quiet origin

  local latest
  latest="$(latest_tag)"
  check_monotonic "$version" "$latest"

  local branch
  branch="$(git rev-parse --abbrev-ref HEAD)"
  [[ "$branch" == "main" ]] || die "must be on 'main' to release (currently on '${branch}'); main+tags replaced branch-per-version (ADR-KV-014)"

  if [[ -n "$(git status --porcelain)" ]]; then
    die "working tree is not clean; commit or stash changes before releasing. (Regen output belongs in its own PR, reviewed and merged to main *before* running 'make release'.)"
  fi

  git fetch origin main --quiet
  local head_sha origin_sha
  head_sha="$(git rev-parse HEAD)"
  origin_sha="$(git rev-parse origin/main)"
  if [[ "$head_sha" != "$origin_sha" ]]; then
    die "local main (${head_sha:0:12}) does not match origin/main (${origin_sha:0:12}); pull/push so the tag lands on a commit that's actually on GitHub main"
  fi

  log "sanity build/vet before tagging ${version} at ${head_sha:0:12}"
  go build ./...
  go vet ./...

  log "About to release ${version}:"
  log "  - annotated tag ${version} @ ${head_sha:0:12} on ${GITHUB_REPO}"
  log "  - push tag to origin"
  log "  - gh release create ${version} --repo ${GITHUB_REPO} --generate-notes"
  log "  - Gitea mirror-sync: ${GITEA_MIRRORS}"

  if [[ "$dry_run" == "true" ]]; then
    log "DRY RUN: stopping before tag/push/release/mirror-sync. Nothing was written."
    return 0
  fi

  log "creating annotated tag ${version}"
  git tag -a "$version" -m "Release ${version}"

  log "pushing tag ${version} to origin"
  git push origin "$version"

  log "publishing GitHub release ${version}"
  gh release create "$version" \
    --repo "$GITHUB_REPO" \
    --title "$version" \
    --generate-notes

  gitea_mirror_sync
}

gitea_mirror_sync() {
  if [[ -z "${GITEA_TOKEN:-}" ]]; then
    warn "GITEA_TOKEN is not set — skipping Gitea mirror-sync for: ${GITEA_MIRRORS}"
    warn "Run manually later: curl -sS -X POST -H \"Authorization: token \$GITEA_TOKEN\" ${GITEA_BASE_URL}/repos/<owner>/<repo>/mirror-sync"
    return 0
  fi

  local mirror owner repo status
  for mirror in $GITEA_MIRRORS; do
    owner="${mirror%%/*}"
    repo="${mirror##*/}"
    log "Gitea mirror-sync: ${mirror}"
    status="$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
	  --data '' \
      -H "Authorization: token ${GITEA_TOKEN}" \
      "${GITEA_BASE_URL}/repos/${owner}/${repo}/mirror-sync")"
    if [[ "$status" != "200" ]]; then
      warn "mirror-sync for ${mirror} returned HTTP ${status} (expected 200) — check manually at https://code.tnxs.net/${mirror}/settings"
    else
      log "mirror-sync triggered for ${mirror} (HTTP 200; sync happens async on the Gitea side)"
    fi
  done
}

# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------

main() {
  local cmd="${1:-}"
  shift || true
  case "$cmd" in
    install-swagger) cmd_install_swagger "$@" ;;
    regen) cmd_regen "$@" ;;
    release) cmd_release "$@" ;;
    *) usage; exit 1 ;;
  esac
}

main "$@"
