use axum::{extract::Extension, extract::State, http::StatusCode, response::IntoResponse, Json};
use mongodb::bson::oid::ObjectId;
use serde::{Deserialize, Serialize};
use validator::Validate;

use crate::{
    auth::jwt::Claims, config::AppState, middleware::ClientIp, models::audit_log::AuditEventType,
    models::user::User,
};

#[derive(Debug, Deserialize, Validate)]
pub struct RegisterRequest {
    #[validate(email)]
    pub email: String,
    #[validate(length(min = 3))]
    pub username: String,
    #[validate(length(min = 8))]
    pub password: String,
    /// Optional recovery phrase for password recovery
    pub recovery_phrase: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct AuthResponse {
    pub token: String,
    pub refresh_token: String,
    pub user_id: String,
    pub email: String,
    pub username: String,
}

pub async fn register(
    State(state): State<AppState>,
    Extension(client_ip): Extension<ClientIp>,
    Json(req): Json<RegisterRequest>,
) -> impl IntoResponse {
    if let Err(errors) = req.validate() {
        return (
            StatusCode::BAD_REQUEST,
            Json(serde_json::json!({
                "error": "validation failed",
                "details": errors.to_string()
            })),
        )
            .into_response();
    }

    // Check if user already exists
    match state.db.find_user_by_email(&req.email).await {
        Ok(Some(_)) => {
            return (
                StatusCode::CONFLICT,
                Json(serde_json::json!({
                    "error": "user already exists"
                })),
            )
                .into_response()
        }
        Ok(None) => {}
        Err(e) => {
            tracing::error!("Failed to check user existence: {}", e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({
                    "error": "database error"
                })),
            )
                .into_response();
        }
    }

    // Create new user
    let user = match User::new(
        req.email.clone(),
        req.username.clone(),
        req.password,
        req.recovery_phrase,
    ) {
        Ok(u) => u,
        Err(e) => {
            tracing::error!("Failed to create user: {}", e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({
                    "error": "failed to create user"
                })),
            )
                .into_response();
        }
    };

    // Get token_version before saving
    let token_version = user.token_version;

    // Save user to database
    let user_id = match state.db.create_user(&user).await {
        Ok(Some(id)) => {
            // Log registration (Phase 2.6)
            if let Some(ref audit) = state.audit_logger {
                let _ = audit
                    .log_event(
                        AuditEventType::LoginSuccess, // Using LoginSuccess as registration event
                        Some(id),
                        Some(req.email.clone()),
                        client_ip.as_str().to_string(),
                        None,
                        None,
                    )
                    .await;
            }
            id
        }
        Ok(None) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({
                    "error": "failed to create user"
                })),
            )
                .into_response()
        }
        Err(e) => {
            tracing::error!("Failed to save user: {}", e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({
                    "error": "database error"
                })),
            )
                .into_response();
        }
    };

    // Generate JWT token pair and persist the refresh token.
    let claims = Claims::new(
        user_id.to_string(),
        user.email.clone(),
        token_version,
        state.jwt_service.access_expiry(),
    );
    let (token, refresh_token) = match state.jwt_service.generate_tokens(claims) {
        Ok(t) => t,
        Err(e) => {
            tracing::error!("Failed to generate token: {}", e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({
                    "error": "failed to generate token"
                })),
            )
                .into_response();
        }
    };
    if let Some(ref repo) = state.refresh_token_repo {
        let expires_at = refresh_expiry_bson(&state.jwt_service);
        if let Err(e) = repo
            .create(&user_id.to_string(), &refresh_token, expires_at)
            .await
        {
            tracing::warn!("Failed to persist refresh token on register: {}", e);
        }
    }

    let response = AuthResponse {
        token,
        refresh_token,
        user_id: user_id.to_string(),
        email: user.email,
        username: user.username,
    };

    (StatusCode::CREATED, Json(response)).into_response()
}

#[derive(Debug, Deserialize, Validate)]
pub struct LoginRequest {
    #[validate(email)]
    pub email: String,
    #[validate(length(min = 1))]
    pub password: String,
}

