"""Runtime observability helpers for aiohttp routes."""

from __future__ import annotations

import os
import time
from typing import Any
from uuid import uuid4

from aiohttp import web

from .shared import get_logger, request_id_var
from .utils import env_float

logger = get_logger(__name__)

_CLIENT_DISCONNECT_ERRORS = (
    ConnectionResetError,
    BrokenPipeError,
    ConnectionAbortedError,
    OSError,
)
_CLIENT_DISCONNECT_ERRNO = frozenset({10053, 10054, 104, 32, 9})

MS_PER_S = 1000.0
_DEFAULT_LOG_RATELIMIT_MS = 2000.0
_DEFAULT_SLOW_MS = 750.0
_DEFAULT_RATELIMIT_CLEAN_INTERVAL_MS = 60_000.0
_DEFAULT_RATELIMIT_CLEAN_WINDOW_MULT = 4.0
_DEFAULT_RATELIMIT_CLEAN_MIN_CUTOFF_MS = 120_000.0

_LOG_RATELIMIT_STATE: dict[str, float] = {}
_LOG_RATELIMIT_CLEAN_AT = 0.0
_SENSITIVE_QUERY_KEYS = frozenset(
    {
        "token",
        "api_token",
        "apikey",
        "api_key",
        "authorization",
        "auth",
        "password",
        "secret",
        "key",
    }
)


def _is_client_disconnect(exc: BaseException) -> bool:
    if isinstance(exc, (ConnectionResetError, BrokenPipeError, ConnectionAbortedError)):
        return True
    if isinstance(exc, OSError):
        errno = getattr(exc, "errno", None)
        if errno in _CLIENT_DISCONNECT_ERRNO:
            return True
        winerror = getattr(exc, "winerror", None)
        if winerror in _CLIENT_DISCONNECT_ERRNO:
            return True
    return False


def _new_request_id() -> str:
    return uuid4().hex


def _get_request_id(request: web.Request) -> str:
    rid = (request.headers.get("X-Request-ID") or "").strip()
    return rid or _new_request_id()


def _env_flag(name: str, default: bool = False) -> bool:
    try:
        raw = os.environ.get(name)
    except Exception:
        raw = None
    if raw is None:
        return default
    value = str(raw).strip().lower()
    return value in ("1", "true", "yes", "on")


def _should_emit_log(key: str, *, window_ms: float) -> bool:
    global _LOG_RATELIMIT_CLEAN_AT
    now = time.monotonic() * MS_PER_S
    try:
        last = _LOG_RATELIMIT_STATE.get(key, 0.0)
        if last and now - last < window_ms:
            return False
        _LOG_RATELIMIT_STATE[key] = now

        clean_interval_ms = env_float("MJR_OBS_RATELIMIT_CLEAN_INTERVAL_MS", _DEFAULT_RATELIMIT_CLEAN_INTERVAL_MS)
        clean_window_mult = env_float("MJR_OBS_RATELIMIT_CLEAN_WINDOW_MULT", _DEFAULT_RATELIMIT_CLEAN_WINDOW_MULT)
        clean_min_cutoff_ms = env_float("MJR_OBS_RATELIMIT_CLEAN_MIN_CUTOFF_MS", _DEFAULT_RATELIMIT_CLEAN_MIN_CUTOFF_MS)

        if not _LOG_RATELIMIT_CLEAN_AT or now - _LOG_RATELIMIT_CLEAN_AT > clean_interval_ms:
            cutoff = now - max(window_ms * clean_window_mult, clean_min_cutoff_ms)
            for k, ts in list(_LOG_RATELIMIT_STATE.items()):
                if ts < cutoff:
                    _LOG_RATELIMIT_STATE.pop(k, None)
            _LOG_RATELIMIT_CLEAN_AT = now
    except Exception:
        return True
    return True


def _request_path_or_empty(request: web.Request) -> str:
    try:
        return request.path or ""
    except Exception:
        return ""


def _is_majoor_route(path: str) -> bool:
    return path.startswith("/mjr/am/")


