"""Runtime error handling middleware for FastAPI applications."""

from typing import Any, Callable, Dict, List, Optional, cast

from fastapi import Request, Response
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from pydantic import ValidationError

from ..routes.response import return_response
from ...core.exception import RequestException, exception_to_dict
from ...log import footprint

__all__ = ("Runtime",)

DEFAULT_SENSITIVE_HEADERS: List[str] = ["authorization", "cookie"]


class Runtime:
    """Middleware for catching and handling runtime exceptions in FastAPI apps.

    Description:
        Intercepts all exceptions during request processing, logs them with
        detailed context, and returns standardized error responses to clients.
    """

    _default_error_message: str = (
        "An unexpected issue has occurred; our team has been notified "
        "and is working diligently to resolve it promptly."
    )

    def __init__(
        self,
        hide_error_messages: bool = True,
        log_request_errors: bool = True,
        log_validation_errors: bool = True,
        log_unexpected_errors: bool = True,
        log_request_body: bool = True,
        max_body_log_size: int = 1 * 1024 * 1024,
        log_headers: bool = True,
        sensitive_headers: Optional[List[str]] = None,
        exclude_paths: Optional[List[str]] = None,
        custom_error_message: Optional[str] = None,
        validation_status_code: int = 422,
    ) -> None:
        """Initialize the runtime error handler.

        Description:
            Configures error response behavior, logging granularity, header
            redaction, path exclusions, and optional error callbacks.

        Args:
            hide_error_messages: If True, returns generic error messages to clients.
            log_request_errors: If True, logs RequestException errors via footprint.
            log_validation_errors: If True, logs Pydantic validation errors via footprint.
            log_unexpected_errors: If True, logs unexpected errors via footprint.
            log_request_body: If True, includes request body in error payloads.
            max_body_log_size: Maximum body size in bytes to include in logs.
            log_headers: If True, includes request headers in error payloads.
            sensitive_headers: Header names to redact from logs (default: authorization, cookie).
            exclude_paths: Request paths to skip error handling for (pass-through).
            custom_error_message: Override the default generic 500 error message.
            validation_status_code: HTTP status code for validation errors (default: 422).

        Returns:
            None
        """
        self.hide_error_messages = hide_error_messages
        self.log_request_errors = log_request_errors
        self.log_validation_errors = log_validation_errors
        self.log_unexpected_errors = log_unexpected_errors
        self.log_request_body = log_request_body
        self.max_body_log_size = max_body_log_size
        self.log_headers = log_headers
        self.sensitive_headers = [
            h.lower()
            for h in (
                sensitive_headers
                if sensitive_headers is not None
                else DEFAULT_SENSITIVE_HEADERS
            )
        ]
        self.exclude_paths = exclude_paths or []
        self.custom_error_message = custom_error_message or self._default_error_message
        self.validation_status_code = validation_status_code

    async def get_request_body(self, request: Request) -> Dict[str, Any]:
        """Extract and serialize the request body if it's not too large.

        Description:
            Safely extracts the request body for logging, skipping bodies larger
            than ``max_body_log_size`` to avoid memory issues. Returns an empty
            dict when ``log_request_body`` is disabled.

        Args:
            request: The incoming FastAPI request.

        Returns:
            Dict[str, Any]: Dictionary containing content metadata and body JSON.
        """
        if not self.log_request_body:
            return {}

        content_length = request.headers.get("content-length")
        content_type = request.headers.get("content-type")
        try:
            if content_length and int(content_length) > self.max_body_log_size:
                return {}
            body = await request.body()
            return {
                "content_length": content_length,
                "content_type": content_type,
                "json": jsonable_encoder(body.decode("utf-8")),
            }
        except Exception:
            return {
                "content_length": content_length,
                "content_type": content_type,
            }

    def _redact_headers(self, request: Request) -> Dict[str, str]:
        """Return request headers with sensitive values redacted.

        Args:
            request: The incoming FastAPI request.

        Returns:
            Dict[str, str]: Headers dict with sensitive values replaced by ``[REDACTED]``.
        """
        return {
            key: "[REDACTED]" if key.lower() in self.sensitive_headers else value
            for key, value in request.headers.items()
        }

    async def create_payload(self, request: Request, exception: Exception) -> Dict[str, Any]:
        """Build a detailed payload for logging exceptions.

        Description:
            Constructs a comprehensive payload containing request details (path,
            method, headers, body) and exception information for logging. Headers
            are omitted or redacted based on configuration.

        Args:
            request: The incoming FastAPI request that triggered the exception.
            exception: The exception instance that was raised.

        Returns:
            Dict[str, Any]: JSON-serializable payload for logging.
        """
        body = await self.get_request_body(request)
        data: Dict[str, Any] = {
            "path": request.url.path,
            "method": request.method,
            "query_parameters": request.query_params,
            "path_parameters": request.path_params,
            "body": body,
            **exception_to_dict(exception),
        }
        if self.log_headers:
            data["headers"] = self._redact_headers(request)
        return cast(Dict[str, Any], jsonable_encoder(data))

    async def __call__(self, request: Request, call_next: Callable) -> Response:
        """Process requests and handle any exceptions that occur.

        Description:
            Starlette middleware dispatch method that wraps the request processing
            chain, catching RequestException, ValidationError, and general Exception
            instances, logging them based on configuration, and returning appropriate
            error responses. Paths listed in ``exclude_paths`` are passed through
            without error handling.

        Args:
            request: The incoming FastAPI request.
            call_next: The next middleware or route handler in the chain.

        Returns:
            Response: Either the successful response from call_next or an error response.
        """
        if request.url.path in self.exclude_paths:
            excluded_response: Response = await call_next(request)
            return excluded_response

        controller = f"{__name__}.Runtime.__call__"
        try:
            response: Response = await call_next(request)
        except RequestException as e:
            payload = await self.create_payload(request, e)
            if not e.skip_footprint and self.log_request_errors:
                footprint.leave(
                    log_type="warning",
                    message=e.message,
                    controller=e.controller or controller,
                    subject="Request Error",
                    payload=payload,
                )

            return return_response(
                data=str(e.message),
                status_code=e.status_code,
                response_class=JSONResponse,
            )
        except ValidationError as e:
            if self.log_validation_errors:
                payload = await self.create_payload(request, e)
                footprint.leave(
                    log_type="warning",
                    message=str(e),
                    controller=controller,
                    subject="Validation Error",
                    payload=payload,
                )

            return return_response(
                data=e.errors(),
                status_code=self.validation_status_code,
                response_class=JSONResponse,
            )
        except Exception as e:
            payload = await self.create_payload(request, e)
            try:
                message = str(e)
            except Exception:
                message = "Unrecognized Error has happened."

            if self.log_unexpected_errors:
                footprint.leave(
                    log_type="error",
                    message=message,
                    controller=controller,
                    subject="Unrecognized Error",
                    payload=payload,
                )

            return return_response(
                data=(
                    self.custom_error_message
                    if self.hide_error_messages
                    else message
                ),
                status_code=500,
                response_class=JSONResponse,
            )
        else:
            return response
