"""Context token budget estimation for agent LLM requests.

Uses the same heuristic as legacy ``estimate_tokens`` in ``agent_research``:
approximate tokens = total_relevant_chars / CHARS_PER_TOKEN where each token is
~0.75 characters (inverse: multiply chars by 1/0.75).

Adds **tools schema** payload size, which the legacy message-only estimate omitted.
That omission made proactive trim / summarization thresholds miscalibrate when
MCP or large tool lists inflated the real request.

Runtime transcript trimming (``trim_old_large_tool_results``, emergency trim)
mutates the in-memory ``messages`` list used for the **outbound** API call. The
SQLite-backed chat store may still retain full tool bodies for UI/history unless
a separate write path persists the trimmed copy — see ``chat_storage`` and
``agent_worker`` for persistence vs request-only behavior.
"""

from __future__ import annotations

import json
from typing import Any

# Legacy parity: tokens ≈ chars / 0.75 (see agent_research.estimate_tokens).
CHARS_PER_TOKEN = 0.75


def chars_to_token_estimate(char_count: int) -> int:
    """Convert a character count to an approximate token count."""
    if char_count <= 0:
        return 0
    return int(char_count / CHARS_PER_TOKEN)


def estimate_message_tokens(messages: list | None) -> int:
    """Approximate tokens for message bodies plus serialized assistant ``tool_calls``."""
    total_chars = 0
    for m in messages or []:
        if not isinstance(m, dict):
            continue
        content = m.get("content")
        if isinstance(content, str):
            total_chars += len(content)
        elif isinstance(content, list):
            for part in content:
                if isinstance(part, dict) and isinstance(part.get("text"), str):
                    total_chars += len(part["text"])
        if m.get("tool_calls"):
            try:
                total_chars += len(json.dumps(m.get("tool_calls"), ensure_ascii=False))
            except Exception:
                total_chars += 200
    return chars_to_token_estimate(total_chars)


def estimate_tools_schema_tokens(tools: list | None) -> int:
    """Approximate tokens for the OpenAI ``tools`` JSON array sent on each request."""
    if not tools:
        return 0
    try:
        return chars_to_token_estimate(len(json.dumps(tools, ensure_ascii=False, default=str)))
    except Exception:
        return chars_to_token_estimate(len(str(tools)))


def estimate_request_budget(
    *,
    messages: list | None,
    tools: list | None,
    vision_extra_chars: int = 0,
) -> dict[str, Any]:
    """Return message estimate, tools estimate, and total (approximate tokens)."""
    msg_t = estimate_message_tokens(messages)
    tool_t = estimate_tools_schema_tokens(tools)
    vis_t = chars_to_token_estimate(max(0, int(vision_extra_chars)))
    return {
        "messages_est": msg_t,
        "tools_est": tool_t,
        "vision_est": vis_t,
        "total_est": msg_t + tool_t + vis_t,
    }


def estimate_total_request_tokens(messages: list | None, tools: list | None) -> int:
    """Single scalar total for pressure / cap checks."""
    return int(estimate_request_budget(messages=messages, tools=tools)["total_est"])
