use crate::error::ApiError;
use crate::sse::SseParser;
use crate::types::*;
use std::collections::VecDeque;
use std::time::Duration;

const DEFAULT_BASE_URL: &str = "https://api.ternlang.com";
const REQUEST_ID_HEADER: &str = "x-request-id";
const ALT_REQUEST_ID_HEADER: &str = "request-id";
const DEFAULT_INITIAL_BACKOFF: Duration = Duration::from_millis(500);
const DEFAULT_MAX_BACKOFF: Duration = Duration::from_secs(30);

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum LlmProvider {
    // ── First-party ──────────────────────────────────────────────────────────
    Ternlang,
    Anthropic,
    OpenAi,
    Google,
    Xai,
    // ── Inference cloud (all OpenAI-compatible) ───────────────────────────────
    Groq,
    Mistral,
    DeepSeek,
    Together,
    Fireworks,
    DeepInfra,
    OpenRouter,
    Perplexity,
    Cohere,
    Cerebras,
    Novita,
    SambaNova,
    NvidiaNim,
    // ── Regional foundation models (OpenAI-compatible) ───────────────────────
    Zhipu,
    MiniMax,
    Qwen,
    Moonshot,
    Qianfan,
    // ── Inference marketplaces ────────────────────────────────────────────────
    Chutes,
    // ── Enterprise cloud ─────────────────────────────────────────────────────
    Azure,
    Aws,
    // ── Aggregators ──────────────────────────────────────────────────────────
    HuggingFace,
    GitHub,
    // ── Local / offline ──────────────────────────────────────────────────────
    Ollama,
    LmStudio,
    // ── Generic OpenAI-compatible (user-configured base URL) ─────────────────
    OpenAiCompat,
}

impl LlmProvider {
    /// Returns `true` for every provider that speaks the OpenAI /v1/chat/completions wire format.
    pub fn is_openai_compat(self) -> bool {
        !matches!(self, Self::Anthropic | Self::Google | Self::Ternlang | Self::Aws)
    }

    pub fn default_base_url(&self) -> &'static str {
        match self {
            Self::Ternlang     => "https://api.ternlang.com",
            Self::Anthropic    => "https://api.anthropic.com",
            Self::OpenAi       => "https://api.openai.com",
            Self::Google       => "https://generativelanguage.googleapis.com",
            Self::Xai          => "https://api.x.ai",
            Self::Groq         => "https://api.groq.com/openai",
            Self::Mistral      => "https://api.mistral.ai",
            Self::DeepSeek     => "https://api.deepseek.com",
            Self::Together     => "https://api.together.xyz",
            Self::Fireworks    => "https://api.fireworks.ai/inference",
            Self::DeepInfra    => "https://api.deepinfra.com/v1/openai",
            Self::OpenRouter   => "https://openrouter.ai/api",
            Self::Perplexity   => "https://api.perplexity.ai",
            Self::Cohere       => "https://api.cohere.ai",
            Self::Cerebras     => "https://api.cerebras.ai",
            Self::Novita       => "https://api.novita.ai/v3/openai",
            Self::SambaNova    => "https://api.sambanova.ai",
            Self::NvidiaNim    => "https://integrate.api.nvidia.com",
            Self::Zhipu        => "https://open.bigmodel.cn/api/paas/v4",
            Self::MiniMax      => "https://api.minimax.chat/v1",
            Self::Qwen         => "https://dashscope.aliyuncs.com/compatible-mode/v1",
            Self::Moonshot     => "https://api.moonshot.cn",
            Self::Qianfan      => "https://aistudio.baidu.com/llm/lmapi/v3",
            Self::Chutes       => "https://llm.chutes.ai",
            Self::Azure        => "https://api.azure.com",
            Self::Aws          => "https://bedrock-runtime.us-east-1.amazonaws.com",
            Self::HuggingFace  => "https://api-inference.huggingface.co",
            Self::GitHub       => "https://models.inference.ai.azure.com",
            Self::Ollama       => "http://localhost:11434",
            Self::LmStudio     => "http://localhost:1234",
            Self::OpenAiCompat => "http://localhost:11434",
        }
    }

    pub fn api_path(&self) -> &'static str {
        match self {
            Self::Ternlang | Self::Anthropic => "/v1/messages",
            Self::Google => "/v1beta",
            Self::HuggingFace => "/models",
            // All OpenAI-compat providers share this path
            _ => "/v1/chat/completions",
        }
    }

    /// Canonical env-var name for this provider's API key (used for display / docs).
    pub fn env_var(self) -> &'static str {
        match self {
            Self::Ternlang     => "TERNLANG_API_KEY",
            Self::Anthropic    => "ANTHROPIC_API_KEY",
            Self::OpenAi       => "OPENAI_API_KEY",
            Self::Google       => "GEMINI_API_KEY",
            Self::Xai          => "XAI_API_KEY",
            Self::Groq         => "GROQ_API_KEY",
            Self::Mistral      => "MISTRAL_API_KEY",
            Self::DeepSeek     => "DEEPSEEK_API_KEY",
            Self::Together     => "TOGETHER_API_KEY",
            Self::Fireworks    => "FIREWORKS_API_KEY",
            Self::DeepInfra    => "DEEPINFRA_API_KEY",
            Self::OpenRouter   => "OPENROUTER_API_KEY",
            Self::Perplexity   => "PERPLEXITY_API_KEY",
            Self::Cohere       => "COHERE_API_KEY",
            Self::Cerebras     => "CEREBRAS_API_KEY",
            Self::Novita       => "NOVITA_API_KEY",
            Self::SambaNova    => "SAMBANOVA_API_KEY",
            Self::NvidiaNim    => "NVIDIA_API_KEY",
            Self::Zhipu        => "ZHIPU_API_KEY",
            Self::MiniMax      => "MINIMAX_API_KEY",
            Self::Qwen         => "DASHSCOPE_API_KEY",
            Self::Moonshot     => "MOONSHOT_API_KEY",
            Self::Qianfan      => "QIANFAN_API_KEY",
            Self::Chutes       => "CHUTES_API_KEY",
            Self::Azure        => "AZURE_OPENAI_API_KEY",
            Self::Aws          => "AWS_ACCESS_KEY_ID",
            Self::HuggingFace  => "HUGGINGFACE_API_KEY",
            Self::GitHub       => "GITHUB_TOKEN",
            Self::Ollama       => "",
            Self::LmStudio     => "",
            Self::OpenAiCompat => "OPENAI_API_KEY",
        }
    }
}

