"""
Global Exception Handlers for the Knowhere API.

=============================================================================
SECURITY: THE "4xx vs 5xx" MESSAGE PATTERN
=============================================================================

This module enforces the dual-message pattern for all exceptions:

    - `internal_message`: Technical details for LOGS ONLY. NEVER sent to client.
    - `user_message`:     Safe message for CLIENT. ALWAYS sent to user.

The `knowhere_exception_handler` is the central point that:
    1. Logs `internal_message` for debugging (server-side only)
    2. Returns `user_message` to the client (via to_dict)
    3. NEVER leaks internal_message to the response

=============================================================================

All exceptions are converted to KnowhereException and handled uniformly.
This ensures clients always receive a consistent, secure JSON response.

Architecture:
    1. Each handler converts its exception type to a KnowhereException subclass
    2. All handlers delegate to `knowhere_exception_handler` for actual response
    3. Internal details are logged but NEVER sent to client

Response Format:
    {
        "success": false,
        "error": {
            "code": "INVALID_ARGUMENT",
            "message": "<user_message>",  // NEVER internal_message
            "request_id": "req_abc123",
            "details": {...}  // Optional, schema varies by exception type
        }
    }

Exception Sources:
    - KnowhereException: Raised explicitly by our code (domain exceptions)
    - HTTPException: Raised by FastAPI/Starlette for HTTP-level errors
        - 401: Missing/invalid auth header
        - 403: Permission denied by middleware
        - 404: Route not found
        - 405: Method not allowed
    - RequestValidationError: Raised by Pydantic when request body/params invalid
    - Exception: Unexpected errors (bugs, syntax errors, external failures)
"""

import uuid
from collections.abc import Awaitable, Callable
from typing import List, cast

from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from loguru import logger
from starlette.exceptions import HTTPException as StarletteHTTPException

from shared.core.exceptions import (
    KnowhereException,
    UnknownException,
    ValidationException,
)
from shared.core.exceptions.domain_exceptions import RateLimitException, Violation
from shared.core.logging import LogEvent
from shared.core.response import ErrorCodeMapper


def _get_request_id(request: Request) -> str:
    """Extract request ID from state/header or generate one for tracing."""
    state_request_id = getattr(request.state, "request_id", None)
    if isinstance(state_request_id, str) and state_request_id:
        return state_request_id

    header_request_id = request.headers.get("X-Request-ID")
    if header_request_id:
        request.state.request_id = header_request_id
        return header_request_id

    generated_request_id = str(uuid.uuid4())
    request.state.request_id = generated_request_id
    logger.bind(
        event=LogEvent.CORRELATION_REQUEST_ID_MISSING.value,
        request_id=generated_request_id,
    ).info("Request ID generated (missing upstream ID)")
    return generated_request_id


async def knowhere_exception_handler(
    request: Request, exc: KnowhereException
) -> JSONResponse:
    """
    This handler enforces the separation between:
    - `internal_message`: Logged for debugging (NEVER in response)
    - `user_message`: Returned to client (via to_client)

    The response ONLY contains `user_message` via exc.to_client().
    The logs contain full exception details via exc.logging().

    ==========================================================================

    This is the ONLY place that builds the actual response.
    All other handlers convert their exceptions and delegate here.

    Logging:
        - Uses exc.logging() which automatically includes context (request_id, etc.)
        - 5xx: ERROR level with internal_message and stack trace
        - 4xx: WARNING level with user_message

    The `original_exception` field is used to:
        1. Log the underlying cause for debugging (e.g., Redis timeout)
        2. Include stack trace in logs without exposing to client
        3. Wrap unexpected exceptions while preserving debug info
    """
    request_id = _get_request_id(request)

    # Use canonical logging method and force request_id presence in logs
    exc.logging(request_id=request_id)

    # Always include request ID header for client-side correlation
    headers = {"X-Request-ID": request_id}
    retry_after = exc.details.get("retry_after")
    if retry_after:
        headers["Retry-After"] = str(retry_after)

    # Add rate limit headers when the exception is a RateLimitException
    if isinstance(exc, RateLimitException):
        details = exc.details or {}
        if "limit" in details:
            headers["X-RateLimit-Limit"] = str(details["limit"])
        if "remaining" in details:
            headers["X-RateLimit-Remaining"] = str(details["remaining"])
        if "reset" in details:
            headers["X-RateLimit-Reset"] = str(details["reset"])
        if "retry_after" in details:
            headers["Retry-After"] = str(details["retry_after"])
        if "period" in details:
            headers["X-RateLimit-Period"] = str(details["period"])

    # SECURITY: to_client() returns user_message, NEVER internal_message
    return JSONResponse(
        status_code=exc.http_status_code,
        content=exc.to_client(request_id),
        headers=headers,
    )


