"""
core/deps_checker.py - 自动依赖检测与安装
==========================================
开箱即用：启动时自动检测缺失依赖并安装，确保所有技能可直接使用。

设计原则:
  - 轻量化：仅检测实际需要的模块，不做全量 pip check
  - 高效率：使用 importlib.util.find_spec 快速检测，0网络开销
  - 自动修复：缺失时自动 pip install，无需用户干预
  - 幂等安全：已安装的模块直接跳过，重复调用无副作用
  - 平台适配：自动区分 Windows/macOS/Linux，跳过不适用的依赖

依赖映射:
  核心功能: openai, aiohttp, requests
  搜索技能: bs4
  PDF 处理: PyPDF2
  系统技能: psutil, chardet
  托盘功能: pystray, PIL
  语音合成: edge_tts
  浏览器自动化: chrome-devtools-mcp (Node.js, Chrome DevTools Protocol)
  桌面GUI自动化: mss, pynput, pygetwindow
"""
from __future__ import annotations

import importlib.util
import os
import re
import subprocess
import sys
import threading
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple

from core.logger import get_logger

logger = get_logger("myagent.deps")

# 平台检测
IS_MACOS = sys.platform == "darwin"
IS_WINDOWS = sys.platform == "win32"
IS_LINUX = sys.platform.startswith("linux")


# ── 依赖定义 ──────────────────────────────────────────────

@dataclass
class DepInfo:
    """单个依赖的信息"""
    import_name: str          # Python import 名
    pip_name: str             # pip install 名
    min_version: str = ""     # 最低版本（可选）
    category: str = "core"    # 分类：core/browser/gui/tts/tray/optional
    platform: str = "all"     # 适用平台：all/windows/macos/linux
    note: str = ""            # 安装说明（失败时展示给用户）


# 所有需要自动管理的依赖
DEPENDENCIES: List[DepInfo] = [
    # ── 核心依赖 ──
    DepInfo("openai", "openai", "1.12.0", "core", "all"),
    DepInfo("aiohttp", "aiohttp", "3.9.0", "core", "all"),
    DepInfo("requests", "requests", "2.31.0", "core", "all"),

    # ── 搜索技能 ──
    DepInfo("bs4", "beautifulsoup4", "4.12.0", "search", "all"),

    # ── PDF 处理 ──
    DepInfo("PyPDF2", "PyPDF2", "3.0.0", "pdf", "all",
            note="纯 Python PDF 文本提取（pdftotext 失败时的备用方案）"),
    DepInfo("pypdf", "pypdf", "4.0.0", "pdf", "all",
            note="PDF 文本提取 (PyPDF2 新版, 优先使用)"),
    DepInfo("reportlab", "reportlab", "4.0.0", "pdf", "all",
            note="PDF 文档生成引擎"),
    DepInfo("PIL", "Pillow", "10.0.0", "pdf", "all",
            note="图片处理 (PDF 插图, 报告图表)"),

    # ── 文档处理 (Excel/Word/PPT) ──
    DepInfo("openpyxl", "openpyxl", "3.1.0", "doc", "all",
            note="Excel (.xlsx) 文件读取和生成"),
    DepInfo("docx", "python-docx", "1.1.0", "doc", "all",
            note="Word (.docx) 文件读取和生成"),
    DepInfo("pptx", "python-pptx", "0.6.21", "doc", "all",
            note="PowerPoint (.pptx) 文件读取和生成"),
    DepInfo("xlrd", "xlrd", "2.0.0", "doc", "all",
            note="旧版 Excel (.xls) 文件读取"),

    # ── 文件编码检测 ──
    DepInfo("chardet", "chardet", "5.0.0", "system", "all",
            note="文件编码自动检测"),

    # ── 系统技能 ──
    DepInfo("psutil", "psutil", "5.9.0", "system", "all"),

    # ── 系统托盘 ──
    DepInfo("pystray", "pystray", "0.19.5", "tray", "all"),
    DepInfo("PIL", "Pillow", "10.0.0", "tray", "all"),

    # ── 语音合成 ──
    DepInfo("edge_tts", "edge-tts", "6.1.0", "tts", "all"),

    # ── 语音识别 (STT) ──
    # [v1.18.8] torch/torchaudio 是 SenseVoice (funasr) 的必需依赖
    DepInfo("torch", "torch", "2.0.0", "stt", "all",
            note="PyTorch 深度学习框架 (SenseVoice 必需, CPU版约200MB)"),
    DepInfo("torchaudio", "torchaudio", "2.0.0", "stt", "all",
            note="PyTorch 音频处理库 (SenseVoice 必需)"),
    DepInfo("funasr", "funasr", "1.1.0", "stt", "all",
            note="[v1.18.8] SenseVoice 中文语音识别（首选，阿里达摩院）"),

    DepInfo("speech_recognition", "SpeechRecognition", "3.10.0", "stt", "all",
            note="在线语音识别 (Google API，纯 Python 无需编译，Termux 兼容)"),

    # ── 浏览器自动化 (ChromeDev MCP) ──
    # Playwright 已移除，浏览器自动化统一使用 ChromeDevTools Protocol (MCP)
    # 需要 Node.js >= 20.19，参见 aiskills/chromedev_mcp.py

    # ── 桌面 GUI 自动化 ──
    DepInfo("mss", "mss", "9.0.0", "gui", "all",
            note="跨平台屏幕截图库 (~1MB)"),
    DepInfo("pynput", "pynput", "1.7.6", "gui", "all",
            note="鼠标/键盘控制 (~500KB)"),
    DepInfo("pygetwindow", "pygetwindow", "0.0.9", "gui", "windows_macos",
            note="窗口管理 (仅 Windows/macOS)"),

    # ── 远程桌面 (VNC) [v1.17.0] ──
    # VNC 依赖通过 apt 安装 (xvfb, x11vnc)，websockify 通过 pip 安装
    # 实际检测和安装在 VNCManager.ensure_dependencies() 中完成

    # ── 系统工具 [v1.20.9] ──
    # ffmpeg: SenseVoice 音频解码必需 (WAV→16kHz转换)
]

