#!/bin/bash
# =============================================================================
# runtime-logs.sh -- Log Manager & Error Scanner
# =============================================================================
# Category: runtime | Bird: cormorant | Tier: 3 | Node-aware: yes
# Absorbs: logs.sh, runtime-error.sh, error-scan.sh, deploy-error-scan.sh,
#          php-error-watch.sh, daily-digest.sh, alert.sh, deploy_alerts.sh,
#          ssh-login-alert.sh, runtime-alert.sh, runtime-cron.sh
#
# Actions:
#   --status     JSON pill (error/warning counts)
#   --errors     Full error scan across all log sources
#   --tail       Tail recent log activity
#   --scan       Quick scan for critical patterns
#   --digest     Human-readable error report
#   --alerts     Recent alerts summary
#   --cron       Cron job status
#   --send       Full scan + cache results
#   --flush      Clear scan cache + truncate logs
# =============================================================================

TOOL_NAME="runtime-logs"
TOOL_VERSION="3.0.0"
TOOL_TIER=3
TOOL_BIRD="cormorant"
TOOL_LOG="/opt/binary/logs/tools/${TOOL_NAME}.log"
TOOL_CONFIG="/etc/binary/${TOOL_NAME}.conf"

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [[ -f "${SCRIPT_DIR}/common.sh" ]]; then source "${SCRIPT_DIR}/common.sh"
elif [[ -f "${SCRIPT_DIR}/../common.sh" ]]; then source "${SCRIPT_DIR}/../common.sh"
else echo "FATAL: common.sh not found" >&2; exit 1; fi

export BIRD_ID="${TOOL_BIRD}"

[ -f "$TOOL_CONFIG" ] && source "$TOOL_CONFIG" 2>/dev/null

CACHE_FILE="${DATA_DIR}/error_scan_cache.json"

# --errors (full Python-based error scanner from runtime-error.sh)
_do_errors_impl() {
    local category="${1:-}"
    command -v python3 >/dev/null 2>&1 || { log_error "python3 required"; return 1; }

    python3 - "${category}" <<'PY'
import collections, datetime as _dt, glob, json, os, re, shutil, socket, subprocess, sys

category_filter = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1] else ""
node_id = os.environ.get("NODE_ID") or socket.gethostname().split(".")[0] or "unknown"
CATEGORY_DEFS = {
    "websites": {"label":"Websites","keywords":["nginx","php-fpm","php-error","caddy","hosting"]},
    "server":   {"label":"Server","keywords":["s3rv1c3agent","cron","disk","health","watchdog","runtime","sshd","secure","messages"]},
    "services": {"label":"Services","keywords":["services","wireguard","dns","email","firewall","fail2ban","intrusion","anomaly","canary","crowdsec"]},
    "vault":    {"label":"Vault","keywords":["database","backup","restore","container","podman","docker","audit"]},
    "payments": {"label":"Payments","keywords":["payment","wallet","mining","billing","xmrig"]},
}
ERROR_RE = re.compile(r"\b(ERROR|FAIL|FAILED|CRITICAL|DENIED|REFUSED|FATAL|PANIC|SEGFAULT|OOM)\b", re.I)
WARN_RE = re.compile(r"\b(WARN|WARNING|TIMEOUT|ANOMAL|ISSUE|DEGRADED|RETRY)\b", re.I)
NOISE_RE = re.compile(r"issues=0|fail=0|failures=0|result=ok|status=ok", re.I)
TAIL_LINES = int(os.environ.get("ERROR_SCAN_TAIL_LINES", "700"))
MAX_FINDINGS = int(os.environ.get("ERROR_SCAN_MAX_FINDINGS", "80"))

