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

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
    available_capabilities: tuple[TerminalCapability, ...]
    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 能力位映射为终端能力枚举."""
        capabilities = TerminalProbe._capabilities_from_feature_flags(has_file, has_sixel)
        if not capabilities:
            return None
        return capabilities[0]

    @staticmethod
    def _capabilities_from_feature_flags(has_file: bool, has_sixel: bool) -> tuple[TerminalCapability, ...]:
        """将 Feature Reporting 能力位映射为终端能力集合."""
        capabilities: list[TerminalCapability] = []
        if has_file:
            program = os.environ.get("TERM_PROGRAM", "").lower()
            if "wezterm" in program:
                capabilities.append(TerminalCapability.WEZTERM)
            else:
                capabilities.append(TerminalCapability.ITERM2)
        if has_sixel:
            capabilities.append(TerminalCapability.SIXEL)
        return tuple(capabilities)

    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 _probe_available_capabilities_env(self) -> tuple[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)
        capabilities = self._capabilities_from_feature_flags(has_file, has_sixel)
        return capabilities or None

    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)

    def _probe_available_capabilities_query(
        self, timeout: float, *, remaining_budget: float | None = None
    ) -> tuple[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))
        capabilities = self._capabilities_from_feature_flags(has_file, has_sixel)
        return capabilities or None

    @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("TERM", ""),
            os.environ.get("KITTY_WINDOW_ID", ""),
            os.environ.get("ITERM_SESSION_ID", ""),
            os.environ.get("TERM_PROGRAM", ""),
            os.environ.get("VSCODE_PID", ""),
            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)
            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")

        term = os.environ.get("TERM", "").lower()
        if term == "xterm-kitty" or "KITTY_WINDOW_ID" in os.environ:
            return _finalize(TerminalCapability.KITTY, "kitty_env")

        if "ITERM_SESSION_ID" in os.environ:
            return _finalize(TerminalCapability.ITERM2, "iterm2_env")

        if "VSCODE_PID" in os.environ:
            return _finalize(TerminalCapability.FALLBACK, "vscode_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 probe_available_capabilities(self, timeout: float = 0.1) -> tuple[TerminalCapability, ...]:
        """探测终端可用渲染能力集合."""
        env_capabilities = self._probe_available_capabilities_env()
        if env_capabilities is not None:
            return env_capabilities

        term = os.environ.get("TERM", "").lower()
        if term == "xterm-kitty" or "KITTY_WINDOW_ID" in os.environ:
            return (TerminalCapability.KITTY,)

        if "ITERM_SESSION_ID" in os.environ:
            return (TerminalCapability.ITERM2,)

        if "VSCODE_PID" in os.environ:
            return (TerminalCapability.FALLBACK,)

        if self._can_use_wezterm_heuristic():
            return (TerminalCapability.WEZTERM,)

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

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

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

            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 (TerminalCapability.KITTY,)

            remain = _remaining()
            if remain > 0:
                cap_timeout = min(remain, _STEP_TIMEOUT_CAP_MS / 1000.0)
                capabilities = self._probe_available_capabilities_query(cap_timeout, remaining_budget=_remaining())
                if capabilities is not None:
                    return capabilities

            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 (TerminalCapability.SIXEL,)

        return (TerminalCapability.FALLBACK,)

    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()
        available_capabilities = self.probe_available_capabilities(timeout=timeout)
        capability = available_capabilities[0] if available_capabilities else TerminalCapability.FALLBACK
        self._cache = _ProbeCache(self._cache_key(), capability, "capabilities_snapshot", 0.0)
        capability_cache = self._cache
        color_level = self.probe_color(timeout=timeout)
        color_cache = self._color_cache
        value = TerminalCapabilities(
            capability=capability,
            color_level=color_level,
            available_capabilities=available_capabilities,
        )
        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,
            available_capabilities,
            capability_source,
            color_source,
            elapsed_ms,
        )
        logger.debug(
            "capabilities: capability=%s available=%s(%s) color=%s(%s) %.1fms",
            capability.name,
            ",".join(cap.name for cap in available_capabilities),
            capability_source,
            color_level.name,
            color_source,
            elapsed_ms,
        )
        return value
