"""Detect when the user explicitly names a web tool but the model answered without calling it.

Used by agent_loop for a single bounded retry (see ``mandatory_tool_retry_needed``).
"""
from __future__ import annotations

import json
import re
from typing import Any

from utils.transcript_roles import is_real_user_message

# Longer names first so regex overlap does not mis-detect.
_ORDERED_TOOLS = ("search_web_public", "search_web", "read_webpage")

# User asked for X; these runtime tool names satisfy the obligation (research remaps, etc.).
_TOOL_SATISFACTION_ALIASES: dict[str, frozenset[str]] = {
    "search_web": frozenset({"search_web", "deep_web_search"}),
    "search_web_public": frozenset({"search_web_public"}),
    "read_webpage": frozenset({"read_webpage", "read_and_analyze_page"}),
}

_NEGATION_NEAR_VERB_RE = re.compile(
    r"(?i)\b(do\s*not|don't|never|skip|avoid)\s+(?:call|use|invoke|run)\b",
)


def _last_real_user_index(messages: list[dict[str, Any]]) -> int:
    for i in range(len(messages) - 1, -1, -1):
        if is_real_user_message(messages[i]):
            return i
    return -1


def get_last_real_user_text(messages: list[dict[str, Any]]) -> str:
    i = _last_real_user_index(messages)
    if i < 0:
        return ""
    c = messages[i].get("content")
    if isinstance(c, str):
        return c
    if isinstance(c, list):
        for part in c:
            if isinstance(part, dict) and part.get("type") == "text":
                return str(part.get("text") or "")
    return ""


def _user_negated_tool_use(user_text: str) -> bool:
    """True when the user is clearly asking *not* to run tools (broad heuristic)."""
    return bool(_NEGATION_NEAR_VERB_RE.search(user_text))


def detect_user_commanded_tool(user_text: str) -> str | None:
    """Return built-in tool name if the user explicitly directed invoking it."""
    if not user_text or not user_text.strip():
        return None
    t = user_text.strip()
    if len(t) > 12000:
        t = t[:12000]
    if _user_negated_tool_use(t):
        return None
    for name in _ORDERED_TOOLS:
        pat = (
            rf"(?i)\b(?:call|invoke|run|use)\s+(?:the\s+)?(?:tool\s+)?[`']?"
            rf"{re.escape(name)}[`']?(?:\b|[\s(])"
        )
        if re.search(pat, t):
            return name
        if re.search(rf"(?i)\btool\s+[`']?{re.escape(name)}[`']?\b", t):
            return name
    return None


def _tool_names_match_executed(executed: str, requested: str) -> bool:
    acceptable = _TOOL_SATISFACTION_ALIASES.get(requested, frozenset({requested}))
    return executed in acceptable


def tool_ran_after_user(messages: list[dict[str, Any]], user_idx: int, tool_name: str) -> bool:
    """True if an acceptable tool invocation appears after the last real user message."""
    if user_idx < 0:
        return False
    for m in messages[user_idx + 1 :]:
        if not isinstance(m, dict):
            continue
        role = (m.get("role") or "").strip()
        if role == "tool":
            c = m.get("content")
            if isinstance(c, str) and c.strip():
                try:
                    meta = json.loads(c)
                    if isinstance(meta, dict):
                        tn = meta.get("tool_name")
                        if isinstance(tn, str) and _tool_names_match_executed(tn, tool_name):
                            return True
                except json.JSONDecodeError:
                    pass
        elif role == "assistant":
            for tc in m.get("tool_calls") or []:
                if not isinstance(tc, dict):
                    continue
                fn = tc.get("function")
                if isinstance(fn, dict):
                    tcn = fn.get("name")
                    if isinstance(tcn, str) and _tool_names_match_executed(tcn, tool_name):
                        return True
    return False


def mandatory_tool_retry_needed(
    messages: list[dict[str, Any]],
    allowed_tool_names: set[str],
) -> tuple[str | None, str]:
    """If the user commanded a tool by name and it has not run since their message, return (name, nudge)."""
    uidx = _last_real_user_index(messages)
    if uidx < 0:
        return None, ""
    utext = get_last_real_user_text(messages)
    needed = detect_user_commanded_tool(utext)
    if not needed:
        return None, ""
    acceptable = _TOOL_SATISFACTION_ALIASES.get(needed, frozenset({needed}))
    if not any(t in allowed_tool_names for t in acceptable):
        return None, ""
    if tool_ran_after_user(messages, uidx, needed):
        return None, ""
    nudge = (
        f"The user explicitly asked you to run `{needed}` in this turn. "
        f"You must call `{needed}` now with appropriate arguments — do not answer from memory or invent live web results. "
        f"After the tool returns, summarize what you found."
    )
    return needed, nudge
