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):
    """Set the correlation ID for the current thread context."""
    _request_context.correlation_id = cid


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

    def __init__(self):
        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()
