"""
OpenCode Style agent 的模型绑定与执行工具。

本模块从 `factory.py` 中拆分出“模型调用路径”的通用逻辑：
- 根据 response_format / 模型能力 / tools 决定如何 bind 模型（含 ProviderStrategy / ToolStrategy）
- 统一处理模型输出（messages + optional structured_response）
- 提供同步 / 异步模型执行的辅助函数，供 model 节点使用

注意：这里不包含图路由（edges）或 OpenCode 状态字段更新，只关注“模型 I/O 层”。
"""

from __future__ import annotations

from typing import Any, Dict, Tuple, List

from langchain.agents.structured_output import (
    AutoStrategy,
    ProviderStrategy,
    ProviderStrategyBinding,
    ResponseFormat,
    StructuredOutputError,
    StructuredOutputValidationError,
    ToolStrategy,
    OutputToolBinding,
    MultipleStructuredOutputsError,
)
from langchain_core.messages import AIMessage, ToolMessage
from langchain_core.tools import BaseTool
from langgraph.types import Command

from .schema_and_format import _supports_provider_strategy, _handle_structured_output_error


def handle_model_output(
    output: AIMessage,
    effective_response_format: ResponseFormat[Any] | None,
    structured_output_tools: Dict[str, OutputToolBinding[Any]],
) -> Dict[str, Any]:
    """处理模型输出（含结构化输出），返回 {'messages': [...], 'structured_response': ...}."""
    if isinstance(effective_response_format, ProviderStrategy):
        if not output.tool_calls:
            provider_strategy_binding = ProviderStrategyBinding.from_schema_spec(
                effective_response_format.schema_spec
            )
            try:
                structured_response = provider_strategy_binding.parse(output)
            except Exception as exc:
                schema_name = getattr(
                    effective_response_format.schema_spec.schema, "__name__", "response_format"
                )
                validation_error = StructuredOutputValidationError(schema_name, exc, output)
                raise validation_error from exc
            else:
                return {"messages": [output], "structured_response": structured_response}
        return {"messages": [output]}

    if (
        isinstance(effective_response_format, ToolStrategy)
        and isinstance(output, AIMessage)
        and output.tool_calls
    ):
        structured_tool_calls = [
            tc for tc in output.tool_calls if tc["name"] in structured_output_tools
        ]

        if structured_tool_calls:
            exception: StructuredOutputError | None = None
            if len(structured_tool_calls) > 1:
                tool_names = [tc["name"] for tc in structured_tool_calls]
                exception = MultipleStructuredOutputsError(tool_names, output)
                should_retry, error_message = _handle_structured_output_error(
                    exception, effective_response_format
                )
                if not should_retry:
                    raise exception

                tool_messages = [
                    ToolMessage(
                        content=error_message,
                        tool_call_id=tc["id"],
                        name=tc["name"],
                    )
                    for tc in structured_tool_calls
                ]
                return {"messages": [output, *tool_messages]}

            tool_call = structured_tool_calls[0]
            try:
                structured_tool_binding = structured_output_tools[tool_call["name"]]
                structured_response = structured_tool_binding.parse(tool_call["args"])

                tool_message_content = (
                    effective_response_format.tool_message_content
                    or f"Returning structured response: {structured_response}"
                )

                return {
                    "messages": [
                        output,
                        ToolMessage(
                            content=tool_message_content,
                            tool_call_id=tool_call["id"],
                            name=tool_call["name"],
                        ),
                    ],
                    "structured_response": structured_response,
                }
            except Exception as exc:
                exception = StructuredOutputValidationError(tool_call["name"], exc, output)
                should_retry, error_message = _handle_structured_output_error(
                    exception, effective_response_format
                )
                if not should_retry:
                    raise exception from exc

                return {
                    "messages": [
                        output,
                        ToolMessage(
                            content=error_message,
                            tool_call_id=tool_call["id"],
                            name=tool_call["name"],
                        ),
                    ],
                }

    return {"messages": [output]}


def get_bound_model(
    *,
    request,
    structured_output_tools: Dict[str, OutputToolBinding[Any]],
    initial_response_format: ResponseFormat[Any] | None,
    tool_strategy_for_setup,
    tool_node,
    wrap_tool_call_wrapper,
    awrap_tool_call_wrapper,
    dynamic_tool_error_template: str,
) -> Tuple[Any, ResponseFormat[Any] | None]:
    """根据 request / response_format / tools 返回 (bound_model, effective_response_format)."""
    has_wrap_tool_call = wrap_tool_call_wrapper or awrap_tool_call_wrapper

    available_tools_by_name: Dict[str, Any] = {}
    if tool_node:
        available_tools_by_name = tool_node.tools_by_name.copy()

    if not has_wrap_tool_call:
        unknown_tool_names: List[str] = []
        for t in request.tools:
            if isinstance(t, dict):
                continue
            if isinstance(t, BaseTool) and t.name not in available_tools_by_name:
                unknown_tool_names.append(t.name)

        if unknown_tool_names:
            available_tool_names = sorted(available_tools_by_name.keys())
            msg = dynamic_tool_error_template.format(
                unknown_tool_names=unknown_tool_names,
                available_tool_names=available_tool_names,
            )
            raise ValueError(msg)

    response_format: ResponseFormat[Any] | Any | None = request.response_format
    if response_format is not None and not isinstance(
        response_format, (AutoStrategy, ToolStrategy, ProviderStrategy)
    ):
        response_format = AutoStrategy(schema=response_format)

    if isinstance(response_format, AutoStrategy):
        if _supports_provider_strategy(request.model, tools=request.tools):
            effective_response_format: ResponseFormat[Any] | None = ProviderStrategy(
                schema=response_format.schema
            )
        elif response_format is initial_response_format and tool_strategy_for_setup is not None:
            effective_response_format = tool_strategy_for_setup
        else:
            effective_response_format = ToolStrategy(schema=response_format.schema)
    else:
        effective_response_format = response_format

    final_tools: List[Any] = list(request.tools)
    if isinstance(effective_response_format, ToolStrategy):
        structured_tools = [info.tool for info in structured_output_tools.values()]
        final_tools.extend(structured_tools)

    if isinstance(effective_response_format, ProviderStrategy):
        kwargs = effective_response_format.to_model_kwargs()
        return (
            request.model.bind_tools(
                final_tools, strict=True, **kwargs, **request.model_settings
            ),
            effective_response_format,
        )

    if isinstance(effective_response_format, ToolStrategy):
        for tc in effective_response_format.schema_specs:
            if tc.name not in structured_output_tools:
                msg = (
                    f"ToolStrategy specifies tool '{tc.name}' "
                    "which wasn't declared in the original "
                    "response format when creating the agent."
                )
                raise ValueError(msg)

        tool_choice = "any" if structured_output_tools else request.tool_choice
        return (
            request.model.bind_tools(
                final_tools, tool_choice=tool_choice, **request.model_settings
            ),
            effective_response_format,
        )

    if final_tools:
        return (
            request.model.bind_tools(
                final_tools, tool_choice=request.tool_choice, **request.model_settings
            ),
            None,
        )
    return request.model.bind(**request.model_settings), None


__all__ = [
    "handle_model_output",
    "get_bound_model",
]

