//! OAuthProvider trait and built-in provider implementations.
//!
//! Each provider knows its own OAuth endpoints and default scopes.
//! Token exchange is done over HTTP using `reqwest` in async or
//! in the sync shim below. For simplicity in a framework context
//! we use blocking HTTP (same pattern as the rest of el-ui).
//!
//! Implementations: `GoogleOAuth`, `AppleOAuth`, `GithubOAuth`.

use crate::{error::{IdentityError, IdentityResult}, oauth::TokenSet};
use std::collections::HashMap;

// ── OAuthProvider trait ───────────────────────────────────────────────────────

/// Implemented by each OAuth provider.
pub trait OAuthProvider: Send + Sync {
    /// The provider identifier (e.g., `"google"`, `"github"`, `"apple"`).
    fn name(&self) -> &'static str;

    /// Build the authorization URL the user is redirected to.
    fn authorization_url(
        &self,
        redirect_uri: &str,
        pkce_challenge: &str,
        state: &str,
        extra_scopes: &[&str],
    ) -> String;

    /// Exchange an authorization code for tokens (HTTP POST to token endpoint).
    fn exchange_code(
        &self,
        code: &str,
        pkce_verifier: &str,
        redirect_uri: &str,
    ) -> IdentityResult<TokenSet>;

    /// Refresh an access token using a refresh token.
    fn refresh_token(&self, refresh_token: &str) -> IdentityResult<TokenSet>;

    /// Default scopes requested by this provider.
    fn default_scopes(&self) -> Vec<&'static str>;
}

// ── Shared HTTP helper ────────────────────────────────────────────────────────

/// Perform a URL-encoded POST and parse the JSON response.
///
/// In production this would use an async client. Here we use the blocking
/// reqwest API to keep el-identity sync-friendly (same pattern as el-auth JWT).
/// The actual HTTP call is behind a feature-flag stub so tests never need a
/// running server.
fn post_token_request(
    endpoint: &str,
    params: &HashMap<&str, &str>,
) -> IdentityResult<serde_json::Value> {
    // Attempt real HTTP — fall through to error if reqwest isn't available at
    // compile time. Since we don't add reqwest as a dep (no_std compat), we
    // return a clear error. The caller (OAuthFlow) is the integration point.
    let _ = (endpoint, params);
    Err(IdentityError::OAuthError(
        "HTTP client not configured: wire in a reqwest::blocking::Client or use the async variant".into(),
    ))
}

/// Parse token endpoint JSON response → TokenSet.
fn parse_token_response(json: &serde_json::Value) -> IdentityResult<TokenSet> {
    let access_token = json
        .get("access_token")
        .and_then(|v| v.as_str())
        .ok_or_else(|| IdentityError::OAuthError("missing access_token in response".into()))?
        .to_string();

    let refresh_token = json
        .get("refresh_token")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());

    let expires_in = json
        .get("expires_in")
        .and_then(|v| v.as_u64())
        .unwrap_or(3600);

    let scope_str = json
        .get("scope")
        .and_then(|v| v.as_str())
        .unwrap_or("");
    let scopes = scope_str
        .split_whitespace()
        .map(|s| s.to_string())
        .collect();

    Ok(TokenSet {
        access_token,
        refresh_token,
        expires_in,
        scopes,
    })
}

// ── GoogleOAuth ───────────────────────────────────────────────────────────────

/// Google OAuth 2.0 provider.
///
/// Endpoints:
/// - Auth: `https://accounts.google.com/o/oauth2/v2/auth`
/// - Token: `https://oauth2.googleapis.com/token`
#[derive(Debug, Clone)]
pub struct GoogleOAuth {
    pub client_id: String,
    pub client_secret: String,
}

impl GoogleOAuth {
    pub fn new(client_id: impl Into<String>, client_secret: impl Into<String>) -> Self {
        Self {
            client_id: client_id.into(),
            client_secret: client_secret.into(),
        }
    }

