#!/usr/bin/env bash

#########################################################################################
#### nextcloud-exporter.sh — Prometheus metrics exporter for Nextcloud               ####
#### Exports server info, storage stats, share counts, active users,                 ####
#### and system health as Prometheus metrics                                          ####
#### Requires: bash 4+, curl, jq                                                    ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.00                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   ./nextcloud-exporter.sh --http --port 9618                                    ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -uo pipefail

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

NEXTCLOUD_URL="${NEXTCLOUD_URL:-}"
NEXTCLOUD_USER="${NEXTCLOUD_USER:-}"
NEXTCLOUD_PASS="${NEXTCLOUD_PASS:-}"
TEXTFILE_DIR="/var/lib/node_exporter"
OUTPUT_FILE=""
HTTP_MODE=false
HTTP_PORT="${HTTP_PORT:-9618}"

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

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

Export Nextcloud server info as Prometheus metrics.

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

OPTIONS:
    -p, --port        HTTP port (default: 9618)
    -o, --output      Output file path
    --url             Nextcloud base URL (or set NEXTCLOUD_URL)
    --user            Nextcloud username (or set NEXTCLOUD_USER)
    --pass            Nextcloud app password (or set NEXTCLOUD_PASS)

EXAMPLES:
    $0 --http --port 9618
    $0 --textfile
    $0 --url https://cloud.example.com --user admin --pass app-password
    $0 -o /tmp/nextcloud.prom

ENVIRONMENT VARIABLES:
    NEXTCLOUD_URL     Nextcloud base URL (e.g., https://cloud.example.com)
    NEXTCLOUD_USER    Nextcloud username
    NEXTCLOUD_PASS    Nextcloud app password
    HTTP_PORT         HTTP server port (default: 9618)

EOF
    exit 0
}

parse_args() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            -h|--help) show_usage ;;
            --textfile) OUTPUT_FILE="$TEXTFILE_DIR/nextcloud.prom"; shift ;;
            --http) HTTP_MODE=true; shift ;;
            -p|--port) HTTP_PORT="$2"; shift 2 ;;
            -o|--output) OUTPUT_FILE="$2"; shift 2 ;;
            --url) NEXTCLOUD_URL="$2"; shift 2 ;;
            --user) NEXTCLOUD_USER="$2"; shift 2 ;;
            --pass) NEXTCLOUD_PASS="$2"; shift 2 ;;
            *) echo "Unknown option: $1" >&2; exit 1 ;;
        esac
    done
}

check_prerequisites() {
    if ! command -v curl >/dev/null 2>&1; then
        echo "ERROR: curl not found" >&2
        return 1
    fi

    if ! command -v jq >/dev/null 2>&1; then
        echo "ERROR: jq not found (required for JSON parsing)" >&2
        return 1
    fi

    if [[ -z "$NEXTCLOUD_URL" ]]; then
        echo "ERROR: NEXTCLOUD_URL not set. Use --url or set NEXTCLOUD_URL" >&2
        return 1
    fi

    if [[ -z "$NEXTCLOUD_USER" || -z "$NEXTCLOUD_PASS" ]]; then
        echo "ERROR: NEXTCLOUD_USER and NEXTCLOUD_PASS required. Use --user/--pass or environment variables" >&2
        return 1
    fi

    return 0
}

# Escape special characters in Prometheus label values
# Args: $1 - string to escape
# Returns: escaped string safe for Prometheus labels
prom_escape() {
    local val="$1"
    val="${val//\\/\\\\}"
    val="${val//\"/\\\"}"
    val="${val//$'\n'/}"
    echo "$val"
}

# Fetch Nextcloud server info from OCS API
# Returns: JSON response on stdout, or empty on error
fetch_server_info() {
    local api_url="${NEXTCLOUD_URL%/}/ocs/v2.php/apps/serverinfo/api/v1/info?format=json"

    curl -sf --max-time 15 \
        -u "${NEXTCLOUD_USER}:${NEXTCLOUD_PASS}" \
        -H "OCS-APIRequest: true" \
        "$api_url" 2>/dev/null
}

# Safely extract a numeric value from JSON
# Args: $1 - JSON string, $2 - jq path
# Returns: numeric value or 0
jq_num() {
    local result
    result=$(echo "$1" | jq -r "$2" 2>/dev/null)
    if [[ -z "$result" || "$result" == "null" ]]; then
        echo "0"
    else
        echo "$result"
    fi
}

