#!/usr/bin/env python3
"""Independent completion gate for the O-0176 manuscript route."""

from __future__ import annotations

import hashlib
import json
from pathlib import Path
import re
import shutil
import subprocess
import sys
import unicodedata


HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
SOURCES = HERE / "sources"
BUILD = HERE / "build"
PDF = BUILD / "manuscript.pdf"
FORMAL_SCOPE = "Set.insert target (Set.range counterexample)"
EXPECTED_PROGRAMS = {
    "heat-flow",
    "jensen",
    "laguerre",
    "weil",
    "de-branges",
    "canonical-systems",
    "total-positivity",
}
EXPECTED_CONTROLS = {"li", "nyman-beurling"}
EXPECTED_SOURCES = {
    "baez-duarte-2002",
    "bombieri-lagarias-1999",
    "bombieri-weil-2000",
    "conrey-li-2000",
    "csordas-norfolk-varga-1986",
    "csordas-vishnyakova-2013",
    "debruijn-1950",
    "farmer-2022",
    "griffin-ono-rolen-zagier-2019",
    "grochenig-2023",
    "osullivan-2021",
    "rodgers-tao-2020",
    "suzuki-canonical-2018",
    "suzuki-weil-2026",
}
ALLOWED_ACTUAL_VERDICTS = {
    "actual_paper_nonmember",
    "theorem_input_not_member",
    "fails_at_target",
}
COMPLETION_VERDICT = "ruled_out_if_admission_clauses_hold"
EXPECTED_BIB_KEYS = {
    "BaezDuarte2003",
    "Bombieri2000",
    "BombieriLagarias1999",
    "ConreyLi2000",
    "CsordasNorfolkVarga1986",
    "CsordasVishnyakova2013",
    "deBruijn1950",
    "Farmer2022",
    "Grochenig2023",
    "GriffinOnoRolenZagier2019",
    "OSullivan2021",
    "RodgersTao2020",
    "SuzukiCanonical",
    "SuzukiWeil2026",
}
CANON_RECORD = (
    ROOT
    / "canon"
    / "obstructions"
    / "O-0176-uniform-interior-certificates-cannot-prove-rh.md"
)


def run(
    args: list[str],
    *,
    cwd: Path = HERE,
    capture: bool = False,
) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        args,
        cwd=cwd,
        check=True,
        text=True,
        capture_output=capture,
    )


def load_json(path: Path) -> dict:
    with path.open(encoding="utf-8") as handle:
        return json.load(handle)


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()


def normalized(text: str) -> str:
    text = unicodedata.normalize("NFKD", text)
    text = "".join(char for char in text if not unicodedata.combining(char))
    return " ".join(
        "".join(char.lower() if char.isalnum() else " " for char in text).split()
    )


def parse_sha_file(path: Path) -> dict[str, str]:
    entries: dict[str, str] = {}
    for raw_line in path.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if not line:
            continue
        match = re.fullmatch(r"([0-9a-f]{64})  [*]?(.+)", line)
        if not match:
            raise AssertionError(f"malformed SHA256SUMS line: {raw_line!r}")
        digest, filename = match.groups()
        if filename in entries:
            raise AssertionError(f"duplicate SHA256SUMS entry: {filename}")
        entries[filename] = digest
    return entries


def verify_archive_manifest() -> None:
    manifest = ROOT / "SHA256SUMS"
    if not manifest.is_file():
        return
    entries = parse_sha_file(manifest)
    if not entries:
        raise AssertionError("archive SHA256SUMS is empty")
    for relative, expected in entries.items():
        path = ROOT / relative
        if not path.is_file():
            raise AssertionError(f"archive manifest path missing: {relative}")
        if sha256(path) != expected:
            raise AssertionError(f"archive manifest hash changed: {relative}")


