"""Loop detection for the Hikaflow agent REPL.

Detects when the agent is stuck in a repetitive tool-call loop (e.g., paginating
through scan_codebase results indefinitely) and injects a break message.

Three detection strategies:
1. Exact duplicates: same tool + same args called LOOP_THRESHOLD times
2. Pagination: sequential page/offset arguments
3. Semantic loops: same tool called SEMANTIC_THRESHOLD times with different args
"""

from __future__ import annotations

import hashlib
import json
import logging
from collections import Counter, deque
from dataclasses import dataclass, field
from typing import Any, Dict, Optional

logger = logging.getLogger(__name__)

# Number of identical tool calls before we declare a loop
LOOP_THRESHOLD = 3

# Number of same-tool calls (any args) before we declare a semantic loop
SEMANTIC_THRESHOLD = 6

# How many recent calls to track
RECENT_CALLS_WINDOW = 20


@dataclass
class LoopDetector:
    """Tracks recent tool calls and detects repetitive patterns.

    Usage:
        detector = LoopDetector()

        # After each tool call:
        loop_msg = detector.record_call("scan_codebase", {"page": 3})
        if loop_msg:
            # Inject loop_msg as a system message to break the loop
            ...
    """

    # Sliding window of recent call hashes
    recent_calls: deque = field(default_factory=lambda: deque(maxlen=RECENT_CALLS_WINDOW))

    # Count of consecutive identical calls per hash
    _call_counts: Dict[str, int] = field(default_factory=dict)

    # Count of calls per tool name (regardless of args) for semantic detection
    _tool_name_counts: Counter = field(default_factory=Counter)

    # Track which patterns we've already warned about (don't spam)
    _warned_patterns: set = field(default_factory=set)

    # Total loops detected this session
    loops_detected: int = 0

    def _hash_call(self, tool_name: str, args: Dict[str, Any]) -> str:
        """Create a stable hash for a tool call signature."""
        key = f"{tool_name}:{json.dumps(args, sort_keys=True, default=str)}"
        return hashlib.sha256(key.encode()).hexdigest()[:16]

    def record_call(self, tool_name: str, args: Optional[Dict[str, Any]] = None) -> Optional[str]:
        """Record a tool call and check for loops.

        Args:
            tool_name: Name of the tool being called.
            args: Arguments passed to the tool.

        Returns:
            A break message string if a loop is detected, or None if no loop.
        """
        args = args or {}
        call_hash = self._hash_call(tool_name, args)

        self.recent_calls.append(call_hash)
        self._call_counts[call_hash] = self._call_counts.get(call_hash, 0) + 1
        self._tool_name_counts[tool_name] += 1

        count = self._call_counts[call_hash]

        # Strategy 1: Exact duplicate detection
        if count >= LOOP_THRESHOLD and call_hash not in self._warned_patterns:
            self._warned_patterns.add(call_hash)
            self.loops_detected += 1

            args_summary = ", ".join(f"{k}={v}" for k, v in list(args.items())[:3])
            if len(args) > 3:
                args_summary += ", ..."

            logger.warning(
                "Loop detected: %s(%s) called %d times",
                tool_name, args_summary, count,
            )

            return (
                f"LOOP DETECTED: You have called {tool_name}({args_summary}) "
                f"{count} times. You are stuck in a repetitive loop. "
                f"STOP calling {tool_name} with the same or incrementing arguments. "
                f"Instead: summarize what you've found so far and move on to the "
                f"next step. If you were paginating through results, the first page "
                f"contains the highest-priority findings — use those."
            )

        # Strategy 2: Pagination pattern
        if len(self.recent_calls) >= LOOP_THRESHOLD:
            pagination_msg = self._check_pagination_pattern(tool_name, args)
            if pagination_msg:
                return pagination_msg

        # Strategy 3: Semantic loop (same tool, different args, called too many times)
        semantic_msg = self._check_semantic_loop(tool_name)
        if semantic_msg:
            return semantic_msg

        return None

    def _check_semantic_loop(self, tool_name: str) -> Optional[str]:
        """Detect semantic loops: same tool called many times with different args.

        Catches patterns like search_codebase("query_1"), search_codebase("query_2"), ...
        where the agent keeps searching without synthesizing results.
        """
        pattern_key = f"semantic:{tool_name}"
        if pattern_key in self._warned_patterns:
            return None

        total_calls = self._tool_name_counts[tool_name]
        if total_calls < SEMANTIC_THRESHOLD:
            return None

        self._warned_patterns.add(pattern_key)
        self.loops_detected += 1

        logger.warning(
            "Semantic loop detected: %s called %d times with varying args",
            tool_name, total_calls,
        )

        return (
            f"SEMANTIC LOOP DETECTED: You have called {tool_name} {total_calls} times "
            f"with different arguments. You are over-searching without synthesizing results. "
            f"STOP calling {tool_name}. Summarize the information you already have and "
            f"present your findings to the user."
        )

    def _check_pagination_pattern(self, tool_name: str, args: Dict[str, Any]) -> Optional[str]:
        """Detect sequential pagination (page=1, page=2, page=3...).

        This catches the case where args differ (different page numbers)
        but the pattern is clearly pagination.
        """
        page_val = args.get("page") or args.get("offset") or args.get("skip")
        if page_val is None:
            return None

        try:
            page_num = int(page_val)
        except (ValueError, TypeError):
            return None

        if page_num < LOOP_THRESHOLD:
            return None

        # Only warn once per tool name
        pattern_key = f"pagination:{tool_name}"
        if pattern_key in self._warned_patterns:
            return None

        # page >= LOOP_THRESHOLD means we've been paginating (pages are sequential)
        self._warned_patterns.add(pattern_key)
        self.loops_detected += 1

        logger.warning(
            "Pagination loop detected: %s at page=%d",
            tool_name, page_num,
        )

        return (
            f"PAGINATION LOOP DETECTED: You have called {tool_name} up to page {page_num}. "
            f"Stop paginating. The first page contains the highest-severity findings "
            f"sorted by confidence. Summarize what you have and move on. "
            f"Paginating through all results wastes tool calls and increases cost."
        )

    def reset(self) -> None:
        """Reset the detector for a new session."""
        self.recent_calls.clear()
        self._call_counts.clear()
        self._tool_name_counts.clear()
        self._warned_patterns.clear()
        self.loops_detected = 0
