use std::sync::Arc;

use arc_swap::{ArcSwap, Guard};
use kameleoon_trace_logging::trace_method;

use crate::{
    logging::LogLevel,
    network::query_builder::{QueryBuilder, QueryParam},
    types::RemoteVisitorDataFilter,
};

// MARK: - UrlProviderTrait
pub(crate) trait UrlProvider: Send + Sync {
    fn apply_url_domains(
        &self,
        data_api_domain: Option<&str>,
        streaming_domain: Option<&str>,
        access_token_domain: Option<&str>,
    );

    fn make_tracking_url(&self) -> String;
    fn make_visitor_data_url(
        &self,
        visitor_code: &str,
        filter: &RemoteVisitorDataFilter,
        is_unique_identifier: bool,
    ) -> String;
    fn make_remote_data_url(&self, key: &str) -> String;
    fn make_datafile_url(&self, environment: Option<&str>, timestamp: Option<u64>) -> String;

    fn make_real_time_url(&self) -> String;

    fn make_access_token_url(&self) -> String;

    #[cfg(test)]
    fn site_code(&self) -> &str;
    #[cfg(test)]
    fn configuration_domain(&self) -> Arc<String>;
    #[cfg(test)]
    fn data_api_domain(&self) -> Arc<String>;
    #[cfg(test)]
    fn streaming_domain(&self) -> Arc<String>;
    #[cfg(test)]
    fn access_token_domain(&self) -> Arc<String>;
}

// MARK: - ServiceDomain
// Enum for different domain types
pub(crate) enum ServiceDomain {
    Configuration,
    DataApi,
    Streaming,
    AccessToken,
}

impl ServiceDomain {
    const DOMAIN_TYPES: [ServiceDomain; 4] =
        [ServiceDomain::Configuration, ServiceDomain::DataApi, ServiceDomain::Streaming, ServiceDomain::AccessToken];

    const COUNT: usize = Self::DOMAIN_TYPES.len();

    pub const fn index(&self) -> usize {
        match self {
            ServiceDomain::Configuration => 0,
            ServiceDomain::DataApi => 1,
            ServiceDomain::Streaming => 2,
            ServiceDomain::AccessToken => 3,
        }
    }

    pub fn default_domain(&self) -> &'static str {
        match self {
            ServiceDomain::Configuration => "sdk-config.kameleoon.eu",
            ServiceDomain::DataApi => "eu-data.kameleoon.io",
            ServiceDomain::Streaming => "events.kameleoon.eu:8110",
            ServiceDomain::AccessToken => "api.kameleoon.com",
        }
    }

    fn subdomain_prefix(&self) -> &'static str {
        match self {
            ServiceDomain::Configuration => "sdk-config",
            ServiceDomain::AccessToken => "api",
            _ => "",
        }
    }
}

// MARK: - ApiPath

// Enum for different API endpoints - more type-safe than string constants
enum ApiPath {
    Configuration,
    Tracking,
    VisitorData,
    GetData,
    Streaming,
    AccessToken,
}

impl ApiPath {
    fn path(&self) -> &'static str {
        match self {
            ApiPath::Configuration => "/v3",
            ApiPath::Tracking => "/visit/events",
            ApiPath::VisitorData => "/visit/visitor",
            ApiPath::GetData => "/map/map",
            ApiPath::Streaming => "/sse",
            ApiPath::AccessToken => "/oauth/token",
        }
    }
}

// MARK: - UrlProvider

pub(crate) struct UrlProviderImpl {
    site_code: String,
    domains: [ArcSwap<String>; ServiceDomain::DOMAIN_TYPES.len()],
    post_query_base: String,
}

impl UrlProviderImpl {
    pub fn new(site_code: String, network_domain: Option<String>, sdk_name: String, sdk_version: String) -> Self {
        let domains = Self::resolve_domains(&network_domain);
        let post_query_base = Self::make_post_query_base(&site_code, &sdk_name, &sdk_version);
        Self {
            site_code,
            domains,
            post_query_base,
        }
    }
}

