#!/usr/bin/env bash
#########################################################################################
#                                                                                       #
#  OCI Free Tier Monitor                                                                #
#  Monitor Oracle Cloud Infrastructure Always Free tier usage                           #
#                                                                                       #
#  Author:   Phil Connor                                                                #
#  Contact:  contact@mylinux.work                                                       #
#  License:  MIT                                                                        #
#  Version:  1.00                                                                       #
#                                                                                       #
#########################################################################################

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────────────

WARN_PCT="${OFT_WARN_PCT:-80}"
CRIT_PCT="${OFT_CRIT_PCT:-95}"
FORMAT="text"
INTERVAL="${OFT_INTERVAL:-3600}"
SLACK_WEBHOOK="${OFT_SLACK_WEBHOOK:-}"
COMPARTMENT_ID="${OCI_COMPARTMENT_ID:-}"
OCI_PROFILE="${OCI_CLI_PROFILE:-DEFAULT}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
RUN_MODE=""
SCRIPT_NAME="$(basename "$0")"

# ── Always Free Limits ────────────────────────────────────────────────────────

LIMIT_AMD_INSTANCES=2
LIMIT_ARM_OCPU=4
LIMIT_ARM_RAM_GB=24
LIMIT_BLOCK_STORAGE_GB=200
LIMIT_VOLUME_BACKUPS=5
LIMIT_OBJECT_STORAGE_GB=10
LIMIT_AUTONOMOUS_DB=2
LIMIT_LOAD_BALANCERS=1

# ── Colors ────────────────────────────────────────────────────────────────────

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

# ── Logging ───────────────────────────────────────────────────────────────────

log()     { printf "${GREEN}%s${RESET}\n" "$*"; }
warn()    { printf "${YELLOW}⚠ %s${RESET}\n" "$*" >&2; }
err()     { printf "${RED}✗ %s${RESET}\n" "$*" >&2; }
verbose() { [[ "$VERBOSE" == "true" ]] && printf "${DIM}  %s${RESET}\n" "$*" >&2 || true; }
die()     { err "$*"; exit 1; }

# ── Help ──────────────────────────────────────────────────────────────────────

show_help() {
    cat <<EOF
Usage: $SCRIPT_NAME [MODE] [OPTIONS]

Modes:
  --check           Quick status check (exit 0=ok, 1=warn, 2=crit)
  --report          Detailed usage report for all resources
  --watch           Continuous monitoring loop
  --alerts          Show only resources exceeding thresholds

Options:
  --format FMT      Output format: text, json, prometheus (default: text)
  --warn PCT        Warning threshold percentage (default: 80)
  --crit PCT        Critical threshold percentage (default: 95)
  --interval SECS   Watch mode poll interval in seconds (default: 3600)
  --slack-webhook URL   Slack webhook URL for threshold alerts
  --compartment-id ID   OCI compartment OCID (default: tenancy root)
  --no-color        Disable colored output
  --verbose         Debug output
  --help            Show this help

Environment Variables:
  OCI_CLI_PROFILE       OCI CLI config profile (default: DEFAULT)
  OCI_COMPARTMENT_ID    Compartment OCID
  OFT_WARN_PCT          Warning threshold (default: 80)
  OFT_CRIT_PCT          Critical threshold (default: 95)
  OFT_SLACK_WEBHOOK     Slack webhook URL
  OFT_INTERVAL          Watch interval in seconds (default: 3600)
  VERBOSE               Debug output (true/false)
  COLOR                 Color mode: auto, always, never

Examples:
  $SCRIPT_NAME --check
  $SCRIPT_NAME --report --format json
  $SCRIPT_NAME --watch --interval 1800 --slack-webhook "https://hooks.slack.com/..."
  $SCRIPT_NAME --alerts --warn 70 --crit 90
  $SCRIPT_NAME --report --format prometheus > /var/lib/node_exporter/textfile/oci.prom
EOF
}

# ── Argument Parsing ──────────────────────────────────────────────────────────

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --check)            RUN_MODE="check"; shift ;;
            --report)           RUN_MODE="report"; shift ;;
            --watch)            RUN_MODE="watch"; shift ;;
            --alerts)           RUN_MODE="alerts"; shift ;;
            --format)           FORMAT="$2"; shift 2 ;;
            --warn)             WARN_PCT="$2"; shift 2 ;;
            --crit)             CRIT_PCT="$2"; shift 2 ;;
            --interval)         INTERVAL="$2"; shift 2 ;;
            --slack-webhook)    SLACK_WEBHOOK="$2"; shift 2 ;;
            --compartment-id)   COMPARTMENT_ID="$2"; shift 2 ;;
            --no-color)         COLOR="never"; shift ;;
            --verbose)          VERBOSE="true"; shift ;;
            --help)             show_help; exit 0 ;;
            *)                  die "Unknown option: $1" ;;
        esac
    done

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

