package services import ( "bytes" "context" "encoding/json" "fmt" "html/template" "os" "time" "github.com/HoHD/PR-Helper/models" "github.com/chromedp/chromedp" "github.com/chromedp/cdproto/page" ) // ReportData holds all data needed to render the PDF report template. type ReportData struct { RepoURL string BaseRef string HeadRef string ReviewedAt string AnalysisID int64 Result string // raw JSON from analysis Notes []models.ReviewNote } // ParsedReview holds the structured review data for the template. type ParsedReview struct { Score int Overall string Findings string Recommendations string FileReviews []FileReviewForReport } // FileReviewForReport is a file review entry formatted for the report template. type FileReviewForReport struct { FileName string ChangeLines int Suggestions []SuggestionForReport Notes []string } // SuggestionForReport is a single suggestion formatted for the report template. type SuggestionForReport struct { Severity string SeverityCN string Description string Suggestion string CodeExample string Notes []string } // parseReportData converts raw analysis JSON + notes into template-ready structures. func parseReportData(data ReportData) ParsedReview { result := ParsedReview{} // Index notes by scope:scopeKey noteMap := make(map[string][]string) for _, n := range data.Notes { key := n.Scope + ":" + n.ScopeKey noteMap[key] = append(noteMap[key], n.Content) } // Try to parse structured analysis result (new ReviewResult format) var reviewResult ReviewResult if err := json.Unmarshal([]byte(data.Result), &reviewResult); err == nil && len(reviewResult.FileReviews) > 0 { // New format: { file_reviews: [...], summary: {...}, top_n: N } result.Score = reviewResult.Summary.Score result.Overall = reviewResult.Summary.Overall result.Findings = reviewResult.Summary.Findings result.Recommendations = reviewResult.Summary.Recommendations for _, fr := range reviewResult.FileReviews { fileReport := FileReviewForReport{ FileName: fr.FileName, ChangeLines: fr.ChangeLines, } // Collect file-level notes fileReport.Notes = noteMap["file:"+fr.FileName] for _, s := range fr.Suggestions { sug := SuggestionForReport{ Severity: s.Severity, SeverityCN: severityCN(s.Severity), Description: s.Description, Suggestion: s.Suggestion, CodeExample: s.CodeExample, } fileReport.Suggestions = append(fileReport.Suggestions, sug) } result.FileReviews = append(result.FileReviews, fileReport) } return result } // Fallback: try legacy flat format { score, overall, findings, recommendations } var analysisMap map[string]interface{} if err := json.Unmarshal([]byte(data.Result), &analysisMap); err == nil { if score, ok := analysisMap["score"].(float64); ok { result.Score = int(score) } if overall, ok := analysisMap["overall"].(string); ok { result.Overall = overall } if findings, ok := analysisMap["findings"].(string); ok { result.Findings = findings } if recs, ok := analysisMap["recommendations"].(string); ok { result.Recommendations = recs } } return result } // severityCN returns the Chinese label for a severity level. func severityCN(severity string) string { switch severity { case "critical": return "严重" case "warning": return "建议" case "info": return "提示" default: return "提示" } } // GeneratePDFReport generates a PDF from the review report data using chromedp. func GeneratePDFReport(data ReportData) ([]byte, error) { review := parseReportData(data) // Collect overall notes var overallNotes []string for _, n := range data.Notes { if n.Scope == "overall" { overallNotes = append(overallNotes, n.Content) } } // Build template data tmplData := struct { RepoURL string BaseRef string HeadRef string ReviewedAt string Score int Overall string Findings string Recommendations string FileReviews []FileReviewForReport OverallNotes []string Result string }{ RepoURL: data.RepoURL, BaseRef: data.BaseRef, HeadRef: data.HeadRef, ReviewedAt: data.ReviewedAt, Score: review.Score, Overall: review.Overall, Findings: review.Findings, Recommendations: review.Recommendations, FileReviews: review.FileReviews, OverallNotes: overallNotes, Result: data.Result, } // Render HTML from template tmpl, err := template.ParseFiles("templates/reports/review.html") if err != nil { return nil, fmt.Errorf("parse template: %w", err) } var htmlBuf bytes.Buffer if err := tmpl.Execute(&htmlBuf, tmplData); err != nil { return nil, fmt.Errorf("execute template: %w", err) } // Write HTML to temp file for chromedp tmpFile, err := os.CreateTemp("", "pr-helper-report-*.html") if err != nil { return nil, fmt.Errorf("create temp file: %w", err) } defer os.Remove(tmpFile.Name()) if _, err := tmpFile.Write(htmlBuf.Bytes()); err != nil { tmpFile.Close() return nil, fmt.Errorf("write temp file: %w", err) } tmpFile.Close() // Use chromedp to convert HTML to PDF ctx, cancel := chromedp.NewContext(context.Background()) defer cancel() ctx, cancel = context.WithTimeout(ctx, 30*time.Second) defer cancel() var pdfBytes []byte fileURL := "file://" + tmpFile.Name() err = chromedp.Run(ctx, chromedp.Navigate(fileURL), chromedp.WaitReady("body"), chromedp.ActionFunc(func(ctx context.Context) error { var err error pdfBytes, _, err = page.PrintToPDF(). WithDisplayHeaderFooter(false). WithPrintBackground(true). Do(ctx) return err }), ) if err != nil { return nil, fmt.Errorf("chromedp: %w", err) } return pdfBytes, nil }