"""render_paper_pdf — convierte un paper académico IMRaD en Markdown a un PDF.

Toma un paper escrito en Markdown con frontmatter YAML opcional (título,
autores, fecha, abstract) más un cuerpo dividido en secciones IMRaD por
encabezados de nivel 1 (``# Introduction``, ``# Methods``, ...) y produce un PDF
``out/paper.pdf`` junto al paper.

REUTILIZA el paginador de flujo del paquete ``automatic_eda`` (el mismo motor
que rinde los informes EDA en PDF móvil A5): no reimplementa paginación ni toca
matplotlib directamente. Cada sección IMRaD se mapea a un ``Chapter`` (empieza
en página nueva). El motor ``_place_markdown`` parsea por sí mismo headings,
listas, tablas pipe, párrafos y ``**negrita**`` dentro del texto, pero NO
entiende la sintaxis de imagen Markdown ``![alt](src)``; por eso esta función
detecta esas líneas y las convierte en bloques ``Image`` separados, partiendo el
texto Markdown alrededor de cada imagen.

dict-no-throw (estilo del grupo eda): NUNCA lanza. Devuelve
``{status, pdf_path, n_pages, note}``; ante cualquier fallo devuelve
``status="error"`` con ``pdf_path=None`` y la causa en ``note``.
"""

from __future__ import annotations

import datetime as _dt
import os
import re

from datascience.automatic_eda import Chapter, Heading, Image, Markdown, render_pdf

# Una línea cuyo único contenido es una imagen Markdown: ![alt](src)
_IMG_LINE = re.compile(r"^\s*!\[([^\]]*)\]\(\s*([^)\s]+)\s*\)\s*$")
# Un encabezado de nivel 1 al inicio de línea (un solo '#' seguido de espacio).
_H1_LINE = re.compile(r"^#[ \t]+(.+?)\s*$")


def render_paper_pdf(paper_dir: str) -> dict:
    """Renderiza un paper académico Markdown IMRaD en un PDF.

    Args:
        paper_dir: ruta al directorio del paper (``papers/<slug>/``, del que se
            lee ``paper.md``) o directamente la ruta a un archivo ``paper.md``.

    Returns:
        dict (nunca lanza): ``{status: "ok"|"error", pdf_path: str|None,
        n_pages: int, note: str}``. En éxito ``pdf_path`` es la ruta escrita y
        ``n_pages`` el total de páginas; en error ``pdf_path`` es None y
        ``note`` explica la causa.
    """
    try:
        # 1) Resolver el path del paper.md y el directorio base.
        arg = str(paper_dir)
        md_path = arg if arg.endswith(".md") else os.path.join(arg, "paper.md")

        # 2) Si el paper.md no existe, degradar sin crash.
        if not os.path.isfile(md_path):
            return {"status": "error", "pdf_path": None, "n_pages": 0,
                    "note": f"paper.md no encontrado: {md_path}"}

        base_dir = os.path.dirname(os.path.abspath(md_path))

        # 3) Leer el archivo y separar frontmatter del cuerpo.
        with open(md_path, "r", encoding="utf-8") as fh:
            text = fh.read()
        fm_text, body = _split_frontmatter(text)
        fm = _parse_frontmatter(fm_text)

        title = _safe_str(fm.get("title")).strip()
        authors = fm.get("authors")
        date_raw = fm.get("date")
        abstract = _safe_str(fm.get("abstract")).strip()

        # 4) Construir los capítulos: portada (si hay título) + cuerpo IMRaD.
        chapters: list = []
        if title:
            cover_md = _portada_markdown(authors, date_raw, abstract)
            cover_blocks: list = [Heading(text=title, level=1)]
            if cover_md.strip():
                cover_blocks.append(Markdown(text=cover_md))
            chapters.append(Chapter(id="portada", title=title, version="1.0.0",
                                    blocks=cover_blocks))

        preamble, sections = _split_body_sections(body)

        if not sections:
            # Sin encabezados H1: todo el cuerpo en un único capítulo.
            chapters.append(Chapter(
                id="cuerpo", title="Cuerpo", version="1.0.0",
                blocks=_markdown_to_blocks(body, base_dir)))
        else:
            # Texto antes del primer H1 (si lo hay) como capítulo previo.
            if preamble.strip():
                chapters.append(Chapter(
                    id="cuerpo", title="Cuerpo", version="1.0.0",
                    blocks=_markdown_to_blocks(preamble, base_dir)))
            for idx, (sec_title, sec_body) in enumerate(sections):
                blocks: list = [Heading(text=sec_title, level=1)]
                blocks.extend(_markdown_to_blocks(sec_body, base_dir))
                chapters.append(Chapter(
                    id=_slugify(sec_title) or f"sec{idx}",
                    title=sec_title, version="1.0.0", blocks=blocks))

        # 5) Renderizar con el motor de automatic_eda.
        out_path = os.path.join(base_dir, "out", "paper.pdf")
        res = render_pdf(chapters, out_path, meta={"title": title or "paper"})

        # 6) Mapear el retorno del motor a la forma de esta función.
        path = res.get("path")
        return {
            "status": "ok" if path else "error",
            "pdf_path": path,
            "n_pages": int(res.get("n_pages") or 0),
            "note": res.get("note"),
        }
    except Exception as e:  # noqa: BLE001 — dict-no-throw estricto.
        return {"status": "error", "pdf_path": None, "n_pages": 0,
                "note": f"fallo: {e}"}


