//! User-related database queries.

use sqlx::{PgConnection, PgPool};
use uuid::Uuid;

use crate::models::{AgeCategory, GenderPreference, StaffMember, User, UserWithAuth};
use chattyness_error::AppError;

/// Get a user by their ID.
pub async fn get_user_by_id(pool: &PgPool, id: Uuid) -> Result<Option<User>, AppError> {
    let user = sqlx::query_as::<_, User>(
        r#"
        SELECT
            id,
            username,
            email,
            display_name,
            bio,
            avatar_url,
            birthday,
            gender_preference,
            age_category,
            reputation_tier,
            status,
            email_verified,
            tags,
            created_at,
            updated_at
        FROM auth.users
        WHERE id = $1 AND status = 'active'
        "#,
    )
    .bind(id)
    .fetch_optional(pool)
    .await?;

    Ok(user)
}

/// Get a user by their username.
pub async fn get_user_by_username(pool: &PgPool, username: &str) -> Result<Option<User>, AppError> {
    let user = sqlx::query_as::<_, User>(
        r#"
        SELECT
            id,
            username,
            email,
            display_name,
            bio,
            avatar_url,
            birthday,
            gender_preference,
            age_category,
            reputation_tier,
            status,
            email_verified,
            tags,
            created_at,
            updated_at
        FROM auth.users
        WHERE username = $1 AND status = 'active'
        "#,
    )
    .bind(username)
    .fetch_optional(pool)
    .await?;

    Ok(user)
}

/// Get a user by their email.
pub async fn get_user_by_email(pool: &PgPool, email: &str) -> Result<Option<User>, AppError> {
    let user = sqlx::query_as::<_, User>(
        r#"
        SELECT
            id,
            username,
            email,
            display_name,
            bio,
            avatar_url,
            birthday,
            gender_preference,
            age_category,
            reputation_tier,
            status,
            email_verified,
            tags,
            created_at,
            updated_at
        FROM auth.users
        WHERE lower(email) = lower($1) AND status = 'active'
        "#,
    )
    .bind(email)
    .fetch_optional(pool)
    .await?;

    Ok(user)
}

/// Row type for password verification query.
#[derive(sqlx::FromRow)]
struct PasswordRow {
    id: Uuid,
    password_hash: Option<String>,
}

/// Verify a user's password and return the user if valid.
pub async fn verify_password(
    pool: &PgPool,
    username: &str,
    password: &str,
) -> Result<Option<User>, AppError> {
    // First get the password hash
    let row = sqlx::query_as::<_, PasswordRow>(
        r#"
        SELECT id, password_hash
        FROM auth.users
        WHERE username = $1 AND status = 'active' AND auth_provider = 'local'
        "#,
    )
    .bind(username)
    .fetch_optional(pool)
    .await?;

    let Some(row) = row else {
        return Ok(None);
    };

    let Some(ref password_hash) = row.password_hash else {
        return Ok(None);
    };

    // Verify the password using argon2
    use argon2::{Argon2, PasswordHash, PasswordVerifier};
    let parsed_hash = PasswordHash::new(password_hash)
        .map_err(|e| AppError::Internal(format!("Invalid password hash: {}", e)))?;

    if Argon2::default()
        .verify_password(password.as_bytes(), &parsed_hash)
        .is_err()
    {
        return Ok(None);
    }

    // Password is valid, fetch the full user
    get_user_by_id(pool, row.id).await
}

/// Create a new user session.
pub async fn create_session(
    pool: &PgPool,
    user_id: Uuid,
    token_hash: &str,
    user_agent: Option<&str>,
    ip_address: Option<std::net::IpAddr>,
    expires_at: chrono::DateTime<chrono::Utc>,
) -> Result<Uuid, AppError> {
    let ip_str = ip_address.map(|ip| ip.to_string());

    let session_id: (Uuid,) = sqlx::query_as(
        r#"
        INSERT INTO auth.sessions (user_id, token_hash, user_agent, ip_address, expires_at)
        VALUES ($1, $2, $3, $4::inet, $5)
        RETURNING id
        "#,
    )
    .bind(user_id)
    .bind(token_hash)
    .bind(user_agent)
    .bind(ip_str)
    .bind(expires_at)
    .fetch_one(pool)
    .await?;

    Ok(session_id.0)
}

