//! Canonical JSON serialization.
//!
//! Writes JSON without whitespace and with lexicographically sorted
//! object keys. This makes the representation deterministic across
//! runs, platforms, and `serde_json` versions, which is what makes it
//! suitable as the pre-image for [`crate::def_hash`] /
//! [`crate::module_hash`].
//!
//! The single entry point is [`to_bytes`]. This module deliberately
//! does **not** parse, validate, or hash — it only emits bytes. It also
//! does not attempt to be a general-purpose canonical-JSON library;
//! Number formatting follows whatever `serde_json::Number::to_string`
//! does — used only for the AST integer literals. Float literals do
//! not go through the number path; [`crate::ast::Literal::Float`]
//! carries its IEEE-754 bit pattern as a `u64` and serialises via a
//! 16-character lowercase hex *string*, which the canonical writer
//! treats like any other string. The string path is what makes float
//! canonical bytes bit-stable across `serde_json` versions and what
//! lets NaN / ±Inf round-trip at all (they cannot, as JSON numbers).
//!
//! # Examples
//!
//! ```ignore
//! use ailang_core::canonical;
//! use serde_json::json;
//!
//! // Object keys are sorted; no whitespace appears in the output.
//! let bytes = canonical::to_bytes(&json!({ "b": 1, "a": 2 }));
//! assert_eq!(std::str::from_utf8(&bytes).unwrap(), r#"{"a":2,"b":1}"#);
//! ```

use std::io::Write;

/// Serialize `value` to canonical JSON bytes.
///
/// Object keys are sorted lexicographically, no whitespace is emitted,
/// and arrays preserve their input order. The output is the byte
/// pre-image used by [`crate::def_hash`] and
/// [`crate::workspace::module_hash`]; if you change what bytes this
/// function emits for a given value, all on-disk hashes change with it.
///
/// # Panics
///
/// Panics if `value` cannot be converted to a `serde_json::Value`
/// (i.e. its `Serialize` impl errors). For the AST types in
/// [`crate::ast`] this never happens.
pub fn to_bytes<T: serde::Serialize>(value: &T) -> Vec<u8> {
    let v = serde_json::to_value(value).expect("serializable");
    let mut out = Vec::new();
    write_value(&v, &mut out).expect("write to Vec");
    out
}

/// Iter 22b.1: 16-hex-char hash of a [`crate::ast::Type`] in isolation.
///
/// Used by [`crate::workspace::Registry`] to key `InstanceDef`s by
/// their target type. Parallel in shape to [`crate::def_hash`] and
/// [`crate::workspace::module_hash`]: BLAKE3 over canonical-JSON
/// bytes, truncated to 16 hex chars.
///
/// The key property is determinism — two `Type` values that are
/// canonically equal MUST hash identically — so the workspace
/// registry's "is `instance C T` already declared?" check is
/// representation-independent.
pub fn type_hash(t: &crate::ast::Type) -> String {
    let bytes = to_bytes(t);
    let h = blake3::hash(&bytes);
    h.to_hex().as_str()[..16].to_string()
}

