use serde::{Deserialize, Serialize};
use sqlx::{PgConnection, PgPool};
use utoipa::ToSchema;

use crate::{
    db::{
        Id,
        models::{MFAMethod, device::UserDevice, group::Group, user::User},
    },
    types::group_diff::GroupDiff,
};

#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
pub struct OAuth2AuthorizedAppInfo {
    pub oauth2client_id: Id,
    pub oauth2client_name: String,
}

// Basic user info used in user list, etc.
#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
pub struct UserInfo {
    pub id: Id,
    pub username: String,
    pub last_name: String,
    pub first_name: String,
    name: String,
    pub email: String,
    pub phone: Option<String>,
    pub mfa_enabled: bool,
    pub totp_enabled: bool,
    pub email_mfa_enabled: bool,
    pub groups: Vec<String>,
    pub mfa_method: MFAMethod,
    pub authorized_apps: Vec<OAuth2AuthorizedAppInfo>,
    pub is_active: bool,
    pub enrolled: bool,
    pub is_admin: bool,
    pub ldap_pass_requires_change: bool,
    pub devices: Vec<UserDevice>,
}

impl UserInfo {
    /// Convert [`User`] to [`UserInfo`].
    pub async fn from_user(pool: &PgPool, user: User<Id>) -> sqlx::Result<Self> {
        let name = format!("{} {}", user.first_name, user.last_name);
        let groups = user.member_of_names(pool).await?;
        let authorized_apps = user.oauth2authorizedapps(pool).await?;
        let enrolled = user.is_enrolled();
        let is_admin = user.is_admin(pool).await?;
        let devices = user.user_devices(pool).await?;

        Ok(Self {
            id: user.id,
            username: user.username,
            last_name: user.last_name,
            first_name: user.first_name,
            name,
            email: user.email,
            phone: user.phone,
            mfa_enabled: user.mfa_enabled,
            totp_enabled: user.totp_enabled,
            email_mfa_enabled: user.email_mfa_enabled,
            groups,
            mfa_method: user.mfa_method,
            authorized_apps,
            is_active: user.is_active,
            enrolled,
            is_admin,
            ldap_pass_requires_change: user.ldap_pass_randomized,
            devices,
        })
    }

    /// Copy status to [`User`]. This function should be used by administrators.
    ///
    /// Return `true` if status was changed, `false` otherwise.
    /// If status was changed to inactive, all user sessions will be invalidated.
    pub async fn handle_status_change(
        &self,
        transaction: &mut PgConnection,
        user: &mut User<Id>,
    ) -> sqlx::Result<bool> {
        if self.is_active == user.is_active {
            Ok(false)
        } else {
            if !self.is_active {
                user.logout_all_sessions(&mut *transaction).await?;
            }
            user.is_active = self.is_active;
            user.save(&mut *transaction).await?;
            Ok(true)
        }
    }

    /// Copy groups to [`User`]. This function should be used by administrators.
    ///
    /// Return `true` if groups were changed, `false` otherwise.
    pub async fn handle_user_groups(
        &self,
        transaction: &mut PgConnection,
        user: &mut User<Id>,
    ) -> sqlx::Result<GroupDiff> {
        // initialize return value
        let mut group_diff = GroupDiff::default();

        // handle groups
        let mut present_groups = user.member_of(&mut *transaction).await?;

        // add to groups if not already a member
        for groupname in &self.groups {
            match present_groups
                .iter()
                .position(|group| &group.name == groupname)
            {
                Some(index) => {
                    present_groups.swap_remove(index);
                }
                None => {
                    if let Some(group) = Group::find_by_name(&mut *transaction, groupname).await? {
                        user.add_to_group(&mut *transaction, &group).await?;
                        group_diff.added.insert(group.name);
                    }
                }
            }
        }

        // remove from remaining groups
        for group in present_groups {
            user.remove_from_group(&mut *transaction, &group).await?;
            group_diff.removed.insert(group.name);
        }

        Ok(group_diff)
    }

    /// Copy fields over to the given [`User`].
    /// Additional flags control which fields are copied over.
    pub fn handle_update_user_fields(
        self,
        user: &mut User<Id>,
        is_admin: bool,
        is_updating_self: bool,
    ) {
        if is_admin {
            user.username = self.username;
            user.last_name = self.last_name;
            user.first_name = self.first_name;
            user.email = self.email;
        }

        if is_updating_self {
            user.mfa_method = self.mfa_method;
        }

        user.phone = self.phone;
    }
}

