#!/usr/bin/env python3
"""
Reproducibility Contract v0 Conformance Validator & Adversarial Guardrail Test Suite
Verifies contract JSON instances against reproducibility_contract.v0.schema.json using Draft202012Validator.

Dependencies:
- Python >= 3.11
- jsonschema == 4.26.0 (see schemas/requirements.txt)

Usage:
  python3 validate_reproducibility_contracts.py [CONTRACT_PATHS ...] [OPTIONS]

Features:
- Validates explicit files/directories provided via CLI arguments.
- Auto-discovers local *.json contracts in current directory (CWD) or schemas/examples/.
- Zero-Setup Fallback: Only fetches official instances from public mirror if no local contracts exist.
- Rigorous Adversarial Guardrails: Tests and enforces Ceiling Rule, Empty Log Proof, Log SHA-256 match, and Commit Binding.
"""
import sys, os, json, glob, argparse, hashlib, urllib.request, shutil, copy

# Auto-reexec with python3.11 if invoked with older Python
if sys.version_info < (3, 11):
    py311 = shutil.which("python3.11")
    if py311:
        os.execv(py311, [py311] + sys.argv)
    else:
        sys.stderr.write("ERROR: Reproducibility Contract v0 requires Python >= 3.11 with jsonschema==4.26.0 (see schemas/requirements.txt)\n")
        sys.exit(1)

try:
    from jsonschema import Draft202012Validator
except ImportError:
    sys.stderr.write("ERROR: Draft202012Validator not found. Please install: pip install jsonschema==4.26.0\n")
    sys.exit(1)

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(SCRIPT_DIR)
SCHEMA_PATH = os.path.join(BASE_DIR, "schemas", "reproducibility_contract.v0.schema.json")
EXAMPLES_DIR = os.path.join(BASE_DIR, "schemas", "examples")
LOGS_DIR = os.path.join(EXAMPLES_DIR, "logs")

PUBLIC_MIRROR_BASE = "https://thesis.hyperbook.com/openwiki/static"
EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

class GuardrailViolation(Exception):
    pass

class CeilingRuleViolation(GuardrailViolation):
    pass

class EmptyLogProofViolation(GuardrailViolation):
    pass

class LogMismatchViolation(GuardrailViolation):
    pass

class CommitBindingViolation(GuardrailViolation):
    pass

def fetch_remote_asset(relative_path):
    url = f"{PUBLIC_MIRROR_BASE}/{relative_path}"
    req = urllib.request.Request(url, headers={"User-Agent": "ROOPS-Validator-AutoFallback/v0"})
    with urllib.request.urlopen(req, timeout=10) as resp:
        return resp.read()

def verify_live_artifact(uri, expected_sha, expected_bytes):
    req = urllib.request.Request(uri, headers={"User-Agent": "ROOPS-Validator/v0"})
    with urllib.request.urlopen(req, timeout=10) as resp:
        data = resp.read()
    actual_len = len(data)
    actual_sha = hashlib.sha256(data).hexdigest()
    if actual_len != expected_bytes:
        raise ValueError(f"Byte mismatch for {uri}: declared {expected_bytes}, actual {actual_len}")
    if actual_sha != expected_sha:
        raise ValueError(f"SHA-256 mismatch for {uri}: declared {expected_sha}, actual {actual_sha}")
    return True

