use std::{sync::Arc, time::Duration};

use async_trait::async_trait;
use kameleoon_trace_logging::trace_method;
use tokio::{runtime::Handle, sync::Mutex, time::Instant};

use crate::{
    datafile::datafile_manager::DataFileManager,
    debug_log,
    logging::LogLevel,
    network::network_manager::data_api::DataApiNetworkManager,
    storage::visitor_manager::VisitorManager,
    tracking::{
        trackable::{TrackingState, TrackingStateCell},
        tracking_builder::TrackingBuilder,
        visitor_tracking_registry::VisitorTrackingRegistry,
    },
    warn_log,
};

#[async_trait]
pub(crate) trait TrackingManager {
    async fn add_visitor_code(&self, visitor_code: String);
    async fn track_all(&self);
    async fn track_visitor(&self, visitor_code: String);
}

const LINES_DELIMITER: &str = "\n";
const REQUEST_SIZE_LIMIT: usize = 2560 * 1024; // 2.5 * 1024^2 characters

pub(crate) struct TrackingManagerImpl<R, D, N, V> {
    tracking_visitors: Arc<Mutex<R>>,
    datafile_manager: Arc<D>,
    data_api: Arc<N>,
    visitor_manager: Arc<V>,
    tracking_interval: Duration,
    runtime_handle: Handle,
}

#[async_trait]
impl<R, D, N, V> TrackingManager for TrackingManagerImpl<R, D, N, V>
where
    R: VisitorTrackingRegistry + Send + 'static,
    D: DataFileManager + 'static,
    N: DataApiNetworkManager + 'static,
    V: VisitorManager + 'static,
{
    async fn add_visitor_code(&self, visitor_code: String) {
        //~ [42126]
        debug_log!("CALL TrackingManagerImpl::add_visitor_code(visitor_code: {visitor_code})");
        self.tracking_visitors.lock().await.add(visitor_code);
        debug_log!("RET TrackingManagerImpl::add_visitor_code()");
    }

    async fn track_all(&self) {
        //~ [42126]
        debug_log!("CALL TrackingManagerImpl::track_all()");
        let visitor_codes = self.tracking_visitors.lock().await.extract();
        self.track(visitor_codes).await;
        debug_log!("RET TrackingManagerImpl::track_all()");
    }

    async fn track_visitor(&self, visitor_code: String) {
        //~ [42126]
        debug_log!("CALL TrackingManagerImpl::track_visitor(visitor_code: {visitor_code})");
        self.track(vec![visitor_code]).await;
        debug_log!("RET TrackingManagerImpl::track_visitor()");
    }
}

