//! OAuth Client
//!
//! Main OAuth client implementation with PKCE support.

use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;

use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use parking_lot::RwLock;
use rand::RngExt;
use reqwest::Client;
use serde::Deserialize;
use sha2::{Digest, Sha256};
use tokio::sync::Mutex as TokioMutex;
use tracing::{debug, info, warn};
use url::Url;

use super::callback;
use super::metadata::{self, AuthorizationServerMetadata, ProtectedResourceMetadata};
use super::storage::{TokenInfo, TokenStorage};
use crate::{Error, Result};

/// OAuth client for a specific backend
pub struct OAuthClient {
    /// HTTP client for token requests
    http_client: Client,

    /// Backend name (for storage key)
    backend_name: String,

    /// Resource URL (MCP endpoint)
    resource_url: String,

    /// OAuth server base URL (discovered from metadata)
    oauth_base_url: Option<String>,

    /// Authorization server metadata
    auth_metadata: Option<AuthorizationServerMetadata>,

    /// Protected resource metadata
    resource_metadata: Option<ProtectedResourceMetadata>,

    /// Token storage
    storage: Arc<TokenStorage>,

    /// Current token (cached)
    current_token: RwLock<Option<TokenInfo>>,

    /// Requested scopes
    scopes: Vec<String>,

    /// Client ID (registered or generated)
    client_id: RwLock<Option<String>>,

    /// Pre-configured client secret (for providers like Slack / Figma).
    client_secret: Option<String>,

    /// Callback host override (default: "localhost", dual-binds IPv4+IPv6).
    callback_host: Option<String>,

    /// Fixed callback port (None = OS-assigned).
    callback_port: Option<u16>,

    /// Callback URL path (default: "/oauth/callback").
    callback_path: Option<String>,

    /// Seconds before expiry at which the background task proactively refreshes.
    ///
    /// The task triggers when `time_until_expiry < max(lifetime * 10%, buffer)`.
    token_refresh_buffer_secs: u64,
}

/// OAuth token response
#[derive(Debug, Deserialize)]
struct TokenResponse {
    access_token: String,
    token_type: Option<String>,
    expires_in: Option<u64>,
    refresh_token: Option<String>,
    scope: Option<String>,
}

/// Client registration response
#[derive(Debug, Deserialize)]
struct ClientRegistrationResponse {
    client_id: String,
    #[allow(dead_code)]
    client_secret: Option<String>,
}

/// Configuration for constructing an [`OAuthClient`].
///
/// Bundles the optional per-provider settings so that [`OAuthClient::new`]
/// stays within Clippy's argument-count limit.
#[derive(Debug, Default)]
pub struct OAuthClientConfig {
    /// Pre-configured client ID.
    pub client_id: Option<String>,
    /// Pre-configured client secret (Slack, Figma, …).
    pub client_secret: Option<String>,
    /// Callback host override (default: `"localhost"`, dual-binds IPv4+IPv6).
    pub callback_host: Option<String>,
    /// Fixed callback port (`None` = OS-assigned).
    pub callback_port: Option<u16>,
    /// Callback URL path (default: `"/oauth/callback"`).
    pub callback_path: Option<String>,
    /// Seconds before expiry to proactively refresh (default: 300).
    pub token_refresh_buffer_secs: u64,
}

impl OAuthClient {
    /// Create a new OAuth client for a backend
    #[must_use]
    pub fn new(
        http_client: Client,
        backend_name: String,
        resource_url: String,
        scopes: Vec<String>,
        storage: Arc<TokenStorage>,
        cfg: OAuthClientConfig,
    ) -> Self {
        Self {
            http_client,
            backend_name,
            resource_url,
            oauth_base_url: None,
            auth_metadata: None,
            resource_metadata: None,
            storage,
            current_token: RwLock::new(None),
            scopes,
            client_id: RwLock::new(cfg.client_id),
            client_secret: cfg.client_secret,
            callback_host: cfg.callback_host,
            callback_port: cfg.callback_port,
            callback_path: cfg.callback_path,
            token_refresh_buffer_secs: cfg.token_refresh_buffer_secs,
        }
    }

