#!/usr/bin/env bash
#
# Flarum Prometheus Metrics Exporter
#
# Prometheus exporter for Flarum forum software.
# Uses MariaDB/MySQL to query the Flarum database directly for
# user counts, discussion/post stats, tag breakdowns, engagement
# metrics (likes, flags), and overall forum health.
#
# Usage:
#   FLARUM_DB_PASS="xxx" ./flarum-exporter.sh
#   FLARUM_DB_PASS="xxx" ./flarum-exporter.sh --textfile
#   FLARUM_DB_PASS="xxx" ./flarum-exporter.sh --http
#
# Parameters:
#   --textfile   Write to textfile collector directory
#   --http       Start a simple HTTP server on port 9199
#   --help       Show usage
#
# Environment:
#   FLARUM_DB_HOST    Database host (default: localhost)
#   FLARUM_DB_PORT    Database port (default: 3306)
#   FLARUM_DB_NAME    Database name (default: flarum)
#   FLARUM_DB_USER    Database user (default: flarum)
#   FLARUM_DB_PASS    Database password (required)
#   TEXTFILE_DIR      Textfile collector directory (default: /var/lib/node_exporter/textfile_collector)
#
# Author:  Phil Connor
# Contact: contact@mylinux.work
# Website: https://mylinux.work
# License: MIT
# Version: 1.0
#
# Metrics Exported:
#   Core:
#     - flarum_up
#     - flarum_exporter_info{version}
#
#   Users:
#     - flarum_users_total
#     - flarum_users_email_confirmed
#     - flarum_users_suspended
#     - flarum_users_active_1d
#     - flarum_users_active_7d
#     - flarum_users_active_30d
#
#   Content:
#     - flarum_discussions_total
#     - flarum_discussions_open
#     - flarum_discussions_locked
#     - flarum_discussions_sticky
#     - flarum_posts_total
#     - flarum_comments_total
#
#   Tags:
#     - flarum_tags_total
#     - flarum_discussions_per_tag{tag}
#
#   Engagement:
#     - flarum_likes_total
#     - flarum_flags_total
#
#   Exporter:
#     - flarum_exporter_duration_seconds
#     - flarum_exporter_last_run_timestamp

set -euo pipefail

# --- Configuration ---
readonly VERSION="1.0"
readonly SCRIPT_NAME="$(basename "$0")"
FLARUM_DB_HOST="${FLARUM_DB_HOST:-localhost}"
FLARUM_DB_PORT="${FLARUM_DB_PORT:-3306}"
FLARUM_DB_NAME="${FLARUM_DB_NAME:-flarum}"
FLARUM_DB_USER="${FLARUM_DB_USER:-flarum}"
FLARUM_DB_PASS="${FLARUM_DB_PASS:-}"
TEXTFILE_DIR="${TEXTFILE_DIR:-/var/lib/node_exporter/textfile_collector}"
TEXTFILE_MODE=false
HTTP_MODE=false
readonly HTTP_PORT=9199
OUTPUT=""
START_TIME=""

# --- Functions ---

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

Flarum Prometheus Metrics Exporter

Options:
  --textfile    Write metrics to textfile collector directory
  --http        Start HTTP server on port ${HTTP_PORT} serving /metrics
  --help        Show this help message

Environment Variables:
  FLARUM_DB_HOST    Database host (default: localhost)
  FLARUM_DB_PORT    Database port (default: 3306)
  FLARUM_DB_NAME    Database name (default: flarum)
  FLARUM_DB_USER    Database user (default: flarum)
  FLARUM_DB_PASS    Database password (required)
  TEXTFILE_DIR      Output directory (default: /var/lib/node_exporter/textfile_collector)

Examples:
  FLARUM_DB_PASS="xxx" $SCRIPT_NAME
  FLARUM_DB_PASS="xxx" $SCRIPT_NAME --textfile
  FLARUM_DB_PASS="xxx" $SCRIPT_NAME --http
EOF
    exit 0
}

