#!/usr/bin/env bash

#########################################################################################
#### contabo-fleet-manager.sh — Inventory, health checks, and bulk operations for    ####
#### Contabo VPS/VDS instances via the REST API. Fleet-wide visibility and control    ####
#### Requires: bash 4+, curl, jq                                                    ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.01                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   ./contabo-fleet-manager.sh --inventory --all                                  ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -euo pipefail

# ── Colors (pre-initialized) ─────────────────────────────────────────
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""

setup_colors() {
    if [[ "${COLOR:-auto}" == "never" ]]; then
        return
    fi
    if [[ "${COLOR:-auto}" == "always" ]] || [[ -t 1 ]]; then
        RED='\033[0;31m'
        GREEN='\033[0;32m'
        YELLOW='\033[0;33m'
        BLUE='\033[0;34m'
        CYAN='\033[0;36m'
        BOLD='\033[1m'
        DIM='\033[2m'
        RESET='\033[0m'
    fi
}

# ── Logging ───────────────────────────────────────────────────────────
log()     { echo -e "${BLUE}[INFO]${RESET} $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
err()     { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }
die()     { err "$*"; exit 1; }

section_header() {
    echo ""
    echo -e "  ${BOLD}${CYAN}── $1 ──${RESET}"
    echo ""
}

field() {
    printf "  ${BOLD}%-22s${RESET} %s\n" "$1" "$2"
}

field_color() {
    printf "  ${BOLD}%-22s${RESET} %b\n" "$1" "$2"
}

elapsed() {
    local end_time
    end_time=$(date +%s)
    echo "$(( end_time - START_TIME ))s"
}

# ── Defaults ──────────────────────────────────────────────────────────
RUN_MODE=""
INSTANCE_ID=""
TARGET_ALL="false"
TAG_ID=""
TAG_SUB_MODE=""
OUTPUT_FORMAT="${CFM_FORMAT:-text}"
PING_CHECK="false"
FORCE="false"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"

# ── Credentials ───────────────────────────────────────────────────────
CONTABO_CLIENT_ID="${CONTABO_CLIENT_ID:-}"
CONTABO_CLIENT_SECRET="${CONTABO_CLIENT_SECRET:-}"
CONTABO_API_USER="${CONTABO_API_USER:-}"
CONTABO_API_PASS="${CONTABO_API_PASS:-}"

# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
START_TIME=""
ACTION_OK=0
ACTION_FAIL=0

# ── API helpers ──────────────────────────────────────────────────────
contabo_token() {
    local resp
    resp=$(curl -s -d "client_id=${CONTABO_CLIENT_ID}" \
        -d "client_secret=${CONTABO_CLIENT_SECRET}" \
        --data-urlencode "username=${CONTABO_API_USER}" \
        --data-urlencode "password=${CONTABO_API_PASS}" \
        -d "grant_type=password" \
        "https://auth.contabo.com/auth/realms/contabo/protocol/openid-connect/token")
    local token
    token=$(echo "$resp" | jq -r '.access_token // empty' 2>/dev/null)
    if [[ -z "$token" ]]; then
        die "Failed to obtain access token — check credentials"
    fi
    echo "$token"
}

contabo_api() {
    local method="$1" endpoint="$2"
    shift 2
    local attempt=0 max_attempts=3

    while (( attempt < max_attempts )); do
        local http_code
        http_code=$(curl -s -o /tmp/cfm_resp.json -w "%{http_code}" \
            -X "$method" \
            -H "Authorization: Bearer $(contabo_token)" \
            -H "Content-Type: application/json" \
            -H "x-request-id: $(cat /proc/sys/kernel/random/uuid 2>/dev/null || date +%s%N)" \
            "https://api.contabo.com/v1${endpoint}" "$@")

        verbose "API ${method} ${endpoint} → HTTP ${http_code}"

        if [[ "$http_code" == "429" ]]; then
            ((attempt++)) || true
            local wait=$(( attempt * 5 ))
            warn "Rate limited — retrying in ${wait}s (attempt ${attempt}/${max_attempts})"
            sleep "$wait"
            continue
        fi

        cat /tmp/cfm_resp.json
        return 0
    done

    err "API request failed after ${max_attempts} attempts: ${method} ${endpoint}"
    return 1
}

check_credentials() {
    [[ -z "$CONTABO_CLIENT_ID" ]] && die "CONTABO_CLIENT_ID not set"
    [[ -z "$CONTABO_CLIENT_SECRET" ]] && die "CONTABO_CLIENT_SECRET not set"
    [[ -z "$CONTABO_API_USER" ]] && die "CONTABO_API_USER not set"
    [[ -z "$CONTABO_API_PASS" ]] && die "CONTABO_API_PASS not set"
}

check_deps() {
    command -v curl &>/dev/null || die "curl is required"
    command -v jq &>/dev/null || die "jq is required"
}

# ── Instance helpers ─────────────────────────────────────────────────
get_all_instance_ids() {
    local page=1 size=100 ids=""
    while true; do
        local resp
        resp=$(contabo_api GET "/compute/instances?page=${page}&size=${size}")
        local page_ids
        page_ids=$(echo "$resp" | jq -r '.data[].instanceId' 2>/dev/null)
        [[ -z "$page_ids" ]] && break
        ids="${ids}${ids:+$'\n'}${page_ids}"
        local count
        count=$(echo "$page_ids" | wc -l)
        (( count < size )) && break
        ((page++)) || true
    done
    echo "$ids"
}

get_instance_name() {
    local iid="$1"
    contabo_api GET "/compute/instances/${iid}" \
        | jq -r '.data[0].name // .data[0].displayName // "unknown"' 2>/dev/null
}

get_instance_ids() {
    if [[ "$TARGET_ALL" == "true" ]]; then
        get_all_instance_ids
    elif [[ -n "$INSTANCE_ID" ]]; then
        echo "$INSTANCE_ID"
    elif [[ -n "$TAG_ID" ]]; then
        get_instances_by_tag "$TAG_ID"
    else
        die "Specify --instance ID, --all, or --tag TAG_ID"
    fi
}

get_instances_by_tag() {
    local tid="$1"
    local page=1 size=100 ids=""
    while true; do
        local resp
        resp=$(contabo_api GET "/compute/instances?page=${page}&size=${size}")
        local page_ids
        page_ids=$(echo "$resp" | jq -r --arg tid "$tid" \
            '.data[] | select(.tags[]? | .tagId == ($tid | tonumber)) | .instanceId' 2>/dev/null)
        [[ -z "$page_ids" ]] && break
        ids="${ids}${ids:+$'\n'}${page_ids}"
        local count
        count=$(echo "$page_ids" | wc -l)
        (( count < size )) && break
        ((page++)) || true
    done
    echo "$ids"
}

# ══════════════════════════════════════════════════════════════════════
#                          INVENTORY
# ══════════════════════════════════════════════════════════════════════
do_inventory() {
    local page=1 size=100 all_data="[]"

    while true; do
        local resp
        resp=$(contabo_api GET "/compute/instances?page=${page}&size=${size}")
        local page_data
        page_data=$(echo "$resp" | jq '.data // []' 2>/dev/null)
        local page_count
        page_count=$(echo "$page_data" | jq 'length' 2>/dev/null || echo 0)
        [[ "$page_count" -eq 0 ]] && break
        all_data=$(echo -e "${all_data}\n${page_data}" | jq -s 'add' 2>/dev/null)
        (( page_count < size )) && break
        ((page++)) || true
    done

    local total
    total=$(echo "$all_data" | jq 'length' 2>/dev/null || echo 0)
    [[ "$total" -eq 0 ]] && die "No instances found"

    # Filter by tag if specified
    if [[ -n "$TAG_ID" ]]; then
        all_data=$(echo "$all_data" | jq --arg tid "$TAG_ID" \
            '[.[] | select(.tags[]? | .tagId == ($tid | tonumber))]' 2>/dev/null)
        total=$(echo "$all_data" | jq 'length' 2>/dev/null || echo 0)
        [[ "$total" -eq 0 ]] && die "No instances found with tag ${TAG_ID}"
    fi

    case "$OUTPUT_FORMAT" in
        json)
            echo "$all_data" | jq '.'
            ;;
        ansible)
            echo "[contabo]"
            echo "$all_data" | jq -r \
                '.[] | (.ipConfig.v4.ip // "unknown") + " # " + (.name // .displayName // "unknown") + " id=" + (.instanceId | tostring)' \
                2>/dev/null
            ;;
        *)
            section_header "Fleet Inventory"

            printf "  ${BOLD}%-13s %-20s %-11s %-16s %-8s %-8s${RESET}\n" \
                "INSTANCE_ID" "NAME" "STATUS" "IP" "REGION" "PRODUCT"
            printf "  %s\n" "$(printf '%.0s─' {1..78})"

            echo "$all_data" | jq -r \
                '.[] | "\(.instanceId)\t\(.name // .displayName // "unknown")\t\(.status // "unknown")\t\(.ipConfig.v4.ip // "—")\t\(.region // "—")\t\(.productId // "—")"' \
                2>/dev/null \
                | while IFS=$'\t' read -r iid name status ip region product; do
                    printf "  %-13s %-20s %-11s %-16s %-8s %-8s\n" \
                        "$iid" "${name:0:18}" "$status" "$ip" "${region:0:6}" "$product"
                done

            echo ""
            field "Total:" "$total"
            ;;
    esac
}

