// SPDX-License-Identifier: MIT
// Copyright (c) 2025 Jason Ish
//
// Provider management for multiple AI backends.

use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;

use crate::compaction;
use crate::config::{Config, ConfigFile, ProviderType};
use crate::error::Result;
use crate::output::OutputContext;
use crate::provider::anthropic::AnthropicProvider;
use crate::provider::antigravity::AntigravityProvider;
use crate::provider::copilot::CopilotProvider;
use crate::provider::openai::OpenAiProvider;
use crate::provider::openai_compat::OpenAiCompatProvider;
use crate::provider::openrouter::OpenRouterProvider;
use crate::provider::zen::ZenProvider;
use crate::provider::{ContentBlock, Message, MessageContent, Role};
use crate::services::Services;

/// Callback type for saving session after tool iterations.
/// Called with a reference to the current messages after each tool loop iteration.
pub(crate) type SessionSaveCallback = Box<dyn Fn(&[Message]) + Send>;

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum ModelProvider {
    Antigravity,
    OpenCodeZen,
    GitHubCopilot,
    Claude,
    OpenAi,
    OpenAiCompat,
    OpenRouter,
}

impl ModelProvider {
    pub(crate) fn display_name(&self) -> &'static str {
        match self {
            ModelProvider::Antigravity => "Antigravity",
            ModelProvider::OpenCodeZen => "OpenCode Zen",
            ModelProvider::GitHubCopilot => "GitHub Copilot",
            ModelProvider::Claude => "Anthropic Claude",
            ModelProvider::OpenAi => "OpenAI",
            ModelProvider::OpenAiCompat => "OpenAI Compatible",
            ModelProvider::OpenRouter => "OpenRouter",
        }
    }

    pub(crate) fn id(&self) -> &'static str {
        match self {
            ModelProvider::Antigravity => "antigravity",
            ModelProvider::OpenCodeZen => "zen",
            ModelProvider::GitHubCopilot => "copilot",
            ModelProvider::Claude => "claude",
            ModelProvider::OpenAi => "openai",
            ModelProvider::OpenAiCompat => "openai_compat",
            ModelProvider::OpenRouter => "openrouter",
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ThinkingState {
    pub enabled: bool,
    pub mode: Option<String>,
}

impl ThinkingState {
    pub(crate) fn new(enabled: bool, mode: Option<String>) -> Self {
        Self { enabled, mode }
    }

    pub(crate) fn label(&self) -> &str {
        if !self.enabled {
            "off"
        } else if let Some(mode) = self.mode.as_deref() {
            mode
        } else {
            "on"
        }
    }
}

fn thinking_state_from_label(label: &str) -> ThinkingState {
    match label {
        "off" => ThinkingState::new(false, None),
        "on" => ThinkingState::new(true, None),
        _ => ThinkingState::new(true, Some(label.to_string())),
    }
}

fn next_thinking_label<'a>(current: &'a str, modes: &'a [&'a str]) -> &'a str {
    if modes.is_empty() {
        return current;
    }

    let fallback_idx = modes.len().saturating_sub(1);
    let current_idx = modes
        .iter()
        .position(|m| *m == current)
        .unwrap_or(fallback_idx);
    let next_idx = (current_idx + 1) % modes.len();
    modes[next_idx]
}

pub(crate) fn default_thinking_state(provider: ModelProvider, model_id: &str) -> ThinkingState {
    match provider {
        ModelProvider::Antigravity => AntigravityProvider::default_thinking_state(model_id),
        ModelProvider::OpenCodeZen => ZenProvider::default_thinking_state(model_id),
        ModelProvider::Claude => AnthropicProvider::default_thinking_state(model_id),
        ModelProvider::GitHubCopilot => {
            if let Some(variant) = get_model_variant(model_id) {
                ThinkingState::new(variant != "off", Some(variant.to_string()))
            } else {
                ThinkingState::new(true, None)
            }
        }
        ModelProvider::OpenAi => {
            // For OpenAI models with variants, use the variant as the mode
            if let Some(variant) = get_model_variant(model_id) {
                ThinkingState::new(true, Some(variant.to_string()))
            } else {
                ThinkingState::new(true, None)
            }
        }
        _ => ThinkingState::new(true, None),
    }
}

pub(crate) fn cycle_thinking_state(
    provider: ModelProvider,
    model_id: &str,
    current: &ThinkingState,
) -> ThinkingState {
    let modes: &[&str] = match provider {
        ModelProvider::OpenCodeZen => ZenProvider::thinking_modes(model_id),
        ModelProvider::Claude => AnthropicProvider::thinking_modes(),
        ModelProvider::GitHubCopilot
        | ModelProvider::OpenAi
        | ModelProvider::OpenAiCompat
        | ModelProvider::OpenRouter
        | ModelProvider::Antigravity => &["off", "on"],
    };

    if modes.is_empty() {
        return current.clone();
    }

    let current_label = current.label();
    let next_label = next_thinking_label(current_label, modes);
    thinking_state_from_label(next_label)
}