check_dependencies() {
    local missing=()
    for cmd in mysql bc; 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 "$FLARUM_DB_PASS" ]]; then
        echo "ERROR: FLARUM_DB_PASS environment variable is required" >&2
        exit 1
    fi
}

db_query() {
    local query="$1"
    mysql -h "$FLARUM_DB_HOST" -P "$FLARUM_DB_PORT" -u "$FLARUM_DB_USER" \
        -p"$FLARUM_DB_PASS" "$FLARUM_DB_NAME" -N -B -e "$query" 2>/dev/null || echo ""
}

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

collect_health() {
    local result
    result=$(db_query "SELECT 1;")

    if [[ -z "$result" || "$result" != "1" ]]; then
        add_metric "flarum_up" "gauge" "Flarum database reachability (1=up, 0=down)" "0"
        return 1
    fi

    add_metric "flarum_up" "gauge" "Flarum database reachability (1=up, 0=down)" "1"
    return 0
}

collect_users() {
    local total
    total=$(db_query "SELECT COUNT(*) FROM users;")
    add_metric "flarum_users_total" "gauge" "Total number of registered users" "${total:-0}"

    local confirmed
    confirmed=$(db_query "SELECT COUNT(*) FROM users WHERE is_email_confirmed = 1;")
    add_metric "flarum_users_email_confirmed" "gauge" "Users with confirmed email" "${confirmed:-0}"

    local suspended
    suspended=$(db_query "SELECT COUNT(*) FROM users WHERE suspended_until IS NOT NULL AND suspended_until > NOW();")
    add_metric "flarum_users_suspended" "gauge" "Currently suspended users" "${suspended:-0}"

    local active_1d
    active_1d=$(db_query "SELECT COUNT(*) FROM users WHERE last_seen_at >= NOW() - INTERVAL 1 DAY;")
    add_metric "flarum_users_active_1d" "gauge" "Users active in the past 24 hours" "${active_1d:-0}"

    local active_7d
    active_7d=$(db_query "SELECT COUNT(*) FROM users WHERE last_seen_at >= NOW() - INTERVAL 7 DAY;")
    add_metric "flarum_users_active_7d" "gauge" "Users active in the past 7 days" "${active_7d:-0}"

    local active_30d
    active_30d=$(db_query "SELECT COUNT(*) FROM users WHERE last_seen_at >= NOW() - INTERVAL 30 DAY;")
    add_metric "flarum_users_active_30d" "gauge" "Users active in the past 30 days" "${active_30d:-0}"
}

collect_content() {
    local discussions_total
    discussions_total=$(db_query "SELECT COUNT(*) FROM discussions;")
    add_metric "flarum_discussions_total" "gauge" "Total number of discussions" "${discussions_total:-0}"

    local discussions_open
    discussions_open=$(db_query "SELECT COUNT(*) FROM discussions WHERE hidden_at IS NULL;")
    add_metric "flarum_discussions_open" "gauge" "Open discussions (not hidden/deleted)" "${discussions_open:-0}"

    local discussions_locked
    discussions_locked=$(db_query "SELECT COUNT(*) FROM discussions WHERE is_locked = 1;")
    add_metric "flarum_discussions_locked" "gauge" "Locked discussions" "${discussions_locked:-0}"

    local discussions_sticky
    discussions_sticky=$(db_query "SELECT COUNT(*) FROM discussions WHERE is_sticky = 1;")
    add_metric "flarum_discussions_sticky" "gauge" "Sticky discussions" "${discussions_sticky:-0}"

    local posts_total
    posts_total=$(db_query "SELECT COUNT(*) FROM posts;")
    add_metric "flarum_posts_total" "gauge" "Total number of posts" "${posts_total:-0}"

    local comments_total
    comments_total=$(db_query "SELECT COALESCE(SUM(comment_count), 0) FROM discussions;")
    add_metric "flarum_comments_total" "gauge" "Total comment count across all discussions" "${comments_total:-0}"
}

