#!/usr/bin/env bash

# Small helper for an on-demand, Docker Compose based Pterodactyl installation.
#
# Features:
#   - Starts/stops the Pterodactyl Compose stack.
#   - Waits for Panel and Wings to become reachable.
#   - Starts selected "core" servers through the Pterodactyl Client API.
#   - Gracefully stops every running Pterodactyl server before shutting down.
#   - Opens the Panel in Firefox/Flatpak Firefox/xdg-open.
#   - Shows Compose status, running game servers, and logs.
#
# This is intentionally a personal-workstation wrapper, not a generic installer.

set -uo pipefail

# -----------------------------------------------------------------------------
# Configuration
# -----------------------------------------------------------------------------

# Docker Compose project.
PTERO_DIR="${PTERO_DIR:-$HOME/.local/share/pterodactyl}"
COMPOSE_FILE="${COMPOSE_FILE:-$PTERO_DIR/compose.yml}"

# Local endpoints used by this workstation setup.
PANEL_URL="${PANEL_URL:-http://panel.pterodactyl.localhost:8085}"
WINGS_URL="${WINGS_URL:-http://wings.pterodactyl.test:8080}"

# Client API key. The key should be an administrator key because the script
# requests type=admin-all when resolving and stopping servers.
API_KEY_FILE="${API_KEY_FILE:-$HOME/.config/pterodactyl/client-api-key}"

# Wings-managed Docker network containing game-server containers.
GAME_NETWORK="${GAME_NETWORK:-pterodactyl_nw}"

# Timeouts, in seconds.
PANEL_START_TIMEOUT="${PANEL_START_TIMEOUT:-60}"
WINGS_START_TIMEOUT="${WINGS_START_TIMEOUT:-30}"
CORE_START_TIMEOUT="${CORE_START_TIMEOUT:-120}"
SHUTDOWN_TIMEOUT="${SHUTDOWN_TIMEOUT:-300}"

# Open the Panel after a successful start. Set OPEN_BROWSER=0 to disable.
OPEN_BROWSER="${OPEN_BROWSER:-1}"

# Servers that should always be running whenever Pterodactyl is started.
# Names must match the names shown in the Panel exactly.
CORE_SERVERS=(
    "Lobby"
    "Velocity"
)

# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------

die() {
    echo "pterodactyl: $*" >&2
    exit 1
}

require_command() {
    command -v "$1" >/dev/null 2>&1 ||
        die "required command not found: $1"
}

compose() {
    docker compose \
        --project-directory "$PTERO_DIR" \
        -f "$COMPOSE_FILE" \
        "$@"
}

api_request() {
    local method="$1"
    local endpoint="$2"
    local body="${3:-}"

    [[ -r "$API_KEY_FILE" ]] ||
        die "API key not found or unreadable: $API_KEY_FILE"

    local key
    key="$(<"$API_KEY_FILE")"

    local args=(
        --silent
        --show-error
        --fail-with-body
        --request "$method"
        --header "Authorization: Bearer $key"
        --header "Accept: application/json"
    )

    if [[ -n "$body" ]]; then
        args+=(
            --header "Content-Type: application/json"
            --data "$body"
        )
    fi

    curl "${args[@]}" "${PANEL_URL}${endpoint}"
}

fetch_servers() {
    api_request GET '/api/client?type=admin-all&per_page=100'
}

running_servers() {
    docker ps \
        --filter "network=$GAME_NETWORK" \
        --format '{{.ID}}\t{{.Names}}'
}

running_server_uuids() {
    docker ps \
        --filter "network=$GAME_NETWORK" \
        --format '{{.Names}}'
}

# -----------------------------------------------------------------------------
# Health checks
# -----------------------------------------------------------------------------

wait_for_panel() {
    local deadline=$((SECONDS + PANEL_START_TIMEOUT))

    printf "Waiting for Panel"

    while (( SECONDS < deadline )); do
        if curl -fsS "$PANEL_URL" >/dev/null 2>&1; then
            echo
            return 0
        fi

        printf "."
        sleep 1
    done

    echo
    return 1
}

wait_for_wings() {
    local deadline=$((SECONDS + WINGS_START_TIMEOUT))
    local status

    printf "Waiting for Wings"

    while (( SECONDS < deadline )); do
        status="$(
            curl \
                -sS \
                -o /dev/null \
                -w '%{http_code}' \
                "$WINGS_URL" 2>/dev/null || true
        )"

        # An unauthenticated request to a healthy Wings instance returns 401.
        if [[ "$status" == "401" ]]; then
            echo
            return 0
        fi

        printf "."
        sleep 1
    done

    echo
    return 1
}

# -----------------------------------------------------------------------------
# Server lookup / control
# -----------------------------------------------------------------------------

