#!/bin/bash
################################################################################
# Script Name: linux-log-analyzer.sh
# Version: 1.00
# Description: Analyze Linux system logs across five log types — system, auth,
#              Docker, Java, GitLab, and PostgreSQL. Parses syslog, auth.log,
#              journalctl, Java/log4j logs, GitLab JSON logs, and PostgreSQL
#              logs to surface errors, failed logins, service failures, stack
#              traces, slow queries, and crash events.
#
# Author: Phil Connor
# Contact: contact@mylinux.work
# Website: https://mylinux.work
# License: MIT
#
# Prerequisites:
#   - bash, awk, grep, sort
#   - journalctl (optional, for systemd-based systems)
#   - Root/sudo for auth and system log types
#
# Usage:
#   sudo ./linux-log-analyzer.sh --type system
#   sudo ./linux-log-analyzer.sh --type auth --since 24h
#   sudo ./linux-log-analyzer.sh --type docker
#   ./linux-log-analyzer.sh --type java --log /opt/app/logs/application.log
#   sudo ./linux-log-analyzer.sh --type gitlab
#   ./linux-log-analyzer.sh --type postgresql
#   sudo ./linux-log-analyzer.sh --all-types
#   sudo ./linux-log-analyzer.sh --type system --json
#   sudo ./linux-log-analyzer.sh --all-types --no-color > report.txt
#
# Log Types:
#   system      - syslog/messages: service failures, OOM, disk errors, panics
#   auth        - auth.log/secure: SSH brute force, sudo, account changes
#   docker      - Docker daemon: container crashes, restart loops, OOM
#   java        - log4j/logback/catalina: stack traces, OOM, GC pauses, errors
#   gitlab      - GitLab Omnibus: 5xx errors, slow requests, Sidekiq failures
#   postgresql  - PostgreSQL: deadlocks, slow queries, connection errors
################################################################################

set -uo pipefail

# ============================================================================
# DEFAULTS
# ============================================================================

VERSION="1.00"
LOG_TYPE=""
ALL_TYPES=false
SINCE=""
SINCE_EPOCH=""
TOP_N=20
JSON_MODE=false
NO_COLOR=false
OUTPUT_FILE=""
CUSTOM_LOG=""

# Colors
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

# JSON accumulator
JSON_OUTPUT=""

# ============================================================================
# USAGE
# ============================================================================

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

Analyze Linux system logs (v${VERSION}).

LOG TYPES:
    --type TYPE      Log type: system, auth, docker, java, gitlab, postgresql
    --all-types      Run all available analyzers in one pass

OPTIONS:
    --since RANGE    Time filter: 1h, 24h, 7d, 30d, or ISO date (default: all)
    --top N          Entries per section (default: ${TOP_N})
    --json           Output as JSON
    --no-color       Disable colored output
    --output FILE    Save report to file
    --log PATH       Override default log path
    -h, --help       Show this help

EXAMPLES:
    sudo $0 --type system
    sudo $0 --type auth --since 24h
    sudo $0 --type docker --since 7d
    $0 --type java --log /opt/tomcat/logs/catalina.out
    sudo $0 --type gitlab
    $0 --type postgresql --log /var/log/postgresql/postgresql-16-main.log
    sudo $0 --all-types --no-color > report.txt
    sudo $0 --type system --json | jq .

EOF
    exit 0
}

# ============================================================================
# HELPERS
# ============================================================================

disable_colors() {
    RED="" YELLOW="" GREEN="" CYAN="" BOLD="" DIM="" NC=""
}

log_info()  { echo -e "${CYAN}[INFO]${NC} $1"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }

check_root() {
    if [[ $EUID -ne 0 ]]; then
        log_error "This log type requires root. Run with: sudo $0 $*"
        return 1
    fi
    return 0
}

section_header() {
    local title="$1"
    echo ""
    echo -e "${BOLD}===================================================="
    echo -e "  ${title}"
    echo -e "====================================================${NC}"
}

subsection_header() {
    local title="$1"
    echo ""
    echo -e "${BOLD}── ${title} ─────────────────────────────────────────${NC}"
}

# Convert relative time strings to epoch
parse_since() {
    local since="$1"
    local now
    now=$(date +%s)

    case "$since" in
        *h)
            local hours="${since%h}"
            SINCE_EPOCH=$((now - hours * 3600))
            ;;
        *d)
            local days="${since%d}"
            SINCE_EPOCH=$((now - days * 86400))
            ;;
        *)
            # Try ISO date
            SINCE_EPOCH=$(date -d "$since" +%s 2>/dev/null || echo "")
            if [[ -z "$SINCE_EPOCH" ]]; then
                log_error "Invalid --since value: $since"
                exit 1
            fi
            ;;
    esac
}

# Get journalctl --since string from SINCE
journalctl_since_arg() {
    if [[ -n "$SINCE" ]]; then
        case "$SINCE" in
            *h) echo "--since=${SINCE%h} hours ago" ;;
            *d) echo "--since=${SINCE%d} days ago" ;;
            *)  echo "--since=$SINCE" ;;
        esac
    fi
}

