"""Agent loop — the core of Tsumugi.

Flow:
    User input -> LLM streaming call
        |-> Text response -> display, done
        |-> Tool call(s) -> permission check -> execute -> feed results -> loop
"""

from __future__ import annotations

import json
import time
from typing import Any

from tsumugi.permissions import PermissionManager
from tsumugi.providers.base import (
    AssistantMessage,
    ProviderBase,
    StreamEvent,
    StreamEventType,
    TextBlock,
    ToolCallBlock,
    ToolResultBlock,
)
from tsumugi.tools import ToolRegistry
from tsumugi.tools.base import PermissionLevel, ToolSpec
from tsumugi.ui import display

# Retry settings
MAX_RETRIES = 3
RETRY_DELAYS = [1, 2, 4]  # seconds


def _is_retryable(error: Exception) -> bool:
    """Check if an error is transient and worth retrying."""
    # Network / timeout errors
    if isinstance(error, (ConnectionError, TimeoutError, OSError)):
        return True
    # HTTP status code based (works with both anthropic and openai SDKs)
    # 529 = Anthropic overloaded_error
    status = getattr(error, "status_code", None)
    if status in (429, 500, 502, 503, 504, 529):
        return True
    # Fallback: check error message for known transient patterns
    msg = str(error).lower()
    if "overloaded" in msg or "rate limit" in msg:
        return True
    return False


def _summarize_args(args: dict[str, Any], max_len: int = 200) -> str:
    """Create a brief summary of tool arguments for display."""
    parts = []
    for key, value in args.items():
        val_str = str(value)
        if len(val_str) > 80:
            val_str = val_str[:80] + "..."
        parts.append(f"{key}={val_str}")
    summary = ", ".join(parts)
    if len(summary) > max_len:
        summary = summary[:max_len] + "..."
    return summary


def _simulate_tool_call(tool_name: str, spec: ToolSpec, input_args: dict[str, Any]) -> str:
    """Plan mode: return a summary instead of executing."""
    args_summary = _summarize_args(input_args, max_len=300)
    return (
        f"[Plan Mode] {tool_name} (permission: {spec.permission.value})\n"
        f"Arguments: {args_summary}\n"
        f"(実際には実行されていません — プランモード中)"
    )


