#!/bin/bash
################################################################################
# Script Name: jira-metrics.sh
# Version: 1.4
# Author: Phil Connor <contact@mylinux.work>
# License: MIT
# Description: Prometheus exporter for Jira metrics via REST API
#
# Exports metrics for:
#   - Issue counts by status (To Do, In Progress, Done, etc.)
#   - Issue counts by priority (Highest, High, Medium, Low, Lowest)
#   - Issue counts by type (Bug, Story, Task, Epic, Sub-task)
#   - Issue counts by project
#   - Backlog size and age (unresolved issues)
#   - Created vs resolved issue rates (24h, 7d)
#   - Average resolution time (resolved last 7d)
#   - Overdue issues count
#   - Sprint active issue counts (if boards configured)
#   - Component issue counts
#   - Assignee workload (top 15)
#   - Unassigned issue count
#   - SLA/resolution time buckets
#   - Login activity from security log (logins, failed logins, per-user)
#   - Plugin/add-on inventory (installed, enabled, disabled, user-installed)
#   - Service Desk queues, request types, SLA metrics, CSAT, organizations
#   - Scrape metadata
#
# Modes:
#   --textfile  Write to node_exporter textfile collector
#   --http      Run HTTP server for direct Prometheus scraping
#   stdout      Default: print metrics to stdout
#
# Requirements:
#   - curl and jq must be available
#   - Jira base URL, username, and API token (or password)
#   - For Jira Cloud: use email + API token
#   - For Jira Server/DC: use username + password or PAT
#
# Configuration:
#   Set environment variables or edit the defaults below:
#     JIRA_BASE_URL   - Full URL including port
#                       e.g. https://yourcompany.atlassian.net (Cloud)
#                       e.g. http://localhost:8080 (Server/DC)
#     JIRA_USER       - e.g. user or user@company.com (not needed for PAT)
#     JIRA_API_TOKEN  - API token, password, or Personal Access Token (PAT)
#     JIRA_PROJECTS   - Comma-separated project keys (default: all)
#     JIRA_HOME       - Jira home/data directory (for security log parsing)
#                       e.g. /mnt/ebs/application-data/jira
#
# Authentication:
#   Uses Bearer token authentication (Authorization: Bearer <token>).
#   - Jira Cloud:     Generate API token at
#                     https://id.atlassian.com/manage-profile/security/api-tokens
#   - Jira Server/DC: Generate a PAT via Profile → Personal Access Tokens
#                     (requires Jira 8.14+)
#
# Troubleshooting:
#   All metrics return zero:
#     1. Verify JIRA_BASE_URL includes the port (e.g. http://localhost:8080,
#        NOT http://localhost). Test with:
#          curl -H "Authorization: Bearer $JIRA_API_TOKEN" \
#            "$JIRA_BASE_URL/rest/api/2/search?jql=&maxResults=0"
#        You should see a JSON response with "total" > 0.
#     2. If the above returns blank, the URL or port is wrong.
#     3. If it returns HTML or a 404, check for a context path
#        (e.g. http://localhost:8080/jira).
#
#   403 Forbidden / AUTHENTICATION_DENIED:
#     - Basic auth may be blocked. This script uses Bearer token auth
#       which requires a PAT (Server/DC) or API token (Cloud).
#     - CAPTCHA may be triggered from prior failed logins. Clear it via
#       Jira Admin → User Management → find user → Reset failed login count.
#     - Ensure the X-Atlassian-Token: no-check header is present (included
#       by default in this script).
#
#   401 Unauthorized:
#     - PAT may be expired or revoked. Generate a new one.
#     - Verify the token is correct (no trailing whitespace/newline).
#
#   Partial metrics (some sections zero):
#     - The user associated with the PAT must have Browse Projects
#       permission on the target projects.
#     - Login metrics require JIRA_HOME to be set correctly and the
#       script must have read access to the Jira security log at
#       $JIRA_HOME/log/atlassian-jira-security.log
#     - Service Desk metrics require Jira Service Management.
#
#   Login metrics all zero:
#     - Verify JIRA_HOME points to the correct Jira data directory.
#     - Check the security log exists:
#         ls $JIRA_HOME/log/atlassian-jira-security.log
#     - Ensure the user running this script can read the log file.
#     - Note: The audit log REST API (/rest/api/2/auditing/record)
#       does not exist on Jira 10.x. This script parses the security
#       log file directly instead.
#
# Changelog:
#   1.4 - Auto-discover all projects when JIRA_PROJECTS is not set
#         Fixed per-project metrics (HELP/TYPE headers always emitted)
#         Fixed missing metrics: all HELP/TYPE headers now always present
#           even when data is empty (assignee, component, plugin, service
#           desk queue/org, user logins, resolution time)
#         Per-project metrics now grouped by metric name per Prometheus
#           exposition format (HELP/TYPE then all values, not interleaved)
#         Switched login metrics from audit log REST API (404 on Jira 10.x)
#           to parsing $JIRA_HOME/log/atlassian-jira-security.log directly
#         Added JIRA_HOME configuration variable and --jira-home CLI option
#         Added troubleshooting section for login metrics
#   1.3 - Switched authentication from basic auth to Bearer token (PAT)
#         Fixed JQL URL encoding (use curl --data-urlencode)
#         Added troubleshooting documentation
#   1.2 - Added plugin/add-on inventory metrics from UPM API
#         Added Service Desk metrics (queues, requests, SLAs, CSAT,
#         request types, organizations)
#   1.1 - Added login activity metrics from Jira audit log
#         (total logins, failed logins, unique users, per-user counts)
#   1.0 - Initial release
################################################################################

SCRIPT_VERSION="1.4"
TEXTFILE_DIR="/var/lib/node_exporter"
OUTPUT_FILE=""
HTTP_MODE=false
HTTP_PORT=9418
LOCK_FILE="/var/run/jira-metrics.lock"

# Jira connection settings (edit these or override via environment variables)
JIRA_BASE_URL="${JIRA_BASE_URL:-https://yourcompany.atlassian.net}"
JIRA_USER="${JIRA_USER:-user@company.com}"
JIRA_API_TOKEN="${JIRA_API_TOKEN:-your-api-token-here}"
JIRA_PROJECTS="${JIRA_PROJECTS:-}"   # comma-separated, empty = all (e.g. "PROJ1,PROJ2")
JIRA_HOME="${JIRA_HOME:-/mnt/ebs/application-data/jira}"  # Jira home directory (for log parsing)

