"""render_automatic_eda_folder — EDA de una CARPETA / base multi-tabla one-shot.

Pipeline impuro del grupo de capacidad `eda`, a nivel de BASE. Dada una CARPETA
de archivos tabulares (CSV/Parquet/JSON) o una DuckDB ya existente, produce el
informe AutomaticEDA de la BASE en sus tres formatos a la vez (PDF móvil A5 +
PPTX 16:9 + Markdown autocontenido), con los capítulos POBLADOS, en una sola
llamada. Es el hermano a nivel de base de ``render_automatic_eda`` (que perfila
UNA tabla): aquí el documento por capítulos resume TODAS las tablas y, sobre
todo, sus RELACIONES inter-tabla (FK candidatas + join graph).

Compone funciones del registry SIN reimplementar su lógica:

  - load_folder_to_duckdb : carga una carpeta de archivos a una DuckDB temporal
                            (rama "carpeta"). En la rama "ya es duckdb" se omite.
  - profile_database      : perfila TODA la base (resumen de cada tabla,
                            TableProfiles completos, FK candidatas por
                            containment y join graph con diagrama Mermaid).
  - render_automatic_eda_pdf      : renderiza el documento-base por capítulos a PDF.
  - render_automatic_eda_pptx     : renderiza el mismo documento-base a PPTX.
  - render_automatic_eda_markdown : serializa el mismo documento-base a Markdown
                                    autocontenido (texto + tablas markdown).
  - build_document        : (solo con per_table_eda=True) ensambla los capítulos
                            canónicos de CADA tabla para anexarlos al documento.

La capa propia de este pipeline es ENSAMBLAR EL DOCUMENTO-BASE de capítulos a
partir del ``DatabaseProfile`` que devuelve ``profile_database`` y cablear los
tres renderers del motor AutomaticEDA. El documento-base mínimo tiene tres
capítulos: portada-base (nombre/nº tablas/totales/fecha/fuente), resumen de
tablas (una fila por tabla) y relaciones inter-tabla (FK candidatas + diagrama
Mermaid). Con ``per_table_eda=True`` anexa, por cada tabla, sus capítulos de
mini-EDA.

Estilo dict-no-throw del grupo `eda`: nunca lanza; captura cualquier error y
degrada a ``{"status": "error", "error": str}``.
"""

import os
from datetime import datetime, timezone

from datascience import (
    draw_join_graph_figure,
    render_automatic_eda_markdown,
    render_automatic_eda_pdf,
    render_automatic_eda_pptx,
)
from datascience.automatic_eda import build_document
from infra import load_folder_to_duckdb
from pipelines.profile_database import profile_database

# Mapa profile_level -> tamaño de muestra por columna del perfil de cada tabla.
# A nivel de base el coste lo domina el nº de tablas; el preset solo ajusta el
# sample que profile_database pasa a profile_table.
_SAMPLE_BY_LEVEL = {"lite": 2000, "standard": 5000, "full": 5000}

# Extensiones que se consideran "una DuckDB ya hecha" en la rama directa.
_DUCKDB_EXTS = (".duckdb", ".ddb", ".db")


def _fmt_num(v) -> str:
    """Formatea un entero con separador de millar; '—' si no es número."""
    if isinstance(v, bool) or not isinstance(v, (int, float)):
        return "—"
    try:
        return f"{int(v):,}".replace(",", ".")
    except Exception:  # noqa: BLE001
        return str(v)