#[cfg(test)]
mod test {
    use sqlx::postgres::{PgConnectOptions, PgPoolOptions};

    use super::*;
    use crate::db::setup_pool;

    /// Build a minimal `UserInfo` from an existing saved `User<Id>`.
    /// Only the fields exercised by `handle_update_user_fields` need to be set
    /// here; the rest are left at their DB-loaded defaults.
    async fn user_info_from_db(pool: &PgPool, username: &str) -> (UserInfo, User<Id>) {
        let user = User::find_by_username(pool, username)
            .await
            .unwrap()
            .unwrap();
        let info = UserInfo::from_user(pool, user.clone()).await.unwrap();
        (info, user)
    }

    #[sqlx::test]
    async fn test_user_info(_: PgPoolOptions, options: PgConnectOptions) {
        let pool = setup_pool(options).await;

        let user = User::new(
            "hpotter",
            Some("pass123"),
            "Potter",
            "Harry",
            "h.potter@hogwart.edu.uk",
            None,
        )
        .save(&pool)
        .await
        .unwrap();

        let group1 = Group::new("Gryffindor").save(&pool).await.unwrap();
        let group2 = Group::new("Hufflepuff").save(&pool).await.unwrap();
        let group3 = Group::new("Ravenclaw").save(&pool).await.unwrap();
        let group4 = Group::new("Slytherin").save(&pool).await.unwrap();

        user.add_to_group(&pool, &group1).await.unwrap();
        user.add_to_group(&pool, &group2).await.unwrap();

        let mut user_info = UserInfo::from_user(&pool, user).await.unwrap();
        assert_eq!(user_info.groups, ["Gryffindor", "Hufflepuff"]);

        user_info.groups = vec!["Gryffindor".into(), "Ravenclaw".into()];
        let mut user = User::find_by_username(&pool, "hpotter")
            .await
            .unwrap()
            .unwrap();

        let mut transaction = pool.begin().await.unwrap();
        user_info
            .handle_user_groups(&mut transaction, &mut user)
            .await
            .unwrap();
        // admin updating their own account: is_admin=true, is_updating_self=true
        user_info.handle_update_user_fields(&mut user, true, true);
        transaction.commit().await.unwrap();

        assert_eq!(group1.member_usernames(&pool).await.unwrap(), ["hpotter"]);
        assert_eq!(group3.member_usernames(&pool).await.unwrap(), ["hpotter"]);
        assert!(group2.member_usernames(&pool).await.unwrap().is_empty());
        assert!(group4.member_usernames(&pool).await.unwrap().is_empty());
    }

    // Admin updating another user must be able to change all profile
    // fields (username, first/last name, email) and phone, but NOT mfa_method.
    #[sqlx::test]
    async fn test_handle_update_admin_updating_other_user(
        _: PgPoolOptions,
        options: PgConnectOptions,
    ) {
        let pool = setup_pool(options).await;
        let mut user = User::new(
            "hpotter",
            Some("pass123"),
            "Potter",
            "Harry",
            "h.potter@hogwart.edu.uk",
            Some("+48100200300".to_string()),
        )
        .save(&pool)
        .await
        .unwrap();

        let (mut info, _) = user_info_from_db(&pool, "hpotter").await;
        info.username = "h_potter_new".into();
        info.first_name = "UpdatedFirst".into();
        info.last_name = "Pot".into();
        info.email = "updated@hogwart.edu.uk".into();
        info.phone = Some("+48999888777".into());
        info.mfa_method = MFAMethod::OneTimePassword;

        // is_admin=true, is_updating_self=false (admin editing someone else)
        info.handle_update_user_fields(&mut user, true, false);

        assert_eq!(user.username, "h_potter_new");
        assert_eq!(user.first_name, "UpdatedFirst");
        assert_eq!(user.last_name, "Pot");
        assert_eq!(user.email, "updated@hogwart.edu.uk");
        assert_eq!(user.phone, Some("+48999888777".into()));
        // mfa_method must NOT change because is_updating_self=false
        assert_eq!(user.mfa_method, MFAMethod::None);
    }

