use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::num::NonZeroU64;

// ============================================================================
// TraceContext — cloneable, type-erased context for request tracing
// ============================================================================

/// Object-safe trait for opaque tracing context attached to requests.
///
/// `Clone` is not object-safe, so we use a `clone_box` method instead.
/// Any concrete type that is `Clone + Send + Sync + Debug + 'static` gets a
/// blanket impl, so callers just do:
///
/// ```ignore
/// request.trace = Some(Box::new(my_concrete_trace));
/// ```
pub trait TraceContext: std::any::Any + Send + Sync + std::fmt::Debug {
    /// Clone this trace context into a new `Box`.
    fn clone_box(&self) -> Box<dyn TraceContext>;

    /// Upcast to `&dyn Any` for downcasting back to the concrete type.
    fn as_any(&self) -> &dyn std::any::Any;
}

/// Blanket impl: any `T: Clone + Send + Sync + Debug + 'static` is a `TraceContext`.
impl<T> TraceContext for T
where
    T: Clone + Send + Sync + std::fmt::Debug + 'static,
{
    fn clone_box(&self) -> Box<dyn TraceContext> {
        Box::new(self.clone())
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

impl Clone for Box<dyn TraceContext> {
    fn clone(&self) -> Self {
        // Explicitly dereference to `&dyn TraceContext` so `clone_box()` dispatches
        // through the vtable to the concrete type's implementation.
        //
        // Without this, `self.clone_box()` resolves via auto-deref to
        // `<Box<dyn TraceContext> as TraceContext>::clone_box()` (from the blanket impl),
        // which calls `self.clone()` → `self.clone_box()` → infinite recursion.
        let inner: &dyn TraceContext = &**self;
        inner.clone_box()
    }
}

/// Deserialize a field that may be `null` as the default value.
/// This is useful for fields like `Vec<T>` where `null` should become `vec![]`.
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
    D: Deserializer<'de>,
    T: Default + Deserialize<'de>,
{
    Option::<T>::deserialize(deserializer).map(|opt| opt.unwrap_or_default())
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChatCompletionRequest {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
    pub messages: Vec<ChatRequestMessage>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub temperature: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_tokens: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub top_p: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub frequency_penalty: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub presence_penalty: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tools: Option<Vec<ToolDefinition>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tool_choice: Option<ToolChoice>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub search_parameters: Option<SearchParameters>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub response_format: Option<crate::rs::ResponseFormat>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reasoning_effort: Option<ReasoningEffort>,

    /// custom headers
    #[serde(skip)]
    pub x_kigi_conv_id: Option<String>,
    #[serde(skip)]
    pub x_kigi_req_id: Option<String>,
    #[serde(skip)]
    pub x_kigi_session_id: Option<String>,
    #[serde(skip)]
    pub x_kigi_turn_idx: Option<String>,
    #[serde(skip)]
    pub x_kigi_agent_id: Option<String>,
    #[serde(skip)]
    pub x_kigi_deployment_id: Option<String>,
    #[serde(skip)]
    pub x_kigi_user_id: Option<String>,

    /// Optional opaque tracing context (e.g., where to persist the finalized request payload).
    /// This is intentionally not serialized or deserialized.
    /// Consumers downcast via `trace.as_ref().unwrap().as_any().downcast_ref::<T>()`.
    #[serde(skip)]
    pub trace: Option<Box<dyn TraceContext>>,
}

impl ChatCompletionRequest {
    pub fn new(model: impl Into<String>, messages: Vec<ChatRequestMessage>) -> Self {
        Self {
            model: Some(model.into()),
            messages,
            temperature: None,
            max_tokens: None,
            top_p: None,
            frequency_penalty: None,
            presence_penalty: None,
            user: None,
            tools: None,
            tool_choice: None,
            search_parameters: None,
            response_format: None,
            reasoning_effort: None,
            x_kigi_conv_id: None,
            x_kigi_req_id: None,
            x_kigi_session_id: None,
            x_kigi_turn_idx: None,
            x_kigi_agent_id: None,
            x_kigi_deployment_id: None,
            x_kigi_user_id: None,
            trace: None,
        }
    }

    pub fn from_messages(messages: Vec<ChatRequestMessage>) -> Self {
        Self {
            model: None,
            messages,
            temperature: None,
            max_tokens: None,
            top_p: None,
            frequency_penalty: None,
            presence_penalty: None,
            user: None,
            tools: None,
            tool_choice: None,
            search_parameters: None,
            response_format: None,
            reasoning_effort: None,
            x_kigi_conv_id: None,
            x_kigi_req_id: None,
            x_kigi_session_id: None,
            x_kigi_turn_idx: None,
            x_kigi_agent_id: None,
            x_kigi_deployment_id: None,
            x_kigi_user_id: None,
            trace: None,
        }
    }

    pub fn with_tools(mut self, tools: Vec<ToolDefinition>) -> Self {
        self.tools = Some(tools);
        self
    }

    pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
        self.tool_choice = Some(tool_choice);
        self
    }

    pub fn set_max_tokens(mut self, max_tokens: u32) -> Self {
        self.max_tokens = Some(max_tokens);
        self
    }

    pub fn with_temperature(mut self, temperature: f32) -> Self {
        self.temperature = Some(temperature);
        self
    }

    pub fn with_top_p(mut self, top_p: f32) -> Self {
        self.top_p = Some(top_p);
        self
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ImageUrl {
    pub url: String,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
pub enum ChatContentBlock {
    #[serde(rename = "text")]
    Text { text: String },
    #[serde(rename = "image_url")]
    ImageUrl { image_url: ImageUrl },
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum MessageContent {
    Text(String),
    Blocks(Vec<ChatContentBlock>),
}

impl MessageContent {
    pub fn is_empty(&self) -> bool {
        match self {
            MessageContent::Blocks(blocks) => blocks.is_empty(),
            MessageContent::Text(text) => text.is_empty(),
        }
    }

    pub fn blocks(&self) -> Vec<ChatContentBlock> {
        match self {
            MessageContent::Blocks(blocks) => blocks.clone(),
            MessageContent::Text(text) => vec![ChatContentBlock::Text { text: text.clone() }],
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChatRequestMessage {
    pub role: Role,
    pub content: MessageContent,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tool_calls: Vec<ToolCallRequest>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tool_call_id: Option<String>,
    /// The model used for this message (typically set on assistant responses)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model_id: Option<String>,
    /// The reasoning/thinking content from the model (for models that support extended thinking)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reasoning_content: Option<String>,
}

impl ChatRequestMessage {
    pub fn system(content: impl Into<String>) -> Self {
        Self {
            role: Role::System,
            content: MessageContent::Text(content.into()),
            name: None,
            tool_calls: Vec::new(),
            tool_call_id: None,
            model_id: None,
            reasoning_content: None,
        }
    }

    pub fn user(content: impl Into<String>) -> Self {
        Self {
            role: Role::User,
            content: MessageContent::Text(content.into()),
            name: None,
            tool_calls: Vec::new(),
            tool_call_id: None,
            model_id: None,
            reasoning_content: None,
        }
    }

    pub fn assistant(
        content: impl Into<String>,
        model_id: impl Into<String>,
        reasoning_content: Option<String>,
    ) -> Self {
        Self {
            role: Role::Assistant,
            content: MessageContent::Text(content.into()),
            name: None,
            tool_calls: Vec::new(),
            tool_call_id: None,
            model_id: Some(model_id.into()),
            reasoning_content,
        }
    }

    pub fn assistant_tool_call(tool_call: ToolCallRequest) -> Self {
        Self {
            role: Role::Assistant,
            content: MessageContent::Text("".into()),
            name: None,
            tool_calls: vec![tool_call],
            tool_call_id: None,
            model_id: None,
            reasoning_content: None,
        }
    }

    pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
        Self {
            role: Role::Tool,
            content: MessageContent::Text(content.into()),
            name: None,
            tool_calls: Vec::new(),
            tool_call_id: Some(tool_call_id.into()),
            model_id: None,
            reasoning_content: None,
        }
    }

    pub fn is_system_message(&self) -> bool {
        self.role == Role::System
    }

    /// Extract text content from the message content blocks
    pub fn text_content(&self) -> String {
        self.content
            .blocks()
            .iter()
            .filter_map(|block| match block {
                ChatContentBlock::Text { text } => Some(text.clone()),
                ChatContentBlock::ImageUrl { .. } => None,
            })
            .collect::<Vec<_>>()
            .join("\n")
    }

    /// Set text content, replacing all existing content blocks
    pub fn set_text_content(&mut self, text: impl Into<String>) {
        self.content = MessageContent::Text(text.into());
    }

    /// Append text content to existing content
    pub fn append_text_content(&mut self, text: impl Into<String>) {
        if self.content.is_empty() {
            self.set_text_content(text);
            return;
        }

        let new_content = match &self.content {
            MessageContent::Text(prev) => MessageContent::Text(format!("{}{}", prev, text.into())),
            MessageContent::Blocks(blocks) => {
                let mut blocks = blocks.clone();
                blocks.push(ChatContentBlock::Text { text: text.into() });
                MessageContent::Blocks(blocks)
            }
        };

        self.content = new_content;
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
    System,
    User,
    Assistant,
    Tool,
}

/// Calculate how many chat messages to keep for a given target prompt index (0-based, inclusive).
pub fn chat_truncate_for_prompt(
    chat_history: &[ChatRequestMessage],
    target_prompt_index: usize,
) -> usize {
    let mut user_count = 0;
    let mut keep_count = 0;

    for (i, msg) in chat_history.iter().enumerate() {
        if matches!(msg.role, Role::User) {
            user_count += 1;
            // If we've seen more user messages than target + 1, stop here
            if user_count > target_prompt_index + 1 {
                keep_count = i;
                break;
            }
        }
        keep_count = i + 1;
    }

    keep_count
}

#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum ToolType {
    Function,
}

// Re-export ToolDefinition and FunctionTool from kigi-tools.
// The canonical definitions now live there; this re-export keeps
// all existing `crate::sampling::types::ToolDefinition` imports working.
pub use kigi_tools::types::definition::{FunctionTool, ToolDefinition};

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum ToolChoice {
    Preset(String),
    Function {
        #[serde(rename = "type")]
        kind: ToolType,
        function: ToolChoiceFunction,
    },
}

impl ToolChoice {
    pub fn auto() -> Self {
        Self::Preset("auto".to_string())
    }

    pub fn none() -> Self {
        Self::Preset("none".to_string())
    }

    pub fn required() -> Self {
        Self::Preset("required".to_string())
    }

    pub fn function(name: impl Into<String>) -> Self {
        Self::Function {
            kind: ToolType::Function,
            function: ToolChoiceFunction { name: name.into() },
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ToolChoiceFunction {
    pub name: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ToolCallRequest {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    #[serde(rename = "type")]
    pub kind: ToolType,
    pub function: ToolCallFunction,
}

impl ToolCallRequest {
    pub fn function(name: impl Into<String>, arguments: impl Into<String>) -> Self {
        Self {
            id: None,
            kind: ToolType::Function,
            function: ToolCallFunction::new(name, arguments),
        }
    }

    pub fn with_id(mut self, id: impl Into<String>) -> Self {
        self.id = Some(id.into());
        self
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChatCompletionResponse {
    pub id: String,
    pub object: String,
    pub created: u64,
    pub model: String,
    pub choices: Vec<ChatChoice>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub usage: Option<Usage>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub citations: Option<Vec<String>>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChatChoice {
    pub index: u32,
    pub message: ChatResponseMessage,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub finish_reason: Option<FinishReason>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum FinishReason {
    Stop,
    Length,
    ToolCalls,
    ContentFilter,
    FunctionCall,
}

/// Wire `content` field on a chat response/delta. OpenAI-compatible providers
/// send a plain string, but reasoning providers (Mistral) send an ARRAY of
/// typed chunks: `{"type":"text","text":".."}` for the answer and
/// `{"type":"thinking","thinking":[{"type":"text","text":".."}],...}` for the
/// chain of thought (verified against the mistralai/client-python SDK models).
/// The chunk union is OPEN — unknown chunk types are ignored, never fatal.
#[derive(Deserialize)]
#[serde(untagged)]
enum WireContent {
    Text(String),
    Chunks(Vec<Value>),
}

/// Read a chunk's `text` string field into `dst` when present (defensive: a
/// reference/tool-reference/unknown chunk simply has no `text`).
fn push_chunk_text(dst: &mut String, chunk: &Value) {
    if let Some(t) = chunk.get("text").and_then(Value::as_str) {
        dst.push_str(t);
    }
}

fn non_empty(s: String) -> Option<String> {
    if s.is_empty() { None } else { Some(s) }
}

impl WireContent {
    /// Split into `(visible answer text, thinking text)`. A plain string is
    /// the answer with no thinking; an array routes `text` chunks to the
    /// answer and the nested `text` of `thinking` chunks to the reasoning
    /// channel. Byte-identical for string content (the only shape non-Mistral
    /// providers ever send).
    fn split(self) -> (Option<String>, Option<String>) {
        match self {
            // Preserve string content verbatim (incl. empty) — byte-identical
            // to the pre-change `content: Option<String>` for every provider
            // that sends a string.
            WireContent::Text(s) => (Some(s), None),
            WireContent::Chunks(chunks) => {
                let mut answer = String::new();
                let mut thinking = String::new();
                for c in &chunks {
                    match c.get("type").and_then(Value::as_str) {
                        Some("text") => push_chunk_text(&mut answer, c),
                        Some("thinking") => {
                            // `thinking` is a nested list of chunks.
                            if let Some(inner) = c.get("thinking").and_then(Value::as_array) {
                                for ic in inner {
                                    push_chunk_text(&mut thinking, ic);
                                }
                            }
                        }
                        // reference / tool_reference / unknown → not text.
                        _ => {}
                    }
                }
                (non_empty(answer), non_empty(thinking))
            }
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(from = "RawChatResponseMessage")]
pub struct ChatResponseMessage {
    pub role: Role,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reasoning_content: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tool_calls: Vec<ToolCallResponse>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tool_call_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub citations: Option<Vec<String>>,
}

/// Deserialization mirror of [`ChatResponseMessage`] that accepts array
/// `content` and folds thinking chunks into `reasoning_content`.
#[derive(Deserialize)]
struct RawChatResponseMessage {
    role: Role,
    #[serde(default)]
    content: Option<WireContent>,
    #[serde(default)]
    reasoning_content: Option<String>,
    #[serde(default)]
    tool_calls: Vec<ToolCallResponse>,
    #[serde(default)]
    tool_call_id: Option<String>,
    #[serde(default)]
    citations: Option<Vec<String>>,
}

impl From<RawChatResponseMessage> for ChatResponseMessage {
    fn from(r: RawChatResponseMessage) -> Self {
        let (answer, thinking) = r.content.map(WireContent::split).unwrap_or((None, None));
        ChatResponseMessage {
            role: r.role,
            content: answer,
            // The wire's own `reasoning_content` wins; else thinking chunks.
            reasoning_content: r.reasoning_content.or(thinking),
            tool_calls: r.tool_calls,
            tool_call_id: r.tool_call_id,
            citations: r.citations,
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ToolCallResponse {
    pub id: String,
    #[serde(rename = "type")]
    pub kind: String,
    pub function: ToolCallFunction,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ToolCallFunction {
    pub name: String,
    pub arguments: String,
}

impl ToolCallFunction {
    pub fn new(name: impl Into<String>, arguments: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            arguments: arguments.into(),
        }
    }

    pub fn from_json(name: impl Into<String>, arguments: &Value) -> Self {
        Self {
            name: name.into(),
            arguments: arguments.to_string(),
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Usage {
    pub prompt_tokens: u32,
    pub completion_tokens: u32,
    pub total_tokens: u32,
    /// Kimi/Moonshot deviation: the Moonshot chat/completions API reports
    /// cache hits as a top-level `cached_tokens` field on `usage` instead of
    /// the OpenAI-standard `prompt_tokens_details.cached_tokens`. Ported from
    /// kimi-cli's parser (packages/kosong/src/kosong/chat_provider/kimi.py:427-437,
    /// which checks the top-level field first and cites
    /// platform.moonshot.cn/docs/api/chat). `From<Usage> for TokenUsage`
    /// applies the same precedence.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cached_tokens: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub prompt_tokens_details: Option<PromptTokensDetails>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub completion_tokens_details: Option<CompletionTokensDetails>,
    /// xAI extension: request price in USD ticks (1 USD = 1e10 ticks).
    /// The REST mapper backfills `0` for unbilled requests; capture sites
    /// normalize `0` to "unreported" (see `stream/chat_completions.rs`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cost_in_usd_ticks: Option<i64>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct PromptTokensDetails {
    #[serde(default)]
    pub cached_tokens: u32,
    #[serde(default)]
    pub audio_tokens: u32,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct CompletionTokensDetails {
    #[serde(default)]
    pub reasoning_tokens: u32,
    #[serde(default)]
    pub audio_tokens: u32,
    #[serde(default)]
    pub accepted_prediction_tokens: u32,
    #[serde(default)]
    pub rejected_prediction_tokens: u32,
}
// ============ Streaming types ============

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChatCompletionChunk {
    pub id: String,
    pub object: String,
    pub created: u64,
    pub model: String,
    pub choices: Vec<ChatChunkChoice>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub usage: Option<Usage>,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "crate::serde_helpers::empty_string_as_none"
    )]
    pub system_fingerprint: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChatChunkChoice {
    pub index: u32,
    pub delta: ChatChunkDelta,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub finish_reason: Option<FinishReason>,
    /// Kimi/Moonshot deviation: some Kimi deployments attach the final
    /// `usage` object to the last *choice* instead of (or in addition to)
    /// the chunk's top-level `usage`. Ported from kimi-cli's
    /// `extract_usage_from_chunk`
    /// (packages/kosong/src/kosong/chat_provider/kimi.py:522-533), which
    /// falls back to `choices[0].usage` when `chunk.usage` is absent.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub usage: Option<Usage>,
}

/// Streaming delta for a tool call.
///
/// In OpenAI-compatible streaming, tool calls arrive across multiple chunks:
/// - The first chunk carries `id`, `type`, `index`, and the `function.name` + start of `arguments`.
/// - Subsequent chunks only carry `index` and a `function.arguments` fragment (no `id`, no `name`).
///
/// All fields except `index` are therefore optional so we can deserialize every chunk.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct ToolCallDelta {
    /// The positional index of the tool call being streamed.
    /// Used to correlate delta chunks belonging to the same tool call.
    #[serde(default)]
    pub index: u32,
    /// Only present in the first chunk for this tool call.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    /// Only present in the first chunk (usually "function").
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    pub kind: Option<String>,
    /// The function name and/or argument fragment.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub function: Option<ToolCallFunctionDelta>,
}

/// Streaming delta for function name/arguments within a tool call.
///
/// `name` is only present in the first chunk; `arguments` may arrive across many chunks.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct ToolCallFunctionDelta {
    /// Only present in the first chunk for this tool call.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Argument fragment (may be empty or partial JSON).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub arguments: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(from = "RawChatChunkDelta")]
pub struct ChatChunkDelta {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub role: Option<Role>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content: Option<String>,
    pub reasoning_content: Option<String>,
    /// Tool call deltas. Handles `null` in JSON as empty vec.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tool_calls: Vec<ToolCallDelta>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tool_call_id: Option<String>,
}

/// Deserialization mirror of [`ChatChunkDelta`] that accepts array `content`
/// (Mistral reasoning streams a thinking-chunk array during the thinking
/// phase, then plain-string answer deltas) and folds thinking into
/// `reasoning_content`.
#[derive(Deserialize)]
struct RawChatChunkDelta {
    #[serde(default)]
    role: Option<Role>,
    #[serde(default)]
    content: Option<WireContent>,
    #[serde(default)]
    reasoning_content: Option<String>,
    #[serde(default, deserialize_with = "deserialize_null_default")]
    tool_calls: Vec<ToolCallDelta>,
    #[serde(default)]
    tool_call_id: Option<String>,
}

impl From<RawChatChunkDelta> for ChatChunkDelta {
    fn from(r: RawChatChunkDelta) -> Self {
        let (answer, thinking) = r.content.map(WireContent::split).unwrap_or((None, None));
        ChatChunkDelta {
            role: r.role,
            content: answer,
            reasoning_content: r.reasoning_content.or(thinking),
            tool_calls: r.tool_calls,
            tool_call_id: r.tool_call_id,
        }
    }
}

/// Parameters to control realtime data.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SearchParameters {
    /// Choose the mode to query realtime data:
    /// * `off`: no search performed and no external will be considered.
    /// * `on` (default): the model will search in every source for relevant data.
    /// * `auto`: the model chooses whether to search data or not and where to search the data.
    pub mode: Option<String>,
    /// List of sources to search in. If no sources are specified, the model will look over the web and X by default.
    pub sources: Option<Vec<SearchSource>>,
    /// Date from which to consider the results in ISO-8601 YYYY-MM-DD.
    pub from_date: Option<String>,
    /// Date up to which to consider the results in ISO-8601 YYYY-MM-DD.
    pub to_date: Option<String>,
    /// Whether to return citations in the response or not.
    pub return_citations: Option<bool>,
    /// Maximum number of search results to use.
    pub max_search_results: Option<i32>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(tag = "type")]
pub enum SearchSource {
    #[serde(rename = "x")]
    X {
        /// X Handles of the users from whom to consider the posts.
        included_x_handles: Option<Vec<String>>,
        /// DEPRECATED in favor of `included_x_handles`. Use `included_x_handles` instead.
        x_handles: Option<Vec<String>>,
        /// List of X handles to exclude from the search results.
        excluded_x_handles: Option<Vec<String>>,
        /// The minimum favorite count of the X posts to consider.
        post_favorite_count: Option<i32>,
        /// The minimum view count of the X posts to consider.
        post_view_count: Option<i32>,
    },
    #[serde(rename = "web")]
    Web {
        /// List of website to exclude from the search results.
        excluded_websites: Option<Vec<String>>,
        /// List of website to allow in the search results.
        allowed_websites: Option<Vec<String>>,
        /// ISO alpha-2 code of the country.
        country: Option<String>,
        /// If set to true, mature content won't be considered during the search.
        safe_search: Option<bool>,
    },
    #[serde(rename = "news")]
    News {
        /// List of website to exclude from the search results.
        excluded_websites: Option<Vec<String>>,
        /// ISO alpha-2 code of the country.
        country: Option<String>,
        /// If set to true, mature content won't be considered during the search.
        safe_search: Option<bool>,
    },
    #[serde(rename = "rss")]
    Rss {
        /// Links of the RSS feeds.
        links: Vec<String>,
    },
}

/// Per-model config for the `x-compaction-at` request header (a token count).
///
/// Deserialized from a polymorphic remote-config value: `true` enables the
/// header with a value computed as `context_window * auto_compact_threshold_percent / 100`;
/// `false` (or absent) disables it; an integer `N` sends the constant `N`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum CompactionAtTokens {
    Enabled(bool),
    Fixed(u64),
}

impl CompactionAtTokens {
    /// Resolve the absolute token count to send, or `None` when disabled.
    pub fn resolve(self, context_window: u64, threshold_percent: u8) -> Option<u64> {
        match self {
            CompactionAtTokens::Enabled(false) => None,
            CompactionAtTokens::Enabled(true) => {
                Some(context_window * u64::from(threshold_percent) / 100)
            }
            CompactionAtTokens::Fixed(n) => Some(n),
        }
    }
}

/// Per-model config for the `x-compactions-remaining` request header.
///
/// `true` sends the dynamic value (1 on the uncompacted prefix, 0 once the session compacts);
/// `false`/absent disables the header; an integer `N` sends the constant `N`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum CompactionsRemaining {
    Dynamic(bool),
    Fixed(u8),
}

impl CompactionsRemaining {
    /// Resolve the header value to send, or `None` when disabled.
    pub fn resolve(self, has_compaction_summary: bool) -> Option<u8> {
        match self {
            CompactionsRemaining::Dynamic(false) => None,
            CompactionsRemaining::Dynamic(true) => Some(u8::from(!has_compaction_summary)),
            CompactionsRemaining::Fixed(n) => Some(n),
        }
    }
}

/// Reasoning effort level. `None`/`Minimal` are omitted on the Anthropic Messages API.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ReasoningEffort {
    None,
    Minimal,
    Low,
    #[default]
    Medium,
    High,
    Xhigh,
    /// Distinct top tier above `xhigh` (OpenAI Responses and Anthropic
    /// Messages both accept `xhigh` AND `max` as separate levels in 2026;
    /// the Kimi wire spells its top tier `max` with no `xhigh`).
    Max,
    /// Codex-only top tier above `max` (the ChatGPT Codex backend exposes an
    /// `ultra` reasoning effort on its flagship models). Reachable ONLY via a
    /// model's server-declared effort menu (openai-codex); no built-in fallback
    /// menu offers it, so other providers never emit it.
    Ultra,
}

impl ReasoningEffort {
    /// The canonical effort behind a typed Responses-API echo
    /// (`response.reasoning.effort`). `max` echoes never reach the typed
    /// enum — [`normalize_effort_echo`] drops them pre-parse (async-openai
    /// has no such variant); the request direction writes the wire string
    /// via [`patch_reasoning_effort`].
    pub fn from_responses_api(effort: crate::rs::ReasoningEffort) -> Self {
        match effort {
            crate::rs::ReasoningEffort::None => Self::None,
            crate::rs::ReasoningEffort::Minimal => Self::Minimal,
            crate::rs::ReasoningEffort::Low => Self::Low,
            crate::rs::ReasoningEffort::Medium => Self::Medium,
            crate::rs::ReasoningEffort::High => Self::High,
            crate::rs::ReasoningEffort::Xhigh => Self::Xhigh,
        }
    }

    pub fn as_str(self) -> &'static str {
        match self {
            Self::None => "none",
            Self::Minimal => "minimal",
            Self::Low => "low",
            Self::Medium => "medium",
            Self::High => "high",
            Self::Xhigh => "xhigh",
            Self::Max => "max",
            Self::Ultra => "ultra",
        }
    }

    /// Anthropic Messages API effort string; `None` for unsupported variants.
    /// `xhigh` and `max` are distinct levels on the 2026 Messages API (both
    /// appear in `GET /v1/models` `capabilities.effort`). `ultra` is codex-only
    /// and never selected on an Anthropic model, but maps to its own string for
    /// completeness (the Responses path writes effort via `as_str`, not this).
    pub fn to_messages_api(self) -> Option<&'static str> {
        match self {
            Self::None | Self::Minimal => None,
            Self::Low => Some("low"),
            Self::Medium => Some("medium"),
            Self::High => Some("high"),
            Self::Xhigh => Some("xhigh"),
            Self::Max => Some("max"),
            Self::Ultra => Some("ultra"),
        }
    }
}

impl std::fmt::Display for ReasoningEffort {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

impl std::str::FromStr for ReasoningEffort {
    type Err = String;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "none" => Ok(Self::None),
            "minimal" => Ok(Self::Minimal),
            "low" => Ok(Self::Low),
            "medium" => Ok(Self::Medium),
            "high" => Ok(Self::High),
            "xhigh" => Ok(Self::Xhigh),
            "max" => Ok(Self::Max),
            "ultra" => Ok(Self::Ultra),
            _ => Err(format!(
                "invalid reasoning effort: {s:?} (expected one of: none, minimal, low, medium, high, xhigh, max, ultra)"
            )),
        }
    }
}

/// Canonical wire parse; remapped menu ids need a model catalog.
pub fn parse_canonical_effort_token(token: &str) -> Option<ReasoningEffort> {
    token.parse().ok()
}

/// Deserialize an optional effort LENIENTLY for persisted stores (session
/// summaries, chat history): a token this binary doesn't know — written by a
/// newer kigi after the effort vocabulary grew, exactly what happened when
/// `max` split from `xhigh` — degrades to `None` with a warning instead of
/// failing the whole record, so listings and resume survive version
/// rollback. Non-string values still error (real corruption stays loud), and
/// config-TOML parsing stays strict — its layer warn-skips explicitly.
pub fn lenient_reasoning_effort_opt<'de, D>(d: D) -> Result<Option<ReasoningEffort>, D::Error>
where
    D: Deserializer<'de>,
{
    let raw = Option::<String>::deserialize(d)?;
    Ok(raw.and_then(|s| match s.parse::<ReasoningEffort>() {
        Ok(effort) => Some(effort),
        Err(error) => {
            tracing::warn!(token = %s, %error, "persisted reasoning_effort unknown; dropping");
            None
        }
    }))
}

/// Write the canonical effort onto a serialized Responses request body
/// (`body.reasoning.effort`). This is the ONLY place effort reaches the
/// Responses wire: the typed `rs::ReasoningEffort` tops out at `xhigh`, so
/// `max` must be written post-serialize. A `None` effort leaves the body
/// untouched (the provider default applies).
pub fn patch_reasoning_effort(body: &mut Value, effort: Option<ReasoningEffort>) {
    let Some(effort) = effort else { return };
    let Some(obj) = body.as_object_mut() else {
        return;
    };
    let reasoning = obj
        .entry("reasoning")
        .or_insert_with(|| Value::Object(serde_json::Map::new()));
    if let Some(reasoning) = reasoning.as_object_mut() {
        reasoning.insert(
            "effort".to_string(),
            Value::String(effort.as_str().to_string()),
        );
    }
}

/// Adapt a serialized Responses request body to the ChatGPT/Codex backend
/// contract (`chatgpt.com/backend-api/codex/responses`) — ported from the
/// same reference as the identity headers (official Codex CLI + Pi's
/// `api/openai-codex-responses.ts`):
///
/// 1. The backend rejects `role: system` input items outright
///    (400 `{"detail":"System messages are not allowed"}`); its system
///    channel is the top-level `instructions` field. Hoist every system
///    input message there (order preserved, blank-line joined, appended to
///    any existing instructions) and remove them from `input`.
/// 2. `store` is always `false` on this backend, so reasoning continuity
///    is stateless: `include: ["reasoning.encrypted_content"]` is required
///    for the response to carry replayable encrypted reasoning.
/// 3. For the same reason, a replayed reasoning item WITHOUT
///    `encrypted_content` (captured from a stateful api.openai.com session
///    that never requested the include) references server state
///    chatgpt.com does not have — drop it rather than 400.
///
/// openai-codex-GATED at the call sites — API-key `openai` Responses
/// bodies stay byte-identical.
pub fn adapt_body_for_codex_backend(body: &mut Value) {
    // 1. Hoist system messages into `instructions`.
    let mut hoisted: Vec<String> = Vec::new();
    if let Some(input) = body.get_mut("input").and_then(|v| v.as_array_mut()) {
        input.retain(|item| {
            let is_system = item.get("role").and_then(|r| r.as_str()) == Some("system");
            if is_system {
                match item.get("content") {
                    Some(Value::String(s)) => hoisted.push(s.clone()),
                    Some(Value::Array(parts)) => {
                        for p in parts {
                            if let Some(t) = p.get("text").and_then(|t| t.as_str()) {
                                hoisted.push(t.to_string());
                            }
                        }
                    }
                    _ => {}
                }
            }
            !is_system
        });
    }
    if !hoisted.is_empty() {
        let mut instructions = body
            .get("instructions")
            .and_then(|v| v.as_str())
            .map(str::to_owned)
            .unwrap_or_default();
        for part in hoisted {
            if !instructions.is_empty() {
                instructions.push_str("\n\n");
            }
            instructions.push_str(&part);
        }
        body["instructions"] = Value::String(instructions);
    }

    // 2. Request replayable encrypted reasoning.
    let include = body
        .as_object_mut()
        .map(|obj| obj.entry("include").or_insert_with(|| Value::Array(vec![])));
    if let Some(Value::Array(entries)) = include {
        let key = Value::String("reasoning.encrypted_content".to_string());
        if !entries.contains(&key) {
            entries.push(key);
        }
    }

    // 3. Drop reasoning items with no encrypted payload: stateless codex
    // cannot resolve a bare `rs_*` reference.
    if let Some(input) = body.get_mut("input").and_then(|v| v.as_array_mut()) {
        input.retain(|item| {
            item.get("type").and_then(|t| t.as_str()) != Some("reasoning")
                || item
                    .get("encrypted_content")
                    .and_then(|v| v.as_str())
                    .is_some_and(|s| !s.is_empty())
        });
    }
}

/// Neutralize a `reasoning.effort` echo the typed `rs` enum cannot parse
/// (`max`): remove it so response deserialization succeeds. The turn's
/// canonical effort lives in the session sampling config regardless; only
/// the per-item echo metadata is dropped, recorded as not-echoed.
/// (Ceiling: async-openai lacks a Max variant; delete this when it grows
/// one.) Handles both bare response bodies (`/reasoning/effort`) and
/// stream-event envelopes (`/response/reasoning/effort`).
pub fn normalize_effort_echo(value: &mut Value) {
    for path in ["/reasoning", "/response/reasoning"] {
        if let Some(reasoning) = value.pointer_mut(path).and_then(|v| v.as_object_mut())
            && let Some(effort) = reasoning.get("effort").and_then(|v| v.as_str())
            && crate::rs::ReasoningEffort::deserialize(serde_json::Value::String(
                effort.to_string(),
            ))
            .is_err()
        {
            tracing::debug!(
                effort,
                "reasoning.effort echo unrepresentable in the typed enum; dropping"
            );
            reasoning.remove("effort");
        }
    }
}

/// The `anthropic-version` header value kigi speaks on Anthropic-style
/// wires (Messages inference and the /v1/models listing).
pub const ANTHROPIC_VERSION: &str = "2023-06-01";

/// `anthropic-beta` value for the Claude-Code OAuth (Pro/Max subscription)
/// path — required on BOTH the Messages inference request and the `/v1/models`
/// listing when the bearer is an OAuth `sk-ant-oat…` token. API-key Anthropic
/// and MiniMax NEVER send this (their requests stay byte-identical).
pub const ANTHROPIC_OAUTH_BETA: &str = "claude-code-20250219,oauth-2025-04-20";

/// User-Agent kigi presents on the Claude-Code OAuth path (mirrors the
/// official Claude Code CLI). OAuth-gated: unrelated to the default kigi UA.
pub const CLAUDE_CODE_USER_AGENT: &str = "claude-cli/2.1.75";

/// System-prompt prefix REQUIRED on the Claude-Code OAuth Messages path: the
/// OAuth token is Claude-Code-scoped, so Anthropic rejects the request unless
/// the system prompt's first block is exactly this line. OAuth-gated.
pub const CLAUDE_CODE_SYSTEM_PREFIX: &str =
    "You are Claude Code, Anthropic's official CLI for Claude.";

// ── GitHub Copilot editor-identity headers ──────────────────────────────────
// The VS Code Copilot Chat client identity. Copilot's proxy authorizes the
// short-lived copilot token AND validates these editor headers, so they ride
// the `copilot_internal/v2/token` exchange, the `/models` listing, and every
// `/chat/completions` inference request. github-copilot-GATED: no other
// platform sends them, so their requests stay byte-identical. Values are
// non-secret wire constants (ported from Pi `api/github-copilot-headers.ts` +
// `auth/oauth/github-copilot.ts`).

/// `User-Agent` for the Copilot path (overrides the default kigi UA, OAuth-gated).
pub const COPILOT_USER_AGENT: &str = "GitHubCopilotChat/0.35.0";
/// `Editor-Version` — the host editor Copilot believes it is talking to.
pub const COPILOT_EDITOR_VERSION: &str = "vscode/1.107.0";
/// `Editor-Plugin-Version` — the Copilot Chat plugin build.
pub const COPILOT_EDITOR_PLUGIN_VERSION: &str = "copilot-chat/0.35.0";
/// `Copilot-Integration-Id` — the integration the token is scoped to.
pub const COPILOT_INTEGRATION_ID: &str = "vscode-chat";
/// `X-GitHub-Api-Version` — sent ONLY on the `/models` listing.
pub const COPILOT_API_VERSION: &str = "2026-06-01";
/// `X-Initiator` value — sent ONLY on inference (`user`, per the spec).
pub const COPILOT_INITIATOR: &str = "user";

// ── ChatGPT/Codex (openai-codex) OAuth-inference headers ─────────────────────
// The ChatGPT Codex backend authorizes an OAuth bearer AND validates the Codex
// client identity. These ride the `/codex/responses` inference request ONLY.
// openai-codex-GATED: no other Responses provider (API-key `openai`) sends them,
// so their requests stay byte-identical. Values are non-secret wire constants
// (ported from the official Codex CLI + Pi `api/openai-codex-responses.ts`).

/// `originator` header identifying the Codex CLI client (matches the authorize
/// `originator` param).
pub const CODEX_ORIGINATOR: &str = "codex_cli_rs";
/// `OpenAI-Beta` opt-in the Codex Responses endpoint requires.
pub const CODEX_OPENAI_BETA: &str = "responses=experimental";
/// `User-Agent` presented on the Codex path (overrides the default kigi UA,
/// openai-codex-gated). The Codex backend does not strictly validate the UA
/// string (Pi ships its own and it works), so this is a stable best-effort
/// identity, not a pinned build.
pub const CODEX_USER_AGENT: &str = "codex_cli_rs/0.104.0";
/// JWT payload claim namespace carrying the ChatGPT account id.
const CODEX_JWT_AUTH_CLAIM: &str = "https://api.openai.com/auth";

/// Extract the `chatgpt_account_id` from a Codex OAuth access token (a JWT):
/// base64url-decode the payload segment and read
/// `["https://api.openai.com/auth"]["chatgpt_account_id"]`. Returns `None` when
/// the token is not a well-formed JWT or the claim is missing/empty.
///
/// Used BOTH at login (fail-fast: a token without the claim is useless) and at
/// inference (the header is derived STATELESSLY from the current bearer, so a
/// refreshed token — which still carries the claim — needs no persisted field).
///
/// SECURITY: the token, its payload, and the returned account id are NEVER
/// logged by this function or its callers.
pub fn chatgpt_account_id_from_jwt(token: &str) -> Option<String> {
    use base64::Engine;
    // A JWT is exactly three dot-separated segments; anything else is not a
    // token we can read (fail closed rather than decode a lookalike).
    let mut segments = token.split('.');
    let (_header, payload_b64, _signature) = (segments.next()?, segments.next()?, segments.next()?);
    if segments.next().is_some() {
        return None;
    }
    // JWT payloads are base64url without padding; be tolerant of either.
    let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
        .decode(payload_b64)
        .or_else(|_| base64::engine::general_purpose::URL_SAFE.decode(payload_b64))
        .ok()?;
    let claims: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
    let account_id = claims
        .get(CODEX_JWT_AUTH_CLAIM)?
        .get("chatgpt_account_id")?
        .as_str()?;
    (!account_id.is_empty()).then(|| account_id.to_string())
}

/// ChatCompletions request-body adaptation dialect. Providers disagree on
/// how thinking rides an OpenAI-compatible body: Kimi wants
/// `thinking:{type,effort}`, DeepSeek wants
/// `thinking:{type,reasoning_effort}`, most others take the OpenAI-style
/// `reasoning_effort` scalar untouched.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChatCompat {
    /// Kimi wire (`thinking:{type,effort}`, canonical xhigh spelled max).
    /// The default: BYOK/custom ChatCompletions endpoints keep the
    /// historical kigi behavior.
    #[default]
    Kimi,
    /// DeepSeek wire (`thinking:{type,reasoning_effort}`, official docs:
    /// low/medium map to high and xhigh to max server-side).
    DeepSeek,
    /// Leave the body as-is (OpenAI-style `reasoning_effort` passes through).
    Passthrough,
    /// Strict OpenAI-compatible validators (Mistral, Cerebras) reject any
    /// out-of-schema request field with a 4xx (`additionalProperties:false`).
    /// kigi injects `stream_options.include_usage` on every streaming
    /// request, which such validators reject, so it is stripped (streaming
    /// usage falls back to token estimation). `reasoning_effort` passes
    /// through; private message fields are stripped like Passthrough.
    /// (Serde alias `mistral` keeps sessions persisted before the rename.)
    #[serde(alias = "mistral")]
    StrictOpenAi,
}

pub const REASONING_EFFORT_META_KEY: &str = "reasoningEffort";
pub const SUPPORTS_REASONING_EFFORT_META_KEY: &str = "supportsReasoningEffort";

pub fn supports_reasoning_effort_meta(
    meta: Option<&serde_json::Map<String, serde_json::Value>>,
) -> bool {
    meta.and_then(|m| m.get(SUPPORTS_REASONING_EFFORT_META_KEY))
        .and_then(|v| v.as_bool())
        .unwrap_or(false)
}

/// Returns `None` on type-mismatch or unknown variant (logs a warn so we don't
/// overwrite the user's persisted pref on the next save).
pub fn parse_reasoning_effort_meta(
    meta: Option<&serde_json::Map<String, serde_json::Value>>,
) -> Option<ReasoningEffort> {
    let raw = meta?.get(REASONING_EFFORT_META_KEY)?;
    let s = match raw.as_str() {
        Some(s) => s,
        None => {
            tracing::warn!(value = %raw, "meta.reasoningEffort: expected string, ignoring");
            return None;
        }
    };
    match s.parse() {
        Ok(eff) => Some(eff),
        Err(err) => {
            tracing::warn!(value = %s, error = %err, "meta.reasoningEffort: parse failed, ignoring");
            None
        }
    }
}

pub fn reasoning_effort_meta_value(effort: ReasoningEffort) -> serde_json::Value {
    serde_json::Value::String(effort.as_str().to_string())
}

pub const REASONING_EFFORTS_META_KEY: &str = "reasoningEfforts";

/// A single selectable reasoning-effort option for a model. `id`/`label` are
/// presentation and input; `value` is the canonical value sent on the wire.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct ReasoningEffortOption {
    pub id: String,
    pub value: ReasoningEffort,
    pub label: String,
    pub description: Option<String>,
    pub default: bool,
}

/// Deserialization shape accepting either a bare canonical value string
/// (`"xhigh"`) or a table with `value` required and everything else optional.
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum RawReasoningEffortOption {
    Bare(String),
    Full {
        value: ReasoningEffort,
        id: Option<String>,
        label: Option<String>,
        description: Option<String>,
        #[serde(default)]
        default: bool,
    },
}

/// Uppercase the first character of an id for a default label; `"xhigh"` becomes
/// `"Xhigh"`, `"deep"` becomes `"Deep"`.
fn humanize_effort_id(id: &str) -> String {
    let mut chars = id.chars();
    match chars.next() {
        Some(first) => first.to_uppercase().chain(chars).collect(),
        None => String::new(),
    }
}

impl<'de> serde::Deserialize<'de> for ReasoningEffortOption {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        Ok(match RawReasoningEffortOption::deserialize(deserializer)? {
            RawReasoningEffortOption::Bare(s) => {
                let value = s
                    .parse::<ReasoningEffort>()
                    .map_err(serde::de::Error::custom)?;
                let id = value.as_str().to_string();
                let label = humanize_effort_id(&id);
                ReasoningEffortOption {
                    id,
                    value,
                    label,
                    description: None,
                    default: false,
                }
            }
            RawReasoningEffortOption::Full {
                value,
                id,
                label,
                description,
                default,
            } => {
                let id = id.unwrap_or_else(|| value.as_str().to_string());
                let label = label.unwrap_or_else(|| humanize_effort_id(&id));
                ReasoningEffortOption {
                    id,
                    value,
                    label,
                    description,
                    default,
                }
            }
        })
    }
}

/// Parse a JSON array of reasoning-effort options element-by-element, skipping
/// (and warning on) any entry whose `value` fails to parse (forward-compat for
/// tiers a newer server introduces). The single home for the skip-invalid rule,
/// shared by the meta reader and the remote `/models` parser.
pub fn parse_reasoning_effort_options(arr: &[serde_json::Value]) -> Vec<ReasoningEffortOption> {
    arr.iter()
        .filter_map(
            |el| match serde_json::from_value::<ReasoningEffortOption>(el.clone()) {
                Ok(opt) => Some(opt),
                Err(err) => {
                    tracing::warn!(value = %el, error = %err, "reasoningEfforts: skipping invalid entry");
                    None
                }
            },
        )
        .collect()
}

/// Parse the per-model reasoning-effort menu from a model's ACP `meta`. Returns
/// `None` when the key is absent, is not an array, or yields no usable options
/// after skip-invalid — so "absent" and "present-but-unusable" collapse to the
/// same fallback path in every consumer.
pub fn parse_reasoning_efforts_meta(
    meta: Option<&serde_json::Map<String, serde_json::Value>>,
) -> Option<Vec<ReasoningEffortOption>> {
    let raw = meta?.get(REASONING_EFFORTS_META_KEY)?;
    let arr = match raw.as_array() {
        Some(arr) => arr,
        None => {
            tracing::warn!(value = %raw, "meta.reasoningEfforts: expected array, ignoring");
            return None;
        }
    };
    let options = parse_reasoning_effort_options(arr);
    (!options.is_empty()).then_some(options)
}

pub fn reasoning_efforts_meta_value(opts: &[ReasoningEffortOption]) -> serde_json::Value {
    serde_json::to_value(opts).unwrap_or_else(|_| serde_json::Value::Array(Vec::new()))
}

/// Which API backend to use for model inference.
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ApiBackend {
    /// Use the Chat Completions API (/v1/chat/completions)
    #[default]
    ChatCompletions,
    /// Use the Responses API (/v1/responses)
    Responses,
    /// Use the Anthropic Messages API (/v1/messages)
    Messages,
}

impl ApiBackend {
    /// Whether the backend enforces a response JSON schema natively alongside
    /// tool calls. The Messages API does not (a schema there blocks tool use),
    /// so structured output there goes through the StructuredOutput tool.
    pub fn supports_native_schema(&self) -> bool {
        matches!(self, Self::ChatCompletions | Self::Responses)
    }
}

/// Sampling client configuration (API key excluded — that stays in the client).
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct SamplingConfig {
    pub base_url: String,
    pub model: String,
    pub max_completion_tokens: Option<u32>,
    pub temperature: Option<f32>,
    pub top_p: Option<f32>,
    /// ChatCompletions body-adaptation dialect (per-platform; serde-default
    /// Kimi keeps pre-field sessions and BYOK endpoints on the historical
    /// behavior).
    #[serde(default)]
    pub chat_compat: ChatCompat,
    /// Which API backend to use for this model
    #[serde(default)]
    pub api_backend: ApiBackend,
    /// Extra headers to send with requests (e.g., for BYOK scenarios).
    #[serde(default, skip_serializing_if = "indexmap::IndexMap::is_empty")]
    pub extra_headers: indexmap::IndexMap<String, String>,
    /// Total context window size in tokens. Used for auto-compact thresholds.
    pub context_window: NonZeroU64,
    /// Reasoning effort level for reasoning models.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reasoning_effort: Option<ReasoningEffort>,
    /// When true, inject `stream_tool_calls: true` into the Responses
    /// API request body so the upstream emits per-chunk argument deltas.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub stream_tool_calls: Option<bool>,
}

// ============ Responses API wrapper ============

/// Wrapper around `async_openai::types::responses::CreateResponse` that adds
/// custom header fields for xAI request tracking, similar to
/// `ChatCompletionRequest`.
#[derive(Debug, Clone, Default)]
pub struct CreateResponseWrapper {
    /// The inner Responses API request.
    pub inner: crate::rs::CreateResponse,

    /// Canonical reasoning effort for this request. The typed
    /// `inner.reasoning.effort` stays `None` (async-openai's enum cannot
    /// spell `max`); [`patch_reasoning_effort`] writes this value onto the
    /// serialized body just before send.
    pub reasoning_effort: Option<ReasoningEffort>,

    /// Custom header: conversation ID for tracking.
    pub x_kigi_conv_id: Option<String>,

    /// Custom header: request ID for tracking.
    pub x_kigi_req_id: Option<String>,

    pub x_kigi_session_id: Option<String>,
    pub x_kigi_turn_idx: Option<String>,
    pub x_kigi_agent_id: Option<String>,
    pub x_kigi_deployment_id: Option<String>,
    pub x_kigi_user_id: Option<String>,

    /// Optional tracing context (e.g., where to persist the finalized request payload).
    pub trace: Option<Box<dyn TraceContext>>,

    /// xAI-specific tool definitions that can't be expressed via
    /// `async_openai`'s `rs::Tool` enum (e.g., `x_search`). Injected
    /// as raw JSON into the serialized request body's `tools` array.
    pub extra_raw_tools: Vec<serde_json::Value>,
}

impl CreateResponseWrapper {
    /// Create a new wrapper from an existing `CreateResponse`.
    pub fn new(inner: crate::rs::CreateResponse) -> Self {
        Self {
            inner,
            reasoning_effort: None,
            x_kigi_conv_id: None,
            x_kigi_req_id: None,
            x_kigi_session_id: None,
            x_kigi_turn_idx: None,
            x_kigi_agent_id: None,
            x_kigi_deployment_id: None,
            x_kigi_user_id: None,
            trace: None,
            extra_raw_tools: vec![],
        }
    }

    /// Set the conversation ID header.
    pub fn with_conv_id(mut self, conv_id: impl Into<String>) -> Self {
        self.x_kigi_conv_id = Some(conv_id.into());
        self
    }

    /// Set the request ID header.
    pub fn with_req_id(mut self, req_id: impl Into<String>) -> Self {
        self.x_kigi_req_id = Some(req_id.into());
        self
    }

    /// Set the trace context for request logging.
    pub fn with_trace(mut self, trace: impl TraceContext + 'static) -> Self {
        self.trace = Some(Box::new(trace));
        self
    }
}

impl From<crate::rs::CreateResponse> for CreateResponseWrapper {
    fn from(inner: crate::rs::CreateResponse) -> Self {
        Self::new(inner)
    }
}

// ============ Messages API wrapper ============

/// Wrapper around `MessagesRequest` that adds custom header fields for xAI
/// request tracking, analogous to `CreateResponseWrapper`.
#[derive(Debug, Clone, Default)]
pub struct MessagesRequestWrapper {
    /// The inner Messages API request.
    pub inner: crate::messages::MessagesRequest,

    /// Custom header: conversation ID for tracking.
    pub x_kigi_conv_id: Option<String>,

    /// Custom header: request ID for tracking.
    pub x_kigi_req_id: Option<String>,

    pub x_kigi_session_id: Option<String>,
    pub x_kigi_turn_idx: Option<String>,
    pub x_kigi_agent_id: Option<String>,
    pub x_kigi_deployment_id: Option<String>,
    pub x_kigi_user_id: Option<String>,

    /// Optional tracing context (e.g., where to persist the finalized request payload).
    pub trace: Option<Box<dyn TraceContext>>,
}

impl MessagesRequestWrapper {
    /// Create a new wrapper from an existing `MessagesRequest`.
    pub fn new(inner: crate::messages::MessagesRequest) -> Self {
        Self {
            inner,
            x_kigi_conv_id: None,
            x_kigi_req_id: None,
            x_kigi_session_id: None,
            x_kigi_turn_idx: None,
            x_kigi_agent_id: None,
            x_kigi_deployment_id: None,
            x_kigi_user_id: None,
            trace: None,
        }
    }

    /// Set the conversation ID header.
    pub fn with_conv_id(mut self, conv_id: impl Into<String>) -> Self {
        self.x_kigi_conv_id = Some(conv_id.into());
        self
    }

    /// Set the request ID header.
    pub fn with_req_id(mut self, req_id: impl Into<String>) -> Self {
        self.x_kigi_req_id = Some(req_id.into());
        self
    }

    /// Set the trace context for request logging.
    pub fn with_trace(mut self, trace: impl TraceContext + 'static) -> Self {
        self.trace = Some(Box::new(trace));
        self
    }
}

impl From<crate::messages::MessagesRequest> for MessagesRequestWrapper {
    fn from(inner: crate::messages::MessagesRequest) -> Self {
        Self::new(inner)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    /// The Codex backend rejects `role: system` input outright
    /// (400 `{"detail":"System messages are not allowed"}`) — its system
    /// channel is the top-level `instructions` field, and stateless
    /// (`store: false`) reasoning replay needs
    /// `include: ["reasoning.encrypted_content"]`. The adapter must hoist
    /// every system item (string or parts content, order preserved),
    /// append to existing instructions, and leave the rest of the input
    /// untouched.
    #[test]
    fn codex_adapter_hoists_system_messages_and_requests_encrypted_reasoning() {
        let mut body = json!({
            "model": "gpt-5.2-codex",
            "instructions": "base",
            "input": [
                {"type": "message", "role": "system", "content": "sys head"},
                {"type": "message", "role": "user", "content": "hello"},
                {"type": "message", "role": "system", "content": [
                    {"type": "input_text", "text": "memory reminder"}
                ]},
                {"type": "message", "role": "assistant", "content": "hi"}
            ]
        });
        adapt_body_for_codex_backend(&mut body);

        let input = body["input"].as_array().unwrap();
        assert_eq!(input.len(), 2, "system items removed from input: {body:#}");
        assert!(
            input.iter().all(|i| i["role"] != "system"),
            "no system role may remain: {body:#}"
        );
        assert_eq!(
            body["instructions"], "base\n\nsys head\n\nmemory reminder",
            "system content hoisted into instructions, order preserved"
        );
        assert_eq!(
            body["include"],
            json!(["reasoning.encrypted_content"]),
            "stateless reasoning replay requires the include"
        );

        // Idempotent: a second pass changes nothing.
        let before = body.clone();
        adapt_body_for_codex_backend(&mut body);
        assert_eq!(body, before);
    }

    /// Stateless codex cannot resolve a bare `rs_*` reference: reasoning
    /// input items without an encrypted payload are dropped; items WITH
    /// one pass through untouched.
    #[test]
    fn codex_adapter_drops_reasoning_without_encrypted_payload() {
        let mut body = json!({
            "model": "gpt-5.2-codex",
            "input": [
                {"type": "message", "role": "user", "content": "q"},
                {"type": "reasoning", "id": "rs_bare", "summary": []},
                {"type": "reasoning", "id": "rs_full", "summary": [],
                 "encrypted_content": "gAAAA-blob"},
                {"type": "message", "role": "assistant", "content": "a"}
            ]
        });
        adapt_body_for_codex_backend(&mut body);
        let input = body["input"].as_array().unwrap();
        assert_eq!(input.len(), 3, "bare rs_* item dropped: {body:#}");
        assert!(
            input
                .iter()
                .any(|i| i.get("id").and_then(|v| v.as_str()) == Some("rs_full")),
            "encrypted reasoning passes through: {body:#}"
        );
        assert!(
            !input
                .iter()
                .any(|i| i.get("id").and_then(|v| v.as_str()) == Some("rs_bare")),
            "{body:#}"
        );
    }

    /// No system items and no prior instructions: input untouched, no
    /// empty-string instructions invented, include still requested.
    #[test]
    fn codex_adapter_without_system_messages_only_adds_include() {
        let mut body = json!({
            "model": "gpt-5.2-codex",
            "input": [{"type": "message", "role": "user", "content": "q"}]
        });
        adapt_body_for_codex_backend(&mut body);
        assert!(body.get("instructions").is_none(), "{body:#}");
        assert_eq!(body["input"].as_array().unwrap().len(), 1);
        assert_eq!(body["include"], json!(["reasoning.encrypted_content"]));
    }

    /// String content (the only shape non-Mistral providers send) stays the
    /// answer verbatim with no thinking — byte-identical to the pre-change
    /// deserialization.
    /// The StrictOpenAi dialect kept the serde alias `mistral`, so sessions
    /// persisted before the rename still deserialize.
    #[test]
    fn chat_compat_mistral_alias_deserializes_to_strict_openai() {
        let v: ChatCompat = serde_json::from_str("\"mistral\"").unwrap();
        assert_eq!(v, ChatCompat::StrictOpenAi);
        // New value round-trips as strict_open_ai.
        let v: ChatCompat = serde_json::from_str("\"strict_open_ai\"").unwrap();
        assert_eq!(v, ChatCompat::StrictOpenAi);
        assert_eq!(
            serde_json::to_string(&ChatCompat::StrictOpenAi).unwrap(),
            "\"strict_open_ai\""
        );
    }

    #[test]
    fn chunk_delta_string_content_unchanged() {
        let delta: ChatChunkDelta =
            serde_json::from_value(json!({ "content": "hello world" })).unwrap();
        assert_eq!(delta.content.as_deref(), Some("hello world"));
        assert_eq!(delta.reasoning_content, None);
        // A DeepSeek-style wire reasoning_content still rides its own field.
        let delta: ChatChunkDelta =
            serde_json::from_value(json!({ "content": "hi", "reasoning_content": "because" }))
                .unwrap();
        assert_eq!(delta.content.as_deref(), Some("hi"));
        assert_eq!(delta.reasoning_content.as_deref(), Some("because"));
        // Null / absent content → None.
        let delta: ChatChunkDelta = serde_json::from_value(json!({})).unwrap();
        assert_eq!(delta.content, None);
    }

    /// Mistral reasoning: array content routes `text` chunks to the answer
    /// and the nested `text` of `thinking` chunks to reasoning_content
    /// (verified shapes from the mistralai/client-python SDK).
    #[test]
    fn chunk_delta_array_content_splits_thinking_and_answer() {
        // Thinking phase: array with only a thinking chunk (nested list).
        let delta: ChatChunkDelta = serde_json::from_value(json!({
            "content": [
                { "type": "thinking", "thinking": [{ "type": "text", "text": "step 1" }],
                  "signature": null, "closed": false }
            ]
        }))
        .unwrap();
        assert_eq!(delta.content, None, "thinking phase has no visible answer");
        assert_eq!(delta.reasoning_content.as_deref(), Some("step 1"));

        // Answer phase: plain string delta.
        let delta: ChatChunkDelta =
            serde_json::from_value(json!({ "content": "the answer" })).unwrap();
        assert_eq!(delta.content.as_deref(), Some("the answer"));
        assert_eq!(delta.reasoning_content, None);

        // Transition delta: one array carrying both a closing thinking chunk
        // and the first text chunk → both channels populated.
        let delta: ChatChunkDelta = serde_json::from_value(json!({
            "content": [
                { "type": "thinking", "thinking": [{ "type": "text", "text": "done" }] },
                { "type": "text", "text": "Answer: 22" }
            ]
        }))
        .unwrap();
        assert_eq!(delta.content.as_deref(), Some("Answer: 22"));
        assert_eq!(delta.reasoning_content.as_deref(), Some("done"));
    }

    /// The chunk union is OPEN: unknown chunk types and reference chunks are
    /// ignored, never fatal (SDK's UnknownContentChunk fallback).
    #[test]
    fn content_array_tolerates_unknown_chunk_types() {
        let delta: ChatChunkDelta = serde_json::from_value(json!({
            "content": [
                { "type": "reference", "reference_ids": [1, 2] },
                { "type": "some_future_chunk", "payload": { "x": 1 } },
                { "type": "text", "text": "visible" }
            ]
        }))
        .unwrap();
        assert_eq!(delta.content.as_deref(), Some("visible"));
        assert_eq!(delta.reasoning_content, None);
    }

    /// Non-streaming ChatResponseMessage gets the same array handling.
    #[test]
    fn response_message_array_content_splits() {
        let msg: ChatResponseMessage = serde_json::from_value(json!({
            "role": "assistant",
            "content": [
                { "type": "thinking", "thinking": [{ "type": "text", "text": "reasoning" }] },
                { "type": "text", "text": "final" }
            ]
        }))
        .unwrap();
        assert_eq!(msg.content.as_deref(), Some("final"));
        assert_eq!(msg.reasoning_content.as_deref(), Some("reasoning"));
        // String content still works.
        let msg: ChatResponseMessage = serde_json::from_value(json!({
            "role": "assistant", "content": "plain"
        }))
        .unwrap();
        assert_eq!(msg.content.as_deref(), Some("plain"));
    }

    #[test]
    fn reasoning_effort_serde_lowercase_round_trip() {
        for v in [
            ReasoningEffort::None,
            ReasoningEffort::Minimal,
            ReasoningEffort::Low,
            ReasoningEffort::Medium,
            ReasoningEffort::High,
            ReasoningEffort::Xhigh,
            ReasoningEffort::Max,
        ] {
            let json = serde_json::to_string(&v).unwrap();
            assert_eq!(json, format!("\"{}\"", v.as_str()), "serialize {v:?}");
            let back: ReasoningEffort = serde_json::from_str(&json).unwrap();
            assert_eq!(back, v, "round-trip {v:?}");
        }
        assert!(serde_json::from_str::<ReasoningEffort>("\"BOGUS\"").is_err());
    }

    #[test]
    fn reasoning_effort_from_str_max_and_xhigh_are_distinct() {
        assert_eq!(
            "max".parse::<ReasoningEffort>().unwrap(),
            ReasoningEffort::Max
        );
        assert_eq!(
            "MAX".parse::<ReasoningEffort>().unwrap(),
            ReasoningEffort::Max
        );
        assert_eq!(
            "xhigh".parse::<ReasoningEffort>().unwrap(),
            ReasoningEffort::Xhigh
        );
        // Messages API: distinct wire tokens per level (2026 capabilities).
        assert_eq!(ReasoningEffort::Xhigh.to_messages_api(), Some("xhigh"));
        assert_eq!(ReasoningEffort::Max.to_messages_api(), Some("max"));
    }

    /// The codex-only `ultra` tier parses, serializes, and patches onto a
    /// Responses body as `reasoning.effort = "ultra"` (the crux of surfacing a
    /// codex model's full thinking menu). It is a DISTINCT level above `max`.
    #[test]
    fn reasoning_effort_ultra_is_a_distinct_codex_tier() {
        assert_eq!(
            "ultra".parse::<ReasoningEffort>().unwrap(),
            ReasoningEffort::Ultra
        );
        assert_eq!(
            "ULTRA".parse::<ReasoningEffort>().unwrap(),
            ReasoningEffort::Ultra
        );
        assert_ne!(ReasoningEffort::Ultra, ReasoningEffort::Max);
        assert_eq!(ReasoningEffort::Ultra.as_str(), "ultra");
        let json = serde_json::to_string(&ReasoningEffort::Ultra).unwrap();
        assert_eq!(json, "\"ultra\"");
        assert_eq!(
            serde_json::from_str::<ReasoningEffort>("\"ultra\"").unwrap(),
            ReasoningEffort::Ultra
        );
        let mut body = serde_json::json!({ "model": "gpt-5.6-sol" });
        patch_reasoning_effort(&mut body, Some(ReasoningEffort::Ultra));
        assert_eq!(body["reasoning"]["effort"], "ultra");
    }

    /// The account id is decoded STATELESSLY from the bearer JWT payload's
    /// `["https://api.openai.com/auth"]["chatgpt_account_id"]` claim; a token
    /// without the claim (or not a JWT) yields `None` (login fails fast on it).
    #[test]
    fn chatgpt_account_id_extracted_from_jwt_claim() {
        use base64::Engine;
        let make_jwt = |payload: serde_json::Value| -> String {
            let header =
                base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(br#"{"alg":"none"}"#);
            let body = base64::engine::general_purpose::URL_SAFE_NO_PAD
                .encode(serde_json::to_vec(&payload).unwrap());
            format!("{header}.{body}.sig")
        };
        let good = make_jwt(serde_json::json!({
            "https://api.openai.com/auth": { "chatgpt_account_id": "acct-abc-123" },
            "sub": "user-1"
        }));
        assert_eq!(
            chatgpt_account_id_from_jwt(&good).as_deref(),
            Some("acct-abc-123")
        );
        // Claim namespace present but no account id → None (fail-fast).
        let no_account = make_jwt(serde_json::json!({
            "https://api.openai.com/auth": { "user_id": "u" }
        }));
        assert_eq!(chatgpt_account_id_from_jwt(&no_account), None);
        // Empty account id → None.
        let empty = make_jwt(serde_json::json!({
            "https://api.openai.com/auth": { "chatgpt_account_id": "" }
        }));
        assert_eq!(chatgpt_account_id_from_jwt(&empty), None);
        // Not a JWT (no payload segment) → None.
        assert_eq!(chatgpt_account_id_from_jwt("not-a-jwt"), None);
        assert_eq!(chatgpt_account_id_from_jwt(""), None);
        // A JWT is EXACTLY three segments: a lookalike with too few or too many
        // is rejected outright rather than decoded (fail closed).
        let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
            serde_json::to_vec(&serde_json::json!({
                "https://api.openai.com/auth": { "chatgpt_account_id": "acct-abc-123" }
            }))
            .unwrap(),
        );
        assert_eq!(
            chatgpt_account_id_from_jwt(&format!("hdr.{payload}")),
            None,
            "two segments is not a JWT"
        );
        assert_eq!(
            chatgpt_account_id_from_jwt(&format!("hdr.{payload}.sig.extra")),
            None,
            "four segments is not a JWT"
        );
    }

    #[test]
    fn parse_canonical_effort_token_helper() {
        assert_eq!(
            parse_canonical_effort_token("max"),
            Some(ReasoningEffort::Max)
        );
        assert_eq!(
            parse_canonical_effort_token("high"),
            Some(ReasoningEffort::High)
        );
        assert!(parse_canonical_effort_token("deep").is_none());
        assert!(parse_canonical_effort_token("bogus").is_none());
    }

    #[test]
    fn reasoning_effort_option_deserializes_bare_string() {
        let opt: ReasoningEffortOption = serde_json::from_value(json!("xhigh")).unwrap();
        assert_eq!(
            opt,
            ReasoningEffortOption {
                id: "xhigh".to_string(),
                value: ReasoningEffort::Xhigh,
                label: "Xhigh".to_string(),
                description: None,
                default: false,
            }
        );
    }

    #[test]
    fn reasoning_effort_option_table_defaults_id_and_label_from_value() {
        let opt: ReasoningEffortOption =
            serde_json::from_value(json!({ "value": "high" })).unwrap();
        assert_eq!(opt.id, "high");
        assert_eq!(opt.label, "High");
        assert_eq!(opt.value, ReasoningEffort::High);
        assert!(!opt.default);
    }

    #[test]
    fn reasoning_effort_option_table_honors_explicit_fields() {
        let opt: ReasoningEffortOption = serde_json::from_value(json!({
            "id": "deep",
            "value": "xhigh",
            "label": "Deep",
            "description": "Maximum reasoning",
            "default": true,
        }))
        .unwrap();
        assert_eq!(opt.id, "deep");
        assert_eq!(opt.value, ReasoningEffort::Xhigh);
        assert_eq!(opt.label, "Deep");
        assert_eq!(opt.description.as_deref(), Some("Maximum reasoning"));
        assert!(opt.default);
    }

    #[test]
    fn parse_reasoning_efforts_meta_absent_is_none() {
        assert!(parse_reasoning_efforts_meta(None).is_none());
        assert!(
            parse_reasoning_efforts_meta(Some(json!({ "agentType": "kigi" }).as_object().unwrap()))
                .is_none()
        );
    }

    #[test]
    fn parse_reasoning_efforts_meta_skips_invalid_value() {
        let meta = json!({
            REASONING_EFFORTS_META_KEY: [
                { "value": "high" },
                { "value": "quantum" },
                "low",
            ]
        })
        .as_object()
        .cloned()
        .unwrap();
        let parsed = parse_reasoning_efforts_meta(Some(&meta)).unwrap();
        assert_eq!(parsed.len(), 2);
        assert_eq!(parsed[0].value, ReasoningEffort::High);
        assert_eq!(parsed[1].value, ReasoningEffort::Low);
    }

    #[test]
    fn parse_reasoning_efforts_meta_present_but_unusable_is_none() {
        // Explicit empty, non-array, and all-entries-skip-invalidated all collapse
        // to `None` so consumers fall back exactly as they do for an absent key.
        for meta in [
            json!({ REASONING_EFFORTS_META_KEY: [] }),
            json!({ REASONING_EFFORTS_META_KEY: "nope" }),
            json!({ REASONING_EFFORTS_META_KEY: [{ "value": "quantum" }] }),
        ] {
            let meta = meta.as_object().cloned().unwrap();
            assert!(
                parse_reasoning_efforts_meta(Some(&meta)).is_none(),
                "expected None for {meta:?}"
            );
        }
    }

    #[test]
    fn reasoning_efforts_meta_value_round_trips() {
        let opts = vec![
            ReasoningEffortOption {
                id: "deep".to_string(),
                value: ReasoningEffort::Xhigh,
                label: "Deep".to_string(),
                description: Some("Maximum reasoning".to_string()),
                default: true,
            },
            ReasoningEffortOption {
                id: "balanced".to_string(),
                value: ReasoningEffort::Medium,
                label: "Balanced".to_string(),
                description: None,
                default: false,
            },
        ];
        let meta = json!({ REASONING_EFFORTS_META_KEY: reasoning_efforts_meta_value(&opts) })
            .as_object()
            .cloned()
            .unwrap();
        assert_eq!(parse_reasoning_efforts_meta(Some(&meta)).unwrap(), opts);
    }

    #[test]
    fn compactions_remaining_resolve_covers_all_variants() {
        assert_eq!(CompactionsRemaining::Dynamic(false).resolve(false), None);
        assert_eq!(CompactionsRemaining::Dynamic(false).resolve(true), None);
        assert_eq!(CompactionsRemaining::Dynamic(true).resolve(false), Some(1));
        assert_eq!(CompactionsRemaining::Dynamic(true).resolve(true), Some(0));
        assert_eq!(CompactionsRemaining::Fixed(1).resolve(false), Some(1));
        assert_eq!(CompactionsRemaining::Fixed(1).resolve(true), Some(1));
    }

    #[test]
    fn compactions_remaining_untagged_serde_prefers_dynamic_for_bools() {
        assert_eq!(
            serde_json::from_str::<CompactionsRemaining>("true").unwrap(),
            CompactionsRemaining::Dynamic(true)
        );
        assert_eq!(
            serde_json::from_str::<CompactionsRemaining>("false").unwrap(),
            CompactionsRemaining::Dynamic(false)
        );
        assert_eq!(
            serde_json::from_str::<CompactionsRemaining>("1").unwrap(),
            CompactionsRemaining::Fixed(1)
        );
    }

    #[test]
    fn parse_reasoning_effort_meta_handles_all_inputs() {
        let as_map = |v: serde_json::Value| v.as_object().cloned().unwrap();
        assert_eq!(parse_reasoning_effort_meta(None), None);
        let empty = as_map(serde_json::json!({}));
        assert_eq!(parse_reasoning_effort_meta(Some(&empty)), None);
        let ok = as_map(serde_json::json!({"reasoningEffort": "xhigh"}));
        assert_eq!(
            parse_reasoning_effort_meta(Some(&ok)),
            Some(ReasoningEffort::Xhigh)
        );
        let bad_type = as_map(serde_json::json!({"reasoningEffort": 3}));
        assert_eq!(parse_reasoning_effort_meta(Some(&bad_type)), None);
        // `ultra` is now a real codex tier; a genuinely-unknown token still None.
        let unknown = as_map(serde_json::json!({"reasoningEffort": "MEGA"}));
        assert_eq!(parse_reasoning_effort_meta(Some(&unknown)), None);
    }

    #[test]
    fn test_chat_text_content_serialization() {
        let test = vec![ChatContentBlock::Text {
            text: "Hello World!".to_string(),
        }];

        let json = serde_json::to_string(&test).unwrap();
        assert_eq!(json, r#"[{"type":"text","text":"Hello World!"}]"#);
    }

    #[test]
    fn test_chat_all_content_serialization() {
        let test = vec![
            ChatContentBlock::ImageUrl {
                image_url: ImageUrl {
                    url: "https://www.test.com".to_string(),
                },
            },
            ChatContentBlock::Text {
                text: "Hello".to_string(),
            },
        ];

        let json = serde_json::to_string(&test).unwrap();
        assert_eq!(
            json,
            r#"[{"type":"image_url","image_url":{"url":"https://www.test.com"}},{"type":"text","text":"Hello"}]"#
        );
    }

    #[test]
    fn test_content_string_deserialization() {
        let expected_contents = vec!["", "Hello world!"];

        for expected_content in expected_contents {
            let json = format!(r#"{{"content":"{}","role":"assistant"}}"#, expected_content);

            let msg: ChatRequestMessage = serde_json::from_str(&json)
                .unwrap_or_else(|_| panic!("Should deserialize {}", expected_content));

            let blocks = msg.content.blocks();
            assert_eq!(blocks.len(), 1);
            match &blocks[0] {
                ChatContentBlock::Text { text } => assert_eq!(text, expected_content),
                _ => panic!("Expected empty Text block"),
            }
        }
    }

    #[test]
    fn test_chat_chunk_delta_deserialize_with_null_tool_calls() {
        let delta_json = r#"{
            "reasoning": null,
            "reasoning_details": [],
            "content": "",
            "function_call": null,
            "refusal": null,
            "role": "assistant",
            "tool_calls": null
        }"#;

        let result = serde_json::from_str::<ChatChunkDelta>(delta_json);
        assert!(result.is_ok(), "Failed to deserialize: {:?}", result.err());

        let delta = result.unwrap();
        assert_eq!(delta.role, Some(Role::Assistant));
        assert_eq!(delta.content, Some("".to_string()));
        assert!(delta.tool_calls.is_empty());
    }

    /// Regression test: cloning `Box<dyn TraceContext>` must not infinitely recurse.
    ///
    /// The blanket `impl<T: Clone + ...> TraceContext for T` applies to
    /// `Box<dyn TraceContext>` itself. Without the explicit dereference in
    /// `Clone for Box<dyn TraceContext>`, `self.clone_box()` resolves to the
    /// blanket impl's method (via auto-deref) instead of dispatching through
    /// the vtable, causing `clone()` → `clone_box()` → `clone()` → stack overflow.
    #[test]
    fn clone_box_dyn_trace_context_does_not_recurse() {
        #[derive(Debug, Clone)]
        struct TestTrace(String);

        let trace: Box<dyn TraceContext> = Box::new(TestTrace("hello".into()));
        let cloned = trace.clone();

        // Verify the clone produced a valid TraceContext with the same data.
        // Note: `as_any()` must be called through `&dyn TraceContext` (not on the Box
        // directly) to use vtable dispatch rather than the blanket impl.
        let inner: &dyn TraceContext = &*trace;
        let original = inner.as_any().downcast_ref::<TestTrace>().unwrap();

        let cloned_inner_ref: &dyn TraceContext = &*cloned;
        let cloned_inner = cloned_inner_ref
            .as_any()
            .downcast_ref::<TestTrace>()
            .unwrap();
        assert_eq!(original.0, cloned_inner.0);
    }

    /// Verify that cloning a `ChatCompletionRequest` with a trace does not recurse.
    #[test]
    fn clone_chat_completion_request_with_trace() {
        #[derive(Debug, Clone)]
        struct TestTrace(String);

        let mut request = ChatCompletionRequest::new("test-model", vec![]);
        request.trace = Some(Box::new(TestTrace("trace-data".into())));

        let cloned = request.clone();
        assert!(cloned.trace.is_some());

        let cloned_trace = cloned.trace.unwrap();
        let inner: &dyn TraceContext = &*cloned_trace;
        let downcast = inner.as_any().downcast_ref::<TestTrace>().unwrap();
        assert_eq!(downcast.0, "trace-data");
    }
}
