"""Impure EDA helper: heatmap of missingness co-occurrence (`eda` group).

Builds a matplotlib heatmap of the pairwise missingness correlation matrix of a
dataset: a value near ``+1`` means two columns tend to be null together, near
``-1`` means when one is null the other tends to be present, and ``0`` means
their absences are independent. Returns a ready-to-rasterize
``matplotlib.figure.Figure``; it never shows nor saves it.

Impure because it touches matplotlib's rendering machinery. It uses the headless
Agg backend and the object-oriented ``Figure`` API (no ``pyplot``) so it leaks no
global state and is safe to call repeatedly from a report renderer.
"""

import matplotlib

matplotlib.use("Agg")

from matplotlib.figure import Figure  # noqa: E402

# Muted gray for secondary text (no-data / fallback messages).
_MUTED_TEXT = "#5f6b7a"
# Soft red for the error fallback message (kept readable, not alarming).
_ERROR_TEXT = "#b00020"


def _truncate(text, width: int = 14) -> str:
    """Truncate ``text`` to ``width`` chars, appending an ellipsis if cut."""
    s = "" if text is None else str(text)
    if len(s) <= width:
        return s
    if width <= 1:
        return s[:width]
    return s[: width - 1] + "…"


def _message_figure(message: str, color: str = _MUTED_TEXT) -> "Figure":
    """Return a fallback ``Figure`` carrying a single centered message."""
    fig = Figure(figsize=(6.4, 4.0), dpi=150)
    ax = fig.add_subplot(111)
    ax.axis("off")
    ax.text(
        0.5,
        0.5,
        message,
        ha="center",
        va="center",
        fontsize=12,
        color=color,
        wrap=True,
        transform=ax.transAxes,
    )
    fig.tight_layout()
    return fig


def missingness_corr_heatmap_figure(
    matrix,
    labels,
    title: str = "Co-ocurrencia de ausencias",
) -> "matplotlib.figure.Figure":
    """Build a heatmap figure of a missingness correlation matrix.

    Renders an ``NxN`` matrix of missingness correlations in ``[-1, 1]`` with a
    diverging ``coolwarm`` colormap (fixed ``vmin=-1``, ``vmax=1`` so the color
    scale is comparable across reports). Both axes are tick-labelled with the
    column names (truncated to ~14 chars; the X labels rotated 45°). A colorbar
    is attached. When the matrix is small (``N <= 12``) each cell is annotated
    with its numeric value; for larger matrices the annotations are omitted to
    avoid an unreadable grid.

    The function is fully defensive: empty/ragged/non-numeric input never raises.
    When there is nothing valid to draw it returns a ``Figure`` carrying a
    centered "sin columnas con ausencia variable" message, and any unexpected
    error is caught and turned into a fallback ``Figure`` carrying the error text.

    Args:
        matrix: List of lists (``NxN``) of floats in ``[-1, 1]`` — the pairwise
            missingness correlation. May be empty; rows of unequal length are
            tolerated by treating the matrix as invalid only when it is empty or
            its label count does not match. Non-numeric/``None`` cells are
            coerced to ``0.0``.
        labels: List of ``N`` column names, parallel to ``matrix``. May be empty.
            Truncated for display; the originals are not mutated.
        title: Figure title. Default "Co-ocurrencia de ausencias".

    Returns:
        A ``matplotlib.figure.Figure`` with a single heatmap Axes plus a
        colorbar. The caller is responsible for rasterizing/closing it.
    """
    try:
        # --- Validate shape: need a non-empty square-ish matrix with labels.
        if (
            not isinstance(matrix, (list, tuple))
            or not isinstance(labels, (list, tuple))
            or len(matrix) == 0
            or len(labels) == 0
        ):
            return _message_figure("sin columnas con ausencia variable")

        n = len(labels)
        # Build a clean NxN grid: coerce each cell to float, default 0.0, pad/clip
        # rows so a ragged input never crashes imshow.
        grid = []
        for i in range(n):
            row_src = matrix[i] if i < len(matrix) else []
            if not isinstance(row_src, (list, tuple)):
                row_src = []
            row = []
            for j in range(n):
                cell = row_src[j] if j < len(row_src) else 0.0
                try:
                    val = float(cell)
                except (TypeError, ValueError):
                    val = 0.0
                if val != val:  # NaN guard.
                    val = 0.0
                row.append(val)
            grid.append(row)

        fig = Figure(figsize=(6.4, 5.2), dpi=150)
        ax = fig.add_subplot(111)

        im = ax.imshow(grid, vmin=-1, vmax=1, cmap="coolwarm", aspect="equal")

        short = [_truncate(lab, 14) for lab in labels]
        ax.set_xticks(range(n))
        ax.set_yticks(range(n))
        ax.set_xticklabels(short, rotation=45, ha="right", fontsize=8)
        ax.set_yticklabels(short, fontsize=8)

        # Annotate each cell only when the grid is small enough to stay legible.
        if n <= 12:
            for i in range(n):
                for j in range(n):
                    val = grid[i][j]
                    # White text over saturated (dark) cells, dark over pale.
                    txt_color = "white" if abs(val) >= 0.55 else "#202020"
                    ax.text(
                        j,
                        i,
                        f"{val:.2f}",
                        ha="center",
                        va="center",
                        fontsize=7,
                        color=txt_color,
                    )

        cbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
        cbar.ax.tick_params(labelsize=8)
        cbar.set_label("correlación de ausencias", fontsize=8)

        if title:
            ax.set_title(_truncate(title, 60), fontsize=12, loc="center", pad=10)

        fig.tight_layout()
        return fig
    except Exception as exc:  # noqa: BLE001 — never raise from a figure builder.
        return _message_figure(f"error al dibujar heatmap: {exc}", color=_ERROR_TEXT)
