"""Trabajo programado de sitemaps."""

import logging

from celery import shared_task

from apps.core.errors import ApiError
from apps.domains.models import AccessState, Domain
from apps.jobs.models import BatchOrigin
from apps.sitemaps.services import sync_domain

logger = logging.getLogger(__name__)


@shared_task(name='apps.sitemaps.tasks.sync_all_domains')
def sync_all_domains() -> dict:
    """
    Sincroniza los dominios operativos, uno por tarea.

    Se encola una tarea por dominio en vez de recorrerlos en una sola: un
    dominio grande que tarda una hora no tiene por qué demorar a los demás, y un
    fallo en el tercero no puede dejar sin sincronizar al cuarto.

    Los dados de baja quedan afuera: `access_state` describe lo que pasa en
    Search Console, no si la cuenta sigue trabajando con el sitio.
    """
    domains = Domain.objects.filter(
        access_state=AccessState.OPERATIONAL, deactivated_at__isnull=True
    ).values_list('id', flat=True)
    for domain_id in domains:
        sync_one_domain.delay(str(domain_id))

    return {'queued': len(domains)}


@shared_task(name='apps.sitemaps.tasks.sync_one_domain', bind=True)
def sync_one_domain(
    self, domain_id: str, origin: str = BatchOrigin.SCHEDULED, batch_id: str | None = None
) -> dict:
    """
    Sincroniza un dominio.

    Los errores previstos —dominio sin acceso, credencial que no sirve— no se
    reintentan: son condiciones que se resuelven en una pantalla, no con el paso
    del tiempo, y reintentarlas sólo llenaría la cola.

    `batch_id` llega cuando alguien pidió la sincronización desde la pantalla o
    la API: el lote ya existe en `QUEUED` y esta tarea lo toma. Si no llega, lo
    crea el servicio, que es el camino del ciclo automático.
    """
    domain = Domain.objects.filter(id=domain_id).select_related('account').first()
    if domain is None:
        return {'skipped': 'el dominio ya no existe'}

    batch = _queued_batch(batch_id)
    if batch_id and batch is None:
        # El lote se borró entre el encolado y la ejecución, o ya lo tomó otro
        # trabajador. Crear uno nuevo duplicaría el trabajo y los envíos.
        return {'skipped': 'el lote ya no está en cola'}

    try:
        batch = sync_domain(domain, origin=origin, batch=batch)
    except ApiError as exc:
        logger.warning('No se sincronizó %s: %s', domain.hostname, exc.message)
        _fail(batch, exc)
        return {'skipped': exc.code, 'detail': exc.message}

    return {'batch_id': str(batch.id), 'state': batch.state, 'summary': batch.summary}


def _queued_batch(batch_id: str | None):
    """
    El lote encolado, sólo si sigue en cola.

    Exigir `QUEUED` es lo que hace inofensivo un reintento: Celery reconoce las
    tareas al terminar, así que una que muera a mitad vuelve a la cola, y sin
    esta condición el segundo intento reenviaría a Google sitemaps que ya se
    habían enviado.
    """
    if not batch_id:
        return None

    from apps.jobs.models import Batch, BatchState

    return Batch.objects.filter(id=batch_id, state=BatchState.QUEUED).first()


def _fail(batch, exc: ApiError) -> None:
    """
    Cierra en `FAILED` el lote que alguien está mirando.

    Sin esto, un dominio que perdió el acceso entre el clic y la ejecución
    dejaría el lote en «en curso» para siempre, y la pantalla consultando cada
    cinco segundos un estado que no va a cambiar nunca (RT-13).
    """
    if batch is None:
        return

    from django.utils import timezone

    from apps.jobs.models import BatchState

    batch.state = BatchState.FAILED
    batch.finished_at = timezone.now()
    batch.summary = {'errors': [exc.message]}
    batch.save(update_fields=['state', 'finished_at', 'summary', 'updated_at'])
