use std::{num::ParseFloatError, sync::OnceLock};

use regex::Regex;
use serde::Deserialize;

use crate::{
    datafile::datafile::DataFile,
    error_log,
    targeting::{
        conditions::TargetingCondition, models::CustomDatumCPModel, BooleanTargetingOperator, NumberTargetingOperator,
        StringTargetingOperator, TargetingData, TargetingOperator,
    },
    utils::regex_utils::make_prohibiting_regex,
};

#[derive(Deserialize, PartialEq)]
enum JValue {
    #[serde(untagged)]
    String(String),
    #[serde(untagged)]
    Number(i64),
    #[serde(untagged)]
    Boolean(bool),
}

impl JValue {
    fn into_string(self) -> String {
        match self {
            JValue::String(str) => str,
            JValue::Number(num) => num.to_string(),
            JValue::Boolean(flag) => flag.to_string(),
        }
    }
}

pub(crate) struct CustomDatum {
    index: u32,
    operator: TargetingOperator,
    value: Option<String>,
    value_regex: OnceLock<Regex>,
    value_matches: OnceLock<Vec<String>>,
}

impl CustomDatum {
    pub fn from_json(model: CustomDatumCPModel) -> Option<Self> {
        return Some(Self {
            index: model.custom_data_index.parse().ok()?,
            operator: TargetingOperator::from_str(&model.value_match_type),
            value: model.value,
            value_regex: OnceLock::new(),
            value_matches: OnceLock::new(),
        });
    }

    fn check_targeting(&self, values: Vec<&String>) -> bool {
        match self.operator {
            TargetingOperator::Undefined => false,
            TargetingOperator::Number(operator) => self.value.as_deref().map_or(false, |cond_value| {
                Self::check_number_values(cond_value, values, operator).unwrap_or_else(|err| {
                    error_log!("Failed to parse value for CustomDatum: {err}");
                    false
                })
            }),
            TargetingOperator::String(operator) => {
                self.value.as_deref().map_or(false, |cond_value| self.check_string_values(cond_value, values, operator))
            }
            TargetingOperator::Boolean(operator) => self.check_boolean_values(values, operator),
            TargetingOperator::AmongValues => {
                self.value.as_deref().map_or(false, |cond_values| self.check_among_values(cond_values, values))
            }
        }
    }

    fn check_number_values(
        cond_value: &str,
        values: Vec<&String>,
        operator: NumberTargetingOperator,
    ) -> Result<bool, ParseFloatError> {
        let cond_value: f64 = cond_value.parse()?;
        Ok(values.iter().filter_map(|v| v.parse::<f64>().ok()).any(|value| match operator {
            NumberTargetingOperator::Equal => value == cond_value,
            NumberTargetingOperator::Greater => value > cond_value,
            NumberTargetingOperator::Lower => value < cond_value,
        }))
    }

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

    fn check_boolean_values(&self, values: Vec<&String>, operator: BooleanTargetingOperator) -> bool {
        match operator {
            BooleanTargetingOperator::IsTrue => values.iter().any(|v| "true".eq_ignore_ascii_case(v)),
            BooleanTargetingOperator::IsFalse => values.iter().any(|v| "false".eq_ignore_ascii_case(v)),
        }
    }

    fn check_among_values(&self, cond_values: &str, values: Vec<&String>) -> bool {
        let value_matches = self.value_matches.get_or_init(|| {
            serde_json::from_str::<Vec<JValue>>(cond_values).map_or_else(
                |err| {
                    error_log!("Failed to parse matches '{}' for CustomDatum: {err}", cond_values);
                    Vec::new()
                },
                |jvalues| jvalues.into_iter().map(JValue::into_string).collect(),
            )
        });
        return values.iter().any(|v| value_matches.contains(v));
    }
}