def verify_sources() -> set[str]:
    manifest = load_json(HERE / "source_manifest.json")
    if manifest.get("schema_version") != 2:
        raise AssertionError("source manifest schema changed")
    sources = manifest.get("sources")
    if not isinstance(sources, dict) or set(sources) != EXPECTED_SOURCES:
        raise AssertionError("expected the exact fourteen-source corpus")

    sha_entries = parse_sha_file(SOURCES / "SHA256SUMS")
    expected_sha_entries: dict[str, str] = {}
    for key, source in sources.items():
        for field in ("citation", "canonical_url", "pdf", "text", "sha256"):
            if not isinstance(source.get(field), str) or not source[field]:
                raise AssertionError(f"{key}: missing source field {field}")

        pdf_path = HERE / source["pdf"]
        text_path = HERE / source["text"]
        if not pdf_path.is_file() or pdf_path.stat().st_size < 10_000:
            raise AssertionError(f"{key}: primary PDF missing or implausibly small")
        if not text_path.is_file() or text_path.stat().st_size < 100:
            raise AssertionError(f"{key}: extraction/OCR aid missing")

        actual_hash = sha256(pdf_path)
        if actual_hash != source["sha256"]:
            raise AssertionError(f"{key}: primary PDF hash changed")
        expected_sha_entries[source["pdf"]] = actual_hash

        extraction = normalized(text_path.read_text(encoding="utf-8", errors="replace"))
        anchors = source.get("anchors")
        if not isinstance(anchors, list) or len(anchors) < 2:
            raise AssertionError(f"{key}: fewer than two primary-source anchors")
        for anchor in anchors:
            location = anchor.get("location")
            text = anchor.get("text")
            if not isinstance(location, str) or not location:
                raise AssertionError(f"{key}: anchor location missing")
            if not isinstance(text, str) or not text:
                raise AssertionError(f"{key}: anchor text missing")
            if normalized(text) not in extraction:
                raise AssertionError(
                    f"{key}: anchor not found in pinned extraction: {text!r}"
                )

    if sha_entries != expected_sha_entries:
        missing = sorted(set(expected_sha_entries) - set(sha_entries))
        extra = sorted(set(sha_entries) - set(expected_sha_entries))
        changed = sorted(
            name
            for name in set(sha_entries) & set(expected_sha_entries)
            if sha_entries[name] != expected_sha_entries[name]
        )
        raise AssertionError(
            "SHA256SUMS differs from manifest: "
            f"missing={missing}, extra={extra}, changed={changed}"
        )
    return set(sources)


