use std::collections::{HashMap, HashSet};

use num_traits::FromPrimitive;
use rocket::{Route, serde::json::Json};
use serde_json::Value;

use crate::{
    CONFIG,
    api::admin::FAKE_ADMIN_UUID,
    api::{
        EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
        core::{CipherSyncData, CipherSyncType, accept_org_invite, log_event, two_factor},
    },
    auth::{AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders, OwnerHeaders, decode_invite},
    db::{
        DbConn,
        models::{
            Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, EventType,
            Group, GroupId, GroupUser, Invitation, Membership, MembershipId, MembershipStatus, MembershipType,
            OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, User, UserId,
        },
    },
    mail,
    sso::FAKE_SSO_IDENTIFIER,
    util::{NumberOrString, convert_json_key_lcase_first},
};

pub fn routes() -> Vec<Route> {
    routes![
        get_organization,
        create_organization,
        delete_organization,
        post_delete_organization,
        leave_organization,
        get_user_collections,
        get_org_collections,
        get_org_collections_details,
        get_org_collection_detail,
        get_collection_users,
        put_organization,
        post_organization,
        post_organization_collections,
        post_bulk_access_collections,
        post_organization_collection_update,
        put_organization_collection_update,
        delete_organization_collection,
        post_organization_collection_delete,
        bulk_delete_organization_collections,
        post_bulk_collections,
        get_org_details,
        get_org_domain_sso_verified,
        get_members,
        send_invite,
        reinvite_member,
        bulk_reinvite_members,
        confirm_invite,
        bulk_confirm_invite,
        accept_invite,
        get_org_user_mini_details,
        get_user,
        edit_member,
        put_member,
        delete_member,
        bulk_delete_member,
        post_org_import,
        list_policies,
        list_policies_token,
        get_dummy_master_password_policy,
        get_master_password_policy,
        get_policy,
        put_policy,
        put_policy_vnext,
        get_plans,
        post_org_keys,
        get_organization_keys,
        get_organization_public_key,
        bulk_public_keys,
        revoke_member,
        bulk_revoke_members,
        restore_member,
        restore_member_vnext,
        bulk_restore_members,
        get_groups,
        get_groups_details,
        post_groups,
        get_group,
        put_group,
        post_group,
        get_group_details,
        delete_group,
        post_delete_group,
        bulk_delete_groups,
        get_group_members,
        put_group_members,
        post_delete_group_member,
        put_reset_password_enrollment,
        get_reset_password_details,
        put_reset_password,
        put_recover_account,
        get_org_export,
        post_api_key,
        rotate_api_key,
        get_billing_metadata,
        get_billing_warnings,
        get_auto_enroll_status,
        get_self_host_billing_metadata,
    ]
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct OrgData {
    billing_email: String,
    collection_name: String,
    key: String,
    name: String,
    keys: Option<OrgKeyData>,
    #[allow(dead_code)]
    plan_type: NumberOrString, // Ignored, always use the same plan
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct OrganizationUpdateData {
    billing_email: String,
    name: String,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct FullCollectionData {
    name: String,
    groups: Vec<CollectionGroupData>,
    users: Vec<CollectionMembershipData>,
    id: Option<CollectionId>,
    external_id: Option<String>,
}

impl FullCollectionData {
    pub async fn validate(&self, org_id: &OrganizationId, conn: &DbConn) -> EmptyResult {
        let org_groups = Group::find_by_organization(org_id, conn).await;
        let org_group_ids: HashSet<&GroupId> = org_groups.iter().map(|c| &c.uuid).collect();
        if let Some(e) = self.groups.iter().find(|g| !org_group_ids.contains(&g.id)) {
            err!("Invalid group", format!("Group {} does not belong to organization {}!", e.id, org_id))
        }

        let org_memberships = Membership::find_by_org(org_id, conn).await;
        let org_membership_ids: HashSet<&MembershipId> = org_memberships.iter().map(|m| &m.uuid).collect();
        if let Some(e) = self.users.iter().find(|m| !org_membership_ids.contains(&m.id)) {
            err!("Invalid member", format!("Member {} does not belong to organization {}!", e.id, org_id))
        }

        Ok(())
    }
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CollectionGroupData {
    hide_passwords: bool,
    id: GroupId,
    read_only: bool,
    manage: bool,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CollectionMembershipData {
    hide_passwords: bool,
    id: MembershipId,
    read_only: bool,
    manage: bool,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct OrgKeyData {
    encrypted_private_key: String,
    public_key: String,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct BulkGroupIds {
    ids: Vec<GroupId>,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct BulkMembershipIds {
    ids: Vec<MembershipId>,
}

#[post("/organizations", data = "<data>")]
async fn create_organization(headers: Headers, data: Json<OrgData>, conn: DbConn) -> JsonResult {
    if !CONFIG.is_org_creation_allowed(&headers.user.email) {
        err!("User not allowed to create organizations")
    }
    if OrgPolicy::is_applicable_to_user(&headers.user.uuid, OrgPolicyType::SingleOrg, None, &conn).await {
        err!(
            "You may not create an organization. You belong to an organization which has a policy that prohibits you from being a member of any other organization."
        )
    }

    let data: OrgData = data.into_inner();
    let (private_key, public_key) = if let Some(keys) = data.keys {
        (Some(keys.encrypted_private_key), Some(keys.public_key))
    } else {
        (None, None)
    };

    let org = Organization::new(data.name, &data.billing_email, private_key, public_key);
    let mut member = Membership::new(headers.user.uuid, org.uuid.clone(), None);
    let collection = Collection::new(org.uuid.clone(), data.collection_name, None);

    member.akey = data.key;
    member.access_all = true;
    member.atype = MembershipType::Owner as i32;
    member.status = MembershipStatus::Confirmed as i32;

    org.save(&conn).await?;
    member.save(&conn).await?;
    collection.save(&conn).await?;

    Ok(Json(org.to_json()))
}

#[delete("/organizations/<org_id>", data = "<data>")]
async fn delete_organization(
    org_id: OrganizationId,
    data: Json<PasswordOrOtpData>,
    headers: OwnerHeaders,
    conn: DbConn,
) -> EmptyResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: PasswordOrOtpData = data.into_inner();

    data.validate(&headers.user, true, &conn).await?;

    match Organization::find_by_uuid(&org_id, &conn).await {
        None => err!("Organization not found"),
        Some(org) => org.delete(&conn).await,
    }
}

#[post("/organizations/<org_id>/delete", data = "<data>")]
async fn post_delete_organization(
    org_id: OrganizationId,
    data: Json<PasswordOrOtpData>,
    headers: OwnerHeaders,
    conn: DbConn,
) -> EmptyResult {
    delete_organization(org_id, data, headers, conn).await
}

#[post("/organizations/<org_id>/leave")]
async fn leave_organization(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> EmptyResult {
    if headers.membership.status != MembershipStatus::Confirmed as i32 {
        err!("You need to be a Member of the Organization to call this endpoint")
    }
    let membership = headers.membership;

    if membership.atype == MembershipType::Owner
        && Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1
    {
        err!("The last owner can't leave")
    }

    log_event(
        EventType::OrganizationUserLeft as i32,
        &membership.uuid,
        &org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        &conn,
    )
    .await;

    membership.delete(&conn).await
}

#[get("/organizations/<org_id>")]
async fn get_organization(org_id: OrganizationId, headers: OwnerHeaders, conn: DbConn) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    if let Some(organization) = Organization::find_by_uuid(&org_id, &conn).await {
        Ok(Json(organization.to_json()))
    } else {
        err!("Can't find organization details")
    }
}

#[put("/organizations/<org_id>", data = "<data>")]
async fn put_organization(
    org_id: OrganizationId,
    headers: OwnerHeaders,
    data: Json<OrganizationUpdateData>,
    conn: DbConn,
) -> JsonResult {
    post_organization(org_id, headers, data, conn).await
}

#[post("/organizations/<org_id>", data = "<data>")]
async fn post_organization(
    org_id: OrganizationId,
    headers: OwnerHeaders,
    data: Json<OrganizationUpdateData>,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }

    let data: OrganizationUpdateData = data.into_inner();

    let Some(mut org) = Organization::find_by_uuid(&org_id, &conn).await else {
        err!("Organization not found")
    };

    org.name = data.name;
    org.billing_email = data.billing_email.to_lowercase();

    org.save(&conn).await?;

    log_event(
        EventType::OrganizationUpdated as i32,
        org_id.as_ref(),
        &org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        &conn,
    )
    .await;

    Ok(Json(org.to_json()))
}

// GET /api/collections?writeOnly=false
#[get("/collections")]
async fn get_user_collections(headers: Headers, conn: DbConn) -> Json<Value> {
    Json(json!({
        "data":
            Collection::find_by_user_uuid(headers.user.uuid, &conn).await
            .iter()
            .map(Collection::to_json)
            .collect::<Value>(),
        "object": "list",
        "continuationToken": null,
    }))
}

// Called during the SSO enrollment
// The `identifier` should be the value returned by `get_org_domain_sso_verified`
// The returned `Id` will then be passed to `get_master_password_policy` which will mainly ignore it
#[get("/organizations/<identifier>/auto-enroll-status")]
async fn get_auto_enroll_status(identifier: &str, headers: Headers, conn: DbConn) -> JsonResult {
    let org = if identifier == FAKE_SSO_IDENTIFIER {
        match Membership::find_main_user_org(&headers.user.uuid, &conn).await {
            Some(member) => Organization::find_by_uuid(&member.org_uuid, &conn).await,
            None => None,
        }
    } else {
        Organization::find_by_uuid(&identifier.into(), &conn).await
    };

    let (id, identifier, rp_auto_enroll) = match org {
        None => (identifier.to_owned(), identifier.to_owned(), false),
        Some(org) => (
            org.uuid.to_string(),
            org.uuid.to_string(),
            OrgPolicy::org_is_reset_password_auto_enroll(&org.uuid, &conn).await,
        ),
    };

    Ok(Json(json!({
        "id": id,
        "identifier": identifier,
        "resetPasswordEnabled": rp_auto_enroll,
    })))
}

#[get("/organizations/<org_id>/collections")]
async fn get_org_collections(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
    if org_id != headers.membership.org_uuid {
        err!("Organization not found", "Organization id's do not match");
    }

    if !headers.membership.has_full_access() {
        err_code!("Resource not found.", "User does not have full access", rocket::http::Status::NotFound.code);
    }

    Ok(Json(json!({
        "data": get_org_collections_impl(&org_id, &conn).await,
        "object": "list",
        "continuationToken": null,
    })))
}

#[get("/organizations/<org_id>/collections/details")]
async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
    if org_id != headers.membership.org_uuid {
        err!("Organization not found", "Organization id's do not match");
    }

    let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await else {
        err!("User is not part of organization")
    };

    // get all collection memberships for the current organization
    let col_users = CollectionUser::find_by_organization_swap_user_uuid_with_member_uuid(&org_id, &conn).await;
    // Generate a HashMap to get the correct MembershipType per user to determine the manage permission
    // We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser
    let membership_type: HashMap<MembershipId, i32> =
        Membership::find_confirmed_by_org(&org_id, &conn).await.into_iter().map(|m| (m.uuid, m.atype)).collect();

    // check if current user has full access to the organization (either directly or via any group)
    let has_full_access_to_org = member.has_full_access()
        || (CONFIG.org_groups_enabled() && GroupUser::has_full_access_by_member(&org_id, &member.uuid, &conn).await);

    // Get all admins, owners and managers who can manage/access all
    // Those are currently not listed in the col_users but need to be listed too.
    let manage_all_members: Vec<Value> = Membership::find_confirmed_and_manage_all_by_org(&org_id, &conn)
        .await
        .into_iter()
        .map(|member| {
            json!({
                "id": member.uuid,
                "readOnly": false,
                "hidePasswords": false,
                "manage": true,
            })
        })
        .collect();

    let mut data = Vec::new();
    for col in Collection::find_by_organization(&org_id, &conn).await {
        // check whether the current user has access to the given collection
        let assigned = has_full_access_to_org
            || CollectionUser::has_access_to_collection_by_user(&col.uuid, &member.user_uuid, &conn).await
            || (CONFIG.org_groups_enabled()
                && GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &conn).await);

        // If the user is a manager, and is not assigned to this collection, skip this and continue with the next collection
        if !assigned {
            continue;
        }

        // get the users assigned directly to the given collection
        let mut users: Vec<Value> = col_users
            .iter()
            .filter(|collection_member| collection_member.collection_uuid == col.uuid)
            .map(|collection_member| {
                collection_member.to_json_details_for_member(
                    *membership_type.get(&collection_member.membership_uuid).unwrap_or(&(MembershipType::User as i32)),
                )
            })
            .collect();
        users.extend_from_slice(&manage_all_members);

        // get the group details for the given collection
        let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
            CollectionGroup::find_by_collection(&col.uuid, &conn)
                .await
                .iter()
                .map(CollectionGroup::to_json_details_for_group)
                .collect()
        } else {
            Vec::new()
        };

        let mut json_object = col.to_json_details(&headers.user.uuid, None, &conn).await;
        json_object["assigned"] = json!(assigned);
        json_object["users"] = json!(users);
        json_object["groups"] = json!(groups);
        json_object["object"] = json!("collectionAccessDetails");
        json_object["unmanaged"] = json!(false);
        data.push(json_object);
    }

    Ok(Json(json!({
        "data": data,
        "object": "list",
        "continuationToken": null,
    })))
}

async fn get_org_collections_impl(org_id: &OrganizationId, conn: &DbConn) -> Value {
    Collection::find_by_organization(org_id, conn).await.iter().map(Collection::to_json).collect::<Value>()
}

#[post("/organizations/<org_id>/collections", data = "<data>")]
async fn post_organization_collections(
    org_id: OrganizationId,
    headers: ManagerHeadersLoose,
    data: Json<FullCollectionData>,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.membership.org_uuid {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: FullCollectionData = data.into_inner();
    data.validate(&org_id, &conn).await?;

    if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all {
        err!("You don't have permission to create collections")
    }

    let collection = Collection::new(org_id.clone(), data.name, data.external_id);
    collection.save(&conn).await?;

    log_event(
        EventType::CollectionCreated as i32,
        &collection.uuid,
        &org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        &conn,
    )
    .await;

    for group in data.groups {
        CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords, group.manage)
            .save(&org_id, &conn)
            .await?;
    }

    for user in data.users {
        let Some(member) = Membership::find_by_uuid_and_org(&user.id, &org_id, &conn).await else {
            err!("User is not part of organization")
        };

        if member.access_all {
            continue;
        }

        CollectionUser::save(
            &member.user_uuid,
            &collection.uuid,
            user.read_only,
            user.hide_passwords,
            user.manage,
            &conn,
        )
        .await?;
    }

    Ok(Json(collection.to_json_details(&headers.membership.user_uuid, None, &conn).await))
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct BulkCollectionAccessData {
    collection_ids: Vec<CollectionId>,
    groups: Vec<CollectionGroupData>,
    users: Vec<CollectionMembershipData>,
}

#[post("/organizations/<org_id>/collections/bulk-access", data = "<data>", rank = 1)]
async fn post_bulk_access_collections(
    org_id: OrganizationId,
    headers: ManagerHeadersLoose,
    data: Json<BulkCollectionAccessData>,
    conn: DbConn,
) -> EmptyResult {
    if org_id != headers.membership.org_uuid {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: BulkCollectionAccessData = data.into_inner();

    if Organization::find_by_uuid(&org_id, &conn).await.is_none() {
        err!("Can't find organization details")
    }

    // The collections and members are checked below, the groups only here.
    let org_groups = Group::find_by_organization(&org_id, &conn).await;
    let org_group_ids: HashSet<&GroupId> = org_groups.iter().map(|g| &g.uuid).collect();
    if let Some(g) = data.groups.iter().find(|g| !org_group_ids.contains(&g.id)) {
        err!("Invalid group", format!("Group {} does not belong to organization {}!", g.id, org_id))
    }

    for col_id in data.collection_ids {
        let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await else {
            err!("Collection not found")
        };

        if !collection.is_manageable_by_user(&headers.membership.user_uuid, &conn).await {
            err!("Collection not found", "The current user isn't a manager for this collection")
        }

        // update collection modification date
        collection.save(&conn).await?;

        log_event(
            EventType::CollectionUpdated as i32,
            &collection.uuid,
            &org_id,
            &headers.user.uuid,
            headers.device.atype,
            &headers.ip.ip,
            &conn,
        )
        .await;

        CollectionGroup::delete_all_by_collection(&col_id, &org_id, &conn).await?;
        for group in &data.groups {
            CollectionGroup::new(col_id.clone(), group.id.clone(), group.read_only, group.hide_passwords, group.manage)
                .save(&org_id, &conn)
                .await?;
        }

        CollectionUser::delete_all_by_collection(&col_id, &conn).await?;
        for user in &data.users {
            let Some(member) = Membership::find_by_uuid_and_org(&user.id, &org_id, &conn).await else {
                err!("User is not part of organization")
            };

            if member.access_all {
                continue;
            }

            CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, user.manage, &conn)
                .await?;
        }
    }

    Ok(())
}

#[put("/organizations/<org_id>/collections/<col_id>", data = "<data>")]
async fn put_organization_collection_update(
    org_id: OrganizationId,
    col_id: CollectionId,
    headers: ManagerHeaders,
    data: Json<FullCollectionData>,
    conn: DbConn,
) -> JsonResult {
    post_organization_collection_update(org_id, col_id, headers, data, conn).await
}

#[post("/organizations/<org_id>/collections/<col_id>", data = "<data>", rank = 2)]
async fn post_organization_collection_update(
    org_id: OrganizationId,
    col_id: CollectionId,
    headers: ManagerHeaders,
    data: Json<FullCollectionData>,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: FullCollectionData = data.into_inner();
    data.validate(&org_id, &conn).await?;

    if Organization::find_by_uuid(&org_id, &conn).await.is_none() {
        err!("Can't find organization details")
    }

    let Some(mut collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await else {
        err!("Collection not found")
    };

    collection.name = data.name;
    collection.external_id = match data.external_id {
        Some(external_id) if !external_id.trim().is_empty() => Some(external_id),
        _ => None,
    };

    collection.save(&conn).await?;

    log_event(
        EventType::CollectionUpdated as i32,
        &collection.uuid,
        &org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        &conn,
    )
    .await;

    CollectionGroup::delete_all_by_collection(&col_id, &org_id, &conn).await?;

    for group in data.groups {
        CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords, group.manage)
            .save(&org_id, &conn)
            .await?;
    }

    CollectionUser::delete_all_by_collection(&col_id, &conn).await?;

    for user in data.users {
        let Some(member) = Membership::find_by_uuid_and_org(&user.id, &org_id, &conn).await else {
            err!("User is not part of organization")
        };

        if member.access_all {
            continue;
        }

        CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, user.manage, &conn)
            .await?;
    }

    Ok(Json(collection.to_json_details(&headers.user.uuid, None, &conn).await))
}

async fn delete_organization_collection_impl(
    org_id: &OrganizationId,
    col_id: &CollectionId,
    headers: &ManagerHeaders,
    conn: &DbConn,
) -> EmptyResult {
    if org_id != &headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let Some(collection) = Collection::find_by_uuid_and_org(col_id, org_id, conn).await else {
        err!("Collection not found", "Collection does not exist or does not belong to this organization")
    };
    log_event(
        EventType::CollectionDeleted as i32,
        &collection.uuid,
        org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        conn,
    )
    .await;
    collection.delete(conn).await
}

#[delete("/organizations/<org_id>/collections/<col_id>")]
async fn delete_organization_collection(
    org_id: OrganizationId,
    col_id: CollectionId,
    headers: ManagerHeaders,
    conn: DbConn,
) -> EmptyResult {
    delete_organization_collection_impl(&org_id, &col_id, &headers, &conn).await
}

#[post("/organizations/<org_id>/collections/<col_id>/delete")]
async fn post_organization_collection_delete(
    org_id: OrganizationId,
    col_id: CollectionId,
    headers: ManagerHeaders,
    conn: DbConn,
) -> EmptyResult {
    delete_organization_collection_impl(&org_id, &col_id, &headers, &conn).await
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct BulkCollectionIds {
    ids: Vec<CollectionId>,
}

#[delete("/organizations/<org_id>/collections", data = "<data>")]
async fn bulk_delete_organization_collections(
    org_id: OrganizationId,
    headers: ManagerHeadersLoose,
    data: Json<BulkCollectionIds>,
    conn: DbConn,
) -> EmptyResult {
    if org_id != headers.membership.org_uuid {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: BulkCollectionIds = data.into_inner();

    let collections = data.ids;

    let headers = ManagerHeaders::from_loose(headers, &collections, &conn).await?;

    for col_id in collections {
        delete_organization_collection_impl(&org_id, &col_id, &headers, &conn).await?;
    }
    Ok(())
}

#[get("/organizations/<org_id>/collections/<col_id>/details")]
async fn get_org_collection_detail(
    org_id: OrganizationId,
    col_id: CollectionId,
    headers: ManagerHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    match Collection::find_by_uuid_and_user(&col_id, headers.user.uuid.clone(), &conn).await {
        None => err!("Collection not found"),
        Some(collection) => {
            if collection.org_uuid != org_id {
                err!("Collection is not owned by organization")
            }

            let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await else {
                err!("User is not part of organization")
            };

            let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
                CollectionGroup::find_by_collection(&collection.uuid, &conn)
                    .await
                    .iter()
                    .map(CollectionGroup::to_json_details_for_group)
                    .collect()
            } else {
                // The Bitwarden clients seem to call this API regardless of whether groups are enabled,
                // so just act as if there are no groups.
                Vec::new()
            };

            // Generate a HashMap to get the correct MembershipType per user to determine the manage permission
            // We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser
            let membership_type: HashMap<MembershipId, i32> = Membership::find_confirmed_by_org(&org_id, &conn)
                .await
                .into_iter()
                .map(|m| (m.uuid, m.atype))
                .collect();

            let users: Vec<Value> =
                CollectionUser::find_by_org_and_coll_swap_user_uuid_with_member_uuid(&org_id, &collection.uuid, &conn)
                    .await
                    .iter()
                    .map(|collection_member| {
                        collection_member.to_json_details_for_member(
                            *membership_type
                                .get(&collection_member.membership_uuid)
                                .unwrap_or(&(MembershipType::User as i32)),
                        )
                    })
                    .collect();

            let assigned = Collection::can_access_collection(&member, &collection.uuid, &conn).await;

            let mut json_object = collection.to_json_details(&headers.user.uuid, None, &conn).await;
            json_object["assigned"] = json!(assigned);
            json_object["users"] = json!(users);
            json_object["groups"] = json!(groups);
            json_object["object"] = json!("collectionAccessDetails");

            Ok(Json(json_object))
        }
    }
}

#[get("/organizations/<org_id>/collections/<col_id>/users")]
async fn get_collection_users(
    org_id: OrganizationId,
    col_id: CollectionId,
    headers: ManagerHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    // Get org and collection, check that collection is from org
    let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await else {
        err!("Collection not found in Organization")
    };

    let mut member_list = Vec::new();
    for col_user in CollectionUser::find_by_collection(&collection.uuid, &conn).await {
        member_list.push(
            Membership::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn)
                .await
                .unwrap()
                .to_json_user_access_restrictions(&col_user),
        );
    }

    Ok(Json(json!(member_list)))
}

#[derive(FromForm)]
struct OrgIdData {
    #[field(name = "organizationId")]
    organization_id: OrganizationId,
}

#[get("/ciphers/organization-details?<data..>")]
async fn get_org_details(data: OrgIdData, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
    if data.organization_id != headers.membership.org_uuid {
        err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code);
    }

    if !headers.membership.has_full_access() {
        err_code!("Resource not found.", "User does not have full access", rocket::http::Status::NotFound.code);
    }

    Ok(Json(json!({
        "data": get_org_details_impl(&data.organization_id, &headers.host, &headers.user.uuid, &conn).await?,
        "object": "list",
        "continuationToken": null,
    })))
}

async fn get_org_details_impl(
    org_id: &OrganizationId,
    host: &str,
    user_id: &UserId,
    conn: &DbConn,
) -> Result<Value, crate::Error> {
    let ciphers = Cipher::find_by_org(org_id, conn).await;
    let cipher_sync_data = CipherSyncData::new(user_id, CipherSyncType::Organization, conn).await;

    let mut ciphers_json = Vec::with_capacity(ciphers.len());
    for c in ciphers {
        ciphers_json.push(c.to_json(host, user_id, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await?);
    }
    Ok(json!(ciphers_json))
}

// Returning a Domain/Organization here allow to prefill it and prevent prompting the user
// So we return a dummy value, since we only support a single SSO integration, and do not use the response anywhere
// In use since `v2025.6.0`, appears to use only the first `organizationIdentifier`
#[post("/organizations/domain/sso/verified")]
fn get_org_domain_sso_verified() -> JsonResult {
    // Always return a dummy value, no matter if SSO is enabled or not
    Ok(Json(json!({
        "object": "list",
        "data": [{
            "organizationIdentifier": FAKE_SSO_IDENTIFIER,
            // These appear to be unused
            "organizationName": FAKE_SSO_IDENTIFIER,
            "domainName": CONFIG.domain()
        }],
        "continuationToken": null
    })))
}

#[derive(FromForm)]
struct GetOrgUserData {
    #[field(name = "includeCollections")]
    include_collections: Option<bool>,
    #[field(name = "includeGroups")]
    include_groups: Option<bool>,
}

#[get("/organizations/<org_id>/users?<data..>")]
async fn get_members(
    data: GetOrgUserData,
    org_id: OrganizationId,
    headers: ManagerHeadersLoose,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.membership.org_uuid {
        err!("Organization not found", "Organization id's do not match");
    }

    if !headers.membership.has_full_access() {
        err_code!("Resource not found.", "User does not have full access", rocket::http::Status::NotFound.code);
    }

    let mut users_json = Vec::new();
    for u in Membership::find_by_org(&org_id, &conn).await {
        users_json.push(
            u.to_json_user_details(
                data.include_collections.unwrap_or(false),
                data.include_groups.unwrap_or(false),
                &conn,
            )
            .await,
        );
    }

    Ok(Json(json!({
        "data": users_json,
        "object": "list",
        "continuationToken": null,
    })))
}

#[post("/organizations/<org_id>/keys", data = "<data>")]
async fn post_org_keys(
    org_id: OrganizationId,
    data: Json<OrgKeyData>,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: OrgKeyData = data.into_inner();

    let mut org = if let Some(organization) = Organization::find_by_uuid(&org_id, &conn).await {
        if organization.private_key.is_some() && organization.public_key.is_some() {
            err!("Organization Keys already exist")
        }
        organization
    } else {
        err!("Can't find organization details")
    };

    org.private_key = Some(data.encrypted_private_key);
    org.public_key = Some(data.public_key);

    org.save(&conn).await?;

    Ok(Json(json!({
        "object": "organizationKeys",
        "publicKey": org.public_key,
        "privateKey": org.private_key,
    })))
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct InviteData {
    emails: Vec<String>,
    groups: Vec<GroupId>,
    r#type: NumberOrString,
    collections: Option<Vec<CollectionData>>,
    #[serde(default)]
    permissions: HashMap<String, Value>,
}

impl InviteData {
    async fn validate(&self, org_id: &OrganizationId, conn: &DbConn) -> EmptyResult {
        let org_collections = Collection::find_by_organization(org_id, conn).await;
        let org_collection_ids: HashSet<&CollectionId> = org_collections.iter().map(|c| &c.uuid).collect();
        if let Some(e) = self.collections.iter().flatten().find(|c| !org_collection_ids.contains(&c.id)) {
            err!("Invalid collection", format!("Collection {} does not belong to organization {}!", e.id, org_id))
        }

        let org_groups = Group::find_by_organization(org_id, conn).await;
        let org_group_ids: HashSet<&GroupId> = org_groups.iter().map(|c| &c.uuid).collect();
        if let Some(e) = self.groups.iter().find(|g| !org_group_ids.contains(g)) {
            err!("Invalid group", format!("Group {} does not belong to organization {}!", e, org_id))
        }

        Ok(())
    }
}

#[post("/organizations/<org_id>/users/invite", data = "<data>")]
async fn send_invite(
    org_id: OrganizationId,
    data: Json<InviteData>,
    headers: AdminHeaders,
    conn: DbConn,
) -> EmptyResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: InviteData = data.into_inner();
    data.validate(&org_id, &conn).await?;

    // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission
    // The from_str() will convert the custom role type into a manager role type
    let raw_type = &data.r#type.into_string();
    // Membership::from_str will convert custom (4) to manager (3)
    let new_type = if let Some(new_type) = MembershipType::from_str(raw_type) {
        new_type as i32
    } else {
        err!("Invalid type")
    };

    if new_type != MembershipType::User && headers.membership_type != MembershipType::Owner {
        err!("Only Owners can invite Managers, Admins or Owners")
    }

    // HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag
    // Since the parent checkbox is not sent to the server we need to check and verify the child checkboxes
    // If the box is not checked, the user will still be a manager, but not with the access_all permission
    let access_all = new_type >= MembershipType::Admin
        || (raw_type.eq("4")
            && data.permissions.get("editAnyCollection") == Some(&json!(true))
            && data.permissions.get("deleteAnyCollection") == Some(&json!(true))
            && data.permissions.get("createNewCollections") == Some(&json!(true)));

    let mut user_created: bool = false;
    for email in &data.emails {
        let mut member_status = MembershipStatus::Invited as i32;
        let user = match User::find_by_mail(email, &conn).await {
            None => {
                if !CONFIG.invitations_allowed() {
                    err!(format!("User does not exist: {email}"))
                }

                if !CONFIG.is_email_domain_allowed(email) {
                    err!("Email domain not eligible for invitations")
                }

                if !CONFIG.mail_enabled() {
                    Invitation::new(email).save(&conn).await?;
                }

                let mut new_user = User::new(email, None);
                new_user.save(&conn).await?;
                user_created = true;
                new_user
            }
            Some(user) => {
                if Membership::find_by_user_and_org(&user.uuid, &org_id, &conn).await.is_some() {
                    err!(format!("User already in organization: {email}"))
                }

                if !CONFIG.mail_enabled() {
                    if user.password_hash.is_empty() {
                        Invitation::new(email).save(&conn).await?;
                    } else {
                        // automatically accept existing users if mail is disabled
                        member_status = MembershipStatus::Accepted as i32;
                    }
                }
                user
            }
        };

        let mut new_member = Membership::new(user.uuid.clone(), org_id.clone(), Some(headers.user.email.clone()));
        new_member.access_all = access_all;
        new_member.atype = new_type;
        new_member.status = member_status;
        new_member.save(&conn).await?;

        if CONFIG.mail_enabled() {
            let org_name = if let Some(org) = Organization::find_by_uuid(&org_id, &conn).await {
                org.name
            } else {
                err!("Error looking up organization")
            };

            if let Err(e) = mail::send_invite(
                &user,
                org_id.clone(),
                new_member.uuid.clone(),
                &org_name,
                Some(headers.user.email.clone()),
            )
            .await
            {
                // Upon error delete the user, invite and org member records when needed
                if user_created {
                    user.delete(&conn).await?;
                } else {
                    new_member.delete(&conn).await?;
                }

                err!(format!("Error sending invite: {e:?} "));
            }
        }

        log_event(
            EventType::OrganizationUserInvited as i32,
            &new_member.uuid,
            &org_id,
            &headers.user.uuid,
            headers.device.atype,
            &headers.ip.ip,
            &conn,
        )
        .await;

        // If no accessAll, add the collections received
        if !access_all {
            for col in data.collections.iter().flatten() {
                match Collection::find_by_uuid_and_org(&col.id, &org_id, &conn).await {
                    None => err!("Collection not found in Organization"),
                    Some(collection) => {
                        CollectionUser::save(
                            &user.uuid,
                            &collection.uuid,
                            col.read_only,
                            col.hide_passwords,
                            col.manage,
                            &conn,
                        )
                        .await?;
                    }
                }
            }
        }

        for group_id in &data.groups {
            if Group::find_by_uuid_and_org(group_id, &org_id, &conn).await.is_none() {
                err!("Group not found in Organization")
            }
            let mut group_entry = GroupUser::new(group_id.clone(), new_member.uuid.clone());
            group_entry.save(&conn).await?;
        }
    }

    Ok(())
}

#[post("/organizations/<org_id>/users/reinvite", data = "<data>")]
async fn bulk_reinvite_members(
    org_id: OrganizationId,
    data: Json<BulkMembershipIds>,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: BulkMembershipIds = data.into_inner();

    let mut bulk_response = Vec::new();
    for member_id in data.ids {
        let err_msg = match reinvite_member_impl(&org_id, &member_id, &headers.user.email, &conn).await {
            Ok(()) => String::new(),
            Err(e) => format!("{e:?}"),
        };

        bulk_response.push(json!(
            {
                "object": "OrganizationBulkConfirmResponseModel",
                "id": member_id,
                "error": err_msg
            }
        ));
    }

    Ok(Json(json!({
        "data": bulk_response,
        "object": "list",
        "continuationToken": null
    })))
}

#[post("/organizations/<org_id>/users/<member_id>/reinvite")]
async fn reinvite_member(
    org_id: OrganizationId,
    member_id: MembershipId,
    headers: AdminHeaders,
    conn: DbConn,
) -> EmptyResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    reinvite_member_impl(&org_id, &member_id, &headers.user.email, &conn).await
}

async fn reinvite_member_impl(
    org_id: &OrganizationId,
    member_id: &MembershipId,
    invited_by_email: &str,
    conn: &DbConn,
) -> EmptyResult {
    let Some(member) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else {
        err!("The user hasn't been invited to the organization.")
    };

    if member.status != MembershipStatus::Invited as i32 {
        err!("The user is already accepted or confirmed to the organization")
    }

    let Some(user) = User::find_by_uuid(&member.user_uuid, conn).await else {
        err!("User not found.")
    };

    if !CONFIG.invitations_allowed() && user.password_hash.is_empty() {
        err!("Invitations are not allowed.")
    }

    let org_name = if let Some(org) = Organization::find_by_uuid(org_id, conn).await {
        org.name
    } else {
        err!("Error looking up organization.")
    };

    if CONFIG.mail_enabled() {
        mail::send_invite(&user, org_id.clone(), member.uuid, &org_name, Some(invited_by_email.to_owned())).await?;
    } else if user.password_hash.is_empty() {
        let invitation = Invitation::new(&user.email);
        invitation.save(conn).await?;
    } else {
        Invitation::take(&user.email, conn).await;
        let mut member = member;
        member.status = MembershipStatus::Accepted as i32;
        member.save(conn).await?;
    }

    Ok(())
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct AcceptData {
    token: String,
    reset_password_key: Option<String>,
}

#[post("/organizations/<org_id>/users/<member_id>/accept", data = "<data>")]
async fn accept_invite(
    org_id: OrganizationId,
    member_id: MembershipId,
    data: Json<AcceptData>,
    headers: Headers,
    conn: DbConn,
) -> EmptyResult {
    // The web-vault passes org_id and member_id in the URL, but we are just reading them from the JWT instead
    let data: AcceptData = data.into_inner();
    let claims = decode_invite(&data.token)?;

    // Don't allow other users from accepting an invitation.
    if !claims.email.eq(&headers.user.email) {
        err!("Invitation was issued to a different account", "Claim does not match user_id")
    }

    // If a claim org_id does not match the one in from the URI, something is wrong.
    if !claims.org_id.eq(&org_id) {
        err!("Error accepting the invitation", "Claim does not match the org_id")
    }

    // If a claim does not have a member_id or it does not match the one in from the URI, something is wrong.
    if !claims.member_id.eq(&member_id) {
        err!("Error accepting the invitation", "Claim does not match the member_id")
    }

    let member_id = &claims.member_id;
    Invitation::take(&claims.email, &conn).await;

    // skip invitation logic when we were invited via the /admin panel
    if **member_id != FAKE_ADMIN_UUID {
        let Some(mut membership) = Membership::find_by_uuid_and_org(member_id, &claims.org_id, &conn).await else {
            err!("Error accepting the invitation")
        };

        let reset_password_key = match OrgPolicy::org_is_reset_password_auto_enroll(&membership.org_uuid, &conn).await {
            true if data.reset_password_key.is_none() => err!("Reset password key is required, but not provided."),
            true => data.reset_password_key,
            false => None,
        };

        // In case the user was invited before the mail was saved in db.
        membership.invited_by_email = membership.invited_by_email.or(claims.invited_by_email);

        accept_org_invite(&headers.user, membership, reset_password_key, &conn).await?;
    } else if CONFIG.mail_enabled() {
        // User was invited from /admin, so they are automatically confirmed
        let org_name = CONFIG.invitation_org_name();
        mail::send_invite_confirmed(&claims.email, &org_name).await?;
    }

    Ok(())
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ConfirmData {
    id: Option<MembershipId>,
    key: Option<String>,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct BulkConfirmData {
    keys: Option<Vec<ConfirmData>>,
}

#[post("/organizations/<org_id>/users/confirm", data = "<data>")]
async fn bulk_confirm_invite(
    org_id: OrganizationId,
    data: Json<BulkConfirmData>,
    headers: AdminHeaders,
    conn: DbConn,
    nt: Notify<'_>,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data = data.into_inner();

    let mut bulk_response = Vec::new();
    match data.keys {
        Some(keys) => {
            for invite in keys {
                let member_id = invite.id.unwrap();
                let user_key = invite.key.unwrap_or_default();
                let err_msg = match confirm_invite_impl(&org_id, &member_id, &user_key, &headers, &conn, &nt).await {
                    Ok(()) => String::new(),
                    Err(e) => format!("{e:?}"),
                };

                bulk_response.push(json!(
                    {
                        "object": "OrganizationBulkConfirmResponseModel",
                        "id": member_id,
                        "error": err_msg
                    }
                ));
            }
        }
        None => error!("No keys to confirm"),
    }

    Ok(Json(json!({
        "data": bulk_response,
        "object": "list",
        "continuationToken": null
    })))
}

#[post("/organizations/<org_id>/users/<member_id>/confirm", data = "<data>")]
async fn confirm_invite(
    org_id: OrganizationId,
    member_id: MembershipId,
    data: Json<ConfirmData>,
    headers: AdminHeaders,
    conn: DbConn,
    nt: Notify<'_>,
) -> EmptyResult {
    let data = data.into_inner();
    let user_key = data.key.unwrap_or_default();
    confirm_invite_impl(&org_id, &member_id, &user_key, &headers, &conn, &nt).await
}

async fn confirm_invite_impl(
    org_id: &OrganizationId,
    member_id: &MembershipId,
    key: &str,
    headers: &AdminHeaders,
    conn: &DbConn,
    nt: &Notify<'_>,
) -> EmptyResult {
    if org_id != &headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    if key.is_empty() || member_id.is_empty() {
        err!("Key or UserId is not set, unable to process request");
    }

    let Some(mut member_to_confirm) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else {
        err!("The specified user isn't a member of the organization")
    };

    if member_to_confirm.atype != MembershipType::User && headers.membership_type != MembershipType::Owner {
        err!("Only Owners can confirm Managers, Admins or Owners")
    }

    if member_to_confirm.status != MembershipStatus::Accepted as i32 {
        err!("User in invalid state")
    }

    member_to_confirm.status = MembershipStatus::Confirmed as i32;
    member_to_confirm.akey = key.to_owned();

    // This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type
    OrgPolicy::check_user_allowed(&member_to_confirm, "confirm", conn).await?;

    log_event(
        EventType::OrganizationUserConfirmed as i32,
        &member_to_confirm.uuid,
        org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        conn,
    )
    .await;

    if CONFIG.mail_enabled() {
        let org_name = if let Some(org) = Organization::find_by_uuid(org_id, conn).await {
            org.name
        } else {
            err!("Error looking up organization.")
        };
        let address = if let Some(user) = User::find_by_uuid(&member_to_confirm.user_uuid, conn).await {
            user.email
        } else {
            err!("Error looking up user.")
        };
        mail::send_invite_confirmed(&address, &org_name).await?;
    }

    let save_result = member_to_confirm.save(conn).await;

    if let Some(user) = User::find_by_uuid(&member_to_confirm.user_uuid, conn).await {
        nt.send_user_update(UpdateType::SyncOrgKeys, &user, headers.device.push_uuid.as_ref(), conn).await;
    }

    save_result
}

#[get("/organizations/<org_id>/users/mini-details", rank = 1)]
async fn get_org_user_mini_details(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
    if org_id != headers.membership.org_uuid {
        err!("Organization not found", "Organization id's do not match");
    }
    let mut members_json = Vec::new();
    for m in Membership::find_by_org(&org_id, &conn).await {
        members_json.push(m.to_json_mini_details(&conn).await);
    }

    Ok(Json(json!({
        "data": members_json,
        "object": "list",
        "continuationToken": null,
    })))
}

#[get("/organizations/<org_id>/users/<member_id>?<data..>", rank = 2)]
async fn get_user(
    org_id: OrganizationId,
    member_id: MembershipId,
    data: GetOrgUserData,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let Some(user) = Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await else {
        err!("The specified user isn't a member of the organization")
    };

    // In this case, when groups are requested we also need to include collections.
    // Else these will not be shown in the interface, and could lead to missing collections when saved.
    let include_groups = data.include_groups.unwrap_or(false);
    Ok(Json(user.to_json_user_details(data.include_collections.unwrap_or(include_groups), include_groups, &conn).await))
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct EditUserData {
    r#type: NumberOrString,
    collections: Option<Vec<CollectionData>>,
    groups: Option<Vec<GroupId>>,
    #[serde(default)]
    permissions: HashMap<String, Value>,
}

#[put("/organizations/<org_id>/users/<member_id>", data = "<data>", rank = 1)]
async fn put_member(
    org_id: OrganizationId,
    member_id: MembershipId,
    data: Json<EditUserData>,
    headers: AdminHeaders,
    conn: DbConn,
) -> EmptyResult {
    edit_member(org_id, member_id, data, headers, conn).await
}

#[post("/organizations/<org_id>/users/<member_id>", data = "<data>", rank = 1)]
async fn edit_member(
    org_id: OrganizationId,
    member_id: MembershipId,
    data: Json<EditUserData>,
    headers: AdminHeaders,
    conn: DbConn,
) -> EmptyResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: EditUserData = data.into_inner();

    // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission
    // The from_str() will convert the custom role type into a manager role type
    let raw_type = &data.r#type.into_string();
    // MembershipType::from_str will convert custom (4) to manager (3)
    let Some(new_type) = MembershipType::from_str(raw_type) else {
        err!("Invalid type")
    };

    // HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag
    // Since the parent checkbox is not sent to the server we need to check and verify the child checkboxes
    // If the box is not checked, the user will still be a manager, but not with the access_all permission
    let access_all = new_type >= MembershipType::Admin
        || (raw_type.eq("4")
            && data.permissions.get("editAnyCollection") == Some(&json!(true))
            && data.permissions.get("deleteAnyCollection") == Some(&json!(true))
            && data.permissions.get("createNewCollections") == Some(&json!(true)));

    let Some(mut member_to_edit) = Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await else {
        err!("The specified user isn't member of the organization")
    };

    if new_type != member_to_edit.atype
        && (member_to_edit.atype >= MembershipType::Admin || new_type >= MembershipType::Admin)
        && headers.membership_type != MembershipType::Owner
    {
        err!("Only Owners can grant and remove Admin or Owner privileges")
    }

    if member_to_edit.atype == MembershipType::Owner && headers.membership_type != MembershipType::Owner {
        err!("Only Owners can edit Owner users")
    }

    if member_to_edit.atype == MembershipType::Owner
        && new_type != MembershipType::Owner
        && member_to_edit.status == MembershipStatus::Confirmed as i32
    {
        // Removing owner permission, check that there is at least one other confirmed owner
        if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 {
            err!("Can't delete the last owner")
        }
    }

    member_to_edit.access_all = access_all;
    member_to_edit.atype = new_type as i32;

    // This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type
    // We need to perform the check after changing the type since `admin` is exempt.
    OrgPolicy::check_user_allowed(&member_to_edit, "modify", &conn).await?;

    // Delete all the odd collections
    for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &member_to_edit.user_uuid, &conn).await {
        c.delete(&conn).await?;
    }

    // If no accessAll, add the collections received
    if !access_all {
        for col in data.collections.iter().flatten() {
            match Collection::find_by_uuid_and_org(&col.id, &org_id, &conn).await {
                None => err!("Collection not found in Organization"),
                Some(collection) => {
                    CollectionUser::save(
                        &member_to_edit.user_uuid,
                        &collection.uuid,
                        col.read_only,
                        col.hide_passwords,
                        col.manage,
                        &conn,
                    )
                    .await?;
                }
            }
        }
    }

    GroupUser::delete_all_by_member(&member_to_edit.uuid, &conn).await?;

    for group_id in data.groups.iter().flatten() {
        if Group::find_by_uuid_and_org(group_id, &org_id, &conn).await.is_none() {
            err!("Group not found in Organization")
        }
        let mut group_entry = GroupUser::new(group_id.clone(), member_to_edit.uuid.clone());
        group_entry.save(&conn).await?;
    }

    log_event(
        EventType::OrganizationUserUpdated as i32,
        &member_to_edit.uuid,
        &org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        &conn,
    )
    .await;

    member_to_edit.save(&conn).await
}

#[delete("/organizations/<org_id>/users", data = "<data>")]
async fn bulk_delete_member(
    org_id: OrganizationId,
    data: Json<BulkMembershipIds>,
    headers: AdminHeaders,
    conn: DbConn,
    nt: Notify<'_>,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: BulkMembershipIds = data.into_inner();

    let mut bulk_response = Vec::new();
    for member_id in data.ids {
        let err_msg = match delete_member_impl(&org_id, &member_id, &headers, &conn, &nt).await {
            Ok(()) => String::new(),
            Err(e) => format!("{e:?}"),
        };

        bulk_response.push(json!(
            {
                "object": "OrganizationBulkConfirmResponseModel",
                "id": member_id,
                "error": err_msg
            }
        ));
    }

    Ok(Json(json!({
        "data": bulk_response,
        "object": "list",
        "continuationToken": null
    })))
}

#[delete("/organizations/<org_id>/users/<member_id>")]
async fn delete_member(
    org_id: OrganizationId,
    member_id: MembershipId,
    headers: AdminHeaders,
    conn: DbConn,
    nt: Notify<'_>,
) -> EmptyResult {
    delete_member_impl(&org_id, &member_id, &headers, &conn, &nt).await
}

async fn delete_member_impl(
    org_id: &OrganizationId,
    member_id: &MembershipId,
    headers: &AdminHeaders,
    conn: &DbConn,
    nt: &Notify<'_>,
) -> EmptyResult {
    if org_id != &headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let Some(member_to_delete) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else {
        err!("User to delete isn't member of the organization")
    };

    if member_to_delete.atype != MembershipType::User && headers.membership_type != MembershipType::Owner {
        err!("Only Owners can delete Admins or Owners")
    }

    if member_to_delete.atype == MembershipType::Owner && member_to_delete.status == MembershipStatus::Confirmed as i32
    {
        // Removing owner, check that there is at least one other confirmed owner
        if Membership::count_confirmed_by_org_and_type(org_id, MembershipType::Owner, conn).await <= 1 {
            err!("Can't delete the last owner")
        }
    }

    log_event(
        EventType::OrganizationUserRemoved as i32,
        &member_to_delete.uuid,
        org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        conn,
    )
    .await;

    if let Some(user) = User::find_by_uuid(&member_to_delete.user_uuid, conn).await {
        nt.send_user_update(UpdateType::SyncOrgKeys, &user, headers.device.push_uuid.as_ref(), conn).await;

        if !CONFIG.mail_enabled()
            && !Membership::find_invited_by_user(&user.uuid, conn)
                .await
                .into_iter()
                .any(|m| m.uuid != member_to_delete.uuid)
        {
            Invitation::take(&user.email, conn).await;
        }
    }

    member_to_delete.delete(conn).await
}

#[post("/organizations/<org_id>/users/public-keys", data = "<data>")]
async fn bulk_public_keys(
    org_id: OrganizationId,
    data: Json<BulkMembershipIds>,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: BulkMembershipIds = data.into_inner();

    let mut bulk_response = Vec::new();
    // Check all received Membership UUID's and find the matching User to retrieve the public-key.
    // If the user does not exists, just ignore it, and do not return any information regarding that Membership UUID.
    // The web-vault will then ignore that user for the following steps.
    for member_id in data.ids {
        match Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await {
            Some(member) => match User::find_by_uuid(&member.user_uuid, &conn).await {
                Some(user) => bulk_response.push(json!(
                    {
                        "object": "organizationUserPublicKeyResponseModel",
                        "id": member_id,
                        "userId": user.uuid,
                        "key": user.public_key
                    }
                )),
                None => debug!("User doesn't exist"),
            },
            None => debug!("Membership doesn't exist"),
        }
    }

    Ok(Json(json!({
        "data": bulk_response,
        "object": "list",
        "continuationToken": null
    })))
}

use super::ciphers::CipherData;
use super::ciphers::update_cipher_from_data;

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ImportData {
    ciphers: Vec<CipherData>,
    collections: Vec<FullCollectionData>,
    collection_relationships: Vec<RelationsData>,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RelationsData {
    // Cipher index
    key: usize,
    // Collection index
    value: usize,
}

// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/ImportCiphersController.cs#L62
#[post("/ciphers/import-organization?<query..>", data = "<data>")]
async fn post_org_import(
    query: OrgIdData,
    data: Json<ImportData>,
    headers: OrgMemberHeaders,
    conn: DbConn,
    nt: Notify<'_>,
) -> EmptyResult {
    let org_id = query.organization_id;
    if org_id != headers.membership.org_uuid {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: ImportData = data.into_inner();

    // Validate the import before continuing
    // Bitwarden does not process the import if there is one item invalid.
    // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.
    // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
    Cipher::validate_cipher_data(&data.ciphers)?;

    let existing_collections: HashMap<CollectionId, Collection> =
        Collection::find_by_organization(&org_id, &conn).await.into_iter().map(|c| (c.uuid.clone(), c)).collect();
    let mut collections: Vec<CollectionId> = Vec::with_capacity(data.collections.len());
    for col in data.collections {
        let existing = col.id.as_ref().and_then(|col_id| existing_collections.get(col_id));
        let collection_uuid = if let Some(collection) = existing {
            // When not an Owner or Admin, check if the member is allowed to write to the collection.
            if headers.membership.atype < MembershipType::Admin
                && !collection.is_writable_by_user(&headers.membership.user_uuid, &conn).await
            {
                err!(Compact, "The current user isn't allowed to manage this collection")
            }
            collection.uuid.clone()
        } else {
            // We do not allow users or managers which can not manage all collections to create new collections
            // If there is any collection other than an existing import collection, abort the import.
            if headers.membership.atype <= MembershipType::Manager && !headers.membership.has_full_access() {
                err!(Compact, "The current user isn't allowed to create new collections")
            }
            let new_collection = Collection::new(org_id.clone(), col.name, col.external_id);
            new_collection.save(&conn).await?;
            new_collection.uuid
        };

        collections.push(collection_uuid);
    }

    // Read the relations between collections and ciphers
    // Ciphers can be in multiple collections at the same time
    let mut relations = Vec::with_capacity(data.collection_relationships.len());
    for relation in data.collection_relationships {
        relations.push((relation.key, relation.value));
    }

    let headers: Headers = headers.into();

    let mut ciphers: Vec<CipherId> = Vec::with_capacity(data.ciphers.len());
    for mut cipher_data in data.ciphers {
        // Always clear folder_id's via an organization import
        cipher_data.folder_id = None;
        // Replace the client-provided, unvalidated organizationId with the real target org
        cipher_data.organization_id = Some(org_id.clone());
        let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone());
        update_cipher_from_data(
            &mut cipher,
            cipher_data,
            &headers,
            Some(collections.clone()),
            &conn,
            &nt,
            UpdateType::None,
        )
        .await
        .ok();
        ciphers.push(cipher.uuid);
    }

    // Assign the collections
    for (cipher_index, col_index) in relations {
        let (Some(cipher_id), Some(col_id)) = (ciphers.get(cipher_index), collections.get(col_index)) else {
            err!(Compact, "Invalid collection relationship")
        };
        CollectionCipher::save(cipher_id, col_id, &conn).await?;
    }

    let mut user = headers.user;
    user.update_revision(&conn).await
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct BulkCollectionsData {
    organization_id: OrganizationId,
    cipher_ids: Vec<CipherId>,
    collection_ids: HashSet<CollectionId>,
    remove_collections: bool,
}

// This endpoint is only reachable via the organization view, therefore this endpoint is located here
// Also Bitwarden does not send out Notifications for these changes, it only does this for individual cipher collection updates
#[post("/ciphers/bulk-collections", data = "<data>")]
async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers, conn: DbConn) -> EmptyResult {
    let data: BulkCollectionsData = data.into_inner();

    if Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &data.organization_id, &conn).await.is_none() {
        err!("You need to be a Member of the Organization to call this endpoint")
    }

    // Get all the collection available to the user in one query
    // Also filter based upon the provided collections
    let user_collections: HashMap<CollectionId, Collection> =
        Collection::find_by_organization_and_user_uuid(&data.organization_id, &headers.user.uuid, &conn)
            .await
            .into_iter()
            .filter_map(|c| {
                if data.collection_ids.contains(&c.uuid) {
                    Some((c.uuid.clone(), c))
                } else {
                    None
                }
            })
            .collect();

    // Verify if all the collections requested exists and are writable for the user, else abort
    for collection_uuid in &data.collection_ids {
        match user_collections.get(collection_uuid) {
            Some(collection) if collection.is_writable_by_user(&headers.user.uuid, &conn).await => (),
            _ => err_code!("Resource not found", "User does not have access to a collection", 404),
        }
    }

    for cipher_id in &data.cipher_ids {
        // Only act on existing cipher uuid's
        // Do not abort the operation just ignore it, it could be a cipher was just deleted for example
        if let Some(cipher) = Cipher::find_by_uuid_and_org(cipher_id, &data.organization_id, &conn).await
            && cipher.is_write_accessible_to_user(&headers.user.uuid, &conn).await
        {
            // When selecting a specific collection from the left filter list, and use the bulk option, you can remove an item from that collection
            // In these cases the client will call this endpoint twice, once for adding the new collections and a second for deleting.
            if data.remove_collections {
                for collection in &data.collection_ids {
                    CollectionCipher::delete(&cipher.uuid, collection, &conn).await?;
                }
            } else {
                for collection in &data.collection_ids {
                    CollectionCipher::save(&cipher.uuid, collection, &conn).await?;
                }
            }
        }
    }

    Ok(())
}

#[get("/organizations/<org_id>/policies")]
async fn list_policies(org_id: OrganizationId, headers: AdminHeaders, conn: DbConn) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let policies = OrgPolicy::find_by_org(&org_id, &conn).await;
    let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();

    Ok(Json(json!({
        "data": policies_json,
        "object": "list",
        "continuationToken": null
    })))
}

#[get("/organizations/<org_id>/policies/token?<token>")]
async fn list_policies_token(org_id: OrganizationId, token: &str, conn: DbConn) -> JsonResult {
    let invite = decode_invite(token)?;

    if invite.org_id != org_id {
        err!("Token doesn't match request organization");
    }

    // exit early when we have been invited via /admin panel
    if org_id.as_ref() == FAKE_ADMIN_UUID {
        return Ok(Json(json!({})));
    }

    // TODO: We receive the invite token as ?token=<>, validate it contains the org id
    let policies = OrgPolicy::find_by_org(&org_id, &conn).await;
    let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();

    Ok(Json(json!({
        "data": policies_json,
        "object": "list",
        "continuationToken": null
    })))
}

// Called during the SSO enrollment return the default policy
#[get("/organizations/00000000-01DC-01DC-01DC-000000000000/policies/master-password", rank = 1)]
fn get_dummy_master_password_policy() -> JsonResult {
    let (enabled, data) = match CONFIG.sso_master_password_policy_value() {
        Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()),
        _ => (false, "null".to_owned()),
    };
    let policy = OrgPolicy::new(FAKE_SSO_IDENTIFIER.into(), OrgPolicyType::MasterPassword, enabled, data);
    Ok(Json(policy.to_json()))
}

// Called during the SSO enrollment return the org policy if it exists
#[get("/organizations/<org_id>/policies/master-password", rank = 2)]
async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberHeaders, conn: DbConn) -> JsonResult {
    let policy =
        OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &conn).await.unwrap_or_else(|| {
            let (enabled, data) = match CONFIG.sso_master_password_policy_value() {
                Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()),
                _ => (false, "null".to_owned()),
            };

            OrgPolicy::new(org_id, OrgPolicyType::MasterPassword, enabled, data)
        });

    Ok(Json(policy.to_json()))
}

