use std::env;

use anyhow::{anyhow, Result};
use oauth2::{
    basic::BasicClient, basic::BasicErrorResponseType, basic::BasicTokenType, AuthUrl,
    AuthorizationCode, Client, ClientId, ClientSecret, CsrfToken, EmptyExtraTokenFields,
    EndpointNotSet, EndpointSet, PkceCodeChallenge, RedirectUrl, RevocationErrorResponseType,
    Scope, StandardErrorResponse, StandardRevocableToken, StandardTokenIntrospectionResponse,
    StandardTokenResponse, TokenResponse, TokenUrl,
};
use rustelo_core_lib::auth::{AuthError, OAuthProvider, OAuthUserInfo};
use serde::{Deserialize, Serialize};

// Type alias for fully configured OAuth2 client
type ConfiguredOAuthClient = Client<
    StandardErrorResponse<BasicErrorResponseType>,
    StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>,
    StandardTokenIntrospectionResponse<EmptyExtraTokenFields, BasicTokenType>,
    StandardRevocableToken,
    StandardErrorResponse<RevocationErrorResponseType>,
    EndpointSet,
    EndpointNotSet,
    EndpointNotSet,
    EndpointNotSet,
    EndpointSet,
>;

#[derive(Debug, Clone)]
pub struct OAuthService {
    base_redirect_url: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct OAuthAuthorizationUrl {
    pub authorization_url: String,
    pub state: String,
    pub pkce_verifier: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct OAuthCallback {
    pub code: String,
    pub state: String,
}

// Provider-specific user info structures
#[derive(Debug, Clone, Serialize, Deserialize)]
struct GoogleUserInfo {
    pub id: String,
    pub email: String,
    pub name: Option<String>,
    pub picture: Option<String>,
    pub given_name: Option<String>,
    pub family_name: Option<String>,
    pub locale: Option<String>,
    pub verified_email: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct GitHubUserInfo {
    pub id: u64,
    pub login: String,
    pub name: Option<String>,
    pub email: Option<String>,
    pub avatar_url: Option<String>,
    pub bio: Option<String>,
    pub location: Option<String>,
    pub company: Option<String>,
    pub blog: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct DiscordUserInfo {
    pub id: String,
    pub username: String,
    pub discriminator: String,
    pub avatar: Option<String>,
    pub email: Option<String>,
    pub verified: Option<bool>,
    pub locale: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct MicrosoftUserInfo {
    pub id: String,
    pub mail: Option<String>,
    pub user_principal_name: Option<String>,
    pub display_name: Option<String>,
    pub given_name: Option<String>,
    pub surname: Option<String>,
    pub job_title: Option<String>,
    pub mobile_phone: Option<String>,
    pub preferred_language: Option<String>,
}

impl OAuthService {
    pub fn new() -> Result<Self> {
        let base_redirect_url = env::var("OAUTH_REDIRECT_BASE_URL")
            .unwrap_or_else(|_| "http://localhost:3030/auth/callback".to_string());

        Ok(Self { base_redirect_url })
    }

    fn create_client(&self, provider: &OAuthProvider) -> Result<ConfiguredOAuthClient> {
        match provider {
            OAuthProvider::Google => {
                let client_id = env::var("GOOGLE_CLIENT_ID").map_err(|_| {
                    anyhow!("Google OAuth not configured: missing GOOGLE_CLIENT_ID")
                })?;
                let client_secret = env::var("GOOGLE_CLIENT_SECRET").map_err(|_| {
                    anyhow!("Google OAuth not configured: missing GOOGLE_CLIENT_SECRET")
                })?;

                let auth_url =
                    AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string())?;
                let token_url = TokenUrl::new("https://oauth2.googleapis.com/token".to_string())?;
                let redirect_url = RedirectUrl::new(format!("{}/google", self.base_redirect_url))?;

                Ok(BasicClient::new(ClientId::new(client_id))
                    .set_client_secret(ClientSecret::new(client_secret))
                    .set_auth_uri(auth_url)
                    .set_token_uri(token_url)
                    .set_redirect_uri(redirect_url))
            }
            OAuthProvider::GitHub => {
                let client_id = env::var("GITHUB_CLIENT_ID").map_err(|_| {
                    anyhow!("GitHub OAuth not configured: missing GITHUB_CLIENT_ID")
                })?;
                let client_secret = env::var("GITHUB_CLIENT_SECRET").map_err(|_| {
                    anyhow!("GitHub OAuth not configured: missing GITHUB_CLIENT_SECRET")
                })?;

                let auth_url =
                    AuthUrl::new("https://github.com/login/oauth/authorize".to_string())?;
                let token_url =
                    TokenUrl::new("https://github.com/login/oauth/access_token".to_string())?;
                let redirect_url = RedirectUrl::new(format!("{}/github", self.base_redirect_url))?;

                Ok(BasicClient::new(ClientId::new(client_id))
                    .set_client_secret(ClientSecret::new(client_secret))
                    .set_auth_uri(auth_url)
                    .set_token_uri(token_url)
                    .set_redirect_uri(redirect_url))
            }
            OAuthProvider::Discord => {
                let client_id = env::var("DISCORD_CLIENT_ID").map_err(|_| {
                    anyhow!("Discord OAuth not configured: missing DISCORD_CLIENT_ID")
                })?;
                let client_secret = env::var("DISCORD_CLIENT_SECRET").map_err(|_| {
                    anyhow!("Discord OAuth not configured: missing DISCORD_CLIENT_SECRET")
                })?;

                let auth_url =
                    AuthUrl::new("https://discord.com/api/oauth2/authorize".to_string())?;
                let token_url = TokenUrl::new("https://discord.com/api/oauth2/token".to_string())?;
                let redirect_url = RedirectUrl::new(format!("{}/discord", self.base_redirect_url))?;

                Ok(BasicClient::new(ClientId::new(client_id))
                    .set_client_secret(ClientSecret::new(client_secret))
                    .set_auth_uri(auth_url)
                    .set_token_uri(token_url)
                    .set_redirect_uri(redirect_url))
            }
            OAuthProvider::Microsoft => {
                let client_id = env::var("MICROSOFT_CLIENT_ID").map_err(|_| {
                    anyhow!("Microsoft OAuth not configured: missing MICROSOFT_CLIENT_ID")
                })?;
                let client_secret = env::var("MICROSOFT_CLIENT_SECRET").map_err(|_| {
                    anyhow!("Microsoft OAuth not configured: missing MICROSOFT_CLIENT_SECRET")
                })?;

                let tenant_id =
                    env::var("MICROSOFT_TENANT_ID").unwrap_or_else(|_| "common".to_string());
                let auth_url = AuthUrl::new(format!(
                    "https://login.microsoftonline.com/{}/oauth2/v2.0/authorize",
                    tenant_id
                ))?;
                let token_url = TokenUrl::new(format!(
                    "https://login.microsoftonline.com/{}/oauth2/v2.0/token",
                    tenant_id
                ))?;
                let redirect_url =
                    RedirectUrl::new(format!("{}/microsoft", self.base_redirect_url))?;

                Ok(BasicClient::new(ClientId::new(client_id))
                    .set_client_secret(ClientSecret::new(client_secret))
                    .set_auth_uri(auth_url)
                    .set_token_uri(token_url)
                    .set_redirect_uri(redirect_url))
            }
            OAuthProvider::Custom(name) => {
                Err(anyhow!("Custom OAuth provider '{}' not supported", name))
            }
        }
    }

    pub fn get_authorization_url(&self, provider: &OAuthProvider) -> Result<OAuthAuthorizationUrl> {
        let client = self.create_client(provider)?;
        let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();

        let scopes = self.get_scopes(provider);
        let mut auth_request = client
            .authorize_url(CsrfToken::new_random)
            .set_pkce_challenge(pkce_challenge);

        for scope in scopes {
            auth_request = auth_request.add_scope(Scope::new(scope));
        }

        let (auth_url, csrf_token) = auth_request.url();

        Ok(OAuthAuthorizationUrl {
            authorization_url: auth_url.to_string(),
            state: csrf_token.secret().clone(),
            pkce_verifier: Some(pkce_verifier.secret().clone()),
        })
    }

    pub async fn handle_callback(
        &self,
        provider: &OAuthProvider,
        callback: OAuthCallback,
        pkce_verifier: Option<String>,
    ) -> Result<OAuthUserInfo> {
        let client = self.create_client(provider)?;

        // Create HTTP client
        let http_client = reqwest::ClientBuilder::new()
            .redirect(reqwest::redirect::Policy::none())
            .build()
            .map_err(|e| AuthError::OAuthError(format!("Failed to create HTTP client: {}", e)))?;

        // Concrete Sized error for the adapter — Box<dyn Error> is not Sized so cannot
        // satisfy the AsyncHttpClient bound `RE: Error + 'static`.
        #[derive(Debug)]
        struct HttpAdapterError(String);
        impl std::fmt::Display for HttpAdapterError {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                f.write_str(&self.0)
            }
        }
        impl std::error::Error for HttpAdapterError {}

        // Adapter closure: bridges oauth2 5.0 HttpRequest → reqwest 0.13 → oauth2
        // HttpResponse. Required because oauth2/reqwest feature targets reqwest
        // 0.12, not 0.13.
        let adapter = {
            let client = http_client.clone();
            move |request: oauth2::HttpRequest| {
                let c = client.clone();
                async move {
                    let (parts, body) = request.into_parts();
                    let resp = c
                        .request(parts.method, parts.uri.to_string())
                        .headers(parts.headers)
                        .body(body)
                        .send()
                        .await
                        .map_err(|e| HttpAdapterError(e.to_string()))?;
                    let status = resp.status();
                    let headers = resp.headers().clone();
                    let bytes = resp
                        .bytes()
                        .await
                        .map_err(|e| HttpAdapterError(e.to_string()))?
                        .to_vec();
                    let mut builder = axum::http::Response::builder().status(status);
                    for (name, value) in &headers {
                        builder = builder.header(name, value);
                    }
                    builder
                        .body(bytes)
                        .map_err(|e| HttpAdapterError(e.to_string()))
                }
            }
        };

        // Exchange authorization code for access token
        let mut token_request = client.exchange_code(AuthorizationCode::new(callback.code));

        if let Some(verifier) = pkce_verifier {
            token_request =
                token_request.set_pkce_verifier(oauth2::PkceCodeVerifier::new(verifier));
        }

        let token_response = token_request
            .request_async(&adapter)
            .await
            .map_err(|e| AuthError::OAuthError(format!("Token exchange failed: {}", e)))?;

        let access_token = token_response.access_token();

        // Fetch user info from provider
        let user_info = self
            .fetch_user_info(provider, access_token.secret())
            .await?;

        Ok(user_info)
    }

    async fn fetch_user_info(
        &self,
        provider: &OAuthProvider,
        access_token: &str,
    ) -> Result<OAuthUserInfo> {
        let client = reqwest::Client::new();

        match provider {
            OAuthProvider::Google => {
                let response = client
                    .get("https://www.googleapis.com/oauth2/v2/userinfo")
                    .bearer_auth(access_token)
                    .send()
                    .await?;

                let google_user: GoogleUserInfo = response.json().await?;
                let raw_data = serde_json::to_value(&google_user)?
                    .as_object()
                    .ok_or_else(|| {
                        AuthError::OAuthError("Failed to serialize Google user data".to_string())
                    })?
                    .iter()
                    .map(|(k, v)| (k.clone(), v.clone()))
                    .collect();

                Ok(OAuthUserInfo {
                    provider: provider.clone(),
                    provider_id: google_user.id,
                    email: google_user.email,
                    username: google_user.name.clone(),
                    display_name: google_user.name,
                    avatar_url: google_user.picture.clone(),
                    raw_data,
                })
            }
            OAuthProvider::GitHub => {
                let response = client
                    .get("https://api.github.com/user")
                    .bearer_auth(access_token)
                    .header("User-Agent", "rustelo-auth")
                    .send()
                    .await?;

                let github_user: GitHubUserInfo = response.json().await?;

                // GitHub might not return email in the main endpoint
                let email = if github_user.email.is_none() {
                    self.fetch_github_email(access_token).await?
                } else {
                    github_user.email.clone()
                };

                let raw_data = serde_json::to_value(&github_user)?
                    .as_object()
                    .ok_or_else(|| {
                        AuthError::OAuthError("Failed to serialize GitHub user data".to_string())
                    })?
                    .iter()
                    .map(|(k, v)| (k.clone(), v.clone()))
                    .collect();

                Ok(OAuthUserInfo {
                    provider: provider.clone(),
                    provider_id: github_user.id.to_string(),
                    email: email.unwrap_or_default(),
                    username: Some(github_user.login.clone()),
                    display_name: github_user.name,
                    avatar_url: github_user.avatar_url.clone(),
                    raw_data,
                })
            }
            OAuthProvider::Discord => {
                let response = client
                    .get("https://discord.com/api/users/@me")
                    .bearer_auth(access_token)
                    .send()
                    .await?;

                let discord_user: DiscordUserInfo = response.json().await?;
                let avatar_url = discord_user.avatar.as_ref().map(|hash| {
                    format!(
                        "https://cdn.discordapp.com/avatars/{}/{}.png",
                        discord_user.id, hash
                    )
                });

                let raw_data = serde_json::to_value(&discord_user)?
                    .as_object()
                    .ok_or_else(|| {
                        AuthError::OAuthError("Failed to serialize Discord user data".to_string())
                    })?
                    .iter()
                    .map(|(k, v)| (k.clone(), v.clone()))
                    .collect();

                Ok(OAuthUserInfo {
                    provider: provider.clone(),
                    provider_id: discord_user.id.clone(),
                    email: discord_user.email.unwrap_or_default(),
                    username: Some(format!(
                        "{}#{}",
                        discord_user.username, discord_user.discriminator
                    )),
                    display_name: Some(discord_user.username.clone()),
                    avatar_url,
                    raw_data,
                })
            }
            OAuthProvider::Microsoft => {
                let response = client
                    .get("https://graph.microsoft.com/v1.0/me")
                    .bearer_auth(access_token)
                    .send()
                    .await?;

                let microsoft_user: MicrosoftUserInfo = response.json().await?;
                let email = microsoft_user
                    .mail
                    .clone()
                    .or(microsoft_user.user_principal_name.clone());

                let raw_data = serde_json::to_value(&microsoft_user)?
                    .as_object()
                    .ok_or_else(|| {
                        AuthError::OAuthError("Failed to serialize Microsoft user data".to_string())
                    })?
                    .iter()
                    .map(|(k, v)| (k.clone(), v.clone()))
                    .collect();

                Ok(OAuthUserInfo {
                    provider: provider.clone(),
                    provider_id: microsoft_user.id,
                    email: email.unwrap_or_default(),
                    username: microsoft_user.display_name.clone(),
                    display_name: microsoft_user.display_name.clone(),
                    avatar_url: None, // Microsoft Graph doesn't provide avatar in basic profile
                    raw_data,
                })
            }
            OAuthProvider::Custom(name) => {
                Err(anyhow!("Custom OAuth provider '{}' not implemented", name))
            }
        }
    }

    async fn fetch_github_email(&self, access_token: &str) -> Result<Option<String>> {
        let client = reqwest::Client::new();
        let response = client
            .get("https://api.github.com/user/emails")
            .bearer_auth(access_token)
            .header("User-Agent", "rustelo-auth")
            .send()
            .await?;

        #[derive(Deserialize)]
        struct GitHubEmail {
            email: String,
            primary: bool,
            verified: bool,
        }

        let emails: Vec<GitHubEmail> = response.json().await?;

        // Find primary verified email
        let primary_email = emails
            .iter()
            .find(|e| e.primary && e.verified)
            .or_else(|| emails.first());

        Ok(primary_email.map(|e| e.email.clone()))
    }

    pub fn is_provider_configured(&self, provider: &OAuthProvider) -> bool {
        match provider {
            OAuthProvider::Google => {
                env::var("GOOGLE_CLIENT_ID").is_ok() && env::var("GOOGLE_CLIENT_SECRET").is_ok()
            }
            OAuthProvider::GitHub => {
                env::var("GITHUB_CLIENT_ID").is_ok() && env::var("GITHUB_CLIENT_SECRET").is_ok()
            }
            OAuthProvider::Discord => {
                env::var("DISCORD_CLIENT_ID").is_ok() && env::var("DISCORD_CLIENT_SECRET").is_ok()
            }
            OAuthProvider::Microsoft => {
                env::var("MICROSOFT_CLIENT_ID").is_ok()
                    && env::var("MICROSOFT_CLIENT_SECRET").is_ok()
            }
            OAuthProvider::Custom(_) => false,
        }
    }

    fn get_scopes(&self, provider: &OAuthProvider) -> Vec<String> {
        match provider {
            OAuthProvider::Google => vec!["profile".to_string(), "email".to_string()],
            OAuthProvider::GitHub => vec!["user:email".to_string()],
            OAuthProvider::Discord => vec!["identify".to_string(), "email".to_string()],
            OAuthProvider::Microsoft => vec![
                "profile".to_string(),
                "email".to_string(),
                "User.Read".to_string(),
            ],
            OAuthProvider::Custom(_) => vec![],
        }
    }

    pub fn get_configured_providers(&self) -> Vec<OAuthProvider> {
        let mut providers = Vec::new();

        if self.is_provider_configured(&OAuthProvider::Google) {
            providers.push(OAuthProvider::Google);
        }
        if self.is_provider_configured(&OAuthProvider::GitHub) {
            providers.push(OAuthProvider::GitHub);
        }
        if self.is_provider_configured(&OAuthProvider::Discord) {
            providers.push(OAuthProvider::Discord);
        }
        if self.is_provider_configured(&OAuthProvider::Microsoft) {
            providers.push(OAuthProvider::Microsoft);
        }

        providers
    }
}

impl Default for OAuthService {
    fn default() -> Self {
        Self::new().expect("Failed to create OAuth service")
    }
}
