"""
Decodificación robusta de códigos QR desde una imagen en disco.

Función del registry (grupo de capacidad `qr`, dominio `datascience`). Pensada para el caso real
en el que un lector básico (pyzbar, `cv2.QRCodeDetector` sobre la imagen cruda) NO capta el QR:
screenshots de pantalla con QR pálidos (bajo contraste) o pequeños. En vez de un único intento,
genera varias variantes preprocesadas de la imagen y prueba cada detector disponible sobre cada
variante, parando al primer acierto.

Impura: lee un archivo de disco y depende de OpenCV (`opencv-contrib-python-headless`). Degrada
limpio (devuelve `[]`) si la imagen no se puede leer o si ningún QR se decodifica; no lanza.

Detectores (se usan los que estén instalados; el import se envuelve en try/except para degradar):
  - `cv2.QRCodeDetectorAruco`  (preferido — OpenCV puro, sin libs de sistema)
  - `cv2.QRCodeDetector`       (fallback OpenCV puro)
  - `cv2.wechat_qrcode.WeChatQRCode`  (excelente con bajo contraste; SOLO si los modelos cargan)
  - `pyzbar`                   (bonus opcional; requiere la lib de sistema `libzbar0`)

Cero dependencias de sistema obligatorias: con solo OpenCV la función ya funciona.
"""

from __future__ import annotations

import sys

import numpy as np

# OpenCV (cv2) se importa de forma perezosa dentro de las funciones que lo usan:
# un import a nivel de módulo rompería `import datascience` en entornos sin
# opencv instalado (p. ej. venvs de analysis que solo usan las funciones de
# series temporales o perfilado del paquete).


# --------------------------------------------------------------------------------------------
# Detectores. Cada uno se normaliza a una función run(img) -> list[str] que nunca lanza.
# --------------------------------------------------------------------------------------------
def _make_opencv_runner(detector):
    """Envuelve un cv2.QRCodeDetector(Aruco) en run(img) -> list[str]."""
    import cv2

    def run(img):
        out: list[str] = []
        # detectAndDecodeMulti: capta varios QR en la misma imagen.
        try:
            ok, decoded, _points, _ = detector.detectAndDecodeMulti(img)
            if ok and decoded:
                out = [s for s in decoded if s]
        except cv2.error:
            pass
        if not out:
            # Fallback al decodificador de un solo QR.
            try:
                s, _pts, _ = detector.detectAndDecode(img)
                if s:
                    out = [s]
            except cv2.error:
                pass
        return out

    return run


def _make_wechat_runner(wd):
    """Envuelve un cv2.wechat_qrcode.WeChatQRCode en run(img) -> list[str]."""

    def run(img):
        try:
            texts, _points = wd.detectAndDecode(img)
            return [t for t in texts if t]
        except Exception:
            # Si los modelos no están cargados o el detector falla, degradar sin romper.
            return []

    return run


def _make_pyzbar_runner(zbar_decode):
    """Envuelve pyzbar.decode en run(img) -> list[str]."""

    def run(img):
        out: list[str] = []
        try:
            for sym in zbar_decode(img):
                try:
                    out.append(sym.data.decode("utf-8", "replace"))
                except Exception:
                    pass
        except Exception:
            return []
        return out

    return run


def _build_detectors(debug=False):
    """Construye la lista de (nombre, runner) de detectores disponibles, en orden de preferencia."""
    import cv2

    detectors = []

    # OpenCV Aruco (preferido): no requiere libs de sistema ni descarga de modelos.
    if hasattr(cv2, "QRCodeDetectorAruco"):
        try:
            detectors.append(("opencv_aruco", _make_opencv_runner(cv2.QRCodeDetectorAruco())))
        except Exception:
            pass

    # OpenCV clásico (fallback puro).
    if hasattr(cv2, "QRCodeDetector"):
        try:
            detectors.append(("opencv", _make_opencv_runner(cv2.QRCodeDetector())))
        except Exception:
            pass

    # WeChat QR (excelente con bajo contraste) — SOLO si los modelos cargan; opcional.
    if hasattr(cv2, "wechat_qrcode"):
        try:
            wd = cv2.wechat_qrcode.WeChatQRCode()
            detectors.append(("wechat", _make_wechat_runner(wd)))
        except Exception:
            # Modelos no presentes / build sin soporte → saltar sin romper.
            pass

    # pyzbar (bonus): requiere libzbar0 (lib de sistema). Degrada si falta.
    try:
        from pyzbar.pyzbar import decode as _zbar_decode  # type: ignore

        detectors.append(("pyzbar", _make_pyzbar_runner(_zbar_decode)))
    except (ImportError, OSError, Exception):  # noqa: B014 - OSError = libzbar0 ausente
        pass

    if debug:
        print(
            f"[decode_qr_image] detectores disponibles: {[n for n, _ in detectors]}",
            file=sys.stderr,
        )
    return detectors


