# src/svy/ui/printing.py
from __future__ import annotations

import io
import logging
import os
import sys

from typing import Any, Iterable, Mapping

from svy.core.constants import SVY_DEFAULT_PRINT_WIDTH
from svy.ui.theme import THEME


log = logging.getLogger(__name__)


# -----------------------------------------------------------------------------
# Width resolution
# -----------------------------------------------------------------------------
def resolve_width(
    owner: object | None = None,
    default: int = SVY_DEFAULT_PRINT_WIDTH,
    **kwargs: Any,
) -> int:
    """
    Resolve print width strictly.
    Hierarchy:
      1. Instance override (obj._print_width)
      2. Class override (Class.PRINT_WIDTH)
      3. SVY_PRINT_WIDTH env var
      4. Default (usually 90-100)

    NOTE: We explicitly IGNORE os.environ["COLUMNS"] and shutil.get_terminal_size()
    to prevent modern terminals (like Ghostty) from forcing full-width output.
    """
    obj = owner or kwargs.get("obj")

    def _as_int(x):
        try:
            return int(x)
        except Exception:
            return None

    # 1. Instance-level hint
    w = getattr(obj, "_print_width", None) if obj is not None else None
    if isinstance(w, int) and w > 20:
        return w

    # 2. Class / Module hints
    if obj is not None:
        cw = getattr(type(obj), "PRINT_WIDTH", None)
        if isinstance(cw, int) and cw > 20:
            return cw
        try:
            mod = sys.modules.get(obj.__class__.__module__)
            if mod is not None and hasattr(mod, "SVY_PRINT_WIDTH"):
                mw = _as_int(getattr(mod, "SVY_PRINT_WIDTH"))
                if isinstance(mw, int) and mw > 20:
                    return mw
        except Exception:
            pass

    # 3. SVY Specific Env Var (The only way to force global width override)
    ew = _as_int(os.environ.get("SVY_PRINT_WIDTH"))
    if isinstance(ew, int) and ew > 20:
        return ew

    # 4. Fallback to constant
    return default


# -----------------------------------------------------------------------------
# Simple string padding utility
# -----------------------------------------------------------------------------
def pad(text: str, *, indent: int = 2, surround: bool = False) -> str:
    if text is None:
        return ""
    text = str(text).rstrip("\n")
    if indent > 0:
        pad_ = " " * indent
        text = "\n".join(pad_ + line if line else pad_ for line in text.splitlines())
    return f"\n{text}\n" if surround else text


# -----------------------------------------------------------------------------
# Central style + feature routing
# -----------------------------------------------------------------------------
def _feature_kind_from_key(key: str | None) -> str:
    if not key:
        return "panel"
    k = key.lower()
    if "error" in k:
        return "error"
    if "estimate" in k:
        return "estimate"
    if "ttest" in k:
        return "ttest"
    if "sample" in k:
        return "sample"
    if "size" in k:
        return "size"
    if "table" in k:
        return "estimate"
    return "panel"


def styles(
    obj_or_key: object | str | None = None,
    *,
    kind: str | None = None,
    title: str | None = None,
    border: str | None = None,
    header: str | None = None,
) -> Mapping[str, str]:
    # Robustly get theme components
    try:
        c = THEME.components
    except Exception:
        # Fallback if THEME is not initialized/broken
        return {
            "border": border or "cyan",
            "header": header or "bold",
            "title": title or "bold",
        }

    if isinstance(obj_or_key, str):
        eff_kind = _feature_kind_from_key(obj_or_key)
        obj = None
    else:
        eff_kind = kind or "panel"
        obj = obj_or_key

    # Theme defaults
    if eff_kind == "error":
        border_def = getattr(c, "error_border", "red")
        header_def = getattr(c, "error_header_style", "bold red")
        title_def = getattr(c, "error_title_style", "bold red")
    elif eff_kind == "ttest":
        border_def = getattr(c, "ttest_border", "green")
        header_def = getattr(c, "header_style", "bold")
        title_def = getattr(c, "title_style", "bold")
    elif eff_kind == "sample":
        border_def = getattr(c, "sample_border", "cyan")
        header_def = getattr(c, "header_style", "bold")
        title_def = getattr(c, "title_style", "bold")
    elif eff_kind == "estimate":
        border_def = getattr(c, "estimate_border", "blue")
        header_def = getattr(c, "header_style", "bold")
        title_def = getattr(c, "title_style", "bold")
    elif eff_kind == "size":
        border_def = getattr(c, "size_border", "magenta")
        header_def = getattr(c, "header_style", "bold")
        title_def = getattr(c, "title_style", "bold")
    else:
        border_def = getattr(c, "panel_border", "white")
        header_def = getattr(c, "header_style", "bold")
        title_def = getattr(c, "title_style", "bold")

    if obj is not None:
        border_def = getattr(obj, "_PANEL_BORDER", None) or border_def
        header_def = getattr(obj, "_HEADER_STYLE", None) or header_def
        title_def = getattr(obj, "_TITLE_STYLE", None) or title_def

    return {
        "border": border or border_def,
        "header": header or header_def,
        "title": title or title_def,
    }