def _is_health_poll_path(path: str) -> bool:
    return path in ("/mjr/am/health", "/mjr/am/health/counters")


def _is_error_status(status: int | None) -> bool:
    return status is not None and status >= 400


def _request_header_lower(request: web.Request, header_name: str) -> str:
    try:
        return (request.headers.get(header_name) or "").strip().lower()
    except Exception:
        return ""


def _is_obs_client_opt_out(value: str) -> bool:
    return value in ("0", "false", "off", "disable", "disabled", "no")


def _is_obs_client_opt_in(value: str) -> bool:
    return value in ("1", "true", "on", "enable", "enabled", "yes")


def _client_observability_flags(request: web.Request) -> tuple[str, bool]:
    client_flag = _request_header_lower(request, "X-MJR-OBS")
    return client_flag, _is_obs_client_opt_in(client_flag)


def _should_log(request: web.Request, *, status: int | None, duration_ms: float) -> bool:
    path = _request_path_or_empty(request)
    if not _is_majoor_route(path):
        return False
    client_flag, client_explicit_on = _client_observability_flags(request)
    if _is_obs_client_opt_out(client_flag):
        return False
    if _env_flag("MJR_OBS_LOG_ALL", default=False):
        return True
    if _is_health_poll_path(path):
        return _is_error_status(status)
    if _is_error_status(status):
        return True
    if not _env_flag("MJR_OBS_LOG_SUCCESS", default=False):
        return False
    if not client_explicit_on:
        return False
    if not _env_flag("MJR_OBS_LOG_SLOW", default=False):
        return False
    slow_ms = env_float("MJR_OBS_SLOW_MS", _DEFAULT_SLOW_MS)
    return duration_ms >= slow_ms


@web.middleware
async def request_context_middleware(request: web.Request, handler):
    if _env_flag("MJR_OBS_DISABLE", default=False):
        return await handler(request)

    rid = _get_request_id(request)
    request["mjr_request_id"] = rid
    token = _set_request_id_context(rid)
    start = time.perf_counter()
    status: int | None = None
    error: str | None = None
    error_type: str | None = None
    try:
        response = await handler(request)
        status = _response_status_code(response)
        _attach_request_id_header(response, rid)
        return response
    except web.HTTPException as exc:
        status = _http_exception_status_code(exc)
        error_type, error = _exception_error_fields(exc, fallback="HTTPException")
        _attach_request_id_header_to_http_exception(exc, rid)
        raise
    except RuntimeError as exc:
        if "resetting" in str(exc).lower():
            status = 503
            error_type = "DB_RESETTING"
            error = str(exc)
            raise web.HTTPServiceUnavailable(reason="Database is temporarily unavailable") from exc
        status = 500
        error_type, error = _exception_error_fields(exc, fallback="Unhandled error")
        raise
    except Exception as exc:
        status = 500
        error_type, error = _exception_error_fields(exc, fallback="Unhandled error")
        raise
    finally:
        duration_ms = (time.perf_counter() - start) * 1000.0
        request["mjr_duration_ms"] = duration_ms
        _reset_request_id_context(token)
        _emit_request_log(request, status=status, duration_ms=duration_ms, error=error, error_type=error_type)


def _response_status_code(response: Any) -> int:
    try:
        return int(getattr(response, "status", 200) or 200)
    except Exception:
        return 200


def _http_exception_status_code(exc: web.HTTPException) -> int:
    try:
        return int(getattr(exc, "status", 500) or 500)
    except Exception:
        return 500


def _exception_error_fields(exc: Exception, *, fallback: str) -> tuple[str, str]:
    error_type = exc.__class__.__name__
    try:
        return error_type, str(exc)
    except Exception:
        return error_type, fallback


def _attach_request_id_header(response: Any, rid: str) -> None:
    try:
        response.headers["X-Request-ID"] = rid
    except Exception:
        return


def _attach_request_id_header_to_http_exception(exc: web.HTTPException, rid: str) -> None:
    try:
        exc.headers["X-Request-ID"] = rid
    except (AttributeError, TypeError, KeyError) as exc2:
        logger.debug("Unable to attach X-Request-ID to HTTPException: %s", exc2)


