"""
KivotRunner - Drop-in replacement for OpenAI Agents Runner.

Routes model and tool calls to Go activities while preserving
the official OpenAI Agents SDK developer experience.

In standalone mode (outside Temporal workflow), provides canonical
event logging and budget tracking.
"""

import json
import time
from datetime import timedelta
from typing import Any, Dict, List, Optional, Union

from .types import AgentResult

# Conditional import: temporalio is optional (only needed for durable mode)
try:
    from temporalio import workflow
    from temporalio.common import RetryPolicy

    _HAS_TEMPORAL = True
except ImportError:
    _HAS_TEMPORAL = False


class KivotRunner:
    """
    Drop-in replacement for official OpenAI Agents Runner.

    Usage:
        from agents import Agent
        from kivot_agents import KivotRunner

        agent = Agent(name="Assistant", instructions="You are helpful.")
        result = await KivotRunner.run(agent, input="Hello!")

    Inside Temporal workflows, model and tool calls are routed to Go activities.
    Outside workflows, uses Runner.run_streamed() with canonical event logging
    and budget tracking.

    Policy Support:
        To enable policy enforcement, add policy_refs attribute to your agent:

        agent = Agent(name="Assistant", instructions="...")
        agent.policy_refs = ["dev-policy"]  # Custom attribute

        KivotRunner will pass these to Go ToolStepActivity for enforcement.
    """

    @staticmethod
    async def run(
        agent: Any,  # agents.Agent type
        input: Union[str, Dict[str, Any]],
        *,
        thread_id: Optional[str] = None,
        budgets: Optional["BudgetConfig"] = None,
        quiet: bool = False,
        **kwargs,
    ) -> AgentResult:
        """
        Run agent with the given input.

        Args:
            agent: Agent instance from OpenAI Agents SDK
            input: User input (string or dict with "messages" key)
            thread_id: Optional thread ID for conversation continuity
            budgets: Optional budget config for token/cost limits (standalone mode)
            quiet: If True, suppress canonical event output (standalone mode)
            **kwargs: Additional arguments (for compatibility)

        Returns:
            AgentResult with final_output, messages, and usage stats

        Raises:
            RuntimeError: If called inside workflow without Go activities available
        """
        if _HAS_TEMPORAL and workflow.in_workflow():
            return await KivotRunner._run_durable(
                agent, input, thread_id=thread_id, **kwargs
            )
        else:
            return await KivotRunner._run_standalone(
                agent, input, budgets=budgets, quiet=quiet, **kwargs
            )

    @staticmethod
    async def _run_standalone(
        agent: Any,
        input: Union[str, Dict[str, Any]],
        *,
        budgets: Optional["BudgetConfig"] = None,
        quiet: bool = False,
        **kwargs,
    ) -> AgentResult:
        """Standalone mode: OpenAI Runner + canonical event log + budget tracking.

        Uses Runner.run_streamed() to get intermediate events
        and maps them to Kivot canonical events.
        """
        try:
            from agents import Runner as OfficialRunner
            from agents.stream_events import (
                RawResponsesStreamEvent,
                RunItemStreamEvent,
                AgentUpdatedStreamEvent,
            )
        except ImportError:
            raise RuntimeError(
                "KivotRunner called outside workflow without OpenAI Agents SDK. "
                "Install: pip install openai-agents"
            )

        from .standalone import StandaloneEventLogger, generate_local_run_id
        from .budget import BudgetTracker

        run_id = generate_local_run_id()
        model_name = str(getattr(agent, "model", "gpt-4o-mini"))

        logger = StandaloneEventLogger(run_id) if not quiet else None
        tracker = BudgetTracker(model=model_name, config=budgets)

        if logger:
            logger.emit("run.started")

        _tool_meta: dict[str, dict] = {}  # call_id -> {name, start_time}

        try:
            result = OfficialRunner.run_streamed(agent, input=input, **kwargs)

            async for event in result.stream_events():

                # === Raw LLM events ===
                if isinstance(event, RawResponsesStreamEvent):
                    data = event.data
                    data_type = getattr(data, "type", "")

                    if data_type == "response.created":
                        if logger:
                            logger.emit("model.start", model=model_name)

                    elif data_type == "response.output_text.delta":
                        delta_text = getattr(data, "delta", "")
                        if delta_text and logger:
                            logger.emit("model.delta", text=delta_text)

                    elif data_type == "response.completed":
                        if logger:
                            logger.emit("model.end", model=model_name)

                # === Processed artifacts ===
                elif isinstance(event, RunItemStreamEvent):

                    if event.name == "tool_called":
                        item = event.item  # ToolCallItem
                        raw = item.raw_item
                        tool_name = getattr(raw, "name", None) or "unknown"
                        arguments = getattr(raw, "arguments", "{}")
                        call_id = getattr(raw, "call_id", "") or ""

                        _tool_meta[call_id] = {
                            "name": tool_name,
                            "start_time": time.monotonic(),
                        }
                        tracker.record_tool_call()

                        if logger:
                            logger.emit(
                                "tool.call.started",
                                tool_name=tool_name,
                                arguments=_try_parse_json(arguments),
                                call_id=call_id,
                            )

                    elif event.name == "tool_output":
                        item = event.item  # ToolCallOutputItem
                        output = item.output
                        call_id = getattr(item.raw_item, "call_id", "") or ""

                        meta = _tool_meta.pop(call_id, {})
                        tool_name = meta.get("name", "unknown")
                        start_t = meta.get("start_time")
                        duration_ms = (
                            (time.monotonic() - start_t) * 1000 if start_t else 0
                        )

                        if logger:
                            logger.emit(
                                "tool.call.completed",
                                tool_name=tool_name,
                                result=str(output),
                                call_id=call_id,
                                duration_ms=duration_ms,
                            )

                    elif event.name == "message_output_created":
                        item = event.item  # MessageOutputItem
                        content_parts = getattr(item.raw_item, "content", [])
                        text = ""
                        for part in content_parts:
                            if hasattr(part, "text"):
                                text += part.text
                        if text and logger:
                            logger.emit(
                                "message.emitted", content=text, role="assistant"
                            )

                    elif event.name == "reasoning_item_created":
                        # Reasoning events (o3/o3-mini) -- log but don't display
                        pass

                # === Agent handoffs ===
                elif isinstance(event, AgentUpdatedStreamEvent):
                    new_name = getattr(event.new_agent, "name", "unknown")
                    if logger:
                        logger.emit("agent.updated", agent=new_name)

            # Collect final usage
            usage = getattr(result, "context_wrapper", None)
            if usage:
                usage = getattr(usage, "usage", None)
            tracker.record_usage(usage)

            # Check budget (post-hoc in standalone mode)
            budget_violation = tracker.check_budget()
            if budget_violation and logger:
                logger.emit(
                    "run.failed", error=f"budget_exceeded: {budget_violation}"
                )

            summary = tracker.summary
            if logger:
                logger.emit(
                    "run.completed",
                    duration_s=summary["duration_s"],
                    total_tokens=summary["total_tokens"],
                    cost_usd=summary["cost_usd"],
                    input_tokens=summary["input_tokens"],
                    output_tokens=summary["output_tokens"],
                    tool_calls=summary["tool_calls"],
                )

            return AgentResult(
                final_output=result.final_output,
                messages=_extract_messages_from_stream(result),
                usage={
                    "input_tokens": summary["input_tokens"],
                    "output_tokens": summary["output_tokens"],
                    "total_tokens": summary["total_tokens"],
                    "cost_usd": summary["cost_usd"],
                },
            )

        except Exception as e:
            if logger:
                logger.emit("run.failed", error=str(e))
            raise

    @staticmethod
    async def _run_durable(
        agent: Any,
        input: Union[str, Dict[str, Any]],
        thread_id: Optional[str] = None,
        **kwargs,
    ) -> AgentResult:
        """
        Execute agent loop with Go activities.

        Flow:
        1. If thread_id provided, load conversation history
        2. Prepare messages from input, insert history after system message
        3. Call Go ModelStepActivity
        4. If tool_calls in response:
           - Execute each tool via Go ToolStepActivity
           - Add tool results to messages
           - Go back to step 3
        5. Else: save new messages to thread and return final output
        """
        from .overrides import TruncationConfig, get_kivot_config
        from .session import KivotSession

        info = workflow.info()
        run_id = info.workflow_id
        logger = workflow.logger

        # Get config from overrides (if plugin is used) or use defaults
        config = get_kivot_config()

        # Extract activity parameters
        truncation_config: Optional[TruncationConfig] = None
        if config:
            task_queue = config.go_activities_task_queue
            model_timeout = (
                config.model_params.start_to_close_timeout
                or timedelta(seconds=60)
            )
            model_retry = config.model_params.retry_policy or RetryPolicy(
                maximum_attempts=3,
                initial_interval=timedelta(seconds=1),
                maximum_interval=timedelta(seconds=10),
            )
            tool_timeout = (
                config.tool_params.start_to_close_timeout or timedelta(seconds=30)
            )
            tool_retry = config.tool_params.retry_policy or RetryPolicy(
                maximum_attempts=2,
                initial_interval=timedelta(seconds=2),
                maximum_interval=timedelta(seconds=10),
            )
            default_policy_refs = list(config.default_policy_refs)
            truncation_config = config.truncation
        else:
            # Defaults when no plugin config
            task_queue = "core-activities"
            model_timeout = timedelta(seconds=60)
            model_retry = RetryPolicy(
                maximum_attempts=3,
                initial_interval=timedelta(seconds=1),
                maximum_interval=timedelta(seconds=10),
            )
            tool_timeout = timedelta(seconds=30)
            tool_retry = RetryPolicy(
                maximum_attempts=2,
                initial_interval=timedelta(seconds=2),
                maximum_interval=timedelta(seconds=10),
            )
            default_policy_refs = []

        # Thread session support: load history if thread_id is provided
        session: Optional[KivotSession] = None
        history_messages: List[Dict[str, Any]] = []

        if thread_id:
            thread_timeout = timedelta(seconds=10)
            thread_retry = RetryPolicy(
                maximum_attempts=3,
                initial_interval=timedelta(seconds=1),
                maximum_interval=timedelta(seconds=5),
            )
            history_limit = 50

            if config:
                if config.thread_params and config.thread_params.start_to_close_timeout:
                    thread_timeout = config.thread_params.start_to_close_timeout
                if config.thread_params and config.thread_params.retry_policy:
                    thread_retry = config.thread_params.retry_policy
                history_limit = config.thread_history_limit

            session = KivotSession(
                task_queue=task_queue,
                timeout=thread_timeout,
                retry_policy=thread_retry,
            )

            # Load conversation history from Go ThreadStore
            history_messages = await session.load_history(thread_id, limit=history_limit)
            logger.info(
                "Thread history loaded",
                extra={
                    "thread_id": thread_id,
                    "history_count": len(history_messages),
                },
            )

        # Prepare initial messages from agent instructions + current input
        messages = _prepare_messages(agent, input)

        # Insert history AFTER system message but BEFORE current user input.
        # messages at this point is typically: [system, user_input]
        # We want: [system, ...history..., user_input]
        if history_messages:
            if messages and messages[0].get("role") == "system":
                system_msg = messages[0]
                user_msgs = messages[1:]
                messages = [system_msg] + history_messages + user_msgs
            else:
                messages = history_messages + messages

        logger.info(
            "KivotRunner started",
            extra={
                "run_id": run_id,
                "agent_name": getattr(agent, "name", "unknown"),
                "message_count": len(messages),
                "thread_id": thread_id or "",
            },
        )

        # Track where new messages start (after system + history)
        # so we only save new messages to the thread after completion.
        new_messages_start = 0
        if messages and messages[0].get("role") == "system":
            new_messages_start = 1
        new_messages_start += len(history_messages)

        # Build local tools lookup: tools defined in Python with @function_tool
        # are executed in-process; MCP/external tools go to Go ToolStepActivity.
        local_tools: Dict[str, Any] = {}
        for tool in getattr(agent, "tools", []):
            tool_name = getattr(tool, "name", None) or getattr(tool, "__name__", None)
            if tool_name:
                local_tools[tool_name] = tool

        # Agent loop
        iteration = 0
        max_iterations = kwargs.get("max_iterations", 50)  # Safety limit

        while iteration < max_iterations:
            iteration += 1
            logger.info("Agent iteration", extra={"iteration": iteration})

            # 0. Apply context window truncation if configured
            messages = await _maybe_truncate(
                messages,
                truncation_config=truncation_config,
                run_id=run_id,
                thread_id=thread_id,
                task_queue=task_queue,
                logger=logger,
            )

            # 1. Call Go ModelStepActivity
            model_response = await workflow.execute_activity(
                "ModelStepActivity",
                {
                    "run_id": run_id,
                    "step_id": f"model-{iteration}",
                    "agent_id": getattr(agent, "name", "agent"),
                    "model_ref": str(getattr(agent, "model", None) or "gpt-4o-mini"),
                    "messages": messages,
                    "instructions": getattr(agent, "instructions", ""),
                    "tools": _format_tools(getattr(agent, "tools", [])),
                    "stream": True,
                },
                task_queue=task_queue,
                start_to_close_timeout=model_timeout,
                retry_policy=model_retry,
            )

            # Add assistant message to history
            assistant_message = {
                "role": "assistant",
                "content": model_response.get("content", ""),
            }

            # Check for tool calls
            tool_calls = model_response.get("tool_calls")
            if tool_calls:
                assistant_message["tool_calls"] = tool_calls
                messages.append(assistant_message)

                logger.info("Tool calls detected", extra={"count": len(tool_calls)})

                # 2. Execute tools: local tools in-process, MCP tools via Go ToolStepActivity
                for tool_call in tool_calls:
                    tool_name = tool_call["name"]
                    step_id = f"tool-{iteration}-{tool_call['id']}"
                    agent_name = getattr(agent, "name", "agent")

                    if tool_name in local_tools:
                        # Local tool: execute in Python, emit events via Go activity
                        tool_success = True
                        tool_error = ""
                        try:
                            tool_output = await _execute_local_tool(
                                local_tools[tool_name],
                                tool_call.get("arguments", "{}"),
                            )
                        except Exception as e:
                            tool_output = str(e)
                            tool_success = False
                            tool_error = str(e)

                        # Report events via LocalToolActivity
                        await workflow.execute_activity(
                            "LocalToolActivity",
                            {
                                "run_id": run_id,
                                "step_id": step_id,
                                "agent_id": agent_name,
                                "tool_name": tool_name,
                                "tool_call_id": tool_call["id"],
                                "arguments": json.loads(tool_call.get("arguments", "{}")) if isinstance(tool_call.get("arguments"), str) else tool_call.get("arguments", {}),
                                "output": tool_output,
                                "success": tool_success,
                                "duration_ms": 0,
                                "error": tool_error,
                            },
                            task_queue=task_queue,
                            start_to_close_timeout=timedelta(seconds=10),
                        )

                        tool_content = tool_output
                    else:
                        # MCP/external tool: delegate to Go ToolStepActivity
                        policy_refs = getattr(agent, "policy_refs", default_policy_refs)

                        tool_result = await workflow.execute_activity(
                            "ToolStepActivity",
                            {
                                "run_id": run_id,
                                "step_id": step_id,
                                "agent_id": agent_name,
                                "tool_ref": tool_name,
                                "tool_call_id": tool_call["id"],
                                "arguments": tool_call.get("arguments", "{}"),
                                "policy_refs": policy_refs,
                            },
                            task_queue=task_queue,
                            start_to_close_timeout=tool_timeout,
                            retry_policy=tool_retry,
                        )

                        tool_content = tool_result.get("output", "")

                    # Add tool result to messages
                    messages.append(
                        {
                            "role": "tool",
                            "tool_call_id": tool_call["id"],
                            "name": tool_name,
                            "content": tool_content,
                        }
                    )

                # Loop continues with tool results
                continue

            # 3. No tools - agent finished
            messages.append(assistant_message)

            # Save new messages to thread if session is active
            if session and thread_id:
                new_messages = messages[new_messages_start:]
                if new_messages:
                    saved = await session.save_messages(thread_id, run_id, new_messages)
                    logger.info(
                        "Thread messages saved",
                        extra={
                            "thread_id": thread_id,
                            "saved_count": saved,
                        },
                    )

            logger.info(
                "Agent completed",
                extra={
                    "iterations": iteration,
                    "final_output_length": len(model_response.get("content", "")),
                },
            )

            return AgentResult(
                final_output=model_response.get("content", ""),
                messages=messages,
                usage=model_response.get("usage"),
            )

        # Max iterations reached
        logger.error("Max iterations reached", extra={"max": max_iterations})
        raise RuntimeError(f"Agent loop exceeded max iterations ({max_iterations})")


