#!/usr/bin/env python3
"""
Generates the branded "Player & Facilitator Guide" PDF for the Airport Cyber
Resilience Simulator V2 — how to play, strategy, and what it communicates.
Reuses the shared design system from build_guide.py.
"""
import os
from build_guide import (  # shared palette, fonts, styles and flowable helpers
    A4, mm, HexColor, TA_CENTER, ParagraphStyle,
    BaseDocTemplate, PageTemplate, Frame, Paragraph, Spacer, Table, TableStyle,
    KeepTogether, NextPageTemplate, PageBreak,
    BG, PANEL, PANEL2, LINE, TEXT, DIM, GREEN, CYAN, VIOLET, AMBER, RED, WHITE, CODEBG,
    PAGE_W, PAGE_H, MX,
    st_body, st_dim, st_h1, st_kicker, st_cover_t, st_cover_s, st_cover_d,
    st_th, st_td, code_block, step, bullets, info_table, section, S, ACCENTS,
)

HEADER_RIGHT = "V2 · PLAYER & FACILITATOR GUIDE"

st_lead = S("lead", fontSize=11.5, leading=17, textColor=TEXT)
st_quote = S("quote", fontName="Bahn", fontSize=13, leading=18, textColor=CYAN, alignment=TA_CENTER)


def callout(title, body, accent=CYAN):
    inner = [Paragraph(title, S("ct", fontName="SegoeB", fontSize=11, leading=15, textColor=WHITE)),
             Spacer(1, 3), Paragraph(body, st_body)]
    t = Table([[inner]], colWidths=[PAGE_W - 2 * MX])
    t.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), HexColor("#0a1c26")),
        ("LINEBEFORE", (0, 0), (0, -1), 3, accent),
        ("BOX", (0, 0), (-1, -1), 0.5, LINE),
        ("LEFTPADDING", (0, 0), (-1, -1), 12), ("RIGHTPADDING", (0, 0), (-1, -1), 12),
        ("TOPPADDING", (0, 0), (-1, -1), 10), ("BOTTOMPADDING", (0, 0), (-1, -1), 10),
    ]))
    return KeepTogether([t, Spacer(1, 8)])


def paint_bg(c, doc):
    c.setFillColor(BG); c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)
    c.setFillColor(HexColor("#081726")); c.rect(0, PAGE_H - 26 * mm, PAGE_W, 26 * mm, stroke=0, fill=1)
    c.setFillColor(GREEN); c.circle(MX + 3, PAGE_H - 15 * mm, 3, stroke=0, fill=1)
    c.setFont("Bahn", 10); c.setFillColor(WHITE)
    c.drawString(MX + 10, PAGE_H - 16 * mm, "AIRPORT CYBER RESILIENCE SIMULATOR")
    c.setFillColor(CYAN); c.drawRightString(PAGE_W - MX, PAGE_H - 16 * mm, HEADER_RIGHT)
    c.setStrokeColor(LINE); c.setLineWidth(0.6)
    c.line(MX, PAGE_H - 19 * mm, PAGE_W - MX, PAGE_H - 19 * mm)
    c.line(MX, 14 * mm, PAGE_W - MX, 14 * mm)
    c.setFont("Segoe", 8); c.setFillColor(DIM)
    c.drawString(MX, 10 * mm, "A decision game about resilience, trade-offs and consequence")
    c.setFont("MonoB", 8); c.setFillColor(GREEN)
    c.drawRightString(PAGE_W - MX, 10 * mm, f"{doc.page:02d}")


def paint_cover(c, doc):
    c.setFillColor(BG); c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)
    c.setStrokeColor(HexColor("#0c2033")); c.setLineWidth(0.5)
    for x in range(0, int(PAGE_W), 26):
        c.line(x, 0, x, PAGE_H)
    for y in range(0, int(PAGE_H), 26):
        c.line(0, y, PAGE_W, y)
    cx, cy = PAGE_W / 2, PAGE_H - 82 * mm
    r = 20 * mm
    c.setStrokeColor(CYAN); c.setLineWidth(2.4); c.setFillColor(HexColor("#08131f"))
    p = c.beginPath()
    p.moveTo(cx, cy + r); p.lineTo(cx + r * 0.8, cy + r * 0.5)
    p.lineTo(cx + r * 0.8, cy - r * 0.35); p.lineTo(cx, cy - r)
    p.lineTo(cx - r * 0.8, cy - r * 0.35); p.lineTo(cx - r * 0.8, cy + r * 0.5); p.close()
    c.drawPath(p, stroke=1, fill=1)
    # play triangle inside the emblem
    c.setFillColor(GREEN); c.setStrokeColor(GREEN)
    pt = c.beginPath()
    pt.moveTo(cx - 5 * mm, cy + 7 * mm); pt.lineTo(cx - 5 * mm, cy - 7 * mm); pt.lineTo(cx + 8 * mm, cy); pt.close()
    c.drawPath(pt, stroke=0, fill=1)
    sw = 18 * mm
    x0 = cx - (sw * 5) / 2
    for i, col in enumerate([GREEN, CYAN, VIOLET, AMBER, RED]):
        c.setFillColor(col); c.rect(x0 + i * sw, 40 * mm, sw - 2, 4 * mm, stroke=0, fill=1)


