"""终端能力探测模块, 用于检测当前终端是否支持特定的图形协议."""

import contextlib
import logging
import os
import re
import select
import sys
import time
from collections.abc import Generator
from dataclasses import dataclass
from typing import Any

from .contracts import TerminalCapabilities, TerminalCapability, TerminalColorLevel

logger = logging.getLogger(__name__)

if sys.platform == "win32":
    termios: Any | None = None
    tty: Any | None = None
    msvcrt: Any | None
    try:
        import msvcrt
    except ImportError:
        msvcrt = None
else:
    import termios
    import tty

    msvcrt: Any | None = None

_PROBE_TOTAL_BUDGET_MS = 80
_TMUX_PROBE_BUDGET_MS = 20
_STEP_TIMEOUT_KITTY_MS = 25
_STEP_TIMEOUT_CAP_MS = 25
_STEP_TIMEOUT_DA1_MS = 20
_RETRY_TIMEOUT_MS = 10


@dataclass(frozen=True)
class _ProbeCache:
    key: tuple[object, ...]
    capability: TerminalCapability
    probe_source: str
    elapsed_ms: float


@dataclass(frozen=True)
class _ColorProbeCache:
    key: tuple[object, ...]
    level: TerminalColorLevel
    probe_source: str
    elapsed_ms: float


@dataclass(frozen=True)
class _CapabilitiesCache:
    key: tuple[object, ...]
    value: TerminalCapabilities
    capability_source: str
    color_source: str
    elapsed_ms: float