def panel_enabled(feature_key: str | None = None) -> bool:
    v = os.environ.get("SVY_PRINT_PANEL")
    if v is not None and v.lower() in {"0", "false", "off", "no"}:
        return False
    return True


# -----------------------------------------------------------------------------
# Rich availability check
# -----------------------------------------------------------------------------
def rich_available() -> bool:
    """Return True if the rich package is installed and importable."""
    try:
        import rich  # noqa: F401

        return True
    except ImportError:
        return False


# -----------------------------------------------------------------------------
# Console + rendering helpers
# -----------------------------------------------------------------------------
def _console(width: int):
    from rich.console import Console

    return Console(
        file=io.StringIO(),
        record=True,
        width=width,
        force_terminal=True,
        color_system="auto",
        emoji=False,
        # soft_wrap=False ensures Tables calculate wrapping correctly within the width
        soft_wrap=False,
    )


def render_rich_to_str(renderable, *, width: int) -> str:
    """
    Render a Rich renderable (or any object with __rich_console__) to a plain string.

    Falls back to plain_text_fallback(renderable) if rich is not installed,
    or to repr() if rendering raises an unexpected error.

    NOTE: The fallback must NOT call str(renderable) — that would re-enter
    __str__ on the object and cause infinite recursion.
    """
    if not rich_available():
        return plain_text_fallback(renderable, width=width)
    try:
        console = _console(width)
        console.print(renderable)
        return console.file.getvalue().rstrip("\r\n")
    except Exception:
        return repr(renderable)


def plain_text_fallback(renderable, *, width: int) -> str:
    """
    Produce a minimal plain-text representation when rich is not available.

    Checks for a __plain_str__ method first (opt-in per-class), then falls
    back to repr().  Never calls str() on the object to avoid recursion.
    """
    fn = getattr(renderable, "__plain_str__", None)
    if callable(fn):
        try:
            return fn()
        except Exception:
            pass
    return repr(renderable)


# -----------------------------------------------------------------------------
# Panel & Table builders (centralized)
# -----------------------------------------------------------------------------
def make_panel(
    children: Iterable,
    *,
    title: str,
    obj: object | None = None,
    kind: str = "panel",
):
    from rich.console import Group
    from rich.panel import Panel

    s = styles(obj, kind=kind)

    # Use explicit markup for title style if needed, or let Panel handle it if string
    return Panel(
        Group(*children),
        title=title,
        border_style=s["border"],
        padding=(0, 1),  # Internal padding between border and content
        expand=False,
    )


def make_table(
    *,
    header_names: Iterable[str],
    numeric: set[str] | None = None,
    obj: object | None = None,
    kind: str = "panel",
    variant: str = "rounded",
):
    from rich import box
    from rich.table import Table

    s = styles(obj, kind=kind)

    if variant == "header_only":
        chosen_box = getattr(box, "HORIZONTALS", box.SIMPLE_HEAD)
    elif variant == "minimal_head":
        chosen_box = getattr(box, "MINIMAL_HEAVY_HEAD", getattr(box, "SIMPLE_HEAVY", box.SIMPLE))
    else:
        chosen_box = box.ROUNDED

    t = Table(
        box=chosen_box,
        show_header=True,
        header_style=s["header"],
        pad_edge=False,
        expand=False,
        padding=(0, 1),
        show_edge=False if variant in {"header_only", "minimal_head"} else True,
        show_lines=False,
    )

    numeric = numeric or set()
    for name in header_names:
        justify = "right" if name in numeric else "left"
        t.add_column(name, justify=justify, no_wrap=(justify == "right"), overflow="fold")
    return t
