"""插件模块导入器。

负责插件的索引、加载和卸载。
"""

import sys
import importlib
import importlib.util
import importlib.machinery
import toml
from pathlib import Path
import re
import copy
from types import ModuleType
from typing import Dict, List, Optional

from ncatbot.utils import get_log

from ..config import config
from ..packhelper import PackageHelper
from .finder import (
    TOP_PACKAGE_NAME,
    ensure_finder_installed,
    register_plugin_path,
)

LOG = get_log("ModuleImporter")
_AUTO_INSTALL = config.auto_install_pip_pack


class _ModuleImporter:
    """把「目录->模块对象」的细节收敛到这里，方便做单元测试。"""

    def __init__(self):
        # 存储每个插件解析出的 manifest 数据： name -> dict
        self._manifests: Dict[str, dict] = {}
        # 插件名 -> 插件目录的绝对路径
        self._plugin_folders: Dict[str, Path] = {}
        # 原始插件名 -> sanitized 名称（用于生成合法的包名）
        self._sanitized_names: Dict[str, str] = {}
        # 插件名 -> 文件夹名称
        self._folder_names: Dict[str, str] = {}
        self._used_sanitized: set = set()

    def _ensure_sanitized(self, plugin_name: str) -> str:
        """Generate and record a sanitized package-friendly name for a plugin.

        The sanitized name contains only ASCII letters, digits and underscores,
        and will not start with a digit. If a collision occurs the name will be
        suffixed with `_1`, `_2`, ... to ensure uniqueness. Returns the
        sanitized name.
        """
        if plugin_name in self._sanitized_names:
            return self._sanitized_names[plugin_name]

        base_sanitized = re.sub(r"[^0-9a-zA-Z_]", "_", plugin_name)
        if base_sanitized and base_sanitized[0].isdigit():
            base_sanitized = f"_{base_sanitized}"
        if not base_sanitized:
            base_sanitized = "_plugin"
        sanitized = base_sanitized
        idx = 1
        while sanitized in self._used_sanitized:
            sanitized = f"{base_sanitized}_{idx}"
            idx += 1
        self._used_sanitized.add(sanitized)
        self._sanitized_names[plugin_name] = sanitized
        LOG.debug("插件名映射: %s -> %s", plugin_name, sanitized)
        return sanitized

    def _ensure_entry(self, plugin_dir: Path, main_field: str) -> bool:
        """Check whether the manifest 'main' entry exists under `plugin_dir`.

        Supports values with or without a `.py` suffix. Returns True if the
        entry file exists, False otherwise.
        """
        main_path = plugin_dir / main_field
        if main_path.exists():
            return True
        # 如果未带 .py 后缀，再尝试添加 .py
        if (
            not main_field.endswith(".py")
            and (plugin_dir / (main_field + ".py")).exists()
        ):
            return True
        LOG.warning(
            "文件夹 %s 的 manifest 'main' 指向的入口文件未找到: %s，跳过",
            plugin_dir.name,
            main_field,
        )
        return False

    def _index_single_plugin(self, plugin_dir: Path) -> Optional[str]:
        """索引单个插件目录

        Args:
            plugin_dir: 插件目录路径

        Returns:
            插件名称，如果索引失败则返回 None
        """
        plugin_dir = Path(plugin_dir).resolve()
        manifest_path = plugin_dir / "manifest.toml"

        if not plugin_dir.is_dir():
            LOG.debug("跳过非目录: %s", plugin_dir)
            return None

        if not manifest_path.exists():
            LOG.debug("跳过缺少 manifest.toml 的目录: %s", plugin_dir)
            return None

        try:
            manifest = toml.load(manifest_path)

            # 基本校验：必须包含 name/version/main
            plugin_name = manifest.get("name")
            version = manifest.get("version")
            main_field = manifest.get("main")

            if not plugin_name or not version or not main_field:
                LOG.warning("插件 %s 的 manifest 缺少必填字段，跳过", plugin_dir)
                return None

            if not self._ensure_entry(plugin_dir, main_field):
                return None

            if plugin_name in self._plugin_folders:
                LOG.warning("插件 %s 已存在，跳过加载", plugin_name)
                LOG.info(
                    "文件夹 %s 和文件夹 %s 下存在同名插件 %s",
                    self._plugin_folders[plugin_name],
                    plugin_dir,
                    plugin_name,
                )
                return None

            self._plugin_folders[plugin_name] = plugin_dir
            self._manifests[plugin_name] = manifest
            folder_name = plugin_dir.name
            self._folder_names[plugin_name] = folder_name
            sanitized = self._ensure_sanitized(plugin_name)

            # 关键：在索引阶段就注册插件路径到 Finder
            # 这样其他插件在加载时可以通过相对导入找到此插件
            register_plugin_path(sanitized, str(plugin_dir), folder_name)

            LOG.debug(
                "已索引插件: %s (路径: %s, 文件夹: %s)",
                plugin_name,
                plugin_dir,
                folder_name,
            )
            return plugin_name

        except Exception:
            LOG.exception("解析 manifest 失败: %s", manifest_path)
            return None

    def index_all_plugins(self, plugin_root: Path) -> List[str]:
        """扫描并索引 directory 下所有合法的插件，返回插件名列表"""
        # 确保 Finder 已安装
        ensure_finder_installed()
        for entry in plugin_root.iterdir():
            self._index_single_plugin(entry)
        return list(self._plugin_folders.keys())

    def index_external_plugin(self, plugin_dir: Path) -> Optional[str]:
        """索引一个外部插件文件夹

        Args:
            plugin_dir: 插件文件夹路径

        Returns:
            插件名称，如果索引失败则返回 None
        """
        ensure_finder_installed()
        return self._index_single_plugin(plugin_dir)

    def get_plugin_name_by_folder(self, folder_name: str) -> Optional[str]:
        """根据文件夹名称获取插件名称

        支持两种情况：
        1. folder_name 是完整路径
        2. folder_name 是文件夹名称，需要匹配存储路径的最后一部分
        """
        folder_path = Path(folder_name)
        for plugin_name, stored_path in self._plugin_folders.items():
            # 完整路径匹配
            if stored_path == folder_path or str(stored_path) == folder_name:
                return plugin_name
            # 匹配路径的最后一部分（文件夹名）
            if stored_path.name == folder_name:
                return plugin_name
        return None

    def get_plugin_manifests(self, plugin_root: Path) -> Dict[str, Dict]:
        if not self._manifests:
            raise ValueError("未索引任何插件")
        return copy.copy(self._manifests)

    def unload_plugin_module(self, plugin_name: str) -> bool:
        """卸载指定模块及其所有子模块"""
        if not plugin_name or not isinstance(plugin_name, str):
            LOG.warning("无效的模块名称: %s", plugin_name)
            return False

        pkg_name = self._get_plugin_pkg_name(plugin_name)

        # 从 Finder 中注销插件（可选，保留以支持重新加载）
        # sanitized_name = self._sanitized_names.get(plugin_name, plugin_name)
        # folder_name = self._folder_names.get(plugin_name)
        # unregister_plugin_path(sanitized_name, folder_name)

        # Collect candidates: modules that are the plugin package or its submodules.
        modules_to_remove = [
            module_name
            for module_name in list(sys.modules.keys())
            if module_name == pkg_name or module_name.startswith(f"{pkg_name}.")
        ]

        if not modules_to_remove:
            LOG.debug("模块 %s 未加载，无需卸载", plugin_name)
            return True

        removed_count = 0
        for module_name in modules_to_remove:
            try:
                del sys.modules[module_name]
                removed_count += 1
                LOG.debug("已卸载模块: %s", module_name)
            except KeyError:
                # already removed concurrently; ignore
                pass

        # Invalidate import caches in case files change on disk later
        try:
            importlib.invalidate_caches()
        except Exception:
            LOG.debug("invalidate_caches failed for %s", plugin_name)

        LOG.info("成功卸载模块 %s (共 %d 个模块)", plugin_name, removed_count)
        return True

    def load_plugin_module(self, plugin_name: str) -> Optional[ModuleType]:
        # 确保自定义 Finder 已安装
        ensure_finder_installed()

        # 清除导入缓存，确保读取最新的源文件
        importlib.invalidate_caches()

        # 清除插件目录的 __pycache__，确保重新编译源文件
        plugin_dir = self._get_plugin_dir(plugin_name)
        pycache_dir = plugin_dir / "__pycache__"
        if pycache_dir.exists():
            import shutil

            try:
                shutil.rmtree(pycache_dir)
                LOG.debug("已清除插件 %s 的字节码缓存", plugin_name)
            except Exception as e:
                LOG.warning("清除字节码缓存失败: %s", e)

        original_sys_path = sys.path.copy()
        created_pkg = False
        created_module = False
        try:
            plugin_path = str(plugin_dir)
            sys.path.insert(0, plugin_path)

            pkg_name = self._get_plugin_pkg_name(plugin_name)
            module_name = self._get_plugin_main_module_name(plugin_name)

            # 首先确保顶级虚拟包 ncatbot_plugin 存在
            if TOP_PACKAGE_NAME not in sys.modules:
                top_pkg_module = importlib.util.module_from_spec(
                    importlib.machinery.ModuleSpec(
                        TOP_PACKAGE_NAME, None, is_package=True
                    )
                )
                # 顶级虚拟包的 __path__ 为空，因为它只是一个命名空间包
                top_pkg_module.__path__ = []
                sys.modules[TOP_PACKAGE_NAME] = top_pkg_module

            if pkg_name not in sys.modules:
                # 创建一个空的模块作为包
                pkg_module = importlib.util.module_from_spec(
                    importlib.machinery.ModuleSpec(pkg_name, None, is_package=True)
                )
                pkg_module.__path__ = [plugin_path]
                sys.modules[pkg_name] = pkg_module
                created_pkg = True

            entry_file = self._get_plugin_entry_file(plugin_name)
            spec = importlib.util.spec_from_file_location(module_name, entry_file)
            if spec is None or spec.loader is None:
                LOG.exception(
                    "无法为插件 %s 创建模块 spec 或 loader 缺失: %s",
                    plugin_name,
                    entry_file,
                )
                raise ImportError(
                    f"Cannot load plugin {plugin_name}: spec or loader is None for {entry_file}"
                )
            module = importlib.util.module_from_spec(spec)
            # 明确保存 spec，便于调试/反射
            module.__spec__ = spec

            # 显式指定包名，确保相对导入 (from . import xxx) 正常工作
            module.__package__ = pkg_name

            # 注册模块对象（如果 exec 失败，需要回滚）
            sys.modules[module_name] = module
            created_module = True

            try:
                spec.loader.exec_module(module)
            except Exception:
                # 回滚我们对此次导入所做的 sys.modules 修改（仅回滚此函数中新建的项）
                if created_module:
                    sys.modules.pop(module_name, None)
                if created_pkg:
                    sys.modules.pop(pkg_name, None)
                # 注意：不回滚顶级包 ncatbot_plugin，因为它可能被其他插件使用
                # 顶级包是一个轻量级的命名空间包，保留它不会有负面影响
                raise

            return module
        except Exception as e:
            LOG.exception(e)
            raise
        finally:
            sys.path = original_sys_path

    def _get_plugin_pkg_name(self, name: str) -> str:
        # 使用 sanitized 名称生成包名，若未记录则退回到原始 name
        sanitized = self._sanitized_names.get(name, name)
        return f"{TOP_PACKAGE_NAME}.{sanitized}"

    def _get_plugin_entry_stem(self, name: str) -> str:
        """手滑把 .py 漏了也无所谓了"""
        manifest = self.get_plugin_manifest(name)
        if manifest is None:
            raise ValueError(f"插件 {name} 的 manifest 未找到")
        filename: str = manifest.get("main", "")
        if filename.endswith(".py"):
            return filename[:-3]
        return filename

    def _get_plugin_entry_file(self, name: str) -> str:
        return str(
            self._get_plugin_dir(name) / (self._get_plugin_entry_stem(name) + ".py")
        )

    def _get_plugin_main_module_name(self, name: str) -> str:
        return f"{self._get_plugin_pkg_name(name)}.{self._get_plugin_entry_stem(name)}"

    def get_plugin_manifest(self, plugin_name: str) -> Optional[dict]:
        """返回已解析的 manifest（若 inspect_all 已解析过）。

        注意：如果未调用 `inspect_all` 此方法可能返回 `None`。
        """
        return self._manifests.get(plugin_name)

    def _get_plugin_dir(self, name: str) -> Path:
        """获取插件目录路径"""
        return self._plugin_folders[name]


# 依赖解析和加载相关的功能
class _DependencyResolver:
    """插件依赖解析器。"""

    def __init__(self, importer: _ModuleImporter):
        self._importer = importer

    def resolve_load_order(self, plugin_names: List[str]) -> List[str]:
        """解析插件加载顺序，考虑依赖关系。

        返回按依赖顺序排列的插件列表。
        """
        # 简单实现：目前不处理复杂依赖，直接返回原列表
        # 未来可以实现拓扑排序
        return list(plugin_names)


def _install_pip_deps(manifest: dict) -> bool:
    """安装插件的 pip 依赖"""
    deps = manifest.get("pip_dependencies", [])
    if not deps:
        return True

    if not _AUTO_INSTALL:
        LOG.info("需要安装的依赖: %s (自动安装已禁用)", deps)
        return True

    LOG.info("正在安装插件依赖: %s", deps)
    return PackageHelper().install_packages(deps)


# 为了向后兼容，保留这些导出
__all__ = [
    "_ModuleImporter",
    "_DependencyResolver",
    "_install_pip_deps",
    "TOP_PACKAGE_NAME",
]
