"""LLM interaction loop: create agno Agent, stream events, collect response.

Extracted from AgentLoop._run_agent() to allow independent development
of the LLM orchestration layer.
"""

from __future__ import annotations

import re
from typing import TYPE_CHECKING, Any, Awaitable, Callable

from loguru import logger

from asedclaw.agent.context import ContextBuilder
from asedclaw.agent.guardrails import RoundGuardrails
from asedclaw.agent.tools.message import MessageTool
from asedclaw.media.parse import extract_media_from_tool_result
from asedclaw.media.validate import validate_media_path

if TYPE_CHECKING:
    from pathlib import Path

    from asedclaw.agent.tools.registry import ToolRegistry
    from asedclaw.providers.base import LLMProvider


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def strip_think(text: str | None) -> str | None:
    """Remove <think>…</think> blocks from model output."""
    if not text:
        return text
    return re.sub(r"<think>.*?</think>\s*", "", text, flags=re.DOTALL).strip() or None


def history_to_prompt(messages: list[dict[str, Any]]) -> str:
    """Convert message history to a single prompt string for agno."""
    parts: list[str] = []
    for m in messages:
        if m.get("role") == "system":
            continue
        text = m.get("content", "")
        if isinstance(text, list):
            text = " ".join(
                c.get("text", "") for c in text if isinstance(c, dict) and c.get("type") == "text"
            )
        if not text:
            continue
        parts.append(f"{m.get('role')}: {text}")
    return "Conversation so far:\n\n" + "\n\n".join(parts)


def format_tool_hint(name: str, args: Any) -> str:
    """Build a short human-readable hint for a tool call."""
    hint_val = None
    if isinstance(args, dict) and args:
        hint_val = next(iter(args.values()))
    if isinstance(hint_val, str):
        return f'{name}("{hint_val[:40]}…")' if len(hint_val) > 40 else f'{name}("{hint_val}")'
    return name


# ---------------------------------------------------------------------------
# Core runner
# ---------------------------------------------------------------------------

