use std::{collections::HashMap, sync::Arc};

use serde::Deserialize;
use kameleoon_trace_logging::trace_method;

use crate::{
    cookies::accessor::CoreCookieAccessor,
    data::simulated_variation::{SimulatedTargetingInfo, SimulatedVariation},
    datafile::{
        datafile::DataFile, datafile_manager::DataFileManager, experiment::Experiment, feature_flag::FeatureFlag,
        rule::Rule, settings::Settings,
    },
    debug_log,
    error::{ErrorCode, KErr},
    error_log,
    logging::LogLevel,
    storage::visitor_manager::VisitorManager,
    utils::{KRes, string_generator::VisitorCodeGenerator, visitor_code_validator::VisitorCodeValidator},
};

pub(crate) trait CookieManager {
    fn get_or_add_visitor_code(
        &self,
        accessor: &mut impl CoreCookieAccessor,
        default_visitor_code: Option<&str>,
    ) -> KRes<String>;

    fn update_visitor_code(&self, accessor: Option<&mut impl CoreCookieAccessor>, visitor_code: &str, consent: bool);

    fn process_simulated_variations(&self, accessor: &mut impl CoreCookieAccessor, visitor_code: &str) -> KRes<()>;
}

const VISITOR_CODE_COOKIE: &str = "kameleoonVisitorCode";
const SIMULATION_COOKIE: &str = "kameleoonSimulationFFData";
const MAX_AGE_COOKIE_SECONDS: u32 = 380 * 86400; // days * seconds in day

pub(crate) struct CookieManagerImpl<D, V> {
    datafile_manager: Arc<D>,
    visitor_manager: Arc<V>,
    top_level_domain: String,
}

impl<D: DataFileManager, V: VisitorManager> CookieManager for CookieManagerImpl<D, V> {
    #[trace_method(LogLevel::Debug, skip(self, accessor))]
    fn get_or_add_visitor_code(
        &self,
        accessor: &mut impl CoreCookieAccessor,
        default_visitor_code: Option<&str>,
    ) -> KRes<String> {
        let (visitor_code, force_add) = if let Some(code) = accessor.get(VISITOR_CODE_COOKIE) {
            VisitorCodeValidator::validate(&code)?;
            debug_log!("Read visitor code '{code}' from cookies");
            (code, false)
        } else if let Some(code) = default_visitor_code {
            VisitorCodeValidator::validate(code)?;
            debug_log!("Used default visitor code '{code}'");
            (code.to_owned(), true)
        } else {
            let code = VisitorCodeGenerator::generate();
            debug_log!("Generated new visitor code '{code}'");
            (code, false)
        };
        if Self::sdk_manage_visitor_code_cookie(self.datafile_manager.datafile().as_ref()) || force_add {
            accessor.set(VISITOR_CODE_COOKIE, &visitor_code, MAX_AGE_COOKIE_SECONDS, Some(&self.top_level_domain));
            debug_log!("Wrote visitor code '{visitor_code}' to cookies");
        }
        return Ok(visitor_code);
    }

    #[trace_method(LogLevel::Debug, skip(self, accessor))]
    fn update_visitor_code(&self, accessor: Option<&mut impl CoreCookieAccessor>, visitor_code: &str, consent: bool) {
        if consent {
            if let Some(accessor) = accessor {
                accessor.set(VISITOR_CODE_COOKIE, visitor_code, MAX_AGE_COOKIE_SECONDS, Some(&self.top_level_domain));
            }
        }
    }

    #[trace_method(LogLevel::Debug, skip(self, accessor))]
    fn process_simulated_variations(&self, accessor: &mut impl CoreCookieAccessor, visitor_code: &str) -> KRes<()> {
        let raw = match accessor.get(SIMULATION_COOKIE) {
            Some(raw) => raw,
            None => return Ok(()),
        };
        let variations = Self::parse_simulated_variations(&self, &raw)?;
        let visitor = self.visitor_manager.get_or_create(visitor_code);
        let mut visitor_data = visitor.wdata();
        visitor_data.simulated_variations.clear();
        for sv in variations {
            visitor_data.simulated_variations.put(sv);
        }
        return Ok(());
    }
}

impl<D: DataFileManager, V: VisitorManager> CookieManagerImpl<D, V> {
    #[trace_method(LogLevel::Debug, skip(retval, datafile_manager, visitor_manager))]
    pub fn new(datafile_manager: Arc<D>, visitor_manager: Arc<V>, top_level_domain: String) -> Self {
        Self {
            datafile_manager,
            visitor_manager,
            top_level_domain,
        }
    }

    fn sdk_manage_visitor_code_cookie(datafile: &impl DataFile) -> bool {
        return !datafile.settings().consent_required() || datafile.has_targeted_delivery_rule();
    }