# --------------------------------------------------------------------------- #
# Frontmatter
# --------------------------------------------------------------------------- #
def _split_frontmatter(text: str):
    """Separa el bloque frontmatter YAML inicial del cuerpo.

    Devuelve ``(fm_text|None, body)``. Si el archivo no empieza con una valla
    ``---`` o no se cierra, no hay frontmatter y el cuerpo es el texto entero.
    """
    if text.startswith("﻿"):
        text = text.lstrip("﻿")
    lines = text.split("\n")
    if not lines or lines[0].strip() != "---":
        return None, text
    for i in range(1, len(lines)):
        if lines[i].strip() == "---":
            return "\n".join(lines[1:i]), "\n".join(lines[i + 1:])
    # Valla de apertura sin cierre: tratar todo como cuerpo.
    return None, text


def _parse_frontmatter(fm_text) -> dict:
    """Parsea el frontmatter. Intenta YAML; si no, parser line-based simple."""
    if not fm_text:
        return {}
    try:
        import yaml  # type: ignore
        data = yaml.safe_load(fm_text)
        if isinstance(data, dict):
            return data
    except Exception:  # noqa: BLE001 — yaml ausente o frontmatter inválido.
        pass
    # Fallback degradado: 'clave: valor' por línea.
    out: dict = {}
    for line in fm_text.split("\n"):
        stripped = line.strip()
        if not stripped or stripped.startswith("#") or ":" not in stripped:
            continue
        k, _, v = stripped.partition(":")
        k = k.strip()
        v = v.strip().strip('"').strip("'")
        if k:
            out[k] = v
    return out


# --------------------------------------------------------------------------- #
# Portada
# --------------------------------------------------------------------------- #
def _portada_markdown(authors, date_raw, abstract) -> str:
    """Markdown de la portada: autores, fecha y, si hay, el abstract."""
    parts: list = []
    authors_str = _fmt_authors(authors)
    if authors_str:
        parts.append(f"**Autores:** {authors_str}")
    if date_raw not in (None, ""):
        parts.append(f"**Fecha:** {_fmt_date(date_raw)}")
    md = "\n\n".join(parts)
    abstract = _safe_str(abstract).strip()
    if abstract:
        md = (md + "\n\n" if md else "") + "## Abstract\n\n" + abstract
    return md