#[get("/organizations/<org_id>/policies/<pol_type>", rank = 3)]
async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders, conn: DbConn) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }

    let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else {
        err!("Invalid or unsupported policy type")
    };

    let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await {
        Some(p) => p,
        None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "null".to_owned()),
    };

    Ok(Json(policy.to_json()))
}

#[derive(Deserialize)]
struct PolicyData {
    enabled: bool,
    data: Option<Value>,
}

#[derive(Deserialize)]
struct PutPolicy {
    policy: PolicyData,
    // Ignore metadata for now as we do not yet support this
    // "metadata": {
    //     "defaultUserCollectionName": "2.xx|xx==|xx="
    // }
}

#[put("/organizations/<org_id>/policies/<pol_type>", data = "<data>")]
async fn put_policy(
    org_id: OrganizationId,
    pol_type: i32,
    data: Json<PutPolicy>,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: PolicyData = data.into_inner().policy;

    let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else {
        err!("Invalid or unsupported policy type")
    };

    // Bitwarden only allows the Reset Password policy when Single Org policy is enabled
    // Vaultwarden encouraged to use multiple orgs instead of groups because groups were not available in the past
    // Now that groups are available we can enforce this option when wanted.
    // We put this behind a config option to prevent breaking current installation.
    // Maybe we want to enable this by default in the future, but currently it is disabled by default.
    if CONFIG.enforce_single_org_with_reset_pw_policy() {
        if pol_type_enum == OrgPolicyType::ResetPassword && data.enabled {
            let single_org_policy_enabled =
                match OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::SingleOrg, &conn).await {
                    Some(p) => p.enabled,
                    None => false,
                };

            if !single_org_policy_enabled {
                err!("Single Organization policy is not enabled. It is mandatory for this policy to be enabled.")
            }
        }

        // Also prevent the Single Org Policy to be disabled if the Reset Password policy is enabled
        if pol_type_enum == OrgPolicyType::SingleOrg && !data.enabled {
            let reset_pw_policy_enabled =
                match OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::ResetPassword, &conn).await {
                    Some(p) => p.enabled,
                    None => false,
                };

            if reset_pw_policy_enabled {
                err!("Account recovery policy is enabled. It is not allowed to disable this policy.")
            }
        }
    }

    // When enabling the TwoFactorAuthentication policy, revoke all members that do not have 2FA
    if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled {
        two_factor::enforce_2fa_policy_for_org(
            &org_id,
            &headers.user.uuid,
            headers.device.atype,
            &headers.ip.ip,
            &conn,
        )
        .await?;
    }

    // When enabling the SingleOrg policy, remove this org's members that are members of other orgs
    if pol_type_enum == OrgPolicyType::SingleOrg && data.enabled {
        for mut member in Membership::find_by_org(&org_id, &conn).await {
            // Policy only applies to non-Owner/non-Admin members who have accepted joining the org
            // Exclude invited and revoked users when checking for this policy.
            // Those users will not be allowed to accept or be activated because of the policy checks done there.
            if member.atype < MembershipType::Admin
                && member.status != MembershipStatus::Invited as i32
                && Membership::count_accepted_and_confirmed_by_user(&member.user_uuid, &member.org_uuid, &conn).await
                    > 0
            {
                if CONFIG.mail_enabled() {
                    let org = Organization::find_by_uuid(&member.org_uuid, &conn).await.unwrap();
                    let user = User::find_by_uuid(&member.user_uuid, &conn).await.unwrap();

                    mail::send_single_org_removed_from_org(&user.email, &org.name).await?;
                }

                log_event(
                    EventType::OrganizationUserRemoved as i32,
                    &member.uuid,
                    &org_id,
                    &headers.user.uuid,
                    headers.device.atype,
                    &headers.ip.ip,
                    &conn,
                )
                .await;

                member.revoke();
                member.save(&conn).await?;
            }
        }
    }

    let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await {
        Some(p) => p,
        None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "{}".to_owned()),
    };

    policy.enabled = data.enabled;
    policy.data = serde_json::to_string(&data.data)?;
    policy.save(&conn).await?;

    log_event(
        EventType::PolicyUpdated as i32,
        policy.uuid.as_ref(),
        &org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        &conn,
    )
    .await;

    Ok(Json(policy.to_json()))
}

