"""Generate PDF booklet for Trifecta-Bro predictions.""" from __future__ import annotations from pathlib import Path from typing import Any 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, Table, TableStyle, Paragraph, Spacer from src.prediction_model import Race, Runner, TrifectaPredictor TITLE = "Trifecta-Bro" SUBTITLE = "Australian Gallops Prediction Booklet" def build_booklet(races: list[dict[str, Any]], output_path: Path) -> Path: predictor = TrifectaPredictor() doc = SimpleDocTemplate( str(output_path), pagesize=A4, title=TITLE, author="Trifecta-Bro", ) styles = getSampleStyleSheet() title_style = ParagraphStyle( "TBTitle", parent=styles["Title"], fontSize=22, textColor=colors.HexColor("#111827"), spaceAfter=4, ) subtitle_style = ParagraphStyle( "TBSubtitle", parent=styles["Normal"], fontSize=10, textColor=colors.HexColor("#6B7280"), spaceAfter=12, ) heading_style = ParagraphStyle( "TBHeading", parent=styles["Heading2"], fontSize=14, textColor=colors.HexColor("#0F172A"), spaceAfter=6, ) body_style = ParagraphStyle( "TBBody", parent=styles["Normal"], fontSize=9, leading=12, ) story: list[Any] = [ Paragraph(TITLE, title_style), Paragraph(SUBTITLE, subtitle_style), Spacer(1, 4 * mm), ] for race_payload in races: race = _to_race(race_payload) story.append(Paragraph(f"Race {race.race_number}: {race.race_name}", heading_style)) meta = Table( [ ["Distance", race.distance, "Condition", race.condition], ["Weather", race.weather or "", "Class", race.race_class], ["Prize", f"${race.prize_money}", "Runners", str(race.number_of_runners)], ], colWidths=[28 * mm, 45 * mm, 28 * mm, 45 * mm], ) meta.setStyle(TableStyle([ ("FONTNAME", (0, 0), (-1, -1), "Helvetica"), ("FONTSIZE", (0, 0), (-1, -1), 8), ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#F3F4F6")), ("BACKGROUND", (2, 0), (2, -1), colors.HexColor("#F3F4F6")), ("LEFTPADDING", (0, 0), (-1, -1), 6), ("RIGHTPADDING", (0, 0), (-1, -1), 6), ("TOPPADDING", (0, 0), (-1, -1), 4), ("BOTTOMPADDING", (0, 0), (-1, -1), 4), ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#E5E7EB")), ])) story.append(meta) story.append(Spacer(1, 4 * mm)) prediction = predictor.predict(race) if "error" in prediction: story.append(Paragraph(f"Prediction unavailable: {prediction['error']}", body_style)) story.append(Spacer(1, 6 * mm)) continue trifecta_table = Table( [ ["Primary", prediction.get("primary", "")], ["Secondary", prediction.get("secondary") or "-"], ["Value", prediction.get("value") or "-"], ], colWidths=[32 * mm, 114 * mm], ) trifecta_table.setStyle(TableStyle([ ("FONTNAME", (0, 0), (-1, -1), "Helvetica-Bold"), ("FONTSIZE", (0, 0), (-1, -1), 9), ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#0F172A")), ("TEXTCOLOR", (0, 0), (0, -1), colors.white), ("LEFTPADDING", (0, 0), (-1, -1), 6), ("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5), ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#E5E7EB")), ])) story.append(trifecta_table) story.append(Spacer(1, 4 * mm)) runner_rows = [[ "#", "Horse", "Form", "Score", "Win%", "Place%", "Bar", "Jockey" ]] for item in prediction.get("top3", []): runner_rows.append([ str(item["number"]), item["name"], "", str(item["score"]), f"{item['win_prob']:.0f}%", f"{item['place_prob']:.0f}%", "", "", ]) runner_table = Table(runner_rows, repeatRows=1) runner_table.setStyle(TableStyle([ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), ("FONTSIZE", (0, 0), (-1, -1), 8), ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#F3F4F6")), ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#E5E7EB")), ("LEFTPADDING", (0, 0), (-1, -1), 5), ("TOPPADDING", (0, 0), (-1, -1), 4), ("BOTTOMPADDING", (0, 0), (-1, -1), 4), ])) story.append(runner_table) story.append(Spacer(1, 6 * mm)) doc.build(story) return output_path def _to_race(payload: dict[str, Any]) -> Race: form = payload.get("form", {}) runners = [] for r in form.get("runners", []): runners.append(Runner( number=r.get("number", 0), name=r.get("name", ""), jockey=r.get("jockey", ""), trainer=r.get("trainer", ""), weight=r.get("weight"), barrier=r.get("barrier"), form=r.get("form", ""), last20Starts=r.get("last20Starts", ""), careerPrizeMoney=r.get("careerPrizeMoney", "$0"), scratched=bool(r.get("scratched", False)), stats=r.get("stats", {}), )) return Race( date=payload.get("date", ""), track=payload.get("track", ""), track_slug=payload.get("track_slug", ""), race_number=str(payload.get("race_number", "")), race_name=payload.get("race_name", ""), distance=payload.get("distance", ""), condition=payload.get("condition", ""), weather=payload.get("weather", ""), race_class=payload.get("race_class", ""), start_time=payload.get("start_time", ""), prize_money=payload.get("prize_money", ""), number_of_runners=int(payload.get("number_of_runners", 0) or 0), runners=runners, )