#!/usr/bin/env python3
"""Validator for the H1 2026 report. Run after analyze.py; exits non-zero on
any failure. Checks:

1. rendered report exists, no unresolved template tokens
2. key aggregates recomputed independently match figures.json
3. per-case figures quoted in prose match the dataset records
4. every tracker URL cited resolves to a built page in ../site
5. every lawlab.africa/analysis URL matches an S3 staged analysis
6. every chart referenced exists (svg + png)
7. editorial rules: no em dashes, no banned AI-tell phrases
8. prose word count inside the 4,000-7,000 commission
"""

import json
import re
import statistics
import sys
from collections import Counter
from pathlib import Path

HERE = Path(__file__).resolve().parent
SITE = HERE.parent / "site"
STAGING = (HERE.parent.parent.parent / "lawlab-organized"
           / "data_protection_digest" / "analysis-staging")
REPORT = HERE / "state-of-enforcement-h1-2026.md"

fails = []


def check(ok, msg):
    print(("  ok " if ok else "FAIL ") + msg)
    if not ok:
        fails.append(msg)


text = REPORT.read_text()
figures = json.loads((HERE / "figures.json").read_text())
decisions = json.loads((HERE.parent / "data" / "decisions.json").read_text())
landscape = json.loads((HERE.parent / "data" / "landscape.json").read_text())
by_id = {x["id"]: x for x in decisions}

# 1 — tokens
check("{{" not in text, "no unresolved template tokens")

# 2 — independent recompute of key aggregates
check(figures["n_total"] == f"{len(decisions):,}", "n_total matches dataset")
pens = sorted(x["penalty_usd"] for x in decisions if x.get("penalty_usd"))
check(figures["penalty_median_usd"] ==
      f"USD {round(statistics.median(pens)):,}", "penalty median matches")
comps = sorted(x["compensation_amount"] for x in decisions
               if x.get("compensation_usd"))
check(figures["comp_median_kes"] ==
      f"KES {round(statistics.median(comps)):,}", "compensation median matches")
check(figures["n_laws_inforce"] == str(sum(
    1 for c in landscape
    if (c.get("dp_law") or {}).get("status") == "in_force")),
    "laws-in-force count matches landscape")
check(figures["n_publishing"] == str(sum(
    1 for c in landscape if c.get("decisions_published"))),
    "publishing-regulator count matches landscape")
h1 = [x for x in decisions
      if "2026-01" <= str(x["date_decided"])[:7] <= "2026-06"]
check(figures["n_h1_2026"] == f"{len(h1):,}", "H1 2026 count matches")
check(all(x["confidence"] == "verified" for x in decisions),
      "every dataset record is verified")

# 3 — per-case figures quoted in prose match the records
quoted = {
    "ke-high-court-2026-0001": ("KES 250,000", lambda r: True),
    "ng-fccpc-2024-0001": ("USD 220,000,000",
                           lambda r: r["penalty_amount"] == 220000000
                           and r["penalty_currency"] == "USD"),
    "ng-ndpc-2025-0001": ("USD 32,800,000",
                          lambda r: r["penalty_amount"] == 32800000
                          and r["penalty_currency"] == "USD"),
    "ng-ndpc-2025-0004": ("NGN 766,242,500",
                          lambda r: r["penalty_amount"] == 766242500
                          and r["penalty_currency"] == "NGN"),
    "ng-ndpc-2024-0002": ("NGN 555,800,000",
                          lambda r: r["penalty_amount"] == 555800000
                          and r["penalty_currency"] == "NGN"),
    "ng-nitda-2021-0001": ("NGN 10,000,000",
                           lambda r: r["penalty_amount"] == 10000000
                           and r["penalty_currency"] == "NGN"),
    "za-fsca-2026-0002": ("R358,750,000",
                          lambda r: r["penalty_amount"] == 358750000
                          and r["penalty_currency"] == "ZAR"),
    "za-ir-2023-0003": ("R5,000,000",
                        lambda r: r["penalty_amount"] == 5000000
                        and r["penalty_currency"] == "ZAR"),
    "za-ir-2024-0007": ("R5,000,000",
                        lambda r: r["penalty_amount"] == 5000000
                        and r["penalty_currency"] == "ZAR"),
    "ug-pdpo-2025-0001": ("UGX 300,000",
                          lambda r: r["penalty_amount"] == 300000
                          and r["penalty_currency"] == "UGX"),
    "ke-odpc-2023-0008": ("KES 5,000,000",
                          lambda r: r["compensation_amount"] == 5000000),
    "ke-odpc-2025-0013": ("KES 10,000,000",
                          lambda r: r["compensation_amount"] == 10000000),
}
for rid, (figure, pred) in quoted.items():
    rec = by_id.get(rid)
    check(rec is not None and pred(rec) and figure in text,
          f"quoted figure {figure} matches record {rid}")

