//! OAuth flow implementation for Anthropic

use crate::error::{Error, Result};
use crate::pkce::Pkce;
use serde::{Deserialize, Serialize};
use url::Url;

use super::{CLIENT_ID, REDIRECT_URI, SCOPES};

/// Authentication mode
#[derive(Debug, Clone, Copy)]
pub enum AuthMode {
    /// Claude Pro/Max subscription (claude.ai)
    Max,
    /// API key creation via console (console.anthropic.com)
    Console,
}

impl AuthMode {
    fn host(&self) -> &'static str {
        match self {
            AuthMode::Max => "claude.ai",
            AuthMode::Console => "console.anthropic.com",
        }
    }
}

/// OAuth tokens from successful authentication
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuthTokens {
    pub access_token: String,
    pub refresh_token: String,
    /// Expiration timestamp (milliseconds since epoch)
    pub expires_at: u64,
}

impl OAuthTokens {
    /// Check if the access token is expired
    pub fn is_expired(&self) -> bool {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_millis() as u64;
        self.expires_at < now
    }
}

/// Token response from Anthropic OAuth endpoint
#[derive(Debug, Deserialize)]
struct TokenResponse {
    access_token: String,
    refresh_token: String,
    expires_in: u64,
}

/// Anthropic OAuth client
#[derive(Debug)]
pub struct AnthropicOAuth {
    client: reqwest::Client,
}

impl AnthropicOAuth {
    pub fn new() -> Self {
        Self {
            client: reqwest::Client::new(),
        }
    }

    /// Generate the authorization URL and PKCE verifier
    ///
    /// Returns (authorization_url, pkce_verifier)
    pub fn authorize_url(&self, mode: AuthMode) -> Result<(String, String)> {
        let pkce = Pkce::generate();

        let mut url = Url::parse(&format!("https://{}/oauth/authorize", mode.host()))?;

        {
            let mut params = url.query_pairs_mut();
            params.append_pair("code", "true");
            params.append_pair("client_id", CLIENT_ID);
            params.append_pair("response_type", "code");
            params.append_pair("redirect_uri", REDIRECT_URI);
            params.append_pair("scope", SCOPES);
            params.append_pair("code_challenge", &pkce.challenge);
            params.append_pair("code_challenge_method", Pkce::method());
            params.append_pair("state", &pkce.verifier);
        }

        Ok((url.to_string(), pkce.verifier))
    }

    /// Exchange authorization code for tokens
    ///
    /// The `code` may contain a `#state` suffix which is handled automatically.
    pub async fn exchange(&self, code: &str, verifier: &str) -> Result<OAuthTokens> {
        // Handle code#state format
        let (code, state) = if let Some(pos) = code.find('#') {
            (&code[..pos], Some(&code[pos + 1..]))
        } else {
            (code, None)
        };

        let mut body = serde_json::json!({
            "grant_type": "authorization_code",
            "code": code,
            "client_id": CLIENT_ID,
            "redirect_uri": REDIRECT_URI,
            "code_verifier": verifier,
        });

        if let Some(state) = state {
            body["state"] = serde_json::Value::String(state.to_string());
        }

        let response = self
            .client
            .post("https://console.anthropic.com/v1/oauth/token")
            .header("Content-Type", "application/json")
            .json(&body)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(Error::OAuth(format!(
                "Token exchange failed: {}",
                response.status()
            )));
        }

        let token_response: TokenResponse = response.json().await?;
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_millis() as u64;

        Ok(OAuthTokens {
            access_token: token_response.access_token,
            refresh_token: token_response.refresh_token,
            expires_at: now + (token_response.expires_in * 1000),
        })
    }

    /// Refresh an expired access token
    pub async fn refresh(&self, refresh_token: &str) -> Result<OAuthTokens> {
        let body = serde_json::json!({
            "grant_type": "refresh_token",
            "refresh_token": refresh_token,
            "client_id": CLIENT_ID,
        });

        let response = self
            .client
            .post("https://console.anthropic.com/v1/oauth/token")
            .header("Content-Type", "application/json")
            .json(&body)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(Error::TokenRefresh {
                status: response.status().as_u16(),
            });
        }

        let token_response: TokenResponse = response.json().await?;
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_millis() as u64;

        Ok(OAuthTokens {
            access_token: token_response.access_token,
            refresh_token: token_response.refresh_token,
            expires_at: now + (token_response.expires_in * 1000),
        })
    }
}

impl Default for AnthropicOAuth {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_authorize_url_max() {
        let oauth = AnthropicOAuth::new();
        let (url, verifier) = oauth.authorize_url(AuthMode::Max).unwrap();

        assert!(url.starts_with("https://claude.ai/oauth/authorize"));
        assert!(url.contains("client_id="));
        assert!(url.contains("code_challenge="));
        assert!(url.contains("code_challenge_method=S256"));
        assert!(!verifier.is_empty());
    }

    #[test]
    fn test_authorize_url_console() {
        let oauth = AnthropicOAuth::new();
        let (url, _) = oauth.authorize_url(AuthMode::Console).unwrap();

        assert!(url.starts_with("https://console.anthropic.com/oauth/authorize"));
    }

    #[test]
    fn test_token_expiry() {
        let tokens = OAuthTokens {
            access_token: "test".to_string(),
            refresh_token: "test".to_string(),
            expires_at: 0, // Expired
        };
        assert!(tokens.is_expired());

        let tokens = OAuthTokens {
            access_token: "test".to_string(),
            refresh_token: "test".to_string(),
            expires_at: u64::MAX, // Far future
        };
        assert!(!tokens.is_expired());
    }
}
