#!/usr/bin/env python3
"""Fail-closed validator for the immutable authz-web oracle registry."""
import hashlib
import json
import pathlib
import sys
from uuid import UUID
ROOT = pathlib.Path(__file__).resolve().parent
try:
    from jsonschema import Draft202012Validator, FormatChecker
except ImportError:
    print("BLOCKED dependency=jsonschema reason=Draft2020-12 validator unavailable")
    raise SystemExit(2)

def load(name):
    with (ROOT / name).open(encoding="utf-8") as handle:
        return json.load(handle)

def canonical(value):
    return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()

def digest(value):
    return "sha256:" + hashlib.sha256(value).hexdigest()

def fail(message):
    print("FAIL " + message)
    raise SystemExit(1)

def validate(instance, schema, name):
    errors = sorted(Draft202012Validator(schema, format_checker=FormatChecker()).iter_errors(instance), key=lambda e: list(e.absolute_path))
    if errors:
        fail(name + " " + "; ".join(f"/{'/'.join(map(str, e.absolute_path))}: {e.message}" for e in errors[:20]))

principals = load("principals.json")
validate(principals, load("principals.schema.json"), "principals.json")
manifest = load("manifest.json")
rows = []
with (ROOT / "cases.jsonl").open(encoding="utf-8") as handle:
    for number, line in enumerate(handle, 1):
        if not line.strip():
            continue
        try:
            row = json.loads(line, object_pairs_hook=lambda pairs: dict(pairs) if len(pairs) == len(dict(pairs)) else (_ for _ in ()).throw(ValueError("duplicate member")))
        except Exception as error:
            fail(f"cases.jsonl:{number} parse: {error}")
        validate(row, load("case.schema.json"), f"cases.jsonl:{number}")
        rows.append(row)
ids = [row["id"] for row in rows]
if ids != manifest["orderedIds"] or len(rows) != manifest["caseCount"]:
    fail("manifest membership/order/count mismatch")
if len(ids) != len(set(ids)) or ids != [f"AW-{number:03}" for number in range(1, len(ids) + 1)]:
    fail("IDs not unique contiguous ordered")
semantic = {row["id"]: row["description"] for row in rows}
if semantic != manifest["semanticKeys"] or len(set(semantic.values())) != len(semantic):
    fail("immutable semantic registry mismatch or duplicate")
if set(manifest.get("tombstones", [])) & set(ids):
    fail("tombstoned ID reused")
if digest(canonical(semantic)) != manifest["semanticRegistrySha256"]:
    fail("semantic registry digest mismatch")
if digest((ROOT / "cases.jsonl").read_bytes()) != manifest["casesSha256"]:
    fail("cases digest mismatch")
log_graph = manifest["loggingInventory"]
if digest(canonical(log_graph)) != manifest["loggingInventorySha256"]:
    fail("logging inventory digest mismatch")
nodes = {node["hopId"]: node for node in log_graph["nodes"]}
if log_graph["root"] not in nodes or not log_graph.get("closureSignature"):
    fail("logging graph root/signature missing")
reachable = {log_graph["root"]}
while True:
    added = {name for name, node in nodes.items() if set(node["upstream"]) & reachable}
    newer = reachable | added
    if newer == reachable:
        break
    reachable = newer
if reachable != set(nodes) or any(not node["ownerSignature"] for node in nodes.values()):
    fail("logging graph is unsigned or disconnected")
# Externally anchored immutable registries. These roots are deliberately compiled into
# the validator, outside every mutable manifest they protect.
PINNED_PRIOR = "sha256:3b571ff3c90c6ae743660958ab331654c24a414d09efeecdcc18b55b8f563417"
PINNED_INVENTORY = "sha256:a64d5b374abeda35b2e6b19b7d84d9c49bd5b361f6d476a3ec2a514401f7a118"
PINNED_CONTRACTS = "sha256:5262671230dfec73599753f0a3f3406b35be7a233476e567c2607963e6863f27"
PINNED_OPERATIONS = "sha256:25f4d11a4b74b84dcfe4680832d49daddc38bdab6d89e0fac7d6fc825d407811"
PINNED_NEGATIVE = "sha256:d486b37717db7d8fc23dbb1fbb655894a5cc83927b9d72ac8bee5ffa2215ce27"
PINNED_EVIDENCE = "sha256:e3556e1e8a820cdc18d168c21655c005fd6ab980e04513e5bfa836b4c20166e3"
PINNED_AUTHORITY = "sha256:eb3bfc06c5ea97a4bb21cb1aed2c54cd072ccc629e13417766ea3e0596ad752a"
authority = load("authority-baseline.json")
if authority["authoritySha256"] != PINNED_AUTHORITY or digest(canonical({k: authority[k] for k in ("principals", "objects", "credentials")})) != PINNED_AUTHORITY:
    fail("external principal/object authority anchor mismatch")
