#!/usr/bin/env python3
"""Insert/update ## Table of contents in all docs/*.md (GitHub-style anchor links)."""

from __future__ import annotations

import re
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
DOCS = ROOT / "docs"

TOC_START = "<!-- toc -->"
TOC_END = "<!-- /toc -->"
TOC_HEADING = "## Table of contents"
INDEX_NAME = "README.md"

# One-line blurbs for docs/README.md
DOC_BLURBS: dict[str, str] = {
    "README.md": "Master index of all documentation (this file).",
    "INFRA.md": "Production topology: Gentoo FE → Alpine BE, ports, nginx, sshfs.",
    "GIT_FLOW.md": "Branches (`master` / `next`), release, hotfix, dual remote.",
    "ROADMAP.md": "Implementation roadmap, alpha definition, deferred work.",
    "ALPHA.md": "Alpha feature freeze, QA sign-off, defaults.",
    "ALPHA_SOAK.md": "Short LAN soak checklist for demo validation.",
    "BUILD_DEPLOY.md": "Docker CI, staging OTA, builder console deploy.",
    "OTA.md": "OTA channel layout v0, manifests, MQTT, publish flow.",
    "CRASH_REPORTER.md": "Anonymous crash upload (app + backend).",
    "OPEN_API.md": "Crash ingest API design, triage tags, heuristics.",
    "TICKETS_ROADMAP.md": "Tickets console, Gitea, attachments, agent queue.",
    "COMMERCIAL.md": "Developer-gated ads / IAP toggles.",
    "PRO_AAR.md": "cast-core / cast-pro AAR split and Play Billing.",
    "USB_HDMI_CAST.md": "Roadmap D/E: HDMI awareness, USB-tether transport.",
    "GRAFANA_vs_others_graphvis_pivot.md": "Custom graphs vs Grafana decision memo.",
    "20260602_REVERSE_SSH_proposals_summary.md": "On-demand remote access (WG/SSH/file API) proposals.",
    "20260607-2FA-email-mobile-auth-flow.md": "Email forwarding + registration/2FA (alpha must-have).",
    "20260608_ALPHA_PRIORITIES.md": "**Master priority plan** (0.1–9.x renumber, owners, alpha path).",
    "20260608_BE_SERVICES_and_infra.md": "BE service map, validation status, SFU preview (gray).",
    "20260608_STREAM_DUMP_DEBUG.md": "FR 0.1 stream dump spec.",
    "20260610_SERVICES.md": "Exposed services catalog — web UI, APIs, curl, validation (2026-06-10).",
    "OPEN_TASKS_GRAPH.md": "Open tasks dependency graph (Mermaid).",
    "OPUS_SPEEX_VALIDATION.md": "Opus/Speex + UDP stream protection E2E checklist.",
    "REMOTE_ACCESS_IMPL.md": "WireGuard v1 impl + deploy.",
    "REMOTE_ACCESS_VALIDATION.md": "Linux CLI suite + mobile parity.",
    "AV_QUALITY_QA_SESSION.md": "AV enhancement Q&A: sender vs receiver pipelines.",
    "specs/20100611_3_url_shortener.md": "URL shortener service — SPEC (`s.f0xx.org`, post-alpha).",
    "DRs/20100611_3_url_shortener.md": "URL shortener — design review (auth, slug algo, task graph).",
}

SPEC_DIR = DOCS / "specs"
DR_DIR = DOCS / "DRs"


def github_slug(text: str) -> str:
    """Approximate GitHub/Cursor heading anchor slugs."""
    text = text.strip().lower()
    text = re.sub(r"`([^`]+)`", r"\1", text)
    text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
    text = re.sub(r"[*_]+", "", text)
    text = re.sub(r"[^\w\s-]", "", text)
    text = re.sub(r"\s+", "-", text)
    text = re.sub(r"-+", "-", text)
    return text.strip("-")


def collect_headings(lines: list[str]) -> list[tuple[int, str, str]]:
    """Return (level, title, slug) for ## and ### only."""
    out: list[tuple[int, str, str]] = []
    used: dict[str, int] = {}
    for line in lines:
        m = re.match(r"^(#{2,3})\s+(.+?)\s*$", line)
        if not m:
            continue
        level = len(m.group(1))
        title = m.group(2).strip()
        if title == "Table of contents":
            continue
        base = github_slug(title)
        if not base:
            continue
        n = used.get(base, 0)
        slug = base if n == 0 else f"{base}-{n}"
        used[base] = n + 1
        out.append((level, title, slug))
    return out


