"""harness_template.py

Exercise VI-A. A reproducibility harness to wrap around any earlier pipeline.

The worked example wraps the three variable need index from Exercise II-A,
Block 4, and sweeps the two choices that exercise showed to matter, the
geography and the standardization. To wrap a different pipeline, replace the body of
`pipeline` and the contents of `GRID`, and leave everything else alone.

The acceptance test is mechanical. Run with --twice and the harness writes
its report two times and compares the files byte for byte. A difference means
something in the pipeline is not pinned, and finding it is the exercise.

    python harness_template.py            write the report once
    python harness_template.py --twice    write it twice and compare
"""
from __future__ import annotations

import filecmp
import hashlib
import itertools
import json
import platform
import random
import sys
from pathlib import Path

import numpy as np

sys.path.insert(0, str(Path(__file__).resolve().parent))
import common as C                                     # noqa: E402

SNAPSHOT = C.SNAP / "acs2023_mississippi_asi.csv"
MANIFEST = C.SNAP / "manifest.json"
SEED = 20260824
TOPN = 10
CV_LIMIT = 30.0

# The parameter grid. Each key names a choice an earlier exercise showed to
# move the answer; each list holds the values worth comparing.
GRID = {
    "geography": ["block group", "tract"],
    "standardization": ["rank", "z", "minmax"],
}


# --------------------------------------------------------------------------
# Pinning.
# --------------------------------------------------------------------------
def verify_snapshot():
    """Fail loudly when the data under the analysis has changed."""
    expected = json.loads(MANIFEST.read_text())["sha256"]
    actual = hashlib.sha256(SNAPSHOT.read_bytes()).hexdigest()
    if actual != expected:
        raise SystemExit(f"SNAPSHOT MISMATCH\n  expected {expected}\n"
                         f"  found    {actual}\nThe data changed under the "
                         f"analysis. Stop and find out why.")
    return actual


def set_seeds():
    random.seed(SEED)
    np.random.seed(SEED)
    return SEED


def environment():
    """Versions recorded without timestamps, so two runs stay identical."""
    import matplotlib
    import pandas
    import scipy
    return {"python": platform.python_version(),
            "numpy": np.__version__, "pandas": pandas.__version__,
            "scipy": scipy.__version__, "matplotlib": matplotlib.__version__}


# --------------------------------------------------------------------------
# The pipeline under test. Replace this function to wrap another exercise.
# --------------------------------------------------------------------------
def pipeline(data, geography, standardization):
    d = data[data.level == geography].dropna(
        subset=["pct65", "pct_pov", "pct_nv"]).copy()
    d["idx"] = C.need_index(d, standardization)
    if geography == "block group":
        # Express block group results by parent tract so the geographies
        # can be compared on the same units.
        d["unit"] = d["code"].str[:11]
        top = (d.sort_values("idx", ascending=False)
               .drop_duplicates("unit").head(TOPN)["unit"])
    else:
        top = d.nlargest(TOPN, "idx")["code"]
    rel = d.dropna(subset=["medinc", "medinc_moe"])
    cv = 100 * (rel.medinc_moe / 1.645) / rel.medinc
    return {"top": sorted(top.tolist()),
            "failing_screen": int((cv > CV_LIMIT).sum()),
            "units": int(len(d))}


# --------------------------------------------------------------------------
# The report.
# --------------------------------------------------------------------------
def report():
    digest = verify_snapshot()
    seed = set_seeds()
    env = environment()
    data = C.load_acs()
    combos = list(itertools.product(*GRID.values()))
    results = {c: pipeline(data, *c) for c in combos}

    lines = ["UNCERTAINTY STRESS TEST REPORT", "=" * 60,
             "PROVENANCE",
             f"  snapshot   {SNAPSHOT.name}",
             f"  sha256     {digest}",
             f"  seed       {seed}"]
    lines += [f"  {k:<10} {v}" for k, v in env.items()]
    lines += ["", "PARAMETER GRID"]
    lines += [f"  {k:<16} {', '.join(v)}" for k, v in GRID.items()]
    lines += ["", f"TOP {TOPN} UNDER EACH COMBINATION"]
    for c, r in results.items():
        # Nine digits, county plus tract, is unique within one state. Six
        # digits collide across counties and look like duplicates.
        lines.append(f"  {' / '.join(c):<24} {' '.join(u[-9:] for u in r['top'])}")

    jac = [C.jaccard(results[a]["top"], results[b]["top"])
           for a, b in itertools.combinations(combos, 2)]
    fail = {c: r["failing_screen"] for c, r in results.items()}
    lines += ["", "THE FOUR MEASUREMENTS",
              f"  decision stability   Jaccard across {len(jac)} pairs, "
              f"min {min(jac):.3f}, mean {np.mean(jac):.3f}",
              "  reliability screen   units above "
              f"{CV_LIMIT:.0f} percent CV: "
              + ", ".join(f"{g} {n}" for g, n in
                          {c[0]: fail[c] for c in combos}.items()),
              "  cross validated error  not applicable, this pipeline "
              "fits no model",
              "  Monte Carlo spread     not applicable here, see Exercise "
              "IV-A for the propagated interval",
              ""]
    return "\n".join(lines) + "\n"


def write(path):
    path.write_text(report(), encoding="utf-8", newline="\n")
    return path


if __name__ == "__main__":
    C.ensure_dirs()
    a = write(C.OUT / "harness_report_run1.txt")
    print(a.read_text(encoding="utf-8"))
    if "--twice" in sys.argv:
        b = write(C.OUT / "harness_report_run2.txt")
        same = filecmp.cmp(a, b, shallow=False)
        print("REPRODUCIBILITY:", "byte identical across two runs" if same
              else "RUNS DIFFER, something is not pinned")
