"""
PHOENIX alert integrity -- post-quantum signed event JSON (+ QRNG entropy).

The one quantum-adjacent item with real deployable value regardless of any
detection advantage: sign every PHOENIX alert/event so a downstream consumer
(citizen app, Gaetano's ground nodes, the public API) can verify it was issued by
PHOENIX and not tampered with -- and do it with a post-quantum signature so the
guarantee survives a future quantum adversary ("harvest now, verify later").

Primitives (NIST standards, via pqcrypto / liboqs):
  - ML-DSA-65  (FIPS 204, Dilithium)  -- event signatures
  - ML-KEM-768 (FIPS 203, Kyber)      -- available for key exchange (feed transport)
  - QRNG entropy for the per-event nonce (optional external source; OS CSPRNG fallback)

Boundary: this is the PHOENIX instance. OLDGABE/INGV run their own key material and
their own signer from the same code -- separate keys, separate feeds, never shared.

Keys live in the secrets store, never in source or on the public site (per the
2026-06-30 secret-hygiene directive). This module reads/writes key paths; it never
embeds a key.
"""
from __future__ import annotations
import os, json, base64, hashlib, time
from pqcrypto.sign import ml_dsa_65 as mldsa

ALG = "ML-DSA-65"


# --------------------------------------------------------------------------
# QRNG entropy (optional external source, OS CSPRNG fallback)
# --------------------------------------------------------------------------
def qrng_bytes(n: int = 32) -> tuple[bytes, str]:
    """Return n random bytes and the source used. Tries a configured QRNG endpoint
    (env QRNG_URL, expected to return raw bytes), else the OS CSPRNG. Key generation
    itself uses the vetted OS CSPRNG inside pqcrypto; QRNG is used for event nonces."""
    url = os.environ.get("QRNG_URL")
    if url:
        try:
            import urllib.request
            with urllib.request.urlopen(url, timeout=3) as r:
                b = r.read(n)
                if len(b) >= n:
                    return b[:n], f"qrng:{url.split('//')[-1].split('/')[0]}"
        except Exception:
            pass
    return os.urandom(n), "os_csprng"


# --------------------------------------------------------------------------
# Keys
# --------------------------------------------------------------------------
def generate_keypair(secret_path: str, public_path: str) -> dict:
    pk, sk = mldsa.generate_keypair()
    os.makedirs(os.path.dirname(secret_path) or ".", exist_ok=True)
    with open(secret_path, "wb") as f:
        f.write(sk)
    with open(public_path, "wb") as f:
        f.write(pk)
    return {"alg": ALG, "public_fingerprint": _fingerprint(pk),
            "public_path": public_path, "secret_path": secret_path,
            "sig_bytes": len(mldsa.sign(sk, b"x")), "pk_bytes": len(pk), "sk_bytes": len(sk)}


def _fingerprint(pk: bytes) -> str:
    return "sha256:" + hashlib.sha256(pk).hexdigest()[:16]


def _canonical(event: dict) -> bytes:
    """Deterministic serialization so signer and verifier hash identical bytes."""
    return json.dumps(event, sort_keys=True, separators=(",", ":")).encode("utf-8")


# --------------------------------------------------------------------------
# Sign / verify
# --------------------------------------------------------------------------
def sign_event(event: dict, secret_key: bytes, public_key: bytes) -> dict:
    """Return event augmented with an _integrity block. The signature covers the
    canonical bytes of the event WITH the nonce/timestamp but WITHOUT _integrity."""
    nonce, src = qrng_bytes(16)
    core = dict(event)
    core["_nonce"] = base64.b64encode(nonce).decode()
    core["_signed_at"] = event.get("_signed_at") or int(time.time())
    msg = _canonical(core)
    sig = mldsa.sign(secret_key, msg)
    out = dict(core)
    out["_integrity"] = {
        "alg": ALG,
        "sig": base64.b64encode(sig).decode(),
        "public_fingerprint": _fingerprint(public_key),
        "hash": "sha256:" + hashlib.sha256(msg).hexdigest(),
        "nonce_source": src,
    }
    return out


def verify_event(signed: dict, public_key: bytes) -> dict:
    integ = signed.get("_integrity")
    if not integ:
        return {"valid": False, "reason": "no _integrity block"}
    if _fingerprint(public_key) != integ.get("public_fingerprint"):
        return {"valid": False, "reason": "public-key fingerprint mismatch"}
    core = {k: v for k, v in signed.items() if k != "_integrity"}
    msg = _canonical(core)
    if "sha256:" + hashlib.sha256(msg).hexdigest() != integ.get("hash"):
        return {"valid": False, "reason": "payload hash mismatch (tampered)"}
    try:
        mldsa.verify(public_key, msg, base64.b64decode(integ["sig"]))
        return {"valid": True, "alg": integ["alg"], "fingerprint": integ["public_fingerprint"]}
    except Exception as e:
        return {"valid": False, "reason": f"signature invalid: {e}"}


# --------------------------------------------------------------------------
# Demo / self-test
# --------------------------------------------------------------------------
if __name__ == "__main__":
    import tempfile
    d = tempfile.mkdtemp()
    sk_p, pk_p = os.path.join(d, "phoenix_sign.sk"), os.path.join(d, "phoenix_sign.pk")
    meta = generate_keypair(sk_p, pk_p)
    print("keypair:", json.dumps({k: meta[k] for k in ("alg", "public_fingerprint",
          "sig_bytes", "pk_bytes", "sk_bytes")}))
    sk = open(sk_p, "rb").read(); pk = open(pk_p, "rb").read()

    alert = {"event": "wildfire_candidate", "lat": 37.573, "lon": 13.453,
             "confidence": 0.78, "source": "FCI+VIIRS+camera",
             "aoi": "Alessandria della Rocca"}
    signed = sign_event(alert, sk, pk)
    print("signed alert _integrity:", json.dumps(signed["_integrity"], indent=2)[:400])
    print("verify (untampered):", verify_event(signed, pk))

    tampered = dict(signed); tampered["confidence"] = 0.99
    print("verify (tampered confidence 0.78->0.99):", verify_event(tampered, pk))

    wrong_pk, _ = mldsa.generate_keypair()
    print("verify (wrong key):", verify_event(signed, wrong_pk))
