#!/usr/bin/env python3
"""Independent R10 receipt/decision verifier. Python standard library only.

Checks internal integrity, typed decision constraints and an optional separately
trusted bundle digest. --source-root additionally reads the local package files.
Neither a self-consistent bundle nor a same-site digest proves host provenance,
independent authorship, native build reproducibility or platform certification.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
from pathlib import Path, PurePosixPath

HEX = re.compile(r'^[a-f0-9]{64}$')
MAX_BYTES = 1_048_576

def canonical(value):
    return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(',', ':'), allow_nan=False)

def digest(value):
    return hashlib.sha256(canonical(value).encode()).hexdigest()

def pairs(items):
    result = {}
    for key, value in items:
        if key in result:
            raise ValueError('DUPLICATE_JSON_KEY')
        result[key] = value
    return result

def loads(raw):
    if len(raw) > MAX_BYTES:
        raise ValueError('BUNDLE_TOO_LARGE')
    return json.loads(raw, object_pairs_hook=pairs, parse_constant=lambda x: (_ for _ in ()).throw(ValueError('NONFINITE_JSON')))

def verify(bundle, expected_root=None):
    errors = []
    def require(ok, code):
        if not ok:
            errors.append(code)
    try:
        require(isinstance(bundle, dict), 'BUNDLE_OBJECT_REQUIRED')
        if errors:
            return {'ok': False, 'errors': errors, 'claim_passed': False}
        fields = {'schema','run_id','scenario','kind','parent_id','config_version','profile','started_at_ms',
                  'finished_at_ms','elapsed_ms','status','reason','source','executor','operation','checks',
                  'events','trust_scope','build_attestation','production_certification','bundle_sha256'}
        require(set(bundle) == fields, 'BUNDLE_FIELDS_INVALID')
        require(bundle.get('schema') == 'xps.live-lab.bundle.v1', 'SCHEMA_INVALID')
        require(isinstance(bundle.get('run_id'), str) and bool(re.fullmatch(r'xlr_[a-f0-9]{32}', bundle['run_id'])), 'RUN_ID_INVALID')
        require(bundle.get('scenario') in ('source_integrity','native_roundtrip','native_attack'), 'SCENARIO_INVALID')
        require(bundle.get('kind') in ('baseline','challenge','replay'), 'RUN_KIND_INVALID')
        require(bundle.get('status') in ('PASSED','BLOCKED'), 'STATUS_INVALID')
        require(bundle.get('production_certification') is False and bundle.get('build_attestation') == 'NOT_ESTABLISHED', 'AUTHORITY_ESCALATION_REJECTED')
        require(bundle.get('trust_scope') == 'SERVER_OBSERVATION_WITH_HASH_LINKED_RECEIPTS_NOT_INDEPENDENT_ATTESTATION', 'TRUST_SCOPE_INVALID')
        supplied = bundle.get('bundle_sha256')
        require(isinstance(supplied, str) and bool(HEX.fullmatch(supplied)), 'ROOT_INVALID')
        computed = digest({k:v for k,v in bundle.items() if k != 'bundle_sha256'})
        require(supplied == computed, 'BUNDLE_DIGEST_MISMATCH')
        if expected_root is not None:
            require(isinstance(expected_root, str) and bool(HEX.fullmatch(expected_root)) and computed == expected_root, 'TRUSTED_DIGEST_MISMATCH')
        for key in ('started_at_ms','finished_at_ms','elapsed_ms','config_version'):
            require(type(bundle.get(key)) is int and 0 <= bundle[key] < 2**53, 'INTEGER_INVALID:' + key)
        require(bundle['started_at_ms'] <= bundle['finished_at_ms'], 'TIME_ORDER_INVALID')
        checks = bundle.get('checks')
        require(isinstance(checks, list) and 3 <= len(checks) <= 30, 'CHECK_SET_INVALID')
        names = set()
        for check in checks:
            require(isinstance(check, dict) and set(check) == {'id','ok'} and type(check.get('ok')) is bool, 'CHECK_INVALID')
            require(check['id'] not in names, 'DUPLICATE_CHECK'); names.add(check['id'])
        require({'RELEASE_FINGERPRINT','SOURCE_MANIFEST_READBACK','COMPANION_SHA256SUMS'} <= names, 'SOURCE_CHECKS_MISSING')
        passed = all(c['ok'] for c in checks)
        require((bundle['status'] == 'PASSED') == passed, 'CLAIM_CHECK_MISMATCH')
        require((bundle.get('reason') is None) == passed, 'CLAIM_REASON_MISMATCH')
        require(isinstance(bundle['profile'], dict) and isinstance(bundle['executor'], dict) and isinstance(bundle['operation'], dict), 'EXECUTION_OBJECTS_REQUIRED')
        profile=bundle['profile']
        require(set(profile)=={'public_enabled','guest_enabled','publication_ttl_seconds','fixture','rounds','native_unit','source_integrity','native_roundtrip','native_attack'}, 'PROFILE_FIELDS_INVALID')
        for key in ('public_enabled','guest_enabled','source_integrity','native_roundtrip','native_attack'):
            require(type(profile.get(key)) is bool, 'PROFILE_BOOLEAN_INVALID')
        require(profile.get(bundle['scenario']) is True, 'SCENARIO_NOT_ENABLED')
        require(type(profile.get('rounds')) is int and 1<=profile['rounds']<=64, 'ROUNDS_INVALID')
        require(type(profile.get('publication_ttl_seconds')) is int and 60<=profile['publication_ttl_seconds']<=86400, 'TTL_INVALID')
        require(profile.get('fixture') in ('telemetry','policy','batch') and profile.get('native_unit') in ('veloseal64','integrity','pqc','simd','hw'), 'PROFILE_SELECTION_INVALID')
        source = bundle['source']
        if passed:
            for key in ('expected_tree_sha256','observed_tree_sha256','manifest_sha256'):
                require(isinstance(source.get(key), str) and bool(HEX.fullmatch(source[key])), 'SOURCE_DIGEST_INVALID')
            require(source['expected_tree_sha256'] == source['observed_tree_sha256'], 'SOURCE_IDENTITY_MISMATCH')
            require(type(source.get('checked_files')) is int and source['checked_files'] > 0, 'SOURCE_COUNT_INVALID')
            require(source.get('mismatch_count') == 0, 'SOURCE_MISMATCH_COUNT')
        previous = '0' * 64; previous_time = bundle['started_at_ms']
        evs = bundle.get('events')
        require(isinstance(evs, list) and 3 <= len(evs) <= 50, 'EVENT_SET_INVALID')
        for index, event in enumerate(evs, 1):
            require(set(event) == {'run_id','sequence','at_ms','event','details','parent_sha256','event_sha256'}, 'EVENT_FIELDS_INVALID')
            require(event['run_id'] == bundle['run_id'] and event['sequence'] == index, 'EVENT_IDENTITY_INVALID')
            require(event['parent_sha256'] == previous, 'EVENT_PARENT_MISMATCH')
            require(event['event_sha256'] == digest({k:v for k,v in event.items() if k != 'event_sha256'}), 'EVENT_DIGEST_MISMATCH')
            require(type(event['at_ms']) is int and previous_time <= event['at_ms'] <= bundle['finished_at_ms'], 'EVENT_TIME_INVALID')
            previous = event['event_sha256']; previous_time = event['at_ms']
        require(evs[-1]['event'] == 'VERIFICATION_DECIDED' and evs[-1]['details']['status'] == bundle['status'], 'FINAL_RECEIPT_MISMATCH')
        op = bundle['operation']
        if passed and bundle['scenario']=='source_integrity' and bundle['kind']!='challenge':
            require(op.get('executor')=='XPScerpto._source_manifest_readback' and op.get('checked_files')==source['checked_files'], 'SOURCE_OPERATION_BINDING_INVALID')
        if passed and bundle['kind'] == 'challenge':
            require(bundle['scenario'] == 'source_integrity', 'CHALLENGE_SCOPE_INVALID')
            require(op.get('executor') == 'SERVER_ISOLATED_FILE_CHALLENGE', 'CHALLENGE_EXECUTOR_INVALID')
            require(op.get('production_write') is False and op.get('original_unchanged') is True, 'CHALLENGE_BOUNDARY_INVALID')
            require(op.get('modified_bytes') == 1 and op.get('candidate_decision') == 'REJECTED', 'CHALLENGE_DECISION_INVALID')
            require(op.get('expected_sha256') == op.get('baseline_sha256') and op.get('expected_sha256') != op.get('modified_sha256'), 'CHALLENGE_DIGEST_CONSTRAINT_INVALID')
            require({'BASELINE_FILE_MATCH','MODIFIED_FILE_REJECTED','ORIGINAL_FILE_UNCHANGED'} <= names, 'CHALLENGE_CHECKS_MISSING')
        if passed and bundle['scenario'].startswith('native_'):
            require(op.get('executor') == 'EXISTING_NATIVE_PORTAL_ADAPTER', 'NATIVE_EXECUTOR_INVALID')
            require({'ADAPTER_ENTRYPOINT_UNCHANGED','EXPLICIT_NATIVE_RESULT'} <= names, 'NATIVE_CHECKS_MISSING')
            if bundle['scenario'] == 'native_roundtrip':
                require(op.get('roundtrip_match') is True and 'EXPLICIT_ROUNDTRIP_MATCH' in names, 'ROUNDTRIP_CONSTRAINT_INVALID')
            else:
                require(type(op.get('total_cases')) is int and op['total_cases'] > 0 and op.get('total_rejected') == op['total_cases'] and 'EXPLICIT_ALL_CASES_REJECTED' in names, 'ATTACK_CONSTRAINT_INVALID')
        if bundle['kind'] != 'baseline':
            require(isinstance(bundle.get('parent_id'), str) and bool(re.fullmatch(r'xlr_[a-f0-9]{32}', bundle['parent_id'])), 'PARENT_INVALID')
        if passed and bundle['kind'] == 'replay':
            require(op.get('replay',{}).get('source_match') is True and 'REPLAY_SOURCE_IDENTITY_MATCH' in names, 'REPLAY_CONSTRAINT_INVALID')
    except (TypeError, KeyError, IndexError, ValueError, AttributeError, OverflowError, RecursionError) as exc:
        errors.append('MALFORMED_BUNDLE:' + type(exc).__name__)
    return {'ok': not errors, 'errors': errors, 'claim_passed': not errors and bundle.get('status') == 'PASSED',
            'scope': 'BUNDLE_INTEGRITY_AND_DECISION_CONSTRAINTS_NOT_HOST_ATTESTATION',
            'trusted_digest_supplied': expected_root is not None}


def verify_source(bundle, root):
    root = Path(root).resolve(strict=True)
    raw = (root/'SOURCE_FILE_MANIFEST.json').read_bytes()
    if hashlib.sha256(raw).hexdigest() != bundle['source']['manifest_sha256']:
        raise ValueError('LOCAL_MANIFEST_DIGEST_MISMATCH')
    manifest = loads(raw); paths = set(); count = 0
    for row in manifest['files']:
        rel = row['path']; p = PurePosixPath(rel)
        if p.is_absolute() or '..' in p.parts or p.as_posix() != rel or rel in paths:
            raise ValueError('UNSAFE_OR_DUPLICATE_MANIFEST_PATH')
        paths.add(rel); target = root
        for part in p.parts:
            target = target/part
            if target.is_symlink():
                raise ValueError('SYMLINK_REJECTED')
        target.resolve(strict=True).relative_to(root)
        raw = target.read_bytes()
        if len(raw) != row['bytes'] or hashlib.sha256(raw).hexdigest() != row['sha256']:
            raise ValueError('LOCAL_FILE_IDENTITY_MISMATCH')
        count += 1
    if count != bundle['source']['checked_files']:
        raise ValueError('LOCAL_FILE_COUNT_MISMATCH')
    return {'ok': True, 'checked_files': count, 'scope': 'FILE_BYTES_AND_SIZES_NOT_MODE_OR_BUILD_ATTESTATION'}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('bundle', type=Path); parser.add_argument('--expected-root'); parser.add_argument('--source-root', type=Path)
    args = parser.parse_args()
    try:
        if args.bundle.stat().st_size > MAX_BYTES:
            raise ValueError('BUNDLE_TOO_LARGE')
        bundle = loads(args.bundle.read_bytes()); result = verify(bundle, args.expected_root)
        if args.source_root and result['ok']:
            result['local_source'] = verify_source(bundle, args.source_root)
    except (ValueError, OSError, KeyError, TypeError, RecursionError) as exc:
        result = {'ok': False, 'errors': [str(exc)], 'claim_passed': False}
    print(json.dumps(result, indent=2)); return 0 if result['ok'] else 1

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