use crate::{Region, RegionError};
use regex::Regex;
use std::sync::LazyLock;
use url::Url;

/// Regex to extract the region from a host FQDN or a URL.
static REGION_HOST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"^(?:https://){0,1}([^\.]+)\.([^\.]+)\.viturhosted\.net/?$").expect("Invalid regex")
});

/// Regex to extract the region from a CTS host FQDN or URL.
static CTS_REGION_HOST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"^(?:https://){0,1}([^\.]+)\.([^\.]+)\.cts\.cipherstashmanaged\.net/?$")
        .expect("Invalid regex")
});

/// Domain name for ZeroKMS service discovery.
static DOMAIN_NAME: &str = "viturhosted.net";

/// Domain name for CTS service discovery.
static CTS_DOMAIN_NAME: &str = "cts.cipherstashmanaged.net";

/// Simple service discover trait that defines the name of the service and the endpoint for a given region.
pub trait ServiceDiscovery {
    fn name(&self) -> &'static str;
    fn fqdn(region: Region) -> String;
    fn endpoint(region: Region) -> Result<Url, RegionError>;
}

/// ZeroKms service.
pub struct ZeroKmsServiceDiscovery;

impl ServiceDiscovery for ZeroKmsServiceDiscovery {
    fn name(&self) -> &'static str {
        "zerokms"
    }

    /// Returns the FQDN for the service in the given region.
    /// The FQDN is in the format `<region>.<provider>.viturhosted.net`.
    fn fqdn(region: Region) -> String {
        format!("{}.{DOMAIN_NAME}", region.identifier())
    }

    /// Returns the URL for the service in the given region.
    /// The URL is in the format `https://<region>.<provider>.viturhosted.net/`.
    ///
    /// When the `test_utils` feature is enabled, the `ZEROKMS_DISCOVERY_OVERRIDE` environment
    /// variable can be set to override the endpoint URL.
    fn endpoint(region: Region) -> Result<Url, RegionError> {
        #[cfg(feature = "test_utils")]
        if let Ok(override_url) = std::env::var("ZEROKMS_DISCOVERY_OVERRIDE") {
            return Url::parse(&override_url).map_err(|e| {
                RegionError::InvalidRegion(format!(
                    "Invalid ZEROKMS_DISCOVERY_OVERRIDE URL '{}': {}",
                    override_url, e
                ))
            });
        }

        Url::parse(&format!("https://{}.{DOMAIN_NAME}/", region.identifier())).map_err(|e| {
            RegionError::InvalidRegion(format!(
                "Invalid service URL for ZeroKMS in region {}: {}",
                region.identifier(),
                e
            ))
        })
    }
}

impl ZeroKmsServiceDiscovery {
    /// Attempt to extract the region from a host FQDN or URL.
    /// The FQDN must be in the format `<region>.<provider>.viturhosted.net`.
    /// The URL must be in the format `https://<region>.<provider>.viturhosted.net/`.
    pub fn region_from_host_fqdn(host_fqdn: &str) -> Result<Region, RegionError> {
        REGION_HOST_REGEX
            .captures(host_fqdn)
            .and_then(|caps| Some((caps.get(1)?, caps.get(2)?)))
            .map(|(r, p)| format!("{}.{}", r.as_str(), p.as_str()))
            .ok_or_else(|| RegionError::InvalidHostFqdn(host_fqdn.to_string()))
            .and_then(|ident| Region::new(&ident))
    }
}

/// Domain name for Secrets service discovery (global, not regional).
static SECRETS_DOMAIN_NAME: &str = "dashboard.cipherstash.com";

/// Secrets service.
pub struct SecretsServiceDiscovery;

