use std::sync::OnceLock;

use regex::Regex;
use kameleoon_trace_logging::trace_method;

use crate::{error_log, logging::LogLevel, warn_log};

pub(crate) struct DomainValidator;

impl DomainValidator {
    const HTTP: &str = "http://";
    const HTTPS: &str = "https://";
    const LOCALHOST: &str = "localhost";

    #[trace_method(LogLevel::Debug)]
    pub fn validate_top_level_domain(input: Option<String>) -> Option<String> {
        let domain = Self::check_and_trim_protocol(input)?;
        if !Self::check_regex(&domain) && domain != Self::LOCALHOST {
            error_log!(
                "The top-level domain '{}' is invalid. The value has been set as provided, \
                but it does not meet the required format for proper SDK functionality. \
                Please check the domain for correctness.",
                domain
            );
        }
        Some(domain)
    }

    #[trace_method(LogLevel::Debug)]
    pub fn validate_network_domain(input: Option<String>) -> Option<String> {
        let mut domain = Self::check_and_trim_protocol(input)?;
        // Remove leading/trailing dots
        domain = domain.trim_matches('.').to_string();
        if !Self::check_regex(&domain) && domain != Self::LOCALHOST {
            error_log!("The network domain '{}' is invalid.", domain);
            return None;
        }
        Some(domain)
    }

    fn check_and_trim_protocol(domain: Option<String>) -> Option<String> {
        let domain = domain?;
        let domain = domain.trim();
        if domain.is_empty() {
            return None;
        }

        let mut domain = domain.to_lowercase();

        for protocol in [Self::HTTP, Self::HTTPS] {
            if domain.starts_with(protocol) {
                domain.replace_range(..protocol.len(), "");
                warn_log!("The domain contains '{}'. Domain after protocol trimming: '{}'", protocol, domain);
                break;
            }
        }
        Some(domain)
    }

    fn check_regex(domain: &str) -> bool {
        static REGEX_DOMAIN: OnceLock<Regex> = OnceLock::new();
        let regex = REGEX_DOMAIN.get_or_init(|| {
            Regex::new(r"^(\.?(([a-zA-Z\d][a-zA-Z\d-]*[a-zA-Z\d])|[a-zA-Z\d]))(\.(([a-zA-Z\d][a-zA-Z\d-]*[a-zA-Z\d])|[a-zA-Z\d])){1,126}$")
            .expect("Invalid regex")
        });
        regex.is_match(&domain) || domain == Self::LOCALHOST
    }
}

#[cfg(test)]
mod tests {
    use mockall::mock;
    use rstest::rstest;
    use serial_test::serial;

    use crate::logging::{KameleoonLogger, LogLevel, Logger};

    use super::*;

    mock! {
        pub Logger {}

        impl Logger for Logger {
            fn log(&self, level: LogLevel, message: &str);
        }
    }

    #[rstest]
    #[case("kameleoon.com", "kameleoon.com", false)]
    #[case(".kameleoon.com", ".kameleoon.com", false)]
    #[case(".kame-leoon.com", ".kame-leoon.com", false)]
    #[case(".kame-leoon.c-om", ".kame-leoon.c-om", false)]
    #[case("http://kameleoon.com", "kameleoon.com", false)]
    #[case("https://kameleoon.com", "kameleoon.com", false)]
    #[case("g.g.g.r", "g.g.g.r", false)]
    #[case("g.gg.g.ru", "g.gg.g.ru", false)]
    #[case("gg.gg.g.ru", "gg.gg.g.ru", false)]
    #[case("localhost", "localhost", false)]
    #[case("com", "com", true)]
    #[case(".com", ".com", true)]
    #[case(".kameleoon.", ".kameleoon.", true)]
    #[case(".kameleoon..com", ".kameleoon..com", true)]
    #[case(".kameleoon.com.", ".kameleoon.com.", true)]
    #[case("-kameleoon.com", "-kameleoon.com", true)]
    #[case(".-kameleoon.com", ".-kameleoon.com", true)]
    #[case("kameleoon-.com", "kameleoon-.com", true)]
    #[case(".kameleoon-.com", ".kameleoon-.com", true)]
    #[case("kame.leo.on%f0.com", "kame.leo.on%f0.com", true)]
    #[case("kame.leo.on%32.com", "kame.leo.on%32.com", true)]
    #[case("kame.leo.on$.com", "kame.leo.on$.com", true)]
    #[case("g.-com-", "g.-com-", true)]
    #[case("g.%com", "g.%com", true)]
    #[serial(logger)]
    fn test_validate_top_level_domain(#[case] input: &str, #[case] expected: &str, #[case] is_error: bool) {
        use std::sync::{
            Arc,
            atomic::{AtomicBool, Ordering},
        };

        let mut mock_logger = MockLogger::new();

        let error_called = Arc::new(AtomicBool::new(false));
        let error_flag = Arc::clone(&error_called);

        mock_logger.expect_log().returning(move |level, msg| {
            if level == LogLevel::Error && msg.contains("invalid") {
                error_flag.store(true, Ordering::SeqCst);
            }
        });
        KameleoonLogger::set_logger(Box::new(mock_logger));

        let actual = DomainValidator::validate_top_level_domain(Some(input.to_owned()));
        assert_eq!(actual.as_deref(), Some(expected));
        assert_eq!(error_called.load(Ordering::SeqCst), is_error);
    }

    #[rstest]
    #[case("kameleoon.com", Some("kameleoon.com"))]
    #[case(".kameleoon.com", Some("kameleoon.com"))]
    #[case(".kame-leoon.c-om", Some("kame-leoon.c-om"))]
    #[case("http://kameleoon.com", Some("kameleoon.com"))]
    #[case("https://kameleoon.com", Some("kameleoon.com"))]
    #[case("g.g.g.r", Some("g.g.g.r"))]
    #[case("localhost", Some("localhost"))]
    #[case("com", None)]
    #[case(".kameleoon.", None)]
    #[case(".kameleoon..com", None)]
    #[case("-kameleoon.com", None)]
    #[case(".-kameleoon.com", None)]
    #[case("kameleoon-.com", None)]
    #[case("kame.leo.on%f0.com", None)]
    #[case("kame.leo.on%32.com", None)]
    #[case("kame.leo.on$.com", None)]
    #[serial(logger)]
    fn test_validate_network_domain(#[case] input: &str, #[case] expected: Option<&str>) {
        let actual = DomainValidator::validate_network_domain(Some(input.to_owned()));
        assert_eq!(actual.as_deref(), expected);
    }
}
