"""
Log Analyzer
=============
Parses crash logs to extract failure patterns, recurring errors,
and crash-restart cycles. Feeds insights into the failure intelligence engine.
"""

from __future__ import annotations

import logging
import os
import re
from collections import Counter, defaultdict
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional

logger = logging.getLogger("watchdog.log_analyzer")

_PROJECT_ROOT = Path(__file__).resolve().parent.parent
_LOGS_DIR = _PROJECT_ROOT / "logs"

LOG_FILES = {
    "bot": _LOGS_DIR / "bot.log",
    "bot_runner": _LOGS_DIR / "bot_runner.log",
    "watchdog": _LOGS_DIR / "watchdog.log",
    "health_monitor": _LOGS_DIR / "health_monitor.log",
    "dashboard": _LOGS_DIR / "dashboard.log",
}

# Patterns to extract from logs
_RE_TIMESTAMP = re.compile(r"(\d{4}-\d{2}-\d{2}[\sT]\d{2}:\d{2}:\d{2})")
_RE_ERROR_LINE = re.compile(r"\b(ERROR|CRITICAL|EXCEPTION|Traceback)\b", re.I)
_RE_WARNING_LINE = re.compile(r"\bWARNING\b", re.I)
_RE_CRASH = re.compile(r"(crash|exited|killed|terminated|SIGKILL|SIGTERM|exit code \d+)", re.I)
_RE_RESTART = re.compile(r"(restart|starting|launched|restarted)", re.I)

# Normalization: strip variable parts from error messages
_NORMALIZE_PATTERNS = [
    (re.compile(r"\d{4}-\d{2}-\d{2}[\sT]\d{2}:\d{2}:\d{2}[.\d]*"), "<TIMESTAMP>"),
    (re.compile(r"PID \d+"), "PID <N>"),
    (re.compile(r"port \d+"), "port <N>"),
    (re.compile(r"0x[0-9a-fA-F]+"), "0x<HEX>"),
    (re.compile(r"\b\d{5,}\b"), "<ID>"),
    (re.compile(r"/.+?\.py:\d+"), "<FILE>:<LINE>"),
    (re.compile(r"\\\\?.+?\\.+?\.py:\d+"), "<FILE>:<LINE>"),
]


def _normalize_error(msg: str) -> str:
    """Normalize an error message for grouping."""
    for pattern, replacement in _NORMALIZE_PATTERNS:
        msg = pattern.sub(replacement, msg)
    return msg.strip()


def _parse_timestamp(line: str) -> Optional[datetime]:
    """Extract datetime from a log line."""
    m = _RE_TIMESTAMP.search(line)
    if m:
        try:
            return datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S")
        except ValueError:
            try:
                return datetime.strptime(m.group(1), "%Y-%m-%dT%H:%M:%S")
            except ValueError:
                return None
    return None


def _read_recent_lines(filepath: Path, hours: int = 24, max_lines: int = 10000) -> list[str]:
    """Read lines from the last N hours of a log file."""
    if not filepath.exists():
        return []

    cutoff = datetime.now() - timedelta(hours=hours)
    lines = []

    try:
        # Read from end of file for efficiency
        with open(filepath, "r", encoding="utf-8", errors="replace") as f:
            # Read last max_lines lines
            all_lines = f.readlines()
            recent = all_lines[-max_lines:] if len(all_lines) > max_lines else all_lines

        for line in recent:
            ts = _parse_timestamp(line)
            if ts and ts < cutoff:
                continue
            lines.append(line.rstrip())

    except Exception as exc:
        logger.error("Error reading %s: %s", filepath, exc)

    return lines


# ---------------------------------------------------------------------------
# Analysis functions
# ---------------------------------------------------------------------------

def analyze_recent(hours: int = 24) -> dict:
    """
    Scan all log files for errors in the last N hours.
    Returns categorized counts and sample messages.
    """
    results = {}

    for source, filepath in LOG_FILES.items():
        lines = _read_recent_lines(filepath, hours)
        errors = []
        warnings = []

        for line in lines:
            if _RE_ERROR_LINE.search(line):
                errors.append(line)
            elif _RE_WARNING_LINE.search(line):
                warnings.append(line)

        results[source] = {
            "total_lines": len(lines),
            "error_count": len(errors),
            "warning_count": len(warnings),
            "sample_errors": errors[-5:],  # Last 5 errors
            "sample_warnings": warnings[-3:],
        }

    return results


