//! Helper functions for authentication operations.
//!
//! This module provides HTTP API client functions for communicating with
//! the Control Center authentication service.

use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
use std::io::{self, Write};

use crate::error::{AuthError, AuthErrorKind};

/// Request payload for login endpoint.
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginRequest {
    /// Username for authentication
    pub username: String,
    /// Password for authentication
    pub password: String,
}

/// Response from login endpoint containing JWT tokens.
#[derive(Debug, Serialize, Deserialize)]
pub struct TokenResponse {
    /// JWT access token
    pub access_token: String,
    /// JWT refresh token
    pub refresh_token: String,
    /// Token expiration time in seconds
    pub expires_in: i64,
    /// User information
    pub user: UserInfo,
}

/// User information from login response.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfo {
    /// User ID
    pub id: String,
    /// Username
    pub username: String,
    /// Email address
    pub email: String,
    /// User roles
    pub roles: Vec<String>,
}

/// Request payload for logout endpoint.
#[derive(Debug, Serialize)]
pub struct LogoutRequest {
    /// Access token to revoke
    pub access_token: String,
}

/// Session information for auth sessions command.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
    /// Session ID
    pub id: String,
    /// User ID
    pub user_id: String,
    /// Username
    pub username: String,
    /// User roles
    pub roles: Vec<String>,
    /// Session creation time (ISO 8601)
    pub created_at: String,
    /// Session expiration time (ISO 8601)
    pub expires_at: String,
    /// Whether session is currently active
    pub is_active: bool,
    /// IP address of session origin
    #[serde(default)]
    pub ip_address: String,
    /// User agent of session
    #[serde(default)]
    pub user_agent: String,
}

/// Token verification response.
#[derive(Debug, Serialize, Deserialize)]
pub struct VerifyResponse {
    /// Whether the token is valid
    pub valid: bool,
    /// User ID if valid
    pub user_id: Option<String>,
    /// Username if valid
    pub username: Option<String>,
    /// Roles if valid
    pub roles: Option<Vec<String>>,
    /// Expiration time (ISO 8601) if valid
    pub expires_at: Option<String>,
}

/// MFA enrollment request payload.
#[derive(Debug, Serialize)]
pub struct MfaEnrollRequest {
    /// MFA type: "totp" or "webauthn"
    pub mfa_type: String,
}

/// MFA enrollment response with secret and QR code.
#[derive(Debug, Deserialize)]
pub struct MfaEnrollResponse {
    /// TOTP secret (base32 encoded)
    pub secret: String,
    /// QR code URI for authenticator apps
    pub qr_code_uri: String,
    /// Backup recovery codes
    pub backup_codes: Vec<String>,
}

/// MFA verification request payload.
#[derive(Debug, Serialize)]
pub struct MfaVerifyRequest {
    /// 6-digit TOTP code
    pub code: String,
}

// =============================================================================
// HTTP Client Functions
// =============================================================================

/// Creates a configured HTTP client with appropriate timeout settings.
fn create_client() -> Result<Client, AuthError> {
    Client::builder()
        .timeout(std::time::Duration::from_secs(30))
        .build()
        .map_err(|e| AuthError::network_error(format!("Failed to create HTTP client: {}", e)))
}

/// Sends a login request to the Control Center.
///
/// # Arguments
///
/// * `url` - Base URL of the Control Center
/// * `username` - Username for authentication
/// * `password` - Password for authentication
///
/// # Returns
///
/// Returns `TokenResponse` containing JWT tokens and user info on success.
pub fn send_login_request(
    url: &str,
    username: &str,
    password: &str,
) -> Result<TokenResponse, AuthError> {
    let client = create_client()?;

    let response = client
        .post(format!("{}/auth/login", url))
        .json(&LoginRequest {
            username: username.to_string(),
            password: password.to_string(),
        })
        .send()
        .map_err(|e| AuthError::network_error(format!("HTTP request failed: {}", e)))?;

    let status = response.status();
    if !status.is_success() {
        let error_text = response
            .text()
            .unwrap_or_else(|_| "Unknown error".to_string());

        return if status.as_u16() == 401 {
            Err(AuthError::invalid_credentials(error_text))
        } else {
            Err(AuthError::server_error(format!(
                "HTTP {} - {}",
                status, error_text
            )))
        };
    }

    response
        .json::<TokenResponse>()
        .map_err(|e| AuthError::server_error(format!("Failed to parse response: {}", e)))
}

/// Sends a logout request to the Control Center.
///
/// # Arguments
///
/// * `url` - Base URL of the Control Center
/// * `access_token` - Access token to revoke
pub fn send_logout_request(url: &str, access_token: &str) -> Result<(), AuthError> {
    let client = create_client()?;

    let response = client
        .post(format!("{}/auth/logout", url))
        .bearer_auth(access_token)
        .json(&LogoutRequest {
            access_token: access_token.to_string(),
        })
        .send()
        .map_err(|e| AuthError::network_error(format!("HTTP request failed: {}", e)))?;

    if !response.status().is_success() {
        let status = response.status();
        let error_text = response
            .text()
            .unwrap_or_else(|_| "Unknown error".to_string());
        return Err(AuthError::server_error(format!(
            "Logout failed: HTTP {} - {}",
            status, error_text
        )));
    }

    Ok(())
}