/// Check if a provider uses model variants (like `model#variant`) for reasoning levels.
/// For these providers, Ctrl+T should cycle through model variants instead of thinking modes.
pub(crate) fn uses_model_variants(provider: ModelProvider, model_id: &str) -> bool {
    match provider {
        ModelProvider::GitHubCopilot => CopilotProvider::model_variants(model_id).len() > 1,
        ModelProvider::Claude => AnthropicProvider::model_variants(model_id).len() > 1,
        ModelProvider::OpenAi => {
            // OpenAI models with variants use them for reasoning effort
            OpenAiProvider::model_variants(model_id).len() > 1
        }
        ModelProvider::Antigravity => {
            // Antigravity models use #variant for thinking levels
            AntigravityProvider::model_variants(model_id).len() > 1
        }
        _ => false,
    }
}

/// Cycle to the next model variant for providers that use model variants for reasoning.
/// Returns the new model ID, or the same model ID if no variants are available.
pub(crate) fn cycle_model_variant(provider: ModelProvider, model_id: &str) -> String {
    match provider {
        ModelProvider::GitHubCopilot => CopilotProvider::cycle_model_variant(model_id),
        ModelProvider::Claude => AnthropicProvider::cycle_model_variant(model_id),
        ModelProvider::OpenAi => OpenAiProvider::cycle_model_variant(model_id),
        ModelProvider::Antigravity => AntigravityProvider::cycle_model_variant(model_id),
        _ => model_id.to_string(),
    }
}

/// Get the current variant/reasoning level from a model ID.
/// Returns the variant suffix (e.g., "high", "medium") or None if no variant.
pub(crate) fn get_model_variant(model_id: &str) -> Option<&str> {
    model_id.split_once('#').map(|(_, variant)| variant)
}

/// A model choice representing a provider/model combination
#[derive(Clone, Debug)]
pub(crate) struct ModelChoice {
    pub provider: ModelProvider,
    pub model_id: String,
    pub custom_provider: Option<String>,
    /// Whether this model is marked as a favorite
    pub is_favorite: bool,
}

impl ModelChoice {
    pub(crate) fn display(&self) -> String {
        if let Some(custom) = &self.custom_provider {
            format!("{}: {}", custom, self.model_id)
        } else {
            format!("{}: {}", self.provider.display_name(), self.model_id)
        }
    }

    /// Get the display suffix (provider type name in parentheses) for custom providers
    pub(crate) fn display_suffix(&self) -> Option<String> {
        if self.custom_provider.is_some() {
            Some(format!(" ({})", self.provider.display_name()))
        } else {
            None
        }
    }

    pub(crate) fn short_display(&self) -> String {
        match self.provider {
            ModelProvider::OpenAiCompat
            | ModelProvider::OpenCodeZen
            | ModelProvider::OpenRouter => {
                if let Some(custom) = &self.custom_provider {
                    format!("{}/{}", custom, self.model_id)
                } else {
                    format!("{}/{}", self.provider.id(), self.model_id)
                }
            }
            _ => format!("{}/{}", self.provider.id(), self.model_id),
        }
    }
}

impl std::fmt::Display for ModelChoice {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let star = if self.is_favorite { "*" } else { " " };
        write!(f, "{}{}", star, self.display())
    }
}