pub async fn login(
    State(state): State<AppState>,
    Extension(client_ip): Extension<ClientIp>,
    Json(req): Json<LoginRequest>,
) -> impl IntoResponse {
    if let Err(errors) = req.validate() {
        return (
            StatusCode::BAD_REQUEST,
            Json(serde_json::json!({
                "error": "validation failed",
                "details": errors.to_string()
            })),
        )
            .into_response();
    }

    // Check account lockout status (Phase 2.6)
    if let Some(ref lockout) = state.account_lockout {
        match lockout.check_lockout(&req.email).await {
            Ok(true) => {
                return (
                    StatusCode::TOO_MANY_REQUESTS,
                    Json(serde_json::json!({
                        "error": "account is temporarily locked due to too many failed attempts",
                        "retry_after": "please try again later"
                    })),
                )
                    .into_response()
            }
            Ok(false) => {}
            Err(e) => {
                tracing::error!("Failed to check lockout status: {}", e);
            }
        }
    }

    // Find user by email
    let user = match state.db.find_user_by_email(&req.email).await {
        Ok(Some(u)) => u,
        Ok(None) => {
            // Record failed attempt (Phase 2.6)
            if let Some(ref lockout) = state.account_lockout {
                let _ = lockout.record_failed_attempt(&req.email).await;
            }

            // Log failed login (Phase 2.6)
            if let Some(ref audit) = state.audit_logger {
                let _ = audit
                    .log_event(
                        AuditEventType::LoginFailed,
                        None,
                        Some(req.email.clone()),
                        client_ip.as_str().to_string(),
                        None,
                        None,
                    )
                    .await;
            }

            return (
                StatusCode::UNAUTHORIZED,
                Json(serde_json::json!({
                    "error": "invalid credentials"
                })),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to find user: {}", e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({
                    "error": "database error"
                })),
            )
                .into_response();
        }
    };

    // A persisted user must have an id; if not, return a clean 500 instead of
    // panicking (the previous ok_or_else(...).unwrap() discarded this response).
    let user_id = match user.id {
        Some(id) => id,
        None => {
            tracing::error!("Login user record has no id for email: {}", req.email);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({
                    "error": "invalid user state"
                })),
            )
                .into_response();
        }
    };

    // Verify password
    match user.verify_password(&req.password) {
        Ok(true) => {
            // Reset failed attempts on successful login (Phase 2.6)
            if let Some(ref lockout) = state.account_lockout {
                let _ = lockout.reset_attempts(&req.email).await;
            }
        }
        Ok(false) => {
            // Record failed attempt (Phase 2.6)
            if let Some(ref lockout) = state.account_lockout {
                let _ = lockout.record_failed_attempt(&req.email).await;
            }

            // Log failed login (Phase 2.6)
            if let Some(ref audit) = state.audit_logger {
                let _ = audit
                    .log_event(
                        AuditEventType::LoginFailed,
                        Some(user_id),
                        Some(req.email.clone()),
                        client_ip.as_str().to_string(),
                        None,
                        None,
                    )
                    .await;
            }

            return (
                StatusCode::UNAUTHORIZED,
                Json(serde_json::json!({
                    "error": "invalid credentials"
                })),
            )
                .into_response();
        }
        Err(e) => {
            tracing::error!("Failed to verify password: {}", e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({
                    "error": "authentication error"
                })),
            )
                .into_response();
        }
    }

    // Update last active timestamp (TODO: Implement in database layer)

    // Generate JWT token pair and persist the refresh token.
    let claims = Claims::new(
        user_id.to_string(),
        user.email.clone(),
        user.token_version,
        state.jwt_service.access_expiry(),
    );
    let (token, refresh_token) = match state.jwt_service.generate_tokens(claims) {
        Ok(t) => t,
        Err(e) => {
            tracing::error!("Failed to generate token: {}", e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({
                    "error": "failed to generate token"
                })),
            )
                .into_response();
        }
    };
    if let Some(ref repo) = state.refresh_token_repo {
        let expires_at = refresh_expiry_bson(&state.jwt_service);
        if let Err(e) = repo
            .create(&user_id.to_string(), &refresh_token, expires_at)
            .await
        {
            tracing::warn!("Failed to persist refresh token on login: {}", e);
        }
    }

    // Log successful login (Phase 2.6)
    if let Some(ref audit) = state.audit_logger {
        let _ = audit
            .log_event(
                AuditEventType::LoginSuccess,
                Some(user_id),
                Some(req.email.clone()),
                client_ip.as_str().to_string(),
                None,
                None,
            )
            .await;
    }

    let response = AuthResponse {
        token,
        refresh_token,
        user_id: user_id.to_string(),
        email: user.email,
        username: user.username,
    };

    (StatusCode::OK, Json(response)).into_response()
}

#[derive(Debug, Deserialize, Validate)]
pub struct RecoverPasswordRequest {
    #[validate(email)]
    pub email: String,
    #[validate(length(min = 1))]
    pub recovery_phrase: String,
    #[validate(length(min = 8))]
    pub new_password: String,
}

