"""
Sitemaps de un dominio y su estado de envío (V8).

La pregunta que trae al usuario acá es una sola: «¿Google tiene lo último que
publiqué?». Todo lo que la vista arma está al servicio de responderla sin
prometer de más: qué sitemaps conocemos, cuándo los leímos y qué le mandamos a
Google. Enviar no es que Google lo procese, y ninguno de los dos es que las URLs
queden indexadas.

El listado es una tabla larga de verdad —un sitio con un índice de cincuenta
archivos son cincuenta y una filas— así que pagina, ordena y filtra el servidor
(RT-09). El orden por omisión es el del árbol, para que cada archivo descubierto
caiga debajo del índice que lo declaró.
"""

from urllib.parse import urlencode

from django.contrib.auth.decorators import login_required
from django.db.models import Case, IntegerField, Value, When
from django.db.models.functions import Coalesce
from django.shortcuts import redirect
from django.urls import reverse
from django.views.decorators.http import require_http_methods
from inertia import render

from apps.core.errors import ApiError, as_error, stash_errors
from apps.core.requests import field
from apps.domains.views import shared_domain_or_404
from apps.jobs.models import Batch, BatchKind, BatchOrigin
from apps.sitemaps.models import Sitemap, SitemapKind, SitemapSource, SubmitResult
from apps.sitemaps.services import check_sitemap, queue_sync, register_sitemap

#: La rama del árbol a la que pertenece cada fila: la ubicación del índice raíz
#: que terminó declarándola, o la propia si no la declaró nadie.
#:
#: Se sigue hasta tres niveles porque tres es lo que la lectura acepta encadenar
#: (`MAX_DEPTH`); más que eso el lector lo corta por ciclo, así que no hay
#: filas de nivel cuatro que ordenar.
_BRANCH = Coalesce(
    'parent__parent__parent__location',
    'parent__parent__location',
    'parent__location',
    'location',
)

_DEPTH = Case(
    When(parent__isnull=True, then=Value(0)),
    When(parent__parent__isnull=True, then=Value(1)),
    When(parent__parent__parent__isnull=True, then=Value(2)),
    default=Value(3),
    output_field=IntegerField(),
)

@login_required
def index(request, domain_id):
    domain = shared_domain_or_404(request, domain_id)
    batch = _last_sync(domain)

    # Una corrida abierta es lo que convierte «sin leer» en «leyendo»: sin esta
    # fecha las dos se ven igual, y un sitemap recién registrado parecería roto
    # mientras el trabajo está justamente ocurriendo.
    reading_since = None
    if batch is not None and not batch.is_terminal:
        reading_since = batch.started_at or batch.created_at

    sitemaps = (
        Sitemap.objects.filter(domain=domain)
        .select_related('parent')
        .annotate(branch=_BRANCH, depth=_DEPTH)
        .order_by('branch', 'depth', 'location')
    )

    return render(
        request,
        'Sitemaps/Index',
        props={
            'domain': {
                'id': str(domain.id),
                'hostname': domain.hostname,
                # El código y no la etiqueta: la palabra de cada estado vive una
                # sola vez, en el catálogo del cliente, que es el único que sabe
                # en qué idioma está leyendo esta persona.
                'access_state': domain.access_state,
                'is_operational': domain.is_operational,
            },
            'sitemaps': [
                _row(sitemap, reading_since=reading_since) for sitemap in sitemaps
            ],
            'options': {
                'kind': _options(SitemapKind),
                'source': _options(SitemapSource),
                'result': _options(SubmitResult),
            },
            'last_sync': _batch_props(batch),
        },
    )


@login_required
@require_http_methods(['POST'])
def create(request, domain_id):
    domain = shared_domain_or_404(request, domain_id)
    location = field(request, 'location').strip()

    if not location:
        stash_errors(request, {'location': {'code': 'SITEMAP_LOCATION_REQUIRED'}})
        return redirect(_sitemaps_url(domain.id, new=''))

    if not location.startswith(('http://', 'https://')):
        stash_errors(request, {'location': {'code': 'SITEMAP_LOCATION_RELATIVE'}})
        return redirect(_sitemaps_url(domain.id, new=''))

    # Se comprueba antes de guardar (T094). El error va junto al campo y no en
    # un mensaje efímero: lo que hay que corregir es lo que se acaba de escribir,
    # y el mensaje ya trae el dato concreto —el 404, la etiqueta que faltaba— más
    # qué revisar (RT-08, RT-17).
    try:
        check_sitemap(domain, location)
    except ApiError as exc:
        stash_errors(request, {'location': as_error(exc)})
        return redirect(_sitemaps_url(domain.id, new=''))

    register_sitemap(domain, location)
    return redirect(_sitemaps_url(domain.id))