/// Build a list of all available model choices from configured providers
pub(crate) fn build_model_choices() -> Vec<ModelChoice> {
    let mut choices = Vec::new();

    let config = match ConfigFile::load().ok() {
        Some(c) => c,
        None => return choices,
    };

    // Helper to check if a model is a favorite
    let is_favorite =
        |choice: &ModelChoice| -> bool { config.is_favorite(&choice.short_display()) };

    // Count how many providers of each type are enabled
    let mut type_counts: HashMap<ProviderType, usize> = HashMap::new();
    for provider_config in config.providers.entries.values() {
        if provider_config.is_enabled() {
            let is_usable = match provider_config.provider_type() {
                ProviderType::Zen => provider_config
                    .as_zen()
                    .is_some_and(|c| !c.api_key.is_empty()),
                _ => true,
            };
            if is_usable {
                *type_counts
                    .entry(provider_config.provider_type())
                    .or_insert(0) += 1;
            }
        }
    }

    for (local_id, provider_config) in &config.providers.entries {
        if !provider_config.is_enabled() {
            continue;
        }

        let provider_type = provider_config.provider_type();
        let use_custom_name = type_counts.get(&provider_type).copied().unwrap_or(0) > 1;

        match provider_type {
            ProviderType::Antigravity => {
                for &model in AntigravityProvider::models() {
                    let mut choice = ModelChoice {
                        provider: ModelProvider::Antigravity,
                        model_id: model.to_string(),
                        custom_provider: Some(local_id.clone()),
                        is_favorite: false,
                    };
                    choice.is_favorite = is_favorite(&choice);
                    choices.push(choice);
                }
            }
            ProviderType::GithubCopilot => {
                for &model in CopilotProvider::models() {
                    let mut choice = ModelChoice {
                        provider: ModelProvider::GitHubCopilot,
                        model_id: model.to_string(),
                        custom_provider: if use_custom_name {
                            Some(local_id.clone())
                        } else {
                            None
                        },
                        is_favorite: false,
                    };
                    choice.is_favorite = is_favorite(&choice);
                    choices.push(choice);
                }
            }
            ProviderType::Zen => {
                if let Some(zen_config) = provider_config.as_zen()
                    && !zen_config.api_key.is_empty()
                {
                    for &model in ZenProvider::models() {
                        let mut choice = ModelChoice {
                            provider: ModelProvider::OpenCodeZen,
                            model_id: model.to_string(),
                            custom_provider: Some(local_id.clone()),
                            is_favorite: false,
                        };
                        choice.is_favorite = is_favorite(&choice);
                        choices.push(choice);
                    }
                }
            }
            ProviderType::Claude => {
                for &model in AnthropicProvider::models() {
                    let mut choice = ModelChoice {
                        provider: ModelProvider::Claude,
                        model_id: model.to_string(),
                        custom_provider: if use_custom_name {
                            Some(local_id.clone())
                        } else {
                            None
                        },
                        is_favorite: false,
                    };
                    choice.is_favorite = is_favorite(&choice);
                    choices.push(choice);
                }
            }
            ProviderType::Openai => {
                for &model in OpenAiProvider::models() {
                    let mut choice = ModelChoice {
                        provider: ModelProvider::OpenAi,
                        model_id: model.to_string(),
                        custom_provider: if use_custom_name {
                            Some(local_id.clone())
                        } else {
                            None
                        },
                        is_favorite: false,
                    };
                    choice.is_favorite = is_favorite(&choice);
                    choices.push(choice);
                }
            }
            ProviderType::OpenaiCompat => {
                if let Some(compat_config) = provider_config.as_openai_compat() {
                    let models = compat_config.all_models();
                    if models.is_empty() {
                        let mut choice = ModelChoice {
                            provider: ModelProvider::OpenAiCompat,
                            model_id: "default".to_string(),
                            custom_provider: Some(local_id.clone()),
                            is_favorite: false,
                        };
                        choice.is_favorite = is_favorite(&choice);
                        choices.push(choice);
                    } else {
                        for model in models {
                            let mut choice = ModelChoice {
                                provider: ModelProvider::OpenAiCompat,
                                model_id: model,
                                custom_provider: Some(local_id.clone()),
                                is_favorite: false,
                            };
                            choice.is_favorite = is_favorite(&choice);
                            choices.push(choice);
                        }
                    }
                }
            }
            ProviderType::OpenRouter => {
                if let Some(openrouter_config) = provider_config.as_openrouter() {
                    let models = openrouter_config.all_models();
                    if models.is_empty() {
                        let mut choice = ModelChoice {
                            provider: ModelProvider::OpenRouter,
                            model_id: "default".to_string(),
                            custom_provider: if use_custom_name {
                                Some(local_id.clone())
                            } else {
                                None
                            },
                            is_favorite: false,
                        };
                        choice.is_favorite = is_favorite(&choice);
                        choices.push(choice);
                    } else {
                        for model in models {
                            let mut choice = ModelChoice {
                                provider: ModelProvider::OpenRouter,
                                model_id: model,
                                custom_provider: if use_custom_name {
                                    Some(local_id.clone())
                                } else {
                                    None
                                },
                                is_favorite: false,
                            };
                            choice.is_favorite = is_favorite(&choice);
                            choices.push(choice);
                        }
                    }
                }
            }
        }
    }

    // Sort choices for predictable ordering: by provider name, then model ID
    choices.sort_by(|a, b| {
        let provider_a = a.custom_provider.as_deref().unwrap_or(a.provider.id());
        let provider_b = b.custom_provider.as_deref().unwrap_or(b.provider.id());
        provider_a
            .cmp(provider_b)
            .then_with(|| a.model_id.cmp(&b.model_id))
    });

    choices
}

/// Get only the favorite model choices, sorted by display name
pub(crate) fn get_favorite_models() -> Vec<ModelChoice> {
    build_model_choices()
        .into_iter()
        .filter(|c| c.is_favorite)
        .collect()
}

/// Cycle to the next favorite model.
/// Returns the next favorite model, or None if there are no favorites.
/// If the current model is not a favorite, returns the first favorite.
/// `reverse` cycles backwards through the list.
pub(crate) fn cycle_favorite_model(current_model: &str, reverse: bool) -> Option<ModelChoice> {
    let favorites = get_favorite_models();
    if favorites.is_empty() {
        return None;
    }

    // Find current model's position in favorites
    let current_idx = favorites
        .iter()
        .position(|c| c.short_display() == current_model);

    let next_idx = match current_idx {
        Some(idx) => {
            if reverse {
                if idx == 0 {
                    favorites.len() - 1
                } else {
                    idx - 1
                }
            } else {
                (idx + 1) % favorites.len()
            }
        }
        // Current model not in favorites, start from first (or last if reverse)
        None => {
            if reverse {
                favorites.len() - 1
            } else {
                0
            }
        }
    };

    favorites.into_iter().nth(next_idx)
}