    fn parse_simulated_variations(&self, raw: &str) -> Result<Vec<SimulatedVariation>, KErr> {
        let variations: HashMap<String, SimVarModel> = serde_json::from_str(raw)
            .map_err(|err| KErr::new(ErrorCode::InvalidSimVarCookieFormat, err.to_string()))?;

        let datafile = self.datafile_manager.datafile();
        let datafile = datafile.as_ref();

        let variations = variations
            .into_iter()
            .filter_map(|(ff_key, model)| Self::sim_var_from_datafile(datafile, ff_key, model))
            .collect();
        return Ok(variations);
    }

    fn sim_var_from_datafile(
        datafile: &impl DataFile,
        feature_key: String,
        sim_var: SimVarModel,
    ) -> Option<SimulatedVariation> {
        let Some(ff) = datafile.feature_flags().get(&feature_key) else {
            error_log!("Simulated feature flag '{feature_key}' not found");
            return None;
        };

        if sim_var.exp_id == 0 {
            return Some(SimulatedVariation::new(None, feature_key));
        }
        let Some(rule) = ff.rules().iter().find(|r| r.experiment().id() == sim_var.exp_id) else {
            error_log!("Simulated experiment {} not found", sim_var.exp_id);
            return None;
        };

        let Some(var_id) = sim_var.var_id.filter(|&var_id| rule.experiment().get_variation_by_id(var_id).is_some())
        else {
            error_log!("Simulated variation {:?} not found", sim_var.var_id);
            return None;
        };

        Some(SimulatedVariation::new(Some(SimulatedTargetingInfo::new(rule.id(), var_id)), feature_key))
    }
}

#[derive(Deserialize)]
struct SimVarModel {
    #[serde(rename = "expId")]
    pub exp_id: u32,
    #[serde(rename = "varId")]
    pub var_id: Option<u32>,
}

// MARK: - Tests

#[cfg(test)]
mod tests {
    use std::sync::{LazyLock, Mutex};

    use arc_swap::Guard;
    use rstest::rstest;
    use tokio::runtime::Handle;

    use crate::{
        cookies::accessor::MockCoreCookieAccessor,
        datafile::{
            datafile_test_utils::{MockDataFile, MockDataFileManager, MockExperiment, MockFeatureFlag, MockRule},
            settings::MockSettings,
            variation_exposition::MockVariationByExposition,
        },
        storage::{storage_test_utils::MockVisitorManager, visitor_manager::VisitorManagerImpl},
        utils::string_generator::VISITOR_CODE_LENGTH,
    };

    use super::*;

    const TOP_LEVEL_DOMAIN: &str = "example.com";

    #[rstest]
    fn test_constants() {
        assert_eq!("kameleoonVisitorCode", VISITOR_CODE_COOKIE);
        assert_eq!("kameleoonSimulationFFData", SIMULATION_COOKIE);
    }

