use anyhow::{Context, Result, anyhow};
use reqwest::header::{
    ACCEPT, AUTHORIZATION, HeaderMap, HeaderName, HeaderValue, LINK, USER_AGENT,
};
use reqwest::{Client, StatusCode};
use serde::de::DeserializeOwned;

use super::spec::{ProviderKind, RemoteSpec};

pub struct RemoteClient {
    client: Client,
}

impl RemoteClient {
    pub fn new(spec: &RemoteSpec) -> Result<Self> {
        let headers = build_headers(spec)?;
        let client = Client::builder()
            .default_headers(headers)
            .build()
            .context("failed to build remote API client")?;

        Ok(Self { client })
    }

    pub async fn get_json<T>(&self, url: &str) -> Result<T>
    where
        T: DeserializeOwned,
    {
        self.client
            .get(url)
            .send()
            .await
            .with_context(|| format!("failed to send request to {url}"))?
            .error_for_status()
            .with_context(|| format!("remote API request failed for {url}"))?
            .json::<T>()
            .await
            .with_context(|| format!("failed to decode JSON response from {url}"))
    }

    pub async fn get_json_optional<T>(&self, url: &str) -> Result<Option<T>>
    where
        T: DeserializeOwned,
    {
        let response = self
            .client
            .get(url)
            .send()
            .await
            .with_context(|| format!("failed to send request to {url}"))?;

        if response.status() == StatusCode::NOT_FOUND {
            return Ok(None);
        }

        let response = response
            .error_for_status()
            .with_context(|| format!("remote API request failed for {url}"))?;

        response
            .json::<T>()
            .await
            .map(Some)
            .with_context(|| format!("failed to decode JSON response from {url}"))
    }

    pub async fn get_json_with_headers<T>(&self, url: &str) -> Result<(T, HeaderMap)>
    where
        T: DeserializeOwned,
    {
        let response = self
            .client
            .get(url)
            .send()
            .await
            .with_context(|| format!("failed to send request to {url}"))?
            .error_for_status()
            .with_context(|| format!("remote API request failed for {url}"))?;

        let headers = response.headers().clone();
        let body = response
            .json::<T>()
            .await
            .with_context(|| format!("failed to decode JSON response from {url}"))?;

        Ok((body, headers))
    }
}

fn build_headers(spec: &RemoteSpec) -> Result<HeaderMap> {
    let mut headers = HeaderMap::new();
    headers.insert(USER_AGENT, HeaderValue::from_static("madgit"));
    headers.insert(ACCEPT, HeaderValue::from_static("application/json"));

    if let Some((name, value)) = auth_header(spec)? {
        headers.insert(name, value);
    }

    Ok(headers)
}

fn auth_header(spec: &RemoteSpec) -> Result<Option<(HeaderName, HeaderValue)>> {
    let Some(token) = spec.auth_token() else {
        return Ok(None);
    };

    match spec.kind {
        ProviderKind::GitHub => Ok(Some((
            AUTHORIZATION,
            header_value(spec, &format!("Bearer {token}"))?,
        ))),
        ProviderKind::GitLab => Ok(Some((
            HeaderName::from_static("private-token"),
            header_value(spec, &token)?,
        ))),
        ProviderKind::Forgejo => Ok(Some((
            AUTHORIZATION,
            header_value(spec, &format!("token {token}"))?,
        ))),
    }
}

fn header_value(spec: &RemoteSpec, value: &str) -> Result<HeaderValue> {
    HeaderValue::from_str(value).with_context(|| {
        format!(
            "failed to construct authorization header from {}",
            spec.auth_env_var()
        )
    })
}

pub fn normalize_base_url(base_url: Option<&str>, default: &str) -> String {
    let raw = base_url.unwrap_or(default).trim();

    if raw.is_empty() {
        return default.to_string();
    }

    let prefixed = if raw.contains("://") {
        raw.to_string()
    } else {
        format!("https://{raw}")
    };

    prefixed.trim_end_matches('/').to_string()
}

pub fn parse_next_link(headers: &HeaderMap) -> Result<Option<String>> {
    let Some(link_value) = headers.get(LINK) else {
        return Ok(None);
    };

    let link_value = link_value
        .to_str()
        .context("failed to parse pagination link header")?;

    for part in link_value.split(',') {
        let mut sections = part.trim().split(';');
        let Some(url_part) = sections.next() else {
            continue;
        };

        let is_next = sections.any(|section| section.trim() == "rel=\"next\"");
        if !is_next {
            continue;
        }

        let url = url_part
            .trim()
            .strip_prefix('<')
            .and_then(|value| value.strip_suffix('>'))
            .ok_or_else(|| anyhow!("invalid pagination link header format"))?;

        return Ok(Some(url.to_string()));
    }

    Ok(None)
}

#[cfg(test)]
mod tests {
    use reqwest::header::{HeaderMap, HeaderValue, LINK};

    use super::{RemoteClient, auth_header, build_headers, normalize_base_url, parse_next_link};
    use crate::remotes::RemoteSpec;
    use crate::testing::{StubResponse, env_lock, remove_env_var, set_env_var, spawn_server};

