#!/usr/bin/env python3
"""
Generates a branded, dark command-center themed Setup & Configuration Guide PDF
for the Airport Cyber Resilience Simulator V2. Palette mirrors the app.
"""
import os
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.colors import HexColor
from reportlab.lib.enums import TA_LEFT, TA_CENTER
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import (
    BaseDocTemplate, PageTemplate, Frame, Paragraph, Spacer, Table, TableStyle,
    KeepTogether, FrameBreak, NextPageTemplate, PageBreak,
)
from reportlab.lib.styles import ParagraphStyle

# ---- palette (from styles.css) ----
BG       = HexColor("#050b16")
PANEL    = HexColor("#0d1f33")
PANEL2   = HexColor("#0a1826")
LINE     = HexColor("#173355")
TEXT     = HexColor("#cfe4ff")
DIM      = HexColor("#6f8bad")
GREEN    = HexColor("#39ff9e")
CYAN     = HexColor("#38e6ff")
VIOLET   = HexColor("#a06bff")
AMBER    = HexColor("#ffb03a")
RED      = HexColor("#ff3b52")
WHITE    = HexColor("#eaf6ff")
CODEBG   = HexColor("#071019")

FONTS = r"C:\Windows\Fonts"
pdfmetrics.registerFont(TTFont("Bahn", os.path.join(FONTS, "bahnschrift.ttf")))
pdfmetrics.registerFont(TTFont("Segoe", os.path.join(FONTS, "segoeui.ttf")))
pdfmetrics.registerFont(TTFont("SegoeB", os.path.join(FONTS, "segoeuib.ttf")))
pdfmetrics.registerFont(TTFont("Mono", os.path.join(FONTS, "consola.ttf")))
pdfmetrics.registerFont(TTFont("MonoB", os.path.join(FONTS, "consolab.ttf")))

PAGE_W, PAGE_H = A4
MX = 16 * mm

# ---- styles ----
def S(name, **kw):
    base = dict(fontName="Segoe", fontSize=10.5, leading=15, textColor=TEXT)
    base.update(kw)
    return ParagraphStyle(name, **base)

st_body    = S("body")
st_dim     = S("dim", textColor=DIM, fontSize=9.5, leading=13)
st_h1       = S("h1", fontName="Bahn", fontSize=19, leading=23, textColor=WHITE, spaceBefore=2, spaceAfter=2)
st_kicker   = S("kick", fontName="Bahn", fontSize=10, leading=13, textColor=CYAN)
st_step_t   = S("stept", fontName="SegoeB", fontSize=12.5, leading=16, textColor=WHITE)
st_code     = S("code", fontName="Mono", fontSize=9, leading=13.5, textColor=CYAN)
st_code_c   = S("codec", fontName="Mono", fontSize=8.5, leading=12, textColor=DIM)
st_bullet   = S("bul", leftIndent=10, bulletIndent=0, spaceBefore=1, spaceAfter=1)
st_th       = S("th", fontName="SegoeB", fontSize=9.5, leading=12, textColor=WHITE)
st_td       = S("td", fontSize=9.5, leading=12.5, textColor=TEXT)
st_cover_t  = S("covt", fontName="Bahn", fontSize=34, leading=38, textColor=WHITE, alignment=TA_CENTER)
st_cover_s  = S("covs", fontName="Bahn", fontSize=13, leading=18, textColor=CYAN, alignment=TA_CENTER)
st_cover_d  = S("covd", fontSize=11, leading=17, textColor=DIM, alignment=TA_CENTER)

ACCENTS = {"green": GREEN, "cyan": CYAN, "violet": VIOLET, "amber": AMBER, "red": RED}


def code_block(lines):
    """A dark code/command box with a cyan monospace body."""
    body = "<br/>".join(l.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;") for l in lines)
    p = Paragraph(body, st_code)
    t = Table([[p]], colWidths=[PAGE_W - 2 * MX - 8 * mm])
    t.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), CODEBG),
        ("BOX", (0, 0), (-1, -1), 0.75, LINE),
        ("LEFTPADDING", (0, 0), (-1, -1), 10), ("RIGHTPADDING", (0, 0), (-1, -1), 10),
        ("TOPPADDING", (0, 0), (-1, -1), 8), ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
    ]))
    return t


