#!/usr/bin/env bash
#
# Dogfood — build, deploy, and inspect the running stack.
#
#   ./run.sh                 rebuild the image and restart the containers
#   ./run.sh build           build the image only
#   ./run.sh restart         restart without rebuilding
#   ./run.sh up              start (build if the image is missing)
#   ./run.sh down            stop and remove the containers
#   ./run.sh logs [-f]       show logs
#   ./run.sh status          containers, health, and the public endpoint
#   ./run.sh test            run the full Rust test suite in Docker
#   ./run.sh check           cargo check
#   ./run.sh fixtures        regenerate the jj/git fixture corpus
#   ./run.sh psql            open a psql shell on the configured database
#   ./run.sh token           show the outstanding admin setup token status
#   ./run.sh shell           a shell in the web container
#   ./run.sh metrics         scrape /metrics from inside the container
#   ./run.sh reindex [id]    re-run indexing (all repositories, or one)
#   ./run.sh backup [dir]    pg_dump + a snapshot of the repository volume
#   ./run.sh rehearse        prove a backup restores (spec §10)
#   ./run.sh loadtest        drive a large repository over HTTP (spec §11)
#
# The deploy path is `docker compose`, so this script is a convenience wrapper
# and never the only way to operate the stack.

set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE=(docker compose -f "$ROOT/docker/compose.yaml")
SERVICE=web
CONTAINER=dogfood-web
# Every service built and restarted by the default `deploy` path.
SERVICES=(web worker ssh)

# Pinned so a toolchain bump is a deliberate edit. 1.83 and older fail to parse
# a transitive `time-core` manifest.
RUST_IMAGE="rust:slim"

CARGO_ARGS=(
    --rm
    -v "$ROOT":/w -w /w
    -v dogfood-cargo-registry:/usr/local/cargo/registry
    -v dogfood-target:/w/target
)

c_red() { printf '\033[31m%s\033[0m\n' "$*"; }
c_green() { printf '\033[32m%s\033[0m\n' "$*"; }
c_dim() { printf '\033[2m%s\033[0m\n' "$*"; }

require_env() {
    if [[ ! -f "$ROOT/.env" ]]; then
        c_red "No .env at $ROOT/.env — the containers cannot start without it."
        echo "Copy .env.example and fill in DATABASE_URL and the OIDC_* values."
        exit 1
    fi
}

# Read a value out of .env without sourcing it, so quoting and stray shell
# metacharacters in secrets cannot execute.
env_get() {
    sed -n "s/^$1=//p" "$ROOT/.env" | head -1
}

cmd_build() {
    require_env
    c_dim "building $SERVICE…"
    "${COMPOSE[@]}" build "${SERVICES[@]}"
    c_green "built"
}

cmd_up() {
    require_env
    "${COMPOSE[@]}" up -d
    wait_healthy
}

cmd_down() {
    "${COMPOSE[@]}" down
}

cmd_restart() {
    require_env
    # --force-recreate so a changed .env is actually picked up; compose does not
    # recreate a container just because its env_file changed on disk.
    "${COMPOSE[@]}" up -d --force-recreate
    wait_healthy
}

cmd_deploy() {
    cmd_build
    cmd_restart
}

# Poll until the app answers, so the script fails loudly rather than returning
# success on a container that crash-looped.
wait_healthy() {
    c_dim "waiting for $CONTAINER to serve…"
    for _ in $(seq 1 40); do
        if docker exec "$CONTAINER" wget -qO- http://127.0.0.1:8080/healthz >/dev/null 2>&1; then
            c_green "healthy"
            cmd_status
            return 0
        fi
        if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
            c_red "container exited — last 30 log lines:"
            docker logs --tail 30 "$CONTAINER" 2>&1 || true
            return 1
        fi
        sleep 1
    done
    c_red "timed out waiting for health — last 30 log lines:"
    docker logs --tail 30 "$CONTAINER" 2>&1 || true
    return 1
}

cmd_logs() {
    "${COMPOSE[@]}" logs "${@:-}" "${SERVICES[@]}"
}

