//! Authentication method implementation for ACP.
//!
//! This module implements the `authenticate` JSON-RPC method for agents that
//! require authentication after initialization. The authentication flow is optional
//! and only used if the agent advertises auth_methods in the initialize response.

use crate::acp::transport::{JsonRpcRequest, JsonRpcResult, Transport, TransportError};
use serde::{Deserialize, Serialize};

/// Authentication request sent to the agent.
///
/// This is sent after initialization if the agent advertised auth_methods
/// and the client has credentials available.
#[derive(Clone, Serialize, Deserialize)]
pub struct AuthenticateRequest {
    /// Authentication method to use (must be one of the methods advertised by agent).
    pub method: String,

    /// Method-specific credentials (structure depends on the authentication method).
    /// Common examples:
    /// - API key: `{"api_key": "secret"}`
    /// - OAuth: `{"token": "bearer_token"}`
    pub credentials: serde_json::Value,
}

impl AuthenticateRequest {
    /// Create a new authentication request.
    pub fn new(method: impl Into<String>, credentials: serde_json::Value) -> Self {
        Self {
            method: method.into(),
            credentials,
        }
    }

    /// Convert to JSON-RPC request.
    pub fn to_jsonrpc(&self, id: u64) -> JsonRpcRequest {
        JsonRpcRequest::new(
            id,
            "authenticate",
            Some(serde_json::to_value(self).expect("Failed to serialize AuthenticateRequest")),
        )
    }
}

// Custom Debug implementation that redacts credentials (no derive(Debug) above)
impl std::fmt::Debug for AuthenticateRequest {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AuthenticateRequest")
            .field("method", &self.method)
            .field("credentials", &"[REDACTED]")
            .finish()
    }
}

/// Authentication response from the agent.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuthenticateResponse {
    /// Whether authentication succeeded.
    pub success: bool,

    /// Error message if authentication failed.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

impl AuthenticateResponse {
    /// Parse from JSON-RPC result.
    pub fn from_jsonrpc(result: &serde_json::Value) -> Result<Self, serde_json::Error> {
        serde_json::from_value(result.clone())
    }
}

/// Result of authentication attempt.
#[derive(Debug)]
pub enum AuthenticateResult {
    /// Authentication succeeded.
    Success,
    /// Authentication failed with error message.
    Failed(String),
    /// Agent does not require authentication.
    NotRequired,
}

/// Perform authentication with the agent.
///
/// This sends an authenticate request if the agent requires authentication.
/// If auth_methods is empty, returns NotRequired without sending a request.
///
/// # Arguments
///
/// * `transport` - The transport to use for communication
/// * `auth_methods` - Authentication methods advertised by the agent (from initialize response)
/// * `method` - Authentication method to use (must be in auth_methods)
/// * `credentials` - Method-specific credentials
///
/// # Returns
///
/// Result of the authentication attempt.
///
/// # Security
///
/// Credentials are never logged in plain text. The AuthenticateRequest Debug
/// implementation redacts credentials.
pub async fn authenticate(
    transport: &mut dyn Transport,
    auth_methods: &[String],
    method: impl Into<String>,
    credentials: serde_json::Value,
) -> Result<AuthenticateResult, TransportError> {
    let method_str = method.into();

    // Check if authentication is required
    if auth_methods.is_empty() {
        return Ok(AuthenticateResult::NotRequired);
    }

    // Validate that the requested method is supported
    if !auth_methods.contains(&method_str) {
        return Ok(AuthenticateResult::Failed(format!(
            "Authentication method '{}' not supported by agent. Supported methods: {:?}",
            method_str, auth_methods
        )));
    }

    // Create authenticate request
    let request = AuthenticateRequest::new(method_str, credentials);
    let jsonrpc_request = request.to_jsonrpc(2); // Use ID 2 (ID 1 is for initialize)

    // Send request and await response
    let result = transport.send_request(jsonrpc_request).await?;

    // Handle response
    match result {
        JsonRpcResult::Success(response) => {
            // Parse authenticate response
            let auth_response = AuthenticateResponse::from_jsonrpc(&response.result)
                .map_err(|e| TransportError::SerializationError(e))?;

            if auth_response.success {
                Ok(AuthenticateResult::Success)
            } else {
                Ok(AuthenticateResult::Failed(
                    auth_response
                        .error
                        .unwrap_or_else(|| "Authentication failed".to_string()),
                ))
            }
        }
        JsonRpcResult::Error(error_response) => {
            // Authentication failed with JSON-RPC error
            Ok(AuthenticateResult::Failed(error_response.error.message))
        }
    }
}

/// Check if authentication is required based on agent's auth_methods.
pub fn is_auth_required(auth_methods: &[String]) -> bool {
    !auth_methods.is_empty()
}

/// Validate that credentials do not appear in log output.
///
/// This is a utility function for testing that ensures credentials are properly
/// redacted in debug output.
#[cfg(test)]
pub fn ensure_credentials_redacted(debug_str: &str) -> bool {
    !debug_str.contains("secret") && !debug_str.contains("password") && !debug_str.contains("token")
}

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

    #[test]
    fn test_authenticate_request_creation() {
        let creds = serde_json::json!({"api_key": "secret123"});
        let request = AuthenticateRequest::new("api_key", creds.clone());

        assert_eq!(request.method, "api_key");
        assert_eq!(request.credentials, creds);
    }

    #[test]
    fn test_authenticate_request_redacts_credentials() {
        let creds = serde_json::json!({"api_key": "secret123"});
        let request = AuthenticateRequest::new("api_key", creds);

        let debug_str = format!("{:?}", request);

        // Should contain method
        assert!(debug_str.contains("api_key"));
        // Should NOT contain credentials
        assert!(!debug_str.contains("secret123"));
        // Should show redacted marker
        assert!(debug_str.contains("REDACTED"));
    }

    #[test]
    fn test_authenticate_request_to_jsonrpc() {
        let creds = serde_json::json!({"api_key": "secret"});
        let request = AuthenticateRequest::new("api_key", creds);

        let jsonrpc = request.to_jsonrpc(42);

        assert_eq!(jsonrpc.method, "authenticate");
        assert_eq!(jsonrpc.id, serde_json::Value::Number(42.into()));
        assert!(jsonrpc.params.is_some());
    }

    #[test]
    fn test_authenticate_response_success() {
        let json = serde_json::json!({
            "success": true
        });

        let response = AuthenticateResponse::from_jsonrpc(&json).unwrap();

        assert!(response.success);
        assert!(response.error.is_none());
    }

    #[test]
    fn test_authenticate_response_failure() {
        let json = serde_json::json!({
            "success": false,
            "error": "Invalid API key"
        });

        let response = AuthenticateResponse::from_jsonrpc(&json).unwrap();

        assert!(!response.success);
        assert_eq!(response.error, Some("Invalid API key".to_string()));
    }

    #[test]
    fn test_is_auth_required() {
        assert!(!is_auth_required(&[]));
        assert!(is_auth_required(&["api_key".to_string()]));
        assert!(is_auth_required(&["api_key".to_string(), "oauth".to_string()]));
    }

    #[test]
    fn test_authenticate_response_serialization() {
        let response = AuthenticateResponse {
            success: true,
            error: None,
        };

        let json = serde_json::to_string(&response).unwrap();
        let deserialized: AuthenticateResponse = serde_json::from_str(&json).unwrap();

        assert_eq!(response, deserialized);
    }
}
