use serde::{
    de::{Error, MapAccess, Unexpected, Visitor},
    ser::SerializeStruct,
    Deserialize, Deserializer, Serialize, Serializer,
};

use super::read_field;
use crate::legacy::{DataType, Key, ObjType, ObjectId, Op, OpId, OpType, ScalarValue, SortedVec};

impl Serialize for Op {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut fields = 4;

        if self.insert {
            fields += 1
        }

        let numerical_datatype = match &self.action {
            OpType::Put(value) => value.as_numerical_datatype(),
            _ => None,
        };

        if numerical_datatype.is_some() {
            fields += 2
        } else if !matches!(&self.action, OpType::Make(..)) {
            fields += 1
        };

        let mut op = serializer.serialize_struct("Operation", fields)?;
        op.serialize_field("action", &self.action)?;
        op.serialize_field("obj", &self.obj)?;
        op.serialize_field(
            if self.key.is_map_key() {
                "key"
            } else {
                "elemId"
            },
            &self.key,
        )?;
        if self.insert {
            op.serialize_field("insert", &self.insert)?;
        }
        if let Some(datatype) = numerical_datatype {
            op.serialize_field("datatype", &datatype)?;
        }
        match &self.action {
            OpType::Increment(n) => op.serialize_field("value", &n)?,
            OpType::Put(ScalarValue::Counter(c)) => op.serialize_field("value", &c.start)?,
            OpType::Put(value) => op.serialize_field("value", &value)?,
            _ => {}
        }
        op.serialize_field("pred", &self.pred)?;
        op.end()
    }
}

// We need to manually implement deserialization for `RawOpType`
// b/c by default rmp-serde (serde msgpack integration) serializes enums as maps with a
// single KV pair where the key is an integer representing the enum variant & the value
// is the associated data
// But we serialize `RawOpType` as a string, causing rmp-serde to choke on deserialization
#[derive(PartialEq, Debug, Clone, Copy)]
pub(crate) enum RawOpType {
    MakeMap,
    MakeTable,
    MakeList,
    MakeText,
    Del,
    Inc,
    Set,
}

impl Serialize for RawOpType {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let s = match self {
            RawOpType::MakeMap => "makeMap",
            RawOpType::MakeTable => "makeTable",
            RawOpType::MakeList => "makeList",
            RawOpType::MakeText => "makeText",
            RawOpType::Del => "del",
            RawOpType::Inc => "inc",
            RawOpType::Set => "set",
        };
        serializer.serialize_str(s)
    }
}

impl<'de> Deserialize<'de> for RawOpType {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        const VARIANTS: &[&str] = &[
            "makeMap",
            "makeTable",
            "makeList",
            "makeText",
            "del",
            "inc",
            "set",
            "mark",
            "unmark",
        ];
        // TODO: Probably more efficient to deserialize to a `&str`
        let raw_type = String::deserialize(deserializer)?;
        match raw_type.as_str() {
            "makeMap" => Ok(RawOpType::MakeMap),
            "makeTable" => Ok(RawOpType::MakeTable),
            "makeList" => Ok(RawOpType::MakeList),
            "makeText" => Ok(RawOpType::MakeText),
            "del" => Ok(RawOpType::Del),
            "inc" => Ok(RawOpType::Inc),
            "set" => Ok(RawOpType::Set),
            other => Err(Error::unknown_variant(other, VARIANTS)),
        }
    }
}

impl<'de> Deserialize<'de> for Op {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        const FIELDS: &[&str] = &["ops", "deps", "message", "seq", "actor", "requestType"];
        struct OperationVisitor;
        impl<'de> Visitor<'de> for OperationVisitor {
            type Value = Op;

            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                formatter.write_str("An operation object")
            }