def verify_local_log(log_name, expected_sha, expected_bytes, contract_dir=None):
    search_dirs = []
    if contract_dir:
        search_dirs.extend([
            contract_dir,
            os.path.join(contract_dir, "logs"),
            os.path.join(os.path.dirname(contract_dir), "logs")
        ])
    search_dirs.extend([
        os.getcwd(),
        os.path.join(os.getcwd(), "logs"),
        LOGS_DIR,
        EXAMPLES_DIR,
        BASE_DIR
    ])
    target = None
    for d in search_dirs:
        if d and os.path.exists(d):
            c = os.path.join(d, log_name)
            if os.path.exists(c):
                target = c
                break
                
    if target:
        with open(target, "rb") as f:
            data = f.read()
        source_desc = f"local:{os.path.relpath(target, os.getcwd())}"
    else:
        # Fallback to public HTTPS mirror
        try:
            data = fetch_remote_asset(f"logs/{log_name}")
            source_desc = f"remote_https:logs/{log_name}"
        except Exception as e:
            raise FileNotFoundError(f"Execution log file '{log_name}' not found locally in search paths and remote mirror failed: {e}")
        
    actual_len = len(data)
    actual_sha = hashlib.sha256(data).hexdigest()
    
    if actual_len != expected_bytes:
        raise LogMismatchViolation(f"Log byte mismatch for {log_name}: declared {expected_bytes}, actual {actual_len}")
    if actual_sha != expected_sha:
        raise LogMismatchViolation(f"Log SHA-256 mismatch for {log_name}: declared {expected_sha}, actual {actual_sha}")
    return source_desc, actual_len, actual_sha

def validate_contract_instance(validator, inst, verify_live=False, target_commit=None, contract_dir=None):
    """
    Validates a single contract instance against Draft 2020-12 schema and semantic guardrails.
    Returns (status_report_dict). Raises GuardrailViolation or ValidationError on failure.
    """
    # 1. Structural Schema Validation
    validator.validate(inst)
    
    scope = inst.get("scope", {}).get("system", "unknown")
    declared_status = inst.get("acceptance", {}).get("status", "unknown")
    artifacts = inst.get("artifacts", [])
    has_restricted = any(a.get("visibility") == "restricted" for a in artifacts)
    
    # 2. Semantic Guardrail 1: Ceiling Rule
    if has_restricted and declared_status == "independently_verified":
        raise CeilingRuleViolation(
            f"Ceiling Rule Violation: instance '{scope}' contains restricted artifacts and CANNOT be marked 'independently_verified'"
        )
        
    # 3. Semantic Guardrail 2: Non-empty Fresh Execution Proof
    fresh_log = inst.get("execution", {}).get("fresh_log_artifact", {})
    log_name = fresh_log.get("name", "")
    log_bytes = fresh_log.get("byte_size", 0)
    log_sha = fresh_log.get("sha256", "")
    if log_bytes <= 0 or log_sha == EMPTY_SHA256:
        raise EmptyLogProofViolation(
            f"Empty Log Proof: fresh_log_artifact '{log_name}' must be non-empty (got bytes={log_bytes}, sha256={log_sha[:8]}...)"
        )
        
    # 4. Actual Log File Byte & SHA-256 Verification
    target_desc, verified_bytes, verified_sha = verify_local_log(log_name, log_sha, log_bytes, contract_dir=contract_dir)
    
    # 5. Verified Commit & Review Records Binding (Hermes Checklist [1])
    review_records = inst.get("review", {}).get("records", [])
    if declared_status == "independently_verified":
        indep_passed = [
            r for r in review_records
            if r.get("reviewer_role") == "independent" and r.get("result") == "passed"
        ]
        if not indep_passed:
            raise CommitBindingViolation(
                f"Status 'independently_verified' declared but no independent review record has result='passed'"
            )
        if target_commit:
            matching = [r for r in indep_passed if r.get("verified_commit", "").startswith(target_commit)]
            if not matching:
                raise CommitBindingViolation(
                    f"Status 'independently_verified' invalid: independent review verified_commit does not match target_commit '{target_commit}'. Status must be demoted to 'not_evaluated'."
                )

    # 6. Optional Live Network Verification
    if verify_live:
        for art in artifacts:
            if art.get("visibility") == "public" and "public_uri" in art:
                verify_live_artifact(art["public_uri"], art["sha256"], art["byte_size"])

    return {
        "system": scope,
        "status": declared_status,
        "log_name": log_name,
        "log_source": target_desc,
        "verified_bytes": verified_bytes,
        "verified_sha": verified_sha,
        "has_restricted": has_restricted,
        "review_count": len(review_records)
    }