// Deprecated with client v2026.5.0
#[put("/organizations/<org_id>/policies/<pol_type>/vnext", data = "<data>")]
async fn put_policy_vnext(
    org_id: OrganizationId,
    pol_type: i32,
    data: Json<PutPolicy>,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    put_policy(org_id, pol_type, data, headers, conn).await
}

#[get("/plans")]
fn get_plans() -> Json<Value> {
    // Respond with a minimal json just enough to allow the creation of an new organization.
    Json(json!({
        "object": "list",
        "data": [{
            "object": "plan",
            "type": 0,
            "product": 0,
            "name": "Free",
            "nameLocalizationKey": "planNameFree",
            "bitwardenProduct": 0,
            "maxUsers": 0,
            "descriptionLocalizationKey": "planDescFree"
        },{
            "object": "plan",
            "type": 0,
            "product": 1,
            "name": "Free",
            "nameLocalizationKey": "planNameFree",
            "bitwardenProduct": 1,
            "maxUsers": 0,
            "descriptionLocalizationKey": "planDescFree"
        }],
        "continuationToken": null
    }))
}

#[get("/organizations/<_org_id>/billing/metadata")]
fn get_billing_metadata(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> Json<Value> {
    // Prevent a 404 error, which also causes Javascript errors.
    Json(empty_data_json())
}

#[get("/organizations/<_org_id>/billing/vnext/warnings")]
fn get_billing_warnings(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> Json<Value> {
    Json(json!({
        "freeTrial":null,
        "inactiveSubscription":null,
        "resellerRenewal":null,
        "taxId":null,
    }))
}

#[get("/organizations/<_org_id>/billing/vnext/self-host/metadata")]
fn get_self_host_billing_metadata(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> Json<Value> {
    // Prevent a 404 error, which also causes Javascript errors.
    Json(json!({
        "isOnSecretsManagerStandalone": false, // Secrets Manager is not supported by Vaultwarden
        "organizationOccupiedSeats": 0 // Vaultwarden does not count seats
    }))
}

fn empty_data_json() -> Value {
    json!({
        "object": "list",
        "data": [],
        "continuationToken": null
    })
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct BulkRevokeMembershipIds {
    ids: Option<Vec<MembershipId>>,
}

#[put("/organizations/<org_id>/users/<member_id>/revoke")]
async fn revoke_member(
    org_id: OrganizationId,
    member_id: MembershipId,
    headers: AdminHeaders,
    conn: DbConn,
) -> EmptyResult {
    revoke_member_impl(&org_id, &member_id, &headers, &conn).await
}

#[put("/organizations/<org_id>/users/revoke", data = "<data>")]
async fn bulk_revoke_members(
    org_id: OrganizationId,
    data: Json<BulkRevokeMembershipIds>,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data = data.into_inner();

    let mut bulk_response = Vec::new();
    match data.ids {
        Some(members) => {
            for member_id in members {
                let err_msg = match revoke_member_impl(&org_id, &member_id, &headers, &conn).await {
                    Ok(()) => String::new(),
                    Err(e) => format!("{e:?}"),
                };

                bulk_response.push(json!(
                    {
                        "object": "OrganizationUserBulkResponseModel",
                        "id": member_id,
                        "error": err_msg
                    }
                ));
            }
        }
        None => error!("No users to revoke"),
    }

    Ok(Json(json!({
        "data": bulk_response,
        "object": "list",
        "continuationToken": null
    })))
}

async fn revoke_member_impl(
    org_id: &OrganizationId,
    member_id: &MembershipId,
    headers: &AdminHeaders,
    conn: &DbConn,
) -> EmptyResult {
    if org_id != &headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    match Membership::find_by_uuid_and_org(member_id, org_id, conn).await {
        Some(mut member) if member.status > MembershipStatus::Revoked as i32 => {
            if member.user_uuid == headers.user.uuid {
                err!("You cannot revoke yourself")
            }
            if member.atype == MembershipType::Owner && headers.membership_type != MembershipType::Owner {
                err!("Only owners can revoke other owners")
            }
            if member.atype == MembershipType::Owner
                && Membership::count_confirmed_by_org_and_type(org_id, MembershipType::Owner, conn).await <= 1
            {
                err!("Organization must have at least one confirmed owner")
            }

            member.revoke();
            member.save(conn).await?;

            log_event(
                EventType::OrganizationUserRevoked as i32,
                &member.uuid,
                org_id,
                &headers.user.uuid,
                headers.device.atype,
                &headers.ip.ip,
                conn,
            )
            .await;
        }
        Some(_) => err!("User is already revoked"),
        None => err!("User not found in organization"),
    }
    Ok(())
}

#[put("/organizations/<org_id>/users/<member_id>/restore/vnext")]
async fn restore_member_vnext(
    org_id: OrganizationId,
    member_id: MembershipId,
    headers: AdminHeaders,
    conn: DbConn,
) -> EmptyResult {
    // Vaultwarden does not (yet) support the per User Collection linked to the `Enforce organization data ownership` policy.
    // Therefor we ignore the `defaultUserCollectionName` data sent and just call restore_member
    restore_member_impl(&org_id, &member_id, &headers, &conn).await
}

#[put("/organizations/<org_id>/users/<member_id>/restore")]
async fn restore_member(
    org_id: OrganizationId,
    member_id: MembershipId,
    headers: AdminHeaders,
    conn: DbConn,
) -> EmptyResult {
    restore_member_impl(&org_id, &member_id, &headers, &conn).await
}

#[put("/organizations/<org_id>/users/restore", data = "<data>")]
async fn bulk_restore_members(
    org_id: OrganizationId,
    data: Json<BulkMembershipIds>,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data = data.into_inner();

    let mut bulk_response = Vec::new();
    for member_id in data.ids {
        let err_msg = match restore_member_impl(&org_id, &member_id, &headers, &conn).await {
            Ok(()) => String::new(),
            Err(e) => format!("{e:?}"),
        };

        bulk_response.push(json!(
            {
                "object": "OrganizationUserBulkResponseModel",
                "id": member_id,
                "error": err_msg
            }
        ));
    }

    Ok(Json(json!({
        "data": bulk_response,
        "object": "list",
        "continuationToken": null
    })))
}

async fn restore_member_impl(
    org_id: &OrganizationId,
    member_id: &MembershipId,
    headers: &AdminHeaders,
    conn: &DbConn,
) -> EmptyResult {
    if org_id != &headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    match Membership::find_by_uuid_and_org(member_id, org_id, conn).await {
        Some(mut member) if member.status < MembershipStatus::Accepted as i32 => {
            if member.user_uuid == headers.user.uuid {
                err!("You cannot restore yourself")
            }
            if member.atype == MembershipType::Owner && headers.membership_type != MembershipType::Owner {
                err!("Only owners can restore other owners")
            }

            member.restore();
            // This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type
            // This check need to be done after restoring to work with the correct status
            OrgPolicy::check_user_allowed(&member, "restore", conn).await?;
            member.save(conn).await?;

            log_event(
                EventType::OrganizationUserRestored as i32,
                &member.uuid,
                org_id,
                &headers.user.uuid,
                headers.device.atype,
                &headers.ip.ip,
                conn,
            )
            .await;
        }
        Some(_) => err!("User is already active"),
        None => err!("User not found in organization"),
    }
    Ok(())
}

async fn get_groups_data(
    details: bool,
    org_id: OrganizationId,
    headers: ManagerHeadersLoose,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.membership.org_uuid {
        err!("Organization not found", "Organization id's do not match");
    }

    // The details view (group→collection/user mappings) needs full org access; the plain list only
    // needs manage access to a collection, so a manager of a collection (directly or via a group)
    // can load it to assign groups.
    let has_full_access = headers.membership.has_full_access()
        || (CONFIG.org_groups_enabled()
            && GroupUser::has_full_access_by_member(&org_id, &headers.membership.uuid, &conn).await);
    let allowed = if details {
        has_full_access
    } else {
        has_full_access
            || Collection::has_manageable_collection_by_user(&org_id, &headers.membership.user_uuid, &conn).await
    };
    if !allowed {
        err_code!("Resource not found.", "User does not have access", rocket::http::Status::NotFound.code);
    }

    let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
        let groups = Group::find_by_organization(&org_id, &conn).await;
        let mut groups_json = Vec::with_capacity(groups.len());

        if details {
            for g in groups {
                groups_json.push(g.to_json_details(&conn).await);
            }
        } else {
            for g in groups {
                groups_json.push(g.to_json());
            }
        }
        groups_json
    } else {
        // The Bitwarden clients seem to call this API regardless of whether groups are enabled,
        // so just act as if there are no groups.
        Vec::new()
    };

    Ok(Json(json!({
        "data": groups,
        "object": "list",
        "continuationToken": null,
    })))
}

#[get("/organizations/<org_id>/groups")]
async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
    get_groups_data(false, org_id, headers, conn).await
}

#[get("/organizations/<org_id>/groups/details", rank = 1)]
async fn get_groups_details(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
    get_groups_data(true, org_id, headers, conn).await
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct GroupRequest {
    name: String,
    #[serde(default)]
    access_all: bool,
    external_id: Option<String>,
    collections: Vec<CollectionData>,
    users: Vec<MembershipId>,
}

impl GroupRequest {
    pub fn to_group(&self, org_uuid: &OrganizationId) -> Group {
        Group::new(org_uuid.clone(), self.name.clone(), self.access_all, self.external_id.clone())
    }

    pub fn update_group(&self, mut group: Group) -> Group {
        group.name.clone_from(&self.name);
        group.access_all = self.access_all;
        // Group Updates do not support changing the external_id
        // These input fields are in a disabled state, and can only be updated/added via ldap_import

        group
    }

    /// Validate if all the collections and members belong to the provided organization
    pub async fn validate(&self, org_id: &OrganizationId, conn: &DbConn) -> EmptyResult {
        let org_collections = Collection::find_by_organization(org_id, conn).await;
        let org_collection_ids: HashSet<&CollectionId> = org_collections.iter().map(|c| &c.uuid).collect();
        if let Some(e) = self.collections.iter().find(|c| !org_collection_ids.contains(&c.id)) {
            err!("Invalid collection", format!("Collection {} does not belong to organization {}!", e.id, org_id))
        }

        let org_memberships = Membership::find_by_org(org_id, conn).await;
        let org_membership_ids: HashSet<&MembershipId> = org_memberships.iter().map(|m| &m.uuid).collect();
        if let Some(e) = self.users.iter().find(|m| !org_membership_ids.contains(m)) {
            err!("Invalid member", format!("Member {} does not belong to organization {}!", e, org_id))
        }

        Ok(())
    }
}

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct CollectionData {
    id: CollectionId,
    read_only: bool,
    hide_passwords: bool,
    manage: bool,
}

impl CollectionData {
    pub fn to_collection_group(&self, groups_uuid: GroupId) -> CollectionGroup {
        CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords, self.manage)
    }
}

#[post("/organizations/<org_id>/groups/<group_id>", data = "<data>")]
async fn post_group(
    org_id: OrganizationId,
    group_id: GroupId,
    data: Json<GroupRequest>,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    put_group(org_id, group_id, data, headers, conn).await
}

#[post("/organizations/<org_id>/groups", data = "<data>")]
async fn post_groups(
    org_id: OrganizationId,
    headers: AdminHeaders,
    data: Json<GroupRequest>,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    if !CONFIG.org_groups_enabled() {
        err!("Group support is disabled");
    }

    let group_request = data.into_inner();
    group_request.validate(&org_id, &conn).await?;

    let group = group_request.to_group(&org_id);

    log_event(
        EventType::GroupCreated as i32,
        &group.uuid,
        &org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        &conn,
    )
    .await;

    add_update_group(group, group_request.collections, group_request.users, org_id, &headers, &conn).await
}

#[put("/organizations/<org_id>/groups/<group_id>", data = "<data>")]
async fn put_group(
    org_id: OrganizationId,
    group_id: GroupId,
    data: Json<GroupRequest>,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    if !CONFIG.org_groups_enabled() {
        err!("Group support is disabled");
    }

    let Some(group) = Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await else {
        err!("Group not found", "Group uuid is invalid or does not belong to the organization")
    };

    let group_request = data.into_inner();
    group_request.validate(&org_id, &conn).await?;

    let updated_group = group_request.update_group(group);

    CollectionGroup::delete_all_by_group(&group_id, &org_id, &conn).await?;
    GroupUser::delete_all_by_group(&group_id, &org_id, &conn).await?;

    log_event(
        EventType::GroupUpdated as i32,
        &updated_group.uuid,
        &org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        &conn,
    )
    .await;

    add_update_group(updated_group, group_request.collections, group_request.users, org_id, &headers, &conn).await
}

async fn add_update_group(
    mut group: Group,
    collections: Vec<CollectionData>,
    members: Vec<MembershipId>,
    org_id: OrganizationId,
    headers: &AdminHeaders,
    conn: &DbConn,
) -> JsonResult {
    group.save(conn).await?;

    for col_selection in collections {
        let mut collection_group = col_selection.to_collection_group(group.uuid.clone());
        collection_group.save(&org_id, conn).await?;
    }

    for assigned_member in members {
        let mut user_entry = GroupUser::new(group.uuid.clone(), assigned_member.clone());
        user_entry.save(conn).await?;

        log_event(
            EventType::OrganizationUserUpdatedGroups as i32,
            &assigned_member,
            &org_id,
            &headers.user.uuid,
            headers.device.atype,
            &headers.ip.ip,
            conn,
        )
        .await;
    }

    Ok(Json(json!({
        "id": group.uuid,
        "organizationId": group.organizations_uuid,
        "name": group.name,
        "accessAll": group.access_all,
        "externalId": group.external_id,
        "object": "group"
    })))
}

#[get("/organizations/<org_id>/groups/<group_id>/details")]
async fn get_group_details(
    org_id: OrganizationId,
    group_id: GroupId,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    if !CONFIG.org_groups_enabled() {
        err!("Group support is disabled");
    }

    let Some(group) = Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await else {
        err!("Group not found", "Group uuid is invalid or does not belong to the organization")
    };

    Ok(Json(group.to_json_details(&conn).await))
}

#[post("/organizations/<org_id>/groups/<group_id>/delete")]
async fn post_delete_group(
    org_id: OrganizationId,
    group_id: GroupId,
    headers: AdminHeaders,
    conn: DbConn,
) -> EmptyResult {
    delete_group_impl(&org_id, &group_id, &headers, &conn).await
}

#[delete("/organizations/<org_id>/groups/<group_id>")]
async fn delete_group(org_id: OrganizationId, group_id: GroupId, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
    delete_group_impl(&org_id, &group_id, &headers, &conn).await
}

async fn delete_group_impl(
    org_id: &OrganizationId,
    group_id: &GroupId,
    headers: &AdminHeaders,
    conn: &DbConn,
) -> EmptyResult {
    if org_id != &headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    if !CONFIG.org_groups_enabled() {
        err!("Group support is disabled");
    }

    let Some(group) = Group::find_by_uuid_and_org(group_id, org_id, conn).await else {
        err!("Group not found", "Group uuid is invalid or does not belong to the organization")
    };

    log_event(
        EventType::GroupDeleted as i32,
        &group.uuid,
        org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        conn,
    )
    .await;

    group.delete(org_id, conn).await
}

#[delete("/organizations/<org_id>/groups", data = "<data>")]
async fn bulk_delete_groups(
    org_id: OrganizationId,
    data: Json<BulkGroupIds>,
    headers: AdminHeaders,
    conn: DbConn,
) -> EmptyResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    if !CONFIG.org_groups_enabled() {
        err!("Group support is disabled");
    }

    let data: BulkGroupIds = data.into_inner();

    for group_id in data.ids {
        delete_group_impl(&org_id, &group_id, &headers, &conn).await?;
    }
    Ok(())
}

#[get("/organizations/<org_id>/groups/<group_id>", rank = 2)]
async fn get_group(org_id: OrganizationId, group_id: GroupId, headers: AdminHeaders, conn: DbConn) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    if !CONFIG.org_groups_enabled() {
        err!("Group support is disabled");
    }

    let Some(group) = Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await else {
        err!("Group not found", "Group uuid is invalid or does not belong to the organization")
    };

    Ok(Json(group.to_json()))
}

