use std::sync::Arc;

use async_trait::async_trait;
use kameleoon_trace_logging::trace_method;

use crate::{
    datafile::{datafile::DataFile, datafile_manager::DataFileManager},
    debug_log,
    error::{ErrorCode, KErr},
    logging::LogLevel,
    network::network_manager::data_api::DataApiNetworkManager,
    remote_visitor_data::parsing::RemoteVisitorData,
    storage::visitor_manager::VisitorManager,
    types::RemoteVisitorDataFilter,
    utils::{visitor_code_validator::VisitorCodeValidator, KRes},
};

mod dto;
mod parsing;

#[async_trait]
#[cfg_attr(test, mockall::automock)]
pub trait RemoteVisitorDataManager: Send + Sync {
    async fn fetch(&self, visitor_code: &str, filter: Option<RemoteVisitorDataFilter>) -> KRes<()>;
}

pub(crate) struct RemoteVisitorDataManagerImpl<V, D, N> {
    visitor_manager: Arc<V>,
    datafile_manager: Arc<D>,
    data_api: Arc<N>,
}

#[async_trait]
impl<V: VisitorManager, D: DataFileManager, N: DataApiNetworkManager> RemoteVisitorDataManager
    for RemoteVisitorDataManagerImpl<V, D, N>
{
    async fn fetch(&self, visitor_code: &str, filter: Option<RemoteVisitorDataFilter>) -> KRes<()> {
        //~ [42126]
        debug_log!("CALL RemoteVisitorDataManagerImpl::fetch(visitor_code: {visitor_code}, filter: {:?})", filter);
        let result = self._fetch(visitor_code, filter).await;
        debug_log!("RET RemoteVisitorDataManagerImpl::fetch(visitor_code: {visitor_code}) → {:?}", result);
        result
    }
}

impl<V: VisitorManager, D: DataFileManager, N: DataApiNetworkManager> RemoteVisitorDataManagerImpl<V, D, N> {
    #[trace_method(LogLevel::Debug, skip(retval, visitor_manager, datafile_manager, data_api))]
    pub fn new(visitor_manager: Arc<V>, datafile_manager: Arc<D>, data_api: Arc<N>) -> Self {
        return Self {
            visitor_manager,
            datafile_manager,
            data_api,
        };
    }

    async fn _fetch(&self, visitor_code: &str, filter: Option<RemoteVisitorDataFilter>) -> KRes<()> {
        VisitorCodeValidator::validate(visitor_code)?;
        let filter = filter.unwrap_or_else(RemoteVisitorDataFilter::default);
        let visitor = self.visitor_manager.get_or_create(visitor_code);
        let is_unique_identifier = visitor.is_unique_identifier();
        let response = self.data_api.get_remote_visitor_data(visitor_code, &filter, is_unique_identifier).await?;
        if response.status_code / 100 != 2 {
            let msg = format!("Failed to fetch remote visitor data: unexpected status code {}", response.status_code);
            return Err(KErr::new(ErrorCode::Network, msg));
        }
        let model = response.json()?;
        let mut rvd = RemoteVisitorData::new(&filter);
        rvd.parse(model);
        rvd.mark_visitor_data_as_sent(self.datafile_manager.datafile().custom_data_info());
        // Localize visitor visits
        if let Some(vv) = rvd.visitor_visits {
            rvd.visitor_visits = Some(vv.into_local(visitor.rdata().time_started()));
        }
        // We apply visitor code from the latest visit fetched from Data API
        if filter.visitor_code {
            if let Some(mi) = &rvd.visitor_code {
                visitor.wdata().set_mapping_identifier(mi);
            }
        }
        // Cannot use 'visitor_manager.add_data' because it could use remote visitor data for mapping
        let data_to_add = rvd.collect_visitor_data_to_add();
        visitor.add_data(data_to_add.into_iter(), false);
        return Ok(());
    }
}

// MARK: - Tests

#[cfg(test)]
mod tests {
    use std::{
        collections::HashMap,
        mem,
        time::{Duration, SystemTime},
    };

    use crate::{
        data::{
            assigned_variation::AssignedVariation,
            cbscores::{CBScores, ScoredVarId},
            geolocation::GeolocationBuilder,
            page_view_visit::PageViewVisit,
            personalization::Personalization,
            visitor_visits::{Visit, VisitorVisits},
            BaseData, Browser, BrowserKind, Conversion, CustomData, Device, DeviceKind, OperatingSystem,
            OperatingSystemKind, PageView, UniqueIdentifier,
        },
        datafile::{
            datafile::DataFileImpl, datafile_manager::MockDataFileManager, datafile_test_utils::make_cdi_datafile_dto,
            rule::RuleType,
        },
        error::ErrorCode,
        network::{dto::CustomDataDto, network_manager::data_api::MockDataApiNetworkManager, response::Response},
        storage::{visitor::Visitor, visitor_manager::VisitorManagerImpl},
        tracking::trackable::{Trackable, TrackingState},
        types::remote_visitor_data_filter::RemoteVisitorDataFilterBuilder,
        utils::{
            test_utils::{self},
            time_utils::millis_to_time,
        },
    };

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

