use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;

use crate::error::GoogleError;

/// Stored Google OAuth2 tokens
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoogleTokens {
    pub access_token: String,
    pub refresh_token: String,
    /// Unix timestamp in milliseconds when the access token expires
    pub expiry_date: i64,
    pub scope: String,
    pub token_type: String,
}

/// Google OAuth2 client credentials
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoogleCredentials {
    pub client_id: String,
    pub client_secret: String,
}

/// Trait for providing Google access tokens.
/// Consumers implement this to control persistence and refresh strategy.
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
pub trait GoogleAuth: Send + Sync {
    /// Get a valid access token (refreshing if needed).
    async fn get_access_token(&self) -> Result<String, GoogleError>;

    /// Called when tokens have been refreshed so the consumer can persist them.
    async fn on_tokens_refreshed(&self, _tokens: &GoogleTokens) -> Result<(), GoogleError> {
        Ok(())
    }
}

/// Buffer in milliseconds before expiry to trigger refresh (5 minutes)
const EXPIRY_BUFFER_MS: i64 = 5 * 60 * 1000;

/// Google token endpoint for refreshing
const TOKEN_ENDPOINT: &str = "https://oauth2.googleapis.com/token";

/// Concrete GoogleAuth implementation that holds tokens in memory
/// and auto-refreshes via Google's token endpoint.
pub struct GoogleTokenAuth {
    credentials: GoogleCredentials,
    tokens: RwLock<GoogleTokens>,
    http: reqwest::Client,
    /// Optional callback for persisting refreshed tokens
    on_refresh: Option<Arc<dyn Fn(&GoogleTokens) + Send + Sync>>,
}

impl GoogleTokenAuth {
    pub fn new(credentials: GoogleCredentials, tokens: GoogleTokens) -> Self {
        Self {
            credentials,
            tokens: RwLock::new(tokens),
            http: reqwest::Client::new(),
            on_refresh: None,
        }
    }

    pub fn with_http(mut self, http: reqwest::Client) -> Self {
        self.http = http;
        self
    }

    fn is_expired(tokens: &GoogleTokens) -> bool {
        let now_ms = chrono::Utc::now().timestamp_millis();
        tokens.expiry_date - now_ms < EXPIRY_BUFFER_MS
    }

    async fn refresh_token(&self) -> Result<GoogleTokens, GoogleError> {
        let current = self.tokens.read().await;
        let refresh_token = current.refresh_token.clone();
        drop(current);

        #[derive(Serialize)]
        struct RefreshRequest<'a> {
            client_id: &'a str,
            client_secret: &'a str,
            refresh_token: &'a str,
            grant_type: &'a str,
        }

        #[derive(Deserialize)]
        struct RefreshResponse {
            access_token: String,
            expires_in: i64,
            scope: Option<String>,
            token_type: String,
        }

        let resp = self
            .http
            .post(TOKEN_ENDPOINT)
            .form(&RefreshRequest {
                client_id: &self.credentials.client_id,
                client_secret: &self.credentials.client_secret,
                refresh_token: &refresh_token,
                grant_type: "refresh_token",
            })
            .send()
            .await
            .map_err(|e| GoogleError::token_refresh(e.to_string()))?;

        if !resp.status().is_success() {
            let status = resp.status().as_u16();
            let body = resp
                .text()
                .await
                .unwrap_or_else(|_| "unknown".to_string());
            return Err(GoogleError::token_refresh(format!(
                "HTTP {status}: {body}"
            )));
        }

        let refresh_resp: RefreshResponse = resp
            .json()
            .await
            .map_err(|e| GoogleError::token_refresh(e.to_string()))?;

        let now_ms = chrono::Utc::now().timestamp_millis();
        let mut tokens = self.tokens.write().await;
        tokens.access_token = refresh_resp.access_token;
        tokens.expiry_date = now_ms + (refresh_resp.expires_in * 1000);
        tokens.token_type = refresh_resp.token_type;
        if let Some(scope) = refresh_resp.scope {
            tokens.scope = scope;
        }
        // Google doesn't always return a new refresh_token; keep the old one