@login_required
@require_http_methods(['POST'])
def sync(request, domain_id):
    """Lee los sitemaps y envía a Google los que cambiaron."""
    domain = shared_domain_or_404(request, domain_id)

    try:
        queue_sync(domain, origin=BatchOrigin.MANUAL)
    except ApiError as exc:
        # El código viaja junto al mensaje porque la pantalla tiene un mapa
        # cerrado de códigos (RT-08): sin él, «no hay credencial comprobada» y
        # «se agotó el cupo» llegarían como dos oraciones que la vista no puede
        # distinguir, y la acción que ofrece tendría que ser la misma para las
        # dos.
        stash_errors(request, {'sync': as_error(exc)})

    return redirect(_sitemaps_url(domain.id, tab='sync'))


def _sitemaps_url(domain_id, **query: str) -> str:
    """Conserva el panel que debe mostrar el resultado del POST."""
    location = reverse('sitemaps', kwargs={'domain_id': domain_id})
    return f'{location}?{urlencode(query)}' if query else location


def _options(choices) -> list[dict]:
    """Opciones crudas: la tabla compara estos códigos con los de cada fila."""
    return [{'value': value, 'label': label} for value, label in choices.choices]


def _row(sitemap: Sitemap, *, reading_since) -> dict:
    """
    Un renglón de la tabla.

    `parent_location` y `depth` viajan juntos y siempre: la sangría sola no
    comunica de quién depende un archivo —no la lee un lector de pantalla ni
    sobrevive a ordenar por otra columna— así que la relación se dice también
    con el nombre del índice que lo declaró.
    """
    reading = reading_since is not None and (
        sitemap.last_read_at is None or sitemap.last_read_at < reading_since
    )

    return {
        'id': str(sitemap.id),
        'location': sitemap.location,
        'kind': sitemap.kind,
        'source': sitemap.source,
        'parent_location': sitemap.parent.location if sitemap.parent else None,
        'depth': getattr(sitemap, 'depth', 0),
        # Son las URLs que el archivo **declara**, no las que Google indexó.
        # Confundirlas convertiría un número que describe un archivo en una
        # afirmación sobre la cobertura del sitio.
        'url_count': sitemap.url_count,
        'last_read_at': sitemap.last_read_at.isoformat() if sitemap.last_read_at else None,
        'last_submitted_at': (
            sitemap.last_submitted_at.isoformat() if sitemap.last_submitted_at else None
        ),
        'last_submit_result': sitemap.last_submit_result or None,
        'last_error': sitemap.last_error or None,
        'last_error_code': sitemap.last_error_code or None,
        'foreign_url_count': sitemap.foreign_url_count,
        'needs_submit': sitemap.needs_submit,
        'reading': reading,
    }


def _last_sync(domain) -> Batch | None:
    return Batch.objects.filter(domain=domain, kind=BatchKind.SITEMAP_SYNC).first()


def _batch_props(batch: Batch | None) -> dict | None:
    """
    La última corrida, con sus cifras.

    Van las cifras y no un texto ya resuelto porque `C-17` dibuja el avance
    desde ellas: un lote parcial tiene que verse parcial —la barra sin tocar el
    borde— y no como uno terminado con otro color.
    """
    if batch is None:
        return None

    return {
        'id': str(batch.id),
        'state': batch.state,
        'origin': batch.origin,
        'total_items': batch.total_items,
        'processed_items': batch.processed_items,
        'failed_items': batch.failed_items,
        'quota_consumed': batch.quota_consumed,
        'started_at': batch.started_at.isoformat() if batch.started_at else None,
        'finished_at': batch.finished_at.isoformat() if batch.finished_at else None,
        'duration_seconds': _duration(batch),
        'summary': batch.summary,
        'is_terminal': batch.is_terminal,
    }


def _duration(batch: Batch) -> int | None:
    """Duración cerrada de la corrida; mientras está abierta no se adivina."""
    if not batch.started_at or not batch.finished_at:
        return None
    return max(int((batch.finished_at - batch.started_at).total_seconds()), 0)
