# ScreamingFace local dev stack — gateway (:9105) + scoreboard (:9106) + engine (:9108) — plus notebooks.
# just stack-up | stack-down | stack-status | stack-logs | stack-prepare | jupyter

assets := "/tmp/screamingface-benchmark-assets"  # datasets for url4-cloud benchmarks
rundir := env_var_or_default(
    "SCREAMINGFACE_STACK_DIR",
    env_var_or_default("XDG_RUNTIME_DIR", env_var_or_default("TMPDIR", "/tmp")) / "screamingface-stack",
)  # pid files and logs
repo := justfile_directory() / "../.."
# During stacked development the SDK and Engine may be separate worktrees.
engine_repo := env_var_or_default("SCREAMINGFACE_ENGINE_REPO", repo)

default:
    @just --list

# Sync the SDK venv and open JupyterLab in examples/ (run just stack-up first).
# Notebooks talk to the LOCAL engine by default — a bare sf.connect() would
# otherwise reach the deployed fusion.dev engine, silently testing whatever is
# deployed there instead of this checkout. Export SCREAMINGFACE_ENGINE_URL to
# target another engine deliberately.
jupyter:
    #!/usr/bin/env bash
    set -euo pipefail
    uv sync --extra notebook
    SCREAMINGFACE_ENGINE_URL=${SCREAMINGFACE_ENGINE_URL:-http://127.0.0.1:9108} \
        uv run jupyter lab examples

# Start AI Gateway (:9105), scoreboard (:9106), and url4-cloud engine (:9108) in the background
stack-up:
    #!/usr/bin/env bash
    set -euo pipefail
    mkdir -p {{rundir}}
    chmod 700 {{rundir}}
    # Mental model: fixed-port services — each service owns one well-known port
    # (gateway 9105, scoreboard 9106, engine 9108), so holding the port IS being
    # the service, and only one stack can exist at a time. An occupied port means
    # one of two things: our own service is already up (adopt it — rewrite its pid
    # file so stack-down can stop it), or someone else holds our port (error out;
    # reusing a stranger's service would run benchmarks against another branch's
    # code and produce results that look right but aren't).
    repo_root=$(cd {{repo}} && pwd -P)
    engine_root=$(cd {{engine_repo}} && pwd -P)
    claim() {  # claim <name> <port> <expected_root> <pidfile> → 0 adopted, 1 port free
        local name=$1 port=$2 root=$3 pidfile=$4 pid command
        pid=$(lsof -nP -tiTCP:$port -sTCP:LISTEN 2>/dev/null | head -n1 || true)
        if [ -z "$pid" ]; then return 1; fi
        command=$(ps -p "$pid" -o command= 2>/dev/null || true)
        case "$command" in
            "$root"/*)
                echo "$pid" >"$pidfile"
                echo "$name :$port already running (pid $pid, adopted)"
                return 0
                ;;
            *)
                echo "ERROR: $name :$port is held by another stack; refusing to reuse it:" >&2
                echo "  pid $pid — ${command:-<command gone>}" >&2
                echo "Stop it first with: just stack-down   (works from any checkout)" >&2
                exit 1
                ;;
        esac
    }
    start_gateway=0; start_scoreboard=0; start_engine=0
    claim "gateway   " 9105 "$repo_root" {{rundir}}/aigateway.pid || start_gateway=1
    claim "scoreboard" 9106 "$repo_root" {{rundir}}/scoreboard.pid || start_scoreboard=1
    claim "engine    " 9108 "$engine_root" {{rundir}}/url4-cloud.pid || start_engine=1
    # Each service runs from its own venv; sync is idempotent and near-instant
    # when the lockfile is unchanged, so a fresh worktree just works. The first
    # sync in a new worktree builds the venvs and can take a few minutes.
    echo "syncing gateway venv ({{repo}}/apps/aigateway)…"
    (cd {{repo}}/apps/aigateway && uv sync) || {
        echo "AI Gateway environment failed to sync under {{repo}}/apps/aigateway"
        exit 1
    }
    # Apply pending gateway DB migrations — a fresh worktree has an empty SQLite DB
    # and the server refuses to boot without its tables (e.g. secret_master_keys).
    # Same invocation as the Helm migrate job; run from apps/aigateway because the
    # default DB URL is the relative sqlite://./aigateway.sqlite3 the server uses.
    echo "migrating gateway database…"
    (cd {{repo}}/apps/aigateway && .venv/bin/python -m tortoise -c aigateway.db.TORTOISE_CONFIG migrate) || {
        echo "AI Gateway database migration failed"
        exit 1
    }
    echo "syncing scoreboard venv ({{repo}}/apps/scoreboard)…"
    (cd {{repo}}/apps/scoreboard && uv sync) || {
        echo "Scoreboard environment failed to sync under {{repo}}/apps/scoreboard"
        exit 1
    }
    # Same fresh-worktree story as the gateway: seed/serve need the tables first.
    echo "migrating scoreboard database…"
    (cd {{repo}}/apps/scoreboard && uv run tortoise migrate) || {
        echo "Scoreboard database migration failed"
        exit 1
    }
    echo "syncing engine venv ({{engine_repo}}/apps/url4-cloud)…"
    (cd {{engine_repo}}/apps/url4-cloud && uv sync) || {
        echo "URL4 Cloud environment failed to sync under {{engine_repo}}/apps/url4-cloud"
        exit 1
    }
    (
        cd {{repo}}/apps/scoreboard
        .venv/bin/python -m scoreboard.seed --benchmarks-json \
            '[{"id":"draco/smoke","display_name":"DRACO Smoke","description":"Local end-to-end development protocol"}]'
    )
    if [ "$start_gateway" = 1 ]; then
        # AUTH DISABLED = anonymous loopback mode; no admin bootstrap, key stored under
        # the anonymous account. Matches how the local stack has been run so far.
        # BOOTSTRAP flag re-activates stored provider profiles (e.g. the OpenRouter key)
        # at boot — without it a restarted gateway exposes no openrouter/* models.
        # OPENROUTER_ENABLED is REQUIRED too: the provider plugin defaults to disabled,
        # Without it the Gateway catalog contains no openrouter/* models. The Engine projects
        # that caller-visible catalog onto its declared routes; it does not fabricate providers.
        (
            cd {{repo}}/apps/aigateway
            AIGATEWAY_AUTH_ENABLED=false AIGATEWAY_BOOTSTRAP_FROM_CLAUDE_CODE=1 \
                AIGW_OPENROUTER_ENABLED=true \
                nohup .venv/bin/uvicorn aigateway.main:app --port 9105 \
                >{{rundir}}/aigateway.log 2>&1 &
            echo $! >{{rundir}}/aigateway.pid
        )
        echo "gateway   :9105 starting  (log: {{rundir}}/aigateway.log)"
    fi
    if [ "$start_scoreboard" = 1 ]; then
        (
            cd {{repo}}/apps/scoreboard
            nohup .venv/bin/uvicorn scoreboard.main:app --port 9106 \
                >{{rundir}}/scoreboard.log 2>&1 &
            echo $! >{{rundir}}/scoreboard.pid
        )
        echo "scoreboard :9106 starting  (log: {{rundir}}/scoreboard.log)"
    fi
    if [ "$start_engine" = 1 ]; then
        (
            cd {{engine_repo}}/apps/url4-cloud
            URL4_BENCHMARK_ASSETS={{assets}} \
                URL4_CLOUD_AIGATEWAY_BASE_URL=http://127.0.0.1:9105 \
                nohup .venv/bin/url4-cloud serve --local \
                >{{rundir}}/url4-cloud.log 2>&1 &
            echo $! >{{rundir}}/url4-cloud.pid
        )
        echo "engine    :9108 starting  (log: {{rundir}}/url4-cloud.log, assets: {{assets}})"
    fi
    for attempt in {1..20}; do
        if lsof -nP -iTCP:9105 -sTCP:LISTEN >/dev/null 2>&1 && \
           lsof -nP -iTCP:9106 -sTCP:LISTEN >/dev/null 2>&1 && \
           lsof -nP -iTCP:9108 -sTCP:LISTEN >/dev/null 2>&1; then
            just stack-status
            exit 0
        fi
        sleep 1
    done
    just stack-status
    echo "Local stack failed to become ready; inspect: just stack-logs"
    exit 1

# Stop all services
stack-down:
    #!/usr/bin/env bash
    set -u
    for name in aigateway scoreboard url4-cloud; do
        pidfile={{rundir}}/$name.pid
        if [ -f "$pidfile" ]; then
            IFS= read -r pid < "$pidfile"
            case "$pid" in
                ''|*[!0-9]*)
                    echo "$name has an invalid pid file; refusing to signal it"
                    ;;
                *)
                    command=$(ps -p "$pid" -o command= 2>/dev/null || true)
                    case "$name:$command" in
                        aigateway:*uvicorn*aigateway.main:app*|scoreboard:*uvicorn*scoreboard.main:app*|url4-cloud:*url4-cloud*serve*)
                            if kill "$pid" 2>/dev/null; then
                                echo "$name stopped"
                            else
                                echo "$name pid $pid could not be signalled; still listed below if alive"
                            fi
                            ;;
                        *:)
                            ;;
                        *)
                            echo "$name pid $pid belongs to another command; refusing to signal it"
                            ;;
                    esac
                    ;;
            esac
        fi
        rm -f "$pidfile"
    done
    # Same fixed-port mental model: the port, not the pid file, says whether a
    # service is running — pid files are hints that go stale (lost, or written by
    # a stack-up run elsewhere). So after the pid-file pass, visit each service
    # port and stop whoever holds it, IF its command matches a known service
    # signature. A non-matching holder is not our service — leave it alone.
    for entry in "aigateway:9105" "scoreboard:9106" "url4-cloud:9108"; do
        name=${entry%%:*}; port=${entry##*:}
        for pid in $(lsof -nP -tiTCP:$port -sTCP:LISTEN 2>/dev/null); do
            command=$(ps -p "$pid" -o command= 2>/dev/null || true)
            case "$name:$command" in
                aigateway:*uvicorn*aigateway.main:app*|scoreboard:*uvicorn*scoreboard.main:app*|url4-cloud:*url4-cloud*serve*)
                    if kill "$pid" 2>/dev/null; then
                        echo "$name stopped (found by port :$port, pid $pid)"
                    else
                        echo "$name pid $pid on :$port could not be signalled"
                    fi
                    ;;
                *:)
                    ;;
                *)
                    echo "port :$port is held by an unrecognized command; leaving it alone: $command"
                    ;;
            esac
        done
    done
    exit 0

# Show what is listening
stack-status:
    #!/usr/bin/env bash
    for entry in "gateway:9105" "scoreboard:9106" "engine:9108"; do
        name=${entry%%:*}; port=${entry##*:}
        pid=$(lsof -nP -tiTCP:$port -sTCP:LISTEN 2>/dev/null | head -n1)
        if [ -n "$pid" ]; then
            # Naming the owning checkout makes a foreign stack visible at a glance.
            command=$(ps -p "$pid" -o command= 2>/dev/null || true)
            owner=${command%%/apps/*}
            if [ "$owner" = "$command" ]; then owner="unrecognized: $command"; fi
            echo "$name   :$port UP   ($owner)"
        else
            echo "$name   :$port down"
        fi
    done

# Tail all service logs
stack-logs:
    tail -n 40 -f {{rundir}}/aigateway.log {{rundir}}/scoreboard.log {{rundir}}/url4-cloud.log

# One-time: download pinned benchmark assets into {{assets}}
stack-prepare:
    cd {{engine_repo}}/apps/url4-cloud && uv run --with datasets python -m url4_cloud.benchmarks.draco.prepare --out {{assets}}/draco
    cd {{engine_repo}}/apps/url4-cloud && uv run --with datasets python -m url4_cloud.benchmarks.ifeval.prepare --out {{assets}}/ifeval
    cd {{engine_repo}}/apps/url4-cloud && uv run --with datasets python -m url4_cloud.benchmarks.healthbench.prepare --out {{assets}}/healthbench