    #[rstest]
    #[case(None, None, false, false, Ok(None), true)]
    #[case(None, None, false, true, Ok(None), true)]
    #[case(None, None, true, true, Ok(None), true)]
    #[case(None, None, true, false, Ok(None), false)]
    #[case(Some("vc1"), None, false, false, Ok(Some("vc1")), true)]
    #[case(Some("vc1"), None, false, true, Ok(Some("vc1")), true)]
    #[case(Some("vc1"), None, true, true, Ok(Some("vc1")), true)]
    #[case(Some("vc1"), None, true, false, Ok(Some("vc1")), false)]
    #[case(None, Some("dvc"), false, false, Ok(Some("dvc")), true)]
    #[case(None, Some("dvc"), false, true, Ok(Some("dvc")), true)]
    #[case(None, Some("dvc"), true, true, Ok(Some("dvc")), true)]
    #[case(None, Some("dvc"), true, false, Ok(Some("dvc")), true)]
    #[case(Some("vc1"), Some("dvc"), false, false, Ok(Some("vc1")), true)]
    #[case(Some("vc1"), Some("dvc"), false, true, Ok(Some("vc1")), true)]
    #[case(Some("vc1"), Some("dvc"), true, true, Ok(Some("vc1")), true)]
    #[case(Some("vc1"), Some("dvc"), true, false, Ok(Some("vc1")), false)]
    #[case(Some(""), None, false, false, Err(ErrorCode::VisitorCodeInvalid), false)]
    #[case(None, Some(""), false, false, Err(ErrorCode::VisitorCodeInvalid), false)]
    #[tokio::test]
    async fn test_get_or_add_visitor_code(
        #[case] existent_vc: Option<&'static str>,
        #[case] default_vc: Option<&str>,
        #[case] consent_required: bool,
        #[case] has_td_rule: bool,
        #[case] expected: Result<Option<&'static str>, ErrorCode>,
        #[case] expect_vc_set: bool,
    ) {
        // arrange
        let mut accessor = MockCoreCookieAccessor::new();
        let cookie_values = Arc::new(Mutex::new(HashMap::from([
            (VISITOR_CODE_COOKIE.to_owned(), existent_vc.map(|vc| vc.to_owned())),
            (SIMULATION_COOKIE.to_owned(), None),
        ])));
        let cookie_values_get = cookie_values.clone();
        accessor.expect_get().times(1..3).returning(move |key| cookie_values_get.lock().unwrap().remove(key).unwrap());
        if expect_vc_set {
            let expected_value = expected.clone().unwrap();
            accessor.expect_set().once().returning(move |key, value, max_age, tld| {
                assert_eq!(VISITOR_CODE_COOKIE, key);
                match expected_value {
                    Some(expected_value) => assert_eq!(expected_value, value),
                    None => assert!(!value.is_empty()),
                }
                assert_eq!(TOP_LEVEL_DOMAIN, tld.unwrap());
                assert_eq!(MAX_AGE_COOKIE_SECONDS, max_age);
            });
        }
        let mut datafile_manager = MockDataFileManager::new();
        datafile_manager.expect_datafile().returning(move || {
            let mut df = MockDataFile::default();
            let mut settings = MockSettings::new();
            settings.expect_consent_required().return_const(consent_required);
            df.expect_settings().return_const(settings);
            df.expect_has_targeted_delivery_rule().return_const(has_td_rule);
            Guard::from_inner(Arc::new(df))
        });
        let datafile_manager = Arc::new(datafile_manager);
        let visitor_manager = Arc::new(VisitorManagerImpl::new(datafile_manager.clone(), Handle::current()));
        let cookie_manager = CookieManagerImpl::new(datafile_manager, visitor_manager, TOP_LEVEL_DOMAIN.to_owned());
        // act
        let actual = cookie_manager.get_or_add_visitor_code(&mut accessor, default_vc);
        // assert
        match expected {
            Ok(expected_vc) => {
                if let Some(expected_vc) = expected_vc {
                    assert_eq!(expected_vc, actual.unwrap());
                } else {
                    assert_eq!(VISITOR_CODE_LENGTH, actual.unwrap().len()); // generated
                }
            }
            Err(expected_err_code) => assert_eq!(expected_err_code, actual.unwrap_err().code()),
        }
        accessor.checkpoint();
        assert!(!cookie_values.lock().unwrap().contains_key(VISITOR_CODE_COOKIE));
    }