def build(path):
    doc = BaseDocTemplate(path, pagesize=A4, leftMargin=MX, rightMargin=MX,
                          topMargin=24 * mm, bottomMargin=18 * mm,
                          title="Airport Cyber Resilience Simulator V2 — Player & Facilitator Guide",
                          author="Airport Cyber Resilience Simulator")
    frame = Frame(MX, 18 * mm, PAGE_W - 2 * MX, PAGE_H - 24 * mm - 18 * mm, id="main",
                  leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0)
    cover_frame = Frame(MX, 20 * mm, PAGE_W - 2 * MX, 98 * mm, id="cover")
    doc.addPageTemplates([
        PageTemplate(id="cover", frames=[cover_frame], onPage=paint_cover),
        PageTemplate(id="content", frames=[frame], onPage=paint_bg),
    ])
    story = []

    # ---------- COVER ----------
    story.append(Spacer(1, 2 * mm))
    story.append(Paragraph("AIRPORT CYBER RESILIENCE SIMULATOR", st_cover_t))
    story.append(Spacer(1, 3 * mm))
    story.append(Paragraph("VERSION 2 · PLAYER &amp; FACILITATOR GUIDE", st_cover_s))
    story.append(Spacer(1, 6 * mm))
    story.append(Paragraph("How to play, how to think, and what the experience is designed to "
                           "teach — a companion for visitors at the kiosk and for the facilitators "
                           "running the stand.", st_cover_d))
    story.append(NextPageTemplate("content"))
    story.append(PageBreak())

    # ---------- WHAT IT COMMUNICATES ----------
    story += section("What this simulation is about", "The message")
    story.append(Paragraph(
        "The Airport Cyber Resilience Simulator is not a quiz with a right answer — it is a "
        "<b>decision game about consequence</b>. You take the seat of an airport's incident "
        "commander during a live cyber attack and discover, in ninety seconds, that every option "
        "on the table costs something. The goal is to make the trade-offs <i>felt</i>, not just "
        "explained.", st_lead))
    story.append(Spacer(1, 8))
    story.append(Paragraph("Five ideas it is designed to land:", st_dim))
    story.append(Spacer(1, 5))
    story.append(info_table(
        [["1 · Small door, big house",
          "A single unglamorous entry point — a USB stick, a default password, a phishing email — cascades across critical infrastructure."],
         ["2 · There are no free wins",
          "Every mitigation trades security against operational capacity and cash. Doing nothing also has a price, ticking up every second."],
         ["3 · Consequence scales",
          "Heathrow and a regional airport face the same attack but very different financial and regulatory exposure."],
         ["4 · Comms is response",
          "How you talk to the public moves the loss line as sharply as any technical control."],
         ["5 · Regulation is real",
          "Each node maps to an NCSC CAF objective and a UK Cyber Security and Resilience Bill duty — the fines are not invented."]],
        ["Idea", "What the player experiences"], [46 * mm, PAGE_W - 2 * MX - 46 * mm]))

    # ---------- HOW TO PLAY ----------
    story.append(Spacer(1, 10))
    story += section("How to play", "The approach — one round, about 2-3 minutes")

    story.append(step(1, "Set the stakes — choose an airport", "green", [
        Paragraph("Pick the airport on the configuration screen. This is a difficulty dial: a "
                  "Tier-1 hub bleeds cash fast and carries maximum statutory fines; a regional "
                  "airport is a gentler first game. Everything downstream scales to this choice.", st_body)]))

    story.append(step(2, "Choose a threat and start", "cyan", [
        Paragraph("Select one of the nine playbooks and press <b>START SIMULATION</b>. Use the "
                  "5-second countdown to orient: find the glowing ingress node and trace the dotted "
                  "links leading away from it — that is the likely blast radius.", st_body)]))

    story.append(step(3, "Read the board", "violet", [
        Paragraph("At zero, the method-of-ingress icon pulses over the entry node, then it turns "
                  "red. Red particles race down the links to the next target on a timer. Watch the "
                  "sidebar: the financial odometer starts climbing white → amber → red, and the "
                  "Live Incident Log narrates every event.", st_body)]))

    story.append(step(4, "Decide under pressure", "amber", [
        Paragraph("Tap a compromised node and pick a mitigation. You cannot save everything — the "
                  "spread timer is running, so choose which node to defend and how. The Regulatory "
                  "Insights tab tells you why a node matters.", st_body)]))

    story.append(step(5, "Brief the media and the regulators", "red", [
        Paragraph("Two duties run in parallel. The Crisis Communications panel opens a 15-second "
                  "proactive window — handle it, silence is expensive. At the same time the "
                  "<b>Notify CAA</b> and <b>Notify DfT</b> buttons light amber if this incident type "
                  "carries a statutory reporting duty; press the ones that apply. Failing to notify a "
                  "required regulator adds an enforcement penalty at the end.", st_body)]))

    story.append(step(6, "Reach containment and close out", "green", [
        Paragraph("Keep isolating or patching until the spread can no longer advance. Once no new "
                  "node is compromised for five seconds the simulation ends with <b>ATTACK "
                  "CONTAINED</b> and the clock and financial bleed freeze. Read the final numbers, "
                  "then press RESET to CONFIG for the next player.", st_body)]))

    story.append(Spacer(1, 6))

    # ---------- STRATEGY ----------
    story += section("Strategy", "How to think about the trade-offs")
    story.append(Paragraph("There is no single winning line, but there is skilful play. Strong "
                           "players triage: they decide what to sacrifice <i>before</i> the spread "
                           "forces their hand.", st_body))
    story.append(Spacer(1, 8))

    story.append(Paragraph("THE MITIGATION DECISION MATRIX", st_kicker))
    story.append(Spacer(1, 5))
    story.append(info_table(
        [["Isolate Node",
          "The next hop is a crown jewel (identity, border, power, ATC) and you cannot afford the spread.",
          "Stops spread dead. Accept 0% capacity and maximum operational bleed on that node."],
         ["Reduce / Throttle",
          "You want to keep partial service running and buy time to think or to patch elsewhere.",
          "Slows the spread 80% and holds 40% capacity — a hedge, not a cure."],
         ["Stop Attack (Patch)",
          "You can start the 5-second patch early enough to beat the attack to the next node — you will see it closing in as the patch counts down.",
          "Restores the node to normal — but £50k is spent whether or not you win the race."]],
        ["Move", "Reach for it when…", "What you are buying"],
        [30 * mm, (PAGE_W - 2 * MX) * 0.38, None]))
    story.append(Spacer(1, 10))

    story.append(callout("Protect the data-sensitive nodes first",
        "Identity Store, Passenger Services, Frequent Flyer DB, Border Control API and Ticketing DB "
        "trigger huge one-off regulatory fines the instant they are breached — scaled by your "
        "airport. If one of these is the next hop, isolating or patching upstream before it falls is "
        "usually worth the operational hit.", RED))

    story.append(callout("Win the comms game every time",
        "Releasing a proactive statement in the first 15 seconds halves the reputational multiplier "
        "(×0.5), which slows the entire financial bleed. Miss it and answer the flashing phone/email "
        "to hold the baseline (×1.0). Ignore the barrage and a blackout <b>triples</b> it (×3.0) — "
        "the single most expensive mistake available.", AMBER))

    story.append(callout("Mind the clock — and the tempo",
        "Every scenario has its own spread speed, and it shifts slightly each run. Fast attacks — "
        "ransomware, signal spoofing, the perimeter breach — usually outrun a patch entirely, so "
        "isolate instead. Slower, stealthier ones give a patch time to land. Throttling also "
        "multiplies the remaining delay ×5, which can turn a patch you would have lost into one you "
        "win. Read the tempo before you commit £50k to a patch.", CYAN))

    story.append(callout("Notify the right regulator — the duty matches the breach",
        "Attacks that touch air traffic, runway or aircraft systems must be reported to the <b>Civil "
        "Aviation Authority</b>; disruption to critical transport infrastructure and passenger data "
        "goes to the <b>Department for Transport</b>; the most serious incidents require <b>both</b>. "
        "The sidebar buttons light up only for the duties this scenario actually triggers — press "
        "them, or take a per-regulator enforcement penalty at the end. The deck below shows who to "
        "call for each playbook.", VIOLET))

    # ---------- SCENARIO DECK ----------
    story.append(Spacer(1, 10))
    story += section("The scenario deck", "Nine playbooks, nine lessons")
    story.append(Paragraph("Each playbook is a self-contained talking point. Rotate them to keep a "
                           "stand fresh and to show how varied the attack surface really is.", st_body))
    story.append(Spacer(1, 7))
    story.append(info_table(
        [["1", "The OT Gridlock", "USB drive", "DfT", "Removable media walks straight past the perimeter into OT."],
         ["2", "The Silent Choke", "Default password", "CAA + DfT", "Cheap smart-building IoT bridges into safety-critical systems."],
         ["3", "The Third-Party Siphon", "Phishing email", "DfT", "Your suppliers' security is your security."],
         ["4", "The Rogue Insider", "Rogue smartphone", "CAA", "Stolen legitimate access defeats the perimeter entirely."],
         ["5", "The Blind Spot", "VPN breach", "CAA + DfT", "An unmanaged path can reach kinetic, life-safety systems."],
         ["6", "The Information Hijack", "Password crack", "DfT", "Public-facing displays and PA are an integrity and panic risk."],
         ["7", "The Perimeter Ghost", "Rogue router", "DfT", "Physical and cyber security are one problem, not two."],
         ["8", "The Data Hemorrhage", "Cloud leak", "CAA + DfT", "A misconfiguration can leak PII at scale and stack fines."],
         ["9", "The Grid Overload", "Malware update", "DfT", "Energy and EV infrastructure is now part of the attack surface."]],
        ["#", "Scenario", "Ingress", "Notify", "The lesson it teaches"],
        [7 * mm, 37 * mm, 27 * mm, 21 * mm, None]))

    # ---------- DEBRIEF ----------
    story.append(Spacer(1, 10))
    story += section("Reading the result", "What good looks like")
    story.append(Paragraph("The best possible ending is <b>ATTACK CONTAINED</b> — you stopped the "
                           "spread before it ran its course. Whether contained or fully spread, the "
                           "debrief lives in a handful of numbers and the story behind them. Use "
                           "these as discussion prompts:", st_body))
    story.append(Spacer(1, 7))
    story.append(info_table(
        [["Outcome", "ATTACK CONTAINED before full spread", "Which mitigation finally halted it — and could it have come sooner?"],
         ["Total Financial Loss", "As low as the situation allowed", "What drove it — bleed, fines, comms failure, or a missed notification?"],
         ["Security Integrity", "Kept as high as possible", "Which nodes were sacrificed, and were they the right ones?"],
         ["Operational Capacity", "Traded knowingly, not accidentally", "Did isolating for safety visibly close the runway or stop bags?"],
         ["Reputation multiplier", "×0.5 (proactive)", "Was the public told early, or did the story get away?"],
         ["Statutory notifications", "CAA / DfT discharged as required", "Did you match the notification to the type of breach?"]],
        ["Metric", "A good outcome", "Debrief question"],
        [40 * mm, 46 * mm, None], head_color=GREEN))

    story.append(Spacer(1, 10))
    story += section("Running the stand", "Facilitator notes")
    story += bullets([
        "<b>Session length:</b> one round is 2-3 minutes. Let a visitor lose badly once — the "
        "trade-off lands harder than a clean win.",
        "<b>Audio:</b> set it on the configuration screen to match the hall. The countdown ticks "
        "and ingress thud are strong attention-grabbers on a busy floor.",
        "<b>Kiosk mode:</b> full-screen the browser (F11). After 90 seconds idle the app returns "
        "to the attract loop by itself.",
        "<b>Reset ritual:</b> press RESET TO CONFIG between visitors so each person sets their own "
        "airport and owns the outcome.",
        "<b>Great opening question:</b> \"You have one attack incoming and you cannot save every "
        "system — what do you protect first?\"",
    ], GREEN)
    story.append(Spacer(1, 6))
    story.append(Paragraph("Ultimately the simulator argues one thing: resilience is not the "
                           "absence of incidents, it is the quality of the decisions you make "
                           "during one.", st_quote))

    doc.build(story)
    print("wrote", path)


if __name__ == "__main__":
    here = os.path.dirname(os.path.abspath(__file__))
    build(os.path.join(here, "Airport-Cyber-Sim-V2-Player-Guide.pdf"))
