"""
Serializers del canal de correo.

Dos formas de enviar:

* `MailSendSerializer`: envio general, con control sobre remitente,
  destinatarios, cuerpo, adjuntos y plantillas del proveedor.
* `MailTemplateSerializer`: el contrato que ya usaban los sistemas internos
  (`to`, `to_cco`, `system`, `notification`). Se conserva tal cual para no
  romper a nadie, pero ahora envia directamente en lugar de hacer una peticion
  HTTP a la API `api-mails`.

Se aceptan `application/json` (adjuntos en base64) y `multipart/form-data`
(adjuntos como archivos). En multipart, los archivos del campo `attachments`
son adjuntos normales y cualquier otro campo de archivo se incrusta en el
mensaje usando el nombre del campo como `content_id`.
"""

from __future__ import annotations

import base64
import binascii
import json

from django.conf import settings
from rest_framework import serializers

from notifications.channels.email.base import Attachment
from notifications.channels.email.registry import available_providers
from notifications.serializers.validators import (
    HeaderSafeCharField,
    HeaderSafeEmailField,
    sanitize_filename,
    validate_attachment,
    validate_content_id,
    validate_extra_headers,
    validate_sender,
)


class EmailListField(serializers.ListField):
    """
    Lista de direcciones tolerante con el formato de entrada.

    Acepta una lista JSON, un campo repetido de formulario, una cadena con una
    lista JSON dentro o una cadena separada por comas. Los clientes mandan una
    u otra segun la libreria HTTP que usen, y rechazarlas solo genera friccion
    sin aportar seguridad: la validacion real la hace el campo hijo sobre cada
    direccion.
    """

    child = HeaderSafeEmailField(max_length=254)

    def to_internal_value(self, data):
        if isinstance(data, str):
            data = _split_addresses(data)
        elif (
            isinstance(data, (list, tuple))
            and len(data) == 1
            and isinstance(data[0], str)
        ):
            data = _split_addresses(data[0])

        addresses = super().to_internal_value(data)

        # Duplicados fuera, conservando el orden.
        seen: set[str] = set()
        unique = []
        for address in addresses:
            key = address.lower()
            if key not in seen:
                seen.add(key)
                unique.append(address)
        return unique


def _split_addresses(value: str) -> list[str]:
    value = value.strip()
    if value.startswith("["):
        try:
            parsed = json.loads(value)
        except ValueError:
            raise serializers.ValidationError(
                "La lista de direcciones no es un JSON valido."
            ) from None
        if not isinstance(parsed, list):
            raise serializers.ValidationError("Se esperaba una lista de direcciones.")
        return parsed
    return [part.strip() for part in value.split(",") if part.strip()]


class JSONAttachmentSerializer(serializers.Serializer):
    """Adjunto enviado en base64 dentro de un cuerpo JSON."""

    filename = HeaderSafeCharField(max_length=200)
    content = serializers.CharField(
        help_text="Contenido del archivo codificado en base64."
    )
    inline = serializers.BooleanField(
        default=False,
        help_text="True para mostrarlo dentro del mensaje en lugar de como adjunto.",
    )
    content_id = HeaderSafeCharField(
        required=False,
        allow_blank=True,
        max_length=64,
        help_text='Identificador para referenciarlo en el HTML con "cid:".',
    )

    def validate(self, attrs):
        if attrs.get("inline") and not attrs.get("content_id"):
            raise serializers.ValidationError(
                {"content_id": "Es obligatorio para los adjuntos incrustados."}
            )
        if attrs.get("content_id"):
            validate_content_id(attrs["content_id"])
        return attrs


