"""Export caveats report to PDF.""" import io import logging from datetime import datetime from reportlab.lib import colors from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import mm from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.gmal import GmalAsset from app.models.project import Project, ClientAsset, Match logger = logging.getLogger(__name__) CONFIDENCE_COLORS = { "exact": colors.HexColor("#22c55e"), "close": colors.HexColor("#f59e0b"), "multiple": colors.HexColor("#3b82f6"), "none": colors.HexColor("#ef4444"), } async def export_caveats_pdf(db: AsyncSession, project: Project) -> bytes: """Generate a PDF caveats report for a project. Returns the PDF as bytes. """ buf = io.BytesIO() doc = SimpleDocTemplate(buf, pagesize=A4, topMargin=20 * mm, bottomMargin=20 * mm) styles = getSampleStyleSheet() title_style = ParagraphStyle("CustomTitle", parent=styles["Title"], fontSize=18, spaceAfter=12) heading_style = ParagraphStyle("CustomHeading", parent=styles["Heading2"], fontSize=13, spaceAfter=6) body_style = styles["BodyText"] caveat_style = ParagraphStyle("Caveat", parent=body_style, textColor=colors.HexColor("#92400e"), leftIndent=10) elements = [] # Title elements.append(Paragraph(f"Scope Builder - Caveats Report", title_style)) elements.append(Paragraph(f"Project: {project.name}", heading_style)) if project.client_name: elements.append(Paragraph(f"Client: {project.client_name}", body_style)) elements.append(Paragraph(f"Generated: {datetime.utcnow().strftime('%d %B %Y')}", body_style)) elements.append(Spacer(1, 12)) # Load client assets and matches assets_result = await db.execute( select(ClientAsset).where(ClientAsset.project_id == project.id).order_by(ClientAsset.sort_order) ) client_assets = assets_result.scalars().all() for ca in client_assets: matches_result = await db.execute( select(Match).where(Match.client_asset_id == ca.id).order_by(Match.rank) ) matches = matches_result.scalars().all() elements.append(Paragraph(f"{ca.raw_name} (Volume: {ca.volume})", heading_style)) if ca.raw_description: elements.append(Paragraph(f"Client description: {ca.raw_description}", body_style)) elements.append(Spacer(1, 4)) if not matches: elements.append(Paragraph("No matches found.", caveat_style)) elements.append(Spacer(1, 8)) continue for match in matches: # Load GMAL asset gmal_result = await db.execute(select(GmalAsset).where(GmalAsset.id == match.gmal_asset_id)) gmal = gmal_result.scalar_one_or_none() if not gmal: continue selected_marker = " [SELECTED]" if match.is_selected else "" conf_label = match.confidence.value.upper() score_pct = f"{float(match.confidence_score) * 100:.0f}%" if match.confidence_score else "N/A" elements.append(Paragraph( f"Match #{match.rank}: {gmal.gmal_id} - {gmal.unique_name or gmal.asset_name}{selected_marker}", body_style, )) elements.append(Paragraph(f"Confidence: {conf_label} ({score_pct})", body_style)) if match.ai_reasoning: elements.append(Paragraph(f"Reasoning: {match.ai_reasoning}", body_style)) if match.caveat_text: elements.append(Paragraph(f"Caveats:", body_style)) elements.append(Paragraph(match.caveat_text, caveat_style)) elements.append(Spacer(1, 6)) elements.append(Spacer(1, 12)) doc.build(elements) buf.seek(0) return buf.read()