//! Authentication handlers for user registration, login, and session management

use crate::error::{ApiError, ApiResult};
use crate::state::AppState;
use argon2::{
    password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
    Argon2,
};
use axum::{extract::State, http::StatusCode, Extension, Json};
use chrono::Utc;
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// User registration request
#[derive(Debug, Deserialize)]
pub struct RegisterRequest {
    /// User email
    pub email: String,
    /// User password
    pub password: String,
    /// User name
    pub name: String,
    /// Optional tenant ID for multi-tenancy
    pub tenant_id: Option<String>,
}

/// Login request
#[derive(Debug, Deserialize)]
pub struct LoginRequest {
    /// User email
    pub email: String,
    /// User password
    pub password: String,
}

/// Authentication response
#[derive(Debug, Serialize)]
pub struct AuthResponse {
    /// User ID
    pub user_id: String,
    /// User email
    pub email: String,
    /// User name
    pub name: String,
    /// User role
    pub role: String,
    /// JWT token
    pub token: String,
    /// Token expiration timestamp
    pub expires_at: String,
}

/// User info response
#[derive(Debug, Serialize)]
pub struct UserInfo {
    /// User ID
    pub id: String,
    /// User email
    pub email: String,
    /// User name
    pub name: String,
    /// User role
    pub role: String,
    /// Tenant ID
    pub tenant_id: Option<String>,
    /// Account created timestamp
    pub created_at: String,
}

/// Hash a password using Argon2
fn hash_password(password: &str) -> Result<String, ApiError> {
    let salt = SaltString::generate(&mut OsRng);
    let argon2 = Argon2::default();

    argon2
        .hash_password(password.as_bytes(), &salt)
        .map(|hash| hash.to_string())
        .map_err(|e| ApiError::Internal(format!("Password hashing failed: {}", e)))
}

/// Verify a password against a hash
fn verify_password(password: &str, hash: &str) -> Result<bool, ApiError> {
    let parsed_hash = PasswordHash::new(hash)
        .map_err(|e| ApiError::Internal(format!("Invalid password hash: {}", e)))?;

    let argon2 = Argon2::default();
    Ok(argon2
        .verify_password(password.as_bytes(), &parsed_hash)
        .is_ok())
}

/// Generate JWT token using JwtProvider
///
/// Creates a properly signed JWT token with claims extracted from user data
async fn generate_jwt_token(
    state: &AppState,
    user_id: &str,
    role: &str,
    tenant_id: Option<&str>,
) -> ApiResult<(String, String)> {
    let jwt_provider = state
        .jwt_provider
        .as_ref()
        .ok_or_else(|| ApiError::Internal("JWT provider not configured".to_string()))?;

    // Create custom claims with role and optional tenant_id
    let expiration_seconds = jwt_provider.expiration_seconds();
    let mut claims = shared_api_lib::auth::jwt::Claims::new(
        user_id,
        chrono::Duration::seconds(expiration_seconds),
    )
    .with_role(role);

    if let Some(tenant) = tenant_id {
        claims = claims.with_tenant_id(tenant);
    }

    // Encode the JWT token
    let token = jwt_provider
        .encode_token(&claims)
        .map_err(|e| ApiError::Internal(format!("Token generation failed: {}", e)))?;

    // Calculate expiration time from claims
    let expires_at = chrono::DateTime::<Utc>::from_timestamp(claims.exp, 0)
        .map(|dt| dt.to_rfc3339())
        .unwrap_or_else(|| Utc::now().to_rfc3339());

    Ok((token, expires_at))
}