    fn new_static_data_filter() -> RemoteVisitorDataFilter {
        RemoteVisitorDataFilterBuilder::default().device(true).browser(true).operating_system(true).build().unwrap()
    }

    fn make_cbs() -> CBScores {
        CBScores::new(&HashMap::from([
            (
                1,
                vec![
                    ScoredVarId {
                        variation_id: 1,
                        score: 0.3428662318524164,
                    },
                    ScoredVarId {
                        variation_id: 2,
                        score: 1.2303009341855722,
                    },
                ],
            ),
            (
                2,
                vec![
                    ScoredVarId {
                        variation_id: 3,
                        score: 0.5,
                    },
                    ScoredVarId {
                        variation_id: 4,
                        score: 0.5,
                    },
                ],
            ),
        ]))
    }

    fn mark_as_sent_if_required(sent: bool, trackable: &impl Trackable) {
        if sent {
            trackable.state_cell().change_state(TrackingState::Sent);
        }
    }

    fn make_existent_data() -> Vec<BaseData> {
        vec![
            make_custom_data(false, 8, "other_value_2"),
            make_assigned_variation(false, 151233, 859906, 1234),
            make_geolocation(false),
            make_device(false, DeviceKind::Desktop),
            make_browser(false, BrowserKind::Chrome, Some(15.5)),
            make_operating_system(false, OperatingSystemKind::IOS),
        ]
    }

    fn make_custom_data(sent: bool, index: u32, value: &str) -> BaseData {
        let cd = CustomData::new_with_index(index, vec![value.to_owned()]);
        mark_as_sent_if_required(sent, &cd);
        return BaseData::CustomData(cd);
    }

    fn make_conversion(sent: bool, goal_id: u32, revenue: f32, negative: bool, assignment_ts: u64) -> BaseData {
        let c = Conversion::new_with_opts_time(
            goal_id,
            crate::data::ConversionOpts::new().revenue(revenue).negative(negative),
            millis_to_time(assignment_ts),
        );
        mark_as_sent_if_required(sent, &c);
        return BaseData::Conversion(c);
    }

    fn make_page_view_visit(sent: bool, url: &str, count: u32, last_ts: u64) -> BaseData {
        let pv = PageView::new(url, Some("title"), vec![]);
        mark_as_sent_if_required(sent, &pv);
        let pvv = PageViewVisit::new(pv, count, Some(millis_to_time(last_ts)));
        return BaseData::PageViewVisit(pvv);
    }

    fn make_assigned_variation(sent: bool, exp_id: u32, var_id: u32, assignment_ts: u64) -> BaseData {
        let av = AssignedVariation::new_with_time(exp_id, var_id, millis_to_time(assignment_ts), RuleType::Unknown);
        mark_as_sent_if_required(sent, &av);
        return BaseData::AssignedVariation(av);
    }

    fn make_geolocation(sent: bool) -> BaseData {
        let g = GeolocationBuilder::default()
            .country("France")
            .region("Île-de-France".to_owned())
            .city("Paris".to_owned())
            .build()
            .unwrap();
        mark_as_sent_if_required(sent, &g);
        return BaseData::Geolocation(g);
    }

    fn make_device(sent: bool, device_type: DeviceKind) -> BaseData {
        let d = Device::new(device_type);
        mark_as_sent_if_required(sent, &d);
        return BaseData::Device(d);
    }

    fn make_browser(sent: bool, browser_type: BrowserKind, version: Option<f32>) -> BaseData {
        let b = Browser::new(browser_type, version);
        mark_as_sent_if_required(sent, &b);
        return BaseData::Browser(b);
    }

    fn make_operating_system(sent: bool, os_type: OperatingSystemKind) -> BaseData {
        let os = OperatingSystem::new(os_type);
        mark_as_sent_if_required(sent, &os);
        return BaseData::OperatingSystem(os);
    }

