"""
核心 Agent - 透過 OpenAI Chat Completions API 格式與 LiteLLM Proxy 通訊

透過 FWAuto Server 的 /api/v1/chat/completions 端點，
經 LiteLLM 路由支援 Claude / Gemini / OpenAI 等多模型。
支援 Agentic Loop（工具呼叫循環）。
"""

import asyncio
import importlib.metadata
import logging
import os
import platform
import time
import uuid
from collections.abc import AsyncIterator
from dataclasses import dataclass, field

import httpx

from .tools import TOOL_DEFINITIONS, ToolExecutor
from .types import (
    AgentMessage,
    AssistantMessage,
    ChatCompletionResponse,
)

# ============================================================================
# 預設配置
# ============================================================================

DEFAULT_MODEL = ""
DEFAULT_PROXY_URL = "https://fwauto-server-189969833984.asia-east1.run.app/api"
_PROCESS_SESSION_ID = str(uuid.uuid4())

try:
    _CLIENT_VERSION = importlib.metadata.version("fwauto")
except importlib.metadata.PackageNotFoundError:
    _CLIENT_VERSION = "dev"
_PYTHON_VERSION = platform.python_version()
_CLIENT_OS = platform.system()


def _build_api_headers(api_key: str, options: "ClaudeAgentOptions") -> dict[str, str]:
    """Build HTTP headers for LLM API requests with client metadata."""
    return {
        "x-fwauto-token": api_key,
        "Content-Type": "application/json",
        "X-FWAuto-Version": _CLIENT_VERSION,
        "X-FWAuto-Python-Version": _PYTHON_VERSION,
        "X-FWAuto-OS": _CLIENT_OS,
        "X-FWAuto-Source": options.source,
        "X-FWAuto-Session-ID": options.session_id,
    }


def _build_request_ai_data(options: "ClaudeAgentOptions", iteration: int, messages: list[dict]) -> dict:
    """Build ai_data dict for REQUEST log."""
    ai_data: dict = {
        "type": "REQUEST",
        "session_id": options.session_id,
        "source": options.source,
        "iteration": iteration,
        "model": options.model,
        "max_tokens": options.max_tokens,
    }
    if iteration == 1:
        for msg in reversed(messages):
            if msg.get("role") == "user":
                content = msg.get("content", "")
                ai_data["user_input"] = content[:500] if isinstance(content, str) else str(content)[:500]
                break
    else:
        tool_results = []
        for msg in messages:
            if msg.get("role") == "tool":
                tool_results.append(
                    {
                        "tool_call_id": msg.get("tool_call_id", ""),
                        "name": msg.get("name", ""),
                        "content_length": len(msg.get("content", "")),
                    }
                )
        if tool_results:
            ai_data["tool_results"] = tool_results[-5:]
    return ai_data


def _build_response_ai_data(
    options: "ClaudeAgentOptions",
    iteration: int,
    response: "ChatCompletionResponse",
    duration_ms: int,
    status_code: int,
) -> dict:
    """Build ai_data dict for successful RESPONSE log."""
    ai_data: dict = {
        "type": "RESPONSE",
        "session_id": options.session_id,
        "source": options.source,
        "iteration": iteration,
        "status": "success",
        "status_code": status_code,
        "model": response.model or options.model,
        "duration_ms": duration_ms,
    }
    if response.usage:
        ai_data["prompt_tokens"] = response.usage.prompt_tokens
        ai_data["completion_tokens"] = response.usage.completion_tokens
        ai_data["total_tokens"] = response.usage.total_tokens
    local_tool_calls = response.get_local_tool_calls()
    ai_data["tool_calls"] = _summarize_tool_calls(local_tool_calls) if local_tool_calls else []
    return ai_data


