"""Pluggable LLM-backed alt text. The package supplies no transport;
the caller provides any provider via ``backend``."""

from typing import Callable, Optional

from a11yviz.alt_text import alt_text


def describe(fig, backend: Callable[[dict], str], attach: bool = True):
    """Generate alt text for a figure via a user-supplied backend.

    Extracts deterministic plot context (chart type, axes, group counts)
    and passes it as a dict to ``backend``, which must return a single
    string. The result is attached to the figure via :func:`alt_text`
    when ``attach=True``.

    The package depends on no LLM SDK; ``backend`` is a function the
    caller writes against any provider (OpenAI, Gemini, Anthropic,
    Ollama, ...). See the docs for example backends.

    Args:
        fig: A matplotlib Figure / Axes or plotly Figure.
        backend: ``Callable[[dict], str]``. Receives a context dict with
            keys ``chart_type``, ``title``, ``x``, ``y``, ``color``,
            ``n_observations``. Returns a single string.
        attach: If True (default), attach via :func:`alt_text` and return
            the figure. If False, return the string itself.
    """
    context = _plot_context(fig)
    text = backend(context)
    return alt_text(fig, text) if attach else text


def _plot_context(fig) -> dict:
    if type(fig).__module__.startswith("matplotlib."):
        return _context_matplotlib(fig)
    if type(fig).__module__.startswith("plotly."):
        return _context_plotly(fig)
    raise TypeError("describe() supports matplotlib and plotly figures")


def _context_matplotlib(fig) -> dict:
    ax = fig if hasattr(fig, "get_xlabel") else (fig.axes[0] if fig.axes else None)
    if ax is None:
        return {"chart_type": "Plot", "title": None, "x": None, "y": None,
                "color": None, "n_observations": None}

    chart_type = _detect_mpl_type(ax)
    legend = ax.get_legend()
    color: Optional[dict] = None
    if legend is not None and legend.texts:
        labels = [t.get_text() for t in legend.texts]
        color = {"label": legend.get_title().get_text() or None, "levels": labels}

    return {
        "chart_type":     chart_type,
        "title":          ax.get_title() or None,
        "x":              {"label": ax.get_xlabel() or "x",
                           "range": _ax_range(ax, "x")},
        "y":              {"label": ax.get_ylabel() or "y",
                           "range": _ax_range(ax, "y")},
        "color":          color,
        "n_observations": _count_mpl_points(ax),
    }


def _context_plotly(fig) -> dict:
    chart_type = "Plot"
    if fig.data:
        first = fig.data[0]
        t     = getattr(first, "type", "scatter") or "scatter"
        mode  = getattr(first, "mode", None)
        chart_type = {
            "scatter:markers":       "Scatter plot",
            "scatter:lines":         "Line plot",
            "scatter:lines+markers": "Line plot with markers",
            "bar":                   "Bar chart",
            "heatmap":               "Heatmap",
            "box":                   "Boxplot",
            "violin":                "Violin plot",
            "histogram":             "Histogram",
        }.get(f"{t}:{mode}" if (t == "scatter" and mode) else t, "Plot")
    title = None
    if fig.layout.title and fig.layout.title.text:
        title = fig.layout.title.text
    return {
        "chart_type":     chart_type,
        "title":          title,
        "x":              {"label": _plotly_axis_title(fig, "xaxis") or "x"},
        "y":              {"label": _plotly_axis_title(fig, "yaxis") or "y"},
        "color":          None,
        "n_observations": None,
        "n_traces":       len(fig.data),
    }


def _detect_mpl_type(ax):
    if ax.images:      return "Heatmap"
    if ax.collections: return "Scatter plot"
    if ax.patches:     return "Bar chart"
    if ax.lines:       return "Line plot"
    return "Plot"


def _count_mpl_points(ax):
    if ax.collections: return sum(int(c.get_offsets().shape[0]) for c in ax.collections)
    if ax.lines:       return sum(len(line.get_xdata()) for line in ax.lines)
    if ax.patches:     return len(ax.patches)
    return None


def _ax_range(ax, axis):
    try:
        lo, hi = ax.get_xlim() if axis == "x" else ax.get_ylim()
    except Exception:
        return None
    return [round(lo, 3), round(hi, 3)]


def _plotly_axis_title(fig, axis_name):
    ax = getattr(fig.layout, axis_name, None)
    if ax is None: return None
    title = getattr(ax, "title", None)
    if title is None: return None
    return getattr(title, "text", None)
