//! Manifest format per `CACHE-011`.
//!
//! The manifest is the *atomicity signal* of a cache entry: its
//! presence in an entry directory means the entry is complete and
//! usable; its absence (or a parse failure) makes the entry
//! invisible to lookup (`CACHE-016`, `CACHE-022`).
//!
//! Serialised as JSON (file name `manifest.json`). The format
//! denies unknown fields: forward-compatibility flows through the
//! `chapter_revision` byte of the schema-version prefix
//! (`CACHE-003`), not through lenient parsing.
//!
//! Beyond the fields listed in `CACHE-011`, the manifest also
//! records the hashes of the captured stdout and stderr byte
//! streams (`stdout_hash`, `stderr_hash`). This is a deliberate
//! extension: `CACHE-007` requires consumer key derivation to
//! hash predecessor streams, and storing those hashes once at
//! store time lets each downstream consumer skip the re-hash.

use haz_domain::path::CanonicalPath;
use serde::{Deserialize, Serialize};
use snafu::{ResultExt, Snafu};

use crate::key::CacheKey;
use crate::key::prefix::CHAPTER_REVISION;

/// Failure modes for [`Manifest::from_json`].
#[derive(Debug, Snafu)]
pub enum ManifestParseError {
    /// Bytes did not parse as JSON, or the JSON shape did not
    /// match the manifest schema (missing required field, unknown
    /// field, malformed hex digest, unknown `hash_function`
    /// value).
    #[snafu(display("manifest is not valid JSON or does not match the schema: {source}"))]
    InvalidJson {
        /// Underlying serde-json error.
        source: serde_json::Error,
    },
}

/// One entry in the manifest's output-blob list (`CACHE-011`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OutputBlob {
    /// Workspace-absolute path at which the blob's content will be
    /// materialised on restore (`CACHE-019`). Validated as a
    /// [`CanonicalPath`] at deserialisation time, so a manifest
    /// that smuggles a path-traversal segment (`..`) or a
    /// bidirectional-control codepoint into this field fails to
    /// parse and is treated as a cache miss
    /// (`CACHE-016`/`CACHE-022`).
    #[serde(with = "canonical_path_serde")]
    pub workspace_absolute_path: CanonicalPath,

    /// Content hash of the blob bytes under the manifest's
    /// declared `hash_function`, as 64 lowercase hex characters.
    #[serde(with = "hex_digest")]
    pub content_hash: [u8; 32],

    /// Size of the blob in bytes.
    pub size: u64,

    /// Unix permission bits of the materialised file
    /// (`CACHE-013`). Stored as a decimal integer in JSON; the
    /// owner-write bit (`0o200`) is the only one Windows honours
    /// at restore time, per the trait note.
    pub mode: u32,
}

/// Identifier string for the cache's hash function, matching the
/// `CACHE-002` registry: `"blake3"` or `"sha256"`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HashFunctionLabel {
    /// BLAKE3-256 (default).
    Blake3,
    /// SHA-256.
    Sha256,
}

impl From<haz_domain::settings::cache::HashAlgo> for HashFunctionLabel {
    fn from(algo: haz_domain::settings::cache::HashAlgo) -> Self {
        match algo {
            haz_domain::settings::cache::HashAlgo::Blake3 => Self::Blake3,
            haz_domain::settings::cache::HashAlgo::Sha256 => Self::Sha256,
        }
    }
}

impl From<HashFunctionLabel> for haz_domain::settings::cache::HashAlgo {
    fn from(label: HashFunctionLabel) -> Self {
        match label {
            HashFunctionLabel::Blake3 => Self::Blake3,
            HashFunctionLabel::Sha256 => Self::Sha256,
        }
    }
}