def verify_dossiers(source_keys: set[str]) -> None:
    data = load_json(HERE / "dossiers.json")
    if data.get("schema_version") != 2:
        raise AssertionError("dossier schema changed")
    class_data = data.get("class")
    if not isinstance(class_data, dict):
        raise AssertionError("class definition missing")
    if set(class_data.get("conditions", {})) != {"M1", "M2", "M3", "M4"}:
        raise AssertionError("M1-M4 class definition changed")
    if (
        class_data.get("object_scope")
        != "the target together with one explicitly named counterexample sequence"
    ):
        raise AssertionError("dossier object scope changed")

    programs = data.get("claimed_programs")
    if not isinstance(programs, list) or len(programs) != 7:
        raise AssertionError("expected exactly seven claimed programs")
    used_sources: set[str] = set()
    seen_programs: set[str] = set()
    programs_by_id: dict[str, dict] = {}
    for program in programs:
        program_id = program.get("id")
        if program_id not in EXPECTED_PROGRAMS or program_id in seen_programs:
            raise AssertionError(f"unexpected or duplicate program: {program_id!r}")
        seen_programs.add(program_id)
        programs_by_id[program_id] = program
        keys = program.get("source_keys")
        if not isinstance(keys, list) or not keys:
            raise AssertionError(f"{program_id}: no primary sources")
        if not set(keys) <= source_keys:
            raise AssertionError(f"{program_id}: unpinned source key")
        used_sources.update(keys)

        premises = program.get("source_premises")
        if not isinstance(premises, dict) or set(premises) != {
            "M1",
            "M2",
            "M3",
            "M4",
        }:
            raise AssertionError(f"{program_id}: incomplete source M1-M4 audit")
        if program.get("actual_paper_verdict") not in ALLOWED_ACTUAL_VERDICTS:
            raise AssertionError(f"{program_id}: paper admitted to AIC")
        if program.get("completion_verdict") != COMPLETION_VERDICT:
            raise AssertionError(f"{program_id}: completion verdict weakened")
        clauses = program.get("admission_clauses")
        if not isinstance(clauses, list) or len(clauses) < 4:
            raise AssertionError(f"{program_id}: admission checklist incomplete")
        survivors = program.get("surviving_mechanisms")
        if not isinstance(survivors, list) or len(survivors) < 3:
            raise AssertionError(f"{program_id}: escape hatches omitted")

    if seen_programs != EXPECTED_PROGRAMS:
        raise AssertionError("program coverage changed")

    controls = data.get("boundary_controls")
    if not isinstance(controls, list) or len(controls) != 2:
        raise AssertionError("expected two boundary controls")
    seen_controls: set[str] = set()
    for control in controls:
        control_id = control.get("id")
        if control_id not in EXPECTED_CONTROLS or control_id in seen_controls:
            raise AssertionError(f"unexpected or duplicate control: {control_id!r}")
        seen_controls.add(control_id)
        keys = control.get("source_keys")
        if not isinstance(keys, list) or not set(keys) <= source_keys:
            raise AssertionError(f"{control_id}: unpinned source key")
        used_sources.update(keys)
        if control.get("actual_paper_verdict") == "member":
            raise AssertionError(f"{control_id}: control admitted to AIC")

    if seen_controls != EXPECTED_CONTROLS:
        raise AssertionError("boundary-control coverage changed")
    if used_sources != source_keys:
        raise AssertionError(
            f"primary-source corpus and dossiers differ: "
            f"unused={sorted(source_keys - used_sources)}, "
            f"unknown={sorted(used_sources - source_keys)}"
        )

    jensen = programs_by_id["jensen"]
    if "Theta_t(w)=H_t(2i sqrt(w))" not in jensen["source_statement"]:
        raise AssertionError("Jensen same-heat transform is missing")
    if "hyperbolicity-preserving" not in (
        jensen["certificate_model"]["normalization"]
    ):
        raise AssertionError("Jensen normalization discipline is missing")
    laguerre = programs_by_id["laguerre"]
    if "m>=1" not in laguerre["certificate_model"]["carrier"]:
        raise AssertionError("Laguerre strict hierarchy includes automatic order zero")
    if "L_0=F^2" not in " ".join(laguerre.get("hazards", [])):
        raise AssertionError("Laguerre order-zero hazard is missing")
    weil = programs_by_id["weil"]
    if "Rayleigh" not in weil["source_statement"]:
        raise AssertionError("Weil pointwise-strict/coercive distinction is missing")
    if programs_by_id["de-branges"]["actual_paper_verdict"] != "fails_at_target":
        raise AssertionError("de Branges source-level failure was lost")
    if "omega_is_not_heat_time" not in (
        programs_by_id["canonical-systems"]["source_premises"]["M2"]
    ):
        raise AssertionError("canonical-system parameter mismatch was lost")
    pf_hazards = " ".join(programs_by_id["total-positivity"].get("hazards", []))
    if "coalesc" not in pf_hazards:
        raise AssertionError("total-positivity coalescence hazard is missing")

    prose = (HERE / "SOURCE_DOSSIERS.md").read_text(encoding="utf-8")
    if "No cited paper is classified as an actual AIC member." not in prose:
        raise AssertionError("prose dossier lost actual-paper disclaimer")
    for number in range(1, 8):
        if f"## Dossier {number}:" not in prose:
            raise AssertionError(f"prose dossier {number} missing")