#[get("/organizations/<org_id>/groups/<group_id>/users")]
async fn get_group_members(
    org_id: OrganizationId,
    group_id: GroupId,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    if !CONFIG.org_groups_enabled() {
        err!("Group support is disabled");
    }

    if Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await.is_none() {
        err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization")
    }

    let group_members: Vec<MembershipId> = GroupUser::find_by_group(&group_id, &org_id, &conn)
        .await
        .iter()
        .map(|entry| entry.users_organizations_uuid.clone())
        .collect();

    Ok(Json(json!(group_members)))
}

#[put("/organizations/<org_id>/groups/<group_id>/users", data = "<data>")]
async fn put_group_members(
    org_id: OrganizationId,
    group_id: GroupId,
    headers: AdminHeaders,
    data: Json<Vec<MembershipId>>,
    conn: DbConn,
) -> EmptyResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    if !CONFIG.org_groups_enabled() {
        err!("Group support is disabled");
    }

    if Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await.is_none() {
        err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization")
    }

    let assigned_members = data.into_inner();

    let org_memberships = Membership::find_by_org(&org_id, &conn).await;
    let org_membership_ids: HashSet<&MembershipId> = org_memberships.iter().map(|m| &m.uuid).collect();
    if let Some(e) = assigned_members.iter().find(|m| !org_membership_ids.contains(m)) {
        err!("Invalid member", format!("Member {} does not belong to organization {}!", e, org_id))
    }

    GroupUser::delete_all_by_group(&group_id, &org_id, &conn).await?;
    for assigned_member in assigned_members {
        let mut user_entry = GroupUser::new(group_id.clone(), assigned_member.clone());
        user_entry.save(&conn).await?;

        log_event(
            EventType::OrganizationUserUpdatedGroups as i32,
            &assigned_member,
            &org_id,
            &headers.user.uuid,
            headers.device.atype,
            &headers.ip.ip,
            &conn,
        )
        .await;
    }

    Ok(())
}

