//! Convert DynamoDB JSON values back to regular JSON.
//!
//! Supports all standard DynamoDB type descriptors:
//! `S`, `N`, `BOOL`, `NULL`, `L`, `M`, `SS`, `NS`, `BS`, `B`.

use rayon::prelude::*;
use serde_json::{Map, Number, Value};

use crate::error::{DynoError, Result};

/// Unmarshall a single DynamoDB-typed JSON value to a plain JSON value.
///
/// Expects the input to be a single-key object whose key is a DynamoDB type
/// descriptor (e.g. `{"S": "hello"}`).
///
/// # Errors
///
/// Returns [`DynoError::UnknownType`] for unrecognised descriptors, or
/// [`DynoError::InvalidNumber`] when an `N` value cannot be parsed.
pub fn unmarshall_value(value: &Value) -> Result<Value> {
    let obj = match value.as_object() {
        Some(o) if o.len() == 1 => o,
        // If it's not a single-key object, return it unchanged — this handles
        // raw scalars or multi-key objects that aren't DynamoDB-typed.
        _ => return Ok(value.clone()),
    };

    let (key, inner) = obj.iter().next().expect("checked len == 1");

    match key.as_str() {
        "S" => Ok(inner.clone()),
        "N" => parse_number(inner),
        "BOOL" => Ok(inner.clone()),
        "NULL" => Ok(Value::Null),
        "L" => {
            let empty = Vec::new();
            let arr = inner.as_array().unwrap_or(&empty);
            let items: Result<Vec<Value>> = arr.iter().map(unmarshall_value).collect();
            Ok(Value::Array(items?))
        }
        "M" => unmarshall_map(inner),
        "SS" => Ok(inner.clone()),
        "NS" => {
            let empty = Vec::new();
            let arr = inner.as_array().unwrap_or(&empty);
            let nums: Result<Vec<Value>> = arr.iter().map(parse_number).collect();
            Ok(Value::Array(nums?))
        }
        "BS" => Ok(inner.clone()),
        "B" => Ok(inner.clone()),
        _ => Err(DynoError::UnknownType {
            key: key.to_owned(),
        }),
    }
}

/// Unmarshall a DynamoDB `M` (map) value to a plain JSON object.
fn unmarshall_map(value: &Value) -> Result<Value> {
    let obj = match value.as_object() {
        Some(o) => o,
        None => return Ok(value.clone()),
    };
    let mut map = Map::new();
    for (k, v) in obj {
        map.insert(k.clone(), unmarshall_value(v)?);
    }
    Ok(Value::Object(map))
}

/// Unmarshall a top-level DynamoDB JSON value.
///
/// - If the input is an **object**, each value is unmarshalled (representing a
///   single DynamoDB Item).
/// - If the input is an **array**, elements are inspected: when every element
///   is a single-key DynamoDB type descriptor (e.g. `{"L": …}`, `{"M": …}`),
///   each is unmarshalled as a typed value. Otherwise each element is treated
///   as a DynamoDB Item.
pub fn unmarshall_top_level(value: &Value) -> Result<Value> {
    match value {
        Value::Object(obj) => {
            let pairs: Result<Vec<(String, Value)>> = obj
                .into_iter()
                .collect::<Vec<_>>()
                .par_iter()
                .map(|(k, v)| Ok((k.to_string(), unmarshall_value(v)?)))
                .collect();
            Ok(Value::Object(pairs?.into_iter().collect()))
        }
        Value::Array(arr) => unmarshall_array(arr),
        other => unmarshall_value(other),
    }
}

/// Known DynamoDB type descriptor keys.
const DDB_TYPE_KEYS: &[&str] = &["S", "N", "BOOL", "NULL", "L", "M", "SS", "NS", "BS", "B"];

/// Check whether a value looks like a single-key DynamoDB typed descriptor.
fn is_ddb_typed_value(value: &Value) -> bool {
    value
        .as_object()
        .filter(|obj| obj.len() == 1)
        .and_then(|obj| obj.keys().next())
        .is_some_and(|key| DDB_TYPE_KEYS.contains(&key.as_str()))
}

/// Unmarshall an array. If every element is a single-key DynamoDB type
/// descriptor (e.g. `{"L": …}`), treat them as typed values. Otherwise treat
/// each element as a DynamoDB Item (top-level record).
fn unmarshall_array(arr: &[Value]) -> Result<Value> {
    if arr.iter().all(is_ddb_typed_value) {
        // Each element is a DDB typed value — unmarshall individually.
        let items: Result<Vec<Value>> = arr.par_iter().map(unmarshall_value).collect();
        return Ok(Value::Array(items?));
    }

    // Treat each element as a DynamoDB Item.
    let items: Result<Vec<Value>> = arr.par_iter().map(unmarshall_top_level).collect();
    Ok(Value::Array(items?))
}