def _portada_chapter(db_profile: dict, source_path: str, db_path: str,
                     meta_ctx: dict) -> dict:
    """Capítulo de portada a nivel de base (NO reusa chapters/portada.py, que es
    de tabla única): nombre de la base, nº de tablas, totales y procedencia."""
    tables = db_profile.get("tables", []) or []
    total_rows = sum(
        (t.get("n_rows") or 0) for t in tables if isinstance(t.get("n_rows"), (int, float))
    )
    total_cols = sum(
        (t.get("n_cols") or 0) for t in tables if isinstance(t.get("n_cols"), (int, float))
    )
    base_name = (meta_ctx or {}).get("dataset_name") or os.path.basename(
        os.path.normpath(source_path)
    ) or source_path

    rows = [
        ("Base", base_name),
        ("Tablas", _fmt_num(db_profile.get("n_tables"))),
        ("Filas totales", _fmt_num(total_rows)),
        ("Columnas totales", _fmt_num(total_cols)),
        ("Relaciones FK", _fmt_num(len(db_profile.get("fk_candidates", []) or []))),
        ("Fuente", source_path),
        ("DuckDB", db_path),
        ("Generado", datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")),
    ]
    blocks = [
        {"kind": "heading", "text": f"EDA de la base — {base_name}", "level": 1},
        {"kind": "kv_table", "rows": rows, "title": "Resumen de la base"},
    ]
    errs = db_profile.get("errors", []) or []
    if errs:
        blocks.append({
            "kind": "note",
            "text": f"{len(errs)} aviso(s) durante el perfilado (ver detalle).",
        })
    return {"id": "portada_base", "title": "Portada", "version": "1.0.0",
            "blocks": blocks}


def _resumen_chapter(db_profile: dict) -> dict:
    """Capítulo con una fila por tabla: filas, columnas, calidad, key_candidates."""
    header = ["Tabla", "Filas", "Columnas", "Calidad", "key_candidates"]
    rows = []
    for t in db_profile.get("tables", []) or []:
        keys = ", ".join(t.get("key_candidates") or []) or "—"
        rows.append([
            t.get("table"),
            _fmt_num(t.get("n_rows")),
            _fmt_num(t.get("n_cols")),
            t.get("quality_score"),
            keys,
        ])
    if rows:
        blocks = [{
            "kind": "data_table", "header": header, "rows": rows,
            "title": "Tablas de la base",
            "note": "Una fila por tabla. Calidad = score agregado del TableProfile.",
        }]
    else:
        blocks = [{"kind": "note",
                   "text": "La base no contiene tablas perfilables."}]
    return {"id": "resumen_tablas", "title": "Resumen de tablas",
            "version": "1.0.0", "blocks": blocks}


def _relaciones_chapter(db_profile: dict) -> dict:
    """Capítulo de relaciones inter-tabla: tabla de FK candidatas + diagrama
    Mermaid del join graph (vuelca el Mermaid como bloque de código)."""
    fks = db_profile.get("fk_candidates", []) or []
    blocks = [{
        "kind": "heading", "text": "Relaciones inter-tabla", "level": 2,
    }]
    if fks:
        header = ["From", "To", "Inclusión", "Cardinalidad"]
        rows = []
        for fk in fks:
            frm = f"{fk.get('from_table')}.{fk.get('from_col')}"
            to = f"{fk.get('to_table')}.{fk.get('to_col')}"
            inc = fk.get("inclusion")
            inc_s = f"{inc:.3f}" if isinstance(inc, (int, float)) else str(inc)
            rows.append([frm, to, inc_s, fk.get("cardinality")])
        blocks.append({
            "kind": "data_table", "header": header, "rows": rows,
            "title": "FK candidatas (por containment de valores)",
            "note": "Inclusión = fracción de valores de From contenidos en To.",
        })
    else:
        blocks.append({
            "kind": "note",
            "text": "Sin relaciones FK candidatas detectadas entre las tablas.",
        })

    join_graph = db_profile.get("join_graph") or {}
    has_edges = bool(join_graph.get("edges"))
    if has_edges:
        blocks.append({"kind": "heading", "text": "Diagrama (join graph)",
                       "level": 3})
        # Figure matplotlib REAL del grafo de relaciones (nodos = tablas,
        # aristas = FK). Lazy via `make`: el renderer la construye solo al
        # paginar, y se rasteriza en PDF/PPTX. draw_join_graph_figure nunca
        # lanza (devuelve una Figure de error si algo falla).
        blocks.append({
            "kind": "figure",
            "make": (lambda jg=join_graph: draw_join_graph_figure(
                jg, title="Join graph (relaciones inter-tabla)")),
            "caption": "Grafo de relaciones: nodos = tablas, flechas = FK "
                       "candidatas (etiqueta from_col→to_col).",
            "height_in": 4.5,
        })
        # Además, el Mermaid en texto: en el Markdown queda como diagrama
        # renderizable y es útil para pegar a un LLM.
        mermaid = (join_graph.get("mermaid", "") or "").strip()
        if mermaid:
            blocks.append({"kind": "markdown",
                           "text": "```mermaid\n" + mermaid + "\n```"})
    return {"id": "relaciones", "title": "Relaciones inter-tabla",
            "version": "1.0.0", "blocks": blocks}


def _build_db_document(db_profile: dict, source_path: str, db_path: str,
                       meta_ctx: dict, per_table_eda: bool) -> list:
    """Ensambla el documento-base por capítulos a partir del DatabaseProfile.

    Mínimo: portada-base + resumen de tablas + relaciones. Con per_table_eda
    True anexa, por cada tabla, un capítulo separador + los capítulos canónicos
    de su mini-EDA (reusando build_document sobre cada TableProfile)."""
    chapters = [
        _portada_chapter(db_profile, source_path, db_path, meta_ctx),
        _resumen_chapter(db_profile),
        _relaciones_chapter(db_profile),
    ]
    if per_table_eda:
        for prof in db_profile.get("table_profiles", []) or []:
            tname = prof.get("table") or "tabla"
            chapters.append({
                "id": f"tabla_{tname}", "title": f"Tabla: {tname}",
                "version": "1.0.0",
                "blocks": [{"kind": "heading", "text": f"Tabla: {tname}",
                            "level": 1}],
            })
            try:
                # build_document devuelve los capítulos canónicos de la tabla.
                # ctx None -> los capítulos que necesitan datos crudos degradan,
                # pero salen completos los de portada/overview/distrib/calidad.
                chapters.extend(build_document(prof, None) or [])
            except Exception:  # noqa: BLE001 — una tabla mala no rompe el doc.
                chapters.append({
                    "id": f"tabla_{tname}_err", "title": f"Tabla: {tname}",
                    "version": "1.0.0",
                    "blocks": [{"kind": "note",
                                "text": "No se pudo ensamblar el mini-EDA de "
                                        "esta tabla."}],
                })
    return chapters


def _resolve_db_path(path: str) -> dict:
    """Resuelve el DuckDB a perfilar desde ``path``.

    - Directorio  -> carga la carpeta con load_folder_to_duckdb (DuckDB temp).
    - Archivo .duckdb/.ddb/.db -> se usa directo (rama "ya es duckdb").
    - Otro archivo / inexistente -> error.

    Devuelve {status, db_path, loaded, n_tables, load_errors}.
    """
    if os.path.isdir(path):
        lr = load_folder_to_duckdb(path)
        if lr.get("status") != "ok":
            return {"status": "error",
                    "error": f"load_folder_to_duckdb falló: {lr.get('error')}"}
        return {
            "status": "ok",
            "db_path": lr.get("db_path"),
            "loaded": True,
            "n_tables": len(lr.get("tables", []) or []),
            "load_errors": lr.get("errors", []) or [],
        }
    if os.path.isfile(path):
        if path.lower().endswith(_DUCKDB_EXTS):
            return {"status": "ok", "db_path": path, "loaded": False,
                    "n_tables": None, "load_errors": []}
        return {"status": "error",
                "error": f"'{path}' no es un directorio ni una DuckDB "
                         f"(extensiones {_DUCKDB_EXTS})."}
    return {"status": "error", "error": f"path no existe: {path}"}


def render_automatic_eda_folder(
    path: str,
    out_dir: str = "reports",
    basename: str = None,
    profile_level: str = "standard",
    emit_pdf: bool = True,
    emit_pptx: bool = True,
    emit_md: bool = True,
    per_table_eda: bool = False,
    min_inclusion: float = 0.9,
    ctx_extra: dict = None,
) -> dict:
    """Perfila una CARPETA (o una DuckDB) y emite el informe AutomaticEDA de la base.

    Args:
        path: o bien un DIRECTORIO con archivos tabulares (CSV/Parquet/JSON) que
            se cargan a una DuckDB temporal, o bien una DuckDB ya existente
            (``.duckdb``/``.ddb``/``.db``) que se perfila directa.
        out_dir: directorio de salida (se crea si no existe). Default "reports".
        basename: nombre base de los archivos sin extensión. Default
            "aeda_base_<nombre>_<timestamp>".
        profile_level: preset de coste del perfil por tabla ("lite"/"standard"/
            "full"); ajusta el ``sample`` que profile_database pasa a cada tabla.
        emit_pdf / emit_pptx / emit_md: qué formatos emitir. Default los tres.
        per_table_eda: si True, anexa al documento-base los capítulos de mini-EDA
            de cada tabla (un Heading "Tabla: <n>" + build_document por tabla).
            Default False (solo el documento-base: portada + resumen + relaciones).
        min_inclusion: umbral de inclusión para emitir una FK candidata (0-1).
        ctx_extra: dict opcional de claves de presentación (p.ej. dataset_name,
            description) que se mezclan en el contexto de la portada.

    Returns:
        dict (nunca lanza). En éxito::

            {"status": "ok", "pdf_path": str|None, "pptx_path": str|None,
             "md_path": str|None, "manifest_path": str|None,
             "n_tables": int, "n_pages": int|None, "n_slides": int|None,
             "md_chars": int|None, "db_path": str, "db_profile": <DatabaseProfile>}

        En error: {"status": "error", "error": str}.
    """
    try:
        # 1) Resolver la DuckDB a perfilar (cargar carpeta o usar la dada).
        rdb = _resolve_db_path(path)
        if rdb.get("status") != "ok":
            return {"status": "error", "error": rdb.get("error")}
        db_path = rdb.get("db_path")

        # 2) Perfilar la base entera (resumen + FK + join graph). Sin report
        # propio (write_report/emit_pdf False): este pipeline emite el suyo.
        sample = _SAMPLE_BY_LEVEL.get(profile_level, 5000)
        pres = profile_database(
            db_path, sample=sample, write_report=False,
            min_inclusion=min_inclusion, emit_pdf=False,
        )
        if pres.get("status") != "ok":
            return {"status": "error",
                    "error": f"profile_database falló: {pres.get('error')}"}
        db_profile = pres.get("db_profile") or {}

        # 3) Ensamblar el documento-base por capítulos.
        meta_ctx = dict(ctx_extra or {})
        chapters = _build_db_document(
            db_profile, path, db_path, meta_ctx, per_table_eda
        )

        # 4) Render a los tres formatos desde el MISMO documento por capítulos.
        os.makedirs(out_dir, exist_ok=True)
        ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
        nm = (meta_ctx.get("dataset_name")
              or os.path.basename(os.path.normpath(path)) or "base")
        nm = "".join(c if c.isalnum() else "_" for c in str(nm)).strip("_") or "base"
        base = basename or f"aeda_base_{nm}_{ts}"
        title = f"EDA base — {meta_ctx.get('dataset_name') or nm}"
        meta = {"title": title}

        pdf_path = pptx_path = md_path = manifest_path = None
        n_pages = n_slides = md_chars = None

        if emit_pdf:
            target = os.path.join(out_dir, base + ".pdf")
            rpdf = render_automatic_eda_pdf(chapters, target, meta) or {}
            pdf_path = rpdf.get("path")
            n_pages = rpdf.get("n_pages")
            manifest_path = rpdf.get("manifest_path")
        if emit_pptx:
            target = os.path.join(out_dir, base + ".pptx")
            rpptx = render_automatic_eda_pptx(chapters, target, meta) or {}
            pptx_path = rpptx.get("path")
            n_slides = rpptx.get("n_slides")
        if emit_md:
            target = os.path.join(out_dir, base + ".md")
            rmd = render_automatic_eda_markdown(chapters, target, meta) or {}
            md_path = rmd.get("path")
            md_chars = rmd.get("n_chars")

        return {
            "status": "ok",
            "pdf_path": pdf_path,
            "pptx_path": pptx_path,
            "md_path": md_path,
            "manifest_path": manifest_path,
            "n_tables": db_profile.get("n_tables"),
            "n_pages": n_pages,
            "n_slides": n_slides,
            "md_chars": md_chars,
            "db_path": db_path,
            "db_profile": db_profile,
        }
    except Exception as e:  # noqa: BLE001 — dict-no-throw: degradar, nunca lanzar.
        return {"status": "error", "error": str(e)}
