"""插件公开能力投影。"""

import inspect
from typing import Any, Callable, Dict, List, Mapping, Optional

from app.runtime.extensions.plugin.contracts import (
    PluginDashboardError,
    PluginNotFoundError,
    supports_plugin_hook,
)
from app.runtime.log import logger as default_logger
from app.schemas.plugin import PluginDashboard


class PluginProjection:
    """把运行态插件投影为宿主命令、API、服务、模块和动作清单。"""

    def __init__(
        self,
        running_plugins: Mapping[str, Any],
        log: Any = default_logger,
        remote_entry_factory: Optional[Callable[[str, str], str]] = None,
    ) -> None:
        """保存运行态插件映射和错误日志端口。"""
        self._running_plugins = running_plugins
        self._logger = log
        self._remote_entry_factory = remote_entry_factory

    def _items(self, pid: Optional[str]) -> list[tuple[str, Any]]:
        """返回指定插件或运行态插件的稳定快照。"""
        snapshot = dict(self._running_plugins)
        if pid:
            plugin = snapshot.get(pid)
            return [(pid, plugin)] if plugin is not None else []
        return list(snapshot.items())

    def commands(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
        """聚合插件命令并补充插件 ID。"""
        commands: list[dict] = []
        for plugin_id, plugin in self._items(pid):
            if not supports_plugin_hook(plugin, "get_command"):
                continue
            try:
                if not plugin.get_state():
                    continue
                for command in plugin.get_command() or []:
                    command["pid"] = plugin_id
                    commands.append(command)
            except Exception as error:
                self._logger.error(f"获取插件命令出错：{str(error)}")
        return commands

    def apis(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
        """聚合插件 API 并补充宿主路径和默认认证方式。"""
        apis: list[dict] = []
        for plugin_id, plugin in self._items(pid):
            if not supports_plugin_hook(plugin, "get_api"):
                continue
            try:
                for source_api in plugin.get_api() or []:
                    api = dict(source_api)
                    api["path"] = f"/{plugin_id}{api['path']}"
                    if not api.get("auth"):
                        api["auth"] = "apikey"
                    apis.append(api)
            except Exception as error:
                self._logger.error(f"获取插件 {plugin_id} API出错：{str(error)}")
        return apis

    def services(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
        """聚合启用插件的定时服务。"""
        services: list[dict] = []
        for plugin_id, plugin in self._items(pid):
            if not supports_plugin_hook(plugin, "get_service"):
                continue
            try:
                if plugin.get_state():
                    services.extend(plugin.get_service() or [])
            except Exception as error:
                self._logger.error(f"获取插件 {plugin_id} 服务出错：{str(error)}")
        return services

    def modules(self, pid: Optional[str] = None) -> Dict[tuple, Dict[str, Any]]:
        """聚合启用插件的模块方法清单。"""
        modules: dict[tuple, dict] = {}
        for plugin_id, plugin in self._items(pid):
            if not supports_plugin_hook(plugin, "get_module"):
                continue
            try:
                if plugin.get_state():
                    declared = plugin.get_module()
                    # 基类默认实现返回 None；只接受映射，防止把 list 当成方法表传入调度器
                    if declared is None:
                        continue
                    if not isinstance(declared, Mapping):
                        self._logger.error(
                            f"插件 {plugin_id} 的 get_module() 返回值必须是字典，实际是 {type(declared).__name__}"
                        )
                        continue
                    modules[(plugin_id, plugin.get_name())] = declared
            except Exception as error:
                self._logger.error(f"获取插件 {plugin_id} 模块出错：{str(error)}")
        return modules

    def media_sources(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
        """聚合启用插件声明的媒体数据源。"""
        sources: list[dict] = []
        for plugin_id, plugin in self._items(pid):
            if not supports_plugin_hook(plugin, "get_media_source"):
                continue
            try:
                if not plugin.get_state():
                    continue
                for source in plugin.get_media_source() or []:
                    if isinstance(source, dict):
                        item = source.copy()
                        item.setdefault("plugin_id", plugin_id)
                        sources.append(item)
            except Exception as error:
                self._logger.error(f"获取插件 {plugin_id} 媒体数据源出错：{str(error)}")
        return sources

    def actions(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
        """聚合启用插件的工作流动作。"""
        actions: list[dict] = []
        for plugin_id, plugin in self._items(pid):
            if not supports_plugin_hook(plugin, "get_actions"):
                continue
            try:
                if not plugin.get_state():
                    continue
                plugin_actions = plugin.get_actions()
                if plugin_actions:
                    actions.append({
                        "plugin_id": plugin_id,
                        "plugin_name": plugin.plugin_name,
                        "actions": plugin_actions,
                    })
            except Exception as error:
                self._logger.error(f"获取插件 {plugin_id} 动作出错：{str(error)}")
        return actions

    def remotes(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
        """投影插件联邦远程入口，并保持旧渲染模式筛选语义。"""
        remotes = []
        for plugin_id, plugin in self._items(pid):
            if not supports_plugin_hook(plugin, "get_render_mode"):
                continue
            render_mode, dist_path = plugin.get_render_mode()
            if render_mode != "vue":
                continue
            if not self._remote_entry_factory:
                raise RuntimeError("插件联邦入口生成器尚未配置")
            remote = {
                "id": plugin_id,
                "url": self._remote_entry_factory(plugin_id, dist_path),
                "name": plugin.plugin_name,
            }
            source_plugin_id = getattr(plugin, "plugin_source_id", None)
            if source_plugin_id:
                remote["source_plugin_id"] = source_plugin_id
            remotes.append(remote)
        return remotes

    def auth_providers(self) -> List[Dict[str, Any]]:
        """投影启用插件声明的登录认证提供方。"""
        providers = []
        for plugin_id, plugin in self._items(None):
            if not plugin.get_state() or not supports_plugin_hook(
                    plugin, "get_auth_providers"
            ):
                continue
            try:
                plugin_providers = plugin.get_auth_providers() or []
            except Exception as error:
                self._logger.error(
                    f"获取插件 {plugin_id} 登录认证提供方出错：{str(error)}"
                )
                continue
            render_mode = None
            dist_path = None
            if supports_plugin_hook(plugin, "get_render_mode"):
                render_mode, dist_path = plugin.get_render_mode()
            for raw_provider in plugin_providers:
                if not raw_provider or not isinstance(raw_provider, dict):
                    continue
                provider = raw_provider.copy()
                provider["type"] = "plugin"
                provider["plugin_id"] = plugin_id
                provider.setdefault("id", f"plugin:{plugin_id}")
                provider.setdefault("name", plugin.plugin_name)
                provider.setdefault("enabled", True)
                if render_mode == "vue" and dist_path:
                    if not self._remote_entry_factory:
                        raise RuntimeError("插件联邦入口生成器尚未配置")
                    provider.setdefault("component", "AuthPage")
                    remote = {
                        "id": plugin_id,
                        "url": self._remote_entry_factory(plugin_id, dist_path),
                        "name": plugin.plugin_name,
                    }
                    source_plugin_id = getattr(plugin, "plugin_source_id", None)
                    if source_plugin_id:
                        remote["source_plugin_id"] = source_plugin_id
                    provider["remote"] = remote
                providers.append(provider)
        return providers

    def sidebar(self) -> List[Dict[str, Any]]:
        """投影启用 Vue 插件的侧栏导航，并规整权限、分区和顺序。"""
        valid_sections = {"start", "discovery", "subscribe", "organize", "system"}
        valid_permissions = {"subscribe", "discovery", "search", "manage", "admin"}
        items = []
        for plugin_id, plugin in self._items(None):
            if not plugin.get_state() or not supports_plugin_hook(
                    plugin, "get_sidebar_nav"
            ):
                continue
            if not supports_plugin_hook(plugin, "get_render_mode"):
                continue
            render_mode, _ = plugin.get_render_mode()
            if render_mode != "vue":
                continue
            try:
                nav_list = plugin.get_sidebar_nav()
                if not nav_list:
                    continue
                for raw in nav_list:
                    if not raw or not isinstance(raw, dict):
                        continue
                    nav_key = str(
                        raw.get("nav_key") or raw.get("key") or "main"
                    ).strip()
                    if not nav_key or any(
                            character in nav_key for character in ["/", "?", "#", " "]
                    ):
                        self._logger.warning(
                            f"插件[{plugin_id}]侧栏项 nav_key 无效，已跳过: "
                            f"{nav_key!r}"
                        )
                        continue
                    section = str(raw.get("section") or "system").lower()
                    if section not in valid_sections:
                        section = "system"
                    permission = raw.get("permission")
                    if permission is not None and str(permission) not in valid_permissions:
                        permission = None
                    elif permission is not None:
                        permission = str(permission)
                    try:
                        order = int(raw.get("order", 0))
                    except (TypeError, ValueError):
                        order = 0
                    items.append({
                        "plugin_id": plugin_id,
                        "nav_key": nav_key,
                        "title": raw.get("title") or plugin.plugin_name,
                        "icon": raw.get("icon") or "mdi-puzzle",
                        "section": section,
                        "permission": permission,
                        "order": order,
                    })
            except Exception as error:
                self._logger.error(
                    f"获取插件[{plugin_id}]侧栏导航出错：{str(error)}"
                )
        items.sort(
            key=lambda item: (
                item["section"],
                item["order"],
                item["plugin_id"],
                item["nav_key"],
            )
        )
        return items

    def dashboard_metadata(self) -> List[Dict[str, str]]:
        """投影启用插件的单仪表板或多仪表板元信息。"""
        metadata = []
        for plugin_id, plugin in self._items(None):
            if not supports_plugin_hook(plugin, "get_dashboard"):
                continue
            try:
                if not plugin.get_state():
                    continue
                if supports_plugin_hook(plugin, "get_dashboard_meta"):
                    plugin_metadata = plugin.get_dashboard_meta()
                    if plugin_metadata:
                        metadata.extend({
                            "id": plugin_id,
                            "name": item.get("name"),
                            "key": item.get("key"),
                        } for item in plugin_metadata if item)
                else:
                    metadata.append({
                        "id": plugin_id,
                        "name": plugin.plugin_name,
                        "key": "",
                    })
            except Exception as error:
                self._logger.error(
                    f"获取插件[{plugin_id}]仪表盘元数据出错：{str(error)}"
                )
        return metadata

    def dashboard(
        self,
        plugin_id: str,
        key: str,
        user_agent: Optional[str] = None,
    ) -> Optional[PluginDashboard]:
        """调用插件仪表板钩子并返回稳定投影，不依赖 HTTP 异常。"""
        plugin = self._running_plugins.get(plugin_id)
        if not plugin:
            raise PluginNotFoundError(f"插件 {plugin_id} 不存在或未加载")
        try:
            render_mode, _ = plugin.get_render_mode()
            method = plugin.get_dashboard
            count = len(inspect.signature(method).parameters)
            if count > 1:
                dashboard = method(key=key, user_agent=user_agent)
            elif count > 0:
                dashboard = method(user_agent=user_agent)
            else:
                dashboard = method()
        except Exception as error:  # noqa: BLE001
            self._logger.error(f"插件 {plugin_id} 调用方法 get_dashboard 出错: {error}")
            raise PluginDashboardError(
                f"插件 {plugin_id} 调用方法 get_dashboard 出错: {error}"
            ) from error
        if dashboard is None:
            return None
        if not isinstance(dashboard, (tuple, list)) or len(dashboard) != 3:
            self._logger.error(f"插件 {plugin_id} 返回的仪表盘数据格式错误")
            raise PluginDashboardError(
                f"插件 {plugin_id} 返回的仪表盘数据格式错误"
            )
        cols, attrs, elements = dashboard
        return PluginDashboard(
            id=plugin_id,
            name=plugin.plugin_name,
            key=key,
            render_mode=render_mode,
            cols=cols or {},
            attrs=attrs or {},
            elements=elements,
            source_plugin_id=getattr(plugin, "plugin_source_id", None),
            is_instance=bool(getattr(plugin, "plugin_source_id", None)),
            instance_mode=(
                "virtual" if getattr(plugin, "plugin_source_id", None) else None
            ),
        )