# Keys worth capturing per tool for token tracking
_TOOL_KEY_ARGS: dict[str, list[str]] = {
    "Read": ["file_path"],
    "Edit": ["file_path"],
    "Write": ["file_path"],
    "Grep": ["pattern", "path", "glob"],
    "Glob": ["pattern", "path"],
    "Bash": ["command"],
    "WebFetch": ["url"],
}


def _summarize_tool_calls(tool_calls: list) -> list[dict[str, str]]:
    """Extract tool name + key arguments for logging."""
    summaries = []
    for tc in tool_calls:
        entry: dict[str, str] = {"name": tc.function.name}
        keys = _TOOL_KEY_ARGS.get(tc.function.name)
        if keys:
            try:
                args = tc.function.get_parsed_arguments()
                for k in keys:
                    if k in args:
                        val = str(args[k])
                        if len(val) > 200:
                            val = val[:200] + "..."
                        entry[k] = val
            except Exception:
                pass
        summaries.append(entry)
    return summaries


def _build_error_ai_data(
    options: "ClaudeAgentOptions",
    iteration: int,
    status_code: int,
    error_text: str,
    duration_ms: int,
) -> dict:
    """Build ai_data dict for error RESPONSE log."""
    return {
        "type": "RESPONSE",
        "session_id": options.session_id,
        "source": options.source,
        "iteration": iteration,
        "status": "error",
        "status_code": status_code,
        "error": f"API error ({status_code}): {error_text[:500]}",
        "duration_ms": duration_ms,
    }


def _get_ai_logger() -> logging.Logger:
    """Get the AI API logger."""
    return logging.getLogger("fwauto.ai")


# ============================================================================
# ClaudeAgentOptions - 與 claude_agent_sdk 相容
# ============================================================================


@dataclass
class ClaudeAgentOptions:
    """
    Agent 設定（相容 claude_agent_sdk.ClaudeAgentOptions）

    用法：
        options = ClaudeAgentOptions(
            cwd="/path/to/project",
            permission_mode="bypassPermissions",
        )

    多輪對話用法：
        # 第一輪
        agent = Agent(options)
        async for msg in agent.run("你好"):
            print(msg)

        # 第二輪（傳入歷史）
        options.messages = agent.get_messages()
        async for msg in agent.run("繼續剛才的話題"):
            print(msg)
    """

    # 工具與權限
    allowed_tools: list[str] = field(
        default_factory=lambda: ["Read", "Write", "Edit", "MultiEdit", "Glob", "Grep", "Bash", "WebFetch"]
    )
    permission_mode: str = "default"  # "default" | "bypassPermissions"
    add_dirs: list[str] = field(default_factory=list)
    cwd: str | None = None
    env: dict[str, str] | None = None

    # 對話上下文
    messages: list[dict] | None = None  # 傳入歷史對話以延續多輪對話

    # 模型與 API
    model: str = DEFAULT_MODEL
    api_key: str | None = None
    base_url: str | None = None
    system_prompt: str | None = None
    max_iterations: int = 20
    timeout: int = 120
    max_tokens: int = 4096
    temperature: float | None = None

    # Web Search（透過 web_search_options 參數，不走 tools）
    web_search: bool = False
    search_context_size: str = "medium"  # "low" / "medium" / "high"

    # AI API logging
    source: str = "unknown"
    session_id: str = field(default_factory=lambda: _PROCESS_SESSION_ID)

    def get_api_key(self) -> str:
        """
        取得 FWAuto Token

        優先順序：
        1. 直接設定的 api_key
        2. env 中的 FWAUTO_TOKEN
        3. 環境變數 FWAUTO_TOKEN
        4. env 中的 ANTHROPIC_API_KEY（舊版 fallback）
        5. 環境變數 ANTHROPIC_API_KEY（舊版 fallback）
        """
        if self.api_key:
            return self.api_key
        if self.env and "FWAUTO_TOKEN" in self.env:
            return self.env["FWAUTO_TOKEN"]
        token = os.environ.get("FWAUTO_TOKEN")
        if token:
            return token
        # 舊版 fallback
        if self.env and "ANTHROPIC_API_KEY" in self.env:
            return self.env["ANTHROPIC_API_KEY"]
        return os.environ.get("ANTHROPIC_API_KEY", "")

    def get_base_url(self) -> str:
        """
        取得 API Base URL

        優先順序：
        1. 直接設定的 base_url
        2. env 中的 FWAUTO_BASE_URL
        3. 環境變數 FWAUTO_BASE_URL
        4. env 中的 ANTHROPIC_BASE_URL（舊版 fallback）
        5. 環境變數 ANTHROPIC_BASE_URL（舊版 fallback）
        6. DEFAULT_PROXY_URL
        """
        if self.base_url:
            return self.base_url
        if self.env and "FWAUTO_BASE_URL" in self.env:
            return self.env["FWAUTO_BASE_URL"]
        url = os.environ.get("FWAUTO_BASE_URL")
        if url:
            return url
        # 舊版 fallback
        if self.env and "ANTHROPIC_BASE_URL" in self.env:
            return self.env["ANTHROPIC_BASE_URL"]
        return os.environ.get("ANTHROPIC_BASE_URL") or DEFAULT_PROXY_URL


