use async_trait::async_trait;
use fabro_model::{Catalog, ProviderId};

use crate::context::{AuthContextRequest, AuthContextResponse};
use crate::credential::{OAuthConfig, OAuthCredential};
use crate::strategies::api_key::ApiKeyStrategy;
use crate::strategies::codex_device::CodexDeviceStrategy;

pub const CODEX_CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
pub const CODEX_AUTH_URL: &str = "https://auth.openai.com";
pub const CODEX_TOKEN_URL: &str = "https://auth.openai.com/oauth/token";

#[derive(Debug, Clone)]
pub enum LoginResult {
    ApiKey {
        provider: ProviderId,
        key:      String,
    },
    OAuth {
        provider:   ProviderId,
        credential: OAuthCredential,
    },
}

#[async_trait]
pub trait AuthStrategy: Send {
    async fn init(&mut self) -> anyhow::Result<AuthContextRequest>;
    async fn complete(&mut self, response: AuthContextResponse) -> anyhow::Result<LoginResult>;
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthMethod {
    ApiKey,
    CodexDevice(OAuthConfig),
}

#[must_use]
pub fn codex_oauth_config() -> OAuthConfig {
    OAuthConfig {
        auth_url:     CODEX_AUTH_URL.to_string(),
        token_url:    CODEX_TOKEN_URL.to_string(),
        client_id:    CODEX_CLIENT_ID.to_string(),
        scopes:       vec![
            "openid".to_string(),
            "profile".to_string(),
            "email".to_string(),
            "offline_access".to_string(),
        ],
        redirect_uri: Some(format!("{CODEX_AUTH_URL}/deviceauth/callback")),
        use_pkce:     false,
    }
}

#[must_use]
pub fn strategy_for(
    provider_id: &ProviderId,
    method: AuthMethod,
    catalog: &Catalog,
) -> Box<dyn AuthStrategy> {
    match method {
        AuthMethod::ApiKey => {
            let provider = catalog
                .provider(provider_id)
                .expect("API key auth requires a catalog provider");
            Box::new(ApiKeyStrategy::new(provider))
        }
        AuthMethod::CodexDevice(config) => {
            // Programming invariant: every call site that constructs
            // `AuthMethod::CodexDevice` pairs it with `ProviderId::OPENAI`.
            // `pick_auth_method` returns CodexDevice only when provider ==
            // openai(), and the install flow hard-codes `ProviderId::openai()`.
            // This check catches future regressions where a new call site
            // forgets the constraint.
            assert_eq!(
                provider_id.as_str(),
                ProviderId::OPENAI,
                "CodexDevice auth is only constructed by CLI code for the \
                 OpenAI provider; all existing call sites enforce this pairing: \
                 got provider_id={provider_id}"
            );
            Box::new(CodexDeviceStrategy::new(config))
        }
    }
}

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

    #[test]
    fn codex_oauth_config_has_expected_defaults() {
        let config = codex_oauth_config();
        assert_eq!(config.auth_url, CODEX_AUTH_URL);
        assert_eq!(config.token_url, CODEX_TOKEN_URL);
        assert_eq!(config.client_id, CODEX_CLIENT_ID);
        assert!(!config.use_pkce);
        assert!(config.scopes.contains(&"offline_access".to_string()));
    }

    #[tokio::test]
    async fn api_key_strategy_uses_provider_env_names() {
        let catalog = Catalog::builtin();
        let provider = catalog.provider(&ProviderId::anthropic()).unwrap();
        let mut strategy = ApiKeyStrategy::new(provider);
        let request = strategy.init().await.unwrap();
        assert_eq!(request, AuthContextRequest::ApiKey {
            provider_id:   ProviderId::anthropic(),
            display_name:  "Anthropic".to_string(),
            env_var_names: vec!["ANTHROPIC_API_KEY".to_string()],
            api_key_url:   Some("https://console.anthropic.com/settings/keys".to_string()),
        });
    }
}
