"""Trabajo programado del módulo de rendimiento en la búsqueda."""

import logging

from celery import shared_task

from apps.credentials.services import CredentialNotReady
from apps.domains.models import AccessState, Domain
from apps.gsc.client import backoff
from apps.gsc.errors import PERMANENT_CODES, GoogleCallError
from apps.seo import services
from apps.seo.models import SyncRun

logger = logging.getLogger(__name__)

#: Cuántas veces se reintenta una corrida que falló por algo pasajero.
MAX_RETRIES = 3


@shared_task(name='apps.seo.tasks.sync_all_domains')
def sync_all_domains() -> dict:
    """
    La corrida diaria, para todos los sitios operativos.

    Se filtra igual que el recorrido de cobertura: `access_state` describe lo que
    pasa en Search Console y no alcanza por sí solo, porque un sitio puede estar
    operativo allá y dado de baja acá.
    """
    domains = Domain.objects.filter(
        access_state=AccessState.OPERATIONAL, deactivated_at__isnull=True
    ).values_list('id', flat=True)

    queued = 0
    for domain_id in domains:
        sync_one_domain.delay(str(domain_id))
        queued += 1

    return {'queued': queued}


@shared_task(name='apps.seo.tasks.sync_one_domain', bind=True)
def sync_one_domain(self, domain_id: str) -> dict:
    """
    Trae los días que falten de un sitio.

    En régimen normal es **un día**: el que Google acaba de dar por cerrado. Si
    la tarea no corrió en varias jornadas, el rango sale del estado guardado y
    recupera solo lo que faltó, sin lógica especial y sin reconsultar nada que ya
    esté bien.
    """
    domain = Domain.objects.filter(id=domain_id).first()
    if domain is None:
        return {'skipped': 'unknown-domain'}

    run = services.queue_daily_sync(domain)
    if run is None:
        # O nunca se importó historia —y entonces lo que corresponde es un
        # backfill, que se pide a mano— o el módulo ya está al día. Ninguna de
        # las dos es un error.
        return {'skipped': 'nothing-to-fetch'}

    return _execute_with_retry(self, run)


@shared_task(name='apps.seo.tasks.run_backfill', bind=True)
def run_backfill(self, run_id: str) -> dict:
    """Corre una importación de historia ya registrada por la pantalla."""
    run = SyncRun.objects.filter(id=run_id).select_related('domain').first()
    if run is None:
        return {'skipped': 'unknown-run'}

    return _execute_with_retry(self, run)


def _execute_with_retry(task, run: SyncRun) -> dict:
    """
    Ejecuta y decide si vale la pena volver a intentar.

    La partición entre lo que se reintenta y lo que no ya está escrita en
    `PERMANENT_CODES`: insistir con un permiso denegado gasta tiempo para volver
    al mismo lugar, mientras que un 503 suele resolverse solo. La espera reusa el
    mismo retroceso con jitter del cliente, para que dos trabajadores que fallan
    a la vez no vuelvan a la vez.
    """
    try:
        services.execute(run)
    except CredentialNotReady:
        logger.warning('SEO: la credencial no está lista para %s', run.domain_id)
        return {'failed': 'credential-not-ready'}
    except GoogleCallError as error:
        if error.code in PERMANENT_CODES or task.request.retries >= MAX_RETRIES:
            logger.warning('SEO: corrida %s abandonada con %s', run.id, error.code)
            return {'failed': error.code}
        raise task.retry(exc=error, countdown=backoff(task.request.retries + 1))

    return {'run': str(run.id), **run.summary}
