//! Attested content-key release — `wai.video.keys`.
//!
//! A DRM system wraps and delivers the content key. This profile adds a portable,
//! checkable record of *why* a key was released — under which entitlement, before
//! which expiry: every release, and every refusal, is a signed receipt whose
//! decision a verifier **recomputes** from the bound inputs.
//!
//! The decision is a pure function of `(grant capability, required entitlement,
//! issued time, expiry)`. [`KeyReleaseReceipt::verify`] re-runs [`decide_release`]
//! over the receipt's own fields and rejects any receipt whose stored decision
//! does not match — so "we released keys only to entitled, unexpired sessions"
//! stops being a policy assertion and becomes a fact a licensor can check
//! against the signed log.
//!
//! The key itself never enters the receipt. A granted release carries a
//! `key_commit` — BLAKE3 over the released key bytes — so a verifier confirms
//! *which* key was released (it matches the KID's key on the encrypted content)
//! without the record ever carrying key material. A refusal carries no
//! commitment, and the coupling is enforced, not merely documented.
//!
//! Only the measurement — `joules_micro`, its acquisition class and the class's
//! evidence — is measured-attested: signed, and not re-measurable. Reuses the
//! manifest module's `EnergyProvenance` and the world Merkle floor's Ed25519 + `GrantRef`; links
//! to a manifest via `parent_receipt_hash`. Reuse, not reinvention.

use ed25519_dalek::{Signer, SigningKey};

use crate::quantum_ops::GrantRef;
use crate::energy_class::{figure_and_class_cohere, write_optional};
use crate::wai_video_manifest::EnergyProvenance;

const DOMAIN_RECEIPT: &[u8] = b"wai:video-keys-receipt\x01";
const DOMAIN_RECEIPT_ID: &[u8] = b"wai:video-keys-receipt-id\x01";
const DOMAIN_KEY: &[u8] = b"wai:video-keys-commit\x01";

/// The DRM system the release targets. Sealed so the record says which client
/// ecosystem the key went to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
// The variants name DRM systems as the receipt records them; the names are part of
// the signed label (`label`) and the wire tag (`tag`). allow-name: DRM system identifiers
pub enum DrmSystem {
    Widevine, // allow-name: DRM system identifier
    PlayReady, // allow-name: DRM system identifier
    FairPlay, // allow-name: DRM system identifier
    ClearKey,
}

impl DrmSystem {
    fn tag(self) -> u8 {
        match self {
            DrmSystem::Widevine => 1, // allow-name: DRM system identifier
            DrmSystem::PlayReady => 2, // allow-name: DRM system identifier
            DrmSystem::FairPlay => 3, // allow-name: DRM system identifier
            DrmSystem::ClearKey => 4,
        }
    }
    pub fn label(self) -> &'static str {
        match self {
            DrmSystem::Widevine => "Widevine", // allow-name: signed label of the DRM system
            DrmSystem::PlayReady => "PlayReady", // allow-name: signed label of the DRM system
            DrmSystem::FairPlay => "FairPlay", // allow-name: signed label of the DRM system
            DrmSystem::ClearKey => "ClearKey",
        }
    }
}

/// The outcome of a release decision — a grant *and* every refusal is a
/// first-class, signed record.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReleaseDecision {
    Granted,
    DeniedNoEntitlement,
    DeniedExpired,
}

impl ReleaseDecision {
    fn tag(self) -> u8 {
        match self {
            ReleaseDecision::Granted => 1,
            ReleaseDecision::DeniedNoEntitlement => 2,
            ReleaseDecision::DeniedExpired => 3,
        }
    }
    pub fn label(self) -> &'static str {
        match self {
            ReleaseDecision::Granted => "Granted",
            ReleaseDecision::DeniedNoEntitlement => "DeniedNoEntitlement",
            ReleaseDecision::DeniedExpired => "DeniedExpired",
        }
    }
    pub fn is_granted(self) -> bool {
        self == ReleaseDecision::Granted
    }
}