/// Verifies a token with the Control Center.
///
/// # Arguments
///
/// * `url` - Base URL of the Control Center
/// * `token` - Token to verify
pub fn verify_token(url: &str, token: &str) -> Result<VerifyResponse, AuthError> {
    let client = create_client()?;

    let response = client
        .get(format!("{}/auth/verify", url))
        .bearer_auth(token)
        .send()
        .map_err(|e| AuthError::network_error(format!("HTTP request failed: {}", e)))?;

    if !response.status().is_success() {
        return Ok(VerifyResponse {
            valid: false,
            user_id: None,
            username: None,
            roles: None,
            expires_at: None,
        });
    }

    response
        .json::<VerifyResponse>()
        .map_err(|e| AuthError::server_error(format!("Failed to parse response: {}", e)))
}

/// Lists active sessions for the authenticated user.
///
/// # Arguments
///
/// * `url` - Base URL of the Control Center
/// * `token` - Access token for authentication
/// * `active_only` - If true, only return active sessions
pub fn list_sessions(
    url: &str,
    token: &str,
    active_only: bool
) -> Result<Vec<SessionInfo>, AuthError> {
    let client = create_client()?;

    let mut request = client
        .get(format!("{}/auth/sessions", url))
        .bearer_auth(token);

    if active_only {
        request = request.query(&[("active", "true")]);
    }

    let response = request
        .send()
        .map_err(|e| AuthError::network_error(format!("HTTP request failed: {}", e)))?;

    if !response.status().is_success() {
        let status = response.status();
        return Err(AuthError::server_error(format!(
            "Failed to list sessions: HTTP {}",
            status
        )));
    }

    response
        .json::<Vec<SessionInfo>>()
        .map_err(|e| AuthError::server_error(format!("Failed to parse response: {}", e)))
}

// =============================================================================
// MFA Functions
// =============================================================================

/// Sends MFA enrollment request to the Control Center.
///
/// # Arguments
///
/// * `url` - Base URL of the Control Center
/// * `access_token` - Access token for authentication
/// * `mfa_type` - Type of MFA to enroll ("totp" or "webauthn")
pub fn send_mfa_enroll_request(
    url: &str,
    access_token: &str,
    mfa_type: &str,
) -> Result<MfaEnrollResponse, AuthError> {
    let client = create_client()?;

    let response = client
        .post(format!("{}/mfa/enroll/{}", url, mfa_type))
        .bearer_auth(access_token)
        .json(&MfaEnrollRequest {
            mfa_type: mfa_type.to_string(),
        })
        .send()
        .map_err(|e| AuthError::network_error(format!("HTTP request failed: {}", e)))?;

    if !response.status().is_success() {
        let status = response.status();
        let error_text = response
            .text()
            .unwrap_or_else(|_| "Unknown error".to_string());
        return Err(AuthError::mfa_failed(format!(
            "MFA enrollment failed: HTTP {} - {}",
            status, error_text
        )));
    }

    response
        .json::<MfaEnrollResponse>()
        .map_err(|e| AuthError::server_error(format!("Failed to parse response: {}", e)))
}

/// Sends MFA verification request to the Control Center.
///
/// # Arguments
///
/// * `url` - Base URL of the Control Center
/// * `access_token` - Access token for authentication
/// * `code` - 6-digit TOTP code
pub fn send_mfa_verify_request(
    url: &str,
    access_token: &str,
    code: &str,
) -> Result<bool, AuthError> {
    let client = create_client()?;

    let response = client
        .post(format!("{}/mfa/verify", url))
        .bearer_auth(access_token)
        .json(&MfaVerifyRequest {
            code: code.to_string(),
        })
        .send()
        .map_err(|e| AuthError::network_error(format!("HTTP request failed: {}", e)))?;

    Ok(response.status().is_success())
}

// =============================================================================
// Password Input Functions
// =============================================================================

/// Prompts for password without echoing to terminal.
///
/// # Arguments
///
/// * `prompt` - The prompt text to display
///
/// # Returns
///
/// Returns the entered password as a string.
pub fn prompt_password(prompt: &str) -> Result<String, AuthError> {
    print!("{}", prompt);
    io::stdout()
        .flush()
        .map_err(|e| AuthError::new(AuthErrorKind::InternalError, format!("Flush error: {}", e)))?;

    rpassword::read_password()
        .map_err(|e| AuthError::new(AuthErrorKind::InternalError, format!("Password read error: {}", e)))
}

// =============================================================================
// QR Code Functions
// =============================================================================

