#!/usr/bin/env python3
"""Build 20260521_next_gen_steps_egress_review.pdf from summary text and diagram PNGs."""

from __future__ import annotations

import subprocess
import sys
from pathlib import Path

from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.platypus import (
    Image,
    PageBreak,
    Paragraph,
    SimpleDocTemplate,
    Spacer,
    Table,
    TableStyle,
)

ROOT = Path(__file__).resolve().parents[2]
BUILD = Path(__file__).resolve().parent
OUT_PDF = ROOT / "20260521_next_gen_steps_egress_review.pdf"

MM = [
    ("diagram_current_arch.mmd", "diagram_current_arch.png", "Current sender path (1 encode, N unicast sends)"),
    ("diagram_multicast_proposal.mmd", "diagram_multicast_proposal.png", "Proposed multicast setup (handshake then one media flow)"),
]


def render_mermaid() -> None:
    for mmd, png, _ in MM:
        src = BUILD / mmd
        dst = BUILD / png
        subprocess.run(
            [
                "npx",
                "--yes",
                "@mermaid-js/mermaid-cli@11.4.0",
                "-i",
                str(src),
                "-o",
                str(dst),
                "-b",
                "white",
                "-w",
                "1400",
                "-H",
                "900",
            ],
            cwd=str(ROOT),
            check=True,
            timeout=180,
        )


def p(text: str, style) -> Paragraph:
    return Paragraph(text.replace("\n", "<br/>"), style)