def utc_now(): return _dt.datetime.now(_dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00","Z")
def read_tail(path):
    buf = collections.deque(maxlen=TAIL_LINES)
    try:
        with open(path, "r", encoding="utf-8", errors="replace") as fh:
            for line in fh: buf.append(line.rstrip("\n"))
    except (OSError, UnicodeError): return []
    return list(buf)

patterns = ["/var/log/nginx/error.log","/var/log/nginx/*_error.log","/var/log/php-fpm/error.log",
    "/var/log/secure","/var/log/messages","/opt/binary/logs/tools/*.log","/opt/binary/logs/audit/audit.log"]
sources = []
seen = set()
for p in patterns:
    for path in glob.glob(p):
        if path in seen or not os.path.isfile(path): continue
        seen.add(path)
        sources.append((path, read_tail(path)))

cats = {k: {"label":d["label"],"errors":0,"warnings":0,"total_lines":0,"status":"none","findings":[]} for k,d in CATEGORY_DEFS.items()}
for source, lines in sources:
    for line in lines:
        line = line.strip()
        if not line: continue
        haystack = f"{os.path.basename(source)} {line}".lower()
        matched = [k for k,d in CATEGORY_DEFS.items() if any(kw in haystack for kw in d["keywords"])]
        if not matched:
            sl = source.lower()
            if "/var/log/secure" in sl or "/var/log/messages" in sl: matched = ["server"]
            elif "/var/log/nginx" in sl: matched = ["websites"]
            elif "/opt/binary/logs/tools" in sl: matched = ["server"]
        if category_filter: matched = [c for c in matched if c == category_filter]
        if not matched: continue
        if NOISE_RE.search(line): continue
        is_err = bool(ERROR_RE.search(line))
        is_warn = bool(WARN_RE.search(line))
        for k in matched:
            cats[k]["total_lines"] += 1
            if is_err: cats[k]["errors"] += 1
            elif is_warn: cats[k]["warnings"] += 1
            if (is_err or is_warn) and len(cats[k]["findings"]) < MAX_FINDINGS:
                cats[k]["findings"].append({"source":source,"severity":"error" if is_err else "warning","message":line[:500]})

te = sum(c["errors"] for c in cats.values())
tw = sum(c["warnings"] for c in cats.values())
for c in cats.values():
    c["status"] = "error" if c["errors"]>0 else ("warning" if c["warnings"]>0 else ("pass" if c["total_lines"]>0 else "none"))
status = "error" if te>0 else ("warning" if tw>0 else "pass")
print(json.dumps({"ok":True,"tool":"runtime-logs","node":node_id,"timestamp":utc_now(),"status":status,
    "total_errors":te,"total_warnings":tw,"total_log_files":len(sources),"categories":cats}, ensure_ascii=False, separators=(",",":")))
PY
}

# --status
_do_status_impl() {
    export TOOL_JSON=1
    local result; result=$(_do_errors_impl 2>/dev/null)
    if [ -n "$result" ]; then
        echo "$result" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(json.dumps({'tool':'runtime-logs','node':d['node'],'status':d['status'],'total_errors':d['total_errors'],'total_warnings':d['total_warnings'],'checked_at':d['timestamp']}))" 2>/dev/null
    else
        echo "{\"tool\":\"${TOOL_NAME}\",\"node\":\"${NODE_ID}\",\"status\":\"unknown\",\"checked_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
    fi
}

# --tail
_do_tail_impl() {
    local target="${1:-/opt/binary/logs/tools}"
    local lines="${2:-50}"
    if [ -d "$target" ]; then
        find "$target" -name "*.log" -newer /proc -type f 2>/dev/null | head -5 | while read -r f; do
            echo "--- ${f} ---"
            tail -n "$lines" "$f" 2>/dev/null
            echo ""
        done
    elif [ -f "$target" ]; then
        tail -n "$lines" "$target"
    fi
}

