#!/usr/bin/env bash
# seat-logins.sh — project seat credentials into tea's login config.
#
# Issue: mosaicstack/stack#1356 (tea login resolution fails open).
#
# WHY THIS EXISTS. tea 0.14.0 has no --token on its operations; it can only use a
# login already stored in ~/.config/tea/config.yml. So the wrappers cannot read the
# seat secrets dir on the tea path. The secrets dir stays authoritative and this
# script projects it into tea's config, which is a DERIVED CACHE: regenerate it,
# never hand-edit it. Same shape as the config-registry projector, same reason —
# a third-party tool that cannot read our store has to be fed.
#
# Canonical login name is "<instance>-<seat>", which is what the identity ladder in
# detect-platform.sh computes from the seat name. A login the ladder cannot compute
# is a fail-open surface, so an ad-hoc name is a defect, not a style.
#
# COLLISIONS. tea refuses to store one token under two names ("token already been
# used, delete login 'X' first"). A hand-made alias holding a seat's token there-
# fore BLOCKS its canonical name. Detected up front by hashing, so a dry run shows
# it; --adopt resolves it by deleting the alias and re-minting canonically. Same
# token, same access, only the label changes.
#
# Tokens are never printed, never logged, and never passed on a visible command
# line beyond tea's own --token, which is unavoidable with this client. tea's
# stderr is echoed on failure with any token-shaped string redacted.
#
# Usage:
#   seat-logins.sh                 # dry run, all seats  (default: changes nothing)
#   seat-logins.sh --apply         # mint/refresh all seats
#   seat-logins.sh --seat <seat>   # limit to one seat
#   seat-logins.sh --apply --adopt # also rename ad-hoc aliases to canonical names
set -euo pipefail

BRAIN_HOME="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
TEA_CONFIG="${TEA_CONFIG:-$HOME/.config/tea/config.yml}"
APPLY=0
ADOPT=0
ONLY_SEAT=""

# Instance -> server URL.
#
# Instances are named here because there is no registry to read them from yet.
# Override per-instance without editing this file, which is how a deployment adds
# its own hosts:  MOSAIC_GITEA_URL_<INSTANCE>=https://...
declare -A INSTANCE_URL=(
  [mosaicstack]="https://git.mosaicstack.dev"
  [usc]="https://git.uscllc.com"
)

while [ $# -gt 0 ]; do
  case "$1" in
    --apply) APPLY=1; shift ;;
    --adopt) ADOPT=1; shift ;;
    --seat)  ONLY_SEAT="${2:?--seat needs a name}"; shift 2 ;;
    -h|--help) sed -n '2,33p' "$0"; exit 0 ;;
    *) echo "seat-logins.sh: unknown argument '$1'" >&2; exit 2 ;;
  esac
done

command -v tea >/dev/null || { echo "seat-logins.sh: tea not on PATH" >&2; exit 1; }

url_for() {
  local inst="$1" ovr
  ovr="MOSAIC_GITEA_URL_$(printf '%s' "$inst" | tr '[:lower:]-' '[:upper:]_')"
  if [ -n "${!ovr:-}" ]; then printf '%s' "${!ovr}"; return 0; fi
  printf '%s' "${INSTANCE_URL[$inst]:-}"
}

# Redact anything token-shaped before any tea output reaches a log.
redact() { sed -E 's/[A-Za-z0-9]{30,}/<REDACTED>/g'; }

# token sha256 -> login name, for every login tea already holds. This is what
# makes collisions visible in a DRY RUN instead of only as an apply-time error.
declare -A TOKEN_OWNER=()
if [ -r "$TEA_CONFIG" ]; then
  while read -r sha lname; do
    [ -n "${sha:-}" ] && TOKEN_OWNER["$sha"]="$lname"
  done < <(python3 - "$TEA_CONFIG" <<'PY'
import sys, yaml, hashlib
try:
    cfg = yaml.safe_load(open(sys.argv[1])) or {}
except Exception:
    sys.exit(0)
for l in (cfg.get('logins') or []):
    t = l.get('token')
    if t:
        print(hashlib.sha256(t.encode()).hexdigest(), l.get('name'))
PY
  )