# [v1.20.9] 系统级工具依赖列表（非 Python 包，需通过系统包管理器安装）
SYSTEM_TOOLS = [
    {"name": "ffmpeg", "note": "FFmpeg 多媒体处理工具 (SenseVoice STT 必需, 用于音频格式转换)",
     "install": {"linux": "sudo apt install ffmpeg", "macos": "brew install ffmpeg"}},
    {"name": "convert", "note": "ImageMagick 图像处理工具 (VNC 截图备用)",
     "install": {"linux": "sudo apt install imagemagick", "macos": "brew install imagemagick"}},
]


# ── 检测与安装 ────────────────────────────────────────────

def _is_platform_match(dep: DepInfo) -> bool:
    """检查依赖是否适用于当前平台"""
    if dep.platform == "all":
        return True
    if dep.platform == "windows" and IS_WINDOWS:
        return True
    if dep.platform == "macos" and IS_MACOS:
        return True
    if dep.platform == "linux" and IS_LINUX:
        return True
    if dep.platform == "windows_macos" and (IS_WINDOWS or IS_MACOS):
        return True
    return False


def _is_module_available(import_name: str) -> bool:
    """快速检测 Python 模块是否可导入（不实际导入，无副作用）"""
    spec = importlib.util.find_spec(import_name)
    return spec is not None


def _get_python_executable() -> str:
    """获取当前 Python 解释器路径"""
    return sys.executable


def _get_pip_index_args() -> List[str]:
    """获取 pip 镜像源参数。国内环境自动使用清华镜像，避免超时。"""
    # 检测是否需要使用国内镜像（通过环境变量或语言环境判断）
    use_mirror = os.environ.get("MYAGENT_PIP_MIRROR", "").lower() in ("1", "true", "yes", "cn")
    if not use_mirror:
        # 通过语言环境判断是否在国内
        lang = os.environ.get("LANG", "").lower()
        lc_all = os.environ.get("LC_ALL", "").lower()
        if "zh_cn" in lang or "zh_cn" in lc_all or "chinese" in lang:
            use_mirror = True

    if use_mirror:
        # 依次尝试的国内镜像（主源 + 备用）
        return ["-i", "https://pypi.tuna.tsinghua.edu.cn/simple",
                "--trusted-host", "pypi.tuna.tsinghua.edu.cn"]
    return []


