import os
import re
import urllib.request
from uuid import UUID
from sqlalchemy.orm import Session, joinedload, selectinload
from models.job_model import JobService, JobStatus
from pathlib import Path
from datetime import datetime, timezone
from models.project_model import Project
from models.ticket_model import Ticket
from models.user_model import User
from models.comment_model import Comment
from models.activity_log_model import ActivityLog
from models.label_model import TicketLabel, TicketRelease
import tempfile
import asyncio
from utils.send_report_email import send_report_email
from fpdf import FPDF
from fpdf.enums import XPos, YPos

# ── constants ─────────────────────────────────────────────────────────────
LEFT        = 15
LEFT_COL_W  = 115
RIGHT_COL_X = 135

# ── helpers ────────────────────────────────────────────────────────────────
def get_val(enum_field):
    return enum_field.value if hasattr(enum_field, 'value') else enum_field

def get_time_ago(dt_obj):
    if not dt_obj: return ""
    now = datetime.now(timezone.utc) if getattr(dt_obj, 'tzinfo', None) else datetime.now()
    diff = now - dt_obj
    days = diff.days
    if days == 0:   return "Today"
    if days == 1:   return "1d ago"
    if days < 30:   return f"{days}d ago"
    if days < 365:  return f"{days//30}mo ago"
    return f"{days//365}y ago"

def safe_str(val):
    if not val: return ""
    return str(val).encode('latin-1', 'replace').decode('latin-1')

def reset_margins(pdf):
    """Always call this after any multi_cell / write_html block."""
    pdf.set_left_margin(LEFT)
    pdf.set_right_margin(LEFT)
    pdf.set_x(LEFT)

def md_to_plain(text):
    """
    Convert markdown to plain text safe for fpdf helvetica rendering.
    We avoid write_html entirely in the left column — it was the source
    of the stray-line bug because it internally calls set_right_margin.
    """
    if not text: return ""
    text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
    text = re.sub(r'\*(.+?)\*',     r'\1', text)
    text = re.sub(r'~~(.+?)~~',     r'\1', text)
    text = re.sub(r'^#{1,6}\s+',    '',    text, flags=re.MULTILINE)
    text = re.sub(r'^[-*+]\s+',     '  - ',text, flags=re.MULTILINE)
    text = re.sub(r'!\[.*?\]\(.*?\)', '',  text)
    return text.strip()

def draw_pill(pdf, x, y, w, h, text, bg_color, text_color):
    pdf.set_fill_color(*bg_color)
    pdf.rect(x, y, w, h, style='F', round_corners=True, corner_radius=h/2)
    pdf.set_text_color(*text_color)
    pdf.set_xy(x, y + h/2 - 1.5)
    pdf.cell(w, 3, safe_str(text), align='C')

def section_bar(pdf, label, width=LEFT_COL_W):
    """Slate-100 section header bar. Moves cursor 2 mm below bar."""
    sec_y = pdf.get_y()
    pdf.set_fill_color(241, 245, 249)
    pdf.rect(LEFT, sec_y, width, 6, 'F')
    pdf.set_xy(LEFT + 3, sec_y + 1.2)
    pdf.set_font('helvetica', 'B', 6)
    pdf.set_text_color(100, 116, 139)
    pdf.cell(width - 3, 4, label)
    pdf.set_y(sec_y + 8)

def render_text_block(pdf, text, left_indent=3, line_h=4.5):
    """
    Render a plain-text block inside the left column.
    Uses an EXPLICIT width on multi_cell — never touches right_margin.
    This is the core fix for the stray-gradient-lines bug.
    """
    if not text: return
    reset_margins(pdf)
    pdf.set_x(LEFT + left_indent)
    col_w = LEFT_COL_W - left_indent
    pdf.set_font('helvetica', '', 7)
    pdf.set_text_color(51, 65, 85)
    clean = md_to_plain(safe_str(text))
    pdf.multi_cell(col_w, line_h, clean, new_x=XPos.LMARGIN, new_y=YPos.NEXT)
    reset_margins(pdf)

def new_ticket_bg(pdf):
    """Fill slate-50 background and reset margins for a fresh ticket page."""
    pdf.set_fill_color(248, 250, 252)
    pdf.rect(0, 0, 210, 297, 'F')
    reset_margins(pdf)

