"""The documents the site reads at runtime so a page cannot drift from them: the review process,
and how a featured list is chosen.

Sources of truth: this module, `docs/REVIEW-PROCESS.md` and `docs/FEATURED.md` (the authorities),
`routers/review.py` (`GET /api/review/process`), `routers/featured.py` (`GET /api/featured/method`),
`tests/test_process_doc.py`.

`docs/REVIEW-PROCESS.md` is the one file a review pass reads before it proposes anything: the
boundary between what a rule may do alone and what must be proposed, the grouping default for
each vertical, what a proposal must carry, and what a person spot-checks. The review area's first
tab shows it to a person who is not going to read the repository.

**It is READ, never restated.** A second copy of a rule in TypeScript is a copy that goes stale
the first time the doc changes, and the doc's own version line is a rules version -- changing it
is a deploy and, when the boundary moves, a rederive. So the server parses the file into its
sections and hands the markdown over; the panel renders it. When the doc changes, the page
changes at the next deploy and nobody has to remember.

The file ships in the image (`Dockerfile`, the runtime stage copies these docs and nothing else
from `docs/`); in a checkout it is found beside the package. When neither exists the reader says
so rather than inventing content, and the page shows the sentence instead of a blank tab.

`docs/FEATURED.md` (Stream AW2) is read the same way by `read_doc("FEATURED")`: the public
methodology page `/how-we-choose` and `GET /api/featured/method` hand over its sections, and its
version line equals `featured.VERSION` (a two-way test), so the rule a shopper reads is the rule
the code runs. A document without a version line is not an authority, and the API 404s it.

The two legal pages are read the same way: `docs/legal/privacy.md` and `docs/legal/terms.md`
become `/privacy` and `/terms` (and `GET /api/legal/{name}`) through `legal_page(name)`. Their
version line is the "last updated" stamp the page prints; the copy is the client's to change,
so an edit is a deploy and nothing else.
"""

from __future__ import annotations

import pathlib
import re
from typing import Any

from app.services.markdown import heading_id, render_markdown

#: The container's copy first, then the checkout's (`main/docs/`), as `services/items.py` does.
_CANDIDATES = (
    pathlib.Path("/srv/app/docs/REVIEW-PROCESS.md"),
    pathlib.Path(__file__).resolve().parents[2] / "docs" / "REVIEW-PROCESS.md",
)
_FEATURED_CANDIDATES = (
    pathlib.Path("/srv/app/docs/FEATURED.md"),
    pathlib.Path(__file__).resolve().parents[2] / "docs" / "FEATURED.md",
)
_LEGAL_DIRS = (pathlib.Path("/srv/app/docs/legal"), pathlib.Path(__file__).resolve().parents[2] / "docs" / "legal")
#: The public address word of each legal page, and the document behind it.
LEGAL = {"privacy": "PRIVACY", "terms": "TERMS"}
#: Each document the site reads: where to look (the image's copy first), and what its preamble
#: is called when it is handed over as the first section. The review process leads with its
#: standing rules, the version line among them; the featured method leads with its lede alone,
#: the version line shown as the page's own standing instead (`hide_version`).
_DOCS: dict[str, dict] = {
    "REVIEW-PROCESS": {"candidates": _CANDIDATES, "lead_title": "The standing rules", "lead_anchor": "standing",
                       "hide_version": False},
    "FEATURED": {"candidates": _FEATURED_CANDIDATES, "lead_title": "In short", "lead_anchor": "in-short",
                 "hide_version": True},
    # The legal pages lead with their own opening paragraph; the version line is the page's
    # "last updated" stamp, so it is dropped from the lede like the featured method's.
    "PRIVACY": {"candidates": tuple(d / "privacy.md" for d in _LEGAL_DIRS), "lead_title": "About this policy",
                "lead_anchor": "about", "hide_version": True},
    "TERMS": {"candidates": tuple(d / "terms.md" for d in _LEGAL_DIRS), "lead_title": "About these terms",
              "lead_anchor": "about", "hide_version": True},
}
_HEADING = re.compile(r"^##\s+(?:(\d+)\.\s*)?(.+?)\s*$")
_VERSION = re.compile(r"\*\*Version\s+(\d+)\s*,\s*([0-9-]+)\*\*")
_TITLE = re.compile(r"^#\s+(.+?)\s*$", re.M)


def path(name: str = "REVIEW-PROCESS") -> pathlib.Path | None:
    """The copy of the named document this process reads, or None. An unknown name is a KeyError."""
    return next((p for p in _DOCS[name]["candidates"] if p.is_file()), None)


def _split(source: str) -> tuple[str, list[tuple[str | None, str, list[str]]]]:
    """The text before the first `##` heading, then one entry per section."""
    preamble: list[str] = []
    sections: list[tuple[str | None, str, list[str]]] = []
    current: list[str] | None = None
    for line in source.splitlines():
        match = _HEADING.match(line)
        if match:
            current = []
            sections.append((match.group(1), match.group(2), current))
        elif current is not None:
            current.append(line)
        else:
            preamble.append(line)
    return "\n".join(preamble), sections


