//! **Reproducible-reconstruction binding** — a cryptographic *hard* binding over the
//! deterministically decoded pixels of a neural-coded asset.
//!
//! Provenance systems (C2PA, JPEG Trust) bind a manifest to an asset with a **hard
//! binding**: a hash over the asset bytes. For neural-coded media that hash covers the
//! *compressed* codestream — and it is the only hard option available, because a neural
//! decode is normally not reproducible: two conformant decoders can produce different
//! pixels, so a hash of the *decoded image* is not portable. JPEG AI makes this explicit
//! — its normative conformance point is the output of the entropy decoder, and its
//! multi-branch design lets one codestream reconstruct through up to three synthesis
//! transforms. So provenance over the *rendered* result has had to fall back to fuzzy
//! **soft** bindings (perceptual hashes, watermarks).
//!
//! WAI's integer-exact decoders (decode-equivalence) reconstruct byte-identically on
//! every conforming sink, and its exact JPEG AI decoder pins one reconstruction that
//! reproduces across machines under the conditions in `extensions/determinism-tiers.md`
//! §5 (a property of that decoder; `wai.image.jpegai` itself is registered at
//! entropy-consistency). Either restores the precondition a hard binding needs: a
//! stable hash of the decoded pixels. This module produces that binding — a SHA-256
//! (the C2PA default hash) over the canonical decoded RGB, plus the descriptor that
//! makes the decode reproducible (which synthesis branch, profile/level, pixel layout,
//! colour space). A verifier with a WAI decoder re-decodes per the descriptor, re-hashes,
//! and compares.
//!
//! It is the reconstruct-side analogue of [`crate::provenance`]: that binds the opaque
//! asset bytes (content-hash-equivalence); this binds the reconstructed pixels. The
//! digest is expressed as a custom C2PA / JPEG-Trust assertion — see
//! `extensions/reconstruction-binding.md` for the on-the-wire mapping. WAI is
//! self-referential about it: it states the capability (a reproducible-reconstruction
//! binding), not a comparison.

use sha2::{Digest, Sha256};

/// What must be pinned for the decode to be reproducible. Different values here decode
/// the same codestream to different pixels, so the descriptor is part of the binding —
/// a verifier decodes under exactly these parameters before hashing.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReconstructionDescriptor {
    /// The synthesis branch the reconstruction is pinned to (e.g. the JPEG-AI
    /// `decoderID`, 0/1/2). Multi-branch codecs decode one codestream several ways;
    /// the binding fixes one.
    pub decoder_id: u8,
    /// Codec profile identifier (capability-specific, e.g. a JPEG-AI profile id).
    pub profile: String,
    /// Codec level identifier.
    pub level: String,
    pub width: u32,
    pub height: u32,
    /// Canonical pixel layout of the hashed buffer. This module hashes `RGB8`:
    /// row-major, 3 bytes/pixel, no row padding, no alpha.
    pub format: String,
    /// Colour space / transfer of the decoded pixels (e.g. `"bt709"`).
    pub color_space: String,
}

impl ReconstructionDescriptor {
    /// The byte length the canonical `RGB8` buffer must have.
    pub fn canonical_len(&self) -> usize {
        self.width as usize * self.height as usize * 3
    }
}

/// A reproducible-reconstruction binding: the descriptor plus a hash over the canonical
/// decoded pixels, ready to be carried as a C2PA / JPEG-Trust assertion or in a WAI
/// receipt.
#[derive(Clone, Debug)]
pub struct ReconstructionBinding {
    /// Hash algorithm id, in the C2PA registry's spelling. Always `"sha256"` here — the
    /// C2PA default every validator is required to support.
    pub alg: String,
    /// SHA-256 over the canonical `RGB8` buffer.
    pub hash: [u8; 32],
    pub descriptor: ReconstructionDescriptor,
}

impl ReconstructionBinding {
    /// The custom C2PA / JPEG-Trust assertion label (reverse-domain, as the C2PA spec
    /// requires for third-party assertions). The full CBOR mapping is specified in
    /// `extensions/reconstruction-binding.md`.
    pub const C2PA_LABEL: &'static str = "science.transaction.wai.hash.reconstruction";

