"""Task-driven tool assembler.

这层和 SkillAssembler 的位置类似：它不执行工具，只决定本轮 run 应该把哪些
tool schema 暴露给模型。
"""

from __future__ import annotations

from collections.abc import Sequence
from typing import TYPE_CHECKING

from beaver.engine.context import SkillContext
from beaver.foundation.embedding import EmbeddingRetriever
from beaver.tools.base import ToolSpec
from beaver.tools.registry import ToolRegistry

if TYPE_CHECKING:
    from beaver.engine.providers.runtime import ProviderRuntime
    from beaver.skills.catalog.loader import SkillsLoader


class ToolAssembler:
    """Use skill hints and embedding retrieval to select run-scoped tools."""

    def __init__(
        self,
        *,
        retriever: EmbeddingRetriever | None = None,
        always_tool_names: Sequence[str] | None = None,
    ) -> None:
        self.retriever = retriever or EmbeddingRetriever()
        self.always_tool_names = tuple(always_tool_names or ("memory", "session_search"))

    async def assemble(
        self,
        *,
        task_description: str,
        registry: ToolRegistry,
        skills_loader: SkillsLoader | None = None,
        activated_skills: Sequence[SkillContext] | None = None,
        embedding_runtime: ProviderRuntime | None = None,
        top_k: int = 10,
    ) -> list[ToolSpec]:
        """Return selected tool specs for the current run.

        Selection order is intentionally deterministic:
        1. always tools from config/spec
        2. tools explicitly declared by activated skills
        3. embedding top-k tools for the task
        """

        selected: list[ToolSpec] = []
        selected_names: set[str] = set()

        def add_specs(specs: Sequence[ToolSpec]) -> None:
            for spec in specs:
                if spec.name in selected_names:
                    continue
                selected.append(spec)
                selected_names.add(spec.name)

        add_specs(registry.list_always_specs())
        add_specs(registry.get_specs(self.always_tool_names))

        skill_tool_names = self._collect_skill_tool_names(
            skills_loader=skills_loader,
            activated_skills=activated_skills or (),
        )
        add_specs(registry.get_specs(skill_tool_names))

        candidates = [
            spec.to_embedding_candidate()
            for spec in registry.list_specs()
            if spec.name not in selected_names
        ]
        retrieved = await self.retriever.retrieve(
            query=task_description,
            candidates=candidates,
            top_k=top_k,
            api_key=embedding_runtime.api_key if embedding_runtime is not None else None,
            api_base=embedding_runtime.api_base if embedding_runtime is not None else None,
            model=embedding_runtime.model if embedding_runtime is not None else None,
            extra_headers=embedding_runtime.extra_headers if embedding_runtime is not None else None,
            timeout_seconds=(
                embedding_runtime.request_timeout_seconds if embedding_runtime is not None else None
            ),
            fallback_top_k=top_k,
        )
        add_specs(registry.get_specs([item["name"] for item in retrieved]))
        return selected

    @staticmethod
    def _collect_skill_tool_names(
        *,
        skills_loader: SkillsLoader | None,
        activated_skills: Sequence[SkillContext],
    ) -> list[str]:
        if skills_loader is None or not activated_skills:
            return []

        result: list[str] = []
        for skill in activated_skills:
            names = list(skill.tool_hints) if getattr(skill, "tool_hints", None) else skills_loader.get_skill_tool_hints(skill.name)
            for name in names:
                if name not in result:
                    result.append(name)
        return result