# status claims
check(by_id["ng-ndpc-2024-0002"]["status"] == "appealed",
      "Fidelity Bank status is 'appealed' as the prose claims")
check(by_id["za-ir-2024-0007"]["status"] == "appealed",
      "DBE status is 'appealed' as the prose claims")
check("12.7 million" in by_id["ng-ndpc-2026-0001"]["summary"]
      and "12.7 million" in text, "Temu user figure traces to record summary")
check("1,369" in by_id["ng-ndpc-2025-0005"]["respondent"]
      and "1,369" in text, "1,369-organisation figure traces to record")

# prosecution routes exist in KE (ODPC recommendations) and MU (s.20 police
# referrals) - the prose names both; anything from a third jurisdiction means
# the claim needs re-editing (wave-1 lesson: pattern-claims expire with data)
pros = [x for x in decisions if "prosecution" in x["outcome"] or
        ("police" in x["outcome"] and x["jurisdiction"] == "MU")]
check(all(x["jurisdiction"] in ("KE", "MU") for x in pros),
      "prosecution-route records are KE or MU only (prose names both)")

# 4 — tracker URLs resolve against the built S2 site
tracker_urls = sorted(set(re.findall(
    r"https://research\.lawlab\.africa(/tracker/[a-z0-9/-]+/)", text)))
for u in tracker_urls:
    check((SITE / u.strip("/") / "index.html").exists(), f"site page {u}")

# 5 — analysis URLs match S3 staged analyses
analysis_urls = sorted(set(re.findall(
    r"https://lawlab\.africa/analysis/([a-z0-9-]+)/", text)))
for slug in analysis_urls:
    check((STAGING / f"{slug}.html").exists(), f"analysis staged: {slug}")

# 6 — charts exist
for m in sorted(set(re.findall(r"\]\((charts/[a-z0-9.-]+)\)", text))):
    check((HERE / m).exists(), f"chart {m}")
    check((HERE / m.replace(".svg", ".png")).exists(), f"chart {m} png twin")

# 7 — editorial rules
check("—" not in text, "zero em dashes")
banned = ["delve", "robust", "crucial", "seamless", "leverage as",
          "game-changer", "underscores", "testament to",
          "at the intersection of", "dive into", "unpack", "key takeaways",
          "navigate the complex", "rapidly evolving", "important to note",
          "worth noting", "significant implications", "furthermore",
          "moreover", "in conclusion", "not only"]
low = text.lower()
for b in banned:
    check(b not in low, f"banned phrase absent: {b}")

# 8 — word count
prose = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", text)
prose = re.sub(r"https?://\S+", " ", prose)
prose = re.sub(r"[|#*_`>-]", " ", prose)
n = len(prose.split())
check(4000 <= n <= 7000, f"prose word count {n:,} within 4,000-7,000")

print()
if fails:
    print(f"{len(fails)} FAILURES")
    sys.exit(1)
print("all checks passed")
