#!/bin/bash
################################################################################
# Script Name: n8n-exporter.sh
# Version: 1.0
# Description: Prometheus exporter for n8n workflow automation — pulls built-in
#              /metrics data and supplements with API-sourced per-workflow info,
#              execution stats, credential counts, and health checks. Designed
#              for node_exporter textfile collector.
#
# Author: Phil Connor
# Contact: contact@mylinux.work
# Website: https://mylinux.work
# License: MIT
#
# Prerequisites:
#   - n8n with N8N_METRICS=true (for /metrics endpoint)
#   - curl and jq
#   - n8n API key (for supplemental metrics — optional)
#   - netcat (nc) for HTTP mode
#
# Usage:
#   ./n8n-exporter.sh                                 # stdout
#   ./n8n-exporter.sh --textfile                      # node_exporter textfile
#   ./n8n-exporter.sh --http -p 9200                  # HTTP server
#   ./n8n-exporter.sh --url http://n8n:5678 --api-key KEY
#
################################################################################

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

TEXTFILE_DIR="/var/lib/node_exporter"
OUTPUT_FILE=""
HTTP_MODE=false
HTTP_PORT=9200

# Source environment file if present (for cron deployments)
[ -f /etc/default/n8n-exporter ] && . /etc/default/n8n-exporter

N8N_URL="${N8N_URL:-http://localhost:5678}"
N8N_API_KEY="${N8N_API_KEY:-}"

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

