use std::collections::{HashMap, HashSet};

use kameleoon_trace_logging::trace_method;

use crate::{
    logging::{KameleoonLogger, LogLevel},
    network::dto::CustomDataDto,
};

const SCOPE_VISITOR: &str = "VISITOR";

#[cfg_attr(test, mockall::automock)]
pub(crate) trait CustomDataInfo {
    fn is_local_only(&self, index: u32) -> bool;
    fn is_visitor_scope(&self, index: u32) -> bool;
    fn get_custom_data_index_by_id(&self, custom_data_id: u32) -> Option<u32>;
    fn get_custom_data_index_by_name(&self, custom_data_name: &str) -> Option<u32>;
    fn mapping_identifier_index(&self) -> Option<u32>;
}

#[derive(Debug, Clone)]
pub(crate) struct CustomDataInfoImpl {
    local_only: HashSet<u32>,
    visitor_scope: HashSet<u32>,
    custom_data_index_by_id: HashMap<u32, u32>,
    custom_data_index_by_name: HashMap<String, u32>,
    mapping_identifier_index: Option<u32>,
}

impl CustomDataInfo for CustomDataInfoImpl {
    #[trace_method(LogLevel::Debug, skip(self))]
    fn is_local_only(&self, index: u32) -> bool {
        self.local_only.contains(&index)
    }

    #[trace_method(LogLevel::Debug, skip(self))]
    fn is_visitor_scope(&self, index: u32) -> bool {
        self.visitor_scope.contains(&index)
    }

    #[trace_method(LogLevel::Debug, skip(self))]
    fn get_custom_data_index_by_id(&self, custom_data_id: u32) -> Option<u32> {
        self.custom_data_index_by_id.get(&custom_data_id).copied()
    }

    fn mapping_identifier_index(&self) -> Option<u32> {
        self.mapping_identifier_index
    }

    #[trace_method(LogLevel::Debug, skip(self))]
    fn get_custom_data_index_by_name(&self, custom_data_name: &str) -> Option<u32> {
        self.custom_data_index_by_name.get(custom_data_name).copied()
    }
}

impl CustomDataInfoImpl {
    #[trace_method(LogLevel::Debug)]
    pub fn new(custom_data: Vec<CustomDataDto>) -> Self {
        custom_data.into_iter().fold(
            Self {
                local_only: HashSet::new(),
                visitor_scope: HashSet::new(),
                custom_data_index_by_id: HashMap::new(),
                custom_data_index_by_name: HashMap::new(),
                mapping_identifier_index: None,
            },
            |mut acc, data| {
                // local only
                if data.local_only {
                    acc.local_only.insert(data.index);
                }
                // visitor scope
                if data.scope == SCOPE_VISITOR {
                    acc.visitor_scope.insert(data.index);
                }
                // index by id
                if data.id != u32::MAX {
                    acc.custom_data_index_by_id.insert(data.id, data.index);
                }
                // index by name
                acc.custom_data_index_by_name.insert(data.name, data.index);
                // mapping identifier
                if data.is_mapping_identifier {
                    if acc.mapping_identifier_index.is_some() {
                        KameleoonLogger::warn(
                            "More than one mapping identifier is set. Undefined behavior may occur on
                            cross-device reconciliation.",
                        );
                    }
                    acc.mapping_identifier_index = Some(data.index);
                }
                acc
            },
        )
    }
}

#[cfg(test)]
mod tests {
    use crate::datafile::datafile_test_utils::make_custom_data_dto;

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

    #[rstest]
    #[case(vec![], 0, false, false, None)] // empty
    #[case(vec![make_custom_data_dto(1, 10, "", true, false, false)], 10, true, false, None)]
    #[case(vec![make_custom_data_dto(2, 20, "", false, true, false)], 20, false, true, None)]
    #[case(vec![make_custom_data_dto(3, 30, "", false, false, true)], 30, false, false, Some(30))]
    fn test_custom_data_info(
        #[case] dtos: Vec<CustomDataDto>,
        #[case] index: u32,
        #[case] local_only: bool,
        #[case] visitor_scope: bool,
        #[case] mapping_identifier_index: Option<u32>,
    ) {
        let info = CustomDataInfoImpl::new(dtos);

        assert_eq!(info.is_local_only(index), local_only);
        assert_eq!(info.is_visitor_scope(index), visitor_scope);
        assert_eq!(info.mapping_identifier_index(), mapping_identifier_index);
    }

    #[test]
    fn test_get_custom_data_index_by_id() {
        let dtos = vec![
            make_custom_data_dto(1, 10, "", true, false, false),
            make_custom_data_dto(2, 20, "", false, true, false),
        ];
        let info = CustomDataInfoImpl::new(dtos);

        assert_eq!(info.get_custom_data_index_by_id(1), Some(10));
        assert_eq!(info.get_custom_data_index_by_id(2), Some(20));
        assert_eq!(info.get_custom_data_index_by_id(3), None);
    }

    #[rstest]
    #[case(vec![make_custom_data_dto(1, 10, "", false, false, true)], 10)]
    #[case(
        vec![make_custom_data_dto(1, 10, "", false, false, true), make_custom_data_dto(2, 20, "", false, false, true)],
        20
    )]
    fn test_mapping_identifier_cases(#[case] dtos: Vec<CustomDataDto>, #[case] expected_index: u32) {
        let info = CustomDataInfoImpl::new(dtos);
        assert_eq!(info.mapping_identifier_index(), Some(expected_index));
    }

    #[test]
    fn test_get_custom_data_index_by_name() {
        let dtos = vec![
            make_custom_data_dto(1, 10, "first", true, false, false),
            make_custom_data_dto(2, 20, "second", false, true, false),
        ];
        let info = CustomDataInfoImpl::new(dtos);

        assert_eq!(info.get_custom_data_index_by_name("first"), Some(10));
        assert_eq!(info.get_custom_data_index_by_name("second"), Some(20));
        assert_eq!(info.get_custom_data_index_by_name("third"), None);
    }
}