    /// Initialize the OAuth client by discovering metadata
    ///
    /// # Errors
    ///
    /// Returns an error if authorization server metadata discovery fails.
    ///
    /// # Panics
    ///
    /// Panics if `oauth_base_url` is `None` after metadata discovery, which
    /// should not occur since both success and error paths set it.
    pub async fn initialize(&mut self) -> Result<()> {
        let base_url = metadata::base_url(&self.resource_url)?;

        // Try to discover protected resource metadata first
        match ProtectedResourceMetadata::discover(&self.http_client, &base_url).await {
            Ok(meta) => {
                debug!(resource = %meta.resource, "Found protected resource metadata");

                // Get authorization server from metadata
                if let Some(auth_server) = meta.authorization_server() {
                    self.oauth_base_url = Some(auth_server.to_string());
                } else {
                    // Fallback to same base URL
                    self.oauth_base_url = Some(base_url.clone());
                }

                // Use scopes from metadata if not specified
                if self.scopes.is_empty() && !meta.scopes_supported.is_empty() {
                    self.scopes.clone_from(&meta.scopes_supported);
                }

                self.resource_metadata = Some(meta);
            }
            Err(e) => {
                debug!(error = %e, "No protected resource metadata, using base URL");
                self.oauth_base_url = Some(base_url.clone());
            }
        }

        // Discover authorization server metadata
        let auth_base = self.oauth_base_url.as_ref().unwrap();
        self.auth_metadata =
            Some(AuthorizationServerMetadata::discover(&self.http_client, auth_base).await?);

        // Load any cached token
        if let Some(token) = self.storage.load(&self.backend_name, &self.resource_url) {
            *self.current_token.write() = Some(token);
        }

        info!(backend = %self.backend_name, "OAuth client initialized");
        Ok(())
    }

    /// Get a valid access token, refreshing or re-authorizing as needed
    ///
    /// # Errors
    ///
    /// Returns an error if token refresh and re-authorization both fail.
    pub async fn get_token(&self) -> Result<String> {
        // Check if we have a valid cached token
        {
            let token = self.current_token.read();
            if let Some(ref t) = *token
                && !t.is_expired()
            {
                return Ok(t.access_token.clone());
            }
        }

        // Try to refresh if we have a refresh token
        let refresh_token_opt = {
            let token = self.current_token.read();
            token.as_ref().and_then(|t| t.refresh_token.clone())
        };

        if let Some(refresh_token) = refresh_token_opt
            && let Ok(new_token) = self.refresh_token(&refresh_token).await
        {
            return Ok(new_token);
        }

        // Need to authorize from scratch
        let token = self.authorize().await?;
        Ok(token)
    }

    /// Return the backend name (used by the background refresh task for logging).
    #[must_use]
    pub fn backend_name(&self) -> &str {
        &self.backend_name
    }

    /// Check if the client has a valid token
    pub fn has_valid_token(&self) -> bool {
        let token = self.current_token.read();
        token.as_ref().is_some_and(|t| !t.is_expired())
    }

    /// Return true if the token should be proactively refreshed.
    ///
    /// Triggers when remaining lifetime is below `max(lifetime * 10%, buffer)`.
    #[must_use]
    pub fn needs_proactive_refresh(&self) -> bool {
        let token = self.current_token.read();
        let Some(ref t) = *token else { return false };

        // Tokens with no expiry never need proactive refresh
        let Some(expires_at) = t.expires_at else {
            return false;
        };

        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        let remaining = expires_at.saturating_sub(now);

        // Compute 10% of total lifetime using the stored expires_at as a proxy.
        // We don't store issued_at, so approximate lifetime as (expires_at - now + remaining)
        // which simplifies to: use a fixed fraction of the buffer itself.
        // Practical rule: trigger refresh at max(buffer, 10% of remaining_at_last_check).
        // Since we check every 60s, use the simpler form: remaining < buffer.
        remaining < self.token_refresh_buffer_secs
    }

    /// Attempt client-credentials grant (headless re-auth, no browser required).
    ///
    /// Returns `Ok(token)` only when the authorization server explicitly lists
    /// `"client_credentials"` in `grant_types_supported` — so we never try it
    /// against a server that won't accept it.
    async fn try_client_credentials(&self) -> Result<String> {
        let auth_meta = self
            .auth_metadata
            .as_ref()
            .ok_or_else(|| Error::OAuth("OAuth not initialized".to_string()))?;

        if !auth_meta
            .grant_types_supported
            .iter()
            .any(|g| g == "client_credentials")
        {
            return Err(Error::OAuth(
                "Server does not support client_credentials grant".to_string(),
            ));
        }

        let client_id = self
            .client_id
            .read()
            .clone()
            .ok_or_else(|| Error::OAuth("No client ID for client_credentials".to_string()))?;

        let scope_str = self.scopes.join(" ");
        let mut params = HashMap::new();
        params.insert("grant_type", "client_credentials");
        params.insert("client_id", client_id.as_str());
        if !scope_str.is_empty() {
            params.insert("scope", scope_str.as_str());
        }

        let response = self
            .http_client
            .post(&auth_meta.token_endpoint)
            .form(&params)
            .send()
            .await
            .map_err(|e| Error::OAuth(format!("Client credentials request failed: {e}")))?;

        if !response.status().is_success() {
            let status = response.status();
            let body = response.text().await.unwrap_or_default();
            return Err(Error::OAuth(format!(
                "Client credentials failed: HTTP {status} - {body}"
            )));
        }

        let token_response: TokenResponse = response
            .json()
            .await
            .map_err(|e| Error::OAuth(format!("Failed to parse credentials response: {e}")))?;

        let token = TokenInfo::from_response(
            token_response.access_token,
            token_response.token_type,
            token_response.refresh_token,
            token_response.expires_in,
            token_response.scope,
        );

        self.storage
            .save(&self.backend_name, &self.resource_url, &token)?;
        *self.current_token.write() = Some(token.clone());

        info!(backend = %self.backend_name, "Token renewed via client_credentials");
        Ok(token.access_token)
    }

