use std::sync::Arc;

use crate::{
    data::keyed::Keyed,
    network::query_builder::{QueryBuilder, QueryParam},
    tracking::trackable::{Trackable, TrackableBase, TrackingStateCell},
    utils::{self, bool_to_str},
};

const EVENT_TYPE: &str = "customData";
const UNDEFINED_INDEX: u32 = u32::MAX;

#[derive(Debug)]
#[cfg_attr(test, derive(Clone, PartialEq))]
pub struct CustomData {
    pub index: u32,
    pub name: Option<String>,
    pub values: Vec<String>,
    pub overwrite: bool,
    pub(crate) is_mapping_identifier: bool,
    pub(crate) trackable_base: TrackableBase,
}

impl CustomData {
    fn new(index: u32, name: Option<String>, values: Vec<String>, opts: CustomDataOpts) -> Self {
        CustomData {
            index,
            name,
            values,
            overwrite: opts.overwrite,
            trackable_base: TrackableBase::default(),
            is_mapping_identifier: false,
        }
    }

    pub fn new_with_index(index: u32, values: Vec<String>) -> Self {
        Self::new_with_index_opts(index, values, CustomDataOpts::new())
    }

    pub fn new_with_index_opts(index: u32, values: Vec<String>, opts: CustomDataOpts) -> Self {
        Self::new(index, None, values, opts)
    }

    pub fn new_with_name(name: impl Into<String>, values: Vec<String>) -> Self {
        Self::new_with_name_opts(name, values, CustomDataOpts::new())
    }

    pub fn new_with_name_opts(name: impl Into<String>, values: Vec<String>, opts: CustomDataOpts) -> Self {
        Self::new(UNDEFINED_INDEX, Some(name.into()), values, opts)
    }

    pub(crate) fn named_to_indexed(self, index: u32) -> Self {
        Self { index, ..self }
    }

    pub(crate) fn into_mapping_identifier(self) -> Self {
        CustomData {
            trackable_base: TrackableBase::new(true),
            is_mapping_identifier: true,
            ..self
        }
    }
}

impl Keyed<u32> for CustomData {
    fn key(&self) -> &u32 {
        return &self.index;
    }
}

impl Trackable for CustomData {
    fn encode_query(&self) -> String {
        let mut qb = QueryBuilder::with_params(vec![
            (QueryParam::Nonce, self.trackable_base.nonce().into()),
            (QueryParam::EventType, EVENT_TYPE.into()),
            (QueryParam::Index, self.index.to_string().into()),
            (QueryParam::ValuesCountMap, self.make_values_count_map_param().into()),
            (QueryParam::Overwrite, bool_to_str(self.overwrite).into()),
        ]);
        if self.is_mapping_identifier {
            qb.add_ptr_param(QueryParam::MappingIdentifier, "true");
        }
        return qb.build();
    }

    fn state_cell(&self) -> &Arc<TrackingStateCell> {
        self.trackable_base.state_cell()
    }
}

// Trackable Helper
impl CustomData {
    fn make_values_count_map_param(&self) -> String {
        let mut param = String::with_capacity(self.values.len() * 16 + 2);
        param.push('{');
        for value in self.values.iter() {
            param.push('"');
            utils::push_str_json_escaped(&mut param, value);
            param.push_str("\":1,");
        }
        if param.len() > 1 {
            param.pop();
        }
        param.push('}');
        return param;
    }
}

#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub struct CustomDataOpts {
    overwrite: bool,
}

impl Default for CustomDataOpts {
    fn default() -> Self {
        Self { overwrite: true }
    }
}

impl CustomDataOpts {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn overwrite(mut self, overwrite: bool) -> Self {
        self.overwrite = overwrite;
        self
    }
}

#[cfg(test)]
mod tests {
    use crate::utils::{test_utils::remove_nonce_parameter, url_encoder::UrlEncoder};

    use super::*;
    use rstest::rstest;

    #[rstest]
    fn test_event_type() {
        assert_eq!("customData", EVENT_TYPE);
    }