/// The one definition of the policy, used by both the issuer and every verifier
/// so they cannot disagree. Entitlement is an exact class match; a granted key
/// requires the request to arrive at or before the entitlement's expiry.
pub fn decide_release(
    grant_capability: &str,
    required_entitlement: &str,
    issued_epoch: u64,
    expiry_epoch: u64,
) -> ReleaseDecision {
    if grant_capability != required_entitlement {
        return ReleaseDecision::DeniedNoEntitlement;
    }
    if issued_epoch > expiry_epoch {
        return ReleaseDecision::DeniedExpired;
    }
    ReleaseDecision::Granted
}

/// BLAKE3 commitment to released key bytes — proves *which* key without carrying
/// it. Domain-separated so it can never collide with another module's hash of
/// the same bytes.
pub fn key_commitment(key_bytes: &[u8]) -> [u8; 32] {
    let mut h = blake3::Hasher::new();
    h.update(DOMAIN_KEY);
    h.update(&(key_bytes.len() as u32).to_be_bytes());
    h.update(key_bytes);
    *h.finalize().as_bytes()
}

/// A signed content-key release decision.
#[derive(Clone, Debug, PartialEq)]
pub struct KeyReleaseReceipt {
    /// The key identifier (KID) on the encrypted content.
    pub key_id: [u8; 16],
    pub content_id: String,
    pub drm: DrmSystem,
    /// The entitlement class the content demanded.
    pub required_entitlement: String,
    pub decision: ReleaseDecision,
    /// BLAKE3 over the released key bytes — `Some` iff `decision` is `Granted`.
    pub key_commit: Option<[u8; 32]>,
    /// The entitlement window enforced, and when the decision was taken.
    pub expiry_epoch: u64,
    pub issued_epoch: u64,
    pub joules_micro: u64,
    pub energy_provenance: Option<EnergyProvenance>,
    /// The grant presented; its `capability` is the entitlement checked.
    pub grant: GrantRef,
    pub parent_receipt_hash: Option<[u8; 32]>,
    pub signer_pubkey: [u8; 32],
    pub signer_id: String,
    pub sig: [u8; 64],
}

impl KeyReleaseReceipt {
    fn signing_payload(&self) -> Vec<u8> {
        let mut o = DOMAIN_RECEIPT.to_vec();
        o.extend_from_slice(&self.key_id);
        o.extend_from_slice(&(self.content_id.len() as u32).to_be_bytes());
        o.extend_from_slice(self.content_id.as_bytes());
        o.push(self.drm.tag());
        o.extend_from_slice(&(self.required_entitlement.len() as u32).to_be_bytes());
        o.extend_from_slice(self.required_entitlement.as_bytes());
        o.push(self.decision.tag());
        match self.key_commit {
            Some(c) => {
                o.push(1);
                o.extend_from_slice(&c);
            }
            None => o.push(0),
        }
        o.extend_from_slice(&self.expiry_epoch.to_be_bytes());
        o.extend_from_slice(&self.issued_epoch.to_be_bytes());
        o.extend_from_slice(&self.joules_micro.to_be_bytes());
        write_optional(self.energy_provenance.as_ref(), &mut o);
        o.extend_from_slice(&self.grant.grant_hash);
        o.extend_from_slice(&(self.grant.capability.len() as u32).to_be_bytes());
        o.extend_from_slice(self.grant.capability.as_bytes());
        o.extend_from_slice(&self.grant.joule_ceiling_micro.to_be_bytes());
        match self.grant.funds_ceiling {
            Some(f) => {
                o.push(1);
                o.extend_from_slice(&f.to_be_bytes());
            }
            None => o.push(0),
        }
        match self.parent_receipt_hash {
            Some(h) => {
                o.push(1);
                o.extend_from_slice(&h);
            }
            None => o.push(0),
        }
        o.extend_from_slice(&self.signer_pubkey);
        let id = self.signer_id.as_bytes();
        o.extend_from_slice(&(id.len() as u32).to_be_bytes());
        o.extend_from_slice(id);
        o
    }