# Filter lines by timestamp — uses a reference date to avoid per-line date calls
filter_by_time() {
    if [[ -z "$SINCE_EPOCH" ]]; then
        cat
        return
    fi
    local cutoff_date
    cutoff_date=$(date -d "@${SINCE_EPOCH}" "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
    local cutoff_syslog
    cutoff_syslog=$(date -d "@${SINCE_EPOCH}" "+%b %d %H:%M:%S" 2>/dev/null)
    local current_year
    current_year=$(date +%Y)

    awk -v cutoff_iso="$cutoff_date" -v cutoff_syslog="$cutoff_syslog" -v yr="$current_year" '
    BEGIN {
        # Month name to number for syslog comparison
        split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec", m)
        for (i = 1; i <= 12; i++) mon_num[m[i]] = sprintf("%02d", i)
    }
    {
        # ISO: 2026-04-13 14:22:03 or 2026-04-13T14:22:03
        if (match($0, /^[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}:[0-9]{2}/)) {
            ts = substr($0, RSTART, RLENGTH)
            gsub(/T/, " ", ts)
            if (ts >= cutoff_iso) print
            next
        }
        # Syslog: Apr 13 14:22:03
        if (match($0, /^[A-Z][a-z]{2} [ 0-9][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2}/)) {
            ts = substr($0, RSTART, RLENGTH)
            # Convert to sortable: YYYY-MM-DD HH:MM:SS
            mon_name = substr(ts, 1, 3)
            day = substr(ts, 5, 2)
            gsub(/ /, "0", day)
            time_part = substr(ts, 8)
            sortable = yr "-" mon_num[mon_name] "-" day " " time_part
            if (sortable >= cutoff_iso) print
            next
        }
        # No timestamp — include line (continuation / multi-line)
        print
    }'
}

# Find first available log file from a list
find_log() {
    for f in "$@"; do
        if [[ -r "$f" ]]; then
            echo "$f"
            return 0
        fi
    done
    return 1
}

# Read log source — file or journalctl
read_log_source() {
    local log_file="$1"
    local journal_unit="${2:-}"

    if [[ -n "$CUSTOM_LOG" && -r "$CUSTOM_LOG" ]]; then
        cat "$CUSTOM_LOG" | filter_by_time
        return
    fi

    if [[ -n "$log_file" && -r "$log_file" ]]; then
        cat "$log_file" | filter_by_time
        return
    fi

    if [[ -n "$journal_unit" ]] && command -v journalctl &>/dev/null; then
        local since_arg
        since_arg=$(journalctl_since_arg)
        if [[ -n "$since_arg" ]]; then
            journalctl -u "$journal_unit" --no-pager -q $since_arg 2>/dev/null
        else
            journalctl -u "$journal_unit" --no-pager -q 2>/dev/null
        fi
        return
    fi

    return 1
}

# Emit a JSON key-value pair (accumulates into JSON_OUTPUT)
json_add() {
    local key="$1"
    local value="$2"
    if [[ -n "$JSON_OUTPUT" ]]; then
        JSON_OUTPUT="${JSON_OUTPUT},"
    fi
    JSON_OUTPUT="${JSON_OUTPUT}\"${key}\":${value}"
}

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

# ============================================================================
# SYSTEM LOG ANALYZER
# ============================================================================

analyze_system() {
    if ! check_root; then return 1; fi

    local log_file
    log_file=$(find_log /var/log/syslog /var/log/messages)

    local log_data
    log_data=$(read_log_source "${log_file:-}" "")

    # Also pull journalctl if available and no file source
    if [[ -z "$log_data" ]] && command -v journalctl &>/dev/null; then
        local since_arg
        since_arg=$(journalctl_since_arg)
        if [[ -n "$since_arg" ]]; then
            log_data=$(journalctl --no-pager -q $since_arg 2>/dev/null)
        else
            log_data=$(journalctl --no-pager -q --since "24 hours ago" 2>/dev/null)
        fi
    fi

    if [[ -z "$log_data" ]]; then
        log_warn "No system log sources found (syslog, messages, or journalctl)"
        return 1
    fi

    local total_lines
    total_lines=$(echo "$log_data" | wc -l)

    if ! $JSON_MODE; then
        section_header "System Log Analysis"
        echo ""
        echo -e "  Source: ${DIM}${log_file:-journalctl}${NC}"
        echo -e "  Lines:  ${total_lines}"
    fi

    # Service failures
    local service_failures
    service_failures=$(echo "$log_data" | grep -iE "(failed|error|fatal)" | \
        grep -oP '(\S+\.service)' | sort | uniq -c | sort -rn | head -"$TOP_N")

    if ! $JSON_MODE; then
        subsection_header "Service Failures"
        if [[ -n "$service_failures" ]]; then
            printf "   ${BOLD}%-4s %-35s %s${NC}\n" "#" "Service" "Failures"
            echo "$service_failures" | awk '{printf "   %-4d %-35s %d\n", NR, $2, $1}' | head -"$TOP_N"
        else
            echo "   None found."
        fi
    fi

    # OOM kills
    local oom_kills
    oom_kills=$(echo "$log_data" | grep -i "oom-kill\|oom_kill\|out of memory\|killed process" | head -"$TOP_N")

    local oom_count=0
    if [[ -n "$oom_kills" ]]; then
        oom_count=$(echo "$oom_kills" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "OOM Kills"
        if [[ -n "$oom_kills" ]]; then
            echo "$oom_kills" | while IFS= read -r line; do
                local proc
                proc=$(echo "$line" | grep -oP "process \d+ \(\K[^)]+|Killed process \d+ \(\K[^)]+|oom_kill.*task=\K\S+" | head -1)
                local pid
                pid=$(echo "$line" | grep -oP "process \K\d+|Killed process \K\d+" | head -1)
                local ts
                ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+")
                printf "   %-22s  PID %-7s  %s\n" "${ts:-unknown}" "${pid:-?}" "${proc:-unknown}"
            done
        else
            echo "   None found."
        fi
    fi

    # Disk errors
    local disk_errors
    disk_errors=$(echo "$log_data" | grep -iE "(I/O error|EXT[234]-fs.*(warning|error)|XFS.*(error|corruption)|BTRFS.*error|SMART.*error|end_request.*I/O|medium error|blk_update_request.*error|Buffer I/O error|remount.*read-only)" | head -"$TOP_N")

    local disk_error_count=0
    if [[ -n "$disk_errors" ]]; then
        disk_error_count=$(echo "$disk_errors" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Disk Errors"
        if [[ -n "$disk_errors" ]]; then
            echo "$disk_errors" | while IFS= read -r line; do
                local ts
                ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9]{2}:[0-9]{2}:[0-9]{2}")
                local msg
                msg=$(echo "$line" | sed -E 's/^.*kernel(\[[0-9.]+\])?: //' | head -c 80)
                printf "   %-22s  %s\n" "${ts:-unknown}" "$msg"
            done
        else
            echo "   None found."
        fi
    fi

    # Failed logins (from syslog)
    local failed_logins
    failed_logins=$(echo "$log_data" | grep -iE "(failed password|authentication failure|failed login)" | \
        grep -oP "user[ =]\K\S+|for \K\S+(?= from)" | sort | uniq -c | sort -rn | head -"$TOP_N")

    local failed_login_total=0
    if [[ -n "$failed_logins" ]]; then
        failed_login_total=$(echo "$failed_logins" | awk '{s+=$1} END {print s+0}')
    fi

    if ! $JSON_MODE; then
        subsection_header "Failed Logins"
        if [[ -n "$failed_logins" ]]; then
            printf "   ${BOLD}%-4s %-25s %s${NC}\n" "#" "User" "Failures"
            echo "$failed_logins" | awk '{printf "   %-4d %-25s %d\n", NR, $2, $1}' | head -"$TOP_N"
        else
            echo "   None found."
        fi
    fi

    # Sudo usage
    local sudo_usage
    sudo_usage=$(echo "$log_data" | grep -i "sudo:" | grep "COMMAND=" | \
        grep -oP "^\S+\s+\S+\s+\S+\s+\S+\s+\K\S+(?=\s)" | sort | uniq -c | sort -rn | head -"$TOP_N")

    local sudo_total=0
    if [[ -n "$sudo_usage" ]]; then
        sudo_total=$(echo "$sudo_usage" | awk '{s+=$1} END {print s+0}')
    fi

    if ! $JSON_MODE; then
        subsection_header "Sudo Usage"
        if [[ -n "$sudo_usage" ]]; then
            printf "   ${BOLD}%-4s %-25s %s${NC}\n" "#" "User" "Commands"
            echo "$sudo_usage" | awk '{printf "   %-4d %-25s %d\n", NR, $2, $1}' | head -"$TOP_N"
        else
            echo "   None found."
        fi
    fi

    # Kernel panics / segfaults
    local kernel_panics
    kernel_panics=$(echo "$log_data" | grep -iE "(kernel panic|segfault|general protection fault|BUG:|Oops:)" | head -"$TOP_N")

    local panic_count=0
    if [[ -n "$kernel_panics" ]]; then
        panic_count=$(echo "$kernel_panics" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Kernel Panics / Segfaults"
        if [[ -n "$kernel_panics" ]]; then
            echo "$kernel_panics" | while IFS= read -r line; do
                local ts
                ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+")
                local msg
                msg=$(echo "$line" | sed 's/^.*\(kernel\)[^:]*: //')
                printf "   %-22s  %s\n" "${ts:-unknown}" "${msg:0:80}"
            done
        else
            echo "   None found."
        fi
    fi

    # Cron failures
    local cron_failures
    cron_failures=$(echo "$log_data" | grep -iE "(cron.*error|CRON.*FAILED|cron.*exit status [^0])" | head -"$TOP_N")

    local cron_fail_count=0
    if [[ -n "$cron_failures" ]]; then
        cron_fail_count=$(echo "$cron_failures" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Cron Failures"
        if [[ -n "$cron_failures" ]]; then
            echo "$cron_failures" | while IFS= read -r line; do
                local ts
                ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+")
                local msg
                msg=$(echo "$line" | sed 's/^.*CRON[^:]*: //' | head -c 80)
                printf "   %-22s  %s\n" "${ts:-unknown}" "$msg"
            done
        else
            echo "   None found."
        fi
    fi

    # Top error patterns
    local error_patterns
    error_patterns=$(echo "$log_data" | grep -iE "(error|fail|fatal|critical)" | \
        sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[^ ]+ [^ ]+ [^:]+: //' | \
        sed -E 's/^[A-Z][a-z]{2} +[0-9]{1,2} [0-9:]{8} [^ ]+ [^:]+: //' | \
        sed -E ':a; s/^(time|ts|level|caller|source|component|host)=[^ ]+ //; ta' | \
        sed -E 's/^\[[0-9T:.Z+ -]+ [A-Z]+ [^]]+\] //' | \
        sed 's/[0-9]\{2,\}/#/g' | sort | uniq -c | sort -rn | head -"$TOP_N")

    if ! $JSON_MODE; then
        subsection_header "Top Error Patterns"
        if [[ -n "$error_patterns" ]]; then
            printf "   ${BOLD}%-4s %-6s %s${NC}\n" "#" "Count" "Pattern"
            echo "$error_patterns" | awk '{
                count = $1
                $1 = ""
                sub(/^ +/, "")
                msg = substr($0, 1, 70)
                printf "   %-4d %-6d %s\n", NR, count, msg
            }' | head -"$TOP_N"
        else
            echo "   None found."
        fi
    fi

    # Service failure count
    local svc_fail_count=0
    if [[ -n "$service_failures" ]]; then
        svc_fail_count=$(echo "$service_failures" | awk '{s+=$1} END {print s+0}')
    fi

    # Summary
    if ! $JSON_MODE; then
        section_header "Summary"
        echo ""
        printf "  %-25s %d\n" "Service failures:" "$svc_fail_count"
        printf "  %-25s %d\n" "OOM kills:" "$oom_count"
        printf "  %-25s %d\n" "Disk errors:" "$disk_error_count"
        printf "  %-25s %d\n" "Failed logins:" "$failed_login_total"
        printf "  %-25s %d\n" "Sudo commands:" "$sudo_total"
        printf "  %-25s %d\n" "Kernel panics:" "$panic_count"
        printf "  %-25s %d\n" "Cron failures:" "$cron_fail_count"
        printf "  %-25s %d\n" "Total lines parsed:" "$total_lines"
    else
        json_add "system" "{\"service_failures\":${svc_fail_count},\"oom_kills\":${oom_count},\"disk_errors\":${disk_error_count},\"failed_logins\":${failed_login_total},\"sudo_commands\":${sudo_total},\"kernel_panics\":${panic_count},\"cron_failures\":${cron_fail_count},\"total_lines\":${total_lines}}"
    fi
}

# ============================================================================
# AUTH LOG ANALYZER
# ============================================================================

analyze_auth() {
    if ! check_root; then return 1; fi

    local log_file
    log_file=$(find_log /var/log/auth.log /var/log/secure)

    local log_data
    log_data=$(read_log_source "${log_file:-}" "ssh")

    if [[ -z "$log_data" ]]; then
        log_warn "No auth log sources found (auth.log, secure, or journalctl)"
        return 1
    fi

    local total_lines
    total_lines=$(echo "$log_data" | wc -l)

    if ! $JSON_MODE; then
        section_header "Auth Log Analysis"
        echo ""
        echo -e "  Source: ${DIM}${log_file:-journalctl}${NC}"
        echo -e "  Lines:  ${total_lines}"
    fi

    # SSH login failures by IP
    local ssh_fail_ips
    ssh_fail_ips=$(echo "$log_data" | grep -i "failed password" | \
        grep -oP "from \K[0-9a-f.:]+(?= port)" | sort | uniq -c | sort -rn | head -"$TOP_N")

    if ! $JSON_MODE; then
        subsection_header "SSH Failed Logins by IP"
        if [[ -n "$ssh_fail_ips" ]]; then
            printf "   ${BOLD}%-4s %-40s %s${NC}\n" "#" "IP Address" "Failures"
            echo "$ssh_fail_ips" | awk '{printf "   %-4d %-40s %d\n", NR, $2, $1}' | head -"$TOP_N"
        else
            echo "   None found."
        fi
    fi

    # Brute force detection (>10 failures)
    local brute_force
    brute_force=$(echo "$log_data" | grep -i "failed password" | \
        grep -oP "from \K[0-9a-f.:]+(?= port)" | sort | uniq -c | sort -rn | awk '$1 > 10')

    local brute_force_count=0
    if [[ -n "$brute_force" ]]; then
        brute_force_count=$(echo "$brute_force" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Brute Force Suspects (>10 failures)"
        if [[ -n "$brute_force" ]]; then
            printf "   ${BOLD}%-40s %s${NC}\n" "IP Address" "Failures"
            echo "$brute_force" | awk '{printf "   %-40s %s\n", $2, $1}'
            echo ""
            echo -e "   ${RED}${brute_force_count} IP(s) with >10 failed attempts${NC}"
        else
            echo -e "   ${GREEN}None found.${NC}"
        fi
    fi

    # SSH login successes by user
    local ssh_success
    ssh_success=$(echo "$log_data" | grep -i "accepted" | \
        grep -oP "for \K\S+(?= from)" | sort | uniq -c | sort -rn | head -"$TOP_N")

    if ! $JSON_MODE; then
        subsection_header "SSH Successful Logins by User"
        if [[ -n "$ssh_success" ]]; then
            printf "   ${BOLD}%-4s %-25s %s${NC}\n" "#" "User" "Logins"
            echo "$ssh_success" | awk '{printf "   %-4d %-25s %d\n", NR, $2, $1}' | head -"$TOP_N"
        else
            echo "   None found."
        fi
    fi

    # SSH key vs password
    local key_logins=0 pass_logins=0
    key_logins=$(echo "$log_data" | grep -ic "accepted publickey" || true)
    pass_logins=$(echo "$log_data" | grep -ic "accepted password" || true)

    if ! $JSON_MODE; then
        subsection_header "Auth Method Breakdown"
        printf "   %-25s %d\n" "SSH key:" "$key_logins"
        printf "   %-25s %d\n" "Password:" "$pass_logins"
    fi

    # Root login attempts
    local root_attempts
    root_attempts=$(echo "$log_data" | grep -iE "(failed password|accepted).* for root " | wc -l || true)
    local root_success
    root_success=$(echo "$log_data" | grep -i "accepted.* for root " | wc -l || true)

    if ! $JSON_MODE; then
        subsection_header "Root Login Attempts"
        printf "   %-25s %d\n" "Total attempts:" "$root_attempts"
        printf "   %-25s %d\n" "Successful:" "$root_success"
        if [[ "$root_success" -gt 0 ]]; then
            echo ""
            echo "$log_data" | grep -i "accepted.* for root " | tail -5 | while IFS= read -r line; do
                local ts ip
                ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+")
                ip=$(echo "$line" | grep -oP "from \K[0-9a-f.:]+")
                printf "   %-22s  from %s\n" "${ts:-unknown}" "${ip:-unknown}"
            done
        fi
    fi

    # Sudo failures
    local sudo_failures
    sudo_failures=$(echo "$log_data" | grep -iE "sudo.*authentication failure|sudo.*incorrect password|sudo.*3 incorrect" | head -"$TOP_N")

    local sudo_fail_count=0
    if [[ -n "$sudo_failures" ]]; then
        sudo_fail_count=$(echo "$sudo_failures" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Sudo Failures"
        if [[ -n "$sudo_failures" ]]; then
            echo "$sudo_failures" | while IFS= read -r line; do
                local ts user
                ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+")
                user=$(echo "$line" | grep -oP "user[ =]\K\S+|;\s+\K\S+(?=\s+:)" | head -1)
                printf "   %-22s  user: %s\n" "${ts:-unknown}" "${user:-unknown}"
            done
        else
            echo "   None found."
        fi
    fi

    # Account changes (useradd, usermod, groupadd, userdel, passwd)
    local account_changes
    account_changes=$(echo "$log_data" | grep -iE "(useradd|usermod|userdel|groupadd|groupdel|passwd|new user|new group|delete user)" | head -"$TOP_N")

    local account_change_count=0
    if [[ -n "$account_changes" ]]; then
        account_change_count=$(echo "$account_changes" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Account Changes"
        if [[ -n "$account_changes" ]]; then
            echo "$account_changes" | while IFS= read -r line; do
                local ts msg
                ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+")
                msg=$(echo "$line" | sed 's/^.*\(useradd\|usermod\|userdel\|groupadd\|passwd\)/\1/' | head -c 70)
                printf "   %-22s  %s\n" "${ts:-unknown}" "$msg"
            done
        else
            echo "   None found."
        fi
    fi

    # Totals
    local total_failures
    total_failures=$(echo "$log_data" | grep -ic "failed password" || true)
    local total_success
    total_success=$(echo "$log_data" | grep -ic "accepted" || true)
    local total_auth=$((total_failures + total_success))
    local success_rate=0
    if [[ "$total_auth" -gt 0 ]]; then
        success_rate=$(awk "BEGIN {printf \"%.1f\", ($total_success / $total_auth) * 100}")
    fi

    if ! $JSON_MODE; then
        section_header "Summary"
        echo ""
        printf "  %-25s %d\n" "Total auth events:" "$total_auth"
        printf "  %-25s %d\n" "Successful logins:" "$total_success"
        printf "  %-25s %d\n" "Failed logins:" "$total_failures"
        printf "  %-25s %s%%\n" "Success rate:" "$success_rate"
        printf "  %-25s %d\n" "Brute force IPs:" "$brute_force_count"
        printf "  %-25s %d\n" "Sudo failures:" "$sudo_fail_count"
        printf "  %-25s %d\n" "Account changes:" "$account_change_count"
    else
        json_add "auth" "{\"total_auth_events\":${total_auth},\"successful_logins\":${total_success},\"failed_logins\":${total_failures},\"success_rate\":${success_rate},\"brute_force_ips\":${brute_force_count},\"sudo_failures\":${sudo_fail_count},\"account_changes\":${account_change_count}}"
    fi
}

# ============================================================================
# DOCKER LOG ANALYZER
# ============================================================================

analyze_docker() {
    if ! command -v docker &>/dev/null && ! [[ -r /var/log/docker.log ]]; then
        log_warn "Docker not found and no /var/log/docker.log — skipping"
        return 1
    fi

    local log_data=""

    # Try journalctl first (most common on systemd systems)
    if command -v journalctl &>/dev/null; then
        local since_arg
        since_arg=$(journalctl_since_arg)
        if [[ -n "$since_arg" ]]; then
            log_data=$(journalctl -u docker.service --no-pager -q $since_arg 2>/dev/null)
        else
            log_data=$(journalctl -u docker.service --no-pager -q 2>/dev/null)
        fi
    fi

    # Fallback to docker.log
    if [[ -z "$log_data" && -r /var/log/docker.log ]]; then
        log_data=$(cat /var/log/docker.log | filter_by_time)
    fi

    # Also pull docker events if docker is available
    local docker_ps_data=""
    if command -v docker &>/dev/null; then
        docker_ps_data=$(docker ps -a --format '{{.Names}}\t{{.Status}}\t{{.CreatedAt}}' 2>/dev/null || true)
    fi

    if [[ -z "$log_data" && -z "$docker_ps_data" ]]; then
        log_warn "No Docker log data available"
        return 1
    fi

    local total_lines=0
    if [[ -n "$log_data" ]]; then
        total_lines=$(echo "$log_data" | wc -l)
    fi

    if ! $JSON_MODE; then
        section_header "Docker Log Analysis"
        echo ""
        echo -e "  Source: ${DIM}journalctl/docker.log${NC}"
        echo -e "  Lines:  ${total_lines}"
    fi

    # Container start/stop/restart events
    local container_events
    container_events=$(echo "$log_data" | grep -oP "container \K(start|stop|restart|kill|die|create|destroy)\S*" 2>/dev/null | \
        sort | uniq -c | sort -rn)

    if ! $JSON_MODE; then
        subsection_header "Container Events"
        if [[ -n "$container_events" ]]; then
            printf "   ${BOLD}%-20s %s${NC}\n" "Event" "Count"
            echo "$container_events" | awk '{printf "   %-20s %d\n", $2, $1}'
        else
            echo "   None found in daemon logs."
        fi
    fi

    # Container restart loops (from docker ps)
    local restart_loops=""
    if [[ -n "$docker_ps_data" ]]; then
        restart_loops=$(echo "$docker_ps_data" | grep -i "restarting" || true)
    fi

    if ! $JSON_MODE; then
        subsection_header "Containers in Restart Loop"
        if [[ -n "$restart_loops" ]]; then
            echo "$restart_loops" | while IFS=$'\t' read -r name status created; do
                printf "   %-30s  %s\n" "$name" "$status"
            done
        else
            echo -e "   ${GREEN}None found.${NC}"
        fi
    fi

    # OOM kills in containers
    local docker_oom
    docker_oom=$(echo "$log_data" | grep -iE "(oom|out of memory)" | head -"$TOP_N")

    local docker_oom_count=0
    if [[ -n "$docker_oom" ]]; then
        docker_oom_count=$(echo "$docker_oom" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Container OOM Kills"
        if [[ -n "$docker_oom" ]]; then
            echo "$docker_oom" | while IFS= read -r line; do
                local ts
                ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+")
                local msg
                msg=$(echo "$line" | sed 's/^.*dockerd[^:]*: //' | head -c 80)
                printf "   %-22s  %s\n" "${ts:-unknown}" "$msg"
            done
        else
            echo "   None found."
        fi
    fi

    # Docker daemon errors
    local daemon_errors
    daemon_errors=$(echo "$log_data" | grep -iE "(error|fatal|panic)" | \
        grep -v "level=info" | grep -v "level=warning" | head -"$TOP_N")

    local daemon_error_count=0
    if [[ -n "$daemon_errors" ]]; then
        daemon_error_count=$(echo "$daemon_errors" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Daemon Errors"
        if [[ -n "$daemon_errors" ]]; then
            echo "$daemon_errors" | while IFS= read -r line; do
                local ts msg
                ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+")
                msg=$(echo "$line" | grep -oP 'msg="\K[^"]+|msg=\K\S+' | head -1)
                printf "   %-22s  %s\n" "${ts:-unknown}" "${msg:-$(echo "$line" | tail -c 80)}"
            done
        else
            echo "   None found."
        fi
    fi

    # Health check failures
    local health_failures
    health_failures=$(echo "$log_data" | grep -iE "(health.*unhealthy|health check|health_status)" | head -"$TOP_N")

    local health_fail_count=0
    if [[ -n "$health_failures" ]]; then
        health_fail_count=$(echo "$health_failures" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Health Check Failures"
        if [[ -n "$health_failures" ]]; then
            echo "$health_failures" | while IFS= read -r line; do
                local ts msg
                ts=$(echo "$line" | grep -oP "^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+|^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:]+")
                msg=$(echo "$line" | sed 's/^.*dockerd[^:]*: //' | head -c 80)
                printf "   %-22s  %s\n" "${ts:-unknown}" "$msg"
            done
        else
            echo "   None found."
        fi
    fi

    # Docker warnings
    local daemon_warnings
    daemon_warnings=$(echo "$log_data" | grep -i "level=warning\|WARN" | \
        grep -oP 'msg="\K[^"]+|msg=\K\S+' | sort | uniq -c | sort -rn | head -"$TOP_N")

    if ! $JSON_MODE; then
        subsection_header "Top Warning Patterns"
        if [[ -n "$daemon_warnings" ]]; then
            printf "   ${BOLD}%-6s %s${NC}\n" "Count" "Warning"
            echo "$daemon_warnings" | awk '{
                count = $1; $1 = ""
                sub(/^ +/, "")
                printf "   %-6d %s\n", count, substr($0, 1, 70)
            }' | head -"$TOP_N"
        else
            echo "   None found."
        fi
    fi

    # Exited containers (from docker ps -a)
    local exited_containers=""
    if [[ -n "$docker_ps_data" ]]; then
        exited_containers=$(echo "$docker_ps_data" | grep -i "exited" | head -"$TOP_N" || true)
    fi

    if ! $JSON_MODE; then
        subsection_header "Exited Containers"
        if [[ -n "$exited_containers" ]]; then
            printf "   ${BOLD}%-30s  %s${NC}\n" "Container" "Status"
            echo "$exited_containers" | while IFS=$'\t' read -r name status created; do
                printf "   %-30s  %s\n" "$name" "$status"
            done
        else
            echo "   None found."
        fi
    fi

    local restart_count=0
    if [[ -n "$restart_loops" ]]; then
        restart_count=$(echo "$restart_loops" | wc -l)
    fi

    if ! $JSON_MODE; then
        section_header "Summary"
        echo ""
        printf "  %-25s %d\n" "Daemon errors:" "$daemon_error_count"
        printf "  %-25s %d\n" "OOM kills:" "$docker_oom_count"
        printf "  %-25s %d\n" "Health check failures:" "$health_fail_count"
        printf "  %-25s %d\n" "Restart loops:" "$restart_count"
        printf "  %-25s %d\n" "Log lines parsed:" "$total_lines"
    else
        json_add "docker" "{\"daemon_errors\":${daemon_error_count},\"oom_kills\":${docker_oom_count},\"health_check_failures\":${health_fail_count},\"restart_loops\":${restart_count},\"total_lines\":${total_lines}}"
    fi
}

# ============================================================================
# JAVA LOG ANALYZER
# ============================================================================

analyze_java() {
    local log_file=""

    if [[ -n "$CUSTOM_LOG" ]]; then
        log_file="$CUSTOM_LOG"
    else
        # Search common Java/Tomcat log locations
        log_file=$(find_log \
            /opt/tomcat/logs/catalina.out \
            /var/log/tomcat*/catalina.out \
            /opt/tomcat*/logs/catalina.out \
            /var/log/wildfly/server.log \
            /opt/wildfly/standalone/log/server.log \
            /var/log/jetty/jetty.log \
            /opt/sonarqube/logs/sonar.log \
            /opt/nexus/sonatype-work/nexus3/log/nexus.log \
            /opt/jenkins/log/jenkins.log \
            /var/log/jenkins/jenkins.log)
    fi

    if [[ -z "$log_file" || ! -r "$log_file" ]]; then
        log_warn "No Java log sources found — use --log to specify the path"
        return 1
    fi

    local log_data
    log_data=$(cat "$log_file" | filter_by_time)

    if [[ -z "$log_data" ]]; then
        log_warn "No log data in ${log_file} for the specified time range"
        return 1
    fi

    local total_lines
    total_lines=$(echo "$log_data" | wc -l)

    if ! $JSON_MODE; then
        section_header "Java Log Analysis"
        echo ""
        echo -e "  Source: ${DIM}${log_file}${NC}"
        echo -e "  Lines:  ${total_lines}"
    fi

    # ERROR / WARN / FATAL counts
    local error_count warn_count fatal_count
    error_count=$(echo "$log_data" | grep -cE "\bERROR\b" || true)
    warn_count=$(echo "$log_data" | grep -cE "\bWARN\b" || true)
    fatal_count=$(echo "$log_data" | grep -cE "\bFATAL\b" || true)

    if ! $JSON_MODE; then
        subsection_header "Log Level Breakdown"
        printf "   %-15s %d\n" "FATAL:" "$fatal_count"
        printf "   %-15s %d\n" "ERROR:" "$error_count"
        printf "   %-15s %d\n" "WARN:" "$warn_count"
    fi

    # Stack traces (lines starting with "at " or "Caused by:")
    local stack_traces
    stack_traces=$(echo "$log_data" | grep -cE "^\s+at |^Caused by:" || true)
    local exception_types
    exception_types=$(echo "$log_data" | grep -oP "^\S+Exception|^\S+Error|Caused by: \K\S+Exception|\S+Exception(?=:)" | \
        sort | uniq -c | sort -rn | head -"$TOP_N")

    if ! $JSON_MODE; then
        subsection_header "Exceptions / Stack Traces"
        printf "   %-25s %d\n" "Stack trace lines:" "$stack_traces"
        echo ""
        if [[ -n "$exception_types" ]]; then
            printf "   ${BOLD}%-4s %-6s %s${NC}\n" "#" "Count" "Exception"
            echo "$exception_types" | awk '{printf "   %-4d %-6d %s\n", NR, $1, $2}' | head -"$TOP_N"
        else
            echo "   No exceptions found."
        fi
    fi

    # OOM errors
    local oom_errors
    oom_errors=$(echo "$log_data" | grep -iE "(OutOfMemoryError|java.lang.OutOfMemoryError|GC overhead limit exceeded|Java heap space|Metaspace)" | head -"$TOP_N")

    local oom_count=0
    if [[ -n "$oom_errors" ]]; then
        oom_count=$(echo "$oom_errors" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "OutOfMemoryErrors"
        if [[ -n "$oom_errors" ]]; then
            echo "$oom_errors" | while IFS= read -r line; do
                local ts
                ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9]{2}:[0-9]{2}:[0-9]{2}|^[A-Z][a-z]{2} [0-9]{2}, [0-9]{4} [0-9:]+")
                local msg
                msg=$(echo "$line" | grep -oP "OutOfMemoryError.*|GC overhead limit exceeded|Java heap space|Metaspace" | head -c 70)
                printf "   %-22s  %s\n" "${ts:-unknown}" "${msg:-OOM}"
            done
            echo ""
            echo -e "   ${RED}${oom_count} OOM event(s) — check JVM heap settings${NC}"
        else
            echo -e "   ${GREEN}None found.${NC}"
        fi
    fi

    # GC pauses (from GC log lines embedded in app logs or GC-related messages)
    local gc_events
    gc_events=$(echo "$log_data" | grep -iE "(GC pause|Full GC|GC\(|Allocation Failure|G1 Evacuation Pause|CMS-concurrent|to-space exhausted)" | head -"$TOP_N")

    local gc_count=0
    if [[ -n "$gc_events" ]]; then
        gc_count=$(echo "$gc_events" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "GC Events"
        if [[ -n "$gc_events" ]]; then
            # Show Full GC vs minor GC breakdown
            local full_gc_count minor_gc_count
            full_gc_count=$(echo "$gc_events" | grep -c "Full GC" || true)
            minor_gc_count=$((gc_count - full_gc_count))
            printf "   %-25s %d\n" "Full GC:" "$full_gc_count"
            printf "   %-25s %d\n" "Minor GC:" "$minor_gc_count"
            printf "   %-25s %d\n" "Total:" "$gc_count"
        else
            echo "   None found in application log (check dedicated GC log if separate)."
        fi
    fi

    # Thread deadlocks
    local deadlocks
    deadlocks=$(echo "$log_data" | grep -iE "(deadlock|DEADLOCK|Found one Java-level deadlock)" | head -"$TOP_N")

    local deadlock_count=0
    if [[ -n "$deadlocks" ]]; then
        deadlock_count=$(echo "$deadlocks" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Thread Deadlocks"
        if [[ -n "$deadlocks" ]]; then
            echo "$deadlocks" | while IFS= read -r line; do
                local ts
                ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9]{2}:[0-9]{2}:[0-9]{2}|^[A-Z][a-z]{2} [0-9]{2}, [0-9]{4} [0-9:]+")
                printf "   %s\n" "${ts:-unknown} — deadlock detected"
            done
            echo ""
            echo -e "   ${RED}${deadlock_count} deadlock(s) found${NC}"
        else
            echo "   None found."
        fi
    fi

    # Connection/network errors
    local conn_errors
    conn_errors=$(echo "$log_data" | grep -iE "(ConnectException|SocketTimeoutException|ConnectionRefused|Connection reset|NoRouteToHostException|UnknownHostException)" | \
        grep -oP "\S*(Exception|Error)\S*" | sort | uniq -c | sort -rn | head -"$TOP_N")

    if ! $JSON_MODE; then
        subsection_header "Connection / Network Errors"
        if [[ -n "$conn_errors" ]]; then
            printf "   ${BOLD}%-6s %s${NC}\n" "Count" "Exception"
            echo "$conn_errors" | awk '{printf "   %-6d %s\n", $1, $2}' | head -"$TOP_N"
        else
            echo "   None found."
        fi
    fi

    # Top ERROR messages (deduped)
    local error_patterns
    error_patterns=$(echo "$log_data" | grep -E "\bERROR\b" | \
        sed 's/^[0-9T:.+Z -]*//' | sed 's/\[.*\] //' | \
        sed 's/[0-9]\{3,\}/#/g' | sort | uniq -c | sort -rn | head -"$TOP_N")

    if ! $JSON_MODE; then
        subsection_header "Top ERROR Patterns"
        if [[ -n "$error_patterns" ]]; then
            printf "   ${BOLD}%-6s %s${NC}\n" "Count" "Pattern"
            echo "$error_patterns" | awk '{
                count = $1; $1 = ""
                sub(/^ +/, "")
                printf "   %-6d %s\n", count, substr($0, 1, 65)
            }' | head -"$TOP_N"
        else
            echo "   None found."
        fi
    fi

    # Top WARN messages (deduped)
    local warn_patterns
    warn_patterns=$(echo "$log_data" | grep -E "\bWARN\b" | \
        sed 's/^[0-9T:.+Z -]*//' | sed 's/\[.*\] //' | \
        sed 's/[0-9]\{3,\}/#/g' | sort | uniq -c | sort -rn | head -"$TOP_N")

    if ! $JSON_MODE; then
        subsection_header "Top WARN Patterns"
        if [[ -n "$warn_patterns" ]]; then
            printf "   ${BOLD}%-6s %s${NC}\n" "Count" "Pattern"
            echo "$warn_patterns" | awk '{
                count = $1; $1 = ""
                sub(/^ +/, "")
                printf "   %-6d %s\n", count, substr($0, 1, 65)
            }' | head -"$TOP_N"
        else
            echo "   None found."
        fi
    fi

    # Unique exception count
    local unique_exceptions=0
    if [[ -n "$exception_types" ]]; then
        unique_exceptions=$(echo "$exception_types" | wc -l)
    fi

    if ! $JSON_MODE; then
        section_header "Summary"
        echo ""
        printf "  %-25s %d\n" "FATAL entries:" "$fatal_count"
        printf "  %-25s %d\n" "ERROR entries:" "$error_count"
        printf "  %-25s %d\n" "WARN entries:" "$warn_count"
        printf "  %-25s %d\n" "Stack trace lines:" "$stack_traces"
        printf "  %-25s %d\n" "Unique exceptions:" "$unique_exceptions"
        printf "  %-25s %d\n" "OOM errors:" "$oom_count"
        printf "  %-25s %d\n" "GC events:" "$gc_count"
        printf "  %-25s %d\n" "Deadlocks:" "$deadlock_count"
        printf "  %-25s %d\n" "Total lines parsed:" "$total_lines"
    else
        json_add "java" "{\"fatal\":${fatal_count},\"error\":${error_count},\"warn\":${warn_count},\"stack_trace_lines\":${stack_traces},\"unique_exceptions\":${unique_exceptions},\"oom_errors\":${oom_count},\"gc_events\":${gc_count},\"deadlocks\":${deadlock_count},\"total_lines\":${total_lines}}"
    fi
}

# ============================================================================
# GITLAB LOG ANALYZER
# ============================================================================

analyze_gitlab() {
    local gitlab_log_dir="/var/log/gitlab"

    if [[ -n "$CUSTOM_LOG" ]]; then
        gitlab_log_dir="$CUSTOM_LOG"
    fi

    if [[ ! -d "$gitlab_log_dir" ]]; then
        log_warn "GitLab log directory not found: ${gitlab_log_dir}"
        return 1
    fi

    if ! $JSON_MODE; then
        section_header "GitLab Log Analysis"
        echo ""
        echo -e "  Source: ${DIM}${gitlab_log_dir}${NC}"
    fi

    # Production JSON log (Rails)
    local prod_log="${gitlab_log_dir}/gitlab-rails/production_json.log"
    local total_requests=0 error_5xx=0 slow_requests=0 auth_failures=0

    if [[ -r "$prod_log" ]]; then
        local prod_data
        prod_data=$(cat "$prod_log" | filter_by_time)
        total_requests=$(echo "$prod_data" | wc -l)

        # 5xx errors
        local fivexx_data
        fivexx_data=$(echo "$prod_data" | awk -F'"status":' '{
            if (NF > 1) {
                split($2, a, /[,}]/)
                status = a[1] + 0
                if (status >= 500) print
            }
        }')

        error_5xx=0
        if [[ -n "$fivexx_data" ]]; then
            error_5xx=$(echo "$fivexx_data" | wc -l)
        fi

        if ! $JSON_MODE; then
            subsection_header "HTTP 5xx Errors (Rails)"
            if [[ "$error_5xx" -gt 0 ]]; then
                # Top paths with 5xx
                echo "$fivexx_data" | awk -F'"path":"' '{
                    if (NF > 1) {
                        split($2, a, "\"")
                        print a[1]
                    }
                }' | sort | uniq -c | sort -rn | head -"$TOP_N" | \
                    awk '{printf "   %-6d %s\n", $1, $2}'
            else
                echo -e "   ${GREEN}None found.${NC}"
            fi
        fi

        # Slow requests (>5s)
        local slow_data
        slow_data=$(echo "$prod_data" | awk -F'"duration_s":' '{
            if (NF > 1) {
                split($2, a, /[,}]/)
                dur = a[1] + 0
                if (dur > 5.0) print
            }
        }')

        slow_requests=0
        if [[ -n "$slow_data" ]]; then
            slow_requests=$(echo "$slow_data" | wc -l)
        fi

        if ! $JSON_MODE; then
            subsection_header "Slow Requests (>5s)"
            if [[ "$slow_requests" -gt 0 ]]; then
                echo "$slow_data" | awk -F'"' '{
                    path = ""; dur = ""
                    for (i = 1; i <= NF; i++) {
                        if ($i == "path") path = $(i+2)
                        if ($i == "duration_s") {
                            split($(i+1), d, /[:,}]/)
                            dur = d[2]
                        }
                    }
                    if (path != "") printf "   %.2fs  %s\n", dur+0, path
                }' | sort -rn | head -"$TOP_N"
            else
                echo "   None found."
            fi
        fi

        # Auth failures
        local auth_fail_data
        auth_fail_data=$(echo "$prod_data" | grep -i '"status":401\|"status":403' || true)
        auth_failures=0
        if [[ -n "$auth_fail_data" ]]; then
            auth_failures=$(echo "$auth_fail_data" | wc -l)
        fi

        if ! $JSON_MODE; then
            subsection_header "Authentication Failures (401/403)"
            if [[ "$auth_failures" -gt 0 ]]; then
                echo "$auth_fail_data" | awk -F'"' '{
                    path = ""; remote_ip = ""
                    for (i = 1; i <= NF; i++) {
                        if ($i == "path") path = $(i+2)
                        if ($i == "remote_ip") remote_ip = $(i+2)
                    }
                    if (remote_ip != "") printf "   %-20s  %s\n", remote_ip, path
                }' | sort | uniq -c | sort -rn | head -"$TOP_N" | \
                    awk '{printf "   %-6d %-20s  %s\n", $1, $2, $3}'
            else
                echo "   None found."
            fi
        fi

        # Top endpoints by request count
        if ! $JSON_MODE; then
            subsection_header "Top Endpoints by Request Count"
            echo "$prod_data" | awk -F'"path":"' '{
                if (NF > 1) {
                    split($2, a, "\"")
                    print a[1]
                }
            }' | sort | uniq -c | sort -rn | head -"$TOP_N" | \
                awk '{printf "   %-6d %s\n", $1, $2}'
        fi
    else
        if ! $JSON_MODE; then
            log_warn "GitLab production log not found: ${prod_log}"
        fi
    fi

    # Sidekiq failures
    local sidekiq_log="${gitlab_log_dir}/sidekiq/current"
    local sidekiq_failures=0

    if [[ -r "$sidekiq_log" ]]; then
        local sidekiq_data
        sidekiq_data=$(cat "$sidekiq_log" | filter_by_time)

        local sidekiq_fail_data
        sidekiq_fail_data=$(echo "$sidekiq_data" | grep -i '"job_status":"fail"\|"severity":"ERROR"' || true)

        if [[ -n "$sidekiq_fail_data" ]]; then
            sidekiq_failures=$(echo "$sidekiq_fail_data" | wc -l)
        fi

        if ! $JSON_MODE; then
            subsection_header "Sidekiq Failed Jobs"
            if [[ "$sidekiq_failures" -gt 0 ]]; then
                echo "$sidekiq_fail_data" | awk -F'"' '{
                    job_class = ""; error = ""
                    for (i = 1; i <= NF; i++) {
                        if ($i == "class") job_class = $(i+2)
                        if ($i == "error_class") error = $(i+2)
                    }
                    if (job_class != "") printf "   %s — %s\n", job_class, error
                }' | sort | uniq -c | sort -rn | head -"$TOP_N" | \
                    awk '{
                        count = $1; $1 = ""
                        sub(/^ +/, "")
                        printf "   %-6d %s\n", count, $0
                    }'
            else
                echo "   None found."
            fi
        fi
    fi

    # Gitaly errors
    local gitaly_log="${gitlab_log_dir}/gitaly/current"
    local gitaly_errors=0

    if [[ -r "$gitaly_log" ]]; then
        local gitaly_data
        gitaly_data=$(cat "$gitaly_log" | filter_by_time)

        local gitaly_error_data
        gitaly_error_data=$(echo "$gitaly_data" | grep -iE '"level":"error"|"level":"fatal"' || true)

        if [[ -n "$gitaly_error_data" ]]; then
            gitaly_errors=$(echo "$gitaly_error_data" | wc -l)
        fi

        if ! $JSON_MODE; then
            subsection_header "Gitaly Errors"
            if [[ "$gitaly_errors" -gt 0 ]]; then
                echo "$gitaly_error_data" | awk -F'"' '{
                    msg = ""
                    for (i = 1; i <= NF; i++) {
                        if ($i == "msg" || $i == "error") {
                            msg = $(i+2)
                            break
                        }
                    }
                    if (msg != "") print msg
                }' | sort | uniq -c | sort -rn | head -"$TOP_N" | \
                    awk '{
                        count = $1; $1 = ""
                        sub(/^ +/, "")
                        printf "   %-6d %s\n", count, substr($0, 1, 70)
                    }'
            else
                echo "   None found."
            fi
        fi
    fi

    # Error rate
    local error_rate="0.0"
    if [[ "$total_requests" -gt 0 ]]; then
        error_rate=$(awk "BEGIN {printf \"%.2f\", ($error_5xx / $total_requests) * 100}")
    fi

    if ! $JSON_MODE; then
        section_header "Summary"
        echo ""
        printf "  %-25s %d\n" "Total requests:" "$total_requests"
        printf "  %-25s %d (%.2f%%)\n" "5xx errors:" "$error_5xx" "$error_rate"
        printf "  %-25s %d\n" "Slow requests (>5s):" "$slow_requests"
        printf "  %-25s %d\n" "Auth failures:" "$auth_failures"
        printf "  %-25s %d\n" "Sidekiq failures:" "$sidekiq_failures"
        printf "  %-25s %d\n" "Gitaly errors:" "$gitaly_errors"
    else
        json_add "gitlab" "{\"total_requests\":${total_requests},\"5xx_errors\":${error_5xx},\"error_rate\":${error_rate},\"slow_requests\":${slow_requests},\"auth_failures\":${auth_failures},\"sidekiq_failures\":${sidekiq_failures},\"gitaly_errors\":${gitaly_errors}}"
    fi
}