# Timeouts
CURL_TIMEOUT=30
MAX_RESULTS=1000

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

Export Jira metrics as Prometheus metrics (v${SCRIPT_VERSION}).

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

OPTIONS:
    -p, --port      HTTP port (default: $HTTP_PORT)
    -o, --output    Output file
    -u, --url       Jira base URL
    --user          Jira username/email
    --token         Jira API token
    --projects      Comma-separated project keys
    -h, --help      Show help

ENVIRONMENT VARIABLES:
    JIRA_BASE_URL   Jira instance URL
    JIRA_USER       Jira username or email
    JIRA_API_TOKEN  API token or password
    JIRA_PROJECTS   Comma-separated project keys

REQUIREMENTS:
    - curl and jq must be installed
    - Valid Jira credentials

EOF
    exit 0
}

parse_args() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            -h|--help) show_usage ;;
            --textfile) OUTPUT_FILE="$TEXTFILE_DIR/jira_metrics.prom"; shift ;;
            --http) HTTP_MODE=true; shift ;;
            -p|--port) HTTP_PORT="$2"; shift 2 ;;
            -o|--output) OUTPUT_FILE="$2"; shift 2 ;;
            -u|--url) JIRA_BASE_URL="$2"; shift 2 ;;
            --user) JIRA_USER="$2"; shift 2 ;;
            --token) JIRA_API_TOKEN="$2"; shift 2 ;;
            --projects) JIRA_PROJECTS="$2"; shift 2 ;;
            --jira-home) JIRA_HOME="$2"; shift 2 ;;
            *) echo "Unknown: $1"; exit 1 ;;
        esac
    done
}

preflight_check() {
    local fail=0
    if ! command -v curl >/dev/null 2>&1; then
        echo "ERROR: curl is required" >&2; fail=1
    fi
    if ! command -v jq >/dev/null 2>&1; then
        echo "ERROR: jq is required" >&2; fail=1
    fi
    if [ -z "$JIRA_BASE_URL" ]; then
        echo "ERROR: JIRA_BASE_URL not set" >&2; fail=1
    fi
    if [ -z "$JIRA_USER" ] || [ -z "$JIRA_API_TOKEN" ]; then
        echo "ERROR: JIRA_USER and JIRA_API_TOKEN must be set" >&2; fail=1
    fi
    [ "$fail" -eq 1 ] && exit 1
}

acquire_lock() {
    if [ -f "$LOCK_FILE" ]; then
        local pid
        pid=$(cat "$LOCK_FILE" 2>/dev/null)
        if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
            echo "ERROR: Another instance is already running (PID: $pid)" >&2
            exit 1
        else
            echo "Removing stale lock file" >&2
            rm -f "$LOCK_FILE"
        fi
    fi
    echo $$ > "$LOCK_FILE"
    trap cleanup EXIT INT TERM
}

cleanup() {
    rm -f "$LOCK_FILE"
}

# ---------------------------------------------------------------------------
# Jira API helper
# ---------------------------------------------------------------------------

jira_api() {
    local endpoint="$1"
    local url="${JIRA_BASE_URL}/rest/api/2${endpoint}"
    curl -s --max-time "$CURL_TIMEOUT" \
        -H "Authorization: Bearer ${JIRA_API_TOKEN}" \
        -H "Content-Type: application/json" \
        -H "X-Atlassian-Token: no-check" \
        "$url" 2>/dev/null
}

jira_search() {
    local jql="$1"
    local max_results="${2:-0}"
    local fields="${3:-}"
    local url="${JIRA_BASE_URL}/rest/api/2/search"
    local data_args=("--data-urlencode" "jql=${jql}" "--data-urlencode" "maxResults=${max_results}")
    if [ -n "$fields" ]; then
        data_args+=("--data-urlencode" "fields=${fields}")
    fi
    curl -s -G --max-time "$CURL_TIMEOUT" \
        -H "Authorization: Bearer ${JIRA_API_TOKEN}" \
        -H "Content-Type: application/json" \
        -H "X-Atlassian-Token: no-check" \
        "${data_args[@]}" \
        "$url" 2>/dev/null
}

jira_search_count() {
    local jql="$1"
    local result
    result=$(jira_search "$jql" 0)
    echo "$result" | jq -r '.total // 0' 2>/dev/null
}

# Build project filter clause
get_project_filter() {
    if [ -z "$JIRA_PROJECTS" ]; then
        echo ""
        return
    fi
    local clause="project in ("
    local first=true
    IFS=',' read -ra PROJ_ARRAY <<< "$JIRA_PROJECTS"
    for proj in "${PROJ_ARRAY[@]}"; do
        proj=$(echo "$proj" | tr -d ' ')
        if [ "$first" = true ]; then
            clause="${clause}\"${proj}\""
            first=false
        else
            clause="${clause},\"${proj}\""
        fi
    done
    clause="${clause})"
    echo "$clause"
}

# ---------------------------------------------------------------------------
# Data collection (cached in temp dir)
# ---------------------------------------------------------------------------

CACHE_DIR=""