    fn clear_auth_env() {
        remove_env_var("GITHUB_TOKEN");
        remove_env_var("GITLAB_TOKEN");
        remove_env_var("FORGEJO_TOKEN");
    }

    #[test]
    fn normalize_base_url_adds_https_and_trims_trailing_slash() {
        assert_eq!(
            normalize_base_url(Some("gitlab.example.com/"), "https://gitlab.com"),
            "https://gitlab.example.com"
        );
    }

    #[test]
    fn normalize_base_url_falls_back_when_value_is_blank() {
        assert_eq!(
            normalize_base_url(Some("   "), "https://codeberg.org"),
            "https://codeberg.org"
        );
    }

    #[test]
    fn parse_next_link_returns_next_url_when_present() {
        let mut headers = HeaderMap::new();
        headers.insert(
            LINK,
            HeaderValue::from_static(
                r#"<https://example.com/page=2>; rel="next", <https://example.com/page=5>; rel="last""#,
            ),
        );

        assert_eq!(
            parse_next_link(&headers).unwrap(),
            Some("https://example.com/page=2".to_string())
        );
    }

    #[test]
    fn parse_next_link_errors_on_invalid_next_link_format() {
        let mut headers = HeaderMap::new();
        headers.insert(
            LINK,
            HeaderValue::from_static(r#"https://example.com/page=2; rel="next""#),
        );

        assert!(parse_next_link(&headers).is_err());
    }

    #[test]
    fn parse_next_link_returns_none_when_header_is_missing() {
        let headers = HeaderMap::new();

        assert_eq!(parse_next_link(&headers).unwrap(), None);
    }

    #[test]
    fn parse_next_link_returns_none_when_next_relation_is_absent() {
        let mut headers = HeaderMap::new();
        headers.insert(
            LINK,
            HeaderValue::from_static(r#"<https://example.com/page=5>; rel="last""#),
        );

        assert_eq!(parse_next_link(&headers).unwrap(), None);
    }

    #[test]
    fn build_headers_sets_default_headers() {
        let _guard = env_lock().lock().unwrap();
        clear_auth_env();
        let spec = RemoteSpec::github("wykwit").unwrap();
        let headers = build_headers(&spec).unwrap();

        assert_eq!(
            headers.get("user-agent").unwrap(),
            &HeaderValue::from_static("madgit")
        );
        assert_eq!(
            headers.get("accept").unwrap(),
            &HeaderValue::from_static("application/json")
        );
    }

    #[test]
    fn auth_header_uses_provider_specific_formats() {
        let _guard = env_lock().lock().unwrap();

        set_env_var("GITHUB_TOKEN", "gh-token");
        let github_header = auth_header(&RemoteSpec::github("wykwit").unwrap())
            .unwrap()
            .unwrap();
        assert_eq!(github_header.1, HeaderValue::from_static("Bearer gh-token"));
        remove_env_var("GITHUB_TOKEN");

        set_env_var("GITLAB_TOKEN", "gl-token");
        let gitlab_header = auth_header(&RemoteSpec::gitlab("gitlab.example.com/wykwit").unwrap())
            .unwrap()
            .unwrap();
        assert_eq!(gitlab_header.0.as_str(), "private-token");
        assert_eq!(gitlab_header.1, HeaderValue::from_static("gl-token"));
        remove_env_var("GITLAB_TOKEN");

        set_env_var("FORGEJO_TOKEN", "fj-token");
        let forgejo_header = auth_header(&RemoteSpec::forgejo("codeberg.org/wykwit").unwrap())
            .unwrap()
            .unwrap();
        assert_eq!(forgejo_header.1, HeaderValue::from_static("token fj-token"));
        remove_env_var("FORGEJO_TOKEN");
    }

    #[test]
    fn auth_header_returns_none_without_token() {
        let _guard = env_lock().lock().unwrap();
        remove_env_var("GITHUB_TOKEN");

        assert_eq!(
            auth_header(&RemoteSpec::github("wykwit").unwrap()).unwrap(),
            None
        );
    }

    #[test]
    fn auth_header_errors_for_invalid_token_value() {
        let _guard = env_lock().lock().unwrap();
        set_env_var("GITHUB_TOKEN", "bad\nvalue");

        let error = auth_header(&RemoteSpec::github("wykwit").unwrap()).unwrap_err();
        assert!(
            error
                .to_string()
                .contains("failed to construct authorization header from GITHUB_TOKEN")
        );

        remove_env_var("GITHUB_TOKEN");
    }

    #[tokio::test]
    async fn get_json_reads_a_json_response() {
        let _guard = env_lock().lock().unwrap();
        clear_auth_env();
        let server = spawn_server(vec![(
            "/repos".to_string(),
            StubResponse::json(r#"{"name":"madgit"}"#),
        )])
        .await;
        let spec = RemoteSpec::github("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();

        let payload = client
            .get_json::<serde_json::Value>(&format!("{}/repos", server.url()))
            .await
            .unwrap();

        assert_eq!(payload["name"], "madgit");
        assert_eq!(server.requests(), vec!["/repos"]);
    }

    #[tokio::test]
    async fn get_json_surfaces_http_failures() {
        let _guard = env_lock().lock().unwrap();
        clear_auth_env();
        let server = spawn_server(vec![(
            ("/repos").to_string(),
            StubResponse::json("{}").with_status("500 Internal Server Error"),
        )])
        .await;
        let spec = RemoteSpec::github("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();

        let error = client
            .get_json::<serde_json::Value>(&format!("{}/repos", server.url()))
            .await
            .unwrap_err();

        assert!(error.to_string().contains(&format!(
            "remote API request failed for {}/repos",
            server.url()
        )));
    }

    #[tokio::test]
    async fn get_json_surfaces_json_decode_failures() {
        let _guard = env_lock().lock().unwrap();
        clear_auth_env();
        let server = spawn_server(vec![(
            ("/repos").to_string(),
            StubResponse::json("not-json"),
        )])
        .await;
        let spec = RemoteSpec::github("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();

        let error = client
            .get_json::<serde_json::Value>(&format!("{}/repos", server.url()))
            .await
            .unwrap_err();

        assert!(error.to_string().contains(&format!(
            "failed to decode JSON response from {}/repos",
            server.url()
        )));
    }

    #[tokio::test]
    async fn get_json_optional_returns_none_for_not_found() {
        let _guard = env_lock().lock().unwrap();
        clear_auth_env();
        let server = spawn_server(vec![("/missing".to_string(), StubResponse::not_found())]).await;
        let spec = RemoteSpec::github("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();

        let payload = client
            .get_json_optional::<serde_json::Value>(&format!("{}/missing", server.url()))
            .await
            .unwrap();

        assert_eq!(payload, None);
    }

    #[tokio::test]
    async fn get_json_optional_surfaces_http_failures() {
        let _guard = env_lock().lock().unwrap();
        clear_auth_env();
        let server = spawn_server(vec![(
            ("/repos").to_string(),
            StubResponse::json("{}").with_status("403 Forbidden"),
        )])
        .await;
        let spec = RemoteSpec::github("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();

        let error = client
            .get_json_optional::<serde_json::Value>(&format!("{}/repos", server.url()))
            .await
            .unwrap_err();

        assert!(error.to_string().contains(&format!(
            "remote API request failed for {}/repos",
            server.url()
        )));
    }

    #[tokio::test]
    async fn get_json_optional_surfaces_json_decode_failures() {
        let _guard = env_lock().lock().unwrap();
        clear_auth_env();
        let server = spawn_server(vec![(
            ("/repos").to_string(),
            StubResponse::json("not-json"),
        )])
        .await;
        let spec = RemoteSpec::github("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();

        let error = client
            .get_json_optional::<serde_json::Value>(&format!("{}/repos", server.url()))
            .await
            .unwrap_err();

        assert!(error.to_string().contains(&format!(
            "failed to decode JSON response from {}/repos",
            server.url()
        )));
    }

    #[tokio::test]
    async fn get_json_with_headers_returns_response_headers() {
        let _guard = env_lock().lock().unwrap();
        clear_auth_env();
        let server = spawn_server(vec![(
            "/page".to_string(),
            StubResponse::json("[]")
                .with_header("link", r#"<https://example.com/next>; rel="next""#),
        )])
        .await;
        let spec = RemoteSpec::github("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();

        let (_payload, headers) = client
            .get_json_with_headers::<Vec<serde_json::Value>>(&format!("{}/page", server.url()))
            .await
            .unwrap();

        assert_eq!(
            headers.get("link").unwrap(),
            &HeaderValue::from_static(r#"<https://example.com/next>; rel="next""#)
        );
    }

    #[tokio::test]
    async fn get_json_with_headers_surfaces_http_failures() {
        let _guard = env_lock().lock().unwrap();
        clear_auth_env();
        let server = spawn_server(vec![(
            ("/page").to_string(),
            StubResponse::json("{}").with_status("502 Bad Gateway"),
        )])
        .await;
        let spec = RemoteSpec::github("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();

        let error = client
            .get_json_with_headers::<serde_json::Value>(&format!("{}/page", server.url()))
            .await
            .unwrap_err();

        assert!(error.to_string().contains(&format!(
            "remote API request failed for {}/page",
            server.url()
        )));
    }

    #[tokio::test]
    async fn get_json_with_headers_surfaces_json_decode_failures() {
        let _guard = env_lock().lock().unwrap();
        clear_auth_env();
        let server = spawn_server(vec![(
            ("/page").to_string(),
            StubResponse::json("not-json"),
        )])
        .await;
        let spec = RemoteSpec::github("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();

        let error = client
            .get_json_with_headers::<serde_json::Value>(&format!("{}/page", server.url()))
            .await
            .unwrap_err();

        assert!(error.to_string().contains(&format!(
            "failed to decode JSON response from {}/page",
            server.url()
        )));
    }
}