pub async fn recover_password(
    State(state): State<AppState>,
    Extension(client_ip): Extension<ClientIp>,
    Json(req): Json<RecoverPasswordRequest>,
) -> impl IntoResponse {
    if let Err(errors) = req.validate() {
        return (
            StatusCode::BAD_REQUEST,
            Json(serde_json::json!({
                "error": "validation failed",
                "details": errors.to_string()
            })),
        )
            .into_response();
    }

    // Find user by email
    let mut user = match state.db.find_user_by_email(&req.email).await {
        Ok(Some(u)) => u,
        Ok(None) => {
            return (
                StatusCode::UNAUTHORIZED,
                Json(serde_json::json!({
                    "error": "invalid credentials"
                })),
            )
                .into_response()
        }
        Err(e) => {
            tracing::error!("Failed to find user: {}", e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({
                    "error": "database error"
                })),
            )
                .into_response();
        }
    };

    // Verify recovery phrase
    match user.verify_recovery_phrase(&req.recovery_phrase) {
        Ok(true) => {}
        Ok(false) => {
            return (
                StatusCode::UNAUTHORIZED,
                Json(serde_json::json!({
                    "error": "invalid credentials"
                })),
            )
                .into_response()
        }
        Err(e) => {
            tracing::error!("Failed to verify recovery phrase: {}", e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({
                    "error": "authentication error"
                })),
            )
                .into_response();
        }
    }

    // Update password
    match user.update_password(req.new_password) {
        Ok(_) => {}
        Err(e) => {
            tracing::error!("Failed to update password: {}", e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({
                    "error": "failed to update password"
                })),
            )
                .into_response();
        }
    }

    // Save updated user
    match state.db.update_user(&user).await {
        Ok(_) => {
            // Password change bumps token_version, so existing access tokens are
            // rejected; also revoke every refresh token so they can't be used to
            // mint new access tokens.
            if let Some(ref repo) = state.refresh_token_repo {
                if let Some(ref uid) = user.id.as_ref().map(|oid| oid.to_string()) {
                    if let Err(e) = repo.revoke_all_by_user(uid).await {
                        tracing::warn!("Failed to revoke refresh tokens on recovery: {}", e);
                    }
                }
            }
            if let Some(ref uid) = user.id {
                state.token_version_cache.invalidate(uid).await;
            }

            // Log password recovery (Phase 2.6)
            if let Some(ref audit) = state.audit_logger {
                let user_id_for_log = user.id;
                let _ = audit
                    .log_event(
                        AuditEventType::PasswordRecovery,
                        user_id_for_log,
                        Some(req.email.clone()),
                        client_ip.as_str().to_string(),
                        None,
                        None,
                    )
                    .await;
            }

            (StatusCode::NO_CONTENT, ()).into_response()
        }
        Err(e) => {
            tracing::error!("Failed to save user: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({
                    "error": "database error"
                })),
            )
                .into_response()
        }
    }
}

/// Compute the BSON expiry timestamp for a freshly issued refresh token, from
/// the configured refresh lifetime on the JWT service.
fn refresh_expiry_bson(jwt: &crate::auth::JwtService) -> mongodb::bson::DateTime {
    let now = std::time::SystemTime::now();
    let expiry = now + jwt.refresh_expiry().to_std().unwrap_or_default();
    mongodb::bson::DateTime::from_system_time(expiry)
}

#[derive(Debug, Deserialize)]
pub struct RefreshRequest {
    pub refresh_token: String,
}

#[derive(Debug, Serialize)]
pub struct RefreshResponse {
    pub token: String,
    pub refresh_token: String,
}

