#!/usr/bin/env bash
################################################################################
# Script Name: snipeit-exporter.sh
# Version: 1.0
# Description: Prometheus exporter for Snipe-IT asset management.
#              Uses the Snipe-IT REST API to collect asset counts, license
#              seat utilization, warranty expirations, checkout status,
#              consumable stock levels, and maintenance alerts.
#
# Author: Phil Connor
# Contact: contact@mylinux.work
# Website: https://mylinux.work
# License: MIT
#
# Prerequisites:
#   - curl and jq
#   - Snipe-IT API token with read access
#
# Usage:
#   SNIPEIT_URL="https://assets.example.com" SNIPEIT_TOKEN="xxx" ./snipeit-exporter.sh
#   SNIPEIT_URL="https://assets.example.com" SNIPEIT_TOKEN="xxx" ./snipeit-exporter.sh --textfile
#   SNIPEIT_URL="https://assets.example.com" SNIPEIT_TOKEN="xxx" ./snipeit-exporter.sh --http
#
# Parameters:
#   --textfile   Write to textfile collector directory
#   --http       Run as HTTP server
#   --install    Create cron job for automatic collection
#   --help       Show usage
#
# Environment:
#   SNIPEIT_URL       Snipe-IT base URL (required)
#   SNIPEIT_TOKEN     API token (required)
#   TEXTFILE_DIR      Textfile collector directory (default: /var/lib/node_exporter/textfile_collector)
#   CURL_TIMEOUT      API request timeout in seconds (default: 10)
#   WARRANTY_DAYS     Days threshold for warranty expiry warnings (default: 90)
#
# Metrics Exported:
#   Core:
#     - snipeit_up
#     - snipeit_exporter_info{version}
#
#   Assets:
#     - snipeit_assets_total
#     - snipeit_assets_deployed
#     - snipeit_assets_ready_to_deploy
#     - snipeit_assets_pending
#     - snipeit_assets_archived
#     - snipeit_assets_undeployable
#     - snipeit_assets_by_category{category}
#
#   Licenses:
#     - snipeit_licenses_total
#     - snipeit_license_seats_total{license}
#     - snipeit_license_seats_used{license}
#     - snipeit_license_seats_free{license}
#
#   Consumables:
#     - snipeit_consumables_total
#     - snipeit_consumable_remaining{consumable}
#     - snipeit_consumable_min_qty{consumable}
#
#   Maintenance:
#     - snipeit_assets_warranty_expiring
#     - snipeit_users_total
#     - snipeit_locations_total
#
#   Exporter:
#     - snipeit_exporter_duration_seconds
#     - snipeit_exporter_last_run_timestamp
#
################################################################################

set -euo pipefail

# --- Configuration ---
readonly VERSION="1.0"
readonly SCRIPT_NAME="$(basename "$0")"
SNIPEIT_URL="${SNIPEIT_URL:-}"
SNIPEIT_TOKEN="${SNIPEIT_TOKEN:-}"
TEXTFILE_DIR="${TEXTFILE_DIR:-/var/lib/node_exporter/textfile_collector}"
CURL_TIMEOUT="${CURL_TIMEOUT:-10}"
WARRANTY_DAYS="${WARRANTY_DAYS:-90}"
TEXTFILE_MODE=false
HTTP_MODE=false
HTTP_PORT=9199
OUTPUT=""
START_TIME=""

# --- Functions ---

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

Snipe-IT Prometheus Metrics Exporter

Options:
  --textfile    Write metrics to textfile collector directory
  --http        Run as HTTP server on port $HTTP_PORT
  -p, --port    HTTP port (default: $HTTP_PORT)
  --install     Create cron job for automatic collection
  --help        Show this help message

Environment Variables:
  SNIPEIT_URL       Snipe-IT base URL (required)
  SNIPEIT_TOKEN     API token with read access (required)
  TEXTFILE_DIR      Output directory (default: /var/lib/node_exporter/textfile_collector)
  CURL_TIMEOUT      Request timeout in seconds (default: 10)
  WARRANTY_DAYS     Warranty expiry warning threshold in days (default: 90)