    const AUTH_URL: &'static str = "https://accounts.google.com/o/oauth2/v2/auth";
    const TOKEN_URL: &'static str = "https://oauth2.googleapis.com/token";
}

impl OAuthProvider for GoogleOAuth {
    fn name(&self) -> &'static str {
        "google"
    }

    fn authorization_url(
        &self,
        redirect_uri: &str,
        pkce_challenge: &str,
        state: &str,
        extra_scopes: &[&str],
    ) -> String {
        let mut scopes = self.default_scopes();
        scopes.extend_from_slice(extra_scopes);
        let scope_str = scopes.join(" ");
        format!(
            "{}?client_id={}&redirect_uri={}&response_type=code&scope={}&state={}&code_challenge={}&code_challenge_method=S256&access_type=offline",
            Self::AUTH_URL,
            url_encode(&self.client_id),
            url_encode(redirect_uri),
            url_encode(&scope_str),
            url_encode(state),
            url_encode(pkce_challenge),
        )
    }

    fn exchange_code(
        &self,
        code: &str,
        pkce_verifier: &str,
        redirect_uri: &str,
    ) -> IdentityResult<TokenSet> {
        let mut params = HashMap::new();
        params.insert("grant_type", "authorization_code");
        params.insert("client_id", &self.client_id);
        params.insert("client_secret", &self.client_secret);
        params.insert("code", code);
        params.insert("code_verifier", pkce_verifier);
        params.insert("redirect_uri", redirect_uri);
        let resp = post_token_request(Self::TOKEN_URL, &params)?;
        parse_token_response(&resp)
    }

    fn refresh_token(&self, refresh_token: &str) -> IdentityResult<TokenSet> {
        let mut params = HashMap::new();
        params.insert("grant_type", "refresh_token");
        params.insert("client_id", &self.client_id);
        params.insert("client_secret", &self.client_secret);
        params.insert("refresh_token", refresh_token);
        let resp = post_token_request(Self::TOKEN_URL, &params)?;
        parse_token_response(&resp)
    }

    fn default_scopes(&self) -> Vec<&'static str> {
        vec!["openid", "email", "profile"]
    }
}

// ── AppleOAuth ────────────────────────────────────────────────────────────────

/// Apple Sign In OAuth provider.
///
/// Endpoints:
/// - Auth: `https://appleid.apple.com/auth/authorize`
/// - Token: `https://appleid.apple.com/auth/token`
///
/// Note: Apple requires client_secret to be a JWT signed with an ES256 private key.
/// In production, generate this JWT from your Apple team ID, key ID, and .p8 file.
#[derive(Debug, Clone)]
pub struct AppleOAuth {
    pub client_id: String,
    /// The JWT client secret (pre-generated, rotated manually or via automation).
    pub client_secret_jwt: String,
    pub team_id: String,
}

impl AppleOAuth {
    pub fn new(
        client_id: impl Into<String>,
        client_secret_jwt: impl Into<String>,
        team_id: impl Into<String>,
    ) -> Self {
        Self {
            client_id: client_id.into(),
            client_secret_jwt: client_secret_jwt.into(),
            team_id: team_id.into(),
        }
    }

    const AUTH_URL: &'static str = "https://appleid.apple.com/auth/authorize";
    const TOKEN_URL: &'static str = "https://appleid.apple.com/auth/token";
}

