use crate::{
    datafile::{datafile::DataFile, experiment::Experiment, feature_flag::FeatureFlag, rule::Rule},
    targeting::{
        conditions::{
            visitor_scope_condition::{VisitScope, VisitorScopeCondition},
            TargetingCondition,
        },
        models::TargetFeatureFlagCPModel,
        TargetingData,
    },
};

pub(crate) struct TargetFeatureFlagCondition {
    feature_flag_id: u32,
    variation_key: Option<String>,
    rule_id: Option<u32>,
    visitor_scope_condition: VisitorScopeCondition,
}

impl TargetFeatureFlagCondition {
    pub fn from_json(model: TargetFeatureFlagCPModel) -> Option<Self> {
        Some(Self {
            feature_flag_id: model.feature_flag_id,
            variation_key: model.variation_key,
            rule_id: model.rule_id,
            visitor_scope_condition: VisitorScopeCondition::new(
                model.visit_params.visit_scope,
                model.visit_params.visit_count,
                VisitScope::CurrentVisit,
            ),
        })
    }

    fn get_rules<'a>(&self, datafile: &'a impl DataFile) -> Option<&'a [impl Rule]> {
        datafile.feature_flags_by_id().get(&self.feature_flag_id).map(|ff| ff.rules())
    }

    fn check_rule(&self, data: &TargetingData<impl DataFile>, rule: &impl Rule) -> bool {
        if let Some(rule_id) = self.rule_id {
            if rule_id != rule.id() {
                return false;
            }
        }
        let variation = match data.visitor_data.assigned_variations.r().get(&rule.experiment().id()) {
            Some(variation) => variation,
            None => return false,
        };
        if variation.assignment_time < self.visitor_scope_condition.assignment_threshold(data) {
            return false;
        }
        if let Some(cond_var_key) = self.variation_key.as_ref() {
            let var_by_id = data.datafile.vars_by_id().get(&variation.variation_id);
            var_by_id.map_or(false, |var_by_id| var_by_id.variation_key() == cond_var_key)
        } else {
            true
        }
    }
}

impl TargetingCondition for TargetFeatureFlagCondition {
    fn check(&self, data: &TargetingData<impl DataFile>) -> Option<bool> {
        if data.visitor_data.assigned_variations.r().len() == 0 {
            return Some(false);
        }
        Some(self.get_rules(data.datafile)?.iter().any(|rule| self.check_rule(data, rule)))
    }
}

#[cfg(test)]
mod tests {
    use crate::utils::test_utils::new_var;
    use crate::{
        data::BaseData,
        datafile::segment::Segment,
        storage::visitor::Visitor,
        targeting::TargetingDataBuilder,
        utils::test_utils::{load_datafile, new_visitor_visits},
    };

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

    #[rstest]
    #[case(237130, 219003, 897559, 1000, true)]
    #[case(237130, 219003, 897560, 1000, false)]
    #[case(237130, 219004, 897559, 1000, false)]
    #[case(237130, 219005, 897565, 1000, false)]
    #[case(237131, 219003, 897559, 1000, true)]
    #[case(237131, 219003, 897560, 1000, true)]
    #[case(237131, 219004, 897560, 1000, false)]
    #[case(237131, 219005, 897565, 1000, false)]
    #[case(237132, 219003, 897559, 300, true)]
    #[case(237132, 219004, 897561, 300, true)]
    #[case(237132, 219003, 897560, 300, false)]
    #[case(237132, 219005, 897565, 300, false)]
    #[case(237133, 219003, 897559, 300, true)]
    #[case(237133, 219003, 897560, 300, true)]
    #[case(237133, 219004, 897561, 300, true)]
    #[case(237133, 219005, 897565, 300, false)]
    #[case(237130, 219003, 897559, 700, false)]
    #[case(237131, 219003, 897560, 700, false)]
    #[case(237132, 219004, 897561, 50, false)]
    #[case(237133, 219003, 897559, 50, false)]
    fn test_check(
        #[case] segment_id: u32,
        #[case] exp_id: u32,
        #[case] var_id: u32,
        #[case] assignment_ts: u64,
        #[case] expected: bool,
    ) {
        let datafile = load_datafile("resources/conditions/mock_segment_target.json");
        let visitor = Visitor::new();
        visitor.add_data(std::iter::once(BaseData::VisitorVisits(new_visitor_visits())), true);
        visitor.add_data(std::iter::once(BaseData::AssignedVariation(new_var(exp_id, var_id, assignment_ts))), true);
        let tar_tree = datafile.segments().get(&segment_id).unwrap().tree().unwrap();
        let actual = tar_tree.check(&TargetingDataBuilder::new().build(&datafile, &visitor.rdata()));
        assert_eq!(expected, actual);
    }
}