resolve_server_identifier() {
    local name="$1"
    local servers="$2"

    mapfile -t matches < <(
        jq -r \
            --arg name "$name" \
            '.data[]
             | select(.attributes.name == $name)
             | .attributes.identifier' \
            <<< "$servers"
    )

    if (( ${#matches[@]} == 0 )); then
        echo "Could not find Pterodactyl server: $name" >&2
        return 1
    fi

    if (( ${#matches[@]} > 1 )); then
        echo "More than one Pterodactyl server is named: $name" >&2
        return 1
    fi

    printf '%s\n' "${matches[0]}"
}

server_state() {
    local identifier="$1"

    api_request \
        GET \
        "/api/client/servers/$identifier/resources" \
        | jq -r '.attributes.current_state'
}

wait_for_server_running() {
    local name="$1"
    local identifier="$2"
    local deadline=$((SECONDS + CORE_START_TIMEOUT))
    local state="unknown"

    printf "Waiting for %s" "$name"

    while (( SECONDS < deadline )); do
        state="$(
            server_state "$identifier" 2>/dev/null ||
            printf 'unknown'
        )"

        if [[ "$state" == "running" ]]; then
            echo
            return 0
        fi

        printf "."
        sleep 2
    done

    echo
    echo "$name did not reach the running state within ${CORE_START_TIMEOUT}s."
    echo "Last reported state: $state"
    return 1
}

start_server() {
    local name="$1"
    local identifier="$2"

    local state
    state="$(
        server_state "$identifier" 2>/dev/null ||
        printf 'unknown'
    )"

    case "$state" in
        running)
            echo "$name is already running."
            return 0
            ;;

        starting)
            echo "$name is already starting."
            ;;

        stopping)
            local deadline=$((SECONDS + CORE_START_TIMEOUT))

            printf "Waiting for %s to finish stopping" "$name"

            while (( SECONDS < deadline )); do
                state="$(
                    server_state "$identifier" 2>/dev/null ||
                    printf 'unknown'
                )"

                if [[ "$state" == "offline" ]]; then
                    echo
                    break
                fi

                printf "."
                sleep 2
            done

            if [[ "$state" != "offline" ]]; then
                echo
                echo "$name did not finish stopping."
                return 1
            fi

            echo "Starting $name..."

            api_request \
                POST \
                "/api/client/servers/$identifier/power" \
                '{"signal":"start"}' \
                >/dev/null ||
                return 1
            ;;

        *)
            echo "Starting $name..."

            api_request \
                POST \
                "/api/client/servers/$identifier/power" \
                '{"signal":"start"}' \
                >/dev/null ||
                return 1
            ;;
    esac

    wait_for_server_running "$name" "$identifier"
}

