use serde_json::Value;
use std::collections::HashMap;

/// Strategy for storing OAuth state during the authorization flow.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum OAuthStateStrategy {
    /// Stateless: store state in an encrypted cookie (default).
    #[default]
    Cookie,
    /// Stateful: store state in the verification table.
    Database,
}

/// Configuration for the OAuth plugin, containing all registered providers.
#[derive(Debug, Clone, Default)]
pub struct OAuthConfig {
    pub providers: HashMap<String, OAuthProvider>,
    /// Skip state cookie verification (default: false) - SECURITY WARNING
    pub skip_state_cookie_check: bool,
    /// Where to store OAuth state: Cookie (stateless) or Database (default: Cookie)
    pub store_state_strategy: OAuthStateStrategy,
}

/// User information extracted from an OAuth provider's user info endpoint.
#[derive(Debug, Clone)]
pub struct OAuthUserInfo {
    pub id: String,
    pub email: String,
    pub name: Option<String>,
    pub image: Option<String>,
    pub email_verified: bool,
}

/// Configuration for a single OAuth provider.
#[derive(Debug, Clone)]
pub struct OAuthProvider {
    pub client_id: String,
    pub client_secret: String,
    pub auth_url: String,
    pub token_url: String,
    pub user_info_url: String,
    pub scopes: Vec<String>,
    pub map_user_info: fn(Value) -> Result<OAuthUserInfo, String>,
    /// Additional query parameters appended to the authorization URL verbatim.
    /// Use this for provider-specific parameters such as `response_mode`, `prompt`, etc.
    pub extra_auth_params: Vec<(String, String)>,
}

impl OAuthProvider {
    pub fn google(client_id: &str, client_secret: &str) -> Self {
        Self {
            client_id: client_id.to_string(),
            client_secret: client_secret.to_string(),
            auth_url: "https://accounts.google.com/o/oauth2/v2/auth".to_string(),
            token_url: "https://oauth2.googleapis.com/token".to_string(),
            user_info_url: "https://www.googleapis.com/oauth2/v3/userinfo".to_string(),
            scopes: vec![
                "openid".to_string(),
                "email".to_string(),
                "profile".to_string(),
            ],
            map_user_info: |v| {
                Ok(OAuthUserInfo {
                    id: v["sub"].as_str().ok_or("missing sub")?.to_string(),
                    email: v["email"].as_str().ok_or("missing email")?.to_string(),
                    name: v["name"].as_str().map(String::from),
                    image: v["picture"].as_str().map(String::from),
                    email_verified: v["email_verified"].as_bool().unwrap_or(false),
                })
            },
            extra_auth_params: vec![],
        }
    }

    pub fn github(client_id: &str, client_secret: &str) -> Self {
        Self {
            client_id: client_id.to_string(),
            client_secret: client_secret.to_string(),
            auth_url: "https://github.com/login/oauth/authorize".to_string(),
            token_url: "https://github.com/login/oauth/access_token".to_string(),
            user_info_url: "https://api.github.com/user".to_string(),
            scopes: vec!["user:email".to_string()],
            map_user_info: |v| {
                Ok(OAuthUserInfo {
                    id: v["id"]
                        .as_i64()
                        .map(|i| i.to_string())
                        .or_else(|| v["id"].as_str().map(String::from))
                        .ok_or("missing id")?,
                    email: v["email"].as_str().unwrap_or("").to_string(),
                    name: v["name"].as_str().map(String::from),
                    image: v["avatar_url"].as_str().map(String::from),
                    email_verified: true,
                })
            },
            extra_auth_params: vec![],
        }
    }