fi

minted=0; refreshed=0; skipped=0; failed=0; planned=0; adopted=0; blocked=0

existing="$(tea login list --output simple 2>/dev/null | awk '{print $1}' || true)"

shopt -s nullglob
for tokfile in "$BRAIN_HOME"/fleet/agents/*/secrets/gitea-*.token; do
  seat="${tokfile#"$BRAIN_HOME"/fleet/agents/}"; seat="${seat%%/*}"
  [ -n "$ONLY_SEAT" ] && [ "$seat" != "$ONLY_SEAT" ] && continue

  base="$(basename "$tokfile" .token)"          # gitea-<instance>-<seat>
  inst="${base#gitea-}"; inst="${inst%-"$seat"}"
  name="${inst}-${seat}"
  url="$(url_for "$inst")"

  if [ -z "$url" ]; then
    echo "  SKIP  $name — no URL known for instance '$inst' (set MOSAIC_GITEA_URL_${inst^^})"
    skipped=$((skipped+1)); continue
  fi
  if [ ! -r "$tokfile" ]; then
    echo "  SKIP  $name — token not readable"
    skipped=$((skipped+1)); continue
  fi

  action="mint"
  grep -qx "$name" <<<"$existing" && action="refresh"

  # Is this exact token already stored under some OTHER name?
  tsha="$(sha256sum < "$tokfile" | awk '{print $1}')"
  owner="${TOKEN_OWNER[$tsha]:-}"
  collision=""
  [ -n "$owner" ] && [ "$owner" != "$name" ] && collision="$owner"

  if [ "$APPLY" -eq 0 ]; then
    if [ -n "$collision" ]; then
      if [ "$ADOPT" -eq 1 ]; then
        echo "  PLAN  adopt  $collision -> $name  ($url)"
      else
        echo "  BLOCK $name — token already stored as '$collision'; re-run with --adopt"
        blocked=$((blocked+1)); continue
      fi
    else
      echo "  PLAN  $action $name -> $url"
    fi
    planned=$((planned+1)); continue
  fi

  if [ -n "$collision" ]; then
    if [ "$ADOPT" -eq 0 ]; then
      echo "  BLOCK $name — token already stored as '$collision'; re-run with --adopt"
      blocked=$((blocked+1)); continue
    fi
    tea login delete "$collision" >/dev/null 2>&1 || true
    action="adopt"
  fi

  # tea has no idempotent add; refresh is delete-then-add so a rotated token lands.
  [ "$action" = refresh ] && tea login delete "$name" >/dev/null 2>&1 || true

  if err="$(tea login add --name "$name" --url "$url" \
              --token "$(cat "$tokfile")" --no-version-check 2>&1 >/dev/null)"; then
    case "$action" in
      mint)    minted=$((minted+1)) ;;
      refresh) refreshed=$((refreshed+1)) ;;
      adopt)   adopted=$((adopted+1)) ;;
    esac
    if [ "$action" = adopt ]; then
      echo "  OK    adopt  $collision -> $name  ($url)"
    else
      echo "  OK    $action $name -> $url"
    fi
  else
    # A failure here is real information: the seat's token is dead, or the server
    # refused it. Do not paper over it; the seat cannot act until it is reminted.
    # tea's own words, redacted — a summarised FAIL hides whether the cause is the
    # credential or the client, which cost a diagnosis on 2026-08-21.
    echo "  FAIL  $action $name -> $url"
    echo "        tea: $(printf '%s' "$err" | redact | head -1)"
    failed=$((failed+1))
  fi
done

echo
if [ "$APPLY" -eq 0 ]; then
  echo "dry run: $planned login(s) would be written, $skipped skipped, $blocked blocked."
  [ "$blocked" -gt 0 ] && echo "re-run with --adopt to rename ad-hoc aliases to canonical names."
  echo "no changes made. re-run with --apply."
else
  echo "minted=$minted adopted=$adopted refreshed=$refreshed skipped=$skipped blocked=$blocked failed=$failed"
fi
[ "$failed" -eq 0 ] && [ "$blocked" -eq 0 ]