#[post("/organizations/<org_id>/groups/<group_id>/delete-user/<member_id>")]
async fn post_delete_group_member(
    org_id: OrganizationId,
    group_id: GroupId,
    member_id: MembershipId,
    headers: AdminHeaders,
    conn: DbConn,
) -> EmptyResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    if !CONFIG.org_groups_enabled() {
        err!("Group support is disabled");
    }

    if Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await.is_none() {
        err!("User could not be found or does not belong to the organization.");
    }

    if Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await.is_none() {
        err!("Group could not be found or does not belong to the organization.");
    }

    log_event(
        EventType::OrganizationUserUpdatedGroups as i32,
        &member_id,
        &org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        &conn,
    )
    .await;

    GroupUser::delete_by_group_and_member(&group_id, &member_id, &conn).await
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct OrganizationUserResetPasswordEnrollmentRequest {
    reset_password_key: Option<String>,
    master_password_hash: Option<String>,
    otp: Option<String>,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct OrganizationUserRecoverAccountRequest {
    new_master_password_hash: String,
    key: String,

    #[serde(default)]
    reset_master_password: bool,
    #[serde(default)]
    reset_two_factor: bool,
}

// Upstream reports this is the renamed endpoint instead of `/keys`
// But the clients do not seem to use this at all
// Just add it here in case they will
#[get("/organizations/<org_id>/public-key")]
async fn get_organization_public_key(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult {
    if org_id != headers.membership.org_uuid {
        err!("Organization not found", "Organization id's do not match");
    }
    let Some(org) = Organization::find_by_uuid(&org_id, &conn).await else {
        err!("Organization not found")
    };

    Ok(Json(json!({
        "object": "organizationPublicKey",
        "publicKey": org.public_key,
    })))
}

// Obsolete - Renamed to public-key (2023.8), left for backwards compatibility with older clients
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L487-L492
#[get("/organizations/<org_id>/keys")]
async fn get_organization_keys(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult {
    get_organization_public_key(org_id, headers, conn).await
}

// Will allow to reset 2FA too
// https://github.com/bitwarden/clients/blob/web-v2026.4.2/libs/admin-console/src/common/organization-user/models/requests/organization-user-reset-password.request.ts
#[put("/organizations/<org_id>/users/<member_id>/recover-account", data = "<data>")]
async fn put_recover_account(
    org_id: OrganizationId,
    member_id: MembershipId,
    headers: AdminHeaders,
    data: Json<OrganizationUserRecoverAccountRequest>,
    conn: DbConn,
    nt: Notify<'_>,
) -> EmptyResult {
    let req = data.into_inner();
    if req.reset_master_password && !req.reset_two_factor {
        recover_account(org_id, member_id, headers, req, conn, nt).await
    } else {
        err!("Unsupported operation")
    }
}

// Deprecated since `v2026.4.2`
#[put("/organizations/<org_id>/users/<member_id>/reset-password", data = "<data>")]
async fn put_reset_password(
    org_id: OrganizationId,
    member_id: MembershipId,
    headers: AdminHeaders,
    data: Json<OrganizationUserRecoverAccountRequest>,
    conn: DbConn,
    nt: Notify<'_>,
) -> EmptyResult {
    recover_account(org_id, member_id, headers, data.into_inner(), conn, nt).await
}

async fn recover_account(
    org_id: OrganizationId,
    member_id: MembershipId,
    headers: AdminHeaders,
    reset_request: OrganizationUserRecoverAccountRequest,
    conn: DbConn,
    nt: Notify<'_>,
) -> EmptyResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let Some(org) = Organization::find_by_uuid(&org_id, &conn).await else {
        err!("Required organization not found")
    };

    let Some(member) = Membership::find_by_uuid_and_org(&member_id, &org.uuid, &conn).await else {
        err!("User to reset isn't member of required organization")
    };

    let Some(user) = User::find_by_uuid(&member.user_uuid, &conn).await else {
        err!("User not found")
    };

    check_reset_password_applicable_and_permissions(&org_id, &member_id, &headers, &conn).await?;

    if member.reset_password_key.is_none() {
        err!("Password reset not or not correctly enrolled");
    }
    if member.status != (MembershipStatus::Confirmed as i32) {
        err!("Organization user must be confirmed for password reset functionality");
    }

    // Sending email before resetting password to ensure working email configuration and the resulting
    // user notification. Also this might add some protection against security flaws and misuse
    if let Err(e) = mail::send_admin_reset_password(&user.email, user.display_name(), &org.name).await {
        err!(format!("Error sending user reset password email: {e:#?}"));
    }

    let mut user = user;
    user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None, &conn)
        .await?;
    user.save(&conn).await?;

    nt.send_logout(&user, None, &conn).await;

    log_event(
        EventType::OrganizationUserAdminResetPassword as i32,
        &member_id,
        &org_id,
        &headers.user.uuid,
        headers.device.atype,
        &headers.ip.ip,
        &conn,
    )
    .await;

    Ok(())
}

#[get("/organizations/<org_id>/users/<member_id>/reset-password-details")]
async fn get_reset_password_details(
    org_id: OrganizationId,
    member_id: MembershipId,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let Some(org) = Organization::find_by_uuid(&org_id, &conn).await else {
        err!("Required organization not found")
    };

    let Some(member) = Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await else {
        err!("User to reset isn't member of required organization")
    };

    let Some(user) = User::find_by_uuid(&member.user_uuid, &conn).await else {
        err!("User not found")
    };

    check_reset_password_applicable_and_permissions(&org_id, &member_id, &headers, &conn).await?;

    // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs#L190
    Ok(Json(json!({
        "object": "organizationUserResetPasswordDetails",
        "organizationUserId": member_id,
        "kdf": user.client_kdf_type,
        "kdfIterations": user.client_kdf_iter,
        "kdfMemory": user.client_kdf_memory,
        "kdfParallelism": user.client_kdf_parallelism,
        "resetPasswordKey": member.reset_password_key,
        "encryptedPrivateKey": org.private_key,
    })))
}

async fn check_reset_password_applicable_and_permissions(
    org_id: &OrganizationId,
    member_id: &MembershipId,
    headers: &AdminHeaders,
    conn: &DbConn,
) -> EmptyResult {
    check_reset_password_applicable(org_id, conn).await?;

    let Some(target_user) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else {
        err!("Reset target user not found")
    };

    // Resetting user must be higher/equal to user to reset
    match headers.membership_type {
        MembershipType::Owner => Ok(()),
        MembershipType::Admin if target_user.atype <= MembershipType::Admin => Ok(()),
        _ => err!("No permission to reset this user's password"),
    }
}

async fn check_reset_password_applicable(org_id: &OrganizationId, conn: &DbConn) -> EmptyResult {
    if !CONFIG.mail_enabled() {
        err!("Password reset is not supported on an email-disabled instance.");
    }

    let Some(policy) = OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::ResetPassword, conn).await else {
        err!("Policy not found")
    };

    if !policy.enabled {
        err!("Reset password policy not enabled");
    }

    Ok(())
}