# ── Dependency Check ──────────────────────────────────────────────────────────

check_deps() {
    local missing=()
    command -v oci  >/dev/null 2>&1 || missing+=("oci")
    command -v jq   >/dev/null 2>&1 || missing+=("jq")
    command -v curl >/dev/null 2>&1 || missing+=("curl")

    if [[ ${#missing[@]} -gt 0 ]]; then
        die "Missing required tools: ${missing[*]}"
    fi

    verbose "Dependencies satisfied: oci, jq, curl"
}

# ── OCI Helpers ───────────────────────────────────────────────────────────────

oci_cmd() {
    oci --profile "$OCI_PROFILE" "$@" 2>/dev/null
}

get_compartment_id() {
    if [[ -n "$COMPARTMENT_ID" ]]; then
        verbose "Using compartment: $COMPARTMENT_ID"
        return
    fi
    COMPARTMENT_ID=$(oci_cmd iam compartment list \
        --include-root \
        --query 'data[?contains("lifecycle-state", `ACTIVE`)] | [0]."compartment-id"' \
        --raw-output 2>/dev/null || true)

    if [[ -z "$COMPARTMENT_ID" ]]; then
        COMPARTMENT_ID=$(oci_cmd iam region-subscription list \
            --query 'data[0]."tenancy-id"' --raw-output 2>/dev/null || true)
    fi

    if [[ -z "$COMPARTMENT_ID" ]]; then
        die "Cannot determine compartment/tenancy ID. Use --compartment-id."
    fi

    verbose "Resolved root compartment: $COMPARTMENT_ID"
}

get_tenancy_name() {
    local name
    name=$(oci_cmd iam tenancy get --tenancy-id "$COMPARTMENT_ID" \
        --query 'data.name' --raw-output 2>/dev/null || echo "unknown")
    echo "$name"
}

# ── Resource Check Functions ──────────────────────────────────────────────────

declare -A USAGE
declare -A LIMITS
declare -A UNITS

check_compute() {
    verbose "Checking compute instances..."

    local instances
    instances=$(oci_cmd compute instance list \
        --compartment-id "$COMPARTMENT_ID" \
        --lifecycle-state RUNNING \
        --query 'data' 2>/dev/null || echo "[]")

    local amd_count=0
    local arm_ocpu=0
    local arm_ram=0

    while IFS= read -r line; do
        local shape
        shape=$(echo "$line" | jq -r '.shape // ""')
        if [[ "$shape" == *"A1"* ]] || [[ "$shape" == *"Ampere"* ]]; then
            local ocpu mem
            ocpu=$(echo "$line" | jq -r '."shape-config"."ocpus" // 0')
            mem=$(echo "$line" | jq -r '."shape-config"."memory-in-gbs" // 0')
            arm_ocpu=$(echo "$arm_ocpu + $ocpu" | bc)
            arm_ram=$(echo "$arm_ram + $mem" | bc)
        else
            ((amd_count++)) || true
        fi
    done < <(echo "$instances" | jq -c '.[]')

    USAGE[compute_amd]=$amd_count
    LIMITS[compute_amd]=$LIMIT_AMD_INSTANCES
    UNITS[compute_amd]="inst"

    USAGE[compute_arm_ocpu]=$arm_ocpu
    LIMITS[compute_arm_ocpu]=$LIMIT_ARM_OCPU
    UNITS[compute_arm_ocpu]="OCPU"

    USAGE[compute_arm_ram]=$arm_ram
    LIMITS[compute_arm_ram]=$LIMIT_ARM_RAM_GB
    UNITS[compute_arm_ram]="GB"

    verbose "  AMD instances: ${amd_count}/${LIMIT_AMD_INSTANCES}"
    verbose "  Arm OCPU: ${arm_ocpu}/${LIMIT_ARM_OCPU}"
    verbose "  Arm RAM: ${arm_ram}GB/${LIMIT_ARM_RAM_GB}GB"
}

check_block_storage() {
    verbose "Checking block storage..."

    local volumes
    volumes=$(oci_cmd bv volume list \
        --compartment-id "$COMPARTMENT_ID" \
        --lifecycle-state AVAILABLE \
        --query 'data' 2>/dev/null || echo "[]")

    local total_gb=0
    while IFS= read -r size; do
        total_gb=$(echo "$total_gb + $size" | bc)
    done < <(echo "$volumes" | jq -r '.[]."size-in-gbs" // 0')

    local backups
    backups=$(oci_cmd bv volume-backup list \
        --compartment-id "$COMPARTMENT_ID" \
        --lifecycle-state AVAILABLE \
        --query 'length(data)' 2>/dev/null || echo "0")

    USAGE[block_storage]=$total_gb
    LIMITS[block_storage]=$LIMIT_BLOCK_STORAGE_GB
    UNITS[block_storage]="GB"

    USAGE[volume_backups]=$backups
    LIMITS[volume_backups]=$LIMIT_VOLUME_BACKUPS
    UNITS[volume_backups]=""

    verbose "  Block storage: ${total_gb}GB/${LIMIT_BLOCK_STORAGE_GB}GB"
    verbose "  Volume backups: ${backups}/${LIMIT_VOLUME_BACKUPS}"
}

check_object_storage() {
    verbose "Checking object storage..."

    local namespace
    namespace=$(oci_cmd os ns get --query 'data' --raw-output 2>/dev/null || echo "")

    if [[ -z "$namespace" ]]; then
        warn "Cannot determine object storage namespace"
        USAGE[object_storage]=0
        LIMITS[object_storage]=$LIMIT_OBJECT_STORAGE_GB
        UNITS[object_storage]="GB"
        return
    fi

    local buckets
    buckets=$(oci_cmd os bucket list \
        --compartment-id "$COMPARTMENT_ID" \
        --namespace-name "$namespace" \
        --query 'data[].name' 2>/dev/null || echo "[]")

    local total_bytes=0
    while IFS= read -r bucket; do
        [[ -z "$bucket" || "$bucket" == "null" ]] && continue
        local size
        size=$(oci_cmd os bucket get \
            --namespace-name "$namespace" \
            --bucket-name "$bucket" \
            --fields "approximateSize" \
            --query 'data."approximate-size"' --raw-output 2>/dev/null || echo "0")
        [[ "$size" == "null" ]] && size=0
        total_bytes=$(echo "$total_bytes + $size" | bc)
    done < <(echo "$buckets" | jq -r '.[]')

    local total_gb
    total_gb=$(echo "scale=1; $total_bytes / 1073741824" | bc)

    USAGE[object_storage]=$total_gb
    LIMITS[object_storage]=$LIMIT_OBJECT_STORAGE_GB
    UNITS[object_storage]="GB"

    verbose "  Object storage: ${total_gb}GB/${LIMIT_OBJECT_STORAGE_GB}GB"
}

check_autonomous_db() {
    verbose "Checking autonomous databases..."

    local count
    count=$(oci_cmd db autonomous-database list \
        --compartment-id "$COMPARTMENT_ID" \
        --lifecycle-state AVAILABLE \
        --query 'length(data)' 2>/dev/null || echo "0")

    USAGE[autonomous_db]=$count
    LIMITS[autonomous_db]=$LIMIT_AUTONOMOUS_DB
    UNITS[autonomous_db]=""

    verbose "  Autonomous DB: ${count}/${LIMIT_AUTONOMOUS_DB}"
}

check_load_balancer() {
    verbose "Checking load balancers..."

    local count
    count=$(oci_cmd lb load-balancer list \
        --compartment-id "$COMPARTMENT_ID" \
        --lifecycle-state ACTIVE \
        --query 'length(data)' 2>/dev/null || echo "0")

    USAGE[load_balancers]=$count
    LIMITS[load_balancers]=$LIMIT_LOAD_BALANCERS
    UNITS[load_balancers]=""

    verbose "  Load balancers: ${count}/${LIMIT_LOAD_BALANCERS}"
}

# ── Calculation ───────────────────────────────────────────────────────────────

calculate_pct() {
    local used="$1" limit="$2"
    if [[ "$limit" == "0" ]]; then
        echo "0"
        return
    fi
    echo "scale=0; ($used * 100) / $limit" | bc
}

get_status() {
    local pct="$1"
    if (( pct >= CRIT_PCT )); then
        echo "critical"
    elif (( pct >= WARN_PCT )); then
        echo "warning"
    else
        echo "ok"
    fi
}

# ── Collect All Data ──────────────────────────────────────────────────────────

RESOURCE_ORDER=(compute_amd compute_arm_ocpu compute_arm_ram block_storage volume_backups object_storage autonomous_db load_balancers)

RESOURCE_LABELS=(
    "Compute (AMD)"
    "Compute (Arm OCPU)"
    "Compute (Arm RAM)"
    "Block Storage"
    "Volume Backups"
    "Object Storage"
    "Autonomous DB"
    "Load Balancers"
)

collect_all() {
    check_compute
    check_block_storage
    check_object_storage
    check_autonomous_db
    check_load_balancer
}

# ── Output Formatters ─────────────────────────────────────────────────────────

format_text() {
    local alerts_only="${1:-false}"
    local tenancy
    tenancy=$(get_tenancy_name)

    printf "\n${BOLD}OCI Free Tier Monitor${RESET}\n"
    printf "Tenancy:    %s\n" "$tenancy"
    printf "Time:       %s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
    printf "\n  ── Resource Usage ──\n\n"
    printf "  %-22s %-12s %-12s %-8s %s\n" "Resource" "Used" "Limit" "Usage" "Status"
    printf "  %s\n" "$(printf '%.0s─' {1..64})"

    local ok_count=0 warn_count=0 crit_count=0
    for i in "${!RESOURCE_ORDER[@]}"; do
        local key="${RESOURCE_ORDER[$i]}"
        local label="${RESOURCE_LABELS[$i]}"
        local used="${USAGE[$key]}"
        local limit="${LIMITS[$key]}"
        local unit="${UNITS[$key]}"
        local pct
        pct=$(calculate_pct "$used" "$limit")
        local status
        status=$(get_status "$pct")

        case "$status" in
            ok)       ((ok_count++))   || true; status_str="${GREEN}✓ OK${RESET}" ;;
            warning)  ((warn_count++)) || true; status_str="${YELLOW}⚠ Warning${RESET}" ;;
            critical) ((crit_count++)) || true; status_str="${RED}✗ Critical${RESET}" ;;
        esac

        if [[ "$alerts_only" == "true" && "$status" == "ok" ]]; then
            continue
        fi

        local used_str="${used}${unit:+ $unit}"
        local limit_str="${limit}${unit:+ $unit}"
        printf "  %-22s %-12s %-12s %3s%%    ${status_str}\n" \
            "$label" "$used_str" "$limit_str" "$pct"
    done

    printf "\n  Summary: %s OK, %s Warning, %s Critical\n\n" \
        "$ok_count" "$warn_count" "$crit_count"
}

