//! The simulation contract — extension §6, section kind 0x01.
//!
//! Canonical JSON: sorted keys, no insignificant whitespace, lowercase
//! hex. This section participates in hashing (the seal binds it), so
//! emission is by hand here — not through a serializer whose output
//! shape could drift between versions.

use super::WorldError;

#[derive(Clone, Debug, PartialEq)]
pub struct SimContract {
    pub tick_rate: u32,
    /// Registered numeric profile, e.g. [`super::NUMERIC_FIXED64`].
    pub numeric: String,
    pub rng_alg: String,
    pub rng_seed: [u8; 32],
    /// The sim capability that owns mutation-kind semantics and `step`.
    pub sim: String,
    /// Default conflict-resolution function (per-kind mapping is the
    /// sim capability's static registration).
    pub resolve_default: String,
    pub checkpoint_every: u64,
}

impl SimContract {
    /// Canonical bytes: keys sorted (`checkpoint_every, numeric,
    /// resolve, rng, sim, tick_rate`), nested objects likewise, no
    /// whitespace. This exact byte form is what the WWLD container
    /// carries and what the seal hash commits to.
    pub fn to_canonical_json(&self) -> Vec<u8> {
        let seed_hex: String = self.rng_seed.iter().map(|b| format!("{b:02x}")).collect();
        format!(
            "{{\"checkpoint_every\":{},\"numeric\":\"{}\",\"resolve\":{{\"default\":\"{}\"}},\"rng\":{{\"alg\":\"{}\",\"seed\":\"{}\"}},\"sim\":\"{}\",\"tick_rate\":{}}}",
            self.checkpoint_every,
            self.numeric,
            self.resolve_default,
            self.rng_alg,
            seed_hex,
            self.sim,
            self.tick_rate,
        )
        .into_bytes()
    }

    /// Lenient parse (any field order, whitespace tolerated) — readers
    /// MUST NOT depend on key order for semantics, same rule as the
    /// core manifest (SPEC.md §2).
    pub fn from_json(bytes: &[u8]) -> Result<SimContract, WorldError> {
        let v: serde_json::Value = serde_json::from_slice(bytes)
            .map_err(|e| WorldError::BadContract(e.to_string()))?;
        let obj = v.as_object().ok_or_else(|| WorldError::BadContract("not an object".into()))?;

        let u64_field = |k: &str| -> Result<u64, WorldError> {
            obj.get(k)
                .and_then(|x| x.as_u64())
                .ok_or_else(|| WorldError::BadContract(format!("missing/invalid `{k}`")))
        };
        let str_field = |o: &serde_json::Map<String, serde_json::Value>, k: &str| -> Result<String, WorldError> {
            o.get(k)
                .and_then(|x| x.as_str())
                .map(str::to_owned)
                .ok_or_else(|| WorldError::BadContract(format!("missing/invalid `{k}`")))
        };

        let rng = obj
            .get("rng")
            .and_then(|x| x.as_object())
            .ok_or_else(|| WorldError::BadContract("missing/invalid `rng`".into()))?;
        let seed_hex = str_field(rng, "seed")?;
        if seed_hex.len() != 64 || !seed_hex.bytes().all(|b| b.is_ascii_hexdigit()) {
            return Err(WorldError::BadContract("`rng.seed` must be 32 bytes of lowercase hex".into()));
        }
        let mut rng_seed = [0u8; 32];
        for (i, chunk) in seed_hex.as_bytes().chunks(2).enumerate() {
            let s = std::str::from_utf8(chunk).unwrap();
            rng_seed[i] = u8::from_str_radix(s, 16)
                .map_err(|_| WorldError::BadContract("bad hex in `rng.seed`".into()))?;
        }

        let resolve = obj
            .get("resolve")
            .and_then(|x| x.as_object())
            .ok_or_else(|| WorldError::BadContract("missing/invalid `resolve`".into()))?;

        let tick_rate = u64_field("tick_rate")?;
        if tick_rate == 0 || tick_rate > u32::MAX as u64 {
            return Err(WorldError::BadContract("`tick_rate` out of range".into()));
        }
        let checkpoint_every = u64_field("checkpoint_every")?;
        if checkpoint_every == 0 {
            return Err(WorldError::BadContract("`checkpoint_every` must be >= 1".into()));
        }

        Ok(SimContract {
            tick_rate: tick_rate as u32,
            numeric: str_field(obj, "numeric")?,
            rng_alg: str_field(rng, "alg")?,
            rng_seed,
            sim: str_field(obj, "sim")?,
            resolve_default: str_field(resolve, "default")?,
            checkpoint_every,
        })
    }
}

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

    fn sample() -> SimContract {
        SimContract {
            tick_rate: 60,
            numeric: crate::world::NUMERIC_FIXED64.into(),
            rng_alg: "xoshiro256++".into(),
            rng_seed: [0xab; 32],
            sim: "wai.world.sim.kinematic0".into(),
            resolve_default: crate::world::RESOLVE_LWW.into(),
            checkpoint_every: 100,
        }
    }

    #[test]
    fn canonical_json_round_trips() {
        let c = sample();
        let bytes = c.to_canonical_json();
        let back = SimContract::from_json(&bytes).unwrap();
        assert_eq!(back, c);
        // Canonical emit of the parse equals the original bytes.
        assert_eq!(back.to_canonical_json(), bytes);
    }

    #[test]
    fn parse_is_order_lenient_but_emit_is_canonical() {
        let scrambled = br#"{ "tick_rate": 60, "sim": "wai.world.sim.kinematic0",
            "rng": {"seed": "abababababababababababababababababababababababababababababababab", "alg": "xoshiro256++"},
            "resolve": {"default": "wai.world.resolve.lww"},
            "numeric": "wai.det.fixed64", "checkpoint_every": 100 }"#;
        let c = SimContract::from_json(scrambled).unwrap();
        assert_eq!(c, sample());
    }

    #[test]
    fn rejects_zero_checkpoint_cadence() {
        let mut c = sample();
        c.checkpoint_every = 0;
        let bytes = c.to_canonical_json();
        assert!(SimContract::from_json(&bytes).is_err());
    }
}
