1use crate::cli::{Config as CliConfig, LlmTool};
8use crate::utils::error::ContextCreatorError;
9use serde::{Deserialize, Serialize};
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone, Serialize, Deserialize, Default)]
14pub struct ConfigFile {
15    #[serde(default)]
17    pub defaults: Defaults,
18
19    #[serde(default)]
21    pub priorities: Vec<Priority>,
22
23    #[serde(default)]
25    pub ignore: Vec<String>,
26
27    #[serde(default)]
29    pub include: Vec<String>,
30
31    #[serde(default)]
33    pub tokens: TokenLimits,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, Default)]
38pub struct Defaults {
39    pub max_tokens: Option<usize>,
41
42    #[serde(default)]
44    pub llm_tool: Option<String>,
45
46    #[serde(default)]
48    pub progress: bool,
49
50    #[serde(default)]
52    pub verbose: bool,
53
54    #[serde(default)]
56    pub quiet: bool,
57
58    pub directory: Option<PathBuf>,
60
61    pub output_file: Option<PathBuf>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct Priority {
68    pub pattern: String,
70    pub weight: f32,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, Default)]
76pub struct TokenLimits {
77    pub gemini: Option<usize>,
79    pub codex: Option<usize>,
81}
82
83impl ConfigFile {
84    pub fn load_from_file(path: &Path) -> Result<Self, ContextCreatorError> {
86        if !path.exists() {
87            return Err(ContextCreatorError::InvalidPath(format!(
88                "Configuration file does not exist: {}",
89                path.display()
90            )));
91        }
92
93        let content = std::fs::read_to_string(path).map_err(|e| {
94            ContextCreatorError::ConfigError(format!(
95                "Failed to read config file {}: {}",
96                path.display(),
97                e
98            ))
99        })?;
100
101        let config: ConfigFile = toml::from_str(&content).map_err(|e| {
102            ContextCreatorError::ConfigError(format!(
103                "Failed to parse config file {}: {}",
104                path.display(),
105                e
106            ))
107        })?;
108
109        Ok(config)
110    }
111
112    pub fn load_default() -> Result<Option<Self>, ContextCreatorError> {
114        let local_config = Path::new(".context-creator.toml");
116        if local_config.exists() {
117            return Ok(Some(Self::load_from_file(local_config)?));
118        }
119
120        let rc_config = Path::new(".contextrc.toml");
122        if rc_config.exists() {
123            return Ok(Some(Self::load_from_file(rc_config)?));
124        }
125
126        if let Some(home) = dirs::home_dir() {
128            let home_config = home.join(".context-creator.toml");
129            if home_config.exists() {
130                return Ok(Some(Self::load_from_file(&home_config)?));
131            }
132        }
133
134        Ok(None)
135    }
136
137    pub fn apply_to_cli_config(&self, cli_config: &mut CliConfig) {
139        cli_config.custom_priorities = self.priorities.clone();
141
142        cli_config.config_token_limits = Some(self.tokens.clone());
144
145        if cli_config.max_tokens.is_none() && self.defaults.max_tokens.is_some() {
147            cli_config.config_defaults_max_tokens = self.defaults.max_tokens;
148        }
149
150        if let Some(ref tool_str) = self.defaults.llm_tool {
151            if cli_config.llm_tool == LlmTool::default() {
153                match tool_str.as_str() {
154                    "gemini" => cli_config.llm_tool = LlmTool::Gemini,
155                    "codex" => cli_config.llm_tool = LlmTool::Codex,
156                    _ => {} }
158            }
159        }
160
161        if !cli_config.progress && self.defaults.progress {
163            cli_config.progress = self.defaults.progress;
164        }
165
166        if !cli_config.verbose && self.defaults.verbose {
167            cli_config.verbose = self.defaults.verbose;
168        }
169
170        if !cli_config.quiet && self.defaults.quiet {
171            cli_config.quiet = self.defaults.quiet;
172        }
173
174        let current_paths = cli_config.get_directories();
177        if current_paths.len() == 1
178            && current_paths[0] == PathBuf::from(".")
179            && self.defaults.directory.is_some()
180            && cli_config.repo.is_none()
181        {
182            cli_config.paths = Some(vec![self.defaults.directory.clone().unwrap()]);
183        }
184
185        if cli_config.output_file.is_none() && self.defaults.output_file.is_some() {
187            cli_config.output_file = self.defaults.output_file.clone();
188        }
189
190        if cli_config.ignore.is_none() && !self.ignore.is_empty() {
193            cli_config.ignore = Some(self.ignore.clone());
194        }
195
196        if cli_config.include.is_none() && !self.include.is_empty() {
199            cli_config.include = Some(self.include.clone());
200        }
201    }
202}
203
204pub fn create_example_config() -> String {
206    let example = ConfigFile {
207        defaults: Defaults {
208            max_tokens: Some(150000),
209            llm_tool: Some("gemini".to_string()),
210            progress: true,
211            verbose: false,
212            quiet: false,
213            directory: None,
214            output_file: None,
215        },
216        tokens: TokenLimits {
217            gemini: Some(2_000_000),
218            codex: Some(1_500_000),
219        },
220        priorities: vec![
221            Priority {
222                pattern: "src/**/*.rs".to_string(),
223                weight: 100.0,
224            },
225            Priority {
226                pattern: "src/main.rs".to_string(),
227                weight: 150.0,
228            },
229            Priority {
230                pattern: "tests/**/*.rs".to_string(),
231                weight: 50.0,
232            },
233            Priority {
234                pattern: "docs/**/*.md".to_string(),
235                weight: 30.0,
236            },
237            Priority {
238                pattern: "*.toml".to_string(),
239                weight: 80.0,
240            },
241            Priority {
242                pattern: "*.json".to_string(),
243                weight: 60.0,
244            },
245        ],
246        ignore: vec![
247            "target/**".to_string(),
248            "node_modules/**".to_string(),
249            "*.pyc".to_string(),
250            ".env".to_string(),
251        ],
252        include: vec!["!important/**".to_string()],
253    };
254
255    toml::to_string_pretty(&example)
256        .unwrap_or_else(|_| "# Failed to generate example config".to_string())
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use std::fs;
263    use tempfile::TempDir;
264
265    #[test]
266    fn test_config_file_parsing() {
267        let config_content = r#"
268ignore = [
269    "target/**",
270    "node_modules/**"
271]
272
273include = [
274    "!important/**"
275]
276
277[defaults]
278max_tokens = 100000
279llm_tool = "gemini"
280progress = true
281
282[[priorities]]
283pattern = "src/**/*.rs"
284weight = 100.0
285
286[[priorities]]
287pattern = "tests/**/*.rs"
288weight = 50.0
289"#;
290
291        let config: ConfigFile = toml::from_str(config_content).unwrap();
292
293        assert_eq!(config.defaults.max_tokens, Some(100000));
294        assert_eq!(config.defaults.llm_tool, Some("gemini".to_string()));
295        assert!(config.defaults.progress);
296        assert_eq!(config.priorities.len(), 2);
297        assert_eq!(config.priorities[0].pattern, "src/**/*.rs");
298        assert_eq!(config.priorities[0].weight, 100.0);
299        assert_eq!(config.ignore.len(), 2);
300        assert_eq!(config.include.len(), 1);
301    }
302
303    #[test]
304    fn test_config_file_loading() {
305        let temp_dir = TempDir::new().unwrap();
306        let config_path = temp_dir.path().join("config.toml");
307
308        let config_content = r#"
309[defaults]
310max_tokens = 50000
311progress = true
312"#;
313
314        fs::write(&config_path, config_content).unwrap();
315
316        let config = ConfigFile::load_from_file(&config_path).unwrap();
317        assert_eq!(config.defaults.max_tokens, Some(50000));
318        assert!(config.defaults.progress);
319    }
320
321    #[test]
322    fn test_apply_to_cli_config() {
323        let config_file = ConfigFile {
324            defaults: Defaults {
325                max_tokens: Some(75000),
326                llm_tool: Some("codex".to_string()),
327                progress: true,
328                verbose: true,
329                quiet: false,
330                directory: Some(PathBuf::from("/tmp")),
331                output_file: Some(PathBuf::from("output.md")),
332            },
333            tokens: TokenLimits::default(),
334            priorities: vec![],
335            ignore: vec![],
336            include: vec![],
337        };
338
339        let mut cli_config = CliConfig {
340            prompt: None,
341            paths: Some(vec![PathBuf::from(".")]),
342            include: None,
343            ignore: None,
344            repo: None,
345            read_stdin: false,
346            output_file: None,
347            max_tokens: None,
348            llm_tool: LlmTool::default(),
349            quiet: false,
350            verbose: false,
351            config: None,
352            progress: false,
353            copy: false,
354            enhanced_context: false,
355            trace_imports: false,
356            include_callers: false,
357            include_types: false,
358            semantic_depth: 3,
359            custom_priorities: vec![],
360            config_token_limits: None,
361            config_defaults_max_tokens: None,
362        };
363
364        config_file.apply_to_cli_config(&mut cli_config);
365
366        assert_eq!(cli_config.config_defaults_max_tokens, Some(75000));
367        assert_eq!(cli_config.llm_tool, LlmTool::Codex);
368        assert!(cli_config.progress);
369        assert!(cli_config.verbose);
370        assert_eq!(cli_config.get_directories(), vec![PathBuf::from("/tmp")]);
371        assert_eq!(cli_config.output_file, Some(PathBuf::from("output.md")));
372    }
373
374    #[test]
375    fn test_example_config_generation() {
376        let example = create_example_config();
377        assert!(example.contains("[defaults]"));
378        assert!(example.contains("max_tokens"));
379        assert!(example.contains("[tokens]"));
380        assert!(example.contains("gemini"));
381        assert!(example.contains("codex"));
382        assert!(example.contains("[[priorities]]"));
383        assert!(example.contains("pattern"));
384        assert!(example.contains("weight"));
385    }
386
387    #[test]
388    fn test_token_limits_parsing() {
389        let config_content = r#"
390[tokens]
391gemini = 2000000
392codex = 1500000
393
394[defaults]
395max_tokens = 100000
396"#;
397
398        let config: ConfigFile = toml::from_str(config_content).unwrap();
399        assert_eq!(config.tokens.gemini, Some(2_000_000));
400        assert_eq!(config.tokens.codex, Some(1_500_000));
401        assert_eq!(config.defaults.max_tokens, Some(100_000));
402    }
403
404    #[test]
405    fn test_token_limits_partial_parsing() {
406        let config_content = r#"
407[tokens]
408gemini = 3000000
409# codex not specified, should use default
410
411[defaults]
412max_tokens = 150000
413"#;
414
415        let config: ConfigFile = toml::from_str(config_content).unwrap();
416        assert_eq!(config.tokens.gemini, Some(3_000_000));
417        assert_eq!(config.tokens.codex, None);
418    }
419
420    #[test]
421    fn test_token_limits_empty_section() {
422        let config_content = r#"
423[tokens]
424# No limits specified
425
426[defaults]
427max_tokens = 200000
428"#;
429
430        let config: ConfigFile = toml::from_str(config_content).unwrap();
431        assert_eq!(config.tokens.gemini, None);
432        assert_eq!(config.tokens.codex, None);
433    }
434
435    #[test]
436    fn test_apply_to_cli_config_with_token_limits() {
437        let config_file = ConfigFile {
438            defaults: Defaults {
439                max_tokens: Some(75000),
440                llm_tool: Some("gemini".to_string()),
441                progress: true,
442                verbose: false,
443                quiet: false,
444                directory: None,
445                output_file: None,
446            },
447            tokens: TokenLimits {
448                gemini: Some(2_500_000),
449                codex: Some(1_800_000),
450            },
451            priorities: vec![],
452            ignore: vec![],
453            include: vec![],
454        };
455
456        let mut cli_config = CliConfig {
457            prompt: None,
458            paths: Some(vec![PathBuf::from(".")]),
459            include: None,
460            ignore: None,
461            repo: None,
462            read_stdin: false,
463            output_file: None,
464            max_tokens: None,
465            llm_tool: LlmTool::default(),
466            quiet: false,
467            verbose: false,
468            config: None,
469            progress: false,
470            copy: false,
471            enhanced_context: false,
472            trace_imports: false,
473            include_callers: false,
474            include_types: false,
475            semantic_depth: 3,
476            custom_priorities: vec![],
477            config_token_limits: None,
478            config_defaults_max_tokens: None,
479        };
480
481        config_file.apply_to_cli_config(&mut cli_config);
482
483        assert_eq!(cli_config.config_defaults_max_tokens, Some(75000)); assert!(cli_config.config_token_limits.is_some());
486        let token_limits = cli_config.config_token_limits.as_ref().unwrap();
487        assert_eq!(token_limits.gemini, Some(2_500_000));
488        assert_eq!(token_limits.codex, Some(1_800_000));
489    }
490}