cmd_status() {
    echo
    "${COMPOSE[@]}" ps
    local base
    base="$(env_get BASE_URL)"
    base="${base:-https://dogfood.sh}"
    echo
    printf '%-28s ' "$base/healthz"
    curl -s -o /dev/null -w '%{http_code}\n' "$base/healthz" --max-time 15 || echo "unreachable"
    printf '%-28s ' "$base/readyz"
    curl -s -o /dev/null -w '%{http_code}\n' "$base/readyz" --max-time 15 || echo "unreachable"
}

cmd_test() {
    # Fixtures are required so the corpus tests cannot silently skip.
    [[ -d "$ROOT/fixtures/repos" ]] || cmd_fixtures
    docker run "${CARGO_ARGS[@]}" -e DF_REQUIRE_FIXTURES=1 "$RUST_IMAGE" bash -c '
        apt-get update -qq >/dev/null 2>&1
        apt-get install -y -qq git >/dev/null 2>&1
        cargo test --workspace'
}

cmd_check() {
    docker run "${CARGO_ARGS[@]}" "$RUST_IMAGE" cargo check --workspace --all-targets
}

cmd_fixtures() {
    "$ROOT/fixtures/gen.sh"
}

cmd_psql() {
    require_env
    local url
    url="$(env_get DATABASE_URL)"
    [[ -n "$url" ]] || { c_red "DATABASE_URL not set in .env"; exit 1; }
    # Only request a TTY when we actually have one, so `./run.sh psql -tAc '…'`
    # works from a script or a pipeline.
    local tty=()
    [[ -t 0 && -t 1 ]] && tty=(-it)
    docker run --rm "${tty[@]}" postgres:17-alpine psql "$url" "$@"
}

cmd_token() {
    require_env
    local url
    url="$(env_get DATABASE_URL)"
    echo "Outstanding setup tokens (plaintext is only ever printed to the log at first boot):"
    docker run --rm postgres:17-alpine psql "$url" -c \
        "SELECT id, created_at, consumed_at, consumed_by FROM setup_tokens ORDER BY created_at DESC;"
    echo "If none is outstanding and you still need admin, promote a user directly:"
    c_dim "  ./run.sh psql -c \"UPDATE users SET is_admin = true WHERE handle = 'your-handle';\""
}

cmd_shell() {
    docker exec -it "$CONTAINER" bash
}

# /metrics is loopback-only by design (spec §7), so it is scraped from inside
# the container rather than over the network.
cmd_metrics() {
    docker exec "$CONTAINER" wget -qO- http://127.0.0.1:8080/metrics
}

# The reconciling reindex (spec §4, §10). Runs in the worker, which is where the
# repository volume is mounted.
cmd_reindex() {
    if [[ -n "${1:-}" ]]; then
        "${COMPOSE[@]}" exec -T worker dogfood-admin reindex --repo "$1"
    else
        "${COMPOSE[@]}" exec -T worker dogfood-admin reindex --all
    fi
}

case "${1:-deploy}" in
    ""|deploy)  cmd_deploy ;;
    build)      cmd_build ;;
    up)         cmd_up ;;
    down)       cmd_down ;;
    restart)    cmd_restart ;;
    logs)       shift; cmd_logs "$@" ;;
    status)     cmd_status ;;
    test)       cmd_test ;;
    check)      cmd_check ;;
    fixtures)   cmd_fixtures ;;
    psql)       shift; cmd_psql "$@" ;;
    token)      cmd_token ;;
    shell)      cmd_shell ;;
    metrics)    cmd_metrics ;;
    reindex)    shift; cmd_reindex "${1:-}" ;;
    backup)     shift; "$ROOT/scripts/backup.sh" backup "$@" ;;
    rehearse)   shift; "$ROOT/scripts/backup.sh" rehearse "$@" ;;
    loadtest)   shift; "$ROOT/scripts/loadtest.sh" "$@" ;;
    -h|--help|help)
        sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
        ;;
    *)
        c_red "unknown command: $1"
        sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
        exit 1
        ;;
esac