impl UrlProvider for UrlProviderImpl {
    // MARK: - Domains
    #[trace_method(LogLevel::Debug, skip(self))]
    fn apply_url_domains(
        &self,
        data_api_domain: Option<&str>,
        streaming_domain: Option<&str>,
        access_token_domain: Option<&str>,
    ) {
        if let Some(domain) = data_api_domain {
            self.domains[ServiceDomain::DataApi.index()].store(Arc::new(domain.to_owned()));
        }
        if let Some(domain) = streaming_domain {
            self.domains[ServiceDomain::Streaming.index()].store(Arc::new(domain.to_owned()));
        }
        if let Some(domain) = access_token_domain {
            self.domains[ServiceDomain::AccessToken.index()].store(Arc::new(domain.to_owned()));
        }
    }

    // MARK: - Data API
    // TODO: Think about caching strategy of `tracking_url` when NetProvider will be implemented.
    fn make_tracking_url(&self) -> String {
        let query_builder = QueryBuilder::with_params(vec![(QueryParam::BodyUa, "true".into())]);

        let (domain, path) = self.get_domain_path(ServiceDomain::DataApi, ApiPath::Tracking);
        format!("https://{}{}?{}&{}", domain, path, self.post_query_base, query_builder.build())
    }

    fn make_visitor_data_url(
        &self,
        visitor_code: &str,
        filter: &RemoteVisitorDataFilter,
        is_unique_identifier: bool,
    ) -> String {
        let visitor_param = if is_unique_identifier { QueryParam::MappingValue } else { QueryParam::VisitorCode };

        let mut query_builder = QueryBuilder::with_params(vec![
            (QueryParam::SiteCode, self.site_code.as_str().into()),
            (visitor_param, visitor_code.into()),
            (QueryParam::MaxNumberPreviousVisits, filter.previous_visit_amount.to_string().into()),
            (QueryParam::StaticData, "true".into()),
            (QueryParam::Version, "0".into()),
        ]);

        let mut add_flag_if_true = |param: QueryParam, flag: bool| {
            if flag {
                query_builder.add_ptr_param(param, "true");
            }
        };

        // Add conditional flags
        add_flag_if_true(QueryParam::Kcs, filter.kcs);
        add_flag_if_true(QueryParam::CurrentVisit, filter.current_visit);
        add_flag_if_true(QueryParam::CustomData, filter.custom_data);
        add_flag_if_true(QueryParam::Conversion, filter.conversions);
        add_flag_if_true(QueryParam::Geolocation, filter.geolocation);
        add_flag_if_true(QueryParam::Experiment, filter.experiments);
        add_flag_if_true(QueryParam::Page, filter.page_views);
        add_flag_if_true(QueryParam::Personalization, filter.personalizations);
        add_flag_if_true(QueryParam::Cbs, filter.cbs);

        let (domain, path) = self.get_domain_path(ServiceDomain::DataApi, ApiPath::VisitorData);
        format!("https://{}{}?{}", domain, path, query_builder.build())
    }

    fn make_remote_data_url(&self, key: &str) -> String {
        let query_builder = QueryBuilder::with_params(vec![
            (QueryParam::SiteCode, self.site_code.as_str().into()),
            (QueryParam::Key, key.into()),
        ]);

        let (domain, path) = self.get_domain_path(ServiceDomain::DataApi, ApiPath::GetData);
        format!("https://{}{}?{}", domain, path, query_builder.build())
    }

    // MARK: - Configuration