start_core_servers() {
    (( ${#CORE_SERVERS[@]} > 0 )) || return 0

    require_command jq

    echo "Fetching Pterodactyl servers..."

    local servers
    servers="$(fetch_servers)" || {
        echo "Failed to query the Pterodactyl API."
        return 1
    }

    local name
    local identifier

    for name in "${CORE_SERVERS[@]}"; do
        identifier="$(
            resolve_server_identifier "$name" "$servers"
        )" || return 1

        start_server "$name" "$identifier" ||
            return 1
    done

    echo "Core servers are ready."
}

# -----------------------------------------------------------------------------
# Graceful shutdown
# -----------------------------------------------------------------------------

gracefully_stop_servers() {
    mapfile -t running < <(running_server_uuids)

    if (( ${#running[@]} == 0 )); then
        echo "No game servers are running."
        return 0
    fi

    require_command jq

    echo "Fetching server information..."

    local servers
    servers="$(fetch_servers)" || {
        echo "Failed to query the Pterodactyl API."
        return 1
    }

    local failures=0
    local uuid
    local identifier
    local name

    for uuid in "${running[@]}"; do
        identifier="$(
            jq -r \
                --arg uuid "$uuid" \
                '.data[]
                 | select(.attributes.uuid == $uuid)
                 | .attributes.identifier' \
                <<< "$servers" \
                | head -n1
        )"

        name="$(
            jq -r \
                --arg uuid "$uuid" \
                '.data[]
                 | select(.attributes.uuid == $uuid)
                 | .attributes.name' \
                <<< "$servers" \
                | head -n1
        )"

        if [[ -z "$identifier" || "$identifier" == "null" ]]; then
            echo "Could not map running container $uuid to a Panel server."
            failures=1
            continue
        fi

        echo "Stopping: ${name:-$uuid}"

        if ! api_request \
            POST \
            "/api/client/servers/$identifier/power" \
            '{"signal":"stop"}' \
            >/dev/null
        then
            echo "Failed to send stop signal to: ${name:-$uuid}"
            failures=1
        fi
    done

    if (( failures != 0 )); then
        echo
        echo "One or more servers could not be stopped."
        return 1
    fi

    echo
    echo "Waiting for game servers to shut down gracefully..."

    local deadline=$((SECONDS + SHUTDOWN_TIMEOUT))

    while (( SECONDS < deadline )); do
        mapfile -t remaining < <(running_server_uuids)

        if (( ${#remaining[@]} == 0 )); then
            printf '\r%-60s\r' ""
            echo "All game servers are offline."
            return 0
        fi

        printf '\rWaiting... %d server(s) still running   ' \
            "${#remaining[@]}"

        sleep 2
    done

    echo
    echo
    echo "Timed out waiting for graceful shutdown."
    echo "The control plane will NOT be stopped."
    echo
    echo "Still running:"

    printf '  %s\n' "${remaining[@]}"

    return 1
}

# -----------------------------------------------------------------------------
# Browser
# -----------------------------------------------------------------------------

open_panel() {
    [[ "$OPEN_BROWSER" != "0" ]] || return 0

    if command -v firefox >/dev/null 2>&1; then
        firefox --new-tab "$PANEL_URL" >/dev/null 2>&1 &
        disown
        return 0
    fi

    if command -v flatpak >/dev/null 2>&1 &&
       flatpak info org.mozilla.firefox >/dev/null 2>&1; then
        flatpak run org.mozilla.firefox \
            --new-tab "$PANEL_URL" \
            >/dev/null 2>&1 &
        disown
        return 0
    fi

    if command -v xdg-open >/dev/null 2>&1; then
        xdg-open "$PANEL_URL" >/dev/null 2>&1 &
        disown
        return 0
    fi

    echo "Could not find a browser."
    echo "$PANEL_URL"
}

# -----------------------------------------------------------------------------
# Commands
# -----------------------------------------------------------------------------

start() {
    require_command docker
    require_command curl

    [[ -f "$COMPOSE_FILE" ]] ||
        die "Compose file not found: $COMPOSE_FILE"

    echo "Starting Pterodactyl..."

    compose up -d ||
        die "failed to start Compose stack"

    if ! wait_for_panel; then
        echo "Panel did not become reachable."
        echo
        compose ps
        echo
        echo "Try:"
        echo "  pterodactyl logs"
        exit 1
    fi

    if ! wait_for_wings; then
        echo "Wings did not become reachable."
        echo
        compose ps
        echo
        echo "Try:"
        echo "  pterodactyl logs wings"
        exit 1
    fi

    if ! start_core_servers; then
        echo
        echo "Failed to start one or more core servers."
        echo "The Pterodactyl control plane has been left running."
        echo "Check the Panel for details."
        exit 1
    fi

    echo
    echo "Pterodactyl is ready."

    open_panel
}

stop() {
    require_command docker
    require_command curl

    if ! compose ps --status running --quiet | grep -q .; then
        echo "Pterodactyl is already stopped."
        return 0
    fi

    if ! gracefully_stop_servers; then
        echo
        echo "Refusing to stop the Pterodactyl control plane."
        return 2
    fi

    echo
    echo "Stopping Pterodactyl..."

    compose down ||
        die "failed to stop Compose stack"

    echo "Pterodactyl stopped."
}

status() {
    require_command docker

    compose ps

    mapfile -t rows < <(running_servers)

    (( ${#rows[@]} > 0 )) || return 0

    echo
    echo "Running game servers:"

    # Resolve UUIDs to friendly names when the Panel/API is reachable.
    local servers=""
    if command -v curl >/dev/null 2>&1 &&
       command -v jq >/dev/null 2>&1 &&
       [[ -r "$API_KEY_FILE" ]] &&
       curl -fsS "$PANEL_URL" >/dev/null 2>&1; then
        servers="$(fetch_servers 2>/dev/null || true)"
    fi

    printf '%-14s  %s\n' "CONTAINER" "SERVER"

    local row
    local container_id
    local uuid
    local name

    for row in "${rows[@]}"; do
        IFS=$'\t' read -r container_id uuid <<< "$row"

        name=""

        if [[ -n "$servers" ]]; then
            name="$(
                jq -r \
                    --arg uuid "$uuid" \
                    '.data[]
                     | select(.attributes.uuid == $uuid)
                     | .attributes.name' \
                    <<< "$servers" \
                    | head -n1
            )"
        fi

        [[ -n "$name" && "$name" != "null" ]] ||
            name="$uuid"

        printf '%-14s  %s\n' "$container_id" "$name"
    done
}

logs() {
    require_command docker

    if [[ $# -gt 0 ]]; then
        compose logs -f --tail=100 "$@"
    else
        compose logs -f --tail=100
    fi
}

usage() {
    cat <<'EOF'
Usage:
  pterodactyl start
  pterodactyl stop
  pterodactyl restart
  pterodactyl status
  pterodactyl logs [service]
  pterodactyl help

Examples:
  pterodactyl logs
  pterodactyl logs wings
  pterodactyl logs panel

Configuration is kept near the top of the script. Most scalar settings can
also be overridden with environment variables, for example:

  OPEN_BROWSER=0 pterodactyl start
  SHUTDOWN_TIMEOUT=600 pterodactyl stop
EOF
}

case "${1:-}" in
    start)
        start
        ;;

    stop)
        stop
        ;;

    restart)
        stop && start
        ;;

    status)
        status
        ;;

    logs)
        shift
        logs "$@"
        ;;

    help|-h|--help)
        usage
        ;;

    *)
        usage
        exit 1
        ;;
esac