#!/usr/bin/env bash
# staging-env.sh — deterministic per-worktree staging environment.
# SOURCE this file (do not execute). Installed by `scaffold agent-ops install`.
#
# Every worktree gets a deterministic, collision-resistant slot derived from its
# path (254 slots; automatic collision warning + STAGING_WT_OFFSET override):
#   O = (cksum(worktree_path) % 254) + 1          # 1..254
#   compose project = {{PROJECT_NAME}}-wt-<cksum>
#   host port for service S = BAND_S + O
#   subnet = 10.<O>.0.0/16
# The primary checkout gets the shared stack ({{PROJECT_NAME}}, SHARED_* ports).
# Never select the shared stack from a worktree.

# This file is SOURCED, so a bare `set -euo pipefail` would leak into the caller's
# shell — an interactive shell or parent script would then exit on the first
# failing command or unset-variable lookup. Snapshot the caller's options, run the
# body strict, and restore the snapshot before every path that returns control.
_staging_env_old_opts="$(set +o)"
set -euo pipefail

PROJECT_NAME="{{PROJECT_NAME}}"

# --- GENERATED: service port bands (from .scaffold/agent-ops.yaml) ---
{{SERVICE_PORT_BANDS}}
# --- END GENERATED ---

# No services configured (staging installed without a docker section in
# .scaffold/agent-ops.yaml) — there is nothing to set up. No-op with a clear
# message rather than crashing on the unbound $SERVICES below.
if [ -z "${SERVICES:-}" ]; then
    echo "staging-env: no services configured in .scaffold/agent-ops.yaml — staging disabled" >&2
    eval "$_staging_env_old_opts"
    return 0 2>/dev/null || exit 0
fi

# Service names may contain dashes (e.g. redis-cache), which are illegal in shell
# variable names. buildTemplateVars emits BAND_redis_cache / SHARED_redis_cache;
# recompute the same `-`→`_` suffix here to read them back by indirection.
_svc_var_suffix() { printf '%s' "$1" | tr -- '-' '_'; }

_repo_root() { git rev-parse --show-toplevel; }
_primary_root() { git worktree list --porcelain | head -1 | sed 's/^worktree //'; }

is_primary_checkout() { [ "$(_repo_root)" = "$(_primary_root)" ]; }

worktree_offset() {
    if [ -n "${STAGING_WT_OFFSET:-}" ]; then
        echo "${STAGING_WT_OFFSET}"
        return
    fi
    local cksum_val
    cksum_val=$(printf '%s' "$(_repo_root)" | cksum | awk '{print $1}')
    echo $(( (cksum_val % 254) + 1 ))
}

# Deterministic slot allocation is collision-RESISTANT, not collision-free: two
# worktree paths can cksum to the same 1..254 slot and would then share host ports
# and the /16 subnet. Warn loudly (never fail) when this worktree's slot matches a
# live sibling's, naming both paths and the STAGING_WT_OFFSET override that fixes it.
# LIVE_WT_OFFSETS (space/newline-separated) overrides the derived sibling list for
# tests — mirrors LIVE_WT_HASHES in staging-teardown.sh.
_staging_detect_offset_collision() {
    local self mine sibling_path sib_cksum sibling_off collided=""
    mine="$STAGING_OFFSET"
    self="$(_repo_root)"
    if [ -n "${LIVE_WT_OFFSETS:-}" ]; then
        for sibling_off in $LIVE_WT_OFFSETS; do
            [ "$sibling_off" = "$mine" ] && { collided="another live worktree (test override)"; break; }
        done
    else
        while IFS= read -r sibling_path; do
            [ -n "$sibling_path" ] || continue
            [ "$sibling_path" = "$self" ] && continue
            sib_cksum="$(printf '%s' "$sibling_path" | cksum | awk '{print $1}')"
            [ -n "$sib_cksum" ] || continue
            sibling_off=$(( (sib_cksum % 254) + 1 ))
            if [ "$sibling_off" = "$mine" ]; then collided="$sibling_path"; break; fi
        done < <(git worktree list --porcelain 2>/dev/null | awk '/^worktree /{ print substr($0, 10) }')
    fi
    if [ -n "$collided" ]; then
        echo "! staging-env: staging slot ${mine} collides with ${collided}" >&2
        echo "!   (this worktree: ${self}). Its host ports and the 10.${mine}.0.0/16 subnet would clash." >&2
        echo "!   Pick a free slot for THIS worktree: export STAGING_WT_OFFSET=<free 1..254>, then 'make staging-up'." >&2
    fi
    return 0
}

