from __future__ import annotations

import json
import logging
import random
import re
import time
import uuid
from collections.abc import Callable, Iterable
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, cast

from openai.types.chat import ChatCompletionMessageParam
from vv_llm.chat_clients import create_chat_client, format_messages, get_token_counts
from vv_llm.settings import Settings
from vv_llm.types import APIConnectionError, APIStatusError, BackendType

from vv_agent.llm.base import LLMClient
from vv_agent.types import LLMResponse, Message, ToolCall

_STREAM_MODEL_PREFIXES = (
    "qwen3",
    "claude",
    "gemini",
    "kimi",
    "glm-4.",
    "glm-5",
    "gpt-5",
    "MiniMax",
)

_STREAM_MODEL_EXACT = {
    "deepseek-reasoner",
    "deepseek-r1-tools",
}

_STREAM_MODEL_PREFIXES_LOWER = tuple(prefix.lower() for prefix in _STREAM_MODEL_PREFIXES)
_STREAM_MODEL_EXACT_LOWER = {item.lower() for item in _STREAM_MODEL_EXACT}

_DEEPSEEK_REASONING_MODELS = (
    "deepseek-reasoner",
    "deepseek-r1-tools",
)

_MINIMAX_REASONING_MODELS = (
    "minimax-m2.1",
    "minimax-m2.1-lightning",
    "minimax-m2.1-highspeed",
    "minimax-m2.5",
    "minimax-m2.5-highspeed",
)

_MOONSHOT_REASONING_MODELS = (
    "kimi-k2-thinking",
    "kimi-k2.5",
)

_REASONING_CHAIN_MODELS = {
    *_DEEPSEEK_REASONING_MODELS,
    *_MINIMAX_REASONING_MODELS,
    *_MOONSHOT_REASONING_MODELS,
}

_REASONING_CHAIN_PREFIXES = (
    "deepseek-",
    "minimax-m2.",
)

_CLAUDE_THINKING_MODELS = (
    "claude-3-7-sonnet-thinking",
    "claude-opus-4-20250514-thinking",
    "claude-opus-4-1-20250805-thinking",
    "claude-sonnet-4-20250514-thinking",
    "claude-sonnet-4-5-20250929-thinking",
    "claude-opus-4-5-20251101-thinking",
    "claude-opus-4-6-thinking",
    "claude-sonnet-4-6-thinking",
)
_CLAUDE_THINKING_MODELS_LOWER = {item.lower() for item in _CLAUDE_THINKING_MODELS}

_QWEN_THINKING_KEEP_SUFFIX_MODELS = (
    "qwen3-next-80b-a3b-thinking",
    "qwen3-vl-235b-a22b-thinking",
    "qwen3-vl-32b-thinking",
    "qwen3-vl-30b-a3b-thinking",
    "qwen3-vl-8b-thinking",
)
_QWEN_THINKING_KEEP_SUFFIX_MODELS_LOWER = {item.lower() for item in _QWEN_THINKING_KEEP_SUFFIX_MODELS}

_TOOL_CALL_INCREMENTAL_MODELS = {
    "qwen3-coder-plus",
    "qwen3-coder-flash",
    "qwen3-max",
    "qwen3-max-preview",
    "qwen3-next-80b-a3b-thinking",
    "qwen3-next-80b-a3b-instruct",
    "qwen3-235b-a22b-instruct-2507",
    "qwen3-coder-480b-a35b-instruct",
    "qwen3-235b-a22b",
    "qwen3-235b-a22b-thinking",
    "qwen3-32b",
    "qwen3-32b-thinking",
    "qwen3-30b-a3b",
    "qwen3-30b-a3b-thinking",
    "qwen3-14b",
    "qwen3-14b-thinking",
    "qwen3-8b",
    "qwen3-8b-thinking",
    "qwen3-4b",
    "qwen3-4b-thinking",
    "qwen3-1.7b",
    "qwen3-1.7b-thinking",
    "qwen3-0.6b",
    "qwen3-0.6b-thinking",
    "mixtral-8x7b",
}
_TOOL_CALL_INCREMENTAL_MODELS_LOWER = {item.lower() for item in _TOOL_CALL_INCREMENTAL_MODELS}

_TOOL_CALL_INCREMENTAL_ENDPOINT_PREFIXES = (
    "openai",
    "moonshot",
    "anthropic",
    "deepseek",
    "minimax",
    "zhipuai",
)


@dataclass(slots=True)
class _RequestOptions:
    model: str
    temperature: float | None = None
    max_tokens: int | None = None
    thinking: dict[str, Any] | None = None
    reasoning_effort: str | None = None
    extra_body: dict[str, Any] | None = None
    is_gemini_3_model: bool = False
    tool_call_incremental: bool = True