    /// Try all headless renewal strategies (`refresh_token` → `client_credentials`).
    ///
    /// Returns `Ok(true)` on success, `Ok(false)` when all automatic methods
    /// are unavailable and manual re-authorization is required.
    async fn attempt_background_renewal(&self) -> bool {
        // Strategy 1: refresh_token grant
        let refresh_token_opt = {
            let token = self.current_token.read();
            token.as_ref().and_then(|t| t.refresh_token.clone())
        };

        if let Some(refresh_token) = refresh_token_opt {
            match self.refresh_token(&refresh_token).await {
                Ok(_) => return true,
                Err(e) => {
                    debug!(
                        backend = %self.backend_name,
                        error = %e,
                        "Token refresh failed, trying client_credentials"
                    );
                }
            }
        }

        // Strategy 2: client_credentials grant (headless, for Beeper-style tokens)
        match self.try_client_credentials().await {
            Ok(_) => return true,
            Err(e) => {
                debug!(
                    backend = %self.backend_name,
                    error = %e,
                    "client_credentials renewal failed"
                );
            }
        }

        false
    }

    /// Spawn a background task that proactively refreshes the token before it
    /// expires.  The task runs for the lifetime of the provided `Arc`; it
    /// stops automatically when the last strong reference is dropped.
    ///
    /// The returned `JoinHandle` can be aborted to cancel the task.
    ///
    /// # Panics
    ///
    /// Does not panic.
    pub fn spawn_refresh_task(
        client: Arc<TokioMutex<Self>>,
        backend_name: String,
    ) -> tokio::task::JoinHandle<()> {
        tokio::spawn(async move {
            loop {
                tokio::time::sleep(Duration::from_secs(60)).await;

                // Use a weak reference pattern: if the Arc has been dropped
                // (HttpTransport gone), stop the loop.
                let needs_refresh = {
                    let guard = client.lock().await;
                    guard.needs_proactive_refresh()
                };

                if needs_refresh {
                    let success = {
                        let guard = client.lock().await;
                        guard.attempt_background_renewal().await
                    };

                    if !success {
                        warn!(
                            backend = %backend_name,
                            "All automatic token renewal strategies failed — \
                             manual re-authorization required"
                        );
                    }
                }
            }
        })
    }