/// Get a user by their session token hash.
pub async fn get_user_by_session(
    pool: &PgPool,
    token_hash: &str,
) -> Result<Option<User>, AppError> {
    let user = sqlx::query_as::<_, User>(
        r#"
        SELECT
            u.id,
            u.username,
            u.email,
            u.display_name,
            u.bio,
            u.avatar_url,
            u.birthday,
            u.gender_preference,
            u.age_category,
            u.reputation_tier,
            u.status,
            u.email_verified,
            u.tags,
            u.created_at,
            u.updated_at
        FROM auth.users u
        JOIN auth.sessions s ON u.id = s.user_id
        WHERE s.token_hash = $1
          AND s.expires_at > now()
          AND u.status = 'active'
        "#,
    )
    .bind(token_hash)
    .fetch_optional(pool)
    .await?;

    // Update last activity
    if user.is_some() {
        sqlx::query("UPDATE auth.sessions SET last_activity_at = now() WHERE token_hash = $1")
            .bind(token_hash)
            .execute(pool)
            .await?;
    }

    Ok(user)
}

/// Delete a session by token hash.
pub async fn delete_session(pool: &PgPool, token_hash: &str) -> Result<(), AppError> {
    sqlx::query("DELETE FROM auth.sessions WHERE token_hash = $1")
        .bind(token_hash)
        .execute(pool)
        .await?;
    Ok(())
}

/// Update a user's last seen timestamp.
pub async fn update_last_seen(pool: &PgPool, user_id: Uuid) -> Result<(), AppError> {
    sqlx::query("UPDATE auth.users SET last_seen_at = now() WHERE id = $1")
        .bind(user_id)
        .execute(pool)
        .await?;
    Ok(())
}

/// Get a user with auth fields for login verification.
pub async fn get_user_with_auth(
    pool: &PgPool,
    username: &str,
) -> Result<Option<UserWithAuth>, AppError> {
    let user = sqlx::query_as::<_, UserWithAuth>(
        r#"
        SELECT
            id,
            username,
            email,
            display_name,
            avatar_url,
            status,
            force_pw_reset,
            password_hash
        FROM auth.users
        WHERE username = $1 AND auth_provider = 'local'
        "#,
    )
    .bind(username)
    .fetch_optional(pool)
    .await?;

    Ok(user)
}

/// Verify password and return user with auth info.
pub async fn verify_password_with_reset_flag(
    pool: &PgPool,
    username: &str,
    password: &str,
) -> Result<Option<UserWithAuth>, AppError> {
    let user = get_user_with_auth(pool, username).await?;

    let Some(user) = user else {
        return Ok(None);
    };

    let Some(ref password_hash) = user.password_hash else {
        return Ok(None);
    };

    // Verify the password using argon2
    use argon2::{Argon2, PasswordHash, PasswordVerifier};
    let parsed_hash = PasswordHash::new(password_hash)
        .map_err(|e| AppError::Internal(format!("Invalid password hash: {}", e)))?;

    if Argon2::default()
        .verify_password(password.as_bytes(), &parsed_hash)
        .is_err()
    {
        return Ok(None);
    }

    Ok(Some(user))
}

/// Update a user's password.
pub async fn update_password(
    pool: &PgPool,
    user_id: Uuid,
    new_password: &str,
) -> Result<(), AppError> {
    use argon2::{
        Argon2, PasswordHasher,
        password_hash::{SaltString, rand_core::OsRng},
    };

    let salt = SaltString::generate(&mut OsRng);
    let argon2 = Argon2::default();
    let password_hash = argon2
        .hash_password(new_password.as_bytes(), &salt)
        .map_err(|e| AppError::Internal(format!("Failed to hash password: {}", e)))?
        .to_string();

    sqlx::query(
        r#"
        UPDATE auth.users
        SET password_hash = $1, force_pw_reset = false, updated_at = now()
        WHERE id = $2
        "#,
    )
    .bind(&password_hash)
    .bind(user_id)
    .execute(pool)
    .await?;

    Ok(())
}

