#!/usr/bin/env bash
#
# Tracecat multi-cluster management script
#
# Usage:
#   ./cluster [cluster_num] [-p profile] [tenant mode] <command> [args...]
#
# Profiles:
#   dev     docker-compose.dev.yml (default)
#   local   docker-compose.local.yml
#   prod    docker-compose.yml
#
# Examples:
#   ./cluster up -d                # Start next available cluster (auto-selects number)
#   ./cluster up -d --single-tenant # Start cluster with multi-tenancy disabled
#   ./cluster down                 # Stop cluster (auto-selects if only one running)
#   ./cluster restart api          # Restart the api service
#   ./cluster 2 up -d              # Start cluster 2 explicitly
#   ./cluster 2 -p local up -d     # Start cluster 2 with local profile
#   ./cluster ps                   # Show containers (auto-selects cluster)
#   ./cluster logs api             # Show logs (auto-selects cluster)
#   ./cluster attach api           # Attach shell to api service
#   ./cluster list                 # List all running clusters
#   ./cluster 1 ports              # Show port mappings for cluster 1
#
# Port allocation (offset = (cluster_num - 1) * 100):
#   Cluster numbers are GLOBAL across all worktrees to avoid port conflicts.
#   Cluster 1: 80, 3000, 5432, 6379, 7233, 8000, 8081, 9000, 9001
#   Cluster 2: 180, 3100, 5532, 6479, 7333, 8100, 8181, 9100, 9101
#   Cluster 3: 280, 3200, 5632, 6579, 7433, 8200, 8281, 9200, 9201
#   ...

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
ENV_FILE="${REPO_ROOT}/.env"

# Default profile
PROFILE="dev"
CLUSTER_EE_MULTI_TENANT=""
CLUSTER_USE_SANDBOX_COMPOSE=false

# Get worktree identifier for namespacing clusters
# Returns "main" for the main worktree, or a sanitized branch/directory name for worktrees
get_worktree_id() {
    local git_dir git_common_dir

    git_dir=$(git -C "${REPO_ROOT}" rev-parse --git-dir 2>/dev/null) || { echo "main"; return; }
    git_common_dir=$(git -C "${REPO_ROOT}" rev-parse --git-common-dir 2>/dev/null) || { echo "main"; return; }

    # Resolve to absolute paths for comparison
    git_dir=$(cd "${REPO_ROOT}" && cd "${git_dir}" && pwd)
    git_common_dir=$(cd "${REPO_ROOT}" && cd "${git_common_dir}" && pwd)

    if [[ "$git_dir" == "$git_common_dir" ]]; then
        # Main worktree
        echo "main"
    else
        # In a worktree - use branch name, sanitized
        local branch
        branch=$(git -C "${REPO_ROOT}" rev-parse --abbrev-ref HEAD 2>/dev/null) || branch="unknown"
        # Sanitize: replace slashes and special chars with dashes, lowercase
        echo "$branch" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//'
    fi
}

WORKTREE_ID=$(get_worktree_id)

# === Portless integration (optional) ===
# If portless (https://github.com/vercel-labs/portless) is installed and its
# proxy is reachable, register a stable .localhost alias for the cluster's
# UI/API port. You then reach the cluster at e.g.
#   $(portless get "${WORKTREE_ID}.tracecat" --no-worktree)
# instead of remembering the rotating localhost:80/180/280/... port.
#
# Set TRACECAT__USE_PORTLESS=0 or PORTLESS=0 to disable even when installed.

portless_enabled() {
    [[ "${TRACECAT__USE_PORTLESS:-1}" != "0" ]] \
        && [[ "${PORTLESS:-1}" != "0" ]] \
        && [[ "${PORTLESS:-}" != "skip" ]] \
        && command -v portless >/dev/null 2>&1
}

portless_install_tip() {
    if [[ "${TRACECAT__USE_PORTLESS:-1}" == "0" ]] || [[ "${PORTLESS:-}" == "0" ]] || [[ "${PORTLESS:-}" == "skip" ]]; then
        return
    fi

    if command -v portless >/dev/null 2>&1; then
        return
    fi

    cat <<EOF

Tip:
  Install portless for stable per-worktree cluster URLs:
    pnpm add -g portless
  Project: https://github.com/vercel-labs/portless
EOF
}

# Alias name for a cluster. Cluster 1 gets the bare worktree name; higher
# numbered clusters get a c{N} prefix so they can coexist.
portless_alias_name() {
    local cluster_num=$1
    local worktree_id=${2:-$WORKTREE_ID}

    if [[ "$cluster_num" == "1" ]]; then
        echo "${worktree_id}.tracecat"
    else
        echo "c${cluster_num}.${worktree_id}.tracecat"
    fi
}

portless_url() {
    local cluster_num=$1
    local worktree_id=${2:-$WORKTREE_ID}
    local name
    name=$(portless_alias_name "$cluster_num" "$worktree_id")

    # Ask portless for the URL so custom proxy ports, TLS mode, LAN mode, and
    # custom TLD settings are reflected exactly.
    if command -v portless >/dev/null 2>&1; then
        local url
        if url=$(portless get "$name" --no-worktree 2>/dev/null); then
            echo "$url"
            return
        fi
    fi

    local scheme="https"
    local tld="${PORTLESS_TLD:-localhost}"
    local proxy_port="${PORTLESS_PORT:-}"
    [[ "${PORTLESS_HTTPS:-1}" == "0" ]] && scheme="http"
    url="${scheme}://${name}.${tld}"
    if [[ -n "$proxy_port" && "$proxy_port" != "80" && "$proxy_port" != "443" ]]; then
        url="${url}:${proxy_port}"
    fi
    echo "$url"
}

