"""seals.py v2 — three-tier receipt sealing: tripwire / chain / notary.

    TIER 1  crc32          0.3us — bit-flip tripwire on every read (1994 ZIP law)
    TIER 2  blake2b keyed  2.5us — private chain MAC; 9x faster than HMAC-SHA256,
                             and stealing the notary key does NOT let an attacker
                             re-chain (separation of powers)
    TIER 3  ed25519        108us — NON-REPUDIATION. Only asymmetric crypto answers
                             "WHO sealed this?" The notary cannot later deny its
                             own receipts; anyone can verify with the public key.

Key separation law: chain key (symmetric, re-chaining power) and notary key
(asymmetric, signing power) are DIFFERENT keys on purpose. A receipt forgery
now requires stealing both.

Records carry 'seal_v': 2. Legacy sha256-only records (seal_v absent) still
verify — history is never rewritten.
"""
import hashlib, json, os, zlib
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

ROOT = os.path.dirname(os.path.abspath(__file__))
KEYDIR = os.path.join(ROOT, 'keys')
META = ('crc32', 'prev', 'chain_seal', 'notary_sig', 'seal', 'seal_v')

_keys = None


def _load_keys():
    global _keys
    if _keys:
        return _keys
    os.makedirs(KEYDIR, exist_ok=True)
    sk_p = os.path.join(KEYDIR, 'notary_sk.bin')
    pk_p = os.path.join(KEYDIR, 'notary_pk.bin')
    ck_p = os.path.join(KEYDIR, 'chain.key')
    if os.path.exists(sk_p):
        seed = open(sk_p, 'rb').read()
        sk = Ed25519PrivateKey.from_private_bytes(seed)
    else:
        sk = Ed25519PrivateKey.generate()
        open(sk_p, 'wb').write(sk.private_bytes_raw())
        os.chmod(sk_p, 0o600)
    pk = sk.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
    if not os.path.exists(pk_p):
        open(pk_p, 'wb').write(pk)
        os.chmod(pk_p, 0o644)  # public: ship it with the product
    chain_key = open(ck_p, 'rb').read() if os.path.exists(ck_p) else None
    if not chain_key:
        chain_key = os.urandom(32)
        open(ck_p, 'wb').write(chain_key)
        os.chmod(ck_p, 0o600)
    _keys = {'sk': sk, 'pk': pk, 'chain': chain_key}
    return _keys


def public_key_hex():
    return _load_keys()['pk'].hex()


def crc32_of(b: bytes) -> int:
    return zlib.crc32(b) & 0xFFFFFFFF


def _body_bytes(rec):
    return json.dumps({k: v for k, v in rec.items() if k not in META},
                      sort_keys=True).encode()


def seal_record(prev: str, rec: dict) -> dict:
    """Seal one record onto the chain. Adds: crc32, prev, chain_seal, notary_sig."""
    k = _load_keys()
    out = dict(rec)
    body = _body_bytes(out)
    out['crc32'] = f'{crc32_of(prev.encode() + body):08x}'
    base = prev.encode() + out['crc32'].encode() + body
    out['chain_seal'] = hashlib.blake2b(base, digest_size=32, key=k['chain']).hexdigest()
    out['notary_sig'] = k['sk'].sign(base).hex()
    out['prev'] = prev
    out['seal_v'] = 2
    return out


def verify_record(prev: str, rec: dict, chain_key=None, pubkey=None) -> str:
    """First layer that FAILS, or 'OK'. Handles legacy sha256 records too."""
    body = _body_bytes(rec)
    if 'chain_seal' not in rec:  # legacy v1: sha256 chain
        import hashlib as hl
        h = hl.sha256(rec['prev'].encode() + json.dumps(
            {k: v for k, v in rec.items() if k not in ('prev', 'seal')},
            sort_keys=True).encode()).hexdigest()
        return 'OK' if (h == rec['seal'] and rec['prev'] == prev) else 'FORGED (legacy sha256 mismatch)'
    k = _load_keys()
    ck = chain_key or k['chain']
    pk = pubkey or k['pk']
    if f'{crc32_of(prev.encode() + body):08x}' != rec['crc32']:
        return 'CORRUPT (crc32 tripwire: bit-flip)'
    base = prev.encode() + rec['crc32'].encode() + body
    if hashlib.blake2b(base, digest_size=32, key=ck).hexdigest() != rec['chain_seal']:
        return 'FORGED (chain_seal: re-chaining without chain key)'
    try:
        Ed25519PrivateKey.from_private_bytes(b'\0' * 32)  # no-op guard
        from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
        Ed25519PublicKey.from_public_bytes(pk).verify(bytes.fromhex(rec['notary_sig']), base)
    except Exception:
        return 'FORGED (notary_sig: wrong signer / denied non-repudiation)'
    if rec['prev'] != prev:
        return 'BROKEN-CHAIN (link to predecessor wrong)'
    return 'OK'


def verify_chain_file(path, known_scars=None) -> bool:
    """True only if every NON-SCAR record verifies. Tolerates blank lines and
    mixed v1/v2 records (never rewrite history).

    known_scars: set of 0-based record indices quarantined by a sealed
    chain_incident receipt. Scars are masked (reported, not counted as
    breaks) so a NEW break can never hide among old wounds — the tripwire
    must stay loud about fresh corruption, quiet about documented history."""
    prev, n, bad = 'GENESIS', 0, 0
    scars = known_scars or set()
    n_scar = 0
    for line in open(path):
        if not line.strip():
            continue  # tolerate blank lines from append-mode writers
        rec = json.loads(line)
        if verify_record(prev, rec) != 'OK':
            if n in scars:
                n_scar += 1
            else:
                bad += 1
                print(f'  chain broken at record {n}: {verify_record(prev, rec)}')
        prev, n = rec.get('seal') or rec.get('chain_seal'), n + 1
    tag = os.path.basename(path)
    if scars:
        print(f'{tag}: {n} records, {n_scar} known scars (quarantined), '
              f'{bad} NEW breaks -> {"VERIFIED" if not bad else "BROKEN"}')
    else:
        print(f'{tag}: {n} records, {bad} broken -> {"VERIFIED" if not bad else "BROKEN"}')
    return not bad


