"""运行时插件注册表：聚合 prompt、OpenAI 工具片段、handlers；支持热加载。"""

from __future__ import annotations

import json
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable

import yaml

from aicd.config import AppConfig
from aicd.plugins.loader import (
    PluginLoadOutcome,
    builtin_tool_names,
    iter_plugin_dirs,
    load_one_plugin,
)
from aicd.plugins.plugin_log import write_plugin_log

Handler = Callable[[Any, dict[str, Any]], Any]


@dataclass
class PluginRegistry:
    """绑定 HOME layout 与 AppConfig，聚合已加载插件。"""

    layout: dict[str, Path]
    app: AppConfig
    _prompt_block: str = ""
    _openai_extra: list[dict[str, Any]] | None = None
    _handlers: dict[str, Handler] | None = None
    _outcomes: list[PluginLoadOutcome] | None = None
    _load_report: dict[str, Any] | None = None

    @property
    def plugins_root(self) -> Path:
        return self.layout["plugins"]

    @property
    def backups_dir(self) -> Path:
        return self.layout["plugins_backups"]

    @property
    def state_dir(self) -> Path:
        return self.layout["plugins_state"]

    def load_all(self, *, plugin_ids: frozenset[str] | None = None) -> None:
        """扫描 plugins/ 并加载；写入 .state/load_report.json。

        ``plugin_ids`` 为 ``None`` 时加载所有候选目录（仍受 enabled / 校验约束）；
        为空集时不加载任何插件（仅写报告）；非空时只尝试列出的 id。
        """
        self.state_dir.mkdir(parents=True, exist_ok=True)
        roots = iter_plugin_dirs(self.plugins_root)
        if plugin_ids is not None:
            if not plugin_ids:
                roots = []
            else:
                roots = [r for r in roots if r.name in plugin_ids]
        max_n = self.app.plugins.max_plugins
        if len(roots) > max_n:
            roots = roots[:max_n]
        builtins = builtin_tool_names()
        outcomes: list[PluginLoadOutcome] = []
        report_plugins: list[dict[str, Any]] = []

        for root in roots:
            o = load_one_plugin(root, builtins=builtins, app=self.app)
            outcomes.append(o)
            rp: dict[str, Any] = {
                "id": o.plugin_id,
                "ok": o.ok,
                "errors": list(o.errors),
                "warnings": list(o.warnings),
            }
            if o.manifest:
                rp["enabled"] = o.manifest.enabled
                rp["version"] = o.manifest.version
                rp["name"] = o.manifest.name
                rp["display_name"] = o.manifest.display_name or o.manifest.name
                rp["description"] = o.manifest.description
            if o.errors:
                pr = self.plugins_root / o.plugin_id
                write_plugin_log(
                    pr,
                    level="ERROR",
                    source="loader",
                    message="load_failed",
                    detail="; ".join(o.errors)[:6000],
                    retention_days=self.app.plugins.log_retention_days,
                )
            report_plugins.append(rp)

        self._rebuild_from_outcomes(outcomes, builtins)
        meta: dict[str, Any] = {
            "loaded_at": datetime.now(timezone.utc).isoformat(),
            "plugins": report_plugins,
        }
        if plugin_ids is not None:
            meta["plugin_ids_filter"] = (
                "none" if not plugin_ids else sorted(plugin_ids)
            )
        self._load_report = meta
        report_path = self.state_dir / "load_report.json"
        report_path.write_text(
            json.dumps(self._load_report, ensure_ascii=False, indent=2) + "\n",
            encoding="utf-8",
            newline="\n",
        )

    def _rebuild_from_outcomes(
        self, outcomes: list[PluginLoadOutcome], builtins: frozenset[str]
    ) -> None:
        prompt_chunks: list[str] = []
        frags: list[dict[str, Any]] = []
        handlers: dict[str, Handler] = {}
        used_tool_names: set[str] = set(builtins)

        for o in outcomes:
            if not o.ok or o.errors:
                continue
            if o.manifest and not o.manifest.enabled:
                continue
            for part in o.prompt_parts:
                prompt_chunks.append(part)
            for frag in o.openai_fragments:
                n = frag["function"]["name"]
                if n in used_tool_names:
                    continue
                used_tool_names.add(n)
                frags.append(frag)
            for hname, fn in o.handlers.items():
                if hname in handlers:
                    continue
                handlers[hname] = fn

        self._outcomes = outcomes
        if prompt_chunks:
            self._prompt_block = (
                "\n\n## HOME plugins\n\n" + "\n\n".join(prompt_chunks) + "\n"
            )
        else:
            self._prompt_block = ""
        self._openai_extra = frags
        self._handlers = handlers

    def reload(self) -> None:
        self.load_all()

    @property
    def prompt_block(self) -> str:
        return self._prompt_block or ""

    def openai_extra_tools(self) -> list[dict[str, Any]]:
        return list(self._openai_extra or [])

    def plugin_handlers(self) -> dict[str, Handler]:
        return dict(self._handlers or {})

    def summarize(self) -> list[dict[str, Any]]:
        out: list[dict[str, Any]] = []
        for o in self._outcomes or []:
            tool_names: list[str] = []
            for frag in o.openai_fragments:
                fn = frag.get("function") if isinstance(frag, dict) else None
                if isinstance(fn, dict) and fn.get("name"):
                    tool_names.append(str(fn["name"]))
            row: dict[str, Any] = {
                "id": o.plugin_id,
                "ok": o.ok,
                "errors": o.errors,
                "warnings": o.warnings,
                "tools_n": len(o.openai_fragments),
                "plugin_tools": tool_names,
            }
            if o.manifest:
                m = o.manifest
                row["enabled"] = m.enabled
                row["version"] = m.version
                row["name"] = m.name
                row["display_name"] = m.display_name or m.name
                row["description"] = m.description
                row["min_aicd_version"] = m.min_aicd_version
                row["schema_version"] = m.schema_version
                row["config_keys"] = sorted(m.config.keys())[:40]
            else:
                row["display_name"] = o.plugin_id
                row["description"] = ""
            row["log_dir"] = str((self.plugins_root / o.plugin_id / "logs").resolve())
            out.append(row)
        return out

    def set_enabled(self, plugin_id: str, enabled: bool) -> tuple[bool, str]:
        root = self.plugins_root / plugin_id
        mf = root / "plugin.yaml"
        if not mf.is_file():
            return False, "plugin or plugin.yaml not found"
        raw = yaml.safe_load(mf.read_text(encoding="utf-8"))
        if not isinstance(raw, dict):
            return False, "invalid plugin.yaml"
        raw["enabled"] = bool(enabled)
        mf.write_text(
            yaml.safe_dump(raw, allow_unicode=True, sort_keys=False),
            encoding="utf-8",
            newline="\n",
        )
        self.reload()
        return True, f"plugin {plugin_id} enabled={enabled}"
