use std::sync::OnceLock;

use crate::{
    data::Cookie,
    datafile::datafile::DataFile,
    error_log,
    targeting::{conditions::TargetingCondition, models::CookieCPModel, StringTargetingOperator, TargetingData},
    utils::regex_utils::make_prohibiting_regex,
};

use regex::Regex;

pub(crate) struct CookieCondition {
    name: String,
    name_match_type: StringTargetingOperator,
    name_regex: OnceLock<Regex>,
    value: String,
    value_match_type: StringTargetingOperator,
    value_regex: OnceLock<Regex>,
}

impl CookieCondition {
    pub fn from_json(model: CookieCPModel) -> Option<Self> {
        return Some(Self {
            name: model.name,
            name_match_type: StringTargetingOperator::from_str(&model.name_match_type)?,
            name_regex: OnceLock::new(),
            value: model.value,
            value_match_type: StringTargetingOperator::from_str(&model.value_match_type)?,
            value_regex: OnceLock::new(),
        });
    }

    fn select_values<'a>(&self, cookie: &'a Cookie) -> Vec<&'a String> {
        match self.name_match_type {
            StringTargetingOperator::Exact => cookie.cookies.get(&self.name).into_iter().collect(),
            StringTargetingOperator::Contains => {
                cookie.cookies.iter().filter_map(|(k, v)| k.contains(&self.name).then_some(v)).collect()
            }
            StringTargetingOperator::Regex => {
                let name_regex = self.name_regex.get_or_init(|| {
                    Regex::new(&self.name).unwrap_or_else(|err| {
                        error_log!("Failed to compile name regex '{}' for CookieCondition: {err}", self.value);
                        make_prohibiting_regex()
                    })
                });
                cookie.cookies.iter().filter_map(|(k, v)| if name_regex.is_match(k) { Some(v) } else { None }).collect()
            }
        }
    }

    fn check_values(&self, values: Vec<&String>) -> bool {
        match self.value_match_type {
            StringTargetingOperator::Exact => values.iter().any(|v| *v == &self.value),
            StringTargetingOperator::Contains => values.iter().any(|v| v.contains(&self.value)),
            StringTargetingOperator::Regex => {
                let value_regex = self.value_regex.get_or_init(|| {
                    Regex::new(&self.value).unwrap_or_else(|err| {
                        error_log!("Failed to compile value regex '{}' for CookieCondition: {err}", self.value);
                        make_prohibiting_regex()
                    })
                });
                values.iter().any(|v| value_regex.is_match(v))
            }
        }
    }
}

impl TargetingCondition for CookieCondition {
    fn check(&self, data: &TargetingData<impl DataFile>) -> Option<bool> {
        let cookie = data.visitor_data.cookie.r().get()?;
        return Some(self.check_values(self.select_values(cookie)));
    }
}

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

    use crate::{
        data::BaseData, datafile::datafile_test_utils::MockDataFile, storage::visitor::Visitor,
        targeting::TargetingDataBuilder, utils::test_utils::assert_targeting,
    };

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

    #[rstest]
    #[case(None, false)]
    #[case(Some(HashMap::from([
        ("my_key1".to_owned(), "my_value1".to_owned()),
        ("my_key2".to_owned(), "my_value2".to_owned()),
    ])), true)]
    #[case(Some(HashMap::from([
        ("my_key2".to_owned(), "my_value1".to_owned()),
        ("my_key1".to_owned(), "my_value2".to_owned()),
    ])), false)]
    #[case(Some(HashMap::from([("my_key1".to_owned(), "my_value1".to_owned())])), false)]
    #[case(Some(HashMap::from([
        ("my_key1".to_owned(), "my_value1".to_owned()),
        ("my_key".to_owned(), "my_value2".to_owned()),
    ])), false)]
    #[case(Some(HashMap::from([
        ("my_key1".to_owned(), "my_value1".to_owned()),
        ("my_key2".to_owned(), "my_value1".to_owned()),
    ])), false)]
    #[case(Some(HashMap::new()), false)]
    fn test_check(#[case] cookies: Option<HashMap<String, String>>, #[case] expected: bool) {
        let datafile = MockDataFile::default();
        let visitor = Visitor::new();
        if let Some(cookies) = cookies {
            visitor.add_data([BaseData::Cookie(Cookie::new(cookies))].into_iter(), true);
        }
        assert_targeting(
            "resources/conditions/mock_segment_cookie.json",
            &TargetingDataBuilder::new().build(&datafile, &visitor.rdata()),
            expected,
        );
    }
}
