use std::{collections::HashMap, fmt::Debug};

use crate::{
    datafile::{custom_data_info::CustomDataInfo, rule::Rule, variation::Variation},
    network::dto::FeatureFlagDto,
};

pub(crate) trait FeatureFlag: Send + Sync + Debug {
    type Variation: Variation;
    type Rule: Rule;

    fn new<C: CustomDataInfo + 'static>(feature_flag_dto: FeatureFlagDto, cdi: &C) -> Self;

    fn id(&self) -> u32;
    fn feature_key(&self) -> &str;
    fn is_environment_enabled(&self) -> bool;
    fn default_variation_key(&self) -> &str;
    fn me_group_name(&self) -> Option<&str>;
    fn bucketing_custom_data_index(&self) -> Option<u32>;
    fn rules(&self) -> &[Self::Rule];
    fn variations(&self) -> &HashMap<String, Self::Variation>;

    fn get_variation(&self, key: &str) -> Option<&Self::Variation>;
    fn get_rule(&self, rule_id: u32) -> Option<&Self::Rule>;
}

#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub(crate) struct FeatureFlagImpl<V: Variation, R: Rule> {
    id: u32,
    feature_key: String,
    environment_enabled: bool,
    default_variation_key: String,
    me_group_name: Option<String>,
    bucketing_custom_data_index: Option<u32>,
    rules: Vec<R>,
    variations: HashMap<String, V>,
}

impl<V: Variation, R: Rule> FeatureFlag for FeatureFlagImpl<V, R> {
    type Variation = V;
    type Rule = R;

    fn new<C: CustomDataInfo + 'static>(feature_flag_dto: FeatureFlagDto, cdi: &C) -> Self {
        let bucketing_custom_data_index =
            feature_flag_dto.bucketing_custom_data_id.and_then(|id| cdi.get_custom_data_index_by_id(id));
        let rules = feature_flag_dto.rules.into_iter().map(R::new).collect::<Vec<_>>();
        let variations = feature_flag_dto
            .variations
            .into_iter()
            .map(V::new)
            .map(|v| (v.key().to_string(), v))
            .collect::<HashMap<_, _>>();
        Self {
            id: feature_flag_dto.id,
            feature_key: feature_flag_dto.feature_key,
            environment_enabled: feature_flag_dto.environment_enabled,
            default_variation_key: feature_flag_dto.default_variation_key,
            me_group_name: feature_flag_dto.mutually_exclusive_group,
            bucketing_custom_data_index,
            rules,
            variations,
        }
    }

    fn id(&self) -> u32 {
        self.id
    }

    fn feature_key(&self) -> &str {
        &self.feature_key
    }

    fn is_environment_enabled(&self) -> bool {
        self.environment_enabled
    }

    fn default_variation_key(&self) -> &str {
        &self.default_variation_key
    }

    fn me_group_name(&self) -> Option<&str> {
        self.me_group_name.as_deref()
    }

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

    fn rules(&self) -> &[R] {
        &self.rules
    }

    fn variations(&self) -> &HashMap<String, V> {
        &self.variations
    }

    fn get_variation(&self, key: &str) -> Option<&V> {
        self.variations.get(key)
    }

    fn get_rule(&self, rule_id: u32) -> Option<&R> {
        self.rules.iter().find(|r| r.id() == rule_id)
    }
}

// MARK: - Tests

#[cfg(test)]
mod tests {
    use super::*;
    use crate::datafile::custom_data_info::CustomDataInfoImpl;
    use crate::datafile::datafile_test_utils::{make_experiment_dto, make_variable_dto};
    use crate::datafile::rule::RuleImpl;
    use crate::datafile::variation::VariationImpl;
    use crate::network::dto::CustomDataDto;
    use rstest::rstest;
    use serde_json::Value;

    use crate::{
        datafile::{
            datafile_test_utils::{
                make_feature_flag_dto, make_rule_dto, make_variation_dto, make_variation_exposition_dto,
            },
            variable::VariableImpl,
        },
        utils::test_utils::generate_test_string,
    };

