//! Cache key derivation.
//!
//! A cache hit only makes sense when the inputs that produced the
//! cached result have not changed. The key captures those inputs in a
//! canonical form and hashes them to a stable SHA-256. Two runs that
//! pass identical inputs always derive the same key; flipping any one
//! input flips the key.

use serde::Serialize;
use sha2::{Digest, Sha256};

/// Canonical inputs that participate in the cache key.
///
/// Hashing this struct produces a stable SHA-256 hex string that
/// identifies one (test, server, runner) tuple. Borrowed fields keep
/// the call site allocation-free for the hot path. We only need
/// `Serialize` here; the hash is one-way and nothing deserializes the
/// inputs from bytes.
#[derive(Debug, Clone, Serialize)]
pub struct CacheKeyInputs<'a> {
    /// Canonical server spec hash. The caller is responsible for
    /// producing a stable identifier (the server URL, the stdio
    /// command line, etc).
    pub server_id: &'a str,
    /// Tool name being invoked.
    pub tool_name: &'a str,
    /// Tool arguments after normalization (sorted keys, removed
    /// defaults). The caller normalizes; the keying step just hashes.
    pub normalized_args: &'a serde_json::Value,
    /// Optional `server_version:` pin from the YAML. When present,
    /// version bumps invalidate the cache automatically.
    pub server_version_pin: Option<&'a str>,
    /// Runner version string. Bumping the runner invalidates every
    /// entry so behavioral changes never silently serve stale results.
    pub runner_version: &'a str,
    /// Stable signature of the matchers in play (sorted names plus
    /// their hashed params). Re-tuning a matcher invalidates the key
    /// for that test without touching others.
    pub matcher_signature: &'a str,
}

/// Derive the cache key for a set of inputs.
///
/// Serializes `inputs` to canonical JSON (sorted keys, no whitespace)
/// and returns the SHA-256 hex string. Two calls with equal inputs
/// always return the same string.
pub fn derive(inputs: &CacheKeyInputs<'_>) -> String {
    let canonical = canonical_json(inputs);
    let mut hasher = Sha256::new();
    hasher.update(canonical.as_bytes());
    let digest = hasher.finalize();
    hex_encode(&digest)
}

/// Render `inputs` as canonical JSON: sorted keys, no whitespace.
///
/// We rebuild the JSON by hand rather than rely on serde's default
/// emission so the field order is fully deterministic, even if the
/// struct definition is reordered in the future.
fn canonical_json(inputs: &CacheKeyInputs<'_>) -> String {
    // `serde_json::to_value` plus a sorted walk gives us key-ordered
    // output without pulling in a canonical-JSON crate.
    let mut value = serde_json::Map::new();
    value.insert(
        "matcher_signature".to_string(),
        serde_json::Value::String(inputs.matcher_signature.to_string()),
    );
    value.insert(
        "normalized_args".to_string(),
        sort_json(inputs.normalized_args.clone()),
    );
    value.insert(
        "runner_version".to_string(),
        serde_json::Value::String(inputs.runner_version.to_string()),
    );
    value.insert(
        "server_id".to_string(),
        serde_json::Value::String(inputs.server_id.to_string()),
    );
    value.insert(
        "server_version_pin".to_string(),
        match inputs.server_version_pin {
            Some(v) => serde_json::Value::String(v.to_string()),
            None => serde_json::Value::Null,
        },
    );
    value.insert(
        "tool_name".to_string(),
        serde_json::Value::String(inputs.tool_name.to_string()),
    );
    serde_json::Value::Object(value).to_string()
}

/// Recursively sort the keys of any JSON object so the textual form is
/// stable regardless of authoring order.
fn sort_json(value: serde_json::Value) -> serde_json::Value {
    match value {
        serde_json::Value::Object(map) => {
            let mut entries: Vec<(String, serde_json::Value)> = map.into_iter().collect();
            entries.sort_by(|a, b| a.0.cmp(&b.0));
            let mut sorted = serde_json::Map::new();
            for (k, v) in entries {
                sorted.insert(k, sort_json(v));
            }
            serde_json::Value::Object(sorted)
        }
        serde_json::Value::Array(items) => {
            serde_json::Value::Array(items.into_iter().map(sort_json).collect())
        }
        other => other,
    }
}

/// Lowercase hex encode a 32-byte digest into a 64-char string.
fn hex_encode(bytes: &[u8]) -> String {
    const HEX: &[u8; 16] = b"0123456789abcdef";
    let mut out = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        out.push(HEX[(b >> 4) as usize] as char);
        out.push(HEX[(b & 0x0f) as usize] as char);
    }
    out
}

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

    fn base_inputs<'a>(args: &'a serde_json::Value) -> CacheKeyInputs<'a> {
        CacheKeyInputs {
            server_id: "svr-1",
            tool_name: "search",
            normalized_args: args,
            server_version_pin: Some("1.2.3"),
            runner_version: "0.1.0",
            matcher_signature: "exact:abc",
        }
    }

    #[test]
    fn derive_is_stable_across_calls() {
        let args = json!({ "q": "hello" });
        let inputs = base_inputs(&args);
        let a = derive(&inputs);
        let b = derive(&inputs);
        assert_eq!(a, b);
        assert_eq!(a.len(), 64);
    }

    #[test]
    fn derive_differs_on_tool_name_change() {
        let args = json!({ "q": "hello" });
        let inputs_a = base_inputs(&args);
        let mut inputs_b = inputs_a.clone();
        inputs_b.tool_name = "fetch";
        assert_ne!(derive(&inputs_a), derive(&inputs_b));
    }

    #[test]
    fn derive_differs_on_args_change() {
        let args_a = json!({ "q": "hello" });
        let args_b = json!({ "q": "world" });
        assert_ne!(derive(&base_inputs(&args_a)), derive(&base_inputs(&args_b)));
    }

    #[test]
    fn derive_ignores_field_order_in_normalized_args() {
        let args_a = json!({ "a": 1, "b": 2, "c": 3 });
        let args_b = json!({ "c": 3, "b": 2, "a": 1 });
        assert_eq!(derive(&base_inputs(&args_a)), derive(&base_inputs(&args_b)));
    }

    #[test]
    fn derive_changes_on_runner_version_bump() {
        let args = json!({ "q": "hello" });
        let inputs_a = base_inputs(&args);
        let mut inputs_b = inputs_a.clone();
        inputs_b.runner_version = "0.2.0";
        assert_ne!(derive(&inputs_a), derive(&inputs_b));
    }

    #[test]
    fn derive_differs_on_server_version_pin_change() {
        let args = json!({});
        let inputs_a = base_inputs(&args);
        let mut inputs_b = inputs_a.clone();
        inputs_b.server_version_pin = Some("9.9.9");
        assert_ne!(derive(&inputs_a), derive(&inputs_b));
    }

    #[test]
    fn derive_handles_nested_args_consistently() {
        let args_a = json!({ "outer": { "x": 1, "y": 2 } });
        let args_b = json!({ "outer": { "y": 2, "x": 1 } });
        assert_eq!(derive(&base_inputs(&args_a)), derive(&base_inputs(&args_b)));
    }
}