portless_url_for_name() {
    local name=$1

    # Ask portless for the URL so custom proxy ports, TLS mode, LAN mode, and
    # custom TLD settings are reflected exactly.
    if command -v portless >/dev/null 2>&1; then
        local url
        if url=$(portless get "$name" --no-worktree 2>/dev/null); then
            echo "$url"
            return
        fi
    fi

    local scheme="https"
    local tld="${PORTLESS_TLD:-localhost}"
    local proxy_port="${PORTLESS_PORT:-}"
    [[ "${PORTLESS_HTTPS:-1}" == "0" ]] && scheme="http"
    url="${scheme}://${name}.${tld}"
    if [[ -n "$proxy_port" && "$proxy_port" != "80" && "$proxy_port" != "443" ]]; then
        url="${url}:${proxy_port}"
    fi
    echo "$url"
}

portless_start_proxy() {
    if ! portless proxy start; then
        echo "[portless] Could not start proxy. Set TRACECAT__USE_PORTLESS=0 to disable portless integration." >&2
        return 1
    fi
}

# Register the alias and ensure the proxy is running. Returns 0 on success;
# caller decides whether to surface the URL override.
portless_register() {
    local cluster_num=$1
    local port=$2
    local name
    name=$(portless_alias_name "$cluster_num")

    if ! portless_start_proxy; then
        return 1
    fi

    if ! portless alias "$name" "$port" --force >/dev/null 2>&1; then
        echo "[portless] Could not register alias '${name}'." >&2
        echo "[portless] Retry, or set TRACECAT__USE_PORTLESS=0 to disable portless integration." >&2
        return 1
    fi
    echo "[portless] $(portless_url "$cluster_num") -> localhost:${port}"
}

portless_register_alias() {
    local name=$1
    local port=$2

    if ! portless_start_proxy; then
        return 1
    fi

    if ! portless alias "$name" "$port" --force >/dev/null 2>&1; then
        echo "[portless] Could not register alias '${name}'." >&2
        echo "[portless] Retry, or set TRACECAT__USE_PORTLESS=0 to disable portless integration." >&2
        return 1
    fi
    echo "[portless] $(portless_url_for_name "$name") -> localhost:${port}"
}

portless_unregister() {
    local cluster_num=$1
    local name
    name=$(portless_alias_name "$cluster_num")
    portless alias --remove "$name" >/dev/null 2>&1 || true
}

portless_docs_alias_name() {
    echo "docs.${WORKTREE_ID}.tracecat"
}

# Get list of running cluster numbers for this worktree (used for auto-selection)
get_running_clusters() {
    local prefix="tracecat-${WORKTREE_ID}-"

    docker compose ls --format json 2>/dev/null | jq -r '.[].Name' 2>/dev/null | while read -r project; do
        if [[ "$project" == ${prefix}* ]]; then
            local num="${project#"$prefix"}"
            if [[ "$num" =~ ^[0-9]+$ ]]; then
                echo "$num"
            fi
        fi
    done | sort -n
}

# Get ALL running cluster numbers across all worktrees (used for port allocation)
get_all_cluster_nums() {
    local projects
    projects=$(docker compose ls --format json 2>/dev/null | jq -r '.[].Name' 2>/dev/null)

    for project in $projects; do
        # Match any tracecat cluster: tracecat-{worktree}-{num}
        if [[ "$project" =~ ^tracecat-(.+)-([0-9]+)$ ]]; then
            echo "${BASH_REMATCH[2]}"
        fi
    done | sort -n | uniq
}

# Find next available cluster number (globally, to avoid port conflicts)
get_next_cluster_num() {
    local running
    running=$(get_all_cluster_nums)

    if [[ -z "$running" ]]; then
        echo "1"
        return
    fi

    # Find first gap or next number
    local expected=1
    while read -r num; do
        if [[ "$num" -gt "$expected" ]]; then
            echo "$expected"
            return
        fi
        expected=$((num + 1))
    done <<< "$running"

    echo "$expected"
}

# Present an interactive numbered selector when multiple clusters match.
# Reads cluster numbers from stdin (one per line), shows a menu,
# and prints the chosen cluster number to stdout.
select_cluster_interactive() {
    local nums=()
    while IFS= read -r n; do
        [[ -n "$n" ]] && nums+=("$n")
    done

    for n in "${nums[@]}"; do
        calculate_ports "$n"
        echo "  [${n}]  http://localhost:${PUBLIC_APP_PORT}" >&2
    done

    echo "" >&2
    echo -n "Cluster number: " >&2
    local choice
    # Read a single keypress immediately (no Enter required)
    read -r -n 1 choice </dev/tty
    echo "" >&2

    # Validate the input is one of the running cluster numbers
    for n in "${nums[@]}"; do
        if [[ "$choice" == "$n" ]]; then
            echo "$n"
            return
        fi
    done

    echo "Invalid cluster number '${choice}'" >&2
    exit 1
}

# Auto-select cluster: returns the only running cluster, or prompts if multiple
auto_select_cluster() {
    local command=$1
    local running
    running=$(get_running_clusters)

    # Count non-empty lines
    local count=0
    if [[ -n "$running" ]]; then
        count=$(echo "$running" | wc -l | tr -d ' ')
    fi

    if [[ "$count" -eq 0 ]]; then
        if [[ "$command" == "up" ]]; then
            echo "1"
        else
            echo "Error: No clusters running for worktree '${WORKTREE_ID}'" >&2
            echo "Run 'just cluster up -d' to start one" >&2
            exit 1
        fi
    elif [[ "$count" -eq 1 ]]; then
        echo "$running"
    else
        echo "Multiple clusters running — select one:" >&2
        echo "$running" | select_cluster_interactive
    fi
}

