use std::fmt;

use reqwest::Client;
use serde::{Deserialize, Serialize};

use crate::error::DiscordError;
use crate::types::invalid_data_error;

const API_BASE: &str = "https://discord.com/api/v10";
const AUTHORIZE_BASE: &str = "https://discord.com/oauth2/authorize";

#[derive(Clone)]
/// Typed Discord API object for `OAuth2Client`.
pub struct OAuth2Client {
    client: Client,
    client_id: String,
    client_secret: Option<String>,
    #[cfg(test)]
    api_base: String,
    #[cfg(test)]
    authorize_base: String,
}

impl fmt::Debug for OAuth2Client {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("OAuth2Client")
            .field("client_id", &self.client_id)
            .field(
                "client_secret",
                &self.client_secret.as_ref().map(|_| "[redacted]"),
            )
            .finish()
    }
}

impl OAuth2Client {
    /// Creates or returns `new` data.
    pub fn new(client_id: impl Into<String>, client_secret: impl Into<String>) -> Self {
        Self {
            client: Client::new(),
            client_id: client_id.into(),
            client_secret: Some(client_secret.into()),
            #[cfg(test)]
            api_base: API_BASE.to_string(),
            #[cfg(test)]
            authorize_base: AUTHORIZE_BASE.to_string(),
        }
    }

    /// Creates or returns `public_client` data.
    pub fn public_client(client_id: impl Into<String>) -> Self {
        Self {
            client: Client::new(),
            client_id: client_id.into(),
            client_secret: None,
            #[cfg(test)]
            api_base: API_BASE.to_string(),
            #[cfg(test)]
            authorize_base: AUTHORIZE_BASE.to_string(),
        }
    }

    #[cfg(test)]
    fn new_with_base_url(
        client_id: impl Into<String>,
        client_secret: Option<String>,
        api_base: impl Into<String>,
        authorize_base: impl Into<String>,
    ) -> Self {
        Self {
            client: Client::new(),
            client_id: client_id.into(),
            client_secret,
            api_base: api_base.into(),
            authorize_base: authorize_base.into(),
        }
    }

    /// Runs the `authorization_url` operation.
    pub fn authorization_url(
        &self,
        request: OAuth2AuthorizationRequest,
    ) -> Result<String, DiscordError> {
        request.validate()?;
        let scopes = request
            .scopes
            .iter()
            .map(OAuth2Scope::as_str)
            .collect::<Vec<_>>()
            .join(" ");

        let mut pairs = vec![
            ("client_id", self.client_id.as_str()),
            ("redirect_uri", request.redirect_uri.as_str()),
            ("response_type", request.response_type.as_str()),
            ("scope", scopes.as_str()),
        ];
        if let Some(state) = request.state.as_deref() {
            pairs.push(("state", state));
        }
        if let Some(prompt) = request.prompt.as_deref() {
            pairs.push(("prompt", prompt));
        }
        let mut url = self.authorize_base().to_string();
        url.push('?');
        url.push_str(&encode_form_pairs(&pairs));
        if let Some(integration_type) = request.integration_type {
            url.push('&');
            url.push_str("integration_type=");
            url.push_str(&integration_type.to_string());
        }
        Ok(url)
    }

    /// Runs the `exchange_code` operation.
    pub async fn exchange_code(
        &self,
        request: OAuth2CodeExchange,
    ) -> Result<OAuth2TokenResponse, DiscordError> {
        request.validate()?;
        let mut form = vec![
            ("grant_type", "authorization_code".to_string()),
            ("code", request.code),
            ("redirect_uri", request.redirect_uri),
            ("client_id", self.client_id.clone()),
        ];
        if let Some(client_secret) = &self.client_secret {
            form.push(("client_secret", client_secret.clone()));
        }
        self.send_token_request(&form).await
    }