class AttachmentCollectorMixin:
    """Reune los adjuntos en base64 con los archivos de un envio multipart."""

    def collect_attachments(self, json_attachments) -> tuple[Attachment, ...]:
        collected: list[Attachment] = []
        collected.extend(self._from_json(json_attachments or []))
        collected.extend(self._from_files())

        if len(collected) > settings.EMAIL_MAX_ATTACHMENTS:
            raise serializers.ValidationError(
                {
                    "attachments": (
                        f"El maximo es {settings.EMAIL_MAX_ATTACHMENTS} adjuntos "
                        f"por mensaje y se enviaron {len(collected)}."
                    )
                }
            )

        total = sum(a.size for a in collected)
        if total > settings.EMAIL_MAX_TOTAL_ATTACHMENT_BYTES:
            limite = settings.EMAIL_MAX_TOTAL_ATTACHMENT_BYTES / (1024 * 1024)
            raise serializers.ValidationError(
                {
                    "attachments": (
                        f"El total de adjuntos supera el limite de {limite:.1f} MB."
                    )
                }
            )

        content_ids = [a.content_id for a in collected if a.content_id]
        if len(content_ids) != len(set(content_ids)):
            raise serializers.ValidationError(
                {"attachments": "Hay content_id repetidos entre los adjuntos."}
            )

        return tuple(collected)

    def _from_json(self, items) -> list[Attachment]:
        attachments = []
        for index, item in enumerate(items):
            filename = sanitize_filename(item["filename"])
            try:
                content = base64.b64decode(item["content"], validate=True)
            except (binascii.Error, ValueError):
                raise serializers.ValidationError(
                    {
                        "attachments": (
                            f"El contenido del adjunto {index + 1} ('{filename}') "
                            "no es base64 valido."
                        )
                    }
                ) from None

            mime_type = validate_attachment(filename, content)
            attachments.append(
                Attachment(
                    filename=filename,
                    content=content,
                    content_type=mime_type,
                    content_id=item.get("content_id", ""),
                    inline=bool(item.get("inline")),
                )
            )
        return attachments

    def _from_files(self) -> list[Attachment]:
        request = self.context.get("request")
        files = getattr(request, "FILES", None)
        if not files:
            return []

        attachments = []
        for field_name in files:
            for uploaded in files.getlist(field_name):
                content = uploaded.read()
                filename = sanitize_filename(uploaded.name)
                mime_type = validate_attachment(filename, content)

                if field_name == "attachments":
                    attachments.append(
                        Attachment(
                            filename=filename,
                            content=content,
                            content_type=mime_type,
                        )
                    )
                else:
                    # El nombre del campo es el `cid` con el que el HTML
                    # referencia la imagen.
                    attachments.append(
                        Attachment(
                            filename=filename,
                            content=content,
                            content_type=mime_type,
                            content_id=validate_content_id(field_name),
                            inline=True,
                        )
                    )
        return attachments


class MailSendSerializer(AttachmentCollectorMixin, serializers.Serializer):
    """Peticion de envio de correo."""

    provider = serializers.CharField(
        required=False,
        allow_blank=True,
        help_text="Proveedor a usar. Si se omite se usa el configurado por defecto.",
    )

    from_email = HeaderSafeEmailField(max_length=254)
    from_name = HeaderSafeCharField(
        required=False, allow_blank=True, max_length=128, default=""
    )
    reply_to = HeaderSafeEmailField(
        required=False, allow_blank=True, max_length=254, default=""
    )

    to = EmailListField(allow_empty=False)
    cc = EmailListField(required=False, default=list)
    bcc = EmailListField(required=False, default=list)

    subject = HeaderSafeCharField(
        required=False, allow_blank=True, max_length=255, default=""
    )
    html = serializers.CharField(
        required=False, allow_blank=True, default="", trim_whitespace=False
    )
    text = serializers.CharField(
        required=False, allow_blank=True, default="", trim_whitespace=False
    )

    template_id = HeaderSafeCharField(
        required=False, allow_blank=True, max_length=128, default=""
    )
    template_data = serializers.JSONField(required=False, default=dict)

    categories = serializers.ListField(
        child=HeaderSafeCharField(max_length=64),
        required=False,
        default=list,
        max_length=10,
    )
    headers = serializers.JSONField(required=False, default=dict)

    attachments = JSONAttachmentSerializer(many=True, required=False, default=list)

    send_async = serializers.BooleanField(
        default=True,
        help_text=(
            "True encola el envio y responde 202 de inmediato. False espera al "
            "proveedor y responde con el resultado real."
        ),
    )

    # -- Validaciones por campo --------------------------------------------

    def validate_provider(self, value: str) -> str:
        if not value:
            return ""
        value = value.strip().lower()
        opciones = available_providers()
        if value not in opciones:
            raise serializers.ValidationError(
                f"Proveedor no disponible. Opciones: {', '.join(opciones) or 'ninguna'}."
            )
        return value

    def validate_from_email(self, value: str) -> str:
        request = self.context.get("request")
        return validate_sender(value, getattr(request, "api_client", None))

    def validate_html(self, value: str) -> str:
        return _check_body_size(value, "html")

    def validate_text(self, value: str) -> str:
        return _check_body_size(value, "text")

    def validate_headers(self, value):
        if not value:
            return {}
        if not isinstance(value, dict):
            raise serializers.ValidationError(
                "Las cabeceras deben enviarse como un objeto."
            )
        return validate_extra_headers(value)

    def validate_template_data(self, value):
        if not value:
            return {}
        if not isinstance(value, dict):
            raise serializers.ValidationError(
                "Los datos de la plantilla deben enviarse como un objeto."
            )
        if len(json.dumps(value)) > 64 * 1024:
            raise serializers.ValidationError(
                "Los datos de la plantilla superan el limite de 64 KB."
            )
        return value

    # -- Validacion global --------------------------------------------------

    def validate(self, attrs):
        validate_recipients(attrs)
        self._validate_content(attrs)
        attrs["_attachments"] = self.collect_attachments(attrs.get("attachments"))
        return attrs

    def _validate_content(self, attrs) -> None:
        tiene_cuerpo = bool(attrs.get("html") or attrs.get("text"))
        template_id = attrs.get("template_id")

        if not tiene_cuerpo and not template_id:
            raise serializers.ValidationError(
                "Hay que enviar 'html', 'text' o 'template_id'."
            )
        if not template_id and not attrs.get("subject"):
            raise serializers.ValidationError(
                {"subject": "El asunto es obligatorio salvo que se use una plantilla."}
            )
        if template_id:
            proveedor = attrs.get("provider") or settings.EMAIL_DEFAULT_PROVIDER
            if proveedor == "gmail":
                raise serializers.ValidationError(
                    {
                        "template_id": (
                            "Gmail no admite plantillas del proveedor. Usa SendGrid "
                            "o manda el HTML ya renderizado."
                        )
                    }
                )


