"""
Pantalla de configuración: el estado de la conexión con Google.

Es la vista más importante del producto para alguien que llega por primera vez.
Ninguna de sus props se calcula acá: el estado sale de los servicios y la vista
sólo decide qué mostrar junto a qué.

**Los pasos de la guía ya no viajan por acá.** Estaban escritos una sola vez en
`apps/onboarding/content.py` y dibujados tres veces —dos componentes en el
recorrido y un tercero acá, recortado a cuatro dentro de un acordeón—, o sea que
una sola fuente en el servidor no producía una sola forma en la pantalla. El
recorrido guiado quedó como el único lugar donde los pasos se dibujan, y esta
pantalla lleva hasta ahí. De la guía sólo sobrevive **cuántos pasos son**, que es
lo que la invitación necesita para no mentir.
"""

from django.contrib.auth.decorators import login_required
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 stash_errors
from apps.core.requests import field
from apps.credentials.models import Credential, Module
from apps.credentials.services import (
    search_console_module,
    shared_active_credential,
    shared_resource_account,
    upload_credential,
    verify_credential,
)
from apps.credentials.validation import InvalidKeyFile
from apps.domains.models import PropertyType
from apps.onboarding.content import STEPS

#: Tamaño máximo del archivo de clave. Uno real pesa unos dos kilobytes; el
#: tope está para que subir un archivo equivocado y enorme falle rápido y con
#: un mensaje claro, no para ahorrar disco.
MAX_KEY_FILE_BYTES = 64 * 1024


@login_required
def settings_page(request):
    """
    Estado de la conexión, ficha de la credencial, el sitio y formulario de carga.

    **El sitio se conecta acá.** Antes esta pantalla terminaba en «la clave
    sirve» y todavía faltaba un paso —dar de alta la propiedad— que vivía en otra
    pantalla, alcanzable sólo por el menú: es lo que hacía que el estado más
    frecuente fuera «falta autorizar». Una pantalla que se llama «conexión» tiene
    que dejar conectado.

    El sitio en sí no viaja por acá: baja con las props compartidas, junto al
    estado de cuenta, porque el menú también lo necesita en todas las pantallas.
    Lo que sí viaja son las **opciones** del formulario, que son de esta vista.
    """
    credential = shared_active_credential(Module.SEARCH_CONSOLE)
    module = search_console_module()

    return render(
        request,
        'Settings/Index',
        props={
            'module': {
                'code': module.code,
                'name': module.name,
                'required_apis': module.required_apis,
                'required_scopes': module.required_scopes,
            },
            'credential': _credential_props(credential),
            'tour': {'steps_total': len(STEPS)},
            'history': _history_props(credential),
            # El acceso a la Indexing API, que **no se comprueba acá**: no hay
            # una consulta que lo averigüe, y lo único que se le puede pedir a
            # esa API es que publique una dirección de verdad. Lo que viaja es
            # lo que la evidencia ya sabe.
            'indexing': _indexing_props(credential),
            'property_types': [
                {'value': value, 'label': label} for value, label in PropertyType.choices
            ],
        },
    )


