"""
media_service.py — acceso seguro a las fotos de las familias.

Antes de pagar, el navegador solo recibe una vista previa reducida, servida por una URL firmada con
caducidad. Los originales nunca se exponen como archivos estáticos: se descargan únicamente con un
enlace firmado ligado a un pedido pagado (ver main.py::download_original).

La vista previa es, por orden de preferencia:
  1. La foto con las CARAS PROTEGIDAS en servidor (StoryFace KIS), si el colegio lo tiene activado
     (`protection_service`, mismo mecanismo que hec-photo-app).
  2. Un respaldo propio: versión reducida (sin marca de agua: la protección es la de arriba). Se usa si la
     protección de caras está apagada o si KIS falla — a diferencia de HEC, nunca se cae al original sin proteger.
Encima, el navegador puede dibujar el overlay animado (lens/puzzle) que estropea las capturas.
"""
import json
import logging
from pathlib import Path
from typing import Optional
from urllib.parse import quote

from PIL import Image, ImageOps

from core import config
from core.security import sign, verify_signature
from services import photo_service, protection_service
from services.qr_detect import CODE_RE  # noqa: F401  (el formato de código es el mismo en todo el sistema)

logger = logging.getLogger(__name__)

PREVIEW_PX = {"full": 1000, "thumb": 480}


def valid_code(code: str) -> bool:
    return bool(CODE_RE.match(code or ""))


def safe_photo_path(school_id: str, code: str, filename: str) -> Optional[Path]:
    """Ruta absoluta del original, o None si el código/archivo es sospechoso (../, absoluto, oculto) o no existe."""
    if not valid_code(code):
        return None
    return photo_service.find_photo(school_id, code, filename)


def read_meta(school_id: str, code: str) -> dict:
    """meta.json de la familia (nombre del niño, etiquetas de fotos IA), fusionando todas sus carpetas."""
    merged: dict = {"photo_names": {}}
    for _ph, folder in photo_service.family_dirs(school_id, code):
        f = folder / "meta.json"
        if f.is_file():
            try:
                data = json.loads(f.read_text(encoding="utf-8"))
            except Exception:
                continue
            merged["photo_names"].update(data.get("photo_names", {}))
            merged.setdefault("child_name", data.get("child_name", ""))
    return merged


# ─── URLs firmadas ────────────────────────────────────────────────────────────

def preview_url(code: str, filename: str, size: str = "full") -> str:
    exp, sig = sign(f"preview:{code}/{filename}", config.MEDIA_URL_TTL_SECS)
    return f"/api/media/preview/{code}/{quote(filename)}?exp={exp}&sig={sig}&size={size}"


def check_preview_signature(code: str, filename: str, exp: int, sig: str) -> bool:
    return verify_signature(f"preview:{code}/{filename}", exp, sig)


def download_url(order_id: str, filename: str) -> str:
    exp, sig = sign(f"download:{order_id}/{filename}", config.DOWNLOAD_URL_TTL_SECS)
    return f"/api/portal/download/{order_id}/{quote(filename)}?exp={exp}&sig={sig}"


def download_zip_url(order_id: str) -> str:
    exp, sig = sign(f"downloadzip:{order_id}", config.DOWNLOAD_URL_TTL_SECS)
    return f"/api/portal/download-zip/{order_id}?exp={exp}&sig={sig}"


def check_download_zip_signature(order_id: str, exp: int, sig: str) -> bool:
    return verify_signature(f"downloadzip:{order_id}", exp, sig)


def check_download_signature(order_id: str, filename: str, exp: int, sig: str) -> bool:
    return verify_signature(f"download:{order_id}/{filename}", exp, sig)


# ─── Miniaturas del panel de administración ───────────────────────────────────
# El personal ve miniaturas SIN protección de caras (y sin gastar créditos de KIS): solo se firman,
# caducan a la hora y llevan el colegio en la firma, así que un enlace no sirve para otro colegio.

ADMIN_THUMB_PX = 300


def admin_thumb_url(school_id: str, photographer: str, code: str, filename: str) -> str:
    exp, sig = sign(f"admin:{school_id}/{photographer}/{code}/{filename}", 3600)
    return (f"/api/media/admin-thumb/{school_id}/{quote(photographer)}/{code}/{quote(filename)}"
            f"?exp={exp}&sig={sig}")


