//! OAuth2 token exchange and protobuf-level authentication.
//!
//! Two-phase authentication:
//! 1. **HTTP OAuth2** — exchange auth code or refresh token for access token
//! 2. **Protobuf auth** — ApplicationAuth + AccountAuth over the TCP connection

use prost::Message;
use serde::{Deserialize, Serialize};
use tracing::{debug, info};

use crate::config::CTraderConfig;
use crate::connection::CTraderConnection;
use crate::error::{CTraderError, Result};
use crate::proto::{self, ProtoMessage};

// ── OAuth2 (HTTP) ───────────────────────────────────────────────────

const OAUTH_TOKEN_URL: &str = "https://openapi.ctrader.com/apps/token";

/// OAuth2 token response from the cTrader authorization server.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenResponse {
    /// The access token.
    pub access_token: String,
    /// Token type (always "bearer").
    pub token_type: String,
    /// Lifetime in seconds.
    pub expires_in: i64,
    /// Refresh token for obtaining new access tokens.
    pub refresh_token: String,
}

/// Exchange an authorization code for access + refresh tokens.
pub async fn exchange_auth_code(
    client_id: &str,
    client_secret: &str,
    redirect_uri: &str,
    auth_code: &str,
) -> Result<TokenResponse> {
    let params = [
        ("grant_type", "authorization_code"),
        ("code", auth_code),
        ("redirect_uri", redirect_uri),
        ("client_id", client_id),
        ("client_secret", client_secret),
    ];

    let client = reqwest::Client::new();
    let resp = client
        .post(OAUTH_TOKEN_URL)
        .form(&params)
        .send()
        .await
        .map_err(|e| CTraderError::OAuthError(format!("HTTP request failed: {e}")))?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp
            .text()
            .await
            .unwrap_or_else(|_| "<unreadable>".into());
        return Err(CTraderError::OAuthError(format!(
            "token exchange failed ({status}): {body}"
        )));
    }

    resp.json::<TokenResponse>()
        .await
        .map_err(|e| CTraderError::OAuthError(format!("failed to parse token response: {e}")))
}

/// Refresh an access token using a refresh token.
pub async fn refresh_token(
    client_id: &str,
    client_secret: &str,
    refresh_tok: &str,
) -> Result<TokenResponse> {
    let params = [
        ("grant_type", "refresh_token"),
        ("refresh_token", refresh_tok),
        ("client_id", client_id),
        ("client_secret", client_secret),
    ];

    let client = reqwest::Client::new();
    let resp = client
        .post(OAUTH_TOKEN_URL)
        .form(&params)
        .send()
        .await
        .map_err(|e| CTraderError::OAuthError(format!("HTTP request failed: {e}")))?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp
            .text()
            .await
            .unwrap_or_else(|_| "<unreadable>".into());
        return Err(CTraderError::OAuthError(format!(
            "token refresh failed ({status}): {body}"
        )));
    }

    resp.json::<TokenResponse>()
        .await
        .map_err(|e| CTraderError::OAuthError(format!("failed to parse token response: {e}")))
}

// ── Protobuf auth (over TCP+TLS) ───────────────────────────────────

/// Run the full protobuf authentication sequence:
/// 1. Send `ProtoOAApplicationAuthReq` → wait for `ProtoOAApplicationAuthRes`
/// 2. Send `ProtoOAAccountAuthReq` → wait for `ProtoOAAccountAuthRes`
///
/// Uses the connection's `recv()` method directly (before the dispatcher
/// takes over the receive stream).
pub async fn authenticate(
    conn: &mut CTraderConnection,
    config: &CTraderConfig,
) -> Result<()> {
    // Step 1: Application auth
    info!("authenticating application");
    let app_auth = proto::ProtoOaApplicationAuthReq {
        payload_type: Some(proto::PT_APP_AUTH_REQ as i32),
        client_id: config.client_id.clone(),
        client_secret: config.client_secret.clone(),
    };

    let app_auth_bytes = app_auth.encode_to_vec();
    let envelope = ProtoMessage {
        payload_type: proto::PT_APP_AUTH_REQ,
        payload: Some(app_auth_bytes),
        client_msg_id: None,
    };
    conn.send(envelope).await?;

    // Wait for response
    let resp = conn.recv().await?;
    match resp.payload_type {
        proto::PT_APP_AUTH_RES => {
            debug!("application auth successful");
        }
        proto::PT_OA_ERROR_RES | proto::PT_ERROR_RES => {
            let desc = extract_error_description(&resp);
            return Err(CTraderError::AppAuthFailed(desc));
        }
        other => {
            return Err(CTraderError::ProtocolError(format!(
                "expected ApplicationAuthRes ({}), got payloadType {other}",
                proto::PT_APP_AUTH_RES
            )));
        }
    }

    // Step 2: Account auth
    info!(account_id = config.account_id, "authenticating account");
    let acct_auth = proto::ProtoOaAccountAuthReq {
        payload_type: Some(proto::PT_ACCOUNT_AUTH_REQ as i32),
        ctid_trader_account_id: config.account_id,
        access_token: config.access_token.clone(),
    };

    let acct_auth_bytes = acct_auth.encode_to_vec();
    let acct_envelope = ProtoMessage {
        payload_type: proto::PT_ACCOUNT_AUTH_REQ,
        payload: Some(acct_auth_bytes),
        client_msg_id: None,
    };
    conn.send(acct_envelope).await?;

    // Wait for response
    let acct_resp = conn.recv().await?;
    match acct_resp.payload_type {
        proto::PT_ACCOUNT_AUTH_RES => {
            info!("account auth successful");
            Ok(())
        }
        proto::PT_OA_ERROR_RES | proto::PT_ERROR_RES => {
            let desc = extract_error_description(&resp);
            Err(CTraderError::AccountAuthFailed(desc))
        }
        other => Err(CTraderError::ProtocolError(format!(
            "expected AccountAuthRes ({}), got payloadType {other}",
            proto::PT_ACCOUNT_AUTH_RES
        ))),
    }
}

/// Extract an error description from an OA error response or generic error.
fn extract_error_description(msg: &ProtoMessage) -> String {
    if let Some(ref payload) = msg.payload {
        // Try OA error first
        if let Ok(oa_err) = proto::ProtoOaErrorRes::decode(payload.as_slice()) {
            return format!(
                "{}: {}",
                oa_err.error_code,
                oa_err.description.unwrap_or_default()
            );
        }
        // Try common error
        if let Ok(err) = proto::ProtoErrorRes::decode(payload.as_slice()) {
            return format!(
                "{}: {}",
                err.error_code,
                err.description.unwrap_or_default()
            );
        }
    }
    format!("unknown error (payloadType={})", msg.payload_type)
}

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

    #[test]
    fn extract_error_from_oa_error_res() {
        let oa_err = proto::ProtoOaErrorRes {
            payload_type: Some(proto::PT_OA_ERROR_RES as i32),
            ctid_trader_account_id: Some(12345),
            error_code: "CH_CLIENT_AUTH_FAILURE".into(),
            description: Some("Invalid credentials".into()),
            maintenance_end_timestamp: None,
            retry_after: None,
        };

        let msg = ProtoMessage {
            payload_type: proto::PT_OA_ERROR_RES,
            payload: Some(oa_err.encode_to_vec()),
            client_msg_id: None,
        };

        let desc = extract_error_description(&msg);
        assert!(desc.contains("CH_CLIENT_AUTH_FAILURE"));
        assert!(desc.contains("Invalid credentials"));
    }
}
