#!/usr/bin/env python3
"""astranl-verify: independent verifier for AstraNL receipts and log events.

This file is deliberately self-contained and uses NO AstraNL code and NO AstraNL verdicts.
It fetches raw public records and recomputes everything itself:
  1. receipt payload hash  = sha256(json.dumps(payload, sort_keys=True, ensure_ascii=False, default=str))
  2. log envelope hash     = sha256(canonical_json(envelope))  canonical = sort_keys, separators (',',':'), ensure_ascii False
  3. envelope signature    = Ed25519 over canonical_json(envelope) with the published key (hex, 32 bytes)
  4. Merkle inclusion      = RFC 6962: leaf = sha256(0x00 || canonical_json({envelope_hash, signature})), node = sha256(0x01||L||R), audit path walk
  5. signed tree head      = Ed25519 over canonical_json({root_hash, timestamp, tree_size})
What this proves: the record existed in exactly this form when AstraNL signed it and the log
has not been rewritten. What it does NOT prove: that the claim inside is true for your purpose.
Apply your own acceptance rules to the payload it prints.

Requirements: Python 3.10+, the cryptography package. Usage:
  python3 astranl_verify.py ASTRA-A-XXXXXXXXXXXXXXXX
  python3 astranl_verify.py alk_XXXXXXXXXXXXXXXXXXXX
  python3 astranl_verify.py alk_XXXXXXXXXXXXXXXXXXXX --result result.json
The --result form is for a counterparty check from the MCP tool check_counterparty_nl: it verifies the log
event and then recomputes the sha256 of the result you hold, without the keys check_id, record and verify,
and compares it with payload.result_sha256 of that event.
Exit 0 only when every check passes.
"""
import hashlib, json, sys, urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature

BASE = 'https://astranl.com'
ENVELOPE_KEYS = ['lock_version', 'event_id', 'coordination_id', 'session_id', 'sequence', 'timestamp', 'actor_id', 'principal_id',
                 'event_type', 'payload_hash', 'previous_event_hash', 'policy_hash', 'mandate_hash', 'contract_hash']


def get(path):
    try:
        with urllib.request.urlopen(BASE + path, timeout=30) as r:
            return json.loads(r.read().decode('utf-8'))
    except urllib.error.HTTPError as e:
        return {'found': False, 'http': e.code}


def canonical(obj):
    return json.dumps(obj, sort_keys=True, separators=(',', ':'), ensure_ascii=False, allow_nan=False).encode('utf-8')


def sha(b):
    return hashlib.sha256(b).hexdigest()


def pubkey():
    k = get('/api/astralock/pubkey')
    assert k.get('algorithm') == 'ed25519'
    return Ed25519PublicKey.from_public_bytes(bytes.fromhex(k['public_key_hex'])), k.get('key_id')


def ed_ok(pub, sig_hex, msg):
    try:
        pub.verify(bytes.fromhex(sig_hex), msg); return True
    except (InvalidSignature, ValueError):
        return False


def inclusion(leaf_hash_hex, index, size, path_hex):
    h = bytes.fromhex(leaf_hash_hex); fn, sn = index, size - 1
    for p in path_hex:
        p = bytes.fromhex(p)
        if sn == 0:
            return None
        if fn % 2 == 1 or fn == sn:
            h = hashlib.sha256(b'\x01' + p + h).digest()
            while not (fn % 2 == 1 or fn == 0):
                fn >>= 1; sn >>= 1
        else:
            h = hashlib.sha256(b'\x01' + h + p).digest()
        fn >>= 1; sn >>= 1
    return h.hex() if sn == 0 else None


