"""
La pantalla de un lote de indexación y sus acciones.

Es la única del producto donde alguien decide mandarle algo a Google, y por eso
está construida alrededor de una separación que no se puede aflojar: **acá se
dice qué se pidió y qué contestó Google, nunca que una página quedó indexada**.
Que Google honre el pedido no está bajo control de nadie de este lado, y
prometerlo convertiría la evidencia en lo contrario de lo que se buscó al
construirla.

Ninguna de estas acciones envía nada por sí sola salvo «Run», que es el único
paso de borrador a cola.
"""

import json

from django.contrib.auth.decorators import login_required
from django.http import Http404, HttpResponse
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, field_list
from apps.domains.views import shared_domain_or_404
from apps.indexing.models import IndexingRequest, IndexingState
from apps.indexing.rules import discouraged_reason
from apps.indexing.services import (
    NothingToSend,
    batch_progress,
    clear_error,
    create_batch,
    delete_batch,
    eligible_urls,
    remove_url,
    restore_url,
    resume_batch,
    run_batch,
)
from apps.jobs.models import Batch, BatchKind, BatchOrigin, BatchState
from apps.sitemaps.models import Url


@login_required
@require_http_methods(['POST'])
def create(request, domain_id):
    """
    Manda a Google lo que se eligió en la tabla de cobertura.

    **Crea y ejecuta en un paso**, y es el único camino que lo hace. No rompe la
    regla de que nada sale sin confirmación humana: la cumple en otro lado. El
    diálogo de la tabla enumera las direcciones antes de mandarlas, así que la
    revisión ya ocurrió, y un borrador después sería pedir la misma confirmación
    dos veces —lo que enseña a apretar sin leer, que es lo contrario de lo que
    una confirmación busca—.

    Los otros dos caminos siguen naciendo en borrador, porque ahí no hay nadie
    delante: un despliegue por la API y un recorrido de fondo proponen, y la
    persona dispone cuando llega.
    """
    domain = shared_domain_or_404(request, domain_id)
    # `field_list` y no `request.POST`: Inertia manda el cuerpo como JSON salvo
    # que haya un archivo, y con JSON `request.POST` queda vacío. Leerlo directo
    # hacía que la pantalla eligiera cuatro direcciones, el servidor recibiera
    # cero y la persona volviera a la tabla sin que nada dijera qué pasó.
    selected = field_list(request, 'urls')

    urls = list(Url.objects.filter(domain=domain, id__in=selected))

    try:
        if not urls:
            # Se arma el error con la misma pieza que los del servicio: el
            # formato de lo que llega a `errors` es contrato con la pantalla, y
            # un diccionario escrito a mano acá sería el único que no lo cumple.
            raise NothingToSend('No se eligió ninguna dirección.')

        run_batch(create_batch(domain, urls, origin=BatchOrigin.MANUAL))
    except ApiError as exc:
        stash_errors(request, {'indexing': as_error(exc)})
        return redirect(_coverage_indexing(domain))

    return redirect(_coverage_indexing(domain))


def _coverage_indexing(domain) -> str:
    """
    La pestaña de indexación de la cobertura.

    Se vuelve acá y no a la ficha del lote: lo que la persona quiere ver después
    de mandar es **que está saliendo**, y eso es justo lo que la pestaña muestra
    en vivo. La ficha es para reconstruir un lote viejo, que es otra pregunta y
    de otro día.
    """
    return f'{reverse("coverage", kwargs={"domain_id": domain.id})}?tab=indexing'


@login_required
def show(request, batch_id):
    """
    El lote, sus direcciones y la evidencia de las que ya salieron.

    La lista viaja entera y no paginada: un lote de indexación se arma a mano o
    lo propone un despliegue, así que su tamaño es del orden de las decenas. El
    día que alguien arme uno de miles, esto necesita el mismo tope declarado que
    la tabla de cobertura.
    """
    batch = _batch_or_404(batch_id)
    domain = batch.domain

    requests = list(
        batch.indexing_requests.select_related('url').order_by('position')
    )

    return render(
        request,
        'Indexing/Batch',
        props={
            'domain': {
                'id': str(domain.id),
                'hostname': domain.hostname,
                'is_operational': domain.is_operational,
            },
            'batch': {
                '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,
                'created_at': batch.created_at.isoformat(),
                'started_at': batch.started_at.isoformat() if batch.started_at else None,
                'finished_at': batch.finished_at.isoformat() if batch.finished_at else None,
                'is_draft': batch.state == BatchState.DRAFT,
                'is_running': batch.state in (BatchState.QUEUED, BatchState.RUNNING),
                # Por qué se detuvo, en código. Es lo que habilita el botón de
                # limpiar y reanudar sin que la pantalla lea una frase.
                'stopped_by': (batch.summary or {}).get('stopped_by') or '',
            },
            'progress': batch_progress(batch),
            'requests': [_request_props(item) for item in requests],
        },
    )


@login_required
@require_http_methods(['POST'])
def run(request, batch_id):
    """Confirma el borrador: lo pasa a la cola y encola el envío."""
    batch = _batch_or_404(batch_id)

    try:
        run_batch(batch)
    except ApiError as exc:
        stash_errors(request, {'indexing': as_error(exc)})

    return redirect('indexing.batch', batch_id=batch.id)


