use crate::git::load_stored_credential;
use reqwest::blocking::{Client, Response};
use reqwest::header::{ACCEPT, USER_AGENT};
use serde::{Deserialize, Serialize};
use std::time::Duration;

const PAGE_SIZE: usize = 100;

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IntegrationRepository {
    pub id: String,
    pub name: String,
    pub full_name: String,
    pub description: String,
    pub clone_url: String,
    pub ssh_url: String,
    pub web_url: String,
    pub updated_at: String,
    pub private: bool,
}

#[derive(Debug, Deserialize)]
struct GitLabProject {
    id: u64,
    name: String,
    path_with_namespace: String,
    #[serde(default)]
    description: Option<String>,
    http_url_to_repo: String,
    #[serde(default)]
    ssh_url_to_repo: String,
    #[serde(default)]
    web_url: String,
    #[serde(default)]
    last_activity_at: String,
    #[serde(default)]
    visibility: String,
}

#[derive(Debug, Deserialize)]
struct GitHubRepository {
    id: u64,
    name: String,
    full_name: String,
    #[serde(default)]
    description: Option<String>,
    clone_url: String,
    #[serde(default)]
    ssh_url: String,
    #[serde(default)]
    html_url: String,
    #[serde(default)]
    updated_at: String,
    #[serde(default)]
    private: bool,
}

#[derive(Debug, Deserialize)]
struct GiteaRepository {
    id: u64,
    name: String,
    full_name: String,
    #[serde(default)]
    description: String,
    clone_url: String,
    #[serde(default)]
    ssh_url: String,
    #[serde(default)]
    html_url: String,
    #[serde(default)]
    updated_at: String,
    #[serde(default)]
    private: bool,
}

#[derive(Debug, Deserialize)]
struct AzureRepositoryList {
    #[serde(default)]
    value: Vec<AzureRepository>,
}

#[derive(Debug, Deserialize)]
struct AzureRepository {
    id: String,
    name: String,
    project: AzureProject,
    #[serde(default, rename = "remoteUrl")]
    remote_url: String,
    #[serde(default, rename = "sshUrl")]
    ssh_url: String,
    #[serde(default, rename = "webUrl")]
    web_url: String,
}

#[derive(Debug, Deserialize)]
struct AzureProject {
    name: String,
}

fn integration_key(provider: &str, account_id: Option<&str>) -> Result<String, String> {
    if provider == "azure-devops" {
        if let Some(account_id) = account_id.filter(|value| !value.is_empty()) {
            if account_id.len() > 80
                || !account_id.chars().all(|character| {
                    character.is_ascii_alphanumeric() || matches!(character, '-' | '_')
                })
            {
                return Err("Invalid integration account identifier.".to_string());
            }
            if account_id != "default" {
                return Ok(format!("integration:{provider}:{account_id}"));
            }
        }
    }
    Ok(format!("integration:{provider}"))
}

fn client() -> Result<Client, String> {
    Client::builder()
        .timeout(Duration::from_secs(25))
        .build()
        .map_err(|err| format!("Could not initialize the integration client: {err}"))
}

fn normalized_base_url(base_url: &str) -> Result<String, String> {
    let base_url = base_url.trim().trim_end_matches('/');
    if !(base_url.starts_with("https://") || base_url.starts_with("http://")) {
        return Err("The integration URL must start with http:// or https://.".to_string());
    }
    Ok(base_url.to_string())
}

fn github_api_base_url(base_url: &str) -> Result<String, String> {
    match normalized_base_url(base_url)?.to_ascii_lowercase().as_str() {
        "https://github.com" | "https://www.github.com" => Ok("https://api.github.com".to_string()),
        "https://api.github.com" => Ok("https://api.github.com".to_string()),
        _ => Err("The GitHub integration URL must be https://github.com.".to_string()),
    }
}

fn response_error(response: Response, provider: &str) -> String {
    let status = response.status();
    let detail = response.text().ok().and_then(|body| {
        serde_json::from_str::<serde_json::Value>(&body)
            .ok()
            .and_then(|value| {
                value
                    .get("message")
                    .or_else(|| value.get("error"))
                    .and_then(|value| value.as_str())
                    .map(str::to_string)
            })
    });
    match detail {
        Some(detail) if !detail.trim().is_empty() => {
            format!("{provider} returned {status}: {detail}")
        }
        _ => format!("{provider} returned {status}."),
    }
}

