"""
Fuerza sobre un dominio el ciclo que corre solo cada madrugada (T085).

Los tres pasos van en el mismo orden que el ciclo automático —acceso, sitemaps,
inspección— y por la misma razón: si el acceso se perdió, los dos siguientes no
tienen nada que hacer sobre ese dominio y cada intento gastaría cupo en llamadas
que van a fallar. Por eso, cuando el dominio no queda operativo, el comando
corta ahí en vez de seguir.

Existe para operar: retomar un ciclo que no corrió, ver qué contesta Google hoy
sobre un dominio concreto, o confirmar que una autorización recién hecha ya
funciona, sin esperar a las dos de la mañana. Hace el trabajo **acá** y muestra
lo que pasó; con `--queue` lo deja en la cola y contesta los lotes, que es lo
útil cuando hay un trabajador vivo y el sitio es grande.

El cupo que consume es el automático, no la reserva manual: esto reemplaza al
trabajo del ciclo, no lo agrega. Gastar la reserva manual dejaría a la persona
sin el cupo que tiene guardado para lo que dispara desde la pantalla.

**Lo que imprime va en inglés y no pasa por el catálogo.** No es interfaz: es la
salida de una herramienta de operación, que se lee en una consola, se pega en un
ticket y se busca con `grep`. Un texto que cambiara con el idioma de quien lo
corrió rompería cualquier búsqueda armada contra una corrida anterior.
"""

from django.core.management.base import BaseCommand, CommandError

from apps.core.errors import ApiError
from apps.coverage.services import inspect_domain, queue_inspection
from apps.domains.models import AccessState, Domain
from apps.domains.services import check_access
from apps.jobs.models import Batch, BatchOrigin
from apps.sitemaps.services import queue_sync, sync_domain


class Command(BaseCommand):
    help = 'Force the daily cycle on one domain: access, sitemaps and inspection.'

    def add_arguments(self, parser):
        parser.add_argument('domain', help="The domain's hostname or its id")
        parser.add_argument(
            '--limit',
            type=int,
            default=None,
            help="Cap of URLs to inspect. Defaults to the domain's automatic budget",
        )
        parser.add_argument(
            '--queue',
            action='store_true',
            help='Leave the work in the queue instead of doing it here. Needs a live worker',
        )
        parser.add_argument('--skip-access', action='store_true', help='Do not check the access')
        parser.add_argument('--skip-sync', action='store_true', help='Do not read the sitemaps')
        parser.add_argument(
            '--skip-inspection', action='store_true', help='Do not query a single URL'
        )

    def handle(self, *args, **options):
        domain = self._domain(options['domain'])
        self.stdout.write(f'Domain {domain.hostname} ({domain.property_uri})')

        if not options['skip_access'] and not self._check_access(domain):
            return

        if not options['skip_sync']:
            self._step(
                'Sitemaps',
                lambda: (
                    queue_sync(domain, origin=BatchOrigin.SCHEDULED)
                    if options['queue']
                    else sync_domain(domain, origin=BatchOrigin.SCHEDULED)
                ),
            )

        if not options['skip_inspection']:
            limit = options['limit'] or domain.automatic_budget
            self._step(
                'Inspection',
                lambda: (
                    queue_inspection(domain, limit=limit, origin=BatchOrigin.SCHEDULED)
                    if options['queue']
                    else inspect_domain(domain, limit=limit, origin=BatchOrigin.SCHEDULED)
                ),
            )

    def _domain(self, reference: str) -> Domain:
        """
        El dominio, buscado por hostname y también por identificador.

        Por hostname porque es lo que alguien tiene a mano cuando está operando;
        por identificador porque es lo que trae el enlace de un aviso o de una
        pantalla, y obligar a traducirlo a mano es pedir una consulta a la base
        antes de poder usar el comando.

        **Los dados de baja no se encuentran.** `ensure_operational` los frena
        igual más adelante, pero para entonces el ciclo ya gastó una comprobación
        de acceso contra Google sobre un sitio que la cuenta dejó de mirar. Acá
        el comando contesta que no existe, que es lo que la interfaz también dice.
        """
        active = Domain.objects.filter(deactivated_at__isnull=True)
        domain = active.filter(hostname=reference).select_related('account').first()
        if domain is None:
            domain = (
                active.filter(pk=reference).select_related('account').first()
                if _looks_like_uuid(reference)
                else None
            )

        if domain is None:
            raise CommandError(f'No domain is named or identified by "{reference}".')

        return domain

    def _check_access(self, domain: Domain) -> bool:
        """
        Comprueba el acceso y dice si tiene sentido seguir.

        Devuelve falso cuando el dominio no quedó operativo: los pasos que vienen
        llaman a Google sobre una propiedad que no podemos leer, así que sólo
        producirían un lote fallido por dominio y por corrida.
        """
        before = domain.access_state

        try:
            domain = check_access(domain)
        except ApiError as exc:
            self.stderr.write(self.style.ERROR(f'Access: {exc.message}'))
            return False

        line = f'Access: {domain.get_access_state_display()}'
        if domain.access_state != before:
            # El cambio se dice con las dos etiquetas: es el dato por el que se
            # corre este comando después de autorizar la cuenta en Search Console.
            line += f' (was: {AccessState(before).label})'

        if domain.is_operational:
            self.stdout.write(self.style.SUCCESS(line))
            return True

        self.stdout.write(self.style.WARNING(line))
        self.stdout.write('Stopping here: without confirmed access there is nothing to ask Google.')
        return False

    def _step(self, title: str, run) -> None:
        """Ejecuta un paso y cuenta cómo terminó, sin volcar un traceback encima."""
        try:
            batch = run()
        except ApiError as exc:
            self.stderr.write(self.style.ERROR(f'{title}: {exc.message}'))
            return

        self.stdout.write(f'{title}: {self._batch_line(batch)}')
        for note in (batch.summary or {}).get('errors') or []:
            self.stdout.write(f'  · {note}')

    def _batch_line(self, batch: Batch) -> str:
        state = batch.get_state_display()
        if batch.total_items:
            return (
                f'{state} · {batch.processed_items} of {batch.total_items} processed · '
                f'{batch.failed_items} failed · quota spent {batch.quota_consumed} · '
                f'batch {batch.id}'
            )
        return f'{state} · batch {batch.id}'


def _looks_like_uuid(value: str) -> bool:
    """
    Si vale la pena preguntarle a la base por el identificador.

    Sin esta comprobación, un hostname que no existe termina en una consulta por
    clave primaria con un valor que no es un UUID, y lo que se ve no es «no
    encontré ese dominio» sino un error de base de datos.
    """
    from uuid import UUID

    try:
        UUID(value)
    except ValueError:
        return False
    return True