collect_tags() {
    local tags_total
    tags_total=$(db_query "SELECT COUNT(*) FROM tags;")
    add_metric "flarum_tags_total" "gauge" "Total number of tags" "${tags_total:-0}"

    # Per-tag discussion counts
    local tag_data
    tag_data=$(db_query "SELECT t.name, COUNT(dt.discussion_id) FROM tags t LEFT JOIN discussion_tag dt ON t.id = dt.tag_id GROUP BY t.id, t.name;")

    if [[ -n "$tag_data" ]]; then
        OUTPUT+="# HELP flarum_discussions_per_tag Number of discussions per tag
# TYPE flarum_discussions_per_tag gauge
"
        while IFS=$'\t' read -r tag_name count; do
            if [[ -n "$tag_name" ]]; then
                local safe_tag
                safe_tag=$(sanitize_label "$tag_name")
                add_metric_value "flarum_discussions_per_tag" "${count:-0}" "tag=\"${safe_tag}\""
            fi
        done <<< "$tag_data"
    fi
}

collect_engagement() {
    local likes
    likes=$(db_query "SELECT COUNT(*) FROM post_likes;" 2>/dev/null)
    if [[ -n "$likes" ]]; then
        add_metric "flarum_likes_total" "gauge" "Total number of likes" "${likes:-0}"
    fi

    local flags
    flags=$(db_query "SELECT COUNT(*) FROM flags;" 2>/dev/null)
    if [[ -n "$flags" ]]; then
        add_metric "flarum_flags_total" "gauge" "Total number of flags" "${flags:-0}"
    fi
}

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

        mkdir -p "$TEXTFILE_DIR"
        echo "$OUTPUT" > "$temp_file"
        mv "$temp_file" "$output_file"
    else
        echo "$OUTPUT"
    fi
}

serve_http() {
    # Check for netcat
    local nc_cmd=""
    if command -v ncat &>/dev/null; then
        nc_cmd="ncat"
    elif command -v nc &>/dev/null; then
        nc_cmd="nc"
    else
        echo "ERROR: Neither ncat nor nc found. Install nmap-ncat or netcat." >&2
        exit 1
    fi

    echo "# Flarum exporter listening on port ${HTTP_PORT}..."

    while true; do
        {
            # Read the HTTP request
            local request=""
            read -r request || true

            # Collect fresh metrics
            OUTPUT=""
            START_TIME=$(date +%s%N)

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

            if collect_health; then
                collect_users
                collect_content
                collect_tags
                collect_engagement
            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 "flarum_exporter_duration_seconds" "gauge" "Time to generate all metrics" "$duration"
            add_metric "flarum_exporter_last_run_timestamp" "gauge" "Unix timestamp of last successful run" "$(date +%s)"

            # Serve response
            local body="$OUTPUT"
            local content_length=${#body}
            printf "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4; charset=utf-8\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" "$content_length" "$body"
        } | "$nc_cmd" -l -p "$HTTP_PORT" 2>/dev/null || "$nc_cmd" -l "$HTTP_PORT" 2>/dev/null || true
    done
}

# --- Main ---

main() {
    # Parse arguments
    for arg in "$@"; do
        case "$arg" in
            --textfile) TEXTFILE_MODE=true ;;
            --http) HTTP_MODE=true ;;
            --help|-h) usage ;;
            *) echo "Unknown option: $arg" >&2; usage ;;
        esac
    done

    check_dependencies
    validate_config

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

    START_TIME=$(date +%s%N)

    # Exporter info
    add_metric "flarum_exporter_info" "gauge" "Exporter version information" "1" "version=\"${VERSION}\""

    # Collect metrics
    if collect_health; then
        collect_users
        collect_content
        collect_tags
        collect_engagement
    fi

    # Exporter performance
    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 "flarum_exporter_duration_seconds" "gauge" "Time to generate all metrics" "$duration"
    add_metric "flarum_exporter_last_run_timestamp" "gauge" "Unix timestamp of last successful run" "$(date +%s)"

    write_output
}

main "$@"