    pub fn apple(client_id: &str, client_secret: &str) -> Self {
        Self {
            client_id: client_id.to_string(),
            client_secret: client_secret.to_string(),
            auth_url: "https://appleid.apple.com/auth/authorize".to_string(),
            token_url: "https://appleid.apple.com/auth/token".to_string(),
            // Apple's OIDC userinfo endpoint returns the same claims as the id_token.
            // Name is only included on the very first authorization; subsequent sign-ins
            // will not return a name from this endpoint.
            user_info_url: "https://appleid.apple.com/auth/userinfo".to_string(),
            scopes: vec![
                "openid".to_string(),
                "email".to_string(),
                "name".to_string(),
            ],
            map_user_info: |v| {
                let name = v["name"].as_str().map(String::from).or_else(|| {
                    let given = v["given_name"].as_str().unwrap_or("").trim();
                    let family = v["family_name"].as_str().unwrap_or("").trim();
                    let full = format!("{given} {family}").trim().to_string();
                    if full.is_empty() { None } else { Some(full) }
                });

                let email_verified = v["email_verified"]
                    .as_bool()
                    .or_else(|| {
                        v["email_verified"]
                            .as_str()
                            .map(|flag| flag.eq_ignore_ascii_case("true"))
                    })
                    .unwrap_or(false);

                Ok(OAuthUserInfo {
                    id: v["sub"].as_str().ok_or("missing sub")?.to_string(),
                    email: v["email"].as_str().ok_or("missing email")?.to_string(),
                    name,
                    image: None,
                    email_verified,
                })
            },
            // Apple defaults to response_mode=form_post for web flows, which sends the
            // callback as a POST request. Using response_mode=query keeps the callback
            // as a standard GET redirect that the existing /callback/:provider handler
            // can process without modification.
            extra_auth_params: vec![("response_mode".to_string(), "query".to_string())],
        }
    }

    pub fn microsoft(client_id: &str, client_secret: &str) -> Self {
        Self {
            client_id: client_id.to_string(),
            client_secret: client_secret.to_string(),
            auth_url: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize".to_string(),
            token_url: "https://login.microsoftonline.com/common/oauth2/v2.0/token".to_string(),
            user_info_url: "https://graph.microsoft.com/v1.0/me".to_string(),
            scopes: vec![
                "openid".to_string(),
                "profile".to_string(),
                "email".to_string(),
                "offline_access".to_string(),
                "User.Read".to_string(),
            ],
            map_user_info: |v| {
                let name = v["displayName"].as_str().map(String::from).or_else(|| {
                    let given = v["givenName"].as_str().unwrap_or("").trim();
                    let surname = v["surname"].as_str().unwrap_or("").trim();
                    let full = format!("{given} {surname}").trim().to_string();
                    if full.is_empty() { None } else { Some(full) }
                });

                Ok(OAuthUserInfo {
                    id: v["id"].as_str().ok_or("missing id")?.to_string(),
                    email: v["mail"]
                        .as_str()
                        .or_else(|| v["userPrincipalName"].as_str())
                        .ok_or("missing mail or userPrincipalName")?
                        .to_string(),
                    name,
                    image: None,
                    email_verified: true,
                })
            },
            extra_auth_params: vec![],
        }
    }

    pub fn discord(client_id: &str, client_secret: &str) -> Self {
        Self {
            client_id: client_id.to_string(),
            client_secret: client_secret.to_string(),
            auth_url: "https://discord.com/api/oauth2/authorize".to_string(),
            token_url: "https://discord.com/api/oauth2/token".to_string(),
            user_info_url: "https://discord.com/api/users/@me".to_string(),
            scopes: vec!["identify".to_string(), "email".to_string()],
            map_user_info: |v| {
                Ok(OAuthUserInfo {
                    id: v["id"].as_str().ok_or("missing id")?.to_string(),
                    email: v["email"].as_str().ok_or("missing email")?.to_string(),
                    name: v["username"].as_str().map(String::from),
                    image: v["avatar"].as_str().map(|a| {
                        format!(
                            "https://cdn.discordapp.com/avatars/{}/{}.png",
                            v["id"].as_str().unwrap_or(""),
                            a
                        )
                    }),
                    email_verified: v["verified"].as_bool().unwrap_or(false),
                })
            },
            extra_auth_params: vec![],
        }
    }

