project_examer/
reporter.rs

1use crate::{
2    analyzer::{ProjectAnalysis, FileSummary},
3    dependency_graph::DependencyAnalysis,
4    llm::{AnalysisResponse, Priority},
5};
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::{
9    fs,
10    path::PathBuf,
11};
12
13#[derive(Debug, Serialize, Deserialize)]
14pub struct Report {
15    pub metadata: ReportMetadata,
16    pub executive_summary: ExecutiveSummary,
17    pub file_analysis: FileAnalysisReport,
18    pub dependency_analysis: DependencyAnalysisReport,
19    pub llm_insights: Vec<AnalysisResponse>,
20    pub recommendations: Vec<PrioritizedRecommendation>,
21}
22
23#[derive(Debug, Serialize, Deserialize)]
24pub struct ReportMetadata {
25    pub generated_at: String,
26    pub project_name: String,
27    pub total_files: usize,
28    pub total_size: u64,
29    pub analysis_duration_ms: u128,
30    pub version: String,
31    pub llm_provider: String,
32    pub llm_model: String,
33}
34
35#[derive(Debug, Serialize, Deserialize)]
36pub struct ExecutiveSummary {
37    pub overview: String,
38    pub key_findings: Vec<String>,
39    pub critical_issues: Vec<String>,
40    pub architecture_style: String,
41    pub complexity_score: f64,
42    pub maintainability_score: f64,
43}
44
45#[derive(Debug, Serialize, Deserialize)]
46pub struct FileAnalysisReport {
47    pub summary: FileSummary,
48    pub language_breakdown: Vec<LanguageStats>,
49    pub largest_files: Vec<FileStats>,
50    pub complexity_distribution: Vec<ComplexityBucket>,
51}
52
53#[derive(Debug, Serialize, Deserialize)]
54pub struct LanguageStats {
55    pub language: String,
56    pub file_count: usize,
57    pub total_size: u64,
58    pub avg_file_size: f64,
59    pub percentage: f64,
60}
61
62#[derive(Debug, Serialize, Deserialize)]
63pub struct FileStats {
64    pub path: String,
65    pub size: u64,
66    pub language: String,
67    pub functions: usize,
68    pub classes: usize,
69    pub complexity: usize,
70}
71
72#[derive(Debug, Serialize, Deserialize)]
73pub struct ComplexityBucket {
74    pub range: String,
75    pub count: usize,
76    pub percentage: f64,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct DependencyAnalysisReport {
81    pub graph_metrics: DependencyAnalysis,
82    pub circular_dependencies: Vec<CircularDependency>,
83    pub highly_coupled_files: Vec<CouplingInfo>,
84    pub orphaned_files: Vec<String>,
85    pub dependency_depth: DependencyDepthInfo,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct CircularDependency {
90    pub files: Vec<String>,
91    pub severity: String,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct CouplingInfo {
96    pub file: String,
97    pub incoming_dependencies: usize,
98    pub outgoing_dependencies: usize,
99    pub coupling_score: f64,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct DependencyDepthInfo {
104    pub max_depth: usize,
105    pub avg_depth: f64,
106    pub depth_distribution: Vec<DepthBucket>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct DepthBucket {
111    pub depth: usize,
112    pub count: usize,
113}
114
115#[derive(Debug, Serialize, Deserialize)]
116pub struct PrioritizedRecommendation {
117    pub title: String,
118    pub description: String,
119    pub priority: Priority,
120    pub category: String,
121    pub estimated_effort: String,
122    pub potential_impact: String,
123    pub action_items: Vec<String>,
124    pub affected_files: Vec<String>,
125}
126
127pub struct Reporter;
128
129impl Reporter {
130    pub fn new() -> Self {
131        Self
132    }
133
134    pub fn generate_report(&self, analysis: &ProjectAnalysis, duration_ms: u128, llm_provider: &str, llm_model: &str) -> Report {
135        let metadata = self.create_metadata(analysis, duration_ms, llm_provider, llm_model);
136        let executive_summary = self.create_executive_summary(analysis);
137        let file_analysis = self.create_file_analysis_report(analysis);
138        let dependency_analysis = self.create_dependency_analysis_report(analysis);
139        let recommendations = self.prioritize_recommendations(analysis);
140
141        Report {
142            metadata,
143            executive_summary,
144            file_analysis,
145            dependency_analysis,
146            llm_insights: analysis.llm_analysis.clone(),
147            recommendations,
148        }
149    }
150
151    fn create_metadata(&self, analysis: &ProjectAnalysis, duration_ms: u128, llm_provider: &str, llm_model: &str) -> ReportMetadata {
152        let total_size = analysis.files.iter().map(|f| f.size).sum();
153        let project_name = analysis.files.first()
154            .and_then(|f| f.path.parent())
155            .and_then(|p| p.file_name())
156            .and_then(|n| n.to_str())
157            .unwrap_or("unknown")
158            .to_string();
159
160        ReportMetadata {
161            generated_at: chrono::Utc::now().to_rfc3339(),
162            project_name,
163            total_files: analysis.files.len(),
164            total_size,
165            analysis_duration_ms: duration_ms,
166            version: env!("CARGO_PKG_VERSION").to_string(),
167            llm_provider: llm_provider.to_string(),
168            llm_model: llm_model.to_string(),
169        }
170    }
171
172    fn create_executive_summary(&self, analysis: &ProjectAnalysis) -> ExecutiveSummary {
173        let mut key_findings = Vec::new();
174        let mut critical_issues = Vec::new();
175
176        for analysis_result in &analysis.llm_analysis {
177            for insight in &analysis_result.insights {
178                key_findings.push(insight.title.clone());
179            }
180
181            for rec in &analysis_result.recommendations {
182                if matches!(rec.priority, Priority::High | Priority::Critical) {
183                    critical_issues.push(rec.title.clone());
184                }
185            }
186        }
187
188        let overview = if let Some(first_analysis) = analysis.llm_analysis.first() {
189            first_analysis.analysis.clone()
190        } else {
191            "No LLM analysis available".to_string()
192        };
193
194        let complexity_score = self.calculate_complexity_score(analysis);
195        let maintainability_score = self.calculate_maintainability_score(analysis);
196
197        ExecutiveSummary {
198            overview,
199            key_findings,
200            critical_issues,
201            architecture_style: "Unknown".to_string(), // Could be inferred from analysis
202            complexity_score,
203            maintainability_score,
204        }
205    }
206
207    fn create_file_analysis_report(&self, analysis: &ProjectAnalysis) -> FileAnalysisReport {
208        let total_size: u64 = analysis.files.iter().map(|f| f.size).sum();
209        
210        let mut language_stats: std::collections::HashMap<String, (usize, u64)> = std::collections::HashMap::new();
211        for file in &analysis.files {
212            if let Some(ref lang) = file.language {
213                let entry = language_stats.entry(lang.clone()).or_insert((0, 0));
214                entry.0 += 1;
215                entry.1 += file.size;
216            }
217        }
218
219        let language_breakdown: Vec<LanguageStats> = language_stats
220            .into_iter()
221            .map(|(lang, (count, size))| LanguageStats {
222                language: lang,
223                file_count: count,
224                total_size: size,
225                avg_file_size: size as f64 / count as f64,
226                percentage: (count as f64 / analysis.files.len() as f64) * 100.0,
227            })
228            .collect();
229
230        let mut file_stats: Vec<FileStats> = analysis.parsed_files
231            .iter()
232            .map(|pf| FileStats {
233                path: pf.file_info.path.to_string_lossy().to_string(),
234                size: pf.file_info.size,
235                language: pf.file_info.language.clone().unwrap_or_else(|| "unknown".to_string()),
236                functions: pf.functions.len(),
237                classes: pf.classes.len(),
238                complexity: pf.functions.len() + pf.classes.len() * 2,
239            })
240            .collect();
241
242        file_stats.sort_by(|a, b| b.size.cmp(&a.size));
243        let largest_files = file_stats.into_iter().take(10).collect();
244
245        let complexity_distribution = self.calculate_complexity_distribution(analysis);
246
247        FileAnalysisReport {
248            summary: FileSummary {
249                total_files: analysis.files.len(),
250                total_size,
251                language_distribution: std::collections::HashMap::new(),
252                extension_distribution: std::collections::HashMap::new(),
253            },
254            language_breakdown,
255            largest_files,
256            complexity_distribution,
257        }
258    }
259
260    fn create_dependency_analysis_report(&self, analysis: &ProjectAnalysis) -> DependencyAnalysisReport {
261        DependencyAnalysisReport {
262            graph_metrics: analysis.dependency_analysis.clone(),
263            circular_dependencies: Vec::new(), // TODO: Implement circular dependency detection
264            highly_coupled_files: Vec::new(),   // TODO: Implement coupling analysis
265            orphaned_files: Vec::new(),         // TODO: Implement orphan detection
266            dependency_depth: DependencyDepthInfo {
267                max_depth: 0,
268                avg_depth: 0.0,
269                depth_distribution: Vec::new(),
270            },
271        }
272    }
273
274    fn prioritize_recommendations(&self, analysis: &ProjectAnalysis) -> Vec<PrioritizedRecommendation> {
275        let mut recommendations = Vec::new();
276
277        for analysis_result in &analysis.llm_analysis {
278            for rec in &analysis_result.recommendations {
279                recommendations.push(PrioritizedRecommendation {
280                    title: rec.title.clone(),
281                    description: rec.description.clone(),
282                    priority: rec.priority.clone(),
283                    category: "General".to_string(),
284                    estimated_effort: format!("{:?}", rec.effort),
285                    potential_impact: format!("{:?}", rec.impact),
286                    action_items: rec.action_items.clone(),
287                    affected_files: Vec::new(),
288                });
289            }
290        }
291
292        recommendations.sort_by(|a, b| {
293            use Priority::*;
294            let priority_order = |p: &Priority| match p {
295                Critical => 0,
296                High => 1,
297                Medium => 2,
298                Low => 3,
299            };
300            priority_order(&a.priority).cmp(&priority_order(&b.priority))
301        });
302
303        recommendations
304    }
305
306    fn calculate_complexity_score(&self, analysis: &ProjectAnalysis) -> f64 {
307        if analysis.parsed_files.is_empty() {
308            return 0.0;
309        }
310
311        let total_complexity: usize = analysis.parsed_files
312            .iter()
313            .map(|pf| pf.functions.len() + pf.classes.len() * 2 + pf.imports.len())
314            .sum();
315
316        (total_complexity as f64 / analysis.parsed_files.len() as f64).min(10.0)
317    }
318
319    fn calculate_maintainability_score(&self, analysis: &ProjectAnalysis) -> f64 {
320        let complexity = self.calculate_complexity_score(analysis);
321        let coupling = analysis.dependency_analysis.avg_degree;
322        
323        let base_score = 10.0;
324        let complexity_penalty = complexity * 0.5;
325        let coupling_penalty = coupling * 0.3;
326        
327        (base_score - complexity_penalty - coupling_penalty).max(0.0)
328    }
329
330    fn calculate_complexity_distribution(&self, analysis: &ProjectAnalysis) -> Vec<ComplexityBucket> {
331        let mut buckets = vec![
332            ComplexityBucket { range: "0-5".to_string(), count: 0, percentage: 0.0 },
333            ComplexityBucket { range: "6-15".to_string(), count: 0, percentage: 0.0 },
334            ComplexityBucket { range: "16-30".to_string(), count: 0, percentage: 0.0 },
335            ComplexityBucket { range: "31+".to_string(), count: 0, percentage: 0.0 },
336        ];
337
338        for pf in &analysis.parsed_files {
339            let complexity = pf.functions.len() + pf.classes.len() * 2;
340            match complexity {
341                0..=5 => buckets[0].count += 1,
342                6..=15 => buckets[1].count += 1,
343                16..=30 => buckets[2].count += 1,
344                _ => buckets[3].count += 1,
345            }
346        }
347
348        let total = analysis.parsed_files.len() as f64;
349        for bucket in &mut buckets {
350            bucket.percentage = (bucket.count as f64 / total) * 100.0;
351        }
352
353        buckets
354    }
355
356    pub fn export_report(&self, report: &Report, output_dir: &PathBuf) -> Result<Vec<PathBuf>> {
357        fs::create_dir_all(output_dir)?;
358        let mut exported_files = Vec::new();
359
360        // Export JSON report
361        let json_path = output_dir.join("analysis_report.json");
362        let json_content = serde_json::to_string_pretty(report)?;
363        fs::write(&json_path, json_content)?;
364        exported_files.push(json_path);
365
366        // Export HTML report
367        let html_path = output_dir.join("analysis_report.html");
368        let html_content = self.generate_html_report(report)?;
369        fs::write(&html_path, html_content)?;
370        exported_files.push(html_path);
371
372        // Export Markdown summary
373        let md_path = output_dir.join("analysis_summary.md");
374        let md_content = self.generate_markdown_summary(report)?;
375        fs::write(&md_path, md_content)?;
376        exported_files.push(md_path);
377
378        Ok(exported_files)
379    }
380
381    fn generate_html_report(&self, report: &Report) -> Result<String> {
382        let html = format!(
383            r#"<!DOCTYPE html>
384<html lang="en">
385<head>
386    <meta charset="UTF-8">
387    <meta name="viewport" content="width=device-width, initial-scale=1.0">
388    <title>Project Analysis Report - {}</title>
389    <style>
390        body {{ font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }}
391        .header {{ border-bottom: 2px solid #333; padding-bottom: 20px; }}
392        .section {{ margin: 30px 0; }}
393        .metric {{ display: inline-block; margin: 10px 20px 10px 0; padding: 10px; background: #f5f5f5; border-radius: 5px; }}
394        .recommendation {{ margin: 15px 0; padding: 15px; border-left: 4px solid #007acc; background: #f9f9f9; }}
395        .priority-high {{ border-left-color: #ff6b6b; }}
396        .priority-medium {{ border-left-color: #ffa500; }}
397        .priority-low {{ border-left-color: #28a745; }}
398        .insight {{ margin: 10px 0; padding: 10px; background: #e8f4f8; border-radius: 5px; }}
399        .insight-title {{ font-weight: bold; color: #2c3e50; }}
400        .insight-category {{ color: #7f8c8d; font-size: 0.9em; text-transform: uppercase; }}
401        .evidence {{ margin: 5px 0; font-style: italic; color: #555; }}
402        .llm-analysis {{ margin: 20px 0; padding: 20px; background: #f8f9fa; border-radius: 8px; }}
403        .analysis-type {{ font-weight: bold; color: #495057; margin-bottom: 10px; }}
404        .analysis-summary {{ margin: 10px 0; padding: 15px; background: #fff; border-radius: 5px; line-height: 1.6; }}
405        .insights-table, .recommendations-table {{ margin: 15px 0; }}
406        .insights-table th {{ background-color: #e3f2fd; }}
407        .recommendations-table th {{ background-color: #f3e5f5; }}
408        table {{ border-collapse: collapse; width: 100%; margin: 10px 0; }}
409        th, td {{ border: 1px solid #ddd; padding: 12px; text-align: left; vertical-align: top; }}
410        th {{ background-color: #f2f2f2; font-weight: bold; }}
411        .priority-high {{ background-color: #ffebee; }}
412        .priority-medium {{ background-color: #fff3e0; }}
413        .priority-low {{ background-color: #f1f8e9; }}
414        .confidence-high {{ color: #2e7d32; font-weight: bold; }}
415        .confidence-medium {{ color: #f57c00; font-weight: bold; }}
416        .confidence-low {{ color: #d32f2f; font-weight: bold; }}
417        ol {{ list-style-type: decimal; padding-left: 25px; margin: 10px 0; }}
418        ul {{ list-style-type: disc; padding-left: 25px; margin: 10px 0; }}
419        li {{ margin: 8px 0; line-height: 1.4; }}
420        .analysis-summary ul {{ margin: 15px 0; }}
421        .analysis-summary ol {{ margin: 15px 0; }}
422        .analysis-summary li {{ margin: 6px 0; padding-left: 5px; }}
423        .analysis-summary h4 {{ margin: 20px 0 10px 0; color: #2c3e50; }}
424        .analysis-summary h3 {{ margin: 25px 0 15px 0; color: #34495e; }}
425        .analysis-summary p {{ margin: 12px 0; line-height: 1.6; }}
426    </style>
427    <script>
428        function parseJsonContent(jsonText) {{
429            try {{
430                const data = JSON.parse(jsonText);
431                let html = '';
432                
433                // Analysis summary
434                if (data.analysis) {{
435                    html += `<div class="analysis-summary">${{data.analysis}}</div>`;
436                }}
437                
438                // Insights table
439                if (data.insights && data.insights.length > 0) {{
440                    html += `
441                    <h4>Key Insights</h4>
442                    <table class="insights-table">
443                        <thead>
444                            <tr>
445                                <th>Insight</th>
446                                <th>Category</th>
447                                <th>Description</th>
448                                <th>Confidence</th>
449                                <th>Evidence</th>
450                            </tr>
451                        </thead>
452                        <tbody>`;
453                    
454                    data.insights.forEach(insight => {{
455                        const confidenceClass = insight.confidence >= 0.8 ? 'confidence-high' : 
456                                               insight.confidence >= 0.6 ? 'confidence-medium' : 'confidence-low';
457                        const evidence = insight.evidence && insight.evidence.length > 0 ? 
458                                        '• ' + insight.evidence.join('<br>• ') : 'No specific evidence';
459                        
460                        html += `
461                        <tr>
462                            <td><strong>${{insight.title}}</strong></td>
463                            <td>${{insight.category}}</td>
464                            <td>${{insight.description}}</td>
465                            <td class="${{confidenceClass}}">${{Math.round(insight.confidence * 100)}}%</td>
466                            <td>${{evidence}}</td>
467                        </tr>`;
468                    }});
469                    
470                    html += '</tbody></table>';
471                }}
472                
473                // Recommendations table
474                if (data.recommendations && data.recommendations.length > 0) {{
475                    html += `
476                    <h4>Recommendations</h4>
477                    <table class="recommendations-table">
478                        <thead>
479                            <tr>
480                                <th>Title</th>
481                                <th>Description</th>
482                                <th>Priority</th>
483                                <th>Effort</th>
484                                <th>Impact</th>
485                                <th>Action Items</th>
486                            </tr>
487                        </thead>
488                        <tbody>`;
489                    
490                    data.recommendations.forEach(rec => {{
491                        const priorityClass = rec.priority === 'High' || rec.priority === 'Critical' ? 'priority-high' :
492                                             rec.priority === 'Medium' ? 'priority-medium' : 'priority-low';
493                        const actionItems = rec.action_items && rec.action_items.length > 0 ? 
494                                           '• ' + rec.action_items.join('<br>• ') : 'No specific actions';
495                        
496                        html += `
497                        <tr class="${{priorityClass}}">
498                            <td><strong>${{rec.title}}</strong></td>
499                            <td>${{rec.description}}</td>
500                            <td>${{rec.priority}}</td>
501                            <td>${{rec.effort}}</td>
502                            <td>${{rec.impact}}</td>
503                            <td>${{actionItems}}</td>
504                        </tr>`;
505                    }});
506                    
507                    html += '</tbody></table>';
508                }}
509                
510                return html;
511            }} catch (e) {{
512                return `<p>Error parsing JSON content: ${{e.message}}</p>`;
513            }}
514        }}
515        
516        function parseMarkdownContent(markdown) {{
517            let html = markdown;
518            
519            // Convert headers first
520            html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
521            html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
522            html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
523            html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
524            
525            // Convert bold text
526            html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
527            
528            // Process line by line for better list handling
529            let lines = html.split('\n');
530            let processedLines = [];
531            let inUnorderedList = false;
532            let inOrderedList = false;
533            
534            for (let i = 0; i < lines.length; i++) {{
535                let line = lines[i];
536                let trimmedLine = line.trim();
537                
538                // Look ahead to see if there are more list items coming
539                function hasMoreListItems(startIndex, listType) {{
540                    for (let j = startIndex + 1; j < lines.length; j++) {{
541                        let nextTrimmed = lines[j].trim();
542                        if (nextTrimmed === '') continue; // Skip empty lines
543                        
544                        if (listType === 'ordered' && nextTrimmed.match(/^\d+\.\s+/)) {{
545                            return true;
546                        }}
547                        if (listType === 'unordered' && nextTrimmed.match(/^[-*]\s+/)) {{
548                            return true;
549                        }}
550                        
551                        // Stop looking if we hit a header or substantial content
552                        if (nextTrimmed.match(/^<h[1-6]>/) || 
553                            nextTrimmed.match(/^### /) || 
554                            nextTrimmed.match(/^## /) ||
555                            nextTrimmed.match(/^#### /) ||
556                            (nextTrimmed.length > 0 && !nextTrimmed.match(/^[-*\d]\s*/) && !nextTrimmed.match(/^\d+\.\s+/))) {{
557                            break;
558                        }}
559                    }}
560                    return false;
561                }}
562                
563                // Handle unordered list items
564                if (trimmedLine.match(/^[-*]\s+/)) {{
565                    if (!inUnorderedList) {{
566                        if (inOrderedList) {{
567                            processedLines.push('</ol>');
568                            inOrderedList = false;
569                        }}
570                        processedLines.push('<ul>');
571                        inUnorderedList = true;
572                    }}
573                    let content = trimmedLine.replace(/^[-*]\s+/, '');
574                    processedLines.push(`<li>${{content}}</li>`);
575                    
576                    // Only close if no more unordered items are coming
577                    if (!hasMoreListItems(i, 'unordered')) {{
578                        processedLines.push('</ul>');
579                        inUnorderedList = false;
580                    }}
581                }}
582                // Handle ordered list items (1. 2. 3. etc.)
583                else if (trimmedLine.match(/^\d+\.\s+/)) {{
584                    if (!inOrderedList) {{
585                        if (inUnorderedList) {{
586                            processedLines.push('</ul>');
587                            inUnorderedList = false;
588                        }}
589                        processedLines.push('<ol>');
590                        inOrderedList = true;
591                    }}
592                    let content = trimmedLine.replace(/^\d+\.\s+/, '');
593                    processedLines.push(`<li>${{content}}</li>`);
594                    
595                    // Only close if no more ordered items are coming
596                    if (!hasMoreListItems(i, 'ordered')) {{
597                        processedLines.push('</ol>');
598                        inOrderedList = false;
599                    }}
600                }}
601                // Handle regular content
602                else {{
603                    // Close lists when we encounter headers or substantial content
604                    if (trimmedLine && (trimmedLine.startsWith('<h') || 
605                        trimmedLine.match(/^### /) || 
606                        trimmedLine.match(/^## /) ||
607                        trimmedLine.match(/^#### /))) {{
608                        // Close any open lists when we hit headers
609                        if (inUnorderedList) {{
610                            processedLines.push('</ul>');
611                            inUnorderedList = false;
612                        }}
613                        if (inOrderedList) {{
614                            processedLines.push('</ol>');
615                            inOrderedList = false;
616                        }}
617                        processedLines.push(line);
618                    }} else if (trimmedLine && !trimmedLine.startsWith('<ul') && !trimmedLine.startsWith('<ol') && !trimmedLine.startsWith('</')) {{
619                        // Close lists for substantial paragraph content
620                        if (inUnorderedList) {{
621                            processedLines.push('</ul>');
622                            inUnorderedList = false;
623                        }}
624                        if (inOrderedList) {{
625                            processedLines.push('</ol>');
626                            inOrderedList = false;
627                        }}
628                        processedLines.push(`<p>${{line}}</p>`);
629                    }} else {{
630                        // Empty lines and HTML elements - keep them without closing lists
631                        processedLines.push(line);
632                    }}
633                }}
634            }}
635            
636            // Close any remaining open lists
637            if (inUnorderedList) {{
638                processedLines.push('</ul>');
639            }}
640            if (inOrderedList) {{
641                processedLines.push('</ol>');
642            }}
643            
644            return processedLines.join('\n');
645        }}
646        
647        document.addEventListener('DOMContentLoaded', function() {{
648            // Process JSON content in any element that contains JSON
649            function processElementForJson(element) {{
650                const text = element.textContent || element.innerText;
651                if (text.trim().startsWith('```json') && text.trim().endsWith('```')) {{
652                    const jsonContent = text.trim().slice(7, -3); // Remove ```json and ```
653                    const processedHtml = parseJsonContent(jsonContent);
654                    element.innerHTML = processedHtml;
655                    element.style.whiteSpace = 'normal';
656                    return true;
657                }} else if (text.trim().startsWith('{{') && text.trim().endsWith('}}')) {{
658                    // Direct JSON content without markdown code blocks
659                    try {{
660                        const processedHtml = parseJsonContent(text.trim());
661                        element.innerHTML = processedHtml;
662                        element.style.whiteSpace = 'normal';
663                        return true;
664                    }} catch (e) {{
665                        // Not valid JSON, continue to markdown processing
666                    }}
667                }}
668                return false;
669            }}
670            
671            // Process all potential JSON containers
672            document.querySelectorAll('p, div, .analysis-summary, .llm-analysis div').forEach(element => {{
673                if (processElementForJson(element)) {{
674                    return; // Successfully processed as JSON
675                }}
676                
677                // If not JSON, try markdown processing
678                const text = element.textContent || element.innerText;
679                if (text.includes('###') || text.includes('**') || text.includes('- ') || text.includes('#### ')) {{
680                    const processedHtml = parseMarkdownContent(text);
681                    element.innerHTML = processedHtml;
682                    element.style.whiteSpace = 'normal';
683                }}
684            }});
685        }});
686    </script>
687</head>
688<body>
689    <div class="header">
690        <h1>Project Analysis Report</h1>
691        <p><strong>Project:</strong> {}</p>
692        <p><strong>Generated:</strong> {}</p>
693        <p><strong>Analysis Duration:</strong> {}ms</p>
694        <p><strong>LLM Model:</strong> {} ({})</p>
695    </div>
696    
697    <div class="section">
698        <h2>Executive Summary</h2>
699        <div class="metric">
700            <strong>Complexity Score:</strong> {:.2}
701        </div>
702        <div class="metric">
703            <strong>Maintainability Score:</strong> {:.2}
704        </div>
705        <div class="metric">
706            <strong>Total Files:</strong> {}
707        </div>
708        <div class="metric">
709            <strong>Total Size:</strong> {:.2} MB
710        </div>
711        <p>{}</p>
712    </div>
713
714    <div class="section">
715        <h2>Key Recommendations</h2>
716        {}
717    </div>
718
719    <div class="section">
720        <h2>LLM Analysis & Insights</h2>
721        {}
722    </div>
723
724    <div class="section">
725        <h2>File Analysis</h2>
726        <h3>Language Distribution</h3>
727        <table>
728            <tr><th>Language</th><th>Files</th><th>Size (MB)</th><th>Percentage</th></tr>
729            {}
730        </table>
731    </div>
732
733</body>
734</html>"#,
735            report.metadata.project_name,
736            report.metadata.project_name,
737            report.metadata.generated_at,
738            report.metadata.analysis_duration_ms,
739            report.metadata.llm_model,
740            report.metadata.llm_provider,
741            report.executive_summary.complexity_score,
742            report.executive_summary.maintainability_score,
743            report.metadata.total_files,
744            report.metadata.total_size as f64 / (1024.0 * 1024.0),
745            report.executive_summary.overview,
746            report.recommendations.iter().take(5).map(|r| {
747                let priority_class = match r.priority {
748                    Priority::High | Priority::Critical => "priority-high",
749                    Priority::Medium => "priority-medium",
750                    Priority::Low => "priority-low",
751                };
752                format!(r#"<div class="recommendation {}"><strong>{}</strong><p>{}</p></div>"#, 
753                    priority_class, r.title, r.description)
754            }).collect::<Vec<_>>().join("\n"),
755            self.generate_llm_insights_html(&report.llm_insights),
756            report.file_analysis.language_breakdown.iter().map(|l| {
757                format!("<tr><td>{}</td><td>{}</td><td>{:.2}</td><td>{:.1}%</td></tr>",
758                    l.language, l.file_count, l.total_size as f64 / (1024.0 * 1024.0), l.percentage)
759            }).collect::<Vec<_>>().join("\n")
760        );
761
762        Ok(html)
763    }
764
765    fn generate_llm_insights_html(&self, llm_insights: &[AnalysisResponse]) -> String {
766        if llm_insights.is_empty() {
767            return "<p>No LLM analysis was performed for this project.</p>".to_string();
768        }
769
770        let mut html = String::new();
771        
772        for (index, analysis) in llm_insights.iter().enumerate() {
773            // Determine analysis type from position (based on analyzer.rs order)
774            let analysis_type = match index {
775                0 => "Overview",
776                1 => "Architecture", 
777                2 => "Dependencies",
778                3 => "Security",
779                4 => "Refactoring",
780                5 => "Documentation",
781                _ => "Additional Analysis",
782            };
783
784            html.push_str(&format!(r#"<div class="llm-analysis">
785                <div class="analysis-type">{} Analysis</div>"#, analysis_type));
786
787            // Extract and display the main analysis summary
788            let analysis_text = self.extract_analysis_text(&analysis.analysis);
789            html.push_str(&format!(r#"<div class="analysis-summary">{}</div>"#, analysis_text));
790
791            // Extract insights and display in table format
792            let insights = if !analysis.insights.is_empty() {
793                analysis.insights.clone()
794            } else {
795                self.extract_insights_from_text(&analysis.analysis)
796            };
797
798            if !insights.is_empty() {
799                html.push_str(r#"<h4>Key Insights</h4>
800                <table class="insights-table">
801                    <thead>
802                        <tr>
803                            <th>Insight</th>
804                            <th>Category</th>
805                            <th>Description</th>
806                            <th>Confidence</th>
807                            <th>Evidence</th>
808                        </tr>
809                    </thead>
810                    <tbody>"#);
811
812                for insight in &insights {
813                    let confidence_class = if insight.confidence >= 0.8 {
814                        "confidence-high"
815                    } else if insight.confidence >= 0.6 {
816                        "confidence-medium"
817                    } else {
818                        "confidence-low"
819                    };
820                    
821                    let evidence_text = if insight.evidence.is_empty() {
822                        "No specific evidence".to_string()
823                    } else {
824                        insight.evidence.join("<br>• ")
825                    };
826
827                    html.push_str(&format!(r#"<tr>
828                        <td><strong>{}</strong></td>
829                        <td>{:?}</td>
830                        <td>{}</td>
831                        <td class="{}">{:.0}%</td>
832                        <td>• {}</td>
833                    </tr>"#, 
834                    insight.title, insight.category, insight.description, 
835                    confidence_class, insight.confidence * 100.0, evidence_text));
836                }
837                
838                html.push_str("</tbody></table>");
839            }
840
841            // Extract recommendations and display in table format
842            let recommendations = if !analysis.recommendations.is_empty() {
843                analysis.recommendations.clone()
844            } else {
845                self.extract_recommendations_from_text(&analysis.analysis)
846            };
847
848            if !recommendations.is_empty() {
849                html.push_str(r#"<h4>Recommendations</h4>
850                <table class="recommendations-table">
851                    <thead>
852                        <tr>
853                            <th>Title</th>
854                            <th>Description</th>
855                            <th>Priority</th>
856                            <th>Effort</th>
857                            <th>Impact</th>
858                            <th>Action Items</th>
859                        </tr>
860                    </thead>
861                    <tbody>"#);
862
863                for recommendation in &recommendations {
864                    let priority_class = match recommendation.priority {
865                        Priority::High | Priority::Critical => "priority-high",
866                        Priority::Medium => "priority-medium",
867                        Priority::Low => "priority-low",
868                    };
869                    
870                    let action_items_text = if recommendation.action_items.is_empty() {
871                        "No specific actions".to_string()
872                    } else {
873                        recommendation.action_items.join("<br>• ")
874                    };
875
876                    html.push_str(&format!(r#"<tr class="{}">
877                        <td><strong>{}</strong></td>
878                        <td>{}</td>
879                        <td>{:?}</td>
880                        <td>{:?}</td>
881                        <td>{:?}</td>
882                        <td>• {}</td>
883                    </tr>"#, 
884                    priority_class, recommendation.title, recommendation.description,
885                    recommendation.priority, recommendation.effort, recommendation.impact,
886                    action_items_text));
887                }
888                
889                html.push_str("</tbody></table>");
890            }
891
892            html.push_str("</div>");
893        }
894
895        html
896    }
897
898    fn extract_analysis_text(&self, content: &str) -> String {
899        // First try to parse as JSON and extract the analysis field
900        if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(content) {
901            if let Some(analysis_text) = json_value.get("analysis").and_then(|v| v.as_str()) {
902                return analysis_text.to_string();
903            }
904        }
905
906        // Check if content is wrapped in JSON code blocks
907        if content.trim().starts_with("```json") && content.trim().ends_with("```") {
908            let json_content = content.trim()
909                .strip_prefix("```json").unwrap_or(content)
910                .strip_suffix("```").unwrap_or(content)
911                .trim();
912            
913            if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_content) {
914                if let Some(analysis_text) = json_value.get("analysis").and_then(|v| v.as_str()) {
915                    return analysis_text.to_string();
916                }
917            }
918        }
919
920        // If not JSON, return the content as is (cleaning up markdown if needed)
921        content.to_string()
922    }
923
924
925    fn extract_insights_from_text(&self, text: &str) -> Vec<crate::llm::Insight> {
926        // Try to parse JSON and extract insights
927        if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(text) {
928            if let Some(insights_array) = json_value.get("insights").and_then(|v| v.as_array()) {
929                let mut insights = Vec::new();
930                for insight_json in insights_array {
931                    if let Ok(insight) = serde_json::from_value::<crate::llm::Insight>(insight_json.clone()) {
932                        insights.push(insight);
933                    }
934                }
935                return insights;
936            }
937        }
938
939        // Try to parse JSON wrapped in code blocks
940        if text.trim().starts_with("```json") && text.trim().ends_with("```") {
941            let json_content = text.trim()
942                .strip_prefix("```json").unwrap_or(text)
943                .strip_suffix("```").unwrap_or(text)
944                .trim();
945            
946            if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_content) {
947                if let Some(insights_array) = json_value.get("insights").and_then(|v| v.as_array()) {
948                    let mut insights = Vec::new();
949                    for insight_json in insights_array {
950                        if let Ok(insight) = serde_json::from_value::<crate::llm::Insight>(insight_json.clone()) {
951                            insights.push(insight);
952                        }
953                    }
954                    return insights;
955                }
956            }
957        }
958
959        Vec::new()
960    }
961
962    fn extract_recommendations_from_text(&self, text: &str) -> Vec<crate::llm::Recommendation> {
963        // Try to parse JSON and extract recommendations
964        if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(text) {
965            if let Some(recommendations_array) = json_value.get("recommendations").and_then(|v| v.as_array()) {
966                let mut recommendations = Vec::new();
967                for rec_json in recommendations_array {
968                    if let Ok(recommendation) = serde_json::from_value::<crate::llm::Recommendation>(rec_json.clone()) {
969                        recommendations.push(recommendation);
970                    }
971                }
972                return recommendations;
973            }
974        }
975
976        // Try to parse JSON wrapped in code blocks
977        if text.trim().starts_with("```json") && text.trim().ends_with("```") {
978            let json_content = text.trim()
979                .strip_prefix("```json").unwrap_or(text)
980                .strip_suffix("```").unwrap_or(text)
981                .trim();
982            
983            if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_content) {
984                if let Some(recommendations_array) = json_value.get("recommendations").and_then(|v| v.as_array()) {
985                    let mut recommendations = Vec::new();
986                    for rec_json in recommendations_array {
987                        if let Ok(recommendation) = serde_json::from_value::<crate::llm::Recommendation>(rec_json.clone()) {
988                            recommendations.push(recommendation);
989                        }
990                    }
991                    return recommendations;
992                }
993            }
994        }
995
996        Vec::new()
997    }
998
999    fn generate_markdown_summary(&self, report: &Report) -> Result<String> {
1000        let mut md = format!(
1001            "# Project Analysis Summary\n\n**Project:** {}\n**Generated:** {}\n**Analysis Duration:** {}ms\n\n",
1002            report.metadata.project_name,
1003            report.metadata.generated_at,
1004            report.metadata.analysis_duration_ms
1005        );
1006
1007        md.push_str("## Executive Summary\n\n");
1008        md.push_str(&format!("- **Complexity Score:** {:.2}/10\n", report.executive_summary.complexity_score));
1009        md.push_str(&format!("- **Maintainability Score:** {:.2}/10\n", report.executive_summary.maintainability_score));
1010        md.push_str(&format!("- **Total Files:** {}\n", report.metadata.total_files));
1011        md.push_str(&format!("- **Total Size:** {:.2} MB\n\n", report.metadata.total_size as f64 / (1024.0 * 1024.0)));
1012
1013        md.push_str("## Top Recommendations\n\n");
1014        for (i, rec) in report.recommendations.iter().take(5).enumerate() {
1015            md.push_str(&format!("{}. **{}** (Priority: {:?})\n   {}\n\n", 
1016                i + 1, rec.title, rec.priority, rec.description));
1017        }
1018
1019        md.push_str("## Language Distribution\n\n");
1020        for lang in &report.file_analysis.language_breakdown {
1021            md.push_str(&format!("- **{}:** {} files ({:.1}%), {:.2} MB\n", 
1022                lang.language, lang.file_count, lang.percentage, lang.total_size as f64 / (1024.0 * 1024.0)));
1023        }
1024
1025        Ok(md)
1026    }
1027}