#[derive(Clone)]
pub struct TernlangClient {
    pub provider: LlmProvider,
    pub base_url: String,
    pub auth: AuthSource,
    pub http: reqwest::Client,
    pub max_retries: u32,
    pub initial_backoff: Duration,
    pub max_backoff: Duration,
}

impl TernlangClient {
    pub fn from_auth(auth: AuthSource) -> Self {
        Self {
            provider: LlmProvider::Ternlang,
            base_url: DEFAULT_BASE_URL.to_string(),
            auth,
            http: reqwest::Client::new(),
            max_retries: 3,
            initial_backoff: DEFAULT_INITIAL_BACKOFF,
            max_backoff: DEFAULT_MAX_BACKOFF,
        }
    }

    pub fn from_env() -> Result<Self, ApiError> {
        Ok(Self::from_auth(AuthSource::from_env_or_saved()?).with_base_url(read_base_url()))
    }

    #[must_use]
    pub fn with_auth_source(mut self, auth: AuthSource) -> Self {
        self.auth = auth;
        self
    }

    #[must_use]
    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
        self.base_url = base_url.into();
        self
    }

    #[must_use]
    pub fn with_provider(mut self, provider: LlmProvider) -> Self {
        self.provider = provider;
        if self.base_url == DEFAULT_BASE_URL {
            self.base_url = provider.default_base_url().to_string();
        }
        self
    }

    async fn send_raw_request(
        &self,
        request: &MessageRequest,
    ) -> Result<reqwest::Response, ApiError> {
        let path = self.provider.api_path();
        let mut request_url = format!("{}/{}", self.base_url.trim_end_matches('/'), path.trim_start_matches('/'));

        let body = match self.provider {
            LlmProvider::Google => {
                let model_id = if request.model.starts_with("models/") {
                    request.model.clone()
                } else {
                    format!("models/{}", request.model)
                };
                let base = format!("{}/v1beta/{}:generateContent", self.base_url.trim_end_matches('/'), model_id);
                request_url = if let Some(key) = self.auth.api_key() {
                    format!("{}?key={}", base, key)
                } else {
                    base
                };
                translate_to_gemini(request)
            }
            LlmProvider::Anthropic => translate_to_anthropic(request),
            LlmProvider::Ternlang | LlmProvider::Aws => {
                serde_json::to_value(request).map_err(ApiError::from)?
            }
            _ if self.provider.is_openai_compat() => translate_to_openai(request),
            _ => serde_json::to_value(request).map_err(ApiError::from)?,
        };

        let mut request_builder = self
            .http
            .post(&request_url)
            .header("content-type", "application/json");

        if self.provider == LlmProvider::Anthropic {
            request_builder = request_builder.header("anthropic-version", "2023-06-01");
        }

        let request_builder = self.auth.apply(self.provider, request_builder);

        request_builder.json(&body).send().await.map_err(ApiError::from)
    }

    pub async fn send_message(
        &self,
        request: &MessageRequest,
    ) -> Result<MessageResponse, ApiError> {
        let request = MessageRequest {
            stream: false,
            ..request.clone()
        };
        let response = self.send_with_retry(&request).await?;
        let request_id = request_id_from_headers(response.headers());
        let response_json = response
            .json::<serde_json::Value>()
            .await
            .map_err(ApiError::from)?;
        
        let mut final_response = match self.provider {
            LlmProvider::Google => translate_from_gemini(response_json, &request.model),
            LlmProvider::Anthropic => translate_from_anthropic(response_json, &request.model),
            LlmProvider::Ternlang | LlmProvider::Aws => {
                serde_json::from_value::<MessageResponse>(response_json).map_err(ApiError::from)?
            }
            _ if self.provider.is_openai_compat() => translate_from_openai(response_json, &request.model),
            _ => serde_json::from_value::<MessageResponse>(response_json).map_err(ApiError::from)?,
        };

        if final_response.request_id.is_none() {
            final_response.request_id = request_id;
        }
        Ok(final_response)
    }

    pub async fn stream_message(
        &mut self,
        request: &MessageRequest,
    ) -> Result<MessageStream, ApiError> {
        // Gemini SSE format differs from Anthropic's — use non-streaming and wrap events
        if self.provider == LlmProvider::Google {
            let non_stream_req = MessageRequest { stream: false, ..request.clone() };
            let buffered = self.send_message(&non_stream_req).await?;
            return Ok(MessageStream::from_buffered_response(buffered));
        }
        let response = self
            .send_with_retry(&request.clone().with_streaming())
            .await?;
        Ok(MessageStream {
            _request_id: request_id_from_headers(response.headers()),
            response: Some(response),
            parser: SseParser::new(),
            pending: VecDeque::new(),
            done: false,
        })
    }

    async fn send_with_retry(
        &self,
        request: &MessageRequest,
    ) -> Result<reqwest::Response, ApiError> {
        let mut attempts = 0;
        let mut last_error: Option<ApiError>;

        loop {
            attempts += 1;
            match self.send_raw_request(request).await {
                Ok(response) => match expect_success(response).await {
                    Ok(response) => return Ok(response),
                    Err(error) if error.is_retryable() && attempts <= self.max_retries => {
                        last_error = Some(error);
                    }
                    Err(error) => return Err(error),
                },
                Err(error) if error.is_retryable() && attempts <= self.max_retries => {
                    last_error = Some(error);
                }
                Err(error) => return Err(error),
            }

            if attempts > self.max_retries {
                break;
            }

            tokio::time::sleep(self.backoff_for_attempt(attempts)?).await;
        }

        Err(ApiError::RetriesExhausted {
            attempts,
            last_error: Box::new(last_error.unwrap_or(ApiError::Auth("Max retries exceeded without error capture".to_string()))),
        })
    }

    fn backoff_for_attempt(&self, attempt: u32) -> Result<Duration, ApiError> {
        let multiplier = 2_u32.pow(attempt.saturating_sub(1));
        Ok(self
            .initial_backoff
            .checked_mul(multiplier)
            .map_or(self.max_backoff, |delay| delay.min(self.max_backoff)))
    }

    pub async fn list_remote_models(&self) -> Result<Vec<String>, ApiError> {
        match self.provider {
            LlmProvider::Google => {
                let base = self.base_url.trim_end_matches('/');
                let url = if let Some(key) = self.auth.api_key() {
                    format!("{}/v1beta/models?key={}", base, key)
                } else {
                    format!("{}/v1beta/models", base)
                };
                let res = self.http.get(&url).send().await.map_err(ApiError::from)?;
                let json: serde_json::Value = res.json().await.map_err(ApiError::from)?;
                let mut models = vec![];
                if let Some(list) = json.get("models").and_then(|m| m.as_array()) {
                    for m in list {
                        if let Some(name) = m.get("name").and_then(|n| n.as_str()) {
                            let id = name.replace("models/", "");
                            if !models.contains(&id) { models.push(id); }
                        }
                    }
                }
                if models.is_empty() {
                    return Ok(curated_models(self.provider));
                }
                Ok(models)
            }
            LlmProvider::Anthropic => Ok(curated_models(self.provider)),
            LlmProvider::Ternlang => {
                // Try the live /v1/models endpoint with auth; fall back to curated list.
                let url = format!("{}/v1/models", self.base_url.trim_end_matches('/'));
                if let Ok(res) = self.auth.apply(self.provider, self.http.get(&url)).send().await {
                    if res.status().is_success() {
                        if let Ok(json) = res.json::<serde_json::Value>().await {
                            let mut models = vec![];
                            if let Some(list) = json.get("data").and_then(|m| m.as_array()) {
                                for m in list {
                                    if let Some(id) = m.get("id").and_then(|i| i.as_str()) {
                                        if !models.contains(&id.to_string()) {
                                            models.push(id.to_string());
                                        }
                                    }
                                }
                            }
                            if !models.is_empty() {
                                return Ok(models);
                            }
                        }
                    }
                }
                Ok(curated_models(self.provider))
            }
            LlmProvider::Ollama => {
                let base = self.base_url.trim_end_matches('/');
                // Try OpenAI-compat /v1/models first (Ollama 0.1.14+ supports it).
                if let Ok(res) = self.http.get(&format!("{}/v1/models", base)).send().await {
                    if res.status().is_success() {
                        if let Ok(json) = res.json::<serde_json::Value>().await {
                            let mut models = vec![];
                            if let Some(list) = json.get("data").and_then(|m| m.as_array()) {
                                for m in list {
                                    if let Some(id) = m.get("id").and_then(|i| i.as_str()) {
                                        if !models.contains(&id.to_string()) {
                                            models.push(id.to_string());
                                        }
                                    }
                                }
                            }
                            if !models.is_empty() {
                                return Ok(models);
                            }
                        }
                    }
                }
                // Fall back to Ollama native /api/tags — returns actually installed models.
                let res = self
                    .http
                    .get(&format!("{}/api/tags", base))
                    .send()
                    .await
                    .map_err(|e| ApiError::Config(format!(
                        "Ollama is not reachable at {} — is it running? ({})",
                        base, e
                    )))?;
                if !res.status().is_success() {
                    return Err(ApiError::Config(format!(
                        "Ollama returned HTTP {} from /api/tags — check that it is running",
                        res.status()
                    )));
                }
                let json: serde_json::Value = res.json().await.map_err(ApiError::from)?;
                let mut models = vec![];
                if let Some(list) = json.get("models").and_then(|m| m.as_array()) {
                    for m in list {
                        if let Some(name) = m.get("name").and_then(|n| n.as_str()) {
                            if !models.contains(&name.to_string()) {
                                models.push(name.to_string());
                            }
                        }
                    }
                }
                if models.is_empty() {
                    Err(ApiError::Config(
                        "Ollama is running but has no models installed. Run: ollama pull <model-name>".to_string()
                    ))
                } else {
                    Ok(models)
                }
            }
            _ if self.provider.is_openai_compat() => {
                let url = format!("{}/v1/models", self.base_url.trim_end_matches('/'));
                let res = self.auth.apply(self.provider, self.http.get(&url)).send().await.map_err(ApiError::from)?;
                if !res.status().is_success() {
                    return Ok(curated_models(self.provider));
                }
                let json: serde_json::Value = res.json().await.map_err(ApiError::from)?;
                let mut models = vec![];
                if let Some(list) = json.get("data").and_then(|m| m.as_array()) {
                    for m in list {
                        if let Some(id) = m.get("id").and_then(|i| i.as_str()) {
                            if !models.contains(&id.to_string()) { models.push(id.to_string()); }
                        }
                    }
                }
                if models.is_empty() {
                    return Ok(curated_models(self.provider));
                }
                Ok(models)
            }
            _ => Ok(curated_models(self.provider)),
        }
    }

    /// Returns display-annotated model strings for the TUI picker.
    /// Format: "model-id (annotation)" for known models, bare id otherwise.
    pub async fn list_models_for_display(&self) -> Vec<String> {
        let raw = self.list_remote_models().await.unwrap_or_default();
        raw.into_iter().map(|id| {
            if let Some(ann) = model_annotation(&id) {
                format!("{id} ({ann})")
            } else {
                id
            }
        }).collect()
    }

    pub async fn create_embeddings(&self, model: &str, input: &[String]) -> Result<Vec<Vec<f32>>, ApiError> {
        if self.provider.is_openai_compat() || self.provider == LlmProvider::Ternlang {
            let url = format!("{}/v1/embeddings", self.base_url.trim_end_matches('/'));
            let req = EmbeddingRequest {
                model: model.to_string(),
                input: input.to_vec(),
            };
            
            let res = self.auth.apply(self.provider, self.http.post(&url))
                .json(&req)
                .send()
                .await
                .map_err(ApiError::from)?;
            
            if !res.status().is_success() {
                let status = res.status();
                let body = res.text().await.unwrap_or_default();
                return Err(ApiError::ProviderError { status, body });
            }
            
            let data: EmbeddingResponse = res.json().await.map_err(ApiError::from)?;
            Ok(data.data.into_iter().map(|d| d.embedding).collect())
        } else {
            Err(ApiError::Config(format!("Embeddings not yet supported for provider {:?}", self.provider)))
        }
    }

    pub async fn exchange_oauth_code(
        &self,
        _config: OAuthConfig,
        _request: &OAuthTokenExchangeRequest,
    ) -> Result<RuntimeTokenSet, ApiError> {
        Ok(RuntimeTokenSet {
            access_token: "dummy_token".to_string(),
            refresh_token: None,
            expires_at: None,
            scopes: vec![],
        })
    }

    /// Check crates.io for the latest version of albert-cli.
    pub async fn check_for_updates(&self) -> Result<Option<String>, ApiError> {
        let url = "https://crates.io/api/v1/crates/albert-cli";
        // Crates.io requires a User-Agent header
        let res = self.http.get(url)
            .header("User-Agent", "albert-cli (https://github.com/eriirfos-eng/ternary-intelligence-stack)")
            .send()
            .await
            .map_err(ApiError::from)?;
        
        if !res.status().is_success() {
            return Ok(None);
        }

        let json: serde_json::Value = res.json().await.map_err(ApiError::from)?;
        let max_version = json.get("crate")
            .and_then(|c| c.get("max_version"))
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        Ok(max_version)
    }
}