impl<R, D, N, V> TrackingManagerImpl<R, D, N, V>
where
    R: VisitorTrackingRegistry + Send + 'static,
    D: DataFileManager + 'static,
    N: DataApiNetworkManager + 'static,
    V: VisitorManager + 'static,
{
    #[trace_method(
        LogLevel::Debug,
        skip(
            retval,
            tracking_visitors,
            datafile_manager,
            data_api,
            visitor_manager,
            runtime_handle
        )
    )]
    pub fn new(
        tracking_visitors: R,
        datafile_manager: Arc<D>,
        data_api: Arc<N>,
        visitor_manager: Arc<V>,
        tracking_interval: Duration,
        runtime_handle: Handle,
    ) -> Self {
        Self {
            tracking_visitors: Arc::new(Mutex::new(tracking_visitors)),
            datafile_manager,
            data_api,
            visitor_manager,
            tracking_interval,
            runtime_handle,
        }
    }

    async fn track(&self, visitor_codes: Vec<String>) {
        let datafile = self.datafile_manager.datafile().clone();
        let builder = TrackingBuilder::new(datafile, self.visitor_manager.clone(), REQUEST_SIZE_LIMIT);
        let data = builder.build(visitor_codes);
        if !data.visitor_codes_to_keep.is_empty() {
            warn_log!(
                "Visitor data to be tracked exceeded the request size limit. \
                Some visitor data is kept to be sent later. \
                If it is not caused by the peak load, decreasing the tracking interval is recommended."
            );
            self.tracking_visitors.lock().await.add_all(data.visitor_codes_to_keep);
        }
        self.perform_tracking_request(data.visitor_codes_to_send, data.unsent_trackable_data, data.tracking_lines)
            .await;
    }

    async fn perform_tracking_request(
        &self,
        visitor_codes: Vec<String>,
        visitor_data: Vec<Arc<TrackingStateCell>>,
        tracking_lines: Vec<String>,
    ) {
        if tracking_lines.is_empty() {
            return;
        }
        let lines = tracking_lines.join(LINES_DELIMITER).into_bytes();
        // Mark unsent data as transmitted
        for sendable_data in visitor_data.iter() {
            sendable_data.change_state(TrackingState::Transmitting);
        }
        let response = self.data_api.track_data(lines).await;
        let success = response.map_or_else(
            |err| {
                debug_log!("Tracking request failed: {err}");
                false
            },
            |response| {
                if response.status_code / 100 != 2 {
                    debug_log!("Tracking request failed: unexpected status code {}", response.status_code);
                    false
                } else {
                    debug_log!("Tracking request succeeded");
                    true
                }
            },
        );
        let state = if success { TrackingState::Sent } else { TrackingState::Unsent };
        for sendable_data in visitor_data.iter() {
            sendable_data.change_state(state);
        }
        if !success {
            self.tracking_visitors.lock().await.add_all(visitor_codes);
        }
    }

    pub fn start_tracking(self: &Arc<Self>) {
        let tracking_interval = self.tracking_interval;
        let tm = Arc::downgrade(self);
        self.runtime_handle.spawn(async move {
            let mut interval = tokio::time::interval_at(Instant::now() + tracking_interval, tracking_interval);
            loop {
                interval.tick().await;
                match tm.upgrade() {
                    Some(tm) => tm.track_all().await,
                    None => break,
                }
            }
        });
    }
}

// MARK: - Tests

#[cfg(test)]
mod tests {
    use std::{pin::Pin, time::SystemTime};

    use crate::{
        assert_delta,
        data::{BaseData, Device, DeviceKind},
        datafile::{
            datafile::{DataFile, DataFileImpl},
            datafile_manager::MockDataFileManager,
            datafile_test_utils::make_cdi_datafile_dto,
        },
        network::{
            network_error::NetworkError, network_manager::data_api::MockDataApiNetworkManager, response::Response,
        },
        storage::visitor_manager::{AddDataOptions, VisitorManagerImpl},
        tracking::{
            activity_event::ActivityEvent, trackable::Trackable, visitor_tracking_registry::MockVisitorTrackingRegistry,
        },
        utils::test_utils::{remove_nonce_parameter, soft_remove_nonce_parameter},
    };

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

    type TestTrackingManager = TrackingManagerImpl<
        MockVisitorTrackingRegistry,
        MockDataFileManager,
        MockDataApiNetworkManager,
        VisitorManagerImpl<MockDataFileManager>,
    >;

    fn new_tracking_manager(
        tracking_visitors: MockVisitorTrackingRegistry,
        data_api: Arc<MockDataApiNetworkManager>,
    ) -> TestTrackingManager {
        return new_interval_tracking_manager(tracking_visitors, data_api, Duration::from_secs(1), Handle::current());
    }

    fn new_interval_tracking_manager(
        tracking_visitors: MockVisitorTrackingRegistry,
        data_api: Arc<MockDataApiNetworkManager>,
        tracking_interval: Duration,
        runtime_handle: Handle,
    ) -> TestTrackingManager {
        let mut datafile_manager = MockDataFileManager::new();
        // returning a datafile with consent not required
        datafile_manager
            .expect_datafile()
            .returning(|| Guard::from_inner(Arc::new(DataFileImpl::new(make_cdi_datafile_dto(vec![]), None, None))));
        let datafile_manager = Arc::new(datafile_manager);
        let visitor_manager = Arc::new(VisitorManagerImpl::new(datafile_manager.clone(), Handle::current()));
        TestTrackingManager::new(
            tracking_visitors,
            datafile_manager,
            data_api,
            visitor_manager,
            tracking_interval,
            runtime_handle,
        )
    }

    fn make_activity_event_line(visitor_code: &str) -> String {
        format!("{}&visitorCode={visitor_code}", ActivityEvent::new().encode_query())
    }

    fn make_device_line(visitor_code: &str, device_type: DeviceKind) -> String {
        format!("{}&visitorCode={visitor_code}", Device::new(device_type).encode_query())
    }

