#!/usr/bin/env bash

#########################################################################################
#### freeradius-exporter.sh — Prometheus metrics exporter for FreeRADIUS             ####
#### Exports authentication, accounting, and proxy statistics from the               ####
#### FreeRADIUS status server as Prometheus metrics                                  ####
#### Requires: bash 4+, radclient                                                   ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.00                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   ./freeradius-exporter.sh --http --port 9620                                   ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -uo pipefail

# ============================================================================
# CONFIGURATION VARIABLES
# ============================================================================

RADIUS_HOST="${RADIUS_HOST:-localhost}"
RADIUS_STATUS_PORT="${RADIUS_STATUS_PORT:-18121}"
RADIUS_SECRET="${RADIUS_SECRET:-adminsecret}"
TEXTFILE_DIR="/var/lib/node_exporter"
OUTPUT_FILE=""
HTTP_MODE=false
HTTP_PORT=9620

EXPORTER_VERSION="1.00"

# ============================================================================
# HELPER FUNCTIONS
# ============================================================================

show_usage() {
    cat <<EOF
Usage: $0 [OPTIONS]

Export FreeRADIUS statistics as Prometheus metrics via the status server.

MODES:
    --textfile      Write to node_exporter textfile collector
    --http          Run HTTP server on port $HTTP_PORT

OPTIONS:
    -p, --port        HTTP port (default: 9620)
    -o, --output      Output file path
    -H, --host        RADIUS host (default: localhost)
    -s, --secret      RADIUS status secret (default: adminsecret)
    --status-port     RADIUS status port (default: 18121)

ENVIRONMENT VARIABLES:
    RADIUS_HOST          RADIUS server host (default: localhost)
    RADIUS_STATUS_PORT   Status server port (default: 18121)
    RADIUS_SECRET        Status server secret (default: adminsecret)

EXAMPLES:
    $0 --textfile                              # Write to textfile collector
    $0 --http --port 9620                      # Run HTTP server
    $0 -o /tmp/freeradius.prom                 # Write to custom file
    RADIUS_SECRET=mysecret $0 --http           # Custom secret via env

NOTE:
    FreeRADIUS must have the status server enabled. See:
    /etc/freeradius/3.0/sites-enabled/status (Debian/Ubuntu)
    /etc/raddb/sites-enabled/status (RHEL/CentOS)

EOF
    exit 0
}

parse_args() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            -h|--help) show_usage ;;
            --textfile) OUTPUT_FILE="$TEXTFILE_DIR/freeradius.prom"; shift ;;
            --http) HTTP_MODE=true; shift ;;
            -p|--port) HTTP_PORT="$2"; shift 2 ;;
            -o|--output) OUTPUT_FILE="$2"; shift 2 ;;
            -H|--host) RADIUS_HOST="$2"; shift 2 ;;
            -s|--secret) RADIUS_SECRET="$2"; shift 2 ;;
            --status-port) RADIUS_STATUS_PORT="$2"; shift 2 ;;
            *) echo "Unknown option: $1" >&2; exit 1 ;;
        esac
    done
}

# Query the FreeRADIUS status server via radclient
# Returns: raw attribute-value pair output, or empty on failure
query_status_server() {
    echo "Message-Authenticator = 0x00, FreeRADIUS-Statistics-Type = All" \
        | radclient "${RADIUS_HOST}:${RADIUS_STATUS_PORT}" status "${RADIUS_SECRET}" 2>/dev/null
}

# Extract a numeric value from radclient output
# Args: $1 - attribute name, $2 - radclient output
# Returns: numeric value or 0 if not found
extract_value() {
    local attr="$1"
    local data="$2"
    local val
    val=$(echo "$data" | grep -F "$attr" | awk -F'= ' '{print $2}' | tr -d '[:space:]')
    echo "${val:-0}"
}

# ============================================================================
# METRIC GENERATION
# ============================================================================

