#!/usr/bin/env bash
# =============================================================================
# onx-log-tail — Whitelisted log file tail/grep/since reader.
#
# Input (stdin JSON):
#   {
#     "path":   "/var/log/httpd/error_log",
#     "lines":  200,        // 50..5000
#     "filter": "AH02032",  // optional grep pattern (case-insensitive)
#     "since":  "5m"        // optional: 5m / 1h / 1d / iso8601
#   }
#
# Output (stdout JSON):
#   {
#     "path":  "...",
#     "lines": [{"time":"YYYY-MM-DD HH:MM:SS","level":"error","message":"..."}],
#     "count": 42
#   }
#
# Exit codes:
#   0  — success
#   1  — invalid input (bad path / lines out of range)
#   2  — preflight fail (path not readable)
#   3  — execution fail
#
# Security:
#   • Path must match the ALLOWED_PATHS whitelist (glob patterns ok).
#   • No shell injection — we never pass user input to eval-ish constructs.
#   • Apache user runs as non-root; system logs may need ACL or sudoers.
#
# Sudoers:
#   apache ALL=(root) NOPASSWD: /usr/local/onoxsoft/bin/onx-log-tail
#
# Deployed to: /usr/local/onoxsoft/bin/onx-log-tail
# =============================================================================

set -euo pipefail

SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=_lib/common.sh
source "${SCRIPT_DIR}/_lib/common.sh"

onx_json_input

PATH_ARG=$(onx_json_field 'path' '')
LINES=$(onx_json_field 'lines' '200')
FILTER=$(onx_json_field 'filter' '')
SINCE=$(onx_json_field 'since' '')

[[ -n "$PATH_ARG" ]] || onx_die 1 "path is required"

# ── Path whitelist (glob match; bash extglob) ────────────────────────────────
ALLOWED_PATHS=(
    "/var/log/httpd/access_log"
    "/var/log/httpd/error_log"
    "/var/log/httpd/*-access_log"
    "/var/log/httpd/*-error_log"
    # v3.66: multi-webserver log paths (nginx/OLS/caddy aktifken okunabilsin)
    "/var/log/nginx/access.log"
    "/var/log/nginx/error.log"
    "/var/log/nginx/*.log"
    "/usr/local/lsws/logs/access.log"
    "/usr/local/lsws/logs/error.log"
    "/usr/local/lsws/logs/*.log"
    "/var/log/caddy/access.log"
    "/var/log/caddy/error.log"
    "/var/log/caddy/*.log"
    "/var/log/php-fpm/error.log"
    "/var/log/php-fpm/*.log"
    "/var/log/mysql/error.log"
    "/var/log/mariadb/mariadb.log"
    "/var/log/mariadb/slow-queries.log"
    # v3.68: MySQL (MariaDB değil) + datadir slow log yolları — DB engine
    # ne olursa olsun SHOW VARIABLES'tan gelen path okunabilsin
    "/var/log/mariadb/*.log"
    "/var/log/mysqld.log"
    "/var/log/mysql/*.log"
    "/var/lib/mysql/*.log"
    "/var/lib/mysql/*-slow.log"
    "/var/log/maillog"
    "/var/log/exim/main.log"
    "/var/log/secure"
    "/var/log/messages"
    "/var/log/fail2ban.log"
    "/var/log/pure-ftpd/pure-ftpd.log"
    "/var/log/pureftpd.log"
    "/var/log/onox/sysapi.log"
    "/var/log/onox/*.log"
    "/home/*/logs/*-access.log"
    "/home/*/logs/*-error.log"
    # D5-26: per-account PHP error logları (PhpErrorLogController okur/temizler) — eski
    # whitelist php_errors.log'u reddediyordu (*-error.log deseni eşleşmiyor).
    "/home/*/logs/php_errors.log"
    "/home/*/logs/*.php_errors.log"
    # v3.66: panel Laravel log — DOĞRU path'ler (eski /var/www/laravel YANLIŞ idi)
    "/opt/onoxsoft/storage/logs/laravel.log"
    "/opt/onoxsoft/storage/logs/*.log"
    "/var/www/onoxsoft/storage/logs/laravel.log"
    "/var/www/onoxsoft/storage/logs/*.log"
)

allowed=false
for pattern in "${ALLOWED_PATHS[@]}"; do
    # shellcheck disable=SC2053
    if [[ "$PATH_ARG" == $pattern ]]; then
        allowed=true
        break
    fi
done