def extract_crash_sequences(hours: int = 24) -> list[dict]:
    """
    Identify crash-restart cycles from bot_runner log.
    Returns list of crash events with timestamps and details.
    """
    filepath = LOG_FILES.get("bot_runner")
    if not filepath:
        return []

    lines = _read_recent_lines(filepath, hours)
    crashes = []

    for line in lines:
        ts = _parse_timestamp(line)
        if not ts:
            continue

        if _RE_CRASH.search(line):
            crashes.append({
                "timestamp": ts.isoformat(),
                "type": "crash",
                "detail": line[:200],
            })
        elif _RE_RESTART.search(line):
            crashes.append({
                "timestamp": ts.isoformat(),
                "type": "restart",
                "detail": line[:200],
            })

    return crashes


def find_recurring_errors(hours: int = 24, min_count: int = 3) -> list[dict]:
    """
    Group errors by normalized message and return those occurring >= min_count times.
    """
    all_errors = []

    for source, filepath in LOG_FILES.items():
        lines = _read_recent_lines(filepath, hours)
        for line in lines:
            if _RE_ERROR_LINE.search(line):
                normalized = _normalize_error(line)
                all_errors.append((source, normalized, line))

    # Group by normalized message
    counter = Counter()
    examples = {}
    sources = defaultdict(set)

    for source, normalized, raw in all_errors:
        counter[normalized] += 1
        sources[normalized].add(source)
        if normalized not in examples:
            examples[normalized] = raw[:300]

    recurring = []
    for msg, count in counter.most_common():
        if count >= min_count:
            recurring.append({
                "normalized": msg[:200],
                "count": count,
                "sources": list(sources[msg]),
                "example": examples[msg],
            })

    return recurring


def get_error_timeline(service: str, hours: int = 24) -> list[dict]:
    """Return timestamped error entries for a specific service."""
    filepath = LOG_FILES.get(service)
    if not filepath:
        return []

    lines = _read_recent_lines(filepath, hours)
    timeline = []

    for line in lines:
        if not _RE_ERROR_LINE.search(line):
            continue
        ts = _parse_timestamp(line)
        if ts:
            timeline.append({
                "timestamp": ts.isoformat(),
                "message": line[:300],
            })

    return timeline


def get_crash_frequency(hours: int = 24) -> dict:
    """
    Calculate crash frequency metrics.
    Returns: crashes per hour, MTBF (mean time between failures), etc.
    """
    crashes = extract_crash_sequences(hours)
    crash_events = [c for c in crashes if c["type"] == "crash"]

    if not crash_events:
        return {
            "total_crashes": 0,
            "crashes_per_hour": 0,
            "mtbf_minutes": None,
            "period_hours": hours,
        }

    total = len(crash_events)
    crashes_per_hour = round(total / max(hours, 1), 2)

    # MTBF: mean time between failures
    if total >= 2:
        timestamps = []
        for c in crash_events:
            try:
                timestamps.append(datetime.fromisoformat(c["timestamp"]))
            except (ValueError, TypeError):
                pass

        if len(timestamps) >= 2:
            timestamps.sort()
            intervals = [
                (timestamps[i+1] - timestamps[i]).total_seconds() / 60
                for i in range(len(timestamps) - 1)
            ]
            mtbf = round(sum(intervals) / len(intervals), 1)
        else:
            mtbf = None
    else:
        mtbf = None

    return {
        "total_crashes": total,
        "crashes_per_hour": crashes_per_hour,
        "mtbf_minutes": mtbf,
        "period_hours": hours,
        "last_crash": crash_events[-1] if crash_events else None,
    }


def generate_diagnostic_report(hours: int = 24) -> str:
    """Generate a human-readable diagnostic report."""
    lines = []
    lines.append(f"BizNode Diagnostic Report — Last {hours} hours")
    lines.append("=" * 50)

    # Crash frequency
    freq = get_crash_frequency(hours)
    lines.append(f"\nCrash Summary:")
    lines.append(f"  Total crashes: {freq['total_crashes']}")
    lines.append(f"  Crashes/hour: {freq['crashes_per_hour']}")
    if freq["mtbf_minutes"]:
        lines.append(f"  MTBF: {freq['mtbf_minutes']} minutes")

    # Per-service errors
    analysis = analyze_recent(hours)
    lines.append(f"\nPer-Service Errors:")
    for source, data in analysis.items():
        if data["error_count"] > 0:
            lines.append(f"  {source}: {data['error_count']} errors, {data['warning_count']} warnings")

    # Recurring errors
    recurring = find_recurring_errors(hours)
    if recurring:
        lines.append(f"\nRecurring Errors (>= 3 occurrences):")
        for err in recurring[:5]:
            lines.append(f"  [{err['count']}x] {err['normalized'][:100]}")

    return "\n".join(lines)