@dataclass(slots=True)
class EndpointTarget:
    endpoint_id: str
    api_key: str
    api_base: str
    endpoint_type: str = "default"
    model_id: str | None = None


@dataclass(slots=True)
class VVLlmClient(LLMClient):
    endpoint_targets: list[EndpointTarget]
    backend: str = "openai"
    selected_model: str | None = None
    settings: Settings | None = None
    timeout_seconds: float = 90.0
    max_retries_per_endpoint: int = 3
    backoff_seconds: float = 2.0
    randomize_endpoints: bool = True
    debug_dump_dir: str | None = None
    _preferred_endpoint_id: str | None = field(default=None, init=False, repr=False)
    _request_counter: int = field(default=0, init=False, repr=False)

    def complete(
        self,
        *,
        model: str,
        messages: list[Message],
        tools: list[dict[str, object]],
        stream_callback: Callable[[str], None] | None = None,
    ) -> LLMResponse:
        if not self.endpoint_targets:
            raise RuntimeError("No endpoint targets configured")

        backend_type = self._resolve_backend_type(self.backend)
        model_name = self._current_model_name(model)
        settings = self._ensure_settings(model)
        message_payload = self._build_message_payload(
            messages,
            preserve_reasoning_chain=self._should_preserve_reasoning_chain(model),
        )
        tool_payload = self._build_tool_payload(tools)

        ordered_targets = self._ordered_targets()
        errors: list[str] = []
        last_error: Exception | None = None

        for target in ordered_targets:
            selected_model_id = target.model_id or model
            should_stream = self._should_use_stream(selected_model_id)
            request_options = self._resolve_request_options(
                selected_model_id,
                stream=should_stream,
                endpoint_type=target.endpoint_type,
            )
            request_messages = self._prepare_messages_for_model(message_payload, request_options.model)
            formatted_messages = self._format_messages_for_request(
                settings=settings,
                backend_type=backend_type,
                endpoint_type=target.endpoint_type,
                model_name=model_name,
                messages=request_messages,
            )
            self._dump_request_messages(formatted_messages, model_name=model_name)

            for attempt in range(1, self.max_retries_per_endpoint + 1):
                try:
                    chat_client = create_chat_client(
                        backend=backend_type,
                        model=model_name,
                        stream=should_stream,
                        random_endpoint=False,
                        endpoint_id=target.endpoint_id,
                        settings=settings,
                    )
                    if should_stream:
                        response = self._stream_completion(
                            chat_client=chat_client,
                            options=request_options,
                            messages=formatted_messages,
                            model_name=model_name,
                            tool_payload=tool_payload,
                            stream_callback=stream_callback,
                        )
                    else:
                        response = self._non_stream_completion(
                            chat_client=chat_client,
                            options=request_options,
                            messages=formatted_messages,
                            model_name=model_name,
                            tool_payload=tool_payload,
                        )

                    response.raw["used_endpoint_id"] = target.endpoint_id
                    response.raw["used_model_id"] = request_options.model
                    response.raw["stream_mode"] = should_stream
                    self._preferred_endpoint_id = target.endpoint_id
                    return response
                except APIConnectionError as exc:
                    last_error = exc
                    errors.append(f"{target.endpoint_id}: network timeout/connection error (attempt {attempt})")
                    if attempt < self.max_retries_per_endpoint:
                        self._sleep_backoff(attempt)
                        continue
                    break
                except APIStatusError as exc:
                    last_error = exc
                    status = exc.status_code
                    detail = getattr(exc, 'message', '') or str(getattr(exc, 'body', ''))
                    errors.append(f"{target.endpoint_id}: status {status} - {detail} (attempt {attempt})")
                    if status in {429, 500, 502, 503, 504, 408} and attempt < self.max_retries_per_endpoint:
                        self._sleep_backoff(attempt)
                        continue
                    break
                except Exception as exc:
                    last_error = exc
                    errors.append(f"{target.endpoint_id}: unexpected {type(exc).__name__} (attempt {attempt})")
                    if attempt < self.max_retries_per_endpoint:
                        self._sleep_backoff(attempt)
                        continue
                    break

        details = "; ".join(errors) if errors else "no attempts made"
        raise RuntimeError(f"All endpoints failed: {details}") from last_error

    def _ensure_settings(self, model: str) -> Settings:
        if self.settings is not None:
            return self.settings

        backend = self.backend.strip().lower()
        model_name = self._current_model_name(model)
        endpoint_options = [
            {
                "endpoint_id": target.endpoint_id,
                "model_id": target.model_id or model,
            }
            for target in self.endpoint_targets
        ]
        settings_data = {
            "VERSION": "2",
            "backends": {
                backend: {
                    "default_endpoint": self.endpoint_targets[0].endpoint_id,
                    "models": {
                        model_name: {
                            "id": model,
                            "endpoints": endpoint_options,
                            "function_call_available": True,
                            "native_multimodal": True,
                        }
                    },
                }
            },
            "endpoints": [
                {
                    "id": target.endpoint_id,
                    "api_key": target.api_key,
                    "api_base": target.api_base,
                    "endpoint_type": target.endpoint_type,
                }
                for target in self.endpoint_targets
            ],
        }
        self.settings = Settings(**settings_data)
        return self.settings

    def _build_message_payload(
        self,
        messages: list[Message],
        *,
        preserve_reasoning_chain: bool = False,
    ) -> list[ChatCompletionMessageParam]:
        last_assistant_index = max(
            (index for index, message in enumerate(messages) if message.role == "assistant"),
            default=-1,
        )

        payload: list[ChatCompletionMessageParam] = []
        for index, message in enumerate(messages):
            include_reasoning = message.role == "assistant" and (
                preserve_reasoning_chain or index == last_assistant_index
            )
            item = message.to_openai_message(include_reasoning_content=include_reasoning)
            if preserve_reasoning_chain and message.role == "assistant" and "reasoning_content" not in item:
                # Moonshot/DeepSeek/MiniMax reasoning tool-call flows require this field.
                item["reasoning_content"] = message.reasoning_content or ""
            payload.append(cast(ChatCompletionMessageParam, item))
        return payload

    def _should_preserve_reasoning_chain(self, requested_model: str) -> bool:
        for candidate in self._iter_reasoning_model_candidates(requested_model):
            normalized = candidate.strip().lower()
            if not normalized:
                continue
            if normalized in _REASONING_CHAIN_MODELS:
                return True
            if normalized.startswith(_REASONING_CHAIN_PREFIXES):
                return True
        return False

    def _iter_reasoning_model_candidates(self, requested_model: str) -> list[str]:
        candidates: list[str] = [requested_model]
        if self.selected_model:
            candidates.append(self.selected_model)
        for target in self.endpoint_targets:
            if target.model_id:
                candidates.append(target.model_id)
        return candidates

    @staticmethod
    def _resolve_backend_type(raw_backend: str) -> BackendType:
        normalized = raw_backend.strip().lower()
        try:
            return BackendType(normalized)
        except ValueError as exc:
            raise RuntimeError(f"Unsupported backend for vv-llm: {raw_backend!r}") from exc

    def _current_model_name(self, fallback: str) -> str:
        if self.selected_model and self.selected_model.strip():
            return self.selected_model.strip()
        return fallback

    def _ordered_targets(self) -> list[EndpointTarget]:
        targets = list(self.endpoint_targets)

        if self._preferred_endpoint_id:
            preferred_index = None
            for index, target in enumerate(targets):
                if target.endpoint_id == self._preferred_endpoint_id:
                    preferred_index = index
                    break
            if preferred_index is not None:
                preferred = targets.pop(preferred_index)
                if self.randomize_endpoints:
                    random.shuffle(targets)
                targets.insert(0, preferred)
                return targets

        if self.randomize_endpoints:
            random.shuffle(targets)
        return targets

    def _format_messages_for_request(
        self,
        *,
        settings: Settings,
        backend_type: BackendType,
        endpoint_type: str,
        model_name: str,
        messages: list[ChatCompletionMessageParam],
    ) -> list[dict[str, Any]]:
        try:
            backend_settings = settings.get_backend(backend_type)
            model_settings = backend_settings.get_model_setting(model_name)
            format_backend = BackendType.OpenAI if endpoint_type.startswith("openai") else backend_type
            formatted = format_messages(
                messages=messages,
                backend=format_backend,
                native_multimodal=model_settings.native_multimodal,
                function_call_available=model_settings.function_call_available,
            )
            return cast(list[dict[str, Any]], formatted)
        except Exception:
            return [cast(dict[str, Any], dict(message)) for message in messages]

    @staticmethod
    def _build_tool_payload(tools: list[dict[str, object]]) -> list[dict[str, Any]]:
        if not tools:
            return []

        payload: list[dict[str, Any]] = []
        for schema in tools:
            schema_type = schema.get("type")
            schema_function = schema.get("function")
            if schema_type == "function" and isinstance(schema_function, dict):
                payload.append(cast(dict[str, Any], schema))
            else:
                payload.append({"type": "function", "function": schema})
        return payload

    @staticmethod
    def _should_use_stream(model: str) -> bool:
        normalized = model.lower()
        if normalized in _STREAM_MODEL_EXACT_LOWER:
            return True
        return normalized.startswith(_STREAM_MODEL_PREFIXES_LOWER)

    @staticmethod
    def _prepare_messages_for_model(
        messages: list[ChatCompletionMessageParam],
        model: str,
    ) -> list[ChatCompletionMessageParam]:
        # MiniMax endpoints reject requests with multiple system-role turns.
        minimax_strict_system = model.lower().startswith("minimax")
        prepared: list[ChatCompletionMessageParam] = []
        seen_system = False

        for raw_message in messages:
            message = dict(raw_message)
            role = message.get("role")
            if role == "system":
                if not seen_system:
                    seen_system = True
                    prepared.append(cast(ChatCompletionMessageParam, message))
                    continue
                if minimax_strict_system:
                    summary_content = message.get("content")
                    summary_text = summary_content if isinstance(summary_content, str) else ""
                    prefix = "[memory_summary]\n" if message.get("name") == "memory_summary" else ""
                    prepared.append(
                        cast(
                            ChatCompletionMessageParam,
                            {
                                "role": "user",
                                "content": f"{prefix}{summary_text}".strip(),
                            },
                        )
                    )
                    continue

            prepared.append(cast(ChatCompletionMessageParam, message))

        return prepared

    def _resolve_request_options(self, model: str, *, stream: bool, endpoint_type: str | None) -> _RequestOptions:
        resolved_model = model
        normalized_model = resolved_model.lower()
        temperature: float | None = None
        max_tokens: int | None = None
        thinking: dict[str, Any] | None = None
        reasoning_effort: str | None = None
        extra_body: dict[str, Any] | None = None

        if normalized_model in _DEEPSEEK_REASONING_MODELS:
            temperature = 0.6
        elif normalized_model in _CLAUDE_THINKING_MODELS_LOWER:
            resolved_model = self._remove_suffix_case_insensitive(resolved_model, "-thinking")
            normalized_model = resolved_model.lower()
            thinking = {"type": "enabled", "budget_tokens": 16000}
            temperature = 1.0
            max_tokens = 20000

        if normalized_model in ("o3-mini-high", "o4-mini-high"):
            reasoning_effort = "high"
            resolved_model = self._remove_suffix_case_insensitive(resolved_model, "-high")
            normalized_model = resolved_model.lower()

        if normalized_model.startswith("gpt-5") and normalized_model.endswith("-high"):
            reasoning_effort = "high"
            resolved_model = self._remove_suffix_case_insensitive(resolved_model, "-high")
            normalized_model = resolved_model.lower()

        if stream and normalized_model.startswith("qwen3"):
            if normalized_model.endswith("-thinking"):
                if normalized_model not in _QWEN_THINKING_KEEP_SUFFIX_MODELS_LOWER:
                    resolved_model = self._remove_suffix_case_insensitive(resolved_model, "-thinking")
                    normalized_model = resolved_model.lower()
                extra_body = {"enable_thinking": True}
            else:
                extra_body = {"enable_thinking": False}

        if normalized_model.startswith(("glm-4.", "glm-5")) and normalized_model.endswith("-thinking"):
            resolved_model = self._remove_suffix_case_insensitive(resolved_model, "-thinking")
            normalized_model = resolved_model.lower()
            extra_body = {"thinking": {"type": "enabled"}, "tool_stream": True} if stream else {"thinking": {"type": "enabled"}}

        if normalized_model.startswith("gemini-2.5"):
            reasoning_effort = None
            extra_body = {
                "extra_body": {
                    "google": {
                        "thinking_config": {
                            "thinkingBudget": -1,
                            "include_thoughts": True,
                        }
                    }
                }
            }

        is_gemini_3_model = normalized_model.startswith("gemini-3")
        if is_gemini_3_model:
            if temperature is None:
                temperature = 1.0
            if normalized_model in {"gemini-3-pro", "gemini-3-flash"}:
                resolved_model = f"{resolved_model}-preview"
                normalized_model = resolved_model.lower()
            reasoning_effort = None
            extra_body = {
                "extra_body": {
                    "google": {
                        "thinking_config": {
                            "thinkingLevel": "high",
                            "include_thoughts": True,
                        }
                    }
                }
            }

        return _RequestOptions(
            model=resolved_model,
            temperature=temperature,
            max_tokens=max_tokens,
            thinking=thinking,
            reasoning_effort=reasoning_effort,
            extra_body=extra_body,
            is_gemini_3_model=is_gemini_3_model,
            tool_call_incremental=self._tool_call_incremental_enabled(
                model=resolved_model,
                endpoint_type=endpoint_type,
            ),
        )

    @staticmethod
    def _tool_call_incremental_enabled(*, model: str, endpoint_type: str | None) -> bool:
        normalized_model = model.lower()
        if normalized_model in _TOOL_CALL_INCREMENTAL_MODELS_LOWER or normalized_model.startswith("qwen3"):
            return True

        normalized_endpoint = (endpoint_type or "").strip().lower()
        if not normalized_endpoint or normalized_endpoint == "default":
            return True

        return normalized_endpoint.startswith(_TOOL_CALL_INCREMENTAL_ENDPOINT_PREFIXES)

    @staticmethod
    def _remove_suffix_case_insensitive(value: str, suffix: str) -> str:
        if value.lower().endswith(suffix.lower()):
            return value[: -len(suffix)]
        return value

    def _build_request_payload(
        self,
        *,
        options: _RequestOptions,
        messages: list[dict[str, Any]],
        model_name: str,
        tool_payload: list[dict[str, Any]],
        stream: bool,
    ) -> dict[str, Any]:
        payload: dict[str, Any] = {
            "messages": messages,
            "model": model_name,
            "stream": stream,
            "skip_cutoff": True,
            "timeout": self.timeout_seconds,
        }
        if tool_payload:
            payload["tools"] = tool_payload
        if stream:
            payload["stream_options"] = {"include_usage": True}
        if options.temperature is not None:
            payload["temperature"] = options.temperature
        if options.max_tokens is not None:
            payload["max_tokens"] = options.max_tokens
        if options.thinking is not None:
            payload["thinking"] = options.thinking
        if options.reasoning_effort is not None:
            payload["reasoning_effort"] = options.reasoning_effort
        if options.extra_body is not None:
            payload["extra_body"] = options.extra_body
        return payload

    def _non_stream_completion(
        self,
        *,
        chat_client: Any,
        options: _RequestOptions,
        messages: list[dict[str, Any]],
        model_name: str,
        tool_payload: list[dict[str, Any]],
    ) -> LLMResponse:
        payload = self._build_request_payload(
            options=options,
            messages=messages,
            model_name=model_name,
            tool_payload=tool_payload,
            stream=False,
        )

        response = chat_client.create_completion(**payload)
        parsed_tool_calls = self._parse_non_stream_tool_calls(self._read_field(response, "tool_calls"))
        reasoning_content = self._extract_reasoning_content(self._read_field(response, "reasoning_content"))
        if not reasoning_content:
            reasoning_content = self._extract_reasoning_content(self._read_field(response, "reasoning"))

        content = self._extract_content(self._read_field(response, "content"))
        usage_dump = self._usage_to_dict(self._read_field(response, "usage"))
        if not usage_dump:
            usage_dump = self._estimate_usage(messages=messages, content=content, model=options.model)

        raw_payload: dict[str, Any] = {
            "usage": usage_dump,
            "stream_collected": False,
        }
        if reasoning_content:
            raw_payload["reasoning_content"] = reasoning_content

        raw_content = self._read_field(response, "raw_content")
        if raw_content is not None:
            raw_payload["raw_content"] = raw_content

        return LLMResponse(
            content=content,
            tool_calls=parsed_tool_calls,
            raw=raw_payload,
        )

    def _stream_completion(
        self,
        *,
        chat_client: Any,
        options: _RequestOptions,
        messages: list[dict[str, Any]],
        model_name: str,
        tool_payload: list[dict[str, Any]],
        stream_callback: Callable[[str], None] | None = None,
    ) -> LLMResponse:
        payload = self._build_request_payload(
            options=options,
            messages=messages,
            model_name=model_name,
            tool_payload=tool_payload,
            stream=True,
        )

        stream = cast(Iterable[Any], chat_client.create_completion(**payload))

        content_parts: list[str] = []
        reasoning_parts: list[str] = []
        complete_raw_content: list[dict[str, Any]] = []
        tool_call_parts: dict[str, dict[str, Any]] = {}
        last_active_tool_call_id: str | None = None
        usage_dump: dict[str, Any] | None = None

        for chunk in stream:
            usage = self._read_field(chunk, "usage")
            usage_candidate = self._usage_to_dict(usage)
            if usage_candidate:
                usage_dump = usage_candidate

            chunk_reasoning = self._extract_reasoning_content(self._read_field(chunk, "reasoning_content"))
            if chunk_reasoning:
                reasoning_parts.append(chunk_reasoning)

            raw_content = self._read_field(chunk, "raw_content")
            if raw_content is not None:
                self._collect_raw_content(complete_raw_content, raw_content)

            text = self._extract_content(self._read_field(chunk, "content"))
            if text:
                content_parts.append(text)
                if stream_callback is not None:
                    stream_callback(text)

            for tool_call_index, tool_delta in enumerate(self._read_field(chunk, "tool_calls") or []):
                last_active_tool_call_id = self._accumulate_tool_call_delta(
                    tool_call_parts=tool_call_parts,
                    tool_delta=tool_delta,
                    default_index=tool_call_index,
                    last_active_tool_call_id=last_active_tool_call_id,
                    incremental=options.tool_call_incremental,
                    keep_extra_content=options.is_gemini_3_model,
                )

        parsed_tool_calls: list[ToolCall] = []
        tool_call_extra_content: dict[str, Any] = {}
        for _, slot in sorted(tool_call_parts.items(), key=self._tool_call_sort_key):
            name = str(slot.get("name", "")).strip()
            if not name:
                continue
            raw_arguments = str(slot.get("arguments", ""))
            tool_id = str(slot.get("id") or f"call_{uuid.uuid4().hex[:12]}")
            parsed_tool_calls.append(
                ToolCall(
                    id=tool_id,
                    name=name,
                    arguments=self._parse_arguments(raw_arguments),
                )
            )
            if options.is_gemini_3_model and "extra_content" in slot:
                tool_call_extra_content[tool_id] = slot["extra_content"]

        normalized = self._normalize_tool_calls(parsed_tool_calls)
        final_usage = usage_dump or self._estimate_usage(
            messages=messages,
            content="".join(content_parts),
            model=options.model,
        )
        raw_payload: dict[str, Any] = {
            "usage": final_usage,
            "stream_collected": True,
        }
        if reasoning_parts:
            raw_payload["reasoning_content"] = "".join(reasoning_parts)
        if complete_raw_content:
            raw_payload["raw_content"] = complete_raw_content
        if tool_call_extra_content:
            raw_payload["tool_call_extra_content"] = tool_call_extra_content

        return LLMResponse(
            content="".join(content_parts),
            tool_calls=normalized,
            raw=raw_payload,
        )

    @staticmethod
    def _tool_call_sort_key(item: tuple[str, dict[str, Any]]) -> tuple[int, str]:
        call_id, _ = item
        index_text, _, _ = call_id.partition("_")
        try:
            index = int(index_text)
        except ValueError:
            index = 10**9
        return index, call_id

    @classmethod
    def _collect_raw_content(cls, complete_raw_content: list[dict[str, Any]], chunk_raw_content: Any) -> None:
        if isinstance(chunk_raw_content, list):
            for item in chunk_raw_content:
                cls._collect_raw_content(complete_raw_content, item)
            return

        if not isinstance(chunk_raw_content, dict):
            return

        chunk_type_raw = chunk_raw_content.get("type")
        chunk_type = chunk_type_raw if isinstance(chunk_type_raw, str) else ""

        if chunk_type == "thinking_delta":
            thinking_block = cls._find_or_create_raw_block(
                complete_raw_content,
                block_type="thinking",
                defaults={"thinking": "", "signature": ""},
            )
            thinking_block["thinking"] = f"{thinking_block.get('thinking', '')}{chunk_raw_content.get('thinking', '')}"
            return

        if chunk_type == "signature_delta":
            thinking_block = cls._find_or_create_raw_block(
                complete_raw_content,
                block_type="thinking",
                defaults={"thinking": "", "signature": ""},
            )
            thinking_block["signature"] = f"{thinking_block.get('signature', '')}{chunk_raw_content.get('signature', '')}"
            return

        if chunk_type == "text_delta":
            text_block = cls._find_or_create_raw_block(
                complete_raw_content,
                block_type="text",
                defaults={"text": ""},
            )
            text_block["text"] = f"{text_block.get('text', '')}{chunk_raw_content.get('text', '')}"
            return

        if chunk_type == "input_json_delta":
            # input_json_delta is already aggregated via tool call deltas.
            return

        if chunk_type in {"thinking", "text", "tool_use"}:
            if not cls._raw_block_exists(complete_raw_content, chunk_raw_content):
                complete_raw_content.append(dict(chunk_raw_content))
            return

        complete_raw_content.append(dict(chunk_raw_content))

    @staticmethod
    def _find_or_create_raw_block(
        blocks: list[dict[str, Any]],
        *,
        block_type: str,
        defaults: dict[str, Any],
    ) -> dict[str, Any]:
        for block in blocks:
            if isinstance(block, dict) and block.get("type") == block_type:
                return block
        new_block = {"type": block_type, **defaults}
        blocks.append(new_block)
        return new_block

    @staticmethod
    def _raw_block_exists(blocks: list[dict[str, Any]], candidate: dict[str, Any]) -> bool:
        candidate_type = candidate.get("type")
        candidate_id = candidate.get("id")
        for block in blocks:
            if not isinstance(block, dict):
                continue
            if block.get("type") != candidate_type:
                continue
            if candidate_id is not None and block.get("id") == candidate_id:
                return True
            if candidate_id is None and block == candidate:
                return True
        return False

    def _accumulate_tool_call_delta(
        self,
        *,
        tool_call_parts: dict[str, dict[str, Any]],
        tool_delta: Any,
        default_index: int,
        last_active_tool_call_id: str | None,
        incremental: bool,
        keep_extra_content: bool,
    ) -> str | None:
        function = self._read_field(tool_delta, "function")
        if function is None:
            return last_active_tool_call_id

        index_raw = self._read_field(tool_delta, "index")
        index = index_raw if isinstance(index_raw, int) else default_index

        name_raw = self._read_field(function, "name")
        name = name_raw.strip() if isinstance(name_raw, str) else ""
        arguments_raw = self._read_field(function, "arguments")
        arguments = arguments_raw if isinstance(arguments_raw, str) else ""

        delta_id_raw = self._read_field(tool_delta, "id")
        delta_id = delta_id_raw.strip() if isinstance(delta_id_raw, str) else ""

        extra_content = self._read_field(tool_delta, "extra_content") if keep_extra_content else None

        if name:
            tool_id = delta_id or f"generated_{index}_{len(tool_call_parts)}"
            unique_id = f"{index}_{tool_id}"

            if unique_id in tool_call_parts:
                slot = tool_call_parts[unique_id]
                if arguments:
                    slot["arguments"] = self._merge_tool_arguments(
                        existing=str(slot.get("arguments", "")),
                        incoming=arguments,
                        incremental=incremental,
                    )
                if delta_id and (not slot.get("id") or str(slot["id"]).startswith("generated_")):
                    slot["id"] = delta_id
                if keep_extra_content and extra_content:
                    slot["extra_content"] = extra_content
            else:
                tool_call_parts[unique_id] = {
                    "id": tool_id,
                    "name": name,
                    "arguments": arguments,
                }
                if keep_extra_content and extra_content:
                    tool_call_parts[unique_id]["extra_content"] = extra_content

            return unique_id

        if not arguments and not delta_id:
            return last_active_tool_call_id

        target_id = last_active_tool_call_id
        if target_id is None and delta_id:
            for existing_id, slot in tool_call_parts.items():
                if slot.get("id") == delta_id:
                    target_id = existing_id
                    break

        if target_id and target_id in tool_call_parts:
            slot = tool_call_parts[target_id]
            if arguments:
                slot["arguments"] = self._merge_tool_arguments(
                    existing=str(slot.get("arguments", "")),
                    incoming=arguments,
                    incremental=incremental,
                )
            if delta_id and (not slot.get("id") or str(slot["id"]).startswith("generated_")):
                slot["id"] = delta_id

        return target_id

    @staticmethod
    def _merge_tool_arguments(*, existing: str, incoming: str, incremental: bool) -> str:
        if not existing:
            return incoming
        if incremental:
            return existing + incoming
        return incoming

    def _parse_non_stream_tool_calls(self, tool_calls_raw: Any) -> list[ToolCall]:
        parsed_tool_calls: list[ToolCall] = []
        for call in tool_calls_raw or []:
            function_call = self._read_field(call, "function")
            if function_call is None:
                continue

            name_raw = self._read_field(function_call, "name")
            if not isinstance(name_raw, str) or not name_raw.strip():
                continue

            arguments_raw = self._read_field(function_call, "arguments")
            call_id_raw = self._read_field(call, "id")
            parsed_tool_calls.append(
                ToolCall(
                    id=call_id_raw if isinstance(call_id_raw, str) and call_id_raw else f"call_{uuid.uuid4().hex[:12]}",
                    name=name_raw,
                    arguments=self._parse_arguments(arguments_raw if isinstance(arguments_raw, str) else None),
                )
            )

        return self._normalize_tool_calls(parsed_tool_calls)

    @staticmethod
    def _normalize_tool_calls(tool_calls: list[ToolCall]) -> list[ToolCall]:
        normalized: list[ToolCall] = []
        for tool_call in tool_calls:
            tool_id = tool_call.id or f"call_{uuid.uuid4().hex[:12]}"
            tool_name = tool_call.name.replace(" ", "")
            normalized.append(
                ToolCall(
                    id=tool_id,
                    name=tool_name,
                    arguments=tool_call.arguments,
                )
            )
        return normalized

    @staticmethod
    def _parse_arguments(raw_arguments: str | None) -> dict[str, Any]:
        if not raw_arguments:
            return {}
        try:
            payload = json.loads(raw_arguments)
        except json.JSONDecodeError:
            return {"_raw": raw_arguments}
        if isinstance(payload, dict):
            return payload
        return {"_value": payload}

    @staticmethod
    def _extract_content(content: Any) -> str:
        if isinstance(content, str):
            return content

        if isinstance(content, list):
            parts: list[str] = []
            for block in content:
                text: str | None = None
                if isinstance(block, dict):
                    value = block.get("text")
                    if isinstance(value, str):
                        text = value
                else:
                    value = getattr(block, "text", None)
                    if isinstance(value, str):
                        text = value
                if text:
                    parts.append(text)
            return "\n".join(parts)

        if content is None:
            return ""

        return str(content)

    @staticmethod
    def _extract_reasoning_content(content: Any) -> str:
        if isinstance(content, str):
            return content

        if isinstance(content, dict):
            for key in ("reasoning_content", "reasoning", "thinking", "text", "content"):
                value = content.get(key)
                if isinstance(value, str) and value:
                    return value
            return ""

        if isinstance(content, list):
            parts: list[str] = []
            for item in content:
                if isinstance(item, str):
                    if item:
                        parts.append(item)
                    continue

                if isinstance(item, dict):
                    for key in ("reasoning_content", "reasoning", "thinking", "text", "content"):
                        value = item.get(key)
                        if isinstance(value, str) and value:
                            parts.append(value)
                            break
                    continue

                for key in ("reasoning_content", "reasoning", "thinking", "text", "content"):
                    value = getattr(item, key, None)
                    if isinstance(value, str) and value:
                        parts.append(value)
                        break

            return "".join(parts)

        for key in ("reasoning_content", "reasoning", "thinking", "text", "content"):
            value = getattr(content, key, None)
            if isinstance(value, str) and value:
                return value

        return ""

    @staticmethod
    def _read_field(source: Any, key: str) -> Any:
        if isinstance(source, dict):
            return source.get(key)
        return getattr(source, key, None)

    @staticmethod
    def _usage_to_dict(usage: Any) -> dict[str, Any]:
        if usage is None:
            return {}
        if isinstance(usage, dict):
            return dict(usage)
        model_dump = getattr(usage, "model_dump", None)
        if callable(model_dump):
            dumped = model_dump(exclude_none=True)
            if isinstance(dumped, dict):
                return dumped
        return {}

    def _estimate_usage(self, *, messages: list[dict[str, Any]], content: str, model: str) -> dict[str, Any]:
        prompt_payload = json.dumps(messages, ensure_ascii=False, default=str)
        prompt_tokens = self._estimate_token_count(prompt_payload, model)
        completion_tokens = self._estimate_token_count(content, model)
        return {
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "total_tokens": prompt_tokens + completion_tokens,
        }

    @staticmethod
    def _estimate_token_count(text: str, model: str) -> int:
        if not text:
            return 0
        try:
            count = get_token_counts(text, model=model, use_token_server_first=False)
            if isinstance(count, int) and count >= 0:
                return count
        except Exception:
            pass

        # Heuristic fallback when tokenizer is unavailable for this model.
        return max(len(text) // 4, 1)

    def _sleep_backoff(self, attempt: int) -> None:
        jitter = random.uniform(0.0, 0.5)
        sleep_seconds = self.backoff_seconds * attempt + jitter
        time.sleep(sleep_seconds)

    def _dump_request_messages(self, messages: list[dict[str, Any]], *, model_name: str) -> None:
        if not self.debug_dump_dir:
            return
        self._request_counter += 1
        dump_dir = Path(self.debug_dump_dir)
        try:
            dump_dir.mkdir(parents=True, exist_ok=True)
            safe_model_name = re.sub(r"[^a-zA-Z0-9._-]+", "_", model_name).strip("_") or "model"
            filename = f"request_{self._request_counter:03d}_{safe_model_name}.json"
            payload = {
                "request_index": self._request_counter,
                "model": model_name,
                "message_count": len(messages),
                "messages": messages,
            }
            (dump_dir / filename).write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
        except Exception:
            logging.getLogger(__name__).debug("Failed to dump request messages", exc_info=True)