# ============================================================================
# Agent 核心
# ============================================================================


class Agent:
    """
    核心 Agent

    透過 OpenAI Chat Completions API 格式與 LiteLLM Proxy 通訊，
    支援 Agentic Loop（工具呼叫循環）和多輪對話。

    多輪對話用法：
        agent = Agent(options)

        # 第一輪對話
        async for msg in agent.run("讀取 README.md"):
            print(msg)

        # 第二輪對話（自動保留上下文）
        async for msg in agent.run("幫我修改剛才那個檔案"):
            print(msg)

        # 取得完整對話歷史
        history = agent.get_messages()
    """

    def __init__(self, options: ClaudeAgentOptions):
        self.options = options
        self.project_root = options.cwd or "."
        self.tool_executor = ToolExecutor(
            project_root=self.project_root,
            allowed_dirs=options.add_dirs or [self.project_root],
        )
        self._iteration = 0
        # 對話歷史：如果 options 有傳入就用，否則建立新的
        self._messages: list[dict] = list(options.messages) if options.messages else []

    def _get_system_prompt(self) -> str:
        """取得系統提示詞"""
        if self.options.system_prompt:
            return self.options.system_prompt

        return f"""你是一個程式開發助手。
你可以讀取、寫入、編輯程式碼檔案，也可以執行 shell 命令。
專案目錄: {self.project_root}
請用繁體中文回答。

當你需要操作檔案時，請使用提供的工具。
在編輯檔案前，請先讀取檔案內容以了解上下文。
執行命令時請注意安全，避免執行危險操作。"""

    def _get_tools(self) -> list[dict] | None:
        """
        取得啟用的工具定義（OpenAI Function Calling 格式）

        WebSearch 不再作為 tool，改用 request body 的 web_search_options 參數
        """
        allowed = set(self.options.allowed_tools)
        tools = []

        for t in TOOL_DEFINITIONS:
            if t["name"] in allowed:
                tools.append(
                    {
                        "type": "function",
                        "function": {
                            "name": t["name"],
                            "description": t["description"],
                            "parameters": t["input_schema"],
                        },
                    }
                )

        return tools if tools else None

    def _build_messages(self, messages: list[dict]) -> list[dict]:
        """
        在 messages 最前面插入 system message

        OpenAI 格式把 system prompt 放在 messages 陣列第一個，
        而非 Anthropic 的頂層 system 參數。
        """
        system_msg = {"role": "system", "content": self._get_system_prompt()}
        return [system_msg] + messages

    async def run(self, prompt: str) -> AsyncIterator[AgentMessage]:
        """
        執行 Agent

        支援多輪對話：同一個 Agent 實例可以多次呼叫 run()，
        會自動保留對話歷史。

        Args:
            prompt: 使用者輸入

        Yields:
            AgentMessage 物件
        """
        self._iteration = 0
        self._messages.append({"role": "user", "content": prompt})
        messages = self._messages

        while self._iteration < self.options.max_iterations:
            self._iteration += 1
            # 只在第一輪帶 web_search，後續工具回合不帶
            is_first_round = self._iteration == 1

            try:
                response = await self._call_api(messages, include_web_search=is_first_round)
            except Exception as e:
                yield AgentMessage(type="error", content=str(e))
                return

            # 輸出文字內容
            text_content = response.get_text()
            if text_content:
                yield AgentMessage(type="text", content=text_content)

            # 取得需要本地執行的工具呼叫（過濾掉 web_search）
            local_tool_calls = response.get_local_tool_calls()

            if not local_tool_calls:
                # 沒有工具呼叫，加入 assistant 訊息並完成
                messages.append(response.to_assistant_message_dict())
                return

            # 有工具呼叫，加入 assistant 訊息（含 tool_calls）
            messages.append(response.to_assistant_message_dict())

            # 逐一執行工具並加入 tool result messages
            for tool_call in local_tool_calls:
                name = tool_call.function.name
                args = tool_call.function.get_parsed_arguments()

                yield AgentMessage(
                    type="tool_use",
                    tool_name=name,
                    tool_input=args,
                )

                result = await self.tool_executor.execute(name, args)

                yield AgentMessage(
                    type="tool_result",
                    tool_name=name,
                    tool_result=result,
                )

                # OpenAI 格式：每個 tool result 是獨立的 message
                messages.append(
                    {
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "name": name,
                        "content": result,
                    }
                )

        yield AgentMessage(type="error", content=f"達到迭代上限 ({self.options.max_iterations})")

    async def _call_api(self, messages: list[dict], include_web_search: bool = True) -> ChatCompletionResponse:
        """呼叫 OpenAI Chat Completions API

        Args:
            messages: 對話訊息
            include_web_search: 是否帶 web_search_options（只在第一輪帶，後續工具回合不帶）
        """
        ai_logger = _get_ai_logger()

        api_key = self.options.get_api_key()
        base_url = self.options.get_base_url()

        if not api_key:
            raise RuntimeError("未設定 API Token (FWAUTO_TOKEN 或 ANTHROPIC_API_KEY)")

        api_url = base_url.rstrip("/") + "/v1/chat/completions"

        # 組裝 request body（OpenAI 格式）
        request_body: dict = {
            "model": self.options.model,
            "messages": self._build_messages(messages),
            "max_tokens": self.options.max_tokens,
        }

        # 工具定義（有工具才傳）
        tools = self._get_tools()
        if tools:
            request_body["tools"] = tools

        # 可選參數
        if self.options.temperature is not None:
            request_body["temperature"] = self.options.temperature

        # Web Search — 只在第一輪帶，後續工具回合不帶（避免非 Claude 模型每輪多一次搜尋）
        if self.options.web_search and include_web_search:
            request_body["web_search_options"] = {
                "search_context_size": self.options.search_context_size,
            }

        headers = _build_api_headers(api_key, self.options)

        # Log REQUEST
        if ai_logger.handlers:
            req_data = _build_request_ai_data(self.options, self._iteration, messages)
            if ai_logger.isEnabledFor(logging.DEBUG):
                req_data["headers"] = {k: v for k, v in headers.items() if k != "x-fwauto-token"}
                req_data["body"] = request_body
            ai_logger.info("", extra={"ai_data": req_data})

        start_time = time.monotonic()

        async with httpx.AsyncClient(timeout=self.options.timeout) as client:
            response = await client.post(api_url, headers=headers, json=request_body)

            duration_ms = int((time.monotonic() - start_time) * 1000)

            if response.status_code != 200:
                error_text = response.text
                # Log error RESPONSE
                if ai_logger.handlers:
                    ai_logger.log(
                        logging.WARNING if response.status_code == 429 else logging.ERROR,
                        "",
                        extra={
                            "ai_data": _build_error_ai_data(
                                self.options,
                                self._iteration,
                                response.status_code,
                                error_text,
                                duration_ms,
                            )
                        },
                    )
                raise RuntimeError(f"API error ({response.status_code}): {error_text}")

            data = response.json()
            result = ChatCompletionResponse.from_dict(data)

            # Log success RESPONSE
            if ai_logger.handlers:
                resp_data = _build_response_ai_data(
                    self.options,
                    self._iteration,
                    result,
                    duration_ms,
                    response.status_code,
                )
                if ai_logger.isEnabledFor(logging.DEBUG):
                    resp_data["body"] = data
                ai_logger.info("", extra={"ai_data": resp_data})

            return result

    def get_messages(self) -> list[dict]:
        """
        取得完整對話歷史

        Returns:
            對話訊息列表
        """
        return list(self._messages)

    def clear_messages(self):
        """清除對話歷史"""
        self._messages.clear()

    @property
    def message_count(self) -> int:
        """取得目前對話訊息數量"""
        return len(self._messages)

    @property
    def turn_count(self) -> int:
        """取得目前對話輪數（使用者訊息數量）"""
        return len([m for m in self._messages if m.get("role") == "user"])