[[ "$allowed" == "true" ]] || onx_die 1 "path_not_allowed: $PATH_ARG"

# Validate lines (50..5000)
[[ "$LINES" =~ ^[0-9]+$ ]] || onx_die 1 "lines must be integer"
(( LINES >= 50 && LINES <= 5000 )) || onx_die 1 "lines out of range (50..5000)"

# Reject path-traversal sneakiness
[[ "$PATH_ARG" == *".."* ]] && onx_die 1 "path traversal not allowed"

# ── Reject filter regex chars that would break a literal grep -F ─────────────
# Force literal grep so users cannot run arbitrary regex (DoS / catastrophic
# backtracking). Filter is also length-capped.
if [[ -n "$FILTER" ]]; then
    if (( ${#FILTER} > 200 )); then
        onx_die 1 "filter too long"
    fi
fi

# Translate "since" → epoch seconds for awk timestamp filter
SINCE_EPOCH=0
if [[ -n "$SINCE" ]]; then
    case "$SINCE" in
        5m|5min)  SINCE_EPOCH=$(date -d '-5 minutes' +%s 2>/dev/null || echo 0) ;;
        15m)      SINCE_EPOCH=$(date -d '-15 minutes' +%s 2>/dev/null || echo 0) ;;
        1h)       SINCE_EPOCH=$(date -d '-1 hour' +%s 2>/dev/null || echo 0) ;;
        24h|1d)   SINCE_EPOCH=$(date -d '-1 day' +%s 2>/dev/null || echo 0) ;;
        *)        SINCE_EPOCH=$(date -d "$SINCE" +%s 2>/dev/null || echo 0) ;;
    esac
fi

# Existence + readability check (preflight; soft-fail if not yet rotated)
if [[ ! -r "$PATH_ARG" ]]; then
    if [[ -f "$PATH_ARG" ]]; then
        onx_die 2 "log file not readable (permissions)"
    fi
    # Empty result for missing files (rotated or never created yet)
    printf '{"path":%s,"lines":[],"count":0}\n' "$(printf '%s' "$PATH_ARG" | jq -Rs '.')"
    exit 0
fi

require_cmd jq
require_cmd tail

# ── Read tail with optional filter & timestamp gate ──────────────────────────
read_log() {
    if [[ -n "$FILTER" ]]; then
        tail -n "$LINES" "$PATH_ARG" 2>/dev/null | grep -F -i -- "$FILTER" || true
    else
        tail -n "$LINES" "$PATH_ARG" 2>/dev/null || true
    fi
}

# Level classifier — best-effort regex over each raw line. Buckets unknown to
# "info" so the UI always has something to colorize.
classify_level() {
    local line="$1"
    local lower
    lower="${line,,}"
    case "$lower" in
        *" emerg"*|*"emergency"*) printf 'crit' ;;
        *" alert"*) printf 'crit' ;;
        *" crit"*|*"critical"*) printf 'crit' ;;
        *" error"*|*" err "*|*"[error]"*|*"[err]"*|*"php fatal error"*|*"php parse error"*) printf 'error' ;;
        *" warn"*|*"warning"*|*"[warn]"*|*"php notice"*) printf 'warn' ;;
        *" notice"*|*"[notice]"*) printf 'notice' ;;
        *" debug"*|*"[debug]"*) printf 'debug' ;;
        *) printf 'info' ;;
    esac
}

# Extract iso/syslog timestamp (best-effort)
extract_ts() {
    local line="$1"
    # Apache combined: [Tue Sep 24 10:11:12.123456 2024]
    if [[ "$line" =~ \[([A-Z][a-z]{2}\ [A-Z][a-z]{2}\ +[0-9]{1,2}\ [0-9:]+(\.[0-9]+)?\ [0-9]{4})\] ]]; then
        printf '%s' "${BASH_REMATCH[1]}"
        return
    fi
    # syslog / maillog: Sep 24 10:11:12
    if [[ "$line" =~ ^([A-Z][a-z]{2}\ +[0-9]{1,2}\ [0-9:]{8}) ]]; then
        printf '%s' "${BASH_REMATCH[1]}"
        return
    fi
    # Laravel ISO: [2024-09-24 10:11:12]
    if [[ "$line" =~ \[([0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9:]+)\] ]]; then
        printf '%s' "${BASH_REMATCH[1]}"
        return
    fi
    printf ''
}