# ============================================================================
# POSTGRESQL LOG ANALYZER
# ============================================================================

analyze_postgresql() {
    local log_file=""

    if [[ -n "$CUSTOM_LOG" ]]; then
        log_file="$CUSTOM_LOG"
    else
        # Search common PostgreSQL log locations
        log_file=$(find_log \
            /var/log/postgresql/postgresql-*-main.log \
            /var/log/postgresql/postgresql.log \
            /var/lib/pgsql/data/log/postgresql-*.log \
            /var/lib/pgsql/data/pg_log/postgresql-*.log \
            /var/log/pgsql/postgresql.log)
    fi

    local log_data=""

    if [[ -n "$log_file" && -r "$log_file" ]]; then
        log_data=$(cat "$log_file" | filter_by_time)
    elif command -v journalctl &>/dev/null; then
        local since_arg
        since_arg=$(journalctl_since_arg)
        if [[ -n "$since_arg" ]]; then
            log_data=$(journalctl -u postgresql* --no-pager -q $since_arg 2>/dev/null)
        else
            log_data=$(journalctl -u postgresql* --no-pager -q 2>/dev/null)
        fi
    fi

    if [[ -z "$log_data" ]]; then
        log_warn "No PostgreSQL log sources found"
        return 1
    fi

    local total_lines
    total_lines=$(echo "$log_data" | wc -l)

    if ! $JSON_MODE; then
        section_header "PostgreSQL Log Analysis"
        echo ""
        echo -e "  Source: ${DIM}${log_file:-journalctl}${NC}"
        echo -e "  Lines:  ${total_lines}"
    fi

    # FATAL/ERROR/PANIC counts
    local fatal_count error_count panic_count
    fatal_count=$(echo "$log_data" | grep -c " FATAL: " || true)
    error_count=$(echo "$log_data" | grep -c " ERROR: " || true)
    panic_count=$(echo "$log_data" | grep -c " PANIC: " || true)

    if ! $JSON_MODE; then
        subsection_header "Severity Breakdown"
        printf "   %-15s %d\n" "PANIC:" "$panic_count"
        printf "   %-15s %d\n" "FATAL:" "$fatal_count"
        printf "   %-15s %d\n" "ERROR:" "$error_count"
    fi

    # Connection errors
    local conn_errors
    conn_errors=$(echo "$log_data" | grep -iE "(too many connections|connection refused|remaining connection slots|sorry, too many clients)" | head -"$TOP_N")

    local conn_error_count=0
    if [[ -n "$conn_errors" ]]; then
        conn_error_count=$(echo "$conn_errors" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Connection Errors"
        if [[ -n "$conn_errors" ]]; then
            echo "$conn_errors" | while IFS= read -r line; do
                local ts msg
                ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]+|^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+")
                msg=$(echo "$line" | grep -oP "(FATAL|ERROR): \K.*" | head -c 70)
                printf "   %-22s  %s\n" "${ts:-unknown}" "${msg:-$line}"
            done
        else
            echo -e "   ${GREEN}None found.${NC}"
        fi
    fi

    # Deadlocks
    local deadlocks
    deadlocks=$(echo "$log_data" | grep -i "deadlock detected" | head -"$TOP_N")

    local deadlock_count=0
    if [[ -n "$deadlocks" ]]; then
        deadlock_count=$(echo "$deadlocks" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Deadlocks"
        if [[ -n "$deadlocks" ]]; then
            echo "$deadlocks" | while IFS= read -r line; do
                local ts
                ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]+|^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+")
                printf "   %s\n" "${ts:-unknown}"
            done
            echo ""
            echo "   Total: ${deadlock_count}"
        else
            echo "   None found."
        fi
    fi

    # Slow queries (log_min_duration_statement)
    local slow_queries
    slow_queries=$(echo "$log_data" | grep -i "duration:" | grep -v "LOG:  connection\|LOG:  disconnection" | head -"$TOP_N")

    local slow_query_count=0
    if [[ -n "$slow_queries" ]]; then
        slow_query_count=$(echo "$slow_queries" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Slow Queries"
        if [[ "$slow_query_count" -gt 0 ]]; then
            echo "$slow_queries" | while IFS= read -r line; do
                local ts dur stmt
                ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]+|^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+")
                dur=$(echo "$line" | grep -oP "duration: \K[0-9.]+ ms")
                stmt=$(echo "$line" | grep -oP "statement: \K.*" | head -c 60)
                printf "   %-22s  %-12s  %s\n" "${ts:-unknown}" "${dur:-?}" "${stmt:-...}"
            done
        else
            echo "   None found (requires log_min_duration_statement in postgresql.conf)."
        fi
    fi

    # Checkpoint warnings
    local checkpoint_warns
    checkpoint_warns=$(echo "$log_data" | grep -iE "(checkpoint.*too frequent|checkpoints are occurring too frequently|checkpoint.*taking)" | head -"$TOP_N")

    local checkpoint_count=0
    if [[ -n "$checkpoint_warns" ]]; then
        checkpoint_count=$(echo "$checkpoint_warns" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Checkpoint Warnings"
        if [[ -n "$checkpoint_warns" ]]; then
            echo "$checkpoint_warns" | while IFS= read -r line; do
                local ts msg
                ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]+|^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+")
                msg=$(echo "$line" | grep -oP "LOG: \K.*|WARNING: \K.*" | head -c 70)
                printf "   %-22s  %s\n" "${ts:-unknown}" "${msg:-checkpoint warning}"
            done
        else
            echo "   None found."
        fi
    fi

    # WAL/replication errors
    local wal_errors
    wal_errors=$(echo "$log_data" | grep -iE "(WAL.*error|replication.*error|could not receive|streaming replication|recovery target)" | head -"$TOP_N")

    local wal_error_count=0
    if [[ -n "$wal_errors" ]]; then
        wal_error_count=$(echo "$wal_errors" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "WAL / Replication Errors"
        if [[ -n "$wal_errors" ]]; then
            echo "$wal_errors" | while IFS= read -r line; do
                local ts msg
                ts=$(echo "$line" | grep -oP "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]+|^[A-Z][a-z]{2} [ 0-9]{2} [0-9:]+")
                msg=$(echo "$line" | grep -oP "(FATAL|ERROR|LOG): \K.*" | head -c 70)
                printf "   %-22s  %s\n" "${ts:-unknown}" "${msg:-$line}"
            done
        else
            echo "   None found."
        fi
    fi

    # Auth failures
    local pg_auth_failures
    pg_auth_failures=$(echo "$log_data" | grep -iE "(password authentication failed|no pg_hba.conf entry|authentication failed)" | head -"$TOP_N")

    local pg_auth_fail_count=0
    if [[ -n "$pg_auth_failures" ]]; then
        pg_auth_fail_count=$(echo "$pg_auth_failures" | wc -l)
    fi

    if ! $JSON_MODE; then
        subsection_header "Authentication Failures"
        if [[ -n "$pg_auth_failures" ]]; then
            echo "$pg_auth_failures" | awk -F'"' '{
                # Try to extract user and database
                line = $0
            }
            {
                match($0, /user "([^"]+)"/, u)
                match($0, /database "([^"]+)"/, d)
                user = (u[1] != "") ? u[1] : "?"
                db = (d[1] != "") ? d[1] : "?"
                printf "   user=%-15s db=%-15s\n", user, db
            }' | sort | uniq -c | sort -rn | head -"$TOP_N" | \
                awk '{printf "   %-6d %s %s\n", $1, $2, $3}'
        else
            echo "   None found."
        fi
    fi

    # Top error patterns
    local pg_error_patterns
    pg_error_patterns=$(echo "$log_data" | grep -E " (ERROR|FATAL): " | \
        sed 's/^[^:]*: //' | sed 's/[0-9]\{2,\}/#/g' | \
        sort | uniq -c | sort -rn | head -"$TOP_N")

    if ! $JSON_MODE; then
        subsection_header "Top Error Patterns"
        if [[ -n "$pg_error_patterns" ]]; then
            printf "   ${BOLD}%-6s %s${NC}\n" "Count" "Pattern"
            echo "$pg_error_patterns" | awk '{
                count = $1; $1 = ""
                sub(/^ +/, "")
                printf "   %-6d %s\n", count, substr($0, 1, 65)
            }' | head -"$TOP_N"
        else
            echo "   None found."
        fi
    fi

    if ! $JSON_MODE; then
        section_header "Summary"
        echo ""
        printf "  %-25s %d\n" "PANIC entries:" "$panic_count"
        printf "  %-25s %d\n" "FATAL entries:" "$fatal_count"
        printf "  %-25s %d\n" "ERROR entries:" "$error_count"
        printf "  %-25s %d\n" "Connection errors:" "$conn_error_count"
        printf "  %-25s %d\n" "Deadlocks:" "$deadlock_count"
        printf "  %-25s %d\n" "Slow queries:" "$slow_query_count"
        printf "  %-25s %d\n" "Checkpoint warnings:" "$checkpoint_count"
        printf "  %-25s %d\n" "WAL/replication errors:" "$wal_error_count"
        printf "  %-25s %d\n" "Auth failures:" "$pg_auth_fail_count"
        printf "  %-25s %d\n" "Total lines parsed:" "$total_lines"
    else
        json_add "postgresql" "{\"panic\":${panic_count},\"fatal\":${fatal_count},\"error\":${error_count},\"connection_errors\":${conn_error_count},\"deadlocks\":${deadlock_count},\"slow_queries\":${slow_query_count},\"checkpoint_warnings\":${checkpoint_count},\"wal_errors\":${wal_error_count},\"auth_failures\":${pg_auth_fail_count},\"total_lines\":${total_lines}}"
    fi
}

