"""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()