    #[rstest]
    fn test_new_with_index() {
        const INDEX: u32 = 123;
        let values = vec!["v1".to_owned(), "v2".to_owned()];
        let cd = CustomData::new_with_index(INDEX, values.clone());
        assert!(!cd.is_mapping_identifier);
        assert_eq!(INDEX, cd.index);
        assert_eq!(None, cd.name);
        assert_eq!(values, cd.values);
        assert!(cd.overwrite);
    }

    #[rstest]
    fn test_new_with_name() {
        let values = vec!["v1".to_owned(), "v2".to_owned()];
        let cd = CustomData::new_with_name("status".to_owned(), values.clone());
        assert!(!cd.is_mapping_identifier);
        assert_eq!(UNDEFINED_INDEX, cd.index);
        assert_eq!(Some("status".to_owned()), cd.name);
        assert_eq!(values, cd.values);
        assert!(cd.overwrite);
    }

    #[rstest]
    fn test_new_with_index_opts() {
        let cd = CustomData::new_with_index_opts(123, vec!["v1".to_owned()], CustomDataOpts::new().overwrite(false));
        assert_eq!(123, cd.index);
        assert!(!cd.overwrite);
    }

    #[rstest]
    fn test_new_with_name_opts() {
        let cd = CustomData::new_with_name_opts(
            "status".to_owned(),
            vec!["v1".to_owned()],
            CustomDataOpts::new().overwrite(false),
        );
        assert_eq!(UNDEFINED_INDEX, cd.index);
        assert_eq!(Some("status".to_owned()), cd.name);
        assert!(!cd.overwrite);
    }

    #[rstest]
    fn test_into_mapping_identifier() {
        const INDEX: u32 = 321;
        let values = vec!["a".to_owned()];
        let mi = CustomData::new_with_index(INDEX, values.clone()).into_mapping_identifier();
        assert!(mi.is_mapping_identifier);
        assert_eq!(INDEX, mi.index);
        assert_eq!(None, mi.name);
        assert_eq!(values, mi.values);
        assert!(mi.overwrite);
    }

    #[rstest]
    fn test_key() {
        const INDEX: u32 = 98;
        let cd = CustomData::new_with_index(INDEX, Vec::new());
        assert_eq!(INDEX, *cd.key());
    }

    const URL_TO_ENCODE: &str = "https://google.com?key='aaa~aaa'&val_ue=10&aa@a-3 3 3-_.!~*'()";

    #[rstest]
    #[case(CustomData::new_with_index(1, vec![]), "{}")]
    #[case(CustomData::new_with_index(2, vec![URL_TO_ENCODE.to_owned()]), &format!("{{\"{URL_TO_ENCODE}\":1}}"))]
    #[case(CustomData::new_with_index(3, vec!["a".to_owned(), "b".to_owned(), "c".to_owned()]), "{\"a\":1,\"b\":1,\"c\":1}")]
    #[case(CustomData::new_with_index(4, vec!["\\".to_owned(), "\"".to_owned()]), "{\"\\\\\":1,\"\\\"\":1}")]
    #[case(CustomData::new_with_index_opts(5, vec!["v".to_owned()], CustomDataOpts::new().overwrite(false)), "{\"v\":1}")]
    fn test_encode_query(#[case] custom_data: CustomData, #[case] values_count_map: &str) {
        // arrange
        #[rustfmt::skip]
        let expected = format!(
            "{}={}&{}={}&{}={}&{}={}",
            QueryParam::EventType.as_str(), EVENT_TYPE,
            QueryParam::Index.as_str(), custom_data.index,
            QueryParam::ValuesCountMap.as_str(), UrlEncoder::encode_uri_component(values_count_map),
            QueryParam::Overwrite.as_str(), custom_data.overwrite,
        );
        // act
        let actual = custom_data.encode_query();
        // assert
        let actual = remove_nonce_parameter(&actual);
        assert_eq!(expected, actual);
    }
}