/// On-disk manifest of a cache entry (`CACHE-011`).
///
/// Stored at `<workspace-root>/.haz/cache/<shard>/<key>/manifest.json`
/// per `CACHE-010`. The file's presence is the atomicity signal;
/// readers that find it must check its `chapter_revision` and
/// `hash_function` fields against the current configuration and
/// treat any mismatch as a cache miss (`CACHE-016`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
    /// Revision of the cache-key composition rules under which
    /// this entry was stored. Bumped by a normative change to
    /// `CACHE-004..009`.
    pub chapter_revision: u8,

    /// Identifier of the hash function used to derive
    /// `key`/`content_hash`/`stdout_hash`/`stderr_hash`, matching
    /// the `CACHE-002` registry.
    pub hash_function: HashFunctionLabel,

    /// The cache key this entry corresponds to, as 64 hex
    /// characters.
    #[serde(with = "hex_key")]
    pub key: CacheKey,

    /// Output blobs materialised by this entry (`CACHE-013`).
    /// Order is preserved as supplied; restoration MAY parallelise
    /// blob writes per `CACHE-020`.
    pub outputs: Vec<OutputBlob>,

    /// Byte length of the captured stdout stream.
    pub stdout_len: u64,

    /// Byte length of the captured stderr stream.
    pub stderr_len: u64,

    /// Hash of the captured stdout bytes under `hash_function`,
    /// as 64 hex characters. Recording this once at store time
    /// lets downstream consumers' key derivation skip re-hashing
    /// (`CACHE-007`).
    #[serde(with = "hex_digest")]
    pub stdout_hash: [u8; 32],

    /// Hash of the captured stderr bytes under `hash_function`.
    #[serde(with = "hex_digest")]
    pub stderr_hash: [u8; 32],

    /// Process exit status of the recorded run. Always `0` per
    /// `CACHE-018` (only successful runs are stored); the field
    /// exists for future revisions.
    pub exit_status: i32,

    /// Unix seconds since the epoch at which the manifest was
    /// created (informative; MUST NOT contribute to the key).
    pub created_at_unix: u64,
}

impl Manifest {
    /// Convenience: the chapter revision field matches the value
    /// the cache currently writes (`CHAPTER_REVISION` from
    /// [`crate::key::prefix`]).
    #[must_use]
    pub fn current_chapter_revision_matches(&self) -> bool {
        self.chapter_revision == CHAPTER_REVISION
    }

    /// Serialise this manifest to JSON bytes. Two-space-indented
    /// pretty-printed form, terminated with a newline; the bytes
    /// are deterministic for a given [`Manifest`] value (modulo
    /// platform line endings).
    ///
    /// # Panics
    ///
    /// Panics only if `serde_json` fails to serialise this
    /// manifest, which is impossible given the schema: every
    /// field type maps to a valid JSON shape.
    #[must_use]
    pub fn to_json_bytes(&self) -> Vec<u8> {
        let mut bytes =
            serde_json::to_vec_pretty(self).expect("Manifest serialises to JSON unconditionally");
        bytes.push(b'\n');
        bytes
    }

    /// Parse a manifest from JSON bytes.
    ///
    /// # Errors
    ///
    /// Returns [`ManifestParseError::InvalidJson`] on any JSON
    /// parse failure (malformed JSON, missing required field,
    /// unknown field, malformed hex digest, unknown
    /// `hash_function` value, type mismatch).
    pub fn from_json(bytes: &[u8]) -> Result<Self, ManifestParseError> {
        serde_json::from_slice(bytes).context(InvalidJsonSnafu)
    }
}

mod hex_digest {
    use serde::de::Error as _;
    use serde::{Deserializer, Serializer};

    use crate::hex;

    pub fn serialize<S: Serializer>(bytes: &[u8; 32], s: S) -> Result<S::Ok, S::Error> {
        s.serialize_str(&hex::encode_32(bytes))
    }

    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 32], D::Error> {
        use serde::Deserialize as _;
        let s = String::deserialize(d)?;
        hex::decode_32(&s).map_err(D::Error::custom)
    }
}

mod hex_key {
    use serde::de::Error as _;
    use serde::{Deserializer, Serializer};

    use crate::key::CacheKey;

    pub fn serialize<S: Serializer>(key: &CacheKey, s: S) -> Result<S::Ok, S::Error> {
        s.serialize_str(&key.to_hex())
    }

    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<CacheKey, D::Error> {
        use serde::Deserialize as _;
        let s = String::deserialize(d)?;
        CacheKey::from_hex(&s).map_err(D::Error::custom)
    }
}

