#!/usr/bin/env python3
"""Generate executive PDF brief for Kedebah Commerce new architecture."""

from pathlib import Path

from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.platypus import (
    HRFlowable,
    KeepTogether,
    ListFlowable,
    ListItem,
    PageBreak,
    Paragraph,
    SimpleDocTemplate,
    Spacer,
    Table,
    TableStyle,
)

OUT = Path(__file__).resolve().parent / "Kedebah_Commerce_Architecture_Executive_Brief.pdf"

# Palette — professional teal/slate (avoid purple/cream AI clichés)
NAVY = colors.HexColor("#0F2A3D")
TEAL = colors.HexColor("#0D7377")
TEAL_LIGHT = colors.HexColor("#E6F3F3")
SLATE = colors.HexColor("#334155")
MUTED = colors.HexColor("#64748B")
LINE = colors.HexColor("#CBD5E1")
WHITE = colors.white
AMBER_BG = colors.HexColor("#FFF7ED")
AMBER = colors.HexColor("#9A3412")
GREEN_BG = colors.HexColor("#ECFDF5")
GREEN = colors.HexColor("#065F46")


def styles():
    base = getSampleStyleSheet()
    s = {
        "cover_title": ParagraphStyle(
            "cover_title",
            parent=base["Title"],
            fontName="Helvetica-Bold",
            fontSize=22,
            leading=28,
            textColor=NAVY,
            alignment=TA_LEFT,
            spaceAfter=6,
        ),
        "cover_sub": ParagraphStyle(
            "cover_sub",
            parent=base["Normal"],
            fontName="Helvetica",
            fontSize=11,
            leading=15,
            textColor=MUTED,
            spaceAfter=4,
        ),
        "h1": ParagraphStyle(
            "h1",
            parent=base["Heading1"],
            fontName="Helvetica-Bold",
            fontSize=14,
            leading=18,
            textColor=NAVY,
            spaceBefore=14,
            spaceAfter=8,
        ),
        "h2": ParagraphStyle(
            "h2",
            parent=base["Heading2"],
            fontName="Helvetica-Bold",
            fontSize=11,
            leading=14,
            textColor=TEAL,
            spaceBefore=10,
            spaceAfter=5,
        ),
        "body": ParagraphStyle(
            "body",
            parent=base["Normal"],
            fontName="Helvetica",
            fontSize=9.5,
            leading=13,
            textColor=SLATE,
            alignment=TA_JUSTIFY,
            spaceAfter=6,
        ),
        "bullet": ParagraphStyle(
            "bullet",
            parent=base["Normal"],
            fontName="Helvetica",
            fontSize=9.5,
            leading=12.5,
            textColor=SLATE,
            leftIndent=0,
        ),
        "callout": ParagraphStyle(
            "callout",
            parent=base["Normal"],
            fontName="Helvetica",
            fontSize=9,
            leading=12,
            textColor=NAVY,
        ),
        "footer": ParagraphStyle(
            "footer",
            parent=base["Normal"],
            fontName="Helvetica",
            fontSize=8,
            textColor=MUTED,
            alignment=TA_CENTER,
        ),
        "table_head": ParagraphStyle(
            "table_head",
            parent=base["Normal"],
            fontName="Helvetica-Bold",
            fontSize=8.5,
            leading=11,
            textColor=WHITE,
        ),
        "table_cell": ParagraphStyle(
            "table_cell",
            parent=base["Normal"],
            fontName="Helvetica",
            fontSize=8.5,
            leading=11,
            textColor=SLATE,
        ),
        "small": ParagraphStyle(
            "small",
            parent=base["Normal"],
            fontName="Helvetica",
            fontSize=8,
            leading=10.5,
            textColor=MUTED,
            spaceAfter=4,
        ),
    }
    return s


def header_footer(canvas, doc):
    canvas.saveState()
    canvas.setStrokeColor(LINE)
    canvas.setLineWidth(0.5)
    canvas.line(18 * mm, A4[1] - 12 * mm, A4[0] - 18 * mm, A4[1] - 12 * mm)
    canvas.setFont("Helvetica", 8)
    canvas.setFillColor(MUTED)
    canvas.drawString(18 * mm, A4[1] - 10 * mm, "Kedebah Commerce — Confidential")
    canvas.drawRightString(A4[0] - 18 * mm, A4[1] - 10 * mm, "Executive Architecture Brief")
    canvas.line(18 * mm, 14 * mm, A4[0] - 18 * mm, 14 * mm)
    canvas.drawCentredString(A4[0] / 2, 9 * mm, f"Page {doc.page}")
    canvas.restoreState()