/// Manages all provider instances and handles routing chat requests
pub(crate) struct ProviderManager {
    zen_provider: ZenProvider,
    antigravity_providers: HashMap<String, AntigravityProvider>,
    copilot_provider: Option<CopilotProvider>,
    anthropic_provider: Option<AnthropicProvider>,
    openai_provider: Option<OpenAiProvider>,
    openai_compat_providers: HashMap<String, OpenAiCompatProvider>,
    openrouter_provider: Option<OpenRouterProvider>,
    current_provider: ModelProvider,
    current_model_id: String,
    current_custom_provider: Option<String>,
    services: Services,
    /// Optional callback to save session after each tool iteration
    session_save_callback: Option<SessionSaveCallback>,
}

impl ProviderManager {
    pub(crate) fn new(config: &Config, services: Services) -> Self {
        let zen_provider = ZenProvider::new(config, services.clone());
        let copilot_provider = CopilotProvider::try_new(services.clone()).ok();
        let anthropic_provider = AnthropicProvider::try_new(services.clone()).ok();
        let openai_provider = OpenAiProvider::try_new(services.clone()).ok();
        let openrouter_provider = OpenRouterProvider::try_new("openrouter", services.clone()).ok();

        // Load all configured OpenAI-compatible providers
        let mut openai_compat_providers = HashMap::new();
        let mut antigravity_providers = HashMap::new();
        if let Ok(cfg) = crate::config::ConfigFile::load() {
            for (name, provider_config) in &cfg.providers.entries {
                if provider_config.as_openai_compat().is_some()
                    && let Ok(provider) = OpenAiCompatProvider::try_new(name, services.clone())
                {
                    openai_compat_providers.insert(name.clone(), provider);
                }
                if provider_config.as_antigravity().is_some()
                    && let Ok(provider) = AntigravityProvider::try_new(name, services.clone())
                {
                    antigravity_providers.insert(name.clone(), provider);
                }
            }
        }

        let (current_provider, current_model_id, current_custom_provider) =
            parse_model_spec(&config.model);

        crate::config::persist_last_used_model(&config.model);

        Self {
            zen_provider,
            antigravity_providers,
            copilot_provider,
            anthropic_provider,
            openai_provider,
            openai_compat_providers,
            openrouter_provider,
            current_provider,
            current_model_id,
            current_custom_provider,
            services,
            session_save_callback: None,
        }
    }

    /// Set a callback to be called after each tool iteration to save the session.
    pub(crate) fn set_session_save_callback(&mut self, callback: SessionSaveCallback) {
        self.session_save_callback = Some(callback);
    }

    /// Set the current model.
    ///
    /// Returns `true` if the provider changed (not just the model within the same provider).
    /// When the provider changes, callers should transform thinking blocks using
    /// `crate::provider::transform_thinking_for_provider_switch()` because thinking signatures
    /// are not valid across providers.
    pub(crate) fn set_model(
        &mut self,
        provider: ModelProvider,
        model_id: String,
        custom_provider: Option<String>,
    ) -> bool {
        let provider_changed =
            self.current_provider != provider || self.current_custom_provider != custom_provider;

        self.current_provider = provider;
        self.current_model_id = model_id.clone();
        self.current_custom_provider = custom_provider.clone();

        match provider {
            ModelProvider::Antigravity => {
                if let Some(custom_name) = &custom_provider {
                    if !self.antigravity_providers.contains_key(custom_name) {
                        if let Ok(provider) =
                            AntigravityProvider::try_new(custom_name, self.services.clone())
                        {
                            self.antigravity_providers
                                .insert(custom_name.clone(), provider);
                        } else {
                            eprintln!("Antigravity provider '{}' not configured.", custom_name);
                            return provider_changed;
                        }
                    }
                    if let Some(p) = self.antigravity_providers.get_mut(custom_name) {
                        p.set_model(model_id);
                    }
                } else {
                    eprintln!("Antigravity provider requires a custom provider name.");
                }
            }
            ModelProvider::OpenCodeZen => self.zen_provider.set_model(model_id),
            ModelProvider::GitHubCopilot => {
                if let Some(ref mut p) = self.copilot_provider {
                    p.set_model(model_id);
                }
            }
            ModelProvider::Claude => {
                if let Some(ref mut p) = self.anthropic_provider {
                    p.set_model(model_id);
                }
            }
            ModelProvider::OpenAi => {
                if let Some(ref mut p) = self.openai_provider {
                    p.set_model(model_id);
                }
            }
            ModelProvider::OpenRouter => {
                if let Some(ref mut p) = self.openrouter_provider {
                    p.set_model(model_id);
                }
            }
            ModelProvider::OpenAiCompat => {
                if let Some(custom_name) = &custom_provider {
                    // Try to get or initialize the specific custom provider
                    if !self.openai_compat_providers.contains_key(custom_name) {
                        if let Ok(provider) =
                            OpenAiCompatProvider::try_new(custom_name, self.services.clone())
                        {
                            self.openai_compat_providers
                                .insert(custom_name.clone(), provider);
                        } else {
                            eprintln!(
                                "OpenAI Compatible provider '{}' not configured.",
                                custom_name
                            );
                            return provider_changed;
                        }
                    }
                    if let Some(p) = self.openai_compat_providers.get_mut(custom_name) {
                        p.set_model(model_id);
                    }
                } else {
                    eprintln!("OpenAI Compatible provider requires a custom provider name.");
                }
            }
        }
        provider_changed
    }