    /// Bind a decoded image: hash the canonical `RGB8` buffer under `descriptor`.
    /// `rgb` MUST be exactly `descriptor.canonical_len()` bytes (row-major RGB8).
    pub fn bind(rgb: &[u8], descriptor: ReconstructionDescriptor) -> Self {
        assert_eq!(
            rgb.len(),
            descriptor.canonical_len(),
            "rgb buffer must be canonical RGB8 (width*height*3, no padding)"
        );
        let hash: [u8; 32] = Sha256::digest(rgb).into();
        ReconstructionBinding { alg: "sha256".to_string(), hash, descriptor }
    }

    /// Re-verify: recompute the hash over a freshly decoded `rgb` and compare. The
    /// caller MUST have decoded under this binding's `descriptor`.
    pub fn verify(&self, rgb: &[u8]) -> bool {
        self.alg == "sha256"
            && rgb.len() == self.descriptor.canonical_len()
            && <[u8; 32]>::from(Sha256::digest(rgb)) == self.hash
    }

    /// Lowercase-hex of the digest.
    pub fn hash_hex(&self) -> String {
        let mut s = String::with_capacity(64);
        for b in self.hash {
            use std::fmt::Write;
            let _ = write!(s, "{b:02x}");
        }
        s
    }

    /// The binding as JSON, the shape a WAI receipt carries. The C2PA CBOR assertion
    /// (label [`Self::C2PA_LABEL`]) is the same fields under the mapping in
    /// `extensions/reconstruction-binding.md`; the digest is identical, so a validator
    /// that re-decodes with a WAI decoder gets a matching hash either way.
    pub fn to_json(&self) -> String {
        let d = &self.descriptor;
        format!(
            "{{\"alg\":\"{}\",\"hash\":\"{}\",\"reconstruction\":{{\"decoderId\":{},\
             \"profile\":{},\"level\":{},\"width\":{},\"height\":{},\"format\":{},\
             \"colorSpace\":{}}}}}",
            self.alg,
            self.hash_hex(),
            d.decoder_id,
            json_str(&d.profile),
            json_str(&d.level),
            d.width,
            d.height,
            json_str(&d.format),
            json_str(&d.color_space),
        )
    }
}

/// Minimal JSON string escaping for the small, controlled descriptor fields.
fn json_str(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 2);
    out.push('"');
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            c => out.push(c),
        }
    }
    out.push('"');
    out
}

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

    fn desc(w: u32, h: u32) -> ReconstructionDescriptor {
        ReconstructionDescriptor {
            decoder_id: 0,
            profile: "main".into(),
            level: "L1".into(),
            width: w,
            height: h,
            format: "RGB8".into(),
            color_space: "bt709".into(),
        }
    }

    #[test]
    fn bind_then_verify_roundtrips() {
        let rgb: Vec<u8> = (0..4 * 4 * 3).map(|i| (i * 7 % 251) as u8).collect();
        let b = ReconstructionBinding::bind(&rgb, desc(4, 4));
        assert!(b.verify(&rgb), "the same pixels must verify");
        assert_eq!(b.alg, "sha256");
        assert_eq!(b.hash_hex().len(), 64);
    }

    #[test]
    fn one_flipped_pixel_fails_verification() {
        let rgb: Vec<u8> = (0..4 * 4 * 3).map(|i| (i * 7 % 251) as u8).collect();
        let b = ReconstructionBinding::bind(&rgb, desc(4, 4));
        let mut tampered = rgb.clone();
        tampered[10] ^= 1;
        assert!(!b.verify(&tampered), "a single flipped byte must break the hard binding");
    }

    #[test]
    fn hash_is_the_c2pa_default_over_canonical_bytes() {
        // The digest is a plain SHA-256 over the RGB8 buffer — reproducible by any
        // SHA-256, which is exactly what makes it portable into a C2PA validator.
        let rgb = vec![0u8; 2 * 2 * 3];
        let b = ReconstructionBinding::bind(&rgb, desc(2, 2));
        let expect: [u8; 32] = Sha256::digest(&rgb).into();
        assert_eq!(b.hash, expect);
    }

    #[test]
    fn json_carries_descriptor_and_hash() {
        let rgb = vec![1u8; 2 * 2 * 3];
        let b = ReconstructionBinding::bind(&rgb, desc(2, 2));
        let j = b.to_json();
        assert!(j.contains("\"alg\":\"sha256\""));
        assert!(j.contains("\"decoderId\":0"));
        assert!(j.contains(&format!("\"hash\":\"{}\"", b.hash_hex())));
        assert!(j.contains("\"colorSpace\":\"bt709\""));
    }
}