# Base ports (cluster 1 defaults)
BASE_PUBLIC_APP_PORT=80
BASE_UI_PORT=3000
BASE_PG_PORT=5432
BASE_REDIS_PORT=6379
BASE_TEMPORAL_PORT=7233
BASE_API_PORT=8000
BASE_TEMPORAL_UI_PORT=8081
BASE_MINIO_PORT=9000
BASE_MINIO_CONSOLE_PORT=9001
# Map profile to compose file
get_compose_file() {
    local profile=$1
    case "$profile" in
        dev)
            echo "${REPO_ROOT}/docker-compose.dev.yml"
            ;;
        local)
            echo "${REPO_ROOT}/docker-compose.local.yml"
            ;;
        prod)
            echo "${REPO_ROOT}/docker-compose.yml"
            ;;
        *)
            echo "Error: Unknown profile '$profile'. Valid profiles: dev, local, prod" >&2
            exit 1
            ;;
    esac
}

usage() {
    cat <<EOF
Usage: ./cluster [cluster_num] [-p profile] [tenant mode] <command> [args...]
       ./cluster list

Cluster number is optional:
  - For 'up': reuses existing cluster, or auto-selects next available
  - For other commands: auto-selects if only one cluster running

Profiles:
  dev       docker-compose.dev.yml (default)
  local     docker-compose.local.yml
  prod      docker-compose.yml

Tenant mode:
  Always defaults to TRACECAT__EE_MULTI_TENANT=true for cluster development.
  .env and shell values are ignored; only the flags below override it.
  --single-tenant                 Set TRACECAT__EE_MULTI_TENANT=false
  --multi-tenant                  Set TRACECAT__EE_MULTI_TENANT=true
  --ee-multi-tenant true|false    Set TRACECAT__EE_MULTI_TENANT explicitly
  --sandbox                       Enable nsjail sandboxing with the ephemeral
                                  executor backend and layer sandbox privileges

Commands:
  up [args]     Start the cluster (e.g., up -d)
                Reuses existing cluster for this worktree if one is running.
                Use --new/-n to force a new cluster instead.
                Use --single-tenant or --multi-tenant to override tenant mode.
                Seeds test@tracecat.com superuser, dev@tracecat.com tenant
                user, and all default tier entitlements by default for the dev
                profile. Use --no-seed to skip.
                Use --default-tier-entitlements all|none|a,b to override.
  seed [args]   Seed or update the dev superuser, tenant user, and default tier
                for an already running cluster. Use
                --default-tier-entitlements all|none|a,b to override.
  down          Stop the cluster (keeps volumes)
  rm            Remove the cluster (down + remove volumes)
  restart [svc] Restart the cluster or specific service
  ps            Show running containers
  logs [svc]    Show logs (optionally for specific service)
  attach <svc>  Attach to a running service container (e.g., attach api)
  db            Open lazysql to the cluster's PostgreSQL database
  docs [args]   Start the Mintlify docs preview from docs/
  open          Open the cluster UI in the default browser
  ports         Show port mappings for the cluster
  list          List all running Tracecat clusters
  nuke [opts]   Destroy cluster and optionally volumes/images

Nuke options:
  nuke                  Stop cluster and remove volumes
  nuke --images         Also remove images
  nuke --dry-run        Show what would be deleted
  nuke <service>        Remove specific service volume (e.g., nuke minio)
  nuke volumes          List and select volumes to remove

Examples:
  ./cluster up -d                Start cluster and create dev users
  ./cluster up -d --single-tenant
                                  Start cluster with multi-tenancy disabled
  ./cluster --ee-multi-tenant false up -d
                                  Start cluster with explicit tenant mode
  ./cluster --sandbox up -d       Start cluster with nsjail sandboxing
  ./cluster up -d --new          Force a new cluster on next available number
  ./cluster up -d --no-seed      Start cluster without creating dev tenant user
  ./cluster up -d --default-tier-entitlements none
  ./cluster up -d --default-tier-entitlements custom_registry,case_addons
  ./cluster seed                 Seed dev users for a running cluster
  ./cluster seed --default-tier-entitlements none
  ./cluster down                 Stop cluster (keeps volumes)
  ./cluster rm                   Remove cluster and volumes
  ./cluster restart api          Restart the api service
  ./cluster 2 up -d              Start cluster 2 explicitly
  ./cluster 2 -p local up -d     Start cluster 2 with local profile
  ./cluster attach api           Attach shell to api service
  ./cluster open                 Open cluster UI in browser
  ./cluster db                   Open lazysql to the database
  ./cluster docs                 Start Mintlify docs preview
  ./cluster docs --no-open       Start docs preview without opening browser
  ./cluster list                 List all clusters
  ./cluster nuke                 Destroy cluster and volumes (interactive)
  ./cluster nuke minio           Remove only minio volume
  ./cluster nuke --images        Destroy cluster, volumes, and images

Current worktree: ${WORKTREE_ID}
EOF
    exit 0
}

normalize_bool() {
    local value
    value=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')

    case "$value" in
        true|1|yes|y|on)
            echo "true"
            ;;
        false|0|no|n|off)
            echo "false"
            ;;
        *)
            echo "Error: expected a boolean value, got '$1'" >&2
            exit 1
            ;;
    esac
}

# Calculate ports for a given cluster number
calculate_ports() {
    local cluster_num=$1
    local offset=$(( (cluster_num - 1) * 100 ))

    PUBLIC_APP_PORT=$(( BASE_PUBLIC_APP_PORT + offset ))
    UI_PORT=$(( BASE_UI_PORT + offset ))
    PG_PORT=$(( BASE_PG_PORT + offset ))
    REDIS_PORT_HOST=$(( BASE_REDIS_PORT + offset ))
    TEMPORAL_PORT=$(( BASE_TEMPORAL_PORT + offset ))
    API_PORT=$(( BASE_API_PORT + offset ))
    TEMPORAL_UI_PORT=$(( BASE_TEMPORAL_UI_PORT + offset ))
    MINIO_PORT=$(( BASE_MINIO_PORT + offset ))
    MINIO_CONSOLE_PORT=$(( BASE_MINIO_CONSOLE_PORT + offset ))
}

