use std::collections::BTreeSet;

use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonDiffEntry {
    pub path: String,
    pub left: Value,
    pub right: Value,
}

pub fn diff_json_values(left: &Value, right: &Value) -> Vec<JsonDiffEntry> {
    let mut differences = Vec::new();
    collect_json_differences("$", left, right, &mut differences);
    differences
}

fn collect_json_differences(
    path: &str,
    left: &Value,
    right: &Value,
    differences: &mut Vec<JsonDiffEntry>,
) {
    match (left, right) {
        (Value::Object(left_map), Value::Object(right_map)) => {
            let mut keys = BTreeSet::new();
            keys.extend(left_map.keys().cloned());
            keys.extend(right_map.keys().cloned());

            for key in keys {
                let next_path = format!("{path}.{key}");
                match (left_map.get(&key), right_map.get(&key)) {
                    (Some(left_value), Some(right_value)) => {
                        collect_json_differences(&next_path, left_value, right_value, differences);
                    }
                    (left_value, right_value) => differences.push(JsonDiffEntry {
                        path: next_path,
                        left: left_value.cloned().unwrap_or(Value::Null),
                        right: right_value.cloned().unwrap_or(Value::Null),
                    }),
                }
            }
        }
        (Value::Array(left_items), Value::Array(right_items)) => {
            let max_len = left_items.len().max(right_items.len());
            for index in 0..max_len {
                let next_path = format!("{path}[{index}]");
                match (left_items.get(index), right_items.get(index)) {
                    (Some(left_value), Some(right_value)) => {
                        collect_json_differences(&next_path, left_value, right_value, differences);
                    }
                    (left_value, right_value) => differences.push(JsonDiffEntry {
                        path: next_path,
                        left: left_value.cloned().unwrap_or(Value::Null),
                        right: right_value.cloned().unwrap_or(Value::Null),
                    }),
                }
            }
        }
        _ if left != right => differences.push(JsonDiffEntry {
            path: path.to_string(),
            left: left.clone(),
            right: right.clone(),
        }),
        _ => {}
    }
}

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

    #[test]
    fn diffs_nested_json_values() {
        let left = serde_json::json!({ "a": { "b": 1 } });
        let right = serde_json::json!({ "a": { "b": 2 } });
        let diff = diff_json_values(&left, &right);
        assert_eq!(diff.len(), 1);
        assert_eq!(diff[0].path, "$.a.b");
    }
}
