"""
Manejo de errores de la API.

El codigo anterior envolvia cada vista en un `try/except:` desnudo que
devolvia siempre 400 con el mensaje "Invalid data.". Eso ocultaba fallos
reales (base de datos caida, credenciales del proveedor caducadas, errores
de programacion) haciendolos parecer errores del cliente, y no dejaba ningun
rastro para diagnosticarlos.

Aqui se centraliza el tratamiento: cada error se traduce a una respuesta con
la misma forma, se registra con su identificador de correlacion, y los
detalles internos nunca salen al cliente.
"""

from __future__ import annotations

import logging

from django.core.exceptions import (
    PermissionDenied,
    RequestDataTooBig,
    TooManyFieldsSent,
)
from django.http import Http404
from rest_framework import status
from rest_framework.exceptions import APIException
from rest_framework.response import Response
from rest_framework.views import exception_handler as drf_exception_handler

from core.logging_utils import request_id_var

logger = logging.getLogger("notifications.errors")


class ServiceUnavailable(APIException):
    """El proveedor de correo no esta disponible o rechazo la peticion."""

    status_code = status.HTTP_503_SERVICE_UNAVAILABLE
    default_detail = "El proveedor de correo no esta disponible en este momento."
    default_code = "provider_unavailable"


class ProviderError(APIException):
    """El proveedor rechazo el mensaje por un motivo atribuible al contenido."""

    status_code = status.HTTP_502_BAD_GATEWAY
    default_detail = "El proveedor de correo rechazo el mensaje."
    default_code = "provider_error"


class ProviderNotConfigured(APIException):
    status_code = status.HTTP_503_SERVICE_UNAVAILABLE
    default_detail = "El proveedor solicitado no esta configurado en este entorno."
    default_code = "provider_not_configured"


def _error_code(exc, response) -> str:
    """Obtiene un codigo de error estable y legible para el cliente."""
    detail = getattr(exc, "detail", None)
    code = getattr(detail, "code", None)
    if isinstance(code, str):
        return code
    code = getattr(exc, "default_code", None)
    if isinstance(code, str):
        return code
    return {
        400: "bad_request",
        401: "unauthenticated",
        403: "permission_denied",
        404: "not_found",
        405: "method_not_allowed",
        413: "payload_too_large",
        415: "unsupported_media_type",
        429: "throttled",
    }.get(response.status_code, "error")


def _message_and_details(exc, response) -> tuple[str, dict | list | None]:
    """
    Separa un mensaje corto de los detalles por campo.

    Los errores de validacion de DRF llegan como un diccionario
    {campo: [mensajes]}; se devuelve tal cual en `details` para que el cliente
    pueda senalar el campo concreto.
    """
    data = response.data
    if isinstance(data, dict) and "detail" in data and len(data) == 1:
        return str(data["detail"]), None
    if isinstance(data, dict):
        return "La peticion contiene datos invalidos.", data
    if isinstance(data, list):
        return "La peticion contiene datos invalidos.", data
    return str(data), None


def api_exception_handler(exc, context):
    """
    Manejador de excepciones de DRF.

    Devuelve siempre la misma estructura:

        {
          "status": "error",
          "error": {"code": "...", "message": "...", "details": {...}},
          "request_id": "..."
        }
    """
    if isinstance(exc, Http404):
        exc = APIException(detail="Recurso no encontrado.", code="not_found")
        exc.status_code = status.HTTP_404_NOT_FOUND
    elif isinstance(exc, PermissionDenied):
        exc = APIException(detail="No tienes permiso para esta operacion.")
        exc.status_code = status.HTTP_403_FORBIDDEN
    elif isinstance(exc, RequestDataTooBig):
        # Django las lanza al superar DATA_UPLOAD_MAX_MEMORY_SIZE y no son
        # excepciones de DRF, asi que salian como 500: un limite de tamano que
        # funciona, contado como fallo del servidor. Ademas de confundir a
        # quien llama, ensucia la monitorizacion y tapa incidentes reales.
        exc = APIException(detail="La peticion es demasiado grande.")
        exc.status_code = status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
    elif isinstance(exc, TooManyFieldsSent):
        exc = APIException(detail="La peticion tiene demasiados campos.")
        exc.status_code = status.HTTP_400_BAD_REQUEST

    response = drf_exception_handler(exc, context)
    request = context.get("request")
    request_id = getattr(request, "request_id", None) or request_id_var.get()
    view = context.get("view")
    view_name = type(view).__name__ if view else "-"

    if response is None:
        # Excepcion no controlada: es un fallo del servicio, no del cliente.
        # Se registra completa y se responde con un mensaje generico para no
        # filtrar rutas de archivos, consultas SQL ni credenciales.
        logger.exception(
            "Error no controlado en %s: %s",
            view_name,
            exc.__class__.__name__,
            extra={"view": view_name, "request_id": request_id},
        )
        return Response(
            {
                "status": "error",
                "error": {
                    "code": "internal_error",
                    "message": "Ocurrio un error interno al procesar la peticion.",
                    "details": None,
                },
                "request_id": request_id,
            },
            status=status.HTTP_500_INTERNAL_SERVER_ERROR,
        )

    code = _error_code(exc, response)
    message, details = _message_and_details(exc, response)

    log = logger.warning if response.status_code < 500 else logger.error
    log(
        "Peticion rechazada en %s: %s (%s)",
        view_name,
        code,
        response.status_code,
        extra={
            "view": view_name,
            "request_id": request_id,
            "status_code": response.status_code,
            "error_code": code,
        },
    )

    payload = {
        "status": "error",
        "error": {"code": code, "message": message, "details": details},
        "request_id": request_id,
    }

    # `Retry-After` en respuestas 429: DRF la calcula en `exc.wait`.
    wait = getattr(exc, "wait", None)
    if wait:
        payload["error"]["retry_after_seconds"] = int(wait)

    response.data = payload
    return response
