use kameleoon_trace_logging::trace_method;
use parking_lot::RwLockReadGuard;

use crate::{
    data::assigned_variation::AssignedVariation,
    datafile::{
        datafile::DataFile,
        experiment::Experiment,
        feature_flag::FeatureFlag,
        me_group::MeGroup,
        rule::{Rule, RuleType},
        segment::Segment,
        variation_exposition::VariationByExposition,
    },
    debug_log,
    evaluation::evaluated_experiment::EvaluatedExperiment,
    logging::{KameleoonLogger, LogLevel},
    storage::visitor_data::VisitorData,
    targeting::TargetingData,
    utils::{hasher::Hasher, version::SemVer},
};

const IN_HOLDOUT_VARIATION_KEY: &str = "in-holdout";

pub(crate) struct EvaluationContext<'a, 'b, D, F> {
    datafile: &'a D,
    visitor_code: &'a str,
    visitor_data: RwLockReadGuard<'b, VisitorData>,
    sdk_name: &'b str,
    sdk_version: SemVer,
    feature_flag: &'a F,
}

impl<'a, 'b, D, F, E, R, V> EvaluationContext<'a, 'b, D, F>
where
    D: DataFile<FeatureFlag = F, Experiment = E>,
    F: FeatureFlag<Rule = R> + 'static,
    R: Rule<Experiment = E> + 'static,
    E: Experiment<VariationByExposition = V> + 'static,
    V: VariationByExposition,
{
    #[trace_method(LogLevel::Debug, skip(retval, datafile, visitor_data))]
    pub fn new(
        datafile: &'a D,
        visitor_code: &'a str,
        visitor_data: RwLockReadGuard<'b, VisitorData>,
        sdk_name: &'b str,
        sdk_version: SemVer,
        feature_flag: &'a F,
    ) -> Self {
        Self {
            datafile,
            visitor_code,
            visitor_data,
            sdk_name,
            sdk_version,
            feature_flag,
        }
    }

    #[trace_method(LogLevel::Debug, skip(self))]
    pub fn evaluate(self) -> (Option<EvaluatedExperiment<'a, V, E>>, Vec<AssignedVariation>) {
        let mut vars_to_assign = Vec::new();
        // 1. Simulated variation overrides everything
        if let Some(simulated_var) = self.visitor_data.simulated_variations.r().get(self.feature_flag.feature_key()) {
            let eval_exp =
                EvaluatedExperiment::from_simulated_variation(self.datafile, simulated_var).unwrap_or_else(|err| {
                    KameleoonLogger::error(err.message());
                    None
                });
            return (eval_exp, vars_to_assign);
        }
        // 2. Holdout or MEGroup restrictions
        let code_for_hash = self.get_code_for_hash();
        if self.is_visitor_in_holdout(code_for_hash, &mut vars_to_assign)
            || self.is_ff_restricted_by_me_group(code_for_hash)
        {
            return (None, vars_to_assign);
        }
        // 3. Normal evaluation path
        let eval_exp = self.calc_eval_exp(code_for_hash);
        if let Some(eval_exp) = &eval_exp {
            let exp_id = eval_exp.experiment.id();
            let var_id = eval_exp.var_by_exp.variation_id();
            vars_to_assign.push(AssignedVariation::new(exp_id, var_id, eval_exp.rule_type));
        }
        (eval_exp, vars_to_assign)
    }

    fn is_visitor_in_holdout(&self, code_for_hash: &str, vars_to_assign: &mut Vec<AssignedVariation>) -> bool {
        let Some(holdout) = self.datafile.holdout() else {
            return false;
        };
        let hash = Hasher::obtain_with_campaign_id(code_for_hash, holdout.id());
        debug_log!("Calculated holdout hash {hash} for code '{code_for_hash}'");
        holdout.get_variation_by_hash(hash).map_or(false, |var_by_exp| {
            let av = AssignedVariation::new(holdout.id(), var_by_exp.variation_id(), RuleType::Experimentation);
            vars_to_assign.push(av);
            var_by_exp.variation_key() == IN_HOLDOUT_VARIATION_KEY
        })
    }

    fn is_ff_restricted_by_me_group(&self, code_for_hash: &str) -> bool {
        let Some(me_group_name) = self.feature_flag.me_group_name() else {
            return false;
        };
        let Some(me_group) = self.datafile.me_groups().get(me_group_name) else {
            return false;
        };
        let hash = Hasher::obtain_with_me_group(code_for_hash, me_group_name);
        debug_log!("Calculated ME hash {hash} for code '{code_for_hash}', ME-group '{me_group_name}'");
        me_group.get_feature_flag_by_hash(hash).map_or(false, |ff| ff.id() != self.feature_flag.id())
    }

    fn calc_eval_exp(&self, code_for_hash: &str) -> Option<EvaluatedExperiment<'a, V, E>> {
        for rule in self.feature_flag.rules() {
            let exp_id = rule.experiment().id();
            let forced_variation = self.visitor_data.forced_variations.r().get(&exp_id);
            // 1. Forced variation — force-targeting mode
            if let Some(forced_variation) = forced_variation {
                if forced_variation.force_targeting {
                    return Self::eval_exp_from_rule_and_variation_id(rule, forced_variation.variation_id);
                }
            }
            // 2. Targeting check
            if !self.check_targeting(rule) {
                continue;
            }
            // 3. Forced variation — targeting-only mode
            if let Some(forced_variation) = forced_variation {
                return Self::eval_exp_from_rule_and_variation_id(rule, forced_variation.variation_id);
            }
            // 4. Rule exposition
            if !Self::check_rule_exposition(code_for_hash, rule) {
                if rule.rule_type() == RuleType::TargetedDelivery {
                    break;
                }
                continue;
            }
            // 5. CBS evaluation
            if let Some(eval_exp) = self.evaluate_cbscores(code_for_hash, rule) {
                return Some(eval_exp);
            }
            // 6. Targeted delivery fallback
            if rule.rule_type() == RuleType::TargetedDelivery {
                let first_var_by_exp = rule.experiment().vars_by_exp().first();
                return first_var_by_exp.map(|v| EvaluatedExperiment::from_var_by_exp_and_rule(v, rule));
            }
            // 7. Variation exposition
            let hash = Hasher::obtain_with_campaign_id_respool_time(code_for_hash, exp_id, rule.respool_time());
            debug_log!("Calculated variation hash {hash} for code '{code_for_hash}'");
            if let Some(var_by_exp) = rule.experiment().get_variation_by_hash(hash) {
                return Some(EvaluatedExperiment::from_var_by_exp_and_rule(var_by_exp, rule));
            }
        }
        None
    }

    fn check_rule_exposition(code_for_hash: &str, rule: &R) -> bool {
        let exposition = rule.exposition();
        if exposition >= 1.0 {
            // Skipping hash calculation if rule.exposition==1.0 - and so hash<=rule.exposition is always true.
            // Calculating a hash is a heavy operation and rule.exposition being equal to 1.0 is not unusual.
            return true;
        }
        let hash = Hasher::obtain_with_campaign_id_respool_time(code_for_hash, rule.id(), rule.respool_time());
        debug_log!("Calculated rule hash {hash} for code '{code_for_hash}', rule {}", rule.id());
        hash <= exposition
    }

    fn eval_exp_from_rule_and_variation_id(rule: &'a R, variation_id: u32) -> Option<EvaluatedExperiment<'a, V, E>> {
        return EvaluatedExperiment::from_rule_and_variation_id(rule, variation_id).map_or_else(
            |err| {
                KameleoonLogger::error(err.message());
                None
            },
            |eval_exp| Some(eval_exp),
        );
    }

    fn check_targeting(&self, rule: &R) -> bool {
        let Some(seg_id) = rule.segment_id() else {
            return true;
        };
        let Some(segment) = self.datafile.segments().get(&seg_id) else {
            return true;
        };
        let Some(targeting_tree) = segment.tree() else {
            return true;
        };
        targeting_tree.check(&TargetingData {
            datafile: self.datafile,
            visitor_code: self.visitor_code,
            visitor_data: &self.visitor_data,
            campaign_id: Some(rule.experiment().id()),
            sdk_name: self.sdk_name,
            sdk_version: self.sdk_version,
        })
    }

    fn evaluate_cbscores(&self, code_for_hash: &str, rule: &'a R) -> Option<EvaluatedExperiment<'a, V, E>> {
        let cbscores = self.visitor_data.cbscores.r().get()?;
        let exp_id = rule.experiment().id();
        let var_id_group_by_scores = cbscores.values().get(&exp_id)?;
        let var_by_exp_in_cbs = var_id_group_by_scores.iter().find_map(|var_group| {
            let vars_by_exp = rule.experiment().vars_by_exp().iter();
            let var_by_exp_in_cbs = Vec::from_iter(vars_by_exp.filter(|v| var_group.ids.contains(&v.variation_id())));
            if var_by_exp_in_cbs.is_empty() {
                None
            } else {
                Some(var_by_exp_in_cbs)
            }
        });
        let var_by_exp_in_cbs = var_by_exp_in_cbs?;
        let size = var_by_exp_in_cbs.len();
        let idx = if size > 1 {
            let hash = Hasher::obtain_with_campaign_id_respool_time(code_for_hash, exp_id, rule.respool_time());
            debug_log!("Calculated CBS hash {hash} for code '{code_for_hash}'");
            ((hash * size as f64) as usize).min(size - 1)
        } else {
            0
        };
        Some(EvaluatedExperiment::from_var_by_exp_and_rule(var_by_exp_in_cbs[idx], rule))
    }

    fn get_code_for_hash(&self) -> &str {
        self.feature_flag
            // Trying to use the bucketing custom data's value if bucketingCustomDataId is defined
            .bucketing_custom_data_index()
            .and_then(|index| self.visitor_data.custom_data_map.r().get(&index))
            .and_then(|custom_data| custom_data.values.first().map(|s| s.as_str()))
            // Using mappingIdentifier instead of visitorCode if it was set up
            .or_else(|| self.visitor_data.mapping_identifier().map(|s| s))
            // Using visitorCode as a default option
            .unwrap_or(self.visitor_code)
    }
}

// MARK: - Tests

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    use crate::{
        data::{
            cbscores::{CBScores, ScoredVarId},
            forced_variation::ForcedVariation,
            simulated_variation::{SimulatedTargetingInfo, SimulatedVariation},
            CustomData,
        },
        storage::visitor::Visitor,
        utils::test_utils::{load_datafile, TestDataFile},
    };
    use rstest::rstest;

    #[rstest]
    #[case("vc1", "ff1", None, None, "vc1")]
    #[case("vc1", "ff1", Some("mi1"), None, "mi1")]
    #[case("vc1", "bucketing_custom_data", None, None, "vc1")]
    #[case("vc1", "bucketing_custom_data", None, Some(CustomData::new_with_index(1, vec![])), "vc1")]
    #[case("vc1", "bucketing_custom_data", None, Some(CustomData::new_with_index(2, vec!["cd1".to_owned()])), "vc1")]
    #[case("vc1", "bucketing_custom_data", None, Some(CustomData::new_with_index(1, vec!["cd1".to_owned()])), "cd1")]
    #[case("vc1", "bucketing_custom_data", Some("mi1"), Some(CustomData::new_with_index(1, vec!["cd1".to_owned()])), "cd1")]
    fn test_get_code_for_hash(
        #[case] vc: &str,
        #[case] ff_key: &str,
        #[case] mapping_identifier: Option<&str>,
        #[case] bucketing_custom_data: Option<CustomData>,
        #[case] expected: &str,
    ) {
        // arrange
        let datafile = make_datafile("evaluation");
        let visitor = Visitor::new();
        if let Some(mi) = mapping_identifier {
            visitor.wdata().set_mapping_identifier(mi);
        }
        if let Some(cd) = bucketing_custom_data {
            visitor.wdata().custom_data_map.put(cd, true);
        }
        let ff = datafile.get_feature_flag(ff_key).unwrap();
        let eval_ctx = EvaluationContext::new(&datafile, vc, visitor.rdata(), "sdk", SemVer::new(1, 2, 3), ff);
        // act
        let actual = eval_ctx.get_code_for_hash();
        // assert
        assert_eq!(expected, actual);
    }

    fn make_datafile(df_name: &str) -> TestDataFile {
        load_datafile(&format!("resources/datafiles/{df_name}_datafile.json"))
    }

    fn assert_eval_exp<'a, V, E>(expected: Option<(u32, u32, RuleType)>, actual: Option<EvaluatedExperiment<'a, V, E>>)
    where
        V: VariationByExposition,
        E: Experiment<VariationByExposition = V> + 'static,
    {
        match expected {
            None => assert!(actual.is_none()),
            Some((expected_exp_id, expected_var_id, expected_rule_type)) => {
                let actual = actual.unwrap();
                assert_eq!(expected_var_id, actual.var_by_exp.variation_id());
                assert_eq!(expected_exp_id, actual.experiment.id());
                assert_eq!(expected_rule_type, actual.rule_type);
            }
        }
    }

    fn assert_vars_to_assign(expected: Vec<(u32, u32, RuleType)>, actual: Vec<AssignedVariation>) {
        assert_eq!(expected.len(), actual.len());
        let actual: HashMap<u32, AssignedVariation> = actual.into_iter().map(|av| (av.experiment_id, av)).collect();
        assert_eq!(expected.len(), actual.len());
        for (expected_exp_id, expected_var_id, expected_rule_type) in expected.into_iter() {
            let av = actual.get(&expected_exp_id).unwrap();
            assert_eq!(expected_exp_id, av.experiment_id);
            assert_eq!(expected_var_id, av.variation_id);
            assert_eq!(expected_rule_type, av.rule_type);
        }
    }

    #[rstest]
    #[case("in1", "IOS", SemVer::new(1, 0, 1), Some((151236, 865016, RuleType::TargetedDelivery)))]
    #[case("in1", "IOS", SemVer::new(1, 0, 0), None)]
    #[case("in1", "IOS", SemVer::new(9, 999, 999), Some((151236, 865016, RuleType::TargetedDelivery)))]
    #[case("in1", "IOS", SemVer::new(10, 0, 0), None)]
    #[case("in1", "ANDROID", SemVer::new(3, 4, 5), None)]
    #[case("out3", "IOS", SemVer::new(3, 4, 5), None)]
    fn test_evaluate_normal(
        #[case] vc: &str,
        #[case] sdk_name: &str,
        #[case] sdk_version: SemVer,
        #[case] expected_eval_exp: Option<(u32, u32, RuleType)>,
    ) {
        // arrange
        let datafile = make_datafile("evaluation");
        let visitor = Visitor::new();
        let ff = datafile.get_feature_flag("ff1").unwrap();
        let eval_ctx = EvaluationContext::new(&datafile, vc, visitor.rdata(), sdk_name, sdk_version, ff);
        // act
        let (eval_exp, vars_to_assign) = eval_ctx.evaluate();
        // assert
        assert_eval_exp(expected_eval_exp, eval_exp);
        assert_vars_to_assign(expected_eval_exp.into_iter().collect(), vars_to_assign);
    }

    #[rstest]
    #[case("in-holdout", None, vec![(12345, 831404, RuleType::Experimentation)])]
    #[case("not-in-holdout", Some((1001, 2002, RuleType::Experimentation)), vec![
        (12345, 831405, RuleType::Experimentation),
        (1001, 2002, RuleType::Experimentation),
    ])]
    fn test_evaluate_holdout(
        #[case] vc: &str,
        #[case] expected_eval_exp: Option<(u32, u32, RuleType)>,
        #[case] expected_vars_to_assign: Vec<(u32, u32, RuleType)>,
    ) {
        // arrange
        const FF_KEY: &str = "ff1";
        let datafile = make_datafile("holdout");
        let visitor = Visitor::new();
        let ff = datafile.get_feature_flag(FF_KEY).unwrap();
        let eval_ctx = EvaluationContext::new(&datafile, vc, visitor.rdata(), "sdk", SemVer::new(1, 2, 3), ff);
        // act
        let (eval_exp, vars_to_assign) = eval_ctx.evaluate();
        // assert
        assert_eval_exp(expected_eval_exp, eval_exp);
        assert_vars_to_assign(expected_vars_to_assign, vars_to_assign);
    }

    #[rstest]
    #[case("vcCodeForFF1", "ff1", Some((1001, 2002, RuleType::Experimentation)))]
    #[case("vcCodeForFF1", "ff2", None)]
    #[case("vcCodeForFF1", "ff3", Some((1003, 3002, RuleType::Experimentation)))]
    #[case("vcCodeFF2", "ff1", None)]
    #[case("vcCodeFF2", "ff2", Some((1100, 2100, RuleType::TargetedDelivery)))]
    #[case("vcCodeFF2", "ff3", Some((1003, 3002, RuleType::Experimentation)))]
    fn test_evaluate_me_group(
        #[case] vc: &str,
        #[case] ff_key: &str,
        #[case] expected_eval_exp: Option<(u32, u32, RuleType)>,
    ) {
        // arrange
        let datafile = make_datafile("me_group");
        let visitor = Visitor::new();
        let ff = datafile.get_feature_flag(ff_key).unwrap();
        let eval_ctx = EvaluationContext::new(&datafile, vc, visitor.rdata(), "sdk", SemVer::new(1, 2, 3), ff);
        // act
        let (eval_exp, vars_to_assign) = eval_ctx.evaluate();
        // assert
        assert_eval_exp(expected_eval_exp, eval_exp);
        assert_vars_to_assign(expected_eval_exp.into_iter().collect(), vars_to_assign);
    }

    fn make_cbs1() -> CBScores {
        CBScores::new(
            &[
                (
                    1000,
                    vec![
                        ScoredVarId {
                            variation_id: 1001,
                            score: 0.5,
                        },
                        ScoredVarId {
                            variation_id: 1002,
                            score: 0.5,
                        },
                        ScoredVarId {
                            variation_id: 1003,
                            score: 1.0,
                        },
                    ],
                ),
                (
                    2000,
                    vec![
                        ScoredVarId {
                            variation_id: 2003,
                            score: 0.1,
                        },
                        ScoredVarId {
                            variation_id: 2001,
                            score: 1.0,
                        },
                        ScoredVarId {
                            variation_id: 0,
                            score: 0.9,
                        },
                    ],
                ),
                (
                    3000,
                    vec![
                        ScoredVarId {
                            variation_id: 3001,
                            score: 1.000001,
                        },
                        ScoredVarId {
                            variation_id: 3003,
                            score: 1.0000007,
                        },
                        ScoredVarId {
                            variation_id: 3002,
                            score: 1.0000005,
                        },
                    ],
                ),
            ]
            .into(),
        )
    }

    fn make_cbs2() -> CBScores {
        CBScores::new(
            &[
                (
                    1000,
                    vec![
                        ScoredVarId {
                            variation_id: 1001,
                            score: 0.5,
                        },
                        ScoredVarId {
                            variation_id: 1002,
                            score: 0.5,
                        },
                        ScoredVarId {
                            variation_id: 1003,
                            score: 1.0,
                        },
                    ],
                ),
                (
                    2000,
                    vec![
                        ScoredVarId {
                            variation_id: 2003,
                            score: 0.1,
                        },
                        ScoredVarId {
                            variation_id: 2002,
                            score: 1.0,
                        },
                        ScoredVarId {
                            variation_id: 0,
                            score: 1.001,
                        },
                    ],
                ),
                (
                    3000,
                    vec![
                        ScoredVarId {
                            variation_id: 3001,
                            score: 1.000001,
                        },
                        ScoredVarId {
                            variation_id: 3003,
                            score: 1.00000015,
                        },
                        ScoredVarId {
                            variation_id: 3002,
                            score: 1.000002,
                        },
                    ],
                ),
                (
                    4000,
                    vec![ScoredVarId {
                        variation_id: 4005,
                        score: 1.000001,
                    }],
                ),
            ]
            .into(),
        )
    }

    #[rstest]
    #[case("vcCodeForFF1", "ff1", make_cbs1(), (1000, 1001, RuleType::Experimentation))]
    #[case("vcCodeForFF1", "ff2", make_cbs1(), (2000, 2001, RuleType::Experimentation))]
    #[case("vcCodeForFF1", "ff3", make_cbs1(), (3000, 3001, RuleType::Experimentation))]
    #[case("vcCodeForFF1", "ff4", make_cbs1(), (4000, 4001, RuleType::Experimentation))]
    #[case("vcCodeFF2", "ff1", make_cbs2(), (1000, 1002, RuleType::Experimentation))]
    #[case("vcCodeFF2", "ff2", make_cbs2(), (2000, 0, RuleType::Experimentation))]
    #[case("vcCodeFF2", "ff3", make_cbs2(), (3000, 3002, RuleType::Experimentation))]
    #[case("vcCodeFF2", "ff4", make_cbs2(), (4000, 4002, RuleType::Experimentation))]
    fn test_evaluate_cbs(
        #[case] vc: &str,
        #[case] ff_key: &str,
        #[case] cbs: CBScores,
        #[case] expected_eval_exp: (u32, u32, RuleType),
    ) {
        // arrange
        let datafile = make_datafile("cbs");
        let visitor = Visitor::new();
        visitor.wdata().cbscores.set(Box::new(cbs), true);
        let ff = datafile.get_feature_flag(ff_key).unwrap();
        let eval_ctx = EvaluationContext::new(&datafile, vc, visitor.rdata(), "sdk", SemVer::new(1, 2, 3), ff);
        // act
        let (eval_exp, vars_to_assign) = eval_ctx.evaluate();
        // assert
        assert_eval_exp(Some(expected_eval_exp), eval_exp);
        assert_vars_to_assign(vec![expected_eval_exp], vars_to_assign);
    }

    #[rstest]
    #[case(SimulatedVariation::new(None, "ff1".to_owned()), None)]
    #[case(
        SimulatedVariation::new(Some(SimulatedTargetingInfo::new(754, 866247)), "ff1".to_owned()),
        Some((151237, 866247, RuleType::TargetedDelivery))
    )]
    fn test_evaluate_simulated_variation(
        #[case] simvar: SimulatedVariation,
        #[case] expected_eval_exp: Option<(u32, u32, RuleType)>,
    ) {
        // arrange
        let datafile = make_datafile("evaluation");
        let visitor = Visitor::new();
        visitor.wdata().simulated_variations.put(simvar);
        let ff = datafile.get_feature_flag("ff1").unwrap();
        let eval_ctx = EvaluationContext::new(&datafile, "vc1", visitor.rdata(), "sdk", SemVer::new(1, 2, 3), ff);
        // act
        let (eval_exp, vars_to_assign) = eval_ctx.evaluate();
        // assert
        assert_eval_exp(expected_eval_exp, eval_exp);
        assert_vars_to_assign(vec![], vars_to_assign);
    }

    #[rstest]
    #[case(ForcedVariation::new("ff1".to_owned(), 3000, 1000, 0, true), (1000, 0, RuleType::TargetedDelivery))]
    #[case(ForcedVariation::new("ff1".to_owned(), 3001, 1001, 2001, true), (1001, 2001, RuleType::Experimentation))]
    #[case(ForcedVariation::new("ff1".to_owned(), 3002, 1002, 2002, true), (1001, 0, RuleType::Experimentation))]
    #[case(ForcedVariation::new("ff1".to_owned(), 3000, 1000, 0, false), (1001, 0, RuleType::Experimentation))]
    #[case(ForcedVariation::new("ff1".to_owned(), 3001, 1001, 2001, false), (1001, 2001, RuleType::Experimentation))]
    #[case(ForcedVariation::new("ff1".to_owned(), 3002, 1002, 2002, false), (1001, 0, RuleType::Experimentation))]
    fn test_evaluate_forced_variation(
        #[case] forced_var: ForcedVariation,
        #[case] expected_eval_exp: (u32, u32, RuleType),
    ) {
        // arrange
        let datafile = make_datafile("forced_variation");
        let visitor = Visitor::new();
        visitor.wdata().forced_variations.put(forced_var);
        let ff = datafile.get_feature_flag("ff1").unwrap();
        let eval_ctx = EvaluationContext::new(&datafile, "vc1", visitor.rdata(), "sdk", SemVer::new(1, 2, 3), ff);
        // act
        let (eval_exp, vars_to_assign) = eval_ctx.evaluate();
        // assert
        assert_eval_exp(Some(expected_eval_exp), eval_exp);
        assert_vars_to_assign(vec![expected_eval_exp], vars_to_assign);
    }
}
