"""The Anthropic ModelProvider (Messages API) — the default.

Auth/endpoint stay env-driven exactly as today: the SDK reads
``ANTHROPIC_API_KEY`` and ``ANTHROPIC_BASE_URL`` itself, so pointing a vault at
the V3.5 gateway remains a two-env-var change (docs/V3.5-GATEWAY-PILOT.md).
The ``anthropic`` package is imported lazily (repo precedent — it lives in the
``[digest]`` extra) and only when no client was injected, so tests and
non-digest deploys never need the SDK.
"""
from .base import DEFAULT_MODEL, ModelProvider, ModelReply


class AnthropicProvider(ModelProvider):
    name = "anthropic"

    def __init__(self, model=None, max_retries=2, client=None):
        self._model = model
        self._max_retries = max_retries
        self._client = client  # injectable for tests / the V3.5 library adapter

    def _get_client(self):
        if self._client is None:
            import anthropic  # lazy: [digest] extra, not a base dep
            self._client = anthropic.Anthropic(max_retries=self._max_retries)
        return self._client

    def chat(self, messages, *, system=None, model=None, max_tokens=4096,
             tools=None, temperature=None, stream=False, tool_choice=None,
             cache=False) -> ModelReply:
        if stream:
            raise NotImplementedError("streaming lands with the chat UI (V4); no V3 consumer streams")
        kwargs = dict(model=model or self._model or DEFAULT_MODEL,
                      max_tokens=max_tokens, messages=messages)
        if system is not None:
            kwargs["system"] = system
        if tools:
            kwargs["tools"] = tools
        if temperature is not None:
            kwargs["temperature"] = temperature
        if tool_choice is not None:
            kwargs["tool_choice"] = tool_choice
        if cache:
            kwargs = _cached_kwargs(kwargs)
        msg = self._get_client().messages.create(**kwargs)
        return _reply_from(msg)

    def caps(self) -> dict:
        return {"tools": True, "streaming": False, "prompt_cache": True}


_EPHEMERAL = {"cache_control": {"type": "ephemeral"}}


def _cached_kwargs(kwargs):
    """Copy-on-write prompt-cache breakpoints (≤3 of the API's 4): the system
    prompt, the last tool schema, and the final block of the final message —
    the standard incremental-conversation pattern, so each loop turn re-reads
    its prefix from cache instead of re-paying for it. Caller-owned structures
    (the loop's transcript, the shared tools list) are never mutated — only
    the outgoing request carries the markers."""
    out = dict(kwargs)
    system = out.get("system")
    if isinstance(system, str) and system:
        out["system"] = [dict({"type": "text", "text": system}, **_EPHEMERAL)]
    tools = out.get("tools")
    if tools and isinstance(tools[-1], dict):
        out["tools"] = list(tools[:-1]) + [dict(tools[-1], **_EPHEMERAL)]
    msgs = out.get("messages")
    if msgs:
        last, content = msgs[-1], msgs[-1].get("content")
        blocks = None
        if isinstance(content, str) and content:
            blocks = [dict({"type": "text", "text": content}, **_EPHEMERAL)]
        elif (isinstance(content, list) and content
              and isinstance(content[-1], dict)):   # SDK echo blocks: skip, not ours
            blocks = list(content[:-1]) + [dict(content[-1], **_EPHEMERAL)]
        if blocks is not None:
            out["messages"] = list(msgs[:-1]) + [dict(last, content=blocks)]
    return out


def _reply_from(msg) -> ModelReply:
    """Map an SDK response onto ModelReply. The text join is the byte-exact
    call-site parse (digest.py:1245); fence-stripping is the caller's opt-in
    via ``reply.unfenced()``."""
    text = "".join(b.text for b in msg.content if getattr(b, "type", None) == "text").strip()
    tool_calls = [{"id": b.id, "name": b.name, "input": b.input}
                  for b in msg.content if getattr(b, "type", None) == "tool_use"]
    usage = getattr(msg, "usage", None)
    u = {"input_tokens": getattr(usage, "input_tokens", 0),
         "output_tokens": getattr(usage, "output_tokens", 0)}
    for k in ("cache_creation_input_tokens", "cache_read_input_tokens"):
        v = getattr(usage, k, 0) or 0
        if v:   # only when the API reports them — wire shapes stay stable otherwise
            u[k] = v
    return ModelReply(
        text=text,
        tool_calls=tool_calls,
        usage=u,
        stop_reason=getattr(msg, "stop_reason", "") or "",
        raw_content=msg.content,
    )
