#!/usr/bin/env python3
"""Build docs/KeluargaMart-Monorepo-Complete-Documentation.docx.

Two passes:
  1. dry run — collect figure and table captions in document order
  2. real run — write the document, injecting the collected lists

Run from anywhere:  python3 docs/docgen/build_doc.py
"""
import sys
from pathlib import Path

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))

from helpers import Builder  # noqa: E402
import content_core  # noqa: E402
import content_apps  # noqa: E402
import content_data  # noqa: E402
import content_manual  # noqa: E402
import facts as F  # noqa: E402

OUT = HERE.parent / "KeluargaMart-Monorepo-Complete-Documentation.docx"


def assemble(b: Builder) -> None:
    content_core.front_matter(b, OUT.name)
    content_core.ch1_exec(b)
    content_core.ch2_scope(b)
    content_core.ch3_overview(b)
    content_core.ch4_repo(b)
    content_apps.ch5_stack(b)
    content_apps.ch6_inventory(b)
    content_apps.ch7_backend(b)
    content_apps.ch8_erp(b)
    content_apps.ch9_pos(b)
    content_apps.ch10_b2b(b)
    content_apps.ch11_nextreport(b)
    content_apps.ch12_packages(b)
    content_data.ch13_routes(b)
    content_data.ch14_domain(b)
    content_data.ch15_workflows(b)
    content_data.ch16_database(b)
    content_data.ch17_objects(b)
    content_data.ch18_dictionary(b)
    content_data.ch19_erd(b)
    content_data.ch20_api(b)
    content_data.ch21_auth(b)
    content_data.ch22_roles(b)
    content_data.ch23_config(b)
    content_manual.ch24_user_manual(b)
    content_manual.ch25_developer(b)
    content_manual.ch26_testing(b)
    content_manual.ch27_cicd(b)
    content_manual.ch28_deploy(b)
    content_manual.ch29_tunnel(b)
    content_manual.ch30_observability(b)
    content_manual.ch31_backup(b)
    content_manual.ch32_troubleshoot(b)
    content_manual.ch33_rules(b)
    content_manual.ch34_thirdparty(b)
    content_manual.ch35_security(b)
    content_manual.ch36_performance(b)
    content_manual.ch37_testing_report(b)
    content_manual.ch38_findings(b)
    content_manual.ch39_roadmap(b)
    content_manual.ch40_maintenance(b)
    content_manual.appendices(b)


def render_lists(b: Builder, figures, tables) -> None:
    """Write the List of Figures / List of Tables at their anchor points."""
    # The anchors are paragraphs added in front_matter; we rebuild those two
    # pages by appending after the fact is not possible in docx, so instead the
    # front matter calls this before saving via a deferred injection.
    raise NotImplementedError


def check_assets() -> None:
    """Fail the build when a figure is referenced but missing, or vice versa."""
    import re
    blob = "\n".join(
        (HERE / f"content_{n}.py").read_text()
        for n in ("core", "apps", "data", "manual")
    )
    referenced = set(re.findall(r"rendered/([a-z0-9\-]+\.png)", blob))
    referenced |= {f"{d}.png" for d in re.findall(r'\("(erd-[a-z\-]+)",', blob)}
    rendered = HERE.parent / "diagrams" / "rendered"
    present = {p.name for p in rendered.glob("*.png")}
    missing = sorted(referenced - present)
    if missing:
        raise SystemExit(f"missing rendered diagrams: {missing}")
    unused = sorted(present - referenced)
    if unused:
        print(f"note: {len(unused)} rendered diagram(s) not embedded: {unused}")


def measure_pages(pdf: Path):
    """Return per-page records: (flat_text, set_of_standalone_lines).

    Headings longer than the line width wrap across lines, so matching is done
    against the flattened page; a heading that is also a listing page title
    ("Table of contents") is recovered from the standalone-line set.
    """
    import re
    import subprocess
    out = subprocess.run(["pdftotext", "-layout", str(pdf), "-"],
                         capture_output=True, text=True).stdout
    pages = []
    for page in out.split("\f"):
        flat = re.sub(r"\s+", " ", page).strip()
        lines = {" ".join(l.split()) for l in page.splitlines() if l.strip()}
        pages.append((flat, lines))
    return pages


def lookup(pages, text: str):
    """First body page (1-based) whose flattened text contains the heading.

    Contents pages are skipped: they list every heading followed by a dot
    leader, so they would otherwise win every match. A page carrying a run of
    12+ dots is a listing page (TOC / list of figures / list of tables), never
    a chapter page.
    """
    import re
    needle = " ".join(text.split())
    for i, (flat, _lines) in enumerate(pages, start=1):
        if re.search(r"\.{12,}", flat):
            continue
        if needle in flat:
            return i
    for i, (_flat, lines) in enumerate(pages, start=1):
        if needle in lines:
            return i
    return ""


def render_and_measure(docx_path: Path):
    import subprocess
    tmp = Path("/tmp/kmdoc-build")
    tmp.mkdir(exist_ok=True)
    for old in tmp.glob("*.pdf"):
        old.unlink()
    subprocess.run(["soffice", "--headless", "--convert-to", "pdf",
                    "--outdir", str(tmp), str(docx_path)],
                   capture_output=True)
    pdf = tmp / (docx_path.stem + ".pdf")
    return measure_pages(pdf) if pdf.exists() else {}


def main() -> None:
    check_assets()
    dry = Builder(OUT, dry=True)
    assemble(dry)
    print(f"figures={len(dry.figures)} tables={len(dry.tables)}")

    # Pass 1: static TOC with entries but blank page numbers, so the TOC length
    # (and therefore every later page number) is already final.
    b = Builder(OUT)
    b.planned_figures = dry.figures
    b.planned_tables = dry.tables
    b.planned_toc = None
    assemble(b)
    headings = list(b.headings)
    b.planned_toc = [(lvl, text, bm, "") for lvl, text, bm in headings]
    # rebuild from scratch with the placeholder TOC
    b = Builder(OUT)
    b.planned_figures = dry.figures
    b.planned_tables = dry.tables
    b.planned_toc = [(lvl, text, bm, "") for lvl, text, bm in headings]
    assemble(b)
    b.save()

    prev = None
    for attempt in range(4):
        pages = render_and_measure(OUT)
        entries = [(lvl, text, bm, lookup(pages, text)) for lvl, text, bm in headings]
        b = Builder(OUT)
        b.planned_figures = dry.figures
        b.planned_tables = dry.tables
        b.planned_toc = entries
        assemble(b)
        b.save()
        newpages = render_and_measure(OUT)
        mapping = {text: lookup(newpages, text) for _, text, _ in headings}
        missing = [t for _, t, _ in headings if not mapping.get(t)]
        if missing:
            print(f"WARNING: {len(missing)} headings not found in render: {missing[:4]}")
        if all(str(lookup(pages, t)) == str(mapping.get(t, "")) for _, t, _ in headings) and not missing:
            print(f"TOC stable after {attempt+1} measurement pass(es)")
            break
        headings = headings  # unchanged
    print(f"wrote {OUT} ({OUT.stat().st_size/1024/1024:.1f} MiB), toc entries={len(headings)}")


if __name__ == "__main__":
    main()
