use std::sync::Arc;

use crate::{
    data::{BaseData, CustomData},
    datafile::{custom_data_info::CustomDataInfo, datafile::DataFile, settings::Settings},
    debug_log, info_log,
    network::query_builder::{QueryBuilder, QueryParam},
    storage::{
        visitor::Visitor,
        visitor_manager::{AddDataOptions, VisitorManager},
    },
    tracking::{
        activity_event::ActivityEvent, data_adding_context::DataAddingContext, trackable_data_selection::*,
        tracking_data::TrackingData,
    },
};

pub(crate) struct TrackingBuilder<D, V> {
    datafile: Arc<D>,
    visitor_manager: Arc<V>,
    request_size_limit: usize,
    total_size: usize,
    result: TrackingData,
}

impl<D: DataFile, V: VisitorManager> TrackingBuilder<D, V> {
    pub fn new(datafile: Arc<D>, visitor_manager: Arc<V>, request_size_limit: usize) -> Self {
        Self {
            datafile,
            visitor_manager,
            request_size_limit,
            total_size: 0,
            result: TrackingData::new(),
        }
    }

    pub fn build(mut self, visitor_codes: Vec<String>) -> TrackingData {
        self.result.reserve_approximately(self.request_size_limit, visitor_codes.len());
        for visitor_code in visitor_codes.into_iter() {
            if self.total_size <= self.request_size_limit {
                let visitor = self.visitor_manager.peek(&visitor_code);
                let is_consent_given = self.is_consent_given(&visitor);
                let added_count = self.add_visitor_tracking_data(&visitor_code, visitor, is_consent_given);
                if added_count > 0 {
                    debug_log!(
                        "Sending tracking request for unsent data {:?} of visitor '{}' \
                        with given (or not required) consent {}",
                        &self.result.tracking_lines[self.result.tracking_lines.len() - added_count..],
                        visitor_code,
                        is_consent_given
                    );
                    self.result.visitor_codes_to_send.push(visitor_code);
                } else {
                    debug_log!(
                        "No data to send for visitor '{}' with given (or not required) consent {}",
                        visitor_code,
                        is_consent_given
                    );
                }
            } else {
                self.result.visitor_codes_to_keep.push(visitor_code);
            }
        }
        return self.result;
    }

    fn is_consent_given(&self, visitor: &Option<Arc<Visitor>>) -> bool {
        !self.datafile.settings().consent_required() || visitor.as_ref().map_or(false, |v| v.rdata().legal_consent)
    }

    fn add_visitor_tracking_data(
        &mut self,
        visitor_code: &str,
        mut visitor: Option<Arc<Visitor>>,
        is_consent_given: bool,
    ) -> usize {
        let use_mapping_value = self.create_self_visitor_link_if_required(visitor_code, &mut visitor);
        info_log!(
            "'{visitor_code}' was used as a {} for visitor data tracking.",
            if use_mapping_value { "mapping value" } else { "visitor code" }
        );
        let visitor_code_param = make_visitor_code_param(visitor_code, use_mapping_value);
        let (size, count) = if is_consent_given {
            self.add_all_unsent_visitor_data(visitor, &visitor_code_param)
        } else {
            self.add_technical_unsent_visitor_data(visitor, &visitor_code_param)
        };
        self.total_size += size;
        count
    }

    fn create_self_visitor_link_if_required(&self, visitor_code: &str, visitor: &mut Option<Arc<Visitor>>) -> bool {
        let (is_mapped, is_unique_identifier) = visitor.as_ref().map_or((false, false), |visitor| {
            (visitor.rdata().mapping_identifier().is_some(), visitor.is_unique_identifier())
        });
        // Need to find if anonymous visitor is behind unique (anonym doesn't exist if MappingIdentifier == null)
        if is_unique_identifier && !is_mapped {
            // We haven't anonymous behind, in this case we should create "fake" anonymous with id == visitorCode
            // and link it with with mapping value == visitorCode (like we do as we have real anonymous visitor)
            let mapping_index = self.datafile.custom_data_info().mapping_identifier_index();
            if let Some(mapping_index) = mapping_index {
                let mi_cd = BaseData::CustomData(CustomData::new_with_index(mapping_index, vec![visitor_code.to_owned()]));
                *visitor = Some(self.visitor_manager.add_data(
                    visitor_code,
                    std::iter::once(mi_cd),
                    AddDataOptions {
                        overwrite: true,
                        track: true,
                    },
                ));
            }
        }
        let use_mapping_value = is_unique_identifier
            && visitor.as_ref().map_or(false, |visitor| Some(visitor_code) != visitor.rdata().mapping_identifier());
        return use_mapping_value;
    }

