//! Score receipt — a JWP profile, the worlds/audio/haptic/splat4d receipt
//! transposed to music. The **score hash** is the identity (the
//! composition, exactly as `clip_hash` / `signal_hash` are the container
//! identity for their classes); the group's objects are the per-segment
//! reconstruction blocks (fixed windows of the canonical `i16` PCM
//! mixdown), Merkle-bound with JWP's exact domains; one Ed25519 signature
//! covers a profile payload carrying score hash + segment hashes + exact
//! reconstruction work + measured joules. Chains via `parent_receipt_hash`
//! exactly as the others — reuse, not reinvention.
//!
//! The blocks partition the *reconstructed quantity* (the PCM stream) by
//! time, the same way the haptic blocks partition by channel and the
//! splat4d blocks partition by frame — not by authoring structure (notes
//! overlap; the mix does not). The Merkle root over the segments is thus
//! the mixdown commitment, bound under the same signature as the
//! score-hash identity.

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

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

/// Fixed PCM window per reconstruction block (samples). The final block may
/// be short; every block carries its index, so the partition is canonical.
pub const SEGMENT_SAMPLES: u64 = 4096;

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

#[derive(Clone, Debug, PartialEq)]
pub struct ScoreReceipt {
    pub score_hash: [u8; 32],
    pub group_id: u64,
    pub segments: Vec<SegmentHash>,
    pub merkle_root: [u8; 32],
    /// Exact, deterministic reconstruction work = mixdown samples × notes
    /// (each sample sums every note's contribution in the Fx floor).
    pub work_note_samples: 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 Score {
    /// Per-segment reconstruction-block hashes — the receipt's objects. The
    /// mixdown is synthesized once and partitioned into `SEGMENT_SAMPLES`
    /// windows; each block hash binds its index and the segment's PCM.
    pub fn segment_hashes(&self) -> Vec<SegmentHash> {
        let pcm = self.mixdown();
        pcm.chunks(SEGMENT_SAMPLES as usize)
            .enumerate()
            .map(|(i, seg)| {
                let mut h = blake3::Hasher::new();
                h.update(DOMAIN_BLOCK);
                h.update(&(i as u64).to_le_bytes());
                for s in seg {
                    h.update(&s.to_le_bytes());
                }
                SegmentHash { index: i as u64, hash: *h.finalize().as_bytes() }
            })
            .collect()
    }
}

impl ScoreReceipt {
    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.score_hash);
        o.extend_from_slice(&self.group_id.to_be_bytes());
        o.extend_from_slice(&self.merkle_root);
        o.extend_from_slice(&(self.segments.len() as u64).to_be_bytes());
        o.extend_from_slice(&self.work_note_samples.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.segments {
            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>,
        score_hash: [u8; 32],
        group_id: u64,
        segments: Vec<SegmentHash>,
        work_note_samples: u64,
        joules_micro: u64,
        parent_receipt_hash: Option<[u8; 32]>,
    ) -> ScoreReceipt {
        let leaves: Vec<[u8; 32]> = segments.iter().map(|b| b.hash).collect();
        let mut r = ScoreReceipt {
            score_hash,
            group_id,
            segments,
            merkle_root: merkle_root(&leaves),
            work_note_samples,
            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
    }

    pub fn verify(&self) -> bool {
        if !label_coheres(self.joules_micro, self.energy_provenance.as_ref()) {
            return false;
        }
        let leaves: Vec<[u8; 32]> = self.segments.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<ScoreReceipt, 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:score-receipt-id\x01");
        h.update(&self.signing_payload());
        h.update(&self.sig);
        *h.finalize().as_bytes()
    }

    pub fn to_json(&self) -> String {
        let hx = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
        let objs: Vec<String> = self.segments.iter().map(|c| format!("\"{}\"", hx(&c.hash))).collect();
        let parent = match self.parent_receipt_hash {
            Some(h) => format!("\"{}\"", hx(&h)),
            None => "null".into(),
        };
        let json = format!(
            "{{\"kind\":\"audio-score\",\"group_id\":{},\"joules_micro\":{},\"leaf_count\":{},\"parent_receipt_hash\":{},\"receipt_hash\":\"{}\",\"root_hash\":\"{}\",\"score_hash\":\"{}\",\"segment_hashes\":[{}],\"sig\":\"{}\",\"signer_id\":{},\"signer_pubkey\":\"{}\",\"work_note_samples\":{}}}",
            self.group_id,
            self.joules_micro,
            self.segments.len(),
            parent,
            hx(&self.receipt_hash()),
            hx(&self.merkle_root),
            hx(&self.score_hash),
            objs.join(","),
            hx(&self.sig),
            serde_json::to_string(&self.signer_id).unwrap(),
            hx(&self.signer_pubkey),
            self.work_note_samples,
        );
        json_with_label(json, self.energy_provenance.as_ref())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::score::{waveform, Note, ScoreContract};
    use crate::world::{Fx, NUMERIC_FIXED64};

    fn key(s: u8) -> SigningKey {
        SigningKey::from_bytes(&[s; 32])
    }
    fn inc(freq: f64, sr: u32) -> Fx {
        Fx::from_bits(((freq / sr as f64) * 4294967296.0).round() as i64)
    }
    fn f(n: i32, d: i32) -> Fx {
        Fx::from_int(n) / Fx::from_int(d)
    }
    fn note(start: u64, dur: u64, freq: f64, wf: u8) -> Note {
        Note { start, dur, phase_inc: inc(freq, 22050), gain: f(1, 2), waveform: wf, attack: 200, decay: 400, sustain: f(7, 10), release: 600 }
    }
    fn score() -> Score {
        let c = ScoreContract { sample_rate: 22050, numeric: NUMERIC_FIXED64.into(), channels: 1, duration: 10000 };
        Score {
            contract_bytes: c.to_canonical_json(),
            contract: c,
            notes: vec![note(0, 4000, 440.0, waveform::SAW), note(2000, 4000, 660.0, waveform::SQUARE)],
        }
    }

    #[test]
    fn segments_partition_the_mixdown() {
        let s = score();
        let segs = s.segment_hashes();
        // 10000 samples / 4096 → 3 windows (4096 + 4096 + 1808).
        assert_eq!(segs.len(), 3);
        assert_eq!(segs[0].index, 0);
        assert_eq!(segs[2].index, 2);
    }

    #[test]
    fn receipt_commits_verifies_and_tamper_fails() {
        let s = score();
        let segs = s.segment_hashes();
        let work = s.contract.duration * s.notes.len() as u64;
        let r = ScoreReceipt::build(&key(9), "did:web:wai#score", s.score_hash(), 0, segs, work, 4321, None);
        assert!(r.verify());
        let mut bad = r.clone();
        bad.joules_micro += 1;
        assert!(!bad.verify());
        let mut bad2 = r.clone();
        bad2.segments[1].hash[0] ^= 1;
        assert!(!bad2.verify());
        let _: serde_json::Value = serde_json::from_str(&r.to_json()).unwrap();
    }

    #[test]
    fn receipts_chain() {
        let s = score();
        let sh = s.score_hash();
        let r0 = ScoreReceipt::build(&key(9), "id", sh, 0, s.segment_hashes(), 20000, 0, None);
        let r1 = ScoreReceipt::build(&key(9), "id", sh, 1, s.segment_hashes(), 20000, 0, Some(r0.receipt_hash()));
        assert!(r0.verify() && r1.verify());
        assert_eq!(r1.parent_receipt_hash, Some(r0.receipt_hash()));
        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 s = score();
        let r = ScoreReceipt::build(&key(9), "did:web:wai#score", s.score_hash(), 0, s.segment_hashes(), 20000, 4321, None);
        crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
    }
}