    /// Runs the `refresh_token` operation.
    pub async fn refresh_token(
        &self,
        request: OAuth2RefreshToken,
    ) -> Result<OAuth2TokenResponse, DiscordError> {
        request.validate()?;
        let mut form = vec![
            ("grant_type", "refresh_token".to_string()),
            ("refresh_token", request.refresh_token),
            ("client_id", self.client_id.clone()),
        ];
        if let Some(client_secret) = &self.client_secret {
            form.push(("client_secret", client_secret.clone()));
        }
        self.send_token_request(&form).await
    }

    async fn send_token_request(
        &self,
        form: &[(&str, String)],
    ) -> Result<OAuth2TokenResponse, DiscordError> {
        let response = self
            .client
            .post(format!("{}/oauth2/token", self.api_base()))
            .header("Content-Type", "application/x-www-form-urlencoded")
            .body(encode_owned_form_pairs(form))
            .send()
            .await?;
        let status = response.status();
        if !status.is_success() {
            let body = response.text().await.unwrap_or_default();
            return Err(format!("Discord OAuth2 token request failed ({status}): {body}").into());
        }
        Ok(response.json::<OAuth2TokenResponse>().await?)
    }

    #[cfg(test)]
    fn api_base(&self) -> &str {
        &self.api_base
    }

    #[cfg(not(test))]
    fn api_base(&self) -> &str {
        API_BASE
    }

    #[cfg(test)]
    fn authorize_base(&self) -> &str {
        &self.authorize_base
    }

    #[cfg(not(test))]
    fn authorize_base(&self) -> &str {
        AUTHORIZE_BASE
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
/// Typed Discord API object for `OAuth2AuthorizationRequest`.
pub struct OAuth2AuthorizationRequest {
    /// Discord API payload field `redirect_uri`.
    pub redirect_uri: String,
    /// Discord API payload field `scopes`.
    pub scopes: Vec<OAuth2Scope>,
    /// Discord API payload field `response_type`.
    pub response_type: String,
    /// Discord API payload field `state`.
    pub state: Option<String>,
    /// Discord API payload field `prompt`.
    pub prompt: Option<String>,
    /// Discord API payload field `integration_type`.
    pub integration_type: Option<u8>,
}

impl OAuth2AuthorizationRequest {
    /// Creates or returns `code` data.
    pub fn code(
        redirect_uri: impl Into<String>,
        scopes: impl IntoIterator<Item = OAuth2Scope>,
    ) -> Self {
        Self {
            redirect_uri: redirect_uri.into(),
            scopes: scopes.into_iter().collect(),
            response_type: "code".to_string(),
            state: None,
            prompt: None,
            integration_type: None,
        }
    }

    /// Runs the `state` operation.
    pub fn state(mut self, state: impl Into<String>) -> Self {
        self.state = Some(state.into());
        self
    }

    /// Runs the `prompt` operation.
    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
        self.prompt = Some(prompt.into());
        self
    }

    /// Runs the `integration_type` operation.
    pub fn integration_type(mut self, integration_type: u8) -> Self {
        self.integration_type = Some(integration_type);
        self
    }

    fn validate(&self) -> Result<(), DiscordError> {
        if self.redirect_uri.trim().is_empty() {
            return Err(invalid_data_error("redirect_uri must not be empty"));
        }
        if self.scopes.is_empty() {
            return Err(invalid_data_error("at least one OAuth2 scope is required"));
        }
        Ok(())
    }
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(transparent)]
/// Typed Discord API object for `OAuth2Scope`.
pub struct OAuth2Scope(String);

impl OAuth2Scope {
    /// Creates or returns `new` data.
    pub fn new(scope: impl Into<String>) -> Self {
        Self(scope.into())
    }

    /// Creates or returns `identify` data.
    pub fn identify() -> Self {
        Self::new("identify")
    }

    /// Creates or returns `email` data.
    pub fn email() -> Self {
        Self::new("email")
    }