impl TargetingCondition for CustomDatum {
    fn check(&self, data: &TargetingData<impl DataFile>) -> Option<bool> {
        let custom_data = data.visitor_data.custom_data_map.r().get(&self.index);
        match custom_data {
            Some(custom_data) => Some(self.check_targeting(Vec::from_iter(custom_data.values.iter()))),
            None => Some(self.operator == TargetingOperator::Undefined),
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        data::{BaseData, CustomData},
        datafile::datafile_test_utils::MockDataFile,
        storage::visitor::Visitor,
        targeting::TargetingDataBuilder,
    };

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

    const CD_INDEX: u32 = 0;

    fn make_custom_datum(operator: TargetingOperator, value: Option<&str>, index: u32) -> CustomDatum {
        return CustomDatum {
            index,
            operator,
            value: value.map(|v| v.to_owned()),
            value_regex: OnceLock::new(),
            value_matches: OnceLock::new(),
        };
    }

    fn check_condition(custom_datum: &CustomDatum, custom_data: Vec<CustomData>) -> Option<bool> {
        let datafile = MockDataFile::default();
        let visitor = Visitor::new();
        visitor.add_data(custom_data.into_iter().map(|cd| BaseData::CustomData(cd)), true);
        let visitor_data = visitor.rdata();
        custom_datum.check(&TargetingDataBuilder::new().build(&datafile, &visitor_data))
    }

    #[rstest]
    #[case(TargetingOperator::Boolean(BooleanTargetingOperator::IsTrue), false)]
    #[case(TargetingOperator::Undefined, true)]
    fn test_check_no_custom_data(#[case] operator: TargetingOperator, #[case] expected: bool) {
        let custom_datum = make_custom_datum(operator, Some(""), CD_INDEX);
        let actual = check_condition(&custom_datum, vec![]);
        assert_eq!(Some(expected), actual);
    }

    #[rstest]
    #[case(TargetingOperator::Boolean(BooleanTargetingOperator::IsTrue), false)]
    #[case(TargetingOperator::Undefined, false)]
    fn test_check_custom_data_no_value(#[case] operator: TargetingOperator, #[case] expected: bool) {
        let custom_datum = make_custom_datum(operator, Some(""), CD_INDEX);
        let actual = check_condition(&custom_datum, vec![CustomData::new_with_index(CD_INDEX, Vec::new())]);
        assert_eq!(Some(expected), actual);
    }

    #[rstest]
    #[case(vec![
        CustomData::new_with_index(12, vec!["true".to_owned()]),
        CustomData::new_with_index(13, vec!["false".to_owned()]),
        CustomData::new_with_index(14, vec!["false".to_owned()]),
    ], 12)]
    #[case(vec![
        CustomData::new_with_index(12, vec!["false".to_owned()]),
        CustomData::new_with_index(13, vec!["false".to_owned()]),
        CustomData::new_with_index(14, vec!["true".to_owned()]),
    ], 14)]
    fn test_check_actual_custom_data_selection(#[case] custom_data: Vec<CustomData>, #[case] index: u32) {
        let operator = TargetingOperator::Boolean(BooleanTargetingOperator::IsTrue);
        let custom_datum = make_custom_datum(operator, Some(""), index);
        let actual = check_condition(&custom_datum, custom_data);
        assert_eq!(Some(true), actual);
    }

    #[rstest]
    #[case(vec!["a;_".to_owned(), ";".to_owned()], false)]
    #[case(vec!["a;".to_owned(), ";".to_owned()], true)]
    fn test_check_regex(#[case] cd_values: Vec<String>, #[case] expected: bool) {
        let operator = TargetingOperator::String(StringTargetingOperator::Regex);
        let custom_datum = make_custom_datum(operator, Some("[^;]+;$"), CD_INDEX);
        let actual = check_condition(&custom_datum, vec![CustomData::new_with_index(CD_INDEX, cd_values)]);
        assert_eq!(Some(expected), actual);
    }

    #[rstest]
    #[case(None, false)]
    #[case(Some("low"), false)]
    #[case(Some("or"), true)]
    fn test_check_contains(#[case] cond_value: Option<&str>, #[case] expected: bool) {
        let operator = TargetingOperator::String(StringTargetingOperator::Contains);
        let custom_datum = make_custom_datum(operator, cond_value, CD_INDEX);
        let actual = check_condition(
            &custom_datum,
            vec![CustomData::new_with_index(CD_INDEX, vec!["Hello".to_owned(), "World".to_owned()])],
        );
        assert_eq!(Some(expected), actual);
    }

    #[rstest]
    #[case(vec!["test-tExt 123".to_owned(), "test-text 321".to_owned()], false)]
    #[case(vec!["test-text 321".to_owned(), "test-text 123".to_owned()], true)]
    fn test_check_exact(#[case] cd_values: Vec<String>, #[case] expected: bool) {
        let operator = TargetingOperator::String(StringTargetingOperator::Regex);
        let custom_datum = make_custom_datum(operator, Some("test-text 123"), CD_INDEX);
        let actual = check_condition(&custom_datum, vec![CustomData::new_with_index(CD_INDEX, cd_values)]);
        assert_eq!(Some(expected), actual);
    }

    #[rstest]
    #[case(NumberTargetingOperator::Equal, None, false)]
    #[case(NumberTargetingOperator::Equal, Some("-123.45601"), false)]
    #[case(NumberTargetingOperator::Equal, Some("-123.45600"), true)]
    #[case(NumberTargetingOperator::Greater, Some("123.45600"), false)]
    #[case(NumberTargetingOperator::Greater, Some("123.45599"), true)]
    #[case(NumberTargetingOperator::Lower, Some("-123.45600"), false)]
    #[case(NumberTargetingOperator::Lower, Some("-123.45599"), true)]
    fn test_check_number_cmp(
        #[case] operator: NumberTargetingOperator,
        #[case] cond_value: Option<&str>,
        #[case] expected: bool,
    ) {
        let custom_datum = make_custom_datum(TargetingOperator::Number(operator), cond_value, CD_INDEX);
        let actual = check_condition(
            &custom_datum,
            vec![CustomData::new_with_index(CD_INDEX, vec!["123.456".to_owned(), "-123.456".to_owned()])],
        );
        assert_eq!(Some(expected), actual);
    }

    #[rstest]
    #[case(BooleanTargetingOperator::IsTrue, vec!["false".to_owned()], false)]
    #[case(BooleanTargetingOperator::IsTrue, vec!["false".to_owned(), "tRue".to_owned()], true)]
    #[case(BooleanTargetingOperator::IsFalse, vec!["true".to_owned()], false)]
    #[case(BooleanTargetingOperator::IsFalse, vec!["true".to_owned(), "faLse".to_owned()], true)]
    fn test_check_bool(
        #[case] operator: BooleanTargetingOperator,
        #[case] cd_values: Vec<String>,
        #[case] expected: bool,
    ) {
        let custom_datum = make_custom_datum(TargetingOperator::Boolean(operator), None, CD_INDEX);
        let actual = check_condition(&custom_datum, vec![CustomData::new_with_index(CD_INDEX, cd_values)]);
        assert_eq!(Some(expected), actual);
    }

    #[rstest]
    #[case(Some("[\"q2f6hsbzpf\",\"8ney4225y65a\",\"3h6vjtz26b\"]"), vec!["notTargeted".to_owned()], false)]
    #[case(Some("[\"q2f6hsbzpf\",\"8ney4225y65a\",\"3h6vjtz26b\"]"), vec!["q2f6hsbzpf".to_owned()], true)]
    #[case(Some("[\"q2f6hsbzpf\",\"8ney4225y65a\",\"3h6vjtz26b\"]"), vec!["3h6vjtz26b".to_owned()], true)]
    #[case(Some("[\"a\",\"b\",\"c\"]"), vec!["false".to_owned(), "notTargeted".to_owned(), "c".to_owned()], true)]
    #[case(Some("[true]"), vec!["false".to_owned()], false)]
    #[case(Some("[true]"), vec!["true".to_owned()], true)]
    #[case(Some("[10,12,15]"), vec!["11".to_owned()], false)]
    #[case(Some("[10,12,15]"), vec!["10".to_owned()], true)]
    #[case(Some("[10,12,15]"), vec!["15".to_owned()], true)]
    fn test_check_among_values(
        #[case] cond_value: Option<&str>,
        #[case] cd_values: Vec<String>,
        #[case] expected: bool,
    ) {
        let custom_datum = make_custom_datum(TargetingOperator::AmongValues, cond_value, CD_INDEX);
        let actual = check_condition(&custom_datum, vec![CustomData::new_with_index(CD_INDEX, cd_values)]);
        assert_eq!(Some(expected), actual);
    }

    #[rstest]
    #[case(1, "EQUAL", "123.456")]
    #[case(2, "GREATER", "-123.456")]
    #[case(3, "LOWER", "10101")]
    #[case(4, "EXACT", "string")]
    #[case(5, "CONTAINS", "str")]
    #[case(6, "REGULAR_EXPRESSION", "[^;]+;$")]
    #[case(7, "TRUE", "false")]
    #[case(8, "FALSE", "true")]
    #[case(9, "AMONG_VALUES", "[654, true]")]
    #[case(10, "UNKNOWN", "sth")]
    fn test_from_json(#[case] index: u32, #[case] operator: &str, #[case] value: &str) {
        let model = CustomDatumCPModel {
            custom_data_index: index.to_string(),
            value: Some(value.to_owned()),
            value_match_type: operator.to_owned(),
        };
        let custom_datum = CustomDatum::from_json(model).unwrap();
        assert_eq!(index, custom_datum.index);
        assert_eq!(TargetingOperator::from_str(operator), custom_datum.operator);
        assert_eq!(value, custom_datum.value.unwrap());
    }
}