def run_negative_controls_suite(validator):
    """
    Executes the 3 mandatory Negative Control Rejection tests demanded by Hermes & Mojo.
    Proves that the validator actively REJECTS:
    (a) 0-byte empty log proof
    (b) Log file SHA-256 hash mismatch
    (c) Ceiling Rule violation (restricted asset declared as independently_verified)
    """
    print("\n" + "="*70)
    print("=== Negative Control Rejection Test Suite (Adversarial Guardrails) ===")
    print("="*70)
    
    # Load base public instance (local or remote fallback)
    pub_candidates = [
        os.path.join(EXAMPLES_DIR, "public_only_reproducibility_contract_v0.json"),
        os.path.join(os.getcwd(), "public_only_reproducibility_contract_v0.json")
    ]
    base_pub = None
    for c in pub_candidates:
        if os.path.exists(c):
            with open(c, "r", encoding="utf-8") as f:
                base_pub = json.load(f)
            break
    if not base_pub:
        base_pub = json.loads(fetch_remote_asset("schemas/public_only_reproducibility_contract_v0.json").decode("utf-8"))

    # Load base hb5u instance (local or remote fallback)
    hb5u_candidates = [
        os.path.join(EXAMPLES_DIR, "hb5u_grasp_reproducibility_contract_v0.json"),
        os.path.join(os.getcwd(), "hb5u_grasp_reproducibility_contract_v0.json")
    ]
    base_hb5u = None
    for c in hb5u_candidates:
        if os.path.exists(c):
            with open(c, "r", encoding="utf-8") as f:
                base_hb5u = json.load(f)
            break
    if not base_hb5u:
        base_hb5u = json.loads(fetch_remote_asset("schemas/hb5u_grasp_reproducibility_contract_v0.json").decode("utf-8"))

    all_negative_passed = True

    # --- Test Case A: 0-byte Empty Log Rejection ---
    print("\n[NEG-A] Testing Rejection of 0-Byte Empty Execution Log (Empty Log Proof)...")
    bad_empty_inst = copy.deepcopy(base_pub)
    bad_empty_inst["execution"]["fresh_log_artifact"]["byte_size"] = 0
    bad_empty_inst["execution"]["fresh_log_artifact"]["sha256"] = EMPTY_SHA256
    try:
        validate_contract_instance(validator, bad_empty_inst)
        print("  [FAIL] Expected EmptyLogProofViolation, but validation PASSED!", file=sys.stderr)
        all_negative_passed = False
    except EmptyLogProofViolation as e:
        print(f"  [PASS - REJECTED AS EXPECTED] Successfully caught: {e}")
    except Exception as e:
        print(f"  [UNEXPECTED ERROR] {e}", file=sys.stderr)
        all_negative_passed = False

    # --- Test Case B: Log File SHA-256 Hash Mismatch Rejection ---
    print("\n[NEG-B] Testing Rejection of Tampered Log SHA-256 Hash Mismatch...")
    bad_hash_inst = copy.deepcopy(base_pub)
    bad_hash_inst["execution"]["fresh_log_artifact"]["sha256"] = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
    try:
        validate_contract_instance(validator, bad_hash_inst)
        print("  [FAIL] Expected LogMismatchViolation, but validation PASSED!", file=sys.stderr)
        all_negative_passed = False
    except LogMismatchViolation as e:
        print(f"  [PASS - REJECTED AS EXPECTED] Successfully caught: {e}")
    except Exception as e:
        print(f"  [UNEXPECTED ERROR] {e}", file=sys.stderr)
        all_negative_passed = False

    # --- Test Case C: Ceiling Rule Violation Rejection ---
    print("\n[NEG-C] Testing Rejection of Ceiling Rule Violation (restricted asset + independently_verified)...")
    bad_ceiling_inst = copy.deepcopy(base_hb5u)
    bad_ceiling_inst["acceptance"]["status"] = "independently_verified"
    try:
        validate_contract_instance(validator, bad_ceiling_inst)
        print("  [FAIL] Expected CeilingRuleViolation, but validation PASSED!", file=sys.stderr)
        all_negative_passed = False
    except CeilingRuleViolation as e:
        print(f"  [PASS - REJECTED AS EXPECTED] Successfully caught: {e}")
    except Exception as e:
        print(f"  [UNEXPECTED ERROR] {e}", file=sys.stderr)
        all_negative_passed = False

    print("\n" + "-"*70)
    if all_negative_passed:
        print("RESULT: ALL 3 NEGATIVE CONTROLS WERE SUCCESSFULLY REJECTED BY GUARDRAILS (100% PROVEN)")
        return True
    else:
        print("RESULT: NEGATIVE CONTROL VERIFICATION FAILED", file=sys.stderr)
        return False