if any(principals[k] != authority[k] for k in ("principals", "objects", "credentials")):
    fail("principal/object registry differs from external authority")
prior, inventory = load("immutable-prior-baseline.json"), load("signed-inventory-baseline.json")
contracts, operations = load("contract-registry.json"), load("operation-registry.json")
negative, evidence = load("negative-matrix.json"), load("adversarial-evidence.json")
if prior["baselineSha256"] != PINNED_PRIOR or digest(canonical({k: prior[k] for k in ("semanticKeys", "orderedIds", "tombstones")})) != PINNED_PRIOR:
    fail("immutable prior baseline anchor mismatch")
if prior["semanticKeys"] != semantic or prior["orderedIds"] != ids or set(prior["tombstones"]) & set(ids):
    fail("stable ID/semantic map differs from immutable prior or reuses tombstone")
if inventory["inventorySha256"] != PINNED_INVENTORY or digest(canonical(inventory["inventory"])) != PINNED_INVENTORY or inventory["inventory"] != log_graph:
    fail("external signed logging inventory mismatch")
if contracts["registrySha256"] != PINNED_CONTRACTS or digest(canonical(contracts["contracts"])) != PINNED_CONTRACTS:
    fail("external normative registry anchor mismatch")
if operations["registrySha256"] != PINNED_OPERATIONS or digest(canonical(operations["operations"])) != PINNED_OPERATIONS:
    fail("operation registry anchor mismatch")
if negative["matrixSha256"] != PINNED_NEGATIVE or digest(canonical(negative["attacks"])) != PINNED_NEGATIVE:
    fail("public negative matrix mismatch")
if evidence["evidenceSha256"] != PINNED_EVIDENCE or digest(canonical(evidence["checks"])) != PINNED_EVIDENCE:
    fail("retained adversarial evidence mismatch")
if {a["id"] for a in negative["attacks"]} != {e["attackId"] for e in evidence["checks"]}:
    fail("negative matrix lacks per-check retained evidence")
principal_ids = {name: item["id"] for name, item in principals["principals"].items()}
owner_by_id = {item["id"]: item["ownerId"] for item in principals["objects"].values()}
owner_by_id.update({item["id"]: item["id"] for item in principals["principals"].values()})

def auth_parser(request):
    authorization = request["headers"].get("Authorization")
    if authorization is not None:
        if authorization == "Bearer valid_bearer_user_a":
            return {"source": "bearer", "state": "valid", "principal": "userA"}
        malformed = not authorization.startswith("Bearer ") or authorization in ("bearer malformed", "Bearer malformed")
        return {"source": "bearer", "state": "malformed" if malformed else "invalid", "principal": None}
    token = request["cookies"].get("__Host-pdf2html_access")
    if token is not None:
        credential = principals["credentials"].get(token)
        return {"source": "cookie", "state": credential["parserState"] if credential else "invalid", "principal": credential["principal"] if credential else None}
    return {"source": "none", "state": "absent", "principal": None}

def derived_preflight(row):
    q, h = row["request"], row["request"]["headers"]
    if row["expected"]["outcome"] == "allow": category = "exact_success"
    elif h.get("Origin") != principals["policy"]["allowedOrigin"]: category = "reject_origin"
    elif "Access-Control-Request-Method" not in h: category = "reject_acr_missing"
    elif h.get("Access-Control-Request-Method") not in ("GET", "POST"): category = "reject_method"
    else: category = "reject_header"
    identity = {"route": q["path"], "origin": h.get("Origin"), "method": h.get("Access-Control-Request-Method"), "headers": h.get("Access-Control-Request-Headers")}
    return category, digest(canonical(identity))