# ============================================================================
# query() 函數 - 與 claude_agent_sdk 相容
# ============================================================================


async def query(
    *,
    prompt: str,
    options: ClaudeAgentOptions | None = None,
) -> AsyncIterator[AgentMessage]:
    """
    與 claude_agent_sdk.query 相容的介面

    用法：
        async for message in query(prompt="修復 bug", options=options):
            print(message)

    Args:
        prompt: 使用者輸入
        options: 設定選項

    Yields:
        AgentMessage 物件
    """
    opts = options or ClaudeAgentOptions()
    agent = Agent(opts)

    async for message in agent.run(prompt):
        yield message


def run_query(
    *,
    prompt: str,
    options: ClaudeAgentOptions | None = None,
) -> list[str]:
    """
    同步版本的 query

    Args:
        prompt: 使用者輸入
        options: 設定選項

    Returns:
        回應字串列表
    """

    async def _run():
        results = []
        async for msg in query(prompt=prompt, options=options):
            results.append(str(msg))
        return results

    return asyncio.run(_run())


# ============================================================================
# 便利函數
# ============================================================================


def create_options(
    project_dir: str,
    api_key: str | None = None,
    base_url: str | None = None,
    model: str = DEFAULT_MODEL,
    bypass_permissions: bool = True,
) -> ClaudeAgentOptions:
    """
    建立選項的便利函數

    Args:
        project_dir: 專案目錄
        api_key: API Key
        base_url: Proxy URL
        model: 模型名稱
        bypass_permissions: 是否跳過權限確認

    Returns:
        ClaudeAgentOptions 物件
    """
    return ClaudeAgentOptions(
        cwd=project_dir,
        api_key=api_key,
        base_url=base_url,
        model=model,
        permission_mode="bypassPermissions" if bypass_permissions else "default",
    )