# Build environment variables for docker compose
build_env() {
    local cluster_num=$1
    calculate_ports "$cluster_num"
    local public_app_url_override="${CLUSTER_PUBLIC_APP_URL_OVERRIDE:-}"
    local public_api_url_override="${CLUSTER_PUBLIC_API_URL_OVERRIDE:-}"

    # URLs that depend on ports
    local PUBLIC_APP_URL="http://localhost:${PUBLIC_APP_PORT}"
    if [[ -n "$public_app_url_override" ]]; then
        PUBLIC_APP_URL="${public_app_url_override%/}"
    fi

    local PUBLIC_API_URL="${PUBLIC_APP_URL}/api"
    if [[ -n "$public_api_url_override" ]]; then
        PUBLIC_API_URL="${public_api_url_override%/}"
    fi

    # Export all variables (include worktree ID for isolation)
    export COMPOSE_PROJECT_NAME="tracecat-${WORKTREE_ID}-${cluster_num}"
    export PUBLIC_APP_PORT
    export PUBLIC_APP_URL
    export PUBLIC_API_URL
    export UI_PORT
    export PG_PORT
    export REDIS_PORT_HOST
    export TEMPORAL_PORT
    export API_PORT
    export TEMPORAL_UI_PORT
    export MINIO_PORT
    export MINIO_CONSOLE_PORT

    # Always compute public URLs per cluster so `just`-loaded `.env` values from
    # cluster 1 do not leak into other clusters. Use explicit override vars for
    # tunnels or custom public hosts when needed.
    export TRACECAT__PUBLIC_APP_URL="${PUBLIC_APP_URL}"
    export TRACECAT__PUBLIC_API_URL="${PUBLIC_API_URL}"
    export NEXT_PUBLIC_APP_URL="${PUBLIC_APP_URL}"
    export NEXT_PUBLIC_API_URL="${PUBLIC_API_URL}"

    # Caddy configuration
    export BASE_DOMAIN=":${PUBLIC_APP_PORT}"

    if [[ "$CLUSTER_USE_SANDBOX_COMPOSE" == "true" ]]; then
        export TRACECAT__DISABLE_NSJAIL=false
        export TRACECAT__EXECUTOR_BACKEND=ephemeral
    else
        # Default to direct executor backend for development
        export TRACECAT__EXECUTOR_BACKEND="${TRACECAT__EXECUTOR_BACKEND:-direct}"
    fi

    # Cluster development always defaults to multi-tenant mode. Only the
    # explicit cluster flags (--single-tenant / --multi-tenant /
    # --ee-multi-tenant) override it; .env and shell values are ignored so
    # fresh worktrees behave consistently.
    export TRACECAT__EE_MULTI_TENANT="${CLUSTER_EE_MULTI_TENANT:-true}"
    FEATURE_FLAGS=$(cd "$REPO_ROOT" && uv run python -c "from tracecat.feature_flags.enums import FeatureFlag; print(','.join(f.value for f in FeatureFlag))" 2>/dev/null) || FEATURE_FLAGS=""
    if [[ -n "$FEATURE_FLAGS" ]]; then
        export TRACECAT__FEATURE_FLAGS="$FEATURE_FLAGS"
    fi
}

# Show port mappings for a cluster
show_ports() {
    local cluster_num=$1
    calculate_ports "$cluster_num"

    cat <<EOF
Cluster ${WORKTREE_ID}-${cluster_num} port mappings:
  UI (Caddy):      http://localhost:${PUBLIC_APP_PORT}
  API:             http://localhost:${PUBLIC_APP_PORT}/api (internal: ${API_PORT})
  PostgreSQL:      localhost:${PG_PORT}
  Redis:           localhost:${REDIS_PORT_HOST}
  Temporal:        localhost:${TEMPORAL_PORT}
  Temporal UI:     http://localhost:${TEMPORAL_UI_PORT}
  MinIO:           localhost:${MINIO_PORT}
  MinIO Console:   http://localhost:${MINIO_CONSOLE_PORT}
  MCP:             http://localhost:${PUBLIC_APP_PORT}/mcp
EOF
}

# List all running Tracecat clusters
list_clusters() {
    echo "Running Tracecat clusters (worktree: ${WORKTREE_ID}):"
    echo ""

    local found=false
    local current_worktree_prefix="tracecat-${WORKTREE_ID}-"

    for project in $(docker compose ls --format json 2>/dev/null | jq -r '.[].Name' 2>/dev/null || docker compose ls -q 2>/dev/null); do
        # Match any tracecat cluster: tracecat-{worktree}-{num}
        if [[ "$project" =~ ^tracecat-(.+)-([0-9]+)$ ]]; then
            found=true
            local project_worktree="${BASH_REMATCH[1]}"
            local cluster_num="${BASH_REMATCH[2]}"
            calculate_ports "$cluster_num"
            local marker=""
            [[ "$project" == "${current_worktree_prefix}${cluster_num}" ]] && marker=" (this worktree)"
            local portless_suffix=""
            if portless_enabled; then
                portless_suffix="  |  $(portless_url "$cluster_num" "$project_worktree")"
            fi
            echo "  ${project}: http://localhost:${PUBLIC_APP_PORT}${portless_suffix}${marker}"
        fi
    done

    if [[ "$found" == "false" ]]; then
        echo "  No clusters running"
    fi
}

