"""
Validacion de numeros de telefono.

Los serializers anteriores aceptaban `serializers.CharField()` sin mas, asi
que cualquier cadena llegaba a Twilio: un texto vacio, un correo electronico o
un numero con formato local. Twilio lo rechazaba con un error que el `except:`
desnudo convertia en "Invalid data", sin decir cual de los destinatarios
fallaba ni por que.

Ahora se exige E.164, que es el unico formato que Twilio acepta sin ambiguedad:
un `+`, el codigo de pais y el numero nacional, sin espacios ni separadores.
"""

from __future__ import annotations

import re

from django.conf import settings
from rest_framework import serializers

#: E.164: `+`, un digito 1-9 y hasta 14 digitos mas.
_E164 = re.compile(r"^\+[1-9]\d{6,14}$")

#: Caracteres de presentacion que la gente escribe y que se pueden quitar sin
#: cambiar el numero.
_SEPARADORES = re.compile(r"[\s\-().]")


def normalize_phone(value: str) -> str:
    """
    Normaliza un numero a E.164 y lo valida.

    Se aceptan los separadores habituales (`+52 (55) 1234-5678`) porque
    escribirlos es normal y quitarlos no cambia el numero. Lo que no se acepta
    es un numero sin codigo de pais: adivinarlo llevaria a mandar mensajes al
    pais equivocado.
    """
    if not isinstance(value, str):
        raise serializers.ValidationError("El numero debe ser una cadena de texto.")

    limpio = _SEPARADORES.sub("", value.strip())

    if limpio and not limpio.startswith("+"):
        prefijo = settings.DEFAULT_PHONE_COUNTRY_CODE
        if not prefijo:
            raise serializers.ValidationError(
                f"El numero '{value}' necesita codigo de pais, por ejemplo "
                "+525512345678."
            )
        limpio = f"{prefijo}{limpio.lstrip('0')}"

    if not _E164.match(limpio):
        raise serializers.ValidationError(
            f"El numero '{value}' no tiene un formato E.164 valido "
            "(ejemplo: +525512345678)."
        )

    return limpio


class PhoneNumberField(serializers.CharField):
    """`CharField` que normaliza y valida el numero en formato E.164."""

    def __init__(self, **kwargs):
        kwargs.setdefault("max_length", 20)
        super().__init__(**kwargs)

    def to_internal_value(self, data):
        return normalize_phone(super().to_internal_value(data))


class PhoneListField(serializers.ListField):
    """
    Lista de numeros, tolerante con el formato de entrada.

    Acepta una lista, una cadena con una lista JSON dentro o una cadena
    separada por comas, igual que la lista de direcciones de correo.
    """

    child = PhoneNumberField()

    def to_internal_value(self, data):
        if isinstance(data, str):
            data = _dividir(data)
        elif (
            isinstance(data, (list, tuple))
            and len(data) == 1
            and isinstance(data[0], str)
            and ("," in data[0] or data[0].strip().startswith("["))
        ):
            data = _dividir(data[0])

        numeros = super().to_internal_value(data)

        # Duplicados fuera: mandar dos veces el mismo SMS al mismo numero se
        # cobra dos veces y molesta al destinatario.
        vistos: set[str] = set()
        unicos = []
        for numero in numeros:
            if numero not in vistos:
                vistos.add(numero)
                unicos.append(numero)

        maximo = settings.MESSAGING_MAX_RECIPIENTS
        if len(unicos) > maximo:
            raise serializers.ValidationError(
                f"El maximo es {maximo} destinatarios por peticion y se enviaron "
                f"{len(unicos)}."
            )

        return unicos


def _dividir(value: str) -> list[str]:
    import json

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