"""Spend-tracking e2e client: a ProxyClient plus the spend-specific read endpoints.

Generic proxy operations (keys, customers, chat/embed, route probing, SpendLogs
polling) come from the shared ProxyClient, DI'd in (composition, not inheritance).
This client adds only the spend surface: /spend/calculate, /spend/tags,
key-spend polling, and the route probes the breadth test uses.

Re-exports unwrap / is_ok / unique_marker / SpendLogRow so the tests import their
helpers from one place.
"""

from __future__ import annotations

import time
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone

from e2e_config import unique_marker
from e2e_http import (
    NoBody,
    ProbeResult,
    Result,
    StreamingResponse,
    Success,
    is_ok,
    unwrap,
)
from models import (
    AnthropicMessagesBody,
    ChatBody,
    ChatMessage,
    ChatMetadata,
    ChatResponse,
    DateRangeParams,
    EmbedBody,
    EmbedResponse,
    OpenAPISchema,
    SpendCalculateBody,
    SpendCalculateResponse,
    SpendLogRow,
    SpendLogsPage,
    SpendLogsPageParams,
    SpendTagsResponse,
    TagSpend,
)
from proxy_client import ProxyClient

__all__ = [
    "ProbeResult",
    "SpendClient",
    "SpendLogRow",
    "build_client",
    "is_ok",
    "unique_marker",
    "unwrap",
]


def _chat_body(
    model: str,
    content: str,
    *,
    max_tokens: int | None = None,
    tags: list[str] | None = None,
    user: str | None = None,
    stream: bool = False,
    cache: dict[str, bool] | None = {"no-cache": True},
) -> ChatBody:
    return ChatBody(
        model=model,
        messages=[ChatMessage(role="user", content=content)],
        max_tokens=max_tokens,
        stream=stream,
        user=user,
        metadata=ChatMetadata(tags=tags) if tags else None,
        cache=cache,
    )


@dataclass(frozen=True, slots=True)
class SpendClient:
    proxy: ProxyClient

    def chat(
        self,
        key: str,
        model: str,
        content: str,
        *,
        max_tokens: int | None = None,
        tags: list[str] | None = None,
        user: str | None = None,
        cache: dict[str, bool] | None = {"no-cache": True},
    ) -> Result[ChatResponse]:
        return self.proxy.chat(
            key,
            _chat_body(model, content, max_tokens=max_tokens, tags=tags, user=user, cache=cache),
        )

    def chat_stream(
        self, key: str, model: str, content: str, *, max_tokens: int | None = None
    ) -> StreamingResponse:
        return self.proxy.chat_stream(
            key, _chat_body(model, content, max_tokens=max_tokens, stream=True)
        )

    def messages_stream(
        self, key: str, model: str, content: str, *, max_tokens: int
    ) -> StreamingResponse:
        return self.proxy.messages_stream(
            key,
            AnthropicMessagesBody(
                model=model,
                messages=[ChatMessage(role="user", content=content)],
                max_tokens=max_tokens,
                stream=True,
            ),
        )

    def embed(self, key: str, model: str, content: str) -> Result[EmbedResponse]:
        return self.proxy.embed(key, EmbedBody(model=model, input=content))

    def poll_logs_for_key(
        self,
        key: str,
        *,
        min_rows: int = 1,
        predicate: Callable[[list[SpendLogRow]], bool] | None = None,
    ) -> list[SpendLogRow]:
        return self.proxy.poll_logs_for_key(
            key, min_rows=min_rows, predicate=predicate
        )

    def calculate_spend(self, model: str, content: str) -> float:
        return unwrap(
            self.proxy.transport.post(
                "/spend/calculate",
                headers=self.proxy.transport.master,
                json=SpendCalculateBody(
                    model=model, messages=[ChatMessage(role="user", content=content)]
                ),
                response_type=SpendCalculateResponse,
            )
        ).cost

    def spend_by_tags(self) -> list[TagSpend]:
        result = self.proxy.transport.get(
            "/spend/tags",
            headers=self.proxy.transport.master,
            params=NoBody(),
            response_type=SpendTagsResponse,
        )
        match result:
            case Success(data=data):
                return data.root
            case _:
                return []

    def poll_tag_spend(self, tag: str, *, minimum: float = 0.0) -> TagSpend | None:
        """Poll /spend/tags until the tag's aggregate reaches `minimum`; last seen."""
        deadline = time.monotonic() + self.proxy.poll_timeout
        entry: TagSpend | None = None
        while time.monotonic() < deadline:
            matches = [
                t for t in self.spend_by_tags() if t.individual_request_tag == tag
            ]
            if matches:
                entry = matches[0]
                if (entry.total_spend or 0.0) >= minimum:
                    return entry
            time.sleep(self.proxy.poll_interval)
        return entry

    def poll_key_spend(self, key: str, *, minimum: float = 0.0) -> float:
        deadline = time.monotonic() + self.proxy.poll_timeout
        spend = 0.0
        while time.monotonic() < deadline:
            spend = self.proxy.key_info(key).spend or 0.0
            if spend > minimum:
                return spend
            time.sleep(self.proxy.poll_interval)
        return spend

    def spend_logs_page(
        self, *, api_key: str | None, page: int, page_size: int
    ) -> SpendLogsPage:
        """One page of /spend/logs/v2 over a window wide enough to contain every
        row this test run wrote (the endpoint requires explicit dates)."""
        now = datetime.now(timezone.utc)
        fmt = "%Y-%m-%d %H:%M:%S"
        return unwrap(
            self.proxy.transport.get(
                "/spend/logs/v2",
                headers=self.proxy.transport.master,
                params=SpendLogsPageParams(
                    start_date=(now - timedelta(days=1)).strftime(fmt),
                    end_date=(now + timedelta(days=1)).strftime(fmt),
                    page=page,
                    page_size=page_size,
                    api_key=api_key,
                ),
                response_type=SpendLogsPage,
            )
        )

    def probe(self, path: str, *, params: DateRangeParams) -> ProbeResult:
        return self.proxy.transport.probe(path, params=params)

    def openapi(self) -> OpenAPISchema:
        return unwrap(
            self.proxy.transport.get(
                "/openapi.json",
                headers=self.proxy.transport.master,
                params=NoBody(),
                response_type=OpenAPISchema,
            )
        )


def build_client(proxy: ProxyClient) -> SpendClient:
    return SpendClient(proxy=proxy)
