//! Spatial-audio session receipt — extension §6, a JWP profile.
//!
//! The worlds session receipt, transposed to audio: the **scene hash**
//! is the identity (like `world_hash`), the JWP group's objects are the
//! **PCM blocks** of the rendered mix (one per fixed span of samples,
//! content-hashed), Merkle-bound with JWP's exact domains, and one
//! Ed25519 signature covers a profile payload carrying scene hash +
//! block hashes + exact mix work + measured joules. Receipts chain via
//! `parent_receipt_hash` exactly as worlds' do — reuse, not reinvention.

use super::{AudioError, SpatialScene, Sources};
use crate::world::receipt::merkle_root;
use crate::energy_class::{
    check_label, label_coheres, labelled_domain, write_label, AcquisitionError,
    EnergyProvenance,
};
use ed25519_dalek::{Signer, SigningKey};

const DOMAIN_RECEIPT: &[u8] = b"wai:audio-receipt\x01";
const DOMAIN_BLOCK: &[u8] = b"wai:audio-block\x01";

/// `(block_index, BLAKE3 over the block's i16le PCM)`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BlockHash {
    pub index: u64,
    pub hash: [u8; 32],
}

#[derive(Clone, Debug, PartialEq)]
pub struct SceneReceipt {
    pub scene_hash: [u8; 32],
    pub group_id: u64,
    pub blocks: Vec<BlockHash>,
    pub merkle_root: [u8; 32],
    /// Exact, deterministic mix work = output samples × objects mixed.
    pub work_mac: u64,
    /// Carried microjoules (native meter supplies; 0 = unmetered).
    pub joules_micro: u64,
    /// How `joules_micro` was acquired — an optional label
    /// (`extensions/energy-measurement.md` §2.8). `None` is an unlabelled
    /// figure, as every receipt sealed before the label existed carries; it is
    /// never read as any class. Inside the signature when present.
    pub energy_provenance: Option<EnergyProvenance>,
    pub parent_receipt_hash: Option<[u8; 32]>,
    pub signer_pubkey: [u8; 32],
    pub signer_id: String,
    pub sig: [u8; 64],
}

impl SpatialScene {
    /// Split the rendered mix into fixed-size blocks (per channel-frame)
    /// and content-hash each — the receipt's group objects.
    pub fn block_hashes(
        &self,
        sources: &Sources,
        layout: &str,
        block_frames: usize,
    ) -> Result<Vec<BlockHash>, AudioError> {
        let pcm = self.mixdown(sources, layout)?; // interleaved i16
        let chans = 2usize; // stereo floor
        let per_block = block_frames * chans;
        let mut out = Vec::new();
        for (i, block) in pcm.chunks(per_block).enumerate() {
            let mut h = blake3::Hasher::new();
            h.update(DOMAIN_BLOCK);
            h.update(&(i as u64).to_le_bytes());
            for s in block {
                h.update(&s.to_le_bytes());
            }
            out.push(BlockHash { index: i as u64, hash: *h.finalize().as_bytes() });
        }
        Ok(out)
    }
}

impl SceneReceipt {
    fn signing_payload(&self) -> Vec<u8> {
        let label = self.energy_provenance.as_ref();
        let mut o = labelled_domain(DOMAIN_RECEIPT, label);
        o.extend_from_slice(&self.scene_hash);
        o.extend_from_slice(&self.group_id.to_be_bytes());
        o.extend_from_slice(&self.merkle_root);
        o.extend_from_slice(&(self.blocks.len() as u64).to_be_bytes());
        o.extend_from_slice(&self.work_mac.to_be_bytes());
        o.extend_from_slice(&self.joules_micro.to_be_bytes());
        write_label(label, &mut o);
        match self.parent_receipt_hash {
            Some(h) => {
                o.push(1);
                o.extend_from_slice(&h);
            }
            None => o.push(0),
        }
        for b in &self.blocks {
            o.extend_from_slice(&b.index.to_be_bytes());
            o.extend_from_slice(&b.hash);
        }
        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
    }

