//! Core LLM types shared across the project

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BackendKind {
    Gemini,
    OpenAI,
    Anthropic,
    DeepSeek,
    Mistral,
    OpenRouter,
    Ollama,
    LlamaCpp,
    ZAI,
    Moonshot,
    HuggingFace,
    Minimax,
    MiMo,
    OpenCodeZen,
    OpenCodeGo,
    Qwen,
    StepFun,
    Poolside,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct Usage {
    pub prompt_tokens: u32,
    pub completion_tokens: u32,
    pub total_tokens: u32,
    pub cached_prompt_tokens: Option<u32>,
    pub cache_creation_tokens: Option<u32>,
    pub cache_read_tokens: Option<u32>,
}

impl Usage {
    #[inline]
    fn has_cache_read_metric(&self) -> bool {
        self.cache_read_tokens.is_some() || self.cached_prompt_tokens.is_some()
    }

    #[inline]
    fn has_any_cache_metrics(&self) -> bool {
        self.has_cache_read_metric() || self.cache_creation_tokens.is_some()
    }

    #[inline]
    pub fn cache_read_tokens_or_fallback(&self) -> u32 {
        self.cache_read_tokens
            .or(self.cached_prompt_tokens)
            .unwrap_or(0)
    }

    #[inline]
    pub fn cache_creation_tokens_or_zero(&self) -> u32 {
        self.cache_creation_tokens.unwrap_or(0)
    }

    #[inline]
    pub fn cache_hit_rate(&self) -> Option<f64> {
        if !self.has_any_cache_metrics() {
            return None;
        }
        let read = self.cache_read_tokens_or_fallback() as f64;
        let creation = self.cache_creation_tokens_or_zero() as f64;
        let total = read + creation;
        if total > 0.0 {
            Some((read / total) * 100.0)
        } else {
            None
        }
    }

    #[inline]
    pub fn is_cache_hit(&self) -> Option<bool> {
        self.has_any_cache_metrics()
            .then(|| self.cache_read_tokens_or_fallback() > 0)
    }

    #[inline]
    pub fn is_cache_miss(&self) -> Option<bool> {
        self.has_any_cache_metrics().then(|| {
            self.cache_creation_tokens_or_zero() > 0 && self.cache_read_tokens_or_fallback() == 0
        })
    }

    #[inline]
    pub fn total_cache_tokens(&self) -> u32 {
        let read = self.cache_read_tokens_or_fallback();
        let creation = self.cache_creation_tokens_or_zero();
        read + creation
    }

    #[inline]
    pub fn cache_savings_ratio(&self) -> Option<f64> {
        if !self.has_cache_read_metric() {
            return None;
        }
        let read = self.cache_read_tokens_or_fallback() as f64;
        let prompt = self.prompt_tokens as f64;
        if prompt > 0.0 {
            Some(read / prompt)
        } else {
            None
        }
    }
}

/// Provider-agnostic balance information for account status display.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BalanceInfo {
    /// Human-readable balance string (e.g. "100.00¥", "$50.00").
    pub display: String,
    /// Whether the account has sufficient balance for API calls.
    pub is_available: bool,
}