/// Update a user's password using a connection (for RLS support).
pub async fn update_password_conn(
    conn: &mut sqlx::PgConnection,
    user_id: Uuid,
    new_password: &str,
) -> Result<(), AppError> {
    use argon2::{
        Argon2, PasswordHasher,
        password_hash::{SaltString, rand_core::OsRng},
    };

    let salt = SaltString::generate(&mut OsRng);
    let argon2 = Argon2::default();
    let password_hash = argon2
        .hash_password(new_password.as_bytes(), &salt)
        .map_err(|e| AppError::Internal(format!("Failed to hash password: {}", e)))?
        .to_string();

    sqlx::query(
        r#"
        UPDATE auth.users
        SET password_hash = $1, force_pw_reset = false, updated_at = now()
        WHERE id = $2
        "#,
    )
    .bind(&password_hash)
    .bind(user_id)
    .execute(conn)
    .await?;

    Ok(())
}

/// Clear the force_pw_reset flag for a user.
pub async fn clear_force_pw_reset(pool: &PgPool, user_id: Uuid) -> Result<(), AppError> {
    sqlx::query(
        r#"
        UPDATE auth.users
        SET force_pw_reset = false, updated_at = now()
        WHERE id = $1
        "#,
    )
    .bind(user_id)
    .execute(pool)
    .await?;

    Ok(())
}

/// Check if a username already exists.
pub async fn username_exists(pool: &PgPool, username: &str) -> Result<bool, AppError> {
    let (exists,): (bool,) = sqlx::query_as(
        r#"
        SELECT EXISTS(SELECT 1 FROM auth.users WHERE username = $1)
        "#,
    )
    .bind(username)
    .fetch_one(pool)
    .await?;

    Ok(exists)
}

/// Check if an email already exists.
pub async fn email_exists(pool: &PgPool, email: &str) -> Result<bool, AppError> {
    let (exists,): (bool,) = sqlx::query_as(
        r#"
        SELECT EXISTS(SELECT 1 FROM auth.users WHERE lower(email) = lower($1))
        "#,
    )
    .bind(email)
    .fetch_one(pool)
    .await?;

    Ok(exists)
}

/// Create a new user with hashed password.
pub async fn create_user(
    pool: &PgPool,
    username: &str,
    email: Option<&str>,
    display_name: &str,
    password: &str,
) -> Result<Uuid, AppError> {
    use argon2::{
        Argon2, PasswordHasher,
        password_hash::{SaltString, rand_core::OsRng},
    };

    let salt = SaltString::generate(&mut OsRng);
    let argon2 = Argon2::default();
    let password_hash = argon2
        .hash_password(password.as_bytes(), &salt)
        .map_err(|e| AppError::Internal(format!("Failed to hash password: {}", e)))?
        .to_string();

    let (user_id,): (Uuid,) = sqlx::query_as(
        r#"
        INSERT INTO auth.users (username, email, password_hash, display_name, auth_provider, status)
        VALUES ($1, $2, $3, $4, 'local', 'active')
        RETURNING id
        "#,
    )
    .bind(username)
    .bind(email)
    .bind(&password_hash)
    .bind(display_name)
    .fetch_one(pool)
    .await?;

    Ok(user_id)
}

/// Create a new user using a connection (for RLS support).
pub async fn create_user_conn(
    conn: &mut sqlx::PgConnection,
    username: &str,
    email: Option<&str>,
    display_name: &str,
    password: &str,
) -> Result<Uuid, AppError> {
    create_user_with_preferences_conn(conn, username, email, display_name, password, None, None, None).await
}

