#!/usr/bin/env python3
"""Validator for The ODPC Record. Run after analyze.py; exits non-zero on any
failure. Mirrors the S6 check pattern: recompute aggregates independently,
pin every per-case figure quoted in prose to its dataset record, resolve
every internal URL against the built site and staged analyses, enforce the
editorial rules.
"""

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

HERE = Path(__file__).resolve().parent
SITE = HERE.parent.parent / "site"
STAGING = (HERE.parent.parent.parent.parent / "lawlab-organized"
           / "data_protection_digest" / "analysis-staging")
REPORT = HERE / "odpc-record.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.parent / "data" / "decisions.json").read_text())
ke = [x for x in decisions if x["jurisdiction"] == "KE"]
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_ke"] == f"{len(ke):,}", "n_ke matches dataset")
comp = [x["compensation_amount"] for x in ke if x.get("compensation_amount")]
check(figures["comp_median_kes"] == f"KES {round(statistics.median(comp)):,}",
      "compensation median matches")
check(figures["comp_total_kes"] == f"KES {sum(comp):,}",
      "compensation total matches")
upheld = [x for x in ke if "complaint upheld" in x["outcome"]]
dism = [x for x in ke if "complaint dismissed" in x["outcome"]]
check(figures["pct_upheld"] ==
      f"{round(100 * len(upheld) / (len(upheld) + len(dism)))}%",
      "uphold rate matches")
erasure = sum(1 for x in ke if "failure to respond to erasure request"
              in (x.get("violations") or []))
check(figures["n_viol_erasure"] == f"{erasure:,}", "erasure count matches")

# floors recomputed
floors, filed = defaultdict(int), Counter()
for x in ke:
    if x["decision_type"] != "determination":
        continue
    m = re.search(r"No\.?\s*(\d+)\s+of\s+(\d{4})", x.get("case_number") or "")
    if m and int(m.group(2)) in (2023, 2024, 2025):
        fy = int(m.group(2))
        floors[fy] = max(floors[fy], int(m.group(1)))
        filed[fy] += 1
check(figures["floor_3yr"] == f"{sum(floors.values()):,}",
      "3-year complaint floor matches")
check(figures["pub_3yr"] == f"{sum(filed.values()):,}",
      "3-year published count matches")

# 3 — per-case figures quoted in prose match the records
quoted = {
    "ke-odpc-2025-0013": ("KES 10,000,000",
                          lambda r: r["compensation_amount"] == 10000000),
    "ke-odpc-2024-0042": ("KES 9,043,407",
                          lambda r: r["compensation_amount"] == 9043407),
    "ke-odpc-2023-0008": ("KES 5,000,000",
                          lambda r: r["compensation_amount"] == 5000000),
    "ke-odpc-2025-0035": ("KES 2,000",
                          lambda r: r["compensation_amount"] == 2000),
    "ke-high-court-2026-0001": ("KES 250,000",
                                lambda r: "KES 250,000" in r["summary"]),
}
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}")

# dated events quoted in prose
dated = {
    "ke-odpc-2023-0019": ("2023-09-06", "6 September 2023"),
    "ke-high-court-2025-0001": ("2025-05-05", "5 May 2025"),
    "ke-high-court-2026-0001": ("2026-02-16", "16 February 2026"),
    "ke-odpc-2023-0001": ("2023-06-12", "12 June 2023"),
}
for rid, (iso, human) in dated.items():
    check(str(by_id[rid]["date_decided"]) == iso and human in text,
          f"date {human} traces to record {rid}")

# first determination: earliest decided, 23 co-complainants
dets = [x for x in ke if x["decision_type"] == "determination"]
first = min(dets, key=lambda x: str(x["date_decided"]))
check(first["id"] == "ke-odpc-2023-0001"
      and (first.get("extras") or {}).get("complainant_count") == 23
      and "23" in text,
      "first-determination claim (Firch, 23 complainants) traces to record")

# prose claims re-verified against data
check(all(not x.get("penalty_amount") for x in ke),
      "no penalty notice exists in the Kenyan corpus, as the prose claims")
lags = []
for x in dets:
    m = re.search(r"No\.?\s*\d+\s+of\s+(\d{4})", x.get("case_number") or "")
    if m:
        lags.append(int(str(x["date_decided"])[:4]) - int(m.group(1)))
check(max(lags) <= 1, "no complaint waited past the following year")
provs = Counter()
for x in ke:
    for p in x.get("provisions_cited") or []:
        m = re.match(r"(s\.\d+)", p)
        if m:
            provs[m.group(1)] += 1
check(provs.most_common(1)[0][0] == "s.56",
      "s.56 is the most-cited provision, as the prose claims")
top3 = sorted((x for x in ke if x.get("compensation_amount")),
              key=lambda x: -x["compensation_amount"])[:3]
check(all("unauthorised image/photo use" in (x.get("violations") or [])
          for x in top3),
      "the three largest awards are all image cases")
# September 2023 penalty trio arithmetic (press-sourced, W-CHECK flagged)
check(2975000 + 1850000 + 4550000 == 9375000
      and "KES 9,375,000" in text,
      "penalty-trio figures sum to the quoted total")

# 4 — tracker URLs resolve against the built 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
for slug in sorted(set(re.findall(
        r"https://lawlab\.africa/analysis/([a-z0-9-]+)/", text))):
    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(3800 <= n <= 7000, f"prose word count {n:,} within 3,800-7,000")

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