cache_all_jira_data() {
    CACHE_DIR=$(mktemp -d /tmp/jira_metrics_cache.XXXXXX)
    local pf
    pf=$(get_project_filter)
    local pf_and=""
    [ -n "$pf" ] && pf_and="${pf} AND "

    # Issue counts by status
    for status in "To Do" "In Progress" "In Review" "Done" "Closed" "Open" "Reopened" "Resolved"; do
        local safe_status
        safe_status=$(echo "$status" | tr ' ' '_' | tr '[:upper:]' '[:lower:]')
        jira_search_count "${pf_and}status = \"${status}\"" > "$CACHE_DIR/status_${safe_status}" &
    done

    # Issue counts by priority
    for priority in "Highest" "High" "Medium" "Low" "Lowest"; do
        local safe_priority
        safe_priority=$(echo "$priority" | tr '[:upper:]' '[:lower:]')
        jira_search_count "${pf_and}priority = \"${priority}\"" > "$CACHE_DIR/priority_${safe_priority}" &
    done

    # Issue counts by type
    for itype in "Bug" "Story" "Task" "Epic" "Sub-task" "Incident"; do
        local safe_type
        safe_type=$(echo "$itype" | tr ' ' '_' | tr '-' '_' | tr '[:upper:]' '[:lower:]')
        jira_search_count "${pf_and}issuetype = \"${itype}\"" > "$CACHE_DIR/type_${safe_type}" &
    done

    # Total unresolved (backlog)
    jira_search_count "${pf_and}resolution = Unresolved" > "$CACHE_DIR/backlog_total" &

    # Overdue issues
    jira_search_count "${pf_and}due < now() AND resolution = Unresolved" > "$CACHE_DIR/overdue" &

    # Unassigned issues
    jira_search_count "${pf_and}assignee is EMPTY AND resolution = Unresolved" > "$CACHE_DIR/unassigned" &

    # Created in last 24h
    jira_search_count "${pf_and}created >= -1d" > "$CACHE_DIR/created_24h" &

    # Resolved in last 24h
    jira_search_count "${pf_and}resolved >= -1d" > "$CACHE_DIR/resolved_24h" &

    # Created in last 7d
    jira_search_count "${pf_and}created >= -7d" > "$CACHE_DIR/created_7d" &

    # Resolved in last 7d
    jira_search_count "${pf_and}resolved >= -7d" > "$CACHE_DIR/resolved_7d" &

    # Created in last 30d
    jira_search_count "${pf_and}created >= -30d" > "$CACHE_DIR/created_30d" &

    # Resolved in last 30d
    jira_search_count "${pf_and}resolved >= -30d" > "$CACHE_DIR/resolved_30d" &

    # Aged backlog buckets (unresolved, created more than X days ago)
    jira_search_count "${pf_and}resolution = Unresolved AND created <= -7d" > "$CACHE_DIR/backlog_older_7d" &
    jira_search_count "${pf_and}resolution = Unresolved AND created <= -30d" > "$CACHE_DIR/backlog_older_30d" &
    jira_search_count "${pf_and}resolution = Unresolved AND created <= -90d" > "$CACHE_DIR/backlog_older_90d" &
    jira_search_count "${pf_and}resolution = Unresolved AND created <= -365d" > "$CACHE_DIR/backlog_older_365d" &

    # High-priority unresolved
    jira_search_count "${pf_and}priority in (Highest, High) AND resolution = Unresolved" > "$CACHE_DIR/high_priority_unresolved" &

    # Critical bugs unresolved
    jira_search_count "${pf_and}issuetype = Bug AND priority in (Highest, High) AND resolution = Unresolved" > "$CACHE_DIR/critical_bugs" &

    wait
}

cache_project_data() {
    local proj_list="$JIRA_PROJECTS"
    if [ -z "$proj_list" ]; then
        proj_list=$(jira_api "/project" | jq -r '.[].key' 2>/dev/null | paste -sd ',' -)
        if [ -z "$proj_list" ]; then
            return
        fi
        JIRA_PROJECTS="$proj_list"
    fi
    IFS=',' read -ra PROJ_ARRAY <<< "$proj_list"
    for proj in "${PROJ_ARRAY[@]}"; do
        proj=$(echo "$proj" | tr -d ' ')
        local safe_proj
        safe_proj=$(echo "$proj" | tr '[:upper:]' '[:lower:]')
        jira_search_count "project = \"${proj}\" AND resolution = Unresolved" > "$CACHE_DIR/proj_open_${safe_proj}" &
        jira_search_count "project = \"${proj}\"" > "$CACHE_DIR/proj_total_${safe_proj}" &
        jira_search_count "project = \"${proj}\" AND created >= -7d" > "$CACHE_DIR/proj_created_7d_${safe_proj}" &
        jira_search_count "project = \"${proj}\" AND resolved >= -7d" > "$CACHE_DIR/proj_resolved_7d_${safe_proj}" &
    done
    wait
}

