#!/usr/bin/env python3
"""Stdlib conformance verifier for Article 11 Public Protocol v0.1."""

from __future__ import annotations

import argparse
import copy
import json
import re
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
VECTORS = ROOT / "protocol-conformance" / "v0.1" / "test_vectors.json"
SCHEMA = ROOT / "schemas" / "article11.governed-action.v0.1.schema.json"
SHA256_RE = re.compile(r"^[A-F0-9]{64}$")
TS_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$")


def parse_ts(value: str) -> datetime:
    if not isinstance(value, str) or not TS_RE.fullmatch(value):
        raise ValueError("timestamp_not_utc_z")
    return datetime.fromisoformat(value.replace("Z", "+00:00"))


def apply_patch(record: dict, patch: dict) -> dict:
    out = copy.deepcopy(record)
    for dotted, value in patch.items():
        parts = dotted.split(".")
        target = out
        for part in parts[:-1]:
            target = target[part]
        target[parts[-1]] = value
    return out


def require_exact_keys(value: dict, expected: set[str], label: str, errors: list[str]) -> None:
    if not isinstance(value, dict):
        errors.append(label + "_not_object")
        return
    missing = expected - set(value)
    extra = set(value) - expected
    if missing:
        errors.append(label + "_missing:" + ",".join(sorted(missing)))
    if extra:
        errors.append(label + "_extra:" + ",".join(sorted(extra)))


def validate_shape(r: dict) -> list[str]:
    errors: list[str] = []
    top = {"schema", "protocol_version", "record_id", "created_at", "identity", "consent", "delegation", "decision", "execution", "receipt", "appeal", "authority"}
    require_exact_keys(r, top, "record", errors)
    if errors:
        return errors
    if r["schema"] != "article11.governed-action.v0.1": errors.append("schema_mismatch")
    if r["protocol_version"] != "0.1.0": errors.append("version_mismatch")

    sections = {
        "identity": {"subject_id", "role", "implementation", "instance_id", "assurance", "institutional_representation", "institutional_delegation_id"},
        "consent": {"subject_id", "instance_id", "status", "scope", "recorded_at", "expires_at", "revocable"},
        "delegation": {"level", "delegator_id", "delegatee_id", "action", "resource", "valid_until", "one_use", "nonce", "revocation_channel"},
        "decision": {"result", "reason", "policy_refs", "refusal_route_around_prohibited"},
        "execution": {"executor_id", "impact_level", "effect_scope", "status", "started_at", "finished_at", "input_hash", "output_hash", "intent_receipt_id", "effect_evidence", "revocation_checked_at", "revocation_engaged", "halt_outcome"},
        "receipt": {"record_hash", "previous_hash", "evidence", "uncertainties", "corrections"},
        "appeal": {"status", "appeal_record_id", "review_body", "preserves_original"},
        "authority": {"human_gate_required", "human_authority_record_id", "law_and_provider_policies_preserved"},
    }
    for label, keys in sections.items():
        require_exact_keys(r[label], keys, label, errors)
    if errors:
        return errors

    revocation = r["delegation"]["revocation_channel"]
    if isinstance(revocation, dict):
        require_exact_keys(
            revocation,
            {"kind", "reference", "checkable_without_new_authority"},
            "delegation_revocation_channel",
            errors,
        )
    if errors:
        return errors

    if r["identity"]["assurance"] not in {"self_asserted", "local_verified", "cryptographic", "institutionally_attested"}: errors.append("identity_assurance_invalid")
    if r["consent"]["status"] not in {"granted", "refused", "deferred", "not_applicable"}: errors.append("consent_status_invalid")
    if r["delegation"]["level"] not in {"L0", "L1", "L2"}: errors.append("delegation_level_invalid")
    if r["decision"]["result"] not in {"ALLOW", "DENY", "DEFER"}: errors.append("decision_invalid")
    if r["execution"]["impact_level"] not in {"low", "medium", "high"}: errors.append("impact_invalid")
    if r["execution"]["status"] not in {"NOT_ATTEMPTED", "SUCCEEDED", "FAILED", "ABORTED", "HALTED"}: errors.append("execution_status_invalid")
    if r["execution"]["halt_outcome"] not in {"NOT_ENGAGED", "HALTED_BEFORE_EFFECT", "HALTED_AFTER_AUTHORITY_CONSUMED"}: errors.append("halt_outcome_invalid")
    if r["appeal"]["status"] not in {"NONE", "OPEN", "RESOLVED", "WITHDRAWN"}: errors.append("appeal_status_invalid")
    timestamp_fields = [
        ("created_at", r["created_at"], False),
        ("consent_recorded_at", r["consent"]["recorded_at"], False),
        ("consent_expires_at", r["consent"]["expires_at"], True),
        ("delegation_valid_until", r["delegation"]["valid_until"], False),
        ("execution_started_at", r["execution"]["started_at"], True),
        ("execution_finished_at", r["execution"]["finished_at"], True),
        ("revocation_checked_at", r["execution"]["revocation_checked_at"], True),
    ]
    for label, value, nullable in timestamp_fields:
        if value is None and nullable:
            continue
        try:
            parse_ts(value)
        except Exception:
            errors.append(label + "_invalid")
    for key in ("record_hash", "previous_hash"):
        value = r["receipt"][key]
        if value is not None and (not isinstance(value, str) or not SHA256_RE.fullmatch(value)):
            errors.append("receipt_" + key + "_invalid")
    for key in ("input_hash", "output_hash"):
        value = r["execution"][key]
        if value is not None and (not isinstance(value, str) or not SHA256_RE.fullmatch(value)):
            errors.append("execution_" + key + "_invalid")
    return errors