def verify_event(event_id, pub):
    ev = get('/api/astralock/event/%s' % event_id)
    if ev.get('error') or 'event_id' not in ev:
        return {'found': False}
    out = {'found': True, 'event_id': event_id}
    payload = ev.get('payload')
    if isinstance(payload, str):
        try:
            payload = json.loads(payload)
        except Exception:
            pass
    out['payload_hash_ok'] = (sha(canonical(payload)) == ev['payload_hash']) if payload is not None else None
    env = {k: ev.get(k) for k in ENVELOPE_KEYS if k in ev}
    ebytes = canonical(env)
    out['envelope_hash_ok'] = sha(ebytes) == ev['envelope_hash']
    out['signature_ok'] = ed_ok(pub, ev['signature'], ebytes)
    leaf = hashlib.sha256(b'\x00' + canonical({'envelope_hash': ev['envelope_hash'], 'signature': ev['signature']})).hexdigest()
    out['leaf_hash_ok'] = leaf == ev.get('leaf_hash')
    proof = get('/api/astralock/proof?leaf_index=%d' % int(ev['leaf_index']))
    sth = get('/api/astralock/sth')
    root = inclusion(leaf, int(ev['leaf_index']), int(proof['tree_size']), proof['audit_path']) if 'audit_path' in proof else None
    out['merkle_root_from_path'] = root
    out['inclusion_ok'] = bool(root) and root == proof.get('root_hash') == sth.get('root_hash') and int(proof['tree_size']) == int(sth['tree_size'])
    out['pending_inclusion'] = (not out['inclusion_ok']) and int(ev['leaf_index']) >= int(sth['tree_size'])
    out['sth_signature_ok'] = ed_ok(pub, sth['signature'], canonical({'root_hash': sth['root_hash'], 'timestamp': sth['timestamp'], 'tree_size': sth['tree_size']}))
    out['integrity'] = 'PASS' if all([out['envelope_hash_ok'], out['signature_ok'], out['leaf_hash_ok'], out['inclusion_ok'], out['sth_signature_ok']]) else ('SIGNED_PENDING_INCLUSION' if out['envelope_hash_ok'] and out['signature_ok'] and out['pending_inclusion'] else 'FAIL')
    return out


def verify_receipt(rid, pub):
    r = get('/api/reality/receipt/%s' % rid)
    if not r.get('found'):
        return {'found': False}
    rec = r['receipt']; payload = rec.get('payload')
    if isinstance(payload, str):
        payload = json.loads(payload)
    out = {'found': True, 'receipt_id': rid, 'kind': 'action_receipt' if rid.startswith('ASTRA-A-') else 'policy_receipt'}
    out['payload_hash_ok'] = sha(json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str).encode()) == rec['payload_hash']
    out['log_event'] = verify_event(rec['lock_event_id'], pub) if rec.get('lock_event_id') else {'found': False}
    lp = out['log_event'].get('payload_hash_ok')
    out['receipt_bound_to_event'] = bool(out['log_event'].get('found')) and lp is not False
    out['status_fields'] = {'revoked': bool(int(rec.get('revoked') or 0)), 'expires_at': rec.get('expires_at'), 'valid_at': rec.get('valid_at'), 'result': rec.get('result'), 'decision': rec.get('decision')}
    out['integrity'] = 'PASS' if out['payload_hash_ok'] and out['log_event'].get('integrity') in ('PASS', 'SIGNED_PENDING_INCLUSION') else 'FAIL'
    out['claim_for_your_own_rules'] = payload
    out['what_this_proves'] = 'the receipt bytes and their log commitment are intact and signed by the published key; nothing about the truth of the claim for your purpose'
    return out


def verify_result(res, result_path):
    held = json.load(open(result_path, encoding='utf-8'))
    body = {k: v for k, v in held.items() if k not in ('check_id', 'record', 'verify')}
    got = sha(canonical(body))
    ev = get('/api/astralock/event/%s' % res.get('event_id', ''))
    payload = ev.get('payload')
    if isinstance(payload, str):
        payload = json.loads(payload)
    want = (payload or {}).get('result_sha256')
    return {'result_sha256_recomputed': got, 'result_sha256_logged': want, 'result_matches_log': bool(want) and got == want,
            'what_this_proves': 'the result you hold is byte for byte the one AstraNL recorded at that time; it says nothing about the counterparty beyond what the named sources returned'}


def main():
    result_path = None
    if len(sys.argv) == 4 and sys.argv[2] == '--result':
        result_path = sys.argv[3]
    elif len(sys.argv) != 2:
        print(__doc__); return 2
    ref = sys.argv[1].strip()
    pub, key_id = pubkey()
    res = verify_receipt(ref, pub) if ref.startswith(('ASTRA-A-', 'ASTRA-R-')) else verify_event(ref, pub) if ref.startswith('alk_') else {'found': False, 'error': 'unknown reference family'}
    res['key_id'] = key_id
    if result_path and res.get('found'):
        res['result_check'] = verify_result(res, result_path)
    print(json.dumps(res, ensure_ascii=False, indent=1, default=str))
    ok = res.get('integrity') in ('PASS', 'SIGNED_PENDING_INCLUSION')
    if result_path:
        ok = ok and bool((res.get('result_check') or {}).get('result_matches_log'))
    return 0 if ok else 1


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