#[put("/organizations/<org_id>/users/<user_id>/reset-password-enrollment", data = "<data>")]
async fn put_reset_password_enrollment(
    org_id: OrganizationId,
    user_id: UserId,
    headers: OrgMemberHeaders,
    data: Json<OrganizationUserResetPasswordEnrollmentRequest>,
    conn: DbConn,
) -> EmptyResult {
    if user_id != headers.user.uuid {
        err!("User to enroll isn't member of required organization", "The user_id and acting user do not match");
    }

    let mut membership = headers.membership;

    check_reset_password_applicable(&org_id, &conn).await?;

    let reset_request = data.into_inner();

    let reset_password_key = match reset_request.reset_password_key {
        None => None,
        Some(ref key) if key.is_empty() => None,
        Some(key) => Some(key),
    };

    if reset_password_key.is_none() && OrgPolicy::org_is_reset_password_auto_enroll(&org_id, &conn).await {
        err!("Reset password can't be withdrawn due to an enterprise policy");
    }

    if reset_password_key.is_some() {
        PasswordOrOtpData {
            master_password_hash: reset_request.master_password_hash,
            otp: reset_request.otp,
        }
        .validate(&headers.user, true, &conn)
        .await?;
    }

    membership.reset_password_key = reset_password_key;
    membership.save(&conn).await?;

    let event_type = if membership.reset_password_key.is_some() {
        EventType::OrganizationUserResetPasswordEnroll as i32
    } else {
        EventType::OrganizationUserResetPasswordWithdraw as i32
    };

    log_event(event_type, &membership.uuid, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &conn)
        .await;

    Ok(())
}