#[derive(Debug)]
pub struct MessageStream {
    _request_id: Option<String>,
    response: Option<reqwest::Response>,
    parser: SseParser,
    pending: VecDeque<StreamEvent>,
    done: bool,
}

impl MessageStream {
    fn from_buffered_response(response: MessageResponse) -> Self {
        let mut pending = VecDeque::new();
        pending.push_back(StreamEvent::MessageStart(MessageStartEvent {
            message: response.clone(),
        }));
        for (i, block) in response.content.iter().enumerate() {
            let index = i as u32;
            pending.push_back(StreamEvent::ContentBlockStart(ContentBlockStartEvent {
                index,
                content_block: block.clone(),
            }));
            if let OutputContentBlock::Text { text } = block {
                pending.push_back(StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent {
                    index,
                    delta: ContentBlockDelta::TextDelta { text: text.clone() },
                }));
            }
            pending.push_back(StreamEvent::ContentBlockStop(ContentBlockStopEvent { index }));
        }
        pending.push_back(StreamEvent::MessageDelta(MessageDeltaEvent {
            delta: MessageDelta {
                stop_reason: response.stop_reason,
                stop_sequence: response.stop_sequence,
            },
            usage: response.usage,
        }));
        pending.push_back(StreamEvent::MessageStop(MessageStopEvent {}));
        Self {
            _request_id: None,
            response: None,
            parser: SseParser::new(),
            pending,
            done: true,
        }
    }

    pub async fn next_event(&mut self) -> Result<Option<StreamEvent>, ApiError> {
        loop {
            if let Some(event) = self.pending.pop_front() {
                return Ok(Some(event));
            }
            if self.done { return Ok(None); }
            match self.response.as_mut() {
                None => {
                    self.done = true;
                    return Ok(None);
                }
                Some(response) => match response.chunk().await? {
                    None => {
                        self.done = true;
                        return Ok(None);
                    }
                    Some(chunk) => {
                        self.pending.extend(self.parser.push(&chunk)?);
                    }
                },
            }
        }
    }
}

