"""Observability decorators — structured logging for all proxym functions."""
from __future__ import annotations

import asyncio
import functools
import time
import traceback
from collections import deque
from typing import Any, Callable

import structlog

log = structlog.get_logger()

# ---------------------------------------------------------------------------
# Ring buffer — ostatnie N logów w pamięci, dostępne przez API
# ---------------------------------------------------------------------------

_LOG_BUFFER: deque[dict[str, Any]] = deque(maxlen=1000)


def get_log_buffer(limit: int = 100, level: str | None = None) -> list[dict]:
    """Pobierz ostatnie logi z bufora."""
    items = list(_LOG_BUFFER)
    if level:
        items = [e for e in items if e.get("level") == level]
    return items[-limit:]


def get_error_summary() -> dict[str, Any]:
    """Podsumowanie błędów z ostatnich logów."""
    errors = [e for e in _LOG_BUFFER if e.get("level") == "error"]
    by_source: dict[str, list] = {}
    for e in errors:
        src = e.get("source", "unknown")
        by_source.setdefault(src, []).append(e)
    return {
        "total_errors": len(errors),
        "total_logs": len(_LOG_BUFFER),
        "by_source": {k: len(v) for k, v in by_source.items()},
        "recent_errors": errors[-5:],
    }


def _emit(level: str, source: str, **kwargs: Any) -> None:
    """Zapisz wpis do bufora i do structlog."""
    entry = {
        "timestamp": time.time(),
        "level": level,
        "source": source,
        **kwargs,
    }
    _LOG_BUFFER.append(entry)
    event_msg = kwargs.pop("event", "log")
    getattr(log, level, log.info)(event_msg, source=source, **kwargs)


# ---------------------------------------------------------------------------
# @log_call — uniwersalny dekorator dla dowolnej funkcji
# ---------------------------------------------------------------------------

def log_call(
    *,
    level: str = "debug",
    include_args: bool = True,
    include_result: bool = False,
    error_level: str = "error",
    slow_threshold_ms: float = 1000,
) -> Callable:
    """Dekorator logujący wywołanie, czas i błędy.

    Użycie:
        @log_call()
        def my_function(x, y): ...

        @log_call(level="info", include_result=True, slow_threshold_ms=500)
        async def slow_operation(): ...
    """

    def decorator(fn: Callable) -> Callable:
        source = f"{fn.__module__}.{fn.__qualname__}"
        is_async = asyncio.iscoroutinefunction(fn)

        @functools.wraps(fn)
        async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
            start = time.monotonic()
            call_kwargs = _safe_args(args, kwargs, include_args)
            _emit(level, source, event="call", **call_kwargs)
            try:
                result = await fn(*args, **kwargs)
                duration_ms = (time.monotonic() - start) * 1000
                result_kwargs: dict[str, Any] = {
                    "event": "return", "duration_ms": round(duration_ms, 1),
                }
                if include_result:
                    result_kwargs["result"] = _truncate(result)
                if duration_ms > slow_threshold_ms:
                    result_kwargs["slow"] = True
                    _emit("warning", source, **result_kwargs)
                else:
                    _emit(level, source, **result_kwargs)
                return result
            except Exception as exc:
                duration_ms = (time.monotonic() - start) * 1000
                _emit(error_level, source,
                      event="error", error=str(exc),
                      error_type=type(exc).__name__,
                      duration_ms=round(duration_ms, 1),
                      traceback=traceback.format_exc()[-800:])
                raise

        @functools.wraps(fn)
        def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
            start = time.monotonic()
            call_kwargs = _safe_args(args, kwargs, include_args)
            _emit(level, source, event="call", **call_kwargs)
            try:
                result = fn(*args, **kwargs)
                duration_ms = (time.monotonic() - start) * 1000
                result_kwargs: dict[str, Any] = {
                    "event": "return", "duration_ms": round(duration_ms, 1),
                }
                if include_result:
                    result_kwargs["result"] = _truncate(result)
                if duration_ms > slow_threshold_ms:
                    result_kwargs["slow"] = True
                    _emit("warning", source, **result_kwargs)
                else:
                    _emit(level, source, **result_kwargs)
                return result
            except Exception as exc:
                duration_ms = (time.monotonic() - start) * 1000
                _emit(error_level, source,
                      event="error", error=str(exc),
                      error_type=type(exc).__name__,
                      duration_ms=round(duration_ms, 1),
                      traceback=traceback.format_exc()[-800:])
                raise

        return async_wrapper if is_async else sync_wrapper

    return decorator