# --scan (quick pattern scan)
_do_scan_impl() {
    echo "=== Quick Scan: ${NODE_ID} ==="
    local count=0
    for log in /var/log/nginx/error.log /var/log/secure /var/log/messages /opt/binary/logs/tools/*.log; do
        [ -f "$log" ] || continue
        local hits; hits=$(tail -100 "$log" 2>/dev/null | grep -ciE '(error|fail|critical|denied|panic)' || echo 0)
        if [ "$hits" -gt 0 ]; then
            echo "  [WARN] ${log}: ${hits} issue(s)"
            count=$((count + hits))
        fi
    done
    [ "$count" -eq 0 ] && echo "  [OK] No critical patterns found"
    echo "  Total: ${count} issue(s)"
}

# --alerts
_do_alerts_impl() {
    echo "=== Recent Alerts: ${NODE_ID} ==="
    if [ -d "${NOTIFY_DIR}" ]; then
        find "${NOTIFY_DIR}" -name "*.notif" -mmin -60 2>/dev/null | head -20 | while read -r f; do
            echo "  $(basename "$f")"
            cat "$f" | sed 's/^/    /'
            echo ""
        done
    fi
    echo "--- Failed SSH (1h) ---"
    journalctl -u sshd --since "1 hour ago" --no-pager 2>/dev/null | grep -c "Failed password" | xargs -I{} echo "  {} failed login attempts"
}

# --cron
_do_cron_impl() {
    echo "=== Cron Status: ${NODE_ID} ==="
    echo "--- Active crontabs ---"
    for u in root snowdaddy automation phoenix; do
        local ct; ct=$(crontab -l -u "$u" 2>/dev/null | grep -v '^#' | grep -c '[^ ]' || echo 0)
        [ "$ct" -gt 0 ] && echo "  ${u}: ${ct} job(s)"
    done
    echo ""
    echo "--- Recent cron activity ---"
    journalctl -u crond --since "1 hour ago" --no-pager -n 10 2>/dev/null | tail -10
}

# --send (full scan + cache)
_do_send_impl() {
    tool_audit "send" "node=${NODE_ID}"
    local payload; payload=$(_do_errors_impl "${1:-}")
    if [ -n "$payload" ]; then
        mkdir -p "$(dirname "$CACHE_FILE")" 2>/dev/null
        echo "$payload" > "$CACHE_FILE" 2>/dev/null
    fi
    echo "$payload"
}

# --digest
_do_digest_impl() {
    echo "=== Log Report: ${NODE_ID} ==="
    echo "  Tool: ${TOOL_NAME} v${TOOL_VERSION}"
    echo "  Time: $(ts_local)"
    echo ""
    if [ -f "$CACHE_FILE" ]; then
        python3 -c "
import json
d=json.load(open('${CACHE_FILE}'))
print(f\"  Status: {d.get('status','?')}\")
print(f\"  Errors: {d.get('total_errors',0)}\")
print(f\"  Warnings: {d.get('total_warnings',0)}\")
for k,c in d.get('categories',{}).items():
    print(f\"  [{c.get('status','?').upper():7s}] {c.get('label',k)}: {c.get('errors',0)}E {c.get('warnings',0)}W\")" 2>/dev/null
    else
        echo "  No cached scan. Run --send first."
    fi
}

_do_flush_impl() {
    [ -f "$CACHE_FILE" ] && rm -f "$CACHE_FILE" && echo "  [CHANGED] Removed cache"
    [ -f "$TOOL_LOG" ] && truncate -s 0 "$TOOL_LOG" && echo "  [CHANGED] Truncated log"
    echo "Flush complete on ${NODE_ID}"
}

tool_help() {
    echo "${TOOL_NAME} v${TOOL_VERSION} -- Log Manager & Error Scanner"
    echo ""
    echo "Actions:"
    echo "  --errors [cat]  Full error scan (categories: websites,server,services,vault,payments)"
    echo "  --tail [path]   Tail recent logs"
    echo "  --scan          Quick pattern scan"
    echo "  --alerts        Recent alerts"
    echo "  --cron          Cron job status"
    echo "  --status        JSON pill"
    echo "  --send [cat]    Full scan + cache"
    echo "  --digest        Report from cache"
    echo "  --flush         Clear cache + log"
}

w_send()   { tool_wrap "send"   _do_send_impl "$@"; }
w_status() { tool_wrap "status" _do_status_impl "$@"; }
w_errors() { tool_wrap "errors" _do_errors_impl "$@"; }
w_tail()   { tool_wrap "tail"   _do_tail_impl "$@"; }
w_scan()   { tool_wrap "scan"   _do_scan_impl "$@"; }
w_digest() { tool_wrap "digest" _do_digest_impl "$@"; }
w_flush()  { tool_wrap "flush"  _do_flush_impl "$@"; }
w_alerts() { tool_wrap "alerts" _do_alerts_impl "$@"; }
w_cron()   { tool_wrap "cron"   _do_cron_impl "$@"; }

case "${1:-}" in --send|--status|--errors|status) export TOOL_JSON=1 ;; esac

tool_init
trap 'tool_complete' EXIT
case "${1:---help}" in
    --send)   shift; w_send "$@" ;;   --status) shift; w_status "$@" ;;
    --errors) shift; w_errors "$@" ;; --tail)   shift; w_tail "$@" ;;
    --scan)   shift; w_scan "$@" ;;   --digest) shift; w_digest "$@" ;;
    --flush)  shift; w_flush "$@" ;;  --alerts) shift; w_alerts "$@" ;;
    --cron)   shift; w_cron "$@" ;;   --help)   tool_help ;;
    *)        handle_standard_command "$@" ;;
esac
