use regex::Regex;
use std::sync::LazyLock as Lazy;

static SSH_GIT_REGEX: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"^git::(?P<url>ssh://((?P<user>[^@]+)@)?(?P<host>[^/]+)/(?P<repo>.+)\.git)//(?P<path>[^?]+)(\?ref=(?P<ref>[^?&]+)(&.*)?)?$").unwrap()
});

static AZURE_DEVOPS_SSH_GIT_REGEX: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"^git::(?P<url>ssh://((?P<user>[^@]+)@)?(?P<host>[^/]+)/(?P<org>[^/]+)/(?P<project>[^/]+)/_git/(?P<repo>[^/]+))//(?P<path>[^?]+)(\?ref=(?P<ref>[^?&]+)(&.*)?)?$").unwrap()
});

static HTTPS_GIT_REGEX: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"^git::(?P<url>https?://(?P<host>[^/]+)/(?P<repo>.+)\.git)//(?P<path>[^?]+)(\?ref=(?P<ref>[^?&]+)(&.*)?)?$").unwrap()
});

static AZURE_DEVOPS_HTTPS_GIT_REGEX: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"^git::(?P<url>https?://(?P<host>[^/]+)/(?P<org>[^/]+)/(?P<project>[^/]+)/_git/(?P<repo>[^/]+))//(?P<path>[^?]+)(\?ref=(?P<ref>[^?&]+)(&.*)?)?$").unwrap()
});

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteGitSource {
    pub url: String,
    pub path: String,
    pub git_ref: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteHttpSource {
    pub url: String,
}

pub struct RemoteSource;

impl RemoteSource {
    pub fn parse_git(file: &str) -> Option<RemoteGitSource> {
        Self::parse_git_ssh(file).or_else(|| Self::parse_git_https(file))
    }

    pub(crate) fn parse_git_ssh(file: &str) -> Option<RemoteGitSource> {
        parse_git_with(&SSH_GIT_REGEX, file)
            .or_else(|| parse_git_with(&AZURE_DEVOPS_SSH_GIT_REGEX, file))
    }

    pub(crate) fn parse_git_https(file: &str) -> Option<RemoteGitSource> {
        parse_git_with(&HTTPS_GIT_REGEX, file)
            .or_else(|| parse_git_with(&AZURE_DEVOPS_HTTPS_GIT_REGEX, file))
    }

    pub fn parse_http(file: &str) -> Option<RemoteHttpSource> {
        let url = url::Url::parse(file).ok()?;
        ((url.scheme() == "http" || url.scheme() == "https")
            && url.path().len() > 1
            && !url.path().ends_with('/'))
        .then(|| RemoteHttpSource {
            url: file.to_string(),
        })
    }
}

fn parse_git_with(regex: &Regex, file: &str) -> Option<RemoteGitSource> {
    let captures = regex.captures(file)?;
    let path = captures.name("path").unwrap().as_str();
    if path
        .split('/')
        .any(|component| component.is_empty() || component == "." || component == "..")
    {
        return None;
    }
    Some(RemoteGitSource {
        url: captures.name("url").unwrap().as_str().to_string(),
        path: path.to_string(),
        git_ref: captures.name("ref").map(|m| m.as_str().to_string()),
    })
}

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

    #[test]
    fn parses_git_ssh_sources() {
        let source = RemoteSource::parse_git(
            "git::ssh://git@github.com/myorg/example.git//terraform/myfile?ref=master",
        )
        .unwrap();
        assert_eq!(source.url, "ssh://git@github.com/myorg/example.git");
        assert_eq!(source.path, "terraform/myfile");
        assert_eq!(source.git_ref, Some("master".to_string()));
    }

    #[test]
    fn parses_git_ssh_sources_without_user() {
        let source =
            RemoteSource::parse_git("git::ssh://github.com/myorg/example.git//terraform/myfile")
                .unwrap();
        assert_eq!(source.url, "ssh://github.com/myorg/example.git");
        assert_eq!(source.path, "terraform/myfile");
        assert_eq!(source.git_ref, None);
    }

    #[test]
    fn parses_git_https_sources() {
        let source = RemoteSource::parse_git(
            "git::https://git.acme.com:8080/myorg/example.git//terraform/myfile?ref=master",
        )
        .unwrap();
        assert_eq!(source.url, "https://git.acme.com:8080/myorg/example.git");
        assert_eq!(source.path, "terraform/myfile");
        assert_eq!(source.git_ref, Some("master".to_string()));
    }

    #[test]
    fn parses_git_ref_before_additional_query_params() {
        let source = RemoteSource::parse_git(
            "git::https://git.acme.com/myorg/example.git//terraform/myfile?ref=master&depth=1",
        )
        .unwrap();
        assert_eq!(source.git_ref, Some("master".to_string()));
    }

    #[test]
    fn rejects_git_sources_without_paths() {
        assert!(
            RemoteSource::parse_git("git::https://myserver.com/example.git?ref=master").is_none()
        );
        assert!(RemoteSource::parse_git("git::ssh://user@myserver.com/example.git").is_none());
    }

    #[test]
    fn rejects_git_sources_with_unsafe_paths() {
        assert!(
            RemoteSource::parse_git("git::https://myserver.com/example.git//../plugin").is_none()
        );
        assert!(
            RemoteSource::parse_git("git::https://myserver.com/example.git//plugin/../other")
                .is_none()
        );
        assert!(
            RemoteSource::parse_git("git::https://myserver.com/example.git//plugin//other")
                .is_none()
        );
        assert!(
            RemoteSource::parse_git("git::https://myserver.com/example.git//plugin/./other")
                .is_none()
        );
    }

    #[test]
    fn parses_azure_devops_git_ssh_sources() {
        let source = RemoteSource::parse_git(
            "git::ssh://git@dev.azure/myorg/myproj/_git/example//terraform/myfile?ref=master",
        )
        .unwrap();
        assert_eq!(source.url, "ssh://git@dev.azure/myorg/myproj/_git/example");
        assert_eq!(source.path, "terraform/myfile");
        assert_eq!(source.git_ref, Some("master".to_string()));
    }

    #[test]
    fn parses_azure_devops_git_ssh_sources_without_user() {
        let source = RemoteSource::parse_git(
            "git::ssh://dev.azure/myorg/myproj/_git/example//terraform/myfile",
        )
        .unwrap();
        assert_eq!(source.url, "ssh://dev.azure/myorg/myproj/_git/example");
        assert_eq!(source.path, "terraform/myfile");
        assert_eq!(source.git_ref, None);
    }

    #[test]
    fn parses_azure_devops_git_https_sources() {
        let source = RemoteSource::parse_git(
            "git::https://dev.azure:8080/myorg/myproj/_git/example//terraform/myfile?ref=master",
        )
        .unwrap();
        assert_eq!(
            source.url,
            "https://dev.azure:8080/myorg/myproj/_git/example"
        );
        assert_eq!(source.path, "terraform/myfile");
        assert_eq!(source.git_ref, Some("master".to_string()));
    }

    #[test]
    fn parses_azure_devops_git_ref_before_additional_query_params() {
        let source = RemoteSource::parse_git(
            "git::https://dev.azure/myorg/myproj/_git/example//terraform/myfile?ref=master&depth=1",
        )
        .unwrap();
        assert_eq!(source.git_ref, Some("master".to_string()));
    }

    #[test]
    fn rejects_azure_devops_git_sources_without_paths() {
        assert!(
            RemoteSource::parse_git("git::https://dev.azure/myorg/myproj/_git/example?ref=master")
                .is_none()
        );
        assert!(
            RemoteSource::parse_git("git::ssh://user@dev.azure/myorg/myproj/_git/example")
                .is_none()
        );
    }

    #[test]
    fn rejects_azure_devops_git_sources_with_unsafe_paths() {
        assert!(
            RemoteSource::parse_git("git::https://dev.azure/myorg/myproj/_git/example//../plugin")
                .is_none()
        );
        assert!(
            RemoteSource::parse_git(
                "git::https://dev.azure/myorg/myproj/_git/example//plugin/../other"
            )
            .is_none()
        );
        assert!(
            RemoteSource::parse_git(
                "git::https://dev.azure/myorg/myproj/_git/example//plugin//other"
            )
            .is_none()
        );
        assert!(
            RemoteSource::parse_git(
                "git::https://dev.azure/myorg/myproj/_git/example//plugin/./other"
            )
            .is_none()
        );
    }

    #[test]
    fn parses_http_sources() {
        assert!(RemoteSource::parse_http("http://myhost.com/test.txt").is_some());
        assert!(RemoteSource::parse_http("https://myhost.com/test.txt?query=1").is_some());
    }

    #[test]
    fn rejects_http_directories() {
        assert!(RemoteSource::parse_http("https://myhost.com/js/").is_none());
        assert!(RemoteSource::parse_http("https://myhost.com").is_none());
    }
}