# ---------------------------------------------------------------------------
# @log_endpoint — dekorator dla FastAPI route handlers
# ---------------------------------------------------------------------------

def log_endpoint(
    *,
    include_body: bool = False,
    slow_threshold_ms: float = 2000,
) -> Callable:
    """Dekorator dla FastAPI endpointów — loguje request/response + timing.

    Użycie:
        @router.get("/health")
        @log_endpoint()
        async def health(): ...
    """

    def decorator(fn: Callable) -> Callable:
        source = f"api.{fn.__name__}"

        @functools.wraps(fn)
        async def wrapper(*args: Any, **kwargs: Any) -> Any:
            start = time.monotonic()
            _emit("info", source, event="request",
                  method=kwargs.get("request", {}).method if hasattr(kwargs.get("request"), "method") else "?")
            try:
                result = await fn(*args, **kwargs) if asyncio.iscoroutinefunction(fn) else fn(*args, **kwargs)
                duration_ms = (time.monotonic() - start) * 1000
                lvl = "warning" if duration_ms > slow_threshold_ms else "info"
                _emit(lvl, source, event="response",
                      duration_ms=round(duration_ms, 1),
                      slow=duration_ms > slow_threshold_ms)
                return result
            except Exception as exc:
                _emit("error", source, event="endpoint_error",
                      error=str(exc), error_type=type(exc).__name__,
                      duration_ms=round((time.monotonic() - start) * 1000, 1),
                      traceback=traceback.format_exc()[-500:])
                raise

        return wrapper

    return decorator


# ---------------------------------------------------------------------------
# @retry_with_log — retry z backoff + logowanie prób
# ---------------------------------------------------------------------------

def retry_with_log(
    max_retries: int = 3,
    backoff_base: float = 1.0,
    retryable: tuple[type[Exception], ...] = (ConnectionError, TimeoutError),
) -> Callable:
    """Retry z exponential backoff i logowaniem każdej próby.

    Użycie:
        @retry_with_log(max_retries=3, retryable=(httpx.ConnectError,))
        async def call_provider(): ...
    """

    def decorator(fn: Callable) -> Callable:
        source = f"{fn.__module__}.{fn.__qualname__}"
        is_async = asyncio.iscoroutinefunction(fn)

        @functools.wraps(fn)
        async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
            last_exc = None
            for attempt in range(1, max_retries + 1):
                try:
                    return await fn(*args, **kwargs)
                except retryable as exc:
                    last_exc = exc
                    wait = backoff_base * (2 ** (attempt - 1))
                    _emit("warning", source,
                          event="retry", attempt=attempt,
                          max_retries=max_retries,
                          error=str(exc), wait_s=round(wait, 1))
                    await asyncio.sleep(wait)
            _emit("error", source,
                  event="retries_exhausted",
                  max_retries=max_retries, error=str(last_exc))
            raise last_exc  # type: ignore[misc]

        @functools.wraps(fn)
        def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
            last_exc = None
            for attempt in range(1, max_retries + 1):
                try:
                    return fn(*args, **kwargs)
                except retryable as exc:
                    last_exc = exc
                    wait = backoff_base * (2 ** (attempt - 1))
                    _emit("warning", source,
                          event="retry", attempt=attempt,
                          max_retries=max_retries,
                          error=str(exc), wait_s=round(wait, 1))
                    time.sleep(wait)
            _emit("error", source,
                  event="retries_exhausted",
                  max_retries=max_retries, error=str(last_exc))
            raise last_exc  # type: ignore[misc]

        return async_wrapper if is_async else sync_wrapper

    return decorator


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _safe_args(args: tuple, kwargs: dict, include: bool) -> dict[str, Any]:
    """Bezpiecznie sformatuj argumenty (bez secrets)."""
    if not include:
        return {}
    REDACT = {"api_key", "key", "password", "token", "secret"}
    safe_kw = {}
    for k, v in kwargs.items():
        if k.lower() in REDACT:
            safe_kw[k] = "***"
        else:
            safe_kw[k] = _truncate(v)
    return {"kwargs": safe_kw} if safe_kw else {}


def _truncate(value: Any, max_len: int = 200) -> Any:
    """Skróć duże wartości do logowania."""
    if isinstance(value, str) and len(value) > max_len:
        return value[:max_len] + f"... ({len(value)} chars)"
    if isinstance(value, (list, tuple)) and len(value) > 10:
        return f"[{len(value)} items]"
    if isinstance(value, dict) and len(value) > 10:
        return f"{{{len(value)} keys}}"
    return value