class TerminalProbe:
    """终端能力探测器."""

    _cache: _ProbeCache | None = None
    _color_cache: _ColorProbeCache | None = None
    _capabilities_cache: _CapabilitiesCache | None = None

    @staticmethod
    def _parse_term_features(features: str) -> tuple[bool, bool]:
        """解析 feature string，提取 inline-file(F) 与 sixel(Sx) 能力位."""
        tokens = [t for t in re.split(r"[,\s;]+", features) if t]
        has_file = "F" in tokens
        has_sixel = "Sx" in tokens
        return has_file, has_sixel

    @staticmethod
    def _capability_from_feature_flags(has_file: bool, has_sixel: bool) -> TerminalCapability | None:
        """将 Feature Reporting 能力位映射为终端能力枚举."""
        if has_file:
            program = os.environ.get("TERM_PROGRAM", "").lower()
            if "wezterm" in program:
                return TerminalCapability.WEZTERM
            return TerminalCapability.ITERM2
        if has_sixel:
            return TerminalCapability.SIXEL
        return None

    def _probe_term_features_env(self) -> TerminalCapability | None:
        """优先读取 TERM_FEATURES 环境变量进行强判定."""
        feature_string = os.environ.get("TERM_FEATURES", "").strip()
        if not feature_string:
            return None
        has_file, has_sixel = self._parse_term_features(feature_string)
        return self._capability_from_feature_flags(has_file, has_sixel)

    def _query_capabilities(self, timeout: float, *, remaining_budget: float | None = None) -> str:
        """发送 iTerm2 Feature Reporting Capabilities 查询."""
        return self._query_terminal_retry("\x1b]1337;Capabilities\x07", timeout, remaining_budget=remaining_budget)

    def _probe_term_features_query(
        self, timeout: float, *, remaining_budget: float | None = None
    ) -> TerminalCapability | None:
        """通过 Capabilities 响应进行强判定."""
        response = self._query_capabilities(timeout, remaining_budget=remaining_budget)
        if not response:
            return None
        match = re.search(r"Capabilities=([^\x07\x1b\\]+)", response)
        if not match:
            return None
        has_file, has_sixel = self._parse_term_features(match.group(1))
        return self._capability_from_feature_flags(has_file, has_sixel)

    @contextlib.contextmanager
    def _raw_mode(self) -> Generator[None, None, None]:
        """进入 TTY 原始模式的上下文管理器."""
        if not sys.stdin.isatty():
            yield
            return

        if sys.platform == "win32":
            yield
            return

        if termios is not None and tty is not None:
            try:
                fd = sys.stdin.fileno()
                old_settings = termios.tcgetattr(fd)
                tty.setcbreak(fd)
            except Exception as exc:  # noqa: BLE001
                logger.debug("raw_mode unavailable, fallback to no-op: %r", exc)
                yield
                return

            try:
                yield
            finally:
                try:
                    termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
                except Exception as exc:  # noqa: BLE001
                    logger.debug("raw_mode restore skipped due to tcsetattr failure: %r", exc)
            return
        yield

    def _query_terminal(self, query: str, timeout: float) -> str:
        """向终端发送查询序列并读取响应, 受全局严格超时控制."""
        if not sys.stdin.isatty():
            return ""

        # 清除输入缓冲区
        if sys.platform != "win32":
            try:
                if termios is not None:
                    termios.tcflush(sys.stdin, termios.TCIFLUSH)
            except Exception:  # noqa: BLE001
                pass
        else:
            try:
                if msvcrt is not None:
                    while msvcrt.kbhit():
                        msvcrt.getch()
            except Exception:  # noqa: BLE001
                pass

        sys.stdout.write(query)
        sys.stdout.flush()

        start_time = time.monotonic()
        stdin = sys.stdin
        fd: int | None = None

        if sys.platform != "win32":
            try:
                stdin_fd = stdin.fileno()
            except Exception as exc:  # noqa: BLE001
                logger.debug("query skipped due to invalid stdin fileno: %r", exc)
                return ""
            if not isinstance(stdin_fd, int):
                logger.debug("query skipped due to non-integer stdin fileno: %r", stdin_fd)
                return ""
            fd = stdin_fd

        if sys.platform == "win32" and msvcrt is not None:
            res_bytes = bytearray()
            started = False
            while time.monotonic() - start_time < timeout:
                if msvcrt.kbhit():
                    char = msvcrt.getch()
                    if not started:
                        if char == b"\x1b":
                            started = True
                            res_bytes.clear()
                            res_bytes.extend(char)
                    else:
                        res_bytes.extend(char)
                    # 常见结束符: c (DA), \ (ST), BEL (\x07)
                    if started and char in (b"c", b"\\", b"\x07"):
                        break
                else:
                    # 极短暂休眠防止 CPU 占用率飙升到 100%
                    time.sleep(0.005)
            return res_bytes.decode("ascii", errors="ignore")

        response_chars: list[str] = []
        started = False
        while True:
            # 计算绝对剩余时间, 阻断慢速响应的累加效应
            elapsed = time.monotonic() - start_time
            remaining = timeout - elapsed

            if remaining <= 0:
                break

            # select 此时使用动态收缩的剩余时间
            rlist, _, _ = select.select([fd], [], [], remaining)
            if not rlist:
                break

            char = stdin.read(1)
            if not started:
                if char == "\x1b":
                    started = True
                    response_chars = [char]
            else:
                response_chars.append(char)
            # 常见结束符: c (DA), \ (ST), BEL (\x07)
            if started and char in ("c", "\\", "\x07"):
                break
        return "".join(response_chars)

    def _query_terminal_retry(self, query: str, timeout: float, *, remaining_budget: float | None = None) -> str:
        """发送查询并重试一次以降低脏数据和慢链路导致的偶发误判."""
        first = self._query_terminal(query, timeout)
        if first.startswith("\x1b"):
            return first

        if first == "":
            return ""

        retry_timeout = _RETRY_TIMEOUT_MS / 1000.0
        if remaining_budget is not None:
            retry_timeout = min(retry_timeout, max(0.0, remaining_budget))
        if retry_timeout <= 0:
            return ""
        return self._query_terminal(query, retry_timeout)

    @staticmethod
    def _is_sixel_da1(response: str) -> bool:
        """判断响应是否为包含 Sixel 能力位(4)的 DA1 报文."""
        match = re.match(r"^\x1b\[\??([0-9;]+)c$", response)
        if not match:
            return False
        return "4" in match.group(1).split(";")

    @staticmethod
    def _supports_interactive_probe() -> bool:
        """交互探测要求 stdin/stdout 都是 TTY."""
        return sys.stdin.isatty() and sys.stdout.isatty()

    @staticmethod
    def _budget_seconds() -> float:
        """计算本次探测预算（tmux 下更保守）."""
        budget_ms = _TMUX_PROBE_BUDGET_MS if "TMUX" in os.environ else _PROBE_TOTAL_BUDGET_MS
        return budget_ms / 1000.0

    @staticmethod
    def _is_wezterm_env() -> bool:
        """判断当前环境是否具备 WezTerm 特征."""
        term_program = os.environ.get("TERM_PROGRAM", "").lower()
        if "wezterm" in term_program:
            return True
        return "WEZTERM_EXECUTABLE" in os.environ or "WEZTERM_PANE" in os.environ

    def _can_use_wezterm_heuristic(self) -> bool:
        """判断是否可启用 WezTerm 乐观启发式."""
        return self._is_wezterm_env() and sys.stdout.isatty() and "TMUX" not in os.environ

    @staticmethod
    def _cache_key() -> tuple[object, ...]:
        """构建进程内缓存 key."""
        return (
            os.environ.get("TERM_FEATURES", ""),
            os.environ.get("KITTY_WINDOW_ID", ""),
            os.environ.get("TERM_PROGRAM", ""),
            os.environ.get("WEZTERM_EXECUTABLE", ""),
            os.environ.get("WEZTERM_PANE", ""),
            os.environ.get("TMUX", ""),
            sys.stdin.isatty(),
            sys.stdout.isatty(),
            sys.platform,
        )

    @staticmethod
    def _color_cache_key() -> tuple[object, ...]:
        """构建文本颜色探测缓存 key."""
        return (
            os.environ.get("NO_COLOR", ""),
            os.environ.get("FORCE_COLOR", ""),
            os.environ.get("COLORTERM", ""),
            os.environ.get("TERM", ""),
            os.environ.get("WT_SESSION", ""),
            os.environ.get("ANSICON", ""),
            os.environ.get("ConEmuANSI", ""),
            os.environ.get("TERM_PROGRAM", ""),
            sys.stdout.isatty(),
            sys.platform,
        )

    @classmethod
    def _capabilities_cache_key(cls) -> tuple[object, ...]:
        """构建终端能力快照缓存 key."""
        return cls._cache_key() + cls._color_cache_key()

    @staticmethod
    def _parse_force_color(value: str) -> TerminalColorLevel | None:
        """解析 FORCE_COLOR 语义."""
        raw = value.strip()
        if raw == "":
            return TerminalColorLevel.ANSI16
        if raw == "0":
            return TerminalColorLevel.NONE
        if raw == "1":
            return TerminalColorLevel.ANSI16
        if raw == "2":
            return TerminalColorLevel.ANSI256
        if raw == "3":
            return TerminalColorLevel.TRUECOLOR
        return TerminalColorLevel.ANSI16

    @staticmethod
    def _supports_windows_color_env() -> bool:
        """判断 Windows 环境变量是否表明支持 ANSI 颜色."""
        term_program = os.environ.get("TERM_PROGRAM", "").lower()
        if os.environ.get("WT_SESSION", ""):
            return True
        if os.environ.get("ANSICON", ""):
            return True
        if os.environ.get("ConEmuANSI", "").upper() == "ON":
            return True
        return term_program == "vscode"

    def probe_color(self, timeout: float = 0.05) -> TerminalColorLevel:
        """
        探测终端 ASCII 文本颜色能力等级.

        Args:
            timeout: 保留参数，当前实现仅做非交互探测.

        Returns:
            TerminalColorLevel: 文本颜色能力等级.

        """
        _ = timeout
        key = self._color_cache_key()
        cache = self._color_cache
        if cache is not None and cache.key == key:
            return cache.level

        start = time.monotonic()

        def _finalize(level: TerminalColorLevel, source: str) -> TerminalColorLevel:
            elapsed_ms = (time.monotonic() - start) * 1000.0
            self._color_cache = _ColorProbeCache(key, level, source, elapsed_ms)
            logger.debug("color: %s (source=%s, %.1fms)", level.name, source, elapsed_ms)
            return level

        if os.environ.get("NO_COLOR", "").strip():
            return _finalize(TerminalColorLevel.NONE, "no_color")

        force_raw = os.environ.get("FORCE_COLOR")
        if force_raw is not None:
            level = self._parse_force_color(force_raw)
            if level is not None:
                return _finalize(level, "force_color")

        term = os.environ.get("TERM", "").lower()
        if term == "dumb":
            return _finalize(TerminalColorLevel.NONE, "term_dumb")

        if not sys.stdout.isatty():
            return _finalize(TerminalColorLevel.NONE, "stdout_not_tty")

        colorterm = os.environ.get("COLORTERM", "").lower()
        if "truecolor" in colorterm or "24bit" in colorterm:
            return _finalize(TerminalColorLevel.TRUECOLOR, "colorterm")

        if "256color" in term:
            return _finalize(TerminalColorLevel.ANSI256, "term_256color")

        # 避免 ANSI16 不稳定：常见终端前缀（xterm, screen, tmux 等）通常比预期更强大，
        # 为了防止 16 色槽主题重映射造成的清晰度问题，默认假设 ANSI256 支持。
        basic_term_prefixes = ("xterm", "screen", "tmux", "vt100", "linux", "ansi", "rxvt")
        if term.startswith(basic_term_prefixes):
            return _finalize(TerminalColorLevel.ANSI256, "term_prefix_conservative")

        if sys.platform == "win32" and self._supports_windows_color_env():
            return _finalize(TerminalColorLevel.ANSI256, "windows_env_conservative")

        return _finalize(TerminalColorLevel.NONE, "fallback")

    def probe(self, timeout: float = 0.1) -> TerminalCapability:
        """
        执行终端状态探测.

        Args:
            timeout: I/O 阻塞超时时间(秒).

        Returns:
            TerminalCapability: 探测到的终端能力.

        """
        key = self._cache_key()
        cache = self._cache
        if cache is not None and cache.key == key:
            return cache.capability

        start = time.monotonic()

        def _finalize(cap: TerminalCapability, source: str) -> TerminalCapability:
            elapsed_ms = (time.monotonic() - start) * 1000.0
            self._cache = _ProbeCache(key, cap, source, elapsed_ms)
            logger.debug("probe: %s (source=%s, %.1fms)", cap.name, source, elapsed_ms)
            return cap

        cap_from_env = self._probe_term_features_env()
        if cap_from_env is not None:
            return _finalize(cap_from_env, "term_features_env")

        if "KITTY_WINDOW_ID" in os.environ:
            return _finalize(TerminalCapability.KITTY, "kitty_env")

        if self._can_use_wezterm_heuristic():
            return _finalize(TerminalCapability.WEZTERM, "wezterm_heuristic")

        if not self._supports_interactive_probe():
            return _finalize(TerminalCapability.FALLBACK, "fallback")

        budget = min(timeout, self._budget_seconds())
        with self._raw_mode():

            def _remaining() -> float:
                return max(0.0, budget - (time.monotonic() - start))

            # Level 1: single-shot priority query (Kitty)
            remain = _remaining()
            if remain > 0:
                kitty_timeout = min(remain, _STEP_TIMEOUT_KITTY_MS / 1000.0)
                kitty_query = "\x1b_Gi=31,a=q,s=1,v=1,t=d,f=24;AAAA\x1b\\"
                res = self._query_terminal_retry(kitty_query, kitty_timeout, remaining_budget=_remaining())
                if "i=31;OK" in res:
                    return _finalize(TerminalCapability.KITTY, "kitty_query")

            # Level 1.5: upgrade with remaining budget
            remain = _remaining()
            if remain > 0:
                cap_timeout = min(remain, _STEP_TIMEOUT_CAP_MS / 1000.0)
                cap_from_query = self._probe_term_features_query(cap_timeout, remaining_budget=_remaining())
                if cap_from_query is not None:
                    return _finalize(cap_from_query, "capabilities_query")

            remain = _remaining()
            if remain > 0:
                da1_timeout = min(remain, _STEP_TIMEOUT_DA1_MS / 1000.0)
                da1_query = "\x1b[c"
                res_da1 = self._query_terminal_retry(da1_query, da1_timeout, remaining_budget=_remaining())
                if self._is_sixel_da1(res_da1):
                    return _finalize(TerminalCapability.SIXEL, "da1")

        source = "tmux_conservative_fallback" if "TMUX" in os.environ else "fallback"
        return _finalize(TerminalCapability.FALLBACK, source)

    def capabilities(self, timeout: float = 0.1) -> TerminalCapabilities:
        """
        探测并缓存终端能力快照.

        Args:
            timeout: I/O 阻塞超时时间(秒).

        Returns:
            TerminalCapabilities: 包含图形协议与文本颜色等级的快照.

        """
        key = self._capabilities_cache_key()
        cache = self._capabilities_cache
        if cache is not None and cache.key == key:
            return cache.value

        start = time.monotonic()
        capability = self.probe(timeout=timeout)
        capability_cache = self._cache
        color_level = self.probe_color(timeout=timeout)
        color_cache = self._color_cache
        value = TerminalCapabilities(capability=capability, color_level=color_level)
        elapsed_ms = (time.monotonic() - start) * 1000.0
        capability_source = capability_cache.probe_source if capability_cache is not None else "unknown"
        color_source = color_cache.probe_source if color_cache is not None else "unknown"
        self._capabilities_cache = _CapabilitiesCache(key, value, capability_source, color_source, elapsed_ms)
        logger.debug(
            "capabilities: capability=%s(%s) color=%s(%s) %.1fms",
            capability.name,
            capability_source,
            color_level.name,
            color_source,
            elapsed_ms,
        )
        return value