// NOTE: It seems clients can't handle uppercase-first keys!!
//       We need to convert all keys so they have the first character to be a lowercase.
//       Else the export will be just an empty JSON file.
// We currently only support exports by members of the Admin or Owner status.
// Vaultwarden does not yet support exporting only managed collections!
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/OrganizationExportController.cs#L52
#[get("/organizations/<org_id>/export")]
async fn get_org_export(org_id: OrganizationId, headers: AdminHeaders, conn: DbConn) -> JsonResult {
    if org_id != headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }

    Ok(Json(json!({
        "collections": convert_json_key_lcase_first(get_org_collections_impl(&org_id, &conn).await),
        "ciphers": convert_json_key_lcase_first(get_org_details_impl(&org_id, &headers.host, &headers.user.uuid, &conn).await?),
    })))
}

async fn api_key(
    org_id: &OrganizationId,
    data: Json<PasswordOrOtpData>,
    rotate: bool,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != &headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: PasswordOrOtpData = data.into_inner();
    let user = headers.user;

    // Validate the admin users password/otp
    data.validate(&user, true, &conn).await?;

    let org_api_key = if let Some(mut org_api_key) = OrganizationApiKey::find_by_org_uuid(org_id, &conn).await {
        if rotate {
            org_api_key.api_key = crate::crypto::generate_api_key();
            org_api_key.revision_date = chrono::Utc::now().naive_utc();
            org_api_key.save(&conn).await.expect("Error rotating organization API Key");
        }
        org_api_key
    } else {
        let api_key = crate::crypto::generate_api_key();
        let new_org_api_key = OrganizationApiKey::new(org_id.clone(), api_key);
        new_org_api_key.save(&conn).await.expect("Error creating organization API Key");
        new_org_api_key
    };

    Ok(Json(json!({
      "apiKey": org_api_key.api_key,
      "revisionDate": crate::util::format_date(&org_api_key.revision_date),
      "object": "apiKey",
    })))
}

#[post("/organizations/<org_id>/api-key", data = "<data>")]
async fn post_api_key(
    org_id: OrganizationId,
    data: Json<PasswordOrOtpData>,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    api_key(&org_id, data, false, headers, conn).await
}

#[post("/organizations/<org_id>/rotate-api-key", data = "<data>")]
async fn rotate_api_key(
    org_id: OrganizationId,
    data: Json<PasswordOrOtpData>,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    api_key(&org_id, data, true, headers, conn).await
}