    /// Evaluate the policy and seal the decision. The decision is computed here
    /// from the grant and the request, so a receipt cannot claim an outcome the
    /// policy would not produce; `released_key` is hashed into a commitment only
    /// when the decision is `Granted`.
    #[allow(clippy::too_many_arguments)]
    pub fn seal(
        signer: &SigningKey,
        signer_id: impl Into<String>,
        key_id: [u8; 16],
        content_id: impl Into<String>,
        drm: DrmSystem,
        required_entitlement: impl Into<String>,
        issued_epoch: u64,
        expiry_epoch: u64,
        released_key: &[u8],
        joules_micro: u64,
        energy_provenance: Option<EnergyProvenance>,
        grant: GrantRef,
        parent_receipt_hash: Option<[u8; 32]>,
    ) -> KeyReleaseReceipt {
        let required_entitlement = required_entitlement.into();
        let decision =
            decide_release(&grant.capability, &required_entitlement, issued_epoch, expiry_epoch);
        let key_commit = decision.is_granted().then(|| key_commitment(released_key));
        let mut r = KeyReleaseReceipt {
            key_id,
            content_id: content_id.into(),
            drm,
            required_entitlement,
            decision,
            key_commit,
            expiry_epoch,
            issued_epoch,
            joules_micro,
            energy_provenance,
            grant,
            parent_receipt_hash,
            signer_pubkey: signer.verifying_key().to_bytes(),
            signer_id: signer_id.into(),
            sig: [0u8; 64],
        };
        r.sig = signer.sign(&r.signing_payload()).to_bytes();
        r
    }

    /// Signature + policy consistency. Re-runs [`decide_release`] over the
    /// receipt's own fields and rejects any mismatch, enforces the
    /// granted-iff-committed coupling, and holds the energy figure and its label
    /// together. A refusal is a valid receipt; a *misreported* decision is not.
    pub fn verify(&self) -> bool {
        let recomputed = decide_release(
            &self.grant.capability,
            &self.required_entitlement,
            self.issued_epoch,
            self.expiry_epoch,
        );
        if recomputed != self.decision {
            return false;
        }
        if self.decision.is_granted() != self.key_commit.is_some() {
            return false;
        }
        // The figure and its class stand or fall together, and a hardware
        // class must carry its declared uncertainty (energy_class).
        if !figure_and_class_cohere(self.joules_micro, self.energy_provenance.as_ref()) {
            return false;
        }
        crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
    }

    /// Confirm the released key this receipt commits to is the one the caller
    /// holds — the licensor's check that the sealed commitment matches the KID's
    /// key. `false` for a refusal (there is no committed key).
    pub fn commits_to_key(&self, key_bytes: &[u8]) -> bool {
        self.key_commit == Some(key_commitment(key_bytes))
    }

    /// Amortised microjoules of decision energy per release, with its
    /// acquisition class. `None` when unmetered.
    pub fn uj_per_release(&self) -> Option<(f64, EnergyProvenance)> {
        match &self.energy_provenance {
            Some(p) if self.joules_micro > 0 => Some((self.joules_micro as f64, p.clone())),
            _ => None,
        }
    }