/// Create a new user with preferences using a connection (for RLS support).
pub async fn create_user_with_preferences_conn(
    conn: &mut sqlx::PgConnection,
    username: &str,
    email: Option<&str>,
    display_name: &str,
    password: &str,
    birthday: Option<chrono::NaiveDate>,
    gender_preference: Option<GenderPreference>,
    age_category: Option<AgeCategory>,
) -> Result<Uuid, AppError> {
    use argon2::{
        Argon2, PasswordHasher,
        password_hash::{SaltString, rand_core::OsRng},
    };

    let salt = SaltString::generate(&mut OsRng);
    let argon2 = Argon2::default();
    let password_hash = argon2
        .hash_password(password.as_bytes(), &salt)
        .map_err(|e| AppError::Internal(format!("Failed to hash password: {}", e)))?
        .to_string();

    let (user_id,): (Uuid,) = sqlx::query_as(
        r#"
        INSERT INTO auth.users (
            username, email, password_hash, display_name, auth_provider, status,
            birthday, gender_preference, age_category
        )
        VALUES ($1, $2, $3, $4, 'local', 'active', $5, COALESCE($6, 'gender_neutral'), COALESCE($7, 'adult'))
        RETURNING id
        "#,
    )
    .bind(username)
    .bind(email)
    .bind(&password_hash)
    .bind(display_name)
    .bind(birthday)
    .bind(gender_preference)
    .bind(age_category)
    .fetch_one(conn)
    .await?;

    Ok(user_id)
}

/// Get a staff member by their user ID.
///
/// Returns the staff member with their user info joined.
pub async fn get_staff_member(
    pool: &PgPool,
    user_id: Uuid,
) -> Result<Option<StaffMember>, AppError> {
    let staff = sqlx::query_as::<_, StaffMember>(
        r#"
        SELECT
            s.user_id,
            u.username,
            u.display_name,
            u.email,
            s.role,
            s.appointed_by,
            s.appointed_at
        FROM server.staff s
        JOIN auth.users u ON s.user_id = u.id
        WHERE s.user_id = $1 AND u.status = 'active'
        "#,
    )
    .bind(user_id)
    .fetch_optional(pool)
    .await?;

    Ok(staff)
}

/// Upgrade a guest user to a full user account.
///
/// Updates the user's username, password, and optionally email,
/// and removes the 'guest' tag. Returns error if the user is not a guest.
pub async fn upgrade_guest_to_user(
    pool: &PgPool,
    user_id: Uuid,
    username: &str,
    password: &str,
    email: Option<&str>,
) -> Result<(), AppError> {
    let mut conn = pool.acquire().await?;
    upgrade_guest_to_user_conn(&mut *conn, user_id, username, password, email).await
}

/// Upgrade a guest user to a full user account using an existing connection.
///
/// Updates the user's username, password, and optionally email,
/// and removes the 'guest' tag. Returns error if the user is not a guest.
/// Use this version when you need RLS context set on the connection.
pub async fn upgrade_guest_to_user_conn(
    conn: &mut PgConnection,
    user_id: Uuid,
    username: &str,
    password: &str,
    email: Option<&str>,
) -> Result<(), AppError> {
    use argon2::{
        Argon2, PasswordHasher,
        password_hash::{SaltString, rand_core::OsRng},
    };

    let salt = SaltString::generate(&mut OsRng);
    let argon2 = Argon2::default();
    let password_hash = argon2
        .hash_password(password.as_bytes(), &salt)
        .map_err(|e| AppError::Internal(format!("Failed to hash password: {}", e)))?
        .to_string();

    let result: sqlx::postgres::PgQueryResult = sqlx::query(
        r#"
        UPDATE auth.users
        SET username = $2,
            password_hash = $3,
            email = $4,
            display_name = $2,
            tags = array_remove(tags, 'guest'),
            auth_provider = 'local',
            updated_at = now()
        WHERE id = $1 AND 'guest' = ANY(tags)
        "#,
    )
    .bind(user_id)
    .bind(username)
    .bind(&password_hash)
    .bind(email)
    .execute(&mut *conn)
    .await?;

    if result.rows_affected() == 0 {
        return Err(AppError::Forbidden(
            "User is not a guest or does not exist".to_string(),
        ));
    }

    Ok(())
}