/// Exchange a valid refresh token for a new access/refresh pair (rotation).
///
/// Validation: signature + expiry (JWT), then the token must exist in Mongo,
/// not be revoked, and not have passed its stored `expiresAt`; finally the
/// user's current `token_version` must match the refresh token's claim (so a
/// password change invalidates refresh tokens too). The old refresh token is
/// revoked and a new one issued + stored.
pub async fn refresh(
    State(state): State<AppState>,
    Json(req): Json<RefreshRequest>,
) -> impl IntoResponse {
    // 1. Signature + expiry check on the presented refresh token.
    let refresh_claims = match state.jwt_service.validate_refresh_token(&req.refresh_token) {
        Ok(c) => c,
        Err(_) => {
            return (
                StatusCode::UNAUTHORIZED,
                Json(serde_json::json!({ "error": "invalid refresh token" })),
            )
                .into_response()
        }
    };

    // 2. Must exist and be active in the store.
    let record = match state.refresh_token_repo.as_ref() {
        Some(repo) => match repo.find_by_raw_token(&req.refresh_token).await {
            Ok(Some(r)) => r,
            Ok(None) => {
                return (
                    StatusCode::UNAUTHORIZED,
                    Json(serde_json::json!({ "error": "invalid refresh token" })),
                )
                    .into_response()
            }
            Err(e) => {
                tracing::error!("Refresh token lookup failed: {}", e);
                return (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    Json(serde_json::json!({ "error": "database error" })),
                )
                    .into_response();
            }
        },
        None => {
            tracing::error!("Refresh token repository not configured");
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({ "error": "server misconfiguration" })),
            )
                .into_response();
        }
    };

    if record.revoked {
        return (
            StatusCode::UNAUTHORIZED,
            Json(serde_json::json!({ "error": "refresh token revoked" })),
        )
            .into_response();
    }
    if record.expires_at <= mongodb::bson::DateTime::now() {
        return (
            StatusCode::UNAUTHORIZED,
            Json(serde_json::json!({ "error": "refresh token expired" })),
        )
            .into_response();
    }

    // 3. Load the user and verify token_version matches.
    let user_id = match ObjectId::parse_str(&refresh_claims.sub) {
        Ok(oid) => oid,
        Err(_) => {
            return (
                StatusCode::UNAUTHORIZED,
                Json(serde_json::json!({ "error": "invalid refresh token" })),
            )
                .into_response()
        }
    };
    let user = match state.db.find_user_by_id(&user_id).await {
        Ok(Some(u)) => u,
        Ok(None) => {
            return (
                StatusCode::UNAUTHORIZED,
                Json(serde_json::json!({ "error": "invalid refresh token" })),
            )
                .into_response()
        }
        Err(e) => {
            tracing::error!("Failed to load user during refresh: {}", e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({ "error": "database error" })),
            )
                .into_response();
        }
    };
    if user.token_version != refresh_claims.token_version {
        return (
            StatusCode::UNAUTHORIZED,
            Json(serde_json::json!({ "error": "credentials have changed; please log in again" })),
        )
            .into_response();
    }

    // 4. Rotate: revoke the old refresh token, issue a new pair, store the new
    //    refresh token.
    if let Some(ref repo) = state.refresh_token_repo {
        if let Some(old_id) = record.id {
            if let Err(e) = repo.revoke(&old_id).await {
                tracing::warn!("Failed to revoke old refresh token: {}", e);
            }
        }
    }

    let claims = Claims::new(
        user_id.to_string(),
        user.email.clone(),
        user.token_version,
        state.jwt_service.access_expiry(),
    );
    let (access_token, new_refresh_token) = match state.jwt_service.generate_tokens(claims) {
        Ok(t) => t,
        Err(e) => {
            tracing::error!("Failed to generate token: {}", e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({ "error": "failed to generate token" })),
            )
                .into_response();
        }
    };
    if let Some(ref repo) = state.refresh_token_repo {
        let expires_at = refresh_expiry_bson(&state.jwt_service);
        if let Err(e) = repo
            .create(&user_id.to_string(), &new_refresh_token, expires_at)
            .await
        {
            tracing::warn!("Failed to persist rotated refresh token: {}", e);
        }
    }

    (
        StatusCode::OK,
        Json(RefreshResponse {
            token: access_token,
            refresh_token: new_refresh_token,
        }),
    )
        .into_response()
}

/// Revoke a single refresh token (client-side logout). The access token remains
/// valid until its short expiry; this prevents the refresh token from minting
/// any further access tokens.
pub async fn logout(
    State(state): State<AppState>,
    Json(req): Json<RefreshRequest>,
) -> impl IntoResponse {
    if let Some(ref repo) = state.refresh_token_repo {
        match repo.find_by_raw_token(&req.refresh_token).await {
            Ok(Some(record)) => {
                if let Some(id) = record.id {
                    if let Err(e) = repo.revoke(&id).await {
                        tracing::warn!("Failed to revoke refresh token on logout: {}", e);
                        return (
                            StatusCode::INTERNAL_SERVER_ERROR,
                            Json(serde_json::json!({ "error": "database error" })),
                        )
                            .into_response();
                    }
                }
            }
            Ok(None) => {
                // Already gone or never existed — treat as success (idempotent).
            }
            Err(e) => {
                tracing::error!("Refresh token lookup failed on logout: {}", e);
                return (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    Json(serde_json::json!({ "error": "database error" })),
                )
                    .into_response();
            }
        }
    }

    StatusCode::NO_CONTENT.into_response()
}
