//! iFlow platform (Z.ai / GLM) Authorization Code OAuth flow configuration.
//!
//! Token exchange uses an HTTP Basic Auth header.
//! Callback port: 11451.
//! Provides access to GLM and Kimi K2 models.

use async_trait::async_trait;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use byokey_types::{ByokError, OAuthToken, ProviderId, Result};

/// Local callback port for the OAuth redirect.
pub const CALLBACK_PORT: u16 = 11451;
/// iFlow authorization endpoint.
pub const AUTH_URL: &str = "https://iflow.cn/oauth";
const REDIRECT_URI: &str = "http://localhost:11451/callback";
const REDIRECT_URI_ENCODED: &str = "http%3A%2F%2Flocalhost%3A11451%2Fcallback";

/// Build the authorization URL.
#[must_use]
pub fn build_auth_url(client_id: &str, state: &str) -> String {
    format!(
        "{AUTH_URL}?response_type=code&client_id={client_id}&redirect_uri={REDIRECT_URI_ENCODED}&state={state}&loginMethod=phone&type=phone",
    )
}

/// Generate the HTTP Basic Auth header value.
///
/// Format: `Basic base64(client_id:client_secret)`.
#[must_use]
pub fn basic_auth_header(client_id: &str, client_secret: &str) -> String {
    let cred = format!("{client_id}:{client_secret}");
    format!("Basic {}", STANDARD.encode(cred.as_bytes()))
}

/// Build form parameters for the token exchange request.
///
/// Note: `client_secret` is sent via the Basic Auth header, not in the form body.
#[must_use]
pub fn token_form_params(client_id: &str, code: &str) -> Vec<(String, String)> {
    vec![
        ("grant_type".into(), "authorization_code".into()),
        ("client_id".into(), client_id.into()),
        ("code".into(), code.into()),
        ("redirect_uri".into(), REDIRECT_URI.into()),
    ]
}

/// iFlow `userInfo` endpoint.
pub const USER_INFO_URL: &str = "https://iflow.cn/api/oauth/getUserInfo";

/// Fetch the iFlow API key by exchanging an OAuth access token via the `userInfo` endpoint.
///
/// # Errors
///
/// Returns [`ByokError::Http`] on network failure or [`ByokError::Auth`] if the
/// response is missing the `apiKey` field.
pub async fn fetch_api_key(oauth_token: &str, http: &rquest::Client) -> Result<String> {
    let url = format!("{USER_INFO_URL}?accessToken={oauth_token}");
    let resp = http
        .get(&url)
        .header("Accept", "application/json")
        .send()
        .await?;

    let status = resp.status();
    let json: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| ByokError::Auth(format!("iflow userinfo parse error: {e}")))?;

    if !status.is_success() {
        return Err(ByokError::Auth(format!("iflow userinfo {status}")));
    }

    json.pointer("/data/apiKey")
        .and_then(serde_json::Value::as_str)
        .filter(|s| !s.is_empty())
        .map(String::from)
        .ok_or_else(|| ByokError::Auth("iflow: missing apiKey in userinfo response".into()))
}

// ── AuthCodeFlow implementation ───────────────────────────────────────────────

use crate::credentials::OAuthCredentials;
use crate::flow::auth_code::{self, AuthCodeFlow};

/// iFlow auth-code provider (no PKCE, Basic Auth, API key exchange).
pub struct IFlow;

#[async_trait]
impl AuthCodeFlow for IFlow {
    fn provider_id(&self) -> ProviderId {
        ProviderId::IFlow
    }
    fn provider_name(&self) -> &'static str {
        "iflow"
    }
    fn callback_port(&self) -> u16 {
        CALLBACK_PORT
    }
    fn uses_pkce(&self) -> bool {
        false
    }

    fn build_auth_url(&self, client_id: &str, _challenge: &str, state: &str) -> String {
        build_auth_url(client_id, state)
    }

    async fn exchange_code(
        &self,
        http: &rquest::Client,
        creds: &OAuthCredentials,
        code: &str,
        _verifier: &str,
        _state: &str,
    ) -> Result<OAuthToken> {
        let token_url = creds
            .token_url
            .as_deref()
            .ok_or_else(|| ByokError::Auth("iflow credentials missing token_url".into()))?;
        let client_secret = creds
            .client_secret
            .as_deref()
            .ok_or_else(|| ByokError::Auth("iflow credentials missing client_secret".into()))?;
        let params = token_form_params(&creds.client_id, code);
        let resp = http
            .post(token_url)
            .header(
                "Authorization",
                basic_auth_header(&creds.client_id, client_secret),
            )
            .form(&params)
            .send()
            .await?;
        auth_code::send_and_parse_token(resp).await
    }

    async fn post_process(&self, token: OAuthToken, http: &rquest::Client) -> Result<OAuthToken> {
        let api_key = fetch_api_key(&token.access_token, http).await?;
        Ok(OAuthToken {
            access_token: api_key,
            ..token
        })
    }
}

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

    const TEST_CLIENT_ID: &str = "test-client-id";
    const TEST_CLIENT_SECRET: &str = "test-client-secret";

    #[test]
    fn test_build_auth_url() {
        let url = build_auth_url(TEST_CLIENT_ID, "mystate");
        assert!(url.contains(TEST_CLIENT_ID));
        assert!(url.contains("mystate"));
        assert!(url.contains("response_type=code"));
        assert!(url.contains(REDIRECT_URI_ENCODED));
        assert!(url.contains("loginMethod=phone"));
    }

    #[test]
    fn test_basic_auth_header_format() {
        let header = basic_auth_header(TEST_CLIENT_ID, TEST_CLIENT_SECRET);
        assert!(header.starts_with("Basic "));
        let encoded = header.strip_prefix("Basic ").unwrap();
        let decoded = String::from_utf8(
            base64::engine::general_purpose::STANDARD
                .decode(encoded)
                .unwrap(),
        )
        .unwrap();
        assert_eq!(decoded, format!("{TEST_CLIENT_ID}:{TEST_CLIENT_SECRET}"));
    }

    #[test]
    fn test_token_form_params() {
        let params = token_form_params(TEST_CLIENT_ID, "mycode");
        let map: std::collections::HashMap<&str, &str> = params
            .iter()
            .map(|(k, v)| (k.as_str(), v.as_str()))
            .collect();
        assert_eq!(map["grant_type"], "authorization_code");
        assert_eq!(map["client_id"], TEST_CLIENT_ID);
        assert_eq!(map["code"], "mycode");
        assert_eq!(map["redirect_uri"], REDIRECT_URI);
    }

    #[test]
    fn test_parse_token_response_ok() {
        let resp = json!({"access_token": "tok", "expires_in": 7200});
        let t = crate::token::parse_token_response(&resp).unwrap();
        assert_eq!(t.access_token, "tok");
    }

    #[test]
    fn test_parse_token_response_missing() {
        assert!(crate::token::parse_token_response(&json!({})).is_err());
    }
}