# Build JSON array of parsed entries.
#
# PERF NOT: Eski sürüm her satır için ayrı jq fork ediyordu (5000 satır = 5000
# process = ~24 saniye). Yeni yaklaşım: tüm satırları stdin üzerinden TEK jq
# çağrısına pipe et + jq'da split + parsing yap. 5000 satır <1 saniyede biter.
#
# Timestamp/level extraction Sieve yerine awk ile per-line yapılır, ardından
# jq satır objesini kurar. Eski extract_ts/classify_level bash fonksiyonları
# kept (kullanılmıyor) — backwards-compat referans için.

TMP_RAW=$(mktemp)
trap 'rm -f "$TMP_RAW"' EXIT
read_log > "$TMP_RAW"

# SINCE filter (varsa) — awk ile prune
if (( SINCE_EPOCH > 0 )); then
    TMP_FILTERED=$(mktemp)
    awk -v since="$SINCE_EPOCH" '
    {
        # Çok satırlık timestamp parse — Apache, syslog, Laravel format hepsi
        ts = ""
        if (match($0, /\[([A-Z][a-z]{2} [A-Z][a-z]{2}[ ]+[0-9]+ [0-9:]+(\.[0-9]+)? [0-9]{4})\]/, m)) {
            ts = m[1]
        } else if (match($0, /^([A-Z][a-z]{2} [ 0-9]?[0-9] [0-9:]{8})/, m)) {
            ts = m[1] " " strftime("%Y")
        } else if (match($0, /\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]+)\]/, m)) {
            ts = m[1]
        }
        if (ts == "") { print; next }  # ts parse edemediysek geçir
        cmd = "date -d \"" ts "\" +%s 2>/dev/null"
        cmd | getline line_epoch; close(cmd)
        if (line_epoch + 0 >= since) print
    }
    ' "$TMP_RAW" > "$TMP_FILTERED"
    mv "$TMP_FILTERED" "$TMP_RAW"
fi

LINE_COUNT=$(wc -l < "$TMP_RAW" | tr -d ' ')

if [[ "$LINE_COUNT" -eq 0 ]]; then
    jq -nc --arg path "$PATH_ARG" '{path: $path, lines: [], count: 0}'
    exit 0
fi

# Tek jq çağrısı — her satırı raw input olarak alıp obje haline getir.
# -R: raw string, -s: slurp tüm input, -c: compact output.
jq -Rsc --arg path "$PATH_ARG" '
    split("\n")
    | map(select(length > 0))
    | map({
        time: (
            # Apache combined: [Tue Sep 24 10:11:12.123456 2024]
            (capture("\\[(?<t>[A-Z][a-z]{2} [A-Z][a-z]{2}[ ]+[0-9]+ [0-9:]+(\\.[0-9]+)? [0-9]{4})\\]"; "g")? // null)
            // (capture("^(?<t>[A-Z][a-z]{2}[ ]+[0-9]+ [0-9:]{8})")? // null)
            // (capture("\\[(?<t>[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]+)\\]")? // null)
            // {t: ""}
            | .t
        ),
        level: (
            # v3.67: Level token DELIMITER-anchored (start/space/colon/dot/bracket
            # ile sinirli) tespit edilir. Eski surum bare "fail"/"reject" kullaniyordu
            # -> "fail2ban" icindeki "fail" her INFO satirini "error" (kirmizi)
            # gosteriyordu. Char-class [][ :.] = start/bracket/space/colon/dot.
            # fail2ban "]: INFO", Laravel "production.ERROR:", Apache "[error]",
            # nginx "[warn]", PHP-FPM "WARNING:" hepsi dogru siniflanir.
            # (Bu yorum jq tek-tirnak string icinde -> apostrof KULLANMA!)
            ascii_downcase as $l
            | if ($l | test("(^|[][ :.])(emerg|emergency|alert|crit|critical)([][ :.]|$)")) then "crit"
              elif ($l | test("(^|[][ :.])(err|error)([][ :.]|$)|php fatal error|php parse error|fatal error|segfault|uncaught exception")) then "error"
              elif ($l | test("(^|[][ :.])(warn|warning)([][ :.]|$)|php warning|deprecated")) then "warn"
              elif ($l | test("(^|[][ :.])notice([][ :.]|$)|php notice")) then "notice"
              elif ($l | test("(^|[][ :.])debug([][ :.]|$)")) then "debug"
              else "info" end
        ),
        message: .
    })
    | {path: $path, lines: ., count: length}
' "$TMP_RAW"

exit 0