# --------------------------------------------------------------------------------------------
# Variantes preprocesadas de la imagen. Orden = prioridad; se para en el primer acierto.
# --------------------------------------------------------------------------------------------
def _load_bgr(image_path):
    """Carga la imagen como BGR (uint8). Devuelve None si no se puede leer."""
    import cv2

    bgr = cv2.imread(image_path, cv2.IMREAD_COLOR)
    if bgr is not None:
        return bgr
    # Fallback PIL para formatos que cv2.imread no maneja en esta build.
    try:
        from PIL import Image

        pil = Image.open(image_path).convert("RGB")
        return cv2.cvtColor(np.asarray(pil), cv2.COLOR_RGB2BGR)
    except Exception:
        return None


def _build_variants(image_path, upscale):
    """Genera (nombre, ndarray) de variantes preprocesadas, en orden de prioridad."""
    import cv2

    bgr = _load_bgr(image_path)
    if bgr is None:
        return []

    gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)

    # Contrast stretch (NORM_MINMAX): clave para QR de bajo contraste (gris sobre gris).
    stretch = cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)

    # CLAHE: realce de contraste local.
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(gray)

    # Upscale del stretch: QR pequeño es la causa #1 de fallo.
    if upscale and upscale > 1:
        up = cv2.resize(stretch, None, fx=upscale, fy=upscale, interpolation=cv2.INTER_CUBIC)
    else:
        up = stretch

    # Binarizaciones sobre el stretch (mejor base que el gris crudo).
    _, otsu = cv2.threshold(stretch, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    adaptive = cv2.adaptiveThreshold(
        stretch, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 5
    )

    variants = [
        ("original", bgr),
        ("gray", gray),
        ("contrast_stretch", stretch),
        ("clahe", clahe),
        ("upscale", up),
        ("otsu", otsu),
        ("adaptive_gaussian", adaptive),
    ]

    # Rotaciones sobre la mejor variante binarizada (Otsu).
    for name, rot in (
        ("rot90", cv2.ROTATE_90_CLOCKWISE),
        ("rot180", cv2.ROTATE_180),
        ("rot270", cv2.ROTATE_90_COUNTERCLOCKWISE),
    ):
        variants.append((f"otsu_{name}", cv2.rotate(otsu, rot)))

    return variants


# --------------------------------------------------------------------------------------------
# API pública.
# --------------------------------------------------------------------------------------------
def decode_qr_image(image_path: str, upscale: int = 2, debug: bool = False) -> list[str]:
    """Decodifica los códigos QR de una imagen, robusto a bajo contraste y QR pequeños.

    Genera varias variantes preprocesadas de la imagen (escala de grises, contrast stretch,
    CLAHE, upscale, binarización Otsu/adaptativa, rotaciones) y prueba cada detector disponible
    (OpenCV Aruco/clásico, WeChat si hay modelos, pyzbar si hay libzbar0) sobre cada variante,
    parando al primer acierto.

    Parámetros (`upscale` y `debug` pensados como opciones keyword):
        image_path: ruta del archivo de imagen a leer (png/jpg/...).
        upscale: factor de ampliación (INTER_CUBIC) aplicado a la variante de contraste estirado
            para rescatar QR pequeños. Default 2. <=1 desactiva el upscale.
        debug: si True, imprime a stderr qué variante/detector acertó (o que no se detectó nada).

    Returns:
        Lista de payloads de texto de los QR detectados (deduplicada, preservando orden). Lista
        vacía si no se detecta ninguno o si la imagen no se puede leer. No lanza.
    """
    try:
        variants = _build_variants(image_path, upscale)
    except Exception as exc:  # pragma: no cover - defensa ante imágenes corruptas
        if debug:
            print(f"[decode_qr_image] fallo construyendo variantes: {exc}", file=sys.stderr)
        return []

    if not variants:
        if debug:
            print(f"[decode_qr_image] no se pudo leer la imagen: {image_path}", file=sys.stderr)
        return []

    detectors = _build_detectors(debug=debug)
    if not detectors:
        if debug:
            print("[decode_qr_image] ningún detector QR disponible", file=sys.stderr)
        return []

    for vname, vimg in variants:
        for dname, drun in detectors:
            payloads = drun(vimg)
            uniq = list(dict.fromkeys(p for p in payloads if p))
            if uniq:
                if debug:
                    print(
                        f"[decode_qr_image] acierto variante={vname} detector={dname} "
                        f"n={len(uniq)}",
                        file=sys.stderr,
                    )
                return uniq

    if debug:
        print("[decode_qr_image] ningún QR decodificado en ninguna variante", file=sys.stderr)
    return []


if __name__ == "__main__":
    # Demo CLI para `python3 decode_qr_image.py <image_path> [upscale] [debug]`.
    # (fn run usa su propio runner generado; este bloque es para invocación manual directa.)
    import json

    if len(sys.argv) < 2:
        print(json.dumps({"error": "uso: <image_path> [upscale] [debug]"}))
        sys.exit(1)

    _path = sys.argv[1]
    _upscale = int(sys.argv[2]) if len(sys.argv) > 2 else 2
    _debug = (sys.argv[3].lower() in ("1", "true", "yes")) if len(sys.argv) > 3 else False

    _result = decode_qr_image(_path, upscale=_upscale, debug=_debug)
    print(json.dumps(_result))