for row in rows:
    q, expected, oracle = row["request"], row["expected"], row["oracle"]
    if not all(ref in contracts["contracts"] for ref in row["contractRefs"]):
        fail(row["id"] + " unresolved contractRef")
    if not any(item["method"] == q["method"] and item["path"] == q["path"] for item in operations["operations"].get(q["operation"], [])):
        fail(row["id"] + " operation/method/path outside exact registry")
    parsed = auth_parser(q)
    request_tuple = {"operation": q["operation"], "method": q["method"], "path": q["path"], "body": q["body"], "rawBody": q["rawBody"], "headers": q["headers"], "cookies": q["cookies"], "actor": row["actor"], "parser": parsed}
    response_tuple = {"status": expected["status"], "envelope": expected["envelope"], "outcome": expected["outcome"], "headers": expected["headers"], "effects": expected["effects"]}
    if oracle["parser"] != parsed or oracle["requestBindingSha256"] != digest(canonical(request_tuple)):
        fail(row["id"] + " request/auth/parser replay binding mismatch")
    if oracle["responseBindingSha256"] != digest(canonical(response_tuple)):
        fail(row["id"] + " status/envelope/outcome/header/effect replay binding mismatch")
    if expected["status"] == 200 and "error" in expected["envelope"] and expected["envelope"]["error"]["code"] == "AUTH_REQUIRED":
        fail(row["id"] + " 200 AUTH_REQUIRED is forbidden")
    if q["operation"] == "session" and parsed["state"] == "valid" and expected["outcome"] != "allow":
        fail(row["id"] + " valid bearer/cookie session must change malformed outcome")
    for target in oracle["targets"]:
        if owner_by_id.get(target["id"]) != target["ownerId"]:
            fail(row["id"] + " target owner not authoritative")
        actor_id = principal_ids.get(row["actor"])
        if row["category"] == "ownership" and actor_id != target["ownerId"]:
            if expected["outcome"] != "deny" or any(expected["effects"].values()):
                fail(row["id"] + " non-owner must deny without effects")
            if expected["effects"]["signerCalls"]:
                fail(row["id"] + " non-owner signerCalls must be zero")
    total_mutations = expected["effects"]["authMutations"] + expected["effects"]["businessMutations"]
    transitions = expected["transitions"]
    if total_mutations and (not transitions or len(transitions) != total_mutations):
        fail(row["id"] + " mutations require nonvacuous exact transition bindings")
    if transitions and [t["ordinal"] for t in transitions] != list(range(1, len(transitions) + 1)):
        fail(row["id"] + " transition ordinals not exact")
    audits = expected["auditRecords"]
    if len(audits) != expected["effects"]["auditEvents"]:
        fail(row["id"] + " audit counter/record mismatch")
    if row["category"] == "admin" and expected["outcome"] == "allow":
        if len(audits) != 1 or not audits[0]["immutable"] or audits[0]["actorId"] != principal_ids[row["actor"]] or audits[0]["occurredAt"] != row["fixtureClock"] or audits[0]["idempotencyKey"] != q["headers"].get("Idempotency-Key") or audits[0]["before"] != expected["stateDigestBefore"] or audits[0]["after"] != expected["stateDigestAfter"]:
            fail(row["id"] + " accepted admin command lacks exact immutable audit event")
    if row["concurrency"]:
        events = expected["eventResults"]
        if sum(e["mutationCommitted"] for e in events) != 1 or len({e["resultRef"] for e in events}) != 1:
            fail(row["id"] + " race requires exactly one winner and one shared durable result")
        for event in events:
            if not event["mutationCommitted"]:
                wanted = (409, "IDEMPOTENCY_CONFLICT") if row["id"] == "AW-066" else (200, None)
                if (event["status"], event["errorCode"]) != wanted:
                    fail(row["id"] + " race loser status/code drift")
    if q["operation"] == "preflight":
        category, matrix_key = derived_preflight(row)
        if oracle["preflight"]["category"] != category or oracle["preflight"]["matrixKey"] != matrix_key:
            fail(row["id"] + " preflight row identity/category mismatch")
    if row["category"] == "rate_limit":
        rate, assertion = oracle["rate"], expected["rateAssertion"]
        if q["operation"] == "readiness":
            if assertion is not None or rate["policyId"] is not None:
                fail(row["id"] + " readiness must not invent a rate identity")
        else:
            actor_id = principal_ids.get(row["actor"])
            expected_identity = f"ip:{q['client']['peerIp']}" if q["operation"] in ("registration", "login") else f"principal:{actor_id}"
            if assertion is None or assertion["operation"] != q["operation"] or assertion["identity"] != expected_identity or assertion["key"] != f"rate:{q['operation']}:{expected_identity}" or assertion["windowSeconds"] != 60 or assertion["limit"] != 10 or assertion["decision"] != ("allow" if assertion["count"] <= assertion["limit"] else "reject"):
                fail(row["id"] + " rate key/policy/window/exact-limit identity mismatch")
            if rate["operation"] != q["operation"] or rate["policyId"] not in principals["policy"]["rateOperations"] or rate["windowSeconds"] != assertion["windowSeconds"] or rate["exactLimit"] != assertion["limit"]:
                fail(row["id"] + " rate oracle/policy mismatch")