    pub(crate) fn default_thinking(&self) -> ThinkingState {
        default_thinking_state(self.current_provider, &self.current_model_id)
    }

    pub(crate) fn set_thinking_enabled(&mut self, enabled: bool) {
        match self.current_provider {
            ModelProvider::OpenCodeZen => {
                // Zen provider doesn't use thinking_enabled
            }
            ModelProvider::GitHubCopilot => {
                if let Some(ref mut p) = self.copilot_provider {
                    p.set_thinking_enabled(enabled);
                }
            }
            ModelProvider::OpenAi => {
                if let Some(ref mut p) = self.openai_provider {
                    p.set_thinking_enabled(enabled);
                }
            }
            ModelProvider::Antigravity
            | ModelProvider::OpenRouter
            | ModelProvider::OpenAiCompat
            | ModelProvider::Claude => {
                // These providers use model variants or set_thinking_mode instead
            }
        }
    }

    /// Set the thinking mode for providers that support it
    /// mode should be one of: "off", "minimal", "low", "medium", "high", or None for default
    pub(crate) fn set_thinking_mode(&mut self, mode: Option<String>) {
        match self.current_provider {
            ModelProvider::Antigravity | ModelProvider::OpenAi => {
                if let Some(mode) = mode {
                    let base = self
                        .current_model_id
                        .split_once('#')
                        .map(|(b, _)| b)
                        .unwrap_or(&self.current_model_id);
                    self.current_model_id = format!("{}#{}", base, mode);
                }
            }
            _ => {}
        }
    }