            fn visit_map<V>(self, mut map: V) -> Result<Op, V::Error>
            where
                V: MapAccess<'de>,
            {
                let mut action: Option<RawOpType> = None;
                let mut obj: Option<ObjectId> = None;
                let mut key: Option<Key> = None;
                let mut pred: Option<SortedVec<OpId>> = None;
                let mut insert: Option<bool> = None;
                let mut datatype: Option<DataType> = None;
                let mut value: Option<Option<ScalarValue>> = None;
                let mut name: Option<String> = None;
                let mut expand: Option<bool> = None;
                let mut ref_id: Option<OpId> = None;
                while let Some(field) = map.next_key::<String>()? {
                    match field.as_ref() {
                        "action" => read_field("action", &mut action, &mut map)?,
                        "obj" => read_field("obj", &mut obj, &mut map)?,
                        "key" => {
                            if key.is_some() {
                                return Err(Error::duplicate_field("key"));
                            } else {
                                key = Some(Key::Map(map.next_value()?));
                            }
                        }
                        "elemId" => {
                            if key.is_some() {
                                return Err(Error::duplicate_field("elemId"));
                            } else {
                                key = Some(Key::Seq(map.next_value()?))
                            }
                        }
                        "pred" => read_field("pred", &mut pred, &mut map)?,
                        "insert" => read_field("insert", &mut insert, &mut map)?,
                        "datatype" => read_field("datatype", &mut datatype, &mut map)?,
                        "value" => read_field("value", &mut value, &mut map)?,
                        "name" => read_field("name", &mut name, &mut map)?,
                        "expand" => read_field("expand", &mut expand, &mut map)?,
                        "ref" => read_field("ref", &mut ref_id, &mut map)?,
                        _ => return Err(Error::unknown_field(&field, FIELDS)),
                    }
                }
                let action = action.ok_or_else(|| Error::missing_field("action"))?;
                let obj = obj.ok_or_else(|| Error::missing_field("obj"))?;
                let key = key.ok_or_else(|| Error::missing_field("key"))?;
                let pred = pred.ok_or_else(|| Error::missing_field("pred"))?;
                let insert = insert.unwrap_or(false);
                let action = match action {
                    RawOpType::MakeMap => OpType::Make(ObjType::Map),
                    RawOpType::MakeTable => OpType::Make(ObjType::Table),
                    RawOpType::MakeList => OpType::Make(ObjType::List),
                    RawOpType::MakeText => OpType::Make(ObjType::Text),
                    RawOpType::Del => OpType::Delete,
                    RawOpType::Set => {
                        let value = if let Some(datatype) = datatype {
                            let raw_value = value
                                .ok_or_else(|| Error::missing_field("value"))?
                                .unwrap_or(ScalarValue::Null);
                            raw_value.as_datatype(datatype).map_err(|e| {
                                Error::invalid_value(
                                    Unexpected::Other(e.unexpected.as_str()),
                                    &e.expected.as_str(),
                                )
                            })?
                        } else {
                            value
                                .ok_or_else(|| Error::missing_field("value"))?
                                .unwrap_or(ScalarValue::Null)
                        };
                        OpType::Put(value)
                    }
                    RawOpType::Inc => match value.flatten() {
                        Some(ScalarValue::Int(n)) => Ok(OpType::Increment(n)),
                        Some(ScalarValue::Uint(n)) => Ok(OpType::Increment(n as i64)),
                        Some(ScalarValue::F64(n)) => Ok(OpType::Increment(n as i64)),
                        Some(ScalarValue::Counter(n)) => Ok(OpType::Increment(n.into())),
                        Some(ScalarValue::Timestamp(n)) => Ok(OpType::Increment(n)),
                        Some(ScalarValue::Bytes(s)) => {
                            Err(Error::invalid_value(Unexpected::Bytes(&s), &"a number"))
                        }
                        Some(ScalarValue::Unknown { bytes, .. }) => {
                            Err(Error::invalid_value(Unexpected::Bytes(&bytes), &"a number"))
                        }
                        Some(ScalarValue::Str(s)) => {
                            Err(Error::invalid_value(Unexpected::Str(&s), &"a number"))
                        }
                        Some(ScalarValue::Boolean(b)) => {
                            Err(Error::invalid_value(Unexpected::Bool(b), &"a number"))
                        }
                        Some(ScalarValue::Null) => {
                            Err(Error::invalid_value(Unexpected::Other("null"), &"a number"))
                        }
                        None => Err(Error::missing_field("value")),
                    }?,
                };
                Ok(Op {
                    action,
                    obj,
                    key,
                    pred,
                    insert,
                })
            }
        }
        deserializer.deserialize_struct("Operation", FIELDS, OperationVisitor)
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use super::*;
    use crate::legacy as amp;

    #[test]
    fn test_deserialize_action() {
        struct Scenario {
            name: &'static str,
            json: serde_json::Value,
            expected: Result<Op, serde_json::Error>,
        }
        let scenarios: Vec<Scenario> = vec![
            Scenario {
                name: "Set with Uint",
                json: serde_json::json!({
                    "action": "set",
                    "obj": "_root",
                    "key": "somekey",
                    "datatype": "uint",
                    "value": 123,
                    "pred": []
                }),
                expected: Ok(Op {
                    action: OpType::Put(ScalarValue::Uint(123)),
                    obj: ObjectId::Root,
                    key: "somekey".into(),
                    insert: false,
                    pred: SortedVec::new(),
                }),
            },
            Scenario {
                name: "Set with Int",
                json: serde_json::json!({
                    "action": "set",
                    "obj": "_root",
                    "key": "somekey",
                    "value": -123,
                    "datatype": "int",
                    "pred": []
                }),
                expected: Ok(Op {
                    action: OpType::Put(ScalarValue::Int(-123)),
                    obj: ObjectId::Root,
                    key: "somekey".into(),
                    insert: false,
                    pred: SortedVec::new(),
                }),
            },
            Scenario {
                name: "Set with F64",
                json: serde_json::json!({
                    "action": "set",
                    "obj": "_root",
                    "key": "somekey",
                    "value": -123,
                    "datatype": "float64",
                    "pred": []
                }),
                expected: Ok(Op {
                    action: OpType::Put(ScalarValue::F64(-123.0)),
                    obj: ObjectId::Root,
                    key: "somekey".into(),
                    insert: false,
                    pred: Vec::new().into(),
                }),
            },
            Scenario {
                name: "Set with string",
                json: serde_json::json!({
                    "action": "set",
                    "obj": "_root",
                    "key": "somekey",
                    "value": "somestring",
                    "pred": []
                }),
                expected: Ok(Op {
                    action: OpType::Put(ScalarValue::Str("somestring".into())),
                    obj: ObjectId::Root,
                    key: "somekey".into(),
                    insert: false,
                    pred: SortedVec::new(),
                }),
            },
            Scenario {
                name: "Set with f64",
                json: serde_json::json!({
                    "action": "set",
                    "obj": "_root",
                    "key": "somekey",
                    "value": 1.23,
                    "pred": []
                }),
                expected: Ok(Op {
                    action: OpType::Put(ScalarValue::F64(1.23)),
                    obj: ObjectId::Root,
                    key: "somekey".into(),
                    insert: false,
                    pred: SortedVec::new(),
                }),
            },
            Scenario {
                name: "Set with boolean",
                json: serde_json::json!({
                    "action": "set",
                    "obj": "_root",
                    "key": "somekey",
                    "value": true,
                    "pred": []
                }),
                expected: Ok(Op {
                    action: OpType::Put(ScalarValue::Boolean(true)),
                    obj: ObjectId::Root,
                    key: "somekey".into(),
                    insert: false,
                    pred: SortedVec::new(),
                }),
            },
            Scenario {
                name: "Set without value",
                json: serde_json::json!({
                    "action": "set",
                    "obj": "_root",
                    "key": "somekey",
                    "datatype": "counter",
                    "pred": []
                }),
                expected: Err(serde_json::Error::missing_field("value")),
            },
            Scenario {
                name: "Set with counter",
                json: serde_json::json!({
                    "action": "set",
                    "obj": "_root",
                    "key": "somekey",
                    "value": 123,
                    "datatype": "counter",
                    "pred": []
                }),
                expected: Ok(Op {
                    action: OpType::Put(ScalarValue::Counter(123.into())),
                    obj: ObjectId::Root,
                    key: "somekey".into(),
                    insert: false,
                    pred: SortedVec::new(),
                }),
            },
            Scenario {
                name: "Set with counter datatype and string value",
                json: serde_json::json!({
                    "action": "set",
                    "obj": "_root",
                    "key": "somekey",
                    "value": "somestring",
                    "datatype": "counter",
                    "pred": []
                }),
                expected: Err(serde_json::Error::invalid_value(
                    Unexpected::Other("\"somestring\""),
                    &"an integer",
                )),
            },
            Scenario {
                name: "Set with timestamp datatype and string value",
                json: serde_json::json!({
                    "action": "set",
                    "obj": "_root",
                    "key": "somekey",
                    "value": "somestring",
                    "datatype": "timestamp",
                    "pred": []
                }),
                expected: Err(serde_json::Error::invalid_value(
                    Unexpected::Other("\"somestring\""),
                    &"an integer",
                )),
            },
            Scenario {
                name: "Inc with counter",
                json: serde_json::json!({
                    "action": "inc",
                    "obj": "_root",
                    "key": "somekey",
                    "value": 12,
                    "datatype": "counter",
                    "pred": []
                }),
                expected: Ok(Op {
                    action: OpType::Increment(12),
                    obj: ObjectId::Root,
                    key: "somekey".into(),
                    insert: false,
                    pred: SortedVec::new(),
                }),
            },
            Scenario {
                name: "Inc without counter",
                json: serde_json::json!({
                    "action": "inc",
                    "obj": "_root",
                    "key": "somekey",
                    "value": 12,
                    "pred": []
                }),
                expected: Ok(Op {
                    action: OpType::Increment(12),
                    obj: ObjectId::Root,
                    key: "somekey".into(),
                    insert: false,
                    pred: SortedVec::new(),
                }),
            },
            Scenario {
                name: "Inc without value",
                json: serde_json::json!({
                    "action": "inc",
                    "obj": "_root",
                    "key": "somekey",
                    "pred": []
                }),
                expected: Err(serde_json::Error::missing_field("value")),
            },
            Scenario {
                name: "Set with null",
                json: serde_json::json!({
                    "action": "set",
                    "obj": "_root",
                    "key": "somekey",
                    "value": null,
                    "pred": []
                }),
                expected: Ok(Op {
                    action: OpType::Put(ScalarValue::Null),
                    obj: ObjectId::Root,
                    key: "somekey".into(),
                    insert: false,
                    pred: SortedVec::new(),
                }),
            },
        ];

        for scenario in scenarios.into_iter() {
            let result: serde_json::Result<Op> = serde_json::from_value(scenario.json);
            match (result, scenario.expected) {
                (Ok(result_op), Ok(expected_op)) => assert_eq!(
                    result_op, expected_op,
                    "Scenario {}: Expected Ok({:?}) but got Ok({:?})",
                    scenario.name, expected_op, result_op
                ),
                (Ok(result_op), Err(e)) => panic!(
                    "Scenario {}: expected Err({:?}) but got Ok({:?})",
                    scenario.name, e, result_op
                ),
                (Err(result_e), Err(expected_e)) => assert_eq!(
                    result_e.to_string(),
                    expected_e.to_string(),
                    "Scenario {}: expected Err({:?}) but got Err({:?})",
                    scenario.name,
                    expected_e,
                    result_e
                ),
                (Err(result_e), Ok(expected)) => panic!(
                    "Scenario {}: expected Ok({:?}) but got Err({:?})",
                    scenario.name, expected, result_e
                ),
            }
        }
    }

    #[test]
    fn test_deserialize_obj() {
        let root: Op = serde_json::from_value(serde_json::json!({
            "action": "inc",
            "obj": "_root",
            "key": "somekey",
            "value": 1,
            "pred": []
        }))
        .unwrap();
        assert_eq!(root.obj, amp::ObjectId::Root);

        let opid: Op = serde_json::from_value(serde_json::json!({
            "action": "inc",
            "obj": "1@7ef48769b04d47e9a88e98a134d62716",
            "key": "somekey",
            "value": 1,
            "pred": []
        }))
        .unwrap();
        assert_eq!(
            opid.obj,
            amp::ObjectId::from_str("1@7ef48769b04d47e9a88e98a134d62716").unwrap()
        );

        let invalid: Result<Op, serde_json::Error> = serde_json::from_value(serde_json::json!({
            "action": "inc",
            "obj": "notanobject",
            "key": "somekey",
            "value": 1,
            "pred": []
        }));
        match invalid {
            Ok(_) => panic!("Parsing an invalid object id should fail"),
            Err(e) => assert!(e.to_string().contains("A valid ObjectID")),
        }
    }

    #[test]
    fn test_serialize_key() {
        let map_key = Op {
            action: OpType::Increment(12),
            obj: ObjectId::Root,
            key: "somekey".into(),
            insert: false,
            pred: SortedVec::new(),
        };
        let json = serde_json::to_value(map_key).unwrap();
        let expected: serde_json::Value = "somekey".into();
        assert_eq!(json.as_object().unwrap().get("key"), Some(&expected));

        let elemid_key = Op {
            action: OpType::Increment(12),
            obj: ObjectId::Root,
            key: OpId::from_str("1@7ef48769b04d47e9a88e98a134d62716")
                .unwrap()
                .into(),
            insert: false,
            pred: SortedVec::new(),
        };
        let json = serde_json::to_value(elemid_key).unwrap();
        let expected: serde_json::Value = "1@7ef48769b04d47e9a88e98a134d62716".into();
        assert_eq!(json.as_object().unwrap().get("elemId"), Some(&expected));
    }

    #[test]
    fn test_round_trips() {
        let testcases = vec![
            Op {
                action: OpType::Put(ScalarValue::Uint(12)),
                obj: ObjectId::Root,
                key: "somekey".into(),
                insert: false,
                pred: SortedVec::new(),
            },
            Op {
                action: OpType::Increment(12),
                obj: ObjectId::from_str("1@7ef48769b04d47e9a88e98a134d62716").unwrap(),
                key: "somekey".into(),
                insert: false,
                pred: SortedVec::new(),
            },
            Op {
                action: OpType::Put(ScalarValue::Uint(12)),
                obj: ObjectId::from_str("1@7ef48769b04d47e9a88e98a134d62716").unwrap(),
                key: "somekey".into(),
                insert: false,
                pred: vec![OpId::from_str("1@7ef48769b04d47e9a88e98a134d62716").unwrap()].into(),
            },
            Op {
                action: OpType::Increment(12),
                obj: ObjectId::Root,
                key: "somekey".into(),
                insert: false,
                pred: SortedVec::new(),
            },
            Op {
                action: OpType::Put("seomthing".into()),
                obj: ObjectId::from_str("1@7ef48769b04d47e9a88e98a134d62716").unwrap(),
                key: OpId::from_str("1@7ef48769b04d47e9a88e98a134d62716")
                    .unwrap()
                    .into(),
                insert: false,
                pred: vec![OpId::from_str("1@7ef48769b04d47e9a88e98a134d62716").unwrap()].into(),
            },
        ];
        for (testcase_num, testcase) in testcases.iter().enumerate() {
            #[allow(clippy::expect_fun_call)]
            let serialized = serde_json::to_string(testcase)
                .expect(format!("Failed to serialize testcase {}", testcase_num).as_str());
            #[allow(clippy::expect_fun_call)]
            let deserialized: Op = serde_json::from_str(&serialized)
                .expect(format!("Failed to deserialize testcase {}", testcase_num).as_str());
            assert_eq!(testcase, &deserialized, "Testcase {} failed", testcase_num);
        }
    }
}
