use anyhow::{Result, bail};

use super::client::normalize_base_url;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ProviderKind {
    GitHub,
    GitLab,
    Forgejo,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RemoteSpec {
    pub kind: ProviderKind,
    pub base_url: Option<String>,
    pub owner: String,
}

impl RemoteSpec {
    pub fn github(owner: impl Into<String>) -> Result<Self> {
        let owner = owner.into();
        validate_owner(&owner)?;

        Ok(Self {
            kind: ProviderKind::GitHub,
            base_url: None,
            owner,
        })
    }

    pub fn gitlab(arg: &str) -> Result<Self> {
        Self::with_optional_host(ProviderKind::GitLab, arg)
    }

    pub fn forgejo(arg: &str) -> Result<Self> {
        Self::with_optional_host(ProviderKind::Forgejo, arg)
    }

    pub fn normalized_base_url(&self) -> String {
        match self.kind {
            ProviderKind::GitHub => {
                normalize_base_url(self.base_url.as_deref(), "https://api.github.com")
            }
            ProviderKind::GitLab => {
                normalize_base_url(self.base_url.as_deref(), "https://gitlab.com")
            }
            ProviderKind::Forgejo => {
                normalize_base_url(self.base_url.as_deref(), "https://codeberg.org")
            }
        }
    }

    pub fn auth_env_var(&self) -> &'static str {
        self.kind.auth_env_var()
    }

    pub fn auth_env_var_if_present(&self) -> Option<&'static str> {
        self.auth_token().map(|_| self.auth_env_var())
    }

    pub fn has_auth_token(&self) -> bool {
        self.auth_token().is_some()
    }

    pub(crate) fn auth_token(&self) -> Option<String> {
        read_auth_env_var(self.auth_env_var())
            .ok()
            .filter(|token| !token.trim().is_empty())
    }

    fn with_optional_host(kind: ProviderKind, arg: &str) -> Result<Self> {
        let arg = arg.trim().trim_end_matches('/');
        if arg.is_empty() {
            bail!("remote provider argument cannot be empty");
        }

        let (base_url, owner) = split_host_and_owner(arg)?;
        validate_owner(owner)?;

        Ok(Self {
            kind,
            base_url,
            owner: owner.to_string(),
        })
    }
}

impl ProviderKind {
    pub fn auth_env_var(self) -> &'static str {
        match self {
            ProviderKind::GitHub => "GITHUB_TOKEN",
            ProviderKind::GitLab => "GITLAB_TOKEN",
            ProviderKind::Forgejo => "FORGEJO_TOKEN",
        }
    }
}

fn split_host_and_owner(arg: &str) -> Result<(Option<String>, &str)> {
    if let Some(rest) = arg.strip_prefix("http://") {
        return split_explicit_url("http://", rest);
    }

    if let Some(rest) = arg.strip_prefix("https://") {
        return split_explicit_url("https://", rest);
    }

    if let Some((host, owner)) = arg.rsplit_once('/') {
        if host.contains('.') || host.contains(':') || host.contains("localhost") {
            if owner.is_empty() {
                bail!("remote owner cannot be empty");
            }

            return Ok((Some(host.to_string()), owner));
        }
    }

    Ok((None, arg))
}

fn split_explicit_url<'a>(scheme: &str, rest: &'a str) -> Result<(Option<String>, &'a str)> {
    let Some((host, owner)) = rest.rsplit_once('/') else {
        bail!("expected remote argument in the form HOST/USER");
    };

    if host.is_empty() || owner.is_empty() {
        bail!("expected remote argument in the form HOST/USER");
    }

    Ok((Some(format!("{scheme}{host}")), owner))
}

fn validate_owner(owner: &str) -> Result<()> {
    if owner.trim().is_empty() {
        bail!("remote owner cannot be empty");
    }

    Ok(())
}

#[cfg(test)]
fn read_auth_env_var(key: &str) -> std::result::Result<String, std::env::VarError> {
    crate::testing::read_env_var(key).ok_or(std::env::VarError::NotPresent)
}

#[cfg(not(test))]
fn read_auth_env_var(key: &str) -> std::result::Result<String, std::env::VarError> {
    std::env::var(key)
}