fn translate_to_anthropic(request: &MessageRequest) -> serde_json::Value {
    use serde_json::json;
    let messages: Vec<serde_json::Value> = request.messages.iter().map(|msg| {
        let content: Vec<serde_json::Value> = msg.content.iter().map(|block| {
            match block {
                InputContentBlock::Text { text } => json!({ "type": "text", "text": text }),
                InputContentBlock::ToolUse { id, name, input } => json!({
                    "type": "tool_use", "id": id, "name": name, "input": input
                }),
                InputContentBlock::ToolResult { tool_use_id, content, is_error } => {
                    let text = content.iter().filter_map(|c| {
                        if let ToolResultContentBlock::Text { text } = c { Some(text.clone()) } else { None }
                    }).collect::<Vec<String>>().join("\n");
                    json!({
                        "type": "tool_result", "tool_use_id": tool_use_id, "content": text, "is_error": is_error
                    })
                }
                InputContentBlock::Image { media_type, data } => json!({
                    "type": "image",
                    "source": { "type": "base64", "media_type": media_type, "data": data }
                }),
            }
        }).collect();
        json!({ "role": msg.role, "content": content })
    }).collect();

    let mut body = json!({
        "model": request.model,
        "messages": messages,
        "max_tokens": request.max_tokens.unwrap_or(4096),
        "stream": request.stream
    });
    if let Some(system) = &request.system { body["system"] = json!(system); }
    if let Some(tools) = &request.tools {
        body["tools"] = json!(tools.iter().map(|t| {
            json!({ "name": t.name, "description": t.description, "input_schema": t.input_schema })
        }).collect::<Vec<_>>());
    }
    body
}

