# cache.py
from __future__ import annotations
from typing import Any, Dict, Optional, Protocol

from .client import Client
from .exceptions import HttpError, LayBotError
from .gemini import oa_messages_to_gemini
from .vendor import default_ep


class CacheTooSmallError(LayBotError):
    """Gemini cachedContents 创建内容过小（低于 min_total_token_count）"""
    pass


class CacheNotSupportedError(LayBotError):
    """当前 vendor 不支持“显式缓存资源”的 CRUD。"""
    pass


class _CachesDriver(Protocol):
    def create(
        self,
        model: str,
        contents: list[dict],
        system_instruction: Optional[dict] = None,
        ttl: str = "3600s",
        display_name: Optional[str] = None,
        expire_time: Optional[str] = None,
        tool_config: Optional[dict] = None,
        tools: Optional[list] = None,
    ) -> dict: ...

    def get(self, name: str) -> dict: ...
    def list(self, page_size: int | None = None, page_token: str | None = None) -> dict: ...
    def update(self, name: str, ttl: str | None = None, expire_time: str | None = None) -> dict: ...
    def delete(self, name: str, *, ignore_404: bool = True) -> dict: ...


class _GeminiCachesDriver:
    def __init__(self, cli: Client):
        self.cli = cli
        # 统一从 vendor 表取 endpoint；若不存在则 fallback，避免路径分裂/未来升级漏改
        self._base_ep = default_ep("gemini", "cache") or "/v1beta/cachedContents"

    def _ep(self, suffix: str = "") -> str:
        """
        suffix:
          ""        -> /v1beta/cachedContents
          "/{id}"   -> /v1beta/cachedContents/{id}
        """
        if not suffix:
            return self._base_ep
        return self._base_ep.rstrip("/") + "/" + suffix.lstrip("/")

    @staticmethod
    def _norm_name(name: str) -> str:
        if not name:
            return name
        return name[len("cachedContents/"):] if name.startswith("cachedContents/") else name

    @staticmethod
    def _norm_model(model: str) -> str:
        m = (model or "").strip()
        if m and not m.startswith("models/"):
            m = "models/" + m
        return m

    def create(
        self,
        model: str,
        contents: list[dict],
        system_instruction: Optional[dict] = None,
        ttl: str = "3600s",
        display_name: Optional[str] = None,
        expire_time: Optional[str] = None,
        tool_config: Optional[dict] = None,
        tools: Optional[list] = None,
    ) -> dict:
        body: Dict[str, Any] = {"model": self._norm_model(model), "contents": contents}
        if system_instruction:
            body["systemInstruction"] = system_instruction
        if ttl:
            body["ttl"] = ttl
        if display_name:
            body["displayName"] = display_name
        if expire_time:
            body["expireTime"] = expire_time
        if tool_config:
            body["toolConfig"] = tool_config
        if tools:
            body["tools"] = tools

        try:
            return self.cli.post(self._ep(), body).json()
        except HttpError as e:
            msg = (e.message or "")
            raw = e.raw
            if "Cached content is too small" in msg or "min_total_token_count" in msg:
                raise CacheTooSmallError(msg) from e
            if isinstance(raw, dict):
                emsg = str(((raw.get("error") or {}).get("message")) or "")
                if "Cached content is too small" in emsg or "min_total_token_count" in emsg:
                    raise CacheTooSmallError(emsg) from e
            raise

    def get(self, name: str) -> dict:
        nid = self._norm_name(name)
        return self.cli.get(self._ep(nid)).json()

    def list(self, page_size: int | None = None, page_token: str | None = None) -> dict:
        params: Dict[str, Any] = {}
        if page_size is not None:
            params["pageSize"] = int(page_size)
        if page_token:
            params["pageToken"] = page_token
        return self.cli.get(self._ep(), **params).json()

    def update(self, name: str, ttl: str | None = None, expire_time: str | None = None) -> dict:
        nid = self._norm_name(name)
        body: Dict[str, Any] = {}
        update_mask = []

        if ttl is not None:
            body["ttl"] = ttl
            update_mask.append("ttl")
        if expire_time is not None:
            body["expireTime"] = expire_time
            update_mask.append("expireTime")

        if not body:
            raise LayBotError("Caches.update require ttl or expire_time")

        params = {"updateMask": ",".join(update_mask)}
        return self.cli.patch(self._ep(nid), body, params=params).json()

    def delete(self, name: str, *, ignore_404: bool = True) -> dict:
        nid = self._norm_name(name)
        try:
            r = self.cli.delete(self._ep(nid))
            try:
                return r.json()
            except Exception:
                return {"deleted": True, "name": name}
        except HttpError as e:
            if ignore_404 and e.status_code == 404:
                return {"deleted": True, "name": name, "ignored": "404"}
            raise


