meld 1.1.5

Deterministic filesystem state management using Merkle trees
Documentation
use crate::error::ApiError;
use crate::provider::profile::{ProviderConfig, ProviderType, ValidationResult};
use crate::provider::ProviderRegistry;

pub struct ProviderDiagnosticsService;

impl ProviderDiagnosticsService {
    fn load_provider_for_validation(
        registry: &ProviderRegistry,
        provider_name: &str,
        config_path: &std::path::Path,
        result: &mut ValidationResult,
    ) -> Result<Option<ProviderConfig>, ApiError> {
        if let Some(provider) = registry.get(provider_name) {
            return Ok(Some(provider.clone()));
        }

        if !config_path.exists() {
            result.add_error("Provider not found in registry".to_string());
            return Ok(None);
        }

        let content = match std::fs::read_to_string(config_path) {
            Ok(content) => content,
            Err(e) => {
                result.add_error(format!(
                    "Failed to read config file {}: {}",
                    config_path.display(),
                    e
                ));
                return Ok(None);
            }
        };

        let mut provider: ProviderConfig = match toml::from_str(&content) {
            Ok(config) => config,
            Err(e) => {
                result.add_error(format!(
                    "Failed to parse config file {}: {}",
                    config_path.display(),
                    e
                ));
                return Ok(None);
            }
        };

        if provider.provider_name.is_none() {
            provider.provider_name = Some(provider_name.to_string());
        }

        Ok(Some(provider))
    }

    pub fn resolve_api_key_status(provider: &ProviderConfig) -> String {
        match provider.provider_type {
            ProviderType::OpenAI => {
                if provider.api_key.is_some() {
                    "Set (from config)".to_string()
                } else if std::env::var("OPENAI_API_KEY").is_ok() {
                    "Set (from environment)".to_string()
                } else {
                    "Not set".to_string()
                }
            }
            ProviderType::Anthropic => {
                if provider.api_key.is_some() {
                    "Set (from config)".to_string()
                } else if std::env::var("ANTHROPIC_API_KEY").is_ok() {
                    "Set (from environment)".to_string()
                } else {
                    "Not set".to_string()
                }
            }
            ProviderType::Ollama | ProviderType::LocalCustom => "Not required".to_string(),
        }
    }

    pub fn validate_provider(
        registry: &ProviderRegistry,
        provider_name: &str,
    ) -> Result<ValidationResult, ApiError> {
        let mut result = ValidationResult::new(provider_name.to_string());

        let config_path = registry.provider_config_path(provider_name)?;
        let provider = match Self::load_provider_for_validation(
            registry,
            provider_name,
            &config_path,
            &mut result,
        )? {
            Some(provider) => provider,
            None => return Ok(result),
        };

        if !config_path.exists() {
            result.add_error(format!("Config file not found: {}", config_path.display()));
            return Ok(result);
        }

        let expected_filename = format!("{}.toml", provider_name);
        if config_path
            .file_name()
            .and_then(|n| n.to_str())
            .map(|n| n == expected_filename)
            .unwrap_or(false)
        {
            result.add_check("Provider name matches filename", true);
        } else {
            result.add_error(format!(
                "Provider name '{}' doesn't match filename '{}'",
                provider_name,
                config_path
                    .file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or("unknown")
            ));
        }

        result.add_check("Provider type is valid", true);

        if provider.model.trim().is_empty() {
            result.add_error("Model name cannot be empty".to_string());
        } else {
            result.add_check("Model is not empty", true);
        }

        match provider.provider_type {
            ProviderType::OpenAI | ProviderType::Anthropic => {
                let api_key_available = provider.api_key.is_some()
                    || match provider.provider_type {
                        ProviderType::OpenAI => std::env::var("OPENAI_API_KEY").is_ok(),
                        ProviderType::Anthropic => std::env::var("ANTHROPIC_API_KEY").is_ok(),
                        _ => false,
                    };

                if api_key_available {
                    let source = if provider.api_key.is_some() {
                        "from config"
                    } else {
                        "from environment"
                    };
                    result.add_check(&format!("API key available ({})", source), true);
                } else {
                    let env_var = match provider.provider_type {
                        ProviderType::OpenAI => "OPENAI_API_KEY",
                        ProviderType::Anthropic => "ANTHROPIC_API_KEY",
                        _ => unreachable!(),
                    };
                    result.add_error(format!(
                        "API key not found (set {} or add to config)",
                        env_var
                    ));
                }
            }
            ProviderType::Ollama => {
                result.add_check("API key not required for local provider", true);
            }
            ProviderType::LocalCustom => {
                if provider.api_key.is_some() {
                    result.add_check("API key configured for local custom provider", true);
                } else {
                    result.add_warning(
                        "No API key configured for local custom provider. Some OpenAI-compatible endpoints require authentication.".to_string(),
                    );
                }
            }
        }

        if let Some(endpoint) = &provider.endpoint {
            if ProviderConfig::endpoint_url_is_valid(provider.provider_type, endpoint) {
                result.add_check("Endpoint URL is valid", true);
            } else {
                result.add_error(format!("Invalid endpoint URL: {}", endpoint));
            }
        } else if provider.provider_type == ProviderType::LocalCustom {
            result.add_error("Endpoint is required for local custom provider".to_string());
        } else {
            result.add_check("Endpoint URL (optional)", true);
        }

        if let Some(temp) = provider.default_options.temperature {
            if (0.0..=2.0).contains(&temp) {
                result.add_check("Temperature is in valid range (0.0-2.0)", true);
            } else {
                result.add_error(format!(
                    "Temperature must be between 0.0 and 2.0, got {}",
                    temp
                ));
            }
        }

        if let Some(max_tokens) = provider.default_options.max_tokens {
            if max_tokens > 0 {
                result.add_check("Max tokens is positive", true);
            } else {
                result.add_error("Max tokens must be positive".to_string());
            }
        }

        if let Some(top_p) = provider.default_options.top_p {
            if (0.0..=1.0).contains(&top_p) {
                result.add_check("Top-p is in valid range (0.0-1.0)", true);
            } else {
                result.add_error(format!("Top-p must be between 0.0 and 1.0, got {}", top_p));
            }
        }

        Ok(result)
    }

    pub fn list_available_models(
        registry: &ProviderRegistry,
        provider_name: &str,
    ) -> Result<Vec<String>, ApiError> {
        let client = registry.create_client(provider_name)?;
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| ApiError::ProviderError(format!("Failed to create runtime: {}", e)))?;
        rt.block_on(client.list_models())
    }

    pub fn list_available_models_with_timeout(
        registry: &ProviderRegistry,
        provider_name: &str,
        timeout_secs: u64,
    ) -> Result<Vec<String>, ApiError> {
        let client = registry.create_client(provider_name)?;
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| ApiError::ProviderError(format!("Failed to create runtime: {}", e)))?;
        rt.block_on(async {
            tokio::time::timeout(
                std::time::Duration::from_secs(timeout_secs),
                client.list_models(),
            )
            .await
            .map_err(|_| {
                ApiError::ProviderError(format!("API connectivity timeout ({}s)", timeout_secs))
            })?
        })
    }
}