def _fmt_authors(authors) -> str:
    """Lista o string de autores → string separado por comas."""
    if authors in (None, ""):
        return ""
    if isinstance(authors, (list, tuple)):
        return ", ".join(_safe_str(a).strip() for a in authors
                         if _safe_str(a).strip())
    return _safe_str(authors).strip()


def _fmt_date(raw) -> str:
    """Fecha → ``DD/MM/AAAA`` si es parseable; si no, el valor crudo."""
    if isinstance(raw, _dt.datetime):
        return raw.strftime("%d/%m/%Y")
    if isinstance(raw, _dt.date):
        return raw.strftime("%d/%m/%Y")
    s = _safe_str(raw).strip()
    if not s:
        return s
    for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%d/%m/%Y", "%d-%m-%Y"):
        try:
            return _dt.datetime.strptime(s, fmt).strftime("%d/%m/%Y")
        except ValueError:
            continue
    try:
        return _dt.datetime.fromisoformat(s).strftime("%d/%m/%Y")
    except Exception:  # noqa: BLE001
        return s


# --------------------------------------------------------------------------- #
# Cuerpo y figuras
# --------------------------------------------------------------------------- #
def _split_body_sections(body: str):
    """Divide el cuerpo en (preámbulo, [(título_H1, contenido)...]) por H1."""
    preamble_lines: list = []
    sections: list = []
    current = None  # (titulo, [lineas])
    for line in body.split("\n"):
        m = _H1_LINE.match(line)
        if m and not line.startswith("##"):
            if current is not None:
                sections.append((current[0], "\n".join(current[1])))
            current = (m.group(1).strip(), [])
        elif current is None:
            preamble_lines.append(line)
        else:
            current[1].append(line)
    if current is not None:
        sections.append((current[0], "\n".join(current[1])))
    return "\n".join(preamble_lines), sections


def _markdown_to_blocks(text: str, base_dir: str) -> list:
    """Parte un Markdown en bloques Markdown/Image alrededor de cada figura.

    Las líneas ``![alt](src)`` con ``src`` local se convierten en ``Image``; las
    que apuntan a URLs http(s) se dejan como texto Markdown.
    """
    blocks: list = []
    buf: list = []

    def _flush():
        chunk = "\n".join(buf).strip("\n")
        if chunk.strip():
            blocks.append(Markdown(text=chunk))
        buf.clear()

    for line in text.split("\n"):
        m = _IMG_LINE.match(line)
        if m:
            alt, src = m.group(1), m.group(2)
            if src.lower().startswith(("http://", "https://")):
                buf.append(line)  # URL remota: se mantiene como texto.
                continue
            _flush()
            blocks.append(Image(path=_resolve_src(src, base_dir),
                                caption=(alt or None)))
        else:
            buf.append(line)
    _flush()
    return blocks


def _resolve_src(src: str, base_dir: str) -> str:
    """Resuelve la ruta de una figura relativa al paper.

    Absoluta → tal cual. Relativa → prueba ``base_dir/src`` y
    ``base_dir/figures/<basename>``; usa la primera que exista, o el join con
    ``base_dir`` si ninguna (el motor degrada dibujando el aviso de no-encontrada).
    """
    if os.path.isabs(src):
        return src
    cand1 = os.path.join(base_dir, src)
    cand2 = os.path.join(base_dir, "figures", os.path.basename(src))
    for c in (cand1, cand2):
        if os.path.exists(c):
            return c
    return cand1


def _slugify(text: str) -> str:
    """Slug ASCII corto para el id del capítulo."""
    s = re.sub(r"[^a-z0-9]+", "_", _safe_str(text).lower()).strip("_")
    return s[:40]


def _safe_str(v) -> str:
    """str() que nunca lanza y mapea None a ''."""
    if v is None:
        return ""
    try:
        return str(v)
    except Exception:  # noqa: BLE001
        return ""