async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
    """
    Convert FastAPI/Starlette HTTPException to KnowhereException.

    When does HTTPException occur?
        - 401: Auth middleware rejects request (missing/invalid token)
        - 403: Permission check fails
        - 404: No route matches the path
        - 405: HTTP method not allowed for this route
        - 422: Handled separately by validation_exception_handler

    Security: HTTPException.detail may contain sensitive info from middleware.
    We use a generic user_message and log the original detail internally.
    """
    # Convert HTTP status to ErrorCode using the canonical mapping
    code = ErrorCodeMapper.get_error_code_from_http_status(exc.status_code)

    # Check for specific FastAPI Users error codes in detail
    detail_str = str(exc.detail) if exc.detail else ""

    # Map FastAPI Users error codes to user-friendly messages
    fastapi_users_messages = {
        "REGISTER_USER_ALREADY_EXISTS": "This email is already registered. Please log in or use a different email.",
        "LOGIN_BAD_CREDENTIALS": "Invalid email or password.",
        "LOGIN_USER_NOT_VERIFIED": "Please verify your email before logging in.",
        "RESET_PASSWORD_BAD_TOKEN": "Password reset link is invalid or expired.",
        "VERIFY_USER_BAD_TOKEN": "Email verification link is invalid or expired.",
        "VERIFY_USER_ALREADY_VERIFIED": "Your email is already verified.",
    }

    # Try to match FastAPI Users error code
    user_message = None
    for error_code, message in fastapi_users_messages.items():
        if error_code in detail_str:
            user_message = message
            break

    # Generic safe messages for each status (fallback)
    if user_message is None:
        safe_messages = {
            400: "Bad request",
            401: "Authentication required",
            403: "Permission denied",
            404: "Resource not found",
            405: "Method not allowed",
            409: "Conflict",
            429: "Too many requests",
            500: "Internal server error",
            502: "Bad gateway",
            503: "Service unavailable",
            504: "Gateway timeout",
        }
        user_message = safe_messages.get(exc.status_code, "An error occurred")

    # Create KnowhereException with internal_message for logs, user_message for response
    knowhere_exc = KnowhereException(
        code=code,
        internal_message=f"HTTPException detail: {exc.detail}",  # For logs
        user_message=user_message,  # For client
    )

    # Delegate to central handler
    return await knowhere_exception_handler(request, knowhere_exc)


async def validation_exception_handler(
    request: Request, exc: RequestValidationError
) -> JSONResponse:
    """
    Convert Pydantic validation errors to ValidationException.

    When does RequestValidationError occur?
        - Request body doesn't match Pydantic model
        - Query/path parameters fail validation
        - Type coercion fails (e.g., string where int expected)

    The violations array IS safe to expose as it describes client input issues.
    This is a 4xx error, so user_message is passed directly to client.
    """
    # Transform Pydantic errors into violations format
    violations: List[Violation] = []
    for error in exc.errors():
        field = ".".join(str(loc) for loc in error.get("loc", []))
        violations.append(
            {
                "field": field,
                "description": error.get("msg", "Validation failed"),
            }
        )

    # Create ValidationException with user_message that client will see
    validation_exc = ValidationException(
        user_message="Request validation failed",
        violations=violations,
    )

    # Delegate to central handler
    return await knowhere_exception_handler(request, validation_exc)


async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse:
    """
    Catch-all for unexpected exceptions.

    When does this occur?
        - Syntax errors, bugs in code
        - Unhandled third-party library exceptions
        - Database/Redis connection failures not wrapped by our code

    Security: NEVER expose exception details to client.
    The `original_exception` is stored for internal logging only.
    UnknownException auto-generates a safe user_message.
    """
    # Wrap in UnknownException - this logs internally, returns generic message
    unknown_exc = UnknownException(original_exception=exc)

    # Delegate to central handler
    return await knowhere_exception_handler(request, unknown_exc)


async def _knowhere_exception_handler_adapter(
    request: Request, exc: Exception
) -> JSONResponse:
    """Adapt the typed Knowhere handler to FastAPI's generic signature."""
    return await knowhere_exception_handler(request, cast(KnowhereException, exc))


async def _http_exception_handler_adapter(
    request: Request, exc: Exception
) -> JSONResponse:
    """Adapt the HTTP handler to FastAPI's generic signature."""
    return await http_exception_handler(request, cast(HTTPException, exc))


async def _validation_exception_handler_adapter(
    request: Request, exc: Exception
) -> JSONResponse:
    """Adapt the validation handler to FastAPI's generic signature."""
    return await validation_exception_handler(request, cast(RequestValidationError, exc))


def setup_exception_handlers(app: FastAPI) -> None:
    """
    Register all exception handlers with the FastAPI app.

    Order of registration doesn't matter - FastAPI matches by exception type.
    More specific types (KnowhereException subclasses) are matched before base.
    """
    # KnowhereException and all subclasses
    app.add_exception_handler(KnowhereException, _knowhere_exception_handler_adapter)

    # FastAPI/Starlette HTTP exceptions (convert then delegate)
    http_handler: Callable[[Request, Exception], Awaitable[JSONResponse]] = (
        _http_exception_handler_adapter
    )
    app.add_exception_handler(HTTPException, http_handler)
    app.add_exception_handler(StarletteHTTPException, http_handler)

    # Pydantic validation errors (convert then delegate)
    app.add_exception_handler(
        RequestValidationError, _validation_exception_handler_adapter
    )

    # Catch-all for unexpected exceptions (MUST be last conceptually)
    app.add_exception_handler(Exception, general_exception_handler)

    logger.info(
        "Global exception handlers registered (4xx/5xx message pattern enabled)"
    )
