"""
Lo que la landing recibe de Tools para dibujar el generador.

La landing presenta el mismo recorrido con otro diseño, y para eso necesita
tres cosas que viven acá: el catálogo sin prompts, los textos visibles y una
versión que le diga si lo que tiene guardado sigue vigente.

**Los textos viajan con el catálogo**, sólo en inglés: el website no tiene
versión en español (decisión de la owner, 2026-10-08). Salen del mismo
catálogo de traducción que usa la pantalla de Tools (`frontend/locales/en.json`),
así una opción nueva llega a la landing con su nombre sin que nadie lo escriba
dos veces.

**La versión** es el hash del catálogo, sus textos y el contenido de cada foto.
Las fotos entran porque una foto de muestra puede reemplazarse con el mismo
nombre —ya pasó con los acabados de pileta—, y sin su contenido la landing
nunca se enteraría. Se calcula con direcciones relativas para que no dependa
del dominio por el que se pregunta: el webhook la manda sin petición de por
medio y tiene que coincidir con la del endpoint.
"""

import hashlib
import json
from functools import cache

from django.conf import settings
from django.contrib.staticfiles import finders

from apps.ai import catalog, images, services

#: Las claves del catálogo de traducción que nombran algo del catálogo. Son las
#: que la pantalla de Tools resuelve en `labels.ts`.
TEXT_PREFIXES = ('ai.areas.', 'ai.services.', 'ai.fields.', 'ai.options.')

#: Cuántos caracteres del hash de una foto viajan en su dirección. Alcanza para
#: que dos versiones del mismo archivo no se confundan.
DIGEST_LENGTH = 12


def catalog_payload(static_url: str) -> dict:
    """
    El catálogo público y sus textos, con cada foto marcada por su contenido.

    `photo.max_side` le dice a la landing a qué tamaño comprimir la foto del
    visitante: llegando así, `images.normalize_source` la guarda sin tocarla.
    Va dentro de `catalog` para que la landing lo reciba sin cambiar lo que
    guarda.
    """
    public = catalog.public_catalog_props(static_url)
    public['photo'] = {'max_side': images.SOURCE_MAX_SIDE}
    return {
        'catalog': _with_digests(public, static_url),
        'texts': catalog_texts(),
    }


def catalog_version() -> str:
    """El hash de lo que la landing recibe, sin depender del dominio. Es el `ETag`."""
    payload = catalog_payload(settings.STATIC_URL)
    serialized = json.dumps(payload, sort_keys=True, ensure_ascii=False)
    return hashlib.sha256(serialized.encode()).hexdigest()


def catalog_texts() -> dict[str, str]:
    """Los textos visibles del catálogo, en inglés, por la misma clave que usa la pantalla."""
    path = settings.BASE_DIR / 'frontend' / 'locales' / 'en.json'
    texts = json.loads(path.read_text(encoding='utf-8'))
    return {key: value for key, value in texts.items() if key.startswith(TEXT_PREFIXES)}


def analysis_props(analysis) -> dict:
    """
    Un análisis tal como lo ve la landing. Códigos, nunca texto.

    Sin la foto (`source_url` pide sesión: el visitante ya la tiene en su
    navegador), sin la respuesta cruda del modelo y sin el consumo.

    `photo_size` es `[ancho, alto]` de la foto ya normalizada: la capa de
    marcas tiene que medir exactamente eso (`services.source_size`). Nulo si la
    foto no se pudo leer, que es un análisis que no sirve para marcar.

    `can_generate` dice si se puede seguir hasta generar
    (`PhotoAnalysis.can_generate`). Con `FAILED` + `MALFORMED_RESPONSE` es
    verdadero: el modelo no reconoció las áreas, la persona las elige a mano.
    Con otro fallo es falso: el pedido falló y corresponde reintentar.
    """
    try:
        photo_size = list(services.source_size(analysis))
    except images.NotAnImage:
        photo_size = None

    return {
        'id': str(analysis.id),
        'state': analysis.state,
        'error_code': analysis.error_code or None,
        'can_generate': analysis.can_generate,
        'areas': analysis.areas,
        'pool_present': analysis.has_pool,
        'confidence': dict(analysis.confidence or {}),
        'points': dict(analysis.points or {}),
        'photo_size': photo_size,
        'created_at': analysis.created_at.isoformat(),
        'finished_at': analysis.finished_at.isoformat() if analysis.finished_at else None,
    }


def generation_props(generation, absolute) -> dict:
    """
    Un pedido tal como lo ve la landing.

    `absolute` convierte una dirección del sitio en una completa: la landing
    vive en otro dominio. Sin el prompt, sin la foto y sin las marcas, que
    piden sesión. La nota vuelve para que la landing pueda ofrecer «ajustar»
    con la caja como estaba.
    """
    result_url = absolute(generation.result_url) if generation.result_url else None
    return {
        'id': str(generation.id),
        'analysis_id': str(generation.analysis_id),
        'state': generation.state,
        'error_code': generation.error_code or None,
        'areas': generation.areas,
        'note': generation.markup_note or generation.notes,
        'has_markup': bool(generation.markup_path),
        'result_url': result_url,
        'download_url': f'{result_url}?download=1' if result_url else None,
        'created_at': generation.created_at.isoformat(),
        'finished_at': generation.finished_at.isoformat() if generation.finished_at else None,
    }


def _with_digests(value, static_url: str):
    """
    Cada dirección de foto con `?v=<hash>` de su contenido.

    Sirve a los dos lados: cambia la versión del catálogo cuando cambia una
    foto, y le dice a la landing cuál bajar de nuevo sin comparar archivos.
    """
    if isinstance(value, list):
        return [_with_digests(item, static_url) for item in value]
    if not isinstance(value, dict):
        return value

    result = {}
    for key, item in value.items():
        if key == 'image' and isinstance(item, str):
            result[key] = _versioned(item, static_url)
        elif key == 'images' and isinstance(item, dict):
            result[key] = {answer: _versioned(url, static_url) for answer, url in item.items()}
        else:
            result[key] = _with_digests(item, static_url)
    return result


def _versioned(url: str, static_url: str) -> str:
    if not url.startswith(static_url):
        return url
    digest = _static_digest(url[len(static_url) :])
    return f'{url}?v={digest}' if digest else url


@cache
def _static_digest(relative: str) -> str:
    """
    El hash del archivo estático, o vacío si no está.

    En memoria por proceso: los estáticos sólo cambian con un deploy, y el
    deploy reinicia el proceso.
    """
    path = finders.find(relative)
    if not path:
        return ''
    with open(path, 'rb') as handle:
        return hashlib.sha256(handle.read()).hexdigest()[:DIGEST_LENGTH]