format_json() {
    local alerts_only="${1:-false}"
    local resources="[]"

    for i in "${!RESOURCE_ORDER[@]}"; do
        local key="${RESOURCE_ORDER[$i]}"
        local label="${RESOURCE_LABELS[$i]}"
        local used="${USAGE[$key]}"
        local limit="${LIMITS[$key]}"
        local pct
        pct=$(calculate_pct "$used" "$limit")
        local status
        status=$(get_status "$pct")

        if [[ "$alerts_only" == "true" && "$status" == "ok" ]]; then
            continue
        fi

        resources=$(echo "$resources" | jq \
            --arg key "$key" \
            --arg label "$label" \
            --argjson used "$used" \
            --argjson limit "$limit" \
            --argjson pct "$pct" \
            --arg status "$status" \
            '. + [{"resource": $key, "label": $label, "used": $used, "limit": $limit, "percent": $pct, "status": $status}]')
    done

    jq -n \
        --arg time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
        --argjson warn "$WARN_PCT" \
        --argjson crit "$CRIT_PCT" \
        --argjson resources "$resources" \
        '{"timestamp": $time, "warn_threshold": $warn, "crit_threshold": $crit, "resources": $resources}'
}

format_prometheus() {
    echo "# HELP oci_free_tier_usage_percent OCI Free Tier resource usage percentage"
    echo "# TYPE oci_free_tier_usage_percent gauge"

    for i in "${!RESOURCE_ORDER[@]}"; do
        local key="${RESOURCE_ORDER[$i]}"
        local used="${USAGE[$key]}"
        local limit="${LIMITS[$key]}"
        local pct
        pct=$(calculate_pct "$used" "$limit")
        echo "oci_free_tier_usage_percent{resource=\"${key}\"} ${pct}"
    done

    echo "# HELP oci_free_tier_used OCI Free Tier resource current usage"
    echo "# TYPE oci_free_tier_used gauge"

    for i in "${!RESOURCE_ORDER[@]}"; do
        local key="${RESOURCE_ORDER[$i]}"
        echo "oci_free_tier_used{resource=\"${key}\"} ${USAGE[$key]}"
    done

    echo "# HELP oci_free_tier_limit OCI Free Tier resource limit"
    echo "# TYPE oci_free_tier_limit gauge"

    for i in "${!RESOURCE_ORDER[@]}"; do
        local key="${RESOURCE_ORDER[$i]}"
        echo "oci_free_tier_limit{resource=\"${key}\"} ${LIMITS[$key]}"
    done

    echo "# HELP oci_free_tier_warn_threshold Warning threshold percentage"
    echo "# TYPE oci_free_tier_warn_threshold gauge"
    echo "oci_free_tier_warn_threshold ${WARN_PCT}"

    echo "# HELP oci_free_tier_crit_threshold Critical threshold percentage"
    echo "# TYPE oci_free_tier_crit_threshold gauge"
    echo "oci_free_tier_crit_threshold ${CRIT_PCT}"
}