    fn make_datafile_url(&self, environment: Option<&str>, timestamp: Option<u64>) -> String {
        let mut query_builder = QueryBuilder::new();

        if let Some(env) = environment {
            query_builder.add_ptr_param(QueryParam::Environment, env);
        }

        if let Some(timestamp) = timestamp {
            query_builder.add_owned_param(QueryParam::Ts, timestamp.to_string());
        }
        let query = query_builder.build();

        let (domain, path) = self.get_domain_path(ServiceDomain::Configuration, ApiPath::Configuration);
        if query.is_empty() {
            format!("https://{}{}/{}", domain, path, self.site_code)
        } else {
            format!("https://{}{}/{}?{}", domain, path, self.site_code, query)
        }
    }

    // MARK: - Streaing

    fn make_real_time_url(&self) -> String {
        let query_builder = QueryBuilder::with_params(vec![(QueryParam::SiteCode, self.site_code.as_str().into())]);

        let (domain, path) = self.get_domain_path(ServiceDomain::Streaming, ApiPath::Streaming);
        format!("https://{}{}?{}", domain, path, query_builder.build())
    }

    // MARK: - OAuth

    fn make_access_token_url(&self) -> String {
        let (domain, path) = self.get_domain_path(ServiceDomain::AccessToken, ApiPath::AccessToken);

        format!("https://{}{}", domain, path)
    }

    // MARK: - Getters
    #[cfg(test)]
    fn site_code(&self) -> &str {
        &self.site_code
    }

    #[cfg(test)]
    fn configuration_domain(&self) -> Arc<String> {
        self.domains[ServiceDomain::Configuration.index()].load_full()
    }

    #[cfg(test)]
    fn data_api_domain(&self) -> Arc<String> {
        self.domains[ServiceDomain::DataApi.index()].load_full()
    }

    #[cfg(test)]
    fn streaming_domain(&self) -> Arc<String> {
        self.domains[ServiceDomain::Streaming.index()].load_full()
    }

    #[cfg(test)]
    fn access_token_domain(&self) -> Arc<String> {
        self.domains[ServiceDomain::AccessToken.index()].load_full()
    }
}

// MARK: - Private helpers

impl UrlProviderImpl {
    fn resolve_domains(network_domain: &Option<String>) -> [ArcSwap<String>; ServiceDomain::COUNT] {
        let is_custom_domain = network_domain.as_ref().map(|d| !d.trim().is_empty()).unwrap_or(false);

        let network_domain = network_domain.as_deref().unwrap_or_default();
        ServiceDomain::DOMAIN_TYPES
            .iter()
            .map(|domain_type| match (is_custom_domain, domain_type) {
                (true, ServiceDomain::Configuration | ServiceDomain::AccessToken) => {
                    ArcSwap::from_pointee(format!("{}.{}", domain_type.subdomain_prefix(), network_domain))
                }
                _ => ArcSwap::from_pointee(domain_type.default_domain().to_string()),
            })
            .collect::<Vec<_>>()
            .try_into()
            .unwrap()
    }

    fn make_post_query_base(site_code: &str, sdk_name: &str, sdk_version: &str) -> String {
        QueryBuilder::with_params(vec![
            (QueryParam::SdkName, sdk_name.into()),
            (QueryParam::SdkVersion, sdk_version.into()),
            (QueryParam::SiteCode, site_code.into()),
        ])
        .build()
    }

    #[inline]
    fn get_domain_path<'a>(&'a self, service: ServiceDomain, path: ApiPath) -> (Guard<Arc<String>>, &'a str) {
        let domain = self.domains[service.index()].load();
        let path = path.path();

        return (domain, path);
    }
}

// MARK: - Tests

// Example usage and tests
#[cfg(test)]
mod tests {

    use crate::types::remote_visitor_data_filter::RemoteVisitorDataFilterUtils;

    use super::*;

    use rstest::rstest;

    const SITE_CODE: &str = "abcde12345";
    const HTTPS: &str = "https://";
    const SDK_NAME: &str = "rust-sdk";
    const SDK_VERSION: &str = "1.2.3";

    // Domain Constants
    const SDK_CONFIG_URL: &str = "sdk-config.kameleoon.eu";
    const DATA_API_URL: &str = "eu-data.kameleoon.io";
    const STREAMING_URL: &str = "events.kameleoon.eu:8110";
    const ACCESS_TOKEN_URL: &str = "api.kameleoon.com";