# Safely extract a string value from JSON
# Args: $1 - JSON string, $2 - jq path
# Returns: string value or empty
jq_str() {
    local result
    result=$(echo "$1" | jq -r "$2" 2>/dev/null)
    if [[ "$result" == "null" ]]; then
        echo ""
    else
        echo "$result"
    fi
}

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

generate_metrics() {
    local script_start
    script_start=$(date +%s%N 2>/dev/null || date +%s)

    if ! check_prerequisites; then
        cat <<EOF
# HELP nextcloud_up Whether the Nextcloud API is reachable
# TYPE nextcloud_up gauge
nextcloud_up 0
EOF
        return
    fi

    local api_response
    api_response=$(fetch_server_info)

    if [[ -z "$api_response" ]]; then
        cat <<EOF
# HELP nextcloud_up Whether the Nextcloud API is reachable
# TYPE nextcloud_up gauge
nextcloud_up 0
EOF
        return
    fi

    local data
    data=$(echo "$api_response" | jq '.ocs.data' 2>/dev/null)

    if [[ -z "$data" || "$data" == "null" ]]; then
        cat <<EOF
# HELP nextcloud_up Whether the Nextcloud API is reachable
# TYPE nextcloud_up gauge
nextcloud_up 0
EOF
        return
    fi

    cat <<EOF
# HELP nextcloud_up Whether the Nextcloud API is reachable
# TYPE nextcloud_up gauge
nextcloud_up 1
EOF

    echo ""

    # ========================================================================
    # VERSION INFO
    # ========================================================================

    local version
    version=$(jq_str "$data" '.nextcloud.system.version')

    cat <<EOF
# HELP nextcloud_info Nextcloud version information
# TYPE nextcloud_info gauge
nextcloud_info{version="$(prom_escape "$version")"} 1
EOF

    echo ""

    # ========================================================================
    # STORAGE METRICS
    # ========================================================================

    local num_users num_files num_storages
    num_users=$(jq_num "$data" '.nextcloud.storage.num_users')
    num_files=$(jq_num "$data" '.nextcloud.storage.num_files')
    num_storages=$(jq_num "$data" '.nextcloud.storage.num_storages')

    cat <<EOF
# HELP nextcloud_users_total Total number of users
# TYPE nextcloud_users_total gauge
nextcloud_users_total $num_users

# HELP nextcloud_files_total Total number of files
# TYPE nextcloud_files_total gauge
nextcloud_files_total $num_files

# HELP nextcloud_storages_total Total number of storages
# TYPE nextcloud_storages_total gauge
nextcloud_storages_total $num_storages
EOF

    echo ""

    # ========================================================================
    # ACTIVE USERS
    # ========================================================================

    local active_5m active_1h active_24h
    active_5m=$(jq_num "$data" '.activeUsers.last5minutes')
    active_1h=$(jq_num "$data" '.activeUsers.last1hour')
    active_24h=$(jq_num "$data" '.activeUsers.last24hours')

    cat <<EOF
# HELP nextcloud_active_users Number of active users by period
# TYPE nextcloud_active_users gauge
nextcloud_active_users{period="5m"} $active_5m
nextcloud_active_users{period="1h"} $active_1h
nextcloud_active_users{period="24h"} $active_24h
EOF

    echo ""

    # ========================================================================
    # SHARES
    # ========================================================================

    local shares_user shares_group shares_link shares_mail shares_fed_sent shares_fed_recv
    shares_user=$(jq_num "$data" '.nextcloud.shares.num_shares_user')
    shares_group=$(jq_num "$data" '.nextcloud.shares.num_shares_groups')
    shares_link=$(jq_num "$data" '.nextcloud.shares.num_shares_link')
    shares_mail=$(jq_num "$data" '.nextcloud.shares.num_shares_mail')
    shares_fed_sent=$(jq_num "$data" '.nextcloud.shares.num_fed_shares_sent')
    shares_fed_recv=$(jq_num "$data" '.nextcloud.shares.num_fed_shares_received')

    cat <<EOF
# HELP nextcloud_shares_total Number of shares by type
# TYPE nextcloud_shares_total gauge
nextcloud_shares_total{type="user"} $shares_user
nextcloud_shares_total{type="group"} $shares_group
nextcloud_shares_total{type="link"} $shares_link
nextcloud_shares_total{type="mail"} $shares_mail
nextcloud_shares_total{type="federated_sent"} $shares_fed_sent
nextcloud_shares_total{type="federated_received"} $shares_fed_recv
EOF

    echo ""

    # ========================================================================
    # SYSTEM METRICS
    # ========================================================================

    local freespace mem_total mem_free swap_total swap_free
    freespace=$(jq_num "$data" '.nextcloud.system.freespace')
    mem_total=$(jq_num "$data" '.nextcloud.system.mem_total')
    mem_free=$(jq_num "$data" '.nextcloud.system.mem_free')
    swap_total=$(jq_num "$data" '.nextcloud.system.swap_total')
    swap_free=$(jq_num "$data" '.nextcloud.system.swap_free')

    # mem_total and mem_free from Nextcloud are in KB, convert to bytes
    local mem_total_bytes mem_free_bytes swap_total_bytes swap_free_bytes
    mem_total_bytes=$((mem_total * 1024))
    mem_free_bytes=$((mem_free * 1024))
    swap_total_bytes=$((swap_total * 1024))
    swap_free_bytes=$((swap_free * 1024))

    cat <<EOF
# HELP nextcloud_free_space_bytes Free disk space in bytes
# TYPE nextcloud_free_space_bytes gauge
nextcloud_free_space_bytes $freespace

# HELP nextcloud_system_mem_total_bytes Total system memory in bytes
# TYPE nextcloud_system_mem_total_bytes gauge
nextcloud_system_mem_total_bytes $mem_total_bytes

# HELP nextcloud_system_mem_free_bytes Free system memory in bytes
# TYPE nextcloud_system_mem_free_bytes gauge
nextcloud_system_mem_free_bytes $mem_free_bytes

# HELP nextcloud_system_swap_total_bytes Total swap space in bytes
# TYPE nextcloud_system_swap_total_bytes gauge
nextcloud_system_swap_total_bytes $swap_total_bytes

# HELP nextcloud_system_swap_free_bytes Free swap space in bytes
# TYPE nextcloud_system_swap_free_bytes gauge
nextcloud_system_swap_free_bytes $swap_free_bytes
EOF

    echo ""

    # CPU load averages
    local cpu_1 cpu_5 cpu_15
    cpu_1=$(jq_num "$data" '.nextcloud.system.cpuload[0]')
    cpu_5=$(jq_num "$data" '.nextcloud.system.cpuload[1]')
    cpu_15=$(jq_num "$data" '.nextcloud.system.cpuload[2]')

    cat <<EOF
# HELP nextcloud_system_cpuload System CPU load averages
# TYPE nextcloud_system_cpuload gauge
nextcloud_system_cpuload{core="1"} $cpu_1
nextcloud_system_cpuload{core="5"} $cpu_5
nextcloud_system_cpuload{core="15"} $cpu_15
EOF

    echo ""

    # ========================================================================
    # DATABASE METRICS
    # ========================================================================

    local db_size
    db_size=$(jq_num "$data" '.server.database.size')

    cat <<EOF
# HELP nextcloud_database_size_bytes Database size in bytes
# TYPE nextcloud_database_size_bytes gauge
nextcloud_database_size_bytes $db_size
EOF

    echo ""

    # ========================================================================
    # PHP METRICS
    # ========================================================================

    local php_mem_limit php_upload_max
    php_mem_limit=$(jq_num "$data" '.server.php.memory_limit')
    php_upload_max=$(jq_num "$data" '.server.php.upload_max_filesize')

    cat <<EOF
# HELP nextcloud_php_memory_limit_bytes PHP memory limit in bytes
# TYPE nextcloud_php_memory_limit_bytes gauge
nextcloud_php_memory_limit_bytes $php_mem_limit

# HELP nextcloud_php_upload_max_bytes PHP upload max filesize in bytes
# TYPE nextcloud_php_upload_max_bytes gauge
nextcloud_php_upload_max_bytes $php_upload_max
EOF

    echo ""

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

    local script_end script_duration
    script_end=$(date +%s)
    if [[ "$script_start" =~ ^[0-9]{10}[0-9]+$ ]]; then
        local end_ns
        end_ns=$(date +%s%N 2>/dev/null || date +%s)
        script_duration=$(awk "BEGIN {printf \"%.3f\", ($end_ns - $script_start) / 1000000000}")
    else
        script_duration=$(( $(date +%s) - script_start ))
    fi

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

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

    echo ""
}

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

run_http_server() {
    echo "Starting Nextcloud 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>Nextcloud Exporter v1.00</title></head>
<body>
<h1>Nextcloud Prometheus Exporter v1.00</h1>
<p><a href="/metrics">Metrics</a></p>
<p>Exports Nextcloud server info, storage stats, share counts, active users, and system health.</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}/.nextcloud_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 10 ]]; 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 "$@"
