"""
Avanzar, retomar y omitir el recorrido guiado.

Las tres operaciones se apoyan en la misma idea: **el progreso no decide nada,
lo registra**. Quién está cumplido lo dice la comprobación de cada paso contra el
estado real del sistema (`steps.py`), y acá se guarda ese veredicto junto con los
identificadores de lo que se fue creando. Al revés —una fila que declara por sí
sola en qué paso va la persona— el recorrido terminaría afirmando que Google está
conectado porque alguien apretó «Siguiente» tres veces.

Por eso mismo, cada lectura sincroniza: alguien que hizo todo por los formularios
completos y después abre el recorrido tiene que encontrar sus pasos marcados, no
un asistente que le pide de nuevo lo que ya hizo (FR-018, FR-019). La
sincronización es legítima porque no marca por visita: marca lo que la
comprobación efectiva encontró hecho.
"""

from django.urls import reverse
from django.utils import timezone

from apps.core.errors import NotFound, ValidationFailed
from apps.credentials.validation import InvalidKeyFile
from apps.onboarding import content, steps
from apps.onboarding.models import OnboardingProgress


def progress_for(account) -> OnboardingProgress:
    """
    El progreso de la cuenta, creándolo la primera vez.

    Es `get_or_create` sobre una relación uno a uno: por más veces que se entre
    al recorrido nunca hay dos filas, que es la mitad de «retomar no duplica».
    """
    progress, _created = OnboardingProgress.objects.get_or_create(account=account)
    return progress


def should_offer(account) -> bool:
    """
    Si al ingresar hay que ofrecer el recorrido (FR-015).

    No crea la fila: se pregunta en cada ingreso y en cada pantalla, y escribir
    en la base por una consulta de lectura dejaría filas de progreso para cuentas
    que nunca van a abrir el asistente.

    Se consulta en lugar de leer `account.onboarding` porque esa relación queda
    cacheada en la instancia de la cuenta: en la misma petición en que el
    recorrido avanza, contestaría con la copia vieja y volvería a ofrecer un
    recorrido recién terminado.
    """
    # El recorrido configura la instalación, no cada perfil. Si otro miembro ya
    # conectó el sitio común, ofrecerle el asistente a quien recién entra haría
    # parecer que la empresa está vacía. El progreso por cuenta queda intacto
    # para volver a ser útil cuando existan workspaces multitenant.
    from apps.domains.services import primary_shared_domain

    if primary_shared_domain() is not None:
        return False

    progress = OnboardingProgress.objects.filter(account=account).first()
    if progress is None:
        return True
    return not (progress.is_finished or progress.is_dismissed)


def state(account, *, checked_step: str = '') -> dict:
    """
    Todo lo que la pantalla del recorrido necesita, con los pasos ya comprobados.

    Sólo lee la base de datos: abrir el recorrido no llama a Google ni gasta
    cuota de nadie. Las comprobaciones que necesitan preguntarle a Google se
    hacen cuando la persona toca «Comprobar», nunca al mirar la pantalla.
    """
    progress = progress_for(account)
    ctx = steps.Context(account, progress.context)
    checks = steps.inspect_all(ctx)

    _sync(progress, checks)

    return _payload(progress, checks, ctx, checked_step=checked_step)


def verify(account, step_code: str, data: dict | None = None, *, client=None, http=None) -> dict:
    """
    Ejecuta el «Comprobar» de un paso y devuelve el recorrido entero actualizado.

    Devuelve el estado completo y no sólo el paso comprobado porque una sola
    respuesta de Google puede resolver varios: comprobar la clave contesta
    también si la API está encendida y si la cuenta de servicio tiene propiedades
    autorizadas. Devolver un paso suelto obligaría a la pantalla a pedir el resto
    en otra vuelta, y mientras tanto mostraría como pendiente algo que ya está.
    """
    if not steps.exists(step_code):
        raise NotFound(f'«{step_code}» no es un paso del recorrido.')

    progress = progress_for(account)
    ctx = steps.Context(account, progress.context)

    # Comprobar un paso es estar de vuelta en el recorrido, así que deja de
    # figurar como omitido. Conservar la marca mantendría para siempre el aviso
    # que invita a retomar algo que la persona ya retomó.
    progress.dismissed_at = None

    try:
        steps.act(step_code, ctx, data or {}, client=client, http=http)
    except InvalidKeyFile as exc:
        # Los errores del archivo de clave se traducen al error de validación del
        # contrato para que la pantalla y la API pública muestren exactamente lo
        # mismo, señalando el campo que hay que corregir (principio IV).
        raise ValidationFailed(
            exc.message,
            field=exc.field or 'key_file',
            reason=exc.code,
            params=exc.params,
        ) from exc

    checks = steps.inspect_all(ctx)
    _sync(progress, checks, force_save=True)

    return _payload(progress, checks, ctx, checked_step=step_code)


def dismiss(account) -> dict:
    """
    Omite el recorrido, sin restringir nada (FR-018).

    Lo único que cambia es que deja de ofrecerse: no toca permisos, ni límites,
    ni el estado de ningún objeto. Y no borra el progreso, así que retomarlo
    después arranca donde había quedado.
    """
    progress = progress_for(account)

    if not progress.is_dismissed:
        progress.dismissed_at = timezone.now()
        progress.save(update_fields=['dismissed_at', 'updated_at'])

    return state(account)


# --- Registro del progreso --------------------------------------------------