    pub fn cloudflare(client_id: &str, client_secret: &str) -> Self {
        Self {
            client_id: client_id.to_string(),
            client_secret: client_secret.to_string(),
            auth_url: "https://dash.cloudflare.com/oauth2/auth".to_string(),
            token_url: "https://dash.cloudflare.com/oauth2/token".to_string(),
            user_info_url: "https://dash.cloudflare.com/oauth2/userinfo".to_string(),
            scopes: vec![
                "openid".to_string(),
                "email".to_string(),
                "profile".to_string(),
            ],
            map_user_info: |v| {
                Ok(OAuthUserInfo {
                    id: v["sub"].as_str().ok_or("missing sub")?.to_string(),
                    email: v["email"].as_str().ok_or("missing email")?.to_string(),
                    name: v["name"]
                        .as_str()
                        .map(String::from)
                        .or_else(|| v["preferred_username"].as_str().map(String::from)),
                    image: v["picture"].as_str().map(String::from),
                    email_verified: v["email_verified"].as_bool().unwrap_or(false),
                })
            },
            extra_auth_params: vec![],
        }
    }
}

#[cfg(test)]
mod tests {
    use super::OAuthProvider;
    use serde_json::json;

    #[test]
    fn apple_maps_string_email_verified_flag() {
        let provider = OAuthProvider::apple("apple-client-id", "apple-client-secret");
        let payload = json!({
            "sub": "apple-sub",
            "email": "user@example.com",
            "given_name": "Ava",
            "family_name": "Stone",
            "email_verified": "true",
        });

        let info = (provider.map_user_info)(payload).expect("apple payload should map");
        assert_eq!(info.id, "apple-sub");
        assert_eq!(info.email, "user@example.com");
        assert_eq!(info.name.as_deref(), Some("Ava Stone"));
        assert!(info.email_verified);
    }

    #[test]
    fn microsoft_uses_user_principal_name_when_mail_missing() {
        let provider = OAuthProvider::microsoft("microsoft-client-id", "microsoft-client-secret");
        let payload = json!({
            "id": "microsoft-user-id",
            "displayName": "Sam Carter",
            "userPrincipalName": "sam@contoso.com",
        });

        let info = (provider.map_user_info)(payload).expect("microsoft payload should map");
        assert_eq!(info.id, "microsoft-user-id");
        assert_eq!(info.email, "sam@contoso.com");
        assert_eq!(info.name.as_deref(), Some("Sam Carter"));
        assert!(info.email_verified);
    }

    #[test]
    fn github_allows_null_email_in_user_payload() {
        let provider = OAuthProvider::github("github-client-id", "github-client-secret");
        let payload = json!({
            "id": 42,
            "email": null,
            "name": "Octocat",
            "avatar_url": "https://avatars.githubusercontent.com/u/42",
        });

        let info = (provider.map_user_info)(payload).expect("github payload should map");
        assert_eq!(info.id, "42");
        assert_eq!(info.email, "");
        assert_eq!(info.name.as_deref(), Some("Octocat"));
    }

    #[test]
    fn cloudflare_maps_standard_oidc_claims() {
        let provider =
            OAuthProvider::cloudflare("cloudflare-client-id", "cloudflare-client-secret");
        let payload = json!({
            "sub": "cloudflare-user-id",
            "email": "user@example.com",
            "name": "Casey Cloudflare",
            "picture": "https://example.com/avatar.png",
            "email_verified": true,
        });

        let info = (provider.map_user_info)(payload).expect("cloudflare payload should map");
        assert_eq!(info.id, "cloudflare-user-id");
        assert_eq!(info.email, "user@example.com");
        assert_eq!(info.name.as_deref(), Some("Casey Cloudflare"));
        assert_eq!(
            info.image.as_deref(),
            Some("https://example.com/avatar.png")
        );
        assert!(info.email_verified);
    }
}
