use sha2::{Digest, Sha256};

/// Fields stripped from the strategy JSON before hashing.
///
/// These are execution/sizing parameters that are independent of the signal logic:
/// - `candle_interval` — same rules at different timeframes are the same strategy
/// - `order_quantity` — position size is not part of the signal
/// - `quantity` — `then.quantity` inside RuleBased rule actions
/// - `interval_secs` — throttle timing for compiled strategies
/// - `max_position_quantity` — position limit
const STRIP_KEYS: &[&str] = &[
    "candle_interval",
    "order_quantity",
    "quantity",
    "interval_secs",
    "max_position_quantity",
];

/// Returns a normalized copy of the strategy JSON suitable for hashing.
///
/// Normalization:
/// 1. Strips execution/sizing keys (see [`STRIP_KEYS`]) from all objects recursively.
/// 2. Key order is stable because `serde_json` uses `BTreeMap` (sorted) by default.
pub fn normalize_strategy(strategy: &serde_json::Value) -> serde_json::Value {
    strip_keys(strategy)
}

fn strip_keys(value: &serde_json::Value) -> serde_json::Value {
    match value {
        serde_json::Value::Object(map) => {
            let normalized: serde_json::Map<String, serde_json::Value> = map
                .iter()
                .filter(|(k, _)| !STRIP_KEYS.contains(&k.as_str()))
                .map(|(k, v)| (k.clone(), strip_keys(v)))
                .collect();
            serde_json::Value::Object(normalized)
        }
        serde_json::Value::Array(arr) => {
            serde_json::Value::Array(arr.iter().map(strip_keys).collect())
        }
        other => other.clone(),
    }
}

/// Computes the SHA-256 hex digest of the normalized strategy JSON.
///
/// The input must already be normalized via [`normalize_strategy`].
/// The digest is over the compact (no extra whitespace) JSON serialization,
/// which is deterministic because `serde_json` serializes `Map` keys in sorted order.
pub fn compute_strategy_hash(normalized: &serde_json::Value) -> String {
    let compact = serde_json::to_string(normalized)
        .expect("serde_json::Value is always serializable");
    let digest = Sha256::digest(compact.as_bytes());
    format!("{digest:x}")
}

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

    #[test]
    fn strips_candle_interval() {
        let strategy = json!({
            "type": "rule_based",
            "candle_interval": "1h",
            "rules": []
        });
        let normalized = normalize_strategy(&strategy);
        assert!(normalized.get("candle_interval").is_none());
        assert_eq!(normalized["type"], "rule_based");
    }

    #[test]
    fn strips_order_quantity_from_compiled_strategy() {
        let strategy = json!({
            "type": "ema_crossover",
            "fast_period": 12,
            "slow_period": 26,
            "order_quantity": "0.001",
            "interval_secs": 5
        });
        let normalized = normalize_strategy(&strategy);
        assert!(normalized.get("order_quantity").is_none());
        assert!(normalized.get("interval_secs").is_none());
        assert_eq!(normalized["fast_period"], 12);
        assert_eq!(normalized["slow_period"], 26);
    }

    #[test]
    fn strips_quantity_from_rule_actions() {
        let strategy = json!({
            "type": "rule_based",
            "candle_interval": "5m",
            "rules": [{
                "when": { "kind": "position", "state": "flat" },
                "then": { "side": "buy", "quantity": "0.001" }
            }]
        });
        let normalized = normalize_strategy(&strategy);
        let then = &normalized["rules"][0]["then"];
        assert!(then.get("quantity").is_none());
        assert_eq!(then["side"], "buy");
    }

    #[test]
    fn same_logic_different_sizing_same_hash() {
        let a = json!({
            "type": "ema_crossover",
            "fast_period": 12,
            "slow_period": 26,
            "order_quantity": "0.001",
            "interval_secs": 5
        });
        let b = json!({
            "type": "ema_crossover",
            "fast_period": 12,
            "slow_period": 26,
            "order_quantity": "1.0",
            "interval_secs": 30
        });
        assert_eq!(
            compute_strategy_hash(&normalize_strategy(&a)),
            compute_strategy_hash(&normalize_strategy(&b))
        );
    }

    #[test]
    fn same_logic_different_candle_interval_same_hash() {
        let a = json!({
            "type": "rule_based",
            "candle_interval": "1h",
            "rules": [{ "when": { "kind": "position", "state": "flat" }, "then": { "side": "buy", "quantity": "0.001" } }]
        });
        let b = json!({
            "type": "rule_based",
            "candle_interval": "4h",
            "rules": [{ "when": { "kind": "position", "state": "flat" }, "then": { "side": "buy", "quantity": "0.001" } }]
        });
        assert_eq!(
            compute_strategy_hash(&normalize_strategy(&a)),
            compute_strategy_hash(&normalize_strategy(&b))
        );
    }

    #[test]
    fn different_logic_different_hash() {
        let a = json!({ "type": "ema_crossover", "fast_period": 12, "slow_period": 26 });
        let b = json!({ "type": "ema_crossover", "fast_period": 9, "slow_period": 21 });
        assert_ne!(
            compute_strategy_hash(&normalize_strategy(&a)),
            compute_strategy_hash(&normalize_strategy(&b))
        );
    }

    #[test]
    fn hash_is_deterministic() {
        let strategy = json!({
            "type": "rsi_reversal",
            "rsi_period": 14,
            "overbought": "70",
            "oversold": "30",
            "order_quantity": "0.01"
        });
        let n = normalize_strategy(&strategy);
        assert_eq!(compute_strategy_hash(&n), compute_strategy_hash(&n));
    }

    #[test]
    fn key_order_independent_hash() {
        // serde_json without preserve_order uses BTreeMap — key order is always sorted.
        // Verify that two JSON objects with the same keys in different submission order
        // produce the same hash (they round-trip through BTreeMap in serde_json::Value).
        let a: serde_json::Value = serde_json::from_str(
            r#"{"type":"ema_crossover","fast_period":12,"slow_period":26}"#,
        ).unwrap();
        let b: serde_json::Value = serde_json::from_str(
            r#"{"slow_period":26,"type":"ema_crossover","fast_period":12}"#,
        ).unwrap();
        assert_eq!(
            compute_strategy_hash(&normalize_strategy(&a)),
            compute_strategy_hash(&normalize_strategy(&b))
        );
    }
}