format_output() {
    local alerts_only="${1:-false}"
    case "$FORMAT" in
        text)       format_text "$alerts_only" ;;
        json)       format_json "$alerts_only" ;;
        prometheus) format_prometheus ;;
        *)          die "Unknown format: $FORMAT" ;;
    esac
}

# ── Slack Notification ────────────────────────────────────────────────────────

send_slack_alert() {
    [[ -z "$SLACK_WEBHOOK" ]] && return

    local breached=()
    for i in "${!RESOURCE_ORDER[@]}"; do
        local key="${RESOURCE_ORDER[$i]}"
        local label="${RESOURCE_LABELS[$i]}"
        local used="${USAGE[$key]}"
        local limit="${LIMITS[$key]}"
        local pct
        pct=$(calculate_pct "$used" "$limit")
        local status
        status=$(get_status "$pct")

        if [[ "$status" != "ok" ]]; then
            breached+=("${label}: ${used}/${limit} (${pct}%) [${status}]")
        fi
    done

    if [[ ${#breached[@]} -eq 0 ]]; then
        verbose "No threshold breaches — skipping Slack notification"
        return
    fi

    local message="OCI Free Tier Alert — $(date -u +%Y-%m-%dT%H:%M:%SZ)\n"
    for line in "${breached[@]}"; do
        message+="• ${line}\n"
    done

    local payload
    payload=$(jq -n --arg text "$message" '{"text": $text}')

    if curl -sf -o /dev/null -X POST \
        -H "Content-Type: application/json" \
        -d "$payload" "$SLACK_WEBHOOK"; then
        verbose "Slack notification sent"
    else
        warn "Failed to send Slack notification"
    fi
}

# ── Exit Code ─────────────────────────────────────────────────────────────────

get_exit_code() {
    local max_status="ok"
    for key in "${RESOURCE_ORDER[@]}"; do
        local used="${USAGE[$key]}"
        local limit="${LIMITS[$key]}"
        local pct
        pct=$(calculate_pct "$used" "$limit")
        local status
        status=$(get_status "$pct")

        if [[ "$status" == "critical" ]]; then
            max_status="critical"
            break
        elif [[ "$status" == "warning" ]]; then
            max_status="warning"
        fi
    done

    case "$max_status" in
        ok)       echo 0 ;;
        warning)  echo 1 ;;
        critical) echo 2 ;;
    esac
}

# ── Mode Functions ────────────────────────────────────────────────────────────

run_check() {
    collect_all
    format_output "false"
    send_slack_alert
    local code
    code=$(get_exit_code)
    exit "$code"
}

run_report() {
    collect_all
    format_output "false"
    send_slack_alert
}

run_alerts() {
    collect_all
    format_output "true"
    send_slack_alert
    local code
    code=$(get_exit_code)
    exit "$code"
}

run_watch() {
    log "Starting watch mode (interval: ${INTERVAL}s)"
    while true; do
        collect_all
        format_output "false"
        send_slack_alert
        verbose "Next check in ${INTERVAL}s"
        sleep "$INTERVAL"
    done
}

# ── Main ──────────────────────────────────────────────────────────────────────

main() {
    parse_args "$@"
    setup_colors
    check_deps
    get_compartment_id

    case "$RUN_MODE" in
        check)  run_check ;;
        report) run_report ;;
        alerts) run_alerts ;;
        watch)  run_watch ;;
    esac
}

main "$@"