    // API Path Constants
    const PATH_CONFIGURATION: &str = "/v3";
    const PATH_TRACKING: &str = "/visit/events";
    const PATH_VISITOR_DATA: &str = "/visit/visitor";
    const PATH_GET_DATA: &str = "/map/map";
    const PATH_STREAMING: &str = "/sse";
    const PATH_ACCESS_TOKEN: &str = "/oauth/token";

    fn setup() -> UrlProviderImpl {
        UrlProviderImpl::new(SITE_CODE.to_string(), None, SDK_NAME.to_string(), SDK_VERSION.to_string())
    }

    #[test]
    fn test_default_domain_constants() {
        assert_eq!(SDK_CONFIG_URL, ServiceDomain::Configuration.default_domain());
        assert_eq!(DATA_API_URL, ServiceDomain::DataApi.default_domain());
        assert_eq!(STREAMING_URL, ServiceDomain::Streaming.default_domain());
        assert_eq!(ACCESS_TOKEN_URL, ServiceDomain::AccessToken.default_domain());
    }

    #[test]
    fn test_api_path_constants_match() {
        assert_eq!(PATH_CONFIGURATION, ApiPath::Configuration.path());
        assert_eq!(PATH_TRACKING, ApiPath::Tracking.path());
        assert_eq!(PATH_VISITOR_DATA, ApiPath::VisitorData.path());
        assert_eq!(PATH_GET_DATA, ApiPath::GetData.path());
        assert_eq!(PATH_STREAMING, ApiPath::Streaming.path());
        assert_eq!(PATH_ACCESS_TOKEN, ApiPath::AccessToken.path());
    }

    #[test]
    fn test_getters() {
        let provider = setup();
        assert_eq!(SITE_CODE, provider.site_code());
        assert_eq!(SDK_CONFIG_URL, provider.configuration_domain().as_str());
        assert_eq!(DATA_API_URL, provider.data_api_domain().as_str());
        assert_eq!(STREAMING_URL, provider.streaming_domain().as_str());
        assert_eq!(ACCESS_TOKEN_URL, provider.access_token_domain().as_str());
    }

    #[test]
    fn test_make_tracking_url() {
        let provider = setup();
        let url = provider.make_tracking_url();

        let expected_query = format!(
            "{}={SDK_NAME}&{}={SDK_VERSION}&{}={SITE_CODE}&{}=true",
            QueryParam::SdkName.as_str(),
            QueryParam::SdkVersion.as_str(),
            QueryParam::SiteCode.as_str(),
            QueryParam::BodyUa.as_str(),
        );

        let expected_url = format!("{HTTPS}{DATA_API_URL}{PATH_TRACKING}?{expected_query}");

        assert_eq!(url, expected_url);
    }

