"""Postprocesa un PDF existente insertando link annotations internos (GOTO).

Motor: PyMuPDF (``import fitz``). Pensado para PDFs generados por matplotlib
``PdfPages``, que no soporta hyperlinks internos: tras escribir el PDF, esta
funcion lo reabre y le añade anotaciones "ir a" (GOTO) desde un rectangulo de
una pagina origen hasta un punto de una pagina destino. Util para hacer
clicables terminos que apuntan a su entrada en un glosario al final del
documento.

Estilo dict-no-throw del grupo `eda`: NUNCA lanza; devuelve un dict de estado.
"""

import os


def add_pdf_internal_links(pdf_path: str, links: list) -> dict:
    """Añade link annotations internos (GOTO) a un PDF ya escrito.

    Postprocesa un PDF (p.ej. generado por matplotlib PdfPages, que NO soporta
    hyperlinks internos) insertando, por cada entrada de ``links``, una
    anotacion de tipo "ir a" desde un rectangulo de una pagina origen hasta un
    punto de una pagina destino. Sirve para hacer clicables terminos que apuntan
    a su entrada en un glosario al final del documento.

    Args:
        pdf_path: ruta al PDF existente (se reescribe in situ).
        links: lista de dicts, cada uno:
            {
              "src_page": int,            # indice 0-based de la pagina origen
              "src_rect": [x0,y0,x1,y1],  # rectangulo clicable, en PUNTOS PDF
                                          # (1/72") con origen ARRIBA-IZQUIERDA
              "dst_page": int,            # indice 0-based de la pagina destino
              "dst_point": [x, y],        # punto destino, mismos puntos top-left
            }

    Returns:
        dict (NUNCA lanza): {"status":"ok","n_links":int,"n_skipped":int}
        o {"status":"error","error":str}. Si pymupdf no esta disponible o el
        archivo no existe -> {"status":"error", ...}.
    """
    try:
        try:
            import fitz  # PyMuPDF
        except Exception as exc:  # ImportError u otro fallo de carga
            return {"status": "error", "error": f"pymupdf no disponible: {exc}"}

        if not isinstance(pdf_path, str) or not pdf_path:
            return {"status": "error", "error": "pdf_path debe ser una ruta no vacia"}
        if not os.path.isfile(pdf_path):
            return {"status": "error", "error": f"el archivo no existe: {pdf_path}"}

        if links is None:
            links = []
        if not isinstance(links, (list, tuple)):
            return {"status": "error", "error": "links debe ser una lista de dicts"}

        doc = fitz.open(pdf_path)
        try:
            n_pages = doc.page_count
            n_ok = 0
            n_skipped = 0

            for link in links:
                if not isinstance(link, dict):
                    n_skipped += 1
                    continue

                src_page = link.get("src_page")
                dst_page = link.get("dst_page")
                src_rect = link.get("src_rect")
                dst_point = link.get("dst_point")

                # src_page / dst_page: enteros 0-based en rango.
                if not _is_int(src_page) or not _is_int(dst_page):
                    n_skipped += 1
                    continue
                if not (0 <= src_page < n_pages) or not (0 <= dst_page < n_pages):
                    n_skipped += 1
                    continue

                # src_rect: 4 numeros.
                if not _is_num_seq(src_rect, 4):
                    n_skipped += 1
                    continue
                # dst_point: 2 numeros.
                if not _is_num_seq(dst_point, 2):
                    n_skipped += 1
                    continue

                try:
                    doc[int(src_page)].insert_link(
                        {
                            "kind": fitz.LINK_GOTO,
                            "from": fitz.Rect(*[float(v) for v in src_rect]),
                            "page": int(dst_page),
                            "to": fitz.Point(*[float(v) for v in dst_point]),
                        }
                    )
                    n_ok += 1
                except Exception:
                    n_skipped += 1
                    continue

            # Guardado seguro: escribir a temporal en el mismo directorio y
            # reemplazar atomicamente (evita corromper el PDF original).
            directory = os.path.dirname(os.path.abspath(pdf_path)) or "."
            base = os.path.basename(pdf_path)
            tmp_path = os.path.join(directory, f".{base}.tmp_links")
            doc.save(tmp_path)
        finally:
            doc.close()

        os.replace(tmp_path, pdf_path)

        return {"status": "ok", "n_links": n_ok, "n_skipped": n_skipped}
    except Exception as exc:  # degrada cualquier fallo a dict de error
        return {"status": "error", "error": str(exc)}


def _is_int(value) -> bool:
    """True si value es un entero (no bool)."""
    return isinstance(value, int) and not isinstance(value, bool)


def _is_num_seq(value, length: int) -> bool:
    """True si value es una secuencia de `length` numeros (int/float, no bool)."""
    if not isinstance(value, (list, tuple)) or len(value) != length:
        return False
    for v in value:
        if isinstance(v, bool) or not isinstance(v, (int, float)):
            return False
    return True