def verify_hostile_audit() -> None:
    audit = (HERE / "HOSTILE_AUDIT.md").read_text(encoding="utf-8")
    for number in range(1, 30):
        if f"## Attack {number}:" not in audit:
            raise AssertionError(f"hostile attack {number} missing")
    required = (
        "The title and abstract must not say that all positivity approaches",
        "No cited paper proves all AIC premises",
        "Set.insert target (Set.range counterexample)",
        "outside-reader worksheet",
        "Recorded surviving mechanisms",
        "Surviving research directions",
    )
    for phrase in required:
        if phrase not in audit:
            raise AssertionError(f"hostile audit lost required phrase: {phrase}")


def verify_referee_audit() -> None:
    audit = (HERE / "REFEREE_AUDIT.md").read_text(encoding="utf-8")
    required = (
        "Accept the mathematical barrier",
        "Abstract theorem: pass.",
        "Scalar hierarchy: pass after repair.",
        "Coercive form: pass after repair.",
        "de Bruijn--Newman heat flow",
        "Jensen polynomials",
        "Generalized Laguerre--Turan",
        "Weil--Bombieri forms",
        "de Branges positivity",
        "Canonical systems",
        "Pólya frequency / total positivity",
        "Li coefficients",
        "Nyman--Beurling",
        "Residual limits",
        "What is dead is the precisely defined robust-interior",
    )
    for phrase in required:
        if phrase not in audit:
            raise AssertionError(f"referee disposition lost required text: {phrase}")


def verify_formal_and_canon() -> None:
    shared = ROOT / "lean" / "RHResearch" / "HeatBoundary.lean"
    pinned = ROOT / "canon" / "witnesses" / "O-0176" / "HeatBoundary.lean"
    if shared.read_bytes() != pinned.read_bytes():
        raise AssertionError("shared and pinned Lean files differ")
    lean_text = shared.read_text(encoding="utf-8")
    if lean_text.count(FORMAL_SCOPE) < 2:
        raise AssertionError("formal M4 scope is not exact")
    if "no_certificate_interior_at_accumulation" not in lean_text:
        raise AssertionError("formal arbitrary-interior theorem is missing")
    run([sys.executable, str(ROOT / "canon" / "witnesses" / "O-0176" / "verify.py")])

    canon = CANON_RECORD.read_text(encoding="utf-8")
    canon_words = " ".join(canon.split())
    required_canon = (
        'status: binding',
        'id: O-0176',
        'The formal scope of M4 is exactly',
        'Set.insert target (Set.range counterexample)',
        'No cited paper is an AIC member.',
        'For each row, a proposed completion is ruled out only if every '
        'missing clause is proved.',
        'The seven rows are architecture representatives, not an exhaustive '
        'bibliography of RH equivalences.',
    )
    for phrase in required_canon:
        if " ".join(phrase.split()) not in canon_words:
            raise AssertionError(f"canonical O-0176 scope changed: {phrase}")


def verify_submission_package() -> None:
    author_block = (HERE / "author-block.tex").read_text(encoding="utf-8").strip()
    if author_block != r"\author{Kenan}":
        raise AssertionError("canonical author block changed")

    readme = (HERE / "README.md").read_text(encoding="utf-8")
    for phrase in (
        "submission-neutral final manuscript package",
        "author block until publication",
        "canonical author as Kenan",
        "does not claim that every positivity method is excluded",
        "No journal selection, submission metadata, cover letter, or outbound contact",
    ):
        if phrase not in readme:
            raise AssertionError(f"package README lost boundary: {phrase}")


