"""Sitemaps de un dominio y las URLs que declaran."""

from django.db import models

from apps.core.models import BaseModel


class SitemapKind(models.TextChoices):
    INDEX = 'INDEX', 'Sitemap index'
    URLSET = 'URLSET', 'URL list'


class SitemapSource(models.TextChoices):
    DECLARED = 'DECLARED', 'Registered by hand'
    DISCOVERED = 'DISCOVERED', 'Found in an index'


class SubmitResult(models.TextChoices):
    OK = 'OK', 'Submitted'
    FAILED = 'FAILED', 'Failed'
    SKIPPED_UNCHANGED = 'SKIPPED_UNCHANGED', 'Unchanged, not resubmitted'


class SitemapError(models.TextChoices):
    """
    Por qué falló lo último que se intentó con el sitemap.

    Son cuatro códigos y no un «falló» genérico porque se arreglan en lugares
    distintos: la dirección que se escribió, el archivo que publica el sitio, el
    sitio al que ese archivo pertenece y la propiedad de Search Console. La
    pantalla necesita distinguirlos sin interpretar la prosa del mensaje, que
    trae el dato concreto —el 404, la etiqueta que faltaba— pero no la clase de
    problema.
    """

    UNREACHABLE = 'UNREACHABLE', 'Could not be downloaded'
    MALFORMED = 'MALFORMED', 'Not a valid sitemap'
    #: Ninguna de las URLs que declara pertenece a la propiedad: es el sitemap
    #: de otro sitio. Que declare **algunas** ajenas no es esto: eso se descarta
    #: con un aviso y el archivo sigue sirviendo.
    FOREIGN_URLS = 'FOREIGN_URLS', 'Declares URLs from another site'
    SUBMIT_FAILED = 'SUBMIT_FAILED', 'Submitting to Google failed'


class Sitemap(BaseModel):
    """
    Un sitemap del dominio, registrado a mano o descubierto dentro de un índice.

    `content_hash` es lo que evita reenviar a Google lo mismo todos los días. No
    es una optimización de red: reenviar un sitemap idéntico no le aporta nada a
    Google y sí gasta el presupuesto del dominio, que después falta para lo que
    importa.
    """

    domain = models.ForeignKey('domains.Domain', on_delete=models.CASCADE, related_name='sitemaps')
    parent = models.ForeignKey(
        'self', on_delete=models.CASCADE, null=True, blank=True, related_name='children'
    )
    location = models.URLField(max_length=2000)
    kind = models.CharField(max_length=10, choices=SitemapKind.choices, default=SitemapKind.URLSET)
    source = models.CharField(
        max_length=12, choices=SitemapSource.choices, default=SitemapSource.DECLARED
    )
    url_count = models.PositiveIntegerField(default=0)
    content_hash = models.CharField(max_length=64, blank=True, default='')
    last_read_at = models.DateTimeField(null=True, blank=True)
    last_submitted_at = models.DateTimeField(null=True, blank=True)
    # Huella del contenido en el momento del último envío exitoso. Se guarda
    # aparte de `content_hash` porque son dos preguntas distintas: qué dice hoy
    # el sitemap, y qué decía la última vez que Google lo recibió.
    submitted_content_hash = models.CharField(max_length=64, blank=True, default='')
    last_submit_result = models.CharField(
        max_length=20, choices=SubmitResult.choices, blank=True, default=''
    )
    last_error = models.TextField(blank=True, default='')
    last_error_code = models.CharField(
        max_length=20, choices=SitemapError.choices, blank=True, default=''
    )
    # URLs que el archivo declara y no pertenecen a la propiedad del dominio.
    #
    # Tiene campo propio y no se mezcla con `last_error` porque no es una falla
    # del sitemap: se leyó bien, se envió bien, y sólo parte de lo que declara
    # es de otro sitio. Contarlo como error escondería que el resto sí se
    # registró, y guardarlo únicamente en el resumen del lote lo haría
    # desaparecer del renglón que tiene que explicarlo.
    foreign_url_count = models.PositiveIntegerField(default=0)

    class Meta:
        ordering = ['location']
        constraints = [
            models.UniqueConstraint(
                fields=['domain', 'location'], name='one_sitemap_per_location_and_domain'
            )
        ]

    def __str__(self) -> str:
        return self.location

    @property
    def needs_submit(self) -> bool:
        """
        Si lo que dice ahora difiere de lo que Google recibió la última vez.

        Sin haberlo leído todavía no se puede afirmar que cambió: en ese caso no
        se reenvía, se lee primero.
        """
        if not self.content_hash:
            return False
        return self.content_hash != self.submitted_content_hash