    /// Perform the authorization flow
    ///
    /// # Errors
    ///
    /// Returns an error if any step of the OAuth authorization flow fails
    /// (callback server, client registration, browser auth, or code exchange).
    pub async fn authorize(&self) -> Result<String> {
        let auth_meta = self
            .auth_metadata
            .as_ref()
            .ok_or_else(|| Error::OAuth("OAuth not initialized".to_string()))?;

        // Generate PKCE parameters
        let (code_verifier, code_challenge) = generate_pkce();

        // Generate state for CSRF protection
        let state = generate_state();

        // Start callback server FIRST to get the actual callback URL
        // This must happen BEFORE client registration so we know the port
        let callback_server = callback::start_callback_server(
            state.clone(),
            self.callback_host.as_deref(),
            self.callback_port,
            self.callback_path.as_deref(),
        )
        .await?;
        let callback_url = callback_server.callback_url.clone();

        // Now ensure we have a client ID, passing the actual callback URL for registration
        let client_id = self.ensure_client_id_with_redirect(&callback_url).await?;

        // Build authorization URL with the ACTUAL callback URL
        let mut auth_url = Url::parse(&auth_meta.authorization_endpoint)
            .map_err(|e| Error::OAuth(format!("Invalid auth endpoint: {e}")))?;

        {
            let mut params = auth_url.query_pairs_mut();
            params.append_pair("response_type", "code");
            params.append_pair("client_id", &client_id);
            params.append_pair("redirect_uri", &callback_url);
            params.append_pair("state", &state);
            params.append_pair("code_challenge", &code_challenge);
            params.append_pair("code_challenge_method", "S256");

            if !self.scopes.is_empty() {
                params.append_pair("scope", &self.scopes.join(" "));
            }
        }

        // Open browser
        let auth_url_str = auth_url.to_string();
        info!(url = %auth_url_str, "Opening browser for authorization");

        if !open_browser(&auth_url_str) {
            warn!("Failed to open browser automatically");
            println!("\nPlease authorize this client by visiting:\n{auth_url_str}\n");
        }

        // Wait for callback
        let (actual_callback_url, callback_result) = callback_server.wait_for_callback().await?;

        debug!(code = %callback_result.code, "Received authorization code");

        // Exchange code for token
        let token = self
            .exchange_code(&callback_result.code, &actual_callback_url, &code_verifier)
            .await?;

        // Store and cache the token
        self.storage
            .save(&self.backend_name, &self.resource_url, &token)?;
        *self.current_token.write() = Some(token.clone());

        Ok(token.access_token)
    }

    /// Exchange authorization code for tokens
    async fn exchange_code(
        &self,
        code: &str,
        redirect_uri: &str,
        code_verifier: &str,
    ) -> Result<TokenInfo> {
        let auth_meta = self
            .auth_metadata
            .as_ref()
            .ok_or_else(|| Error::OAuth("OAuth not initialized".to_string()))?;

        let client_id = self
            .client_id
            .read()
            .clone()
            .ok_or_else(|| Error::OAuth("No client ID".to_string()))?;

        let mut params = HashMap::new();
        params.insert("grant_type", "authorization_code");
        params.insert("code", code);
        params.insert("redirect_uri", redirect_uri);
        params.insert("client_id", &client_id);
        params.insert("code_verifier", code_verifier);
        // Include client_secret when the provider requires it (Slack, Figma, etc.)
        let secret_ref: Option<String> = self.client_secret.clone();
        if let Some(ref secret) = secret_ref {
            params.insert("client_secret", secret.as_str());
        }

        let response = self
            .http_client
            .post(&auth_meta.token_endpoint)
            .form(&params)
            .send()
            .await
            .map_err(|e| Error::OAuth(format!("Token request failed: {e}")))?;

        if !response.status().is_success() {
            let status = response.status();
            let body = response.text().await.unwrap_or_default();
            // #143 — structured telemetry: token exchange failure event.
            warn!(
                event = "oauth.token_exchange.failure",
                backend = %self.backend_name,
                http_status = status.as_u16(),
                "OAuth token exchange failed"
            );
            return Err(Error::OAuth(format!(
                "Token exchange failed: HTTP {status} - {body}"
            )));
        }

        let token_response: TokenResponse = response
            .json()
            .await
            .map_err(|e| Error::OAuth(format!("Failed to parse token response: {e}")))?;

        // #143 — structured telemetry: token exchange success event.
        info!(
            event = "oauth.token_exchange.success",
            backend = %self.backend_name,
            has_refresh_token = token_response.refresh_token.is_some(),
            expires_in = token_response.expires_in,
            "OAuth token exchange succeeded"
        );

        Ok(TokenInfo::from_response(
            token_response.access_token,
            token_response.token_type,
            token_response.refresh_token,
            token_response.expires_in,
            token_response.scope,
        ))
    }

    /// Refresh an access token
    async fn refresh_token(&self, refresh_token: &str) -> Result<String> {
        let auth_meta = self
            .auth_metadata
            .as_ref()
            .ok_or_else(|| Error::OAuth("OAuth not initialized".to_string()))?;

        let client_id = self
            .client_id
            .read()
            .clone()
            .ok_or_else(|| Error::OAuth("No client ID".to_string()))?;

        let mut params = HashMap::new();
        params.insert("grant_type", "refresh_token");
        params.insert("refresh_token", refresh_token);
        params.insert("client_id", &client_id);
        let secret_ref: Option<String> = self.client_secret.clone();
        if let Some(ref secret) = secret_ref {
            params.insert("client_secret", secret.as_str());
        }

        let response = self
            .http_client
            .post(&auth_meta.token_endpoint)
            .form(&params)
            .send()
            .await
            .map_err(|e| Error::OAuth(format!("Token refresh failed: {e}")))?;

        if !response.status().is_success() {
            let status = response.status();
            let body = response.text().await.unwrap_or_default();
            return Err(Error::OAuth(format!(
                "Token refresh failed: HTTP {status} - {body}"
            )));
        }

        let token_response: TokenResponse = response
            .json()
            .await
            .map_err(|e| Error::OAuth(format!("Failed to parse refresh response: {e}")))?;

        let token = TokenInfo::from_response(
            token_response.access_token,
            token_response.token_type,
            token_response.refresh_token,
            token_response.expires_in,
            token_response.scope,
        );

        // Store and cache
        self.storage
            .save(&self.backend_name, &self.resource_url, &token)?;
        *self.current_token.write() = Some(token.clone());

        info!(backend = %self.backend_name, "Token refreshed successfully");
        Ok(token.access_token)
    }