# Main
if [[ $# -lt 1 ]]; then
    usage
fi

# Handle 'list' command specially
if [[ "$1" == "list" ]]; then
    list_clusters
    portless_install_tip
    exit 0
fi

# Check if first arg is a cluster number or a command/flag
CLUSTER_NUM=""
AUTO_SELECT=true

if [[ "$1" =~ ^[0-9]+$ ]]; then
    # First arg is a number
    CLUSTER_NUM="$1"
    AUTO_SELECT=false
    if [[ "$CLUSTER_NUM" -lt 1 ]] || [[ "$CLUSTER_NUM" -gt 99 ]]; then
        echo "Error: Cluster number must be between 1 and 99"
        exit 1
    fi
    shift
fi

if [[ $# -lt 1 ]]; then
    usage
fi

# Parse cluster-level flags that appear before the command.
while [[ $# -gt 0 ]]; do
    case "$1" in
        -p|--profile)
            if [[ $# -lt 2 ]]; then
                echo "Error: -p/--profile requires a profile name" >&2
                exit 1
            fi
            PROFILE="$2"
            shift 2
            ;;
        --single-tenant)
            CLUSTER_EE_MULTI_TENANT="false"
            shift
            ;;
        --multi-tenant)
            CLUSTER_EE_MULTI_TENANT="true"
            shift
            ;;
        --ee-multi-tenant)
            if [[ $# -lt 2 ]]; then
                echo "Error: --ee-multi-tenant requires true or false" >&2
                exit 1
            fi
            CLUSTER_EE_MULTI_TENANT=$(normalize_bool "$2")
            shift 2
            ;;
        --ee-multi-tenant=*)
            CLUSTER_EE_MULTI_TENANT=$(normalize_bool "${1#*=}")
            shift
            ;;
        --sandbox)
            CLUSTER_USE_SANDBOX_COMPOSE=true
            shift
            ;;
        *)
            break
            ;;
    esac
done

if [[ $# -lt 1 ]]; then
    usage
fi

COMMAND="$1"
shift

# Handle 'docs' command before cluster auto-selection. The docs preview is
# independent of Docker Compose and should not require a running cluster.
if [[ "$COMMAND" == "docs" ]]; then
    if ! command -v mint >/dev/null 2>&1; then
        echo "Error: Mintlify CLI is not installed" >&2
        echo "Install it with: npm i -g mint" >&2
        exit 1
    fi

    echo "Starting Mintlify docs preview from ${REPO_ROOT}/docs ..."
    cd "${REPO_ROOT}/docs"
    if portless_enabled; then
        DOCS_PORTLESS_ALIAS=$(portless_docs_alias_name)
        trap 'portless alias --remove "$DOCS_PORTLESS_ALIAS" >/dev/null 2>&1 || true' EXIT
        mint dev "$@" 2>&1 | while IFS= read -r line; do
            echo "$line"
            if [[ -z "${DOCS_PORTLESS_REGISTERED:-}" && "$line" =~ localhost:([0-9]+) ]]; then
                DOCS_PORTLESS_REGISTERED=1
                portless_register_alias "$DOCS_PORTLESS_ALIAS" "${BASH_REMATCH[1]}" || true
            fi
        done
        exit "${PIPESTATUS[0]}"
    fi

    portless_install_tip
    exec mint dev "$@"
fi

# Parse cluster-level flags that appear after the command, so both of these work:
#   ./cluster --single-tenant up -d
#   ./cluster up -d --single-tenant
FILTERED_ARGS=()
while [[ $# -gt 0 ]]; do
    case "$1" in
        --single-tenant)
            CLUSTER_EE_MULTI_TENANT="false"
            shift
            ;;
        --multi-tenant)
            CLUSTER_EE_MULTI_TENANT="true"
            shift
            ;;
        --ee-multi-tenant)
            if [[ $# -lt 2 ]]; then
                echo "Error: --ee-multi-tenant requires true or false" >&2
                exit 1
            fi
            CLUSTER_EE_MULTI_TENANT=$(normalize_bool "$2")
            shift 2
            ;;
        --ee-multi-tenant=*)
            CLUSTER_EE_MULTI_TENANT=$(normalize_bool "${1#*=}")
            shift
            ;;
        --sandbox)
            CLUSTER_USE_SANDBOX_COMPOSE=true
            shift
            ;;
        *)
            FILTERED_ARGS+=("$1")
            shift
            ;;
    esac
done
if [[ ${#FILTERED_ARGS[@]} -gt 0 ]]; then
    set -- "${FILTERED_ARGS[@]}"
else
    set --
fi

# Parse --new/-n flag from remaining args (only meaningful for 'up')
FORCE_NEW_CLUSTER=false
if [[ "$COMMAND" == "up" ]]; then
    FILTERED_ARGS=()
    for arg in "$@"; do
        if [[ "$arg" == "--new" || "$arg" == "-n" ]]; then
            FORCE_NEW_CLUSTER=true
        else
            FILTERED_ARGS+=("$arg")
        fi
    done
    set -- ${FILTERED_ARGS[@]+"${FILTERED_ARGS[@]}"}
fi

# Auto-select cluster number if not specified
if [[ "$AUTO_SELECT" == "true" ]]; then
    if [[ "$COMMAND" == "up" ]]; then
        if [[ "$FORCE_NEW_CLUSTER" == "true" ]]; then
            CLUSTER_NUM=$(get_next_cluster_num)
            echo "Auto-selected new cluster ${CLUSTER_NUM} (global) for worktree '${WORKTREE_ID}'"
        else
            # Reuse existing cluster for this worktree if one is running
            running=$(get_running_clusters)
            count=0
            if [[ -n "$running" ]]; then
                count=$(echo "$running" | wc -l | tr -d ' ')
            fi

            if [[ "$count" -eq 0 ]]; then
                CLUSTER_NUM=$(get_next_cluster_num)
                echo "No existing cluster — auto-selected cluster ${CLUSTER_NUM} for worktree '${WORKTREE_ID}'"
            elif [[ "$count" -eq 1 ]]; then
                CLUSTER_NUM="$running"
                echo "Reusing existing cluster ${CLUSTER_NUM} for worktree '${WORKTREE_ID}'"
            else
                echo "Multiple clusters running — select one:" >&2
                CLUSTER_NUM=$(echo "$running" | select_cluster_interactive)
            fi
        fi
    elif [[ "$COMMAND" == "nuke" ]]; then
        # For nuke, try to find any cluster (running or with leftover volumes)
        nuke_running=$(get_running_clusters)
        nuke_count=0
        if [[ -n "$nuke_running" ]]; then
            nuke_count=$(echo "$nuke_running" | wc -l | tr -d ' ')
        fi

        if [[ "$nuke_count" -eq 1 ]]; then
            CLUSTER_NUM="$nuke_running"
        elif [[ "$nuke_count" -gt 1 ]]; then
            echo "Multiple clusters running — select one to nuke:" >&2
            CLUSTER_NUM=$(echo "$nuke_running" | select_cluster_interactive)
        else
            # No running cluster, check for orphaned volumes
            ORPHAN_VOLUME=$(docker volume ls --format "{{.Name}}" | grep "^tracecat-${WORKTREE_ID}-" | head -1 || true)
            if [[ -n "$ORPHAN_VOLUME" && "$ORPHAN_VOLUME" =~ ^tracecat-${WORKTREE_ID}-([0-9]+)_ ]]; then
                CLUSTER_NUM="${BASH_REMATCH[1]}"
                echo "Found orphaned volumes for cluster ${CLUSTER_NUM}"
            else
                CLUSTER_NUM="1"
                echo "No clusters or volumes found, defaulting to cluster 1"
            fi
        fi
    else
        CLUSTER_NUM=$(auto_select_cluster "$COMMAND")
    fi
fi

# Get compose file for profile
COMPOSE_FILE=$(get_compose_file "$PROFILE")
COMPOSE_FILES=(-f "$COMPOSE_FILE")
if [[ "$CLUSTER_USE_SANDBOX_COMPOSE" == "true" ]]; then
    COMPOSE_FILES+=(-f "${REPO_ROOT}/docker-compose.sandbox.yml")
fi

# Handle 'ports' command
if [[ "$COMMAND" == "ports" ]]; then
    show_ports "$CLUSTER_NUM"
    if portless_enabled; then
        echo "  Portless URL:    $(portless_url "$CLUSTER_NUM")"
    else
        portless_install_tip
    fi
    exit 0
fi

# Handle 'open' command - open cluster URL in default browser
if [[ "$COMMAND" == "open" ]]; then
    calculate_ports "$CLUSTER_NUM"
    if portless_enabled; then
        if portless_register "$CLUSTER_NUM" "$PUBLIC_APP_PORT" >/dev/null; then
            target_url="$(portless_url "$CLUSTER_NUM")"
        else
            target_url="http://localhost:${PUBLIC_APP_PORT}"
        fi
    else
        target_url="http://localhost:${PUBLIC_APP_PORT}"
        portless_install_tip
    fi
    echo "Opening ${target_url} ..."
    open "$target_url"
    exit 0
fi

# Handle 'db' command - open TablePlus to postgres
if [[ "$COMMAND" == "db" ]]; then
    if [[ ! -d "/Applications/TablePlus.app" ]]; then
        echo "Error: TablePlus is not installed" >&2
        echo "Install from: https://tableplus.com or brew install --cask tableplus" >&2
        exit 1
    fi

    # Source .env for credentials if it exists
    if [[ -f "$ENV_FILE" ]]; then
        set -a
        # shellcheck disable=SC1090
        source "$ENV_FILE"
        set +a
    fi

    calculate_ports "$CLUSTER_NUM"
    pg_user="${TRACECAT__POSTGRES_USER:-postgres}"
    pg_pass="${TRACECAT__POSTGRES_PASSWORD:-postgres}"

    echo "Opening TablePlus for cluster ${WORKTREE_ID}-${CLUSTER_NUM} (port ${PG_PORT})..."
    open -a TablePlus "postgresql://${pg_user}:${pg_pass}@localhost:${PG_PORT}/postgres?sslmode=disable"
    exit 0
fi

# Handle 'attach' command
if [[ "$COMMAND" == "attach" ]]; then
    if [[ $# -lt 1 ]]; then
        echo "Error: attach requires a service name (e.g., attach api)"
        exit 1
    fi
    SERVICE="$1"
    shift
    build_env "$CLUSTER_NUM"

    # Try bash first, fall back to sh
    exec docker compose "${COMPOSE_FILES[@]}" -p "tracecat-${WORKTREE_ID}-${CLUSTER_NUM}" exec "$SERVICE" /bin/bash "$@" 2>/dev/null \
        || exec docker compose "${COMPOSE_FILES[@]}" -p "tracecat-${WORKTREE_ID}-${CLUSTER_NUM}" exec "$SERVICE" /bin/sh "$@"
fi

# Handle 'restart' command
if [[ "$COMMAND" == "restart" ]]; then
    build_env "$CLUSTER_NUM"
    exec docker compose "${COMPOSE_FILES[@]}" -p "tracecat-${WORKTREE_ID}-${CLUSTER_NUM}" restart "$@"
fi

# Handle 'rm' command - down + remove volumes
if [[ "$COMMAND" == "rm" ]]; then
    build_env "$CLUSTER_NUM"
    portless_enabled && portless_unregister "$CLUSTER_NUM"
    exec docker compose "${COMPOSE_FILES[@]}" -p "tracecat-${WORKTREE_ID}-${CLUSTER_NUM}" down --volumes --remove-orphans "$@"
fi

# Handle 'down' command - stop containers and remove the stale portless alias.
if [[ "$COMMAND" == "down" ]]; then
    build_env "$CLUSTER_NUM"
    portless_enabled && portless_unregister "$CLUSTER_NUM"
    exec docker compose "${COMPOSE_FILES[@]}" -p "tracecat-${WORKTREE_ID}-${CLUSTER_NUM}" down "$@"
fi

# Handle 'nuke' command - destroy cluster and optionally volumes/images
if [[ "$COMMAND" == "nuke" ]]; then
    build_env "$CLUSTER_NUM"
    PROJECT_NAME="tracecat-${WORKTREE_ID}-${CLUSTER_NUM}"

    # Parse nuke options
    NUKE_IMAGES=false
    DRY_RUN=false
    TARGET_SERVICE=""

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --images)
                NUKE_IMAGES=true
                shift
                ;;
            --dry-run)
                DRY_RUN=true
                shift
                ;;
            volumes)
                # List volumes for this project (exact prefix match)
                echo "Volumes for ${PROJECT_NAME}:"
                docker volume ls --filter "name=${PROJECT_NAME}" --format "{{.Name}}" | grep "^${PROJECT_NAME}_" | sed 's/^/  /' || echo "  (none)"
                exit 0
                ;;
            *)
                TARGET_SERVICE="$1"
                shift
                ;;
        esac
    done

    # Get all volumes for this project (exact prefix match to avoid cluster1 matching cluster10)
    get_project_volumes() {
        docker volume ls --filter "name=${PROJECT_NAME}" --format "{{.Name}}" | grep "^${PROJECT_NAME}_" || true
    }

    # Get volume for a specific service
    get_service_volume() {
        local service=$1
        docker volume ls --filter "name=${PROJECT_NAME}" --format "{{.Name}}" | grep "^${PROJECT_NAME}_" | grep -i "${service}" || true
    }

    if [[ -n "$TARGET_SERVICE" ]]; then
        # Nuke specific service volume
        VOLUMES=$(get_service_volume "$TARGET_SERVICE")
        if [[ -z "$VOLUMES" ]]; then
            echo "No volumes found matching '${TARGET_SERVICE}' for ${PROJECT_NAME}"
            echo ""
            echo "Available volumes:"
            get_project_volumes | sed 's/^/  /'
            exit 1
        fi

        echo "Volumes to remove:"
        printf '  %s\n' "${VOLUMES//$'\n'/$'\n'  }"

        if [[ "$DRY_RUN" == "true" ]]; then
            echo ""
            echo "[dry-run] Would remove the above volumes"
            exit 0
        fi

        echo ""
        read -p "Remove these volumes? [y/N] " -n 1 -r
        echo ""
        if [[ $REPLY =~ ^[Yy]$ ]]; then
            echo "$VOLUMES" | xargs docker volume rm
            echo "Done."
        else
            echo "Aborted."
        fi
        exit 0
    fi

    # Full nuke
    echo "=== NUKE: ${PROJECT_NAME} ==="
    echo ""
    echo "This will destroy:"
    echo "  - All containers"
    echo "  - All volumes:"
    get_project_volumes | sed 's/^/      /'

    if [[ "$NUKE_IMAGES" == "true" ]]; then
        echo "  - All images (built for this project)"
    fi

    if [[ "$DRY_RUN" == "true" ]]; then
        echo ""
        echo "[dry-run] Would execute the above"
        exit 0
    fi

    echo ""
    read -p "Are you sure? This cannot be undone. [y/N] " -n 1 -r
    echo ""
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        echo "Aborted."
        exit 0
    fi

    echo ""
    echo "Stopping containers..."
    portless_enabled && portless_unregister "$CLUSTER_NUM"
    docker compose "${COMPOSE_FILES[@]}" -p "$PROJECT_NAME" down --remove-orphans

    echo ""
    echo "Removing volumes..."
    VOLUMES=$(get_project_volumes)
    if [[ -n "$VOLUMES" ]]; then
        echo "$VOLUMES" | xargs docker volume rm
    fi

    if [[ "$NUKE_IMAGES" == "true" ]]; then
        echo ""
        echo "Removing images..."
        # Get images built for this project (tagged with project name)
        IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep "^${PROJECT_NAME}" || true)
        if [[ -n "$IMAGES" ]]; then
            echo "$IMAGES" | xargs docker rmi -f
        else
            echo "  No project-specific images found"
        fi
    fi

    echo ""
    echo "=== NUKE COMPLETE ==="
    exit 0
fi

# If portless is available and the user is bringing the cluster up, register a
# stable .localhost alias and steer the app's PUBLIC_APP_URL through it. Must
# happen before build_env so the override flows into TRACECAT__PUBLIC_APP_URL,
# NEXT_PUBLIC_APP_URL, etc.
if [[ "$COMMAND" == "up" ]] && portless_enabled && [[ -z "${CLUSTER_PUBLIC_APP_URL_OVERRIDE:-}" ]]; then
    calculate_ports "$CLUSTER_NUM"
    if portless_register "$CLUSTER_NUM" "$PUBLIC_APP_PORT"; then
        CLUSTER_PUBLIC_APP_URL_OVERRIDE="$(portless_url "$CLUSTER_NUM")"
        export CLUSTER_PUBLIC_APP_URL_OVERRIDE
    fi
fi

if [[ "$COMMAND" == "up" ]]; then
    portless_install_tip
fi

# Build environment and run docker compose
build_env "$CLUSTER_NUM"

# Auto-create .env from .env.example if not present
ENV_SCRIPT="${REPO_ROOT}/env.sh"
if [[ ! -f "$ENV_FILE" ]] && [[ -f "$ENV_SCRIPT" ]]; then
    echo "No .env file found. Running env.sh to create one with dev defaults..."
    # Pass dev defaults: production=n, url=localhost, ssl=n, email=test@tracecat.com
    (cd "$REPO_ROOT" && bash "$ENV_SCRIPT" <<< $'n\n\nn\ntest@tracecat.com')

    # Auto-populate all feature flags for dev environment
    FEATURE_FLAGS=$(cd "$REPO_ROOT" && uv run python -c "from tracecat.feature_flags.enums import FeatureFlag; print(','.join(f.value for f in FeatureFlag))" 2>/dev/null) || FEATURE_FLAGS=""
    if [[ -n "$FEATURE_FLAGS" ]]; then
        echo "TRACECAT__FEATURE_FLAGS=${FEATURE_FLAGS}" >> "$ENV_FILE"
        echo "Enabled all feature flags: $FEATURE_FLAGS"
    fi
fi

# Source the .env file for other variables (but our exports take precedence)
if [[ -f "$ENV_FILE" ]]; then
    set -a
    # shellcheck disable=SC1090
    source "$ENV_FILE"
    set +a
    # Re-apply our overrides (source may have overwritten them)
    build_env "$CLUSTER_NUM"
fi

if [[ "$COMMAND" == "up" ]]; then
    echo "EE multi-tenant: ${TRACECAT__EE_MULTI_TENANT}"
fi

# Sync dependencies before starting cluster or running the standalone dev seed.
if [[ "$COMMAND" == "up" || "$COMMAND" == "seed" ]]; then
    echo "Syncing dependencies..."
    (cd "$REPO_ROOT" && uv sync --group admin --quiet)
fi
if [[ "$COMMAND" == "up" ]]; then
    (cd "$REPO_ROOT" && pnpm -C frontend install > /dev/null 2>&1)
fi

# Check for seed flags in up command. Dev profile seeds by default.
SEED_USER=false
if [[ "$COMMAND" == "up" && "$PROFILE" == "dev" ]]; then
    SEED_USER=true
fi
DETACHED_UP=false
DEFAULT_TIER_ENTITLEMENTS="${TRACECAT__DEV_DEFAULT_TIER_ENTITLEMENTS:-all}"
UP_ARGS=()
if [[ "$COMMAND" == "up" ]]; then
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -d|--detach)
                DETACHED_UP=true
                UP_ARGS+=("$1")
                shift
                ;;
            --seed)
                SEED_USER=true
                shift
                ;;
            --no-seed)
                SEED_USER=false
                shift
                ;;
            --default-tier-entitlements)
                if [[ $# -lt 2 ]]; then
                    echo "Error: --default-tier-entitlements requires a value" >&2
                    exit 1
                fi
                DEFAULT_TIER_ENTITLEMENTS="$2"
                shift 2
                ;;
            --default-tier-entitlements=*)
                DEFAULT_TIER_ENTITLEMENTS="${1#*=}"
                shift
                ;;
            *)
                UP_ARGS+=("$1")
                shift
                ;;
        esac
    done
    set -- ${UP_ARGS[@]+"${UP_ARGS[@]}"}