def _is_skip_deps() -> bool:
    """检查是否设置了跳过依赖安装的环境变量"""
    return os.environ.get("MYAGENT_SKIP_DEPS") == "1"


def _pip_install(pip_names: List[str], category: str = "") -> Tuple[bool, str]:
    """
    使用当前 Python 解释器执行 pip install。

    Returns:
        (success, message)
    """
    # --skip-deps 时完全跳过任何 pip 安装
    if _is_skip_deps():
        packages = " ".join(pip_names)
        logger.debug(f"跳过依赖安装（--skip-deps）: {packages}")
        return True, f"已跳过: {packages}"
    python = _get_python_executable()
    packages = " ".join(pip_names)

    # 镜像源参数（国内用户自动使用清华镜像）
    mirror_args = _get_pip_index_args()

    # 处理 PEP668 (externally-managed-environment)
    # 尝试: 1) 直接安装  2) --break-system-packages  3) 用户空间 --user
    install_attempts = [
        [python, "-m", "pip", "install", "--quiet", "--disable-pip-version-check"] + mirror_args + pip_names,
        [python, "-m", "pip", "install", "--quiet", "--disable-pip-version-check",
         "--break-system-packages"] + mirror_args + pip_names,
        [python, "-m", "pip", "install", "--quiet", "--disable-pip-version-check",
         "--user"] + mirror_args + pip_names,
    ]

    last_error = ""
    for attempt_cmd in install_attempts:
        try:
            result = subprocess.run(
                attempt_cmd,
                capture_output=True,
                timeout=180,  # 3分钟超时（国内镜像可能较慢）
            )
            # 使用 utf-8 + errors="replace" 解码输出，避免 Windows GBK 编码错误
            stdout = result.stdout.decode("utf-8", errors="replace") if result.stdout else ""
            stderr = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""

            if result.returncode == 0:
                return True, f"已安装: {packages}"
            last_error = stderr.strip()
            # 如果已经安装（满足要求），也算成功
            if "Requirement already satisfied" in stdout or \
               "Requirement already satisfied" in stderr:
                return True, f"已就绪: {packages}"
        except subprocess.TimeoutExpired:
            last_error = "安装超时（180秒）"
            continue
        except Exception as e:
            last_error = str(e)
            continue

    # 所有尝试失败后，尝试使用阿里云镜像重试一次
    if not mirror_args:
        logger.info(f"默认源安装失败，尝试阿里云镜像: {packages}")
        retry_cmd = [python, "-m", "pip", "install", "--quiet", "--disable-pip-version-check",
                     "-i", "https://mirrors.aliyun.com/pypi/simple/",
                     "--trusted-host", "mirrors.aliyun.com"] + pip_names
        try:
            result = subprocess.run(
                retry_cmd,
                capture_output=True,
                timeout=180,
            )
            stdout = result.stdout.decode("utf-8", errors="replace") if result.stdout else ""
            stderr = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
            if result.returncode == 0 or "Requirement already satisfied" in stdout or \
               "Requirement already satisfied" in stderr:
                return True, f"已安装(阿里云镜像): {packages}"
            last_error = stderr.strip()
        except Exception as e:
            last_error = str(e)

    return False, f"安装失败: {packages} - {last_error}"