#[cfg(test)]
mod tests {
    use super::{ProviderKind, RemoteSpec};
    use crate::testing::{env_lock, remove_env_var, set_env_var};

    #[test]
    fn parses_gitlab_owner_without_host() {
        let spec = RemoteSpec::gitlab("wykwit").unwrap();

        assert_eq!(spec.kind, ProviderKind::GitLab);
        assert_eq!(spec.base_url, None);
        assert_eq!(spec.owner, "wykwit");
    }

    #[test]
    fn parses_gitlab_owner_with_host() {
        let spec = RemoteSpec::gitlab("gitlab.kotec.pl/wykwit").unwrap();

        assert_eq!(spec.base_url.as_deref(), Some("gitlab.kotec.pl"));
        assert_eq!(spec.owner, "wykwit");
    }

    #[test]
    fn parses_forgejo_owner_with_https_host() {
        let spec = RemoteSpec::forgejo("https://codeberg.org/wykwit").unwrap();

        assert_eq!(spec.kind, ProviderKind::Forgejo);
        assert_eq!(spec.base_url.as_deref(), Some("https://codeberg.org"));
        assert_eq!(spec.owner, "wykwit");
    }

    #[test]
    fn parses_self_hosted_owner_with_port() {
        let spec = RemoteSpec::gitlab("localhost:3000/wykwit").unwrap();

        assert_eq!(spec.base_url.as_deref(), Some("localhost:3000"));
        assert_eq!(spec.owner, "wykwit");
    }

    #[test]
    fn trims_trailing_slash_from_remote_argument() {
        let spec = RemoteSpec::forgejo("codeberg.org/wykwit/").unwrap();

        assert_eq!(spec.base_url.as_deref(), Some("codeberg.org"));
        assert_eq!(spec.owner, "wykwit");
    }

    #[test]
    fn normalizes_provider_defaults() {
        let github = RemoteSpec::github("wykwit").unwrap();
        let gitlab = RemoteSpec::gitlab("wykwit").unwrap();
        let forgejo = RemoteSpec::forgejo("wykwit").unwrap();

        assert_eq!(github.normalized_base_url(), "https://api.github.com");
        assert_eq!(gitlab.normalized_base_url(), "https://gitlab.com");
        assert_eq!(forgejo.normalized_base_url(), "https://codeberg.org");
    }

    #[test]
    fn rejects_empty_owner_values() {
        assert!(RemoteSpec::github(" ").is_err());
        assert!(RemoteSpec::gitlab(" ").is_err());
        assert!(RemoteSpec::forgejo(" ").is_err());
    }

    #[test]
    fn rejects_explicit_urls_without_owner() {
        assert!(RemoteSpec::gitlab("https://gitlab.example.com").is_err());
        assert!(RemoteSpec::forgejo("http://codeberg.example.com/").is_err());
    }

    #[test]
    fn exposes_provider_specific_auth_environment_variables() {
        assert_eq!(ProviderKind::GitHub.auth_env_var(), "GITHUB_TOKEN");
        assert_eq!(ProviderKind::GitLab.auth_env_var(), "GITLAB_TOKEN");
        assert_eq!(ProviderKind::Forgejo.auth_env_var(), "FORGEJO_TOKEN");
    }

    #[test]
    fn reports_auth_token_presence_from_environment() {
        let _guard = env_lock().lock().unwrap();
        let spec = RemoteSpec::github("wykwit").unwrap();

        set_env_var("GITHUB_TOKEN", "secret-token");
        assert!(spec.has_auth_token());
        assert_eq!(spec.auth_env_var_if_present(), Some("GITHUB_TOKEN"));

        remove_env_var("GITHUB_TOKEN");
        assert!(!spec.has_auth_token());
        assert_eq!(spec.auth_env_var_if_present(), None);
    }

    #[test]
    fn ignores_blank_auth_token_values() {
        let _guard = env_lock().lock().unwrap();
        let spec = RemoteSpec::github("wykwit").unwrap();

        set_env_var("GITHUB_TOKEN", "   ");

        assert_eq!(spec.auth_token(), None);

        remove_env_var("GITHUB_TOKEN");
    }
}