    pub fn receipt_hash(&self) -> [u8; 32] {
        let mut h = blake3::Hasher::new();
        h.update(DOMAIN_RECEIPT_ID);
        h.update(&self.signing_payload());
        h.update(&self.sig);
        *h.finalize().as_bytes()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn key(s: u8) -> SigningKey {
        SigningKey::from_bytes(&[s; 32])
    }

    fn grant(cap: &str) -> GrantRef {
        GrantRef { grant_hash: [5u8; 32], capability: cap.into(), joule_ceiling_micro: 1_000_000, funds_ceiling: None }
    }

    const CEK: &[u8] = b"0123456789abcdef"; // a 16-byte content-encryption key

    #[test]
    fn entitled_and_unexpired_releases_and_commits() {
        let r = KeyReleaseReceipt::seal(
            &key(1), "did:key:license", [0xAB; 16], "asset-abc", DrmSystem::Widevine, // allow-name: DRM system identifier
            "svod:tier2", 1000, 2000, CEK, 300, Some(crate::energy_class::test_on_chip()),
            grant("svod:tier2"), None,
        );
        assert_eq!(r.decision, ReleaseDecision::Granted);
        assert!(r.verify());
        assert!(r.commits_to_key(CEK));
        assert!(!r.commits_to_key(b"the-wrong-key-16"), "a different key must not match the commitment");
    }

    #[test]
    fn wrong_entitlement_is_a_signed_refusal() {
        let r = KeyReleaseReceipt::seal(
            &key(2), "o", [1; 16], "asset", DrmSystem::PlayReady, "svod:tier3", // allow-name: DRM system identifier
            1000, 2000, CEK, 0, None, grant("svod:tier2"), None,
        );
        assert_eq!(r.decision, ReleaseDecision::DeniedNoEntitlement);
        assert!(r.key_commit.is_none(), "a refusal carries no key commitment");
        assert!(r.verify(), "a refusal is still a valid, signed record");
    }

    #[test]
    fn expired_entitlement_denies() {
        let r = KeyReleaseReceipt::seal(
            &key(3), "o", [1; 16], "asset", DrmSystem::FairPlay, "svod:tier2", // allow-name: DRM system identifier
            2500, 2000, CEK, 0, None, grant("svod:tier2"), None,
        );
        assert_eq!(r.decision, ReleaseDecision::DeniedExpired);
        assert!(r.verify());
    }

    #[test]
    fn a_misreported_decision_will_not_verify() {
        // Forge a "Granted" over a request the policy would deny (wrong tier),
        // re-sign it, and confirm the recomputation catches it.
        let mut r = KeyReleaseReceipt::seal(
            &key(4), "o", [1; 16], "asset", DrmSystem::Widevine, "svod:tier3", // allow-name: DRM system identifier
            1000, 2000, CEK, 0, None, grant("svod:tier2"), None,
        );
        assert_eq!(r.decision, ReleaseDecision::DeniedNoEntitlement);
        r.decision = ReleaseDecision::Granted;
        r.key_commit = Some(key_commitment(CEK));
        r.sig = key(4).sign(&r.signing_payload()).to_bytes();
        assert!(!r.verify(), "a decision the bound policy would not produce must fail");
    }

    #[test]
    fn granted_without_commitment_is_rejected() {
        let mut r = KeyReleaseReceipt::seal(
            &key(5), "o", [1; 16], "asset", DrmSystem::ClearKey, "svod:tier2",
            1000, 2000, CEK, 100, Some(EnergyProvenance::ModelBased), grant("svod:tier2"), None,
        );
        assert!(r.verify());
        r.key_commit = None; // granted but no committed key
        r.sig = key(5).sign(&r.signing_payload()).to_bytes();
        assert!(!r.verify());
    }


    fn hex(b: &[u8]) -> String {
        b.iter().map(|x| format!("{x:02x}")).collect()
    }

    /// A receipt sealed with the legacy `HwShunt` label keeps its signed bytes.
    #[test]
    #[allow(deprecated)] // sealing a legacy HwShunt receipt is the point
    fn legacy_hwshunt_receipt_keeps_its_signed_bytes() {
        let r = KeyReleaseReceipt::seal(
            &key(1), "did:key:license", [0xAB; 16], "asset-abc", DrmSystem::Widevine, // allow-name: DRM system identifier
            "svod:tier2", 1000, 2000, CEK, 300, Some(EnergyProvenance::HwShunt),
            grant("svod:tier2"), None,
        );
        assert!(r.verify());
        assert_eq!(hex(&r.receipt_hash()), "12c976fb6f742aeefb1f71af9a7b4eca01be5ba726bd80e1a8ed22cf1ac28249");
    }

    /// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
    /// non-canonical spelling of one, and an R of small order are each refused, though a
    /// lenient check accepts each.
    #[test]
    fn the_signature_is_verified_strictly() {
        let r = KeyReleaseReceipt::seal(
            &key(1), "did:key:license", [0xAB; 16], "asset-abc", DrmSystem::Widevine, // allow-name: DRM system identifier
            "svod:tier2", 1000, 2000, CEK, 300, Some(crate::energy_class::test_on_chip()),
            grant("svod:tier2"), None,
        );
        crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
    }
}