    #[rstest]
    #[case(None, Ok(()), None)]
    #[case(Some("{}"), Ok(()), Some(HashMap::new()))]
    #[case(Some("{"), Err(ErrorCode::InvalidSimVarCookieFormat), None)]
    // ---- Single valid ----
    #[case(
        Some("{\"ff1\":{\"expId\":2001,\"varId\":3001}}"),
        Ok(()),
        Some(HashMap::from([
            (
                "ff1".to_owned(),
                SimulatedVariation::new(
                    Some(SimulatedTargetingInfo::new(1001, 3001)),
                    "ff1".to_owned()
                )
            )
        ]))
    )]
    // ---- Two valid ----
    #[case(
        Some("{\"ff1\":{\"expId\":2001,\"varId\":3001},\"ff2\":{\"expId\":2001,\"varId\":3001}}"),
        Ok(()),
        Some(HashMap::from([
            (
                "ff1".to_owned(),
                SimulatedVariation::new(
                    Some(SimulatedTargetingInfo::new(1001, 3001)),
                    "ff1".to_owned()
                )
            ),
            (
                "ff2".to_owned(),
                SimulatedVariation::new(
                    Some(SimulatedTargetingInfo::new(1002, 3001)),
                    "ff2".to_owned()
                )
            )
        ]))
    )]
    // ---- Invalid cases (no insertions) ----
    #[case(
        Some("{\"ff1\":{\"expId\":2002,\"varId\":3001}}"),
        Ok(()),
        Some(HashMap::new())
    )]
    #[case(
        Some("{\"ff1\":{\"expId\":2001,\"varId\":9999}}"),
        Ok(()),
        Some(HashMap::new())
    )]
    #[case(
        Some("{\"unknown\":{\"expId\":2001,\"varId\":3001}}"),
        Ok(()),
        Some(HashMap::new())
    )]
    fn test_process_simulated_variations(
        #[case] cookie: Option<&str>,
        #[case] expected: Result<(), ErrorCode>,
        #[case] expected_sim_vars: Option<HashMap<String, SimulatedVariation>>,
    ) {
        let runtime = tokio::runtime::Runtime::new().unwrap();
        const VC: &str = "vc123";
        const FF1_KEY: &str = "ff1";
        const FF2_KEY: &str = "ff2";
        const RULE1_ID: u32 = 1001;
        const RULE2_ID: u32 = 1002;
        const EXP_ID: u32 = 2001;
        const VAR_ID: u32 = 3001;
        // arrange
        let mut accessor = MockCoreCookieAccessor::new();
        let cookie_values =
            Arc::new(Mutex::new(HashMap::from([(SIMULATION_COOKIE.to_owned(), cookie.map(|c| c.to_owned()))])));
        let cookie_values_get = cookie_values.clone();
        accessor.expect_get().once().returning(move |key| cookie_values_get.lock().unwrap().remove(key).unwrap());
        let mut datafile_manager = MockDataFileManager::new();
        static VAR_BY_EXP: LazyLock<Arc<MockVariationByExposition>> =
            LazyLock::new(|| Arc::new(MockVariationByExposition::new()));
        datafile_manager.expect_datafile().returning(move || {
            let mut df = MockDataFile::default();
            let mut settings = MockSettings::new();
            settings.expect_consent_required().return_const(true);
            df.expect_settings().return_const(settings);
            df.expect_has_targeted_delivery_rule().return_const(false);
            let ff1 = make_mock_feature_flag(RULE1_ID, EXP_ID, VAR_ID, &VAR_BY_EXP);
            let ff2 = make_mock_feature_flag(RULE2_ID, EXP_ID, VAR_ID, &VAR_BY_EXP);
            df.expect_feature_flags().return_const(HashMap::from([
                (FF1_KEY.to_owned(), Arc::new(ff1)),
                (FF2_KEY.to_owned(), Arc::new(ff2)),
            ]));
            Guard::from_inner(Arc::new(df))
        });
        let datafile_manager = Arc::new(datafile_manager);
        let visitor_manager = Arc::new(VisitorManagerImpl::new(datafile_manager.clone(), runtime.handle().clone()));
        let cookie_manager = CookieManagerImpl::new(datafile_manager, visitor_manager, TOP_LEVEL_DOMAIN.to_owned());

        // act
        let actual_result = cookie_manager.process_simulated_variations(&mut accessor, VC);

        // assert
        if let Err(expected_err_code) = expected {
            assert_eq!(expected_err_code, actual_result.unwrap_err().code());
        } else {
            assert!(actual_result.is_ok());

            let visitor = cookie_manager.visitor_manager.get(VC);

            if let Some(expected_map) = expected_sim_vars {
                let visitor = visitor.unwrap();
                let visitor_data = visitor.rdata();
                let actual_map = visitor_data.simulated_variations.r();
                assert_eq!(expected_map.len(), actual_map.len());
                for (k, v) in &expected_map {
                    assert_eq!(Some(v), actual_map.get(k));
                }
            } else {
                assert!(visitor.is_none());
            }
        }
        accessor.checkpoint();
        assert!(cookie_values.lock().unwrap().is_empty());
    }

    fn make_mock_feature_flag(
        rule_id: u32,
        exp_id: u32,
        var_id: u32,
        var_by_exp: &'static Arc<MockVariationByExposition>,
    ) -> MockFeatureFlag {
        let mut ff = MockFeatureFlag::default();
        let mut rule = MockRule::default();
        rule.expect_id().return_const(rule_id);
        let mut experiment = MockExperiment::new();
        experiment.expect_id().return_const(exp_id);
        experiment.expect_get_variation_by_id().returning(move |id| if id == var_id { Some(var_by_exp) } else { None });
        rule.expect_experiment().return_const(experiment);
        ff.expect_rules().return_const(vec![rule]);
        ff
    }

    #[rstest]
    #[case("vc2", false, "")]
    #[case("vc3", true, "vc3")]
    fn test_update(#[case] visitor_code: &str, #[case] consent: bool, #[case] expected_value: &'static str) {
        // arrange
        let mut accessor = MockCoreCookieAccessor::new();
        if consent {
            accessor.expect_set().once().returning(move |key, value, max_age, tld| {
                assert_eq!(VISITOR_CODE_COOKIE, key);
                assert_eq!(expected_value, value);
                assert_eq!(TOP_LEVEL_DOMAIN, tld.unwrap());
                assert_eq!(MAX_AGE_COOKIE_SECONDS, max_age);
            });
        } else {
            accessor.expect_set().never();
        }
        let datafile_manager = Arc::new(MockDataFileManager::new());
        let visitor_manager = Arc::new(MockVisitorManager::new());
        let cookie_manager = CookieManagerImpl::new(datafile_manager, visitor_manager, TOP_LEVEL_DOMAIN.to_owned());
        // act
        cookie_manager.update_visitor_code(Some(&mut accessor), visitor_code, consent);
        // assert
        accessor.checkpoint();
    }
}