def read_doc(name: str) -> dict[str, Any]:
    """The named document as a page shows it: its title (the `#` heading), its version, and a
    section per `##` heading with its number, title, anchor and rendered body; the preamble leads
    as the first section under the document's own lead title. An unknown name is a KeyError.
    Writes nothing, reads no database, and is read per request: no cache, so an edit is a deploy
    and nothing else."""
    spec = _DOCS[name]
    found = path(name)
    if found is None:
        return {"title": None, "version": None, "version_line": None, "dated": None, "path": None,
                "missing": True, "sections": []}
    source = found.read_text(encoding="utf-8")
    preamble, sections = _split(source)
    version = _VERSION.search(preamble)
    title = _TITLE.search(preamble)
    # The preamble's first heading is the page's own title, not a section; the version line is the
    # standing rules' own opening on the review process, and the page's dated standing on the
    # featured method (so it is dropped from the lede there).
    body = "\n".join(
        line for line in preamble.splitlines()
        if not line.startswith("# ") and not (spec["hide_version"] and version and line.strip() == version.group(0))
    )
    out = [{"number": None, "title": spec["lead_title"], "anchor": spec["lead_anchor"],
            "html": render_markdown(body.strip())}]
    for number, section_title, lines in sections:
        out.append({"number": number, "title": section_title,
                    "anchor": heading_id(f"{number}. {section_title}" if number else section_title),
                    "html": render_markdown("\n".join(lines).strip())})
    return {
        "title": title.group(1) if title else None,
        "version": version.group(1) if version else None,
        "version_line": version.group(0) if version else None,
        "dated": version.group(2) if version else None,
        "path": f"docs/{name}.md",
        "missing": False,
        "sections": out,
    }


def read() -> dict[str, Any]:
    """The review process document as `/review` shows it (`read_doc("REVIEW-PROCESS")`)."""
    return read_doc("REVIEW-PROCESS")


def featured_method() -> dict[str, Any] | None:
    """`docs/FEATURED.md` as `/how-we-choose` and `GET /api/featured/method` hand it over (the
    `FeaturedMethodOut` shape), or None when it is absent or carries no version line or title: a
    document without a version is not an authority, so both answer 404 rather than a blank page."""
    doc = read_doc("FEATURED")
    if doc["missing"] or not doc["version"] or not doc["title"]:
        return None
    return {"title": doc["title"], "version": doc["version"], "dated": doc["dated"],
            "version_line": doc["version_line"],
            "sections": [{"title": s["title"], "anchor": s["anchor"], "html": s["html"]} for s in doc["sections"]]}


def legal_page(name: str) -> dict[str, Any] | None:
    """`docs/legal/<name>.md` as `/privacy` or `/terms` and `GET /api/legal/{name}` hand it over
    (the `LegalPageOut` shape), or None when the name is not a legal page, or its document is
    absent or carries no version line or title: both the page and the API then answer 404, never
    a blank page, because the date a policy was last changed is part of the policy."""
    doc_name = LEGAL.get(name)
    if doc_name is None:
        return None
    doc = read_doc(doc_name)
    if doc["missing"] or not doc["version"] or not doc["title"]:
        return None
    return {"name": name, "title": doc["title"], "version": doc["version"], "dated": doc["dated"],
            "version_line": doc["version_line"],
            "sections": [{"title": s["title"], "anchor": s["anchor"], "html": s["html"]} for s in doc["sections"]]}


def passes(db) -> list[dict[str, Any]]:
    """Every pass that has written proposals, with what is waiting, so each block on the tab can
    link to the rows it produced rather than describing them. One query per column; no writes."""
    from collections import Counter

    from sqlalchemy import func, select

    from app.models import Proposal, ProposalPass

    rows = {p.id: p for p in db.scalars(select(ProposalPass))}
    tallies: dict[int, Counter] = {}
    brands: dict[int, set[str]] = {}
    for pass_id, status, brand_slug, n in db.execute(
            select(Proposal.pass_id, Proposal.status, Proposal.brand_slug, func.count(Proposal.id))
            .group_by(Proposal.pass_id, Proposal.status, Proposal.brand_slug)):
        tallies.setdefault(pass_id, Counter())[status] += n
        tallies[pass_id]["total"] += n
        if status == "open" and brand_slug:
            brands.setdefault(pass_id, set()).add(brand_slug)
    out = []
    for pass_id, row in rows.items():
        counts = tallies.get(pass_id, Counter())
        out.append({"name": row.name, "kind": row.kind, "generator": row.generator,
                    "process_version": row.process_version, "rules_version": row.rules_version,
                    "withdrawn": row.withdrawn_at is not None,
                    "open": int(counts.get("open", 0)), "total": int(counts.get("total", 0)),
                    "brands": sorted(brands.get(pass_id, ()))[:8], "brand_count": len(brands.get(pass_id, ()))})
    out.sort(key=lambda r: (-r["open"], r["name"]))
    return out