def verify_build() -> int:
    tex = (HERE / "manuscript.tex").read_text(encoding="utf-8")
    for token in (
        "O-0176",
        "canon/",
        "scratch/",
        "RHResearch",
        "repository",
        "TODO",
        "FIXME",
    ):
        if token.lower() in tex.lower():
            raise AssertionError(f"internal drafting token in manuscript: {token}")
    for forbidden in (
        "draft",
        "artificial intelligence",
        "language model",
        "automated research",
    ):
        if forbidden in tex.lower():
            raise AssertionError(f"forbidden manuscript metadata: {forbidden}")
    for required in (
        r"\author{Author information withheld until publication}",
        r"\date{Submission-neutral version, July 19, 2026}",
        "None of the cited papers is itself a member",
        "The seven rows are architecture representatives, not a claim that the",
    ):
        if required not in tex:
            raise AssertionError(f"submission text lost required boundary: {required}")
    bibitems = set(re.findall(r"\\bibitem\{([^}]+)\}", tex))
    if bibitems != EXPECTED_BIB_KEYS:
        raise AssertionError("bibliography does not exactly cover the source corpus")
    citation_groups = re.findall(r"\\cite(?:\[[^\]]*\])?\{([^}]+)\}", tex)
    cited = {
        key.strip()
        for group in citation_groups
        for key in group.split(",")
        if key.strip()
    }
    if not bibitems <= cited:
        raise AssertionError(f"uncited bibliography entries: {bibitems - cited}")

    tectonic = shutil.which("tectonic")
    if tectonic is None:
        raise AssertionError("tectonic is required to rebuild the manuscript")
    BUILD.mkdir(parents=True, exist_ok=True)
    run(
        [
            tectonic,
            "manuscript.tex",
            "--outdir",
            "build",
            "--keep-logs",
            "--keep-intermediates",
        ]
    )
    log = (BUILD / "manuscript.log").read_text(encoding="utf-8", errors="replace")
    forbidden_log_fragments = (
        "Overfull",
        "Underfull",
        "LaTeX Warning",
        "Package hyperref Warning",
        "undefined references",
        "multiply defined",
    )
    present = [fragment for fragment in forbidden_log_fragments if fragment in log]
    if present:
        raise AssertionError(f"TeX log contains warnings: {present}")

    info = run(["pdfinfo", str(PDF)], capture=True).stdout
    match = re.search(r"^Pages:\s+(\d+)$", info, re.MULTILINE)
    if not match:
        raise AssertionError("pdfinfo did not report a page count")
    pages = int(match.group(1))
    if pages < 12:
        raise AssertionError(f"manuscript unexpectedly short: {pages} pages")

    pdf_text = normalized(run(["pdftotext", str(PDF), "-"], capture=True).stdout)
    if len(pdf_text.split()) < 5_000:
        raise AssertionError("rendered manuscript is unexpectedly short")
    required_pdf_fragments = (
        "interior barriers for robust positivity programs toward the riemann hypothesis",
        "accumulating counterexample interior certificate",
        "de bruijn newman heat flow and universal factors",
        "jensen polynomial hyperbolicity",
        "generalized laguerre and turan inequalities",
        "weil and bombieri quadratic forms",
        "de branges space positivity",
        "canonical system hamiltonians",
        "polya frequency and total positivity",
        "no cited paper is a member",
        "outside reader worksheet",
        "recorded surviving mechanisms",
        "what survives",
        "set insert target set range counterexample",
    )
    for fragment in required_pdf_fragments:
        if fragment not in pdf_text:
            raise AssertionError(f"rendered PDF missing required text: {fragment}")
    return pages


def main() -> None:
    verify_archive_manifest()
    source_keys = verify_sources()
    verify_dossiers(source_keys)
    verify_hostile_audit()
    verify_referee_audit()
    verify_formal_and_canon()
    verify_submission_package()
    pages = verify_build()
    print(
        "verified manuscript: "
        f"{len(source_keys)} pinned primary sources, 7 nonmember program "
        f"dossiers, 2 controls, synchronized Lean, {pages}-page clean PDF"
    )


if __name__ == "__main__":
    main()
