"""Materialiser selection logic with automatic fallback.

Determines the correct materialisation strategy for the resolved output
format, applying automatic fallbacks when terminal capabilities are
insufficient (e.g. Rich → Table → Color → Plain).

Based on the Output Rendering Framework specification (v3_spec.md §Output
Rendering Framework / Format Resolution).
"""

from __future__ import annotations

import os
import sys

from pydantic import BaseModel

from cleveragents.cli.output.materializers import (
    ColorMaterializer,
    JsonMaterializer,
    MaterializationStrategy,
    PlainMaterializer,
    RichMaterializer,
    TableMaterializer,
    YamlMaterializer,
)


class TerminalCapabilities(BaseModel):
    """Detected terminal capabilities used for format fallback decisions.

    Matches the spec's 11 fields (v3_spec.md §27264-27301).  Previous
    implementation had only 4 fields (SD-16); this version adds the
    7 missing fields: ``width``, ``height``, ``supports_256_color``,
    ``supports_truecolor``, ``supports_unicode``,
    ``supports_alternate_screen``, ``no_color``.

    Field renames from earlier implementation:
    - ``supports_cursor`` → ``supports_cursor_movement``
    - ``term`` → ``term_program``
    """

    is_tty: bool
    width: int = 80
    height: int = 24
    supports_ansi: bool
    supports_256_color: bool = False
    supports_truecolor: bool = False
    supports_unicode: bool = True
    supports_cursor_movement: bool = False
    supports_alternate_screen: bool = False
    no_color: bool = False
    term_program: str | None = None

    @property
    def supports_cursor(self) -> bool:
        """Backward-compatible alias for ``supports_cursor_movement``."""
        return self.supports_cursor_movement

    @property
    def term(self) -> str:
        """Backward-compatible alias for ``term_program``."""
        return self.term_program or ""


def detect_terminal_capabilities() -> TerminalCapabilities:
    """Probe the current terminal for capability flags.

    Returns a ``TerminalCapabilities`` with the full set of spec fields:

    * ``is_tty``                   - ``sys.stdout.isatty()``
    * ``width`` / ``height``       - ``os.get_terminal_size()`` (fallback 80x24)
    * ``supports_ansi``            - True when ``$TERM`` is not ``dumb``
    * ``supports_256_color``       - True when TERM contains "256color"
                                     or COLORTERM is set
    * ``supports_truecolor``       - True when COLORTERM is "truecolor"
                                     or "24bit"
    * ``supports_unicode``         - True when locale encoding is UTF-8
    * ``supports_cursor_movement`` - True when TTY and cursor-capable $TERM
    * ``supports_alternate_screen`` - True when cursor movement supported
    * ``no_color``                 - True when NO_COLOR env var is set
    * ``term_program``             - Value of TERM_PROGRAM env var
    """
    import locale

    is_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
    term = os.environ.get("TERM", "")
    colorterm = os.environ.get("COLORTERM", "")
    supports_ansi = is_tty and term.lower() != "dumb"

    # Terminal dimensions
    try:
        size = os.get_terminal_size()
        width, height = size.columns, size.lines
    except (ValueError, OSError):
        width, height = 80, 24

    # Color depth detection
    supports_256_color = "256color" in term.lower() or bool(colorterm)
    supports_truecolor = colorterm.lower() in {"truecolor", "24bit"}

    # Unicode support
    try:
        encoding = locale.getpreferredencoding(False) or ""
        supports_unicode = "utf" in encoding.lower()
    except Exception:  # pragma: no cover
        supports_unicode = True

    # Cursor movement requires a terminal emulator that supports cursor
    # addressing beyond basic ANSI colour codes.  We check for known
    # cursor-capable $TERM values.  This allows the state
    # ``supports_ansi=True, supports_cursor_movement=False`` (e.g. a
    # colour-only pipe), making the Color fallback tier reachable in the
    # Rich → Table → Color → Plain chain (spec §27177, N15 fix).
    _cursor_terms = {"xterm", "screen", "tmux", "rxvt", "alacritty", "kitty"}
    term_lower = term.lower()
    supports_cursor_movement = supports_ansi and any(
        term_lower.startswith(ct) for ct in _cursor_terms
    )
    supports_alternate_screen = supports_cursor_movement

    # NO_COLOR (https://no-color.org/)
    no_color = os.environ.get("NO_COLOR") is not None

    # TERM_PROGRAM
    term_program = os.environ.get("TERM_PROGRAM") or None

    return TerminalCapabilities(
        is_tty=is_tty,
        width=width,
        height=height,
        supports_ansi=supports_ansi,
        supports_256_color=supports_256_color,
        supports_truecolor=supports_truecolor,
        supports_unicode=supports_unicode,
        supports_cursor_movement=supports_cursor_movement,
        supports_alternate_screen=supports_alternate_screen,
        no_color=no_color,
        term_program=term_program,
    )