def _prepare_messages(agent: Any, input: Union[str, Dict[str, Any]]) -> List[Dict[str, Any]]:
    """
    Convert input to messages format.

    Supports:
    - String input: converted to user message
    - Dict with "messages" key: used directly
    - Dict with "content" key: converted to user message
    """
    messages = []

    # System message from agent instructions
    instructions = getattr(agent, "instructions", None)
    if instructions:
        messages.append({"role": "system", "content": instructions})

    # User input
    if isinstance(input, str):
        messages.append({"role": "user", "content": input})
    elif isinstance(input, dict):
        if "messages" in input:
            # Already in messages format
            messages.extend(input["messages"])
        elif "content" in input:
            # Single message object
            messages.append({"role": "user", "content": input["content"]})
        else:
            # Convert dict to string
            messages.append({"role": "user", "content": json.dumps(input)})
    else:
        # Fallback: convert to string
        messages.append({"role": "user", "content": str(input)})

    return messages


async def _maybe_truncate(
    messages: List[Dict[str, Any]],
    *,
    truncation_config: Any,
    run_id: str,
    thread_id: Optional[str],
    task_queue: str,
    logger: Any,
) -> List[Dict[str, Any]]:
    """Apply context window truncation if configured and threshold exceeded.

    Calls the Go TruncateThreadActivity to perform the actual truncation
    (and optional summarization). Returns the potentially trimmed message list.

    Args:
        messages: Current conversation messages.
        truncation_config: Truncation settings, or None to skip.
        run_id: Current workflow run ID.
        thread_id: Optional thread ID for persistent storage.
        task_queue: Temporal task queue for Go activities.
        logger: Workflow logger.

    Returns:
        The (possibly truncated) message list.
    """
    if truncation_config is None:
        return messages

    # Count non-system messages to decide whether truncation is needed.
    non_system_count = sum(1 for m in messages if m.get("role") != "system")
    threshold = truncation_config.max_messages
    if threshold <= 0 and truncation_config.max_tokens <= 0:
        return messages  # No limits configured

    # Only call the activity when the message count exceeds the threshold
    # (or when token-based truncation is configured -- the activity will
    # check token counts server-side).
    needs_truncation = (
        (threshold > 0 and non_system_count > threshold)
        or truncation_config.max_tokens > 0
    )
    if not needs_truncation:
        return messages

    truncate_result = await workflow.execute_activity(
        "TruncateThreadActivity",
        {
            "run_id": run_id,
            "thread_id": thread_id or "",
            "strategy": {
                "max_messages": truncation_config.max_messages,
                "max_tokens": truncation_config.max_tokens,
                "summarize_removed": truncation_config.summarize_removed,
                "summary_model_ref": truncation_config.summary_model_ref,
                "summary_max_tokens": truncation_config.summary_max_tokens,
                "preserve_system_messages": truncation_config.preserve_system_messages,
            },
            "messages": messages,
        },
        task_queue=task_queue,
        start_to_close_timeout=timedelta(seconds=30),
        retry_policy=RetryPolicy(
            maximum_attempts=2,
            initial_interval=timedelta(seconds=1),
            maximum_interval=timedelta(seconds=10),
        ),
    )

    removed_count = truncate_result.get("removed_count", 0)
    if removed_count > 0:
        logger.info(
            "Context window truncated",
            extra={
                "removed_count": removed_count,
                "summary_inserted": truncate_result.get("summary_inserted", False),
                "estimated_tokens": truncate_result.get("estimated_tokens", 0),
            },
        )
        return truncate_result.get("messages", messages)

    return messages