class MailTemplateSerializer(serializers.Serializer):
    """
    Contrato heredado del endpoint `/mail/send/template`.

    Se conserva igual que antes para no romper a los sistemas que ya lo usan.
    Lo que cambia es lo de debajo: antes se reenviaba por HTTP a la API
    `api-mails`; ahora se compone y se envia aqui.

    `system` y `notification` se renderizan dentro de una plantilla HTML. El
    contenido llega con etiquetas de formato a proposito (`<b>`, `<em>`), asi
    que no se escapa del todo: se sanea permitiendo solo un conjunto acotado.
    Antes se insertaba con el filtro `|safe`, que no filtra nada y permitia
    inyectar HTML arbitrario en un correo firmado por el dominio de la empresa.
    """

    to = EmailListField(allow_empty=False)
    to_cco = EmailListField(required=False, default=list)
    # `system` acaba dentro del asunto (services/templates.py), asi que no
    # puede llevar saltos de linea: un `\r\n` en una cabecera permite anadir
    # cabeceras propias, y `Bcc:` entre ellas. Era el unico de los tres canales
    # sin esta comprobacion —/mail/send la hace sobre `subject` y el canal de
    # WhatsApp sobre este mismo campo—, y `nh3.clean` no sirve para esto:
    # normaliza `\r\n` a `\n`, pero no lo elimina.
    system = HeaderSafeCharField(max_length=200)
    # `notification` solo se incrusta en el cuerpo, donde los saltos de linea
    # son legitimos.
    notification = serializers.CharField(max_length=5000)

    def validate(self, attrs):
        attrs["cc"] = []
        attrs["bcc"] = list(attrs.get("to_cco") or [])
        validate_recipients(attrs)
        return attrs


def validate_recipients(attrs) -> None:
    """Aplica el limite de destinatarios y elimina los repetidos entre campos."""
    to = attrs.get("to") or []
    cc = attrs.get("cc") or []
    bcc = attrs.get("bcc") or []

    total = len(to) + len(cc) + len(bcc)
    if total > settings.EMAIL_MAX_RECIPIENTS:
        raise serializers.ValidationError(
            {
                "to": (
                    f"El mensaje tiene {total} destinatarios y el maximo por envio "
                    f"es {settings.EMAIL_MAX_RECIPIENTS}."
                )
            }
        )

    # Una direccion repetida entre `to`, `cc` y `bcc` hace que el proveedor
    # entregue el mismo mensaje dos veces al mismo buzon.
    principales = {a.lower() for a in to}
    attrs["cc"] = [a for a in cc if a.lower() not in principales]
    combinadas = principales | {a.lower() for a in attrs["cc"]}
    attrs["bcc"] = [a for a in bcc if a.lower() not in combinadas]


def _check_body_size(value: str, campo: str) -> str:
    if len(value.encode("utf-8")) > settings.EMAIL_MAX_BODY_BYTES:
        limite = settings.EMAIL_MAX_BODY_BYTES / 1024
        raise serializers.ValidationError(
            f"El cuerpo '{campo}' supera el limite de {limite:.0f} KB."
        )
    return value