def _check_version(import_name: str, min_version: str) -> bool:
    """检查模块版本是否满足最低要求（使用简单的 tuple 比较，无需额外依赖）"""
    if not min_version:
        return True
    try:
        spec = importlib.util.find_spec(import_name)
        if spec is None:
            return False
        mod = importlib.import_module(import_name)
        ver_str = getattr(mod, "__version__", "0.0.0")
        # 简单版本比较：将版本字符串转为 tuple 进行比较
        def _ver_tuple(v: str):
            parts = []
            for p in re.split(r'[.\-]', v)[:3]:
                m = re.match(r'(\d+)', str(p))
                parts.append(int(m.group(1)) if m else 0)
            return tuple(parts)
        try:
            return _ver_tuple(ver_str) >= _ver_tuple(min_version)
        except (ValueError, TypeError):
            return True
    except Exception:
        return True  # 无法获取版本时保守通过


# ── 主流程 ────────────────────────────────────────────────

def check_and_install_deps(
    categories: Optional[Set[str]] = None,
    auto_fix: bool = True,
    silent: bool = False,
) -> Dict[str, any]:
    """
    检测依赖并在缺失时自动安装。

    Args:
        categories: 要检查的依赖分类集合。
                    None = 检查所有分类。
                    例如: {"browser", "gui"} 只检查浏览器和GUI依赖。
        auto_fix: 是否自动安装缺失的依赖（默认 True）。
        silent: 是否静默模式，不打印日志（默认 False）。

    Returns:
        {
            "checked": int,        # 检查的依赖数量
            "available": int,      # 已安装的数量
            "missing": int,        # 缺失的数量
            "installed": int,      # 本次新安装的数量
            "failed": int,         # 安装失败的数量
            "skipped_platform": int,  # 因平台不匹配而跳过的数量
            "details": {...},      # 每个依赖的状态
            "browser": str,         # ChromeDev MCP 状态
        }
    """
    # --skip-deps 安全网：即使调用方忘记检查，也不会执行安装
    skip_deps = _is_skip_deps()

    stats = {
        "checked": 0,
        "available": 0,
        "missing": 0,
        "installed": 0,
        "failed": 0,
        "skipped_platform": 0,
        "details": {},
        "browser": "not_checked",
    }

    # 按分类收集缺失的依赖，批量安装以减少 pip 调用次数
    missing_by_category: Dict[str, List[str]] = {}  # category -> [pip_name, ...]
    dep_pip_map: Dict[str, DepInfo] = {}  # pip_name -> DepInfo (用于记录安装状态)

    # 第一遍：检测所有依赖
    for dep in DEPENDENCIES:
        # 平台过滤
        if not _is_platform_match(dep):
            stats["skipped_platform"] += 1
            stats["details"][dep.import_name] = {
                "status": "skipped",
                "reason": f"平台不匹配 (需要: {dep.platform}, 当前: {sys.platform})",
            }
            continue

        # 分类过滤
        if categories and dep.category not in categories:
            continue

        stats["checked"] += 1

        # 检测模块是否可用
        if _is_module_available(dep.import_name):
            stats["available"] += 1
            stats["details"][dep.import_name] = {"status": "available"}
            if not silent:
                logger.debug(f"依赖已就绪: {dep.import_name}")
            continue

        # 缺失
        stats["missing"] += 1
        stats["details"][dep.import_name] = {
            "status": "missing",
            "pip_name": dep.pip_name,
            "category": dep.category,
        }

        if dep.pip_name not in dep_pip_map:
            dep_pip_map[dep.pip_name] = dep
            missing_by_category.setdefault(dep.category, []).append(dep.pip_name)

        if not silent:
            logger.info(f"依赖缺失: {dep.import_name} (pip: {dep.pip_name}, 分类: {dep.category})")

    if not auto_fix or stats["missing"] == 0 or skip_deps:
        if skip_deps and stats["missing"] > 0 and not silent:
            logger.info(f"跳过 {stats['missing']} 个缺失依赖的安装（--skip-deps）")
        return stats

    # 第二遍：批量安装缺失的依赖（按分类分批）
    if not silent:
        logger.info(f"开始自动安装 {stats['missing']} 个缺失依赖...")

    total_installed = 0
    total_failed = 0

    for category, pip_names in missing_by_category.items():
        success, message = _pip_install(pip_names, category)

        # 更新所有相关依赖的状态
        for pip_name in pip_names:
            dep = dep_pip_map[pip_name]
            if success:
                stats["installed"] += 1
                stats["details"][dep.import_name]["status"] = "installed"
                total_installed += 1
                if not silent:
                    logger.info(f"  ✓ 已安装: {dep.import_name} ({dep.category})")
            else:
                stats["failed"] += 1
                stats["details"][dep.import_name]["status"] = "install_failed"
                stats["details"][dep.import_name]["error"] = message
                total_failed += 1
                if not silent:
                    logger.warning(f"  ✗ 安装失败: {dep.import_name} - {message}")

    # 汇总日志
    if not silent and (total_installed > 0 or total_failed > 0):
        logger.info(
            f"依赖检查完成: {stats['available']}/{stats['checked']} 已就绪, "
            f"{total_installed} 新安装, {total_failed} 失败"
        )

    return stats