    /// Send a chat request, emitting events via output::emit()
    ///
    /// This method handles the full chat loop including:
    /// - Auto-compaction checks before each provider call
    /// - Tool execution
    /// - Looping until the model finishes
    pub async fn chat(
        &mut self,
        messages: &mut Vec<Message>,
        interrupted: &Arc<AtomicBool>,
        output: &crate::output::OutputContext,
    ) -> Result<()> {
        use crate::chat::{ChatIterationResult, run_chat_iteration};
        use crate::provider::Provider;

        let services = self.services.clone();
        let config = crate::config::ConfigFile::load().unwrap_or_default();

        // Start turn for usage tracking (call once at the start)
        match self.current_provider {
            ModelProvider::Antigravity => {
                if let Some(name) = &self.current_custom_provider
                    && let Some(p) = self.antigravity_providers.get(name)
                {
                    p.start_turn();
                }
            }
            ModelProvider::OpenCodeZen => self.zen_provider.start_turn(),
            ModelProvider::GitHubCopilot => {
                if let Some(p) = &self.copilot_provider {
                    p.start_turn();
                }
            }
            ModelProvider::Claude => {
                if let Some(p) = &self.anthropic_provider {
                    p.start_turn();
                }
            }
            ModelProvider::OpenAi => {
                if let Some(p) = &self.openai_provider {
                    p.start_turn();
                }
            }
            ModelProvider::OpenRouter => {
                if let Some(p) = &self.openrouter_provider {
                    p.start_turn();
                }
            }
            ModelProvider::OpenAiCompat => {
                if let Some(name) = &self.current_custom_provider
                    && let Some(p) = self.openai_compat_providers.get(name)
                {
                    p.start_turn();
                }
            }
        }

        loop {
            // Check for auto-compaction before each provider call
            if config.auto_compact.enabled
                && let Some((usage, limit)) =
                    self.should_auto_compact(config.auto_compact.threshold)
            {
                crate::output::emit_auto_compact_starting(output, usage, limit);
                // Use quiet output for summarization so it doesn't stream to UI
                let quiet_output = crate::output::OutputContext::new_quiet();
                match self
                    .compact_context(messages, config.auto_compact.preserve_turns, &quiet_output)
                    .await
                {
                    Ok(result) => {
                        crate::output::emit_auto_compact_completed(
                            output,
                            result.messages_compacted,
                        );
                    }
                    Err(e) => {
                        // Log error but continue - don't fail chat just because compaction failed
                        crate::output::emit_error(
                            output,
                            &format!("Auto-compaction failed: {}", e),
                        );
                    }
                }
            }

            // Run one chat iteration with the appropriate provider
            let result = match self.current_provider {
                ModelProvider::Antigravity => {
                    if let Some(custom_name) = &self.current_custom_provider.clone() {
                        match self.antigravity_providers.get_mut(custom_name) {
                            Some(p) => {
                                p.set_model(self.current_model_id.clone());
                                run_chat_iteration(p, messages, interrupted, output, &services)
                                    .await?
                            }
                            None => {
                                let msg = format!(
                                    "Antigravity provider '{}' not configured",
                                    custom_name
                                );
                                crate::output::emit_error(output, &msg);
                                return Err(crate::error::Error::Auth(msg));
                            }
                        }
                    } else {
                        let msg = "Antigravity provider requires a custom provider name";
                        crate::output::emit_error(output, msg);
                        return Err(crate::error::Error::Auth(msg.to_string()));
                    }
                }
                ModelProvider::OpenCodeZen => {
                    self.zen_provider.set_model(self.current_model_id.clone());
                    run_chat_iteration(&self.zen_provider, messages, interrupted, output, &services)
                        .await?
                }
                ModelProvider::GitHubCopilot => match self.copilot_provider.as_mut() {
                    Some(p) => {
                        p.set_model(self.current_model_id.clone());
                        run_chat_iteration(p, messages, interrupted, output, &services).await?
                    }
                    None => {
                        let msg = "GitHub Copilot not configured";
                        crate::output::emit_error(output, msg);
                        return Err(crate::error::Error::Auth(msg.to_string()));
                    }
                },
                ModelProvider::Claude => match self.anthropic_provider.as_mut() {
                    Some(p) => {
                        p.set_model(self.current_model_id.clone());
                        run_chat_iteration(p, messages, interrupted, output, &services).await?
                    }
                    None => {
                        let msg = "Anthropic not configured";
                        crate::output::emit_error(output, msg);
                        return Err(crate::error::Error::Auth(msg.to_string()));
                    }
                },
                ModelProvider::OpenAi => match self.openai_provider.as_mut() {
                    Some(p) => {
                        p.set_model(self.current_model_id.clone());
                        run_chat_iteration(p, messages, interrupted, output, &services).await?
                    }
                    None => {
                        let msg = "OpenAI not configured";
                        crate::output::emit_error(output, msg);
                        return Err(crate::error::Error::Auth(msg.to_string()));
                    }
                },
                ModelProvider::OpenRouter => match self.openrouter_provider.as_mut() {
                    Some(p) => {
                        p.set_model(self.current_model_id.clone());
                        run_chat_iteration(p, messages, interrupted, output, &services).await?
                    }
                    None => {
                        let msg = "OpenRouter not configured";
                        crate::output::emit_error(output, msg);
                        return Err(crate::error::Error::Auth(msg.to_string()));
                    }
                },
                ModelProvider::OpenAiCompat => {
                    if let Some(custom_name) = &self.current_custom_provider.clone() {
                        match self.openai_compat_providers.get_mut(custom_name) {
                            Some(p) => {
                                p.set_model(self.current_model_id.clone());
                                run_chat_iteration(p, messages, interrupted, output, &services)
                                    .await?
                            }
                            None => {
                                let msg = format!(
                                    "OpenAI Compatible provider '{}' not configured",
                                    custom_name
                                );
                                crate::output::emit_error(output, &msg);
                                return Err(crate::error::Error::Auth(msg));
                            }
                        }
                    } else {
                        let msg = "OpenAI Compatible provider requires a custom provider name";
                        crate::output::emit_error(output, msg);
                        return Err(crate::error::Error::Auth(msg.to_string()));
                    }
                }
            };

            match result {
                ChatIterationResult::Done => break,
                ChatIterationResult::Continue => {
                    // Save session after each tool iteration if callback is set
                    if let Some(ref callback) = self.session_save_callback {
                        callback(messages);
                    }
                    continue;
                }
            }
        }

        Ok(())
    }

    /// Prepare and return the request that would be sent to the current provider's API.
    pub async fn prepare_request(&mut self, messages: Vec<Message>) -> Result<serde_json::Value> {
        use crate::provider::Provider;

        macro_rules! prepare_optional {
            ($provider:expr, $name:literal) => {
                match $provider.as_mut() {
                    Some(p) => {
                        p.set_model(self.current_model_id.clone());
                        p.prepare_request(messages).await
                    }
                    None => Err(crate::error::Error::Auth(
                        concat!($name, " not configured").to_string(),
                    )),
                }
            };
        }

        match self.current_provider {
            ModelProvider::Antigravity => {
                if let Some(custom_name) = &self.current_custom_provider {
                    match self.antigravity_providers.get_mut(custom_name) {
                        Some(p) => {
                            p.set_model(self.current_model_id.clone());
                            p.prepare_request(messages).await
                        }
                        None => Err(crate::error::Error::Auth(format!(
                            "Antigravity provider '{}' not configured",
                            custom_name
                        ))),
                    }
                } else {
                    Err(crate::error::Error::Auth(
                        "Antigravity provider requires a custom provider name".to_string(),
                    ))
                }
            }
            ModelProvider::OpenCodeZen => {
                self.zen_provider.set_model(self.current_model_id.clone());
                self.zen_provider.prepare_request(messages).await
            }
            ModelProvider::GitHubCopilot => {
                prepare_optional!(self.copilot_provider, "GitHub Copilot")
            }
            ModelProvider::Claude => prepare_optional!(self.anthropic_provider, "Anthropic"),
            ModelProvider::OpenAi => prepare_optional!(self.openai_provider, "OpenAI"),
            ModelProvider::OpenRouter => prepare_optional!(self.openrouter_provider, "OpenRouter"),
            ModelProvider::OpenAiCompat => {
                if let Some(custom_name) = &self.current_custom_provider {
                    match self.openai_compat_providers.get_mut(custom_name) {
                        Some(p) => {
                            p.set_model(self.current_model_id.clone());
                            p.prepare_request(messages).await
                        }
                        None => Err(crate::error::Error::Auth(format!(
                            "OpenAI Compatible provider '{}' not configured",
                            custom_name
                        ))),
                    }
                } else {
                    Err(crate::error::Error::Auth(
                        "OpenAI Compatible provider requires a custom provider name".to_string(),
                    ))
                }
            }
        }
    }