    /// Ensure we have a client ID, registering with the specific redirect URI
    async fn ensure_client_id_with_redirect(&self, redirect_uri: &str) -> Result<String> {
        // Check if we already have one
        if let Some(id) = self.client_id.read().clone() {
            return Ok(id);
        }

        let auth_meta = self
            .auth_metadata
            .as_ref()
            .ok_or_else(|| Error::OAuth("OAuth not initialized".to_string()))?;

        // Try dynamic registration if supported
        if let Some(ref reg_endpoint) = auth_meta.registration_endpoint {
            match self.register_client(reg_endpoint, redirect_uri).await {
                Ok(client_id) => {
                    *self.client_id.write() = Some(client_id.clone());
                    return Ok(client_id);
                }
                Err(e) => {
                    debug!(error = %e, "Dynamic registration failed, using generated ID");
                }
            }
        }

        // Generate a client ID
        let generated = generate_client_id();
        *self.client_id.write() = Some(generated.clone());
        Ok(generated)
    }

    /// Register a new client dynamically with the specified redirect URI
    async fn register_client(&self, endpoint: &str, redirect_uri: &str) -> Result<String> {
        let body = serde_json::json!({
            "client_name": format!("MCP Gateway - {}", self.backend_name),
            "redirect_uris": [redirect_uri],
            "grant_types": ["authorization_code", "refresh_token"],
            "response_types": ["code"],
            "token_endpoint_auth_method": "none"
        });

        let response = self
            .http_client
            .post(endpoint)
            .json(&body)
            .send()
            .await
            .map_err(|e| Error::OAuth(format!("Client registration failed: {e}")))?;

        if !response.status().is_success() {
            let status = response.status();
            let body = response.text().await.unwrap_or_default();
            return Err(Error::OAuth(format!(
                "Client registration failed: HTTP {status} - {body}"
            )));
        }

        let reg_response: ClientRegistrationResponse = response
            .json()
            .await
            .map_err(|e| Error::OAuth(format!("Failed to parse registration response: {e}")))?;

        info!(client_id = %reg_response.client_id, "Registered OAuth client");
        Ok(reg_response.client_id)
    }
}

/// Generate PKCE code verifier and challenge
fn generate_pkce() -> (String, String) {
    // Generate 32 random bytes for verifier
    let verifier_bytes: [u8; 32] = rand::rng().random();
    let verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);

    // SHA256 hash for challenge
    let mut hasher = Sha256::new();
    hasher.update(verifier.as_bytes());
    let challenge_bytes = hasher.finalize();
    let challenge = URL_SAFE_NO_PAD.encode(challenge_bytes);

    (verifier, challenge)
}

/// Generate a random state parameter
fn generate_state() -> String {
    let state_bytes: [u8; 16] = rand::rng().random();
    URL_SAFE_NO_PAD.encode(state_bytes)
}

/// Generate a random client ID
fn generate_client_id() -> String {
    let id_bytes: [u8; 16] = rand::rng().random();
    URL_SAFE_NO_PAD.encode(id_bytes)
}

/// Open a URL in the system default browser.
///
/// Uses `open` on macOS, `xdg-open` on Linux, and `start` on Windows.
/// Returns `true` if the command was spawned successfully.
fn open_browser(url: &str) -> bool {
    #[cfg(target_os = "macos")]
    let cmd = "open";
    #[cfg(target_os = "linux")]
    let cmd = "xdg-open";
    #[cfg(target_os = "windows")]
    let cmd = "cmd";

    #[cfg(target_os = "windows")]
    let result = std::process::Command::new(cmd)
        .args(["/c", "start", url])
        .spawn();
    #[cfg(not(target_os = "windows"))]
    let result = std::process::Command::new(cmd).arg(url).spawn();

    result.is_ok()
}

#[cfg(test)]
mod tests;