def _set_request_id_context(rid: str):
    try:
        if request_id_var is not None:
            return request_id_var.set(rid)
    except Exception:
        return None
    return None


def _reset_request_id_context(token: Any) -> None:
    try:
        if token is not None and request_id_var is not None:
            request_id_var.reset(token)
    except Exception:
        return


def _sanitize_query_params(request: web.Request) -> dict[str, Any]:
    out: dict[str, Any] = {}
    try:
        iterator = request.query.items()
    except Exception:
        return out
    for key, value in iterator:
        k = str(key or "")
        lower = k.lower()
        if lower in _SENSITIVE_QUERY_KEYS or lower.endswith("_token") or lower.endswith("_key"):
            out[k] = "[redacted]"
            continue
        try:
            text = str(value or "")
        except Exception:
            text = ""
        if len(text) > 256:
            text = text[:256] + "..."
        out[k] = text
    return out


def build_request_log_fields(request: web.Request, response_status: int | None = None) -> dict[str, Any]:
    stats = request.get("mjr_stats")
    return {
        "request_id": request.get("mjr_request_id"),
        "method": request.method,
        "path": request.path,
        "query": _sanitize_query_params(request),
        "status": response_status,
        "duration_ms": request.get("mjr_duration_ms"),
        "remote": getattr(request, "remote", None),
        "stats": stats if isinstance(stats, dict) else None,
    }


def _emit_request_log(
    request: web.Request,
    *,
    status: int | None,
    duration_ms: float,
    error: str | None,
    error_type: str | None,
) -> None:
    try:
        fields = build_request_log_fields(request, response_status=status)
        if error:
            fields["error"] = error
            fields["error_type"] = error_type
        if not _should_log(request, status=status, duration_ms=duration_ms):
            return
        key = _request_log_key(request, status)
        window_ms = env_float("MJR_OBS_RATELIMIT_MS", _DEFAULT_LOG_RATELIMIT_MS)
        if _should_emit_log(key, window_ms=window_ms):
            _emit_request_log_by_status(status, fields)
    except Exception as exc:
        try:
            logger.debug("Observability log emit failed: %s", exc)
        except Exception:
            return


def _request_log_key(request: web.Request, status: int | None) -> str:
    try:
        return f"{request.method}:{request.path}:{status}"
    except Exception:
        return f"unknown:{status}"


def _emit_request_log_by_status(status: int | None, fields: dict[str, Any]) -> None:
    method = fields.get("method") or "?"
    path = fields.get("path") or "?"
    dur = fields.get("duration_ms")
    dur_part = f" ({dur:.0f}ms)" if isinstance(dur, (int, float)) else ""
    msg = f"Request handled | {method} {path} -> {status}{dur_part}"
    if status is not None and status >= 500:
        logger.error(msg, extra=fields)
    elif status is not None and status >= 400:
        logger.warning(msg, extra=fields)
    else:
        logger.info(msg, extra=fields)


__all__ = [
    "_CLIENT_DISCONNECT_ERRORS",
    "_CLIENT_DISCONNECT_ERRNO",
    "_attach_request_id_header",
    "_attach_request_id_header_to_http_exception",
    "_client_observability_flags",
    "_emit_request_log",
    "_emit_request_log_by_status",
    "_env_flag",
    "_exception_error_fields",
    "_get_request_id",
    "_http_exception_status_code",
    "_is_client_disconnect",
    "_is_error_status",
    "_is_health_poll_path",
    "_is_majoor_route",
    "_is_obs_client_opt_in",
    "_is_obs_client_opt_out",
    "_new_request_id",
    "_request_header_lower",
    "_request_log_key",
    "_request_path_or_empty",
    "_reset_request_id_context",
    "_response_status_code",
    "_sanitize_query_params",
    "_set_request_id_context",
    "_should_emit_log",
    "_should_log",
    "build_request_log_fields",
    "request_context_middleware",
]