    #[allow(clippy::too_many_arguments)]
    pub fn build(
        signer: &SigningKey,
        signer_id: impl Into<String>,
        scene_hash: [u8; 32],
        group_id: u64,
        blocks: Vec<BlockHash>,
        work_mac: u64,
        joules_micro: u64,
        parent_receipt_hash: Option<[u8; 32]>,
    ) -> SceneReceipt {
        let leaves: Vec<[u8; 32]> = blocks.iter().map(|b| b.hash).collect();
        let mut r = SceneReceipt {
            scene_hash,
            group_id,
            blocks,
            merkle_root: merkle_root(&leaves),
            work_mac,
            joules_micro,
            energy_provenance: None,
            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
    }

    /// Verify the Merkle root recomputes from the block hashes and the
    /// signature is valid.
    pub fn verify(&self) -> bool {
        if !label_coheres(self.joules_micro, self.energy_provenance.as_ref()) {
            return false;
        }
        let leaves: Vec<[u8; 32]> = self.blocks.iter().map(|b| b.hash).collect();
        if merkle_root(&leaves) != self.merkle_root {
            return false;
        }
        crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
    }

    /// Label the figure with its acquisition class and re-sign
    /// (`extensions/energy-measurement.md` §2.8). `signer` must be the key
    /// that sealed the receipt. Refuses whatever [`Self::verify`] would
    /// refuse: legacy `HwShunt`, a class beside a zero figure, a refined class
    /// without its evidence.
    pub fn with_energy_class(
        mut self,
        signer: &SigningKey,
        class: EnergyProvenance,
    ) -> Result<SceneReceipt, AcquisitionError> {
        if signer.verifying_key().to_bytes() != self.signer_pubkey {
            return Err(AcquisitionError::SignerMismatch);
        }
        check_label(self.joules_micro, Some(&class))?;
        self.energy_provenance = Some(class);
        self.sig = signer.sign(&self.signing_payload()).to_bytes();
        Ok(self)
    }

    /// Re-sign whatever the fields now say, bypassing every check — so tests
    /// can present `verify()` with a correctly signed receipt that breaks a
    /// label rule, and see the rule itself refuse it.
    #[cfg(test)]
    pub(crate) fn resign_for_test(&mut self, signer: &SigningKey) {
        self.sig = signer.sign(&self.signing_payload()).to_bytes();
    }

    pub fn receipt_hash(&self) -> [u8; 32] {
        let mut h = blake3::Hasher::new();
        h.update(b"wai:audio-receipt-id\x01");
        h.update(&self.signing_payload());
        h.update(&self.sig);
        *h.finalize().as_bytes()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::audio_scene::{AudioObject, PosKey, SceneContract};
    use crate::world::{Fx, NUMERIC_FIXED64};
    use std::collections::HashMap;

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

    fn scene_and_sources() -> (SpatialScene, Sources) {
        let c = SceneContract {
            sample_rate: 48000,
            numeric: NUMERIC_FIXED64.into(),
            pan_law: super::super::PAN_SQRT_CONSTPOWER.into(),
            distance: super::super::DIST_INVERSE_CLAMPED.into(),
            duration: 480,
            floor_layout: "stereo".into(),
        };
        let scene = SpatialScene {
            contract_bytes: c.to_canonical_json(),
            contract: c,
            objects: vec![AudioObject {
                source_hash: [1u8; 32],
                gain: Fx::ONE,
                keys: vec![PosKey { pos_sample: 0, x: Fx::from_int(-2), y: Fx::ZERO, z: Fx::ONE }],
            }],
            assets: vec![],
            seal: None,
        };
        let mut src = HashMap::new();
        src.insert([1u8; 32], (0..480).map(|i| if (i / 6) % 2 == 0 { 8000 } else { -8000 }).collect());
        (scene, src)
    }

    #[test]
    fn receipt_commits_to_blocks_verifies_and_tamper_fails() {
        let (scene, src) = scene_and_sources();
        let blocks = scene.block_hashes(&src, "stereo", 96).unwrap();
        assert_eq!(blocks.len(), 5, "480 frames / 96 = 5 blocks");
        let work = scene.contract.duration * scene.objects.len() as u64;
        let r = SceneReceipt::build(&key(9), "did:web:wai#audio", scene.scene_hash(), 0, blocks, work, 1234, None);
        assert!(r.verify());

        let mut bad = r.clone();
        bad.joules_micro += 1;
        assert!(!bad.verify(), "mutated signed joules must break the sig");
        let mut bad2 = r.clone();
        bad2.blocks[0].hash[0] ^= 1;
        assert!(!bad2.verify(), "mutated block hash must break the Merkle root");
    }

    #[test]
    fn receipts_chain() {
        let (scene, src) = scene_and_sources();
        let sh = scene.scene_hash();
        let blocks = scene.block_hashes(&src, "stereo", 96).unwrap();
        let r0 = SceneReceipt::build(&key(9), "id", sh, 0, blocks.clone(), 100, 0, None);
        let r1 = SceneReceipt::build(&key(9), "id", sh, 1, blocks, 100, 0, Some(r0.receipt_hash()));
        assert_eq!(r1.parent_receipt_hash, Some(r0.receipt_hash()));
        assert!(r0.verify() && r1.verify());
        assert_ne!(r0.receipt_hash(), r1.receipt_hash());
    }

    /// 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 (scene, src) = scene_and_sources();
        let blocks = scene.block_hashes(&src, "stereo", 96).unwrap();
        let r = SceneReceipt::build(&key(9), "did:web:wai#audio", scene.scene_hash(), 0, blocks, 480, 1234, None);
        crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
    }
}