/// Generates a QR code string for terminal display.
///
/// # Arguments
///
/// * `uri` - The TOTP URI to encode in the QR code
///
/// # Returns
///
/// Returns a string representation of the QR code using Unicode block characters.
pub fn generate_qr_code(uri: &str) -> Result<String, AuthError> {
    use qrcode::render::unicode;
    use qrcode::QrCode;

    let code = QrCode::new(uri)
        .map_err(|e| AuthError::new(AuthErrorKind::InternalError, format!("QR code generation failed: {}", e)))?;

    let qr_string = code
        .render::<unicode::Dense1x2>()
        .dark_color(unicode::Dense1x2::Light)
        .light_color(unicode::Dense1x2::Dark)
        .build();

    Ok(qr_string)
}

/// Displays QR code in terminal with instructions.
///
/// # Arguments
///
/// * `uri` - The TOTP URI to display
pub fn display_qr_code(uri: &str) -> Result<(), AuthError> {
    let qr = generate_qr_code(uri)?;
    println!("\n{}\n", qr);
    println!("Scan this QR code with your authenticator app");

    if let Ok(secret) = extract_secret(uri) {
        println!("Or enter this secret manually: {}", secret);
    }

    Ok(())
}

/// Extracts the secret from a TOTP URI.
///
/// # Arguments
///
/// * `uri` - The TOTP URI (e.g., "otpauth://totp/...?secret=ABC123&...")
fn extract_secret(uri: &str) -> Result<String, AuthError> {
    uri.split("secret=")
        .nth(1)
        .and_then(|s| s.split('&').next())
        .map(|s| s.to_string())
        .ok_or_else(|| AuthError::new(
            AuthErrorKind::InternalError,
            "Failed to extract secret from URI",
        ))
}

// =============================================================================
// Nushell Value Conversion Helpers
// =============================================================================

use nu_protocol::{record, Span, Value};


/// Converts a SessionInfo to a Nushell Value record.
pub fn session_info_to_value(session: &SessionInfo, span: Span) -> Value {
    Value::record(
        record! {
            "id" => Value::string(&session.id, span),
            "user_id" => Value::string(&session.user_id, span),
            "username" => Value::string(&session.username, span),
            "roles" => Value::list(
                session.roles.iter().map(|r| Value::string(r, span)).collect(),
                span
            ),
            "created_at" => Value::string(&session.created_at, span),
            "expires_at" => Value::string(&session.expires_at, span),
            "is_active" => Value::bool(session.is_active, span),
            "ip_address" => Value::string(&session.ip_address, span),
            "user_agent" => Value::string(&session.user_agent, span),
        },
        span,
    )
}

/// Converts a VerifyResponse to a Nushell Value record.
pub fn verify_response_to_value(response: &VerifyResponse, span: Span) -> Value {
    Value::record(
        record! {
            "valid" => Value::bool(response.valid, span),
            "user_id" => response.user_id.as_ref()
                .map(|s| Value::string(s, span))
                .unwrap_or(Value::nothing(span)),
            "username" => response.username.as_ref()
                .map(|s| Value::string(s, span))
                .unwrap_or(Value::nothing(span)),
            "roles" => response.roles.as_ref()
                .map(|roles| Value::list(
                    roles.iter().map(|r| Value::string(r, span)).collect(),
                    span
                ))
                .unwrap_or(Value::nothing(span)),
            "expires_at" => response.expires_at.as_ref()
                .map(|s| Value::string(s, span))
                .unwrap_or(Value::nothing(span)),
        },
        span,
    )
}

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

    #[test]
    fn test_extract_secret() {
        let uri = "otpauth://totp/Provisioning:admin?secret=JBSWY3DPEHPK3PXP&issuer=Provisioning";
        let secret = extract_secret(uri).unwrap();
        assert_eq!(secret, "JBSWY3DPEHPK3PXP");
    }

    #[test]
    fn test_extract_secret_missing() {
        let uri = "otpauth://totp/Provisioning:admin?issuer=Provisioning";
        let result = extract_secret(uri);
        assert!(result.is_err());
    }

    #[test]
    fn test_user_info_to_value() {
        let user = UserInfo {
            id: "user-123".to_string(),
            username: "admin".to_string(),
            email: "admin@example.com".to_string(),
            roles: vec!["admin".to_string()],
        };
        let value = user_info_to_value(&user, Span::test_data());
        assert!(matches!(value, Value::Record { .. }));
    }

    #[test]
    fn test_session_info_to_value() {
        let session = SessionInfo {
            id: "sess-123".to_string(),
            user_id: "user-123".to_string(),
            username: "admin".to_string(),
            roles: vec!["admin".to_string()],
            created_at: "2024-01-01T00:00:00Z".to_string(),
            expires_at: "2024-01-02T00:00:00Z".to_string(),
            is_active: true,
            ip_address: "127.0.0.1".to_string(),
            user_agent: "test".to_string(),
        };
        let value = session_info_to_value(&session, Span::test_data());
        assert!(matches!(value, Value::Record { .. }));
    }
}
