"""Offline teaching exercise, not a network protocol or TPM implementation.

Requires Python 3.10+ and cryptography 50.0.2. Keys exist only in this process.
No network, key files, real devices or production credentials are used.
The single-threaded in-memory verifier omits production concurrency, secure
transport, enrollment, revocation, rate limiting and key lifecycle management.
"""
import json
import secrets
from dataclasses import dataclass

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey


@dataclass(frozen=True)
class Challenge:
    device: str
    service: str
    nonce: str
    expires: int

    def message(self):
        return json.dumps(self.__dict__, sort_keys=True,
                          separators=(",", ":")).encode("utf-8")


class OfflineVerifier:
    def __init__(self, trusted_keys):
        # This mapping represents a previously trusted registration.
        self.trusted_keys = dict(trusted_keys)
        self.pending = {}
        self.used = set()

    def issue(self, device, now):
        if device not in self.trusted_keys:
            raise ValueError("unknown device")
        while True:
            nonce = secrets.token_bytes(16).hex()
            if nonce not in self.pending and nonce not in self.used:
                break
        challenge = Challenge(device, "irrigation-reports-demo-v1", nonce, now + 10)
        self.pending[nonce] = challenge
        return challenge

    def verify(self, device, nonce, signature, now):
        if device not in self.trusted_keys:
            return "unknown identity"
        if nonce in self.used:
            return "replay"
        challenge = self.pending.get(nonce)
        if challenge is None or challenge.device != device:
            return "wrong challenge"
        if now >= challenge.expires:
            return "expired"
        try:
            self.trusted_keys[device].verify(signature, challenge.message())
        except InvalidSignature:
            return "invalid signature"
        # One process, one thread: not an atomic production datastore operation.
        del self.pending[nonce]
        self.used.add(nonce)
        return "accepted"


def main():
    private = Ed25519PrivateKey.generate()
    other = Ed25519PrivateKey.generate()
    verifier = OfflineVerifier({"irrigation-07": private.public_key()})
    name = "irrigation-07"
    a = verifier.issue(name, now=0)
    signed_a = private.sign(a.message())
    assert verifier.verify(name, a.nonce, signed_a, now=2) == "accepted"
    print("PASS valid response")

    b = verifier.issue(name, now=0)
    assert verifier.verify(name, b.nonce, other.sign(b.message()), now=2) == "invalid signature"
    print("PASS wrong key rejected")

    c = verifier.issue(name, now=0)
    changed = c.message().replace(b"irrigation-reports", b"irrigation-control")
    assert changed != c.message()
    assert verifier.verify(name, c.nonce, private.sign(changed), now=2) == "invalid signature"
    print("PASS changed content rejected")

    assert verifier.verify(name, a.nonce, signed_a, now=3) == "replay"
    print("PASS replay rejected")

    d = verifier.issue(name, now=0)
    assert verifier.verify(name, d.nonce, private.sign(d.message()), now=12) == "expired"
    # Exact expiry boundary is also excluded, not just times after expiry.
    assert verifier.verify(name, d.nonce, private.sign(d.message()), now=10) == "expired"
    print("PASS expired response rejected")

    assert verifier.verify("unknown", b.nonce, private.sign(b.message()), now=2) == "unknown identity"
    print("PASS unknown identity rejected")
    # Rejected signatures must not consume a valid open challenge.
    assert verifier.verify(name, b.nonce, private.sign(b.message()), now=2) == "accepted"
    assert len({a.nonce, b.nonce, c.nonce, d.nonce}) == 4


if __name__ == "__main__":
    main()
