#!/usr/bin/env python3
"""Build docs/GIT_FLOW.pdf from structured content and Mermaid diagrams.

Diagrams: Mermaid CLI renders PNG via headless Chromium (text + layout correct).
Do NOT pipe Mermaid SVG through rsvg-convert — Mermaid uses foreignObject HTML labels
that librsvg drops (boxes only, no text).

Requires: reportlab, npx (@mermaid-js/mermaid-cli), Chromium (puppeteer bundled).
Regenerate: python3 docs/_pdf_build/build_git_flow_pdf.py
"""

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.lib.utils import ImageReader
from reportlab.platypus import (
    Image,
    PageBreak,
    Paragraph,
    SimpleDocTemplate,
    Spacer,
    Table,
    TableStyle,
)

ROOT = Path(__file__).resolve().parents[2]
BUILD = Path(__file__).resolve().parent
CONFIG = BUILD / "mermaid-config.json"
OUT_PDF = ROOT / "docs" / "GIT_FLOW.pdf"

# (mmd, png, caption, mermaid width px, height px, scale, max width cm in PDF)
DIAGRAMS = [
    ("git_flow_diagram_branches.mmd", "git_flow_diagram_branches.png",
     "Branch model (green master)", 1800, 900, 2, 15.5),
    ("git_flow_diagram_feature.mmd", "git_flow_diagram_feature.png",
     "Daily development", 2400, 2000, 2, 14.0),
    ("git_flow_diagram_release.mmd", "git_flow_diagram_release.png",
     "Release: next → master", 2400, 2000, 2, 14.0),
    ("git_flow_diagram_hotfix.mmd", "git_flow_diagram_hotfix.png",
     "Hotfix decision and flow", 1300, 1100, 2, 14.5),
]

MAX_DIAGRAM_HEIGHT_CM = 16.5

DOC_VERSION = "1.3"
DOC_DATE = "2026-05-23"
DOC_AUTHOR = "Anton Afanaasyeu"
DOC_SOURCE = "docs/GIT_FLOW.md"


def render_mermaid() -> None:
    if not CONFIG.exists():
        raise FileNotFoundError(CONFIG)
    for mmd, png_name, _, w_px, h_px, scale, _ in DIAGRAMS:
        src = BUILD / mmd
        dst = BUILD / png_name
        if not src.exists():
            raise FileNotFoundError(src)
        subprocess.run(
            [
                "npx", "--yes", "@mermaid-js/mermaid-cli@11.4.0",
                "-i", str(src),
                "-o", str(dst),
                "-c", str(CONFIG),
                "-b", "white",
                "-w", str(w_px),
                "-H", str(h_px),
                "-s", str(scale),
            ],
            cwd=str(ROOT),
            check=True,
            timeout=180,
        )


def diagram_size_cm(png_path: Path, max_width_cm: float) -> tuple[float, float]:
    w_px, h_px = ImageReader(str(png_path)).getSize()
    max_w = max_width_cm * cm
    max_h = MAX_DIAGRAM_HEIGHT_CM * cm
    scale = min(max_w / w_px, max_h / h_px)
    return w_px * scale, h_px * scale


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


def add_table(story, headers: list[str], rows: list[list[str]], col_widths) -> None:
    data = [headers] + rows
    t = Table(data, colWidths=col_widths)
    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)