def demo():
    """Three adversaries, one honest flow, each tier must catch its own."""
    print('=== seals v2 demo ===')
    prev = 'GENESIS'
    honest = seal_record(prev, {'receipt': 'attack-run-42', 'customer': 'acme', 'usd': 990})
    print(f'1. honest record                  -> {verify_record(prev, honest)}')

    bit = dict(honest); bit['usd'] = 990
    bit['customer'] = 'acme-corp'  # bit-flip in storage
    print(f'2. bit-flip                       -> {verify_record(prev, bit)}')

    forged = dict(honest)
    body = _body_bytes({**forged, 'usd': 9})
    forged['usd'] = 9
    forged['crc32'] = f'{crc32_of(prev.encode() + body):08x}'  # attacker fixes tripwire
    # attacker even has the NOTARY key — but not the chain key:
    k = _load_keys()
    forged['notary_sig'] = k['sk'].sign(prev.encode() + forged['crc32'].encode() + body).hex()
    print(f'3. re-signed w/ stolen notary key -> {verify_record(prev, forged)}')

    rechain = seal_record(prev, {'receipt': 'attack-run-42', 'customer': 'acme', 'usd': 9})
    rechain['notary_sig'] = k['sk'].sign(  # resigned but WRONG chain position
        b'OTHER'.hex().encode() + rechain['crc32'].encode() + _body_bytes(rechain)).hex()
    print(f'4. wrong chain position           -> {verify_record(prev, rechain)}')

    import time
    rec = _body_bytes(honest)
    N = 5000
    t0 = time.perf_counter()
    for _ in range(N): zlib.crc32(rec)
    t1 = time.perf_counter()
    for _ in range(N): hashlib.blake2b(rec, digest_size=32, key=k['chain']).hexdigest()
    t2 = time.perf_counter()
    for _ in range(N): k['sk'].sign(rec)
    t3 = time.perf_counter()
    print(f'cost @5k: crc32 {(t1-t0)/N*1e6:.2f}us  chain {(t2-t1)/N*1e6:.2f}us  notary {(t3-t2)/N*1e6:.2f}us')
    print(f'public key (ships with product): {public_key_hex()[:32]}...')
    return True


if __name__ == '__main__':
    demo()


# ---------------------------------------------------------------- atomic append
_LOCK_SUFFIX = '.lock'


def _locked(path, timeout=20.0):
    """An exclusive lock over read-tip -> seal -> append.

    Without this, two processes read the same chain tip and both seal onto it; the
    second record is then mis-sequenced and every verifier calls it corrupt. Six
    such records existed in auth_audit.jsonl and the verdict string ("bit-flip")
    sent the investigation looking for disk rot instead of a race.
    """
    import time as _t
    lock = path + _LOCK_SUFFIX
    deadline = _t.time() + timeout
    while True:
        try:
            fd = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
            os.write(fd, str(os.getpid()).encode())
            os.close(fd)
            return lock
        except (FileExistsError, PermissionError):
            # Windows raises PermissionError (EACCES), not FileExistsError, when
            # O_CREAT|O_EXCL hits an existing file. Catching only FileExistsError
            # let a sixth writer crash instead of waiting its turn.
            # a stale lock from a crashed writer must not wedge the chain forever
            try:
                if _t.time() - os.path.getmtime(lock) > 60:
                    os.unlink(lock)
                    continue
            except OSError:
                pass
            if _t.time() > deadline:
                raise
            _t.sleep(0.01)


def _unlocked(lock):
    try:
        os.unlink(lock)
    except OSError:
        pass


def chain_tip(path):
    """The last seal on a chain, read under the same law as the writer.

    Reads only the TAIL. The first version parsed every line of the file, which made
    each locked append O(rows) — with 10k+ rows on auth_audit.jsonl that turned every
    keyed request into a multi-second stall behind the lock. Correctness is unchanged:
    the tip is the last parseable record.
    """
    if not os.path.exists(path):
        return 'GENESIS'
    try:
        size = os.path.getsize(path)
        with open(path, 'rb') as f:
            f.seek(max(0, size - 131072))
            tail = f.read().decode('utf-8', 'replace')
    except OSError:
        return 'GENESIS'
    for line in reversed(tail.splitlines()):
        if not line.strip():
            continue
        try:
            r = json.loads(line)
        except ValueError:
            continue
        return r.get('seal') or r.get('chain_seal') or 'GENESIS'
    return 'GENESIS'


def append_sealed(path, rec):
    """The ONLY safe way to add a record: read tip, seal, append, all locked.

    Writers that do these three steps separately WILL race, and the loser's record
    reads as corrupt forever.
    """
    lock = _locked(path)
    try:
        row = seal_record(chain_tip(path), rec)
        with open(path, 'a', encoding='utf-8') as f:
            f.write(json.dumps(row, sort_keys=True) + '\n')
            f.flush()
            os.fsync(f.fileno())
        return row
    finally:
        _unlocked(lock)
