"""
Package integrity verification -- Python standard library only.

Runs with no third-party packages installed. It does NOT reproduce the
analysis; it confirms that the package you received is the package that was
built, so that a failure later can be attributed to the environment rather
than to a corrupted or incomplete archive.

    python3 code/verify_package.py

For the scientific audit -- checking every manuscript number against the
computed outputs -- run code/audit_numbers.py, which does require
numpy, scipy, pandas, openpyxl and dendropy.
"""

import hashlib
import json
import os
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

ENSEMBLE_SHA256 = "d35d209a380b3a9db11ac809f0daa120c0ee91b9dbb23299c910aa2e906763e0"

EXPECTED = {
    "code": ["codecore.py", "context3.py", "coral.py",
             "decompose.py", "decompose_ctx.py", "prune.py", "tau2test.py",
             "optimize_code.py", "tree_check.py", "audit_numbers.py",
             "make_figures.py", "verify_package.py"],
    "data": ["cohortB_R_per_species.csv", "confirmatory_aligned.csv",
             "coral_counts.pkl", "nonsense_share.csv",
             "ensemble_summaries.npz", "cv_z_by_clade.csv",
             "cross_metric.csv", "coral_abund.pkl", "DATA_DICTIONARY.md",
             "spectrum_components.csv", "R_all_metrics.csv",
             "per_clade_slopes_phylo.csv", "rmse_decomposition.csv",
             "inverted_loco.csv", "confirmatory_ensemble.json",
             "coral_name_mapping.json", "tree_excluded_species.txt",
             "timetree_5053_species.nwk",
             "timetree_species_list.txt"],
    "protocol": ["protocol_v5_FROZEN.md"],
    "figures": ["Fig1.pdf", "Fig2.pdf", "Fig3.pdf", "Fig4.pdf",
                "Fig1.eps", "Fig2.eps", "Fig3.eps", "Fig4.eps"],
}

problems = []


def line(label, ok, detail=""):
    mark = "OK  " if ok else "FAIL"
    print(f"  [{mark}] {label}{('  -- ' + detail) if detail else ''}")
    if not ok:
        problems.append(label)


print("=" * 66)
print("PACKAGE VERIFICATION  (standard library only)")
print("=" * 66)
print(f"python {sys.version.split()[0]}")
print(f"root   {ROOT}")

print("\nFiles present")
# The public reproduction package (Online Resource 2) ships code, data,
# figures and protocol only; the manuscript sources stay with the authors.
if not os.path.isdir(os.path.join(ROOT, "manuscript")):
    EXPECTED.pop("manuscript", None)
    print("  [note] manuscript/ absent - public reproduction package layout")
for folder, names in EXPECTED.items():
    missing = [n for n in names
               if not os.path.isfile(os.path.join(ROOT, folder, n))]
    line(f"{folder}/  ({len(names) - len(missing)}/{len(names)})",
         not missing, ("missing: " + ", ".join(missing)) if missing else "")

print("\nFrozen ensemble")
path = os.path.join(ROOT, "data", "confirmatory_ensemble.json")
if os.path.isfile(path):
    blob = open(path, "rb").read()
    digest = hashlib.sha256(blob).hexdigest()
    line("SHA-256 matches the protocol", digest == ENSEMBLE_SHA256, digest[:24] + "...")
    try:
        d = json.loads(blob)
        line("10,000 codes, seed 20260727",
             d.get("n") == 10000 and d.get("seed") == 20260727,
             f"n={d.get('n')}, seed={d.get('seed')}")
        iu, ig, ia = (d["codon_order"].index(x) for x in ("UAA", "UAG", "UGA"))
        stops_fixed = all(c[iu] == c[ig] == c[ia] == "*" for c in d["codes"])
        line(f"stop codons fixed in all {len(d['codes']):,} codes", stops_fixed)
        uniq = len(set(d["codes"]))
        line("codes are distinct", uniq == len(d["codes"]),
             f"{uniq:,} distinct of {len(d['codes']):,}")
    except Exception as exc:                                   # noqa: BLE001
        line("ensemble parses as JSON", False, str(exc))
else:
    line("ensemble file present", False)

print("\nDerived data — row counts")
for name, expected_rows, label in [
        ("cohortB_R_per_species.csv", 5053, "species with R computed"),
        ("confirmatory_aligned.csv", 4972, "confirmatory set"),
        ("per_clade_slopes_phylo.csv", 8, "clade slopes"),
        ("rmse_decomposition.csv", 8, "out-of-clade folds"),
        ("inverted_loco.csv", 8, "inverted folds")]:
    p = os.path.join(ROOT, "data", name)
    if os.path.isfile(p):
        with open(p) as fh:
            n = sum(1 for _ in fh) - 1
        line(f"{label}: {expected_rows}", n == expected_rows, f"found {n}")
    else:
        line(f"{label}", False, "file missing")

print("\nPhylogeny")
p = os.path.join(ROOT, "data", "timetree_5053_species.nwk")
if os.path.isfile(p):
    txt = open(p).read()
    tips = txt.count(",") + 1
    line("tree parses and has ~5,030 tips", 4900 < tips < 5200, f"~{tips}")
else:
    line("tree file present", False)

if "manuscript" in EXPECTED:
    print("\nManuscript — word counts (wc -w equivalent)")
for name in EXPECTED.get("manuscript", []):
    p = os.path.join(ROOT, "manuscript", name)
    if os.path.isfile(p):
        n = len(open(p).read().split())
        print(f"  {name:<36} {n:>6}")

# The summary is printed after every check, including the file hashes, so that
# no line claims success before the last check has run.


def check_manifest():
    """Every file in SHA256SUMS.txt still hashes to its recorded value."""
    import hashlib
    man = os.path.join(ROOT, "SHA256SUMS.txt")
    if not os.path.isfile(man):
        print("\n[SKIP] SHA256SUMS.txt absent — file hashes not checked")
        return True
    bad, n = [], 0
    for line in open(man):
        if line.startswith("#") or not line.strip():
            continue
        h, _, rel = line.strip().partition("  ")
        p = os.path.join(ROOT, rel)
        if not os.path.isfile(p):
            bad.append(f"{rel}: missing")
            continue
        n += 1
        if hashlib.sha256(open(p, "rb").read()).hexdigest() != h:
            bad.append(f"{rel}: hash differs")
    if bad:
        print(f"\n[FAIL] SHA256SUMS  ({len(bad)} of {n + len(bad)} files)")
        for b in bad[:8]:
            print(f"       {b}")
        return False
    print(f"\n[OK  ] SHA256SUMS  ({n} files hash as recorded)")
    return True


print("\n" + "=" * 66)
hashes_ok = check_manifest()
print("\n" + "=" * 66)
if problems or not hashes_ok:
    if problems:
        print(f"{len(problems)} PROBLEM(S): " + "; ".join(problems))
    if not hashes_ok:
        print("File hashes do not match SHA256SUMS.txt.")
    raise SystemExit(1)
print("Configured integrity checks passed: file inventory, frozen ensemble,")
print("row counts of the five principal derived tables, and the SHA-256 of")
print("every file in SHA256SUMS.txt.")
print("This audits the deposited outputs. It does NOT verify the science --")
print("for that run code/audit_numbers.py, which needs numpy scipy pandas")
print("openpyxl dendropy matplotlib -- and it does not re-estimate the")
print("analyses that require the external CORAL table.")