# ══════════════════════════════════════════════════════════════════════
#                          HEALTH
# ══════════════════════════════════════════════════════════════════════
do_health() {
    local ids
    ids=$(get_instance_ids)
    [[ -z "$ids" ]] && die "No instances found"

    local running=0 stopped=0 errored=0 total_instances=0
    local results=""

    while IFS= read -r iid; do
        [[ -z "$iid" ]] && continue
        ((total_instances++)) || true

        local resp
        resp=$(contabo_api GET "/compute/instances/${iid}")
        local name status ip
        name=$(echo "$resp" | jq -r '.data[0].name // .data[0].displayName // "unknown"' 2>/dev/null)
        status=$(echo "$resp" | jq -r '.data[0].status // "unknown"' 2>/dev/null)
        ip=$(echo "$resp" | jq -r '.data[0].ipConfig.v4.ip // ""' 2>/dev/null)

        local ping_result="—"
        if [[ "$PING_CHECK" == "true" && -n "$ip" ]]; then
            if ping -c 1 -W 3 "$ip" &>/dev/null; then
                ping_result="reachable"
            else
                ping_result="unreachable"
            fi
        fi

        case "$status" in
            running) ((running++)) || true ;;
            stopped) ((stopped++)) || true ;;
            *)       ((errored++)) || true ;;
        esac

        results="${results}${iid}\t${name}\t${status}\t${ip}\t${ping_result}\n"
    done <<< "$ids"

    if [[ "$OUTPUT_FORMAT" == "prometheus" ]]; then
        cat <<EOF