def _format_tools(tools: List[Any]) -> List[Dict[str, Any]]:
    """
    Format agent tools for ModelStepActivity.

    Converts various tool formats to OpenAI function calling format.
    """
    if not tools:
        return []

    formatted = []
    for tool in tools:
        # Already a dict - use as is
        if isinstance(tool, dict):
            formatted.append(tool)
            continue

        # Has to_dict method (OpenAI SDK objects)
        if hasattr(tool, "to_dict"):
            formatted.append(tool.to_dict())
            continue

        # OpenAI Agents SDK FunctionTool (has name + params_json_schema)
        if hasattr(tool, "name") and hasattr(tool, "params_json_schema"):
            formatted.append(
                {
                    "type": "function",
                    "function": {
                        "name": tool.name,
                        "description": getattr(tool, "description", "") or "",
                        "parameters": tool.params_json_schema,
                    },
                }
            )
            continue

        # Python function - extract signature
        if callable(tool):
            import inspect

            sig = inspect.signature(tool)
            params = {}

            for name, param in sig.parameters.items():
                param_type = "string"  # Default
                if param.annotation != inspect.Parameter.empty:
                    # Map Python types to JSON schema types
                    type_map = {
                        str: "string",
                        int: "integer",
                        float: "number",
                        bool: "boolean",
                        list: "array",
                        dict: "object",
                    }
                    param_type = type_map.get(param.annotation, "string")

                params[name] = {"type": param_type}

            required = [
                name
                for name, param in sig.parameters.items()
                if param.default is inspect.Parameter.empty
            ]
            formatted.append(
                {
                    "type": "function",
                    "function": {
                        "name": tool.__name__,
                        "description": inspect.getdoc(tool) or f"Call {tool.__name__}",
                        "parameters": {
                            "type": "object",
                            "properties": params,
                            "required": required,
                        },
                    },
                }
            )
            continue

        # Unknown format - skip (no logging available outside workflow)

    return formatted