    /// Creates or returns `guilds` data.
    pub fn guilds() -> Self {
        Self::new("guilds")
    }

    /// Creates or returns `guilds_join` data.
    pub fn guilds_join() -> Self {
        Self::new("guilds.join")
    }

    /// Creates or returns `applications_commands_update` data.
    pub fn applications_commands_update() -> Self {
        Self::new("applications.commands.update")
    }

    /// Creates or returns `role_connections_write` data.
    pub fn role_connections_write() -> Self {
        Self::new("role_connections.write")
    }

    /// Runs the `as_str` operation.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

#[derive(Clone, PartialEq, Eq)]
/// Typed Discord API object for `OAuth2CodeExchange`.
pub struct OAuth2CodeExchange {
    /// Discord API payload field `code`.
    pub code: String,
    /// Discord API payload field `redirect_uri`.
    pub redirect_uri: String,
}

impl OAuth2CodeExchange {
    /// Creates or returns `new` data.
    pub fn new(code: impl Into<String>, redirect_uri: impl Into<String>) -> Self {
        Self {
            code: code.into(),
            redirect_uri: redirect_uri.into(),
        }
    }

    fn validate(&self) -> Result<(), DiscordError> {
        if self.code.trim().is_empty() {
            return Err(invalid_data_error("code must not be empty"));
        }
        if self.redirect_uri.trim().is_empty() {
            return Err(invalid_data_error("redirect_uri must not be empty"));
        }
        Ok(())
    }
}

impl fmt::Debug for OAuth2CodeExchange {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("OAuth2CodeExchange")
            .field("code", &"[redacted]")
            .field("redirect_uri", &self.redirect_uri)
            .finish()
    }
}

#[derive(Clone, PartialEq, Eq)]
/// Typed Discord API object for `OAuth2RefreshToken`.
pub struct OAuth2RefreshToken {
    /// Discord API payload field `refresh_token`.
    pub refresh_token: String,
}

impl fmt::Debug for OAuth2RefreshToken {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("OAuth2RefreshToken")
            .field("refresh_token", &"[redacted]")
            .finish()
    }
}

impl OAuth2RefreshToken {
    /// Creates or returns `new` data.
    pub fn new(refresh_token: impl Into<String>) -> Self {
        Self {
            refresh_token: refresh_token.into(),
        }
    }

    fn validate(&self) -> Result<(), DiscordError> {
        if self.refresh_token.trim().is_empty() {
            return Err(invalid_data_error("refresh_token must not be empty"));
        }
        Ok(())
    }
}

#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
/// Typed Discord API object for `OAuth2TokenResponse`.
pub struct OAuth2TokenResponse {
    /// Discord API payload field `access_token`.
    pub access_token: String,
    /// Discord API payload field `token_type`.
    pub token_type: String,
    /// Discord API payload field `expires_in`.
    pub expires_in: u64,
    #[serde(default)]
    /// Discord API payload field `refresh_token`.
    pub refresh_token: Option<String>,
    #[serde(default)]
    /// Discord API payload field `scope`.
    pub scope: Option<String>,
}

impl fmt::Debug for OAuth2TokenResponse {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("OAuth2TokenResponse")
            .field("access_token", &"[redacted]")
            .field("token_type", &self.token_type)
            .field("expires_in", &self.expires_in)
            .field(
                "refresh_token",
                &self.refresh_token.as_ref().map(|_| "[redacted]"),
            )
            .field("scope", &self.scope)
            .finish()
    }
}

fn encode_form_pairs(pairs: &[(&str, &str)]) -> String {
    pairs
        .iter()
        .map(|(key, value)| format!("{}={}", percent_encode(key), percent_encode(value)))
        .collect::<Vec<_>>()
        .join("&")
}

fn encode_owned_form_pairs(pairs: &[(&str, String)]) -> String {
    pairs
        .iter()
        .map(|(key, value)| format!("{}={}", percent_encode(key), percent_encode(value)))
        .collect::<Vec<_>>()
        .join("&")
}