# HELP contabo_fleet_instances_total Total Contabo instances
# TYPE contabo_fleet_instances_total gauge
contabo_fleet_instances_total ${total_instances}
# HELP contabo_fleet_running Running instances
# TYPE contabo_fleet_running gauge
contabo_fleet_running ${running}
# HELP contabo_fleet_stopped Stopped instances
# TYPE contabo_fleet_stopped gauge
contabo_fleet_stopped ${stopped}
# HELP contabo_fleet_error Instances in error state
# TYPE contabo_fleet_error gauge
contabo_fleet_error ${errored}
EOF
        return
    fi

    section_header "Fleet Health Check"

    if [[ "$PING_CHECK" == "true" ]]; then
        printf "  ${BOLD}%-13s %-20s %-11s %-16s %-12s${RESET}\n" \
            "INSTANCE_ID" "NAME" "STATUS" "IP" "PING"
        printf "  %s\n" "$(printf '%.0s─' {1..74})"
    else
        printf "  ${BOLD}%-13s %-20s %-11s %-16s${RESET}\n" \
            "INSTANCE_ID" "NAME" "STATUS" "IP"
        printf "  %s\n" "$(printf '%.0s─' {1..62})"
    fi

    echo -e "$results" | while IFS=$'\t' read -r iid name status ip ping_res; do
        [[ -z "$iid" ]] && continue
        local status_color="$GREEN"
        case "$status" in
            running) status_color="$GREEN" ;;
            stopped) status_color="$YELLOW" ;;
            *)       status_color="$RED" ;;
        esac

        if [[ "$PING_CHECK" == "true" ]]; then
            local ping_color="$DIM"
            case "$ping_res" in
                reachable)   ping_color="$GREEN" ;;
                unreachable) ping_color="$RED" ;;
            esac
            printf "  %-13s %-20s " "$iid" "${name:0:18}"
            echo -ne "${status_color}"
            printf "%-11s" "$status"
            echo -ne "${RESET}"
            printf " %-16s " "$ip"
            echo -e "${ping_color}${ping_res}${RESET}"
        else
            printf "  %-13s %-20s " "$iid" "${name:0:18}"
            echo -ne "${status_color}"
            printf "%-11s" "$status"
            echo -e "${RESET} ${ip}"
        fi
    done

    echo ""
    field "Instances:" "$total_instances"
    field_color "Running:" "${GREEN}${running}${RESET}"
    if [[ "$stopped" -gt 0 ]]; then
        field_color "Stopped:" "${YELLOW}${stopped}${RESET}"
    else
        field_color "Stopped:" "${GREEN}0${RESET}"
    fi
    if [[ "$errored" -gt 0 ]]; then
        field_color "Error:" "${RED}${errored}${RESET}"
    else
        field_color "Error:" "${GREEN}0${RESET}"
    fi
}