def _sync(progress: OnboardingProgress, checks: dict, *, force_save: bool = False) -> None:
    """
    Anota lo que la comprobación encontró cumplido y reubica el paso vigente.

    Lo cumplido no se desmarca aunque después deje de cumplirse. El recorrido
    registra por dónde pasó la persona, y borrarle un paso ya hecho porque hoy la
    credencial está caída convertiría una incidencia pasajera en trabajo perdido.
    Que hoy no se cumpla lo dice la comprobación viva, que viaja en cada paso.
    """
    before = (list(progress.completed_steps), progress.current_step, progress.completed_at)
    dates = progress.context.setdefault('completed_dates', {})

    for code in content.STEP_CODES:
        if checks[code].ok and code not in progress.completed_steps:
            progress.mark_completed(code)
            dates[code] = timezone.now().isoformat()

    current = _first_actionable_step(progress, checks)
    if current:
        progress.current_step = current

    after = (list(progress.completed_steps), progress.current_step, progress.completed_at)
    if force_save or before != after:
        progress.save()


def _first_actionable_step(progress: OnboardingProgress, checks: dict) -> str:
    """
    El primer paso pendiente en el que la persona puede hacer algo.

    Los bloqueados se saltean: el paso 2 no se puede comprobar hasta que exista
    la clave del 3, y dejar el recorrido parado ahí sería pedirle a alguien que
    resuelva algo que todavía no depende de él. Sigue apareciendo pendiente en la
    lista, con su motivo; lo que cambia es dónde se abre el asistente.
    """
    # Sobre los obligatorios: un paso opcional sin cumplir no puede ser el que
    # abre el asistente, o quien no necesita esa API nunca sale de él.
    pending = [c for c in content.REQUIRED_STEP_CODES if c not in progress.completed_steps]
    if not pending:
        return ''

    actionable = [c for c in pending if checks[c].state != steps.CheckState.BLOCKED]
    return actionable[0] if actionable else pending[0]


# --- Forma de la respuesta --------------------------------------------------


def _payload(
    progress: OnboardingProgress, checks: dict, ctx: steps.Context, *, checked_step: str
) -> dict:
    return {
        'should_offer': not (progress.is_finished or progress.is_dismissed),
        'current_step': progress.current_step,
        'checked_step': checked_step,
        'completed_steps': list(progress.completed_steps),
        'is_finished': progress.is_finished,
        'is_dismissed': progress.is_dismissed,
        'completed_at': _date(progress.completed_at),
        'dismissed_at': _date(progress.dismissed_at),
        'started_at': _date(progress.created_at),
        'steps': [_step(code, progress, checks[code], ctx) for code in content.STEP_CODES],
        'created': _created_objects(ctx),
        'key_file_example': content.KEY_FILE_EXAMPLE,
        'key_file_highlights': list(content.KEY_FILE_HIGHLIGHTS),
    }


def _step(
    code: str, progress: OnboardingProgress, check: steps.StepCheck, ctx: steps.Context
) -> dict:
    """
    Un paso, con su guía y su comprobación en la misma estructura.

    La guía sale de `content.py`, que es la misma que usa la pantalla de
    configuración. Repetir acá los textos garantizaría que en algún momento uno
    diga una ruta de menú que Google ya cambió y el otro no.
    """
    guide = content.step(code) or {}

    return {
        'code': code,
        'position': content.STEP_CODES.index(code) + 1,
        'has_example': guide.get('has_example', False),
        'state': check.state,
        'completed': code in progress.completed_steps,
        'completed_at': progress.context.get('completed_dates', {}).get(code),
        'is_current': code == progress.current_step,
        # Qué falta y dónde se resuelve viajan como código: las dos frases las
        # arma el catálogo del cliente, en el idioma de quien está haciendo el
        # recorrido.
        'reason': check.reason,
        'reason_params': check.params,
        'blocked_by': check.blocked_by,
        'evidence': check.evidence,
        'inputs': steps.inputs(code),
        'form_path': steps.form_path(code, ctx),
    }


def _created_objects(ctx: steps.Context) -> dict:
    """
    Los objetos del recorrido, nombrados.

    Es lo que la pantalla usa para decir «Dominio ya dado de alta: ejemplo.com»
    al retomar, y para que el cierre enlace al lote encolado y a la cobertura del
    dominio en vez de dejar a la persona buscándolos en el menú.

    Las tres direcciones salen de `reverse()` y no de cadenas armadas acá. Escritas
    a mano duplicarían la tabla de rutas del proyecto, y el día que una cambie
    éstas quedarían viejas sin que nada avise: el destino existe, así que ni
    siquiera hay un error que rastrear.
    """
    project = ctx.project()
    credential = ctx.credential()
    domain = ctx.domain()
    sitemap = ctx.sitemap()
    batch = ctx.batch()

    return {
        'project_id': project.project_id if project else None,
        'client_email': credential.client_email if credential else None,
        'credential_status': credential.status if credential else None,
        'properties': steps.accessible_properties(ctx),
        'domain_id': str(domain.id) if domain else None,
        'hostname': domain.hostname if domain else None,
        'property_uri': domain.property_uri if domain else None,
        'access_state': domain.access_state if domain else None,
        'sitemap_id': str(sitemap.id) if sitemap else None,
        'sitemap_location': sitemap.location if sitemap else None,
        'batch_id': str(batch.id) if batch else None,
        'batch_state': batch.state if batch else None,
        'batch_path': reverse('batch.show', kwargs={'batch_id': batch.id}) if batch else None,
        'coverage_path': (reverse('coverage', kwargs={'domain_id': domain.id}) if domain else None),
        'sitemaps_path': (reverse('sitemaps', kwargs={'domain_id': domain.id}) if domain else None),
    }


def _date(value) -> str | None:
    return value.isoformat() if value else None