/// JSON adapter for [`CanonicalPath`] in [`OutputBlob`]. The
/// validated typed value lives in `haz-domain`; the cache layer
/// owns the on-disk representation. JSON shape is the rendered
/// path string (`/seg/seg/...`), matching [`CanonicalPath`]'s
/// [`Display`](core::fmt::Display) impl.
mod canonical_path_serde {
    use haz_domain::path::CanonicalPath;
    use serde::de::Error as _;
    use serde::{Deserializer, Serializer};

    pub fn serialize<S: Serializer>(p: &CanonicalPath, s: S) -> Result<S::Ok, S::Error> {
        s.collect_str(p)
    }

    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<CanonicalPath, D::Error> {
        use serde::Deserialize as _;
        let s = String::deserialize(d)?;
        CanonicalPath::parse_workspace_absolute(&s).map_err(D::Error::custom)
    }
}

#[cfg(test)]
mod tests {
    use haz_domain::path::CanonicalPath;

    use crate::CacheKey;
    use crate::manifest::{HashFunctionLabel, Manifest, OutputBlob};

    fn sample_key() -> CacheKey {
        let mut bytes = [0u8; 32];
        bytes[0] = 0xAB;
        bytes[1] = 0xCD;
        CacheKey::from_bytes(bytes)
    }

    fn cp(s: &str) -> CanonicalPath {
        CanonicalPath::parse_workspace_absolute(s)
            .expect("test helper expects a valid workspace-absolute path")
    }

    fn sample_manifest() -> Manifest {
        Manifest {
            chapter_revision: 0,
            hash_function: HashFunctionLabel::Blake3,
            key: sample_key(),
            outputs: vec![OutputBlob {
                workspace_absolute_path: cp("/lib_core/target/debug/lib_core"),
                content_hash: [0x11; 32],
                size: 1024,
                mode: 0o755,
            }],
            stdout_len: 42,
            stderr_len: 0,
            stdout_hash: [0x22; 32],
            stderr_hash: [0x33; 32],
            exit_status: 0,
            created_at_unix: 1_715_718_000,
        }
    }

    // ----- Serialisation round-trip -----

    #[test]
    fn cache_011_round_trip_preserves_every_field() {
        let original = sample_manifest();
        let bytes = original.to_json_bytes();
        let parsed = Manifest::from_json(&bytes).unwrap();
        assert_eq!(parsed, original);
    }

    #[test]
    fn cache_011_round_trip_with_empty_outputs() {
        let mut m = sample_manifest();
        m.outputs.clear();
        let bytes = m.to_json_bytes();
        let parsed = Manifest::from_json(&bytes).unwrap();
        assert_eq!(parsed.outputs.len(), 0);
        assert_eq!(parsed, m);
    }

    #[test]
    fn cache_011_round_trip_with_multiple_outputs() {
        let mut m = sample_manifest();
        m.outputs.push(OutputBlob {
            workspace_absolute_path: cp("/lib_core/target/debug/lib_core.d"),
            content_hash: [0x44; 32],
            size: 7,
            mode: 0o644,
        });
        m.outputs.push(OutputBlob {
            workspace_absolute_path: cp("/lib_core/another"),
            content_hash: [0x55; 32],
            size: 0,
            mode: 0o600,
        });
        let bytes = m.to_json_bytes();
        let parsed = Manifest::from_json(&bytes).unwrap();
        assert_eq!(parsed, m);
        assert_eq!(parsed.outputs.len(), 3);
    }

    #[test]
    fn cache_011_to_json_bytes_ends_with_newline() {
        let m = sample_manifest();
        let bytes = m.to_json_bytes();
        assert_eq!(*bytes.last().unwrap(), b'\n');
    }

    // ----- JSON shape -----

    #[test]
    fn cache_011_hash_function_serialises_as_lowercase_string() {
        let m = sample_manifest();
        let json = String::from_utf8(m.to_json_bytes()).unwrap();
        assert!(json.contains("\"hash_function\": \"blake3\""));
    }

