use crate::config::config_models::ProfileConfig;
use crate::config::enhanced_models::EnhancedScoringProfile;
use crate::config::profiles::ProfileLoader;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Copy)]
pub enum ConfigFormat {
Yaml,
Toml,
Json,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigManager {
pub config: ProfileConfig,
#[serde(skip)]
pub profile_loader: ProfileLoader,
}
impl Default for ConfigManager {
fn default() -> Self {
Self::new()
}
}
impl ConfigManager {
pub fn new() -> Self {
let profile_loader = ProfileLoader::default(); let profiles = Self::extract_profiles_from_loader(&profile_loader);
Self {
config: ProfileConfig {
active_profile: Some("content_article".to_string()),
presets: profiles,
output: Default::default(),
bands: Default::default(),
plugins: Default::default(),
},
profile_loader,
}
}
fn extract_profiles_from_loader(
loader: &ProfileLoader,
) -> std::collections::HashMap<String, EnhancedScoringProfile> {
let mut profiles = std::collections::HashMap::new();
for name in loader.list_profiles() {
if let Some(profile) = loader.get_profile(&name) {
profiles.insert(name, profile.clone());
}
}
profiles
}
pub fn from_file(path: &Path) -> Result<Self, String> {
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("");
let format = match extension {
"yaml" | "yml" => ConfigFormat::Yaml,
"toml" => ConfigFormat::Toml,
"json" => ConfigFormat::Json,
_ => return Err("Unsupported config file format".to_string()),
};
let content = fs::read_to_string(path).map_err(|e| e.to_string())?;
Self::from_str(&content, format)
}
pub(crate) fn from_str(content: &str, format: ConfigFormat) -> Result<Self, String> {
let config: ProfileConfig = match format {
ConfigFormat::Yaml => serde_yaml::from_str(content).map_err(|e| e.to_string())?,
ConfigFormat::Toml => toml::from_str(content).map_err(|e| e.to_string())?,
ConfigFormat::Json => serde_json::from_str(content).map_err(|e| e.to_string())?,
};
Ok(Self {
config,
profile_loader: ProfileLoader::default(),
})
}
pub fn get_profile(&self, name: &str) -> Option<&EnhancedScoringProfile> {
self.config.presets.get(name)
}
pub fn get_active_profile_with_override(
&self,
profile_override: Option<String>,
) -> Result<&EnhancedScoringProfile, String> {
let profile_name = profile_override.as_deref().unwrap_or(
self.config
.active_profile
.as_deref()
.unwrap_or("content_article"),
);
self.get_profile(profile_name)
.ok_or_else(|| format!("Profile '{}' not found.", profile_name))
}
pub fn get_active_profile(&self) -> Result<&EnhancedScoringProfile, String> {
let profile_name = self
.config
.active_profile
.as_deref()
.unwrap_or("content_article");
self.get_profile(profile_name)
.ok_or_else(|| format!("Profile '{}' not found.", profile_name))
}
pub fn set_active_profile(&mut self, name: &str) -> Result<(), String> {
if self.get_profile(name).is_some() {
self.config.active_profile = Some(name.to_string());
Ok(())
} else {
Err(format!("Profile '{}' not found.", name))
}
}
pub fn from_profile_config(config: ProfileConfig) -> Result<Self, String> {
let active_name = config
.active_profile
.as_deref()
.unwrap_or("content_article");
if !config.presets.contains_key(active_name) {
return Err(format!(
"Active profile '{}' not found in presets",
active_name
));
}
Ok(Self {
config,
profile_loader: ProfileLoader::default(),
})
}
#[cfg(feature = "cli")]
pub fn create_sample_config(path: &Path, format: ConfigFormat) -> Result<(), String> {
let loader = ProfileLoader::default();
let profiles = Self::extract_profiles_from_loader(&loader);
let config = ProfileConfig {
active_profile: Some("content_article".to_string()),
presets: profiles,
output: Default::default(),
bands: Default::default(),
plugins: Default::default(),
};
let content = match format {
ConfigFormat::Yaml => serde_yaml::to_string(&config).map_err(|e| e.to_string())?,
ConfigFormat::Toml => toml::to_string_pretty(&config).map_err(|e| e.to_string())?,
ConfigFormat::Json => {
serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?
}
};
fs::write(path, content).map_err(|e| e.to_string())
}
pub fn add_profile(
&mut self,
name: String,
profile: EnhancedScoringProfile,
) -> Result<(), String> {
self.profile_loader
.add_profile(name.clone(), profile.clone())?;
self.config.presets.insert(name, profile);
Ok(())
}
pub fn get_profile_from_loader(&self, name: &str) -> Option<&EnhancedScoringProfile> {
self.profile_loader.get_profile(name)
}
pub fn get_available_profiles(&self) -> Vec<String> {
self.config.presets.keys().cloned().collect()
}
pub fn get_active_profile_name(&self) -> Result<String, String> {
self.config
.active_profile
.clone()
.ok_or_else(|| "No active profile set".to_string())
}
pub fn profile_builder() -> crate::config::profiles::ProfileBuilder {
use crate::config::profiles::ProfileLoader;
ProfileLoader::create_profile_builder()
}
pub fn set_runtime_profile(
&mut self,
profile: EnhancedScoringProfile,
) -> Result<(), crate::AnalyzeError> {
if profile.metadata.name.is_empty() {
return Err(crate::AnalyzeError::InvalidProfile(
"Profile name cannot be empty".to_string(),
));
}
let weight_sum: f32 = profile.category_weights.values().sum();
if (weight_sum - 1.0).abs() > 0.01 {
return Err(crate::AnalyzeError::InvalidProfile(format!(
"Category weights must sum to 1.0, got {}",
weight_sum
)));
}
let profile_name = profile.metadata.name.clone();
self.profile_loader
.add_profile(profile_name.clone(), profile.clone())
.map_err(|e| {
crate::AnalyzeError::ConfigError(format!("Failed to add runtime profile: {}", e))
})?;
self.config.presets.insert(profile_name.clone(), profile);
self.config.active_profile = Some(profile_name);
Ok(())
}
}