    // A regular user updating themselves may only change phone and
    // mfa_method; name/email fields must be left untouched.
    #[sqlx::test]
    async fn test_handle_update_non_admin_updating_self(
        _: PgPoolOptions,
        options: PgConnectOptions,
    ) {
        let pool = setup_pool(options).await;
        let mut user = User::new(
            "hpotter",
            Some("pass123"),
            "Potter",
            "Harry",
            "h.potter@hogwart.edu.uk",
            None,
        )
        .save(&pool)
        .await
        .unwrap();

        let (mut info, _) = user_info_from_db(&pool, "hpotter").await;
        info.username = "changed_username".into();
        info.first_name = "UpdatedFirst".into();
        info.last_name = "UpdatedLast".into();
        info.email = "updated@example.com".into();
        info.phone = Some("+48111222333".into());
        info.mfa_method = MFAMethod::OneTimePassword;

        // is_admin=false, is_updating_self=true
        info.handle_update_user_fields(&mut user, false, true);

        // profile fields must remain unchanged
        assert_eq!(user.username, "hpotter");
        assert_eq!(user.first_name, "Harry");
        assert_eq!(user.last_name, "Potter");
        assert_eq!(user.email, "h.potter@hogwart.edu.uk");
        // phone and mfa_method are always allowed
        assert_eq!(user.phone, Some("+48111222333".into()));
        assert_eq!(user.mfa_method, MFAMethod::OneTimePassword);
    }

    // A non-admin modifying ANOTHER user must not be able to change
    // any protected field, and mfa_method must also stay unchanged because
    // is_updating_self=false.
    #[sqlx::test]
    async fn test_handle_update_non_admin_updating_other_user(
        _: PgPoolOptions,
        options: PgConnectOptions,
    ) {
        let pool = setup_pool(options).await;
        let mut user = User::new(
            "hpotter",
            Some("pass123"),
            "Potter",
            "Harry",
            "h.potter@hogwart.edu.uk",
            Some("+48100200300".to_string()),
        )
        .save(&pool)
        .await
        .unwrap();
        let original_mfa = user.mfa_method;

        let (mut info, _) = user_info_from_db(&pool, "hpotter").await;
        info.username = "changed_username".into();
        info.first_name = "UpdatedFirst".into();
        info.last_name = "UpdatedLast".into();
        info.email = "updated@example.com".into();
        info.phone = Some("+48000000000".into());
        info.mfa_method = MFAMethod::OneTimePassword;

        // is_admin=false, is_updating_self=false
        info.handle_update_user_fields(&mut user, false, false);

        // only phone changes; everything else stays the same
        assert_eq!(user.username, "hpotter");
        assert_eq!(user.first_name, "Harry");
        assert_eq!(user.last_name, "Potter");
        assert_eq!(user.email, "h.potter@hogwart.edu.uk");
        assert_eq!(user.phone, Some("+48000000000".into()));
        assert_eq!(user.mfa_method, original_mfa);
    }

    // Admin updating their own account can change all fields
    // including mfa_method.
    #[sqlx::test]
    async fn test_handle_update_admin_updating_self(_: PgPoolOptions, options: PgConnectOptions) {
        let pool = setup_pool(options).await;
        let mut user = User::new(
            "admin",
            Some("pass123"),
            "Admin",
            "Super",
            "admin@defguard",
            None,
        )
        .save(&pool)
        .await
        .unwrap();

        let (mut info, _) = user_info_from_db(&pool, "admin").await;
        info.username = "admin_renamed".into();
        info.first_name = "NewFirst".into();
        info.last_name = "NewLast".into();
        info.email = "new@defguard".into();
        info.phone = Some("+48777888999".into());
        info.mfa_method = MFAMethod::OneTimePassword;

        // is_admin=true, is_updating_self=true
        info.handle_update_user_fields(&mut user, true, true);

        assert_eq!(user.username, "admin_renamed");
        assert_eq!(user.first_name, "NewFirst");
        assert_eq!(user.last_name, "NewLast");
        assert_eq!(user.email, "new@defguard");
        assert_eq!(user.phone, Some("+48777888999".into()));
        assert_eq!(user.mfa_method, MFAMethod::OneTimePassword);
    }
}