fixed = {
    "INVALID_REQUEST": ("Invalid request", False), "INVALID_CREDENTIALS": ("Invalid credentials", False),
    "AUTH_REQUIRED": ("Authentication required", False), "CSRF_REJECTED": ("CSRF rejected", False),
    "FORBIDDEN": ("Forbidden", False), "STATE_CONFLICT": ("Request conflict", False),
    "IDEMPOTENCY_CONFLICT": ("Idempotency key conflict", False), "RATE_LIMITED": ("Too many requests", True),
    "DEPENDENCY_UNAVAILABLE": ("Service temporarily unavailable", True), "NOT_FOUND": ("Not found", False),
}
for row in rows:
    UUID(row["expected"]["envelope"]["request_id"])
    expected = row["expected"]
    error = expected["envelope"].get("error")
    if error and error["code"] in fixed and (error["message"], error["retryable"]) != fixed[error["code"]]:
        fail(row["id"] + " noncanonical error tuple")
    if error and not isinstance(error["field_errors"], dict):
        fail(row["id"] + " field_errors is not a Record")
    events, concurrency = expected["eventResults"], row["concurrency"]
    if bool(events) != bool(concurrency) or concurrency and [item["eventId"] for item in events] != concurrency["events"]:
        fail(row["id"] + " concurrency/event order mismatch")
    if expected["outcome"] == "blocked" and not expected["blockedReason"]:
        fail(row["id"] + " BLOCKED requires reason")
    if expected["outcome"] != "blocked" and expected["blockedReason"] is not None:
        fail(row["id"] + " non-BLOCKED has reason")
    if row["category"] == "ownership" and expected["status"] == 404 and any(expected["effects"].values()):
        fail(row["id"] + " ownership denial side effect")
routes = ["register", "login", "logout", "session", "csrf_issue", "refresh_csrf", "refresh", "password_reset_request", "password_reset_complete"]
for operation in routes:
    descriptions = {row["description"] for row in rows if operation in row["description"] and row["request"]["operation"] == "preflight"}
    for suffix in ["exact success", "rejects origin", "rejects acr_missing", "rejects method", "rejects header"]:
        if not any(text.endswith(suffix) for text in descriptions):
            fail(f"OPTIONS matrix missing {operation} {suffix}")
required_fragments = ["registration atomic bootstrap", "access expiry", "refresh expiry", "generation increments", "CAS winner", "two tab", "full family ancestor replay", "exact replay", "trusted proxy", "crash seam"]
for fragment in required_fragments:
    if not any(fragment in row["description"] for row in rows):
        fail("required semantic coverage missing " + fragment)
print(f"PASS draft=2020-12 formats=asserted principals={len(principals['principals'])} cases={len(rows)} manifest=exact semantic=immutable digests=recomputed routes=closed logging=graph-closed readiness=BLOCKED")
