"""Per-image token cost learned from the provider's own usage, never from a vendor formula.

A flat per-image constant is wrong in both directions: a 1920x1080 screenshot costs ~1,100 tokens
on one provider and 4,000+ on a local mmproj model. The provider prices every image exactly on the
request that carries it, so the cost is observable: with a fresh usage anchor (real prompt count of
the previous response), the residual between the next real ``prompt_tokens`` and
``anchor + text-only delta`` is the price of the N images that delta introduced (#70328).

The learned value is kept per ``model@host`` in ``~/.hermes/cache/image_token_costs.json`` so a new
session starts calibrated, and bound per turn through a ContextVar so every estimator
(preflight trigger, tail-budget walk, gateway hygiene) prices images the same way.
"""

from __future__ import annotations

import contextlib
import logging
from contextvars import ContextVar
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

DEFAULT_IMAGE_TOKEN_COST = 1500
# Observations outside this band are text-estimate noise, not an image price.
_MIN_PLAUSIBLE, _MAX_PLAUSIBLE = 64, 32_768
_EMA_ALPHA = 0.5

_image_cost_var: ContextVar[Optional[int]] = ContextVar("hermes_image_token_cost", default=None)
_LEARNED: Dict[str, int] = {}
_LOADED = False
# Routed profiles (multiplexed gateway) keep their own table, loaded from THEIR cache file: the
# module slot above is the launch profile's and would otherwise be persisted into every home.
_LEARNED_BY_HOME: Dict[str, Dict[str, int]] = {}


def _cache_path():
    from agent.model_metadata import _cache_file

    return _cache_file("image_token_costs.json")


def _key(model: Any, base_url: Any) -> str:
    from utils import base_url_hostname

    return f"{model or ''}@{base_url_hostname(base_url or '') or ''}"


def _read_cache() -> Dict[str, int]:
    from agent.model_metadata import _load_json_dict

    return {k: v for k, v in _load_json_dict(_cache_path()).items()
            if isinstance(v, int) and _MIN_PLAUSIBLE <= v <= _MAX_PLAUSIBLE}


def _table() -> Dict[str, int]:
    """The active profile's learned table, loaded lazily from its cache file."""
    global _LOADED
    from hermes_constants import get_hermes_home_override, hermes_home_key

    if get_hermes_home_override() is None:
        if not _LOADED:
            _LOADED = True
            _LEARNED.update(_read_cache())
        return _LEARNED
    home_key = hermes_home_key()
    table = _LEARNED_BY_HOME.get(home_key)
    if table is None:
        table = _LEARNED_BY_HOME[home_key] = _read_cache()
    return table


def learned_image_token_cost(model: Any, base_url: Any) -> int:
    """Learned per-image cost for ``model@host``, else the flat default."""
    return _table().get(_key(model, base_url), DEFAULT_IMAGE_TOKEN_COST)


def current_image_token_cost() -> int:
    """Per-image cost bound for the running turn (see :func:`image_cost_context`), else the default."""
    bound = _image_cost_var.get()
    return bound if bound is not None else DEFAULT_IMAGE_TOKEN_COST


@contextlib.contextmanager
def image_cost_context(cost: Optional[int]):
    token = _image_cost_var.set(cost)
    try:
        yield
    finally:
        _image_cost_var.reset(token)


def bind_image_token_cost(agent: Any) -> None:
    """Bind the agent's learned per-image cost to the current context for the rest of the turn."""
    _image_cost_var.set(learned_image_token_cost(getattr(agent, "model", None), getattr(agent, "base_url", None)))


def count_images(messages: List[Dict[str, Any]]) -> int:
    from agent.model_metadata import _count_image_tokens

    return sum(_count_image_tokens(m, 1) for m in messages if isinstance(m, dict))


def calibrate_from_usage(agent: Any, messages: List[Dict[str, Any]], prompt_tokens: Any) -> Optional[int]:
    """Learn the per-image cost from the response that just priced ``messages``.

    Requires the PREVIOUS anchor (real count of the prior request) to still match: the residual
    ``prompt_tokens - (anchor + text-only delta)`` is then the provider's price for the images the
    delta introduced. Returns the new learned cost, or None when this response teaches nothing
    (no anchor, no new images, implausible residual)."""
    from agent.usage_anchor import anchored_context_tokens

    anchor = getattr(agent, "_usage_anchor", None)
    try:
        real = int(prompt_tokens or 0)
    except (TypeError, ValueError):
        return None
    if real <= 0 or not isinstance(anchor, dict) or not isinstance(messages, list):
        return None
    base_count = int(anchor.get("base_count") or 0)
    delta = messages[base_count:]
    if delta and isinstance(delta[0], dict) and delta[0].get("role") == "assistant":
        delta = delta[1:]
    n_images = count_images(delta)
    if n_images <= 0:
        return None
    with image_cost_context(0):
        text_only = anchored_context_tokens(messages, anchor)
    if text_only is None:
        return None
    per_image = (real - text_only) // n_images
    if not _MIN_PLAUSIBLE <= per_image <= _MAX_PLAUSIBLE:
        return None
    key = _key(getattr(agent, "model", None), getattr(agent, "base_url", None))
    table = _table()
    prior = table.get(key)
    learned = per_image if prior is None else int(prior + _EMA_ALPHA * (per_image - prior))
    table[key] = learned
    _image_cost_var.set(learned)
    try:
        from utils import atomic_json_write

        atomic_json_write(_cache_path(), dict(table), indent=0, separators=(",", ":"))
    except Exception:
        logger.debug("image token cost persist failed", exc_info=True)
    logger.info(
        "Image token cost calibrated from provider usage: %s images priced %s tokens each (learned %s for %s)",
        n_images, f"{per_image:,}", f"{learned:,}", key,
    )
    return learned
