"""Classify whether tool output contains usable evidence for the next model turn."""
from __future__ import annotations

import json
from collections.abc import Sequence
from typing import Any


NO_TOOL_EVIDENCE_NUDGE = (
    "The last tool step did not return usable content. Do not infer, quote, or analyze external content from it. "
    "Briefly say what could not be read and ask the user to retry, send a capture, or provide the content."
)

_BROWSER_CAPTURE_TOOLS = frozenset({"request_browser_page", "get_captured_browser_page"})

_BROWSER_NO_EVIDENCE_PREFIXES = (
    "browser capture was not requested",
    "browser capture is disabled",
    "browser capture is busy",
    "browser capture request rate limit reached",
    "browser capture did not return a result",
    "browser capture is still waiting",
    "browser capture did not complete",
    "browser capture job is not complete yet",
    "no captured browser page was found",
    "no browser page has been captured yet",
)

_MODEL_VISIBLE_EVIDENCE_PREFIXES = (
    "the ocr_scanned_pdf result was too long to include here",
    "the ocr_complex_document result was too long to include here",
    "the ocr_with_kraken_api result was too long to include here",
    "the extract_pdf_text result was too long to include here",
)


def _as_text(value: Any) -> str:
    if isinstance(value, str):
        return value
    if value is None:
        return ""
    return str(value)


def _parse_json_object(text: str) -> dict[str, Any] | None:
    try:
        parsed = json.loads(text)
    except Exception:
        return None
    return parsed if isinstance(parsed, dict) else None


def _looks_like_browser_capture_payload(obj: dict[str, Any]) -> bool:
    return bool({"job_id", "final_url", "captured_at", "text", "text_chars"} & set(obj.keys()))


def _int_or_zero(value: Any) -> int:
    try:
        return int(value or 0)
    except (TypeError, ValueError):
        return 0


def _is_large_result_success_stub(text: str) -> bool:
    lowered = text.strip().lower()
    if lowered.startswith(_MODEL_VISIBLE_EVIDENCE_PREFIXES) and "it has been saved to:" in lowered:
        return True
    if "[truncated at" in lowered or "[truncated; save" in lowered:
        return True
    if "[truncated at" in lowered and "summarize from the content above" in lowered:
        return True
    return False


def lacks_usable_evidence(result_text: Any, tool_name: str | None = None) -> bool:
    """Return True when a tool completed but did not provide content to ground an answer.

    This is intentionally narrower than "tool failed": policy blocks, missing files,
    and normal tool exceptions keep their existing failure semantics in the agent loop.
    """
    text = _as_text(result_text).strip()
    name = (tool_name or "").strip()
    if not text:
        return name in _BROWSER_CAPTURE_TOOLS
    if _is_large_result_success_stub(text):
        return False

    obj = _parse_json_object(text)
    if obj is not None and (name in _BROWSER_CAPTURE_TOOLS or _looks_like_browser_capture_payload(obj)):
        body = str(obj.get("text") or "").strip()
        chars = _int_or_zero(obj.get("text_chars"))
        return not body and chars <= 0

    if name in _BROWSER_CAPTURE_TOOLS:
        lowered = text.lower()
        return lowered.startswith(_BROWSER_NO_EVIDENCE_PREFIXES)

    if name == "search_sessions":
        try:
            from tools.session_search_tools import is_search_sessions_success

            return not is_search_sessions_success(text)
        except Exception:
            pass

    return False


def round_lacks_usable_evidence(lacks_flags: Sequence[bool]) -> bool:
    """Round aggregation rule: only trip when every executed tool lacked evidence."""
    flags = list(lacks_flags)
    return bool(flags) and all(flags)


def recent_no_evidence_nudge_present(
    messages: Sequence[dict[str, Any]],
    *,
    nudge: str = NO_TOOL_EVIDENCE_NUDGE,
    scan_messages: int = 12,
) -> bool:
    """Return True if a recent user turn already contains the no-evidence nudge."""
    checked = 0
    for msg in reversed(list(messages)):
        if checked >= scan_messages:
            break
        checked += 1
        if not isinstance(msg, dict) or msg.get("role") != "user":
            continue
        content = msg.get("content")
        if isinstance(content, str) and content.strip() == nudge:
            return True
    return False


def should_inject_no_evidence_nudge(
    lacks_flags: Sequence[bool],
    messages: Sequence[dict[str, Any]],
    *,
    eligible: bool,
    injected_count: int,
    max_injections: int = 2,
) -> bool:
    if not eligible or injected_count >= max_injections:
        return False
    if not round_lacks_usable_evidence(lacks_flags):
        return False
    return not recent_no_evidence_nudge_present(messages)