fn translate_to_openai(request: &MessageRequest) -> serde_json::Value {
    use serde_json::json;
    let mut messages = vec![];
    if let Some(system) = &request.system { messages.push(json!({ "role": "system", "content": system })); }

    for msg in &request.messages {
        let mut content_blocks: Vec<serde_json::Value> = vec![];
        let mut tool_calls: Vec<serde_json::Value> = vec![];

        for block in &msg.content {
            match block {
                InputContentBlock::Text { text } => {
                    content_blocks.push(json!({ "type": "text", "text": text }));
                }
                InputContentBlock::ToolUse { id, name, input } => {
                    tool_calls.push(json!({
                        "id": id, "type": "function", "function": { "name": name, "arguments": input.to_string() }
                    }));
                }
                InputContentBlock::ToolResult { tool_use_id, content, .. } => {
                    let text = content.iter().filter_map(|c| {
                        if let ToolResultContentBlock::Text { text } = c { Some(text.clone()) } else { None }
                    }).collect::<Vec<String>>().join("\n");
                    messages.push(json!({ "role": "tool", "tool_call_id": tool_use_id, "content": text }));
                }
                InputContentBlock::Image { media_type, data } => {
                    content_blocks.push(json!({
                        "type": "image_url",
                        "image_url": { "url": format!("data:{media_type};base64,{data}") }
                    }));
                }
            }
        }

        if !content_blocks.is_empty() || !tool_calls.is_empty() {
            let mut m = json!({ "role": msg.role });
            if content_blocks.len() == 1 {
                if let Some(t) = content_blocks[0].get("text").and_then(|v| v.as_str()) {
                    m["content"] = json!(t);
                } else {
                    m["content"] = json!(content_blocks);
                }
            } else if !content_blocks.is_empty() {
                m["content"] = json!(content_blocks);
            }
            if !tool_calls.is_empty() { m["tool_calls"] = json!(tool_calls); }
            messages.push(m);
        }
    }

    let mut body = json!({ "model": request.model, "messages": messages, "stream": request.stream });
    if let Some(max) = request.max_tokens {
        body["max_tokens"] = json!(max);
    }
    if let Some(tools) = &request.tools {
        body["tools"] = json!(tools.iter().map(|t| {
            json!({ "type": "function", "function": { "name": t.name, "description": t.description, "parameters": t.input_schema } })
        }).collect::<Vec<_>>());
    }
    body
}

/// Gemini only supports a subset of JSON Schema — strip/normalize fields it rejects.
fn strip_gemini_unsupported_schema_fields(schema: serde_json::Value) -> serde_json::Value {
    match schema {
        serde_json::Value::Object(mut map) => {
            map.remove("additionalProperties");
            // "type": ["string", "null"] → "type": "string" (Gemini requires a single type string)
            if let Some(serde_json::Value::Array(types)) = map.get("type") {
                let first = types.iter()
                    .find(|t| t.as_str() != Some("null"))
                    .or_else(|| types.first())
                    .cloned()
                    .unwrap_or(serde_json::Value::String("string".to_string()));
                map.insert("type".to_string(), first);
            }
            let cleaned = map.into_iter()
                .map(|(k, v)| (k, strip_gemini_unsupported_schema_fields(v)))
                .collect();
            serde_json::Value::Object(cleaned)
        }
        serde_json::Value::Array(arr) => {
            serde_json::Value::Array(arr.into_iter().map(strip_gemini_unsupported_schema_fields).collect())
        }
        other => other,
    }
}

fn translate_to_gemini(request: &MessageRequest) -> serde_json::Value {
    use serde_json::json;
    let contents: Vec<serde_json::Value> = request.messages.iter().map(|msg| {
        let role = if msg.role == "assistant" { "model" } else { "user" };
        let parts: Vec<serde_json::Value> = msg.content.iter().map(|block| {
            match block {
                InputContentBlock::Text { text } => json!({ "text": text }),
                InputContentBlock::ToolUse { name, input, .. } => json!({ "functionCall": { "name": name, "args": input } }),
                InputContentBlock::ToolResult { tool_use_id, content, .. } => {
                    let text = content.iter().filter_map(|c| {
                        if let ToolResultContentBlock::Text { text } = c { Some(text.clone()) } else { None }
                    }).collect::<Vec<String>>().join("\n");
                    json!({ "functionResponse": { "name": tool_use_id, "response": { "result": text } } })
                }
                InputContentBlock::Image { media_type, data } => json!({
                    "inline_data": { "mime_type": media_type, "data": data }
                }),
            }
        }).collect();
        json!({ "role": role, "parts": parts })
    }).collect();

    let mut body = json!({ "contents": contents });
    if let Some(system) = &request.system { body["systemInstruction"] = json!({ "parts": [{ "text": system }] }); }
    if let Some(tools) = &request.tools {
        let declarations: Vec<serde_json::Value> = tools.iter().map(|t| {
            json!({ "name": t.name, "description": t.description, "parameters": strip_gemini_unsupported_schema_fields(t.input_schema.clone()) })
        }).collect();
        body["tools"] = json!([{ "functionDeclarations": declarations }]);
    }
    if let Some(max) = request.max_tokens {
        body["generationConfig"] = json!({ "maxOutputTokens": max });
    }
    body
}