fn write_value(v: &serde_json::Value, out: &mut Vec<u8>) -> std::io::Result<()> {
    use serde_json::Value;
    match v {
        Value::Null => out.write_all(b"null"),
        Value::Bool(true) => out.write_all(b"true"),
        Value::Bool(false) => out.write_all(b"false"),
        Value::Number(n) => out.write_all(n.to_string().as_bytes()),
        Value::String(s) => {
            let escaped = serde_json::to_string(s).expect("string serializable");
            out.write_all(escaped.as_bytes())
        }
        Value::Array(arr) => {
            out.write_all(b"[")?;
            for (i, item) in arr.iter().enumerate() {
                if i > 0 {
                    out.write_all(b",")?;
                }
                write_value(item, out)?;
            }
            out.write_all(b"]")
        }
        Value::Object(map) => {
            let mut keys: Vec<&String> = map.keys().collect();
            keys.sort();
            out.write_all(b"{")?;
            for (i, k) in keys.iter().enumerate() {
                if i > 0 {
                    out.write_all(b",")?;
                }
                let kj = serde_json::to_string(k).expect("key serializable");
                out.write_all(kj.as_bytes())?;
                out.write_all(b":")?;
                write_value(&map[*k], out)?;
            }
            out.write_all(b"}")
        }
    }
}

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

    #[test]
    fn sorts_keys() {
        let v = json!({ "b": 1, "a": 2 });
        let bytes = to_bytes(&v);
        assert_eq!(std::str::from_utf8(&bytes).unwrap(), r#"{"a":2,"b":1}"#);
    }

    #[test]
    fn nested_keys_sorted() {
        let v = json!({ "z": { "y": 1, "x": [3, { "b": 2, "a": 1 }] } });
        let bytes = to_bytes(&v);
        assert_eq!(
            std::str::from_utf8(&bytes).unwrap(),
            r#"{"z":{"x":[3,{"a":1,"b":2}],"y":1}}"#
        );
    }

    #[test]
    fn no_whitespace() {
        let v = json!({ "a": 1, "b": [1, 2, 3] });
        let bytes = to_bytes(&v);
        let s = std::str::from_utf8(&bytes).unwrap();
        assert!(!s.contains(' '));
        assert!(!s.contains('\n'));
    }

    /// Iter 22-floats.1 RED: a `Literal::Float` carries the IEEE-754
    /// binary64 bit pattern as a `u64`; canonical JSON encodes it as
    /// `{"bits":"<16-lowercase-hex>","kind":"float"}` — string path,
    /// NOT through `serde_json::Number` (which is not bit-stable for
    /// floats and cannot represent NaN / ±Inf at all).
    ///
    /// Pinned literal: `1.5_f64` → bit pattern `0x3FF8000000000000` →
    /// hex string `"3ff8000000000000"`. Sorted-key order is
    /// `bits` < `kind` lexicographically.
    #[test]
    fn float_literal_canonical_bytes() {
        use crate::ast::Literal;
        let lit = Literal::Float {
            bits: 0x3ff8_0000_0000_0000u64,
        };
        let bytes = to_bytes(&lit);
        let s = std::str::from_utf8(&bytes).unwrap();
        assert_eq!(s, r#"{"bits":"3ff8000000000000","kind":"float"}"#);
    }

    /// A5 guarantee: -0.0 and +0.0 are distinct bit patterns and
    /// therefore distinct Form-A literals (they hash distinctly even
    /// though IEEE-`==` will report them equal at the value level
    /// when arithmetic comparison ships).
    #[test]
    fn negative_zero_and_positive_zero_serialise_distinctly() {
        use crate::ast::Literal;
        let pos = Literal::Float { bits: 0x0u64 };
        let neg = Literal::Float { bits: 0x8000_0000_0000_0000u64 };
        let pos_bytes = to_bytes(&pos);
        let neg_bytes = to_bytes(&neg);
        assert_ne!(pos_bytes, neg_bytes, "+0 and -0 must hash distinctly");
        assert_eq!(
            std::str::from_utf8(&pos_bytes).unwrap(),
            r#"{"bits":"0000000000000000","kind":"float"}"#
        );
        assert_eq!(
            std::str::from_utf8(&neg_bytes).unwrap(),
            r#"{"bits":"8000000000000000","kind":"float"}"#
        );
    }

    /// A1 guarantee: a NaN bit pattern survives canonicalisation
    /// without collapse to JSON `null` (which is what the `serde_json`
    /// number path does for non-finite floats — and the reason
    /// `Literal::Float` routes through the *string* path).
    #[test]
    fn nan_bits_preserved() {
        use crate::ast::Literal;
        let qnan = Literal::Float { bits: 0x7ff8_0000_0000_0000u64 };
        let bytes = to_bytes(&qnan);
        assert_eq!(
            std::str::from_utf8(&bytes).unwrap(),
            r#"{"bits":"7ff8000000000000","kind":"float"}"#
        );
    }

    /// A1 guarantee: ±Inf bit patterns survive canonicalisation —
    /// JSON numbers cannot represent infinity at all, the string path
    /// must.
    #[test]
    fn inf_bits_preserved() {
        use crate::ast::Literal;
        let pos_inf = Literal::Float { bits: 0x7ff0_0000_0000_0000u64 };
        let neg_inf = Literal::Float { bits: 0xfff0_0000_0000_0000u64 };
        assert_eq!(
            std::str::from_utf8(&to_bytes(&pos_inf)).unwrap(),
            r#"{"bits":"7ff0000000000000","kind":"float"}"#
        );
        assert_eq!(
            std::str::from_utf8(&to_bytes(&neg_inf)).unwrap(),
            r#"{"bits":"fff0000000000000","kind":"float"}"#
        );
    }

    /// Round-trip: serialise then deserialise via `serde_json` — bits
    /// preserved bit-for-bit.
    #[test]
    fn float_literal_serde_roundtrip() {
        use crate::ast::Literal;
        let cases = [
            0x0u64,
            0x8000_0000_0000_0000u64,
            0x3ff8_0000_0000_0000u64,           // 1.5
            0x7ff8_0000_0000_0000u64,           // qNaN
            0x7ff0_0000_0000_0000u64,           // +Inf
            0xfff0_0000_0000_0000u64,           // -Inf
            0xffff_ffff_ffff_ffffu64,           // saturated
        ];
        for &bits in &cases {
            let lit = Literal::Float { bits };
            let json = serde_json::to_string(&lit).unwrap();
            let back: Literal = serde_json::from_str(&json).unwrap();
            match back {
                Literal::Float { bits: got } => assert_eq!(
                    got, bits,
                    "round-trip lost bits for {:#018x}: json={}",
                    bits, json
                ),
                other => panic!("expected Literal::Float, got {:?}", other),
            }
        }
    }
}