# Generate all Prometheus metrics
# Returns: Prometheus text format metrics on stdout
generate_metrics() {
    local script_start
    script_start=$(date +%s)

    # Check radclient is available
    if ! command -v radclient >/dev/null 2>&1; then
        echo "ERROR: radclient command not found" >&2
        cat <<EOF
# HELP freeradius_up FreeRADIUS status server reachable
# TYPE freeradius_up gauge
freeradius_up 0
EOF
        return
    fi

    # Query the status server
    local raw_output
    raw_output=$(query_status_server)

    if [[ -z "$raw_output" ]]; then
        cat <<EOF
# HELP freeradius_up FreeRADIUS status server reachable
# TYPE freeradius_up gauge
freeradius_up 0
EOF
        return
    fi

    cat <<EOF
# HELP freeradius_up FreeRADIUS status server reachable
# TYPE freeradius_up gauge
freeradius_up 1
EOF

    echo ""

    # ========================================================================
    # ACCESS / AUTHENTICATION METRICS
    # ========================================================================

    local access_requests access_accepts access_rejects access_challenges
    access_requests=$(extract_value "FreeRADIUS-Total-Access-Requests" "$raw_output")
    access_accepts=$(extract_value "FreeRADIUS-Total-Access-Accepts" "$raw_output")
    access_rejects=$(extract_value "FreeRADIUS-Total-Access-Rejects" "$raw_output")
    access_challenges=$(extract_value "FreeRADIUS-Total-Access-Challenges" "$raw_output")

    cat <<EOF
# HELP freeradius_access_requests_total Total Access-Request packets received
# TYPE freeradius_access_requests_total counter
freeradius_access_requests_total $access_requests

# HELP freeradius_access_accepts_total Total Access-Accept packets sent
# TYPE freeradius_access_accepts_total counter
freeradius_access_accepts_total $access_accepts

# HELP freeradius_access_rejects_total Total Access-Reject packets sent
# TYPE freeradius_access_rejects_total counter
freeradius_access_rejects_total $access_rejects

# HELP freeradius_access_challenges_total Total Access-Challenge packets sent
# TYPE freeradius_access_challenges_total counter
freeradius_access_challenges_total $access_challenges
EOF

    echo ""

    local auth_responses auth_duplicate auth_malformed auth_invalid auth_dropped auth_unknown
    auth_responses=$(extract_value "FreeRADIUS-Total-Auth-Responses" "$raw_output")
    auth_duplicate=$(extract_value "FreeRADIUS-Total-Auth-Duplicate-Requests" "$raw_output")
    auth_malformed=$(extract_value "FreeRADIUS-Total-Auth-Malformed-Requests" "$raw_output")
    auth_invalid=$(extract_value "FreeRADIUS-Total-Auth-Invalid-Requests" "$raw_output")
    auth_dropped=$(extract_value "FreeRADIUS-Total-Auth-Dropped-Requests" "$raw_output")
    auth_unknown=$(extract_value "FreeRADIUS-Total-Auth-Unknown-Types" "$raw_output")

    cat <<EOF
# HELP freeradius_auth_responses_total Total authentication responses sent
# TYPE freeradius_auth_responses_total counter
freeradius_auth_responses_total $auth_responses

# HELP freeradius_auth_duplicate_requests_total Total duplicate authentication requests
# TYPE freeradius_auth_duplicate_requests_total counter
freeradius_auth_duplicate_requests_total $auth_duplicate

# HELP freeradius_auth_malformed_requests_total Total malformed authentication requests
# TYPE freeradius_auth_malformed_requests_total counter
freeradius_auth_malformed_requests_total $auth_malformed

# HELP freeradius_auth_invalid_requests_total Total invalid authentication requests
# TYPE freeradius_auth_invalid_requests_total counter
freeradius_auth_invalid_requests_total $auth_invalid

# HELP freeradius_auth_dropped_requests_total Total dropped authentication requests
# TYPE freeradius_auth_dropped_requests_total counter
freeradius_auth_dropped_requests_total $auth_dropped

# HELP freeradius_auth_unknown_types_total Total unknown authentication packet types
# TYPE freeradius_auth_unknown_types_total counter
freeradius_auth_unknown_types_total $auth_unknown
EOF

    echo ""

    # ========================================================================
    # ACCOUNTING METRICS
    # ========================================================================

    local acct_requests acct_responses acct_duplicate acct_malformed acct_invalid acct_dropped
    acct_requests=$(extract_value "FreeRADIUS-Total-Accounting-Requests" "$raw_output")
    acct_responses=$(extract_value "FreeRADIUS-Total-Accounting-Responses" "$raw_output")
    acct_duplicate=$(extract_value "FreeRADIUS-Total-Acct-Duplicate-Requests" "$raw_output")
    acct_malformed=$(extract_value "FreeRADIUS-Total-Acct-Malformed-Requests" "$raw_output")
    acct_invalid=$(extract_value "FreeRADIUS-Total-Acct-Invalid-Requests" "$raw_output")
    acct_dropped=$(extract_value "FreeRADIUS-Total-Acct-Dropped-Requests" "$raw_output")

    cat <<EOF
# HELP freeradius_accounting_requests_total Total Accounting-Request packets received
# TYPE freeradius_accounting_requests_total counter
freeradius_accounting_requests_total $acct_requests

# HELP freeradius_accounting_responses_total Total Accounting-Response packets sent
# TYPE freeradius_accounting_responses_total counter
freeradius_accounting_responses_total $acct_responses

# HELP freeradius_accounting_duplicate_requests_total Total duplicate accounting requests
# TYPE freeradius_accounting_duplicate_requests_total counter
freeradius_accounting_duplicate_requests_total $acct_duplicate

# HELP freeradius_accounting_malformed_requests_total Total malformed accounting requests
# TYPE freeradius_accounting_malformed_requests_total counter
freeradius_accounting_malformed_requests_total $acct_malformed

# HELP freeradius_accounting_invalid_requests_total Total invalid accounting requests
# TYPE freeradius_accounting_invalid_requests_total counter
freeradius_accounting_invalid_requests_total $acct_invalid

# HELP freeradius_accounting_dropped_requests_total Total dropped accounting requests
# TYPE freeradius_accounting_dropped_requests_total counter
freeradius_accounting_dropped_requests_total $acct_dropped
EOF

    echo ""

    # ========================================================================
    # PROXY METRICS
    # ========================================================================

    local proxy_access_requests proxy_access_accepts proxy_access_rejects
    local proxy_acct_requests proxy_acct_responses
    proxy_access_requests=$(extract_value "FreeRADIUS-Total-Proxy-Access-Requests" "$raw_output")
    proxy_access_accepts=$(extract_value "FreeRADIUS-Total-Proxy-Access-Accepts" "$raw_output")
    proxy_access_rejects=$(extract_value "FreeRADIUS-Total-Proxy-Access-Rejects" "$raw_output")
    proxy_acct_requests=$(extract_value "FreeRADIUS-Total-Proxy-Accounting-Requests" "$raw_output")
    proxy_acct_responses=$(extract_value "FreeRADIUS-Total-Proxy-Accounting-Responses" "$raw_output")

    cat <<EOF
# HELP freeradius_proxy_access_requests_total Total proxied Access-Request packets
# TYPE freeradius_proxy_access_requests_total counter
freeradius_proxy_access_requests_total $proxy_access_requests

# HELP freeradius_proxy_access_accepts_total Total proxied Access-Accept packets
# TYPE freeradius_proxy_access_accepts_total counter
freeradius_proxy_access_accepts_total $proxy_access_accepts

# HELP freeradius_proxy_access_rejects_total Total proxied Access-Reject packets
# TYPE freeradius_proxy_access_rejects_total counter
freeradius_proxy_access_rejects_total $proxy_access_rejects

# HELP freeradius_proxy_accounting_requests_total Total proxied Accounting-Request packets
# TYPE freeradius_proxy_accounting_requests_total counter
freeradius_proxy_accounting_requests_total $proxy_acct_requests

# HELP freeradius_proxy_accounting_responses_total Total proxied Accounting-Response packets
# TYPE freeradius_proxy_accounting_responses_total counter
freeradius_proxy_accounting_responses_total $proxy_acct_responses
EOF

    echo ""

    # ========================================================================
    # EXPORTER RUNTIME
    # ========================================================================

    local script_end script_duration
    script_end=$(date +%s)
    script_duration=$((script_end - script_start))

    cat <<EOF
# HELP freeradius_exporter_duration_seconds Time to generate all metrics
# TYPE freeradius_exporter_duration_seconds gauge
freeradius_exporter_duration_seconds $script_duration

# HELP freeradius_exporter_last_run_timestamp Unix timestamp of last successful run
# TYPE freeradius_exporter_last_run_timestamp gauge
freeradius_exporter_last_run_timestamp $script_end
EOF

    echo ""
}

