cckit 0.1.0

Code Kit Written by rust for Claude model Switch, Support 智普LLM, MiniMax, Kimi 提供的 Claude model
use crate::models::*;
use anyhow::{Context, Result};
use std::fs;
use std::path::PathBuf;
use dirs::home_dir;
use serde_json;

pub struct ConfigManager {
    config_dir: PathBuf,
    config_file: PathBuf,
    claude_settings_file: PathBuf,
}

impl ConfigManager {
    pub fn new() -> Result<Self> {
        let home_dir = home_dir().context("Could not find home directory")?;
        let config_dir = home_dir.join(".cckit");
        let config_file = config_dir.join("config.json");
        let claude_settings_file = home_dir.join(".claude/settings.json");

        Ok(Self {
            config_dir,
            config_file,
            claude_settings_file,
        })
    }

    pub fn initialize(&self) -> Result<()> {
        // Create config directory if it doesn't exist
        if !self.config_dir.exists() {
            fs::create_dir_all(&self.config_dir)
                .context("Failed to create config directory")?;
        }

        // Create config file if it doesn't exist
        if !self.config_file.exists() {
            let default_config = CCKitConfig::new();
            self.save_config(&default_config)?;
        }

        Ok(())
    }

    fn load_config(&self) -> Result<CCKitConfig> {
        if !self.config_file.exists() {
            return Ok(CCKitConfig::new());
        }

        let content = fs::read_to_string(&self.config_file)
            .context("Failed to read config file")?;

        serde_json::from_str(&content)
            .context("Failed to parse config file")
    }

    fn save_config(&self, config: &CCKitConfig) -> Result<()> {
        let content = serde_json::to_string_pretty(config)
            .context("Failed to serialize config")?;

        fs::write(&self.config_file, content)
            .context("Failed to write config file")
    }

    pub fn list_providers(&self) -> Result<Vec<ModelProvider>> {
        let config = self.load_config()?;
        Ok(config.providers)
    }

    pub fn save_provider(&self, provider: &ModelProvider) -> Result<()> {
        let mut config = self.load_config()?;
        config.add_provider(provider.clone());
        self.save_config(&config)
    }

    pub fn get_current_provider(&self) -> Result<Option<ModelProvider>> {
        let config = self.load_config()?;
        Ok(config.get_current_provider().cloned())
    }

    pub fn switch_provider(&mut self, provider_id: &str) -> Result<()> {
        let mut config = self.load_config()?;
        config.set_current_provider(provider_id)?;
        self.save_config(&config)?;

        // Apply the provider settings to Claude's settings.json
        self.apply_provider_to_claude_settings(provider_id)?;

        Ok(())
    }

