1use std::collections::HashMap;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use super::persistence::PlanPersistenceManager;
9use super::types::*;
10
11pub fn default_criteria() -> Vec<ComparisonCriteria> {
13 vec![
14 ComparisonCriteria {
15 name: "complexity".to_string(),
16 description: "Implementation complexity".to_string(),
17 weight: 0.2,
18 score_range: (0.0, 10.0),
19 },
20 ComparisonCriteria {
21 name: "risk".to_string(),
22 description: "Overall risk level".to_string(),
23 weight: 0.25,
24 score_range: (0.0, 10.0),
25 },
26 ComparisonCriteria {
27 name: "maintainability".to_string(),
28 description: "Long-term maintainability".to_string(),
29 weight: 0.2,
30 score_range: (0.0, 10.0),
31 },
32 ComparisonCriteria {
33 name: "performance".to_string(),
34 description: "Expected performance impact".to_string(),
35 weight: 0.15,
36 score_range: (0.0, 10.0),
37 },
38 ComparisonCriteria {
39 name: "time_to_implement".to_string(),
40 description: "Time required to implement".to_string(),
41 weight: 0.2,
42 score_range: (0.0, 10.0),
43 },
44 ]
45}
46
47pub struct PlanComparisonManager;
49
50impl PlanComparisonManager {
51 pub fn compare_plans(
53 plan_ids: &[String],
54 criteria: Option<Vec<ComparisonCriteria>>,
55 ) -> Result<PlanComparison, String> {
56 let criteria = criteria.unwrap_or_else(default_criteria);
57
58 let mut plans = Vec::new();
60 for id in plan_ids {
61 let plan = PlanPersistenceManager::load_plan(id)?;
62 plans.push(plan);
63 }
64
65 if plans.len() < 2 {
66 return Err("Need at least 2 plans to compare".to_string());
67 }
68
69 let mut scores: HashMap<String, HashMap<String, f32>> = HashMap::new();
71 let mut total_scores: HashMap<String, f32> = HashMap::new();
72
73 for plan in &plans {
74 let plan_id = &plan.metadata.id;
75 let mut plan_scores = HashMap::new();
76 let mut weighted_total = 0.0;
77
78 for criterion in &criteria {
79 let score = Self::calculate_score(plan, criterion);
80 plan_scores.insert(criterion.name.clone(), score);
81 weighted_total += score * criterion.weight;
82 }
83
84 scores.insert(plan_id.clone(), plan_scores);
85 total_scores.insert(plan_id.clone(), (weighted_total * 10.0).round() / 10.0);
86 }
87
88 let recommended_plan_id = total_scores
90 .iter()
91 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
92 .map(|(id, _)| id.clone())
93 .unwrap_or_default();
94
95 let analysis = Self::generate_analysis(&plans, &scores, &criteria);
96 let recommendation = Self::generate_recommendation(
97 plans
98 .iter()
99 .find(|p| p.metadata.id == recommended_plan_id)
100 .unwrap(),
101 &plans,
102 &total_scores,
103 );
104
105 Ok(PlanComparison {
106 plans,
107 criteria,
108 scores,
109 total_scores,
110 recommended_plan_id,
111 recommendation,
112 analysis,
113 generated_at: current_timestamp(),
114 })
115 }
116
117 fn calculate_score(plan: &SavedPlan, criterion: &ComparisonCriteria) -> f32 {
119 match criterion.name.as_str() {
120 "complexity" => Self::score_complexity(plan),
121 "risk" => Self::score_risk(plan),
122 "maintainability" => Self::score_maintainability(plan),
123 "performance" => Self::score_performance(plan),
124 "time_to_implement" => Self::score_time_to_implement(plan),
125 _ => 5.0,
126 }
127 }
128
129 fn score_complexity(plan: &SavedPlan) -> f32 {
131 match plan.estimated_complexity {
132 Complexity::Simple => 10.0,
133 Complexity::Moderate => 7.0,
134 Complexity::Complex => 4.0,
135 Complexity::VeryComplex => 1.0,
136 }
137 }
138
139 fn score_risk(plan: &SavedPlan) -> f32 {
141 if plan.risks.is_empty() {
142 return 10.0;
143 }
144
145 let total: f32 = plan
146 .risks
147 .iter()
148 .map(|r| match r.level {
149 RiskLevel::Low => 1.0,
150 RiskLevel::Medium => 2.0,
151 RiskLevel::High => 3.0,
152 RiskLevel::Critical => 4.0,
153 })
154 .sum();
155
156 let avg = total / plan.risks.len() as f32;
157 (10.0 - avg * 2.5).max(1.0)
158 }
159
160 fn score_maintainability(plan: &SavedPlan) -> f32 {
162 let mut score = 5.0;
163
164 if !plan.architectural_decisions.is_empty() {
165 score += (plan.architectural_decisions.len() as f32 * 0.5).min(2.0);
166 }
167
168 if plan.recommendations.as_ref().is_some_and(|r| !r.is_empty()) {
169 score += 1.0;
170 }
171
172 score.clamp(1.0, 10.0)
173 }
174
175 fn score_performance(plan: &SavedPlan) -> f32 {
177 let mut score = 5.0;
178
179 let perf_keywords = ["performance", "optimize", "fast", "speed", "efficient"];
180 let has_perf_focus = plan
181 .requirements_analysis
182 .non_functional_requirements
183 .iter()
184 .any(|req| perf_keywords.iter().any(|k| req.to_lowercase().contains(k)));
185
186 if has_perf_focus {
187 score += 2.0;
188 }
189
190 let perf_risks: Vec<_> = plan
191 .risks
192 .iter()
193 .filter(|r| matches!(r.category, RiskCategory::Performance))
194 .collect();
195
196 if !perf_risks.is_empty() {
197 let avg_level: f32 = perf_risks
198 .iter()
199 .map(|r| match r.level {
200 RiskLevel::Low => 1.0,
201 RiskLevel::Medium => 2.0,
202 RiskLevel::High => 3.0,
203 RiskLevel::Critical => 4.0,
204 })
205 .sum::<f32>()
206 / perf_risks.len() as f32;
207 score -= avg_level * 0.5;
208 }
209
210 score.clamp(1.0, 10.0)
211 }
212
213 fn score_time_to_implement(plan: &SavedPlan) -> f32 {
215 let hours = plan.estimated_hours.unwrap_or(8.0);
216
217 if hours <= 4.0 {
218 10.0
219 } else if hours <= 8.0 {
220 9.0
221 } else if hours <= 16.0 {
222 7.0
223 } else if hours <= 40.0 {
224 5.0
225 } else if hours <= 80.0 {
226 3.0
227 } else {
228 1.0
229 }
230 }
231
232 fn generate_analysis(
234 plans: &[SavedPlan],
235 scores: &HashMap<String, HashMap<String, f32>>,
236 criteria: &[ComparisonCriteria],
237 ) -> ComparisonAnalysis {
238 let mut strengths: HashMap<String, Vec<String>> = HashMap::new();
239 let mut weaknesses: HashMap<String, Vec<String>> = HashMap::new();
240 let mut risk_comparison: HashMap<String, Vec<Risk>> = HashMap::new();
241 let mut complexity_comparison: HashMap<String, String> = HashMap::new();
242
243 for plan in plans {
244 let plan_id = &plan.metadata.id;
245 let mut plan_strengths = Vec::new();
246 let mut plan_weaknesses = Vec::new();
247
248 for criterion in criteria {
249 let score = scores
250 .get(plan_id)
251 .and_then(|s| s.get(&criterion.name))
252 .copied()
253 .unwrap_or(5.0);
254
255 let avg_score: f32 = scores
256 .values()
257 .filter_map(|s| s.get(&criterion.name))
258 .sum::<f32>()
259 / plans.len() as f32;
260
261 if score > avg_score + 1.0 {
262 plan_strengths.push(format!(
263 "Strong {} (score: {:.1})",
264 criterion.description, score
265 ));
266 } else if score < avg_score - 1.0 {
267 plan_weaknesses.push(format!(
268 "Weak {} (score: {:.1})",
269 criterion.description, score
270 ));
271 }
272 }
273
274 if plan_strengths.is_empty() && !plan.steps.is_empty() {
275 plan_strengths.push("Well-structured implementation steps".to_string());
276 }
277
278 strengths.insert(plan_id.clone(), plan_strengths);
279 weaknesses.insert(plan_id.clone(), plan_weaknesses);
280 risk_comparison.insert(plan_id.clone(), plan.risks.clone());
281 complexity_comparison
282 .insert(plan_id.clone(), format!("{:?}", plan.estimated_complexity));
283 }
284
285 ComparisonAnalysis {
286 strengths,
287 weaknesses,
288 risk_comparison,
289 complexity_comparison,
290 }
291 }
292
293 fn generate_recommendation(
295 recommended: &SavedPlan,
296 all_plans: &[SavedPlan],
297 total_scores: &HashMap<String, f32>,
298 ) -> String {
299 let score = total_scores
300 .get(&recommended.metadata.id)
301 .copied()
302 .unwrap_or(0.0);
303 let avg_score: f32 = total_scores.values().sum::<f32>() / all_plans.len() as f32;
304 let diff_pct = ((score / avg_score - 1.0) * 100.0).round();
305
306 let mut reasons = vec![
307 format!(
308 "Plan \"{}\" scored {:.1} out of 10, which is {:.1}% higher than the average.",
309 recommended.metadata.title, score, diff_pct
310 ),
311 format!(
312 "\nThis plan has {:?} complexity with an estimated {} hours to implement.",
313 recommended.estimated_complexity,
314 recommended
315 .estimated_hours
316 .map_or("unknown".to_string(), |h| format!("{:.1}", h))
317 ),
318 ];
319
320 let high_risks: Vec<_> = recommended
321 .risks
322 .iter()
323 .filter(|r| matches!(r.level, RiskLevel::High | RiskLevel::Critical))
324 .collect();
325
326 if !high_risks.is_empty() {
327 reasons.push(format!(
328 "\nNote: This plan has {} high-priority risk(s) that should be addressed.",
329 high_risks.len()
330 ));
331 } else {
332 reasons.push("\nThis plan has relatively low risk profile.".to_string());
333 }
334
335 reasons.join("")
336 }
337
338 pub fn generate_comparison_report(comparison: &PlanComparison) -> String {
340 let mut lines = Vec::new();
341
342 lines.push("# Plan Comparison Report".to_string());
343 lines.push(String::new());
344 lines.push(format!("Comparing {} plans:", comparison.plans.len()));
345
346 for (idx, plan) in comparison.plans.iter().enumerate() {
347 let score = comparison
348 .total_scores
349 .get(&plan.metadata.id)
350 .unwrap_or(&0.0);
351 lines.push(format!(
352 "{}. **{}** ({:?}) - Score: {:.1}/10",
353 idx + 1,
354 plan.metadata.title,
355 plan.metadata.status,
356 score
357 ));
358 }
359
360 lines.push(String::new());
361 lines.push("## Recommendation".to_string());
362 lines.push(comparison.recommendation.clone());
363
364 lines.join("\n")
365 }
366}
367
368fn current_timestamp() -> u64 {
369 SystemTime::now()
370 .duration_since(UNIX_EPOCH)
371 .unwrap_or_default()
372 .as_millis() as u64
373}