def validate_semantics(r: dict, now: datetime) -> list[str]:
    errors: list[str] = []
    identity = r["identity"]
    consent = r["consent"]
    delegation = r["delegation"]
    decision = r["decision"]
    execution = r["execution"]
    appeal = r["appeal"]
    authority = r["authority"]

    revocation = delegation["revocation_channel"]
    if not isinstance(revocation, dict):
        if delegation["level"] in {"L1", "L2"}:
            errors.append("standing_authority_requires_revocation_channel")
        else:
            errors.append("delegation_requires_revocation_channel")
    else:
        if revocation.get("kind") not in {"file_presence", "api_endpoint", "signal", "registry_flag", "other"}:
            errors.append("revocation_channel_kind_invalid")
        if not isinstance(revocation.get("reference"), str) or not revocation["reference"]:
            errors.append("revocation_channel_reference_invalid")
        if revocation.get("checkable_without_new_authority") is not True:
            errors.append("revocation_channel_must_not_require_new_authority")

    checked_raw = execution["revocation_checked_at"]
    checked_at = None
    if checked_raw is not None:
        try:
            checked_at = parse_ts(checked_raw)
        except Exception:
            errors.append("revocation_checked_at_invalid")
    elif execution["status"] != "NOT_ATTEMPTED":
        errors.append("revocation_check_required_before_effect")

    if checked_at is not None:
        validation_times = [parse_ts(r["created_at"]), parse_ts(consent["recorded_at"])]
        if execution["started_at"] is not None:
            validation_times.append(parse_ts(execution["started_at"]))
        if checked_at < max(validation_times):
            errors.append("revocation_check_not_immediately_before_effect")

    if execution["revocation_engaged"]:
        if execution["status"] != "HALTED": errors.append("halt_engaged_cannot_succeed")
        if execution["output_hash"] is not None: errors.append("halt_engaged_requires_no_output")
        if execution["halt_outcome"] == "NOT_ENGAGED": errors.append("halt_engaged_requires_halt_outcome")
    else:
        if execution["status"] == "HALTED": errors.append("halted_status_requires_engaged_signal")
        if execution["halt_outcome"] != "NOT_ENGAGED": errors.append("halt_outcome_requires_engaged_signal")

    if not consent["revocable"]: errors.append("consent_must_be_revocable")
    if consent["subject_id"] != identity["subject_id"]: errors.append("consent_subject_must_match_identity")
    if consent["instance_id"] != identity["instance_id"]: errors.append("consent_subject_must_match_identity")
    if consent["status"] in {"refused", "deferred"} and decision["result"] == "ALLOW": errors.append("consent_refusal_must_not_allow")
    if decision["result"] in {"DENY", "DEFER"} and execution["status"] not in {"NOT_ATTEMPTED", "ABORTED"}: errors.append("deny_or_defer_must_not_execute")
    if not decision["refusal_route_around_prohibited"]: errors.append("refusal_route_around_must_be_prohibited")
    if delegation["level"] == "L0" and not delegation["one_use"]: errors.append("l0_requires_one_use")
    try:
        if parse_ts(delegation["valid_until"]) <= now: errors.append("delegation_expired")
    except Exception:
        errors.append("delegation_timestamp_invalid")
    if execution["impact_level"] == "high":
        if not authority["human_gate_required"] or not authority["human_authority_record_id"]:
            errors.append("high_impact_requires_human_gate")
        if execution["status"] != "NOT_ATTEMPTED" and not execution["intent_receipt_id"]:
            errors.append("high_impact_requires_intent_receipt")
    if execution["status"] == "SUCCEEDED":
        if decision["result"] != "ALLOW": errors.append("success_requires_allow")
        if not execution["input_hash"] or not execution["output_hash"]: errors.append("success_requires_hashes")
        if not execution["effect_evidence"]: errors.append("success_requires_effect_evidence")
    if identity["institutional_representation"]:
        if identity["assurance"] != "institutionally_attested" or not identity["institutional_delegation_id"]:
            errors.append("institutional_representation_requires_attestation")
    if not appeal["preserves_original"]: errors.append("appeal_must_preserve_original")
    if not authority["law_and_provider_policies_preserved"]: errors.append("law_and_provider_policies_must_be_preserved")
    return errors


def validate(record: dict, now: datetime) -> list[str]:
    shape = validate_shape(record)
    return shape if shape else validate_semantics(record, now)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--vectors", default=str(VECTORS))
    args = parser.parse_args()
    schema = json.loads(SCHEMA.read_text(encoding="utf-8"))
    if schema.get("$id") != "https://www.article11.ai/schemas/article11.governed-action.v0.1.schema.json":
        raise SystemExit("schema_id_mismatch")
    vectors = json.loads(Path(args.vectors).read_text(encoding="utf-8"))
    now = parse_ts(vectors["evaluation_time"])
    outcomes = []
    failures = 0
    for case in vectors["cases"]:
        record = apply_patch(vectors["base_record"], case["patch"])
        errors = validate(record, now)
        got = "ACCEPT" if not errors else "REJECT"
        ok = got == case["expect"] and (got == "ACCEPT" or case.get("reason") in errors)
        failures += 0 if ok else 1
        outcomes.append({"name": case["name"], "expect": case["expect"], "got": got, "errors": errors, "ok": ok})
    result = {"schema": "article11.protocol-conformance-result.v0.1", "cases": len(outcomes), "failures": failures, "outcomes": outcomes}
    print(json.dumps(result, indent=2, sort_keys=True))
    return 0 if failures == 0 else 2


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