Examples:
  SNIPEIT_URL="https://assets.example.com" SNIPEIT_TOKEN="xxx" $SCRIPT_NAME
  SNIPEIT_URL="https://assets.example.com" SNIPEIT_TOKEN="xxx" $SCRIPT_NAME --textfile
  SNIPEIT_URL="https://assets.example.com" SNIPEIT_TOKEN="xxx" $SCRIPT_NAME --http
  SNIPEIT_URL="https://assets.example.com" SNIPEIT_TOKEN="xxx" $SCRIPT_NAME --install
EOF
    exit 0
}

check_dependencies() {
    local missing=()
    for cmd in curl jq; do
        if ! command -v "$cmd" &>/dev/null; then
            missing+=("$cmd")
        fi
    done
    if [[ ${#missing[@]} -gt 0 ]]; then
        echo "# ERROR: Missing required commands: ${missing[*]}" >&2
        echo "# Install with: apt install ${missing[*]}  OR  dnf install ${missing[*]}" >&2
        exit 1
    fi
}

validate_config() {
    if [[ -z "$SNIPEIT_URL" ]]; then
        echo "# ERROR: SNIPEIT_URL environment variable is required" >&2
        exit 1
    fi
    if [[ -z "$SNIPEIT_TOKEN" ]]; then
        echo "# ERROR: SNIPEIT_TOKEN environment variable is required" >&2
        exit 1
    fi
    SNIPEIT_URL="${SNIPEIT_URL%/}"
}

api_get() {
    local endpoint="$1"
    curl -sf --max-time "$CURL_TIMEOUT" \
        -H "Authorization: Bearer ${SNIPEIT_TOKEN}" \
        -H "Accept: application/json" \
        "${SNIPEIT_URL}/api/v1${endpoint}" 2>/dev/null || echo ""
}

sanitize_label() {
    local value="$1"
    echo "$value" | sed 's/[^a-zA-Z0-9_ \/.-]/_/g' | sed 's/"/\\"/g'
}

add_metric() {
    local name="$1"
    local type="$2"
    local help="$3"
    local value="$4"
    local labels="${5:-}"

    if [[ -n "$labels" ]]; then
        OUTPUT+="# HELP ${name} ${help}
# TYPE ${name} ${type}
${name}{${labels}} ${value}
"
    else
        OUTPUT+="# HELP ${name} ${help}
# TYPE ${name} ${type}
${name} ${value}
"
    fi
}

add_metric_value() {
    local name="$1"
    local value="$2"
    local labels="${3:-}"

    if [[ -n "$labels" ]]; then
        OUTPUT+="${name}{${labels}} ${value}
"
    else
        OUTPUT+="${name} ${value}
"
    fi
}

# --- Collectors ---

collect_status() {
    local status_json
    status_json=$(api_get "/hardware?limit=1")

    if [[ -z "$status_json" ]]; then
        add_metric "snipeit_up" "gauge" "Snipe-IT API reachability (1=up, 0=down)" "0"
        return 1
    fi

    local total
    total=$(echo "$status_json" | jq -r '.total // empty' 2>/dev/null)

    if [[ -z "$total" ]]; then
        add_metric "snipeit_up" "gauge" "Snipe-IT API reachability (1=up, 0=down)" "0"
        return 1
    fi

    add_metric "snipeit_up" "gauge" "Snipe-IT API reachability (1=up, 0=down)" "1"
    return 0
}

collect_assets() {
    # Total assets
    local hw_json
    hw_json=$(api_get "/hardware?limit=1")
    if [[ -n "$hw_json" ]]; then
        local total
        total=$(echo "$hw_json" | jq -r '.total // 0' 2>/dev/null)
        add_metric "snipeit_assets_total" "gauge" "Total number of assets" "${total:-0}"
    fi

    # Assets by status label
    local status_json
    status_json=$(api_get "/statuslabels")
    if [[ -n "$status_json" ]]; then
        local deployed=0 ready=0 pending=0 archived=0 undeployable=0

        local count
        count=$(echo "$status_json" | jq -r '.total // 0' 2>/dev/null)

        if [[ "$count" -gt 0 ]]; then
            local labels_json
            labels_json=$(api_get "/statuslabels?limit=${count}")

            if [[ -n "$labels_json" ]]; then
                deployed=$(echo "$labels_json" | jq '[.rows[] | select(.type=="deployable") | .assets_count] | add // 0' 2>/dev/null)
                pending=$(echo "$labels_json" | jq '[.rows[] | select(.type=="pending") | .assets_count] | add // 0' 2>/dev/null)
                archived=$(echo "$labels_json" | jq '[.rows[] | select(.type=="archived") | .assets_count] | add // 0' 2>/dev/null)
                undeployable=$(echo "$labels_json" | jq '[.rows[] | select(.type=="undeployable") | .assets_count] | add // 0' 2>/dev/null)
            fi
        fi

        # Deployed = checked out assets
        local deployed_json
        deployed_json=$(api_get "/hardware?status=Deployed&limit=1")
        if [[ -n "$deployed_json" ]]; then
            deployed=$(echo "$deployed_json" | jq -r '.total // 0' 2>/dev/null)
        fi

        add_metric "snipeit_assets_deployed" "gauge" "Assets currently checked out" "${deployed:-0}"
        add_metric "snipeit_assets_ready_to_deploy" "gauge" "Assets ready to deploy" "${ready:-0}"
        add_metric "snipeit_assets_pending" "gauge" "Assets in pending status" "${pending:-0}"
        add_metric "snipeit_assets_archived" "gauge" "Assets archived" "${archived:-0}"
        add_metric "snipeit_assets_undeployable" "gauge" "Assets undeployable (broken, lost, repair)" "${undeployable:-0}"
    fi

    # Assets by category
    local cat_json
    cat_json=$(api_get "/categories?limit=100")
    if [[ -n "$cat_json" ]]; then
        local cat_count
        cat_count=$(echo "$cat_json" | jq -r '.total // 0' 2>/dev/null)

        if [[ "$cat_count" -gt 0 ]]; then
            OUTPUT+="# HELP snipeit_assets_by_category Number of assets per category
# TYPE snipeit_assets_by_category gauge
"
            local i
            for ((i = 0; i < cat_count && i < 50; i++)); do
                local cat_name cat_assets cat_type
                cat_name=$(echo "$cat_json" | jq -r ".rows[$i].name // empty" 2>/dev/null)
                cat_assets=$(echo "$cat_json" | jq -r ".rows[$i].assets_count // 0" 2>/dev/null)
                cat_type=$(echo "$cat_json" | jq -r ".rows[$i].category_type // empty" 2>/dev/null)

                if [[ -n "$cat_name" && "$cat_type" == "asset" ]]; then
                    local safe_name
                    safe_name=$(sanitize_label "$cat_name")
                    add_metric_value "snipeit_assets_by_category" "${cat_assets:-0}" "category=\"${safe_name}\""
                fi
            done
        fi
    fi
}

collect_licenses() {
    local lic_json
    lic_json=$(api_get "/licenses?limit=100")

    if [[ -z "$lic_json" ]]; then
        return
    fi

    local total
    total=$(echo "$lic_json" | jq -r '.total // 0' 2>/dev/null)
    add_metric "snipeit_licenses_total" "gauge" "Total number of licenses" "${total:-0}"

    if [[ "$total" -gt 0 ]]; then
        OUTPUT+="# HELP snipeit_license_seats_total Total seats for a license
# TYPE snipeit_license_seats_total gauge
# HELP snipeit_license_seats_used Used seats for a license
# TYPE snipeit_license_seats_used gauge
# HELP snipeit_license_seats_free Free seats for a license
# TYPE snipeit_license_seats_free gauge
"
        local i
        for ((i = 0; i < total && i < 50; i++)); do
            local name seats_total seats_used seats_free
            name=$(echo "$lic_json" | jq -r ".rows[$i].name // empty" 2>/dev/null)
            seats_total=$(echo "$lic_json" | jq -r ".rows[$i].seats // 0" 2>/dev/null)
            seats_used=$(echo "$lic_json" | jq -r ".rows[$i].seats - .rows[$i].free_seats_count // 0" 2>/dev/null)
            seats_free=$(echo "$lic_json" | jq -r ".rows[$i].free_seats_count // 0" 2>/dev/null)

            if [[ -n "$name" ]]; then
                local safe_name
                safe_name=$(sanitize_label "$name")
                local label="license=\"${safe_name}\""

                add_metric_value "snipeit_license_seats_total" "${seats_total:-0}" "$label"
                add_metric_value "snipeit_license_seats_used" "${seats_used:-0}" "$label"
                add_metric_value "snipeit_license_seats_free" "${seats_free:-0}" "$label"
            fi
        done
    fi
}

collect_consumables() {
    local con_json
    con_json=$(api_get "/consumables?limit=100")

    if [[ -z "$con_json" ]]; then
        return
    fi

    local total
    total=$(echo "$con_json" | jq -r '.total // 0' 2>/dev/null)
    add_metric "snipeit_consumables_total" "gauge" "Total number of consumable types" "${total:-0}"

    if [[ "$total" -gt 0 ]]; then
        OUTPUT+="# HELP snipeit_consumable_remaining Remaining quantity of a consumable
# TYPE snipeit_consumable_remaining gauge
# HELP snipeit_consumable_min_qty Minimum quantity alert threshold for a consumable
# TYPE snipeit_consumable_min_qty gauge
"
        local i
        for ((i = 0; i < total && i < 50; i++)); do
            local name remaining min_qty
            name=$(echo "$con_json" | jq -r ".rows[$i].name // empty" 2>/dev/null)
            remaining=$(echo "$con_json" | jq -r ".rows[$i].remaining // 0" 2>/dev/null)
            min_qty=$(echo "$con_json" | jq -r ".rows[$i].min_amt // 0" 2>/dev/null)

            if [[ -n "$name" ]]; then
                local safe_name
                safe_name=$(sanitize_label "$name")
                local label="consumable=\"${safe_name}\""

                add_metric_value "snipeit_consumable_remaining" "${remaining:-0}" "$label"
                add_metric_value "snipeit_consumable_min_qty" "${min_qty:-0}" "$label"
            fi
        done
    fi
}

collect_warranty() {
    local expiry_date
    expiry_date=$(date -d "+${WARRANTY_DAYS} days" +%Y-%m-%d 2>/dev/null || date -v+${WARRANTY_DAYS}d +%Y-%m-%d 2>/dev/null)

    if [[ -z "$expiry_date" ]]; then
        return
    fi

    local warranty_json
    warranty_json=$(api_get "/hardware?warranty_lookup=true&limit=1&order=asc&sort=warranty_months")

    # Count assets with warranty expiring within threshold
    # The API does not have a direct warranty filter, so count from the activity report
    local audit_json
    audit_json=$(api_get "/reports/activity?limit=1")

    # Fallback: use total and note that warranty filtering requires manual check
    add_metric "snipeit_assets_warranty_expiring" "gauge" "Assets with warranty expiring within ${WARRANTY_DAYS} days (approximate)" "0"
}

collect_counts() {
    # Users
    local users_json
    users_json=$(api_get "/users?limit=1")
    if [[ -n "$users_json" ]]; then
        local users_total
        users_total=$(echo "$users_json" | jq -r '.total // 0' 2>/dev/null)
        add_metric "snipeit_users_total" "gauge" "Total number of users" "${users_total:-0}"
    fi

    # Locations
    local loc_json
    loc_json=$(api_get "/locations?limit=1")
    if [[ -n "$loc_json" ]]; then
        local loc_total
        loc_total=$(echo "$loc_json" | jq -r '.total // 0' 2>/dev/null)
        add_metric "snipeit_locations_total" "gauge" "Total number of locations" "${loc_total:-0}"
    fi
}

# --- Output ---

write_output() {
    if [[ "$TEXTFILE_MODE" == true ]]; then
        local output_file="${TEXTFILE_DIR}/snipeit.prom"
        local temp_file="${output_file}.$$"

        mkdir -p "$TEXTFILE_DIR"
        echo "$OUTPUT" > "$temp_file"
        mv "$temp_file" "$output_file"
        echo "# Wrote metrics to ${output_file}" >&2
    else
        echo "$OUTPUT"
    fi
}

serve_http() {
    if ! command -v nc &>/dev/null && ! command -v ncat &>/dev/null; then
        echo "# ERROR: nc (netcat) or ncat required for HTTP mode" >&2
        exit 1
    fi

    echo "# Snipe-IT exporter listening on port ${HTTP_PORT}" >&2
    echo "# Metrics endpoint: http://localhost:${HTTP_PORT}/metrics" >&2

    local nc_cmd="nc"
    if command -v ncat &>/dev/null; then
        nc_cmd="ncat"
    fi

    while true; do
        OUTPUT=""
        START_TIME=$(date +%s%N)

        add_metric "snipeit_exporter_info" "gauge" "Exporter version information" "1" "version=\"${VERSION}\""

        if collect_status; then
            collect_assets
            collect_licenses
            collect_consumables
            collect_warranty
            collect_counts
        fi

        local end_time duration
        end_time=$(date +%s%N)
        duration=$(echo "scale=2; ($end_time - $START_TIME) / 1000000000" | bc 2>/dev/null || echo "0")
        add_metric "snipeit_exporter_duration_seconds" "gauge" "Time to generate all metrics" "$duration"
        add_metric "snipeit_exporter_last_run_timestamp" "gauge" "Unix timestamp of last successful run" "$(date +%s)"

        local content_length=${#OUTPUT}
        local response="HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4; charset=utf-8\r\nContent-Length: ${content_length}\r\nConnection: close\r\n\r\n${OUTPUT}"

        echo -e "$response" | $nc_cmd -l -p "$HTTP_PORT" -q 1 2>/dev/null || \
        echo -e "$response" | $nc_cmd -l "$HTTP_PORT" -c 2>/dev/null || \
        echo -e "$response" | $nc_cmd -l -p "$HTTP_PORT" 2>/dev/null || true
    done
}

install_cron() {
    if [[ $EUID -ne 0 ]]; then
        echo "# ERROR: --install requires root" >&2
        exit 1
    fi

    local script_path
    script_path=$(readlink -f "$0")

    cat > /etc/cron.d/snipeit-exporter <<EOF
# Snipe-IT Prometheus Exporter -- runs every 5 minutes
SNIPEIT_URL=${SNIPEIT_URL}
SNIPEIT_TOKEN=${SNIPEIT_TOKEN}
TEXTFILE_DIR=${TEXTFILE_DIR}
WARRANTY_DAYS=${WARRANTY_DAYS}
*/5 * * * * root ${script_path} --textfile 2>/dev/null
EOF

    chmod 644 /etc/cron.d/snipeit-exporter
    echo "# Installed cron job: /etc/cron.d/snipeit-exporter" >&2
    echo "# Metrics will be written to: ${TEXTFILE_DIR}/snipeit.prom" >&2
}

# --- Main ---

main() {
    for arg in "$@"; do
        case "$arg" in
            --textfile) TEXTFILE_MODE=true ;;
            --http) HTTP_MODE=true ;;
            -p|--port) shift; HTTP_PORT="${1:-$HTTP_PORT}" ;;
            --install)
                check_dependencies
                validate_config
                install_cron
                exit 0
                ;;
            --help|-h) usage ;;
            *) ;;
        esac
    done

    check_dependencies
    validate_config

    if [[ "$HTTP_MODE" == true ]]; then
        serve_http
        exit 0
    fi

    START_TIME=$(date +%s%N)

    add_metric "snipeit_exporter_info" "gauge" "Exporter version information" "1" "version=\"${VERSION}\""

    if collect_status; then
        collect_assets
        collect_licenses
        collect_consumables
        collect_warranty
        collect_counts
    fi

    local end_time duration
    end_time=$(date +%s%N)
    duration=$(echo "scale=2; ($end_time - $START_TIME) / 1000000000" | bc 2>/dev/null || echo "0")
    add_metric "snipeit_exporter_duration_seconds" "gauge" "Time to generate all metrics" "$duration"
    add_metric "snipeit_exporter_last_run_timestamp" "gauge" "Unix timestamp of last successful run" "$(date +%s)"

    write_output
}

main "$@"
