from __future__ import annotations

"""SalmAlm Production Logging — structured JSON logs, rotation, request tracking.

Upgrades from basic logging to:
  - Structured JSON log format (machine-parseable)
  - Log rotation (max 10MB per file, keep 5 backups)
  - Request/response logging middleware
  - Performance metrics (request duration)
  - Error aggregation
  - Correlation IDs for request tracing
"""


import json
import logging
import logging.handlers
import threading
import uuid
from datetime import datetime

from salmalm.constants import KST, LOG_FILE

# ── Structured JSON Formatter ────────────────────────────────


class JSONFormatter(logging.Formatter):
    """Format log records as JSON lines."""

    def format(self, record: logging.LogRecord) -> str:
        """Format a log record with correlation ID prefix."""
        log_entry = {
            "ts": datetime.now(KST).isoformat(),
            "level": record.levelname,
            "msg": record.getMessage(),
            "module": record.module,
            "func": record.funcName,
            "line": record.lineno,
        }
        if record.exc_info and record.exc_info[0]:
            log_entry["exception"] = {
                "type": record.exc_info[0].__name__,
                "message": str(record.exc_info[1]),
            }
        # Add extra fields
        for key in ("correlation_id", "user", "duration_ms", "status_code", "method", "path", "ip"):
            val = getattr(record, key, None)
            if val is not None:
                log_entry[key] = val
        return json.dumps(log_entry, ensure_ascii=False)


# ── Rotating File Handler ────────────────────────────────────


def setup_production_logging(json_log: bool = True, max_bytes: int = 10_000_000, backup_count: int = 5):
    """Configure production-grade logging with rotation."""
    logger = logging.getLogger("salmalm")

    # Remove existing handlers
    for handler in logger.handlers[:]:
        logger.removeHandler(handler)

    # Rotating file handler
    file_handler = logging.handlers.RotatingFileHandler(
        str(LOG_FILE), maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8"
    )
    if json_log:
        file_handler.setFormatter(JSONFormatter())
    else:
        file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
    logger.addHandler(file_handler)

    # Console handler (human-readable)
    console = logging.StreamHandler()
    console.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
    logger.addHandler(console)

    logger.setLevel(logging.INFO)
    return logger


# ── Request Logger ───────────────────────────────────────────

_request_context = threading.local()


def get_correlation_id() -> str:
    """Get or create correlation ID for current request."""
    cid = getattr(_request_context, "correlation_id", None)
    if not cid:
        cid = str(uuid.uuid4())[:8]
        _request_context.correlation_id = cid
    return cid


def set_correlation_id(cid: str) -> None:
    """Set the correlation ID for the current thread context."""
    _request_context.correlation_id = cid


class RequestLogger:
    """Middleware-style request/response logger."""

    def __init__(self) -> None:
        """Init  ."""
        self._logger = logging.getLogger("salmalm.requests")
        self._metrics: dict = {
            "total_requests": 0,
            "total_errors": 0,
            "by_status": {},
            "by_path": {},
            "avg_duration_ms": 0,
            "_durations": [],
        }
        self._lock = threading.Lock()

    def log_request(
        self,
        method: str,
        path: str,
        ip: str = "",
        user: str = "",
        status_code: int = 200,
        duration_ms: float = 0,
        error: str = "",
    ):
        """Log a request with structured data."""
        extra = {
            "correlation_id": get_correlation_id(),
            "method": method,
            "path": path,
            "ip": ip,
            "user": user,
            "status_code": status_code,
            "duration_ms": round(duration_ms, 2),
        }

        if status_code >= 500:
            self._logger.error(f"{method} {path} -> {status_code} ({duration_ms:.0f}ms)", extra=extra)
        elif status_code >= 400:
            self._logger.warning(f"{method} {path} -> {status_code} ({duration_ms:.0f}ms)", extra=extra)
        else:
            self._logger.info(f"{method} {path} -> {status_code} ({duration_ms:.0f}ms)", extra=extra)

        # Update metrics
        with self._lock:
            self._metrics["total_requests"] += 1
            if status_code >= 400:
                self._metrics["total_errors"] += 1
            sc = str(status_code)
            self._metrics["by_status"][sc] = self._metrics["by_status"].get(sc, 0) + 1
            # Track top paths
            self._metrics["by_path"][path] = self._metrics["by_path"].get(path, 0) + 1
            # Rolling average duration
            self._metrics["_durations"].append(duration_ms)
            if len(self._metrics["_durations"]) > 1000:
                self._metrics["_durations"] = self._metrics["_durations"][-500:]
            if self._metrics["_durations"]:
                self._metrics["avg_duration_ms"] = round(
                    sum(self._metrics["_durations"]) / len(self._metrics["_durations"]), 2
                )

    def get_metrics(self) -> dict:
        """Get request metrics (exclude internal durations list)."""
        with self._lock:
            m = {k: v for k, v in self._metrics.items() if not k.startswith("_")}
            m["error_rate"] = round(self._metrics["total_errors"] / max(self._metrics["total_requests"], 1) * 100, 2)
            # Top 10 paths
            m["top_paths"] = dict(sorted(self._metrics["by_path"].items(), key=lambda x: -x[1])[:10])
            return m


# ── Graceful Error Recovery ──────────────────────────────────


def safe_execute(func, *args, fallback=None, tag="SAFE", **kwargs):
    """Execute a function with graceful error recovery.

    Returns the result on success, or fallback on any exception.
    Logs errors with [TAG] prefix for easy grep.
    """
    try:
        return func(*args, **kwargs)
    except Exception as e:
        logger = logging.getLogger("salmalm")
        logger.error(f"[{tag}] {func.__name__} failed: {type(e).__name__}: {e}")
        return fallback


async def safe_execute_async(coro, fallback=None, tag="SAFE"):
    """Async version of safe_execute."""
    try:
        return await coro
    except Exception as e:
        logger = logging.getLogger("salmalm")
        logger.error(f"[{tag}] Async task failed: {type(e).__name__}: {e}")
        return fallback


# ── Module instances ─────────────────────────────────────────

request_logger = RequestLogger()