async def _execute_local_tool(tool: Any, arguments_json: str) -> str:
    """
    Execute a local Python tool (defined with @function_tool).

    Parses the JSON arguments string and calls the underlying function.
    Returns the result as a string.

    For OpenAI Agents SDK FunctionTool objects, extracts the underlying
    function and calls it directly to avoid asyncio.to_thread() which is
    not supported inside Temporal's workflow sandbox.
    """
    import asyncio

    args = json.loads(arguments_json) if arguments_json else {}

    # OpenAI Agents SDK FunctionTool: extract and call the underlying
    # function directly. on_invoke_tool() uses asyncio.to_thread() which
    # raises NotImplementedError inside Temporal's workflow sandbox.
    if hasattr(tool, "on_invoke_tool") and hasattr(tool, "name") and not callable(tool):
        func = _extract_function_tool_func(tool)
        if func is not None:
            result = func(**args)
            if asyncio.iscoroutine(result) or asyncio.isfuture(result):
                result = await result
            return str(result)

    # Plain callable: call with keyword arguments
    if callable(tool):
        result = tool(**args)
        if asyncio.iscoroutine(result) or asyncio.isfuture(result):
            result = await result
        return str(result)

    raise ValueError(f"Cannot execute local tool: {type(tool)}")


def _extract_function_tool_func(tool: Any) -> Any:
    """
    Extract the original function from an OpenAI Agents SDK FunctionTool.

    FunctionTool wraps the original function in a two-level closure chain:
    on_invoke_tool → _on_invoke_tool_impl → original function.
    We search for a callable whose __name__ matches tool.name rather than
    relying on hardcoded closure cell indices.

    Returns the original function or None if extraction fails.
    """
    try:
        for cell in tool.on_invoke_tool.__closure__ or []:
            try:
                impl = cell.cell_contents
            except ValueError:
                continue
            if not callable(impl) or not hasattr(impl, "__closure__"):
                continue
            for inner_cell in impl.__closure__ or []:
                try:
                    func = inner_cell.cell_contents
                except ValueError:
                    continue
                if callable(func) and getattr(func, "__name__", None) == tool.name:
                    return func
    except (AttributeError, TypeError):
        pass
    return None


def _try_parse_json(s: str) -> Any:
    """Try to parse JSON string, return dict or original string."""
    try:
        return json.loads(s)
    except (json.JSONDecodeError, TypeError):
        return s


def _extract_messages_from_stream(result: Any) -> list:
    """Extract messages list from RunResultStreaming."""
    messages = []
    for item in getattr(result, "new_items", []):
        item_type = getattr(item, "type", "")
        if item_type == "message_output_item":
            content_parts = getattr(item.raw_item, "content", [])
            text = "".join(
                getattr(p, "text", "") for p in content_parts if hasattr(p, "text")
            )
            messages.append({"role": "assistant", "content": text})
        elif item_type == "tool_call_item":
            raw = item.raw_item
            messages.append({
                "role": "assistant",
                "tool_calls": [{
                    "id": getattr(raw, "call_id", ""),
                    "name": getattr(raw, "name", ""),
                    "arguments": getattr(raw, "arguments", "{}"),
                }],
            })
        elif item_type == "tool_call_output_item":
            messages.append({
                "role": "tool",
                "tool_call_id": getattr(item.raw_item, "call_id", ""),
                "content": str(item.output),
            })
    return messages


# Type hint import for BudgetConfig (avoid circular import at runtime)
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .budget import BudgetConfig
