"""真实 Claude Messages API 示例。需要 ANTHROPIC_API_KEY 与 TOOL_MODEL。
运行会产生网络请求与模型费用；本次编写未调用。工具仅访问教学只读数据。
"""
import asyncio
import json
import os
import time
from typing import Awaitable, Callable
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from tool_contracts import DEMO_USER, TOOL_DEFINITIONS, ToolFailure, dispatch

Executor = Callable[[str, dict], Awaitable[tuple[dict, bool]]]


def request_model(body: dict, timeout: float) -> dict:
    key = os.environ["ANTHROPIC_API_KEY"]
    request = Request("https://api.anthropic.com/v1/messages",
                      data=json.dumps(body).encode("utf-8"), method="POST",
                      headers={"x-api-key": key, "anthropic-version": "2023-06-01",
                               "content-type": "application/json"})
    try:
        with urlopen(request, timeout=timeout) as response:
            raw = response.read(2_000_001)
            if len(raw) > 2_000_000:
                raise RuntimeError("模型响应超过演示大小限制")
            return json.loads(raw)
    except HTTPError as error:
        # 不把响应全文、请求头或凭据输出到日志。
        raise RuntimeError(f"模型请求失败，HTTP {error.code}") from None


async def local_executor(name: str, arguments: dict) -> tuple[dict, bool]:
    try:
        return dispatch(name, arguments, DEMO_USER), False
    except ToolFailure as error:
        return {"error": str(error)}, True


async def run_loop(tools: list[dict] = TOOL_DEFINITIONS,
                   executor: Executor = local_executor) -> str:
    model = os.environ["TOOL_MODEL"]  # 使用调用者有权限的型号，不猜测或硬编码最新型号。
    messages = [{"role": "user", "content": "A1042 签收十天后报告质量问题，退货运费谁承担？"}]
    allowed = {tool["name"] for tool in tools}
    seen, total_calls = set(), 0
    deadline = time.monotonic() + 120
    for _ in range(6):
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            raise RuntimeError("task_deadline")
        body = {"model": model, "max_tokens": 1200, "tools": tools,
                "system": "仅咨询，不提交操作。依据工具结果解释条件；质量待核实不等于已经确认。",
                "messages": messages}
        # 非流式最小客户端。线程超时不保证远端取消，因此只连接只读演示工具。
        response = await asyncio.wait_for(
            asyncio.to_thread(request_model, body, min(remaining, 30)), timeout=remaining)
        content = response["content"]
        calls = [block for block in content if block["type"] == "tool_use"]
        stop = response.get("stop_reason")
        if stop == "end_turn" and not calls:
            answer = "\n".join(block["text"] for block in content if block["type"] == "text")
            if not answer.strip():
                raise RuntimeError("empty_final_answer")
            return answer
        if stop != "tool_use" or not calls:
            # max_tokens、拒绝或其他停止情况不能冒充完整工具意图。
            raise RuntimeError(f"unexpected_stop_reason: {stop}")
        if total_calls + len(calls) > 8:
            raise RuntimeError("tool_limit")
        messages.append({"role": "assistant", "content": content})
        results = []
        for call in calls:
            if call["id"] in seen:
                raise RuntimeError("duplicate_tool_call_id")
            seen.add(call["id"])
            total_calls += 1
            if call["name"] not in allowed:
                data, is_error = {"error": "unknown_tool"}, True
            else:
                remaining = deadline - time.monotonic()
                if remaining <= 0:
                    raise RuntimeError("task_deadline")
                data, is_error = await asyncio.wait_for(
                    executor(call["name"], call["input"]), timeout=remaining)
            # 示例采用字符上限控制结果大小；这不是真实 Token 计量。
            serialized = json.dumps(data, ensure_ascii=False)
            if len(serialized) > 50_000:
                # 不静默截断条件或来源，明确终止，让上层采用外置或分步方案。
                raise RuntimeError("tool_result_too_large")
            # Claude 工具结果用 user 内容块回填，并匹配原始 tool_use ID。
            results.append({"type": "tool_result", "tool_use_id": call["id"],
                            "is_error": is_error, "content": serialized})
        messages.append({"role": "user", "content": results})
    raise RuntimeError("iteration_limit")


if __name__ == "__main__":
    print(asyncio.run(run_loop()))
