"""
El recorrido guiado (V3): la pantalla y sus dos acciones.

Las tres vistas traducen entre HTTP y `apps.onboarding.services`, que es el mismo
servicio que consume la API. No es una preferencia de estilo: si la decisión de
cuándo un paso está cumplido viviera acá, la API no la tendría y el recorrido se
comportaría distinto según se lo use desde la pantalla o desde un pipeline
(principio IV). Y como lo que define «cumplido» es la comprobación efectiva
contra el estado real del sistema, tener dos implementaciones sería tener dos
verdades sobre si la configuración de alguien funciona (FR-017).
"""

from django.contrib.auth.decorators import login_required
from django.http import Http404
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.http import urlencode
from django.views.decorators.http import require_http_methods
from inertia import render

from apps.core.errors import ApiError, NotFound, ValidationFailed, as_error, stash_errors
from apps.core.requests import payload
from apps.credentials.views import MAX_KEY_FILE_BYTES
from apps.onboarding import services

#: Qué paso se acaba de comprobar, entre la petición que comprueba y la pantalla
#: que muestra el resultado.
#:
#: Viaja por la sesión y no por la dirección porque el resultado de una
#: comprobación no es un estado enlazable: recargar o compartir la URL no tiene
#: por qué volver a anunciar algo que ya pasó. Es el mismo camino que usan los
#: errores de validación, por el mismo motivo (el flujo de Inertia ante una
#: acción es redirigir).
CHECKED_STEP_KEY = '_onboarding_checked_step'


@login_required
def wizard(request):
    """
    El recorrido, con cada paso ya comprobado contra el estado real de la cuenta.

    Se ofrece siempre que se pida, incluso terminado u omitido: la pantalla queda
    accesible por dirección directa mostrando el resumen. Quién entra acá sin
    pedirlo lo decide la raíz, no esta vista.
    """
    return render(
        request,
        'Onboarding/Wizard',
        props={'onboarding': services.state(request.user, checked_step=_checked_step(request))},
    )


@login_required
@require_http_methods(['POST'])
def verify(request, step):
    """
    Ejecuta el «Comprobar» de un paso y vuelve al recorrido.

    Que un requisito todavía no esté cumplido no deja ningún error acá: es el
    resultado de la operación y viaja en el paso, con su `missing` y su `hint`.
    Lo que sí se guarda para la pantalla es un dato inválido —un identificador
    con mayúsculas, un sitemap sin esquema—, porque ahí hay algo que corregir en
    lo que se envió y el mensaje va pegado a su campo (RT-08).
    """
    data, errors = _step_data(request)

    if errors:
        stash_errors(request, errors)
        return _back(request, step)

    result = None

    try:
        result = services.verify(request.user, step, data)
    except NotFound as exc:
        # Un código de paso que no existe es una dirección mal armada, no una
        # condición que la persona pueda resolver leyendo un mensaje.
        raise Http404(str(exc)) from exc
    except ValidationFailed as exc:
        stash_errors(request, {_field(exc): as_error(exc)})
    except ApiError as exc:
        # Lo que viaja es el código y no la oración porque la pantalla ramifica
        # por código (RT-08): «se agotó el cupo» y «Google no respondió» se
        # resuelven en lugares distintos, y deducir cuál es leyendo el texto se
        # rompe al primer cambio de redacción —y no se puede leer siquiera, si
        # está en un idioma que no es el de quien mira—.
        stash_errors(request, {'verify': as_error(exc)})

    return _back(request, step, result)


@login_required
@require_http_methods(['POST'])
def dismiss(request):
    """
    Omite el recorrido y sale al tablero.

    Omitir no restringe nada (FR-018): los formularios completos siguen
    ofreciendo las mismas operaciones y el progreso queda intacto para retomarlo.
    Lo único que cambia es que deja de ofrecerse al ingresar.

    Sale al tablero de la herramienta y no al listado ni a la raíz: es la
    pantalla de inicio de una cuenta ya en marcha, y quien acaba de omitir el
    recorrido está justamente decidiendo dónde empieza a moverse por su cuenta.

    Antes salía a la raíz porque la raíz **era** ese tablero. Desde que la raíz
    pasó a ser la elección de herramienta, mandar ahí devolvería a elegir la
    herramienta que la persona ya venía usando.
    """
    services.dismiss(request.user)
    return redirect('search_console')


# --- Lo que viaja entre la comprobación y la pantalla -----------------------


def _back(request, step: str, result: dict | None = None):
    """
    Vuelve al recorrido con un paso abierto, dicho en la dirección.

    Cuál queda abierto es parte de la respuesta y no de la pantalla: `?step=` es
    el estado enlazable del recorrido, y decidirlo del lado del navegador
    obligaría a una segunda navegación para acomodarlo.
    """
    request.session[CHECKED_STEP_KEY] = step

    open_step = _open_step(step, result)
    target = reverse('onboarding')
    return redirect(f'{target}?{urlencode({"step": open_step})}' if open_step else target)


def _open_step(step: str, result: dict | None) -> str:
    """
    Qué paso queda abierto al volver del «Comprobar».

    Avanza **sólo si el paso pasó**. Si no pasó se queda donde está: mandar al
    siguiente escondería el motivo justo cuando hay que leerlo, y daría por
    andado un paso que la comprobación no dio por cumplido (FR-017).

    Sin resultado —un dato inválido, un error de Google— tampoco se mueve: no hay
    veredicto sobre el paso, así que no hay nada que haya avanzado.

    Y con el recorrido terminado no se nombra ninguno: ya no queda ningún paso
    por resolver y lo que la pantalla abre es su cierre, que es de la pantalla y
    no de la máquina de pasos.
    """
    if result is None:
        return step
    if result['is_finished']:
        return ''

    checked = next((s for s in result['steps'] if s['code'] == step), None)
    if checked is None or checked['state'] != 'PASSED':
        return step

    return result['current_step'] or step


def _checked_step(request) -> str:
    """
    Lee el paso recién comprobado y lo consume.

    Consumirlo es la mitad del asunto: dejarlo en la sesión haría que recargar la
    pantalla al día siguiente volviera a anunciar el resultado de una
    comprobación vieja como si acabara de pasar.
    """
    return request.session.pop(CHECKED_STEP_KEY, '')


# --- Lo que llega del formulario --------------------------------------------


def _step_data(request) -> tuple[dict, dict]:
    """
    Los campos del paso, con el archivo de clave si vino.

    El tope de tamaño es el mismo que el de la carga de credenciales: subir el
    archivo equivocado tiene que fallar igual de rápido y con el mismo texto en
    las dos pantallas, y dos constantes se separan en cuanto alguien toca una.
    """
    data = {key: value for key, value in payload(request).items() if key != 'key_file'}
    key_file = request.FILES.get('key_file')

    if key_file is None:
        return data, {}

    if key_file.size > MAX_KEY_FILE_BYTES:
        return data, {
            'key_file': {'code': 'KEY_FILE_TOO_BIG', 'params': {'size': key_file.size // 1024}}
        }

    data['key_file'] = key_file.read()
    return data, {}


def _field(exc: ValidationFailed) -> str:
    """El campo que hay que corregir. Sin él, el error quedaría suelto arriba de todo."""
    return exc.details.get('field') or 'verify'