    #[rstest]
    #[case((None, None), &[])]
    #[case((Some("production"), None), &[(QueryParam::Environment, "production")])]
    #[case((Some("production"), Some(0)), &[(QueryParam::Environment, "production"), (QueryParam::Ts, "0")])]
    #[case((None, Some(1682272183)), &[(QueryParam::Ts, "1682272183")])]
    #[case((Some("production"), Some(1682272183)),
        &[
            (QueryParam::Environment, "production"),
            (QueryParam::Ts, "1682272183")
        ]
    )]
    fn test_make_datafile_url(
        #[case] input: (Option<&str>, Option<u64>),
        #[case] expected_params: &[(QueryParam, &str)],
    ) {
        let provider = setup();
        let (env, ts) = input;

        let actual_url = provider.make_datafile_url(env, ts);

        let query = if expected_params.is_empty() {
            String::new()
        } else {
            let joined =
                expected_params.iter().map(|(k, v)| format!("{}={}", k.as_str(), v)).collect::<Vec<_>>().join("&");
            format!("?{}", joined)
        };

        let expected_url = format!("{HTTPS}{SDK_CONFIG_URL}{PATH_CONFIGURATION}/{SITE_CODE}{}", query);

        assert_eq!(expected_url, actual_url);
    }

    #[test]
    fn test_make_real_time_url() {
        let provider = setup();
        let url = provider.make_real_time_url();

        let expected_query = format!("{}={SITE_CODE}", QueryParam::SiteCode.as_str(),);

        assert_eq!(url, format!("{HTTPS}{STREAMING_URL}{PATH_STREAMING}?{expected_query}"));
    }

    #[test]
    fn test_make_access_token_url() {
        let provider = setup();
        let url = provider.make_access_token_url();
        assert_eq!(url, format!("{HTTPS}{ACCESS_TOKEN_URL}{PATH_ACCESS_TOKEN}"));
        RemoteVisitorDataFilterUtils::make(
            0, false, false, false, false, false, false, false, false, false, false, false, false, false,
        );
    }

    fn add_filter_param_if_required(query: &str, param: QueryParam, required: bool) -> String {
        if required { format!("{}&{}=true", query, param.as_str()) } else { query.to_string() }
    }

    const F: bool = false;
    const T: bool = true;

    #[rstest]
    #[case(RemoteVisitorDataFilter::default(), T)]
    #[case(RemoteVisitorDataFilterUtils::make(20, F, F, F, F, F, F, F, F, F, F, F, F, F), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, T, T, T, T, T, T, T, T, T, T, T, T), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, F, F, F, F, F, F, F, F, F, F, F, F), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, T, F, F, F, F, F, F, F, F, F, F, F), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, F, T, F, F, F, F, F, F, F, F, F, F), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, F, F, T, F, F, F, F, F, F, F, F, F), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, F, F, F, T, F, F, F, F, F, F, F, F), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, F, F, F, F, T, F, F, F, F, F, F, F), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, F, F, F, F, F, T, F, F, F, F, F, F), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, F, F, F, F, F, F, T, F, F, F, F, F), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, F, F, F, F, F, F, F, T, F, F, F, F), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, F, F, F, F, F, F, F, F, T, F, F, F), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, F, F, F, F, F, F, F, F, F, T, F, F), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, F, F, F, F, F, F, F, F, F, F, T, F), F)]
    #[case(RemoteVisitorDataFilterUtils::make(5, T, F, F, F, F, F, F, F, F, F, F, F, T), F)]
    fn test_make_visitor_data_get_url(#[case] filter: RemoteVisitorDataFilter, #[case] is_unique_identifier: bool) {
        let provider = setup();
        let visitor_code = "98765zxyw4";
        let base = format!(
            "{}={}&{}={}&{}={}&{}=true&{}=0",
            QueryParam::SiteCode.as_str(),
            SITE_CODE,
            if is_unique_identifier { QueryParam::MappingValue.as_str() } else { QueryParam::VisitorCode.as_str() },
            visitor_code,
            QueryParam::MaxNumberPreviousVisits.as_str(),
            filter.previous_visit_amount,
            QueryParam::StaticData.as_str(),
            QueryParam::Version.as_str()
        );

        let mut query = base;
        query = add_filter_param_if_required(&query, QueryParam::Kcs, filter.kcs);
        query = add_filter_param_if_required(&query, QueryParam::CurrentVisit, filter.current_visit);
        query = add_filter_param_if_required(&query, QueryParam::CustomData, filter.custom_data);
        query = add_filter_param_if_required(&query, QueryParam::Conversion, filter.conversions);
        query = add_filter_param_if_required(&query, QueryParam::Geolocation, filter.geolocation);
        query = add_filter_param_if_required(&query, QueryParam::Experiment, filter.experiments);
        query = add_filter_param_if_required(&query, QueryParam::Page, filter.page_views);
        query = add_filter_param_if_required(&query, QueryParam::Personalization, filter.personalizations);
        query = add_filter_param_if_required(&query, QueryParam::Cbs, filter.cbs);

        let expected_url = format!("{HTTPS}{DATA_API_URL}{PATH_VISITOR_DATA}?{query}");
        let actual_url = provider.make_visitor_data_url(visitor_code, &filter, is_unique_identifier);

        assert_eq!(actual_url, expected_url);
    }

    #[rstest]
    #[case(Some("new_data_api"), Some("new_streaming"), Some("new_access_token"))]
    #[case(None, Some("new_streaming"), Some("new_access_token"))]
    #[case(Some("new_data_api"), None, Some("new_access_token"))]
    #[case(Some("new_data_api"), Some("new_streaming"), None)]
    #[case(None, None, None)]
    fn test_apply_url_domains(
        #[case] data_api_domain: Option<&str>,
        #[case] streaming_domain: Option<&str>,
        #[case] access_token_domain: Option<&str>,
    ) {
        let default_data_api = ServiceDomain::DataApi.default_domain();
        let default_streaming = ServiceDomain::Streaming.default_domain();
        let default_access_token = ServiceDomain::AccessToken.default_domain();

        let provider = setup();

        provider.apply_url_domains(data_api_domain, streaming_domain, access_token_domain);

        assert_eq!(data_api_domain.unwrap_or(default_data_api), provider.data_api_domain().as_str(),);
        assert_eq!(streaming_domain.unwrap_or(default_streaming), provider.streaming_domain().as_str(),);
        assert_eq!(access_token_domain.unwrap_or(default_access_token), provider.access_token_domain().as_str(),);
    }

    #[rstest]
    #[case(
        None,
        "eu-data.kameleoon.io",
        "sdk-config.kameleoon.eu",
        "events.kameleoon.eu:8110",
        "api.kameleoon.com"
    )]
    #[case(Some("test.com".to_owned()), "eu-data.kameleoon.io", "sdk-config.test.com", "events.kameleoon.eu:8110", "api.test.com")]
    fn test_network_domain_resolution(
        #[case] net_domain: Option<String>,
        #[case] expected_data: &str,
        #[case] expected_conf: &str,
        #[case] expected_streaming: &str,
        #[case] expected_access: &str,
    ) {
        let provider =
            UrlProviderImpl::new(SITE_CODE.to_string(), net_domain, SDK_NAME.to_string(), SDK_VERSION.to_string());
        assert_eq!(provider.data_api_domain().as_str(), expected_data);
        assert_eq!(provider.configuration_domain().as_str(), expected_conf);
        assert_eq!(provider.streaming_domain().as_str(), expected_streaming);
        assert_eq!(provider.access_token_domain().as_str(), expected_access);
    }

    #[test]
    fn test_make_urls_with_custom_domain() {
        let provider = UrlProviderImpl::new(
            SITE_CODE.to_owned(),
            Some("test1.com".to_string()),
            SDK_NAME.to_owned(),
            SDK_VERSION.to_owned(),
        );

        provider.apply_url_domains(Some("data.test2.com"), Some("events.test3.com:8110"), Some("api.test4.com"));

        assert!(provider.make_datafile_url(None, None).starts_with("https://sdk-config.test1.com"));

        assert!(provider.make_tracking_url().starts_with("https://data.test2.com"));
        assert!(provider.make_remote_data_url("key").starts_with("https://data.test2.com"));
        assert!(provider.make_real_time_url().starts_with("https://events.test3.com:8110"));
        assert!(provider.make_remote_data_url("key").starts_with("https://data.test2.com"));
        assert!(provider.make_real_time_url().starts_with("https://events.test3.com:8110"));
        assert!(
            provider
                .make_visitor_data_url("12345", &RemoteVisitorDataFilter::default(), true)
                .starts_with("https://data.test2.com")
        );
        assert!(provider.make_access_token_url().starts_with("https://api.test4.com"));
    }
}
