use std::{collections::HashSet, sync::Arc, time::SystemTime};

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

const EVENT_TYPE: &str = "conversion";

#[derive(Debug)]
#[cfg_attr(test, derive(Clone, PartialEq))]
pub struct Conversion {
    pub(crate) trackable_base: TrackableBase,
    pub goal_id: u32,
    pub revenue: f32,
    pub negative: bool,
    pub metadata: Vec<CustomData>,
    pub(crate) assignment_time: SystemTime,
}

impl Conversion {
    pub fn new(goal_id: u32) -> Self {
        Self::new_with_opts(goal_id, ConversionOpts::new())
    }

    pub fn new_with_opts(goal_id: u32, options: ConversionOpts) -> Self {
        Self::new_with_opts_time(goal_id, options, SystemTime::now())
    }

    pub(crate) fn new_with_opts_time(goal_id: u32, options: ConversionOpts, assignment_time: SystemTime) -> Self {
        return Conversion {
            goal_id,
            revenue: options.revenue,
            negative: options.negative,
            metadata: options.metadata,
            assignment_time,
            trackable_base: TrackableBase::default(),
        };
    }
}

impl Trackable for Conversion {
    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::GoalId, self.goal_id.to_string().into()),
            (QueryParam::Revenue, self.revenue.to_string().into()),
            (QueryParam::Negative, bool_to_str(self.negative).into()),
        ]);
        if !self.metadata.is_empty() {
            qb.add_owned_param(QueryParam::Metadata, self.make_metadata_param());
        }
        return qb.build();
    }

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

// Trackable helper
impl Conversion {
    fn make_metadata_param(&self) -> String {
        const AVERAGE_METADATA_SIZE: usize = 32;
        let mut param = String::with_capacity(self.metadata.len() * AVERAGE_METADATA_SIZE + 2);
        param.push('{');
        let mut added_indices = HashSet::new();
        for mcd in self.metadata.iter() {
            if added_indices.contains(&mcd.index) {
                continue;
            }
            added_indices.insert(mcd.index);
            param.push('"');
            param.push_str(&mcd.index.to_string());
            param.push_str("\":[");
            if !mcd.values.is_empty() {
                param.push('"');
                for value in &mcd.values {
                    utils::push_str_json_escaped(&mut param, value);
                    param.push_str("\",\"");
                }
                param.pop();
                param.pop();
            }
            param.push_str("],");
        }
        if param.len() > 1 {
            param.remove(param.len() - 1);
        }
        param.push('}');
        return param;
    }
}

#[derive(Debug, Default)]
#[cfg_attr(test, derive(Clone, PartialEq))]
pub struct ConversionOpts {
    revenue: f32,
    negative: bool,
    metadata: Vec<CustomData>,
}

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

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

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

    pub fn metadata(mut self, metadata: Vec<CustomData>) -> Self {
        self.metadata = metadata;
        self
    }
}

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

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

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

    #[rstest]
    fn test_new() {
        let before = SystemTime::now();
        let conversion = Conversion::new(123);
        let after = SystemTime::now();
        assert_eq!(123, conversion.goal_id);
        assert_eq!(0.0, conversion.revenue);
        assert!(!conversion.negative);
        assert!(conversion.metadata.is_empty());
        assert!((before..=after).contains(&conversion.assignment_time));
    }

    #[rstest]
    #[case(123, 100.0, false, Vec::new())]
    #[case(321, 0.0, true, vec![CustomData::new_with_index(4, vec!["x".to_owned()])])]
    fn test_new_with_opts(
        #[case] goal_id: u32,
        #[case] revenue: f32,
        #[case] negative: bool,
        #[case] metadata: Vec<CustomData>,
    ) {
        let expected_metadata_len = metadata.len();
        let before = SystemTime::now();
        let conversion = Conversion::new_with_opts(
            goal_id,
            ConversionOpts::new().revenue(revenue).negative(negative).metadata(metadata),
        );
        let after = SystemTime::now();
        assert_eq!(goal_id, conversion.goal_id);
        assert_eq!(revenue, conversion.revenue);
        assert_eq!(negative, conversion.negative);
        assert_eq!(expected_metadata_len, conversion.metadata.len());
        assert!((before..=after).contains(&conversion.assignment_time));
    }

    #[rstest]
    fn test_new_with_opts_time() {
        let assignment_time = SystemTime::UNIX_EPOCH + std::time::Duration::from_millis(12345);
        let conversion =
            Conversion::new_with_opts_time(321, ConversionOpts::new().revenue(12.3).negative(true), assignment_time);
        assert_eq!(321, conversion.goal_id);
        assert_eq!(12.3, conversion.revenue);
        assert!(conversion.negative);
        assert_eq!(assignment_time, conversion.assignment_time);
    }

    #[rstest]
    #[case(123, 0.0, false, vec![], None)]
    #[case(321, 100.0, false, vec![], None)]
    #[case(123, 0.0, true, vec![], None)]
    #[case(321, 200.0, true, vec![], None)]
    #[case(
        123, 300.0, false, vec![
            CustomData::new_with_index(3, vec!["toto".to_owned(), "titi".to_owned()]), CustomData::new_with_index(5, vec!["tutu".to_owned()])
        ], Some("%7B%223%22%3A%5B%22toto%22%2C%22titi%22%5D%2C%225%22%3A%5B%22tutu%22%5D%7D")
    )]
    #[case(
        123, 300.0, false,
        vec![CustomData::new_with_index(3, vec!["toto".to_owned()]), CustomData::new_with_index(3, vec!["tutu".to_owned()])],
        Some("%7B%223%22%3A%5B%22toto%22%5D%7D")
    )]
    #[case(
        123, 300.0, false, vec![CustomData::new_with_index(5, vec!["\\\"".to_owned()])],
        Some("%7B%225%22%3A%5B%22%5C%5C%5C%22%22%5D%7D")
    )]
    fn test_encode_query(
        #[case] goal_id: u32,
        #[case] revenue: f32,
        #[case] negative: bool,
        #[case] metadata: Vec<CustomData>,
        #[case] encoded_metadata: Option<&str>,
    ) {
        // arrange
        #[rustfmt::skip]
        let mut expected = format!(
            "{}={}&{}={}&{}={}&{}={}",
            QueryParam::EventType.as_str(), EVENT_TYPE,
            QueryParam::GoalId.as_str(), goal_id,
            QueryParam::Revenue.as_str(), revenue,
            QueryParam::Negative.as_str(), negative,
        );
        if let Some(encoded_metadata) = encoded_metadata {
            expected.push_str(&format!("&{}={encoded_metadata}", QueryParam::Metadata.as_str()));
        }
        let c = Conversion::new_with_opts(
            goal_id,
            ConversionOpts::new().revenue(revenue).negative(negative).metadata(metadata),
        );
        // act
        let actual = c.encode_query();
        // assert
        let actual = remove_nonce_parameter(&actual);
        assert_eq!(expected, actual);
    }
}