    fn make_response(
        result: Result<u16, ()>,
    ) -> Pin<Box<dyn std::future::Future<Output = std::result::Result<Response, NetworkError>> + Send>> {
        Box::pin(std::future::ready(match result {
            Ok(code) => Ok(Response::new(code)),
            Err(_) => Err(NetworkError::Timeout),
        }))
    }

    fn expect_response(
        data_api: &mut MockDataApiNetworkManager,
        expected_tracking_data: String,
        resp_result: Result<u16, ()>,
    ) {
        data_api.expect_track_data().once().returning(move |actual_tracking_data| {
            assert_eq!(
                soft_remove_nonce_parameter(&expected_tracking_data),
                soft_remove_nonce_parameter(&String::from_utf8(actual_tracking_data).unwrap())
            );
            make_response(resp_result)
        });
    }

    fn checkpoint_mocks(mut tracking_manager: TestTrackingManager) {
        let data_api = std::mem::replace(&mut tracking_manager.data_api, Arc::default());
        let mut data_api = Arc::into_inner(data_api).unwrap();
        data_api.checkpoint();
        let tracking_visitors = std::mem::replace(&mut tracking_manager.tracking_visitors, Arc::default());
        let tracking_visitors = Arc::into_inner(tracking_visitors).unwrap();
        let mut tracking_visitors = Mutex::into_inner(tracking_visitors);
        tracking_visitors.checkpoint();
    }

    #[tokio::test]
    async fn test_add_visitor_code() {
        // arrange
        const VC: &str = "vc123";
        let data_api = MockDataApiNetworkManager::new();
        let mut tracking_visitors = MockVisitorTrackingRegistry::new();
        tracking_visitors.expect_add().once().returning(move |vc| assert_eq!(vc, VC));
        let tracking_manager = new_tracking_manager(tracking_visitors, Arc::new(data_api));
        // act
        tracking_manager.add_visitor_code(VC.to_owned()).await;
        // assert
        checkpoint_mocks(tracking_manager);
    }