class CoverageState(models.TextChoices):
    """
    Estado de indexación, traducido a un vocabulario estable.

    `UNKNOWN` sólo existe acá, en la URL, y nunca en un registro de cobertura:
    un registro siempre proviene de una respuesta real de Google, mientras que
    una URL puede estar sin consultar. Confundir «no lo sabemos» con «no está
    indexada» es la mentira más fácil de cometer en este producto (principio I).

    Las etiquetas espejan `coverage.<CODE>.label` del catálogo, que es de donde
    salen las palabras que se leen en el producto. Éstas son las del panel
    interno, y la única que llega a alguien de afuera es la del CSV, que se
    traduce con el catálogo y no con esta columna.
    """

    UNKNOWN = 'UNKNOWN', 'Not checked yet'
    INDEXED = 'INDEXED', 'Indexed'
    CRAWLED_NOT_INDEXED = 'CRAWLED_NOT_INDEXED', 'Crawled, not indexed'
    DISCOVERED_NOT_INDEXED = 'DISCOVERED_NOT_INDEXED', 'Discovered, not crawled'
    DUPLICATE_CANONICAL = 'DUPLICATE_CANONICAL', 'Duplicate with another canonical'
    EXCLUDED_NOINDEX = 'EXCLUDED_NOINDEX', 'Excluded by noindex'
    BLOCKED_ROBOTS = 'BLOCKED_ROBOTS', 'Blocked by robots.txt'
    FETCH_ERROR = 'FETCH_ERROR', 'Could not be fetched'
    REDIRECT = 'REDIRECT', 'Redirects to another URL'
    OTHER_NOT_INDEXED = 'OTHER_NOT_INDEXED', 'Not indexed, another reason'


class Url(BaseModel):
    """
    Una URL vista en un sitemap del dominio.

    `coverage_state` y `last_checked_at` son una copia del último registro de
    cobertura, para poder filtrar sin unir tablas. La copia obliga a un
    invariante: si `last_checked_at` es nulo, el estado tiene que ser `UNKNOWN`.
    Romperlo significaría mostrar como dato lo que nunca se consultó.
    """

    domain = models.ForeignKey('domains.Domain', on_delete=models.CASCADE, related_name='urls')
    sitemap = models.ForeignKey(
        Sitemap, on_delete=models.SET_NULL, null=True, blank=True, related_name='urls'
    )
    loc = models.URLField(max_length=2000)
    # No se borra la fila cuando desaparece del sitemap: su historial de
    # cobertura sigue siendo cierto y sirve para explicar qué pasó después.
    in_sitemap = models.BooleanField(default=True)
    first_seen_at = models.DateTimeField(auto_now_add=True)
    last_seen_in_sitemap_at = models.DateTimeField(null=True, blank=True)
    coverage_state = models.CharField(
        max_length=25, choices=CoverageState.choices, default=CoverageState.UNKNOWN
    )
    last_checked_at = models.DateTimeField(null=True, blank=True)
    #: Cuándo dice el **sitio** que cambió esta página, según el `<lastmod>` de
    #: su sitemap. Nulo cuando el sitemap no lo declara, que es lo normal.
    #:
    #: Es una afirmación del sitio y no una comprobación nuestra: nadie verifica
    #: que el archivo haya cambiado de verdad ese día. Sirve para dos cosas
    #: concretas y para ninguna más: saber sobre qué dirección tiene sentido
    #: pedir una reindexación, y contestar «¿Google la volvió a ver después de
    #: mi cambio?» comparándolo contra `last_crawl_time` del historial.
    lastmod = models.DateTimeField(null=True, blank=True)
    #: Cuándo notamos que ese `lastmod` cambió respecto del que teníamos.
    #:
    #: Va aparte porque son dos preguntas distintas: cuándo dice el sitio que
    #: cambió la página, y cuándo nos enteramos nosotros. Un sitio que publica
    #: una fecha vieja de golpe mueve la primera hacia atrás y la segunda hacia
    #: adelante, y las dos cosas son ciertas.
    lastmod_seen_at = models.DateTimeField(null=True, blank=True)
    priority_score = models.IntegerField(default=0)

    class Meta:
        ordering = ['loc']
        constraints = [models.UniqueConstraint(fields=['domain', 'loc'], name='one_url_per_domain')]
        indexes = [
            models.Index(fields=['domain', 'coverage_state']),
            models.Index(fields=['domain', 'last_checked_at']),
            models.Index(fields=['domain', 'in_sitemap', 'priority_score']),
        ]

    def __str__(self) -> str:
        return self.loc

    @property
    def has_data(self) -> bool:
        return self.last_checked_at is not None