def step(num, title, accent, flowables):
    """A numbered step card with a colored accent bar on the left."""
    col = ACCENTS[accent]
    badge = Table([[Paragraph(f"{num}", ParagraphStyle("b", fontName="Bahn", fontSize=15,
                   textColor=BG, alignment=TA_CENTER))]], colWidths=[9 * mm], rowHeights=[9 * mm])
    badge.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), col), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("LEFTPADDING", (0, 0), (-1, -1), 0), ("RIGHTPADDING", (0, 0), (-1, -1), 0),
        ("TOPPADDING", (0, 0), (-1, -1), 0), ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
    ]))
    header = Table([[badge, Paragraph(title, st_step_t)]], colWidths=[11 * mm, None])
    header.setStyle(TableStyle([("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("LEFTPADDING", (0, 0), (-1, -1), 0), ("BOTTOMPADDING", (0, 0), (-1, -1), 4)]))
    inner = [header] + flowables
    card = Table([[inner]], colWidths=[PAGE_W - 2 * MX])
    card.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), PANEL2),
        ("LINEBEFORE", (0, 0), (0, -1), 3, col),
        ("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), 11), ("BOTTOMPADDING", (0, 0), (-1, -1), 11),
    ]))
    return KeepTogether([card, Spacer(1, 8)])


def bullets(items, color=GREEN):
    out = []
    for it in items:
        out.append(Paragraph(f'<font color="#{color.hexval()[2:]}">&#8226;</font>&nbsp;&nbsp;{it}', st_bullet))
    return out


def info_table(rows, header, widths, head_color=CYAN):
    data = [[Paragraph(h, st_th) for h in header]]
    for r in rows:
        data.append([Paragraph(str(c), st_td) for c in r])
    t = Table(data, colWidths=widths, repeatRows=1)
    t.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), PANEL),
        ("LINEBELOW", (0, 0), (-1, 0), 1, head_color),
        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [PANEL2, HexColor("#08131f")]),
        ("GRID", (0, 0), (-1, -1), 0.4, LINE),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("LEFTPADDING", (0, 0), (-1, -1), 7), ("RIGHTPADDING", (0, 0), (-1, -1), 7),
        ("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
    ]))
    return t


def section(title, kicker=None):
    out = []
    if kicker:
        out.append(Paragraph(kicker.upper(), st_kicker))
    out.append(Paragraph(title, st_h1))
    rule = Table([[""]], colWidths=[PAGE_W - 2 * MX], rowHeights=[2])
    rule.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), LINE)]))
    out.append(Spacer(1, 3)); out.append(rule); out.append(Spacer(1, 9))
    return out


# ---------- page painters ----------
def paint_bg(c, doc):
    c.setFillColor(BG); c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)
    # subtle top glow band
    c.setFillColor(HexColor("#081726")); c.rect(0, PAGE_H - 26 * mm, PAGE_W, 26 * mm, stroke=0, fill=1)
    # header
    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, "V2 · SETUP & CONFIGURATION")
    c.setStrokeColor(LINE); c.setLineWidth(0.6)
    c.line(MX, PAGE_H - 19 * mm, PAGE_W - MX, PAGE_H - 19 * mm)
    # footer
    c.line(MX, 14 * mm, PAGE_W - MX, 14 * mm)
    c.setFont("Segoe", 8); c.setFillColor(DIM)
    c.drawString(MX, 10 * mm, "NCSC CAF · UK Cyber Security and Resilience Bill aligned")
    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)
    # grid pattern
    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)
    # central emblem
    cx, cy = PAGE_W / 2, PAGE_H - 82 * mm
    c.setStrokeColor(GREEN); c.setLineWidth(2.4)
    r = 20 * mm
    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)
    c.setStrokeColor(GREEN); c.setLineWidth(3)
    c.line(cx - 8 * mm, cy, cx - 2 * mm, cy - 6 * mm)
    c.line(cx - 2 * mm, cy - 6 * mm, cx + 9 * mm, cy + 7 * mm)
    # palette bar
    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)