@login_required
@require_http_methods(['POST'])
def upload(request):
    """
    Carga o reemplaza la credencial del módulo.

    El reemplazo no es una operación aparte: subir una credencial nueva desactiva
    la anterior y deja los dominios intactos.
    """
    upload_file = request.FILES.get('key_file')
    project_id = field(request, 'project_id')

    if upload_file is None:
        stash_errors(request, {'key_file': {'code': 'KEY_FILE_REQUIRED'}})
        return _rejected(request)

    if upload_file.size > MAX_KEY_FILE_BYTES:
        stash_errors(
            request,
            {
                'key_file': {
                    'code': 'KEY_FILE_TOO_BIG',
                    'params': {'size': upload_file.size // 1024},
                }
            },
        )
        return _rejected(request)

    try:
        credential = upload_credential(
            shared_resource_account(request.user),
            project_id=project_id,
            key_file=upload_file.read(),
        )
    except InvalidKeyFile as exc:
        # No se llama `field`: este módulo importa `field` de `apps.core.requests`
        # y asignarlo acá lo volvería local en toda la función, dejando sin
        # resolver la llamada de más arriba.
        field_name = exc.field if exc.field == 'project_id' else 'key_file'
        stash_errors(request, {field_name: {'code': exc.code, 'params': exc.params}})
        return _rejected(request)

    # Se comprueba en el acto: dejar la credencial cargada y sin comprobar
    # obligaría a un segundo clic para averiguar lo único que importa, que es si
    # sirve.
    verify_credential(credential)
    return redirect('settings')


def _rejected(request):
    """
    Vuelve a la pantalla con el formulario donde estaba.

    El reemplazo se abre desde la dirección (`?replace=`), así que devolverlo sin
    el parámetro cerraría el diálogo y el error llegaría a una pantalla donde el
    campo que lo causó ya no está: habría que volver a abrirlo, volver a escribir
    y adivinar qué pasó. La carga del estado vacío no manda el parámetro y vuelve
    a la dirección de siempre.
    """
    reopen = field(request, 'replace')
    if not reopen:
        return redirect('settings')
    return redirect(f'{reverse("settings")}?{urlencode({"replace": reopen})}')


@login_required
@require_http_methods(['POST'])
def verify(request, credential_id):
    """Vuelve a preguntarle a Google si la credencial sirve."""
    credential = Credential.objects.filter(id=credential_id).first()
    if credential is None:
        stash_errors(request, {'key_file': {'code': 'CREDENTIAL_NOT_FOUND'}})
        return redirect('settings')

    result = verify_credential(credential)

    # Sólo se arrastra el resultado que no quedó guardado en la credencial. Los
    # demás ya viajan en `last_error_code` con su fecha, y repetirlos acá haría
    # que la pantalla dijera dos veces lo mismo: una en el panel del motivo y
    # otra suelta al lado del botón.
    if not result.ok and not result.persisted:
        stash_errors(request, {'verify': {'code': result.error_code or 'VERIFY_FAILED'}})

    return redirect('settings')


# --- Props ------------------------------------------------------------------


def _indexing_props(credential: Credential | None) -> dict | None:
    """
    Lo que sabemos del acceso a la Indexing API, con la prueba al lado.

    Nulo sin credencial: la Indexing API firma con la misma clave que Search
    Console, así que sin clave no hay nada que decir de ella. Dibujar la tarjeta
    igual la mostraría rota cuando lo que falta es un paso anterior.

    `checked_at` y `url` viajan juntos y **no son decoración**: son lo que
    convierte «funciona» en una afirmación verificable. Sin ellos sería una
    palabra nuestra, que es justo lo que esta parte del producto no vende.
    """
    if credential is None:
        return None

    from apps.indexing.services import connection_state

    connection = connection_state(credential.account)

    return {
        'state': connection.state,
        'checked_at': connection.checked_at.isoformat() if connection.checked_at else None,
        'url': connection.url,
        'batch_id': connection.batch_id,
        # El proyecto va acá y no lo deduce la pantalla: el enlace a la
        # biblioteca de Google Cloud lo necesita para abrir el proyecto correcto,
        # y una pantalla que lo adivine manda a habilitar la API en otro.
        'project_id': credential.google_project.project_id,
    }


def _credential_props(credential: Credential | None) -> dict | None:
    """
    Lo que la pantalla puede mostrar de una credencial.

    Se arma campo por campo y no con un serializador genérico a propósito: es la
    garantía de que `encrypted_key` no puede salir por acá aunque alguien agregue
    un campo al modelo mañana (principio III).
    """
    if credential is None:
        return None

    return {
        'id': str(credential.id),
        'client_email': credential.client_email,
        'project_id': credential.google_project.project_id,
        'key_fingerprint': credential.key_fingerprint,
        'private_key_id': credential.private_key_id,
        'status': credential.status,
        'is_usable': credential.is_usable,
        'last_checked_at': (
            credential.last_checked_at.isoformat() if credential.last_checked_at else None
        ),
        'last_error_code': credential.last_error_code or None,
        'last_error_detail': credential.last_error_detail or None,
        # Lo que la última comprobación pudo leer. Va siempre acompañado de
        # `last_checked_at`: una lista de propiedades sin fecha se lee como el
        # estado de ahora, y es el de la última vez que preguntamos.
        'accessible_properties': list(credential.accessible_properties or []),
        'created_at': credential.created_at.isoformat(),
    }


def _history_props(current: Credential | None) -> list[dict]:
    """
    Credenciales anteriores, para poder reconocer cuál se reemplazó y cuándo.

    Sin esto, reemplazar una clave borra de la vista toda huella de la anterior,
    y después no hay manera de explicar por qué un dominio dejó de responder a
    partir de cierta fecha.

    Lo que define «anterior» es no ser la asignada al módulo. Filtrar por estado
    dejaba afuera justo el caso que importa: reemplazar una credencial que
    funcionaba no cambia su estado —sigue comprobada— y desaparecía del
    historial en el momento exacto en que empezaba a hacer falta.
    """
    previous = Credential.objects.all()
    if current is not None:
        previous = previous.exclude(id=current.id)

    previous = previous.select_related('google_project').order_by('-created_at')[:5]
    return [
        {
            'id': str(c.id),
            'client_email': c.client_email,
            'key_fingerprint': c.key_fingerprint,
            'status': c.status,
            'created_at': c.created_at.isoformat(),
        }
        for c in previous
    ]