    #[rstest]
    #[case( // 1. Error
        "remote_visitor_data_wrong",
        None, false, vec![], vec![],
        vec![], None, None, Some(ErrorCode::Network)
    )]
    #[case( // 2. Default Filters
        "remote_visitor_data_empty",
        None, false, vec![], vec![],
        vec![], None, None, None
    )]
    #[case( // 3. Custom Filters
        "remote_visitor_data_empty",
        Some(
            RemoteVisitorDataFilterBuilder::default()
                .previous_visit_amount(25).geolocation(true).device(true).experiments(true)
                .build().unwrap()
        ),
        false, vec![], vec![],
        vec![], None, None, None
    )]
    #[case( // 4. Check Unique Identifier
        "remote_visitor_data_empty",
        None, true, vec![], vec![BaseData::UniqueIdentifier(UniqueIdentifier::new(true))],
        vec![BaseData::UniqueIdentifier(UniqueIdentifier::new(true))], None, None, None
    )]
    #[case( // 5. Current + One CustomData
        "remote_visitor_data_current_cd",
        None, false, vec![], vec![],
        vec![make_custom_data(true, 8, "other_value")], None, Some("hello"), None
    )]
    #[case( // 6. Current + One CustomData in Visitor Scope
        "remote_visitor_data_current_cd",
        None, false, vec![CustomDataDto {
            id: 1, name: "".to_owned(), index: 8, local_only: false, is_mapping_identifier: false, scope: "VISITOR".to_owned()
        }], vec![],
        vec![make_custom_data(false, 8, "other_value")], None, Some("hello"), None
    )]
    #[case( // 7. RemoteVisitorDataFilter.visitorCode == false
        "remote_visitor_data_current_cd",
        Some(RemoteVisitorDataFilterBuilder::default().previous_visit_amount(25).visitor_code(false).build().unwrap()),
        false, vec![], vec![],
        vec![make_custom_data(true, 8, "other_value")], None, None, None
    )]
    #[case( // 8. Current + All Data
        "remote_visitor_data_current_all",
        Some(new_static_data_filter()), false, vec![], vec![],
        vec![
            make_custom_data(true, 8, "right_value"),
            make_conversion(true, 10, 20.0, false, 1715000252324),
            make_page_view_visit(true, "https://google.com/?aaaa?=1000,", 2, 1710216976907),
            make_assigned_variation(true, 151233, 859906, 1710338712283),
            make_geolocation(true),
            make_device(true, DeviceKind::Phone),
            make_browser(true, BrowserKind::Firefox, Some(10.5)),
            make_operating_system(true, OperatingSystemKind::Linux),
        ], None, Some("hello"), None
    )]
    #[case( // 9. Previous + All Data
        "remote_visitor_data_previous_one",
        Some(new_static_data_filter()), false, vec![], vec![],
        vec![
            make_custom_data(true, 8, "other_value"),
            make_page_view_visit(true, "https://google.com/?aaaa?=1000,", 1, 1710216976906),
            make_assigned_variation(true, 151233, 859906, 1710338712282),
            make_geolocation(true),
            make_device(true, DeviceKind::Phone),
            make_browser(true, BrowserKind::Firefox, Some(10.5)),
            make_operating_system(true, OperatingSystemKind::Linux),
        ], Some(VisitorVisits::new(
            40, vec![Visit::new(millis_to_time(1710338013259), millis_to_time(1710338013260))],
            SystemTime::UNIX_EPOCH, Duration::ZERO
        )), Some("hello"), None
    )]
    #[case( // 10. Current + Previous 2 + All Data
        "remote_visitor_data_current_previous",
        Some(new_static_data_filter()), false, vec![], vec![],
        vec![
            make_custom_data(true, 8, "other_value"),
            make_page_view_visit(true, "https://google.com/?aaaa?=1000,", 2, 1710216976906),
            make_assigned_variation(true, 151233, 859906, 1710338712282),
            make_geolocation(true),
            make_device(true, DeviceKind::Phone),
            make_browser(true, BrowserKind::Firefox, Some(10.5)),
            make_operating_system(true, OperatingSystemKind::Linux),
            BaseData::Personalization(Personalization::new(119037, 0, millis_to_time(1728374170938))),
            BaseData::Personalization(Personalization::new(119038, 1, millis_to_time(1728374170938))),
            BaseData::CBScores(make_cbs()),
        ], Some(VisitorVisits::new(
            40, vec![
                Visit::new(millis_to_time(1710338013259), millis_to_time(1710338013259)),
                Visit::new(millis_to_time(1710338013258), millis_to_time(1710338013258)),
            ], SystemTime::UNIX_EPOCH, Duration::ZERO
        )), Some("hello_cur"), None
    )]
    #[case( // 11. "Current + Previous + All Data
        "remote_visitor_data_current_previous",
        Some(new_static_data_filter()), false, vec![], {
            let mut existent_data = make_existent_data();
            existent_data.push(make_page_view_visit(false, "https://google.com/?aaaa?=1000,", 10, 1810338013259));
            existent_data
        }, {
            let mut expected_data = make_existent_data();
            expected_data.append(&mut vec![
                make_page_view_visit(false, "https://google.com/?aaaa?=1000,", 12, 1810338013259),
                BaseData::Personalization(Personalization::new(119037, 0, millis_to_time(1728374170938))),
                BaseData::Personalization(Personalization::new(119038, 1, millis_to_time(1728374170938))),
                BaseData::CBScores(make_cbs()),
            ]);
            expected_data
        }, Some(VisitorVisits::new(
            40, vec![
                Visit::new(millis_to_time(1710338013259), millis_to_time(1710338013259)),
                Visit::new(millis_to_time(1710338013258), millis_to_time(1710338013258)),
            ], SystemTime::UNIX_EPOCH, Duration::ZERO
        )), Some("hello_cur"), None
    )]
    #[tokio::test]
    async fn test_fetch(
        #[case] response_file_name: String,
        #[case] filter: Option<RemoteVisitorDataFilter>,
        #[case] is_unique_identifier: bool,
        #[case] cdi: Vec<CustomDataDto>,
        #[case] existent_data: Vec<BaseData>,
        #[case] expected_data: Vec<BaseData>,
        #[case] expected_visitor_visits: Option<VisitorVisits>,
        #[case] expected_mapping_identifier: Option<&str>,
        #[case] expected_error: Option<ErrorCode>,
    ) {
        // arrange
        const VISITOR_CODE: &str = "visitor-code";
        let response_body = test_utils::load_file(&format!("resources/remote_data/{response_file_name}.json"));
        let mut datafile_manager = MockDataFileManager::new();
        datafile_manager.expect_datafile().returning(move || {
            Guard::from_inner(Arc::new(DataFileImpl::new(make_cdi_datafile_dto(cdi.clone()), None, None)))
        });
        let datafile_manager = Arc::new(datafile_manager);
        let visitor_manager = Arc::new(VisitorManagerImpl::new(datafile_manager.clone(), Handle::current()));
        if !existent_data.is_empty() {
            visitor_manager.get_or_create(VISITOR_CODE).add_data(existent_data.into_iter(), true);
        }
        let expected_filter = filter.as_ref().map_or_else(RemoteVisitorDataFilter::default, |f| f.clone());
        let mut data_api = MockDataApiNetworkManager::new();
        data_api.expect_get_remote_visitor_data().once().return_once(
            move |actual_visitor_code, actual_filter, actual_is_unique_identifier| {
                assert_eq!(VISITOR_CODE, actual_visitor_code);
                assert_eq!(&expected_filter, actual_filter);
                assert_eq!(is_unique_identifier, actual_is_unique_identifier);
                let response = Response {
                    status_code: 200,
                    body: response_body.into_bytes(),
                    headers: vec![],
                };
                Box::pin(std::future::ready(Ok(response)))
            },
        );
        let data_api = Arc::new(data_api);
        let mut rvd_manager = RemoteVisitorDataManagerImpl::new(visitor_manager, datafile_manager, data_api);
        // act
        let actual_result = rvd_manager.fetch(VISITOR_CODE, filter).await;
        // assert
        match expected_error {
            Some(expected_error) => assert_eq!(expected_error, actual_result.err().unwrap().code()),
            None => {
                assert_eq!(None, actual_result.err());
                // check data
                let visitor = rvd_manager.visitor_manager.get_or_create(VISITOR_CODE);
                let actual_data = visitor.rdata();
                let expected_visitor = Visitor::new();
                expected_visitor.add_data(expected_data.into_iter(), true);
                let mut expected_data = expected_visitor.wdata();
                if let Some(vv) = expected_visitor_visits {
                    expected_data.visitor_visits.set(Box::new(vv.into_local(actual_data.time_started())), true);
                }
                if let Some(mi) = expected_mapping_identifier {
                    expected_data.set_mapping_identifier(mi);
                }
                expected_data.set_time_started(actual_data.time_started());
                expected_data.set_last_activity_time(actual_data.last_activity_time());
                assert_eq!(*expected_data, *actual_data);
            }
        }
        let data_api = mem::replace(&mut rvd_manager.data_api, Arc::new(MockDataApiNetworkManager::new()));
        let mut data_api = Arc::into_inner(data_api).unwrap();
        data_api.checkpoint();
    }
}