/// Register a new user
///
/// Creates a new user account with hashed password and returns an authentication token.
pub async fn register(
    State(state): State<AppState>,
    Json(req): Json<RegisterRequest>,
) -> ApiResult<(StatusCode, Json<AuthResponse>)> {
    // Validate email format
    if !req.email.contains('@') {
        return Err(ApiError::BadRequest("Invalid email format".to_string()));
    }

    // Validate password strength (minimum 8 characters)
    if req.password.len() < 8 {
        return Err(ApiError::BadRequest(
            "Password must be at least 8 characters".to_string(),
        ));
    }

    // Check if user already exists
    let existing_user = sqlx::query("SELECT id FROM users WHERE email = ?")
        .bind(req.email.clone())
        .fetch_optional(state.db.pool())
        .await
        .map_err(|e| ApiError::Database(e.to_string()))?;

    if existing_user.is_some() {
        return Err(ApiError::Conflict(
            "User with this email already exists".to_string(),
        ));
    }

    // Hash password
    let password_hash = hash_password(&req.password)?;

    // Create user
    let user_id = Uuid::new_v4().to_string();
    let now = Utc::now().to_rfc3339();
    let role = "user";

    sqlx::query(
        r#"
        INSERT INTO users (id, email, password_hash, name, role, tenant_id, created_at, updated_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        "#,
    )
    .bind(&user_id)
    .bind(&req.email)
    .bind(&password_hash)
    .bind(&req.name)
    .bind(role)
    .bind(&req.tenant_id)
    .bind(&now)
    .bind(&now)
    .execute(state.db.pool())
    .await
    .map_err(|e| ApiError::Database(e.to_string()))?;

    // Generate JWT token
    let (token, expires_at) =
        generate_jwt_token(&state, &user_id, role, req.tenant_id.as_deref()).await?;

    // Broadcast user authenticated event
    state
        .events
        .broadcast(crate::websocket::ProjectEvent::UserAuthenticated {
            user_id: user_id.clone(),
            user_email: req.email.clone(),
            timestamp: now.clone(),
        });

    Ok((
        StatusCode::CREATED,
        Json(AuthResponse {
            user_id,
            email: req.email,
            name: req.name,
            role: role.to_string(),
            token,
            expires_at,
        }),
    ))
}

/// Login an existing user
///
/// Authenticates a user with email and password, returns a session token.
pub async fn login(
    State(state): State<AppState>,
    Json(req): Json<LoginRequest>,
) -> ApiResult<Json<AuthResponse>> {
    // Find user by email
    let user: (String, String, String, String, String) =
        sqlx::query_as("SELECT id, email, password_hash, name, role FROM users WHERE email = ?")
            .bind(&req.email)
            .fetch_optional(state.db.pool())
            .await
            .map_err(|e| ApiError::Database(e.to_string()))?
            .ok_or_else(|| ApiError::Unauthorized("Invalid email or password".to_string()))?;

    // Verify password
    if !verify_password(&req.password, &user.2)? {
        return Err(ApiError::Unauthorized(
            "Invalid email or password".to_string(),
        ));
    }

    // Generate JWT token
    let (token, expires_at) = generate_jwt_token(&state, &user.0, &user.4, None).await?;
    let now = Utc::now().to_rfc3339();

    // Broadcast user authenticated event
    state
        .events
        .broadcast(crate::websocket::ProjectEvent::UserAuthenticated {
            user_id: user.0.clone(),
            user_email: user.1.clone(),
            timestamp: now,
        });

    Ok(Json(AuthResponse {
        user_id: user.0,
        email: user.1,
        name: user.3,
        role: user.4,
        token,
        expires_at,
    }))
}

/// Logout request (typically just closes client-side session)
#[derive(Debug, Deserialize)]
pub struct LogoutRequest {
    /// Token to revoke (optional - can extract from Authorization header instead)
    #[allow(dead_code)]
    pub token: Option<String>,
}

/// Logout current user
///
/// Invalidates/revokes the current JWT token by adding it to the revoked_tokens table.
pub async fn logout(
    State(state): State<AppState>,
    Extension(claims): Extension<shared_api_lib::auth::jwt::Claims>,
    Json(_req): Json<LogoutRequest>,
) -> ApiResult<StatusCode> {
    // Get the JWT ID from claims (or generate one)
    // For now, use the expiration time as a proxy for jti
    let token_jti = format!("{}_{}", claims.sub, claims.exp);
    let revoked_at = Utc::now().to_rfc3339();
    let expires_at = chrono::DateTime::<Utc>::from_timestamp(claims.exp, 0)
        .map(|dt| dt.to_rfc3339())
        .unwrap_or_else(|| Utc::now().to_rfc3339());

    // Add token to revoked_tokens table
    sqlx::query(
        r#"
        INSERT INTO revoked_tokens (id, token_jti, user_id, reason, revoked_at, expires_at)
        VALUES (?, ?, ?, 'logout', ?, ?)
        "#,
    )
    .bind(Uuid::new_v4().to_string())
    .bind(&token_jti)
    .bind(&claims.sub)
    .bind(&revoked_at)
    .bind(&expires_at)
    .execute(state.db.pool())
    .await
    .map_err(|e| ApiError::Database(format!("Failed to revoke token: {}", e)))?;

    Ok(StatusCode::NO_CONTENT)
}

/// Get current user information
///
/// Returns information about the authenticated user extracted from JWT claims.
/// Requires valid JWT token in Authorization header.
pub async fn current_user(
    State(state): State<AppState>,
    Extension(claims): Extension<shared_api_lib::auth::jwt::Claims>,
) -> ApiResult<Json<UserInfo>> {
    // Extract user ID from JWT claims
    let user_id = &claims.sub;

    // Look up user in database to get full details
    let user: (String, String, String, String, Option<String>, String) = sqlx::query_as(
        "SELECT id, email, name, role, tenant_id, created_at FROM users WHERE id = ?",
    )
    .bind(user_id)
    .fetch_optional(state.db.pool())
    .await
    .map_err(|e| ApiError::Database(e.to_string()))?
    .ok_or_else(|| ApiError::NotFound(format!("User not found: {}", user_id)))?;

    Ok(Json(UserInfo {
        id: user.0,
        email: user.1,
        name: user.2,
        role: user.3,
        tenant_id: user.4,
        created_at: user.5,
    }))
}

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

    #[test]
    fn test_password_hashing() {
        let password = "test_password_123";
        let hash = hash_password(password).expect("Failed to hash password");

        assert!(verify_password(password, &hash).expect("Failed to verify"));
        assert!(!verify_password("wrong_password", &hash).expect("Failed to verify"));
    }

    #[test]
    fn test_password_verification_invalid() {
        let password = "correct_password";
        let hash = hash_password(password).expect("Failed to hash");

        let result = verify_password("wrong_password", &hash).expect("Verification failed");
        assert!(!result);
    }

    #[test]
    fn test_register_request_validation() {
        let req = RegisterRequest {
            email: "test@example.com".to_string(),
            password: "short".to_string(),
            name: "Test User".to_string(),
            tenant_id: None,
        };

        // Password too short
        assert!(req.password.len() < 8);
    }
}
