"""
Avisos dentro de la aplicación.

No son correos. La persona los ve al entrar, con el número de sin leer al lado
de la entrada del menú, y decide qué mirar. El correo puede venir después
apoyado en la misma tabla; lo que no puede pasar es que la interfaz prometa un
aviso que nadie genera.

La clave de todo esto es `dedupe_key`. Un ciclo diario que corre sobre seis
dominios produce seis avisos por día, y una revalidación de acceso que falla
produce uno por intento: sin una llave que diga «esto ya lo avisé», la campana
queda con cuarenta sin leer en una semana y deja de mirarse. Que la unicidad la
imponga la base y no el código que la escribe es a propósito: un aviso duplicado
por una tarea reintentada es exactamente el caso que el código olvida y el índice
no.
"""

from django.db import models

from apps.core.models import BaseModel


class NotificationKind(models.TextChoices):
    """
    Qué clase de aviso es.

    Cada uno existe porque hay algo que la persona no puede saber sin entrar a
    mirar: que un lote terminó, que un dominio dejó de ser accesible, que la
    credencial de la cuenta no sirve más. Los tres cambian lo que se puede hacer
    con la plataforma.
    """

    BATCH_FINISHED = 'BATCH_FINISHED', 'A batch finished'
    BATCH_FAILED = 'BATCH_FAILED', 'A batch failed'
    ACCESS_LOST = 'ACCESS_LOST', 'Access to a property was lost'
    CREDENTIAL_INVALID = 'CREDENTIAL_INVALID', 'The credential stopped working'
    COVERAGE_DROP = 'COVERAGE_DROP', 'URLs that stopped being indexed'
    #: Los dos van aparte de `COVERAGE_DROP` porque se resuelven en otro lado:
    #: uno en el servidor del sitio y el otro en su `robots.txt`. Un único aviso
    #: de «cambió la cobertura» obligaría a entrar a mirar cuál de los tres fue.
    FETCH_ERRORS = 'FETCH_ERRORS', 'URLs Google could not download'
    CRAWL_BLOCKED = 'CRAWL_BLOCKED', 'URLs blocked by robots.txt'
    EXPORT_READY = 'EXPORT_READY', 'An export is ready'
    #: Quedó un lote de indexación esperando que alguien lo confirme. Existe
    #: porque los caminos que lo crean —un despliegue por la API, un recorrido
    #: de fondo— no tienen a nadie delante: sin este aviso, el trabajo se
    #: acumularía invisible, que es el problema con el que empezó el producto.
    INDEXING_BATCH_READY = 'INDEXING_BATCH_READY', 'An indexing batch is waiting'


class NotificationLevel(models.TextChoices):
    """
    Cuánta atención pide.

    No es decoración: decide el orden en que se leen y si el número del menú
    tiene que resaltar. `ACTION` es «esto no se arregla solo».

    **Una palabra cada uno.** Estos rótulos se dibujan como badge al lado del
    título de cada aviso, y un badge de tres palabras deja de leerse de un
    vistazo y le roba el renglón al título, que es lo que se vino a leer.
    """

    INFO = 'INFO', 'Info'
    ACTION = 'ACTION', 'Action'


class Notification(BaseModel):
    """
    Un aviso para una cuenta.

    **Lo que se guarda son los datos del hecho, no su redacción.** Un aviso es el
    registro de algo que pasó en un momento, y eso sigue valiendo: por eso se
    guardan `text_key` y `params` —cuántas URLs, qué dominio, con qué cifras— y
    no el estado actual del objeto. Un aviso de «se perdió el acceso» sigue
    diciendo lo mismo después de recuperarlo, porque sus datos quedaron
    congelados con él.

    Lo que cambió es que la **frase** se arma al leerla, con el catálogo del
    idioma de quien la lee. Guardarla escrita ataba cada aviso al idioma que
    tenía la cuenta el día que ocurrió, y una lista de avisos mitad en español y
    mitad en inglés no la puede leer nadie.
    """

    account = models.ForeignKey(
        'accounts.Account', on_delete=models.CASCADE, related_name='notifications'
    )
    # El dominio es opcional: la credencial rota es de la cuenta, no de una
    # propiedad. Se guarda para poder filtrar y para que el aviso sepa adónde
    # llevar.
    domain = models.ForeignKey(
        'domains.Domain',
        on_delete=models.CASCADE,
        related_name='notifications',
        null=True,
        blank=True,
    )
    kind = models.CharField(max_length=30, choices=NotificationKind.choices)
    level = models.CharField(
        max_length=10, choices=NotificationLevel.choices, default=NotificationLevel.INFO
    )
    #: Qué frase le corresponde, en el catálogo del cliente.
    #:
    #: Es más fina que `kind`: un lote terminado y uno terminado con pendientes
    #: son el mismo `BATCH_FINISHED` y no dicen lo mismo. `LEGACY` es la de los
    #: avisos que se escribieron antes de esto y llevan su texto en `params`.
    text_key = models.CharField(max_length=40, default='LEGACY')
    #: Los datos que la frase necesita: cuántas URLs, con qué cifras cerró el
    #: lote, de qué dominio se habla. Se congelan con el aviso.
    params = models.JSONField(default=dict, blank=True)
    #: Adónde lleva. Se guarda resuelta porque el destino de un aviso viejo tiene
    #: que seguir siendo el mismo aunque el objeto haya cambiado de estado.
    action_path = models.CharField(max_length=255, blank=True, default='')
    #: Qué hecho representa. Dos avisos con la misma llave son el mismo hecho.
    dedupe_key = models.CharField(max_length=255)
    read_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ['-created_at']
        constraints = [
            models.UniqueConstraint(
                fields=['account', 'dedupe_key'], name='one_notification_per_fact_and_account'
            )
        ]
        indexes = [models.Index(fields=['account', 'read_at'])]

    def __str__(self) -> str:
        return f'{self.kind} {self.account_id}'

    @property
    def is_read(self) -> bool:
        return self.read_at is not None
