"""
pqc_sign -- neutral, product-agnostic post-quantum alert signer.

ONE library. THREE separate instances (PHOENIX, INGV, OLDGABE), each with its OWN
keypair, its OWN feed, its OWN data plane. This module contains NO product data and
NO keys; a product instance supplies its name and its key paths. A PHOENIX signer
physically cannot sign as OLDGABE -- it only holds the PHOENIX secret key. There is
deliberately no function that signs for two products. Same boundary as the rest of
the stack: shared technique, never shared runtime or key material.

Primitive: ML-DSA-65 (NIST FIPS 204, Dilithium) via pqcrypto/liboqs. Post-quantum,
so an alert signed today cannot be forged by a future quantum adversary
("harvest now, verify later"). ML-KEM-768 (FIPS 203) is available for feed transport.

Secret keys live ONLY in the secrets store, never in source or on any public site
(per the 2026-06-30 secret-hygiene directive). Public keys are meant to be
distributed -- consumers verify with them.
"""
from __future__ import annotations
import os, json, base64, hashlib, time
from pqcrypto.sign import ml_dsa_65 as mldsa

ALG = "ML-DSA-65"


def qrng_bytes(n: int = 16) -> tuple[bytes, str]:
    """n random bytes + source. Tries env QRNG_URL (raw bytes), else OS CSPRNG.
    Key generation uses the vetted OS CSPRNG inside pqcrypto; QRNG is 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], "qrng:" + url.split("//")[-1].split("/")[0]
        except Exception:
            pass
    return os.urandom(n), "os_csprng"


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


def _canonical(obj: dict) -> bytes:
    return json.dumps(obj, sort_keys=True, separators=(",", ":")).encode("utf-8")


class AlertSigner:
    """One product's signer. Loads (or is told to create) exactly one keypair."""

    def __init__(self, product: str, secret_path: str, public_path: str):
        self.product = product
        self.secret_path = secret_path
        self.public_path = public_path
        self._sk = None
        self._pk = None
        if os.path.exists(public_path):
            self._pk = open(public_path, "rb").read()
        if os.path.exists(secret_path):
            self._sk = open(secret_path, "rb").read()

    # -- key lifecycle --------------------------------------------------
    @classmethod
    def create(cls, product: str, secret_path: str, public_path: str, overwrite=False):
        if os.path.exists(secret_path) and not overwrite:
            raise FileExistsError(f"{product} secret key already exists at {secret_path}; "
                                  "refusing to overwrite (use overwrite=True deliberately)")
        pk, sk = mldsa.generate_keypair()
        os.makedirs(os.path.dirname(secret_path) or ".", exist_ok=True)
        os.makedirs(os.path.dirname(public_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)
        try:
            os.chmod(secret_path, 0o600)
        except Exception:
            pass
        s = cls(product, secret_path, public_path)
        return s, s.key_info()

    def key_info(self) -> dict:
        return {"product": self.product, "alg": ALG,
                "public_fingerprint": _fingerprint(self._pk) if self._pk else None,
                "public_path": self.public_path, "secret_path": self.secret_path,
                "has_secret": self._sk is not None}

    def public_key_b64(self) -> str:
        return base64.b64encode(self._pk).decode()

    # -- sign / verify --------------------------------------------------
    def sign(self, event: dict) -> dict:
        if self._sk is None:
            raise RuntimeError(f"{self.product} secret key not loaded ({self.secret_path})")
        nonce, src = qrng_bytes(16)
        core = dict(event)
        core["_product"] = self.product                 # product tag is covered by the signature
        core["_nonce"] = base64.b64encode(nonce).decode()
        core["_signed_at"] = event.get("_signed_at") or int(time.time())
        msg = _canonical(core)
        sig = mldsa.sign(self._sk, msg)
        out = dict(core)
        out["_integrity"] = {
            "alg": ALG, "product": self.product,
            "sig": base64.b64encode(sig).decode(),
            "public_fingerprint": _fingerprint(self._pk),
            "hash": "sha256:" + hashlib.sha256(msg).hexdigest(),
            "nonce_source": src,
        }
        return out

    def verify(self, signed: dict) -> dict:
        integ = signed.get("_integrity")
        if not integ:
            return {"valid": False, "reason": "no _integrity block"}
        if integ.get("product") != self.product:
            return {"valid": False, "reason": f"product mismatch: signed for "
                    f"{integ.get('product')}, verifying as {self.product}"}
        if self._pk is None or _fingerprint(self._pk) != 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(self._pk, msg, base64.b64decode(integ["sig"]))
            return {"valid": True, "product": self.product, "alg": ALG,
                    "fingerprint": integ["public_fingerprint"]}
        except Exception as e:
            return {"valid": False, "reason": f"signature invalid: {e}"}