def add_diagram(story, png_name: str, caption: str, h2, max_width_cm: float) -> None:
    path = BUILD / png_name
    if not path.exists():
        return
    iw, ih = diagram_size_cm(path, max_width_cm)
    story.append(p(caption, h2))
    story.append(Image(str(path), width=iw, height=ih))
    story.append(Spacer(1, 0.35 * cm))


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

    story: list = []
    story.append(p("Android Cast — Git flow (green master)", title))
    story.append(Spacer(1, 0.15 * cm))
    add_table(
        story,
        ["Field", "Value"],
        [
            ["Version", DOC_VERSION],
            ["Published", DOC_DATE],
            ["Author(s)", DOC_AUTHOR],
            ["Markdown source", DOC_SOURCE],
            ["Diagrams", "Mermaid CLI → PNG (Chromium); proportional embed in PDF"],
        ],
        [4.0 * cm, 12.5 * cm],
    )
    story.append(Spacer(1, 0.25 * cm))
    story.append(
        p(
            "Diagrams are rendered by headless Chromium (not rsvg/SVG rasterize) so labels stay visible. "
            "Regenerate after editing docs/_pdf_build/git_flow_diagram_*.mmd.",
            small,
        ),
    )
    story.append(Spacer(1, 0.3 * cm))

    story.append(p("1. Goals", h1))
    add_table(
        story,
        ["Goal", "Mechanism"],
        [
            ["Always shippable master", "Merge to master only when CI/tests pass"],
            ["Safe integration", "Features land on next first"],
            ["Clear production line", "Release tags only on master"],
            ["Fast production patches", "hotfix/* from master, then sync to next"],
        ],
        [4.5 * cm, 12 * cm],
    )
    story.append(Spacer(1, 0.4 * cm))

    story.append(p("2. Branches", h1))
    add_table(
        story,
        ["Branch", "Role", "Direct commits?"],
        [
            ["master", "Production; green; annotated tags vX.Y.Z", "No — merge only"],
            ["next", "Integration / release candidate", "No — merge only"],
            ["feature/*, fix/*", "Short-lived work", "Yes; merge → next"],
            ["hotfix/*", "Urgent fix for released code", "Yes; merge → master first"],
        ],
        [3 * cm, 8.5 * cm, 4 * cm],
    )
    story.append(Spacer(1, 0.3 * cm))
    add_diagram(story, DIAGRAMS[0][1], DIAGRAMS[0][2], h2, DIAGRAMS[0][6])
    story.append(PageBreak())

    story.append(p("3. Daily development", h1))
    for line in [
        "git fetch && git checkout next && git pull",
        "git checkout -b feature/short-description",
        "Commit on topic branch; run tests before merge",
        "Merge topic → next (after CI green)",
    ]:
        story.append(p(f"• {line}", bullet))
    story.append(Spacer(1, 0.15 * cm))
    story.append(
        p(
            "<b>Topic branch cleanup:</b> optional locally. On the server, enable "
            "<i>delete branch on merge</i> (Gitea/GitLab/GitHub) so merged feature/* "
            "branches are removed automatically on origin.",
            body,
        ),
    )
    story.append(Spacer(1, 0.15 * cm))
    story.append(
        p(
            "<b>Agents:</b> never push directly to master or next unless the user explicitly "
            "requests a release or hotfix merge.",
            body,
        ),
    )
    add_diagram(story, DIAGRAMS[1][1], DIAGRAMS[1][2], h2, DIAGRAMS[1][6])
    story.append(PageBreak())

    story.append(p("4. Release (next → master)", h1))
    for line in [
        "Confirm next is QA-ready and CI green",
        "Merge next → master",
        "Smoke / CI on master",
        "Tag on master: git tag -a v0.1.2 -m \"Release v0.1.2\"",
        "Sync: git checkout next && git merge master && git push",
        "Continue feature branches from updated next",
    ]:
        story.append(p(f"• {line}", bullet))
    add_diagram(story, DIAGRAMS[2][1], DIAGRAMS[2][2], h2, DIAGRAMS[2][6])
    story.append(PageBreak())

    story.append(p("5. Hotfixes — recommended strategy", h1))
    story.append(
        p(
            "<b>Default (option 1):</b> branch hotfix/* from <b>master</b> when the bug affects "
            "released code. After fix and tests: merge → master, tag patch (e.g. v0.1.3), then "
            "<b>merge master → next</b> so integration does not miss the fix.",
            body,
        ),
    )
    story.append(Spacer(1, 0.15 * cm))
    story.append(
        p(
            "<b>Exception (option 2):</b> bug exists only on next — branch fix/* from next, "
            "merge to next only; ships with the next normal release.",
            body,
        ),
    )
    story.append(Spacer(1, 0.15 * cm))
    story.append(
        p(
            "<b>Why not hotfix only on next?</b> next often contains unreleased features; "
            "merging next to master for a patch would ship extra code or block the fix.",
            body,
        ),
    )
    add_diagram(story, DIAGRAMS[3][1], DIAGRAMS[3][2], h2, DIAGRAMS[3][6])

    story.append(p("5.1 Sync cheat sheet", h2))
    add_table(
        story,
        ["Event", "Action"],
        [
            ["After release + tag", "merge master → next"],
            ["After hotfix + tag", "merge master → next"],
            ["Feature done", "merge topic → next only"],
            ["Wrong base branch", "rebase topic onto latest next"],
        ],
        [4.5 * cm, 12 * cm],
    )
    story.append(PageBreak())

    story.append(p("6. Branch protection and CI", h1))
    add_table(
        story,
        ["Rule", "master", "next"],
        [
            ["CI required before merge", "yes", "yes"],
            ["No WIP direct push", "yes", "yes"],
            ["Annotated release tags", "yes", "no"],
            ["Force-push", "never", "maintainer only, rare"],
            ["Delete branch on merge", "n/a", "yes (topic branches)"],
        ],
        [5.5 * cm, 5 * cm, 5 * cm],
    )
    story.append(Spacer(1, 0.2 * cm))
    story.append(
        p(
            "<b>Force-push on next:</b> keep disabled by default. Allow only when you intentionally "
            "rewrite integration history — never on master. Prefer fixing on topic branches before "
            "merging to next.",
            body,
        ),
    )
    story.append(Spacer(1, 0.35 * cm))
    story.append(p("7. Cursor / agent checklist", h1))
    for line in [
        "Branch from next for features and non-production fixes",
        "Run tests before recommending merge to next",
        "Releases and tags only with explicit user approval",
        "Hotfix released code from master; sync master into next after tag",
        "Never force-push master; avoid force-push on next unless user requests",
    ]:
        story.append(p(f"• {line}", bullet))
    story.append(Spacer(1, 0.4 * cm))
    story.append(
        p("Regenerate: python3 docs/_pdf_build/build_git_flow_pdf.py", small),
    )

    doc = SimpleDocTemplate(
        str(OUT_PDF),
        pagesize=A4,
        leftMargin=2 * cm,
        rightMargin=2 * cm,
        topMargin=2 * cm,
        bottomMargin=2 * cm,
    )
    doc.build(story)
    print(f"Wrote {OUT_PDF}")


def main() -> int:
    try:
        render_mermaid()
    except subprocess.CalledProcessError as e:
        print("Mermaid render failed:", e, file=sys.stderr)
        return 1
    except FileNotFoundError as e:
        print(e, file=sys.stderr)
        return 1
    build_pdf()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
