import importlib
import importlib.abc
import importlib.machinery
import importlib.util
import sys
import threading
from types import ModuleType
from typing import Dict

from app.runtime.compat.diagnostics import record_legacy_import
from app.runtime.compat.manifest import (
    MODULE_ALIASES,
    PACKAGE_ALIASES,
    PACKAGE_EXPORTS,
    SYMBOL_ALIASES,
    VIRTUAL_PACKAGES,
    ModuleAlias,
)


_resolution_state = threading.local()


class LegacyAliasLoader(importlib.abc.Loader):
    """将旧模块键绑定到已按 canonical 名称加载的同一模块对象。"""

    _METADATA_NAMES = (
        "__name__",
        "__loader__",
        "__package__",
        "__spec__",
        "__path__",
        "__file__",
        "__cached__",
    )

    def __init__(self, legacy_name: str, alias: ModuleAlias):
        """保存当前旧路径规则，不在构造阶段导入目标模块。"""
        self.legacy_name = legacy_name
        self.alias = alias
        self._metadata: Dict[str, object] = {}

    def create_module(self, spec):
        """惰性导入 canonical 模块并复用其对象，禁止以旧名称二次执行源码。"""
        stack = list(getattr(_resolution_state, "stack", ()))
        if self.legacy_name in stack:
            chain = " -> ".join([*stack, self.legacy_name])
            raise ImportError(f"检测到兼容导入循环：{chain}")
        stack.append(self.legacy_name)
        _resolution_state.stack = stack
        try:
            module = importlib.import_module(self.alias.target)
        except ImportError as err:
            if hasattr(err, "add_note"):
                err.add_note(
                    f"兼容导入 {self.legacy_name} 指向 {self.alias.target} 时失败"
                )
            raise
        finally:
            stack.pop()
            _resolution_state.stack = stack
        self._metadata = {
            name: getattr(module, name)
            for name in self._METADATA_NAMES
            if hasattr(module, name)
        }
        return module

    def exec_module(self, module: ModuleType) -> None:
        """恢复 canonical 元数据并记录旧路径命中，不重复执行目标模块。"""
        target_module = sys.modules.get(self.alias.target)
        if module is not target_module:
            raise ImportError(
                f"兼容导入模块身份不一致：{self.legacy_name} -> {self.alias.target}"
            )
        for name, value in self._metadata.items():
            setattr(module, name, value)
        record_legacy_import(self.legacy_name)


class VirtualLegacyPackageLoader(importlib.abc.Loader):
    """创建无文件系统搜索路径的旧父包，只允许 manifest 白名单成员。"""

    def __init__(self, package_name: str):
        """保存待创建的旧父包名称。"""
        self.package_name = package_name

    def create_module(self, spec):
        """使用默认模块创建流程。"""
        return None

    def exec_module(self, module: ModuleType) -> None:
        """为合成包安装精确符号解析器和稳定的空搜索路径。"""
        exports = PACKAGE_EXPORTS.get(self.package_name, {})

        def resolve_export(name: str):
            """按 manifest 惰性解析旧包级公开符号。"""
            symbol = exports.get(name)
            if not symbol:
                raise AttributeError(
                    f"module {self.package_name!r} has no attribute {name!r}"
                )
            value = getattr(importlib.import_module(symbol.target_module), symbol.target_name)
            setattr(module, name, value)
            return value

        def list_exports():
            """返回合成包已声明的公开符号。"""
            return sorted(set(module.__dict__) | set(exports))

        module.__path__ = []
        module.__all__ = sorted(exports)
        module.__getattr__ = resolve_export
        module.__dir__ = list_exports
        if self.package_name in PACKAGE_ALIASES:
            record_legacy_import(self.package_name)