def build_toc_block(headings: list[tuple[int, str, str]], *, doc_name: str) -> list[str]:
    lines = [TOC_HEADING, "", TOC_START]
    for level, title, slug in headings:
        indent = "  " if level == 3 else ""
        # Strip markdown emphasis for link text
        link_text = re.sub(r"\*\*([^*]+)\*\*", r"\1", title)
        link_text = re.sub(r"`([^`]+)`", r"\1", link_text)
        lines.append(f"{indent}- [{link_text}](#{slug})")
    lines.append(TOC_END)
    if doc_name != INDEX_NAME:
        lines.append("")
        lines.append(f"**Documentation index:** [{INDEX_NAME}]({INDEX_NAME})")
    lines.append("")
    return lines


def strip_existing_toc(lines: list[str]) -> list[str]:
    out: list[str] = []
    i = 0
    while i < len(lines):
        if lines[i].strip() == TOC_HEADING:
            i += 1
            while i < len(lines) and TOC_END not in lines[i]:
                i += 1
            if i < len(lines):
                i += 1  # skip <!-- /toc -->
            while i < len(lines) and lines[i].strip() == "":
                i += 1
            if i < len(lines) and lines[i].strip().startswith("**Documentation index:**"):
                i += 1
            while i < len(lines) and lines[i].strip() == "":
                i += 1
            continue
        out.append(lines[i])
        i += 1
    return out


def find_insert_index(lines: list[str]) -> int:
    """Index to insert TOC block (before first ## section)."""
    for i, line in enumerate(lines):
        if re.match(r"^## (?!Table of contents)", line):
            return i
    return len(lines)


def process_file(path: Path) -> bool:
    text = path.read_text(encoding="utf-8")
    lines = text.splitlines(keepends=True)
    raw = [ln.rstrip("\n") for ln in lines]
    raw = strip_existing_toc(raw)
    headings = collect_headings(raw)
    if not headings and path.name != INDEX_NAME:
        return False
    toc = build_toc_block(headings, doc_name=path.name)
    idx = find_insert_index(raw)
    # Avoid duplicate --- before TOC
    if idx > 0 and raw[idx - 1].strip() == "---":
        new_lines = raw[: idx - 1] + toc + ["---", ""] + raw[idx:]
    else:
        sep = ["---", ""] if idx < len(raw) and raw[idx].startswith("##") else []
        new_lines = raw[:idx] + toc + sep + raw[idx:]
    new_text = "\n".join(new_lines) + "\n"
    if new_text != text.replace("\r\n", "\n"):
        path.write_text(new_text, encoding="utf-8")
        return True
    return False