# Valid format names
VALID_FORMATS = frozenset({"rich", "color", "table", "plain", "json", "yaml"})


def select_materializer(
    format_name: str,
    *,
    capabilities: TerminalCapabilities | None = None,
    explicit: bool = False,
) -> MaterializationStrategy:
    """Select the materialisation strategy for a given format name.

    Parameters
    ----------
    format_name:
        One of ``rich``, ``color``, ``table``, ``plain``, ``json``, ``yaml``.
    capabilities:
        Terminal capabilities.  If *None* they are auto-detected.
    explicit:
        If *True* the user explicitly set the format via ``--format`` and
        no automatic fallback should be applied.

    Returns
    -------
    MaterializationStrategy
        A fresh strategy instance appropriate for the format and terminal.

    Raises
    ------
    ValueError
        If *format_name* is not a recognised format.
    """
    if not format_name:
        raise ValueError("format_name cannot be empty")

    fmt = format_name.lower()
    if fmt not in VALID_FORMATS:
        raise ValueError(
            f"Unknown format {format_name!r}; valid formats are {sorted(VALID_FORMATS)}"
        )

    # JSON and YAML are always supported (no terminal needed)
    if fmt == "json":
        return JsonMaterializer()
    if fmt == "yaml":
        return YamlMaterializer()

    caps = capabilities if capabilities is not None else detect_terminal_capabilities()

    # When user explicitly sets a format, respect it without fallback.
    # Explicit selection takes precedence over NO_COLOR.
    if explicit:
        return _create_strategy(fmt)

    # P1-4 fix (Luis review): respect NO_COLOR env var per
    # https://no-color.org/ and spec §26774.  When set, all visual
    # formats (rich, color, table) fall back to plain.  This replaces
    # SD-14 (previously documented as unimplemented).
    # Precedence: explicit flag > NO_COLOR > terminal capability fallback.
    # Only checked when capabilities are auto-detected (not when
    # explicitly passed by callers simulating a specific environment).
    if capabilities is None and os.environ.get("NO_COLOR") is not None:
        return PlainMaterializer()

    # Automatic fallback: Rich → Table → Color → Plain (N15 — spec §27177)
    # Note: The Color tier (supports_ansi without supports_cursor) is
    # narrowly reachable only when $TERM is a non-cursor-capable value
    # but the output is still a TTY.  In practice, most TTYs have
    # cursor-capable $TERM values, so the Rich → Table path dominates.
    # This is documented as SD-16 in __init__.py (R2-C2 fix #8).
    if fmt == "rich":
        if caps.supports_cursor_movement:
            return RichMaterializer()
        if caps.is_tty:
            return TableMaterializer()
        if caps.supports_ansi:
            return ColorMaterializer()
        return PlainMaterializer()

    if fmt == "color":
        if caps.supports_ansi:
            return ColorMaterializer()
        return PlainMaterializer()

    if fmt == "table":
        if caps.is_tty:
            return TableMaterializer()
        if caps.supports_ansi:
            return ColorMaterializer()
        return PlainMaterializer()

    # plain — always works
    return PlainMaterializer()


def _create_strategy(fmt: str) -> MaterializationStrategy:
    """Create a strategy instance for the exact format, no fallback."""
    strategy_map: dict[str, type] = {
        "rich": RichMaterializer,
        "color": ColorMaterializer,
        "table": TableMaterializer,
        "plain": PlainMaterializer,
        "json": JsonMaterializer,
        "yaml": YamlMaterializer,
    }
    cls = strategy_map[fmt]
    return cls()