@login_required
@require_http_methods(['POST'])
def resume(request, batch_id):
    """
    Retoma un lote detenido, desde la primera dirección que quedó pendiente.

    Vuelve a donde se disparó. Reanudar se ofrece en dos lugares —la pestaña de
    cobertura y la ficha del lote— y mandar siempre a la ficha sacaría de la
    pantalla a quien estaba mirando avanzar el lote, que es justo lo que quiere
    seguir mirando.
    """
    batch = _batch_or_404(batch_id)

    try:
        resume_batch(batch)
    except ApiError as exc:
        stash_errors(request, {'indexing': as_error(exc)})

    if field(request, 'from') == 'coverage':
        return redirect(_coverage_indexing(batch.domain))

    return redirect('indexing.batch', batch_id=batch.id)


@login_required
@require_http_methods(['POST'])
def destroy(request, batch_id):
    """
    Descarta un borrador entero.

    Sólo un borrador. Un lote que ya salió tiene evidencia adentro, y ésa no se
    borra nunca: es el entregable del producto y no un registro de operación.
    """
    batch = _batch_or_404(batch_id)
    domain_id = batch.domain_id

    try:
        delete_batch(batch)
    except ApiError as exc:
        stash_errors(request, {'indexing': as_error(exc)})
        return redirect('indexing.batch', batch_id=batch.id)

    return redirect('coverage', domain_id=domain_id)


@login_required
@require_http_methods(['POST'])
def update_request(request, batch_id, request_id):
    """
    Saca, devuelve o destraba una dirección del lote.

    Las tres viven en una sola dirección porque las tres son «cambiá el estado
    de esta fila», y son excluyentes entre sí: publicar tres rutas para lo mismo
    obligaría a la pantalla a elegir cuál llamar según un estado que el servidor
    ya conoce.
    """
    batch = _batch_or_404(batch_id)
    action = field(request, 'action')

    try:
        if action == 'remove':
            remove_url(batch, request_id)
        elif action == 'restore':
            restore_url(batch, request_id)
        elif action == 'clear':
            item = batch.indexing_requests.filter(id=request_id).first()
            if item is None:
                raise Http404('Esa dirección no está en este lote.')
            clear_error(item)
    except ApiError as exc:
        stash_errors(request, {'indexing': as_error(exc)})

    return redirect('indexing.batch', batch_id=batch.id)


@login_required
def evidence(request, batch_id):
    """
    Baja la evidencia del lote como archivo.

    Es JSON y no CSV a propósito: lo que hay que poder mostrar son los cuerpos
    que viajaron, y un cuerpo anidado adentro de una celda de planilla deja de
    ser verificable. El archivo se arma en la petición porque un lote de
    indexación es de decenas de filas, no de miles.

    Se entrega tal cual quedó guardado. No se resume, no se traduce y no se
    reordena: cualquiera de las tres cosas convertiría la prueba en una lectura
    nuestra de la prueba.
    """
    batch = _batch_or_404(batch_id)

    payload = {
        'batch_id': str(batch.id),
        'domain': batch.domain.hostname,
        'property_uri': batch.domain.property_uri,
        'state': batch.state,
        'created_at': batch.created_at.isoformat(),
        'finished_at': batch.finished_at.isoformat() if batch.finished_at else None,
        'requests': [
            {
                'url': item.loc,
                'position': item.position,
                'state': item.state,
                'sent_at': item.sent_at.isoformat() if item.sent_at else None,
                'request_body': item.request_body,
                'response_status': item.response_status,
                'response_body': item.response_body,
                'error_code': item.error_code,
            }
            for item in batch.indexing_requests.order_by('position')
        ],
    }

    response = HttpResponse(
        json.dumps(payload, indent=2, ensure_ascii=False),
        content_type='application/json; charset=utf-8',
    )
    # El nombre no se traduce: es lo que queda escrito en la carpeta de
    # descargas y en cualquier script que lo levante.
    response['Content-Disposition'] = f'attachment; filename="indexing-{batch.id}.json"'
    return response


def _batch_or_404(batch_id) -> Batch:
    batch = (
        Batch.objects.filter(
            id=batch_id, kind=BatchKind.URL_INDEXING, domain__deactivated_at__isnull=True
        )
        .select_related('domain', 'domain__account')
        .first()
    )
    if batch is None:
        raise Http404('Ese lote de indexación no existe en esta compañía.')
    return batch


def _request_props(item: IndexingRequest) -> dict:
    """
    Una fila del lote.

    `coverage_state` viaja al lado del estado del pedido y **en una clave
    distinta**: son las dos afirmaciones que el producto tiene que mantener
    separadas —«Google dice que no está indexada» y «le pedimos que la indexe»—
    y compartir campo las encimaría en la pantalla igual que en la base.
    """
    return {
        'id': str(item.id),
        'loc': item.loc,
        'state': item.state,
        'position': item.position,
        'sent_at': item.sent_at.isoformat() if item.sent_at else None,
        'response_status': item.response_status,
        'error_code': item.error_code,
        'has_evidence': item.has_evidence,
        'request_body': item.request_body or None,
        'response_body': item.response_body or None,
        # El eje de cobertura, sólo para mostrar la advertencia cuando alguien
        # pide algo desaconsejado. Vacío si la URL ya no está en el inventario.
        'coverage_state': item.url.coverage_state if item.url else '',
        'discouraged': discouraged_reason(item.url.coverage_state) if item.url else '',
    }


def request_states() -> list[str]:
    """Los estados de un pedido, para que la pantalla arme sus filtros sin repetirlos."""
    return list(IndexingState.values)


def preview(domain, locs) -> dict:
    """Lo que entraría y lo que se descartaría de una lista de direcciones."""
    accepted, discarded = eligible_urls(domain, locs)
    return {'accepted': [url.loc for url in accepted], 'discarded': discarded}
