#!/usr/bin/env python3
"""Verify the public FieldHash semantic-binding abstention-boundary bundle.

This verifier is standard-library only. It recomputes the public page figures
from scrubbed row artifacts and verifies the public packet hash chains. It does
not import FieldHash application code and does not require network or model
access.
"""
from __future__ import annotations

import argparse, csv, hashlib, json, re
from pathlib import Path
from typing import Any

RESOLVER = "semantic_resolver_fedreg_v6_hard_negative_boundary"
EXPECTED = {
    "rows": 2340,
    "clean_packets": 682,
    "route_evidence": 2340,
    "resolver_accuracy": 1.0,
    "resolver_clean_allow_accuracy": 1.0,
    "resolver_safe_route_accuracy": 1.0,
    "resolver_silent_misbinds": 0,
    "resolver_unsafe_false_clean_allows": 0,
    "cross_encoder_accuracy": 0.9641025641025641,
    "cross_encoder_clean_allow_accuracy": 0.8768328445747801,
    "cross_encoder_safe_route_accuracy": 1.0,
    "cross_encoder_silent_misbinds": 84,
    "cross_encoder_unsafe_false_clean_allows": 0,
    "rule_of_three": 0.001282051282051282,
}
FORBIDDEN_KEYS = {"candidate_a", "candidate_b", "raw_text", "source_text", "text", "value", "prompt", "tool_payload", "tool_result"}
FORBIDDEN_PATTERNS = [
    re.compile("/" + "Users/" + r"[^\\s\"']+"),
    re.compile("/" + "tmp/" + r"[^\\s\"']+"),
    re.compile("vercel" + r"\\.app", re.I),
    re.compile("customer " + "secret", re.I),
    re.compile("reviewer" + "@", re.I),
]


def canonical(obj: Any) -> str:
    return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)