    #[rstest(case(true), case(false))]
    fn test_feature_flag_impl_creation(#[case] environment_enabled: bool) {
        // Arrange
        let id: u32 = getrandom::u32().unwrap();
        let feature_key = generate_test_string(5);
        let default_variation_key = generate_test_string(5);
        let me_group_name = Some(generate_test_string(5));

        fn random_f64_0_1() -> f64 {
            let x = getrandom::u64().unwrap();
            x as f64 / u64::MAX as f64
        }

        let variations_expo = vec![
            make_variation_exposition_dto(&generate_test_string(5), getrandom::u32().unwrap(), random_f64_0_1()),
            make_variation_exposition_dto(&generate_test_string(5), getrandom::u32().unwrap(), random_f64_0_1()),
        ];

        let rules = vec![
            make_rule_dto(
                getrandom::u32().unwrap(),
                // rng.random(),
                "EXPERIMENTATION",
                random_f64_0_1(),
                make_experiment_dto(getrandom::u32().unwrap(), variations_expo.clone()),
                Some(getrandom::u64().unwrap()),
                Some(getrandom::u32().unwrap()),
            ),
            make_rule_dto(
                getrandom::u32().unwrap(),
                // rng.random(),
                "TARGETED_DELIVERY",
                random_f64_0_1(),
                make_experiment_dto(getrandom::u32().unwrap(), variations_expo.clone()),
                Some(getrandom::u64().unwrap()),
                None,
            ),
        ];
        let expected_rules: Vec<RuleImpl> = rules.iter().map(|r| RuleImpl::new((*r).clone())).collect();

        let variations = vec![
            make_variation_dto(&generate_test_string(3), vec![]),
            make_variation_dto(
                &generate_test_string(3),
                vec![make_variable_dto(&generate_test_string(3), "STRING", Value::String(generate_test_string(3)))],
            ),
        ];
        let expected_variations = variations
            .clone()
            .into_iter()
            .map(VariationImpl::new)
            .map(|v| (v.key().to_string(), v))
            .collect::<HashMap<_, _>>();

        let dto = make_feature_flag_dto(
            id,
            &feature_key,
            environment_enabled,
            &default_variation_key,
            me_group_name.as_deref(),
            None,
            rules,
            variations,
        );
        let cdi = CustomDataInfoImpl::new(vec![]);

        // Act
        let flag: FeatureFlagImpl<VariationImpl<VariableImpl>, RuleImpl> = FeatureFlagImpl::new(dto, &cdi);

        assert_eq!(id, flag.id());
        assert_eq!(feature_key, flag.feature_key());
        assert_eq!(environment_enabled, flag.is_environment_enabled());
        assert_eq!(default_variation_key, flag.default_variation_key());
        assert_eq!(me_group_name.as_deref(), flag.me_group_name());
        assert_eq!(None, flag.bucketing_custom_data_index());

        assert_eq!(expected_rules.as_slice(), flag.rules());
        assert_eq!(&expected_variations, flag.variations());
    }

    fn make_test_feature_flag() -> FeatureFlagImpl<VariationImpl<VariableImpl>, RuleImpl> {
        let variations = vec![make_variation_dto("A", vec![]), make_variation_dto("B", vec![])];

        let exp_201 = make_experiment_dto(201, vec![]);
        let exp_202 = make_experiment_dto(201, vec![]);
        let rules = vec![
            make_rule_dto(101, /*1,*/ "EXPERIMENTATION", 0.5, exp_201, None, None),
            make_rule_dto(102, /*2,*/ "TARGETED_DELIVERY", 0.75, exp_202, None, None),
        ];

        let dto = make_feature_flag_dto(42, "feature-key", true, "A", Some("me_group"), None, rules, variations);
        let cdi = CustomDataInfoImpl::new(vec![]);

        FeatureFlagImpl::new(dto, &cdi)
    }

    fn make_cd_dto(id: u32, index: u32) -> CustomDataDto {
        CustomDataDto {
            id,
            index,
            name: "CD".to_owned(),
            local_only: false,
            is_mapping_identifier: false,
            scope: "PAGE".to_owned(),
        }
    }

    #[rstest(case("A", true), case("B", true), case("C", false))]
    fn test_get_variation(#[case] key: &str, #[case] expected: bool) {
        let flag = make_test_feature_flag();
        let result = flag.get_variation(key);
        assert_eq!(result.is_some(), expected);
    }

    #[rstest(case(101, true), case(102, true), case(999, false))]
    fn test_get_rule(#[case] rule_id: u32, #[case] expected: bool) {
        let flag = make_test_feature_flag();
        let result = flag.get_rule(rule_id);
        assert_eq!(result.is_some(), expected);
    }

    #[rstest]
    #[case(None, vec![], None)]
    #[case(Some(123), vec![], None)]
    #[case(Some(123), vec![make_cd_dto(234, 345)], None)]
    #[case(Some(234), vec![make_cd_dto(234, 345)], Some(345))]
    fn test_bucketing_custom_data_index(
        #[case] bucketing_cd_id: Option<u32>,
        #[case] cd_dtos: Vec<CustomDataDto>,
        #[case] expected_bucketing_cd_index: Option<u32>,
    ) {
        let dto = make_feature_flag_dto(1, "ff1", true, "off", None, bucketing_cd_id, vec![], vec![]);
        let cdi = CustomDataInfoImpl::new(cd_dtos);
        let ff = FeatureFlagImpl::<VariationImpl<VariableImpl>, RuleImpl>::new(dto, &cdi);
        assert_eq!(expected_bucketing_cd_index, ff.bucketing_custom_data_index());
    }
}
