#!/usr/bin/env python3
"""Fail closed on evidence, manuscript, figure, and package inconsistencies."""

from __future__ import annotations

import csv
import json
import re
from pathlib import Path


ROOT = Path(__file__).resolve().parent.parent
TABLES = ROOT / "tables"
TEX = ROOT / "manuscript" / "mva_manuscript.tex"
ESM = ROOT / "supplement" / "ESM_1.tex"
BIB = ROOT / "manuscript" / "references.bib"
FIGURES = ROOT / "figures"
EVIDENCE = ROOT / "evidence"


def read_csv(name: str) -> list[dict[str, str]]:
    with (TABLES / name).open(encoding="utf-8-sig", newline="") as handle:
        return list(csv.DictReader(handle))


def assert_in_tex(tex: str, value: float, digits: int = 4) -> None:
    token = f"{value:.{digits}f}"
    if token not in tex:
        raise AssertionError(f"Formatted evidence value missing from manuscript: {token}")


def main() -> None:
    tex = TEX.read_text(encoding="utf-8")
    esm = ESM.read_text(encoding="utf-8")
    bib = BIB.read_text(encoding="utf-8")

    if "\\documentclass[iicol,pdflatex,sn-mathphys-num]{sn-jnl}" not in tex:
        raise AssertionError("MVA [iicol] Springer class option is missing")
    if re.search(r"/(Users|root|home)/", tex):
        raise AssertionError("Manuscript contains an absolute local path")

    abstract = re.search(r"\\abstract\{(.*?)\}\s*\\keywords", tex, re.S)
    if not abstract:
        raise AssertionError("Abstract was not found")
    abstract_words = re.findall(r"[A-Za-z]+(?:[-'][A-Za-z]+)*", abstract.group(1))
    if not 150 <= len(abstract_words) <= 250:
        raise AssertionError(f"MVA abstract has {len(abstract_words)} words, expected 150--250")
    keyword_match = re.search(r"\\keywords\{([^}]+)\}", tex)
    keyword_count = len([item for item in keyword_match.group(1).split(",") if item.strip()])
    if not 4 <= keyword_count <= 6:
        raise AssertionError(f"MVA keyword count is {keyword_count}, expected 4--6")

    bib_keys = set(re.findall(r"^@\w+\{([^,]+),", bib, re.M))
    cited_keys: set[str] = set()
    for group in re.findall(r"\\cite\{([^}]+)\}", tex):
        cited_keys.update(key.strip() for key in group.split(","))
    if cited_keys != bib_keys:
        raise AssertionError(
            f"Citation mismatch: missing={sorted(cited_keys - bib_keys)}, "
            f"uncited={sorted(bib_keys - cited_keys)}"
        )

    primary = read_csv("table_primary_livecell_test_n3.csv")
    if len(primary) != 4 or any(int(row["n_seeds"]) != 3 for row in primary):
        raise AssertionError("Primary table is not complete at four methods x three seeds")
    for row in primary:
        for key in (
            "segm_AP_mean", "segm_AP_sample_SD", "segm_AP50_mean",
            "segm_AP50_sample_SD", "segm_AR_mean", "segm_AR_sample_SD",
            "bbox_AP_mean", "bbox_AP_sample_SD",
        ):
            assert_in_tex(tex, float(row[key]))

    density = [row for row in read_csv("table_density_strata_n3.csv") if row["density"] != "all"]
    if len(density) != 16 or any(int(row["n_seeds"]) != 3 for row in density):
        raise AssertionError("Density table is not complete at four methods x four strata x three seeds")
    for row in density:
        assert_in_tex(tex, float(row["segm_AP_mean"]))
        assert_in_tex(tex, float(row["segm_AP_sample_SD"]))

    cellpose = read_csv("table_cellpose_external_n3.csv")
    if len(cellpose) != 4 or any(int(row["n_seeds"]) != 3 for row in cellpose):
        raise AssertionError("Cellpose supplementary diagnostic is incomplete")
    for row in cellpose:
        assert_in_tex(esm, float(row["segm_AP_mean"]), digits=6)
        assert_in_tex(esm, float(row["segm_AP_sample_SD"]), digits=6)
    if "\\label{tab:cellpose}" in tex or "Under a shared capped secondary Cellpose protocol" in abstract.group(1):
        raise AssertionError("Cellpose diagnostic was not removed from the main result chain")

    for name in ("matched_random_raw.csv", "factorial_100epoch_raw.csv"):
        for row in read_csv(name):
            assert_in_tex(tex, float(row["AP"]))
    factorial = read_csv("factorial_100epoch_raw.csv")
    factorial_seeds = {int(row["seed"]) for row in factorial}
    if factorial_seeds != {42, 43, 44}:
        raise AssertionError(f"Unexpected factorial seeds: {sorted(factorial_seeds)}")
    if len(factorial) != 4 * len(factorial_seeds):
        raise AssertionError("Factorial table is not a complete four-cell matrix per seed")

    cap_name = "table_maxdets_ablation_n3.csv"
    caps = read_csv(cap_name)
    if len(caps) != 16:
        raise AssertionError("maxDets table must contain four methods x four caps")
    expected_n = 3
    if any(int(row["n_seeds"]) != expected_n for row in caps):
        raise AssertionError("maxDets replication labels are inconsistent")
    for row in caps:
        assert_in_tex(esm, float(row["segm_AP_mean"]))
        assert_in_tex(esm, float(row["segm_AP_sample_SD"]))
        assert_in_tex(esm, float(row["segm_AR_mean"]))
        assert_in_tex(esm, float(row["segm_AR_sample_SD"]))

    for required in (
        EVIDENCE / "cellpose_split_file_manifest.csv",
        EVIDENCE / "cellpose_split_audit.json",
        EVIDENCE / "prediction_count_distribution.csv",
        EVIDENCE / "prediction_count_distribution_audit.json",
        EVIDENCE / "livecell_qualitative_audit.json",
        EVIDENCE / "cellpose_qualitative_audit.json",
    ):
        if not required.is_file() or required.stat().st_size == 0:
            raise AssertionError(f"Missing provenance artifact: {required}")

    with (EVIDENCE / "cellpose_split_audit.json").open(encoding="utf-8") as handle:
        cellpose_audit = json.load(handle)
    expected_cellpose_audit = {
        "train_annotations_total": 2886,
        "train_images_over_128": 7,
        "train_annotations_retained": 2103,
        "train_annotations_excluded": 783,
        "evaluation_annotations_total": 7201,
        "evaluation_images_over_100_gt": 28,
        "evaluation_gt_count_max": 374,
    }
    for key, expected in expected_cellpose_audit.items():
        if cellpose_audit.get(key) != expected:
            raise AssertionError(
                f"Unexpected Cellpose density audit value for {key}: "
                f"{cellpose_audit.get(key)} != {expected}"
            )
    for token in ("783", "2,886", "28", "374"):
        if token not in esm:
            raise AssertionError(f"Cellpose cap-audit token missing from Online Resource 1: {token}")

    prediction_rows = []
    with (EVIDENCE / "prediction_count_distribution.csv").open(
        encoding="utf-8-sig", newline=""
    ) as handle:
        prediction_rows = list(csv.DictReader(handle))
    overall = [row for row in prediction_rows if row["density"] == "all"]
    extreme = [row for row in prediction_rows if row["density"] == "extreme"]
    if len(overall) != 4 or len(extreme) != 4:
        raise AssertionError("Prediction inventory is not complete for four methods")
    if any(int(row["images_at_inference_cap"]) != 0 for row in prediction_rows):
        raise AssertionError("A prediction-inventory row unexpectedly reached the inference cap")
    for row in overall:
        if f"{float(row['pred_p99']):,.0f}" not in tex:
            raise AssertionError(f"Prediction P99 missing from manuscript: {row['method']}")
    for row in extreme:
        assert_in_tex(tex, float(row["mean_pred_to_gt_ratio"]), digits=3)

    maxdet_specs = {
        "coco_full": "coco_full_e20_s44_bblr1",
        "imagenet_coco": "imagenet_coco_e20_s44_bblr5",
        "simclr_coco": "simclr_coco_e20_s44_bblr5",
        "supcon_coco": "supcon_coco_e20_s44_bblr5",
    }
    for method, run_dir in maxdet_specs.items():
        audit_path = (
            EVIDENCE / "seed44_remote_raw" / run_dir / "test_official"
            / "protocol_ablation_maxdets_only" / f"{method}_seed44_maxdets_only_audit.json"
        )
        with audit_path.open(encoding="utf-8") as handle:
            audit = json.load(handle)
        gate = audit.get("formal_reproduction_gate", {})
        if audit.get("status") != "ok" or gate.get("passed") is not True:
            raise AssertionError(f"Failed seed44 maxDets audit: {audit_path}")
        if float(gate.get("absolute_error", float("inf"))) != 0.0:
            raise AssertionError(f"Seed44 AP@3000 was not reproduced exactly: {audit_path}")

    stale_phrases = (
        "seed-42/43 analysis",
        "two-seed maxDets",
        "remains a two-seed",
        "100-epoch factorial use two seeds",
        "100-epoch factorial controls have two",
        "two-seed pattern is descriptive",
    )
    for phrase in stale_phrases:
        if phrase in tex or phrase in esm:
            raise AssertionError(f"Stale maxDets replication wording remains: {phrase}")

    figure_names = set(re.findall(r"\\includegraphics(?:\[[^]]*\])?\{([^}]+)\}", tex))
    expected_figures = {f"Fig{i}_{stem}.pdf" for i, stem in (
        (1, "study_design"),
        (2, "primary_and_density"),
        (3, "component_and_optimization_controls"),
        (4, "dense_evaluation_caps"),
        (5, "livecell_qualitative"),
    )}
    if figure_names != expected_figures:
        raise AssertionError(f"Unexpected figure set: {sorted(figure_names)}")
    for pdf in expected_figures:
        suffixes = (".pdf", ".png") if pdf.startswith("Fig5_") else (".pdf", ".eps", ".png")
        for suffix in suffixes:
            path = FIGURES / (Path(pdf).stem + suffix)
            if not path.is_file() or path.stat().st_size == 0:
                raise AssertionError(f"Missing or empty figure artifact: {path}")

    expected_esm_figures = {
        "FigS_training_losses_seed44.pdf",
        "FigS_livecell_qualitative_supplement.pdf",
        "FigS_cellpose_qualitative_eight_cases.pdf",
    }
    esm_figure_names = set(
        re.findall(r"\\includegraphics(?:\[[^]]*\])?\{([^}]+)\}", esm)
    )
    if esm_figure_names != expected_esm_figures:
        raise AssertionError(f"Unexpected supplementary figure set: {sorted(esm_figure_names)}")
    for pdf in expected_esm_figures:
        for suffix in (".pdf", ".png"):
            path = FIGURES / (Path(pdf).stem + suffix)
            if not path.is_file() or path.stat().st_size == 0:
                raise AssertionError(f"Missing or empty supplementary figure artifact: {path}")

    for required in (
        "\\section*{Statements and Declarations}",
        "\\textbf{Funding}",
        "\\textbf{Competing interests}",
        "\\textbf{Data availability}",
        "\\textbf{Code availability}",
        "\\textbf{Author contributions}",
    ):
        if required not in tex:
            raise AssertionError(f"Required declaration missing: {required}")

    print(
        "MVA_PACKAGE_VALIDATION_OK",
        f"abstract_words={len(abstract_words)}",
        f"keywords={keyword_count}",
        f"references={len(bib_keys)}",
        f"maxdets_seeds={expected_n}",
    )


if __name__ == "__main__":
    main()