    /// Count tokens for the current message state (Claude/Anthropic only).
    pub async fn count_tokens(&mut self, messages: &[Message]) -> Result<serde_json::Value> {
        match self.current_provider {
            ModelProvider::Claude => match self.anthropic_provider.as_ref() {
                Some(p) => p.count_tokens(messages).await,
                None => Err(crate::error::Error::Auth(
                    "Anthropic not configured".to_string(),
                )),
            },
            _ => Err(crate::error::Error::Other(
                "Token counting is only available for Claude/Anthropic provider".to_string(),
            )),
        }
    }

    /// Get the current provider type
    pub(crate) fn current_provider(&self) -> ModelProvider {
        self.current_provider
    }

    /// Get the current model ID
    pub(crate) fn current_model_id(&self) -> &str {
        &self.current_model_id
    }

    /// Get the custom provider name (if any)
    pub(crate) fn current_custom_provider(&self) -> Option<&str> {
        self.current_custom_provider.as_deref()
    }

    /// Get the last context usage (input tokens) for the current provider.
    /// Returns None if usage tracking is not available for this provider.
    pub(crate) fn get_last_context_usage(&self) -> Option<u64> {
        let input = match self.current_provider {
            ModelProvider::Claude => crate::usage::anthropic().last_input(),
            ModelProvider::OpenCodeZen => crate::usage::zen().last_input(),
            ModelProvider::OpenAiCompat => crate::usage::openai_compat().last_input(),
            ModelProvider::OpenAi => crate::usage::openai().last_input(),
            ModelProvider::OpenRouter => crate::usage::openrouter().last_input(),
            ModelProvider::Antigravity => crate::usage::antigravity().last_input(),
            ModelProvider::GitHubCopilot => return None,
        };
        // Only return if we actually have usage data
        if input > 0 { Some(input) } else { None }
    }

    /// Get the context limit for the current provider and model.
    /// Returns None if the limit is unknown.
    pub(crate) fn get_context_limit(&self) -> Option<u64> {
        crate::provider::context_limit(self.current_provider, &self.current_model_id)
    }

    /// Check if auto-compaction should be triggered based on current context usage.
    /// Returns Some((current_usage, limit)) if compaction should trigger, None otherwise.
    pub(crate) fn should_auto_compact(&self, threshold: f64) -> Option<(u64, u64)> {
        let current_usage = self.get_last_context_usage()?;
        let limit = self.get_context_limit()?;

        let usage_ratio = current_usage as f64 / limit as f64;
        if usage_ratio >= threshold {
            Some((current_usage, limit))
        } else {
            None
        }
    }