impl ServiceDiscovery for SecretsServiceDiscovery {
    fn name(&self) -> &'static str {
        "secrets"
    }

    /// Returns the FQDN for the Secrets service.
    ///
    /// The Secrets API is a global endpoint — the region parameter is ignored.
    fn fqdn(_region: Region) -> String {
        SECRETS_DOMAIN_NAME.to_string()
    }

    /// Returns the URL for the Secrets service.
    ///
    /// The Secrets API is a global endpoint — the region parameter is ignored.
    fn endpoint(_region: Region) -> Result<Url, RegionError> {
        Url::parse(&format!("https://{SECRETS_DOMAIN_NAME}/"))
            .map_err(|e| RegionError::InvalidRegion(format!("Invalid Secrets service URL: {e}")))
    }
}

/// CTS service.
pub struct CtsServiceDiscovery;

impl ServiceDiscovery for CtsServiceDiscovery {
    fn name(&self) -> &'static str {
        "cts"
    }

    fn fqdn(region: Region) -> String {
        format!("{}.{CTS_DOMAIN_NAME}", region.identifier())
    }

    fn endpoint(region: Region) -> Result<Url, RegionError> {
        Url::parse(&format!(
            "https://{}.{CTS_DOMAIN_NAME}/",
            region.identifier()
        ))
        .map_err(|e| {
            RegionError::InvalidRegion(format!(
                "Invalid service URL for CTS in region {}: {}",
                region.identifier(),
                e
            ))
        })
    }
}