# ============================================================================
# ARGUMENT PARSING
# ============================================================================

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -h|--help) show_usage ;;
            --type) LOG_TYPE="$2"; shift 2 ;;
            --all-types) ALL_TYPES=true; shift ;;
            --since) SINCE="$2"; parse_since "$2"; shift 2 ;;
            --top) TOP_N="$2"; shift 2 ;;
            --json) JSON_MODE=true; shift ;;
            --no-color) NO_COLOR=true; shift ;;
            --output) OUTPUT_FILE="$2"; shift 2 ;;
            --log) CUSTOM_LOG="$2"; shift 2 ;;
            *) log_error "Unknown option: $1"; show_usage ;;
        esac
    done

    if [[ -z "$LOG_TYPE" ]] && ! $ALL_TYPES; then
        log_error "Specify --type or --all-types"
        echo ""
        show_usage
    fi
}

# ============================================================================
# MAIN
# ============================================================================

main() {
    parse_args "$@"

    # Disable colors if requested or not a terminal
    if $NO_COLOR || [[ ! -t 1 ]]; then
        disable_colors
    fi

    # Redirect output to file if specified
    if [[ -n "$OUTPUT_FILE" ]]; then
        disable_colors
        exec > >(tee "$OUTPUT_FILE") 2>&1
    fi

    if ! $JSON_MODE; then
        echo -e "${BOLD}Linux Log Analyzer v${VERSION}${NC}"
        if [[ -n "$SINCE" ]]; then
            echo -e "Since: ${SINCE}"
        fi
    fi

    if $ALL_TYPES; then
        # Run all available analyzers
        local ran_any=false

        for type in system auth docker java gitlab postgresql; do
            if ! $JSON_MODE; then
                echo ""
                echo -e "${DIM}────────────────────────────────────────────────────${NC}"
                echo -e "${BOLD}  Analyzing: ${type}${NC}"
                echo -e "${DIM}────────────────────────────────────────────────────${NC}"
            fi

            case "$type" in
                system)     analyze_system && ran_any=true || log_warn "system: skipped" ;;
                auth)       analyze_auth && ran_any=true || log_warn "auth: skipped" ;;
                docker)     analyze_docker && ran_any=true || log_warn "docker: skipped" ;;
                java)       analyze_java && ran_any=true || log_warn "java: skipped" ;;
                gitlab)     analyze_gitlab && ran_any=true || log_warn "gitlab: skipped" ;;
                postgresql) analyze_postgresql && ran_any=true || log_warn "postgresql: skipped" ;;
            esac
        done

        if ! $ran_any; then
            log_error "No log sources found for any type"
            exit 1
        fi
    else
        case "$LOG_TYPE" in
            system)     analyze_system ;;
            auth)       analyze_auth ;;
            docker)     analyze_docker ;;
            java)       analyze_java ;;
            gitlab)     analyze_gitlab ;;
            postgresql) analyze_postgresql ;;
            *)          log_error "Unknown log type: $LOG_TYPE"; show_usage ;;
        esac
    fi

    if $JSON_MODE; then
        echo "{${JSON_OUTPUT}}"
    fi
}

main "$@"