/// Create a guest user (no password required).
///
/// Guests are created with the 'guest' tag and no password.
/// The database trigger `trg_auth_users_initialize` automatically creates
/// their default avatar and inventory.
pub async fn create_guest_user(pool: &PgPool, guest_name: &str) -> Result<Uuid, AppError> {
    // Generate unique username from guest_name (e.g., "guest12345")
    let username = guest_name.to_lowercase().replace('_', "");

    let (user_id,): (Uuid,) = sqlx::query_as(
        r#"
        INSERT INTO auth.users (username, display_name, tags)
        VALUES ($1, $2, ARRAY['guest']::auth.user_tag[])
        RETURNING id
        "#,
    )
    .bind(&username)
    .bind(guest_name)
    .fetch_one(pool)
    .await?;

    Ok(user_id)
}

/// Update a user's preferences (birthday, gender_preference, age_category).
pub async fn update_user_preferences(
    pool: &PgPool,
    user_id: Uuid,
    birthday: Option<chrono::NaiveDate>,
    gender_preference: Option<crate::models::GenderPreference>,
    age_category: Option<crate::models::AgeCategory>,
) -> Result<(), AppError> {
    // Build dynamic update based on provided fields
    let mut set_clauses = vec!["updated_at = now()".to_string()];

    if birthday.is_some() {
        set_clauses.push(format!("birthday = ${}", set_clauses.len() + 1));
    }
    if gender_preference.is_some() {
        set_clauses.push(format!("gender_preference = ${}", set_clauses.len() + 1));
    }
    if age_category.is_some() {
        set_clauses.push(format!("age_category = ${}", set_clauses.len() + 1));
    }

    if set_clauses.len() == 1 {
        // Only updated_at, nothing to update
        return Ok(());
    }

    let query = format!(
        "UPDATE auth.users SET {} WHERE id = $1",
        set_clauses.join(", ")
    );

    let mut q = sqlx::query(&query).bind(user_id);
    if let Some(b) = birthday {
        q = q.bind(b);
    }
    if let Some(g) = gender_preference {
        q = q.bind(g);
    }
    if let Some(a) = age_category {
        q = q.bind(a);
    }

    q.execute(pool).await?;

    Ok(())
}

/// Update a user's preferences using a connection (for RLS support).
pub async fn update_user_preferences_conn(
    conn: &mut PgConnection,
    user_id: Uuid,
    birthday: Option<chrono::NaiveDate>,
    gender_preference: Option<crate::models::GenderPreference>,
    age_category: Option<crate::models::AgeCategory>,
) -> Result<(), AppError> {
    // Build dynamic update based on provided fields
    let mut set_clauses = vec!["updated_at = now()".to_string()];

    if birthday.is_some() {
        set_clauses.push(format!("birthday = ${}", set_clauses.len() + 1));
    }
    if gender_preference.is_some() {
        set_clauses.push(format!("gender_preference = ${}", set_clauses.len() + 1));
    }
    if age_category.is_some() {
        set_clauses.push(format!("age_category = ${}", set_clauses.len() + 1));
    }

    if set_clauses.len() == 1 {
        // Only updated_at, nothing to update
        return Ok(());
    }

    let query = format!(
        "UPDATE auth.users SET {} WHERE id = $1",
        set_clauses.join(", ")
    );

    let mut q = sqlx::query(&query).bind(user_id);
    if let Some(b) = birthday {
        q = q.bind(b);
    }
    if let Some(g) = gender_preference {
        q = q.bind(g);
    }
    if let Some(a) = age_category {
        q = q.bind(a);
    }

    q.execute(conn).await?;

    Ok(())
}

/// Generate a random guest name like "Guest_12345".
pub fn generate_guest_name() -> String {
    use rand::Rng;
    let mut rng = rand::thread_rng();
    let number: u32 = rng.gen_range(10000..100000);
    format!("Guest_{}", number)
}
