use shvproto::{DateTime as ShvDateTime, RpcValue};

#[derive(Clone,Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub(crate) struct DataChange {
    pub(crate) value: RpcValue,
    pub(crate) date_time: Option<ShvDateTime>,
    pub(crate) short_time: Option<i32>,
    pub(crate) value_flags: u64,
}

#[derive(Copy,Clone,Debug)]
enum DataChangeMetaTag {
    DateTime = shvrpc::rpctype::Tag::USER as isize,
    ShortTime,
    ValueFlags,
    SpecialListValue,
}

fn meta_value<I: shvproto::metamap::GetIndex>(rv: &RpcValue, key: I) -> Option<&RpcValue> {
    rv.meta.as_ref().and_then(|meta| meta.get(key))
}

fn is_data_change(rv: &RpcValue) -> bool {
    meta_value(rv, shvrpc::rpctype::Tag::MetaTypeNameSpaceId as usize).unwrap_or_default().as_int() == shvrpc::rpctype::global_ns::NAMESPACE_ID &&
        meta_value(rv, shvrpc::rpctype::Tag::MetaTypeId as usize).unwrap_or_default().as_int() == shvrpc::rpctype::global_ns::MetaTypeID::ValueChange as i64
}

impl shvproto::metamap::GetIndex for DataChangeMetaTag {
    fn make_key(&self) -> shvproto::metamap::GetKey<'_> {
        shvproto::metamap::GetKey::Int(*self as i32)
    }
}

impl From<RpcValue> for DataChange {
    fn from(value: RpcValue) -> Self {
        if !is_data_change(&value) {
            return DataChange {
                value,
                date_time: None,
                short_time: None,
                value_flags: Default::default(),
            }
        }

        let date_time = meta_value(&value, DataChangeMetaTag::DateTime)
            .and_then(|v| v.to_datetime());
        let short_time = meta_value(&value, DataChangeMetaTag::ShortTime)
            .map(|v| v.as_i32());
        let value_flags = meta_value(&value, DataChangeMetaTag::ValueFlags)
            .map(|v| v.as_u64()).unwrap_or_default();

        let unpacked_value = if let shvproto::Value::List(lst) = &value.value &&
            lst.len() == 1 &&
            lst[0].meta.as_ref().is_some_and(|meta| !meta.is_empty()) &&
            value.meta.as_ref().is_none_or(|meta| meta.get(DataChangeMetaTag::SpecialListValue).is_none())
        {
            lst[0].clone()
        } else {
            RpcValue {
                meta: None,
                value: value.value,
            }
        };
        DataChange {
            value: unpacked_value,
            date_time,
            short_time,
            value_flags,
        }
    }
}

impl From<DataChange> for RpcValue {
    fn from(data_change: DataChange) -> Self {
        let mut res = if data_change.value.meta.as_ref().is_none_or(|meta| meta.is_empty()) {
            let val = data_change.value.value.clone();
            if let shvproto::Value::List(lst) = &val &&
                lst.len() == 1 &&
                lst[0].meta.as_ref().is_some_and(|meta| !meta.is_empty()) {
                    let mut mm = shvproto::MetaMap::new();
                    mm.insert(DataChangeMetaTag::SpecialListValue as usize, true.into());
                    RpcValue::new(val, Some(mm))
            } else {
                RpcValue::new(val, None)
            }
        } else {
            shvproto::make_list!(data_change.value).into()
        };
        let mm = res.meta
            .get_or_insert_default()
            .insert(shvrpc::rpctype::Tag::MetaTypeId as usize, RpcValue::from(shvrpc::rpctype::global_ns::MetaTypeID::ValueChange as i64));
        if let Some(date_time) = data_change.date_time {
            mm.insert(DataChangeMetaTag::DateTime, date_time.into());
        }
        if let Some(short_time) = data_change.short_time {
            mm.insert(DataChangeMetaTag::ShortTime, short_time.into());
        }
        if data_change.value_flags != 0 {
            mm.insert(DataChangeMetaTag::ValueFlags, data_change.value_flags.into());
        }
        res
    }
}

#[cfg(test)]
mod tests {
    use shvproto::{DateTime as ShvDateTime, RpcValue};

    use crate::journalrw::VALUE_FLAG_PROVISIONAL_BIT;

    use super::DataChange;

    #[test]
    fn rpcvalue_to_datachange() {
        let notification_param = RpcValue::from_cpon(r#"<1:5,8:d"2025-07-14T15:01:00.201Z",10:2>false"#).unwrap();
        let data_change = DataChange::from(notification_param);
        assert_eq!(data_change.value, false.into());
        assert_eq!(data_change.date_time, Some(ShvDateTime::from_iso_str("2025-07-14T15:01:00.201Z").unwrap()));
        assert_eq!(data_change.short_time, None);

        let notification_param = RpcValue::from_cpon("true").unwrap();
        let data_change = DataChange::from(notification_param);
        assert_eq!(data_change.value, true.into());
        assert_eq!(data_change.date_time, None);
        assert_eq!(data_change.short_time, None);

        let notification_param = RpcValue::from_cpon("<1:5,10:4>[<1:123>true]").unwrap();
        let data_change = DataChange::from(notification_param);
        assert_eq!(data_change.value.value, true.into());
        assert!(data_change.value.meta.is_some_and(|m| m.get(1).is_some_and(|v| v == &123.into())));
        assert_eq!(data_change.date_time, None);
        assert_eq!(data_change.short_time, None);
        assert_eq!(data_change.value_flags, 4);

        let notification_param = RpcValue::from_cpon("<1:5,10:4,11:true>[<1:123>true]").unwrap();
        let data_change = DataChange::from(notification_param);
        assert_eq!(data_change.value, shvproto::make_list!(RpcValue::new(true.into(), {let mut mm = shvproto::metamap::MetaMap::new(); mm.insert(1, 123.into()); Some(mm)})).into());
        assert!(data_change.value.meta.is_none());
        assert_eq!(data_change.date_time, None);
        assert_eq!(data_change.short_time, None);
        assert_eq!(data_change.value_flags, 4);
    }

    #[test]
    fn rpcvalue_datachange_conversions() {
        let data_change = DataChange {
            value: 123.into(),
            date_time: None,
            short_time: None,
            value_flags: 0,
        };
        let rv: RpcValue = data_change.clone().into();
        assert_eq!(data_change, rv.into());

        let data_change = DataChange {
            value: "foo".into(),
            date_time: Some(ShvDateTime::from_iso_str("2025-07-14T15:01:00.201Z").unwrap()),
            short_time: Some(10),
            value_flags: 1 << VALUE_FLAG_PROVISIONAL_BIT,
        };
        let rv: RpcValue = data_change.clone().into();
        assert_eq!(data_change, rv.into());
    }
}