def build_index_md() -> str:
    md_files = sorted(p for p in DOCS.glob("*.md") if p.name != INDEX_NAME)
    lines = [
        "# Android Cast — documentation index",
        "",
        "Click a link to open a guide. Section links jump within the document (GitHub, Cursor, and most Markdown previews).",
        "",
        TOC_HEADING,
        "",
        TOC_START,
        "- [All documents](#all-documents)",
        "- [Specifications](#specifications)",
        "- [Design reviews](#design-reviews)",
        "- [By topic](#by-topic)",
        "- [Agent bootstrap](#agent-bootstrap)",
        TOC_END,
        "",
        "---",
        "",
        "## All documents",
        "",
        "| Document | Summary |",
        "|----------|---------|",
    ]
    for p in md_files:
        name = p.name
        blurb = DOC_BLURBS.get(name, "")
        lines.append(f"| [{name}]({name}) | {blurb} |")

    spec_files = sorted(SPEC_DIR.glob("*.md")) if SPEC_DIR.is_dir() else []
    dr_files = sorted(DR_DIR.glob("*.md")) if DR_DIR.is_dir() else []

    lines += [
        "",
        "---",
        "",
        "## Specifications",
        "",
        "| Document | Summary |",
        "|----------|---------|",
    ]
    if spec_files:
        for p in spec_files:
            rel = f"specs/{p.name}"
            blurb = DOC_BLURBS.get(rel, "")
            lines.append(f"| [{p.name}]({rel}) | {blurb} |")
    else:
        lines.append("| _(none yet)_ | |")

    lines += [
        "",
        "---",
        "",
        "## Design reviews",
        "",
        "| Document | Summary |",
        "|----------|---------|",
    ]
    if dr_files:
        for p in dr_files:
            rel = f"DRs/{p.name}"
            blurb = DOC_BLURBS.get(rel, "")
            lines.append(f"| [{p.name}]({rel}) | {blurb} |")
    else:
        lines.append("| _(none yet)_ | |")

    lines += [
        "",
        "---",
        "",
        "## By topic",
        "",
        "### Infrastructure and deploy",
        "",
        f"- [INFRA.md](INFRA.md) — FE/BE topology, nginx, ports ([§1 Topology](INFRA.md#1-topology))",
        f"- [BUILD_DEPLOY.md](BUILD_DEPLOY.md) — Docker CI, builder ([§Docker CI](BUILD_DEPLOY.md#docker-ci))",
        f"- [OTA.md](OTA.md) — OTA v0 layout ([§URL layout](OTA.md#url-layout))",
        "",
        "### Development process",
        "",
        f"- [GIT_FLOW.md](GIT_FLOW.md) — `master` / `next` ([§2 Branches](GIT_FLOW.md#2-branches))",
        f"- [ROADMAP.md](ROADMAP.md) — milestones ([§Alpha definition](ROADMAP.md#alpha-definition))",
        "",
        "### Alpha and QA",
        "",
        f"- [ALPHA.md](ALPHA.md) — freeze and sign-off ([§Alpha scope](ALPHA.md#alpha-scope-what-we-ship))",
        f"- [ALPHA_SOAK.md](ALPHA_SOAK.md) — LAN soak ([§Happy path](ALPHA_SOAK.md#happy-path-15-min))",
        "",
        "### Backend apps",
        "",
        f"- [CRASH_REPORTER.md](CRASH_REPORTER.md) — crash upload",
        f"- [OPEN_API.md](OPEN_API.md) — ingest API ([§Upload API](OPEN_API.md#upload-api-post-apiv1uploadphp))",
        f"- [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md) — tickets + Gitea",
        f"- [GRAFANA_vs_others_graphvis_pivot.md](GRAFANA_vs_others_graphvis_pivot.md) — graphs pivot",
        f"- [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) — remote access ([§Rev. 3 Q&A](20260602_REVERSE_SSH_proposals_summary.md#rev-3--wg-vs-ssh-qa--bastion-deep-dive))",
        f"- [specs/20100611_3_url_shortener.md](specs/20100611_3_url_shortener.md) — URL shortener SPEC ([§REST API](specs/20100611_3_url_shortener.md#6-rest-api))",
        f"- [DRs/20100611_3_url_shortener.md](DRs/20100611_3_url_shortener.md) — URL shortener DR ([§Resolved open questions](DRs/20100611_3_url_shortener.md#2-resolved-open-questions))",
        "",
        "### Product and codecs",
        "",
        f"- [COMMERCIAL.md](COMMERCIAL.md) — ads / IAP",
        f"- [PRO_AAR.md](PRO_AAR.md) — Pro AAR split",
        f"- [USB_HDMI_CAST.md](USB_HDMI_CAST.md) — HDMI / USB-tether",
        f"- [AV_QUALITY_QA_SESSION.md](AV_QUALITY_QA_SESSION.md) — AV enhancement Q&A",
        "",
        "---",
        "",
        "## Agent bootstrap",
        "",
        "After a context reset, read in order:",
        "",
        "1. [INFRA.md](INFRA.md) — topology (no `:8089` for androidcast)",
        "2. [GIT_FLOW.md](GIT_FLOW.md) — branch rules",
        "3. Task-specific: [BUILD_DEPLOY.md](BUILD_DEPLOY.md), [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md), [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md), etc.",
        "",
        "Repo root [AGENTS.md](../AGENTS.md) points here.",
        "",
        "---",
        "",
        "## Maintaining TOCs",
        "",
        "Section links use GitHub/Cursor-style heading anchors. After adding or renaming `##` / `###` headings, run:",
        "",
        "```bash",
        "python3 scripts/generate-docs-toc.py",
        "```",
        "",
    ]
    return "\n".join(lines) + "\n"


def main() -> None:
    index_path = DOCS / INDEX_NAME
    index_path.write_text(build_index_md(), encoding="utf-8")
    print(f"Wrote {index_path}")

    changed = []
    toc_paths: list[Path] = sorted(DOCS.glob("*.md"))
    if SPEC_DIR.is_dir():
        toc_paths.extend(sorted(SPEC_DIR.glob("*.md")))
    if DR_DIR.is_dir():
        toc_paths.extend(sorted(DR_DIR.glob("*.md")))
    for path in toc_paths:
        if process_file(path):
            changed.append(str(path.relative_to(DOCS)))
            print(f"Updated TOC: {path.relative_to(DOCS)}")

    print(f"Done. {len(changed)} file(s) updated.")


if __name__ == "__main__":
    main()
