"""Agent execution loop – the core reactive cycle."""

from __future__ import annotations

from typing import Any

from thryve.core.agent.executor import StepExecutor
from thryve.core.agent.models import LoopConfig, StepOutput, StopReason, TurnResult
from thryve.core.backends.adapter import BackendAdapter
from thryve.core.context.models import Message, ToolCall
from thryve.core.tools.executor import ToolExecutor
from thryve.core.tools.models import Tool
from thryve.core.tools.registry import ToolRegistry
from thryve.utils import get_logger

logger = get_logger("agent.loop")


class AgentLoop:
    """Multi-turn reactive loop: LLM → tool calls → observe → repeat.

    The loop terminates when:
    * The LLM produces a response with **no** tool calls (task completed).
    * The maximum number of turns is reached.
    * A doom-loop (repeated identical tool calls) is detected.
    """

    def __init__(
        self,
        llm: BackendAdapter,
        tools: list[Tool] | None = None,
        config: LoopConfig | None = None,
    ) -> None:
        self.config = config or LoopConfig()
        self._tools = tools or []
        self._registry = ToolRegistry.from_tools(self._tools)
        self._tool_executor = ToolExecutor(self._registry)

        # Bind tools to the LLM so that function-calling metadata is sent.
        self._llm = llm.bind_tools(self._tools) if self._tools else llm
        self._step_executor = StepExecutor(self._llm, self._tool_executor)

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    async def execute(self, messages: list[Message], **llm_kwargs: Any) -> TurnResult:
        """Run the loop until a stop condition is met.

        *messages* should already include the system prompt and user input.
        The list is mutated in place as the loop appends assistant / tool
        messages.
        """
        steps: list[StepOutput] = []

        for turn in range(self.config.max_turns):
            logger.debug("Turn %d/%d", turn + 1, self.config.max_turns)

            response, step = await self._step_executor.execute(
                messages, turn=turn, **llm_kwargs,
            )
            steps.append(step)

            # ---- No tool calls → task done ----
            if not step.tool_calls:
                return TurnResult(
                    final_response=response.content,
                    steps=steps,
                    stop_reason=StopReason.COMPLETED,
                )

            # ---- Append assistant message (with tool_calls) ----
            messages.append(
                Message(
                    role="assistant",
                    content=response.content or "",
                    tool_calls=step.tool_calls,
                )
            )

            # ---- Append each tool result ----
            for tr in step.tool_results:
                messages.append(
                    Message(
                        role="tool",
                        content=tr.content if tr.success else f"Error: {tr.error}",
                        tool_call_id=tr.tool_call_id,
                    )
                )

            # ---- Doom-loop detection ----
            reason = self._check_loop(steps)
            if reason is not None:
                logger.warning("Loop terminated: %s", reason.value)
                return TurnResult(
                    final_response=response.content,
                    steps=steps,
                    stop_reason=reason,
                )

        # Exhausted turns
        logger.warning("Loop reached max turns (%d)", self.config.max_turns)
        return TurnResult(
            final_response=steps[-1].llm_text if steps else "",
            steps=steps,
            stop_reason=StopReason.MAX_TURNS,
        )

    # ------------------------------------------------------------------
    # Loop / doom detection
    # ------------------------------------------------------------------

    def _check_loop(self, steps: list[StepOutput]) -> StopReason | None:
        """Return a :class:`StopReason` if a loop pattern is detected."""
        window = self.config.loop_detection_window
        if len(steps) < window:
            return None

        recent = steps[-window:]

        # Check: all recent steps call the exact same set of tools with the
        # same arguments.
        signatures = [self._step_signature(s) for s in recent]
        if len(set(signatures)) == 1:
            return StopReason.LOOP_DETECTED

        return None

    @staticmethod
    def _step_signature(step: StepOutput) -> str:
        """Produce a hashable string from the tool calls in a step."""
        parts: list[str] = []
        for tc in sorted(step.tool_calls, key=lambda t: t.name):
            parts.append(f"{tc.name}:{sorted(tc.arguments.items())}")
        return "|".join(parts)