impl CtsServiceDiscovery {
    /// Attempt to extract the region from a CTS host FQDN or URL.
    /// The FQDN must be in the format `<region>.<provider>.cts.cipherstashmanaged.net`.
    /// The URL must be in the format `https://<region>.<provider>.cts.cipherstashmanaged.net/`.
    pub fn region_from_host_fqdn(host_fqdn: &str) -> Result<Region, RegionError> {
        CTS_REGION_HOST_REGEX
            .captures(host_fqdn)
            .and_then(|caps| Some((caps.get(1)?, caps.get(2)?)))
            .map(|(r, p)| format!("{}.{}", r.as_str(), p.as_str()))
            .ok_or_else(|| RegionError::InvalidHostFqdn(host_fqdn.to_string()))
            .and_then(|ident| Region::new(&ident))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_region_from_host_fqdn() -> anyhow::Result<()> {
        let host_fqdn = "us-west-1.aws.viturhosted.net";
        let region = ZeroKmsServiceDiscovery::region_from_host_fqdn(host_fqdn)?;
        assert_eq!(region.identifier(), "us-west-1.aws");

        Ok(())
    }

    #[test]
    fn test_region_from_host_endpoint() -> anyhow::Result<()> {
        let host_fqdn = "https://us-west-1.aws.viturhosted.net";
        let region = ZeroKmsServiceDiscovery::region_from_host_fqdn(host_fqdn)?;
        assert_eq!(region.identifier(), "us-west-1.aws");
        Ok(())
    }

    #[test]
    fn test_region_from_host_endpoint_with_trailing_slash() -> anyhow::Result<()> {
        let host_fqdn = "https://us-west-1.aws.viturhosted.net/";
        let region = ZeroKmsServiceDiscovery::region_from_host_fqdn(host_fqdn)?;
        assert_eq!(region.identifier(), "us-west-1.aws");
        Ok(())
    }

    #[test]
    fn test_cts_endpoint_ap_southeast_2() -> anyhow::Result<()> {
        let region = Region::new("ap-southeast-2.aws")?;
        let url = CtsServiceDiscovery::endpoint(region)?;
        assert_eq!(
            url.as_str(),
            "https://ap-southeast-2.aws.cts.cipherstashmanaged.net/"
        );
        Ok(())
    }

    #[test]
    fn test_cts_endpoint_us_east_1() -> anyhow::Result<()> {
        let region = Region::new("us-east-1.aws")?;
        let url = CtsServiceDiscovery::endpoint(region)?;
        assert_eq!(
            url.as_str(),
            "https://us-east-1.aws.cts.cipherstashmanaged.net/"
        );
        Ok(())
    }

    #[test]
    fn test_cts_region_from_host_fqdn() -> anyhow::Result<()> {
        let host_fqdn = "us-east-1.aws.cts.cipherstashmanaged.net";
        let region = CtsServiceDiscovery::region_from_host_fqdn(host_fqdn)?;
        assert_eq!(region.identifier(), "us-east-1.aws");
        Ok(())
    }

    #[test]
    fn test_cts_region_from_host_endpoint() -> anyhow::Result<()> {
        let host_fqdn = "https://ap-southeast-2.aws.cts.cipherstashmanaged.net";
        let region = CtsServiceDiscovery::region_from_host_fqdn(host_fqdn)?;
        assert_eq!(region.identifier(), "ap-southeast-2.aws");
        Ok(())
    }

    #[test]
    fn test_cts_region_from_host_endpoint_with_trailing_slash() -> anyhow::Result<()> {
        let host_fqdn = "https://us-west-2.aws.cts.cipherstashmanaged.net/";
        let region = CtsServiceDiscovery::region_from_host_fqdn(host_fqdn)?;
        assert_eq!(region.identifier(), "us-west-2.aws");
        Ok(())
    }

    #[test]
    fn test_cts_fqdn_us_west_2() -> anyhow::Result<()> {
        let region = Region::new("us-west-2.aws")?;
        assert_eq!(
            CtsServiceDiscovery::fqdn(region),
            "us-west-2.aws.cts.cipherstashmanaged.net"
        );
        Ok(())
    }

    #[test]
    fn test_zerokms_region_from_invalid_host() {
        let result = ZeroKmsServiceDiscovery::region_from_host_fqdn("not-a-valid-host");
        assert!(result.is_err());
    }

    #[test]
    fn test_zerokms_region_from_wrong_domain() {
        let result =
            ZeroKmsServiceDiscovery::region_from_host_fqdn("us-east-1.aws.wrongdomain.net");
        assert!(result.is_err());
    }

    #[test]
    fn test_zerokms_region_from_missing_region_component() {
        let result = ZeroKmsServiceDiscovery::region_from_host_fqdn("viturhosted.net");
        assert!(result.is_err());
    }

    #[test]
    fn test_cts_region_from_invalid_host() {
        let result = CtsServiceDiscovery::region_from_host_fqdn("not-a-valid-host");
        assert!(result.is_err());
    }

    #[test]
    fn test_cts_region_from_wrong_domain() {
        let result = CtsServiceDiscovery::region_from_host_fqdn("us-east-1.aws.wrongdomain.net");
        assert!(result.is_err());
    }

    #[test]
    fn test_cts_region_from_missing_region_component() {
        let result = CtsServiceDiscovery::region_from_host_fqdn("cts.cipherstashmanaged.net");
        assert!(result.is_err());
    }

    #[test]
    fn test_secrets_endpoint_returns_global_url() -> anyhow::Result<()> {
        let region = Region::new("ap-southeast-2.aws")?;
        let url = SecretsServiceDiscovery::endpoint(region)?;
        assert_eq!(
            url.as_str(),
            "https://dashboard.cipherstash.com/",
            "secrets endpoint should be the global dashboard URL regardless of region"
        );
        Ok(())
    }

    #[test]
    fn test_secrets_endpoint_ignores_region() -> anyhow::Result<()> {
        let url_apse2 = SecretsServiceDiscovery::endpoint(Region::new("ap-southeast-2.aws")?)?;
        let url_use1 = SecretsServiceDiscovery::endpoint(Region::new("us-east-1.aws")?)?;
        assert_eq!(
            url_apse2, url_use1,
            "secrets endpoint should be the same for all regions"
        );
        Ok(())
    }

    #[test]
    fn test_secrets_fqdn() -> anyhow::Result<()> {
        let region = Region::new("us-east-1.aws")?;
        assert_eq!(
            SecretsServiceDiscovery::fqdn(region),
            "dashboard.cipherstash.com"
        );
        Ok(())
    }
}
