"""OpenAI 消息与数据库存储之间的序列化。"""

from __future__ import annotations

import json
from typing import Any


def json_safe_dumps(
    obj: Any,
    *,
    ensure_ascii: bool = False,
    max_len: int | None = None,
) -> str:
    """``json.dumps`` 的安全包装：不可序列化对象用 ``repr``；循环引用时退化为摘要。"""

    def _default(o: Any) -> str:
        try:
            r = repr(o)
        except Exception:  # noqa: BLE001
            return f"<{type(o).__name__}>"
        return r if len(r) <= 8000 else r[:7997] + "..."

    try:
        s = json.dumps(obj, ensure_ascii=ensure_ascii, default=_default)
    except (TypeError, ValueError):
        s = _default(obj)
    if max_len is not None and len(s) > max_len:
        return s[: max_len - 24] + '\n..."__truncated__"'
    return s


def tool_result_content_for_llm(result: Any, *, max_len: int = 24_000) -> str:
    """工具返回写入 ``role=tool`` / SQLite 时使用，避免插件误传 ``ToolRouter`` 等导致崩溃。"""
    return json_safe_dumps(result, ensure_ascii=False, max_len=max_len)

# 多模态上下文粗估：与系统提示一致，**每张** vision 图统一按 **8000 token** 计入
# （与 `context_budget_tokens`、hydrate 裁剪对齐；实际计费以各模型为准）。
VISION_IMAGE_TOKENS_PER_IMAGE = 8000
VISION_IMAGE_TOKENS_ESTIMATE = VISION_IMAGE_TOKENS_PER_IMAGE
# 兼容旧名
VISION_IMAGE_TOKENS_MIN = 2000
VISION_IMAGE_TOKENS_MAX = VISION_IMAGE_TOKENS_PER_IMAGE


def tool_calls_to_jsonable(tool_calls: Any) -> list[dict[str, Any]]:
    if not tool_calls:
        return []
    out: list[dict[str, Any]] = []
    for tc in tool_calls:
        fn = getattr(tc, "function", None)
        name = getattr(fn, "name", "") if fn else ""
        args = getattr(fn, "arguments", None) if fn else None
        out.append(
            {
                "id": getattr(tc, "id", "") or "",
                "type": getattr(tc, "type", None) or "function",
                "function": {
                    "name": name,
                    "arguments": args if isinstance(args, str) else (args or "{}"),
                },
            }
        )
    return out


def estimate_message_tokens(msg: dict[str, Any]) -> int:
    """
    粗估 token：纯文本约 4 字符/token；含 ``tool_calls`` JSON。
    若 ``content`` 为 OpenAI 多模态列表（``text`` + ``image_url``），
    每张 ``image_url`` 按 ``VISION_IMAGE_TOKENS_PER_IMAGE``（**8000**）加计，便于与 ``context_budget_tokens`` 对齐。
    """
    raw_c = msg.get("content")
    if isinstance(raw_c, list):
        text_chars = 0
        image_n = 0
        for part in raw_c:
            if not isinstance(part, dict):
                continue
            if part.get("type") == "text":
                text_chars += len(str(part.get("text") or ""))
            elif part.get("type") == "image_url":
                image_n += 1
        text_tok = (text_chars // 4 + 1) if text_chars else 0
        vision_tok = image_n * VISION_IMAGE_TOKENS_PER_IMAGE
        tc = msg.get("tool_calls")
        extra = len(json.dumps(tc, ensure_ascii=False)) // 4 + 1 if tc else 0
        return max(1, text_tok + vision_tok + extra)

    n = len(str(raw_c or ""))
    tc = msg.get("tool_calls")
    if tc:
        n += len(json.dumps(tc, ensure_ascii=False))
    return max(1, n // 4 + 1)