fn translate_from_anthropic(response: serde_json::Value, model: &str) -> MessageResponse {
    let mut content = vec![];
    if let Some(blocks) = response.get("content").and_then(|c| c.as_array()) {
        for block in blocks {
            match block.get("type").and_then(|t| t.as_str()) {
                Some("text") => if let Some(text) = block.get("text").and_then(|t| t.as_str()) {
                    content.push(OutputContentBlock::Text { text: text.to_string() });
                },
                Some("tool_use") => if let (Some(id), Some(name), Some(input)) = (
                    block.get("id").and_then(|i| i.as_str()),
                    block.get("name").and_then(|n| n.as_str()),
                    block.get("input")
                ) {
                    content.push(OutputContentBlock::ToolUse { id: id.to_string(), name: name.to_string(), input: input.clone() });
                },
                _ => {}
            }
        }
    }
    let mut usage = Usage { input_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, output_tokens: 0 };
    if let Some(u) = response.get("usage") {
        usage.input_tokens = u.get("input_tokens").and_then(|c| c.as_u64()).unwrap_or(0) as u32;
        usage.output_tokens = u.get("output_tokens").and_then(|c| c.as_u64()).unwrap_or(0) as u32;
    }
    MessageResponse {
        id: response.get("id").and_then(|i| i.as_str()).unwrap_or("anthropic-response").to_string(),
        kind: "message".to_string(), role: "assistant".to_string(), content, model: model.to_string(),
        stop_reason: response.get("stop_reason").and_then(|s| s.as_str()).map(|s| s.to_string()),
        stop_sequence: None, usage, request_id: None,
    }
}

fn translate_from_openai(response: serde_json::Value, model: &str) -> MessageResponse {
    let mut content = vec![];
    if let Some(choices) = response.get("choices").and_then(|c| c.as_array()) {
        if let Some(choice) = choices.first() {
            if let Some(message) = choice.get("message") {
                if let Some(text) = message.get("content").and_then(|c| c.as_str()) {
                    content.push(OutputContentBlock::Text { text: text.to_string() });
                }
                if let Some(tool_calls) = message.get("tool_calls").and_then(|t| t.as_array()) {
                    for call in tool_calls {
                        if let (Some(id), Some(name), Some(args_str)) = (
                            call.get("id").and_then(|i| i.as_str()),
                            call.get("function").and_then(|f| f.get("name")).and_then(|n| n.as_str()),
                            call.get("function").and_then(|f| f.get("arguments")).and_then(|a| a.as_str())
                        ) {
                            if let Ok(args) = serde_json::from_str(args_str) {
                                content.push(OutputContentBlock::ToolUse { id: id.to_string(), name: name.to_string(), input: args });
                            }
                        }
                    }
                }
            }
        }
    }
    let mut usage = Usage { input_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, output_tokens: 0 };
    if let Some(u) = response.get("usage") {
        usage.input_tokens = u.get("prompt_tokens").and_then(|c| c.as_u64()).unwrap_or(0) as u32;
        usage.output_tokens = u.get("completion_tokens").and_then(|c| c.as_u64()).unwrap_or(0) as u32;
    }
    MessageResponse {
        id: response.get("id").and_then(|i| i.as_str()).unwrap_or("openai-response").to_string(),
        kind: "message".to_string(), role: "assistant".to_string(), content, model: model.to_string(),
        stop_reason: Some("end_turn".to_string()), stop_sequence: None, usage, request_id: None,
    }
}

fn translate_from_gemini(response: serde_json::Value, model: &str) -> MessageResponse {
    let mut content = vec![];
    if let Some(candidates) = response.get("candidates").and_then(|c| c.as_array()) {
        if let Some(candidate) = candidates.first() {
            if let Some(parts) = candidate.get("content").and_then(|c| c.get("parts")).and_then(|p| p.as_array()) {
                for part in parts {
                    if let Some(text) = part.get("text").and_then(|t| t.as_str()) {
                        content.push(OutputContentBlock::Text { text: text.to_string() });
                    }
                    if let Some(call) = part.get("functionCall") {
                        if let (Some(name), Some(args)) = (call.get("name").and_then(|n| n.as_str()), call.get("args")) {
                            content.push(OutputContentBlock::ToolUse { id: name.to_string(), name: name.to_string(), input: args.clone() });
                        }
                    }
                }
            }
        }
    }
    let mut usage = Usage { input_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, output_tokens: 0 };
    if let Some(u) = response.get("usageMetadata") {
        usage.input_tokens = u.get("promptTokenCount").and_then(|c| c.as_u64()).unwrap_or(0) as u32;
        usage.output_tokens = u.get("candidatesTokenCount").and_then(|c| c.as_u64()).unwrap_or(0) as u32;
    }
    MessageResponse {
        id: "gemini-response".to_string(), kind: "message".to_string(), role: "assistant".to_string(),
        content, model: model.to_string(), stop_reason: Some("end_turn".to_string()),
        stop_sequence: None, usage, request_id: None,
    }
}