_cksum_val=$(printf '%s' "$(_repo_root)" | cksum | awk '{print $1}')
STAGING_OFFSET="$(worktree_offset)"
export STAGING_OFFSET

if is_primary_checkout || [ "${STAGING_COMPOSE_PROJECT:-}" = "$PROJECT_NAME" ]; then
    if ! is_primary_checkout; then
        echo "ERROR: the shared '$PROJECT_NAME' stack may only be selected from the primary checkout." >&2
        echo "       From a worktree, use the default per-worktree stack instead." >&2
        eval "$_staging_env_old_opts"    # restore caller options; still return non-zero
        return 1 2>/dev/null || exit 1
    fi
    export COMPOSE_PROJECT_NAME="$PROJECT_NAME"
    for svc in $SERVICES; do
        shared_var="SHARED_$(_svc_var_suffix "$svc")"
        port_var="PORT_$(echo "$svc" | tr '[:lower:]' '[:upper:]' | tr -- '-' '_')"
        # A service that isn't in shared_stack has no SHARED_ port. Warn and skip
        # its export rather than failing the whole source (spec §4.3: never fail).
        if [ -z "${!shared_var:-}" ]; then
            echo "! staging-env: service '$svc' has no shared_stack port — skipping its shared export" >&2
            continue
        fi
        export "$port_var"="${!shared_var}"
    done
    # Shared stack keeps the conventional docker-compose default network.
    export STAGING_SUBNET=""
else
    export COMPOSE_PROJECT_NAME="${PROJECT_NAME}-wt-${_cksum_val}"
    _staging_detect_offset_collision
    for svc in $SERVICES; do
        band_var="BAND_$(_svc_var_suffix "$svc")"
        port_var="PORT_$(echo "$svc" | tr '[:lower:]' '[:upper:]' | tr -- '-' '_')"
        export "$port_var"="$(( ${!band_var} + STAGING_OFFSET ))"
    done
    export STAGING_SUBNET="10.${STAGING_OFFSET}.0.0/16"
fi

# --- Env-file preflight (spec §4.3): warn-only, NEVER fail the source. ---
# Non-derived values (CORS origins, email-link base URLs) live in
# ops/compose/staging.env and must be kept in step with the resolved ports.
# Scan that file's URL-ish `:<port>` tokens: a token that falls in a known
# service's band (BAND_S+1..BAND_S+254) or equals its SHARED_S value, yet does
# not match the port we just resolved for that service, means the file is stale
# for this stack — surface a one-line warning naming the variable, nothing more.
_staging_env_check_envfile() {
    local envfile="ops/compose/staging.env"
    [ -f "$envfile" ] || return 0
    local token svc band_var shared_var port_var band shared resolved
    while IFS= read -r token; do
        [ -n "$token" ] || continue
        for svc in $SERVICES; do
            band_var="BAND_$(_svc_var_suffix "$svc")"
            shared_var="SHARED_$(_svc_var_suffix "$svc")"
            port_var="PORT_$(echo "$svc" | tr '[:lower:]' '[:upper:]' | tr -- '-' '_')"
            band="${!band_var:-}"
            shared="${!shared_var:-}"
            resolved="${!port_var:-}"
            [ -n "$resolved" ] || continue
            if { [ -n "$band" ] && [ "$token" -gt "$band" ] && [ "$token" -le "$((band + 254))" ]; } \
               || { [ -n "$shared" ] && [ "$token" = "$shared" ]; }; then
                if [ "$token" != "$resolved" ]; then
                    printf '! staging.env: service %s uses :%s but resolved %s=%s — update ops/compose/staging.env\n' \
                        "$svc" "$token" "$port_var" "$resolved" >&2
                fi
            fi
        done
    done < <(grep -oE ':[0-9]+' "$envfile" 2>/dev/null | tr -d ':' | sort -u)
    return 0
}
_staging_env_check_envfile || true

# Restore the caller's shell options (snapshot at the top) so sourced strict-mode
# does not leak into an interactive shell or a parent script.
eval "$_staging_env_old_opts"
