"""推理依赖工厂 — 组装一次推理所需的全套依赖。

将 TreeIndex 加载、TreeEnvironment 构建、SkillRegistry 发现、
SearchToolDispatcher 装配、PromptManager 初始化等步骤封装为
单一工厂函数 ``build_inference_deps``，返回不可变的 ``InferenceDeps``。

调用方（runner / inference）只需传入配置参数，无需了解内部装配逻辑。
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from loguru import logger

from app.search.prompt import PromptManager
from app.search.skills import discover_skills
from app.search.tools import SearchToolDispatcher
from app.tree.environment import TreeEnvironment
from app.tree.index import TreeIndex

if TYPE_CHECKING:
    from collections.abc import Callable
    from pathlib import Path

    from app.ports import EmbeddingProvider, OCRProvider
    from core.protocols import LLMProvider, VLMProvider
    from core.types import GeneratedQuestion


@dataclass(frozen=True)
class InferenceDeps:
    """跑一次推理所需的全套依赖（不含 HarnessLog，其生命周期由调用方管理）。

    属性:
        llm: LLM 端口实例。
        tool_dispatch_fn: SearchToolDispatcher.dispatch 的绑定方法。
        prompt_builder: (GeneratedQuestion) -> (system_prompt, user_prompt)。
    """

    llm: LLMProvider
    tool_dispatch_fn: Callable[..., Any]
    prompt_builder: Callable[[GeneratedQuestion], tuple[str, str]]


def build_inference_deps(
    *,
    store_dir: Path,
    video_id: str,
    prompts_dir: Path,
    skills_dir: Path | None,
    skill_mode: str,
    embed_provider: EmbeddingProvider,
    llm: LLMProvider,
    vlm: VLMProvider,
    ocr: OCRProvider | None,
    verify_vision: bool,
    anchor: bool,
    assemble_mode: str,
) -> InferenceDeps:
    """组装一次推理所需的全套依赖。

    参数:
        store_dir: store 根目录（包含 videos/{video_id}/tree.json）。
        video_id: 视频标识。
        prompts_dir: prompt 文件目录。
        skills_dir: skill 文件目录（None 则不加载 skill）。
        skill_mode: skill 模式（"auto"/"manual"/"none"）。
        embed_provider: 嵌入端口实例。
        llm: LLM 端口实例。
        vlm: VLM 端口实例。
        ocr: OCR 端口实例（None 不启用）。
        verify_vision: observe_frame 是否执行验证轮。
        anchor: view_node 是否启用行号锚模式。
        assemble_mode: 锚模式装配形态。

    返回:
        InferenceDeps 实例。

    异常:
        FileNotFoundError: tree.json 不存在。
    """
    # Phase 1: 加载 TreeIndex
    tree_path = store_dir / "videos" / video_id / "tree.json"
    if not tree_path.exists():
        raise FileNotFoundError(f"树索引文件不存在: {tree_path}")
    tree_index = TreeIndex.load_json(str(tree_path))
    logger.info("已加载 TreeIndex: video_id={}, L1 节点数={}", video_id, len(tree_index.roots))

    # Phase 2: 构建 TreeEnvironment
    frames_dir = store_dir / "videos" / video_id / "frames"
    env = TreeEnvironment(index=tree_index, frames_dir=frames_dir)

    # Phase 3: 构建 SkillRegistry
    skills = None
    always_skills_text = ""
    task_skill_map: dict[str, str] = {}
    catalog_text = ""
    if skills_dir is not None:
        always_skills_text, task_skill_map, catalog_text, skills = discover_skills(skills_dir)
        logger.info(
            "已发现 skills: always={} 字符, task_map={} 项",
            len(always_skills_text),
            len(task_skill_map),
        )

    # Phase 4: 构建 SearchToolDispatcher
    dispatcher = SearchToolDispatcher(
        env,
        tool_llm=llm,
        vlm=vlm,
        ocr=ocr,
        prompts_dir=prompts_dir,
        skills=skills,
        embed_fn=embed_provider.embed,
        verify_vision=verify_vision,
        anchor=anchor,
        assemble_mode=assemble_mode,
    )

    # Phase 5: 构建 PromptManager + _prompt_builder 闭包
    pm = PromptManager(prompts_dir)
    l1_ids = [root.id for root in tree_index.roots]

    def _prompt_builder(qa: GeneratedQuestion) -> tuple[str, str]:
        """为单条题目生成 (system_prompt, user_prompt)。

        参数:
            qa: 生成的题目实例。

        返回:
            (system_prompt, user_prompt) 二元组。
        """
        system = pm.build_inference_prompt(
            skill_mode,
            qa.task_type,
            always_skills_text,
            task_skill_map,
            catalog_text,
        )
        user = pm.format_user_prompt(
            qa.question,
            list(qa.options),
            l1_ids,
            qa.task_type,
        )
        return system, user

    logger.info("InferenceDeps 组装完成: video_id={}, skill_mode={}", video_id, skill_mode)
    return InferenceDeps(
        llm=llm,
        tool_dispatch_fn=dispatcher.dispatch,
        prompt_builder=_prompt_builder,
    )