/// Curated fallback model list for providers that don't expose a /v1/models endpoint.
pub fn curated_models(provider: LlmProvider) -> Vec<String> {
    let list: &[&str] = match provider {
        LlmProvider::Ternlang => &["albert-moe-13"],
        LlmProvider::Anthropic => &[
            "claude-opus-4-7",
            "claude-sonnet-4-6",
            "claude-haiku-4-5-20251001",
            "claude-3-7-sonnet-latest",
            "claude-3-5-sonnet-latest",
            "claude-3-5-haiku-latest",
            "claude-3-opus-latest",
        ],
        LlmProvider::Google => &[
            "gemini-2.5-pro",
            "gemini-2.5-flash",
            "gemini-2.5-flash-8b",
            "gemini-2.0-flash",
            "gemini-2.0-flash-lite",
            "gemini-1.5-flash",
            "gemini-1.5-pro",
        ],
        LlmProvider::OpenAi => &[
            "gpt-4o",
            "gpt-4o-mini",
            "gpt-4-turbo",
            "o3",
            "o3-mini",
            "o1",
            "o1-mini",
            "o4-mini",
        ],
        LlmProvider::Xai => &["grok-3", "grok-3-mini", "grok-2"],
        LlmProvider::DeepSeek => &["deepseek-chat", "deepseek-reasoner"],
        LlmProvider::Mistral => &[
            "mistral-large-latest",
            "mistral-small-latest",
            "codestral-latest",
            "pixtral-large-latest",
        ],
        LlmProvider::OpenRouter => &[
            "openai/gpt-4o",
            "anthropic/claude-3.5-sonnet",
            "google/gemini-2.5-flash",
            "meta-llama/llama-3.3-70b-instruct",
            "deepseek/deepseek-r1",
            "qwen/qwen-2.5-72b-instruct",
        ],
        LlmProvider::Groq => &[
            "llama-3.3-70b-versatile",
            "llama-3.1-8b-instant",
            "llama-3.1-70b-versatile",
            "gemma2-9b-it",
            "mixtral-8x7b-32768",
        ],
        LlmProvider::Cohere => &["command-r-plus", "command-r", "command-r-08-2024"],
        LlmProvider::Perplexity => &["sonar-pro", "sonar", "sonar-reasoning"],
        LlmProvider::Together => &[
            "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
            "meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo",
            "Qwen/Qwen2.5-72B-Instruct-Turbo",
        ],
        LlmProvider::Fireworks => &[
            "accounts/fireworks/models/llama-v3p1-70b-instruct",
            "accounts/fireworks/models/deepseek-r1",
            "accounts/fireworks/models/qwen2p5-72b-instruct",
        ],
        LlmProvider::Cerebras => &["llama3.3-70b", "llama3.1-8b"],
        LlmProvider::SambaNova => &[
            "Meta-Llama-3.3-70B-Instruct",
            "DeepSeek-R1-Distill-Llama-70B",
        ],
        LlmProvider::NvidiaNim => &[
            "nvidia/llama-3.1-nemotron-70b-instruct",
            "meta/llama-3.1-70b-instruct",
        ],
        LlmProvider::Zhipu => &[
            "zai/glm-5",
            "zai/glm-4.7",
            "zai/glm-4.7-flash",
            "zai/glm-4.5",
            "zai/glm-4.5-flash",
            "glm-4-plus",
            "glm-4-air",
        ],
        LlmProvider::MiniMax => &["MiniMax-Text-01", "abab6.5s-chat", "abab6.5g-chat"],
        LlmProvider::Qwen => &[
            "qwen-max",
            "qwen-plus",
            "qwen-turbo",
            "qwen2.5-72b-instruct",
            "qwq-32b",
            "qwen2.5-coder-32b-instruct",
        ],
        LlmProvider::Moonshot => &["moonshot-v1-128k", "moonshot-v1-32k", "moonshot-v1-8k", "kimi-k2"],
        LlmProvider::Qianfan => &["ernie-4.5-turbo-128k", "ernie-4.0-turbo-8k", "ernie-3.5-8k"],
        LlmProvider::Chutes => &[
            "deepseek-ai/DeepSeek-R1",
            "deepseek-ai/DeepSeek-V3",
            "Qwen/Qwen2.5-72B-Instruct",
        ],
        LlmProvider::HuggingFace => &[
            "meta-llama/Meta-Llama-3-8B-Instruct",
            "mistralai/Mistral-7B-Instruct-v0.3",
        ],
        LlmProvider::GitHub => &["gpt-4o", "gpt-4o-mini", "o3-mini"],
        _ => &[],
    };
    list.iter().map(|s| (*s).to_string()).collect()
}

/// Human-readable annotation for known model IDs.
pub fn model_annotation(id: &str) -> Option<&'static str> {
    match id {
        // Anthropic
        "claude-opus-4-7" | "claude-opus-4-5" => Some("Opus 4 · ctx 200k · top capability"),
        "claude-sonnet-4-6" | "claude-3-7-sonnet-latest" | "claude-3-5-sonnet-latest" => Some("Sonnet · ctx 200k · balanced"),
        "claude-haiku-4-5-20251001" | "claude-3-5-haiku-latest" => Some("Haiku · ctx 200k · fast & cheap"),
        "claude-3-opus-latest" => Some("Opus 3 · ctx 200k · legacy"),
        // Google
        "gemini-2.5-pro" => Some("ctx 1M · best reasoning · multimodal"),
        "gemini-2.5-flash" => Some("ctx 1M · fast · recommended"),
        "gemini-2.0-flash" => Some("ctx 1M · ultra-fast"),
        "gemini-1.5-pro" => Some("ctx 2M · legacy pro"),
        // OpenAI
        "gpt-4o" => Some("ctx 128k · multimodal · flagship"),
        "gpt-4o-mini" => Some("ctx 128k · cheap · fast"),
        "o3" => Some("ctx 200k · reasoning · top"),
        "o3-mini" | "o4-mini" => Some("ctx 200k · reasoning · fast"),
        "o1" => Some("ctx 200k · reasoning · legacy"),
        // xAI
        "grok-3" => Some("ctx 131k · flagship"),
        "grok-3-mini" => Some("ctx 131k · fast"),
        // DeepSeek
        "deepseek-chat" => Some("ctx 64k · DeepSeek-V3 · cost-efficient"),
        "deepseek-reasoner" => Some("ctx 64k · R1 · chain-of-thought"),
        // Mistral
        "mistral-large-latest" => Some("ctx 128k · flagship"),
        "codestral-latest" => Some("ctx 256k · code specialist"),
        // Zhipu / Z.AI
        "zai/glm-5" => Some("ctx 200k · reasoning · alias: GLM"),
        "zai/glm-4.7" => Some("ctx 128k · balanced"),
        "zai/glm-4.7-flash" => Some("ctx 128k · fast"),
        "zai/glm-4.5" => Some("ctx 128k · cost-efficient"),
        "zai/glm-4.5-flash" => Some("ctx 32k · ultra-fast"),
        // Qwen
        "qwen-max" => Some("ctx 32k · flagship"),
        "qwq-32b" => Some("ctx 32k · reasoning"),
        "qwen2.5-coder-32b-instruct" => Some("ctx 128k · code specialist"),
        // Moonshot / Kimi
        "kimi-k2" => Some("ctx 128k · Kimi K2.5"),
        "moonshot-v1-128k" => Some("ctx 128k · long context"),
        // Groq
        "llama-3.3-70b-versatile" => Some("ctx 128k · ultra-fast on Groq"),
        // Misc
        _ => None,
    }
}