        let updated = tokens.clone();
        drop(tokens);

        Ok(updated)
    }
}

#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl GoogleAuth for GoogleTokenAuth {
    async fn get_access_token(&self) -> Result<String, GoogleError> {
        // Check if current token is still valid
        {
            let tokens = self.tokens.read().await;
            if !Self::is_expired(&tokens) {
                return Ok(tokens.access_token.clone());
            }
        }

        // Token expired — refresh
        let updated = self.refresh_token().await?;
        self.on_tokens_refreshed(&updated).await?;
        Ok(updated.access_token)
    }

    async fn on_tokens_refreshed(&self, tokens: &GoogleTokens) -> Result<(), GoogleError> {
        if let Some(ref cb) = self.on_refresh {
            cb(tokens);
        }
        Ok(())
    }
}

/// Mock auth for testing — always returns a static token
#[cfg(test)]
pub struct MockGoogleAuth {
    pub token: String,
}

#[cfg(test)]
impl MockGoogleAuth {
    pub fn new(token: impl Into<String>) -> Self {
        Self {
            token: token.into(),
        }
    }
}

#[cfg(test)]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl GoogleAuth for MockGoogleAuth {
    async fn get_access_token(&self) -> Result<String, GoogleError> {
        Ok(self.token.clone())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_tokens(expiry_offset_ms: i64) -> GoogleTokens {
        let now = chrono::Utc::now().timestamp_millis();
        GoogleTokens {
            access_token: "ya29.test-access-token".to_string(),
            refresh_token: "1//test-refresh-token".to_string(),
            expiry_date: now + expiry_offset_ms,
            scope: "https://www.googleapis.com/auth/calendar".to_string(),
            token_type: "Bearer".to_string(),
        }
    }

    fn make_credentials() -> GoogleCredentials {
        GoogleCredentials {
            client_id: "test-client-id.apps.googleusercontent.com".to_string(),
            client_secret: "test-client-secret".to_string(),
        }
    }

    #[tokio::test]
    async fn returns_cached_token_when_not_expired() {
        let tokens = make_tokens(60 * 60 * 1000); // 1 hour from now
        let auth = GoogleTokenAuth::new(make_credentials(), tokens.clone());
        let token = auth.get_access_token().await.unwrap();
        assert_eq!(token, "ya29.test-access-token");
    }

    #[test]
    fn token_not_expired_with_buffer() {
        // 10 minutes from now — outside buffer
        let tokens = make_tokens(10 * 60 * 1000);
        assert!(!GoogleTokenAuth::is_expired(&tokens));
    }

    #[test]
    fn token_expired_within_buffer() {
        // 2 minutes from now — inside 5-minute buffer
        let tokens = make_tokens(2 * 60 * 1000);
        assert!(GoogleTokenAuth::is_expired(&tokens));
    }

    #[test]
    fn token_expired_in_past() {
        let tokens = make_tokens(-1000);
        assert!(GoogleTokenAuth::is_expired(&tokens));
    }

    #[test]
    fn tokens_serde_roundtrip() {
        let tokens = make_tokens(3600000);
        let json = serde_json::to_string(&tokens).unwrap();
        let parsed: GoogleTokens = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.access_token, tokens.access_token);
        assert_eq!(parsed.refresh_token, tokens.refresh_token);
        assert_eq!(parsed.expiry_date, tokens.expiry_date);
        assert_eq!(parsed.scope, tokens.scope);
        assert_eq!(parsed.token_type, tokens.token_type);
    }

    #[test]
    fn credentials_serde_roundtrip() {
        let creds = make_credentials();
        let json = serde_json::to_string(&creds).unwrap();
        let parsed: GoogleCredentials = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.client_id, creds.client_id);
        assert_eq!(parsed.client_secret, creds.client_secret);
    }
}
