use crate::models;
use crate::storage::Error;
use pbbson::{Model, bson};
use serde_json::{Map, Value};
use std::collections::HashMap;

const EMPTY_FIELDS_PSEUDOFIELD: &str = "_exists";

pub fn transform_to_redis(model: &Model) -> Result<Vec<(String, String)>, Error> {
    let hashmap = models::to_json(model, false)?;
    let mut map = serde_json::Map::new();
    if hashmap.is_empty() {
        map.insert(EMPTY_FIELDS_PSEUDOFIELD.to_string(), serde_json::Value::Bool(true));
    } else {
        for (k, v) in hashmap.into_iter() {
            map.insert(k, v);
        }
    }
    let mut values = Vec::new();
    add_values(&mut values, &map, "");
    Ok(values)
}

pub fn transform_from_redis(fields: HashMap<String, String>, id: &str) -> Result<Model, Error> {
    if fields.is_empty() {
        return Err(Error::not_found("No such record"));
    }
    let mut fields = fields;
    fields.remove(EMPTY_FIELDS_PSEUDOFIELD);
    let mut map: HashMap<String, serde_json::Value> = HashMap::new();
    let mut prefixes: Vec<String> = vec![];
    for (k, v) in fields.iter() {
        if k.contains(".") {
            if let Some(prefix) = k.split('.').collect::<Vec<_>>().first()
                && !prefixes.contains(&prefix.to_string())
            {
                prefixes.push(prefix.to_string());
            }
        } else {
            map.insert(k.to_string(), serde_json::Value::String(v.to_string()));
        }
    }
    for prefix in prefixes {
        let mut submap = serde_json::Map::new();
        for (k, v) in fields.iter() {
            if let Some(remaining) = k.strip_prefix(&format!("{prefix}.")) {
                submap.insert(remaining.to_string(), serde_json::Value::String(v.to_string()));
            }
        }
        map.insert(prefix.to_string(), serde_json::Value::Object(submap));
    }
    // let doc = bson::serialize_to_document(&fields).map_err(|e| Error::internal(e.to_string()))?;
    let doc = bson::serialize_to_document(&map).map_err(|e| Error::internal(e.to_string()))?;
    // let doc = bson::serialize_to_document(&map_as_json).map_err(|e| Error::internal(e.to_string()))?;
    let mut model = Model::from(doc);
    model.insert("id", id);
    Ok(model)
}

fn add_values(values: &mut Vec<(String, String)>, map: &Map<String, Value>, prefix: &str) {
    for (k, v) in map.iter() {
        let key = {
            if prefix.is_empty() {
                k.clone()
            } else {
                format!("{prefix}.{k}")
            }
        };
        match v {
            serde_json::Value::Object(object) => add_values(values, object, &key),
            serde_json::Value::String(s) => values.push((key, s.clone())),
            _ => values.push((key, v.to_string())),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{transform_from_redis, transform_to_redis};
    use pbbson::{
        Model,
        bson::{Bson, doc},
    };
    use std::collections::HashMap;

    #[test]
    fn can_serialize_flat_object() {
        let model = Model::from(doc! {"id": "123", "name": "abc"});
        let values = transform_to_redis(&model).unwrap();
        assert_eq!(values.len(), 1);
        assert_eq!(values.first(), Some(("name".to_string(), "abc".to_string())).as_ref());
    }

    #[test]
    fn can_serialize_nested_object() {
        let model = Model::from(doc! {"id": "123", "name": "abc", "metadata": doc! {"foo": "bar"}});
        let values = transform_to_redis(&model).unwrap();
        assert_eq!(values.len(), 2);
        for (k, v) in values.iter() {
            if k == "name" {
                assert_eq!(v, "abc");
            } else {
                assert_eq!(k, "metadata.foo");
                assert_eq!(v, "bar");
            }
        }
    }

    #[test]
    fn can_deserialize_flat_object() {
        let fields = HashMap::from([("name".to_string(), "abc".to_string())]);
        let model = transform_from_redis(fields, "123").unwrap();
        assert_eq!(model.len(), 2);
        assert_eq!(model.id().unwrap(), "123".to_string());
        assert_eq!(model.get("name"), Some(Bson::String("abc".to_string())).as_ref());
    }

    #[test]
    fn can_deserialize_nested_object() {
        let fields = HashMap::from([
            ("name".to_string(), "abc".to_string()),
            ("metadata.foo".to_string(), "bar".to_string()),
        ]);
        let model = transform_from_redis(fields, "123").unwrap();
        assert_eq!(model.len(), 3);
        assert_eq!(model.id().unwrap(), "123".to_string());
        assert_eq!(model.get("name"), Some(Bson::String("abc".to_string())).as_ref());
        assert_eq!(
            model.get("metadata"),
            Some(Bson::Document(doc! {"foo": "bar"})).as_ref()
        );
    }
}