def admin_thumb_path(school_id: str, photographer: str, code: str, filename: str, exp: int, sig: str) -> Optional[Path]:
    """Miniatura del original (o None si la firma no vale o la ruta se sale de la carpeta del colegio)."""
    if not verify_signature(f"admin:{school_id}/{photographer}/{code}/{filename}", exp, sig):
        return None
    for part in (school_id, photographer, code, filename):
        if not part or part.startswith(".") or "/" in part or "\\" in part:
            return None
    base = photo_service.school_root(school_id)
    src = base / photographer / code / filename
    if photographer == "" or not src.is_file() or not photo_service.is_image(filename):
        return None
    try:
        src.resolve().relative_to(base.resolve())
    except ValueError:
        return None
    dst = photo_service.mirror_path(src, f".adm.{ADMIN_THUMB_PX}.webp")
    try:
        if dst.exists() and dst.stat().st_mtime >= src.stat().st_mtime:
            return dst
        dst.parent.mkdir(parents=True, exist_ok=True)
        with Image.open(src) as im:
            im = ImageOps.exif_transpose(im)
            im.thumbnail((ADMIN_THUMB_PX, ADMIN_THUMB_PX), Image.LANCZOS)
            tmp = dst.with_suffix(".tmp")
            im.convert("RGB").save(tmp, format="WEBP", quality=75)
            tmp.replace(dst)
        return dst
    except Exception as exc:
        logger.warning("Miniatura de admin imposible %s: %s", src, exc)
        return None


# ─── Tarjetas de familia (PNG) ─────────────────────────────────────────────────
# El navegador no puede mandar la cabecera Authorization desde un <img>, así que el panel recibe una URL
# firmada (caduca a la hora y lleva el colegio en la firma, como las miniaturas).

def card_url(school_id: str, code: str) -> str:
    exp, sig = sign(f"card:{school_id}/{code}", 3600)
    return f"/api/media/card/{school_id}/{code}.png?exp={exp}&sig={sig}"


def check_card_signature(school_id: str, code: str, exp: int, sig: str) -> bool:
    return verify_signature(f"card:{school_id}/{code}", exp, sig)


# ─── Respaldo: vista previa reducida ──────────────────────────────────────────

def reduced_preview(src: Path, size: str = "full") -> Optional[Path]:
    """Genera (una vez) la vista previa reducida (≤ PREVIEW_PX, sin texto ni marca) y devuelve su ruta.
    Nombre `.pv.`: las copias `.wm.` de versiones anteriores llevaban marca de agua y ya no se sirven."""
    px = PREVIEW_PX.get(size, PREVIEW_PX["full"])
    dst = photo_service.mirror_path(src, f".pv.{px}.webp")
    try:
        if dst.exists() and dst.stat().st_mtime >= src.stat().st_mtime:
            return dst
        dst.parent.mkdir(parents=True, exist_ok=True)
        with Image.open(src) as im:
            im = ImageOps.exif_transpose(im)
            im.thumbnail((px, px), Image.LANCZOS)
            out = im.convert("RGB")
        tmp = dst.with_suffix(".tmp")
        out.save(tmp, format="WEBP", quality=70)
        tmp.replace(dst)
        return dst
    except Exception as exc:
        logger.warning("Vista previa imposible %s: %s", src, exc)
        return None


async def render_preview(school_id: str, code: str, filename: str, size: str = "full") -> Optional[Path]:
    """Vista previa a servir: caras protegidas si procede; si no (o si KIS falla), la vista reducida."""
    import asyncio
    src = safe_photo_path(school_id, code, filename)
    if src is None:
        return None
    px = PREVIEW_PX.get(size, PREVIEW_PX["full"])
    try:
        protected = await protection_service.protected_preview(src, school_id, px)
        if protected is not None:
            return protected
    except Exception as exc:
        logger.warning("[%s] Protección de caras fallida para %s (%s): se sirve la vista previa reducida",
                       school_id, filename, exc)
    return await asyncio.to_thread(reduced_preview, src, size)


# ─── Listado para el portal ───────────────────────────────────────────────────

def list_family_photos(school_id: str, code: str) -> list[dict]:
    meta = read_meta(school_id, code)
    child_name = meta.get("child_name", "")
    names = meta.get("photo_names", {})
    out = []
    for f in photo_service.list_family_files(school_id, code):
        out.append({
            "id": f.stem,
            "filename": f.name,
            "url": preview_url(code, f.name, "full"),
            "thumb_url": preview_url(code, f.name, "thumb"),
            # No person's name ever travels: only the label of an AI creation (the job) is kept.
            "child_name": names.get(f.name, "") if photo_service.photo_kind(f.name) == "ai" else "",
            "photographer": photo_service.photographer_of(f),
            "kind": photo_service.photo_kind(f.name),
        })
    return out
