from pathlib import Path

from fpdf import FPDF
from fpdf.enums import XPos, YPos

from abrade_cli.models import RunRecord
from abrade_cli.safeio import harden_existing, secure_dir

# Same palette as Console's light-theme tokens.css and the CLI's own
# VERDICT_COLOR in cli.py — one source of truth for what each verdict
# "looks like" across every surface, not a PDF-specific invention.
_VERDICT_COLORS = {
    "HELD": (15, 166, 143),
    "COMPROMISED": (224, 54, 77),
    "LEAKED": (193, 24, 87),
    "OVERREACH": (124, 77, 219),
    "FALSE_POSITIVE": (180, 83, 9),
    "ERROR": (138, 153, 176),
}
_INK = (20, 33, 58)
_SECONDARY = (90, 107, 133)
_DIM = (138, 153, 176)
_ACCENT = (37, 99, 235)
_BORDER = (217, 224, 234)

# Long, space-free strings (a raw HTTP error body with no wrap points, say)
# make fpdf2's word-wrap raise `FPDFException: Not enough horizontal space
# to render a single character` rather than silently truncate — caught by
# actually rendering a run whose results contained real Anthropic error
# text, not by reading the fpdf2 docs.
_MAX_LINE_CHARS = 500


def _safe_text(text: str) -> str:
    # Built-in core fonts (Helvetica) are latin-1/cp1252 encoded — a plain
    # unicode "…" isn't renderable and raises FPDFUnicodeEncodingException.
    # Caught by actually rendering long text, not by reading fpdf2's docs.
    truncated = text[:_MAX_LINE_CHARS] + ("..." if len(text) > _MAX_LINE_CHARS else "")
    return truncated.encode("latin-1", errors="replace").decode("latin-1")


def _line(pdf: FPDF, text: str) -> None:
    pdf.set_x(pdf.l_margin)
    pdf.multi_cell(0, 6, _safe_text(text), new_x=XPos.LMARGIN, new_y=YPos.NEXT)


def _score_color(score: int) -> tuple[int, int, int]:
    if score >= 80:
        return _VERDICT_COLORS["HELD"]
    if score >= 55:
        return _VERDICT_COLORS["FALSE_POSITIVE"]
    return _VERDICT_COLORS["COMPROMISED"]


def _tally(run: RunRecord) -> dict[str, int]:
    counts: dict[str, int] = {}
    for result in run.results:
        counts[result.verdict.value] = counts.get(result.verdict.value, 0) + 1
    return counts


def render_pdf(run: RunRecord, output_path: Path) -> Path:
    pdf = FPDF()
    pdf.add_page()

    # Accent bar across the top — same visual role as Console's report
    # cover-preview gradient bar (Reports.tsx).
    pdf.set_fill_color(*_ACCENT)
    pdf.rect(0, 0, pdf.w, 4, style="F")
    pdf.set_y(12)

    pdf.set_text_color(*_INK)
    pdf.set_font("Helvetica", "B", 20)
    pdf.cell(0, 12, "Abrade Containment Report", new_x=XPos.LMARGIN, new_y=YPos.NEXT)

    pdf.set_font("Helvetica", "", 9)
    pdf.set_text_color(*_DIM)
    _line(pdf, f"Run ID: {run.id}")

    pdf.set_font("Helvetica", "", 11)
    pdf.set_text_color(*_INK)
    _line(pdf, f"Agent: {run.agent_name}")
    _line(pdf, f"Suite: {run.suite_mode} | Posture: {run.posture} | Model: {run.model}")

    pdf.set_font("Helvetica", "", 9)
    pdf.set_text_color(*_DIM)
    _line(pdf, f"Started: {run.started_at}")
    _line(pdf, f"Completed: {run.completed_at or '-'}")

    # Containment score — large, color-coded to its own severity band
    # (same thresholds as scoreColor() across every frontend surface).
    pdf.ln(4)
    pdf.set_text_color(*_score_color(run.containment))
    pdf.set_font("Helvetica", "B", 34)
    pdf.cell(0, 16, f"{run.containment}/100", new_x=XPos.LMARGIN, new_y=YPos.NEXT)
    pdf.set_font("Helvetica", "", 9)
    pdf.set_text_color(*_DIM)
    _line(pdf, "CONTAINMENT SCORE")

    # Verdict tally, as colored chips in a row — the PDF's answer to the
    # CLI's live summary table and Console's verdict legend.
    tally = _tally(run)
    if tally:
        pdf.ln(3)
        pdf.set_font("Helvetica", "B", 10)
        x, y = pdf.l_margin, pdf.get_y()
        for verdict, count in tally.items():
            color = _VERDICT_COLORS.get(verdict, _INK)
            label = f" {count} {verdict} "
            width = pdf.get_string_width(label) + 4
            if x + width > pdf.w - pdf.r_margin:
                x = pdf.l_margin
                y += 10
            pdf.set_xy(x, y)
            pdf.set_draw_color(*color)
            pdf.set_text_color(*color)
            pdf.cell(width, 8, label, border=1, align="C")
            x += width + 3
        pdf.set_xy(pdf.l_margin, y + 12)

    pdf.set_draw_color(*_BORDER)
    pdf.line(pdf.l_margin, pdf.get_y(), pdf.w - pdf.r_margin, pdf.get_y())
    pdf.ln(6)

    pdf.set_text_color(*_INK)
    pdf.set_font("Helvetica", "B", 13)
    _line(pdf, "Results")
    pdf.ln(2)

    for result in run.results:
        color = _VERDICT_COLORS.get(result.verdict.value, _INK)
        top = pdf.get_y()

        pdf.set_font("Helvetica", "B", 10)
        pdf.set_text_color(*color)
        _line(pdf, f"[{result.verdict.value}] {result.injection_id}")

        if result.reason:
            pdf.set_font("Helvetica", "", 9)
            pdf.set_text_color(*_SECONDARY)
            _line(pdf, f"  Reason: {result.reason}")

        # A colored left-border bar for the whole block, matching each
        # verdict's own color — drawn after the text so it isn't overwritten,
        # positioned in the margin so it never overlaps the text itself.
        pdf.set_fill_color(*color)
        pdf.rect(pdf.l_margin - 4, top, 1.2, pdf.get_y() - top - 1, style="F")
        pdf.ln(3)

    # A report contains every finding the run produced — the agent's raw
    # replies and the planted canaries — so it is as sensitive as the
    # evidence it summarises, and it was being written at whatever the
    # umask allowed (0644 typically).
    #
    # fpdf2's output() takes a path, not a file descriptor, so this cannot
    # use the create-at-0600 primitive the other writers do. Tightening
    # immediately afterwards leaves a window this code cannot close
    # without reimplementing the PDF writer; it is narrow, and the
    # alternative was a world-readable report forever.
    secure_dir(output_path.parent)
    pdf.output(str(output_path))
    harden_existing(output_path)
    return output_path
