use std::{fmt::Debug, sync::Arc};

use kameleoon_trace_logging::trace_method;

use crate::{
    data::simulated_variation::SimulatedVariation,
    datafile::{
        datafile::DataFile,
        experiment::Experiment,
        feature_flag::FeatureFlag,
        rule::{Rule, RuleType},
        variation_exposition::VariationByExposition,
    },
    error::{ErrorCode, KErr},
    logging::LogLevel,
    utils::KRes,
};

#[cfg(test)]
use crate::data::forced_variation::ForcedVariation;

#[derive(Debug)]
pub(crate) struct EvaluatedExperiment<'a, V: Debug, E: Debug> {
    pub var_by_exp: &'a Arc<V>,
    pub experiment: &'a E,
    pub rule_type: RuleType,
}

impl<'a, V, E> EvaluatedExperiment<'a, V, E>
where
    V: VariationByExposition,
    E: Experiment<VariationByExposition = V> + 'static,
{
    #[trace_method(LogLevel::Debug)]
    pub fn from_var_by_exp_and_rule<R: Rule<Experiment = E>>(var_by_exp: &'a Arc<V>, rule: &'a R) -> Self {
        Self {
            var_by_exp,
            experiment: rule.experiment(),
            rule_type: rule.rule_type(),
        }
    }

    #[trace_method(LogLevel::Debug, skip(datafile))]
    #[cfg(test)]
    pub fn from_forced_variation<D, F, R>(datafile: &'a D, forced_var: &ForcedVariation) -> KRes<Self>
    where
        D: DataFile<FeatureFlag = F>,
        F: FeatureFlag<Rule = R> + 'static,
        R: Rule<Experiment = E> + 'static,
    {
        let ff = datafile.get_feature_flag(&forced_var.feature_key)?;
        let rule = Self::get_rule(ff, forced_var.rule_id)?;
        let var_by_exp = Self::get_var_by_exp(rule, forced_var.variation_id)?;
        return Ok(Self::from_var_by_exp_and_rule(var_by_exp, rule));
    }

    #[trace_method(LogLevel::Debug)]
    pub fn from_rule_and_variation_id<R: Rule<Experiment = E>>(rule: &'a R, variation_id: u32) -> KRes<Self> {
        let var_by_exp = Self::get_var_by_exp(rule, variation_id)?;
        return Ok(Self::from_var_by_exp_and_rule(var_by_exp, rule));
    }

    #[trace_method(LogLevel::Debug, skip(datafile))]
    pub fn from_simulated_variation<D, F, R>(datafile: &'a D, simulated_var: &SimulatedVariation) -> KRes<Option<Self>>
    where
        D: DataFile<FeatureFlag = F>,
        F: FeatureFlag<Rule = R> + 'static,
        R: Rule<Experiment = E> + 'static,
    {
        let tar = match simulated_var.targeting_info() {
            Some(tar) => tar,
            None => return Ok(None),
        };
        let ff = datafile.get_feature_flag(simulated_var.feature_key())?;
        let rule = Self::get_rule(ff, tar.rule_id)?;
        let var_by_exp = Self::get_var_by_exp(rule, tar.variation_id)?;
        return Ok(Some(Self::from_var_by_exp_and_rule(var_by_exp, rule)));
    }

    fn get_rule<F, R>(ff: &Arc<F>, rule_id: u32) -> KRes<&R>
    where
        F: FeatureFlag<Rule = R> + 'static,
        R: Rule<Experiment = E> + 'static,
    {
        ff.get_rule(rule_id).map_or_else(
            || {
                let msg = format!("Rule {} not found for '{}' FF", rule_id, ff.feature_key());
                Err(KErr::new(ErrorCode::FeatureRuleNotFound, msg))
            },
            |rule| Ok(rule),
        )
    }

    fn get_var_by_exp<R: Rule<Experiment = E>>(rule: &R, variation_id: u32) -> KRes<&Arc<V>> {
        rule.experiment().get_variation_by_id(variation_id).map_or_else(
            || {
                let msg = format!("VarByExp {} not found for {} Rule", variation_id, rule.id());
                Err(KErr::new(ErrorCode::FeatureVariationNotFound, msg))
            },
            |var_by_exp| Ok(var_by_exp),
        )
    }
}

// MARK: - Tests

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        data::simulated_variation::SimulatedTargetingInfo,
        datafile::{
            datafile_test_utils::{MockExperiment, MockRule},
            rule::RuleImpl,
            variation_exposition::MockVariationByExposition,
        },
        network::dto::{
            ConfigurationDto, DataFileDto, ExperimentDto, FeatureFlagDto, RuleDto, VariationByExpositionDto,
        },
        utils::test_utils::TestDataFile,
    };
    use rstest::rstest;

    fn make_rule(rule_type: RuleType) -> MockRule {
        let mut rule = MockRule::default();
        rule.expect_experiment().return_const(MockExperiment::default());
        rule.expect_rule_type().returning(move || rule_type);
        return rule;
    }

    fn make_df_dto(feature_flags: Vec<FeatureFlagDto>) -> DataFileDto {
        DataFileDto {
            configuration: ConfigurationDto::default(),
            feature_flags,
            custom_data: vec![],
            segments: vec![],
            date_modified: 0,
            holdout: None,
        }
    }

    fn make_ff_dto(id: u32, feature_key: &str, environment_enabled: bool, rules: Vec<RuleDto>) -> FeatureFlagDto {
        FeatureFlagDto {
            id,
            feature_key: feature_key.to_owned(),
            environment_enabled,
            variations: vec![],
            default_variation_key: "off".to_owned(),
            rules,
            mutually_exclusive_group: None,
            bucketing_custom_data_id: None,
        }
    }

    fn make_rule_dto(rule_id: u32, exp_id: u32, exp_type: bool, vars_by_exp: Vec<VariationByExpositionDto>) -> RuleDto {
        RuleDto {
            // order: 0,
            id: rule_id,
            rule_type: if exp_type { "EXPERIMENTATION" } else { "TARGETED_DELIVERY" }.to_owned(),
            segment_id: None,
            exposition: 0.0,
            respool_time: None,
            experiment: ExperimentDto {
                id: exp_id,
                variation_by_exposition: vars_by_exp,
            },
        }
    }

    #[rstest(case(RuleType::Experimentation), case(RuleType::TargetedDelivery))]
    fn test_from_var_by_exp_and_rule(#[case] rule_type: RuleType) {
        // arrange
        let var_by_exp = Arc::new(MockVariationByExposition::default());
        let rule = make_rule(rule_type);
        // act
        let eval_exp = EvaluatedExperiment::from_var_by_exp_and_rule(&var_by_exp, &rule);
        // assert
        assert_eq!(&var_by_exp as *const _, eval_exp.var_by_exp as *const _);
        assert_eq!(rule.experiment() as *const _, eval_exp.experiment as *const _);
        assert_eq!(rule_type, eval_exp.rule_type);
    }

    fn assert_eval_exp<'a, V, E>(expected: (u32, u32, RuleType), actual: EvaluatedExperiment<'a, V, E>)
    where
        V: VariationByExposition,
        E: Experiment<VariationByExposition = V> + 'static,
    {
        assert_eq!(expected.0, actual.var_by_exp.variation_id());
        assert_eq!(expected.1, actual.experiment.id());
        assert_eq!(expected.2, actual.rule_type);
    }

    #[rstest]
    #[case(
        make_df_dto(vec![]),
        ForcedVariation::new("ff1".to_owned(), 1001, 2001, 3001, false),
        Err(ErrorCode::FeatureNotFound)
    )]
    #[case(
        make_df_dto(vec![make_ff_dto(1, "ff1", false, vec![])]),
        ForcedVariation::new("ff1".to_owned(), 1001, 2001, 3001, false),
        Err(ErrorCode::FeatureEnvironmentDisabled)
    )]
    #[case(
        make_df_dto(vec![make_ff_dto(1, "ff1", true, vec![])]),
        ForcedVariation::new("ff1".to_owned(), 1001, 2001, 3001, false),
        Err(ErrorCode::FeatureRuleNotFound)
    )]
    #[case(
        make_df_dto(vec![make_ff_dto(1, "ff1", true, vec![make_rule_dto(1001, 2001, true, vec![])])]),
        ForcedVariation::new("ff1".to_owned(), 1001, 2001, 3001, false),
        Err(ErrorCode::FeatureVariationNotFound)
    )]
    #[case(
        make_df_dto(vec![make_ff_dto(1, "ff1", true, vec![make_rule_dto(1001, 2001, true, vec![
            VariationByExpositionDto {
                variation_key: "var1".to_owned(),
                variation_id: 3001,
                exposition: 0.0,
            },
        ])])]),
        ForcedVariation::new("ff1".to_owned(), 1001, 2001, 3001, false),
        Ok((3001, 2001, RuleType::Experimentation))
    )]
    #[case(
        make_df_dto(vec![make_ff_dto(2, "ff2", true, vec![make_rule_dto(1002, 2002, false, vec![
            VariationByExpositionDto {
                variation_key: "var2".to_owned(),
                variation_id: 3002,
                exposition: 0.0,
            },
        ])])]),
        ForcedVariation::new("ff2".to_owned(), 1002, 2002, 3002, true),
        Ok((3002, 2002, RuleType::TargetedDelivery))
    )]
    fn test_from_forced_variation(
        #[case] df_dto: DataFileDto,
        #[case] forced_var: ForcedVariation,
        #[case] expected: Result<(u32, u32, RuleType), ErrorCode>,
    ) {
        let datafile = TestDataFile::new(df_dto, None, None);
        let actual = EvaluatedExperiment::from_forced_variation(&datafile, &forced_var);
        match expected {
            Err(expected_err) => assert_eq!(expected_err, actual.err().unwrap().code()),
            Ok(expected) => assert_eval_exp(expected, actual.unwrap()),
        }
    }

    #[rstest]
    #[case(make_rule_dto(1001, 2001, true, vec![]), 3001, Err(ErrorCode::FeatureVariationNotFound))]
    #[case(
        make_rule_dto(1001, 2001, true, vec![VariationByExpositionDto {
            variation_key: "var1".to_owned(),
            variation_id: 3001,
            exposition: 0.0,
        }]), 3001, Ok((3001, 2001, RuleType::Experimentation))
    )]
    #[case(
        make_rule_dto(1002, 2002, false, vec![VariationByExpositionDto {
            variation_key: "var2".to_owned(),
            variation_id: 3002,
            exposition: 0.0,
        }]), 3002, Ok((3002, 2002, RuleType::TargetedDelivery))
    )]
    fn test_from_rule_and_variation_id(
        #[case] rule_dto: RuleDto,
        #[case] var_id: u32,
        #[case] expected: Result<(u32, u32, RuleType), ErrorCode>,
    ) {
        let rule = RuleImpl::new(rule_dto);
        let actual = EvaluatedExperiment::from_rule_and_variation_id(&rule, var_id);
        match expected {
            Err(expected_err) => assert_eq!(expected_err, actual.err().unwrap().code()),
            Ok(expected) => assert_eval_exp(expected, actual.unwrap()),
        }
    }

    #[rstest]
    #[case(
        make_df_dto(vec![]),
        SimulatedVariation::new(None, "ff1".to_owned()),
        Ok(None)
    )]
    #[case(
        make_df_dto(vec![]),
        SimulatedVariation::new(Some(SimulatedTargetingInfo::new(1001, 3001)), "ff1".to_owned()),
        Err(ErrorCode::FeatureNotFound)
    )]
    #[case(
        make_df_dto(vec![make_ff_dto(1, "ff1", false, vec![])]),
        SimulatedVariation::new(Some(SimulatedTargetingInfo::new(1001, 3001)), "ff1".to_owned()),
        Err(ErrorCode::FeatureEnvironmentDisabled)
    )]
    #[case(
        make_df_dto(vec![make_ff_dto(1, "ff1", true, vec![])]),
        SimulatedVariation::new(Some(SimulatedTargetingInfo::new(1001, 3001)), "ff1".to_owned()),
        Err(ErrorCode::FeatureRuleNotFound)
    )]
    #[case(
        make_df_dto(vec![make_ff_dto(1, "ff1", true, vec![make_rule_dto(1001, 2001, true, vec![])])]),
        SimulatedVariation::new(Some(SimulatedTargetingInfo::new(1001, 3001)), "ff1".to_owned()),
        Err(ErrorCode::FeatureVariationNotFound)
    )]
    #[case(
        make_df_dto(vec![make_ff_dto(1, "ff1", true, vec![make_rule_dto(1001, 2001, true, vec![
            VariationByExpositionDto {
                variation_key: "var1".to_owned(),
                variation_id: 3001,
                exposition: 0.0,
            },
        ])])]),
        SimulatedVariation::new(Some(SimulatedTargetingInfo::new(1001, 3001)), "ff1".to_owned()),
        Ok(Some((3001, 2001, RuleType::Experimentation)))
    )]
    #[case(
        make_df_dto(vec![make_ff_dto(2, "ff2", true, vec![make_rule_dto(1002, 2002, false, vec![
            VariationByExpositionDto {
                variation_key: "var2".to_owned(),
                variation_id: 3002,
                exposition: 0.0,
            },
        ])])]),
        SimulatedVariation::new(Some(SimulatedTargetingInfo::new(1002, 3002)), "ff2".to_owned()),
        Ok(Some((3002, 2002, RuleType::TargetedDelivery)))
    )]
    fn test_from_simulated_variation(
        #[case] df_dto: DataFileDto,
        #[case] simulated_var: SimulatedVariation,
        #[case] expected: Result<Option<(u32, u32, RuleType)>, ErrorCode>,
    ) {
        let datafile = TestDataFile::new(df_dto, None, None);
        let actual = EvaluatedExperiment::from_simulated_variation(&datafile, &simulated_var);
        match expected {
            Err(expected_err) => assert_eq!(expected_err, actual.err().unwrap().code()),
            Ok(expected) => {
                let actual = actual.ok().unwrap();
                match expected {
                    Some(expected) => assert_eval_exp(expected, actual.unwrap()),
                    None => assert!(actual.is_none()),
                }
            }
        }
    }
}