prom_escape() {
    local s="$1"
    s=${s//\\/\\\\}
    s=${s//\"/\\\"}
    s=${s//$'\n'/\\n}
    printf '%s\n' "$s"
}

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

Export n8n metrics as Prometheus metrics (v1.0).

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

OPTIONS:
    -p, --port          HTTP port (default: 9200)
    -o, --output        Output file path
    --url URL           n8n base URL (default: $N8N_URL)
    --api-key KEY       n8n API key (for supplemental metrics)

EXAMPLES:
    $0 --textfile                              # Write to textfile collector
    $0 --http --port 9200                      # Run HTTP server
    $0 --url http://n8n:5678 --api-key KEY     # Custom URL with API key
    $0 -o /tmp/n8n.prom                        # Write to custom file

ENVIRONMENT VARIABLES:
    N8N_URL             n8n base URL (default: http://localhost:5678)
    N8N_API_KEY         n8n API key for supplemental metrics

EOF
    exit 0
}

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

# ============================================================================
# METRICS GENERATION
# ============================================================================

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

    # ========================================================================
    # Prerequisite Check
    # ========================================================================

    if ! command -v curl >/dev/null 2>&1; then
        echo "# ERROR: curl is required but not found" >&2
        cat <<EOF
# HELP n8n_exporter_up Exporter status (1=up, 0=down)
# TYPE n8n_exporter_up gauge
n8n_exporter_up 0

# HELP n8n_exporter_info Exporter version information
# TYPE n8n_exporter_info gauge
n8n_exporter_info{version="1.0"} 1

EOF
        return
    fi

    if [ -n "$N8N_API_KEY" ] && ! command -v jq >/dev/null 2>&1; then
        echo "# WARNING: jq not found, skipping supplemental API metrics" >&2
        N8N_API_KEY=""
    fi

    # ========================================================================
    # Exporter Identity
    # ========================================================================
    local exporter_up=1

    cat <<EOF
# HELP n8n_exporter_info Exporter version information
# TYPE n8n_exporter_info gauge
n8n_exporter_info{version="1.0"} 1

EOF

    # ========================================================================
    # Step 1: Built-in /metrics Endpoint
    # ========================================================================
    local builtin_metrics
    builtin_metrics=$(curl -sf --max-time 10 "${N8N_URL}/metrics" 2>/dev/null)

    if [ -n "$builtin_metrics" ]; then
        echo "# ── Built-in n8n metrics (/metrics) ──"
        echo "$builtin_metrics"
        echo ""
    else
        echo "# WARNING: could not read ${N8N_URL}/metrics (check n8n is running with N8N_METRICS=true)" >&2
        exporter_up=0
    fi

    # ========================================================================
    # Step 2: Health Check (/healthz)
    # ========================================================================
    local health_status
    local health_http_code
    health_http_code=$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' "${N8N_URL}/healthz" 2>/dev/null)

    if [ $? -eq 0 ] && [ "$health_http_code" = "200" ]; then
        health_status=1
    else
        health_status=0
    fi

    cat <<EOF
# HELP n8n_exporter_health_ok n8n health check status (1=healthy, 0=unhealthy)
# TYPE n8n_exporter_health_ok gauge
n8n_exporter_health_ok $health_status

EOF

    # ========================================================================
    # API-Sourced Supplemental Metrics
    # ========================================================================

    if [ -z "$N8N_API_KEY" ]; then
        echo "# WARNING: N8N_API_KEY not set, skipping supplemental API metrics" >&2
    else
        local api_header="X-N8N-API-KEY: ${N8N_API_KEY}"

        # ====================================================================
        # Step 3: Workflow Info (/api/v1/workflows)
        # ====================================================================
        local workflows_json
        workflows_json=$(curl -sf --max-time 10 -H "$api_header" "${N8N_URL}/api/v1/workflows?limit=250" 2>/dev/null)

        local workflow_info_lines=""

        if [ -n "$workflows_json" ] && [ "$workflows_json" != "null" ]; then
            local workflow_data
            workflow_data=$(echo "$workflows_json" | jq -r '
                .data // [] | .[] |
                "\(.id)\t\(.name)\t\(.active)\t\((.tags // []) | map(.name) | join(","))"
            ' 2>/dev/null)

            if [ -n "$workflow_data" ]; then
                while IFS=$'\t' read -r wf_id wf_name wf_active wf_tags; do
                    [ -z "$wf_id" ] && continue
                    local esc_id esc_name esc_active esc_tags
                    esc_id=$(prom_escape "$wf_id")
                    esc_name=$(prom_escape "$wf_name")
                    esc_active=$(prom_escape "$wf_active")
                    esc_tags=$(prom_escape "$wf_tags")
                    workflow_info_lines="${workflow_info_lines}n8n_exporter_workflow_info{id=\"${esc_id}\",name=\"${esc_name}\",active=\"${esc_active}\",tags=\"${esc_tags}\"} 1
"
                done <<< "$workflow_data"
            fi
        else
            echo "# WARNING: could not read /api/v1/workflows" >&2
        fi

        if [ -n "$workflow_info_lines" ]; then
            echo "# HELP n8n_exporter_workflow_info Workflow information (always 1)"
            echo "# TYPE n8n_exporter_workflow_info gauge"
            printf '%s' "$workflow_info_lines"
            echo ""
        fi

        # ====================================================================
        # Step 4: Execution Stats (/api/v1/executions)
        # ====================================================================
        local executions_json
        executions_json=$(curl -sf --max-time 10 -H "$api_header" "${N8N_URL}/api/v1/executions?limit=100" 2>/dev/null)

        local last_exec_lines=""
        local error_count_lines=""

        if [ -n "$executions_json" ] && [ "$executions_json" != "null" ]; then
            # Extract per-workflow last execution timestamp and error counts
            local exec_stats
            exec_stats=$(echo "$executions_json" | jq -r '
                .data // [] |
                group_by(.workflowId) | .[] |
                {
                    id: .[0].workflowId,
                    name: (.[0].workflowData.name // "unknown"),
                    last_finished: (map(select(.stoppedAt != null) | .stoppedAt) | sort | last // ""),
                    errors: (map(select(.status == "error" or .status == "failed")) | length)
                } |
                "\(.id)\t\(.name)\t\(.last_finished)\t\(.errors)"
            ' 2>/dev/null)

            if [ -n "$exec_stats" ]; then
                while IFS=$'\t' read -r ex_wf_id ex_wf_name ex_last_ts ex_errors; do
                    [ -z "$ex_wf_id" ] && continue
                    local esc_ex_id esc_ex_name
                    esc_ex_id=$(prom_escape "$ex_wf_id")
                    esc_ex_name=$(prom_escape "$ex_wf_name")
                    local labels="id=\"${esc_ex_id}\",name=\"${esc_ex_name}\""

                    # Last execution timestamp
                    if [ -n "$ex_last_ts" ] && [ "$ex_last_ts" != "null" ] && [ "$ex_last_ts" != "" ]; then
                        local epoch_ts
                        epoch_ts=$(date -d "$ex_last_ts" +%s 2>/dev/null)
                        if [ -n "$epoch_ts" ]; then
                            last_exec_lines="${last_exec_lines}n8n_exporter_workflow_last_execution_timestamp{${labels}} ${epoch_ts}
"
                        fi
                    fi

                    # Error count
                    ex_errors=${ex_errors:-0}
                    error_count_lines="${error_count_lines}n8n_exporter_workflow_errors_recent{${labels}} ${ex_errors}
"
                done <<< "$exec_stats"
            fi
        else
            echo "# WARNING: could not read /api/v1/executions" >&2
        fi

        if [ -n "$last_exec_lines" ]; then
            echo "# HELP n8n_exporter_workflow_last_execution_timestamp Unix timestamp of last finished execution per workflow"
            echo "# TYPE n8n_exporter_workflow_last_execution_timestamp gauge"
            printf '%s' "$last_exec_lines"
            echo ""
        fi

        if [ -n "$error_count_lines" ]; then
            echo "# HELP n8n_exporter_workflow_errors_recent Errored executions per workflow (from last 100 executions)"
            echo "# TYPE n8n_exporter_workflow_errors_recent gauge"
            printf '%s' "$error_count_lines"
            echo ""
        fi

        # ====================================================================
        # Step 5: Credentials by Type (/api/v1/credentials)
        # ====================================================================
        local credentials_json
        credentials_json=$(curl -sf --max-time 10 -H "$api_header" "${N8N_URL}/api/v1/credentials?limit=250" 2>/dev/null)

        local cred_type_lines=""

        if [ -n "$credentials_json" ] && [ "$credentials_json" != "null" ]; then
            local cred_counts
            cred_counts=$(echo "$credentials_json" | jq -r '
                .data // [] |
                group_by(.type) | .[] |
                "\(.[0].type)\t\(length)"
            ' 2>/dev/null)

            if [ -n "$cred_counts" ]; then
                while IFS=$'\t' read -r cred_type cred_count; do
                    [ -z "$cred_type" ] && continue
                    local esc_cred_type
                    esc_cred_type=$(prom_escape "$cred_type")
                    cred_type_lines="${cred_type_lines}n8n_exporter_credentials_by_type{type=\"${esc_cred_type}\"} ${cred_count}
"
                done <<< "$cred_counts"
            fi
        else
            echo "# WARNING: could not read /api/v1/credentials" >&2
        fi

        if [ -n "$cred_type_lines" ]; then
            echo "# HELP n8n_exporter_credentials_by_type Number of credentials by type"
            echo "# TYPE n8n_exporter_credentials_by_type gauge"
            printf '%s' "$cred_type_lines"
            echo ""
        fi
    fi

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

    cat <<EOF
# HELP n8n_exporter_up Exporter status (1=up, 0=down)
# TYPE n8n_exporter_up gauge
n8n_exporter_up $exporter_up

# HELP n8n_exporter_duration_seconds Time to generate all metrics
# TYPE n8n_exporter_duration_seconds gauge
n8n_exporter_duration_seconds $script_duration

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

    echo ""
}

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

run_http_server() {
    echo "Starting n8n 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

    trap 'echo "Shutting down n8n exporter..." >&2; exit 0' INT TERM

    while true; do
        {
            read -r request
            local body
            if [[ "$request" =~ ^GET\ /metrics ]]; then
                body=$(generate_metrics)
                printf "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" "${#body}" "$body"
            else
                body=$(cat <<'HTMLEOF'
<!DOCTYPE html>
<html>
<head><title>n8n Exporter v1.0</title></head>
<body>
<h1>n8n Exporter v1.0</h1>
<p><a href="/metrics">Metrics</a></p>
<h2>Sections</h2>
<ul>
<li>Built-in n8n /metrics (version, workflows, executions, cache, queue)</li>
<li>Health check status</li>
<li>Per-workflow info (name, active, tags)</li>
<li>Per-workflow execution stats (last run, error count)</li>
<li>Credentials by type</li>
</ul>
</body>
</html>
HTMLEOF
)
                printf "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" "${#body}" "$body"
            fi
        } | if nc -h 2>&1 | grep -q 'GNU\|traditional'; then
            nc -l -p "$HTTP_PORT" -q 1 2>/dev/null
        else
            nc -l "$HTTP_PORT" 2>/dev/null
        fi
    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}/.n8n_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 3 ]; 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 "$@"
