# src/svy/regression/_prediction.py
"""
Prediction results for fitted GLM models.
"""

from __future__ import annotations

import logging

import msgspec
import numpy as np
import polars as pl


log = logging.getLogger(__name__)


class GLMPred(msgspec.Struct, frozen=True):
    yhat: np.ndarray
    se: np.ndarray
    lci: np.ndarray
    uci: np.ndarray
    df: float
    alpha: float
    residuals: np.ndarray | None = None

    @property
    def conf_level(self) -> float:
        """Confidence level (e.g., 0.95 for 95% CI)."""
        return 1.0 - self.alpha

    def to_polars(self) -> pl.DataFrame:
        """Convert predictions to DataFrame."""
        data = {
            "yhat": self.yhat,
            "se": self.se,
            "lci": self.lci,
            "uci": self.uci,
        }
        if self.residuals is not None:
            data["residuals"] = self.residuals
        return pl.DataFrame(data)

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        d = {
            "yhat": self.yhat.tolist(),
            "se": self.se.tolist(),
            "lci": self.lci.tolist(),
            "uci": self.uci.tolist(),
            "df": self.df,
            "alpha": self.alpha,
        }
        if self.residuals is not None:
            d["residuals"] = self.residuals.tolist()
        return d

    def __len__(self) -> int:
        return len(self.yhat)

    def __str__(self) -> str:
        from svy.ui.printing import render_rich_to_str, resolve_width

        return render_rich_to_str(self, width=resolve_width(self))

    def __plain_str__(self) -> str:
        """Plain-text fallback when rich is not installed. Never calls str(self)."""
        conf_pct = int(self.conf_level * 100)
        lines = [
            f"GLM Predictions (n={len(self)}, {conf_pct}% CI, df={self.df:.1f})",
            f"  Mean ŷ : {self.yhat.mean():.4f}    Mean SE : {self.se.mean():.4f}",
            f"  Min ŷ  : {self.yhat.min():.4f}    Max ŷ   : {self.yhat.max():.4f}",
        ]
        if self.residuals is not None:
            lines.append(
                f"  Mean resid : {self.residuals.mean():.4f}    Std resid : {self.residuals.std():.4f}"
            )
        lines.append("Use .to_polars() for full results")
        return "\n".join(lines)

    def __repr__(self) -> str:
        conf_pct = int(self.conf_level * 100)
        res_str = ", with residuals" if self.residuals is not None else ""
        return f"GLMPred(n={len(self)}, {conf_pct}% CI, df={self.df:.1f}{res_str})"

    def __rich_console__(self, console, options):
        from rich.table import Table as RTable
        from rich.text import Text

        conf_pct = int(self.conf_level * 100)
        yield Text(
            f"GLM Predictions (n={len(self)}, {conf_pct}% CI, df={self.df:.1f})", style="bold cyan"
        )
        yield Text("")

        grid = RTable.grid(padding=(0, 2))
        grid.add_column(style="bold")
        grid.add_column(justify="right")
        grid.add_column(style="bold")
        grid.add_column(justify="right")

        grid.add_row("Mean ŷ", f"{self.yhat.mean():.4f}", "Mean SE", f"{self.se.mean():.4f}")
        grid.add_row("Min ŷ", f"{self.yhat.min():.4f}", "Max ŷ", f"{self.yhat.max():.4f}")

        if self.residuals is not None:
            grid.add_row(
                "Mean resid",
                f"{self.residuals.mean():.4f}",
                "Std resid",
                f"{self.residuals.std():.4f}",
            )

        yield grid
        yield Text("")
        yield Text("Use .to_polars() for full results", style="dim")
