from __future__ import annotations

import re
from collections.abc import MutableMapping
from typing import cast
from uuid import uuid4

from fastapi import FastAPI, Request
from starlette.requests import Request as StarletteRequest
from starlette.responses import JSONResponse, Response
from starlette.types import ASGIApp, Receive, Scope, Send

from core.logging.context import bind_context, clear_context
from core.logging.logger import get_logger


class RequestContextMiddleware:
    app: ASGIApp
    _header_name: str
    _request_id_pattern: re.Pattern[str]

    def __init__(self, app: ASGIApp, header_name: str = "X-Request-ID") -> None:
        self.app = app
        self._header_name = header_name
        self._request_id_pattern = re.compile(r"^[A-Za-z0-9_-]{8,64}$")

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope.get("type") != "http":
            await self.app(scope, receive, send)
            return

        request = StarletteRequest(scope, receive=receive)
        request_id = self._normalize_request_id(request.headers.get(self._header_name))
        client_ip = request.client.host if request.client else None
        user_id = getattr(request.state, "user_id", None)

        request.state.request_id = request_id

        bind_context(
            request_id=request_id,
            method=request.method,
            path=request.url.path,
            client_ip=client_ip,
            user_id=user_id,
        )

        async def send_wrapper(message: MutableMapping[str, object]) -> None:
            if message.get("type") == "http.response.start":
                raw_headers = message.get("headers")
                headers = list(cast(list[tuple[bytes, bytes]], raw_headers or []))
                header_key = self._header_name.lower().encode()
                if not any(item[0].lower() == header_key for item in headers):
                    headers.append((header_key, request_id.encode()))
                message = {**message, "headers": headers}
            await send(message)

        try:
            await self.app(scope, receive, send_wrapper)
        finally:
            clear_context()

    def _normalize_request_id(self, request_id: str | None) -> str:
        if request_id and self._request_id_pattern.match(request_id):
            return request_id
        return str(uuid4())


def register_exception_handlers(app: FastAPI) -> None:
    logger = get_logger("core.logging.exception")

    @app.exception_handler(Exception)
    async def unhandled_exception_handler(request: Request, exc: Exception) -> Response:
        request_id = getattr(request.state, "request_id", None)
        logger.exception(
            "Unhandled exception",
            error_type=exc.__class__.__name__,
            request_id=request_id,
        )
        headers = {"X-Request-ID": request_id} if request_id else None
        return JSONResponse(
            status_code=500,
            content={"detail": "Internal Server Error"},
            headers=headers,
        )