async def run_agent(
    *,
    initial_messages: list[dict],
    provider: LLMProvider,
    model: str,
    temperature: float,
    reasoning_effort: str | None,
    tools: ToolRegistry,
    context: ContextBuilder,
    workspace: Path,
    guardrails_config: Any | None,
    max_iterations: int,
    on_progress: Callable[..., Awaitable[None]] | None = None,
) -> tuple[str | None, list[str], list[dict], list[str]]:
    """Run a single LLM round.

    Returns ``(final_content, tools_used, all_messages, pending_media)``.
    """
    try:
        from agno.agent import Agent, RunEvent
    except Exception:
        logger.exception("agno import failed")
        fallback = "Error: agno runtime unavailable. Please verify agno dependency and model config."
        msgs = context.add_assistant_message(list(initial_messages), fallback)
        return fallback, [], msgs, []

    try:
        system_prompt = next(
            (m["content"] for m in initial_messages
             if m.get("role") == "system" and isinstance(m.get("content"), str)),
            "",
        )
        message_tool = tools.get("message")

        agent = Agent(
            model=provider.build_model(
                model=model,
                temperature=temperature,
                reasoning_effort=reasoning_effort,
            ),
            tools=tools.get_agno_tool_instances(),
            instructions=system_prompt,
            markdown=True,
            stream=True,
            stream_events=True,
            debug_mode=True,
        )
        prompt = history_to_prompt(initial_messages)
        final_content = ""
        tools_used: list[str] = []
        pending_media: list[str] = []

        # Guardrails setup
        guardrails_kwargs: dict[str, Any] = {}
        if guardrails_config:
            guardrails_kwargs["max_total_tool_calls"] = getattr(
                guardrails_config, "max_total_tool_calls", max_iterations,
            )
            guardrails_kwargs["max_same_error_retries"] = getattr(
                guardrails_config, "max_same_error_retries", 3,
            )
            if hasattr(guardrails_config, "tool_budget") and guardrails_config.tool_budget:
                guardrails_kwargs["tool_budget"] = dict(guardrails_config.tool_budget)
        else:
            guardrails_kwargs["max_total_tool_calls"] = max_iterations
        guardrails = RoundGuardrails(**guardrails_kwargs)

        reasoning_emitted = 0
        event_counts: dict[str, int] = {}
        answer_delta_count = 0
        answer_delta_chars = 0
        intermediate_delta_count = 0
        intermediate_delta_chars = 0
        reasoning_delta_count = 0
        reasoning_delta_chars = 0

        # Backward/forward compatibility across agno versions
        ev_run_content = getattr(RunEvent, "run_content", "RunContent")
        ev_run_intermediate = getattr(RunEvent, "run_intermediate_content", "RunIntermediateContent")
        ev_reasoning_delta = getattr(RunEvent, "reasoning_content_delta", "ReasoningContentDelta")
        ev_tool_started = getattr(RunEvent, "tool_call_started", "ToolCallStarted")
        ev_tool_completed = getattr(RunEvent, "tool_call_completed", "ToolCallCompleted")
        ev_tool_error = getattr(RunEvent, "tool_call_error", "ToolCallError")
        ev_run_completed = getattr(RunEvent, "run_completed", "RunCompleted")
        stream_log_every = 20

        def _preview_text(v: Any, max_len: int = 120) -> str:
            if not isinstance(v, str):
                return ""
            s = " ".join(v.split())
            return (s[: max_len - 1] + "…") if len(s) > max_len else s

        async for chunk in agent.arun(prompt, stream=True):
            event = getattr(chunk, "event", None)
            event_name = str(event)
            event_counts[event_name] = event_counts.get(event_name, 0) + 1

            if event == ev_run_content:
                delta = chunk.content
                reasoning_delta = getattr(chunk, "reasoning_content", None)
                if isinstance(reasoning_delta, str) and reasoning_delta.strip() and on_progress:
                    reasoning_emitted += 1
                    reasoning_delta_count += 1
                    reasoning_delta_chars += len(reasoning_delta)
                    if reasoning_delta_count == 1 or reasoning_delta_count % stream_log_every == 0:
                        logger.debug(
                            "chunk={} reasoning#{} total_chars={} preview={}",
                            event_name, reasoning_delta_count, reasoning_delta_chars,
                            _preview_text(reasoning_delta),
                        )
                    try:
                        await on_progress(reasoning_delta, tool_hint=False, item_type="reasoning_content")
                    except TypeError:
                        await on_progress(reasoning_delta, tool_hint=False)
                if isinstance(delta, str) and delta:
                    answer_delta_count += 1
                    answer_delta_chars += len(delta)
                    if answer_delta_count == 1 or answer_delta_count % stream_log_every == 0:
                        logger.debug(
                            "chunk={} answer#{} total_chars={} preview={}",
                            event_name, answer_delta_count, answer_delta_chars,
                            _preview_text(delta),
                        )
                    if on_progress:
                        try:
                            # run_content is LLM answer text. Stream as "content" (process text)
                            # so frontend can distinguish from reasoning_content (thinking).
                            # It will be promoted to answer_text on finalization.
                            await on_progress(delta, tool_hint=False, item_type="content")
                        except TypeError:
                            await on_progress(delta, tool_hint=False)

            elif event == ev_run_intermediate:
                delta = chunk.content
                if isinstance(delta, str) and delta and on_progress:
                    intermediate_delta_count += 1
                    intermediate_delta_chars += len(delta)
                    if (
                        intermediate_delta_count == 1
                        or intermediate_delta_count % stream_log_every == 0
                    ):
                        logger.debug(
                            "chunk={} intermediate#{} total_chars={} preview={}",
                            event_name, intermediate_delta_count, intermediate_delta_chars,
                            _preview_text(delta),
                        )
                    try:
                        # Intermediate content between tool rounds — process text.
                        await on_progress(delta, tool_hint=False, item_type="content")
                    except TypeError:
                        await on_progress(delta, tool_hint=False)

            elif event == ev_reasoning_delta:
                reasoning_delta = getattr(chunk, "reasoning_content", None) or getattr(chunk, "content", None)
                if isinstance(reasoning_delta, str) and reasoning_delta.strip() and on_progress:
                    reasoning_emitted += 1
                    reasoning_delta_count += 1
                    reasoning_delta_chars += len(reasoning_delta)
                    if reasoning_delta_count == 1 or reasoning_delta_count % stream_log_every == 0:
                        logger.debug(
                            "chunk={} reasoning#{} total_chars={} preview={}",
                            event_name, reasoning_delta_count, reasoning_delta_chars,
                            _preview_text(reasoning_delta),
                        )
                    try:
                        await on_progress(reasoning_delta, tool_hint=False, item_type="reasoning_content")
                    except TypeError:
                        await on_progress(reasoning_delta, tool_hint=False)

            elif event == ev_tool_started:
                tool_name = chunk.tool.tool_name
                tool_call_id_start = getattr(chunk.tool, "tool_call_id", None)
                tool_args_raw = chunk.tool.tool_args or {}
                if (
                    tool_name == "message"
                    and isinstance(message_tool, MessageTool)
                    and message_tool._calls_in_turn >= 4
                ):
                    logger.warning("Excessive message tool calls; stopping run.")
                    break
                # Guardrail check
                block_reason = guardrails.check_tool_allowed(tool_name, tool_args_raw)
                if block_reason:
                    logger.warning("Guardrail blocked {}: {}", tool_name, block_reason)
                    break
                if tool_name:
                    tools_used.append(tool_name)
                if on_progress and tool_name:
                    tool_args_start = chunk.tool.tool_args or {}
                    hint = format_tool_hint(tool_name, tool_args_start)
                    logger.debug("tool_hint: {}", hint)
                    try:
                        await on_progress(
                            hint, tool_hint=True, item_type="tool_call",
                            tool_status="running", tool_args=tool_args_start,
                            tool_id=tool_call_id_start,
                        )
                    except TypeError:
                        await on_progress(hint, tool_hint=True)

            elif event == ev_tool_completed:
                tc = getattr(chunk, "tool", None)
                tool_name_done = getattr(tc, "tool_name", None) if tc else None
                result_text = getattr(tc, "result", "") if tc else ""
                tool_args_done = getattr(tc, "tool_args", {}) if tc else {}
                tool_call_id = getattr(tc, "tool_call_id", None) if tc else None
                # Record outcome for guardrails
                is_err = isinstance(result_text, str) and result_text.lstrip().startswith("Error")
                if tool_name_done:
                    guardrails.record_tool_result(tool_name_done, tool_args_done, result_text, is_err)
                if isinstance(result_text, str):
                    for p in extract_media_from_tool_result(result_text):
                        if validate_media_path(p, workspace=workspace):
                            pending_media.append(p)
                        else:
                            logger.warning("Blocked media path: {}", p)
                # Push tool-done hint
                if on_progress and tool_name_done:
                    hint = format_tool_hint(tool_name_done, tool_args_done)
                    result_preview = (result_text or "")[:300].strip() if result_text else None
                    try:
                        await on_progress(
                            hint, tool_hint=True, item_type="tool_call",
                            tool_status="done", tool_args=tool_args_done,
                            tool_result=result_preview, tool_id=tool_call_id,
                        )
                    except TypeError:
                        await on_progress(hint, tool_hint=True)

            elif event == ev_tool_error:
                tc = getattr(chunk, "tool", None)
                tool_name_err = getattr(tc, "tool_name", None) if tc else None
                tool_args_err = getattr(tc, "tool_args", {}) if tc else {}
                tool_call_id_err = getattr(tc, "tool_call_id", None) if tc else None
                tool_error = getattr(chunk, "error", None)
                if tool_name_err:
                    guardrails.record_tool_result(
                        tool_name_err, tool_args_err,
                        tool_error if isinstance(tool_error, str) else str(tool_error),
                        is_error=True,
                    )
                if on_progress and tool_name_err:
                    hint = format_tool_hint(tool_name_err, tool_args_err)
                    try:
                        await on_progress(
                            hint, tool_hint=True, item_type="tool_call",
                            tool_status="failed", tool_args=tool_args_err,
                            tool_result=(tool_error[:300].strip() if isinstance(tool_error, str) else None),
                            tool_id=tool_call_id_err,
                        )
                    except TypeError:
                        await on_progress(hint, tool_hint=True)

            elif event == ev_run_completed:
                if (
                    isinstance(chunk.content, str)
                    and chunk.content.strip()
                ):
                    final_content = chunk.content

        logger.info(
            "run chunk summary events={} answer_deltas={} answer_chars={} "
            "intermediate_deltas={} intermediate_chars={} reasoning_deltas={} reasoning_chars={}",
            event_counts, answer_delta_count, answer_delta_chars,
            intermediate_delta_count, intermediate_delta_chars,
            reasoning_delta_count, reasoning_delta_chars,
        )

        if guardrails._failures:
            logger.info("guardrails stats={}", guardrails.stats)
            hint = guardrails.get_recovery_hint()
            if hint:
                logger.info("guardrails recovery hint: {}", hint)

        if reasoning_effort and reasoning_emitted == 0:
            logger.info(
                "No reasoning stream emitted in this run. model='{}' provider='{}' reasoning_effort='{}' "
                "(provider/model may not expose reasoning deltas in OpenAI-compatible stream).",
                model, type(provider).__name__, reasoning_effort,
            )

        clean = strip_think(final_content)
        all_messages = context.add_assistant_message(list(initial_messages), clean)
        return clean, tools_used, all_messages, pending_media

    except Exception:
        logger.exception("agno run failed")
        fallback = "Error: agent execution failed."
        msgs = context.add_assistant_message(list(initial_messages), fallback)
        return fallback, [], msgs, []