def run_agent_turn(
    messages: list[dict[str, Any]],
    provider: ProviderBase,
    system_prompt: str,
    registry: ToolRegistry,
    permissions: PermissionManager,
    plan_mode: bool = False,
    max_tokens: int = 8192,
) -> None:
    """Run one full agent turn (may involve multiple LLM calls if tools are used).

    Modifies `messages` in place by appending assistant and tool result messages.
    """
    max_iterations = 20  # Safety limit to prevent infinite loops
    turn_input_tokens = 0
    turn_output_tokens = 0

    for _ in range(max_iterations):
        # --- Stream LLM response (with retry) ---
        events: list[StreamEvent] = []
        md_streamer = display.MarkdownStreamer()
        streaming_text = False
        thinking_spinner = display.create_thinking_status()
        thinking_spinner.start()
        interrupted = False

        for attempt in range(MAX_RETRIES + 1):
            try:
                for event in provider.stream(
                    messages=messages,
                    system=system_prompt,
                    tools=registry.to_definitions(),
                    max_tokens=max_tokens,
                ):
                    # Stop thinking spinner on first real event
                    if thinking_spinner is not None:
                        thinking_spinner.stop()
                        thinking_spinner = None

                    events.append(event)

                    if event.type == StreamEventType.TEXT_DELTA:
                        if not streaming_text:
                            md_streamer.start()
                            streaming_text = True
                        md_streamer.feed(event.text)

                    elif event.type == StreamEventType.TOOL_CALL_START:
                        if streaming_text:
                            md_streamer.stop()
                            streaming_text = False

                    elif event.type == StreamEventType.ERROR:
                        if streaming_text:
                            md_streamer.stop()
                            streaming_text = False
                        display.print_error(f"Stream error: {event.error}")
                        return

                # Streaming completed successfully
                break

            except KeyboardInterrupt:
                if thinking_spinner is not None:
                    thinking_spinner.stop()
                    thinking_spinner = None
                if streaming_text:
                    md_streamer.stop()
                    streaming_text = False
                # Preserve partial response in message history
                if events:
                    partial_msg = provider.build_assistant_message(events)
                    content = _message_to_content(partial_msg)
                    if content:
                        messages.append({"role": "assistant", "content": content})
                display.console.print("\n[dim]応答を中断しました[/dim]")
                interrupted = True
                break

            except Exception as e:
                if thinking_spinner is not None:
                    thinking_spinner.stop()
                    thinking_spinner = None
                if streaming_text:
                    md_streamer.stop()
                    streaming_text = False

                if attempt < MAX_RETRIES and _is_retryable(e):
                    delay = RETRY_DELAYS[attempt]
                    display.print_info(
                        f"エラー発生、{delay}秒後にリトライします... ({e})"
                    )
                    time.sleep(delay)
                    # Reset for retry
                    events = []
                    md_streamer = display.MarkdownStreamer()
                    streaming_text = False
                    thinking_spinner = display.create_thinking_status("リトライ中...")
                    thinking_spinner.start()
                    continue

                # Non-retryable or max retries exceeded
                display.print_error(f"API error: {e}")
                return

        if interrupted:
            return

        # Ensure spinner and streamer are stopped
        if thinking_spinner is not None:
            thinking_spinner.stop()
        if streaming_text:
            md_streamer.stop()

        # Build assistant message from events
        assistant_msg = provider.build_assistant_message(events)

        # Accumulate token usage
        turn_input_tokens += assistant_msg.usage.input_tokens
        turn_output_tokens += assistant_msg.usage.output_tokens

        # Convert to message dict for history
        content_for_history = _message_to_content(assistant_msg)
        messages.append({"role": "assistant", "content": content_for_history})

        # Check for tool calls
        tool_calls = [
            b for b in assistant_msg.content if isinstance(b, ToolCallBlock)
        ]

        if not tool_calls:
            # No tool calls — turn is done, show usage
            display.print_usage(turn_input_tokens, turn_output_tokens)
            return

        # Execute tool calls
        tool_results: list[dict[str, Any]] = []

        for tc in tool_calls:
            display.print_tool_call(tc.name, _summarize_args(tc.input))

            tool = registry.get(tc.name)
            if tool is None:
                result_text = f"Error: Unknown tool '{tc.name}'"
                is_error = True
            else:
                spec = tool.spec()

                # Permission check
                if not permissions.check(
                    tc.name, spec.permission, _summarize_args(tc.input)
                ):
                    result_text = "Tool execution denied by user."
                    is_error = True
                elif plan_mode and spec.permission != PermissionLevel.READ:
                    # Plan mode: simulate non-READ tools
                    result_text = _simulate_tool_call(tc.name, spec, tc.input)
                    is_error = False
                else:
                    # Execute tool with spinner
                    tool_spinner = display.create_tool_status(tc.name)
                    try:
                        tool_spinner.start()
                        result_text = tool.execute(**tc.input)
                        is_error = False
                    except Exception as e:
                        result_text = f"Tool execution error: {type(e).__name__}: {e}"
                        is_error = True
                    finally:
                        tool_spinner.stop()

            display.print_tool_result(result_text, is_error=is_error)

            tool_results.append({
                "type": "tool_result",
                "tool_use_id": tc.id,
                "content": result_text,
                "is_error": is_error,
            })

        # Append tool results to messages
        messages.append({"role": "user", "content": tool_results})

        # Loop: LLM will process tool results and may make more calls

    display.print_error("Agent loop iteration limit reached.")
    display.print_usage(turn_input_tokens, turn_output_tokens)


def _message_to_content(msg: AssistantMessage) -> list[dict[str, Any]]:
    """Convert AssistantMessage to Anthropic-format content list."""
    content = []
    for block in msg.content:
        if isinstance(block, TextBlock):
            content.append({"type": "text", "text": block.text})
        elif isinstance(block, ToolCallBlock):
            content.append({
                "type": "tool_use",
                "id": block.id,
                "name": block.name,
                "input": block.input,
            })
    return content