/// Parse a DynamoDB `N` (number) value into a JSON number.
fn parse_number(value: &Value) -> Result<Value> {
    let s = match value {
        Value::String(s) => s.as_str(),
        Value::Number(n) => return Ok(Value::Number(n.clone())),
        _ => {
            return Err(DynoError::InvalidNumber {
                value: value.to_string(),
            })
        }
    };

    // Try integer first, then float.
    if let Ok(i) = s.parse::<i64>() {
        return Ok(Value::Number(Number::from(i)));
    }
    if let Ok(f) = s.parse::<f64>() {
        if let Some(n) = Number::from_f64(f) {
            return Ok(Value::Number(n));
        }
    }
    Err(DynoError::InvalidNumber {
        value: s.to_owned(),
    })
}

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

    #[test]
    fn unmarshall_string() {
        let input = json!({"S": "hello"});
        assert_eq!(unmarshall_value(&input).unwrap(), json!("hello"));
    }

    #[test]
    fn unmarshall_number_integer() {
        let input = json!({"N": "42"});
        assert_eq!(unmarshall_value(&input).unwrap(), json!(42));
    }

    #[test]
    fn unmarshall_number_float() {
        let input = json!({"N": "3.14"});
        assert_eq!(unmarshall_value(&input).unwrap(), json!(3.14));
    }

    #[test]
    fn unmarshall_bool() {
        assert_eq!(
            unmarshall_value(&json!({"BOOL": true})).unwrap(),
            json!(true)
        );
        assert_eq!(
            unmarshall_value(&json!({"BOOL": false})).unwrap(),
            json!(false)
        );
    }

    #[test]
    fn unmarshall_null() {
        assert_eq!(
            unmarshall_value(&json!({"NULL": true})).unwrap(),
            json!(null)
        );
    }

    #[test]
    fn unmarshall_list() {
        let input = json!({"L": [{"S": "a"}, {"S": "b"}, {"S": "c"}]});
        assert_eq!(unmarshall_value(&input).unwrap(), json!(["a", "b", "c"]));
    }

    #[test]
    fn unmarshall_map() {
        let input = json!({"M": {"key1": {"S": "value1"}, "key2": {"S": "value2"}}});
        let expected = json!({"key1": "value1", "key2": "value2"});
        assert_eq!(unmarshall_value(&input).unwrap(), expected);
    }

    #[test]
    fn unmarshall_string_set() {
        let input = json!({"SS": ["a", "b", "c"]});
        assert_eq!(unmarshall_value(&input).unwrap(), json!(["a", "b", "c"]));
    }

    #[test]
    fn unmarshall_number_set() {
        let input = json!({"NS": ["1", "2", "3"]});
        assert_eq!(unmarshall_value(&input).unwrap(), json!([1, 2, 3]));
    }

    #[test]
    fn unmarshall_top_level_object() {
        let input = json!({
            "object": {"M": {
                "array": {"L": [{"S": "a"}, {"S": "b"}, {"S": "c"}]},
                "subobject": {"M": {"key1": {"S": "value1"}, "key2": {"S": "value2"}}}
            }},
            "number": {"N": "1"},
            "string": {"S": "string"},
            "boolean": {"BOOL": true},
            "null": {"NULL": true}
        });
        let expected = json!({
            "object": {
                "array": ["a", "b", "c"],
                "subobject": {"key1": "value1", "key2": "value2"}
            },
            "number": 1,
            "string": "string",
            "boolean": true,
            "null": null
        });
        assert_eq!(unmarshall_top_level(&input).unwrap(), expected);
    }

    #[test]
    fn unmarshall_top_level_array_of_items() {
        let input = json!([
            {"type": {"S": "fruit"}, "foodItems": {"L": [{"S": "apple"}, {"S": "kiwi"}]}},
            {"type": {"S": "meat"}, "foodItems": {"L": [{"S": "beef"}, {"S": "bacon"}]}}
        ]);
        let expected = json!([
            {"type": "fruit", "foodItems": ["apple", "kiwi"]},
            {"type": "meat", "foodItems": ["beef", "bacon"]}
        ]);
        assert_eq!(unmarshall_top_level(&input).unwrap(), expected);
    }

    #[test]
    fn unmarshall_array_of_ddb_list_values() {
        let input = json!([
            {"M": {"object": {"M": {"key1": {"S": "value1"}, "key2": {"S": "value2"}}}}},
            {"M": {"object": {"M": {"key3": {"S": "value3"}, "key4": {"S": "value4"}}}}}
        ]);
        let expected = json!([
            {"object": {"key1": "value1", "key2": "value2"}},
            {"object": {"key3": "value3", "key4": "value4"}}
        ]);
        assert_eq!(unmarshall_top_level(&input).unwrap(), expected);
    }

    #[test]
    fn unmarshall_array_of_arrays() {
        let input = json!([
            {"L": [{"M": {"foo": {"S": "bar"}}}, {"M": {"baz": {"S": "qux"}}}]},
            {"L": [{"M": {"foo": {"S": "bar"}}}, {"M": {"baz": {"S": "qux"}}}]}
        ]);
        let expected = json!([
            [{"foo": "bar"}, {"baz": "qux"}],
            [{"foo": "bar"}, {"baz": "qux"}]
        ]);
        assert_eq!(unmarshall_top_level(&input).unwrap(), expected);
    }

    #[test]
    fn unknown_type_descriptor() {
        let input = json!({"X": "bad"});
        let err = unmarshall_value(&input).unwrap_err();
        assert!(err.to_string().contains("Unknown DynamoDB type descriptor"));
    }

    // ── Binary types ───────────────────────────────────────────────────

    #[test]
    fn unmarshall_binary() {
        // B values are base64-encoded strings — passed through as-is.
        let input = json!({"B": "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk"});
        assert_eq!(
            unmarshall_value(&input).unwrap(),
            json!("dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk")
        );
    }

    #[test]
    fn unmarshall_binary_set() {
        // BS values are arrays of base64-encoded strings.
        let input = json!({"BS": ["U3Vubnk=", "UmFpbnk=", "U25vd3k="]});
        assert_eq!(
            unmarshall_value(&input).unwrap(),
            json!(["U3Vubnk=", "UmFpbnk=", "U25vd3k="])
        );
    }

    // ── Empty values ───────────────────────────────────────────────────

    #[test]
    fn unmarshall_empty_string() {
        let input = json!({"S": ""});
        assert_eq!(unmarshall_value(&input).unwrap(), json!(""));
    }

    #[test]
    fn unmarshall_empty_list() {
        let input = json!({"L": []});
        assert_eq!(unmarshall_value(&input).unwrap(), json!([]));
    }

    #[test]
    fn unmarshall_empty_map() {
        let input = json!({"M": {}});
        assert_eq!(unmarshall_value(&input).unwrap(), json!({}));
    }

    // ── Number edge cases ──────────────────────────────────────────────

    #[test]
    fn unmarshall_negative_number() {
        let input = json!({"N": "-42"});
        assert_eq!(unmarshall_value(&input).unwrap(), json!(-42));
    }

    #[test]
    fn unmarshall_zero() {
        let input = json!({"N": "0"});
        assert_eq!(unmarshall_value(&input).unwrap(), json!(0));
    }

    #[test]
    fn unmarshall_negative_float() {
        let input = json!({"N": "-3.14"});
        assert_eq!(unmarshall_value(&input).unwrap(), json!(-3.14));
    }

    #[test]
    fn unmarshall_scientific_notation() {
        let input = json!({"N": "1.5E4"});
        assert_eq!(unmarshall_value(&input).unwrap(), json!(15000.0));
    }

    #[test]
    fn unmarshall_very_small_number() {
        // DynamoDB supports numbers as small as 1E-130.
        let input = json!({"N": "1E-130"});
        let result = unmarshall_value(&input).unwrap();
        assert!(result.as_f64().unwrap() > 0.0);
    }

    #[test]
    fn unmarshall_number_set_with_floats() {
        // Example from AWS docs: [42.2, -19, 7.5, 3.14]
        let input = json!({"NS": ["42.2", "-19", "7.5", "3.14"]});
        assert_eq!(
            unmarshall_value(&input).unwrap(),
            json!([42.2, -19, 7.5, 3.14])
        );
    }

    // ── Mixed / complex structures ─────────────────────────────────────

    #[test]
    fn unmarshall_mixed_type_list() {
        let input = json!({"L": [
            {"S": "hello"},
            {"N": "42"},
            {"BOOL": true},
            {"NULL": true},
            {"M": {"key": {"S": "val"}}},
            {"L": [{"N": "1"}, {"N": "2"}]}
        ]});
        assert_eq!(
            unmarshall_value(&input).unwrap(),
            json!(["hello", 42, true, null, {"key": "val"}, [1, 2]])
        );
    }

    #[test]
    fn unmarshall_deeply_nested() {
        // 4-level nesting: M → M → L → M
        let input = json!({"M": {
            "level1": {"M": {
                "level2": {"L": [
                    {"M": {"level3": {"S": "deep"}}}
                ]}
            }}
        }});
        assert_eq!(
            unmarshall_value(&input).unwrap(),
            json!({"level1": {"level2": [{"level3": "deep"}]}})
        );
    }

    #[test]
    fn unmarshall_all_types_in_one_item() {
        // A single DynamoDB Item containing every supported type.
        let input = json!({
            "str":  {"S": "hello"},
            "num":  {"N": "42"},
            "bin":  {"B": "AQID"},
            "bool": {"BOOL": false},
            "nul":  {"NULL": true},
            "list": {"L": [{"S": "a"}]},
            "map":  {"M": {"k": {"N": "1"}}},
            "ss":   {"SS": ["x", "y"]},
            "ns":   {"NS": ["10", "20"]},
            "bs":   {"BS": ["AQID", "BAUG"]}
        });
        let expected = json!({
            "str":  "hello",
            "num":  42,
            "bin":  "AQID",
            "bool": false,
            "nul":  null,
            "list": ["a"],
            "map":  {"k": 1},
            "ss":   ["x", "y"],
            "ns":   [10, 20],
            "bs":   ["AQID", "BAUG"]
        });
        assert_eq!(unmarshall_top_level(&input).unwrap(), expected);
    }

    // ── AWS CLI response formats ───────────────────────────────────────

    #[test]
    fn unmarshall_get_item_response() {
        // Simulates: aws dynamodb get-item | dynojson u -g "Item" -
        let item = json!({
            "id":   {"S": "user-123"},
            "name": {"S": "Alice"},
            "age":  {"N": "30"},
            "active": {"BOOL": true},
            "tags": {"SS": ["admin", "dev"]},
            "scores": {"NS": ["100", "95.5"]},
            "profile": {"M": {
                "avatar": {"B": "aW1hZ2U="},
                "bio": {"S": "Engineer"}
            }}
        });
        let expected = json!({
            "id":   "user-123",
            "name": "Alice",
            "age":  30,
            "active": true,
            "tags": ["admin", "dev"],
            "scores": [100, 95.5],
            "profile": {
                "avatar": "aW1hZ2U=",
                "bio": "Engineer"
            }
        });
        assert_eq!(unmarshall_top_level(&item).unwrap(), expected);
    }

    #[test]
    fn unmarshall_scan_items() {
        // Array of Items as returned in a DynamoDB Scan "Items" array.
        let items = json!([
            {
                "pk": {"S": "id-1"},
                "data": {"M": {"x": {"N": "1"}}},
                "tags": {"SS": ["a"]}
            },
            {
                "pk": {"S": "id-2"},
                "data": {"M": {"x": {"N": "2"}}},
                "tags": {"SS": ["b", "c"]}
            }
        ]);
        let expected = json!([
            {"pk": "id-1", "data": {"x": 1}, "tags": ["a"]},
            {"pk": "id-2", "data": {"x": 2}, "tags": ["b", "c"]}
        ]);
        assert_eq!(unmarshall_top_level(&items).unwrap(), expected);
    }

    // ── Roundtrip tests ────────────────────────────────────────────────

    #[test]
    fn roundtrip_marshall_then_unmarshall() {
        use crate::marshall::marshall_top_level;
        let original = json!({
            "name": "Alice",
            "age": 30,
            "active": true,
            "address": null,
            "hobbies": ["reading", "coding"],
            "meta": {"joined": "2024-01-01", "level": 5}
        });
        let marshalled = marshall_top_level(&original);
        let result = unmarshall_top_level(&marshalled).unwrap();
        assert_eq!(result, original);
    }

    #[test]
    fn roundtrip_marshall_then_unmarshall_nested() {
        use crate::marshall::marshall_top_level;
        let original = json!({
            "users": [
                {"name": "Alice", "scores": [100, 95, 88]},
                {"name": "Bob", "scores": [72, 81]}
            ]
        });
        let marshalled = marshall_top_level(&original);
        let result = unmarshall_top_level(&marshalled).unwrap();
        assert_eq!(result, original);
    }
}