# ══════════════════════════════════════════════════════════════════════
#                          START / STOP / RESTART
# ══════════════════════════════════════════════════════════════════════
do_action() {
    local action="$1"
    local ids
    ids=$(get_instance_ids)
    [[ -z "$ids" ]] && die "No instances found"

    if [[ "$action" != "start" && "$FORCE" != "true" ]]; then
        die "${action} is destructive — use --force to confirm"
    fi

    local count
    count=$(echo "$ids" | grep -c . || true)
    local target_label="instance ${INSTANCE_ID}"
    [[ "$TARGET_ALL" == "true" ]] && target_label="all (${count} instances)"
    [[ -n "$TAG_ID" ]] && target_label="tag ${TAG_ID} (${count} instances)"

    section_header "Bulk ${action^}"
    field "Target:" "$target_label"
    field "Action:" "$action"
    echo ""

    while IFS= read -r iid; do
        [[ -z "$iid" ]] && continue
        local iname
        iname=$(get_instance_name "$iid")

        verbose "Sending ${action} to ${iname} (${iid})"

        if contabo_api POST "/compute/instances/${iid}/actions/${action}" \
            -d '{}' > /dev/null 2>&1; then
            echo -e "  ${GREEN}✓${RESET} ${iname} (${iid})  ${action} sent"
            ((ACTION_OK++)) || true
        else
            echo -e "  ${RED}✗${RESET} ${iname} (${iid})  ${action} failed"
            ((ACTION_FAIL++)) || true
        fi

        sleep 1
    done <<< "$ids"

    echo ""
    field_color "Succeeded:" "${GREEN}${ACTION_OK}${RESET}"
    if [[ "$ACTION_FAIL" -gt 0 ]]; then
        field_color "Failed:" "${RED}${ACTION_FAIL}${RESET}"
    fi
}

# ══════════════════════════════════════════════════════════════════════
#                          TAGS
# ══════════════════════════════════════════════════════════════════════
do_tags() {
    if [[ "$TAG_SUB_MODE" == "list" ]]; then
        local resp
        resp=$(contabo_api GET "/tags?page=1&size=100")

        if [[ "$OUTPUT_FORMAT" == "json" ]]; then
            echo "$resp" | jq '.data // []'
            return
        fi

        section_header "Tags"

        printf "  ${BOLD}%-10s %-30s %-10s${RESET}\n" "TAG_ID" "NAME" "COLOR"
        printf "  %s\n" "$(printf '%.0s─' {1..52})"

        echo "$resp" | jq -r '.data[] | "\(.tagId)\t\(.name)\t\(.color // "—")"' 2>/dev/null \
            | while IFS=$'\t' read -r tid tname tcolor; do
                printf "  %-10s %-30s %-10s\n" "$tid" "${tname:0:28}" "$tcolor"
            done
    elif [[ "$TAG_SUB_MODE" == "filter" ]]; then
        [[ -z "$TAG_ID" ]] && die "Specify --filter TAG_ID"
        INSTANCE_ID=""
        TARGET_ALL="false"
        do_inventory
    else
        die "Specify --list or --filter TAG_ID with --tags"
    fi
}