    fn apply_provider_to_claude_settings(&self, provider_id: &str) -> Result<()> {
        let config = self.load_config()?;
        let provider = config.get_provider_by_id(provider_id)
            .context("Provider not found")?;

        // Read existing Claude settings or create new one
        let mut claude_settings = if self.claude_settings_file.exists() {
            let content = fs::read_to_string(&self.claude_settings_file)
                .context("Failed to read Claude settings")?;
            serde_json::from_str(&content)
                .context("Failed to parse Claude settings")?
        } else {
            serde_json::json!({})
        };

        // Create or update the env section based on provider type
        let env_section = match provider.provider_type.as_str() {
            "claude" => {
                // For official Claude, use standard format
                serde_json::json!({
                    "ANTHROPIC_API_KEY": provider.api_key
                })
            }
            "zhipu" | "minimax" => {
                // For 智普LLM and MiniMax, use AUTH_TOKEN format
                serde_json::json!({
                    "ANTHROPIC_AUTH_TOKEN": provider.api_key,
                    "ANTHROPIC_BASE_URL": provider.base_url.as_ref().unwrap_or(&"".to_string()),
                    "ANTHROPIC_MODEL": provider.model.as_ref().unwrap_or(&"claude-3-5-sonnet-20241022".to_string())
                })
            }
            "kimi" => {
                // For Kimi, use API_KEY format
                serde_json::json!({
                    "ANTHROPIC_API_KEY": provider.api_key,
                    "ANTHROPIC_BASE_URL": provider.base_url.as_ref().unwrap_or(&"".to_string()),
                    "ANTHROPIC_MODEL": provider.model.as_ref().unwrap_or(&"kimi-for-coding".to_string()),
                    "ANTHROPIC_DEFAULT_OPUS_MODEL": provider.model.as_ref().unwrap_or(&"kimi-for-coding".to_string()),
                    "ANTHROPIC_DEFAULT_SONNET_MODEL": provider.model.as_ref().unwrap_or(&"kimi-for-coding".to_string()),
                    "ANTHROPIC_DEFAULT_HAIKU_MODEL": provider.model.as_ref().unwrap_or(&"kimi-for-coding".to_string())
                })
            }
            _ => {
                // Default format for other providers
                serde_json::json!({
                    "ANTHROPIC_API_KEY": provider.api_key,
                    "ANTHROPIC_BASE_URL": provider.base_url.as_ref().unwrap_or(&"".to_string()),
                    "ANTHROPIC_MODEL": provider.model.as_ref().unwrap_or(&"claude-3-5-sonnet-20241022".to_string()),
                    "CC_PROVIDER": provider.provider_type
                })
            }
        };

        claude_settings["env"] = env_section;

        // Ensure the .claude directory exists
        if let Some(parent) = self.claude_settings_file.parent() {
            fs::create_dir_all(parent)
                .context("Failed to create .claude directory")?;
        }

        // Write the updated settings
        let content = serde_json::to_string_pretty(&claude_settings)
            .context("Failed to serialize Claude settings")?;

        fs::write(&self.claude_settings_file, content)
            .context("Failed to write Claude settings")?;

        Ok(())
    }

    pub fn reset_to_claude_default(&mut self) -> Result<()> {
        let mut config = self.load_config()?;
        config.current_provider_id = None;
        self.save_config(&config)?;

        // Reset Claude settings to only use official Claude
        let claude_settings = serde_json::json!({
            "env": {
                "ANTHROPIC_API_KEY": ""
            }
        });

        // Ensure the .claude directory exists
        if let Some(parent) = self.claude_settings_file.parent() {
            fs::create_dir_all(parent)
                .context("Failed to create .claude directory")?;
        }

        // Write the default settings
        let content = serde_json::to_string_pretty(&claude_settings)
            .context("Failed to serialize Claude settings")?;

        fs::write(&self.claude_settings_file, content)
            .context("Failed to write Claude settings")?;

        Ok(())
    }

    #[allow(dead_code)]
    pub fn export_config(&self) -> Result<CCKitConfig> {
        self.load_config()
    }

    #[allow(dead_code)]
    pub fn import_config(&mut self, imported_config: CCKitConfig) -> Result<()> {
        self.save_config(&imported_config)?;

        // If there's a current provider, apply its settings
        if let Some(current_id) = &imported_config.current_provider_id {
            self.apply_provider_to_claude_settings(current_id)?;
        }

        Ok(())
    }
}

// Helper functions for default values
pub fn get_default_base_url(provider_type: &str) -> Option<String> {
    match provider_type {
        "zhipu" => Some("https://open.bigmodel.cn/api/anthropic".to_string()),
        "minimax" => Some("https://api.minimaxi.com/anthropic".to_string()),
        "kimi" => Some("https://api.kimi.com/coding/".to_string()),
        "claude" => None, // Uses default Anthropic URL
        _ => None,
    }
}

pub fn get_default_model(provider_type: &str) -> Option<String> {
    match provider_type {
        "zhipu" => Some("GLM-4.6".to_string()),
        "minimax" => Some("MiniMax-M2".to_string()),
        "kimi" => Some("kimi-for-coding".to_string()),
        "claude" => Some("claude-3-5-sonnet-20241022".to_string()),
        _ => Some("claude-3-5-sonnet-20241022".to_string()),
    }
}