    fn add_all_unsent_visitor_data(
        &mut self,
        visitor: Option<Arc<Visitor>>,
        visitor_code_param: &str,
    ) -> (usize, usize) {
        let visitor_data = visitor.map(|visitor| visitor.data().clone());
        let visitor_rdata = visitor_data.as_ref().map(|visitor_data| visitor_data.read());
        let user_agent = visitor_rdata.as_ref().map_or(None, |visitor_rdata| visitor_rdata.user_agent.r().get());
        let mut context = DataAddingContext::new(&mut self.result, visitor_code_param, user_agent);
        if let Some(visitor_rdata) = &visitor_rdata {
            select_all_unsent_trackable_data(&mut context, visitor_rdata);
        }
        if context.count() == 0 {
            context.add_data(&ActivityEvent::new());
        }
        (context.size(), context.count())
    }

    // Only technical data is tracked due to GDPR restrictions.
    fn add_technical_unsent_visitor_data(
        &mut self,
        visitor: Option<Arc<Visitor>>,
        visitor_code_param: &str,
    ) -> (usize, usize) {
        visitor.map_or((0, 0), |visitor| {
            let visitor_data = visitor.rdata();
            let user_agent = visitor_data.user_agent.r().get();
            let mut context = DataAddingContext::new(&mut self.result, visitor_code_param, user_agent);
            select_technical_unsent_trackable_data(&mut context, &visitor_data);
            (context.size(), context.count())
        })
    }
}

fn make_visitor_code_param(visitor_code: &str, use_mapping_value: bool) -> String {
    QueryBuilder::with_params(vec![(
        if use_mapping_value { QueryParam::MappingValue } else { QueryParam::VisitorCode },
        visitor_code.into(),
    )])
    .build()
}

// MARK: - Tests

#[cfg(test)]
mod tests {
    use crate::{
        data::{Device, DeviceKind, UniqueIdentifier},
        datafile::{datafile::DataFileImpl, datafile_manager::MockDataFileManager},
        network::dto::{ConfigurationDto, CustomDataDto, DataFileDto},
        storage::visitor_manager::VisitorManagerImpl,
        tracking::trackable::Trackable,
        utils::test_utils,
    };

    use super::*;
    use arc_swap::ArcSwap;
    use rstest::rstest;

    const BIG_REQUEST_SIZE_LIMIT: usize = 1024;

    fn make_mi_cdi(index: u32) -> CustomDataDto {
        CustomDataDto {
            id: 1,
            index,
            name: "".to_owned(),
            local_only: false,
            is_mapping_identifier: true,
            scope: "".to_owned(),
        }
    }

    fn add_uid_param(data: impl Trackable, vc_param: QueryParam, visitor_code: &str) -> String {
        let mut line = data.encode_query();
        line.push('&');
        line.push_str(vc_param.as_str());
        line.push('=');
        line.push_str(visitor_code);
        return line;
    }

    fn make_device() -> Device {
        Device::new(DeviceKind::Desktop)
    }

    fn make_wrapped_device() -> BaseData {
        BaseData::Device(make_device())
    }

    fn make_cd() -> CustomData {
        CustomData::new_with_index(123, vec!["321".to_owned()])
    }

    fn make_wrapped_cd() -> BaseData {
        BaseData::CustomData(make_cd())
    }

    fn make_mi(index: u32, vc: &str) -> CustomData {
        CustomData::new_with_index(index, vec![vc.to_owned()])
    }

    fn make_wrapped_mi(index: u32, vc: &str) -> BaseData {
        BaseData::CustomData(make_mi(index, vc))
    }

    fn make_ui() -> BaseData {
        BaseData::UniqueIdentifier(UniqueIdentifier::new(true))
    }

