"""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."""

    is_tty: bool
    supports_ansi: bool
    supports_cursor: bool
    term: str


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

    Returns a ``TerminalCapabilities`` with booleans describing the
    connected stdout:

    * ``is_tty``          - ``sys.stdout.isatty()``
    * ``supports_ansi``   - True when ``$TERM`` is not ``dumb``
    * ``supports_cursor`` - True when TTY *and* non-dumb terminal
    * ``term``            - the raw ``$TERM`` value (empty if unset)
    """
    is_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
    term = os.environ.get("TERM", "")
    supports_ansi = is_tty and term.lower() != "dumb"
    # 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=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 = supports_ansi and any(
        term_lower.startswith(ct) for ct in _cursor_terms
    )
    return TerminalCapabilities(
        is_tty=is_tty,
        supports_ansi=supports_ansi,
        supports_cursor=supports_cursor,
        term=term,
    )


# 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:
            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()