    /// Compact the message context by summarizing older messages
    pub async fn compact_context(
        &mut self,
        messages: &mut Vec<Message>,
        preserve_recent_turns: usize,
        output: &OutputContext,
    ) -> Result<compaction::CompactionResult> {
        use crate::provider::Provider;

        let (to_compact, to_preserve) =
            compaction::segment_messages(messages, preserve_recent_turns);

        if to_compact.is_empty() {
            return Err(crate::error::Error::Other(
                "No messages old enough to compact".into(),
            ));
        }

        let messages_compacted = to_compact.len();

        // Build summarization request
        let user_request = compaction::build_summarization_request(&to_compact);
        let system_msg = Message::system(compaction::summarization_system_prompt());
        let request_messages = vec![system_msg, user_request];

        // Get summary from current provider
        let response = match self.current_provider {
            ModelProvider::Antigravity => {
                if let Some(custom_name) = &self.current_custom_provider {
                    match self.antigravity_providers.get_mut(custom_name) {
                        Some(p) => {
                            p.set_model(self.current_model_id.clone());
                            p.chat(request_messages, output).await?
                        }
                        None => {
                            return Err(crate::error::Error::Auth(format!(
                                "Antigravity provider '{}' not configured",
                                custom_name
                            )));
                        }
                    }
                } else {
                    return Err(crate::error::Error::Auth(
                        "Antigravity provider requires a custom provider name".into(),
                    ));
                }
            }
            ModelProvider::OpenCodeZen => {
                self.zen_provider.set_model(self.current_model_id.clone());
                self.zen_provider.chat(request_messages, output).await?
            }
            ModelProvider::GitHubCopilot => match self.copilot_provider.as_mut() {
                Some(p) => {
                    p.set_model(self.current_model_id.clone());
                    p.chat(request_messages, output).await?
                }
                None => {
                    return Err(crate::error::Error::Auth(
                        "GitHub Copilot not configured".into(),
                    ));
                }
            },
            ModelProvider::Claude => match self.anthropic_provider.as_mut() {
                Some(p) => {
                    p.set_model(self.current_model_id.clone());
                    p.chat(request_messages, output).await?
                }
                None => return Err(crate::error::Error::Auth("Anthropic not configured".into())),
            },
            ModelProvider::OpenAi => match self.openai_provider.as_mut() {
                Some(p) => {
                    p.set_model(self.current_model_id.clone());
                    p.chat(request_messages, output).await?
                }
                None => return Err(crate::error::Error::Auth("OpenAI not configured".into())),
            },
            ModelProvider::OpenRouter => match self.openrouter_provider.as_mut() {
                Some(p) => {
                    p.set_model(self.current_model_id.clone());
                    p.chat(request_messages, output).await?
                }
                None => {
                    return Err(crate::error::Error::Auth(
                        "OpenRouter not configured".into(),
                    ));
                }
            },
            ModelProvider::OpenAiCompat => {
                if let Some(custom_name) = &self.current_custom_provider {
                    match self.openai_compat_providers.get_mut(custom_name) {
                        Some(p) => {
                            p.set_model(self.current_model_id.clone());
                            p.chat(request_messages, output).await?
                        }
                        None => {
                            return Err(crate::error::Error::Auth(format!(
                                "OpenAI Compatible provider '{}' not configured",
                                custom_name
                            )));
                        }
                    }
                } else {
                    return Err(crate::error::Error::Auth(
                        "OpenAI Compatible provider requires a custom provider name".into(),
                    ));
                }
            }
        };

        // Extract summary text
        let summary = response
            .content_blocks
            .iter()
            .filter_map(|block| {
                if let ContentBlock::Text { text } = block {
                    Some(text.clone())
                } else {
                    None
                }
            })
            .collect::<Vec<_>>()
            .join("\n");

        // Build new message list
        let summary_message = Message {
            role: Role::User,
            content: MessageContent::Blocks(vec![ContentBlock::Summary {
                summary,
                messages_compacted,
            }]),
        };

        let mut new_messages = vec![summary_message];
        new_messages.extend(to_preserve);
        *messages = new_messages;

        Ok(compaction::CompactionResult { messages_compacted })
    }
}

/// Parse a model string like "zen/big-pickle" or "ollama/llama3.2" into (provider, model_id, custom_provider_name)
/// Returns (provider_type, model_id, optional_custom_provider_name)
pub(crate) fn parse_model_spec(spec: &str) -> (ModelProvider, String, Option<String>) {
    if let Some((prefix, model)) = spec.split_once('/') {
        // Check for built-in providers first
        let provider = match prefix {
            "antigravity" => {
                // Find first enabled antigravity provider
                if let Ok(config) = crate::config::ConfigFile::load() {
                    for (name, provider_config) in &config.providers.entries {
                        if provider_config.as_antigravity().is_some()
                            && provider_config.is_enabled()
                        {
                            return (
                                ModelProvider::Antigravity,
                                model.to_string(),
                                Some(name.clone()),
                            );
                        }
                    }
                }
                // No antigravity configured - use "antigravity" as the name so
                // the error message is helpful ("provider 'antigravity' not configured")
                return (
                    ModelProvider::Antigravity,
                    model.to_string(),
                    Some("antigravity".to_string()),
                );
            }
            "copilot" | "github-copilot" => {
                return (ModelProvider::GitHubCopilot, model.to_string(), None);
            }
            "claude" | "anthropic" => return (ModelProvider::Claude, model.to_string(), None),
            "openai" | "oai" => return (ModelProvider::OpenAi, model.to_string(), None),
            "openrouter" => return (ModelProvider::OpenRouter, model.to_string(), None),
            "zen" => return (ModelProvider::OpenCodeZen, model.to_string(), None),
            _ => {
                // Check if it's a custom OpenAI-compatible or Antigravity provider
                if let Ok(config) = crate::config::ConfigFile::load() {
                    if config
                        .get_provider(prefix)
                        .is_some_and(|p| p.as_openai_compat().is_some())
                    {
                        return (
                            ModelProvider::OpenAiCompat,
                            model.to_string(),
                            Some(prefix.to_string()),
                        );
                    }
                    if config
                        .get_provider(prefix)
                        .is_some_and(|p| p.as_antigravity().is_some())
                    {
                        return (
                            ModelProvider::Antigravity,
                            model.to_string(),
                            Some(prefix.to_string()),
                        );
                    }
                }
                // Default to Zen if not found
                ModelProvider::OpenCodeZen
            }
        };
        return (provider, model.to_string(), None);
    }

    // Default to Zen for unqualified model names
    (ModelProvider::OpenCodeZen, spec.to_string(), None)
}