# ── PDF class ──────────────────────────────────────────────────────────────
class PremiumPDFReport(FPDF):
    def __init__(self, project_json, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.project_json = project_json
        self.set_auto_page_break(auto=True, margin=15)

    def _draw_gradient_line(self):
        steps = 210
        for i in range(steps):
            r = int(255 * i / steps)
            g = int(255 - 255 * i / steps)
            b = 255
            self.set_fill_color(r, g, b)
            self.rect(i * (210 / steps), 0, (210 / steps), 1.5, 'F')

    def header(self):
        if self.page_no() > 1:
            self._draw_gradient_line()
            self.set_font('helvetica', '', 7)
            self.set_text_color(148, 163, 184)
            title = "Project Summary" if self.page_no() == 2 else "Tickets Details"
            self.set_xy(15, 10)
            self.cell(0, 5, safe_str(f"{self.page_no():02d} · {title}"))
            self.set_xy(15, 10)
            self.set_text_color(99, 102, 241)
            self.cell(0, 5, safe_str(self.project_json["project"]["key"]), align='R')
            self.ln(15)
            # IMPORTANT: reset margins after header — prevents contamination
            reset_margins(self)

    def footer(self):
        if self.page_no() > 1:
            self.set_y(-15)
            self.set_font('helvetica', '', 7)
            self.set_text_color(203, 213, 225)
            self.cell(0, 10, "Phoenix Platform v2.0 · Confidential")
            self.set_xy(15, -15)
            self.cell(0, 10, str(self.page_no()), align='R')

# ── main function ──────────────────────────────────────────────────────────
def generate_project_report_pdf(
    project_id: UUID,
    requester_email: str,
    job_id: UUID,
    db: Session,
    report_name: str = None,
    statuses: list = None,
    assignee_ids: list = None,
    reporter_ids: list = None,
    priorities: list = None,
    types: list = None,
    label_ids: list = None,
    release_names: list = None
):
    get_job_details = None
    if job_id:
        get_job_details = db.query(JobService).filter(JobService.id == job_id).first()
        if get_job_details:
            get_job_details.status = JobStatus.IN_PROGRESS
            db.commit()

    try:
        project = db.query(Project).options(joinedload(Project.creator)).filter(Project.id == project_id).first()
        if not project:
            return

        query = db.query(Ticket).options(
            joinedload(Ticket.reporter),
            selectinload(Ticket.comments).joinedload(Comment.author),
            selectinload(Ticket.activity_logs).joinedload(ActivityLog.actor),
            selectinload(Ticket.ticket_labels).joinedload(TicketLabel.label),
            selectinload(Ticket.ticket_releases).joinedload(TicketRelease.release)
        ).filter(Ticket.project_id == project_id)

        if statuses:      query = query.filter(Ticket.status.in_(statuses))
        if reporter_ids:  query = query.filter(Ticket.reporter_id.in_(reporter_ids))
        if assignee_ids:  query = query.filter(Ticket.assignee_ids.overlap(assignee_ids))
        if priorities:    query = query.filter(Ticket.priority.in_(priorities))
        if types:         query = query.filter(Ticket.type.in_(types))

        tickets = query.all()

        org_users = db.query(User).filter(User.org_id == project.org_id).all()
        user_map  = {user.id: user for user in org_users}

        total_tickets     = len(tickets)
        completed_tickets = sum(1 for t in tickets if get_val(t.status) == 'DONE')
        completion_rate   = round((completed_tickets / total_tickets * 100) if total_tickets > 0 else 0)
        critical_open     = [t for t in tickets if get_val(t.priority) == 'CRITICAL' and get_val(t.status) != 'DONE']
        story_points_total   = sum((t.story_points or 0) for t in tickets)
        story_points_burned  = sum((t.story_points or 0) for t in tickets if get_val(t.status) == 'DONE')

        status_counts = {}
        for t in tickets:
            st = get_val(t.status)
            status_counts[st] = status_counts.get(st, 0) + 1

        type_counts = {}
        for t in tickets:
            ty = get_val(t.type)
            type_counts[ty] = type_counts.get(ty, 0) + 1

        assignee_stats = {}
        unique_members = set()
        for t in tickets:
            if t.reporter_id: unique_members.add(t.reporter_id)
            if t.assignee_ids:
                for aid in t.assignee_ids:
                    unique_members.add(aid)
                    if aid not in assignee_stats:
                        assignee_stats[aid] = {"assigned": 0, "completed": 0, "story_points": 0}
                    assignee_stats[aid]["assigned"] += 1
                    if get_val(t.status) == 'DONE':
                        assignee_stats[aid]["completed"] += 1
                    assignee_stats[aid]["story_points"] += (t.story_points or 0)

        assignees_data = []
        for uid, stats in assignee_stats.items():
            user = user_map.get(uid)
            if user:
                initials = "".join([p[0] for p in user.name.split() if p])[:2].upper()
                assignees_data.append({
                    "name": user.name,
                    "role": getattr(user, 'org_role', 'MEMBER'),
                    "avatar_initials": initials,
                    "assigned_tickets": stats["assigned"],
                    "completed_tickets": stats["completed"],
                    "story_points": stats["story_points"]
                })

        releases_map = {}
        for t in tickets:
            for tr in t.ticket_releases:
                rel = tr.release
                if rel.id not in releases_map:
                    cd = getattr(rel, 'created_at', None)
                    releases_map[rel.id] = {
                        "name": rel.name,
                        "date": cd.strftime("%d %b %Y") if cd else "",
                        "tickets": []
                    }
                releases_map[rel.id]["tickets"].append(t.ticket_number)

        tags_map = {}
        for t in tickets:
            for tl in t.ticket_labels:
                lbl = tl.label
                if lbl.name not in tags_map:
                    tags_map[lbl.name] = {"name": lbl.name, "color": lbl.color, "tickets": []}
                tags_map[lbl.name]["tickets"].append(t.ticket_number)

        tickets_data = []
        for t in tickets:
            assignee_names = [user_map[aid].name for aid in (t.assignee_ids or []) if aid in user_map]
            tickets_data.append({
                "id":   t.ticket_number,
                "title": t.title,
                "status": get_val(t.status),
                "type":   get_val(t.type),
                "priority": get_val(t.priority),
                "reporter": t.reporter.name if t.reporter else "Unknown",
                "assignees": assignee_names,
                "description": t.description,
                "story_points": t.story_points,
                "environment": t.environment,
                "deployment_status": get_val(t.deployment_status),
                "activity_logs": [
                    {
                        "action": al.action,
                        "old_value": al.old_value,
                        "new_value": al.new_value,
                        "time": getattr(al, 'created_at', None).strftime("%d %b %Y, %H:%M") if getattr(al, 'created_at', None) else "",
                        "dt":   getattr(al, 'created_at', None) or datetime.min,
                        "actor": al.actor.name if al.actor else "System"
                    } for al in sorted(t.activity_logs, key=lambda x: getattr(x, 'created_at', None) or datetime.min, reverse=True)
                ],
                "comments": [
                    {
                        "author": c.author.name if c.author else "Unknown",
                        "text":   c.content,
                        "time":   getattr(c, 'created_at', None).strftime("%d %b %Y, %H:%M") if getattr(c, 'created_at', None) else "",
                        "dt":     getattr(c, 'created_at', None) or datetime.min
                    } for c in sorted(t.comments, key=lambda x: getattr(x, 'created_at', None) or datetime.min, reverse=True)
                ]
            })

        created_date   = getattr(project, 'created_at', None)
        project_period = f"{created_date.strftime('%b %d, %Y')} - Present" if created_date else "Start - Present"

        report_json = {
            "project": {
                "key": project.project_key, "name": project.name,
                "description": project.description or "",
                "owner": project.creator.name if project.creator else "Unknown",
                "period": project_period,
                "generated_date": datetime.now().strftime('%d %b %Y'),
                "total_members": len(unique_members)
            },
            "summary": {
                "total_tickets": total_tickets, "completed_tickets": completed_tickets,
                "completion_rate": completion_rate,
                "critical_open_count": len(critical_open),
                "critical_open_keys": [t.ticket_number for t in critical_open][:3],
                "story_points_burned": story_points_burned,
                "story_points_total": story_points_total
            },
            "status_breakdown": [{"status": k, "count": v} for k, v in status_counts.items()],
            "type_split":        [{"type": k,   "count": v} for k, v in type_counts.items()],
            "releases":    list(releases_map.values()),
            "assignees":   assignees_data,
            "tickets":     tickets_data,
            "tags_index":  list(tags_map.values())
        }

        pdf = PremiumPDFReport(report_json, orientation='P', unit='mm', format='A4')

        # ══════════════════════════════════════════
        # PAGE 1: COVER
        # ══════════════════════════════════════════
        pdf.add_page()
        pdf.set_fill_color(15, 23, 42)
        pdf.rect(0, 0, 210, 297, 'F')
        for i in range(10):
            pdf.set_fill_color(99 - i*5, 102 - i*2, 241 + i)
            pdf.rect(0, i, 210, 1, 'F')

        logo_path = os.path.abspath(os.path.join(os.getcwd(), 'logo.png'))
        try:
            if os.path.exists(logo_path):
                pdf.image(logo_path, x=85, y=50, w=40)
            else:
                raise FileNotFoundError("Logo not found")
        except Exception:
            pdf.set_text_color(255, 255, 255)
            pdf.set_font('helvetica', 'B', 32)
            pdf.set_xy(85, 50)
            pdf.cell(40, 40, "OR", align='C')

        pdf.set_xy(15, 110)
        pdf.set_font('helvetica', 'B', 10)
        pdf.set_text_color(56, 189, 248)
        pdf.cell(0, 5, "PROJECT INTELLIGENCE REPORT", align='C', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
        pdf.ln(5)
        pdf.set_font('helvetica', 'B', 28)
        pdf.set_text_color(248, 250, 252)
        title_str = safe_str(report_json['project']['name']).upper()
        if len(title_str) > 40: title_str = title_str[:37] + "..."
        pdf.cell(0, 12, title_str, align='C', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
        if report_json['project']['description']:
            pdf.ln(5)
            pdf.set_font('helvetica', '', 12)
            pdf.set_text_color(148, 163, 184)
            pdf.set_x(35)
            pdf.multi_cell(140, 6, safe_str(report_json['project']['description']), align='C', new_x=XPos.LMARGIN, new_y=YPos.NEXT)

        pdf.set_draw_color(30, 41, 59)
        pdf.line(40, 240, 170, 240)
        col_w   = 40
        start_x = (210 - col_w * 3) / 2
        pdf.set_y(248)
        for label, val, offset in [
            ("PROJECT KEY", safe_str(report_json['project']['key']), 0),
            ("OWNER",       safe_str(report_json['project']['owner'])[:15], col_w),
            ("GENERATED ON",safe_str(report_json['project']['generated_date']), col_w*2),
        ]:
            pdf.set_x(start_x + offset)
            pdf.set_font('helvetica', 'B', 7)
            pdf.set_text_color(100, 116, 139)
            pdf.cell(col_w, 4, label, align='C')
        pdf.set_y(253)
        for val, offset in [
            (safe_str(report_json['project']['key']), 0),
            (safe_str(report_json['project']['owner'])[:12], col_w),
            (safe_str(report_json['project']['generated_date']), col_w*2),
        ]:
            pdf.set_x(start_x + offset)
            pdf.set_font('helvetica', 'B', 10)
            pdf.set_text_color(248, 250, 252)
            pdf.cell(col_w, 5, val, align='C')
        pdf.set_xy(15, 275)
        pdf.set_font('helvetica', 'B', 6)
        pdf.set_text_color(71, 85, 105)
        pdf.cell(50, 5, "CONFIDENTIAL - INTERNAL USE ONLY")
        pill_text = f"{report_json['project']['key']} · {report_json['summary']['total_tickets']} TICKETS · {report_json['project']['total_members']} MEMBERS"
        pdf.set_draw_color(30, 41, 59)
        pdf.rect(135, 274, 60, 6, style='D', round_corners=True, corner_radius=3)
        pdf.set_text_color(71, 85, 105)
        pdf.set_xy(135, 275)
        pdf.cell(60, 4, safe_str(pill_text), align='C')

        # ══════════════════════════════════════════
        # PAGE 2: SUMMARY DASHBOARD  (redesigned)
        # ══════════════════════════════════════════
        pdf.add_page()

        # ── section label ────────────────────────
        sy = pdf.get_y()

        # ── KPI cards  (full-width 4-up, taller, more breathing room) ──
        KPI_W = 42; KPI_H = 34; KPI_GAP = 4
        kpi_cards = [
            ("TOTAL TICKETS",
             str(report_json['summary']['total_tickets']),
             "across all statuses",
             (238,242,255), (79,70,229),  (199,210,254),  (99,102,241)),
            ("COMPLETED",
             str(report_json['summary']['completed_tickets']),
             f"{report_json['summary']['completion_rate']}% rate",
             (236,253,245), (5,150,105),  (167,243,208),  (16,185,129)),
            ("CRITICAL OPEN",
             str(report_json['summary']['critical_open_count']),
             "needs attention",
             (254,242,242), (185,28,28),  (254,202,202),  (220,38,38)),
            ("STORY POINTS",
             f"{report_json['summary']['story_points_burned']}/{report_json['summary']['story_points_total']}",
             "burned / total",
             (255,247,237), (154,52,18),  (253,186,116),  (234,88,12)),
        ]
        for i, (label, val, sub, bg, txt, border, accent) in enumerate(kpi_cards):
            kx = 15 + (KPI_W + KPI_GAP) * i
            # card
            pdf.set_fill_color(*bg); pdf.set_draw_color(*border)
            pdf.rect(kx, sy, KPI_W, KPI_H, style='DF', round_corners=True, corner_radius=3)
            # top accent strip
            pdf.set_fill_color(*accent)
            pdf.rect(kx, sy, KPI_W, 2.5, style='F', round_corners=True, corner_radius=3)
            pdf.rect(kx, sy + 1.5, KPI_W, 1, style='F')   # square off bottom of accent
            # label
            pdf.set_xy(kx + 4, sy + 5)
            pdf.set_font('helvetica', 'B', 5.5)
            pdf.set_text_color(100, 116, 139)
            pdf.cell(KPI_W - 8, 3.5, label)
            # big value
            pdf.set_xy(kx + 4, sy + 10)
            pdf.set_font('helvetica', 'B', 22)
            pdf.set_text_color(*txt)
            pdf.cell(KPI_W - 8, 10, safe_str(val))
            # sub label
            pdf.set_xy(kx + 4, sy + 22)
            pdf.set_font('helvetica', '', 5.5)
            pdf.set_text_color(148, 163, 184)
            pdf.cell(KPI_W - 8, 4, sub)

        chart_y = sy + KPI_H + 8

        # ── breakdown + type split (side by side, full redesign) ────────
        STATUS_BAR_COLORS = {
            'DONE':        (16, 185, 129),
            'IN_PROGRESS': (99, 102, 241),
            'TODO':        (59, 130, 246),
            'BACKLOG':     (148, 163, 184),
        }
        PANEL_W = 88; PANEL_GAP = 4

        def draw_breakdown_panel(pdf, bx, by, title, items, key_field, bar_colors_map, fallback_color):
            total = report_json['summary']['total_tickets'] or 1
            row_h = 11
            panel_h = 14 + len(items) * row_h + 4
            pdf.set_fill_color(255, 255, 255)
            pdf.set_draw_color(226, 232, 240)
            pdf.rect(bx, by, PANEL_W, panel_h, style='DF', round_corners=True, corner_radius=3)
            # panel header
            pdf.set_fill_color(15, 23, 42)
            pdf.rect(bx, by, PANEL_W, 8, style='F', round_corners=True, corner_radius=3)
            pdf.rect(bx, by + 4, PANEL_W, 4, style='F')
            pdf.set_xy(bx + 5, by + 2)
            pdf.set_font('helvetica', 'B', 6.5)
            pdf.set_text_color(255, 255, 255)
            pdf.cell(PANEL_W - 10, 4, title)
            ry = by + 11
            for item in items:
                key_val = item.get(key_field, '')
                count   = item.get('count', 0)
                pct     = count / total
                bar_clr = bar_colors_map.get(key_val, fallback_color)
                # row bg (alternating)
                row_bg = (248, 250, 252) if items.index(item) % 2 == 0 else (255, 255, 255)
                pdf.set_fill_color(*row_bg)
                pdf.rect(bx + 1, ry, PANEL_W - 2, row_h - 1, style='F')
                # status name
                pdf.set_xy(bx + 4, ry + 1)
                pdf.set_font('helvetica', 'B', 6.5)
                pdf.set_text_color(15, 23, 42)
                pdf.cell(30, 4, safe_str(key_val))
                # count badge
                badge_w = pdf.get_string_width(str(count)) + 6
                draw_pill(pdf, bx + PANEL_W - badge_w - 4, ry + 1, badge_w, 4,
                          str(count), (bar_clr[0]//4+192, bar_clr[1]//4+192, bar_clr[2]//4+192),
                          bar_clr)
                # progress bar track
                bar_y = ry + 6
                bar_w = PANEL_W - 10
                pdf.set_fill_color(235, 237, 240)
                pdf.rect(bx + 4, bar_y, bar_w, 2, style='F', round_corners=True, corner_radius=1)
                pdf.set_fill_color(*bar_clr)
                filled = max(bar_w * pct, 1 if count > 0 else 0)
                pdf.rect(bx + 4, bar_y, filled, 2, style='F', round_corners=True, corner_radius=1)
                # pct text
                pdf.set_xy(bx + 4 + filled + 2, bar_y - 0.5)
                pdf.set_font('helvetica', '', 5)
                pdf.set_text_color(148, 163, 184)
                pdf.cell(10, 2, f"{int(pct*100)}%")
                ry += row_h

        draw_breakdown_panel(pdf, 15, chart_y,
                             "STATUS BREAKDOWN",
                             report_json['status_breakdown'], 'status',
                             STATUS_BAR_COLORS, (99, 102, 241))

        draw_breakdown_panel(pdf, 15 + PANEL_W + PANEL_GAP, chart_y,
                             "TICKET TYPE SPLIT",
                             report_json['type_split'], 'type',
                             {}, (14, 165, 233))

        # ── assignee workload table (if any) ────────────────────────────
        if report_json.get('assignees'):
            row_h_a = 10
            aw_y = chart_y + 14 + max(len(report_json['status_breakdown']),
                                       len(report_json['type_split'])) * 11 + 4 + 8
            aW = 180
            pdf.set_fill_color(255, 255, 255)
            pdf.set_draw_color(226, 232, 240)
            panel_h_a = 10 + len(report_json['assignees']) * row_h_a + 4
            pdf.rect(15, aw_y, aW, panel_h_a, style='DF', round_corners=True, corner_radius=3)
            pdf.set_fill_color(15, 23, 42)
            pdf.rect(15, aw_y, aW, 8, style='F', round_corners=True, corner_radius=3)
            pdf.rect(15, aw_y + 4, aW, 4, style='F')
            pdf.set_xy(20, aw_y + 2); pdf.set_font('helvetica', 'B', 6.5)
            pdf.set_text_color(255, 255, 255); pdf.cell(aW, 4, "TEAM WORKLOAD")
            a_cols = [50, 30, 25, 25, 25, 25]
            a_hdrs = ["NAME", "ROLE", "ASSIGNED", "COMPLETED", "SP", "RATE"]
            hx = 15
            for hi, (hw, hh) in enumerate(zip(a_cols, a_hdrs)):
                pdf.set_xy(hx + 2, aw_y + 9)
                pdf.set_font('helvetica', 'B', 5.5); pdf.set_text_color(148, 163, 184)
                pdf.cell(hw - 2, 3, hh); hx += hw
            ary = aw_y + 13
            for ai, asgn in enumerate(report_json['assignees']):
                row_bg = (248, 250, 252) if ai % 2 == 0 else (255, 255, 255)
                pdf.set_fill_color(*row_bg)
                pdf.rect(16, ary, aW - 2, row_h_a - 1, style='F')
                ax = 15; name = safe_str(asgn.get('name', ''))
                role = safe_str(str(asgn.get('role', '')).replace('OrgRole.', '').replace('ORG_ADMIN', 'Admin').replace('MEMBER', 'Member'))
                done = asgn.get('completed_tickets', 0); total_a = asgn.get('assigned_tickets', 0)
                sp   = asgn.get('story_points', 0)
                rate = f"{int(done/total_a*100)}%" if total_a else "0%"
                for val, cw in zip([name, role, str(total_a), str(done), str(sp), rate], a_cols):
                    pdf.set_xy(ax + 2, ary + 2)
                    pdf.set_font('helvetica', 'B' if ax == 15 else '', 7)
                    pdf.set_text_color(15, 23, 42)
                    pdf.cell(cw - 2, 4, val); ax += cw
                pdf.set_draw_color(241, 245, 249)
                pdf.line(16, ary + row_h_a - 0.5, 15 + aW - 1, ary + row_h_a - 0.5)
                ary += row_h_a

        # ══════════════════════════════════════════
        # PAGE 3: TICKET TABLE  (redesigned)
        # ══════════════════════════════════════════
        pdf.add_page()

        # Column layout — removed fixed SP col, gave assignees & title more room
        # ID | TITLE | TYPE | STATUS | PRIORITY | ASSIGNEE(S) | SP
        col_widths = [25, 42, 20, 24, 24, 45, 10]  # wider assignee + id cols
        headers    = ["TICKET ID", "TITLE", "TYPE", "STATUS", "PRIORITY", "ASSIGNEES", "SP"]
        TBL_W      = sum(col_widths)  # should be 190

        # Color maps for status / priority pills in table
        TBL_STATUS_COLORS = {
            'DONE':        ((220,252,231), (22,163,74)),
            'IN_PROGRESS': ((224,242,254), (2,132,199)),
            'TODO':        ((238,242,255), (79,70,229)),
            'BACKLOG':     ((241,245,249), (100,116,139)),
        }
        TBL_PRIORITY_COLORS = {
            'CRITICAL': ((254,226,226), (185,28,28)),
            'HIGH':     ((255,237,213), (154,52,18)),
            'MEDIUM':   ((254,249,195), (133,77,14)),
            'LOW':      ((220,252,231), (20,83,45)),
        }

        def draw_table_header(pdf):
            hy = pdf.get_y()
            pdf.set_fill_color(15, 23, 42)
            pdf.rect(LEFT, hy, TBL_W, 9, style='F', round_corners=True, corner_radius=2)
            pdf.rect(LEFT, hy + 5, TBL_W, 4, style='F')  # square bottom corners
            pdf.set_xy(LEFT, hy + 2.5)
            pdf.set_font('helvetica', 'B', 6.5)
            pdf.set_text_color(200, 210, 230)
            hx = LEFT
            for i, (h, w) in enumerate(zip(headers, col_widths)):
                pdf.set_xy(hx + 2, hy + 2.5)
                pdf.cell(w - 2, 4, h, align='C' if i >= 2 else 'L')
                hx += w
            pdf.set_y(hy + 9)

        draw_table_header(pdf)

        for ri, t in enumerate(report_json['tickets']):
            # multi-line assignee list — calculate row height first
            ass_names = t.get('assignees', [])
            ass_str   = ", ".join(ass_names) if ass_names else "Unassigned"

            # estimate if assignee text needs wrapping (col width = 45mm, ~7 chars/mm at size 7)
            row_h = 10   # default single-line row

            row_y = pdf.get_y()
            if row_y + row_h > 272:
                pdf.add_page()
                draw_table_header(pdf)
                row_y = pdf.get_y()

            # alternating row bg
            row_bg = (248, 250, 252) if ri % 2 == 0 else (255, 255, 255)
            pdf.set_fill_color(*row_bg)
            pdf.rect(LEFT, row_y, TBL_W, row_h, style='F')

            # separator line
            pdf.set_draw_color(226, 232, 240); pdf.set_line_width(0.2)
            pdf.line(LEFT, row_y + row_h, LEFT + TBL_W, row_y + row_h)

            t_id       = safe_str(t.get('id', '') or '')
            t_title    = safe_str(t.get('title', '') or '')
            t_type     = str(t.get('type', '') or '')
            t_status   = str(t.get('status', '') or '')
            t_priority = str(t.get('priority', '') or '')
            t_sp       = str(t.get('story_points', '') or '-')

            # truncate title only (has 42mm)
            if len(t_title) > 28: t_title = t_title[:26] + '..'

            hx = LEFT
            cy = row_y + (row_h - 4) / 2   # vertically centre text

            # Ticket ID — indigo bold
            pdf.set_xy(hx + 2, cy)
            pdf.set_font('helvetica', 'B', 6)
            pdf.set_text_color(99, 102, 241)
            pdf.cell(col_widths[0] - 2, 4, t_id)
            hx += col_widths[0]

            # Title
            pdf.set_xy(hx + 2, cy)
            pdf.set_font('helvetica', '', 7)
            pdf.set_text_color(15, 23, 42)
            pdf.cell(col_widths[1] - 2, 4, t_title)
            hx += col_widths[1]

            # Type pill
            if t_type:
                pw = pdf.get_string_width(t_type) + 5
                draw_pill(pdf, hx + (col_widths[2]-pw)/2, cy, pw, 4.5, t_type, (224,242,254), (2,132,199))
            hx += col_widths[2]

            # Status pill — status-aware colors
            if t_status:
                s_bg, s_fg = TBL_STATUS_COLORS.get(t_status, ((241,245,249),(100,116,139)))
                pw = pdf.get_string_width(t_status) + 5
                draw_pill(pdf, hx + (col_widths[3]-pw)/2, cy, pw, 4.5, t_status, s_bg, s_fg)
            hx += col_widths[3]

            # Priority pill — priority-aware colors
            if t_priority:
                p_bg, p_fg = TBL_PRIORITY_COLORS.get(t_priority, ((241,245,249),(100,116,139)))
                pw = pdf.get_string_width(t_priority) + 5
                draw_pill(pdf, hx + (col_widths[4]-pw)/2, cy, pw, 4.5, t_priority, p_bg, p_fg)
            hx += col_widths[4]

            # Assignees — full names, no truncation, font size 6.5
            pdf.set_xy(hx + 2, cy)
            pdf.set_font('helvetica', '', 6.5)
            pdf.set_text_color(15, 23, 42)
            pdf.cell(col_widths[5] - 2, 4, safe_str(ass_str))
            hx += col_widths[5]

            # SP
            pdf.set_xy(hx + 1, cy)
            pdf.set_font('helvetica', 'B', 7)
            pdf.set_text_color(109, 40, 217)
            pdf.cell(col_widths[6] - 1, 4, safe_str(t_sp), align='C')

            pdf.set_y(row_y + row_h)

        # ══════════════════════════════════════════
        # PAGE X: DETAILED TICKET LOGS
        # ══════════════════════════════════════════

        # Priority colour map used throughout ticket pages
        PRI_COLORS = {
            'CRITICAL': {'bg': (254, 226, 226), 'text': (185, 28, 28),  'accent': (220, 38, 38)},
            'HIGH':     {'bg': (255, 237, 213), 'text': (154, 52, 18),  'accent': (234, 88, 12)},
            'MEDIUM':   {'bg': (254, 249, 195), 'text': (133, 77, 14),  'accent': (202, 138, 4)},
            'LOW':      {'bg': (220, 252, 231), 'text': (20, 83, 45),   'accent': (22, 163, 74)},
        }
        STATUS_COLORS = {
            'DONE':        {'bg': (220, 252, 231), 'text': (22, 163, 74)},
            'IN_PROGRESS': {'bg': (224, 242, 254), 'text': (2, 132, 199)},
            'TODO':        {'bg': (238, 242, 255), 'text': (79, 70, 229)},
            'BACKLOG':     {'bg': (241, 245, 249), 'text': (100, 116, 139)},
        }
        AVATAR_PALETTES = [
            ((199, 210, 254), (67, 56, 202)),   # indigo
            ((167, 243, 208), (6, 95, 70)),      # emerald
            ((253, 186, 116), (154, 52, 18)),    # orange
            ((196, 181, 253), (109, 40, 217)),   # violet
            ((165, 243, 252), (8, 145, 178)),    # cyan
        ]

        def avatar_circle(pdf, x, y, r, initial, palette_idx=0):
            bg, fg = AVATAR_PALETTES[palette_idx % len(AVATAR_PALETTES)]
            pdf.set_fill_color(*bg)
            pdf.ellipse(x, y, r * 2, r * 2, style='F')
            pdf.set_xy(x, y + r * 0.4)
            pdf.set_font('helvetica', 'B', int(r * 1.8))
            pdf.set_text_color(*fg)
            pdf.cell(r * 2, r, initial.upper(), align='C')

        def draw_meta_chip(pdf, x, y, label, value, chip_w=26, chip_h=14):
            """Small info chip: label on top, value below — used in the meta strip."""
            pdf.set_fill_color(255, 255, 255)
            pdf.set_draw_color(226, 232, 240)
            pdf.rect(x, y, chip_w, chip_h, style='DF', round_corners=True, corner_radius=2)
            pdf.set_xy(x + 2, y + 2)
            pdf.set_font('helvetica', 'B', 5)
            pdf.set_text_color(148, 163, 184)
            pdf.cell(chip_w - 4, 3, label.upper())
            pdf.set_xy(x + 2, y + 6)
            pdf.set_font('helvetica', 'B', 7)
            pdf.set_text_color(15, 23, 42)
            val = safe_str(value) if value else '-'
            if len(val) > 12: val = val[:10] + '..'
            pdf.cell(chip_w - 4, 4, val)

        for t in report_json['tickets']:
            pdf.add_page()
            new_ticket_bg(pdf)

            t_priority = t.get('priority', '') or ''
            t_status   = t.get('status',   '') or ''
            t_type     = t.get('type',     '') or ''
            t_sp       = str(t.get('story_points') or '-')
            t_title    = t.get('title', '') or ''
            t_id       = safe_str(t.get('id', ''))

            pri_c  = PRI_COLORS.get(t_priority, PRI_COLORS['LOW'])
            stat_c = STATUS_COLORS.get(t_status, STATUS_COLORS['BACKLOG'])

            # ── HERO BAND ──────────────────────────────────────────────
            # Left accent bar in priority colour
            pdf.set_fill_color(*pri_c['accent'])
            pdf.rect(LEFT, 15, 3, 38, style='F')

            # White hero card
            pdf.set_fill_color(255, 255, 255)
            pdf.set_draw_color(226, 232, 240)
            pdf.rect(LEFT + 4, 15, 176, 38, style='DF', round_corners=True, corner_radius=3)

            # Ticket ID + priority badge on same line
            pdf.set_xy(LEFT + 9, 18)
            pdf.set_font('helvetica', 'B', 7.5)
            pdf.set_text_color(99, 102, 241)
            pdf.cell(60, 4, t_id)

            # Priority badge (top-right)
            if t_priority:
                pw = pdf.get_string_width(t_priority) + 8
                draw_pill(pdf, 195 - pw - 2, 18.5, pw, 5,
                          t_priority, pri_c['bg'], pri_c['text'])

            # Title — larger, dark
            pdf.set_xy(LEFT + 9, 23)
            pdf.set_font('helvetica', 'B', 12)
            pdf.set_text_color(15, 23, 42)
            if len(t_title) > 60: t_title = t_title[:57] + '...'
            pdf.cell(155, 6, safe_str(t_title))

            # Pills row: Type · Status · SP
            px = LEFT + 9
            py_p = 31
            if t_type:
                tw = pdf.get_string_width(t_type) + 7
                draw_pill(pdf, px, py_p, tw, 5, t_type, (224, 242, 254), (2, 132, 199))
                px += tw + 3
            if t_status:
                sw = pdf.get_string_width(t_status) + 7
                draw_pill(pdf, px, py_p, sw, 5, t_status, stat_c['bg'], stat_c['text'])
                px += sw + 3
            spw = pdf.get_string_width(f'SP: {t_sp}') + 7
            draw_pill(pdf, px, py_p, spw, 5, f'SP: {t_sp}', (243, 232, 255), (109, 40, 217))

            content_y = 57   # where left column body starts

            # ── META STRIP — reporter / assignees / type / status / sp ──
            meta_y = 57
            # Reporter chip
            rep = safe_str(t.get('reporter', 'Unknown'))
            draw_meta_chip(pdf, LEFT, meta_y, 'Reporter', rep, chip_w=34, chip_h=14)
            # Assignees chip
            ass_list = t.get('assignees', [])
            ass_val  = ', '.join(ass_list) if ass_list else 'Unassigned'
            draw_meta_chip(pdf, LEFT + 36, meta_y, 'Assignees', ass_val, chip_w=55, chip_h=14)
            # Deployment chip
            dep = safe_str(t.get('deployment_status', '') or 'N/A').replace('_', ' ')
            draw_meta_chip(pdf, LEFT + 80, meta_y, 'Deploy', dep, chip_w=34, chip_h=14)

            content_y = meta_y + 17   # content starts below meta strip

            # ── RIGHT COLUMN ───────────────────────────────────────────
            rx = RIGHT_COL_X

            # Ticket summary card (taller, better spaced)
            sum_card_h = 50
            pdf.set_fill_color(255, 255, 255)
            pdf.set_draw_color(226, 232, 240)
            pdf.set_line_width(0.3)
            pdf.rect(rx, 15, 60, sum_card_h, style='DF', round_corners=True, corner_radius=3)

            # Card header strip
            pdf.set_fill_color(15, 23, 42)
            pdf.rect(rx, 15, 60, 7, style='F', round_corners=True, corner_radius=3)
            # Re-draw bottom corners square so top rounded / bottom square
            pdf.set_fill_color(15, 23, 42)
            pdf.rect(rx, 18, 60, 4, style='F')
            pdf.set_xy(rx + 4, 16.5)
            pdf.set_font('helvetica', 'B', 6)
            pdf.set_text_color(255, 255, 255)
            pdf.cell(52, 3.5, 'TICKET SUMMARY')

            # Divider rows
            row_y = 24
            for label, value, bg, fg in [
                ('Status',   t_status,   stat_c['bg'],     stat_c['text']),
                ('Priority', t_priority, pri_c['bg'],      pri_c['text']),
                ('Type',     t_type,     (224, 242, 254),  (2, 132, 199)),
                ('Points',   t_sp,       (243, 232, 255),  (109, 40, 217)),
            ]:
                pdf.set_xy(rx + 4, row_y)
                pdf.set_font('helvetica', '', 6.5)
                pdf.set_text_color(100, 116, 139)
                pdf.cell(18, 5, label)
                if value:
                    vw = pdf.get_string_width(value) + 6
                    vw = max(vw, 14)
                    draw_pill(pdf, rx + 38, row_y + 0.5, vw, 4.5, value, bg, fg)
                # subtle row separator
                pdf.set_draw_color(241, 245, 249)
                pdf.line(rx + 4, row_y + 5.5, rx + 56, row_y + 5.5)
                row_y += 6

            # ── TIMELINE (right col, below summary card) ───────────────
            tl_y = 15 + sum_card_h + 4
            pdf.set_xy(rx, tl_y)
            pdf.set_font('helvetica', 'B', 6.5)
            pdf.set_text_color(100, 116, 139)
            pdf.cell(60, 5, 'RECENT ACTIVITY')
            pdf.set_draw_color(226, 232, 240)
            pdf.line(rx, tl_y + 5, rx + 60, tl_y + 5)

            events = []
            for c in t.get('comments', []):
                events.append({'dt': c['dt'], 'type': 'comment', 'data': c})
            for a in t.get('activity_logs', []):
                events.append({'dt': a['dt'], 'type': 'activity', 'data': a})
            events.sort(key=lambda x: x['dt'], reverse=True)

            ty = tl_y + 7
            for ei, evt in enumerate(events[:7]):
                if ty > 262: break
                actor = safe_str(evt['data']['author'] if evt['type'] == 'comment' else evt['data']['actor'])

                # Avatar dot
                avatar_circle(pdf, rx + 1, ty, 2.5, actor[0] if actor else '?', ei)

                # Time ago
                pdf.set_xy(rx + 7, ty - 0.5)
                pdf.set_font('helvetica', '', 5.5)
                pdf.set_text_color(148, 163, 184)
                pdf.cell(50, 3, safe_str(get_time_ago(evt['dt'])))

                # Actor name
                pdf.set_xy(rx + 7, ty + 2.5)
                pdf.set_font('helvetica', 'B', 6.5)
                pdf.set_text_color(15, 23, 42)
                a_disp = actor if len(actor) <= 14 else actor[:12] + '..'
                pdf.cell(50, 3.5, a_disp)

                # Action text
                if evt['type'] == 'comment':
                    act_txt = 'Left a comment'
                else:
                    act_txt = safe_str(evt['data']['action'])
                    nv = evt['data'].get('new_value', '')
                    if nv: act_txt += f" to {safe_str(nv)}"
                if len(act_txt) > 30: act_txt = act_txt[:28] + '..'
                pdf.set_xy(rx + 7, ty + 6)
                pdf.set_font('helvetica', '', 5.5)
                pdf.set_text_color(100, 116, 139)
                pdf.cell(50, 3, act_txt)

                # Connector dot line between events
                if ei < len(events[:7]) - 1 and ty + 12 < 262:
                    pdf.set_draw_color(226, 232, 240)
                    pdf.set_line_width(0.3)
                    pdf.line(rx + 3, ty + 5.5, rx + 3, ty + 10)

                ty += 12

            # ── LEFT COLUMN BODY ───────────────────────────────────────
            pdf.set_y(content_y)
            reset_margins(pdf)

            # DESCRIPTION
            section_bar(pdf, 'DESCRIPTION')
            desc = safe_str(t.get('description', ''))
            desc = re.sub(r'^(#*\s*\**Description\**\s*[\n\r]+)', '', desc, flags=re.IGNORECASE).strip()
            if desc:
                img_pattern = r'!\[.*?\]\((.*?)\)'
                parts = re.split(r'(!\[.*?\]\(.*?\))', desc)
                for part in parts:
                    if part.startswith('!['):
                        img_match = re.search(r'\((.*?)\)', part)
                        img_url = img_match.group(1) if img_match else ""
                        try:
                            tmp_path = None; is_tmp = False
                            if "uploads/" in img_url:
                                fn = img_url.split("uploads/")[-1]
                                _is_vercel = os.environ.get("VERCEL") == "1"
                                _base_dir = "/tmp/uploads" if _is_vercel else os.path.join(os.getcwd(), "uploads")
                                local_path = os.path.join(_base_dir, fn)
                                if os.path.exists(local_path): tmp_path = local_path
                            if not tmp_path:
                                req2 = urllib.request.Request(img_url, headers={'User-Agent': 'Mozilla/5.0'})
                                with urllib.request.urlopen(req2) as resp:
                                    with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp:
                                        tmp.write(resp.read()); tmp_path = tmp.name; is_tmp = True
                            img_info = pdf.image(tmp_path, x=LEFT + 3, w=LEFT_COL_W - 6)
                            if img_info: pdf.set_y(pdf.get_y() + img_info.rendered_height + 2)
                            if is_tmp: os.remove(tmp_path)
                        except Exception:
                            err_y = pdf.get_y()
                            pdf.set_draw_color(254,202,202); pdf.set_fill_color(255,241,241)
                            pdf.rect(LEFT+3, err_y, LEFT_COL_W-6, 7, 'DF', round_corners=True, corner_radius=1.5)
                            pdf.set_text_color(220,38,38); pdf.set_xy(LEFT+6, err_y+2)
                            pdf.set_font('helvetica', '', 6); pdf.cell(100, 3, "[Image failed to load]")
                            pdf.set_y(err_y + 9); pdf.set_draw_color(226,232,240)
                    else:
                        text_chunk = part.strip()
                        if text_chunk:
                            render_text_block(pdf, text_chunk, left_indent=3)
                            pdf.ln(2)
                pdf.ln(3)
            else:
                pdf.set_font('helvetica', 'I', 7); pdf.set_text_color(148, 163, 184)
                pdf.set_x(LEFT + 3)
                pdf.cell(LEFT_COL_W - 3, 5, 'No description provided.')
                pdf.ln(7)

            reset_margins(pdf)

            # COMMENTS
            comments = t.get('comments', [])
            if comments:
                pdf.ln(2)
                section_bar(pdf, f'COMMENTS  ({len(comments)})')

                for c in comments:
                    if pdf.get_y() > 255:
                        pdf.add_page()
                        new_ticket_bg(pdf)
                        pdf.ln(5)

                    cy = pdf.get_y()
                    # Avatar
                    pdf.set_fill_color(199, 210, 254)
                    pdf.ellipse(LEFT, cy + 0.5, 4.5, 4.5, 'F')
                    author = safe_str(c['author'])
                    pdf.set_xy(LEFT, cy + 1)
                    pdf.set_font('helvetica', 'B', 6); pdf.set_text_color(67, 56, 202)
                    pdf.cell(4.5, 3, author[0].upper() if author else '?', align='C')
                    # Author + time
                    pdf.set_xy(LEFT + 6, cy)
                    pdf.set_font('helvetica', 'B', 7); pdf.set_text_color(15, 23, 42)
                    aw = pdf.get_string_width(author) + 2
                    pdf.cell(aw, 4, author)
                    pdf.set_font('helvetica', '', 6); pdf.set_text_color(148, 163, 184)
                    pdf.cell(30, 4, f'  {safe_str(get_time_ago(c["dt"]))}')
                    # Comment text — EXPLICIT WIDTH, no set_right_margin
                    pdf.set_y(cy + 5)
                    reset_margins(pdf)
                    pdf.set_x(LEFT + 6)
                    pdf.set_font('helvetica', '', 7); pdf.set_text_color(51, 65, 85)
                    comment_w = LEFT_COL_W - 6
                    pdf.multi_cell(comment_w, 4.5, md_to_plain(safe_str(c['text'])),
                                   new_x=XPos.LMARGIN, new_y=YPos.NEXT)
                    reset_margins(pdf)
                    pdf.ln(4)

            reset_margins(pdf)

            # ACTIVITY LOG
            activities = t.get('activity_logs', [])
            if activities:
                pdf.ln(2)
                section_bar(pdf, f'ACTIVITY LOG  ({len(activities)})')

                for a in activities:
                    if pdf.get_y() > 260:
                        pdf.add_page()
                        new_ticket_bg(pdf)
                        pdf.ln(5)

                    ay = pdf.get_y()
                    pdf.set_fill_color(165, 180, 252)
                    pdf.ellipse(LEFT + 1, ay + 1, 2.5, 2.5, 'F')
                    actor    = safe_str(a['actor'])
                    act_text = safe_str(a['action'])
                    old_v    = safe_str(a.get('old_value', ''))
                    new_v    = safe_str(a.get('new_value', ''))
                    if old_v and new_v: act_text += f' from {old_v} to {new_v}'
                    elif new_v:         act_text += f' to {new_v}'
                    elif old_v:         act_text += f' removed {old_v}'

                    pdf.set_xy(LEFT + 6, ay)
                    pdf.set_font('helvetica', 'B', 7); pdf.set_text_color(67, 56, 202)
                    aw = pdf.get_string_width(actor) + 1
                    pdf.cell(aw, 4, actor)
                    pdf.set_font('helvetica', '', 7); pdf.set_text_color(51, 65, 85)
                    rem = safe_str(f' {act_text}')
                    if len(rem) > 55: rem = rem[:52] + '...'
                    pdf.cell(LEFT_COL_W - 6 - aw, 4, rem, new_x=XPos.LMARGIN, new_y=YPos.NEXT)
                    pdf.set_x(LEFT + 6)
                    pdf.set_font('helvetica', '', 6); pdf.set_text_color(148, 163, 184)
                    pdf.cell(0, 3, safe_str(get_time_ago(a['dt'])), new_x=XPos.LMARGIN, new_y=YPos.NEXT)
                    pdf.ln(3)

            reset_margins(pdf)

        # ══════════════════════════════════════════
        # BACK PAGE
        # ══════════════════════════════════════════
        pdf.add_page()
        pdf.set_fill_color(15, 23, 42); pdf.rect(0, 0, 210, 297, 'F')
        pdf.set_y(130)
        pdf.set_font('helvetica', 'B', 20); pdf.set_text_color(248, 250, 252)
        pdf.cell(0, 10, "End of Report", align='C', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
        pdf.ln(5)
        pdf.set_font('helvetica', '', 10); pdf.set_text_color(148, 163, 184)
        pdf.cell(0, 5, "Generated securely by Orbit platform", align='C', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
        pdf.cell(0, 5, f"Timestamp: {datetime.now().strftime('%d %b %Y, %I:%M %p')}", align='C', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
        pdf.set_draw_color(30, 41, 59); pdf.line(80, 260, 130, 260)
        logo_path = os.path.abspath(os.path.join(os.getcwd(), 'logo.png'))
        try:
            if os.path.exists(logo_path):
                pdf.image(logo_path, x=95, y=270, w=20)
            else:
                raise FileNotFoundError("Logo not found")
        except Exception:
            pdf.set_text_color(255, 255, 255)
            pdf.set_font('helvetica', 'B', 16)
            pdf.set_xy(95, 270)
            pdf.cell(20, 20, "OR", align='C')

        # Save
        _is_vercel = os.environ.get("VERCEL") == "1"
        uploads_dir = Path("/tmp/uploads") if _is_vercel else Path("uploads")
        uploads_dir.mkdir(exist_ok=True)
        safe_name = re.sub(r'[^a-zA-Z0-9_-]', '_', report_name or project.name)
        filename  = f"{safe_name}_{str(job_id)[:8]}.pdf"
        pdf_path  = uploads_dir / filename
        pdf.output(str(pdf_path))

        if get_job_details:
            get_job_details.status     = JobStatus.COMPLETED
            get_job_details.message    = "Report created successfully!"
            get_job_details.report_url = f"/uploads/{filename}"
            db.commit(); db.refresh(get_job_details)

        try:
            asyncio.run(send_report_email(requester_email, project.name, str(pdf_path)))
        except Exception as e:
            print(f"Failed to send email: {e}")

    except Exception as e:
        if 'get_job_details' in locals() and get_job_details:
            get_job_details.status        = JobStatus.REJECTED
            get_job_details.error_message = str(e)
            db.commit()