#!/usr/bin/env bash
# ══════════════════════════════════════════════════════════════════════
# devolyn — operator CLI for a stack installed by deploy/install.sh.
#
#   devolyn onboard --slug NAME --path /path/to/checkout [--guard-url URL] [--commit-sha SHA]
#   devolyn status
#   devolyn logs [service] [-f]
#   devolyn restart [service]
#   devolyn reset-admin-password
#   devolyn update            # git pull + rebuild + restart (git installs only)
#   devolyn env               # where the secrets are, and the public URL
#
# `nucleus-guard` is kept as an alias so existing scripts keep working; it runs this same
# file, and the messages below name whichever one you invoked.
#
# Everything runs INSIDE the containers, so nothing needs Python, a venv, or PYTHONPATH on
# the host. `onboard` bind-mounts the checkout you name into a one-off backend container --
# Guard indexes from disk and never clones, so that is how a repository reaches it.
# ══════════════════════════════════════════════════════════════════════
set -Eeuo pipefail

SELF=$(readlink -f "${BASH_SOURCE[0]}")
CLI=$(basename "$0")
ROOT=$(cd "$(dirname "$SELF")/.." && pwd)
COMPOSE_FILE="$ROOT/deploy/docker-compose.prod.yml"
ENV_FILE="$ROOT/deploy/.env"

die() { printf '✘ %s\n' "$*" >&2; exit 1; }
compose() { docker compose -f "$COMPOSE_FILE" "$@"; }
env_get() {
  local line
  line=$(grep -E "^${1}=" "$ENV_FILE" 2>/dev/null | tail -n1 || true)
  line="${line#*=}"; line="${line%\"}"; line="${line#\"}"
  printf '%s' "$line"
}

[[ -f "$ENV_FILE" ]] || die "no deploy/.env at ${ROOT} -- run deploy/install.sh first"

cmd="${1:-help}"; shift || true

case "$cmd" in
  onboard)
    SLUG=""; HOST_PATH=""; GUARD_URL=""; EXTRA=()
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --slug) SLUG="$2"; shift 2 ;;
        --path) HOST_PATH="$2"; shift 2 ;;
        --guard-url) GUARD_URL="$2"; shift 2 ;;
        *) EXTRA+=("$1"); shift ;;
      esac
    done
    [[ -n "$SLUG" && -n "$HOST_PATH" ]] || die "usage: $CLI onboard --slug NAME --path /path/to/checkout [--guard-url URL] [--commit-sha SHA]"
    HOST_PATH=$(readlink -f "$HOST_PATH") || die "no such directory: $HOST_PATH"
    [[ -d "$HOST_PATH" ]] || die "not a directory: $HOST_PATH"
    GUARD_URL="${GUARD_URL:-$(env_get GUARD_PUBLIC_URL)}"
    GUARD_URL="${GUARD_URL:-https://localhost}"
    # `run --rm` joins the compose network and env_file like the real backend, plus the
    # checkout mounted read-only where the script expects it. --no-deps: the stack is
    # already up; don't let compose try to start anything.
    exec docker compose -f "$COMPOSE_FILE" run --rm --no-deps -T \
      -v "${HOST_PATH}:/repos/${SLUG}:ro" \
      backend python scripts/onboard_repo.py \
        --slug "$SLUG" --path "/repos/${SLUG}" --guard-url "$GUARD_URL" "${EXTRA[@]}"
    ;;

  status)
    compose ps --format 'table {{.Service}}\t{{.Status}}'
    printf '\n'
    curl -sk --max-time 5 https://127.0.0.1/health/ready || printf 'health: no response through nginx\n'
    printf '\n\n'

    # Per-repository runner heartbeat.
    #
    # `docker compose ps` says the runner PROCESS is up, which is not the question that
    # matters. From a pull request's side, "no runner" and "runner idle" are
    # indistinguishable -- the check simply never starts, with no error anywhere. The last
    # heartbeat is the only thing that tells them apart, so it is shown per repository
    # rather than as one number for the container.
    #
    # Read straight from Postgres rather than through the API, deliberately: this has to
    # work when the backend is the thing that is unwell.
    db=$(env_get NUCLEUS_DB_DATABASE); db="${db:-nucleus_guard}"
    rows=$(compose exec -T postgres psql -U nucleus -d "$db" -tAF'|' -c \
      "SELECT full_name, state,
              COALESCE(to_char(last_runner_heartbeat_at, 'YYYY-MM-DD HH24:MI'), 'never'),
              COALESCE(EXTRACT(EPOCH FROM (now() - last_runner_heartbeat_at))::bigint::text, '')
         FROM github_repos ORDER BY full_name" 2>/dev/null) || rows=""

    if [[ -z "${rows//[[:space:]]/}" ]]; then
      printf 'GitHub: no repositories connected (Settings -> GitHub in the dashboard)\n'
    else
      printf '%-38s %-14s %-17s %s\n' 'REPOSITORY' 'STATE' 'LAST HEARTBEAT' 'AGE'
      while IFS='|' read -r name state beat age; do
        [[ -n "$name" ]] || continue
        if [[ -z "$age" ]]; then
          note='no runner has served this yet'
        elif (( age < 120 )); then
          note="${age}s ago"
        else
          # Two minutes is four default poll intervals. Past that a loop is not merely
          # quiet, and saying so beats leaving the operator to judge a raw timestamp.
          note="${age}s ago -- STALE, check '$CLI logs runner'"
        fi
        printf '%-38s %-14s %-17s %s\n' "$name" "$state" "$beat" "$note"
      done <<< "$rows"
    fi
    printf '\n'
    ;;

  logs)
    compose logs --tail 100 "$@"
    ;;

  restart)
    compose up -d --force-recreate "$@"
    compose ps --format 'table {{.Service}}\t{{.Status}}'
    ;;

  reset-admin-password)
    exec bash "$ROOT/deploy/install.sh" --no-build --reset-admin-password -y
    ;;

  update)
    git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1 || die "not a git checkout; download the new release archive and run its deploy/install.sh instead"
    git -C "$ROOT" pull --ff-only
    exec bash "$ROOT/deploy/install.sh" -y
    ;;

  env)
    printf 'secrets:     %s  (back this file up)\n' "$ENV_FILE"
    printf 'dashboard:   %s\n' "$(env_get GUARD_PUBLIC_URL)"
    printf 'admin email: %s\n' "$(env_get NUCLEUS_DASHBOARD_ADMIN_EMAIL)"
    printf 'CI token:    set (NUCLEUS_VERIFIER_CI_TOKEN in the file above)\n'
    ;;

  help|-h|--help)
    sed -n '2,18p' "$SELF" | sed 's/^# \{0,1\}//'
    ;;

  *)
    die "unknown command: $cmd (try: $CLI help)"
    ;;
esac