# ============================================================================
# ClaudeSDKClient - 與 claude_agent_sdk.ClaudeSDKClient 相容
# ============================================================================


class ClaudeSDKClient:
    """
    與 claude_agent_sdk.ClaudeSDKClient 相容的客戶端

    用於持續對話，支援 async context manager。

    用法：
        async with ClaudeSDKClient(options=options) as client:
            await client.connect()
            await client.query("你好")
            async for msg in client.receive_response():
                print(msg)

    或不使用 context manager：
        client = ClaudeSDKClient(options=options)
        await client.connect()
        await client.query("你好")
        async for msg in client.receive_response():
            print(msg)
        await client.disconnect()
    """

    def __init__(self, options: ClaudeAgentOptions | None = None):
        """
        初始化客戶端

        Args:
            options: 設定選項
        """
        self.options = options or ClaudeAgentOptions()
        self.project_root = self.options.cwd or "."
        self.tool_executor = ToolExecutor(
            project_root=self.project_root,
            allowed_dirs=self.options.add_dirs or [self.project_root],
        )
        self._messages: list[dict] = []
        self._connected = False
        self._pending_response: ChatCompletionResponse | None = None
        self._http_client: httpx.AsyncClient | None = None
        self._iteration = 0

    async def __aenter__(self) -> "ClaudeSDKClient":
        """Async context manager entry"""
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        """Async context manager exit"""
        await self.disconnect()

    async def connect(self, prompt: str | None = None) -> None:
        """
        連接到 LLM

        Args:
            prompt: 可選的初始提示詞
        """
        self._http_client = httpx.AsyncClient(timeout=self.options.timeout)
        self._connected = True

        if prompt:
            await self.query(prompt)

    async def disconnect(self) -> None:
        """斷開連接"""
        if self._http_client:
            await self._http_client.aclose()
            self._http_client = None
        self._connected = False

    async def query(self, prompt: str, session_id: str = "default") -> None:
        """
        發送查詢

        Args:
            prompt: 使用者輸入
            session_id: 會話 ID（目前未使用，保留相容性）
        """
        if not self._connected:
            raise RuntimeError("Client not connected. Call connect() first.")

        self._iteration = 0
        # 加入使用者訊息
        self._messages.append({"role": "user", "content": prompt})
        self._iteration += 1

        # 呼叫 API（第一輪帶 web_search）
        self._pending_response = await self._call_api(include_web_search=True)

    async def receive_response(self) -> AsyncIterator[AssistantMessage]:
        """
        接收回應

        Yields:
            AssistantMessage 物件
        """
        if not self._pending_response:
            return

        response = self._pending_response
        self._pending_response = None

        # 處理 Agentic Loop
        while True:
            # 產生 AssistantMessage
            assistant_msg = AssistantMessage.from_response(response)
            yield assistant_msg

            # 檢查是否有需要本地執行的工具呼叫
            local_tool_calls = response.get_local_tool_calls()
            if not local_tool_calls:
                # 沒有工具呼叫，加入 assistant 訊息並完成
                self._messages.append(response.to_assistant_message_dict())
                return

            # 加入 assistant 訊息（含 tool_calls）
            self._messages.append(response.to_assistant_message_dict())

            # 執行工具並加入 tool result messages
            for tool_call in local_tool_calls:
                name = tool_call.function.name
                args = tool_call.function.get_parsed_arguments()
                result = await self.tool_executor.execute(name, args)

                # OpenAI 格式：每個 tool result 是獨立的 message
                self._messages.append(
                    {
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "name": name,
                        "content": result,
                    }
                )

            # 繼續呼叫 API（後續工具回合不帶 web_search）
            self._iteration += 1
            response = await self._call_api(include_web_search=False)

    async def receive_messages(self) -> AsyncIterator[AssistantMessage]:
        """
        接收所有訊息（別名，與 claude_agent_sdk 相容）

        Yields:
            AssistantMessage 物件
        """
        async for msg in self.receive_response():
            yield msg

    async def interrupt(self) -> None:
        """
        中斷當前操作

        目前實作為清除 pending response
        """
        self._pending_response = None

    def _get_system_prompt(self) -> str:
        """取得系統提示詞"""
        if self.options.system_prompt:
            return self.options.system_prompt

        return f"""你是一個程式開發助手。
你可以讀取、寫入、編輯程式碼檔案，也可以執行 shell 命令。
專案目錄: {self.project_root}
請用繁體中文回答。

當你需要操作檔案時，請使用提供的工具。
在編輯檔案前，請先讀取檔案內容以了解上下文。
執行命令時請注意安全，避免執行危險操作。"""

    def _get_tools(self) -> list[dict] | None:
        """
        取得啟用的工具定義（OpenAI Function Calling 格式）

        WebSearch 不再作為 tool，改用 request body 的 web_search_options 參數
        """
        allowed = set(self.options.allowed_tools)
        tools = []

        for t in TOOL_DEFINITIONS:
            if t["name"] in allowed:
                tools.append(
                    {
                        "type": "function",
                        "function": {
                            "name": t["name"],
                            "description": t["description"],
                            "parameters": t["input_schema"],
                        },
                    }
                )

        return tools if tools else None

    def _build_messages(self) -> list[dict]:
        """在 messages 最前面插入 system message"""
        system_msg = {"role": "system", "content": self._get_system_prompt()}
        return [system_msg] + self._messages

    async def _call_api(self, include_web_search: bool = True) -> ChatCompletionResponse:
        """呼叫 OpenAI Chat Completions API"""
        ai_logger = _get_ai_logger()

        api_key = self.options.get_api_key()
        base_url = self.options.get_base_url()

        if not api_key:
            raise RuntimeError("未設定 API Token (FWAUTO_TOKEN 或 ANTHROPIC_API_KEY)")

        if not self._http_client:
            raise RuntimeError("HTTP client not initialized")

        api_url = base_url.rstrip("/") + "/v1/chat/completions"

        # 組裝 request body（OpenAI 格式）
        request_body: dict = {
            "model": self.options.model,
            "messages": self._build_messages(),
            "max_tokens": self.options.max_tokens,
        }

        # 工具定義（有工具才傳）
        tools = self._get_tools()
        if tools:
            request_body["tools"] = tools

        # 可選參數
        if self.options.temperature is not None:
            request_body["temperature"] = self.options.temperature

        # Web Search — 只在第一輪帶，後續工具回合不帶
        if self.options.web_search and include_web_search:
            request_body["web_search_options"] = {
                "search_context_size": self.options.search_context_size,
            }

        headers = _build_api_headers(api_key, self.options)

        # Log REQUEST
        if ai_logger.handlers:
            req_data = _build_request_ai_data(self.options, self._iteration, self._messages)
            if ai_logger.isEnabledFor(logging.DEBUG):
                req_data["headers"] = {k: v for k, v in headers.items() if k != "x-fwauto-token"}
                req_data["body"] = request_body
            ai_logger.info("", extra={"ai_data": req_data})

        start_time = time.monotonic()

        response = await self._http_client.post(
            api_url,
            headers=headers,
            json=request_body,
        )

        duration_ms = int((time.monotonic() - start_time) * 1000)

        if response.status_code != 200:
            error_text = response.text
            if ai_logger.handlers:
                ai_logger.log(
                    logging.WARNING if response.status_code == 429 else logging.ERROR,
                    "",
                    extra={
                        "ai_data": _build_error_ai_data(
                            self.options,
                            self._iteration,
                            response.status_code,
                            error_text,
                            duration_ms,
                        )
                    },
                )
            raise RuntimeError(f"API error ({response.status_code}): {error_text}")

        data = response.json()
        result = ChatCompletionResponse.from_dict(data)

        # Log success RESPONSE
        if ai_logger.handlers:
            resp_data = _build_response_ai_data(
                self.options,
                self._iteration,
                result,
                duration_ms,
                response.status_code,
            )
            if ai_logger.isEnabledFor(logging.DEBUG):
                resp_data["body"] = data
            ai_logger.info("", extra={"ai_data": resp_data})

        return result

    def get_messages(self) -> list[dict]:
        """取得完整對話歷史"""
        return list(self._messages)

    def clear_messages(self) -> None:
        """清除對話歷史"""
        self._messages.clear()

    @property
    def message_count(self) -> int:
        """取得目前對話訊息數量"""
        return len(self._messages)

    @property
    def turn_count(self) -> int:
        """取得目前對話輪數"""
        return len([m for m in self._messages if m.get("role") == "user"])