cache_assignee_data() {
    local pf
    pf=$(get_project_filter)
    local pf_and=""
    [ -n "$pf" ] && pf_and="${pf} AND "

    local result
    result=$(jira_search "${pf_and}resolution = Unresolved AND assignee is not EMPTY ORDER BY assignee ASC" "$MAX_RESULTS" "assignee")
    if [ -n "$result" ]; then
        echo "$result" | jq -r '
            [.issues[]? | .fields.assignee.displayName // .fields.assignee.name // "unknown"] |
            group_by(.) |
            map({name: .[0], count: length}) |
            sort_by(-.count) |
            .[:15][] |
            "\(.name)|\(.count)"
        ' 2>/dev/null > "$CACHE_DIR/assignees"
    fi
}

cache_component_data() {
    local pf
    pf=$(get_project_filter)
    local pf_and=""
    [ -n "$pf" ] && pf_and="${pf} AND "

    local result
    result=$(jira_search "${pf_and}resolution = Unresolved AND component is not EMPTY" "$MAX_RESULTS" "components")
    if [ -n "$result" ] && echo "$result" | jq -e '.issues' >/dev/null 2>&1; then
        echo "$result" | jq -r '
            [.issues[]? | .fields.components[]?.name // "unknown"] |
            group_by(.) |
            map({name: .[0], count: length}) |
            sort_by(-.count) |
            .[:20][] |
            "\(.name)|\(.count)"
        ' 2>/dev/null > "$CACHE_DIR/components"
    fi
}

cache_resolution_time_data() {
    local pf
    pf=$(get_project_filter)
    local pf_and=""
    [ -n "$pf" ] && pf_and="${pf} AND "

    local result
    result=$(jira_search "${pf_and}resolved >= -7d" 100 "created,resolutiondate")
    if [ -n "$result" ]; then
        echo "$result" | jq -r '
            [.issues[]? |
                select(.fields.resolutiondate != null and .fields.created != null) |
                ((.fields.resolutiondate | sub("\\.[0-9]+.*";"") | strptime("%Y-%m-%dT%H:%M:%S") | mktime) -
                 (.fields.created | sub("\\.[0-9]+.*";"") | strptime("%Y-%m-%dT%H:%M:%S") | mktime)) / 3600
            ] |
            if length > 0 then
                { avg: (add / length), min: min, max: max, count: length }
            else
                { avg: 0, min: 0, max: 0, count: 0 }
            end |
            "\(.avg)|\(.min)|\(.max)|\(.count)"
        ' 2>/dev/null > "$CACHE_DIR/resolution_times"
    fi
}

cache_login_data() {
    local security_log="${JIRA_HOME}/log/atlassian-jira-security.log"

    if [ ! -f "$security_log" ]; then
        echo "0" > "$CACHE_DIR/logins_total"
        echo "0" > "$CACHE_DIR/logins_success"
        echo "0" > "$CACHE_DIR/logins_failed"
        echo "0" > "$CACHE_DIR/logins_unique_users"
        return
    fi

    local since_date
    since_date=$(date -d '24 hours ago' '+%Y-%m-%d %H:%M' 2>/dev/null || \
                 date -v-1d '+%Y-%m-%d %H:%M' 2>/dev/null)

    # Extract lines from last 24h (compare date prefix YYYY-MM-DD HH:MM)
    local recent_lines
    recent_lines=$(awk -v since="$since_date" '$0 ~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}/ { ts=substr($0,1,16); if (ts >= since) print }' "$security_log" 2>/dev/null)

    if [ -z "$recent_lines" ]; then
        echo "0" > "$CACHE_DIR/logins_total"
        echo "0" > "$CACHE_DIR/logins_success"
        echo "0" > "$CACHE_DIR/logins_failed"
        echo "0" > "$CACHE_DIR/logins_unique_users"
        return
    fi

    # Successful logins: "has PASSED authentication"
    local success_count
    success_count=$(echo "$recent_lines" | grep -c 'has PASSED authentication' 2>/dev/null)
    echo "${success_count:-0}" > "$CACHE_DIR/logins_success"

    # Failed logins: "tried to login but"
    local failed_count
    failed_count=$(echo "$recent_lines" | grep -c 'tried to login but' 2>/dev/null)
    echo "${failed_count:-0}" > "$CACHE_DIR/logins_failed"

    # Total login events
    local total_count
    total_count=$(( success_count + failed_count ))
    echo "${total_count:-0}" > "$CACHE_DIR/logins_total"

    # Unique users (extract username from 'user' has PASSED)
    local unique_users
    unique_users=$(echo "$recent_lines" | grep 'has PASSED authentication' | \
        grep -oP "The user '\K[^']+" 2>/dev/null | \
        sort -u | wc -l)
    echo "${unique_users:-0}" > "$CACHE_DIR/logins_unique_users"

    # Per-user login counts (top 15)
    echo "$recent_lines" | grep 'has PASSED authentication' | \
        grep -oP "The user '\K[^']+" 2>/dev/null | \
        sort | uniq -c | sort -rn | head -15 | \
        awk '{print $2"|"$1}' > "$CACHE_DIR/logins_per_user" 2>/dev/null
}

# ---------------------------------------------------------------------------
# Plugin / add-on metrics (UPM API - Jira Server/DC)
# ---------------------------------------------------------------------------

cache_plugin_data() {
    local result
    result=$(curl -s --max-time "$CURL_TIMEOUT" \
        -H "Authorization: Bearer ${JIRA_API_TOKEN}" \
        -H "Accept: application/vnd.atl.plugins.installed+json" \
        "${JIRA_BASE_URL}/rest/plugins/1.0/" 2>/dev/null)

    if [ -n "$result" ] && echo "$result" | jq -e '.plugins' >/dev/null 2>&1; then
        echo "$result" | jq -r '.plugins | length' > "$CACHE_DIR/plugins_total" 2>/dev/null
        echo "$result" | jq -r '[.plugins[]? | select(.enabled == true)] | length' > "$CACHE_DIR/plugins_enabled" 2>/dev/null
        echo "$result" | jq -r '[.plugins[]? | select(.enabled == false)] | length' > "$CACHE_DIR/plugins_disabled" 2>/dev/null
        echo "$result" | jq -r '[.plugins[]? | select(.userInstalled == true)] | length' > "$CACHE_DIR/plugins_user_installed" 2>/dev/null
        echo "$result" | jq -r '[.plugins[]? | select(.userInstalled == false or .userInstalled == null)] | length' > "$CACHE_DIR/plugins_system" 2>/dev/null

        echo "$result" | jq -r '
            [.plugins[]? | select(.userInstalled == true)] |
            .[] |
            "\(.name // .key)|\(if .enabled then 1 else 0 end)|\(.version // "unknown")"
        ' 2>/dev/null > "$CACHE_DIR/plugins_detail"
    else
        echo "0" > "$CACHE_DIR/plugins_total"
        echo "0" > "$CACHE_DIR/plugins_enabled"
        echo "0" > "$CACHE_DIR/plugins_disabled"
        echo "0" > "$CACHE_DIR/plugins_user_installed"
        echo "0" > "$CACHE_DIR/plugins_system"
    fi
}

# ---------------------------------------------------------------------------
# Service Desk metrics (Jira Service Desk / Service Management API)
# ---------------------------------------------------------------------------

servicedesk_api() {
    local endpoint="$1"
    curl -s --max-time "$CURL_TIMEOUT" \
        -H "Authorization: Bearer ${JIRA_API_TOKEN}" \
        -H "Content-Type: application/json" \
        -H "X-ExperimentalApi: opt-in" \
        "${JIRA_BASE_URL}/rest/servicedeskapi${endpoint}" 2>/dev/null
}

cache_servicedesk_data() {
    local sd_list
    sd_list=$(servicedesk_api "/servicedesk")

    if [ -z "$sd_list" ] || ! echo "$sd_list" | jq -e '.values' >/dev/null 2>&1; then
        echo "0" > "$CACHE_DIR/sd_available"
        return
    fi

    echo "1" > "$CACHE_DIR/sd_available"

    local sd_count
    sd_count=$(echo "$sd_list" | jq -r '.size // (.values | length)' 2>/dev/null)
    echo "${sd_count:-0}" > "$CACHE_DIR/sd_count"

    echo "$sd_list" | jq -r '.values[]? | "\(.id)|\(.projectKey // "unknown")|\(.projectName // "unknown")"' \
        2>/dev/null > "$CACHE_DIR/sd_list"

    local tmp_queues="$CACHE_DIR/sd_queues_all"
    local tmp_request_types="$CACHE_DIR/sd_request_types_all"
    local tmp_orgs="$CACHE_DIR/sd_orgs_all"
    : > "$tmp_queues"
    : > "$tmp_request_types"
    : > "$tmp_orgs"

    while IFS='|' read -r sd_id sd_key _sd_name; do
        [ -z "$sd_id" ] && continue

        local queues
        queues=$(servicedesk_api "/servicedesk/${sd_id}/queue")
        if [ -n "$queues" ] && echo "$queues" | jq -e '.values' >/dev/null 2>&1; then
            echo "$queues" | jq -r --arg proj "$sd_key" '
                .values[]? | "\($proj)|\(.id)|\(.name // "unknown")|\(.issueCount // 0)"
            ' 2>/dev/null >> "$tmp_queues"
        fi

        local rtypes
        rtypes=$(servicedesk_api "/servicedesk/${sd_id}/requesttype")
        if [ -n "$rtypes" ] && echo "$rtypes" | jq -e '.values' >/dev/null 2>&1; then
            echo "$rtypes" | jq -r --arg proj "$sd_key" '
                .values[]? | "\($proj)|\(.id)|\(.name // "unknown")"
            ' 2>/dev/null >> "$tmp_request_types"
        fi

        local orgs
        orgs=$(servicedesk_api "/servicedesk/${sd_id}/organization")
        if [ -n "$orgs" ] && echo "$orgs" | jq -e '.values' >/dev/null 2>&1; then
            local org_count
            org_count=$(echo "$orgs" | jq -r '.size // (.values | length)' 2>/dev/null)
            echo "${sd_key}|${org_count:-0}" >> "$tmp_orgs"
        fi
    done < "$CACHE_DIR/sd_list"

    local pf
    pf=$(get_project_filter)
    local pf_and=""
    [ -n "$pf" ] && pf_and="${pf} AND "

    jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND resolution = Unresolved" \
        > "$CACHE_DIR/sd_open_requests" 2>/dev/null &
    jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND resolved >= -24h" \
        > "$CACHE_DIR/sd_resolved_24h" 2>/dev/null &
    jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND resolved >= -7d" \
        > "$CACHE_DIR/sd_resolved_7d" 2>/dev/null &
    jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND created >= -24h" \
        > "$CACHE_DIR/sd_created_24h" 2>/dev/null &
    jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND created >= -7d" \
        > "$CACHE_DIR/sd_created_7d" 2>/dev/null &

    jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND priority in (Highest, High) AND resolution = Unresolved" \
        > "$CACHE_DIR/sd_high_priority_open" 2>/dev/null &

    jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND due < now() AND resolution = Unresolved" \
        > "$CACHE_DIR/sd_breached_sla" 2>/dev/null &

    wait

    local csat_result
    csat_result=$(jira_search "${pf_and}\"Customer Request Type\" is not EMPTY AND resolved >= -30d AND \"Satisfaction rating\" is not EMPTY" \
        100 "customfield_10421,satisfaction")
    if [ -n "$csat_result" ] && echo "$csat_result" | jq -e '.issues' >/dev/null 2>&1; then
        local csat_total
        csat_total=$(echo "$csat_result" | jq -r '.total // 0' 2>/dev/null)
        echo "${csat_total}" > "$CACHE_DIR/sd_csat_responses"
    else
        echo "0" > "$CACHE_DIR/sd_csat_responses"
    fi
}

# ---------------------------------------------------------------------------
# Read cached values
# ---------------------------------------------------------------------------

read_cache() {
    local key="$1" default="${2:-0}"
    local val
    val=$(tr -d '[:space:]' < "$CACHE_DIR/$key" 2>/dev/null)
    if [ -z "$val" ] || [ "$val" = "null" ]; then
        echo "$default"
    else
        echo "$val"
    fi
}

# ---------------------------------------------------------------------------
# Metric generation
# ---------------------------------------------------------------------------

generate_metrics() {
    local start_time
    start_time=$(date +%s)

    # Read all cached values
    local status_todo status_in_progress status_in_review status_done status_closed status_open status_reopened status_resolved
    status_todo=$(read_cache "status_to_do")
    status_in_progress=$(read_cache "status_in_progress")
    status_in_review=$(read_cache "status_in_review")
    status_done=$(read_cache "status_done")
    status_closed=$(read_cache "status_closed")
    status_open=$(read_cache "status_open")
    status_reopened=$(read_cache "status_reopened")
    status_resolved=$(read_cache "status_resolved")

    local priority_highest priority_high priority_medium priority_low priority_lowest
    priority_highest=$(read_cache "priority_highest")
    priority_high=$(read_cache "priority_high")
    priority_medium=$(read_cache "priority_medium")
    priority_low=$(read_cache "priority_low")
    priority_lowest=$(read_cache "priority_lowest")

    local type_bug type_story type_task type_epic type_sub_task type_incident
    type_bug=$(read_cache "type_bug")
    type_story=$(read_cache "type_story")
    type_task=$(read_cache "type_task")
    type_epic=$(read_cache "type_epic")
    type_sub_task=$(read_cache "type_sub_task")
    type_incident=$(read_cache "type_incident")

    local backlog_total overdue unassigned
    backlog_total=$(read_cache "backlog_total")
    overdue=$(read_cache "overdue")
    unassigned=$(read_cache "unassigned")

    local created_24h resolved_24h created_7d resolved_7d created_30d resolved_30d
    created_24h=$(read_cache "created_24h")
    resolved_24h=$(read_cache "resolved_24h")
    created_7d=$(read_cache "created_7d")
    resolved_7d=$(read_cache "resolved_7d")
    created_30d=$(read_cache "created_30d")
    resolved_30d=$(read_cache "resolved_30d")

    local backlog_older_7d backlog_older_30d backlog_older_90d backlog_older_365d
    backlog_older_7d=$(read_cache "backlog_older_7d")
    backlog_older_30d=$(read_cache "backlog_older_30d")
    backlog_older_90d=$(read_cache "backlog_older_90d")
    backlog_older_365d=$(read_cache "backlog_older_365d")

    local high_priority_unresolved critical_bugs
    high_priority_unresolved=$(read_cache "high_priority_unresolved")
    critical_bugs=$(read_cache "critical_bugs")

    cat <<EOF
# HELP jira_exporter_info Jira exporter metadata
# TYPE jira_exporter_info gauge
jira_exporter_info{version="${SCRIPT_VERSION}",jira_url="${JIRA_BASE_URL}"} 1

# HELP jira_issues_by_status Issue count by status
# TYPE jira_issues_by_status gauge
jira_issues_by_status{status="to_do"} $status_todo
jira_issues_by_status{status="in_progress"} $status_in_progress
jira_issues_by_status{status="in_review"} $status_in_review
jira_issues_by_status{status="done"} $status_done
jira_issues_by_status{status="closed"} $status_closed
jira_issues_by_status{status="open"} $status_open
jira_issues_by_status{status="reopened"} $status_reopened
jira_issues_by_status{status="resolved"} $status_resolved

# HELP jira_issues_by_priority Issue count by priority
# TYPE jira_issues_by_priority gauge
jira_issues_by_priority{priority="highest"} $priority_highest
jira_issues_by_priority{priority="high"} $priority_high
jira_issues_by_priority{priority="medium"} $priority_medium
jira_issues_by_priority{priority="low"} $priority_low
jira_issues_by_priority{priority="lowest"} $priority_lowest

# HELP jira_issues_by_type Issue count by type
# TYPE jira_issues_by_type gauge
jira_issues_by_type{type="bug"} $type_bug
jira_issues_by_type{type="story"} $type_story
jira_issues_by_type{type="task"} $type_task
jira_issues_by_type{type="epic"} $type_epic
jira_issues_by_type{type="sub_task"} $type_sub_task
jira_issues_by_type{type="incident"} $type_incident

# HELP jira_backlog_total Total unresolved issues
# TYPE jira_backlog_total gauge
jira_backlog_total $backlog_total

# HELP jira_overdue_issues Unresolved issues past due date
# TYPE jira_overdue_issues gauge
jira_overdue_issues $overdue

# HELP jira_unassigned_issues Unresolved issues with no assignee
# TYPE jira_unassigned_issues gauge
jira_unassigned_issues $unassigned

# HELP jira_high_priority_unresolved High/Highest priority unresolved issues
# TYPE jira_high_priority_unresolved gauge
jira_high_priority_unresolved $high_priority_unresolved

# HELP jira_critical_bugs_unresolved High/Highest priority unresolved bugs
# TYPE jira_critical_bugs_unresolved gauge
jira_critical_bugs_unresolved $critical_bugs

# HELP jira_issues_created Issues created in time period
# TYPE jira_issues_created gauge
jira_issues_created{period="24h"} $created_24h
jira_issues_created{period="7d"} $created_7d
jira_issues_created{period="30d"} $created_30d

# HELP jira_issues_resolved Issues resolved in time period
# TYPE jira_issues_resolved gauge
jira_issues_resolved{period="24h"} $resolved_24h
jira_issues_resolved{period="7d"} $resolved_7d
jira_issues_resolved{period="30d"} $resolved_30d

# HELP jira_backlog_age_issues Unresolved issues older than threshold
# TYPE jira_backlog_age_issues gauge
jira_backlog_age_issues{older_than="7d"} $backlog_older_7d
jira_backlog_age_issues{older_than="30d"} $backlog_older_30d
jira_backlog_age_issues{older_than="90d"} $backlog_older_90d
jira_backlog_age_issues{older_than="365d"} $backlog_older_365d

EOF

    # Resolution time metrics
    local rt_avg=0 rt_min=0 rt_max=0 rt_count=0
    if [ -f "$CACHE_DIR/resolution_times" ]; then
        local rt_data
        rt_data=$(cat "$CACHE_DIR/resolution_times" 2>/dev/null)
        if [ -n "$rt_data" ] && [ "$rt_data" != "null" ]; then
            rt_avg=$(echo "$rt_data" | cut -d'|' -f1)
            rt_min=$(echo "$rt_data" | cut -d'|' -f2)
            rt_max=$(echo "$rt_data" | cut -d'|' -f3)
            rt_count=$(echo "$rt_data" | cut -d'|' -f4)
        fi
    fi
    cat <<EOF
# HELP jira_resolution_time_hours Issue resolution time in hours (last 7d)
# TYPE jira_resolution_time_hours gauge
jira_resolution_time_hours{stat="avg"} ${rt_avg:-0}
jira_resolution_time_hours{stat="min"} ${rt_min:-0}
jira_resolution_time_hours{stat="max"} ${rt_max:-0}

# HELP jira_resolution_time_sample_count Number of resolved issues used for resolution time calculation
# TYPE jira_resolution_time_sample_count gauge
jira_resolution_time_sample_count ${rt_count:-0}

EOF

    # Per-project metrics
    if [ -n "$JIRA_PROJECTS" ]; then
        IFS=',' read -ra PROJ_ARRAY <<< "$JIRA_PROJECTS"

        echo "# HELP jira_project_issues_open Unresolved issues per project"
        echo "# TYPE jira_project_issues_open gauge"
        for proj in "${PROJ_ARRAY[@]}"; do
            proj=$(echo "$proj" | tr -d '[:space:]')
            [ -z "$proj" ] && continue
            local safe_proj
            safe_proj=$(echo "$proj" | tr '[:upper:]' '[:lower:]')
            echo "jira_project_issues_open{project=\"${proj}\"} $(read_cache "proj_open_${safe_proj}")"
        done

        echo ""
        echo "# HELP jira_project_issues_total Total issues per project"
        echo "# TYPE jira_project_issues_total gauge"
        for proj in "${PROJ_ARRAY[@]}"; do
            proj=$(echo "$proj" | tr -d '[:space:]')
            [ -z "$proj" ] && continue
            local safe_proj
            safe_proj=$(echo "$proj" | tr '[:upper:]' '[:lower:]')
            echo "jira_project_issues_total{project=\"${proj}\"} $(read_cache "proj_total_${safe_proj}")"
        done

        echo ""
        echo "# HELP jira_project_issues_created_7d Issues created in last 7d per project"
        echo "# TYPE jira_project_issues_created_7d gauge"
        for proj in "${PROJ_ARRAY[@]}"; do
            proj=$(echo "$proj" | tr -d '[:space:]')
            [ -z "$proj" ] && continue
            local safe_proj
            safe_proj=$(echo "$proj" | tr '[:upper:]' '[:lower:]')
            echo "jira_project_issues_created_7d{project=\"${proj}\"} $(read_cache "proj_created_7d_${safe_proj}")"
        done

        echo ""
        echo "# HELP jira_project_issues_resolved_7d Issues resolved in last 7d per project"
        echo "# TYPE jira_project_issues_resolved_7d gauge"
        for proj in "${PROJ_ARRAY[@]}"; do
            proj=$(echo "$proj" | tr -d '[:space:]')
            [ -z "$proj" ] && continue
            local safe_proj
            safe_proj=$(echo "$proj" | tr '[:upper:]' '[:lower:]')
            echo "jira_project_issues_resolved_7d{project=\"${proj}\"} $(read_cache "proj_resolved_7d_${safe_proj}")"
        done
        echo ""
    fi

    # Assignee workload metrics
    echo "# HELP jira_assignee_open_issues Open issues per assignee (top 15)"
    echo "# TYPE jira_assignee_open_issues gauge"
    if [ -f "$CACHE_DIR/assignees" ] && [ -s "$CACHE_DIR/assignees" ]; then
        while IFS='|' read -r name count; do
            [ -z "$name" ] && continue
            local safe_name
            safe_name=${name//\"/\\\"}
            echo "jira_assignee_open_issues{assignee=\"${safe_name}\"} ${count}"
        done < "$CACHE_DIR/assignees"
    fi
    echo ""

    # Component metrics
    echo "# HELP jira_component_open_issues Open issues per component (top 20)"
    echo "# TYPE jira_component_open_issues gauge"
    if [ -f "$CACHE_DIR/components" ] && [ -s "$CACHE_DIR/components" ]; then
        while IFS='|' read -r name count; do
            [ -z "$name" ] && continue
            local safe_name
            safe_name=${name//\"/\\\"}
            echo "jira_component_open_issues{component=\"${safe_name}\"} ${count}"
        done < "$CACHE_DIR/components"
    fi
    echo ""

    # Plugin / add-on metrics
    local plugins_total plugins_enabled plugins_disabled plugins_user plugins_system
    plugins_total=$(read_cache "plugins_total")
    plugins_enabled=$(read_cache "plugins_enabled")
    plugins_disabled=$(read_cache "plugins_disabled")
    plugins_user=$(read_cache "plugins_user_installed")
    plugins_system=$(read_cache "plugins_system")

    cat <<PLUGINEOF
# HELP jira_plugins_total Total installed plugins/add-ons
# TYPE jira_plugins_total gauge
jira_plugins_total $plugins_total

# HELP jira_plugins_enabled Enabled plugins
# TYPE jira_plugins_enabled gauge
jira_plugins_enabled $plugins_enabled

# HELP jira_plugins_disabled Disabled plugins
# TYPE jira_plugins_disabled gauge
jira_plugins_disabled $plugins_disabled

# HELP jira_plugins_user_installed User-installed plugins (non-system)
# TYPE jira_plugins_user_installed gauge
jira_plugins_user_installed $plugins_user

# HELP jira_plugins_system System/bundled plugins
# TYPE jira_plugins_system gauge
jira_plugins_system $plugins_system

PLUGINEOF

    echo "# HELP jira_plugin_status Per user-installed plugin status (1=enabled, 0=disabled)"
    echo "# TYPE jira_plugin_status gauge"
    if [ -f "$CACHE_DIR/plugins_detail" ] && [ -s "$CACHE_DIR/plugins_detail" ]; then
        while IFS='|' read -r pname penabled pversion; do
            [ -z "$pname" ] && continue
            local safe_pname
            safe_pname=${pname//\"/\\\"}
            echo "jira_plugin_status{plugin=\"${safe_pname}\",version=\"${pversion}\"} ${penabled}"
        done < "$CACHE_DIR/plugins_detail"
    fi
    echo ""

    # Service Desk metrics
    local sd_available
    sd_available=$(read_cache "sd_available")

    if [ "$sd_available" = "1" ]; then
        local sd_count sd_open sd_resolved_24h sd_resolved_7d sd_created_24h sd_created_7d
        local sd_high_priority sd_breached sd_csat
        sd_count=$(read_cache "sd_count")
        sd_open=$(read_cache "sd_open_requests")
        sd_resolved_24h=$(read_cache "sd_resolved_24h")
        sd_resolved_7d=$(read_cache "sd_resolved_7d")
        sd_created_24h=$(read_cache "sd_created_24h")
        sd_created_7d=$(read_cache "sd_created_7d")
        sd_high_priority=$(read_cache "sd_high_priority_open")
        sd_breached=$(read_cache "sd_breached_sla")
        sd_csat=$(read_cache "sd_csat_responses")

        cat <<SDEOF
# HELP jira_servicedesk_count Number of service desks
# TYPE jira_servicedesk_count gauge
jira_servicedesk_count $sd_count

# HELP jira_servicedesk_open_requests Total open service desk requests
# TYPE jira_servicedesk_open_requests gauge
jira_servicedesk_open_requests $sd_open

# HELP jira_servicedesk_requests_created Service desk requests created in period
# TYPE jira_servicedesk_requests_created gauge
jira_servicedesk_requests_created{period="24h"} $sd_created_24h
jira_servicedesk_requests_created{period="7d"} $sd_created_7d

# HELP jira_servicedesk_requests_resolved Service desk requests resolved in period
# TYPE jira_servicedesk_requests_resolved gauge
jira_servicedesk_requests_resolved{period="24h"} $sd_resolved_24h
jira_servicedesk_requests_resolved{period="7d"} $sd_resolved_7d

# HELP jira_servicedesk_high_priority_open High priority open service desk requests
# TYPE jira_servicedesk_high_priority_open gauge
jira_servicedesk_high_priority_open $sd_high_priority

# HELP jira_servicedesk_sla_breached Requests past SLA due date (unresolved)
# TYPE jira_servicedesk_sla_breached gauge
jira_servicedesk_sla_breached $sd_breached

# HELP jira_servicedesk_csat_responses_30d Customer satisfaction responses in last 30d
# TYPE jira_servicedesk_csat_responses_30d gauge
jira_servicedesk_csat_responses_30d $sd_csat

SDEOF

        # Per-queue issue counts
        echo "# HELP jira_servicedesk_queue_size Issue count per service desk queue"
        echo "# TYPE jira_servicedesk_queue_size gauge"
        if [ -f "$CACHE_DIR/sd_queues_all" ] && [ -s "$CACHE_DIR/sd_queues_all" ]; then
            while IFS='|' read -r q_proj _q_id q_name q_count; do
                [ -z "$q_proj" ] && continue
                local safe_qname
                safe_qname=${q_name//\"/\\\"}
                echo "jira_servicedesk_queue_size{project=\"${q_proj}\",queue=\"${safe_qname}\"} ${q_count}"
            done < "$CACHE_DIR/sd_queues_all"
        fi
        echo ""

        # Request types per service desk
        echo "# HELP jira_servicedesk_request_type Available request types per service desk"
        echo "# TYPE jira_servicedesk_request_type gauge"
        if [ -f "$CACHE_DIR/sd_request_types_all" ] && [ -s "$CACHE_DIR/sd_request_types_all" ]; then
            while IFS='|' read -r rt_proj _rt_id rt_name; do
                [ -z "$rt_proj" ] && continue
                local safe_rtname
                safe_rtname=${rt_name//\"/\\\"}
                echo "jira_servicedesk_request_type{project=\"${rt_proj}\",request_type=\"${safe_rtname}\"} 1"
            done < "$CACHE_DIR/sd_request_types_all"
        fi
        echo ""

        # Organizations per service desk
        echo "# HELP jira_servicedesk_organizations Organization count per service desk"
        echo "# TYPE jira_servicedesk_organizations gauge"
        if [ -f "$CACHE_DIR/sd_orgs_all" ] && [ -s "$CACHE_DIR/sd_orgs_all" ]; then
            while IFS='|' read -r o_proj o_count; do
                [ -z "$o_proj" ] && continue
                echo "jira_servicedesk_organizations{project=\"${o_proj}\"} ${o_count}"
            done < "$CACHE_DIR/sd_orgs_all"
        fi
        echo ""
    fi

    # Login activity metrics (from audit log, last 24h)
    local logins_total logins_success logins_failed logins_unique
    logins_total=$(read_cache "logins_total")
    logins_success=$(read_cache "logins_success")
    logins_failed=$(read_cache "logins_failed")
    logins_unique=$(read_cache "logins_unique_users")

    cat <<LOGINEOF
# HELP jira_logins_total_24h Total login events in last 24h
# TYPE jira_logins_total_24h gauge
jira_logins_total_24h $logins_total

# HELP jira_logins_success_24h Successful logins in last 24h
# TYPE jira_logins_success_24h gauge
jira_logins_success_24h $logins_success

# HELP jira_logins_failed_24h Failed login attempts in last 24h
# TYPE jira_logins_failed_24h gauge
jira_logins_failed_24h $logins_failed

# HELP jira_logins_unique_users_24h Unique users who logged in during last 24h
# TYPE jira_logins_unique_users_24h gauge
jira_logins_unique_users_24h $logins_unique

LOGINEOF

    echo "# HELP jira_user_logins_24h Login count per user in last 24h (top 15)"
    echo "# TYPE jira_user_logins_24h gauge"
    if [ -f "$CACHE_DIR/logins_per_user" ] && [ -s "$CACHE_DIR/logins_per_user" ]; then
        while IFS='|' read -r user count; do
            [ -z "$user" ] && continue
            local safe_user
            safe_user=${user//\"/\\\"}
            echo "jira_user_logins_24h{user=\"${safe_user}\"} ${count}"
        done < "$CACHE_DIR/logins_per_user"
    fi
    echo ""

    cat <<EOF
# HELP jira_scrape_timestamp_seconds Unix timestamp of metric generation
# TYPE jira_scrape_timestamp_seconds gauge
jira_scrape_timestamp_seconds $(date +%s)

# HELP jira_scrape_duration_seconds Time to generate all metrics
# TYPE jira_scrape_duration_seconds gauge
jira_scrape_duration_seconds $(($(date +%s) - start_time))

# HELP jira_scrape_success Whether the last scrape was successful (1=yes, 0=no)
# TYPE jira_scrape_success gauge
jira_scrape_success 1
EOF

    echo ""

    rm -rf "$CACHE_DIR"
}

# ---------------------------------------------------------------------------
# HTTP server
# ---------------------------------------------------------------------------

run_http_server() {
    echo "Starting Jira exporter on port $HTTP_PORT..." >&2

    while true; do
        {
            read -r request
            if [[ "$request" =~ ^GET\ /metrics ]]; then
                printf "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4; charset=utf-8\r\n\r\n"
                cache_all_jira_data
                cache_project_data
                cache_assignee_data
                cache_component_data
                cache_resolution_time_data
                cache_login_data
                cache_plugin_data
                cache_servicedesk_data
                generate_metrics
            else
                printf "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n"
                echo "<h1>Jira Exporter v${SCRIPT_VERSION}</h1><a href='/metrics'>Metrics</a>"
            fi
        } | nc -l -p "$HTTP_PORT" -q 1 2>/dev/null
    done
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

main() {
    parse_args "$@"
    preflight_check

    [ "$HTTP_MODE" != true ] && acquire_lock

    if [ "$HTTP_MODE" = true ]; then
        run_http_server
    elif [ -n "$OUTPUT_FILE" ]; then
        cache_all_jira_data
        cache_project_data
        cache_assignee_data
        cache_component_data
        cache_resolution_time_data
        cache_login_data
        cache_plugin_data
        cache_servicedesk_data

        mkdir -p "$(dirname "$OUTPUT_FILE")"

        local temp_file
        temp_file=$(mktemp /tmp/jira_metrics.XXXXXX)

        generate_metrics > "$temp_file"

        rm -f "$OUTPUT_FILE"
        mv "$temp_file" "$OUTPUT_FILE"
        chmod 644 "$OUTPUT_FILE"
        sync
    else
        cache_all_jira_data
        cache_project_data
        cache_assignee_data
        cache_component_data
        cache_resolution_time_data
        cache_login_data
        cache_plugin_data
        cache_servicedesk_data
        generate_metrics
    fi
}

main "$@"