class _UnsupportedCachesDriver:
    def __init__(self, vendor: str):
        self.vendor = vendor

    def _raise(self):
        raise CacheNotSupportedError(
            f"vendor={self.vendor} does not support explicit cache resources CRUD. "
            f"Gemini supports cachedContents; OpenAI/Claude usually only implicit caching."
        )

    def create(self, *args, **kwargs) -> dict:
        self._raise()

    def get(self, name: str) -> dict:
        self._raise()

    def list(self, page_size: int | None = None, page_token: str | None = None) -> dict:
        self._raise()

    def update(self, name: str, ttl: str | None = None, expire_time: str | None = None) -> dict:
        self._raise()

    def delete(self, name: str, *, ignore_404: bool = True) -> dict:
        self._raise()


class Caches:
    """
    对外统一入口：
    - vendor=gemini => 显式缓存资源 cachedContents CRUD
    - 其他 vendor => 目前不支持（保留扩展位，未来可接入其他显式缓存机制）
    """
    def __init__(self, api_key: str | Client, *, vendor: str = "gemini", base: str | None = None, **cli_kwargs):
        self.cli = api_key if isinstance(api_key, Client) else Client(
            api_key,
            vendor=vendor,
            base=base,
            **cli_kwargs,
        )
        self.vendor = (self.cli.vendor or vendor).lower()

        if self.vendor == "gemini":
            self._driver: _CachesDriver = _GeminiCachesDriver(self.cli)
        else:
            self._driver = _UnsupportedCachesDriver(self.vendor)

    def create(
        self,
        model: str,
        contents: list[dict],
        system_instruction: Optional[dict] = None,
        ttl: str = "3600s",
        display_name: Optional[str] = None,
        expire_time: Optional[str] = None,
        tool_config: Optional[dict] = None,
        tools: Optional[list] = None,
    ) -> dict:
        return self._driver.create(
            model=model,
            contents=contents,
            system_instruction=system_instruction,
            ttl=ttl,
            display_name=display_name,
            expire_time=expire_time,
            tool_config=tool_config,
            tools=tools,
        )

    def create_from_messages(
        self,
        model: str,
        messages: list[dict],
        *,
        ttl: str = "3600s",
        display_name: Optional[str] = None,
        expire_time: Optional[str] = None,
    ) -> dict:
        sys_inst, contents = oa_messages_to_gemini(messages or [])
        return self.create(
            model=model,
            contents=contents,
            system_instruction=sys_inst,
            ttl=ttl,
            display_name=display_name,
            expire_time=expire_time,
        )

    def get(self, name: str) -> dict:
        return self._driver.get(name)

    def list(self, page_size: int | None = None, page_token: str | None = None) -> dict:
        return self._driver.list(page_size=page_size, page_token=page_token)

    def update(self, name: str, ttl: str | None = None, expire_time: str | None = None) -> dict:
        return self._driver.update(name, ttl=ttl, expire_time=expire_time)

    def delete(self, name: str, *, ignore_404: bool = True) -> dict:
        return self._driver.delete(name, ignore_404=ignore_404)