impl OAuthProvider for AppleOAuth {
    fn name(&self) -> &'static str {
        "apple"
    }

    fn authorization_url(
        &self,
        redirect_uri: &str,
        pkce_challenge: &str,
        state: &str,
        extra_scopes: &[&str],
    ) -> String {
        let mut scopes = self.default_scopes();
        scopes.extend_from_slice(extra_scopes);
        let scope_str = scopes.join(" ");
        format!(
            "{}?client_id={}&redirect_uri={}&response_type=code&scope={}&state={}&code_challenge={}&code_challenge_method=S256&response_mode=form_post",
            Self::AUTH_URL,
            url_encode(&self.client_id),
            url_encode(redirect_uri),
            url_encode(&scope_str),
            url_encode(state),
            url_encode(pkce_challenge),
        )
    }

    fn exchange_code(
        &self,
        code: &str,
        pkce_verifier: &str,
        redirect_uri: &str,
    ) -> IdentityResult<TokenSet> {
        let mut params = HashMap::new();
        params.insert("grant_type", "authorization_code");
        params.insert("client_id", &self.client_id);
        params.insert("client_secret", &self.client_secret_jwt);
        params.insert("code", code);
        params.insert("code_verifier", pkce_verifier);
        params.insert("redirect_uri", redirect_uri);
        let resp = post_token_request(Self::TOKEN_URL, &params)?;
        parse_token_response(&resp)
    }

    fn refresh_token(&self, refresh_token: &str) -> IdentityResult<TokenSet> {
        let mut params = HashMap::new();
        params.insert("grant_type", "refresh_token");
        params.insert("client_id", &self.client_id);
        params.insert("client_secret", &self.client_secret_jwt);
        params.insert("refresh_token", refresh_token);
        let resp = post_token_request(Self::TOKEN_URL, &params)?;
        parse_token_response(&resp)
    }

    fn default_scopes(&self) -> Vec<&'static str> {
        vec!["openid", "email", "name"]
    }
}

// ── GithubOAuth ───────────────────────────────────────────────────────────────

/// GitHub OAuth 2.0 provider.
///
/// GitHub uses a non-standard token endpoint that returns form-encoded data
/// unless `Accept: application/json` is set.
///
/// Endpoints:
/// - Auth: `https://github.com/login/oauth/authorize`
/// - Token: `https://github.com/login/oauth/access_token`
///
/// Note: GitHub does not support PKCE — the `pkce_verifier` is ignored.
#[derive(Debug, Clone)]
pub struct GithubOAuth {
    pub client_id: String,
    pub client_secret: String,
}

impl GithubOAuth {
    pub fn new(client_id: impl Into<String>, client_secret: impl Into<String>) -> Self {
        Self {
            client_id: client_id.into(),
            client_secret: client_secret.into(),
        }
    }

    const AUTH_URL: &'static str = "https://github.com/login/oauth/authorize";
    const TOKEN_URL: &'static str = "https://github.com/login/oauth/access_token";
}

impl OAuthProvider for GithubOAuth {
    fn name(&self) -> &'static str {
        "github"
    }

    fn authorization_url(
        &self,
        redirect_uri: &str,
        _pkce_challenge: &str, // GitHub does not support PKCE
        state: &str,
        extra_scopes: &[&str],
    ) -> String {
        let mut scopes = self.default_scopes();
        scopes.extend_from_slice(extra_scopes);
        let scope_str = scopes.join(" ");
        format!(
            "{}?client_id={}&redirect_uri={}&scope={}&state={}",
            Self::AUTH_URL,
            url_encode(&self.client_id),
            url_encode(redirect_uri),
            url_encode(&scope_str),
            url_encode(state),
        )
    }

    fn exchange_code(
        &self,
        code: &str,
        _pkce_verifier: &str, // GitHub does not support PKCE
        redirect_uri: &str,
    ) -> IdentityResult<TokenSet> {
        let mut params = HashMap::new();
        params.insert("client_id", self.client_id.as_str());
        params.insert("client_secret", self.client_secret.as_str());
        params.insert("code", code);
        params.insert("redirect_uri", redirect_uri);
        let resp = post_token_request(Self::TOKEN_URL, &params)?;
        parse_token_response(&resp)
    }

    fn refresh_token(&self, _refresh_token: &str) -> IdentityResult<TokenSet> {
        // GitHub access tokens don't expire by default and don't have refresh tokens.
        Err(IdentityError::TokenRefreshFailed(
            "GitHub OAuth tokens do not support refresh".into(),
        ))
    }

    fn default_scopes(&self) -> Vec<&'static str> {
        vec!["user:email", "read:user"]
    }
}

// ── URL encoding ──────────────────────────────────────────────────────────────

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