def ensure_skill_deps(skill_category: str) -> bool:
    """
    确保指定技能分类的依赖已安装。
    在技能执行前调用，提供懒加载安装能力。

    Args:
        skill_category: 技能分类名称
                        "browser" - 浏览器自动化 (chrome-devtools-mcp)
                        "gui" - 桌面GUI自动化 (mss, pynput, pygetwindow)
                        "search" - 搜索技能
                        "tts" - 语音合成

    Returns:
        True 如果所有依赖已就绪（包括刚安装的）
    """
    # --skip-deps 时跳过懒加载依赖安装
    if _is_skip_deps():
        logger.debug(f"跳过技能依赖检查（--skip-deps）: {skill_category}")
        return True

    category_map = {
        "browser": {"browser"},
        "gui": {"gui"},
        "search": {"search"},
        "tts": {"tts"},
        "stt": {"stt"},
        "tray": {"tray"},
        "pdf": {"pdf"},
        "doc": {"doc"},
    }

    cats = category_map.get(skill_category, {skill_category})
    result = check_and_install_deps(categories=cats, auto_fix=True, silent=True)

    return result["failed"] == 0 and result["missing"] == result["installed"]


def get_deps_status() -> Dict[str, Dict]:
    """
    获取所有依赖的当前状态（不自动安装）。
    供 API 和 UI 展示使用。

    Returns:
        {import_name: {"status": "available"|"missing", "version": "...", "category": "..."}}
    """
    status = {}
    for dep in DEPENDENCIES:
        if not _is_platform_match(dep):
            continue

        available = _is_module_available(dep.import_name)
        version = ""
        if available:
            try:
                mod = importlib.import_module(dep.import_name)
                version = getattr(mod, "__version__", "")
            except Exception:
                pass

        status[dep.import_name] = {
            "status": "available" if available else "missing",
            "version": version,
            "category": dep.category,
            "pip_name": dep.pip_name,
        }

    return status


# ── 后台预安装（非阻塞） ──────────────────────────────────

_background_check_done = False
_background_check_running = False


def start_background_check():
    """
    在后台线程中执行依赖检查和安装（非阻塞）。
    适用于启动时不想等待安装完成的场景。
    """
    global _background_check_done, _background_check_running

    if _background_check_done or _background_check_running:
        return

    _background_check_running = True

    def _do_check():
        global _background_check_done, _background_check_running
        try:
            check_and_install_deps(auto_fix=True, silent=True)
        except Exception as e:
            logger.warning(f"后台依赖检查失败: {e}")
        finally:
            _background_check_done = True
            _background_check_running = False

    t = threading.Thread(target=_do_check, daemon=True, name="deps-checker")
    t.start()
    logger.info("后台依赖检查已启动")


def wait_background_check(timeout: float = 30.0) -> bool:
    """
    等待后台依赖检查完成。

    Args:
        timeout: 最长等待时间（秒）

    Returns:
        True 如果检查已完成，False 如果超时
    """
    import time
    deadline = time.monotonic() + timeout
    while _background_check_running and time.monotonic() < deadline:
        time.sleep(0.5)
    return _background_check_done