fn percent_encode(value: &str) -> String {
    let mut encoded = String::new();
    for byte in value.bytes() {
        match byte {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
                encoded.push(byte as char)
            }
            b' ' => encoded.push('+'),
            byte => encoded.push_str(&format!("%{byte:02X}")),
        }
    }
    encoded
}

#[cfg(test)]
mod tests {
    use super::{
        OAuth2AuthorizationRequest, OAuth2Client, OAuth2CodeExchange, OAuth2RefreshToken,
        OAuth2Scope, OAuth2TokenResponse,
    };
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    use tokio::net::TcpListener;

    #[test]
    fn authorization_url_encodes_scope_state_and_prompt() {
        let client = OAuth2Client::new_with_base_url(
            "123",
            Some("secret".to_string()),
            "http://127.0.0.1/api",
            "https://discord.com/oauth2/authorize",
        );
        let url = client
            .authorization_url(
                OAuth2AuthorizationRequest::code(
                    "https://app.example/callback",
                    [OAuth2Scope::identify(), OAuth2Scope::guilds_join()],
                )
                .state("state with space")
                .prompt("consent")
                .integration_type(1),
            )
            .unwrap();

        assert!(url.contains("client_id=123"));
        assert!(url.contains("redirect_uri=https%3A%2F%2Fapp.example%2Fcallback"));
        assert!(url.contains("scope=identify+guilds.join"));
        assert!(url.contains("state=state+with+space"));
        assert!(url.contains("prompt=consent"));
        assert!(url.contains("integration_type=1"));
    }

    #[test]
    fn authorization_url_rejects_missing_scope() {
        let client = OAuth2Client::public_client("123");
        let error = client
            .authorization_url(OAuth2AuthorizationRequest::code(
                "https://app.example/callback",
                [],
            ))
            .unwrap_err();

        assert!(error.to_string().contains("at least one OAuth2 scope"));
    }

    #[test]
    fn authorization_url_rejects_blank_redirect_and_scope_helpers_are_stable() {
        let client = OAuth2Client::public_client("123");
        let error = client
            .authorization_url(OAuth2AuthorizationRequest::code(
                "   ",
                [OAuth2Scope::identify()],
            ))
            .unwrap_err();
        assert!(error.to_string().contains("redirect_uri must not be empty"));

        let scopes = [
            OAuth2Scope::email(),
            OAuth2Scope::guilds(),
            OAuth2Scope::applications_commands_update(),
            OAuth2Scope::role_connections_write(),
        ];
        assert_eq!(
            scopes.map(|scope| scope.as_str().to_string()),
            [
                "email".to_string(),
                "guilds".to_string(),
                "applications.commands.update".to_string(),
                "role_connections.write".to_string(),
            ]
        );
    }

    #[tokio::test]
    async fn token_exchange_sends_form_without_bot_authorization() {
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
        let port = listener.local_addr().unwrap().port();
        let server = tokio::spawn(async move {
            let (mut stream, _) = listener.accept().await.unwrap();
            let mut buffer = vec![0_u8; 4096];
            let received = stream.read(&mut buffer).await.unwrap();
            let request = String::from_utf8_lossy(&buffer[..received]);

            assert!(request.starts_with("POST /oauth2/token HTTP/1.1"));
            assert!(request.contains("content-type: application/x-www-form-urlencoded"));
            assert!(!request.to_ascii_lowercase().contains("authorization: bot"));
            assert!(request.contains(
                "grant_type=authorization_code&code=abc&redirect_uri=https%3A%2F%2Fapp.example%2Fcallback&client_id=123&client_secret=secret"
            ));

            let body = r#"{"access_token":"access","token_type":"Bearer","expires_in":3600,"refresh_token":"refresh","scope":"identify"}"#;
            let response = format!(
                "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
                body.len(),
                body
            );
            stream.write_all(response.as_bytes()).await.unwrap();
        });

        let client = OAuth2Client::new_with_base_url(
            "123",
            Some("secret".to_string()),
            format!("http://127.0.0.1:{port}"),
            "https://discord.com/oauth2/authorize",
        );
        let token = client
            .exchange_code(OAuth2CodeExchange::new(
                "abc",
                "https://app.example/callback",
            ))
            .await
            .unwrap();

        assert_eq!(token.access_token, "access");
        assert_eq!(token.refresh_token.as_deref(), Some("refresh"));
        server.await.unwrap();
    }