# ══════════════════════════════════════════════════════════════════════
#                          HELP
# ══════════════════════════════════════════════════════════════════════
show_help() {
    cat <<EOF
${BOLD}${SCRIPT_NAME}${RESET} — Contabo Fleet Manager

Inventory, health checks, and bulk operations for Contabo VPS/VDS
instances via the REST API.

${BOLD}MODES${RESET}
  --inventory       Dump instance inventory (table, ansible, json)
  --health          Health check all targeted instances
  --start           Start targeted instances
  --stop            Stop targeted instances (requires --force)
  --restart         Restart targeted instances (requires --force)
  --tags            List tags or filter instances by tag

${BOLD}TARGETING${RESET}
  --instance ID     Target a specific instance
  --all             Target all instances
  --tag TAG_ID      Target instances with a specific tag

${BOLD}OPTIONS${RESET}
  --format FMT      Output: text, json, ansible, prometheus (default: text)
  --ping            Include ICMP ping check in health mode
  --force           Required for stop/restart operations
  --list            List all tags (with --tags)
  --filter TAG_ID   Filter instances by tag (with --tags)
  --verbose         Debug output
  --no-color        Disable colored output
  --help            Show this help message

${BOLD}ENVIRONMENT VARIABLES${RESET}
  CONTABO_CLIENT_ID      OAuth2 Client ID (required)
  CONTABO_CLIENT_SECRET  OAuth2 Client Secret (required)
  CONTABO_API_USER       API username / email (required)
  CONTABO_API_PASS       API password (required)
  CFM_FORMAT             Default output format
  VERBOSE                Enable verbose output (true/false)
  COLOR                  Color mode: auto, always, never

${BOLD}EXAMPLES${RESET}
  # Full fleet inventory
  ${SCRIPT_NAME} --inventory --all

  # Inventory as Ansible inventory file
  ${SCRIPT_NAME} --inventory --all --format ansible

  # Health check all instances
  ${SCRIPT_NAME} --health --all

  # Health check with ping
  ${SCRIPT_NAME} --health --all --ping

  # Prometheus metrics output
  ${SCRIPT_NAME} --health --all --format prometheus

  # Start a single instance
  ${SCRIPT_NAME} --start --instance 12345

  # Stop all instances (requires --force)
  ${SCRIPT_NAME} --stop --all --force

  # Restart instances by tag
  ${SCRIPT_NAME} --restart --tag 42 --force

  # List all tags
  ${SCRIPT_NAME} --tags --list

  # Show instances with a specific tag
  ${SCRIPT_NAME} --tags --filter 42

${BOLD}EXIT CODES${RESET}
  0   Success
  1   Runtime error
EOF
}

# ══════════════════════════════════════════════════════════════════════
#                          PARSE ARGS
# ══════════════════════════════════════════════════════════════════════
parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --inventory)    RUN_MODE="inventory"; shift ;;
            --health)       RUN_MODE="health"; shift ;;
            --start)        RUN_MODE="start"; shift ;;
            --stop)         RUN_MODE="stop"; shift ;;
            --restart)      RUN_MODE="restart"; shift ;;
            --tags)         RUN_MODE="tags"; shift ;;
            --instance)     INSTANCE_ID="${2:?--instance requires an ID}"; shift 2 ;;
            --all)          TARGET_ALL="true"; shift ;;
            --tag)          TAG_ID="${2:?--tag requires a TAG_ID}"; shift 2 ;;
            --format)       OUTPUT_FORMAT="${2:?--format requires a value}"; shift 2 ;;
            --ping)         PING_CHECK="true"; shift ;;
            --force)        FORCE="true"; shift ;;
            --list)         TAG_SUB_MODE="list"; shift ;;
            --filter)       TAG_SUB_MODE="filter"; TAG_ID="${2:?--filter requires a TAG_ID}"; shift 2 ;;
            --verbose)      VERBOSE="true"; shift ;;
            --no-color)     COLOR="never"; shift ;;
            --help|-h)      setup_colors; show_help; exit 0 ;;
            *)              die "Unknown option: $1 (see --help)" ;;
        esac
    done
}

# ══════════════════════════════════════════════════════════════════════
#                          MAIN
# ══════════════════════════════════════════════════════════════════════
main() {
    parse_args "$@"
    setup_colors

    if [[ -z "$RUN_MODE" ]]; then
        err "No mode specified"
        echo ""
        show_help
        exit 1
    fi

    check_deps
    check_credentials

    START_TIME=$(date +%s)

    case "$RUN_MODE" in
        inventory) do_inventory ;;
        health)    do_health ;;
        start)     do_action "start" ;;
        stop)      do_action "stop" ;;
        restart)   do_action "restart" ;;
        tags)      do_tags ;;
        *)         die "Unknown mode: ${RUN_MODE}" ;;
    esac

    if [[ "$OUTPUT_FORMAT" != "prometheus" ]]; then
        echo ""
        field "Duration:" "$(elapsed)"
    fi
}

main "$@"