fn gitlab_repositories(
    client: &Client,
    base_url: &str,
    token: &str,
) -> Result<Vec<IntegrationRepository>, String> {
    let mut repositories = Vec::new();
    let mut page = 1usize;
    loop {
        let response = client
            .get(format!("{base_url}/api/v4/projects"))
            .header(USER_AGENT, "Gitty")
            .header(ACCEPT, "application/json")
            .header("PRIVATE-TOKEN", token)
            .query(&[
                ("membership", "true"),
                ("simple", "true"),
                ("order_by", "last_activity_at"),
                ("sort", "desc"),
                ("per_page", &PAGE_SIZE.to_string()),
                ("page", &page.to_string()),
            ])
            .send()
            .map_err(|err| format!("Could not reach GitLab: {err}"))?;
        if !response.status().is_success() {
            return Err(response_error(response, "GitLab"));
        }
        let next_page = response
            .headers()
            .get("x-next-page")
            .and_then(|value| value.to_str().ok())
            .unwrap_or("")
            .to_string();
        let projects = response
            .json::<Vec<GitLabProject>>()
            .map_err(|err| format!("GitLab returned an unreadable repository list: {err}"))?;
        repositories.extend(projects.into_iter().map(|project| IntegrationRepository {
            id: project.id.to_string(),
            name: project.name,
            full_name: project.path_with_namespace,
            description: project.description.unwrap_or_default(),
            clone_url: project.http_url_to_repo,
            ssh_url: project.ssh_url_to_repo,
            web_url: project.web_url,
            updated_at: project.last_activity_at,
            private: project.visibility == "private",
        }));
        if next_page.is_empty() {
            break;
        }
        page = next_page.parse().unwrap_or(page + 1);
    }
    Ok(repositories)
}

fn github_repositories(
    client: &Client,
    base_url: &str,
    token: &str,
) -> Result<Vec<IntegrationRepository>, String> {
    let api_base_url = github_api_base_url(base_url)?;
    let mut repositories = Vec::new();
    let mut page = 1usize;
    loop {
        let response = client
            .get(format!("{api_base_url}/user/repos"))
            .header(USER_AGENT, "Gitty")
            .header(ACCEPT, "application/vnd.github+json")
            .header("Authorization", format!("Bearer {token}"))
            .header("X-GitHub-Api-Version", "2026-03-10")
            .query(&[
                ("per_page", PAGE_SIZE.to_string()),
                ("page", page.to_string()),
                ("sort", "updated".to_string()),
                ("direction", "desc".to_string()),
            ])
            .send()
            .map_err(|err| format!("Could not reach GitHub: {err}"))?;
        if !response.status().is_success() {
            return Err(response_error(response, "GitHub"));
        }
        let page_repositories = response
            .json::<Vec<GitHubRepository>>()
            .map_err(|err| format!("GitHub returned an unreadable repository list: {err}"))?;
        let count = page_repositories.len();
        repositories.extend(page_repositories.into_iter().map(|repository| {
            IntegrationRepository {
                id: repository.id.to_string(),
                name: repository.name,
                full_name: repository.full_name,
                description: repository.description.unwrap_or_default(),
                clone_url: repository.clone_url,
                ssh_url: repository.ssh_url,
                web_url: repository.html_url,
                updated_at: repository.updated_at,
                private: repository.private,
            }
        }));
        if count < PAGE_SIZE {
            break;
        }
        page += 1;
    }
    Ok(repositories)
}

fn gitea_repositories(
    client: &Client,
    base_url: &str,
    token: &str,
) -> Result<Vec<IntegrationRepository>, String> {
    let mut repositories = Vec::new();
    let mut page = 1usize;
    loop {
        let response = client
            .get(format!("{base_url}/api/v1/user/repos"))
            .header(USER_AGENT, "Gitty")
            .header(ACCEPT, "application/json")
            .header("Authorization", format!("token {token}"))
            .query(&[
                ("limit", PAGE_SIZE.to_string()),
                ("page", page.to_string()),
                ("sort", "updated".to_string()),
            ])
            .send()
            .map_err(|err| format!("Could not reach Gitea: {err}"))?;
        if !response.status().is_success() {
            return Err(response_error(response, "Gitea"));
        }
        let page_repositories = response
            .json::<Vec<GiteaRepository>>()
            .map_err(|err| format!("Gitea returned an unreadable repository list: {err}"))?;
        let count = page_repositories.len();
        repositories.extend(page_repositories.into_iter().map(|repository| {
            IntegrationRepository {
                id: repository.id.to_string(),
                name: repository.name,
                full_name: repository.full_name,
                description: repository.description,
                clone_url: repository.clone_url,
                ssh_url: repository.ssh_url,
                web_url: repository.html_url,
                updated_at: repository.updated_at,
                private: repository.private,
            }
        }));
        if count < PAGE_SIZE {
            break;
        }
        page += 1;
    }
    Ok(repositories)
}

fn azure_repositories(
    client: &Client,
    base_url: &str,
    username: &str,
    token: &str,
) -> Result<Vec<IntegrationRepository>, String> {
    let response = client
        .get(format!("{base_url}/_apis/git/repositories"))
        .header(USER_AGENT, "Gitty")
        .header(ACCEPT, "application/json")
        .basic_auth(username, Some(token))
        .query(&[("api-version", "7.1")])
        .send()
        .map_err(|err| format!("Could not reach Azure DevOps: {err}"))?;
    if !response.status().is_success() {
        return Err(response_error(response, "Azure DevOps"));
    }
    let repositories = response
        .json::<AzureRepositoryList>()
        .map_err(|err| format!("Azure DevOps returned an unreadable repository list: {err}"))?;
    Ok(repositories
        .value
        .into_iter()
        .map(|repository| IntegrationRepository {
            id: repository.id,
            full_name: format!("{}/{}", repository.project.name, repository.name),
            name: repository.name,
            description: String::new(),
            clone_url: repository.remote_url,
            ssh_url: repository.ssh_url,
            web_url: repository.web_url,
            updated_at: String::new(),
            private: true,
        })
        .collect())
}