    #[tokio::test]
    async fn refresh_token_sends_form_and_surfaces_error_bodies() {
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
        let port = listener.local_addr().unwrap().port();
        let server = tokio::spawn(async move {
            let (mut stream, _) = listener.accept().await.unwrap();
            let mut buffer = vec![0_u8; 4096];
            let received = stream.read(&mut buffer).await.unwrap();
            let request = String::from_utf8_lossy(&buffer[..received]);

            assert!(request.starts_with("POST /oauth2/token HTTP/1.1"));
            assert!(request
                .contains("grant_type=refresh_token&refresh_token=refresh+token&client_id=123"));

            let body = r#"{"error":"invalid_grant"}"#;
            let response = format!(
                "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
                body.len(),
                body
            );
            stream.write_all(response.as_bytes()).await.unwrap();
        });

        let client = OAuth2Client::new_with_base_url(
            "123",
            None,
            format!("http://127.0.0.1:{port}"),
            "https://discord.com/oauth2/authorize",
        );
        let error = client
            .refresh_token(OAuth2RefreshToken::new("refresh token"))
            .await
            .unwrap_err();

        assert!(error.to_string().contains("400 Bad Request"));
        assert!(error.to_string().contains("invalid_grant"));
        server.await.unwrap();
    }

    #[test]
    fn token_requests_validate_required_fields() {
        assert!(OAuth2CodeExchange::new("", "https://app.example/callback")
            .validate()
            .unwrap_err()
            .to_string()
            .contains("code must not be empty"));
        assert!(OAuth2CodeExchange::new("abc", "")
            .validate()
            .unwrap_err()
            .to_string()
            .contains("redirect_uri must not be empty"));
        assert!(OAuth2RefreshToken::new("")
            .validate()
            .unwrap_err()
            .to_string()
            .contains("refresh_token must not be empty"));
    }

    #[test]
    fn oauth2_debug_redacts_credentials() {
        let client = OAuth2Client::new("client-id", "SECRET_CLIENT_SENTINEL");
        let code = OAuth2CodeExchange::new("SECRET_CODE_SENTINEL", "https://app.example/callback");
        let refresh = OAuth2RefreshToken::new("SECRET_REFRESH_SENTINEL");
        let token = OAuth2TokenResponse {
            access_token: "SECRET_ACCESS_SENTINEL".to_string(),
            token_type: "Bearer".to_string(),
            expires_in: 3600,
            refresh_token: Some("SECRET_RESPONSE_REFRESH_SENTINEL".to_string()),
            scope: Some("identify".to_string()),
        };

        let debug = format!("{client:?}\n{code:?}\n{refresh:?}\n{token:?}");
        assert!(debug.contains("[redacted]"));
        assert!(debug.contains("client-id"));
        assert!(debug.contains("https://app.example/callback"));
        assert!(!debug.contains("SECRET_CLIENT_SENTINEL"));
        assert!(!debug.contains("SECRET_CODE_SENTINEL"));
        assert!(!debug.contains("SECRET_REFRESH_SENTINEL"));
        assert!(!debug.contains("SECRET_ACCESS_SENTINEL"));
        assert!(!debug.contains("SECRET_RESPONSE_REFRESH_SENTINEL"));
    }
}