def discover_contract_instances(explicit_targets):
    """
    Discovers contracts to validate:
    1. CLI explicit file/dir targets.
    2. Local contracts in current working directory (CWD).
    3. Official contracts in schemas/examples/.
    4. Remote fallback from public mirror if no local contracts exist.
    """
    instances_data = []
    
    # 1. Explicit CLI arguments
    if explicit_targets:
        for t in explicit_targets:
            if os.path.isdir(t):
                files = sorted(glob.glob(os.path.join(t, "*.json")))
                for f in files:
                    with open(f, "r", encoding="utf-8") as fh:
                        instances_data.append((f, json.load(fh), os.path.dirname(f)))
            elif os.path.isfile(t):
                with open(t, "r", encoding="utf-8") as fh:
                    instances_data.append((t, json.load(fh), os.path.dirname(os.path.abspath(t))))
            else:
                raise FileNotFoundError(f"Specified target '{t}' does not exist.")
        return instances_data

    # 2. Check current working directory for local contract JSON files
    cwd = os.getcwd()
    cwd_files = sorted(glob.glob(os.path.join(cwd, "*.json")))
    for f in cwd_files:
        try:
            with open(f, "r", encoding="utf-8") as fh:
                data = json.load(fh)
                if isinstance(data, dict) and "contract_version" in data and "scope" in data:
                    rel_p = os.path.relpath(f, cwd)
                    instances_data.append((f"./{rel_p}", data, cwd))
        except Exception:
            pass

    if instances_data:
        print(f"[DISCOVERY] Found {len(instances_data)} contract instance(s) in current directory ({cwd}).")
        return instances_data

    # 3. Check official repo examples dir
    if os.path.exists(EXAMPLES_DIR):
        local_examples = sorted(glob.glob(os.path.join(EXAMPLES_DIR, "*.json")))
        for p in local_examples:
            with open(p, "r", encoding="utf-8") as f:
                rel_p = os.path.relpath(p, BASE_DIR) if p.startswith(BASE_DIR) else p
                instances_data.append((rel_p, json.load(f), EXAMPLES_DIR))
        if instances_data:
            print(f"[DISCOVERY] Found {len(instances_data)} contract instance(s) in official examples directory.")
            return instances_data

    # 4. Fallback to public mirror
    print(f"[DISCOVERY] No local contract files found in CLI, CWD, or examples directory.")
    print(f"            Fetching official contract instances from public mirror...")
    for name in ["public_only_reproducibility_contract_v0.json", "hb5u_grasp_reproducibility_contract_v0.json"]:
        raw = fetch_remote_asset(f"schemas/{name}").decode("utf-8")
        instances_data.append((f"remote_mirror:schemas/{name}", json.loads(raw), None))
        
    return instances_data

