"""Lotes de trabajo y el presupuesto diario de cuota que consumen."""

from django.db import models

from apps.core.models import BaseModel


class BatchKind(models.TextChoices):
    SITEMAP_SYNC = 'SITEMAP_SYNC', 'Sitemap sync'
    URL_INSPECTION = 'URL_INSPECTION', 'URL inspection'
    # La exportación grande es un lote como los otros dos, y no un caso aparte,
    # porque tiene lo mismo que ellos: se encola, informa progreso, puede fallar
    # y termina. Darle su propia mecánica habría significado una segunda forma
    # de mirar trabajo asíncrono en un producto que ya tiene una (RT-13).
    COVERAGE_EXPORT = 'COVERAGE_EXPORT', 'Coverage export'
    # Pedirle a Google que mire una dirección. Es la otra mitad del producto:
    # los otros tres lotes leen lo que Google sabe, y éste le pide algo.
    URL_INDEXING = 'URL_INDEXING', 'URL indexing'


class BatchOrigin(models.TextChoices):
    SCHEDULED = 'SCHEDULED', 'Scheduled'
    MANUAL = 'MANUAL', 'Manual'
    API = 'API', 'API'


class BatchState(models.TextChoices):
    # Existe y espera a una persona. Es lo contrario de `QUEUED`, que significa
    # «encolado, va a correr solo»: un borrador no sale a Google hasta que
    # alguien lo confirma, y ésa es la garantía que sostiene los tres caminos
    # de la indexación. `run_batch()` es el único paso de DRAFT a QUEUED.
    DRAFT = 'DRAFT', 'Draft'
    QUEUED = 'QUEUED', 'Queued'
    RUNNING = 'RUNNING', 'Running'
    COMPLETED = 'COMPLETED', 'Completed'
    PARTIAL = 'PARTIAL', 'Completed with pending items'
    FAILED = 'FAILED', 'Failed'


TERMINAL_BATCH_STATES = frozenset({BatchState.COMPLETED, BatchState.PARTIAL, BatchState.FAILED})


class Batch(BaseModel):
    """
    Una corrida de trabajo sobre un dominio.

    PARTIAL no es un adorno: es obligatorio cuando el trabajo se cortó por
    cuota. Decir COMPLETED cuando quedaron URLs sin consultar convierte un
    resultado parcial en una afirmación falsa sobre la cobertura del sitio, que
    es precisamente lo que el principio I prohíbe.
    """

    domain = models.ForeignKey('domains.Domain', on_delete=models.CASCADE, related_name='batches')
    kind = models.CharField(max_length=20, choices=BatchKind.choices)
    origin = models.CharField(max_length=20, choices=BatchOrigin.choices)
    state = models.CharField(max_length=20, choices=BatchState.choices, default=BatchState.QUEUED)
    total_items = models.PositiveIntegerField(default=0)
    processed_items = models.PositiveIntegerField(default=0)
    failed_items = models.PositiveIntegerField(default=0)
    quota_consumed = models.PositiveIntegerField(default=0)
    started_at = models.DateTimeField(null=True, blank=True)
    finished_at = models.DateTimeField(null=True, blank=True)
    # Admite nulo, no cadena vacía: la restricción de unicidad tiene que poder
    # distinguir «este lote no trajo clave» de «trajo una clave vacía», y varias
    # cadenas vacías chocarían entre sí.
    idempotency_key = models.CharField(max_length=255, null=True, blank=True)  # noqa: DJ001
    summary = models.JSONField(default=dict, blank=True)

    class Meta:
        ordering = ['-created_at']
        constraints = [
            models.UniqueConstraint(
                fields=['domain', 'idempotency_key'],
                condition=models.Q(idempotency_key__isnull=False),
                name='one_idempotency_key_per_domain',
            )
        ]
        indexes = [models.Index(fields=['domain', 'state'])]

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

    @property
    def is_terminal(self) -> bool:
        return self.state in TERMINAL_BATCH_STATES


class QuotaBudget(BaseModel):
    """
    Cupo de un dominio para un día, con el gasto ya realizado.

    Los límites se copian del dominio al crear la fila en vez de leerse de él en
    cada consulta: si mañana se sube el cupo, el día de ayer tiene que seguir
    contando lo que contaba, o el historial deja de explicar por qué un lote
    terminó parcial.
    """

    domain = models.ForeignKey(
        'domains.Domain', on_delete=models.CASCADE, related_name='quota_budgets'
    )
    date = models.DateField()
    limit_total = models.PositiveIntegerField()
    manual_reserve = models.PositiveIntegerField()
    used_automatic = models.PositiveIntegerField(default=0)
    used_manual = models.PositiveIntegerField(default=0)

    class Meta:
        ordering = ['-date']
        constraints = [
            models.UniqueConstraint(fields=['domain', 'date'], name='one_budget_per_day')
        ]

    def __str__(self) -> str:
        return f'{self.domain_id} {self.date}'

    @property
    def automatic_remaining(self) -> int:
        """Lo que le queda al trabajo automático, sin poder tocar la reserva manual."""
        return max(self.limit_total - self.manual_reserve - self.used_automatic, 0)

    @property
    def manual_remaining(self) -> int:
        """
        Lo que le queda al trabajo manual.

        El manual sí puede usar lo que el automático no gastó: la reserva existe
        para garantizarle un piso, no para ponerle un techo.
        """
        return max(self.limit_total - self.used_automatic - self.used_manual, 0)
