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

Arman props y delegan, como el resto de la interfaz: ninguna lleva regla de
negocio propia. Lo único que se decide acá es **quién puede hacer qué**, porque
es una pregunta de la interfaz y no del servicio.
"""

from urllib.parse import quote

from django.contrib.auth.decorators import login_required
from django.http import Http404, JsonResponse
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.accounts.models import AccountRole
from apps.core.errors import stash_errors
from apps.core.requests import field
from apps.core.tables import from_slug
from apps.domains.services import shared_domains
from apps.seo import services, tasks
from apps.seo.models import (
    CannibalizationSeverity,
    Engagement,
    PageRotation,
    PositionBand,
    RunState,
    SyncRun,
)


def _active_site():
    """
    El sitio con el que trabaja el módulo.

    Mismo criterio que usa el menú: la interfaz trabaja contra un solo sitio, y
    cuál es se decide en un lugar. Devuelve `None` mientras no haya ninguno
    conectado, que no es un error sino el estado inicial de la instalación.
    """
    return shared_domains().first()


def _site_or_404():
    """
    El sitio activo, o un 404.

    **Ninguna dirección de este módulo lleva el identificador del sitio**, y por
    eso esto no recibe parámetro. Se evaluó imitar a las cuatro pantallas de
    Search Console, que sí lo llevan, y se decidió al revés: con la interfaz
    acotada a un sitio, ese segmento no elige nada —se valida para resolver
    siempre al mismo—, y ensuciaba siete direcciones a cambio de ninguna
    decisión.

    Sin sitio conectado contesta 404 y no una pantalla vacía: no hay dato que
    mostrar, y las dos pantallas del módulo que sí saben decir qué falta —el
    inicio y la conexión— no pasan por acá.
    """
    domain = _active_site()
    if domain is None:
        raise Http404('Esta compañía todavía no tiene un sitio conectado.')
    return domain


def _is_super_admin(user) -> bool:
    return getattr(user, 'role', None) == AccountRole.SUPER_ADMIN


def _domain_props(domain) -> dict:
    return {'id': str(domain.id), 'hostname': domain.hostname}


@login_required
def overview(request):
    """
    El inicio de la herramienta: en qué anda el módulo.

    Sin sitio conectado no hay nada que resumir, y lo que corresponde no es una
    pantalla vacía sino mandar a conectarlo, que es donde se resuelve.
    """
    domain = _active_site()
    if domain is None:
        return render(
            request, 'Seo/Index', props={'domain': None, 'overview': None, 'metrics': None}
        )

    overview = services.module_overview(domain)

    return render(
        request,
        'Seo/Index',
        props={
            'domain': _domain_props(domain),
            'overview': overview,
            # Las cuatro tarjetas sólo tienen sentido con historia detrás.
            # Calcularlas sobre una base vacía devolvería cuatro ceros, y cuatro
            # ceros se leen como «tu sitio no aparece por nada» en vez de como
            # «todavía no importamos nada».
            'metrics': (
                services.home_metrics(domain) if overview['last_closed_date'] else None
            ),
            # Aparte de `metrics`, que es de las tarjetas: esto es el gráfico y
            # su tabla, y son dos preguntas distintas sobre el mismo período.
            'top_keywords': services.top_keywords_series(domain),
            'window_days': services.DEFAULT_WINDOW_DAYS,
            'min_impressions': services.MIN_IMPRESSIONS,
        },
    )


@login_required
def cannibalization(request):
    """
    El reporte completo de consultas con varias páginas compitiendo.

    Tiene pantalla propia y no se resuelve en el modal porque cada fila trae **su
    lista de URLs**: es lo que hay que mirar para decidir cuál conservar, y una
    lista adentro de otra lista no entra en un diálogo.
    """
    domain = _site_or_404()

    return render(
        request,
        'Seo/Cannibalization',
        props={
            # El sitio viaja porque la grilla del detalle recorta el origen de
            # cada dirección, y para saber si recortar hay que saber cuál es.
            'domain': _domain_props(domain),
            'rows': services.cannibalized_keywords(domain),
            'filters': _filters_from(request, CANNIBALIZATION_FILTERS),
            'threshold': services.CANNIBALIZATION_THRESHOLD,
            'window_days': services.DEFAULT_WINDOW_DAYS,
        },
    )


@login_required
def cannibalization_pages(request):
    """
    Las URLs que se pisan por una consulta. **Contesta JSON, no una pantalla.**

    Es la única vista del módulo que no dibuja nada, y es a propósito: la
    pantalla de canibalización lista decenas de consultas y de esas se abren dos
    o tres. Mandar las páginas de todas en la carga inicial era traer cientos de
    filas para dibujar unas pocas.

    La consulta llega por la cadena y no en la dirección, por lo mismo que en la
    ficha: puede contener una barra —«patios/pools»— y un segmento de ruta la
    partiría en dos.
    """
    domain = _site_or_404()
    query = (request.GET.get('q') or '').strip()
    if not query:
        raise Http404('Falta la consulta.')

    pages = services.cannibalization_pages(domain, query)
    if pages is None:
        raise Http404('Esa consulta no tiene páginas registradas en este sitio.')

    return JsonResponse({'pages': pages})


@login_required
def connection(request):
    """
    Estado de la conexión del módulo, importación de historia e historial.

    Las tres cosas juntas y no en pantallas separadas: quien viene acá viene a
    resolver que el módulo tenga datos, y el historial es cómo se comprueba que
    quedó resuelto.
    """
    domain = _active_site()
    if domain is None:
        return render(request, 'Seo/Connection', props={'domain': None})

    state = services.state_for(domain)
    imported = state.last_closed_date is not None
    super_admin = _is_super_admin(request.user)

    return render(
        request,
        'Seo/Connection',
        props={
            'domain': _domain_props(domain),
            'overview': services.module_overview(domain),
            'history': services.run_history(domain),
            # El recorte se dice, no se calla: un tope silencioso se lee como
            # «esto es todo lo que hay».
            'history_total': SyncRun.objects.filter(domain=domain).count(),
            'months': list(services.BACKFILL_MONTHS),
            # Hecha la importación el botón desaparece: ya no hay nada que
            # importar. A quien administra no se le esconde, porque para ese
            # perfil el botón hace otra cosa —borrar y repoblar— y es su
            # herramienta para relanzar cuando los datos salen mal.
            'can_import': super_admin or not imported,
            'imported': imported,
            'destructive': super_admin and imported,
        },
    )


@login_required
@require_http_methods(['POST'])
def backfill(request):
    """
    Encola la importación de historia.

    Para quien administra y con la historia ya importada, **borra y repuebla**.
    Es la herramienta de desarrollo: si al mirar los datos aparece un defecto, se
    corrige y se relanza limpio en vez de convivir con una serie a medio arreglar.
    """
    domain = _site_or_404()
    back = reverse('seo.connection')

    try:
        months = int(field(request, 'months'))
    except (TypeError, ValueError):
        stash_errors(request, {'months': {'code': 'MONTHS_INVALID'}})
        return redirect(back)

    state = services.state_for(domain)
    imported = state.last_closed_date is not None
    super_admin = _is_super_admin(request.user)

    if imported and not super_admin:
        stash_errors(request, {'form': {'code': 'ALREADY_IMPORTED'}})
        return redirect(back)

    if SyncRun.objects.filter(domain=domain, state__in=[RunState.QUEUED, RunState.RUNNING]).exists():
        # Dos importaciones a la vez sobre el mismo sitio se pisarían al
        # escribir. Encolarla igual no la haría más rápida.
        stash_errors(request, {'form': {'code': 'ALREADY_RUNNING'}})
        return redirect(back)

    try:
        if imported and super_admin:
            run = services.reset_and_repopulate(domain, months)
        else:
            run = services.queue_backfill(domain, months)
    except services.BackfillNotAllowed:
        stash_errors(request, {'months': {'code': 'MONTHS_INVALID'}})
        return redirect(back)

    tasks.run_backfill.delay(str(run.id))
    return redirect(back)


#: Por qué ejes se puede abrir la tabla de consultas ya recortada.
#:
#: Cada clave nombra un campo que `keyword_rows` calcula en cada fila, y su
#: valor es el juego cerrado que ese campo admite. Existe para que una tarjeta
#: del inicio pueda **enlazar** acá en vez de abrir un modal: la regla de que una
#: cifra se abre donde se la lee admite el enlace cuando el destino contesta la
#: misma pregunta con el mismo conjunto, y sin recorte el destino contesta
#: siempre con el conjunto entero.
KEYWORD_FILTERS = {
    'band': PositionBand.values,
    'engagement': Engagement.values,
}

#: Lo mismo para la tabla de canibalización. Los dos ejes los calcula
#: `cannibalized_keywords()` en cada fila, por lo mismo que los de arriba: el
#: corte se escribe una vez.
CANNIBALIZATION_FILTERS = {
    'severity': CannibalizationSeverity.values,
    'rotation': PageRotation.values,
}


def _filters_from(request, axes: dict[str, list[str]]) -> dict[str, list[str]]:
    """
    Qué recorte pidió la dirección, traducido a lo que la tabla entiende.

    Los valores viajan en su forma pública —`near-first-page`— y vuelven en la
    del enum, que es la que lleva cada fila. Lo que no está en el juego se
    descarta en silencio y no rompe: es el mismo trato que le da `TableSpec` a un
    valor inventado, porque casi siempre es un enlace viejo y no un ataque.

    Recibe los ejes en vez de conocerlos: son dos tablas con dos juegos, y lo
    único que cambia entre ellas es eso.
    """
    chosen: dict[str, list[str]] = {}

    for param, choices in axes.items():
        asked = (request.GET.get(param) or '').split(',')
        values = [value for value in (from_slug(item.strip(), choices) for item in asked) if value]
        if values:
            chosen[param] = values

    return chosen


@login_required
def keywords(request):
    """La tabla de consultas del sitio."""
    domain = _site_or_404()
    state = services.state_for(domain)

    return render(
        request,
        'Seo/Keywords',
        props={
            'domain': _domain_props(domain),
            'rows': services.keyword_rows(domain),
            # Estado **inicial** de los filtros, no estado. Quien mira puede
            # sacarlos y la pantalla no se los vuelve a poner.
            'filters': _filters_from(request, KEYWORD_FILTERS),
            'status': state.status,
            'last_closed_date': (
                state.last_closed_date.isoformat() if state.last_closed_date else None
            ),
            'window_days': services.DEFAULT_WINDOW_DAYS,
            # El mínimo viaja para que la pantalla pueda explicar por qué una
            # celda no tiene número, en vez de dejar un hueco sin motivo.
            'min_impressions': services.MIN_IMPRESSIONS,
        },
    )


@login_required
def keyword(request):
    """
    La ficha de una consulta.

    La consulta llega por la cadena de consulta y no en la dirección: puede
    contener una barra —«patios/pools»— y un segmento de ruta la partiría en dos.
    """
    domain = _site_or_404()
    query = (request.GET.get('q') or '').strip()
    if not query:
        raise Http404('Falta la consulta.')

    detail = services.keyword_detail(domain, query)
    if detail is None:
        raise Http404('Esa consulta no tiene datos ni seguimiento en este sitio.')

    return render(
        request,
        'Seo/Keyword',
        props={
            'keyword': detail,
            'window_days': services.DEFAULT_WINDOW_DAYS,
            'min_impressions': services.MIN_IMPRESSIONS,
        },
    )


@login_required
def pages(request):
    """Qué páginas del sitio traen tráfico."""
    domain = _site_or_404()
    state = services.state_for(domain)

    return render(
        request,
        'Seo/Pages',
        props={
            'domain': _domain_props(domain),
            'rows': services.page_rows(domain),
            'status': state.status,
            'last_closed_date': (
                state.last_closed_date.isoformat() if state.last_closed_date else None
            ),
            'window_days': services.DEFAULT_WINDOW_DAYS,
        },
    )


@login_required
def page(request):
    """Por qué consultas aparece una URL. La pregunta inversa de la ficha de consulta."""
    domain = _site_or_404()
    url = (request.GET.get('url') or '').strip()
    if not url:
        raise Http404('Falta la dirección.')

    detail = services.page_detail(domain, url)
    if detail is None:
        raise Http404('Esa dirección no tiene datos en este sitio.')

    return render(
        request,
        'Seo/Page',
        props={
            'page': detail,
            'window_days': services.DEFAULT_WINDOW_DAYS,
        },
    )


@login_required
@require_http_methods(['POST'])
def track(request):
    """
    Empieza o deja de seguir una consulta.

    Una sola dirección para las dos acciones: son la misma decisión con distinto
    valor, y separarlas obligaría a recordar cuál usa cada botón.
    """
    domain = _site_or_404()
    keyword_value = field(request, 'keyword', '').strip()
    listing = reverse('seo.keywords')

    if not keyword_value:
        stash_errors(request, {'keyword': {'code': 'KEYWORD_REQUIRED'}})
        return redirect(listing)

    if field(request, 'tracked', '') == 'false':
        services.untrack_keyword(domain, keyword_value)
    else:
        services.track_keyword(domain, keyword_value)

    # Adónde volver sale de un campo con dos valores posibles y **no de
    # `HTTP_REFERER`**: esa cabecera la escribe quien envía, así que usarla para
    # redirigir convierte este formulario en un salto a cualquier sitio.
    if field(request, 'origin', '') == 'detail':
        return redirect(f'{reverse("seo.keyword")}?q={quote(keyword_value)}')
    return redirect(listing)