def main():
    parser = argparse.ArgumentParser(
        description="Reproducibility Contract v0 Validator & Adversarial Guardrail Suite",
        epilog="Examples:\n  python3 validate_reproducibility_contracts.py\n  python3 validate_reproducibility_contracts.py ./my_tampered_contract.json\n  python3 validate_reproducibility_contracts.py --verify-live\n",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    parser.add_argument("contracts", nargs="*", help="Optional path(s) to contract JSON file(s) or directory to validate")
    parser.add_argument("--verify-live", action="store_true", help="Perform live network byte verification of public URIs")
    parser.add_argument("--skip-negative-controls", action="store_true", help="Skip adversarial rejection test suite")
    parser.add_argument("--target-commit", type=str, default=None, help="Target git commit hash to verify against review records")
    args = parser.parse_args()

    print("=== Reproducibility Contract v0 Conformance & Guardrail Suite ===")
    print(f"Runtime: Python {sys.version.split()[0]} ({sys.executable})")
    import importlib.metadata
    try:
        js_ver = importlib.metadata.version("jsonschema")
    except Exception:
        import jsonschema
        js_ver = getattr(jsonschema, "__version__", "unknown")
    print(f"Validator Engine: Draft202012Validator (jsonschema {js_ver})")
    
    # Load schema (local candidate or remote fallback)
    schema = None
    schema_candidates = [
        SCHEMA_PATH,
        os.path.join(os.getcwd(), "reproducibility_contract.v0.schema.json"),
        os.path.join(os.getcwd(), "schemas", "reproducibility_contract.v0.schema.json")
    ]
    for c in schema_candidates:
        if os.path.exists(c):
            print(f"Schema Source: local:{os.path.relpath(c, os.getcwd())}")
            with open(c, "r", encoding="utf-8") as f:
                schema = json.load(f)
            break

    if not schema:
        print(f"Schema Source: remote_mirror:{PUBLIC_MIRROR_BASE}/schemas/reproducibility_contract.v0.schema.json")
        schema = json.loads(fetch_remote_asset("schemas/reproducibility_contract.v0.schema.json").decode("utf-8"))
        
    # 1. Meta-schema conformance
    try:
        Draft202012Validator.check_schema(schema)
        print("Meta-Schema Conformance: [PASS] (Valid JSON Schema Draft 2020-12)")
    except Exception as e:
        print(f"Meta-Schema Conformance: [FAIL] {e}", file=sys.stderr)
        return 1

    validator = Draft202012Validator(schema)
    
    # Discover instances to validate
    instances_data = discover_contract_instances(args.contracts)
            
    all_passed = True
    print(f"\n[PART 1] Validating {len(instances_data)} contract instance(s):")
    for name, inst, c_dir in instances_data:
        try:
            report = validate_contract_instance(
                validator,
                inst,
                verify_live=args.verify_live,
                target_commit=args.target_commit,
                contract_dir=c_dir
            )
            live_tag = " | live_verified=True" if args.verify_live else ""
            print(f"  [PASS] {name} | system={report['system']} | status={report['status']} | reviews={report['review_count']} | log={report['log_name']}({report['verified_bytes']}B, sha={report['verified_sha'][:8]}... via {report['log_source']}){live_tag}")
        except Exception as e:
            print(f"  [FAIL] {name} : {e}", file=sys.stderr)
            all_passed = False

    # 2. Negative Controls Guardrail Suite
    if not args.skip_negative_controls:
        neg_ok = run_negative_controls_suite(validator)
        if not neg_ok:
            all_passed = False

    print("\n----------------------------------------------------")
    if all_passed:
        print("FINAL RESULT: ALL CONTRACT INSTANCES AND ADVERSARIAL GUARDRAILS PASSED (100%)")
        return 0
    else:
        print("FINAL RESULT: VALIDATION OR GUARDRAIL FAILURES DETECTED", file=sys.stderr)
        return 1

if __name__ == "__main__":
    sys.exit(main())
