use std::{str::FromStr, sync::LazyLock};

use regex::Regex;

use crate::FWClientError;

static API_KEY_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(
        r"(?i)((?P<api_key_type>bearer|scitran-user) )?((?P<scheme>https?://)?(?P<host>[^:]+)(?P<port>:\d+)?:)?(?P<api_key>.+)"
    ).expect("Failed to compile API key regex")
});

pub fn get_user_agent(name: Option<String>, version: Option<String>) -> String {
    let version = version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
    let name = name.unwrap_or_else(|| env!("CARGO_PKG_NAME").to_string());
    let os = std::env::consts::OS;
    let family = std::env::consts::FAMILY;
    let arch = std::env::consts::ARCH;
    let comments = [("family", family), ("arch", arch), ("os", os)]
        .iter()
        .map(|(k, v)| format!("{}:{}", k, v))
        .collect::<Vec<_>>()
        .join(";");
    format!("{}/{} ({})", name, version, comments)
}

/// Struct representing an API key.
///
/// Requires at least a host and an API key.
/// The API key can be in the format:
/// `[<api-key-type>][<scheme>]://<host>[:<port>]:<key>`, where `api-key-type` can either
///  be `Bearer` or `scitran-user`
#[derive(Debug, Clone)]
pub struct ApiKey {
    pub api_key_type: Option<String>,
    pub scheme: Option<String>,
    pub host: String,
    pub port: Option<String>,
    pub api_key: String,
}

impl<'de> serde::Deserialize<'de> for ApiKey {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let api_key = String::deserialize(deserializer)?;
        ApiKey::from_str(api_key.as_ref()).map_err(serde::de::Error::custom)
    }
}

impl FromStr for ApiKey {
    type Err = FWClientError;

    /// Try to parse API key from a string.
    /// The expected format is: `[<scheme>]://<host>[:<port>][:__force_insecure]:<key>`
    fn from_str(api_key: &str) -> Result<Self, Self::Err> {
        let caps = API_KEY_RE
            .captures(api_key.as_ref())
            .ok_or_else(|| FWClientError::InvalidApiKey(api_key.to_string()))?;
        Ok(ApiKey {
            api_key_type: caps.name("api_key_type").map(|m| m.as_str().to_string()),
            scheme: caps.name("scheme").map(|m| m.as_str().to_string()),
            host: caps
                .name("host")
                .map(|m| m.as_str())
                .ok_or_else(|| FWClientError::MissingComponent("host".to_string()))?
                .to_string(),
            port: caps.name("port").map(|m| m.as_str().to_string()),
            api_key: caps
                .name("api_key")
                .map(|m| m.as_str())
                .ok_or_else(|| FWClientError::MissingComponent("api_key".to_string()))?
                .to_string(),
        })
    }
}

impl std::fmt::Display for ApiKey {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut api_key = String::new();
        match &self.api_key_type {
            Some(api_key_type) => api_key.push_str(format!("{} ", api_key_type).as_str()),
            None => api_key.push_str("scitran-user "),
        }
        api_key.push_str(&self.host);
        api_key.push_str(&format!(":{}", self.api_key));
        write!(f, "{}", api_key)
    }
}

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

    #[rstest]
    #[case::with_api_key_type(
        "scitran-user flywheel.io:my-api-key",
        "scitran-user flywheel.io:my-api-key"
    )]
    #[case::with_bearer(
        "bearer flywheel.io:my-bearer-token",
        "bearer flywheel.io:my-bearer-token"
    )]
    #[case::with_scheme(
        "https://api.flywheel.io:my-api-key",
        "scitran-user api.flywheel.io:my-api-key"
    )]
    #[case::with_port(
        "api.flywheel.io:8000:my-api-key",
        "scitran-user api.flywheel.io:my-api-key"
    )]
    #[case::with_all_components(
        "bearer https://api.flywheel.io:8000:my-api-key",
        "bearer api.flywheel.io:my-api-key"
    )]
    #[case::simple_format("flywheel.io:my-api-key", "scitran-user flywheel.io:my-api-key")]
    // Some weird cases we may want to be errors in the future
    #[case::colon_in_key("fw_instance:443::", "scitran-user fw_instance::")]
    #[case::port_is_key("fw_instance:443", "scitran-user fw_instance:443")]
    fn test_parse_api_key_success(#[case] input_key: &str, #[case] expected_display: &str) {
        // Parse the input key string into an ApiKey struct
        let api_key = ApiKey::from_str(input_key).unwrap();

        // Assert that the string representation matches the expected value
        assert_eq!(api_key.to_string(), expected_display);
    }

    #[rstest]
    #[case::without_host(":my-api-key", FWClientError::MissingComponent("host".to_string()))]
    #[case::invalid_format("invalid-format", FWClientError::MissingComponent("host".to_string()))]
    fn test_parse_api_key_failure(
        #[case] api_key_str: &str,
        #[case] expected_error: FWClientError,
    ) {
        let result = ApiKey::from_str(api_key_str);

        assert!(result.is_err());
        match (result.unwrap_err(), expected_error) {
            (
                FWClientError::MissingComponent(component),
                FWClientError::MissingComponent(expected),
            ) => {
                assert_eq!(component, expected);
            }
            (FWClientError::InvalidApiKey(key), FWClientError::InvalidApiKey(expected)) => {
                assert_eq!(key, expected);
            }
            (err, expected) => panic!("Expected {:?}, got {:?}", expected, err),
        }
    }

    #[test]
    fn test_user_agent() {
        let user_agent = get_user_agent(None, None);
        assert!(user_agent.contains("fw-client"));
        assert!(user_agent.contains("os:"));
        assert!(user_agent.contains("arch:"));
        assert!(user_agent.contains("family:"));
        println!("User agent: {}", user_agent);
    }
}