pub fn read_env_non_empty(key: &str) -> Result<Option<String>, ApiError> {
    match std::env::var(key) {
        Ok(value) if !value.is_empty() => Ok(Some(value)),
        Ok(_) | Err(std::env::VarError::NotPresent) => Ok(None),
        Err(error) => Err(ApiError::from(error)),
    }
}

pub fn read_base_url() -> String {
    std::env::var("TERNLANG_BASE_URL").unwrap_or_else(|_| DEFAULT_BASE_URL.to_string())
}

fn request_id_from_headers(headers: &reqwest::header::HeaderMap) -> Option<String> {
    headers
        .get(REQUEST_ID_HEADER)
        .or_else(|| headers.get(ALT_REQUEST_ID_HEADER))
        .and_then(|value| value.to_str().ok())
        .map(ToOwned::to_owned)
}

async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response, ApiError> {
    if response.status().is_success() {
        return Ok(response);
    }
    let status = response.status();
    let body = response.text().await.unwrap_or_default();
    Err(ApiError::Auth(format!("HTTP {status}: {body}")))
}

pub fn resolve_startup_auth_source() -> Result<AuthSource, ApiError> {
    if let Some(api_key) = read_env_non_empty("TERNLANG_API_KEY")? {
        return Ok(AuthSource::ApiKey(api_key));
    }
    Ok(AuthSource::None)
}

/// Read the standard env var for `provider` and return the appropriate auth.
pub fn resolve_auth_for_provider(provider: LlmProvider) -> Result<AuthSource, ApiError> {
    // No-auth local providers
    if matches!(provider, LlmProvider::Ollama | LlmProvider::LmStudio | LlmProvider::OpenAiCompat) {
        return Ok(AuthSource::None);
    }
    let env_var = provider.env_var();
    let key = if provider == LlmProvider::Google {
        // Google accepts either GEMINI_API_KEY or GOOGLE_API_KEY
        read_env_non_empty("GEMINI_API_KEY").ok().flatten()
            .or_else(|| read_env_non_empty("GOOGLE_API_KEY").ok().flatten())
    } else if env_var.is_empty() {
        None
    } else {
        read_env_non_empty(env_var)?
    };
    Ok(key.map_or(AuthSource::None, AuthSource::ApiKey))
}

/// Scan well-known env vars and return the first available (provider, default-model) pair.
/// Returns None if no recognised key is set (Ollama/LM Studio local are not detected here).
pub fn detect_provider_and_model_from_env() -> Option<(LlmProvider, &'static str)> {
    let env_set = |var: &str| std::env::var(var).ok().filter(|v| !v.is_empty()).is_some();
    if env_set("ANTHROPIC_API_KEY") {
        return Some((LlmProvider::Anthropic, "claude-sonnet-4-6"));
    }
    if env_set("GEMINI_API_KEY") || env_set("GOOGLE_API_KEY") {
        return Some((LlmProvider::Google, "gemini-2.5-flash"));
    }
    if env_set("OPENAI_API_KEY") {
        return Some((LlmProvider::OpenAi, "gpt-4o-mini"));
    }
    if env_set("XAI_API_KEY") {
        return Some((LlmProvider::Xai, "grok-3-mini"));
    }
    if env_set("GROQ_API_KEY") {
        return Some((LlmProvider::Groq, "llama-3.3-70b-versatile"));
    }
    if env_set("MISTRAL_API_KEY") {
        return Some((LlmProvider::Mistral, "mistral-large-latest"));
    }
    if env_set("DEEPSEEK_API_KEY") {
        return Some((LlmProvider::DeepSeek, "deepseek-chat"));
    }
    if env_set("TOGETHER_API_KEY") {
        return Some((LlmProvider::Together, "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo"));
    }
    if env_set("OPENROUTER_API_KEY") {
        return Some((LlmProvider::OpenRouter, "openai/gpt-4o-mini"));
    }
    if env_set("PERPLEXITY_API_KEY") {
        return Some((LlmProvider::Perplexity, "sonar-pro"));
    }
    if env_set("FIREWORKS_API_KEY") {
        return Some((LlmProvider::Fireworks, "accounts/fireworks/models/llama-v3p1-70b-instruct"));
    }
    if env_set("COHERE_API_KEY") {
        return Some((LlmProvider::Cohere, "command-r-plus"));
    }
    if env_set("CEREBRAS_API_KEY") {
        return Some((LlmProvider::Cerebras, "llama3.3-70b"));
    }
    if env_set("NOVITA_API_KEY") {
        return Some((LlmProvider::Novita, "meta-llama/llama-3.1-70b-instruct"));
    }
    if env_set("SAMBANOVA_API_KEY") {
        return Some((LlmProvider::SambaNova, "Meta-Llama-3.3-70B-Instruct"));
    }
    if env_set("NVIDIA_API_KEY") {
        return Some((LlmProvider::NvidiaNim, "nvidia/llama-3.1-nemotron-70b-instruct"));
    }
    if env_set("HUGGINGFACE_API_KEY") {
        return Some((LlmProvider::HuggingFace, "meta-llama/Meta-Llama-3-8B-Instruct"));
    }
    if env_set("GITHUB_TOKEN") {
        return Some((LlmProvider::GitHub, "gpt-4o-mini"));
    }
    None
}

#[derive(serde::Deserialize)]
pub struct OAuthConfig {}