    #[test]
    fn cache_011_hash_function_sha256_serialises_correctly() {
        let mut m = sample_manifest();
        m.hash_function = HashFunctionLabel::Sha256;
        let json = String::from_utf8(m.to_json_bytes()).unwrap();
        assert!(json.contains("\"hash_function\": \"sha256\""));
    }

    #[test]
    fn cache_011_key_serialises_as_hex_string() {
        let m = sample_manifest();
        let json = String::from_utf8(m.to_json_bytes()).unwrap();
        // First two bytes of sample_key() are 0xAB, 0xCD; the rest 0.
        assert!(json.contains("\"key\": \"abcd00"));
    }

    #[test]
    fn cache_011_content_hash_serialises_as_hex_string() {
        let m = sample_manifest();
        let json = String::from_utf8(m.to_json_bytes()).unwrap();
        // sample blob content_hash is 0x11 repeated 32 times.
        assert!(json.contains(&"11".repeat(32)));
    }

    // ----- deny_unknown_fields -----

    #[test]
    fn cache_011_rejects_unknown_top_level_field() {
        let m = sample_manifest();
        let mut value: serde_json::Value = serde_json::from_slice(&m.to_json_bytes()).unwrap();
        value
            .as_object_mut()
            .unwrap()
            .insert("future_field".into(), serde_json::json!("surprise"));
        let bytes = serde_json::to_vec(&value).unwrap();
        let err = Manifest::from_json(&bytes).unwrap_err();
        let msg = format!("{err}");
        assert!(
            msg.contains("future_field") || msg.contains("unknown"),
            "expected unknown-field error, got: {msg}"
        );
    }

    #[test]
    fn cache_011_rejects_unknown_field_in_output_blob() {
        let m = sample_manifest();
        let mut value: serde_json::Value = serde_json::from_slice(&m.to_json_bytes()).unwrap();
        value["outputs"][0]
            .as_object_mut()
            .unwrap()
            .insert("future_field".into(), serde_json::json!(0));
        let bytes = serde_json::to_vec(&value).unwrap();
        let err = Manifest::from_json(&bytes).unwrap_err();
        let msg = format!("{err}");
        assert!(
            msg.contains("future_field") || msg.contains("unknown"),
            "expected unknown-field error, got: {msg}"
        );
    }

    #[test]
    fn cache_011_rejects_missing_required_field() {
        let m = sample_manifest();
        let mut value: serde_json::Value = serde_json::from_slice(&m.to_json_bytes()).unwrap();
        value.as_object_mut().unwrap().remove("hash_function");
        let bytes = serde_json::to_vec(&value).unwrap();
        let err = Manifest::from_json(&bytes).unwrap_err();
        let msg = format!("{err}");
        assert!(
            msg.contains("hash_function") || msg.contains("missing"),
            "expected missing-field error, got: {msg}"
        );
    }

    // ----- hex parsing failures surface as a parse error -----

    #[test]
    fn rejects_short_hex_in_key() {
        let m = sample_manifest();
        let mut value: serde_json::Value = serde_json::from_slice(&m.to_json_bytes()).unwrap();
        value["key"] = serde_json::json!("ab");
        let bytes = serde_json::to_vec(&value).unwrap();
        let err = Manifest::from_json(&bytes).unwrap_err();
        let _ = format!("{err}");
    }

    #[test]
    fn rejects_non_hex_character_in_content_hash() {
        let m = sample_manifest();
        let mut value: serde_json::Value = serde_json::from_slice(&m.to_json_bytes()).unwrap();
        let mut bad = "1".repeat(64);
        bad.replace_range(30..31, "z");
        value["outputs"][0]["content_hash"] = serde_json::json!(bad);
        let bytes = serde_json::to_vec(&value).unwrap();
        let err = Manifest::from_json(&bytes).unwrap_err();
        let _ = format!("{err}");
    }

    #[test]
    fn rejects_unknown_hash_function_label() {
        let m = sample_manifest();
        let mut value: serde_json::Value = serde_json::from_slice(&m.to_json_bytes()).unwrap();
        value["hash_function"] = serde_json::json!("blake2b");
        let bytes = serde_json::to_vec(&value).unwrap();
        let err = Manifest::from_json(&bytes).unwrap_err();
        let _ = format!("{err}");
    }