/// DeepSeek-specific balance info from GET /user/balance
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeepSeekBalanceResponse {
    pub is_available: bool,
    pub balance_infos: Vec<DeepSeekCurrencyBalance>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeepSeekCurrencyBalance {
    pub currency: String,
    pub total_balance: String,
    #[serde(default)]
    pub granted_balance: String,
    #[serde(default)]
    pub topped_up_balance: String,
}

impl From<DeepSeekBalanceResponse> for BalanceInfo {
    fn from(resp: DeepSeekBalanceResponse) -> Self {
        let display = resp
            .balance_infos
            .first()
            .map(|b| {
                let symbol = match b.currency.as_str() {
                    "CNY" => "¥",
                    "USD" => "$",
                    _ => &b.currency,
                };
                format!("{}{}", b.total_balance, symbol)
            })
            .unwrap_or_else(|| "N/A".to_string());
        BalanceInfo {
            display,
            is_available: resp.is_available,
        }
    }
}

#[cfg(test)]
mod usage_tests {
    use super::Usage;

    #[test]
    fn cache_helpers_fall_back_to_cached_prompt_tokens() {
        let usage = Usage {
            prompt_tokens: 1_000,
            completion_tokens: 200,
            total_tokens: 1_200,
            cached_prompt_tokens: Some(600),
            cache_creation_tokens: Some(150),
            cache_read_tokens: None,
        };

        assert_eq!(usage.cache_read_tokens_or_fallback(), 600);
        assert_eq!(usage.cache_creation_tokens_or_zero(), 150);
        assert_eq!(usage.total_cache_tokens(), 750);
        assert_eq!(usage.is_cache_hit(), Some(true));
        assert_eq!(usage.is_cache_miss(), Some(false));
        assert_eq!(usage.cache_savings_ratio(), Some(0.6));
        assert_eq!(usage.cache_hit_rate(), Some(80.0));
    }

    #[test]
    fn cache_helpers_preserve_unknown_without_metrics() {
        let usage = Usage {
            prompt_tokens: 1_000,
            completion_tokens: 200,
            total_tokens: 1_200,
            cached_prompt_tokens: None,
            cache_creation_tokens: None,
            cache_read_tokens: None,
        };

        assert_eq!(usage.total_cache_tokens(), 0);
        assert_eq!(usage.is_cache_hit(), None);
        assert_eq!(usage.is_cache_miss(), None);
        assert_eq!(usage.cache_savings_ratio(), None);
        assert_eq!(usage.cache_hit_rate(), None);
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum FinishReason {
    #[default]
    Stop,
    Length,
    ToolCalls,
    ContentFilter,
    Pause,
    Refusal,
    Error(String),
}

/// Universal tool call that matches OpenAI/Anthropic/Gemini specifications
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolCall {
    /// Unique identifier for this tool call (e.g., "call_123")
    pub id: String,

    /// The type of tool call: "function", "custom" (GPT-5 freeform), or other
    #[serde(rename = "type")]
    pub call_type: String,

    /// Function call details (for function-type tools)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub function: Option<FunctionCall>,

    /// Raw text payload (for custom freeform tools in GPT-5)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub text: Option<String>,

    /// Gemini-specific thought signature for maintaining reasoning context
    #[serde(skip_serializing_if = "Option::is_none")]
    pub thought_signature: Option<String>,
}

/// Function call within a tool call
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FunctionCall {
    /// Optional namespace for grouped or deferred tools.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub namespace: Option<String>,

    /// The name of the function to call
    pub name: String,

    /// The arguments to pass to the function, as a JSON string
    pub arguments: String,
}

impl ToolCall {
    /// Create a new function tool call
    pub fn function(id: String, name: String, arguments: String) -> Self {
        Self::function_with_namespace(id, None, name, arguments)
    }

    /// Create a new function tool call with an optional namespace.
    pub fn function_with_namespace(
        id: String,
        namespace: Option<String>,
        name: String,
        arguments: String,
    ) -> Self {
        Self {
            id,
            call_type: "function".to_owned(),
            function: Some(FunctionCall {
                namespace,
                name,
                arguments,
            }),
            text: None,
            thought_signature: None,
        }
    }

    /// Create a new custom tool call with raw text payload (GPT-5 freeform)
    pub fn custom(id: String, name: String, text: String) -> Self {
        Self {
            id,
            call_type: "custom".to_owned(),
            function: Some(FunctionCall {
                namespace: None,
                name,
                arguments: text.clone(),
            }),
            text: Some(text),
            thought_signature: None,
        }
    }

    /// Returns true when this tool call uses GPT-5 custom/freeform semantics.
    pub fn is_custom(&self) -> bool {
        self.call_type == "custom"
    }

    /// Returns the tool name when the call includes function details.
    pub fn tool_name(&self) -> Option<&str> {
        self.function
            .as_ref()
            .map(|function| function.name.as_str())
    }

    /// Returns the raw payload text exactly as emitted by the model.
    pub fn raw_input(&self) -> Option<&str> {
        self.text.as_deref().or_else(|| {
            self.function
                .as_ref()
                .map(|function| function.arguments.as_str())
        })
    }

    /// Parse the arguments as JSON Value (for function-type tools)
    pub fn parsed_arguments(&self) -> Result<serde_json::Value, serde_json::Error> {
        if let Some(ref func) = self.function {
            parse_tool_arguments(&func.arguments)
        } else {
            // Return an error by trying to parse invalid JSON
            serde_json::from_str("")
        }
    }

    /// Returns the execution payload for this tool call.
    ///
    /// Function tools keep their JSON semantics. Custom tools execute with their
    /// raw text payload wrapped as a JSON string value so freeform inputs can
    /// flow through the existing tool pipeline.
    pub fn execution_arguments(&self) -> Result<serde_json::Value, serde_json::Error> {
        if self.is_custom() {
            return Ok(serde_json::Value::String(
                self.raw_input().unwrap_or_default().to_string(),
            ));
        }

        self.parsed_arguments()
    }

    /// Validate that this tool call is properly formed
    pub fn validate(&self) -> Result<(), String> {
        if self.id.is_empty() {
            return Err("Tool call ID cannot be empty".to_owned());
        }

        match self.call_type.as_str() {
            "function" => {
                if let Some(func) = &self.function {
                    if func.name.is_empty() {
                        return Err("Function name cannot be empty".to_owned());
                    }
                    // Validate that arguments is valid JSON for function tools
                    if let Err(e) = self.parsed_arguments() {
                        return Err(format!("Invalid JSON in function arguments: {}", e));
                    }
                } else {
                    return Err("Function tool call missing function details".to_owned());
                }
            }
            "custom" => {
                // For custom tools, we allow raw text payload without JSON validation
                if let Some(func) = &self.function {
                    if func.name.is_empty() {
                        return Err("Custom tool name cannot be empty".to_owned());
                    }
                } else {
                    return Err("Custom tool call missing function details".to_owned());
                }
            }
            _ => return Err(format!("Unsupported tool call type: {}", self.call_type)),
        }

        Ok(())
    }
}

fn parse_tool_arguments(raw_arguments: &str) -> Result<serde_json::Value, serde_json::Error> {
    let trimmed = raw_arguments.trim();
    match serde_json::from_str(trimmed) {
        Ok(parsed) => Ok(parsed),
        Err(primary_error) => {
            if let Some(candidate) = extract_balanced_json(trimmed)
                && let Ok(parsed) = serde_json::from_str(candidate)
            {
                return Ok(parsed);
            }
            if let Some(candidate) = repair_tag_polluted_json(trimmed)
                && let Ok(parsed) = serde_json::from_str(&candidate)
            {
                return Ok(parsed);
            }
            Err(primary_error)
        }
    }
}

fn extract_balanced_json(input: &str) -> Option<&str> {
    let start = input.find(['{', '['])?;
    let opening = input.as_bytes().get(start).copied()?;
    let closing = match opening {
        b'{' => b'}',
        b'[' => b']',
        _ => return None,
    };

    let mut depth = 0usize;
    let mut in_string = false;
    let mut escaped = false;

    for (offset, ch) in input[start..].char_indices() {
        if in_string {
            if escaped {
                escaped = false;
                continue;
            }
            if ch == '\\' {
                escaped = true;
                continue;
            }
            if ch == '"' {
                in_string = false;
            }
            continue;
        }

        match ch {
            '"' => in_string = true,
            _ if ch as u32 == opening as u32 => depth += 1,
            _ if ch as u32 == closing as u32 => {
                depth = depth.saturating_sub(1);
                if depth == 0 {
                    let end = start + offset + ch.len_utf8();
                    return input.get(start..end);
                }
            }
            _ => {}
        }
    }

    None
}

fn repair_tag_polluted_json(input: &str) -> Option<String> {
    let start = input.find(['{', '['])?;
    let candidate = input.get(start..)?;
    let boundary = find_provider_markup_boundary(candidate)?;
    if boundary == 0 {
        return None;
    }

    close_incomplete_json_prefix(candidate[..boundary].trim_end())
}

fn find_provider_markup_boundary(input: &str) -> Option<usize> {
    const PROVIDER_MARKERS: &[&str] = &[
        "<</",
        "</parameter>",
        "</invoke>",
        "</minimax:tool_call>",
        "<minimax:tool_call>",
        "<parameter name=\"",
        "<invoke name=\"",
        "<tool_call>",
        "</tool_call>",
    ];

    input.char_indices().find_map(|(offset, _)| {
        let rest = input.get(offset..)?;
        PROVIDER_MARKERS
            .iter()
            .any(|marker| rest.starts_with(marker))
            .then_some(offset)
    })
}

fn close_incomplete_json_prefix(prefix: &str) -> Option<String> {
    if prefix.is_empty() {
        return None;
    }

    let mut repaired = String::with_capacity(prefix.len() + 8);
    let mut expected_closers = Vec::new();
    let mut in_string = false;
    let mut escaped = false;

    for ch in prefix.chars() {
        repaired.push(ch);

        if in_string {
            if escaped {
                escaped = false;
                continue;
            }

            match ch {
                '\\' => escaped = true,
                '"' => in_string = false,
                _ => {}
            }
            continue;
        }

        match ch {
            '"' => in_string = true,
            '{' => expected_closers.push('}'),
            '[' => expected_closers.push(']'),
            '}' | ']' => {
                if expected_closers.pop() != Some(ch) {
                    return None;
                }
            }
            _ => {}
        }
    }

    if in_string {
        repaired.push('"');
    }
    for closer in expected_closers.drain(..) {
        repaired.push(closer);
    }

    Some(repaired)
}

/// Universal LLM response structure
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct LLMResponse {
    /// The response content text
    pub content: Option<String>,

    /// Tool calls made by the model
    pub tool_calls: Option<Vec<ToolCall>>,

    /// The model that generated this response
    pub model: String,

    /// Token usage statistics
    pub usage: Option<Usage>,

    /// Why the response finished
    pub finish_reason: FinishReason,

    /// Reasoning content (for models that support it)
    pub reasoning: Option<String>,

    /// Detailed reasoning traces (for models that support it)
    pub reasoning_details: Option<Vec<String>>,

    /// Tool references for context
    pub tool_references: Vec<String>,

    /// Request ID from the provider
    pub request_id: Option<String>,

    /// Organization ID from the provider
    pub organization_id: Option<String>,

    /// Compaction summary content from Anthropic's server-side compaction.
    /// Populated when `stop_reason` is `Pause` (from `"compaction"`).
    /// The caller should pass this back in subsequent requests so the API
    /// can drop prior messages before the compaction block.
    pub compaction: Option<String>,
}

impl LLMResponse {
    /// Create a new LLM response with mandatory fields
    pub fn new(model: impl Into<String>, content: impl Into<String>) -> Self {
        Self {
            content: Some(content.into()),
            tool_calls: None,
            model: model.into(),
            usage: None,
            finish_reason: FinishReason::Stop,
            reasoning: None,
            reasoning_details: None,
            tool_references: Vec::new(),
            request_id: None,
            organization_id: None,
            compaction: None,
        }
    }

    /// Get content or empty string
    pub fn content_text(&self) -> &str {
        self.content.as_deref().unwrap_or("")
    }

    /// Get content as String (clone)
    pub fn content_string(&self) -> String {
        self.content.clone().unwrap_or_default()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LLMErrorMetadata {
    pub provider: Option<String>,
    pub status: Option<u16>,
    pub code: Option<String>,
    pub request_id: Option<String>,
    pub organization_id: Option<String>,
    pub retry_after: Option<String>,
    pub message: Option<String>,
}

impl LLMErrorMetadata {
    /// Boxed constructor because metadata is always stored inside `Option<Box<LLMErrorMetadata>>`
    /// in the LLMError enum variants.
    #[must_use]
    pub fn new(
        provider: impl Into<String>,
        status: Option<u16>,
        code: Option<String>,
        request_id: Option<String>,
        organization_id: Option<String>,
        retry_after: Option<String>,
        message: Option<String>,
    ) -> Box<Self> {
        Box::new(Self {
            provider: Some(provider.into()),
            status,
            code,
            request_id,
            organization_id,
            retry_after,
            message,
        })
    }
}

/// LLM error types with optional provider metadata
#[derive(Debug, thiserror::Error, Serialize, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum LLMError {
    #[error("Authentication failed: {message}")]
    Authentication {
        message: String,
        metadata: Option<Box<LLMErrorMetadata>>,
    },
    #[error("Rate limit exceeded")]
    RateLimit {
        metadata: Option<Box<LLMErrorMetadata>>,
    },
    #[error("Invalid request: {message}")]
    InvalidRequest {
        message: String,
        metadata: Option<Box<LLMErrorMetadata>>,
    },
    #[error("Network error: {message}")]
    Network {
        message: String,
        metadata: Option<Box<LLMErrorMetadata>>,
    },
    #[error("Provider error: {message}")]
    Provider {
        message: String,
        metadata: Option<Box<LLMErrorMetadata>>,
    },
}

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

    #[test]
    fn parsed_arguments_accepts_trailing_characters() {
        let call = ToolCall::function(
            "call_read".to_string(),
            "read_file".to_string(),
            r#"{"path":"src/main.rs"} trailing text"#.to_string(),
        );

        let parsed = call
            .parsed_arguments()
            .expect("arguments with trailing text should recover");
        assert_eq!(parsed, json!({"path":"src/main.rs"}));
    }

    #[test]
    fn parsed_arguments_accepts_code_fenced_json() {
        let call = ToolCall::function(
            "call_read".to_string(),
            "read_file".to_string(),
            "```json\n{\"path\":\"src/lib.rs\",\"limit\":25}\n```".to_string(),
        );

        let parsed = call
            .parsed_arguments()
            .expect("code-fenced arguments should recover");
        assert_eq!(parsed, json!({"path":"src/lib.rs","limit":25}));
    }

    #[test]
    fn parsed_arguments_rejects_incomplete_json() {
        let call = ToolCall::function(
            "call_read".to_string(),
            "read_file".to_string(),
            r#"{"path":"src/main.rs""#.to_string(),
        );

        assert!(call.parsed_arguments().is_err());
    }

    #[test]
    fn parsed_arguments_recovers_truncated_minimax_markup() {
        let call = ToolCall::function(
            "call_search".to_string(),
            "unified_search".to_string(),
            "{\"action\": \"grep\", \"pattern\": \"persistent_memory\", \"path\": \"vtcode-core/src</parameter>\n<</invoke>\n</minimax:tool_call>".to_string(),
        );

        let parsed = call
            .parsed_arguments()
            .expect("minimax markup spillover should recover");
        assert_eq!(
            parsed,
            json!({
                "action": "grep",
                "pattern": "persistent_memory",
                "path": "vtcode-core/src"
            })
        );
    }

    #[test]
    fn function_call_serializes_optional_namespace() {
        let call = ToolCall::function_with_namespace(
            "call_read".to_string(),
            Some("workspace".to_string()),
            "read_file".to_string(),
            r#"{"path":"src/main.rs"}"#.to_string(),
        );

        let json = serde_json::to_value(&call).expect("tool call should serialize");
        assert_eq!(json["function"]["namespace"], "workspace");
        assert_eq!(json["function"]["name"], "read_file");
    }

    #[test]
    fn custom_tool_call_exposes_raw_execution_arguments() {
        let patch = "*** Begin Patch\n*** End Patch\n".to_string();
        let call = ToolCall::custom(
            "call_patch".to_string(),
            "apply_patch".to_string(),
            patch.clone(),
        );

        assert!(call.is_custom());
        assert_eq!(call.tool_name(), Some("apply_patch"));
        assert_eq!(call.raw_input(), Some(patch.as_str()));
        assert_eq!(
            call.execution_arguments().expect("custom arguments"),
            json!(patch)
        );
        assert!(
            call.parsed_arguments().is_err(),
            "custom tool payload should stay freeform rather than JSON"
        );
    }
}