# ---------- build ----------
def build(path):
    doc = BaseDocTemplate(path, pagesize=A4, leftMargin=MX, rightMargin=MX,
                          topMargin=24 * mm, bottomMargin=18 * mm,
                          title="Airport Cyber Resilience Simulator V2 — Setup & Configuration 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 · SETUP &amp; CONFIGURATION GUIDE", st_cover_s))
    story.append(Spacer(1, 6 * mm))
    story.append(Paragraph("A step-by-step guide to deploying, configuring and operating the "
                           "containerised kiosk simulator — from Docker Compose to your first "
                           "live attack scenario.", st_cover_d))
    story.append(NextPageTemplate("content"))
    story.append(PageBreak())

    # ---------- 1. OVERVIEW ----------
    story += section("What you are deploying", "Overview")
    story.append(Paragraph(
        "The Airport Cyber Resilience Simulator is a self-contained, kiosk-grade training and "
        "exhibition tool. A Python (FastAPI) engine runs a per-second simulation loop and streams "
        "live state over WebSockets to an immersive React command-center dashboard. Everything is "
        "data-driven and fully containerised — one command brings the whole stack up.", st_body))
    story.append(Spacer(1, 6))
    story.append(info_table(
        [["Backend", "Python 3.11 · FastAPI · asyncio · WebSockets"],
         ["Frontend", "React + GSAP + Web Audio (all vendored, no CDN needed)"],
         ["Delivery", "Docker + Docker Compose (single service, port 8000)"],
         ["Data", "29 UK airports · 26 nodes · 9 attack playbooks (JSON)"],
         ["Compliance", "NCSC CAF objectives + UK Cyber Security and Resilience Bill"]],
        ["Layer", "Technology"], [40 * mm, PAGE_W - 2 * MX - 40 * mm]))
    story.append(Spacer(1, 12))

    story += section("Setup", "Deployment")

    story.append(step(1, "Confirm prerequisites", "green", [
        Paragraph("Install <b>Docker Desktop</b> (Windows/macOS) or Docker Engine + Compose "
                  "(Linux). This is the only requirement — Python and all frontend libraries ship "
                  "inside the image.", st_body),
        Spacer(1, 5),
        Paragraph("Verify the toolchain is present and the daemon is running:", st_dim),
        Spacer(1, 4),
        code_block(["docker --version", "docker compose version", "docker info"]),
        Spacer(1, 4),
        Paragraph("No Docker? You can also run it directly with Python 3.11+: "
                  "<font name='Mono' color='#38e6ff'>pip install -r backend/requirements.txt</font> "
                  "then <font name='Mono' color='#38e6ff'>uvicorn main:app --port 8000</font>.", st_dim),
    ]))

    story.append(step(2, "Get the project files", "cyan", [
        Paragraph("Place the <font name='Mono'>airport-cyber-sim-v2/</font> folder anywhere on the "
                  "host. The structure you should see:", st_body),
        Spacer(1, 5),
        code_block([
            "airport-cyber-sim-v2/",
            "  docker-compose.yml        sim.ps1 / Makefile   README.md",
            "  backend/",
            "    Dockerfile  main.py  requirements.txt",
            "    data/    airports.json  infrastructure.json  playbooks.json",
            "    static/  index.html  app.js  styles.css  vendor/",
        ]),
    ]))

    story.append(step(3, "Launch with Docker Compose", "violet", [
        Paragraph("From the project root, build and start the container:", st_body),
        Spacer(1, 4),
        code_block(["cd airport-cyber-sim-v2", "docker compose up --build"]),
        Spacer(1, 5),
        Paragraph("The first build downloads the Python base image and installs dependencies "
                  "(about a minute). When you see <font name='Mono' color='#39ff9e'>Uvicorn running "
                  "on 0.0.0.0:8000</font>, the simulator is live.", st_dim),
        Spacer(1, 5),
        Paragraph("Prefer shorthand? Use the bundled helpers:", st_dim),
        Spacer(1, 4),
        code_block([".\\sim.ps1 up          # Windows PowerShell",
                    "make up               # macOS / Linux / WSL"]),
    ]))

    story.append(step(4, "Open the kiosk", "amber", [
        Paragraph("Browse to the address below on the kiosk display. For a true full-screen "
                  "exhibition kiosk, press <b>F11</b> (or launch Chrome/Edge with "
                  "<font name='Mono'>--kiosk http://localhost:8000</font>).", st_body),
        Spacer(1, 4),
        code_block(["http://localhost:8000"]),
        Spacer(1, 4),
        Paragraph("The stage is locked to a 1920×1080 layout and auto-scales to fit any screen, "
                  "so it looks correct on portrait tablets and 4K panels alike.", st_dim),
    ]))

    story.append(PageBreak())

    # ---------- CONFIGURATION ----------
    story += section("Configuration &amp; first run", "Operating the kiosk")

    story.append(step(5, "Profile the airport", "green", [
        Paragraph("On the <b>Initial Configuration Screen</b>, choose one of the 29 UK passenger "
                  "airports. Your selection scales the entire economic model — Heathrow drives the "
                  "highest revenue-per-second and maximum statutory fines; regional airports scale "
                  "down proportionally.", st_body),
        Spacer(1, 4),
        Paragraph("Toggle <b>Enable Simulation Audio</b> to match your stand's noise environment, "
                  "then press <b>INITIALIZE SIMULATOR</b> to enter the dashboard.", st_dim),
    ]))

    story.append(step(6, "Arm and launch a playbook", "cyan", [
        Paragraph("In the left sidebar, pick one of the 9 attack playbooks, then press "
                  "<b>START SIMULATION</b>. A dramatic 5-second countdown plays; the simulation "
                  "clock, financial bleed and the point-of-ingress indicator only begin when it "
                  "hits zero.", st_body),
    ]))

    story.append(step(7, "Respond to the incident", "violet", [
        Paragraph("Tap any node to open its overlay — service detail, live capacity, threat vector, "
                  "mitigation options and a <b>Regulatory Insights</b> tab mapping the node to its "
                  "CAF objective and statutory reporting duty. Choose a mitigation deliberately; "
                  "each carries an operational trade-off:", st_body),
        Spacer(1, 6),
        info_table(
            [["1 · Isolate Node", "Severs links, halts spread", "→ 0% (runway lights go dark)", "Max operational bleed"],
             ["2 · Reduce / Throttle", "Slows spread by 80%", "→ 40% (lights dim &amp; flicker)", "Moderate"],
             ["3 · Stop Attack (Patch)", "5s patch race — reverts to normal only if it beats the attack to the next node", "Unchanged", "£50k flat fee"]],
            ["Mitigation", "Effect on spread", "Capacity", "Cost"],
            [28 * mm, 43 * mm, 44 * mm, (PAGE_W - 2 * MX - 24) - 115 * mm]),
    ]))

    story.append(step(8, "Work the Crisis Communications module", "amber", [
        Paragraph("Once an attack begins you have a 15-second window to <b>Release a Proactive "
                  "Media Statement</b>, halving the reputational-loss multiplier. Miss it and the "
                  "phone and email icons flash red — answer them to hold the baseline penalty. "
                  "Ignore the barrage and a communications blackout <b>triples</b> the reputational "
                  "bleed.", st_body),
    ]))

    story.append(step(9, "Reset between visitors", "red", [
        Paragraph("Press <b>RESET TO CONFIG</b> to clear all state and return to airport selection. "
                  "If the kiosk is idle for 90 seconds it auto-resets to an animated attract loop, "
                  "ready for the next visitor.", st_body),
    ]))

    story.append(Spacer(1, 4))
    story += section("The math engine", "Understanding the metrics")
    story.append(Paragraph("Every second the backend recomputes total loss and streams it to the "
                           "odometer, which shifts colour white → amber → neon-red as it climbs:", st_body))
    story.append(Spacer(1, 6))
    story.append(code_block([
        "Total_Loss = Operational_Cost + Regulatory_Fines + Mitigation_Costs",
        "",
        "Operational_Cost += Revenue_Per_Sec x Node_Impact x Downtime_Penalty",
        "                    x Reputational_Multiplier      (every second)",
        "Regulatory_Fines += Node_Flat_Fine x Airport_Fine_Multiplier",
        "                    (on breach of a data-sensitive node)",
        "Mitigation_Costs += 50,000            (per emergency patch deployed)",
    ]))
    story.append(Spacer(1, 6))
    story += bullets([
        "<b>Downtime penalties:</b> Normal 0.0 · Throttled 0.6 · Isolated 1.0.",
        "<b>Impact factors</b> are per-node (ATC ≈ 0.85, Power ≈ 0.88, Retail POS ≈ 0.10).",
        "<b>Reputational multiplier:</b> 0.5 proactive · 1.0 baseline · 3.0 blackout.",
    ], GREEN)

    story.append(Spacer(1, 10))
    story += section("Customise &amp; troubleshoot", "Reference")
    story.append(Paragraph("The simulator is fully data-driven — extend it by editing the JSON in "
                           "<font name='Mono'>backend/data/</font>; the compose file mounts that "
                           "folder read-only so changes hot-reload without a rebuild.", st_body))
    story.append(Spacer(1, 6))
    story.append(info_table(
        [["Add an airport / change scalars", "backend/data/airports.json"],
         ["Add nodes, links or impact factors", "backend/data/infrastructure.json"],
         ["Add or edit attack playbooks", "backend/data/playbooks.json"],
         ["Port 8000 already in use", "Edit the ports mapping in docker-compose.yml"],
         ["Check container health / logs", "sim.ps1 status  ·  sim.ps1 logs"],
         ["Stop everything", "docker compose down  ·  sim.ps1 down"]],
        ["Task", "Where"], [78 * mm, PAGE_W - 2 * MX - 78 * mm], head_color=AMBER))

    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-Setup-Guide.pdf"))