def callout_box(text, style, bg, border):
    p = Paragraph(text, style)
    t = Table([[p]], colWidths=[170 * mm])
    t.setStyle(
        TableStyle(
            [
                ("BACKGROUND", (0, 0), (-1, -1), bg),
                ("BOX", (0, 0), (-1, -1), 0.8, border),
                ("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 styled_table(headers, rows, col_widths, s):
    data = [[Paragraph(h, s["table_head"]) for h in headers]]
    for row in rows:
        data.append([Paragraph(c, s["table_cell"]) for c in row])
    t = Table(data, colWidths=col_widths, repeatRows=1)
    style_cmds = [
        ("BACKGROUND", (0, 0), (-1, 0), NAVY),
        ("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
        ("ALIGN", (0, 0), (-1, 0), "LEFT"),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("GRID", (0, 0), (-1, -1), 0.4, LINE),
        ("LEFTPADDING", (0, 0), (-1, -1), 5),
        ("RIGHTPADDING", (0, 0), (-1, -1), 5),
        ("TOPPADDING", (0, 0), (-1, -1), 5),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
        ("BACKGROUND", (0, 1), (-1, -1), WHITE),
    ]
    for i in range(1, len(data)):
        if i % 2 == 0:
            style_cmds.append(("BACKGROUND", (0, i), (-1, i), TEAL_LIGHT))
    t.setStyle(TableStyle(style_cmds))
    return t


def bullets(items, s):
    return ListFlowable(
        [ListItem(Paragraph(i, s["bullet"]), leftIndent=8, bulletColor=TEAL) for i in items],
        bulletType="bullet",
        start="•",
        leftIndent=12,
        bulletFontSize=9,
        spaceBefore=2,
        spaceAfter=6,
    )


def build():
    s = styles()
    doc = SimpleDocTemplate(
        str(OUT),
        pagesize=A4,
        leftMargin=18 * mm,
        rightMargin=18 * mm,
        topMargin=18 * mm,
        bottomMargin=18 * mm,
        title="Kedebah Commerce Architecture & Scaling — Executive Brief",
        author="Kedebah Platform Engineering",
    )
    story = []

    # ----- Cover -----
    story.append(Spacer(1, 8 * mm))
    story.append(Paragraph("Kedebah Commerce", s["cover_title"]))
    story.append(
        Paragraph(
            "New Deployment Architecture, Performance Benefits &amp; Scaling Strategy",
            s["cover_sub"],
        )
    )
    story.append(Paragraph("Executive Brief — Top Up Pharmacy Go-Live Platform", s["cover_sub"]))
    story.append(Spacer(1, 3 * mm))
    story.append(HRFlowable(width="100%", thickness=2, color=TEAL, spaceAfter=8))
    story.append(
        Paragraph(
            "<b>Purpose:</b> Explain why the new containerised Commerce stack is faster and more "
            "resilient, what it means for multi-outlet POS (Top Up + merged Lena / Express), and "
            "exactly what to adjust when traffic grows — without requiring engineering jargon.",
            s["body"],
        )
    )
    story.append(
        callout_box(
            "<b>One-sentence summary:</b> We separated the public website edge, the Finance API, "
            "background jobs, realtime websockets, and cache into dedicated containers; tuned PHP "
            "and Nginx for concurrent tills; and protect PostgreSQL with PgBouncer — so more "
            "outlets can trade at once without the old “everything fights for one process” pattern.",
            s["callout"],
            TEAL_LIGHT,
            TEAL,
        )
    )

    # ----- 1. Why -----
    story.append(Paragraph("1. Why this architecture change", s["h1"]))
    story.append(
        Paragraph(
            "Commerce is no longer a single-tenant light load. Top Up Pharmacy now runs as the "
            "survivor on new infrastructure, with Lena and Express Care merged into the same "
            "tenant footprint. That means more concurrent POS sessions, heavier stock and "
            "invoice APIs, and background work (alerts, email, tenant maintenance) that must "
            "not slow the till.",
            s["body"],
        )
    )
    story.append(
        Paragraph(
            "The previous pattern mixed web traffic, jobs, and supporting services more tightly. "
            "Under peak outlet activity, request latency and queue backlog rose together. The new "
            "deployment (<b>kedebah-commerce-deploy</b>) isolates those concerns and sizes each "
            "layer for concurrent use.",
            s["body"],
        )
    )

    # ----- 2. Architecture -----
    story.append(Paragraph("2. How the new stack is organised", s["h1"]))
    story.append(
        Paragraph(
            "Users hit <b>topup.kedebah.com</b> (and the finance portal). Only the <b>edge</b> "
            "proxy is public. Everything else communicates on a private Docker network.",
            s["body"],
        )
    )
    arch_rows = [
        ["Edge (Nginx + TLS)", "Public entry; routes SPA, API, storage, websockets"],
        ["Finance API", "Core Commerce / POS / inventory / invoices API"],
        ["Commerce SPA", "Browser POS &amp; inventory UI"],
        ["Redis", "Cache &amp; sessions (less load on the database)"],
        ["Reverb", "Realtime inventory / commerce events"],
        ["Dedicated workers", "Queues &amp; schedules (jobs off the web path)"],
        ["Auth / Onboarding / Email / SMS", "Supporting services, also containerised"],
        ["PostgreSQL + PgBouncer (host)", "Shared DB; pooling so connections do not explode"],
    ]
    story.append(
        styled_table(
            ["Layer", "Role"],
            arch_rows,
            [55 * mm, 115 * mm],
            s,
        )
    )
    story.append(Spacer(1, 3 * mm))
    story.append(
        Paragraph(
            "<i>Diagram (simplified):</i> Internet → Edge → Finance / Commerce / Reverb; "
            "workers consume queues in the background; all apps → PgBouncer → PostgreSQL.",
            s["small"],
        )
    )

    # ----- 3. Benefits -----
    story.append(Paragraph("3. Business &amp; operational benefits", s["h1"]))
    story.append(Paragraph("Faster under concurrent use", s["h2"]))
    story.append(
        bullets(
            [
                "<b>~216 concurrent PHP request slots</b> across services (Finance alone up to "
                "<b>96</b> workers) — many tills and browsers can be served at once.",
                "<b>OPcache + JIT</b> keeps PHP bytecode hot in memory — less CPU wasted recompiling "
                "the same Laravel code on every request.",
                "<b>Nginx keep-alive</b> and a high-capacity edge reduce connection churn at peak.",
                "<b>Redis</b> for cache/sessions reduces repetitive database work.",
            ],
            s,
        )
    )
    story.append(Paragraph("More stable when busy", s["h2"]))
    story.append(
        bullets(
            [
                "Web traffic no longer shares the same process as email, stock backfill, or "
                "scheduled alerts — <b>background work cannot starve the till</b>.",
                "<b>PgBouncer</b> pools DB connections so raising PHP capacity does not open "
                "hundreds of raw Postgres connections.",
                "Workers recycle periodically (memory hygiene); config/route caches warm on start.",
            ],
            s,
        )
    )
    story.append(Paragraph("Easier to grow deliberately", s["h2"]))
    story.append(
        bullets(
            [
                "Clear knobs: raise PHP workers, scale a worker container, or add a Finance API "
                "replica — without redesigning the product.",
                "Same compose project for deploy, rebuild, and rollback — fewer “works on one "
                "server, mystery on another” incidents.",
            ],
            s,
        )
    )

    # ----- 4. Capacity defaults -----
    story.append(Paragraph("4. Capacity we ship by default", s["h1"]))
    story.append(
        Paragraph(
            "Sized for multi-outlet POS go-live. Approximate RAM budget for this Commerce stack: "
            "<b>14–18 GB</b> on a large shared host.",
            s["body"],
        )
    )
    story.append(
        styled_table(
            ["Service", "PHP workers (max)", "Approx RAM"],
            [
                ["Finance (main API)", "96", "~6.0 GB"],
                ["Commerce SPA host", "32", "~2.0 GB"],
                ["Auth", "32", "~2.0 GB"],
                ["Onboarding", "24", "~1.5 GB"],
                ["Email / Tenant / SMS", "16 each", "~1.0 GB each"],
                ["Redis (cache/session)", "—", "Capped (e.g. 2 GB)"],
            ],
            [55 * mm, 55 * mm, 60 * mm],
            s,
        )
    )

    # ----- 5. Current host -----
    story.append(PageBreak())
    story.append(Paragraph("5. Current host snapshot (live environment)", s["h1"]))
    story.append(
        Paragraph(
            "Snapshot from the production Ubuntu host running Top Up Commerce (shared machine). "
            "Figures rounded for executive readability.",
            s["body"],
        )
    )
    story.append(
        styled_table(
            ["Metric", "Observed", "Reading"],
            [
                [
                    "Memory",
                    "62 GB total; ~20 GB used; ~35 GB available",
                    "Comfortable headroom for Commerce; host is shared with other apps",
                ],
                [
                    "Swap in use",
                    "~14 GB of 31 GB",
                    "Indicates past or ongoing memory pressure on the host overall — watch closely as POS load grows",
                ],
                [
                    "CPU",
                    "~96% idle; load ~1.4–2.7",
                    "Not CPU-bound at this moment; latency issues more likely DB/query or concurrency saturation",
                ],
                [
                    "Kedebah-relevant processes",
                    "Docker (Commerce stack), php-fpm / queue workers, PgBouncer, Postgres (tenant_topup_pharmacy, kedebah_v2)",
                    "Commerce path is active and healthy in this sample",
                ],
            ],
            [38 * mm, 62 * mm, 70 * mm],
            s,
        )
    )
    story.append(Spacer(1, 3 * mm))
    story.append(
        callout_box(
            "<b>Important context for leadership:</b> This host also runs other platforms "
            "(e.g. MySQL, Redis, Node/Python apps, AI/LLM-related processes). Commerce capacity "
            "planning must reserve RAM for those neighbours. Long-term, a dedicated Commerce "
            "host (or moving co-tenants off) unlocks higher Finance worker counts safely.",
            s["callout"],
            AMBER_BG,
            AMBER,
        )
    )

    # ----- 6. Scaling playbook -----
    story.append(Paragraph("6. What happens when traffic increases — and what to tweak", s["h1"]))
    story.append(
        Paragraph(
            "Think of capacity as a funnel. Users hit the edge → PHP workers handle requests → "
            "each busy worker may need a DB connection via PgBouncer → Postgres executes queries. "
            "<b>The database is usually the true ceiling</b>, not Nginx.",
            s["body"],
        )
    )

    story.append(Paragraph("Stage A — Mild growth (more outlets / more concurrent tills)", s["h2"]))
    story.append(
        bullets(
            [
                "<b>Symptom:</b> Occasional slow API calls; FPM status shows workers often busy "
                "or “max children reached”.",
                "<b>Tweak:</b> Raise <b>FINANCE_FPM_MAX_CHILDREN</b> (e.g. 96 → 112) in deploy "
                "<b>.env</b>, recreate finance container. Confirm free RAM and PgBouncer limits first.",
                "<b>Also check:</b> Redis memory under cap; edge/proxy timeouts already set for "
                "long POS operations (~180s).",
            ],
            s,
        )
    )

    story.append(Paragraph("Stage B — Queue / job backlog (alerts, email, stock jobs lag)", s["h2"]))
    story.append(
        bullets(
            [
                "<b>Symptom:</b> UI feels fine but notifications, mail, or background stock work "
                "arrive late.",
                "<b>Tweak:</b> Scale workers — e.g. "
                "<font face='Courier'>docker compose up -d --scale finance-worker=2</font> "
                "(same pattern for email/onboarding if needed).",
                "<b>Why it works:</b> Jobs use separate containers; adding workers increases "
                "throughput without changing the website.",
            ],
            s,
        )
    )

    story.append(Paragraph("Stage C — Sustained API saturation (Finance always at max)", s["h2"]))
    story.append(
        bullets(
            [
                "<b>Symptom:</b> Finance FPM saturated even after raising workers; response times "
                "climb across outlets.",
                "<b>Tweak:</b> Add a second Finance API replica — "
                "<font face='Courier'>docker compose up -d --scale finance=2</font>. "
                "Edge already load-balances via Docker DNS.",
                "<b>Prerequisite:</b> Enough free RAM (~6 GB+ per extra finance replica) and "
                "PgBouncer sized for the extra clients.",
            ],
            s,
        )
    )

    story.append(Paragraph("Stage D — Database pressure (the most common hard limit)", s["h2"]))
    story.append(
        bullets(
            [
                "<b>Symptom:</b> High wait times in PgBouncer; Postgres busy; API slow even with "
                "spare PHP workers.",
                "<b>Tweak:</b> Ensure <b>pool_mode = transaction</b>; raise "
                "<b>max_client_conn</b> (e.g. ≥ 600) and <b>default_pool_size</b> (e.g. 50–80); "
                "apps must connect to <b>PgBouncer</b>, not Postgres directly.",
                "<b>Product/engineering follow-up:</b> Optimise heavy endpoints (large stock "
                "payloads, N+1 queries) — infra cannot fix an expensive query by itself.",
            ],
            s,
        )
    )

    story.append(
        KeepTogether(
            [
                Paragraph("Quick decision table", s["h2"]),
                styled_table(
                    ["If you see…", "First action", "Owner"],
                    [
                        [
                            "Tills waiting / API timeouts at peak",
                            "Check Finance FPM status → raise FINANCE_FPM_MAX_CHILDREN if maxed",
                            "Platform",
                        ],
                        [
                            "Jobs / email / alerts delayed",
                            "Scale the relevant *-worker container",
                            "Platform",
                        ],
                        [
                            "DB waits / PgBouncer saturated",
                            "Tune PgBouncer pools; review slow queries",
                            "Platform + DBA / Eng",
                        ],
                        [
                            "Host RAM / swap climbing",
                            "Do not raise FPM further; free RAM or move co-hosted apps",
                            "Infra / Leadership",
                        ],
                        [
                            "Only one slow screen (e.g. huge stock list)",
                            "Application optimisation — not more servers first",
                            "Engineering",
                        ],
                    ],
                    [48 * mm, 82 * mm, 40 * mm],
                    s,
                ),
            ]
        )
    )

    # ----- 7. Guardrails -----
    story.append(Paragraph("7. Guardrails (so scaling stays safe)", s["h1"]))
    story.append(
        bullets(
            [
                "Budget <b>~50–70 MB RAM per PHP worker</b>. More workers without free RAM causes "
                "swap and makes the system slower, not faster.",
                "Every busy PHP worker + every queue worker can hold a DB client — expect "
                "<b>~250–320</b> potential clients at current defaults; PgBouncer must absorb them.",
                "After OPcache production mode, <b>code changes require container recreate</b> "
                "(expected for performance).",
                "Prefer raising FPM on one Finance service before multi-replica unless RAM allows.",
            ],
            s,
        )
    )

    # ----- 8. Ask -----
    story.append(Paragraph("8. Recommended leadership takeaways", s["h1"]))
    story.append(
        callout_box(
            "<b>1.</b> The new architecture is a deliberate investment in concurrent POS "
            "reliability, not a cosmetic Docker move.<br/>"
            "<b>2.</b> We already have a clear growth path: PHP workers → more queue workers → "
            "Finance replicas → PgBouncer / DB — in that order of cost and complexity.<br/>"
            "<b>3.</b> The host is shared and already shows swap use; protecting Commerce "
            "performance long-term may mean dedicating resources (or reducing co-tenancy) before "
            "simply “turning numbers up”.<br/>"
            "<b>4.</b> Some slowness will remain application-level (large reports/stock payloads); "
            "those need product/engineering fixes alongside infra.",
            s["callout"],
            GREEN_BG,
            GREEN,
        )
    )
    story.append(Spacer(1, 6 * mm))
    story.append(HRFlowable(width="100%", thickness=1, color=LINE, spaceAfter=6))
    story.append(
        Paragraph(
            "Prepared for executive walkthrough · Kedebah Commerce / Top Up platform · "
            "Reference: kedebah-commerce-deploy (architecture + go-live load tuning).",
            s["small"],
        )
    )
    story.append(
        Paragraph(
            "Document version: July 2026 · For internal use",
            s["small"],
        )
    )

    doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
    print(f"Wrote {OUT}")


if __name__ == "__main__":
    build()