elif [[ "$COMMAND" == "seed" ]]; then
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --default-tier-entitlements)
                if [[ $# -lt 2 ]]; then
                    echo "Error: --default-tier-entitlements requires a value" >&2
                    exit 1
                fi
                DEFAULT_TIER_ENTITLEMENTS="$2"
                shift 2
                ;;
            --default-tier-entitlements=*)
                DEFAULT_TIER_ENTITLEMENTS="${1#*=}"
                shift
                ;;
            *)
                echo "Error: unknown seed argument '$1'" >&2
                exit 1
                ;;
        esac
    done
fi

# Function to seed a tenant dev user after cluster starts
seed_dev_user() {
    local api_url="http://localhost:${PUBLIC_APP_PORT}/api"
    local max_attempts=60
    local attempt=1

    echo "[dev-seed] Waiting for API to be ready..."
    while [[ $attempt -le $max_attempts ]]; do
        # Use /ready endpoint which checks full startup completion (not /health which returns immediately)
        local status_code
        status_code=$(curl -s -o /dev/null -w "%{http_code}" "${api_url}/ready" 2>/dev/null || echo "000")
        if [[ "$status_code" == "200" ]]; then
            echo "[dev-seed] API is ready"
            break
        fi
        sleep 2
        attempt=$((attempt + 1))
    done

    if [[ $attempt -gt $max_attempts ]]; then
        echo "[dev-seed] Warning: API did not become ready in time, skipping dev seed"
        return 1
    fi

    local seed_email="${TRACECAT__DEV_USER_EMAIL:-dev@tracecat.com}"
    local seed_password="${TRACECAT__DEV_USER_PASSWORD:-password1234}"
    local seed_superuser_email="${TRACECAT__DEV_SUPERUSER_EMAIL:-test@tracecat.com}"
    local seed_superuser_password="${TRACECAT__DEV_SUPERUSER_PASSWORD:-password1234}"
    local seed_org_role="${TRACECAT__DEV_ORG_ROLE:-organization-owner}"
    local seed_workspace_role="${TRACECAT__DEV_WORKSPACE_ROLE:-workspace-admin}"
    local seed_default_tier_entitlements="${DEFAULT_TIER_ENTITLEMENTS}"
    local seed_db_uri="postgresql+psycopg://postgres:postgres@localhost:${PG_PORT}/postgres"

    echo "[dev-seed] Seeding dev superuser (${seed_superuser_email}), tenant user (${seed_email}), and default tier (${seed_default_tier_entitlements})..."
    (
        cd "$REPO_ROOT"
        TRACECAT__DB_URI="$seed_db_uri" \
            uv run tracecat admin create-dev-user \
            --email "$seed_email" \
            --password "$seed_password" \
            --superuser-email "$seed_superuser_email" \
            --superuser-password "$seed_superuser_password" \
            --default-tier-entitlements "$seed_default_tier_entitlements" \
            --org-role "$seed_org_role" \
            --workspace-role "$seed_workspace_role"
    )
}

if [[ "$COMMAND" == "seed" ]]; then
    seed_dev_user
    exit $?
fi

# Run docker compose with the configured environment
if [[ "$SEED_USER" == "true" && "$COMMAND" == "up" ]]; then
    if [[ "$DETACHED_UP" == "true" ]]; then
        docker compose "${COMPOSE_FILES[@]}" -p "tracecat-${WORKTREE_ID}-${CLUSTER_NUM}" "$COMMAND" "$@"
        seed_dev_user
    else
        # Keep compose attached while a background seeder waits for API readiness.
        seed_dev_user &
        exec docker compose "${COMPOSE_FILES[@]}" -p "tracecat-${WORKTREE_ID}-${CLUSTER_NUM}" "$COMMAND" "$@"
    fi
else
    exec docker compose "${COMPOSE_FILES[@]}" -p "tracecat-${WORKTREE_ID}-${CLUSTER_NUM}" "$COMMAND" "$@"
fi