#[tauri::command]
pub async fn list_integration_repositories(
    provider: String,
    base_url: String,
    account_id: Option<String>,
) -> Result<Vec<IntegrationRepository>, String> {
    tauri::async_runtime::spawn_blocking(move || {
        let credential_key = integration_key(&provider, account_id.as_deref())?;
        let credential = load_stored_credential(&credential_key)?
            .ok_or_else(|| "No token is stored for this integration.".to_string())?;
        let base_url = normalized_base_url(&base_url)?;
        let client = client()?;
        match provider.as_str() {
            "github" => github_repositories(&client, &base_url, &credential.password),
            "gitlab" | "gitlab-self-hosted" => {
                gitlab_repositories(&client, &base_url, &credential.password)
            }
            "azure-devops" => azure_repositories(
                &client,
                &base_url,
                if credential.username.trim().is_empty() {
                    "gitty"
                } else {
                    &credential.username
                },
                &credential.password,
            ),
            "gitea" => gitea_repositories(&client, &base_url, &credential.password),
            _ => Err("Unsupported integration provider.".to_string()),
        }
    })
    .await
    .map_err(|err| format!("Could not load integration repositories: {err}"))?
}

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

    #[test]
    fn base_urls_are_normalized_and_validated() {
        assert_eq!(
            normalized_base_url(" https://gitlab.example.com/ ").unwrap(),
            "https://gitlab.example.com"
        );
        assert!(normalized_base_url("gitlab.example.com").is_err());
        assert_eq!(
            github_api_base_url("https://github.com/").unwrap(),
            "https://api.github.com"
        );
        assert!(github_api_base_url("https://github.example.com").is_err());
    }

    #[test]
    fn integration_credential_keys_match_the_frontend() {
        assert_eq!(
            integration_key("github", None).unwrap(),
            "integration:github"
        );
        assert_eq!(integration_key("gitea", None).unwrap(), "integration:gitea");
        assert_eq!(
            integration_key("azure-devops", Some("org-123")).unwrap(),
            "integration:azure-devops:org-123"
        );
        assert_eq!(
            integration_key("azure-devops", Some("default")).unwrap(),
            "integration:azure-devops"
        );
        assert!(integration_key("azure-devops", Some("../invalid")).is_err());
    }

    #[test]
    fn provider_repository_payloads_deserialize() {
        let github: Vec<GitHubRepository> = serde_json::from_str(
            r#"[{"id":6,"name":"desktop","full_name":"team/desktop","description":null,"clone_url":"https://github.com/team/desktop.git","ssh_url":"git@github.com:team/desktop.git","html_url":"https://github.com/team/desktop","updated_at":"2026-08-29T09:00:00Z","private":true}]"#,
        )
        .unwrap();
        assert_eq!(github[0].full_name, "team/desktop");
        assert!(github[0].description.is_none());

        let gitlab: Vec<GitLabProject> = serde_json::from_str(
            r#"[{"id":7,"name":"app","path_with_namespace":"team/app","description":"Demo","http_url_to_repo":"https://gitlab.test/team/app.git","ssh_url_to_repo":"git@gitlab.test:team/app.git","web_url":"https://gitlab.test/team/app","last_activity_at":"2026-08-29T10:00:00Z","visibility":"private"}]"#,
        )
        .unwrap();
        assert_eq!(gitlab[0].path_with_namespace, "team/app");
        assert_eq!(gitlab[0].visibility, "private");

        let gitea: Vec<GiteaRepository> = serde_json::from_str(
            r#"[{"id":8,"name":"api","full_name":"team/api","description":"","clone_url":"https://gitea.test/team/api.git","ssh_url":"git@gitea.test:team/api.git","html_url":"https://gitea.test/team/api","updated_at":"2026-08-29T11:00:00Z","private":false}]"#,
        )
        .unwrap();
        assert_eq!(gitea[0].full_name, "team/api");
        assert!(!gitea[0].private);

        let azure: AzureRepositoryList = serde_json::from_str(
            r#"{"value":[{"id":"repo-id","name":"web","project":{"name":"Platform"},"remoteUrl":"https://dev.azure.com/org/Platform/_git/web","sshUrl":"git@ssh.dev.azure.com:v3/org/Platform/web","webUrl":"https://dev.azure.com/org/Platform/_git/web"}]}"#,
        )
        .unwrap();
        assert_eq!(azure.value[0].project.name, "Platform");
        assert_eq!(
            azure.value[0].remote_url,
            "https://dev.azure.com/org/Platform/_git/web"
        );
    }
}