# ============================================================================
# HTTP SERVER MODE
# ============================================================================

# Run simple HTTP server using netcat
# Serves metrics on /metrics endpoint
run_http_server() {
    echo "Starting FreeRADIUS exporter on port $HTTP_PORT..." >&2

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

    while true; do
        {
            read -r request
            if [[ "$request" =~ ^GET\ /metrics ]]; then
                echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\n\r"
                generate_metrics
            else
                echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r"
                cat <<EOF
<!DOCTYPE html>
<html>
<head><title>FreeRADIUS Exporter v${EXPORTER_VERSION}</title></head>
<body>
<h1>FreeRADIUS Prometheus Exporter v${EXPORTER_VERSION}</h1>
<p><a href="/metrics">Metrics</a></p>
<p>Authentication, accounting, and proxy statistics from the FreeRADIUS status server.</p>
</body>
</html>
EOF
            fi
        } | nc -l -p "$HTTP_PORT" -q 1 2>/dev/null
    done
}

# ============================================================================
# MAIN EXECUTION
# ============================================================================

main() {
    parse_args "$@"

    if [[ "$HTTP_MODE" = true ]]; then
        run_http_server
    elif [[ -n "$OUTPUT_FILE" ]]; then
        local output_dir
        output_dir="$(dirname "$OUTPUT_FILE")"
        mkdir -p "$output_dir"

        local temp_file
        temp_file=$(mktemp "${output_dir}/.freeradius_metrics.XXXXXX")

        if ! generate_metrics > "$temp_file" 2>/dev/null; then
            rm -f "$temp_file"
            echo "ERROR: Failed to generate metrics" >&2
            exit 1
        fi

        local file_lines
        file_lines=$(wc -l < "$temp_file" 2>/dev/null || echo 0)

        if [[ "$file_lines" -lt 5 ]]; then
            rm -f "$temp_file"
            echo "ERROR: Metrics file too small ($file_lines lines), keeping previous" >&2
            exit 1
        fi

        chmod 644 "$temp_file"
        mv -f "$temp_file" "$OUTPUT_FILE"

        echo "Metrics written to $OUTPUT_FILE ($file_lines lines)" >&2
    else
        generate_metrics
    fi
}

main "$@"