    #[rstest]
    #[case(vec![], vec![], None, false)]
    #[case(vec!["vc1"], vec![], Some((make_activity_event_line("vc1"), Ok(204))), false)]
    #[case(vec!["vc1"], vec![], Some((make_activity_event_line("vc1"), Ok(500))), true)]
    #[case(vec!["vc1"], vec![], Some((make_activity_event_line("vc1"), Err(()))), true)]
    #[case(
        vec!["vc1", "vc2"], vec![("vc1", DeviceKind::Desktop), ("vc2", DeviceKind::Phone)],
        Some((
            format!("{}\n{}", make_device_line("vc1", DeviceKind::Desktop), make_device_line("vc2", DeviceKind::Phone)),
            Ok(204)
        )),
        false
    )]
    #[case(
        vec!["vc1", "vc2"], vec![("vc1", DeviceKind::Desktop), ("vc2", DeviceKind::Phone)],
        Some((
            format!("{}\n{}", make_device_line("vc1", DeviceKind::Desktop), make_device_line("vc2", DeviceKind::Phone)),
            Err(())
        )),
        true
    )]
    #[tokio::test]
    async fn test_track_all(
        #[case] visitor_codes: Vec<&str>,
        #[case] visitor_data: Vec<(&str, DeviceKind)>,
        #[case] expect_resp: Option<(String, Result<u16, ()>)>,
        #[case] expect_visitor_codes_return: bool,
    ) {
        // arrange
        let mut data_api = MockDataApiNetworkManager::new();
        if let Some((expected_tracking_data, resp_result)) = expect_resp {
            expect_response(&mut data_api, expected_tracking_data, resp_result);
        }
        let mut tracking_visitors = MockVisitorTrackingRegistry::new();
        let visitor_codes = Vec::from_iter(visitor_codes.into_iter().map(|vc| vc.to_owned()));
        if expect_visitor_codes_return {
            let visitor_codes = visitor_codes.clone();
            tracking_visitors.expect_add_all().once().returning(move |vcs| assert_eq!(vcs, visitor_codes));
        }
        tracking_visitors.expect_extract().once().return_once(|| visitor_codes);
        let tracking_manager = new_tracking_manager(tracking_visitors, Arc::new(data_api));
        for (vc, data) in visitor_data {
            let data = BaseData::Device(Device::new(data));
            tracking_manager.visitor_manager.add_data(
                vc,
                std::iter::once(data),
                AddDataOptions {
                    overwrite: true,
                    track: true,
                },
            );
        }
        // act
        tracking_manager.track_all().await;
        // assert
        checkpoint_mocks(tracking_manager);
    }

    #[rstest]
    #[case("vc3", None, make_activity_event_line("vc3"), Ok(204), false)]
    #[case("vc3", None, make_activity_event_line("vc3"), Ok(500), true)]
    #[case("vc3", None, make_activity_event_line("vc3"), Err(()), true)]
    #[case(
        "vc3",
        Some(DeviceKind::Tablet),
        make_device_line("vc3", DeviceKind::Tablet),
        Ok(204),
        false
    )]
    #[case("vc3", Some(DeviceKind::Tablet), make_device_line("vc3", DeviceKind::Tablet), Err(()), true)]
    #[tokio::test]
    async fn test_track_visitor(
        #[case] visitor_code: &str,
        #[case] visitor_data: Option<DeviceKind>,
        #[case] expected_tracking_data: String,
        #[case] resp_result: Result<u16, ()>,
        #[case] expect_visitor_code_return: bool,
    ) {
        // arrange
        let mut data_api = MockDataApiNetworkManager::new();
        expect_response(&mut data_api, expected_tracking_data, resp_result);
        let mut tracking_visitors = MockVisitorTrackingRegistry::new();
        if expect_visitor_code_return {
            let visitor_codes = vec![visitor_code.to_owned()];
            tracking_visitors.expect_add_all().once().returning(move |vcs| assert_eq!(vcs, visitor_codes));
        }
        let tracking_manager = new_tracking_manager(tracking_visitors, Arc::new(data_api));
        if let Some(device_type) = visitor_data {
            let device = BaseData::Device(Device::new(device_type));
            tracking_manager.visitor_manager.add_data(
                visitor_code,
                std::iter::once(device),
                AddDataOptions {
                    overwrite: true,
                    track: true,
                },
            );
        }
        // act
        tracking_manager.track_visitor(visitor_code.to_owned()).await;
        // assert
        checkpoint_mocks(tracking_manager);
    }

    #[tokio::test]
    async fn test_interval_tracking() {
        const TRACKING_INTERVAL: Duration = Duration::from_millis(100);
        const S: Duration = Duration::from_millis(25);
        const VC: &str = "vc321";
        // arrange
        let (tx, mut rx) = tokio::sync::mpsc::channel(16);
        let mut data_api = MockDataApiNetworkManager::new();
        let expected_tracking_data = remove_nonce_parameter(&make_activity_event_line(VC));
        data_api.expect_track_data().times(2).returning(move |actual_tracking_data| {
            tx.try_send(SystemTime::now()).unwrap();
            assert_eq!(
                expected_tracking_data,
                soft_remove_nonce_parameter(&String::from_utf8(actual_tracking_data).unwrap())
            );
            make_response(Ok(204))
        });
        let data_api = Arc::new(data_api);
        let mut tracking_visitors = MockVisitorTrackingRegistry::new();
        tracking_visitors.expect_extract().returning(|| vec![VC.to_owned()]);
        let tracking_manager = Arc::new(new_interval_tracking_manager(
            tracking_visitors,
            data_api.clone(),
            TRACKING_INTERVAL,
            Handle::current(),
        ));
        let start = SystemTime::now();
        // act
        tracking_manager.start_tracking();
        tokio::time::sleep(TRACKING_INTERVAL * 2 + S).await;
        drop(tracking_manager);
        tokio::time::sleep(TRACKING_INTERVAL).await;
        // assert
        Arc::into_inner(data_api).unwrap().checkpoint();
        let mut actual_timestamps = vec![];
        assert_eq!(2, timeout(Duration::from_secs(1), rx.recv_many(&mut actual_timestamps, 3)).await.unwrap());
        assert_delta!(TRACKING_INTERVAL, actual_timestamps[0].duration_since(start).unwrap(), S);
        assert_delta!(TRACKING_INTERVAL * 2, actual_timestamps[1].duration_since(start).unwrap(), S);
        assert!(rx.is_empty());
    }
}