    // ----- workspace_absolute_path is validated at deserialise -----

    #[test]
    fn rejects_workspace_absolute_path_with_parent_dir_segment() {
        let m = sample_manifest();
        let mut value: serde_json::Value = serde_json::from_slice(&m.to_json_bytes()).unwrap();
        value["outputs"][0]["workspace_absolute_path"] = serde_json::json!("/foo/../etc/passwd");
        let bytes = serde_json::to_vec(&value).unwrap();
        let err = Manifest::from_json(&bytes).unwrap_err();
        let msg = format!("{err}");
        assert!(
            msg.contains("..") || msg.contains("invalid"),
            "expected traversal rejection, got: {msg}"
        );
    }

    #[test]
    fn rejects_workspace_absolute_path_with_dot_segment() {
        let m = sample_manifest();
        let mut value: serde_json::Value = serde_json::from_slice(&m.to_json_bytes()).unwrap();
        value["outputs"][0]["workspace_absolute_path"] = serde_json::json!("/foo/./bar");
        let bytes = serde_json::to_vec(&value).unwrap();
        Manifest::from_json(&bytes).unwrap_err();
    }

    #[test]
    fn rejects_workspace_absolute_path_that_is_project_relative() {
        let m = sample_manifest();
        let mut value: serde_json::Value = serde_json::from_slice(&m.to_json_bytes()).unwrap();
        value["outputs"][0]["workspace_absolute_path"] = serde_json::json!("foo/bar");
        let bytes = serde_json::to_vec(&value).unwrap();
        Manifest::from_json(&bytes).unwrap_err();
    }

    #[test]
    fn rejects_workspace_absolute_path_bare_root() {
        let m = sample_manifest();
        let mut value: serde_json::Value = serde_json::from_slice(&m.to_json_bytes()).unwrap();
        value["outputs"][0]["workspace_absolute_path"] = serde_json::json!("/");
        let bytes = serde_json::to_vec(&value).unwrap();
        Manifest::from_json(&bytes).unwrap_err();
    }

    #[test]
    fn rejects_workspace_absolute_path_with_bidi_control_codepoint() {
        // U+202E (RIGHT-TO-LEFT OVERRIDE) is in PATH-002's
        // forbidden Format category; PathSegment rejects it and
        // the manifest deserialiser surfaces that as a parse error.
        let m = sample_manifest();
        let mut value: serde_json::Value = serde_json::from_slice(&m.to_json_bytes()).unwrap();
        value["outputs"][0]["workspace_absolute_path"] = serde_json::json!("/foo/bar\u{202E}baz");
        let bytes = serde_json::to_vec(&value).unwrap();
        Manifest::from_json(&bytes).unwrap_err();
    }

    #[test]
    fn workspace_absolute_path_serialises_as_plain_string() {
        let m = sample_manifest();
        let json = String::from_utf8(m.to_json_bytes()).unwrap();
        assert!(
            json.contains("\"workspace_absolute_path\": \"/lib_core/target/debug/lib_core\""),
            "expected JSON to carry the rendered path string, got: {json}"
        );
    }

    // ----- HashFunctionLabel <-> HashAlgo round trip -----

    #[test]
    fn hash_function_label_round_trips_through_domain_algo() {
        use haz_domain::settings::cache::HashAlgo;
        for algo in [HashAlgo::Blake3, HashAlgo::Sha256] {
            let label: HashFunctionLabel = algo.into();
            let back: HashAlgo = label.into();
            assert_eq!(algo, back);
        }
    }

    // ----- current_chapter_revision_matches -----

    #[test]
    fn cache_003_current_chapter_revision_matches_initial_value() {
        let m = sample_manifest();
        assert!(m.current_chapter_revision_matches());
    }

    #[test]
    fn cache_003_current_chapter_revision_does_not_match_future_value() {
        let mut m = sample_manifest();
        m.chapter_revision = m.chapter_revision.saturating_add(1);
        assert!(!m.current_chapter_revision_matches());
    }
}