def build_pdf() -> None:
    styles = getSampleStyleSheet()
    title = ParagraphStyle(
        "DocTitle",
        parent=styles["Title"],
        fontSize=18,
        spaceAfter=12,
        textColor=colors.HexColor("#1565C0"),
    )
    h1 = ParagraphStyle("H1", parent=styles["Heading1"], fontSize=14, spaceBefore=14, spaceAfter=8)
    h2 = ParagraphStyle("H2", parent=styles["Heading2"], fontSize=12, spaceBefore=10, spaceAfter=6)
    body = ParagraphStyle("Body", parent=styles["Normal"], fontSize=10, leading=14, alignment=TA_LEFT)
    small = ParagraphStyle("Small", parent=body, fontSize=9, textColor=colors.grey)

    story = []
    story.append(p("Android Cast — Next-gen egress review", title))
    story.append(
        p(
            "Summary of sender 1:N behavior, bandwidth model, limitations, and feasibility "
            "of single-stream multicast plus multi-tier (full / 720p / 480p) renditions. "
            "Generated 2026-05-21.",
            small,
        )
    )
    story.append(Spacer(1, 0.4 * cm))

    # --- Current state ---
    story.append(p("1. Current sender behavior", h1))
    story.append(
        p(
            "<b>How many receivers?</b> Up to 5 when MULTI_RECEIVER_ENABLED "
            "(CastConfig.MAX_RECEIVERS). Sender opens one CastSession per selected device; "
            "failed handshakes are skipped if at least one peer connects.",
            body,
        )
    )
    story.append(Spacer(1, 0.2 * cm))
    story.append(
        p(
            "<b>CPU / GPU / memory:</b> Capture and encode run once per cast. "
            "CastFanoutPump duplicates each encoded frame to every session. "
            "Network send and per-peer UDP/FEC state scale with N; encode cost does not.",
            body,
        )
    )
    story.append(Spacer(1, 0.2 * cm))
    story.append(
        p(
            "<b>Bandwidth:</b> Per-receiver unicast duplicate of the same bitstream — not IP multicast. "
            "Rough LAN egress ≈ (video + ~96 kbps audio) × N, plus per-peer protocol overhead.",
            body,
        )
    )
    story.append(Spacer(1, 0.5 * cm))

    img1 = BUILD / MM[0][1]
    if img1.exists():
        story.append(p(MM[0][2], h2))
        iw = 16 * cm
        story.append(Image(str(img1), width=iw, height=iw * 0.45))
        story.append(Spacer(1, 0.4 * cm))

    # Table current limits
    story.append(p("1.1 Current limitations (sender-focused)", h2))
    limits = [
        ["Area", "Limitation"],
        ["Primary peer", "First connected peer sets negotiated MIME, adaptive tuning, and network feedback."],
        ["Codec", "Single encoder; all peers must tolerate the same bitstream."],
        ["Send path", "One CastFanoutPump thread; serial fan-out to N sessions."],
        ["Wi‑Fi", "Uplink often saturates before CPU on 3–5 receivers."],
        ["Topology", "Sender dials out; no shared LAN broadcast stream today."],
    ]
    t = Table(limits, colWidths=[3.2 * cm, 13.3 * cm])
    t.setStyle(
        TableStyle(
            [
                ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
                ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
                ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
                ("FONTSIZE", (0, 0), (-1, -1), 9),
                ("VALIGN", (0, 0), (-1, -1), "TOP"),
                ("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
                ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
                ("LEFTPADDING", (0, 0), (-1, -1), 6),
                ("RIGHTPADDING", (0, 0), (-1, -1), 6),
                ("TOPPADDING", (0, 0), (-1, -1), 4),
                ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
            ]
        )
    )
    story.append(t)
    story.append(PageBreak())

    # --- Proposal 1 ---
    story.append(p("2. Proposal: single outgoing stream (multicast)", h1))
    story.append(
        p(
            "<b>Goal:</b> One encoded stream on the air; only handshake-authorized receivers consume it; "
            "save ~(N−1) × bitrate vs today’s unicast fan-out.",
            body,
        )
    )
    story.append(Spacer(1, 0.2 * cm))
    story.append(
        p(
            "<b>Feasible</b> with UDP IP multicast + unicast handshake (group address, port, sessionId, PIN). "
            "Handshake-only targeting without multicast does <b>not</b> reduce bandwidth. "
            "TCP/QUIC cannot deliver true 1× Wi‑Fi airtime.",
            body,
        )
    )
    story.append(Spacer(1, 0.3 * cm))

    img2 = BUILD / MM[1][1]
    if img2.exists():
        story.append(p(MM[1][2], h2))
        iw = 16 * cm
        story.append(Image(str(img2), width=iw, height=iw * 0.55))
        story.append(Spacer(1, 0.4 * cm))

    story.append(p("2.1 Approach comparison", h2))
    approaches = [
        ["Approach", "1× sender airtime?", "Notes"],
        ["Unicast fan-out (today)", "No", "Same encode; N × send()."],
        ["UDP multicast", "Yes on good LAN", "MulticastLock; AP client isolation risk."],
        ["Sender server, N TCP clients", "No", "Flip connect direction; still N wire copies."],
        ["LAN relay receiver", "Sender 1×", "Relay pays duplicate bandwidth."],
    ]
    t2 = Table(approaches, colWidths=[4.5 * cm, 3.2 * cm, 8.8 * cm])
    t2.setStyle(
        TableStyle(
            [
                ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
                ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
                ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
                ("FONTSIZE", (0, 0), (-1, -1), 9),
                ("VALIGN", (0, 0), (-1, -1), "TOP"),
                ("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
                ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
                ("LEFTPADDING", (0, 0), (-1, -1), 6),
                ("RIGHTPADDING", (0, 0), (-1, -1), 6),
                ("TOPPADDING", (0, 0), (-1, -1), 4),
                ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
            ]
        )
    )
    story.append(t2)
    story.append(Spacer(1, 0.3 * cm))
    story.append(
        p(
            "<b>Protocol notes:</b> CastFramingContext.sessionId and CastSessionGate already support "
            "session binding. FEC/NACK must become group-scoped (one retransmit cache, resend to multicast). "
            "Existing code paths: MultiCastCoordinator, CastFanoutPump, UdpCastTransport.",
            body,
        )
    )
    story.append(PageBreak())

    # --- Proposal 2 ---
    story.append(p("3. Proposal: multi-tier streams (e.g. full + 720p + 480p)", h1))
    story.append(
        p(
            "<b>Goal:</b> Several encoded renditions from one capture; each receiver picks a tier at handshake "
            "(e.g. full quality vs 480p for bandwidth-limited devices).",
            body,
        )
    )
    story.append(Spacer(1, 0.2 * cm))
    story.append(
        p(
            "<b>Feasible</b> via multiple encoders fed from one capture (EGL / scaled Surfaces into separate "
            "MediaCodec instances). Cost scales with <b>active</b> tiers (~2–3 HW encoders typical phone limit; "
            "3× software VP9 is heavy). Audio usually stays one AAC stream.",
            body,
        )
    )
    story.append(Spacer(1, 0.2 * cm))
    story.append(
        p(
            "<b>Bandwidth:</b> Unicast per tier → egress ≈ sum of each peer’s tier bitrate. "
            "Multicast per tier → one group per tier; airtime ≈ sum of tiers with ≥1 subscriber.",
            body,
        )
    )
    story.append(Spacer(1, 0.3 * cm))
    story.append(p("3.1 Tier architecture options", h2))
    tiers = [
        ["Option", "Verdict"],
        ["Multiple HW encoders from one capture", "Recommended v1."],
        ["One encode + CPU re-encode", "Too expensive for live cast."],
        ["SVC / simulcast in one bitstream", "Poor Android HW support; defer."],
        ["Receiver-only downscale", "Saves sender encode, not downlink bandwidth."],
    ]
    t3 = Table(tiers, colWidths=[6 * cm, 10.5 * cm])
    t3.setStyle(
        TableStyle(
            [
                ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
                ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
                ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
                ("FONTSIZE", (0, 0), (-1, -1), 9),
                ("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
                ("VALIGN", (0, 0), (-1, -1), "TOP"),
                ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
                ("LEFTPADDING", (0, 0), (-1, -1), 6),
                ("RIGHTPADDING", (0, 0), (-1, -1), 6),
                ("TOPPADDING", (0, 0), (-1, -1), 4),
                ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
            ]
        )
    )
    story.append(t3)
    story.append(PageBreak())

    # --- Rollout ---
    story.append(p("4. Recommended rollout", h1))
    rollout = [
        ["Phase", "Deliverable", "Benefit"],
        [
            "A — Tier selection (unicast)",
            "Handshake tier; 720p + 480p encoders; send chosen tier per peer",
            "Per-device quality without multicast; no AP surprises",
        ],
        [
            "B — Multicast one tier",
            "UDP multicast publish after unicast handshake",
            "~(N−1) egress for same-quality 1:N",
        ],
        [
            "C — Multicast per tier",
            "Separate multicast group per tier; lazy encoder start",
            "Combines (1) and (2) on good LANs",
        ],
    ]
    t4 = Table(rollout, colWidths=[3.5 * cm, 7.5 * cm, 5.5 * cm])
    t4.setStyle(
        TableStyle(
            [
                ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
                ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
                ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
                ("FONTSIZE", (0, 0), (-1, -1), 9),
                ("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
                ("VALIGN", (0, 0), (-1, -1), "TOP"),
                ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
                ("LEFTPADDING", (0, 0), (-1, -1), 6),
                ("RIGHTPADDING", (0, 0), (-1, -1), 6),
                ("TOPPADDING", (0, 0), (-1, -1), 4),
                ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
            ]
        )
    )
    story.append(t4)
    story.append(Spacer(1, 0.4 * cm))

    story.append(p("4.1 Summary judgment", h2))
    summary = [
        ["Idea", "Feasible?", "Main win", "Main cost"],
        [
            "(1) Multicast + handshake gate",
            "Yes (UDP)",
            "~(N−1) uplink on good LAN",
            "Transport refactor; FEC/NACK; AP testing",
        ],
        ["(1) Unicast only", "Trivial", "Auth only", "No bandwidth win"],
        ["(2) Multi-tier encode", "Yes", "Per-receiver quality", "2–3× encode resources"],
        ["(1) + (2) combined", "Yes", "Multicast per tier", "Highest complexity"],
    ]
    t5 = Table(summary, colWidths=[4.2 * cm, 2.2 * cm, 4.5 * cm, 4.6 * cm])
    t5.setStyle(
        TableStyle(
            [
                ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
                ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
                ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
                ("FONTSIZE", (0, 0), (-1, -1), 8),
                ("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
                ("VALIGN", (0, 0), (-1, -1), "TOP"),
                ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
                ("LEFTPADDING", (0, 0), (-1, -1), 4),
                ("RIGHTPADDING", (0, 0), (-1, -1), 4),
                ("TOPPADDING", (0, 0), (-1, -1), 4),
                ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
            ]
        )
    )
    story.append(t5)
    story.append(Spacer(1, 0.5 * cm))
    story.append(
        p(
            "<b>Key code paths today:</b> CastConfig.MAX_RECEIVERS, MultiCastCoordinator, "
            "CastFanoutPump, ScreenCastService (primary peer), UdpCastTransport, CastFramingContext, "
            "CastSessionGate.",
            small,
        )
    )

    doc = SimpleDocTemplate(
        str(OUT_PDF),
        pagesize=A4,
        leftMargin=1.8 * cm,
        rightMargin=1.8 * cm,
        topMargin=1.5 * cm,
        bottomMargin=1.5 * cm,
        title="Android Cast — Next-gen egress review",
        author="Android Cast project",
    )
    doc.build(story)
    print(f"Wrote {OUT_PDF}")


def main() -> int:
    render_mermaid()
    build_pdf()
    return 0


if __name__ == "__main__":
    sys.exit(main())