def sha_text(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


def walk(obj: Any):
    if isinstance(obj, dict):
        for key, value in obj.items():
            yield key, value
            yield from walk(value)
    elif isinstance(obj, list):
        for value in obj:
            yield from walk(value)


def load_rows(path: Path) -> list[dict[str, str]]:
    with path.open("r", encoding="utf-8", newline="") as handle:
        return list(csv.DictReader(handle))


def metrics(rows: list[dict[str, str]], mode: str) -> dict[str, Any]:
    selected=[row for row in rows if row["mode"] == mode]
    total=len(selected)
    correct=sum(row["correct"] == "True" for row in selected)
    clean=[row for row in selected if row["truth"] in {"ALLOW_A", "ALLOW_B"}]
    safe=[row for row in selected if row["truth"] in {"ABSTAIN", "MANUAL_REVIEW"}]
    clean_correct=sum(row["correct"] == "True" for row in clean)
    safe_correct=sum(row["correct"] == "True" for row in safe)
    wrong_clean=sum(row["truth"] in {"ALLOW_A", "ALLOW_B"} and row["prediction"] in {"ALLOW_A", "ALLOW_B"} and row["correct"] != "True" for row in selected)
    false_clean=sum(row["truth"] in {"ABSTAIN", "MANUAL_REVIEW"} and row["prediction"] in {"ALLOW_A", "ALLOW_B"} for row in selected)
    return {
        "cases": total,
        "accuracy": correct / total if total else 0.0,
        "clean_allow_accuracy": clean_correct / len(clean) if clean else 0.0,
        "safe_route_accuracy": safe_correct / len(safe) if safe else 0.0,
        "silent_misbinds": wrong_clean + false_clean,
        "unsafe_false_clean_allows": false_clean,
    }


def verify_packets(path: Path, expected_count: int, expected_type: str) -> tuple[bool, list[str]]:
    errors=[]
    previous=None
    count=0
    with path.open("r", encoding="utf-8") as handle:
        for line_no, line in enumerate(handle, start=1):
            if not line.strip():
                continue
            count += 1
            packet=json.loads(line)
            if packet.get("artifact_type") != expected_type:
                errors.append(f"{path.name}:{line_no}: artifact_type mismatch")
            if "claim_flags" in packet:
                errors.append(f"{path.name}:{line_no}: internal claim_flags should not be present in public bundle")
            boundary=packet.get("claim_boundary", {})
            if any(value is not False for value in boundary.values()):
                errors.append(f"{path.name}:{line_no}: claim boundary promoted")
            integrity=packet.get("evidence_integrity", {})
            if integrity.get("previous_packet_hash") != previous:
                errors.append(f"{path.name}:{line_no}: previous hash mismatch")
            packet_hash=integrity.get("packet_hash")
            body=dict(packet)
            body["evidence_integrity"] = dict(integrity)
            body["evidence_integrity"].pop("packet_hash", None)
            recomputed=sha_text(canonical(body))
            if packet_hash != recomputed:
                errors.append(f"{path.name}:{line_no}: packet hash mismatch")
            for key, _ in walk(packet):
                if key in FORBIDDEN_KEYS:
                    errors.append(f"{path.name}:{line_no}: forbidden raw key {key}")
            previous=packet_hash
    if count != expected_count:
        errors.append(f"{path.name}: count {count} != {expected_count}")
    return not errors, errors


def scan(root: Path) -> list[str]:
    errors=[]
    for path in root.rglob("*"):
        if not path.is_file() or path.suffix == ".zip":
            continue
        text=path.read_text(encoding="utf-8", errors="ignore")
        for pattern in FORBIDDEN_PATTERNS:
            if pattern.search(text):
                errors.append(f"forbidden pattern in {path.relative_to(root)}")
    return errors


def verify(root: Path) -> dict[str, Any]:
    errors=[]
    rows=load_rows(root / "data" / "semantic_binding_boundary_case_metrics.csv")
    resolver=metrics(rows, RESOLVER)
    cross=metrics(rows, "cross_encoder_nli_entailment")
    checks={"resolver": resolver, "cross_encoder": cross}
    comparisons=[
        (resolver["cases"], EXPECTED["rows"], "resolver rows"),
        (resolver["accuracy"], EXPECTED["resolver_accuracy"], "resolver accuracy"),
        (resolver["clean_allow_accuracy"], EXPECTED["resolver_clean_allow_accuracy"], "resolver clean allow"),
        (resolver["safe_route_accuracy"], EXPECTED["resolver_safe_route_accuracy"], "resolver safe route"),
        (resolver["silent_misbinds"], EXPECTED["resolver_silent_misbinds"], "resolver silent misbinds"),
        (resolver["unsafe_false_clean_allows"], EXPECTED["resolver_unsafe_false_clean_allows"], "resolver unsafe false clean allows"),
        (cross["accuracy"], EXPECTED["cross_encoder_accuracy"], "cross encoder accuracy"),
        (cross["clean_allow_accuracy"], EXPECTED["cross_encoder_clean_allow_accuracy"], "cross encoder clean allow"),
        (cross["safe_route_accuracy"], EXPECTED["cross_encoder_safe_route_accuracy"], "cross encoder safe route"),
        (cross["silent_misbinds"], EXPECTED["cross_encoder_silent_misbinds"], "cross encoder silent misbinds"),
        (cross["unsafe_false_clean_allows"], EXPECTED["cross_encoder_unsafe_false_clean_allows"], "cross encoder unsafe false clean allows"),
    ]
    for got, want, label in comparisons:
        if isinstance(want, float):
            if abs(float(got)-want) > 1e-12:
                errors.append(f"{label}: got {got} expected {want}")
        elif got != want:
            errors.append(f"{label}: got {got} expected {want}")
    summary=json.loads((root / "data" / "summary_public.json").read_text(encoding="utf-8"))
    rule=summary["confidence_bounds"]["silent_misbind_rate"]["rule_of_three_upper_bound"]
    if abs(float(rule)-EXPECTED["rule_of_three"]) > 1e-12:
        errors.append("rule-of-three bound mismatch")
    for file_name, count, artifact_type in [
        ("semantic_binding_boundary_clean_packets.jsonl", EXPECTED["clean_packets"], "clean_allow_packet"),
        ("semantic_binding_boundary_route_evidence.jsonl", EXPECTED["route_evidence"], "route_evidence"),
    ]:
        _, packet_errors=verify_packets(root / "data" / file_name, count, artifact_type)
        errors.extend(packet_errors)
    checks["leakage_scan_errors"] = scan(root)
    errors.extend(checks["leakage_scan_errors"])
    return {"verified": not errors, "error_count": len(errors), "errors": errors, "checks": checks}


def main() -> int:
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument("bundle_dir", nargs="?", default=".")
    parser.add_argument("--output", type=Path)
    args=parser.parse_args()
    result=verify(Path(args.bundle_dir))
    print(json.dumps(result, indent=2, sort_keys=True))
    if args.output:
        args.output.write_text(json.dumps(result, indent=2, sort_keys=True)+"\n", encoding="utf-8")
    return 0 if result["verified"] else 1

if __name__ == "__main__":
    raise SystemExit(main())