class LegacySymbolOverlayLoader(importlib.abc.Loader):
    """在标准物理模块执行后叠加旧符号的惰性解析，不修改 canonical 源码。"""

    _STATE_KEY = "__legacy_symbol_overlay_state__"

    def __init__(self, module_name: str, original_loader: importlib.abc.Loader):
        """保存物理模块名称和 PathFinder 已选择的原始 Loader。"""
        self.module_name = module_name
        self.original_loader = original_loader

    def __getattr__(self, name: str):
        """把资源读取等非核心 Loader 能力转交给原始 Loader。"""
        return getattr(self.original_loader, name)

    def create_module(self, spec):
        """沿用原始 Loader 的模块创建逻辑。"""
        creator = getattr(self.original_loader, "create_module", None)
        return creator(spec) if creator else None

    @classmethod
    def _restore_previous_overlay(cls, module: ModuleType) -> None:
        """reload 前恢复物理模块原有的动态属性和 __all__ 状态。"""
        state = module.__dict__.pop(cls._STATE_KEY, None)
        if not state:
            return
        for name in ("__getattr__", "__dir__"):
            previous = state.get(name)
            if previous is None:
                module.__dict__.pop(name, None)
            else:
                module.__dict__[name] = previous
        if state.get("had_all"):
            module.__dict__["__all__"] = state.get("all")
        else:
            module.__dict__.pop("__all__", None)

    def exec_module(self, module: ModuleType) -> None:
        """执行真实模块后安装只对已登记旧符号生效的 __getattr__。"""
        self._restore_previous_overlay(module)
        executor = getattr(self.original_loader, "exec_module", None)
        if not executor:
            raise ImportError(f"模块 {self.module_name} 的原始 Loader 不支持 exec_module")
        executor(module)

        exports = SYMBOL_ALIASES[self.module_name]
        previous_getattr = module.__dict__.get("__getattr__")
        previous_dir = module.__dict__.get("__dir__")
        had_all = "__all__" in module.__dict__
        previous_all = module.__dict__.get("__all__")

        def resolve_export(name: str):
            """惰性解析物理模块中已经迁走的旧符号。"""
            symbol = exports.get(name)
            if symbol:
                record_legacy_import(f"{self.module_name}.{name}")
                target = importlib.import_module(symbol.target_module)
                return getattr(target, symbol.target_name)
            if previous_getattr:
                return previous_getattr(name)
            raise AttributeError(
                f"module {self.module_name!r} has no attribute {name!r}"
            )

        def list_exports():
            """返回物理模块原有名称与兼容符号的并集。"""
            names = set(module.__dict__) | set(exports)
            if previous_dir:
                names.update(previous_dir())
            return sorted(names)

        module.__getattr__ = resolve_export
        module.__dir__ = list_exports
        # 兼容符号不并入 __all__：避免 `from <module> import *` 在包初始化期
        # 急切解析旧符号、反向拉起应用层模块形成循环导入；显式导入与属性
        # 访问仍由上方 __getattr__ 惰性解析兜底
        public_names = {
            name for name in module.__dict__ if not name.startswith("_")
        }
        declared_exports = set(previous_all or ()) if had_all else public_names
        module.__all__ = sorted(declared_exports)
        module.__dict__[self._STATE_KEY] = {
            "__getattr__": previous_getattr,
            "__dir__": previous_dir,
            "had_all": had_all,
            "all": previous_all,
        }


class BlockedLegacyModuleLoader(importlib.abc.Loader):
    """阻止合成旧包从其他 Finder 泄漏未登记的新内部模块。"""

    def __init__(self, module_name: str):
        """保存应拒绝的旧模块路径。"""
        self.module_name = module_name

    def create_module(self, spec):
        """使用默认模块创建流程，错误在执行阶段给出。"""
        return None

    def exec_module(self, module: ModuleType) -> None:
        """对未登记旧路径抛出标准 ModuleNotFoundError。"""
        raise ModuleNotFoundError(
            f"旧模块路径 {self.module_name} 未在兼容映射中登记",
            name=self.module_name,
        )


class LegacyImportFinder(importlib.abc.MetaPathFinder):
    """仅解析 manifest 声明的旧模块和合成父包。"""

    @staticmethod
    def _virtual_package_names():
        """计算显式虚拟根及其已登记模块所需的中间父包。"""
        package_names = set(VIRTUAL_PACKAGES)
        for legacy_name in [*MODULE_ALIASES, *PACKAGE_ALIASES, *PACKAGE_EXPORTS]:
            parts = legacy_name.split(".")
            for index in range(1, len(parts)):
                candidate = ".".join(parts[:index])
                if any(
                    candidate == root or candidate.startswith(f"{root}.")
                    for root in VIRTUAL_PACKAGES
                ):
                    package_names.add(candidate)
        return package_names

    def find_spec(self, fullname: str, path=None, target=None):
        """为精确旧模块返回 alias spec，并封锁虚拟包中的未知后代。"""
        alias = MODULE_ALIASES.get(fullname)
        if alias:
            loader = LegacyAliasLoader(fullname, alias)
            return importlib.util.spec_from_loader(
                fullname,
                loader,
                is_package=alias.is_package,
            )

        if fullname in SYMBOL_ALIASES:
            spec = importlib.machinery.PathFinder.find_spec(fullname, path, target)
            if spec and spec.loader:
                spec.loader = LegacySymbolOverlayLoader(fullname, spec.loader)
                return spec

        virtual_packages = self._virtual_package_names()
        if fullname in virtual_packages:
            loader = VirtualLegacyPackageLoader(fullname)
            spec = importlib.util.spec_from_loader(fullname, loader, is_package=True)
            if spec:
                spec.submodule_search_locations = []
            return spec

        if any(fullname.startswith(f"{root}.") for root in VIRTUAL_PACKAGES):
            return importlib.util.spec_from_loader(
                fullname,
                BlockedLegacyModuleLoader(fullname),
                is_package=False,
            )
        return None


def install_legacy_import_hook() -> LegacyImportFinder:
    """幂等安装旧导入 Finder，并确保它位于标准 PathFinder 之前。"""
    for finder in sys.meta_path:
        if isinstance(finder, LegacyImportFinder):
            return finder
    finder = LegacyImportFinder()
    index = next(
        (
            position
            for position, candidate in enumerate(sys.meta_path)
            if candidate is importlib.machinery.PathFinder
        ),
        len(sys.meta_path),
    )
    sys.meta_path.insert(index, finder)
    return finder


def uninstall_legacy_import_hook() -> None:
    """移除全部旧导入 Finder，仅供隔离测试恢复进程状态。"""
    sys.meta_path[:] = [
        finder for finder in sys.meta_path if not isinstance(finder, LegacyImportFinder)
    ]