    #[rstest]
    // Test general logic
    #[case( // 1. no visitors, no data
        BIG_REQUEST_SIZE_LIMIT,
        vec![],
        vec![],
        vec![],
        false,
        vec![], vec![],
        vec![]
    )]
    #[case( // 2. no visitors, 1 data
        BIG_REQUEST_SIZE_LIMIT,
        vec![],
        vec![("vc1".to_owned(), vec![make_wrapped_cd()], false)],
        vec![],
        false,
        vec![], vec![],
        vec![]
    )]
    #[case( // 3. 1 visitor, 1 data
        BIG_REQUEST_SIZE_LIMIT,
        vec!["vc1"],
        vec![("vc1".to_owned(), vec![make_wrapped_cd()], false)],
        vec![],
        false,
        vec!["vc1"], vec![],
        vec![add_uid_param(make_cd(), QueryParam::VisitorCode, "vc1")]
    )]
    #[case( // 4. 1 visitor, 2 data
        BIG_REQUEST_SIZE_LIMIT,
        vec!["vc1"],
        vec![("vc1".to_owned(), vec![make_wrapped_device(), make_wrapped_cd()], false)],
        vec![],
        false,
        vec!["vc1"], vec![],
        vec![
            add_uid_param(make_device(), QueryParam::VisitorCode, "vc1"),
            add_uid_param(make_cd(), QueryParam::VisitorCode, "vc1"),
        ]
    )]
    // Test size limit
    #[case( // 5. 2 visitors, 2 to send, 0 to keep
        add_uid_param(make_cd(), QueryParam::VisitorCode, "vc1").len() + 1,
        vec!["vc1", "vc2"],
        vec![
            ("vc1".to_owned(), vec![make_wrapped_cd()], false),
            ("vc2".to_owned(), vec![make_wrapped_device()], false),
        ],
        vec![],
        false,
        vec!["vc1", "vc2"], vec![],
        vec![
            add_uid_param(make_cd(), QueryParam::VisitorCode, "vc1"),
            add_uid_param(make_device(), QueryParam::VisitorCode, "vc2"),
        ]
    )]
    #[case( // 6. 2 visitors, 1 to send, 1 to keep
        add_uid_param(make_cd(), QueryParam::VisitorCode, "vc1").len(),
        vec!["vc1", "vc2"],
        vec![
            ("vc1".to_owned(), vec![make_wrapped_cd()], false),
            ("vc2".to_owned(), vec![make_wrapped_device()], false),
        ],
        vec![],
        false,
        vec!["vc1"], vec!["vc2"],
        vec![add_uid_param(make_cd(), QueryParam::VisitorCode, "vc1")]
    )]
    // Test consent
    #[case( // 7. has consent: consent not required, consent given
        BIG_REQUEST_SIZE_LIMIT,
        vec!["vc1"],
        vec![("vc1".to_owned(), vec![make_wrapped_cd()], true)],
        vec![],
        false,
        vec!["vc1"], vec![],
        vec![add_uid_param(make_cd(), QueryParam::VisitorCode, "vc1")]
    )]
    #[case( // 8. no consent: consent required, consent not given
        BIG_REQUEST_SIZE_LIMIT,
        vec!["vc1"],
        vec![("vc1".to_owned(), vec![make_wrapped_cd()], false)],
        vec![],
        true,
        vec![], vec![],
        vec![]
    )]
    #[case( // 9. has consent: consent required, consent given
        BIG_REQUEST_SIZE_LIMIT,
        vec!["vc1"],
        vec![("vc1".to_owned(), vec![make_wrapped_cd()], true)],
        vec![],
        true,
        vec!["vc1"], vec![],
        vec![add_uid_param(make_cd(), QueryParam::VisitorCode, "vc1")]
    )]
    // Test activity event
    #[case( // 10. no data, has consent => activity event added
        BIG_REQUEST_SIZE_LIMIT,
        vec!["vc1"],
        vec![("vc1".to_owned(), vec![], true)],
        vec![],
        true,
        vec!["vc1"], vec![],
        vec![add_uid_param(ActivityEvent::new(), QueryParam::VisitorCode, "vc1")]
    )]
    #[case( // 11. no data, no consent => no activity event
        BIG_REQUEST_SIZE_LIMIT,
        vec!["vc1"],
        vec![("vc1".to_owned(), vec![], false)],
        vec![],
        true,
        vec![], vec![],
        vec![]
    )]
    // Test user identifier types
    #[case( // 12. not mapped, unique_identifier, no cdi => no self link, used mapping value //~ mapping value?
        BIG_REQUEST_SIZE_LIMIT,
        vec!["vc1"],
        vec![("vc1".to_owned(), vec![make_ui()], false)],
        vec![],
        false,
        vec!["vc1"], vec![],
        vec![add_uid_param(ActivityEvent::new(), QueryParam::MappingValue, "vc1")]
    )]
    #[case( // 13. not mapped, unique_identifier, has cdi => self link created, used visitor code
        BIG_REQUEST_SIZE_LIMIT,
        vec!["vc1"],
        vec![("vc1".to_owned(), vec![make_ui()], false)],
        vec![make_mi_cdi(14)],
        false,
        vec!["vc1"], vec![],
        vec![add_uid_param(make_mi(14, "vc1").into_mapping_identifier(), QueryParam::VisitorCode, "vc1")]
    )]
    #[case( // 14. mapped, not unique_identifier => no self link, used visitor code
        BIG_REQUEST_SIZE_LIMIT,
        vec!["vc1"],
        vec![("vc1".to_owned(), vec![make_wrapped_mi(14, "vc2")], false)],
        vec![make_mi_cdi(14)],
        false,
        vec!["vc1"], vec![],
        vec![add_uid_param(make_mi(14, "vc2").into_mapping_identifier(), QueryParam::VisitorCode, "vc1")]
    )]
    #[case( // 15. mapped, unique_identifier => no self link, used mapping value
        BIG_REQUEST_SIZE_LIMIT,
        vec!["vc1"],
        vec![
            ("vc0".to_owned(), vec![make_ui(), make_wrapped_mi(14, "vc1")], false),
            ("vc1".to_owned(), vec![make_ui()], false),
        ],
        vec![make_mi_cdi(14)],
        false,
        vec!["vc1"], vec![],
        vec![add_uid_param(make_mi(14, "vc1").into_mapping_identifier(), QueryParam::MappingValue, "vc1")]
    )]
    fn test_build(
        #[case] request_size_limit: usize,
        #[case] visitor_codes: Vec<&str>,
        #[case] visitor_data: Vec<(String, Vec<BaseData>, bool)>,
        #[case] cdi: Vec<CustomDataDto>,
        #[case] consent_required: bool,
        #[case] expected_visitor_codes_to_send: Vec<&str>,
        #[case] expected_visitor_codes_to_keep: Vec<&str>,
        #[case] expected_tracking_lines: Vec<String>,
    ) {
        let runtime = tokio::runtime::Runtime::new().unwrap();
        // arrange
        let datafile = Arc::new(DataFileImpl::new(make_datafile_dto(cdi, consent_required), None, None));
        let mut datafile_manager = MockDataFileManager::new();
        let datafile_ = ArcSwap::new(datafile.clone());
        datafile_manager.expect_datafile().returning(move || datafile_.load());
        let datafile_manager = Arc::new(datafile_manager);
        let visitor_manager = Arc::new(VisitorManagerImpl::new(datafile_manager, runtime.handle().clone()));
        for (vc, data, consent_given) in visitor_data.into_iter() {
            visitor_manager.add_data(
                &vc,
                data.into_iter(),
                AddDataOptions {
                    overwrite: true,
                    track: true,
                },
            );
            if consent_given {
                visitor_manager.get(&vc).unwrap().wdata().legal_consent = true;
            }
        }
        let tracking_builder = TrackingBuilder::new(datafile, visitor_manager, request_size_limit);
        // act
        let actual = tracking_builder.build(visitor_codes.into_iter().map(|vc| vc.to_owned()).collect());
        // assert
        assert_eq!(expected_visitor_codes_to_send, actual.visitor_codes_to_send);
        assert_eq!(expected_visitor_codes_to_keep, actual.visitor_codes_to_keep);
        assert_eq!(expected_tracking_lines.len(), actual.unsent_trackable_data.len());
        assert_eq!(remove_nonce_from_lines(&expected_tracking_lines), remove_nonce_from_lines(&actual.tracking_lines));
    }

    fn make_datafile_dto(cdi: Vec<CustomDataDto>, consent_required: bool) -> DataFileDto {
        DataFileDto {
            configuration: ConfigurationDto {
                real_time_update: false,
                consent_type: if consent_required { "REQUIRED" } else { "NOT_REQUIRED" }.to_owned(),
                data_api_domain: "".to_owned(),
                streaming_api_domain: "".to_owned(),
            },
            feature_flags: vec![],
            custom_data: cdi,
            segments: vec![],
            date_modified: 0,
            holdout: None,
        }
    }

    fn remove_nonce_from_lines(lines: &Vec<String>) -> Vec<String> {
        lines.iter().map(|line| test_utils::remove_nonce_parameter(line)).collect()
    }
}
