// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use super::iam_error::iam_error_to_s3_error;
use super::{account_info, group, service_account, user_iam, user_lifecycle, user_policy_binding};
use crate::{
    admin::runtime_sources::current_action_credentials,
    admin::{
        auth::{authorize_admin_request, validate_admin_request},
        handlers::site_replication::site_replication_iam_change_hook,
        router::{AdminOperation, Operation, S3Router},
        utils::{encode_compatible_admin_payload, has_space_be, read_compatible_admin_body},
    },
    auth::{check_key_valid, constant_time_eq, get_session_token},
    server::RemoteAddr,
};
use http::{HeaderMap, StatusCode};
use matchit::Params;
use rustfs_config::{MAX_ADMIN_REQUEST_BODY_SIZE, MAX_IAM_IMPORT_EXPANDED_SIZE, MAX_IAM_IMPORT_SIZE};
use rustfs_credentials::Credentials;
use rustfs_iam::{
    store::{GroupInfo, MappedPolicy, UserType},
    sys::{NewServiceAccountOpts, UpdateServiceAccountOpts},
};
use rustfs_madmin::{
    AccountStatus, AddOrUpdateUserReq, IAMEntities, IAMErrEntities, IAMErrEntity, IAMErrPolicyEntity, SITE_REPL_API_VERSION,
    SRIAMItem, SRIAMUser,
    user::{ImportIAMResult, SRSessionPolicy, SRSvcAccCreate},
};
use rustfs_policy::policy::action::{Action, AdminAction};
use rustfs_utils::path::path_join_buf;
use s3s::{
    Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result,
    header::{CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_TYPE},
    s3_error,
};
use serde::Deserialize;
use serde_urlencoded::from_bytes;
use std::io::{Read as _, Write};
use std::{collections::HashMap, io::Cursor, str::from_utf8};
use tracing::{debug, warn};
use zip::{ZipArchive, ZipWriter, result::ZipError, write::SimpleFileOptions};

const LOG_COMPONENT_ADMIN: &str = "admin";
const LOG_SUBSYSTEM_USER: &str = "user";
const EVENT_ADMIN_USER_STATE: &str = "admin_user_state";

#[derive(Debug, Deserialize, Default)]
pub struct AddUserQuery {
    #[serde(rename = "accessKey", alias = "access-key")]
    pub access_key: Option<String>,
    pub status: Option<String>,
}

pub fn register_user_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
    account_info::register_account_info_route(r)?;
    user_lifecycle::register_user_lifecycle_route(r)?;
    group::register_group_management_route(r)?;
    service_account::register_service_account_route(r)?;
    user_iam::register_user_iam_route(r)?;
    user_policy_binding::register_user_policy_binding_route(r)?;

    Ok(())
}

/// Returns the IAM parent user identity for a temporary (Console/STS) session credential.
///
/// Prefers `parent_user` when non-empty; otherwise reads the JWT `parent` claim.
/// Returns [`None`] when neither is available.
fn temp_identity_parent(requester: &Credentials) -> Option<&str> {
    if !requester.parent_user.is_empty() {
        return Some(requester.parent_user.as_str());
    }
    requester
        .claims
        .as_ref()
        .and_then(|c| c.get("parent"))
        .and_then(|v| v.as_str())
}

/// How the IAM parent identity was resolved for logging (no JWT claim values).
fn parent_identity_source(requester: &Credentials) -> &'static str {
    if !requester.parent_user.is_empty() {
        "parent_user_field"
    } else if requester.claims.as_ref().and_then(|c| c.get("parent")).is_some() {
        "jwt_parent"
    } else {
        "none"
    }
}

/// Returns `true` when admin policy checks should use `deny_only` mode (only explicit **Deny** blocks;
/// absence of **Allow** does not deny).
///
/// Eligible cases:
/// - **Long-term credentials**: target is the requester's own `access_key` and there is no `parent_user`.
/// - **Console/STS session (temp)**: target equals the IAM user this session represents, resolved via
///   [`temp_identity_parent`] as `parent_user` when set, else the JWT claim `parent` (some stores
///   persist only the token).
///
/// Service accounts always use full Allow/Deny evaluation.
fn should_check_deny_only(target_access_key: &str, requester: &Credentials) -> bool {
    if requester.is_service_account() {
        return false;
    }
    if requester.is_temp() {
        return temp_identity_parent(requester).is_some_and(|p| p == target_access_key);
    }
    target_access_key == requester.access_key && requester.parent_user.is_empty()
}

/// Returns `true` when a derived credential (Console/STS session or service account) targets the
/// long-term IAM user it was minted from.
///
/// SECURITY: [`should_check_deny_only`] relaxes the admin policy check to deny-only for exactly
/// this target, so without this guard an AddUser call could rewrite the parent's stored secret key
/// and status, turning a short-lived session into permanent control of the account. The parent is
/// resolved the same way [`should_check_deny_only`] resolves it, since some stores persist the
/// parent only inside the session token.
fn add_user_targets_requester_parent(target_access_key: &str, requester: &Credentials) -> bool {
    if !requester.is_temp() && !requester.is_service_account() {
        return false;
    }

    temp_identity_parent(requester).is_some_and(|parent| parent == target_access_key)
}

fn should_reject_group_import_name(group_name: &str, group_lookup: &rustfs_iam::error::Error) -> bool {
    has_space_be(group_name) || !matches!(group_lookup, rustfs_iam::error::Error::NoSuchGroup(_))
}

fn should_restore_group_as_disabled(status: &str) -> bool {
    status.eq_ignore_ascii_case(rustfs_iam::sys::STATUS_DISABLED)
}

fn imported_service_account_status(status: &str) -> Option<String> {
    if status.eq_ignore_ascii_case(rustfs_policy::auth::ACCOUNT_OFF)
        || status.eq_ignore_ascii_case(rustfs_madmin::AccountStatus::Disabled.as_ref())
    {
        return Some(rustfs_policy::auth::ACCOUNT_OFF.to_string());
    }

    if status.eq_ignore_ascii_case(rustfs_policy::auth::ACCOUNT_ON)
        || status.eq_ignore_ascii_case(rustfs_madmin::AccountStatus::Enabled.as_ref())
    {
        return Some(rustfs_policy::auth::ACCOUNT_ON.to_string());
    }

    None
}

const SERVICE_ACCOUNT_PARENT_SCOPE_ERROR: &str = "service account parent is outside requester scope";
const SERVICE_ACCOUNT_ACCESS_KEY_MISMATCH_ERROR: &str = "service account access key does not match import entry";

fn imported_service_account_parent_allowed(parent: &str, requester: &Credentials, owner: bool) -> bool {
    if parent.is_empty() {
        return false;
    }

    if owner {
        return true;
    }

    if requester.is_temp() || requester.is_service_account() {
        return temp_identity_parent(requester).is_some_and(|requester_parent| requester_parent == parent);
    }

    requester.parent_user.is_empty() && requester.access_key == parent
}

fn imported_service_account_parent_scope_failure(
    access_key: &str,
    parent: &str,
    requester: &Credentials,
    owner: bool,
) -> Option<IAMErrEntity> {
    (!imported_service_account_parent_allowed(parent, requester, owner)).then(|| IAMErrEntity {
        name: access_key.to_string(),
        error: SERVICE_ACCOUNT_PARENT_SCOPE_ERROR.to_string(),
    })
}

fn imported_service_account_access_key_failure(entry_access_key: &str, payload_access_key: &str) -> Option<IAMErrEntity> {
    (entry_access_key != payload_access_key).then(|| IAMErrEntity {
        name: entry_access_key.to_string(),
        error: SERVICE_ACCOUNT_ACCESS_KEY_MISMATCH_ERROR.to_string(),
    })
}

pub struct AddUser {}

fn map_add_user_create_error(err: rustfs_iam::error::Error) -> S3Error {
    match err {
        rustfs_iam::error::Error::AccessKeyAlreadyExists => iam_error_to_s3_error(err),
        err => S3Error::with_message(S3ErrorCode::InternalError, format!("failed to create user: {err}")),
    }
}

#[async_trait::async_trait]
impl Operation for AddUser {
    async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
        let query = {
            if let Some(query) = req.uri.query() {
                let input: AddUserQuery =
                    from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?;
                input
            } else {
                AddUserQuery::default()
            }
        };

        let Some(input_cred) = req.credentials else {
            return Err(s3_error!(InvalidRequest, "authentication required"));
        };

        let (cred, owner) =
            check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;

        let ak = query.access_key.as_deref().unwrap_or_default();

        if ak.is_empty() {
            return Err(s3_error!(InvalidArgument, "access key is empty"));
        }

        let body = read_compatible_admin_body(req.input, MAX_ADMIN_REQUEST_BODY_SIZE, req.uri.path(), &cred.secret_key).await?;

        // let body_bytes = decrypt_data(input_cred.secret_key.expose().as_bytes(), &body)
        //     .map_err(|e| S3Error::with_message(S3ErrorCode::InvalidArgument, format!("decrypt_data err {}", e)))?;

        let args: AddOrUpdateUserReq = serde_json::from_slice(&body)
            .map_err(|e| S3Error::with_message(S3ErrorCode::InvalidRequest, format!("invalid JSON: {e}")))?;

        if args.secret_key.is_empty() {
            return Err(s3_error!(InvalidArgument, "secret key is required"));
        }

        if let Some(sys_cred) = current_action_credentials()
            && constant_time_eq(&sys_cred.access_key, ak)
        {
            return Err(s3_error!(InvalidArgument, "cannot create a user with the system access key"));
        }

        let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
            return Err(s3_error!(InternalError, "iam is not initialized"));
        };

        if let Some(user) = iam_store.get_user(ak).await {
            if (user.credentials.is_temp() || user.credentials.is_service_account()) && cred.parent_user == ak {
                return Err(s3_error!(InvalidArgument, "cannot create a user with a service account access key"));
            }
        } else if has_space_be(ak) {
            return Err(s3_error!(InvalidArgument, "access key contains spaces"));
        }

        if from_utf8(ak.as_bytes()).is_err() {
            return Err(s3_error!(InvalidArgument, "access key is not valid UTF-8"));
        }

        if add_user_targets_requester_parent(ak, &cred) {
            return Err(s3_error!(
                InvalidArgument,
                "cannot change the credentials of the parent user of this session"
            ));
        }

        let check_deny_only = should_check_deny_only(ak, &cred);

        debug!(
            component = LOG_COMPONENT_ADMIN,
            subsystem = LOG_SUBSYSTEM_USER,
            event = EVENT_ADMIN_USER_STATE,
            action = "add_user",
            query_access_key = %ak,
            signer_access_key = %cred.access_key,
            is_temp = cred.is_temp(),
            is_service_account = cred.is_service_account(),
            parent_user = %cred.parent_user,
            parent_identity_source = %parent_identity_source(&cred),
            jwt_parent_claim_present = cred.claims.as_ref().and_then(|c| c.get("parent")).is_some(),
            check_deny_only,
            is_owner = owner,
            state = "authorization_context",
            "admin user state"
        );

        // For eligible self operations, only explicit Deny should block the request.
        validate_admin_request(
            &req.headers,
            &cred,
            owner,
            check_deny_only,
            vec![Action::AdminAction(AdminAction::CreateUserAdminAction)],
            req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
        )
        .await?;

        let updated_at = iam_store.create_user(ak, &args).await.map_err(map_add_user_create_error)?;

        if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
            r#type: "iam-user".to_string(),
            iam_user: Some(SRIAMUser {
                access_key: ak.to_string(),
                is_delete_req: false,
                user_req: Some(args.clone()),
                api_version: Some(SITE_REPL_API_VERSION.to_string()),
            }),
            updated_at: Some(updated_at),
            api_version: Some(SITE_REPL_API_VERSION.to_string()),
            ..Default::default()
        })
        .await
        {
            warn!(
                component = LOG_COMPONENT_ADMIN,
                subsystem = LOG_SUBSYSTEM_USER,
                event = EVENT_ADMIN_USER_STATE,
                access_key = %ak,
                action = "create_user",
                result = "site_replication_hook_failed",
                error = ?err,
                "admin user state"
            );
        }

        let mut header = HeaderMap::new();
        header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value"));
        header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value"));
        Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header))
    }
}

pub struct SetUserStatus {}
#[async_trait::async_trait]
impl Operation for SetUserStatus {
    async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
        let query = {
            if let Some(query) = req.uri.query() {
                let input: AddUserQuery =
                    from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?;
                input
            } else {
                AddUserQuery::default()
            }
        };

        let ak = query.access_key.as_deref().unwrap_or_default();

        if ak.is_empty() {
            return Err(s3_error!(InvalidArgument, "access key is empty"));
        }

        let Some(input_cred) = req.credentials.as_ref() else {
            return Err(s3_error!(InvalidRequest, "authentication required"));
        };

        if constant_time_eq(&input_cred.access_key, ak) {
            return Err(s3_error!(InvalidArgument, "cannot change the status of the current user"));
        }

        authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::EnableUserAdminAction)]).await?;

        let status = AccountStatus::try_from(query.status.as_deref().unwrap_or_default())
            .map_err(|e| S3Error::with_message(S3ErrorCode::InvalidArgument, e))?;

        let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
            return Err(s3_error!(InternalError, "iam is not initialized"));
        };

        let updated_at = iam_store
            .set_user_status(ak, status.clone())
            .await
            .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("failed to set user status: {e}")))?;

        if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
            r#type: "iam-user".to_string(),
            iam_user: Some(SRIAMUser {
                access_key: ak.to_string(),
                is_delete_req: false,
                user_req: Some(AddOrUpdateUserReq {
                    secret_key: String::new(),
                    policy: None,
                    status: status.clone(),
                }),
                api_version: Some(SITE_REPL_API_VERSION.to_string()),
            }),
            updated_at: Some(updated_at),
            api_version: Some(SITE_REPL_API_VERSION.to_string()),
            ..Default::default()
        })
        .await
        {
            warn!(
                component = LOG_COMPONENT_ADMIN,
                subsystem = LOG_SUBSYSTEM_USER,
                event = EVENT_ADMIN_USER_STATE,
                access_key = %ak,
                action = "set_user_status",
                result = "site_replication_hook_failed",
                error = ?err,
                "admin user state"
            );
        }

        let mut header = HeaderMap::new();
        header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value"));
        header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value"));
        Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header))
    }
}

#[derive(Debug, Deserialize, Default)]
pub struct BucketQuery {
    #[serde(rename = "bucket")]
    pub bucket: String,
}
pub struct ListUsers {}
#[async_trait::async_trait]
impl Operation for ListUsers {
    async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
        if req.credentials.is_none() {
            return Err(s3_error!(InvalidRequest, "authentication required"));
        }

        let cred = authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::ListUsersAdminAction)]).await?;

        let query = {
            if let Some(query) = req.uri.query() {
                let input: BucketQuery =
                    from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?;
                input
            } else {
                BucketQuery::default()
            }
        };

        let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
            return Err(s3_error!(InternalError, "iam is not initialized"));
        };

        let users = {
            if !query.bucket.is_empty() {
                iam_store
                    .list_bucket_users(query.bucket.as_str())
                    .await
                    .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?
            } else {
                iam_store
                    .list_users()
                    .await
                    .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?
            }
        };

        let data = serde_json::to_vec(&users)
            .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("failed to serialize response: {e}")))?;
        let (data, content_type) = encode_compatible_admin_payload(req.uri.path(), &cred.secret_key, data)?;

        let mut header = HeaderMap::new();
        header.insert(CONTENT_TYPE, content_type.parse().expect("valid header value"));

        Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header))
    }
}

pub struct RemoveUser {}
#[async_trait::async_trait]
impl Operation for RemoveUser {
    async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
        if req.credentials.is_none() {
            return Err(s3_error!(InvalidRequest, "authentication required"));
        }

        let cred = authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::DeleteUserAdminAction)]).await?;

        let query = {
            if let Some(query) = req.uri.query() {
                let input: AddUserQuery =
                    from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?;
                input
            } else {
                AddUserQuery::default()
            }
        };

        let ak = query.access_key.as_deref().unwrap_or_default();

        if ak.is_empty() {
            return Err(s3_error!(InvalidArgument, "access key is empty"));
        }

        let sys_cred = current_action_credentials()
            .ok_or_else(|| S3Error::with_message(S3ErrorCode::InternalError, "failed to load global credentials"))?;

        if ak == sys_cred.access_key || ak == cred.access_key || cred.parent_user == ak {
            return Err(s3_error!(InvalidArgument, "cannot remove the current user"));
        }

        let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
            return Err(s3_error!(InternalError, "iam is not initialized"));
        };

        let (is_temp, _) = iam_store.is_temp_user(ak).await.map_err(|e| {
            S3Error::with_message(S3ErrorCode::InternalError, format!("failed to query temporary user state: {e}"))
        })?;

        if is_temp {
            return Err(s3_error!(InvalidArgument, "cannot remove a temporary user"));
        }

        let (is_service_account, _) = iam_store.is_service_account(ak).await.map_err(|e| {
            S3Error::with_message(S3ErrorCode::InternalError, format!("failed to query service account state: {e}"))
        })?;
        if is_service_account {
            return Err(s3_error!(InvalidArgument, "cannot remove a service account"));
        }

        iam_store
            .delete_user(ak, true)
            .await
            .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("failed to delete user: {e}")))?;

        if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
            r#type: "iam-user".to_string(),
            iam_user: Some(SRIAMUser {
                access_key: ak.to_string(),
                is_delete_req: true,
                user_req: None,
                api_version: Some(SITE_REPL_API_VERSION.to_string()),
            }),
            updated_at: Some(time::OffsetDateTime::now_utc()),
            api_version: Some(SITE_REPL_API_VERSION.to_string()),
            ..Default::default()
        })
        .await
        {
            warn!(
                component = LOG_COMPONENT_ADMIN,
                subsystem = LOG_SUBSYSTEM_USER,
                event = EVENT_ADMIN_USER_STATE,
                access_key = %ak,
                action = "delete_user",
                result = "site_replication_hook_failed",
                error = ?err,
                "admin user state"
            );
        }

        let mut header = HeaderMap::new();
        header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value"));
        header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value"));
        Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header))
    }
}

pub struct GetUserInfo {}
#[async_trait::async_trait]
impl Operation for GetUserInfo {
    async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
        let query = {
            if let Some(query) = req.uri.query() {
                let input: AddUserQuery =
                    from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "failed to decode query"))?;
                input
            } else {
                AddUserQuery::default()
            }
        };

        let ak = query.access_key.as_deref().unwrap_or_default();

        if ak.is_empty() {
            return Err(s3_error!(InvalidArgument, "access key is empty"));
        }

        let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
            return Err(s3_error!(InternalError, "iam is not initialized"));
        };

        let Some(input_cred) = req.credentials else {
            return Err(s3_error!(InvalidRequest, "authentication required"));
        };

        let (cred, owner) =
            check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;

        let check_deny_only = should_check_deny_only(ak, &cred);

        debug!(
            component = LOG_COMPONENT_ADMIN,
            subsystem = LOG_SUBSYSTEM_USER,
            event = EVENT_ADMIN_USER_STATE,
            action = "get_user_info",
            query_access_key = %ak,
            signer_access_key = %cred.access_key,
            is_temp = cred.is_temp(),
            is_service_account = cred.is_service_account(),
            parent_user = %cred.parent_user,
            parent_identity_source = %parent_identity_source(&cred),
            jwt_parent_claim_present = cred.claims.as_ref().and_then(|c| c.get("parent")).is_some(),
            check_deny_only,
            is_owner = owner,
            state = "authorization_context",
            "admin user state"
        );

        // For eligible self operations, only explicit Deny should block the request.
        validate_admin_request(
            &req.headers,
            &cred,
            owner,
            check_deny_only,
            vec![Action::AdminAction(AdminAction::GetUserAdminAction)],
            req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
        )
        .await?;

        let info = iam_store.get_user_info(ak).await.map_err(iam_error_to_s3_error)?;

        let data = serde_json::to_vec(&info)
            .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("failed to serialize response: {e}")))?;

        let mut header = HeaderMap::new();
        header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value"));

        Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header))
    }
}

const ALL_POLICIES_FILE: &str = "policies.json";
const ALL_USERS_FILE: &str = "users.json";
const ALL_GROUPS_FILE: &str = "groups.json";
const ALL_SVC_ACCTS_FILE: &str = "svcaccts.json";
const USER_POLICY_MAPPINGS_FILE: &str = "user_mappings.json";
const GROUP_POLICY_MAPPINGS_FILE: &str = "group_mappings.json";
const STS_USER_POLICY_MAPPINGS_FILE: &str = "stsuser_mappings.json";
const GROUP_POLICY_MAPPING_USER_TYPE: UserType = UserType::Reg;

const IAM_ASSETS_DIR: &str = "iam-assets";

const IAM_EXPORT_FILES: &[&str] = &[
    ALL_POLICIES_FILE,
    ALL_USERS_FILE,
    ALL_GROUPS_FILE,
    ALL_SVC_ACCTS_FILE,
    USER_POLICY_MAPPINGS_FILE,
    GROUP_POLICY_MAPPINGS_FILE,
    STS_USER_POLICY_MAPPINGS_FILE,
];

pub struct ExportIam {}
#[async_trait::async_trait]
impl Operation for ExportIam {
    async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
        if req.credentials.is_none() {
            return Err(s3_error!(InvalidRequest, "get cred failed"));
        }

        authorize_admin_request(&req, vec![Action::AdminAction(AdminAction::ExportIAMAction)]).await?;

        let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
            return Err(s3_error!(InvalidRequest, "iam not init"));
        };

        let mut zip_writer = ZipWriter::new(Cursor::new(Vec::new()));
        let options = SimpleFileOptions::default();

        for &file in IAM_EXPORT_FILES {
            let file_path = path_join_buf(&[IAM_ASSETS_DIR, file]);
            match file {
                ALL_POLICIES_FILE => {
                    let policies: HashMap<String, rustfs_policy::policy::Policy> = iam_store
                        .list_policies("")
                        .await
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    let json_str = serde_json::to_vec(&policies)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .start_file(file_path, options)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .write_all(&json_str)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                }
                ALL_USERS_FILE => {
                    let mut users = HashMap::new();
                    iam_store
                        .load_users(UserType::Reg, &mut users)
                        .await
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;

                    let users: HashMap<String, AddOrUpdateUserReq> = users
                        .into_iter()
                        .map(|(k, v)| {
                            (
                                k,
                                AddOrUpdateUserReq {
                                    secret_key: v.credentials.secret_key,
                                    status: {
                                        if v.credentials.status == "off" {
                                            AccountStatus::Disabled
                                        } else {
                                            AccountStatus::Enabled
                                        }
                                    },
                                    policy: None,
                                },
                            )
                        })
                        .collect::<HashMap<String, AddOrUpdateUserReq>>();

                    let json_str = serde_json::to_vec(&users)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .start_file(file_path, options)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .write_all(&json_str)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                }
                ALL_GROUPS_FILE => {
                    let mut groups: HashMap<String, GroupInfo> = HashMap::new();
                    iam_store
                        .load_groups(&mut groups)
                        .await
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;

                    let json_str = serde_json::to_vec(&groups)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .start_file(file_path, options)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .write_all(&json_str)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                }
                ALL_SVC_ACCTS_FILE => {
                    let mut service_accounts = HashMap::new();
                    iam_store
                        .load_users(UserType::Svc, &mut service_accounts)
                        .await
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;

                    let mut svc_accts: HashMap<String, SRSvcAccCreate> = HashMap::new();
                    for (k, acc) in service_accounts {
                        if k == "siteReplicatorSvcAcc" {
                            continue;
                        }

                        let claims = iam_store
                            .get_claims_for_svc_acc(&acc.credentials.access_key)
                            .await
                            .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;

                        let (sa, police) = iam_store
                            .get_service_account(&acc.credentials.access_key)
                            .await
                            .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;

                        let police_json = if let Some(police) = police {
                            serde_json::to_string(&police)
                                .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?
                        } else {
                            "null".to_string()
                        };

                        let svc_acc_create_req = SRSvcAccCreate {
                            parent: acc.credentials.parent_user,
                            access_key: k.clone(),
                            secret_key: acc.credentials.secret_key,
                            groups: acc.credentials.groups.unwrap_or_default(),
                            claims,
                            session_policy: SRSessionPolicy::from_json(&police_json).unwrap_or_default(),
                            status: acc.credentials.status,
                            name: sa.name.unwrap_or_default(),
                            description: sa.description.unwrap_or_default(),
                            expiration: sa.expiration,
                            api_version: None,
                        };
                        svc_accts.insert(k.clone(), svc_acc_create_req);
                    }

                    let json_str = serde_json::to_vec(&svc_accts)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .start_file(file_path, options)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .write_all(&json_str)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                }
                USER_POLICY_MAPPINGS_FILE => {
                    let mut user_policy_mappings: HashMap<String, MappedPolicy> = HashMap::new();
                    iam_store
                        .load_mapped_policies(UserType::Reg, false, &mut user_policy_mappings)
                        .await
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;

                    let json_str = serde_json::to_vec(&user_policy_mappings)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .start_file(file_path, options)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .write_all(&json_str)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                }
                GROUP_POLICY_MAPPINGS_FILE => {
                    let mut group_policy_mappings = HashMap::new();
                    iam_store
                        .load_mapped_policies(GROUP_POLICY_MAPPING_USER_TYPE, true, &mut group_policy_mappings)
                        .await
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;

                    let json_str = serde_json::to_vec(&group_policy_mappings)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .start_file(file_path, options)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .write_all(&json_str)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                }
                STS_USER_POLICY_MAPPINGS_FILE => {
                    let mut sts_user_policy_mappings: HashMap<String, MappedPolicy> = HashMap::new();
                    iam_store
                        .load_mapped_policies(UserType::Sts, false, &mut sts_user_policy_mappings)
                        .await
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    let json_str = serde_json::to_vec(&sts_user_policy_mappings)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .start_file(file_path, options)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                    zip_writer
                        .write_all(&json_str)
                        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                }
                _ => continue,
            }
        }

        let zip_bytes = zip_writer
            .finish()
            .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
        let mut header = HeaderMap::new();
        header.insert(CONTENT_TYPE, "application/zip".parse().expect("valid header value"));
        header.insert(
            CONTENT_DISPOSITION,
            "attachment; filename=iam-assets.zip".parse().expect("valid header value"),
        );
        header.insert(CONTENT_LENGTH, zip_bytes.get_ref().len().to_string().parse().expect("valid header value"));
        Ok(S3Response::with_headers((StatusCode::OK, Body::from(zip_bytes.into_inner())), header))
    }
}

/// Read one member of an IAM import archive, drawing from a shared expansion budget.
///
/// `MAX_IAM_IMPORT_SIZE` bounds the compressed upload only. Deflate ratios well above
/// 100:1 are easy to construct, so reading members with `read_to_end` lets a small
/// archive expand without bound. The budget is shared across every member so the
/// archive as a whole is capped, not each member independently.
fn read_import_member(file: &mut impl std::io::Read, budget: &mut u64) -> S3Result<Vec<u8>> {
    let mut file_content = Vec::new();
    // Read one byte past the budget: if it arrives, the member exceeded what is left.
    let read = file
        .take(budget.saturating_add(1))
        .read_to_end(&mut file_content)
        .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))? as u64;

    if read > *budget {
        return Err(s3_error!(
            EntityTooLarge,
            "IAM import archive expands beyond the {MAX_IAM_IMPORT_EXPANDED_SIZE} byte limit"
        ));
    }

    *budget -= read;
    Ok(file_content)
}

pub struct ImportIam {}
#[async_trait::async_trait]
impl Operation for ImportIam {
    async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
        let Some(input_cred) = req.credentials else {
            return Err(s3_error!(InvalidRequest, "get cred failed"));
        };

        let (cred, owner) =
            check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;

        validate_admin_request(
            &req.headers,
            &cred,
            owner,
            false,
            vec![Action::AdminAction(AdminAction::ImportIAMAction)],
            req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
        )
        .await?;

        let mut input = req.input;
        let body = match input.store_all_limited(MAX_IAM_IMPORT_SIZE).await {
            Ok(b) => b,
            Err(e) => {
                warn!(
                    component = LOG_COMPONENT_ADMIN,
                    subsystem = LOG_SUBSYSTEM_USER,
                    event = EVENT_ADMIN_USER_STATE,
                    action = "import_iam",
                    result = "body_read_failed",
                    error = ?e,
                    "admin user state"
                );
                return Err(s3_error!(InvalidRequest, "get body failed"));
            }
        };

        let mut zip_reader =
            ZipArchive::new(Cursor::new(body)).map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;

        // Shared across every member below, so the archive as a whole is bounded rather
        // than each member being allowed the full limit.
        let mut expansion_budget = MAX_IAM_IMPORT_EXPANDED_SIZE;

        let Ok(iam_store) = crate::admin::runtime_sources::current_ready_iam_handle() else {
            return Err(s3_error!(InvalidRequest, "iam not init"));
        };

        let skipped = IAMEntities::default();
        let mut removed = IAMEntities::default();
        let mut added = IAMEntities::default();
        let mut failed = IAMErrEntities::default();

        {
            let file_path = path_join_buf(&[IAM_ASSETS_DIR, ALL_POLICIES_FILE]);
            let file_content = match zip_reader.by_name(file_path.as_str()) {
                Err(ZipError::FileNotFound) => None,
                Err(_) => return Err(s3_error!(InvalidRequest, "get file failed")),
                Ok(file) => {
                    let mut file = file;
                    Some(read_import_member(&mut file, &mut expansion_budget)?)
                }
            };

            if let Some(file_content) = file_content {
                let policies: HashMap<String, rustfs_policy::policy::Policy> = serde_json::from_slice(&file_content)
                    .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                for (name, policy) in policies {
                    if policy.is_empty() {
                        let res = iam_store.delete_policy(&name, true).await;
                        removed.policies.push(name.clone());
                        if let Err(e) = res {
                            return Err(s3_error!(InternalError, "delete policy failed, name: {name}, err: {e}"));
                        }
                        continue;
                    }

                    let res = iam_store.set_policy(&name, policy).await;
                    added.policies.push(name.clone());
                    if let Err(e) = res {
                        return Err(s3_error!(InternalError, "set policy failed, name: {name}, err: {e}"));
                    }
                }
            }
        }

        let Some(sys_cred) = current_action_credentials() else {
            return Err(s3_error!(InvalidRequest, "get sys cred failed"));
        };

        {
            let file_path = path_join_buf(&[IAM_ASSETS_DIR, ALL_USERS_FILE]);
            let file_content = match zip_reader.by_name(file_path.as_str()) {
                Err(ZipError::FileNotFound) => None,
                Err(_) => return Err(s3_error!(InvalidRequest, "get file failed")),
                Ok(file) => {
                    let mut file = file;
                    Some(read_import_member(&mut file, &mut expansion_budget)?)
                }
            };

            if let Some(file_content) = file_content {
                let users: HashMap<String, AddOrUpdateUserReq> = serde_json::from_slice(&file_content)
                    .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                for (ak, req) in users {
                    if ak == sys_cred.access_key {
                        return Err(s3_error!(InvalidArgument, "can't create user with system access key"));
                    }

                    if let Some(u) = iam_store.get_user(&ak).await {
                        if u.credentials.is_temp() || u.credentials.is_service_account() {
                            return Err(s3_error!(InvalidArgument, "can't create user with system access key"));
                        }
                    } else if has_space_be(&ak) {
                        return Err(s3_error!(InvalidArgument, "has space be"));
                    }

                    if let Err(e) = iam_store.create_user(&ak, &req).await {
                        failed.users.push(IAMErrEntity {
                            name: ak.clone(),
                            error: e.to_string(),
                        });
                    } else {
                        added.users.push(ak.clone());
                    }
                }
            }
        }

        {
            let file_path = path_join_buf(&[IAM_ASSETS_DIR, ALL_GROUPS_FILE]);
            let file_content = match zip_reader.by_name(file_path.as_str()) {
                Err(ZipError::FileNotFound) => None,
                Err(_) => return Err(s3_error!(InvalidRequest, "get file failed")),
                Ok(file) => {
                    let mut file = file;
                    Some(read_import_member(&mut file, &mut expansion_budget)?)
                }
            };

            if let Some(file_content) = file_content {
                let groups: HashMap<String, GroupInfo> = serde_json::from_slice(&file_content)
                    .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                for (group_name, group_info) in groups {
                    if let Err(e) = iam_store.get_group_description(&group_name).await
                        && should_reject_group_import_name(&group_name, &e)
                    {
                        return Err(s3_error!(InvalidArgument, "group not found or has space be"));
                    }

                    if let Err(e) = iam_store.add_users_to_group(&group_name, group_info.members.clone()).await {
                        failed.groups.push(IAMErrEntity {
                            name: group_name.clone(),
                            error: e.to_string(),
                        });
                    } else {
                        if should_restore_group_as_disabled(&group_info.status) {
                            iam_store.set_group_status(&group_name, false).await.map_err(|e| {
                                S3Error::with_message(
                                    S3ErrorCode::InternalError,
                                    format!("set group status failed, name: {group_name}, err: {e}"),
                                )
                            })?;
                        }
                        added.groups.push(group_name.clone());
                    }
                }
            }
        }

        {
            let file_path = path_join_buf(&[IAM_ASSETS_DIR, ALL_SVC_ACCTS_FILE]);
            let file_content = match zip_reader.by_name(file_path.as_str()) {
                Err(ZipError::FileNotFound) => None,
                Err(_) => return Err(s3_error!(InvalidRequest, "get file failed")),
                Ok(file) => {
                    let mut file = file;
                    Some(read_import_member(&mut file, &mut expansion_budget)?)
                }
            };

            if let Some(file_content) = file_content {
                let svc_accts: HashMap<String, SRSvcAccCreate> = serde_json::from_slice(&file_content)
                    .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                for (ak, mut req) in svc_accts {
                    if skipped.service_accounts.contains(&ak) {
                        continue;
                    }

                    let sp = if let Some(ps) = req.session_policy.as_str() {
                        let sp = rustfs_policy::policy::Policy::parse_config(ps.as_bytes())
                            .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                        Some(sp)
                    } else {
                        None
                    };

                    if has_space_be(&ak) {
                        return Err(s3_error!(InvalidArgument, "has space be {ak}"));
                    }

                    if let Some(err) = imported_service_account_access_key_failure(&ak, &req.access_key) {
                        failed.service_accounts.push(err);
                        continue;
                    }

                    if let Some(err) = imported_service_account_parent_scope_failure(&ak, &req.parent, &cred, owner) {
                        failed.service_accounts.push(err);
                        continue;
                    }

                    let mut update = true;

                    if let Err(e) = iam_store.get_service_account(&ak).await {
                        if !matches!(e, rustfs_iam::error::Error::NoSuchServiceAccount(_)) {
                            return Err(s3_error!(InvalidArgument, "failed to get service account {ak} {e}"));
                        }
                        update = false;
                    }

                    if update {
                        iam_store.delete_service_account(&ak, true).await.map_err(|e| {
                            S3Error::with_message(
                                S3ErrorCode::InternalError,
                                format!("failed to delete service account {ak} {e}"),
                            )
                        })?;
                    }

                    if !owner {
                        let _ = rustfs_iam::sys::remove_verified_federated_policy(&mut req.claims);
                    }
                    let opts = NewServiceAccountOpts {
                        session_policy: sp,
                        access_key: ak.clone(),
                        secret_key: req.secret_key,
                        name: Some(req.name),
                        description: Some(req.description),
                        expiration: req.expiration,
                        allow_site_replicator_account: false,
                        claims: Some(req.claims),
                    };

                    let groups = if req.groups.is_empty() { None } else { Some(req.groups) };

                    if let Err(e) = iam_store.new_service_account(&req.parent, groups, opts).await {
                        failed.service_accounts.push(IAMErrEntity {
                            name: ak.clone(),
                            error: e.to_string(),
                        });
                    } else {
                        if let Some(status) = imported_service_account_status(&req.status)
                            && status == rustfs_policy::auth::ACCOUNT_OFF
                        {
                            iam_store
                                .update_service_account(
                                    &ak,
                                    UpdateServiceAccountOpts {
                                        session_policy: None,
                                        secret_key: None,
                                        name: None,
                                        description: None,
                                        expiration: None,
                                        status: Some(status),
                                        parent_user: None,
                                        allow_site_replicator_account: false,
                                    },
                                )
                                .await
                                .map_err(|e| {
                                    S3Error::with_message(
                                        S3ErrorCode::InternalError,
                                        format!("update service account status failed, name: {ak}, err: {e}"),
                                    )
                                })?;
                        }
                        added.service_accounts.push(ak.clone());
                    }
                }
            }
        }

        {
            let file_path = path_join_buf(&[IAM_ASSETS_DIR, USER_POLICY_MAPPINGS_FILE]);
            let file_content = match zip_reader.by_name(file_path.as_str()) {
                Err(ZipError::FileNotFound) => None,
                Err(_) => return Err(s3_error!(InvalidRequest, "get file failed")),
                Ok(file) => {
                    let mut file = file;
                    Some(read_import_member(&mut file, &mut expansion_budget)?)
                }
            };

            if let Some(file_content) = file_content {
                let user_policy_mappings: HashMap<String, MappedPolicy> = serde_json::from_slice(&file_content)
                    .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                for (user_name, policies) in user_policy_mappings {
                    let has_temp = match iam_store.is_temp_user(&user_name).await {
                        Ok((has_temp, _)) => has_temp,
                        Err(e) => {
                            if !matches!(e, rustfs_iam::error::Error::NoSuchUser(_)) {
                                return Err(s3_error!(InternalError, "is temp user failed, name: {user_name}, err: {e}"));
                            }
                            false
                        }
                    };

                    if has_temp {
                        return Err(s3_error!(InvalidArgument, "can't set policy for temp user {user_name}"));
                    }

                    if let Err(e) = iam_store
                        .policy_db_set(&user_name, UserType::Reg, false, &policies.policies)
                        .await
                    {
                        failed.user_policies.push(IAMErrPolicyEntity {
                            name: user_name.clone(),
                            error: e.to_string(),
                            policies: policies.policies.split(',').map(|s| s.to_string()).collect(),
                        });
                    } else {
                        added.user_policies.push(HashMap::from([(
                            user_name.clone(),
                            policies.policies.split(',').map(|s| s.to_string()).collect(),
                        )]));
                    }
                }
            }
        }

        {
            let file_path = path_join_buf(&[IAM_ASSETS_DIR, GROUP_POLICY_MAPPINGS_FILE]);
            let file_content = match zip_reader.by_name(file_path.as_str()) {
                Err(ZipError::FileNotFound) => None,
                Err(_) => return Err(s3_error!(InvalidRequest, "get file failed")),
                Ok(file) => {
                    let mut file = file;
                    Some(read_import_member(&mut file, &mut expansion_budget)?)
                }
            };

            if let Some(file_content) = file_content {
                let group_policy_mappings: HashMap<String, MappedPolicy> = serde_json::from_slice(&file_content)
                    .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                for (group_name, policies) in group_policy_mappings {
                    if skipped.groups.contains(&group_name) {
                        continue;
                    }

                    if let Err(e) = iam_store
                        .policy_db_set(&group_name, GROUP_POLICY_MAPPING_USER_TYPE, true, &policies.policies)
                        .await
                    {
                        failed.group_policies.push(IAMErrPolicyEntity {
                            name: group_name.clone(),
                            error: e.to_string(),
                            policies: policies.policies.split(',').map(|s| s.to_string()).collect(),
                        });
                    } else {
                        added.group_policies.push(HashMap::from([(
                            group_name.clone(),
                            policies.policies.split(',').map(|s| s.to_string()).collect(),
                        )]));
                    }
                }
            }
        }

        {
            let file_path = path_join_buf(&[IAM_ASSETS_DIR, STS_USER_POLICY_MAPPINGS_FILE]);
            let file_content = match zip_reader.by_name(file_path.as_str()) {
                Err(ZipError::FileNotFound) => None,
                Err(_) => return Err(s3_error!(InvalidRequest, "get file failed")),
                Ok(file) => {
                    let mut file = file;
                    Some(read_import_member(&mut file, &mut expansion_budget)?)
                }
            };

            if let Some(file_content) = file_content {
                let sts_user_policy_mappings: HashMap<String, MappedPolicy> = serde_json::from_slice(&file_content)
                    .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
                for (user_name, policies) in sts_user_policy_mappings {
                    if skipped.users.contains(&user_name) {
                        continue;
                    }

                    let has_temp = match iam_store.is_temp_user(&user_name).await {
                        Ok((has_temp, _)) => has_temp,
                        Err(e) => {
                            if !matches!(e, rustfs_iam::error::Error::NoSuchUser(_)) {
                                return Err(s3_error!(InternalError, "is temp user failed, name: {user_name}, err: {e}"));
                            }
                            false
                        }
                    };

                    if has_temp {
                        return Err(s3_error!(InvalidArgument, "can't set policy for temp user {user_name}"));
                    }

                    if let Err(e) = iam_store
                        .policy_db_set(&user_name, UserType::Sts, false, &policies.policies)
                        .await
                    {
                        failed.sts_policies.push(IAMErrPolicyEntity {
                            name: user_name.clone(),
                            error: e.to_string(),
                            policies: policies.policies.split(',').map(|s| s.to_string()).collect(),
                        });
                    } else {
                        added.sts_policies.push(HashMap::from([(
                            user_name.clone(),
                            policies.policies.split(',').map(|s| s.to_string()).collect(),
                        )]));
                    }
                }
            }
        }

        let ret = ImportIAMResult {
            skipped,
            removed,
            added,
            failed,
        };

        let body = serde_json::to_vec(&ret).map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;

        let mut header = HeaderMap::new();
        header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value"));

        Ok(S3Response::with_headers((StatusCode::OK, Body::from(body)), header))
    }
}

#[cfg(test)]
mod tests {
    use super::{
        ExportIam, GROUP_POLICY_MAPPING_USER_TYPE, HeaderMap, ListUsers, MAX_IAM_IMPORT_EXPANDED_SIZE, MAX_IAM_IMPORT_SIZE,
        Operation, Params, RemoveUser, SERVICE_ACCOUNT_ACCESS_KEY_MISMATCH_ERROR, SERVICE_ACCOUNT_PARENT_SCOPE_ERROR,
        SetUserStatus, add_user_targets_requester_parent, imported_service_account_access_key_failure,
        imported_service_account_parent_allowed, imported_service_account_parent_scope_failure, imported_service_account_status,
        map_add_user_create_error, read_import_member, should_check_deny_only, should_reject_group_import_name,
        should_restore_group_as_disabled,
    };
    use http::{Extensions, Method, Uri};
    use rustfs_credentials::{Credentials, IAM_POLICY_CLAIM_NAME_SA};
    use rustfs_iam::error::Error as IamError;
    use rustfs_madmin::user::SRSvcAccCreate;
    use s3s::{Body, S3ErrorCode, S3Request};
    use serde_json::Value;
    use std::collections::HashMap;

    fn credential_less_request(method: Method, uri: &'static str) -> S3Request<Body> {
        S3Request {
            input: Body::empty(),
            method,
            uri: Uri::from_static(uri),
            headers: HeaderMap::new(),
            extensions: Extensions::new(),
            credentials: None,
            region: None,
            service: None,
            trailing_headers: None,
        }
    }

    async fn assert_missing_credentials(operation: &dyn Operation, method: Method, uri: &'static str, message: &str) {
        let err = operation
            .call(credential_less_request(method, uri), Params::new())
            .await
            .expect_err("a user admin request without credentials must fail");
        assert_eq!(err.code(), &S3ErrorCode::InvalidRequest);
        assert_eq!(err.message(), Some(message));
    }

    fn source_block<'a>(production: &'a str, marker: &str) -> &'a str {
        let block = production
            .split_once(marker)
            .unwrap_or_else(|| panic!("{marker} should exist"))
            .1;
        let end = ["\npub struct ", "\nfn ", "\nconst ", "\n#[cfg(test)]"]
            .into_iter()
            .filter_map(|boundary| block.find(boundary))
            .min()
            .unwrap_or(block.len());
        &block[..end]
    }

    fn assert_shared_gate_wiring(block: &str, item: &str, actions: &[&str], binds_credentials: bool) {
        assert_eq!(
            block.matches("authorize_admin_request(").count(),
            1,
            "{item} must use exactly one shared gate"
        );
        assert_eq!(
            block.matches("Action::AdminAction(").count(),
            actions.len(),
            "{item} must preserve its exact action-vector length"
        );
        for action in actions {
            assert!(block.contains(&format!("AdminAction::{action}")), "{item} must authorize with {action}");
        }
        assert_eq!(
            block.contains("let cred = authorize_admin_request("),
            binds_credentials,
            "{item} credential binding must match its payload-processing contract"
        );
    }

    #[test]
    fn add_user_maps_duplicate_access_keys_to_s3_errors() {
        let err = map_add_user_create_error(IamError::AccessKeyAlreadyExists);

        assert_eq!(err.code(), &S3ErrorCode::InvalidArgument);
        assert_eq!(err.message(), Some("access key is already in use"));
    }

    #[test]
    fn add_user_preserves_internal_error_fallback() {
        let err = map_add_user_create_error(IamError::IamSysNotInitialized);

        assert_eq!(err.code(), &S3ErrorCode::InternalError);
        assert_eq!(err.message(), Some("failed to create user: not initialized"));
    }

    #[test]
    fn add_user_operation_maps_create_errors() {
        let mapper_call = concat!("map_err(map_add_user_", "create_error)");
        assert!(include_str!("user.rs").contains(mapper_call));
    }

    #[test]
    fn test_should_check_deny_only_for_regular_self_request() {
        let cred = Credentials {
            access_key: "alice".to_string(),
            ..Default::default()
        };
        assert!(should_check_deny_only("alice", &cred));
    }

    #[test]
    fn test_should_not_check_deny_only_for_other_user_request() {
        let cred = Credentials {
            access_key: "bob".to_string(),
            ..Default::default()
        };
        assert!(!should_check_deny_only("alice", &cred));
    }

    #[test]
    fn test_should_not_check_deny_only_for_temp_without_parent() {
        // Session token present but no parent_user — not a well-formed self session for deny_only.
        let cred = Credentials {
            access_key: "alice".to_string(),
            session_token: "temp-token".to_string(),
            ..Default::default()
        };
        assert!(!should_check_deny_only("alice", &cred));
    }

    #[test]
    fn test_should_check_deny_only_for_temp_when_target_matches_parent_user() {
        // Console/AssumeRole: signing AK is a session key; IAM user is parent_user.
        let cred = Credentials {
            access_key: "VV0V3VYJK2PV6EG45X2Y".to_string(),
            session_token: "jwt-session-token".to_string(),
            parent_user: "1923".to_string(),
            ..Default::default()
        };
        assert!(should_check_deny_only("1923", &cred));
        assert!(!should_check_deny_only("1924", &cred));
        assert!(!should_check_deny_only("VV0V3VYJK2PV6EG45X2Y", &cred));
    }

    #[test]
    fn test_should_check_deny_only_for_temp_when_parent_only_in_jwt_claims() {
        // STS identity may omit `parentUser` on disk; `check_key_valid` still fills `claims["parent"]`.
        let mut claims = HashMap::new();
        claims.insert("parent".to_string(), Value::String("1923".to_string()));
        let cred = Credentials {
            access_key: "39KNO04Z34D6T4AGL6E6".to_string(),
            session_token: "jwt-session-token".to_string(),
            claims: Some(claims),
            ..Default::default()
        };
        assert!(should_check_deny_only("1923", &cred));
        assert!(!should_check_deny_only("1924", &cred));
    }

    #[test]
    fn test_should_not_check_deny_only_for_service_account_credentials() {
        let mut claims = HashMap::new();
        claims.insert(IAM_POLICY_CLAIM_NAME_SA.to_string(), Value::String("policy".to_string()));
        let cred = Credentials {
            access_key: "alice".to_string(),
            parent_user: "parent-user".to_string(),
            claims: Some(claims),
            ..Default::default()
        };
        assert!(!should_check_deny_only("alice", &cred));
    }

    #[test]
    fn test_should_not_check_deny_only_when_parent_user_present() {
        let cred = Credentials {
            access_key: "alice".to_string(),
            parent_user: "parent-user".to_string(),
            ..Default::default()
        };
        assert!(!should_check_deny_only("alice", &cred));
    }

    #[test]
    fn test_add_user_rejects_sts_session_targeting_its_parent() {
        let cred = Credentials {
            access_key: "VV0V3VYJK2PV6EG45X2Y".to_string(),
            secret_key: "CS_TEST_SECRET_DO_NOT_LOG".to_string(),
            session_token: "jwt-session-token".to_string(),
            parent_user: "alice".to_string(),
            ..Default::default()
        };

        // The relaxed deny-only admin check is reachable for exactly this target, so a guard on the
        // write itself is what keeps the session from rewriting alice's long-term secret.
        assert!(should_check_deny_only("alice", &cred));
        assert!(add_user_targets_requester_parent("alice", &cred));
        assert!(!add_user_targets_requester_parent("bob", &cred));
    }

    #[test]
    fn test_add_user_rejects_sts_session_with_parent_only_in_jwt_claims() {
        let mut claims = HashMap::new();
        claims.insert("parent".to_string(), Value::String("alice".to_string()));
        let cred = Credentials {
            access_key: "39KNO04Z34D6T4AGL6E6".to_string(),
            secret_key: "CS_TEST_SECRET_DO_NOT_LOG".to_string(),
            session_token: "jwt-session-token".to_string(),
            claims: Some(claims),
            ..Default::default()
        };

        assert!(add_user_targets_requester_parent("alice", &cred));
    }

    #[test]
    fn test_add_user_rejects_service_account_targeting_its_parent() {
        let mut claims = HashMap::new();
        claims.insert(IAM_POLICY_CLAIM_NAME_SA.to_string(), Value::String("policy".to_string()));
        let cred = Credentials {
            access_key: "service-account".to_string(),
            parent_user: "alice".to_string(),
            claims: Some(claims),
            ..Default::default()
        };

        assert!(add_user_targets_requester_parent("alice", &cred));
        assert!(!add_user_targets_requester_parent("bob", &cred));
    }

    #[test]
    fn test_add_user_allows_long_term_credentials_to_manage_other_users() {
        let cred = Credentials {
            access_key: "admin".to_string(),
            secret_key: "CS_TEST_SECRET_DO_NOT_LOG".to_string(),
            ..Default::default()
        };

        assert!(!add_user_targets_requester_parent("alice", &cred));
        assert!(!add_user_targets_requester_parent("admin", &cred));
    }

    #[test]
    fn test_add_user_operation_guards_requester_parent_target() {
        let guard_call = concat!("add_user_targets_requester_", "parent(ak, &cred)");
        let source = include_str!("user.rs");
        let guard_at = source
            .find(guard_call)
            .expect("AddUser must reject requests that target the requester's parent user");
        let sink_at = source
            .find("iam_store.create_user(ak, &args)")
            .expect("AddUser create_user sink moved; re-anchor this guard test");
        assert!(guard_at < sink_at, "the parent-user guard must run before the create_user write");
    }

    #[test]
    fn test_group_import_allows_missing_group_without_spaces() {
        assert!(!should_reject_group_import_name(
            "new-group",
            &IamError::NoSuchGroup("new-group".to_string())
        ));
    }

    #[test]
    fn test_group_import_rejects_group_names_with_spaces() {
        assert!(should_reject_group_import_name(
            " bad-group",
            &IamError::NoSuchGroup(" bad-group".to_string())
        ));
    }

    #[test]
    fn test_group_import_restores_disabled_status_only_when_needed() {
        assert!(should_restore_group_as_disabled("disabled"));
        assert!(!should_restore_group_as_disabled("enabled"));
    }

    #[test]
    fn test_imported_service_account_status_maps_on_and_off() {
        assert_eq!(imported_service_account_status("off").as_deref(), Some("off"));
        assert_eq!(imported_service_account_status("on").as_deref(), Some("on"));
        assert_eq!(imported_service_account_status("disabled").as_deref(), Some("off"));
        assert_eq!(imported_service_account_status("enabled").as_deref(), Some("on"));
        assert!(imported_service_account_status("unknown").is_none());
    }

    #[test]
    fn test_import_service_account_parent_rejects_other_parent_for_non_owner() {
        let requester = Credentials {
            access_key: "delegated-importer".to_string(),
            ..Default::default()
        };

        assert!(!imported_service_account_parent_allowed("root-access-key", &requester, false));
    }

    #[test]
    fn test_service_account_parent_scope_failure_records_import_error() {
        let requester = Credentials {
            access_key: "delegated-importer".to_string(),
            ..Default::default()
        };
        let err = imported_service_account_parent_scope_failure("svc-access-key", "root-access-key", &requester, false)
            .expect("non-owner must not import a service account for another parent");

        assert_eq!(err.name, "svc-access-key");
        assert_eq!(err.error, SERVICE_ACCOUNT_PARENT_SCOPE_ERROR);
        assert!(
            imported_service_account_parent_scope_failure("svc-access-key", "delegated-importer", &requester, false).is_none()
        );
    }

    #[test]
    fn test_service_account_import_rejects_payload_access_key_mismatch() {
        let payload = r#"{
            "svcalpha": {
                "parent": "useralpha",
                "accessKey": "svcbeta",
                "secretKey": "svcAlphaSecret123",
                "groups": [],
                "claims": {},
                "sessionPolicy": null,
                "status": "on",
                "name": "uploaderKey",
                "description": "alpha upload key",
                "expiration": "1970-01-01T00:00:00Z"
            }
        }"#;

        let svc_accts: HashMap<String, SRSvcAccCreate> = serde_json::from_str(payload).unwrap();
        let req = svc_accts.get("svcalpha").unwrap();
        let err = imported_service_account_access_key_failure("svcalpha", &req.access_key)
            .expect("mismatched service account access keys must be rejected");

        assert_eq!(err.name, "svcalpha");
        assert_eq!(err.error, SERVICE_ACCOUNT_ACCESS_KEY_MISMATCH_ERROR);
        assert!(imported_service_account_access_key_failure("svcalpha", "svcalpha").is_none());
    }

    #[test]
    fn test_import_service_account_parent_allows_owner_restore() {
        let requester = Credentials {
            access_key: "root-access-key".to_string(),
            ..Default::default()
        };

        assert!(imported_service_account_parent_allowed("any-imported-parent", &requester, true));
    }

    #[test]
    fn test_import_service_account_parent_allows_requester_self_parent() {
        let requester = Credentials {
            access_key: "delegated-importer".to_string(),
            ..Default::default()
        };

        assert!(imported_service_account_parent_allowed("delegated-importer", &requester, false));
    }

    #[test]
    fn test_import_service_account_parent_allows_derived_requester_parent() {
        let requester = Credentials {
            access_key: "derived-access-key".to_string(),
            parent_user: "parent-user".to_string(),
            session_token: "session-token".to_string(),
            ..Default::default()
        };

        assert!(imported_service_account_parent_allowed("parent-user", &requester, false));
        assert!(!imported_service_account_parent_allowed("other-parent", &requester, false));
    }

    #[test]
    fn test_service_account_import_accepts_null_groups_and_epoch_expiration() {
        let payload = r#"{
            "svcalpha": {
                "parent": "useralpha",
                "accessKey": "svcalpha",
                "secretKey": "svcAlphaSecret123",
                "groups": null,
                "claims": {
                    "accessKey": "svcalpha",
                    "parent": "useralpha",
                    "sa-policy": "inherited-policy"
                },
                "sessionPolicy": null,
                "status": "on",
                "name": "uploaderKey",
                "description": "alpha upload key",
                "expiration": "1970-01-01T00:00:00Z"
            }
        }"#;

        let svc_accts: HashMap<String, SRSvcAccCreate> = serde_json::from_str(payload).unwrap();
        let svc = svc_accts.get("svcalpha").unwrap();

        assert!(svc.groups.is_empty());
        assert!(svc.expiration.is_none());
    }

    #[test]
    fn test_service_account_import_preserves_non_zero_expiration() {
        let payload = r#"{
            "svcalpha": {
                "parent": "useralpha",
                "accessKey": "svcalpha",
                "secretKey": "svcAlphaSecret123",
                "groups": [],
                "claims": {},
                "sessionPolicy": null,
                "status": "on",
                "name": "uploaderKey",
                "description": "alpha upload key",
                "expiration": "2030-01-02T03:04:05Z"
            }
        }"#;

        let svc_accts: HashMap<String, SRSvcAccCreate> = serde_json::from_str(payload).unwrap();
        let svc = svc_accts.get("svcalpha").unwrap();

        assert_eq!(svc.expiration.map(|expiration| expiration.unix_timestamp()), Some(1893553445));
    }

    #[test]
    fn test_group_policy_mappings_use_regular_user_type() {
        assert_eq!(GROUP_POLICY_MAPPING_USER_TYPE, rustfs_iam::store::UserType::Reg);
    }

    /// A single member must not be able to exhaust the archive's expansion budget:
    /// `MAX_IAM_IMPORT_SIZE` bounds the compressed upload only, so an unbounded
    /// `read_to_end` here would let a small archive expand without limit.
    #[test]
    fn import_member_read_is_bounded_by_the_expansion_budget() {
        let oversized = vec![b'a'; 1024];
        let mut budget: u64 = 512;

        let err = read_import_member(&mut oversized.as_slice(), &mut budget)
            .expect_err("a member larger than the remaining budget must be rejected");
        assert!(
            matches!(*err.code(), S3ErrorCode::EntityTooLarge),
            "expected EntityTooLarge, got {:?}",
            err.code()
        );
    }

    /// The budget is shared across members, so successive reads draw it down and the
    /// archive as a whole is capped rather than each member independently.
    #[test]
    fn import_member_budget_is_shared_across_members() {
        let member = vec![b'a'; 400];
        let mut budget: u64 = 1000;

        let first = read_import_member(&mut member.as_slice(), &mut budget).expect("first member fits");
        assert_eq!(first.len(), 400);
        assert_eq!(budget, 600, "budget must be drawn down by what was read");

        read_import_member(&mut member.as_slice(), &mut budget).expect("second member still fits");
        assert_eq!(budget, 200);

        read_import_member(&mut member.as_slice(), &mut budget)
            .expect_err("third member must be rejected once the shared budget is exhausted");
    }

    /// Guards the bound from being set so tight that ordinary imports break.
    #[test]
    fn import_expansion_budget_leaves_headroom_over_the_compressed_cap() {
        assert!(
            MAX_IAM_IMPORT_EXPANDED_SIZE >= MAX_IAM_IMPORT_SIZE as u64,
            "expanded budget must not be smaller than the compressed upload cap"
        );
    }

    /// The shared admin gate answers a credential-less request with `InvalidRequest "get cred failed"`.
    /// Each converted handler keeps a pre-check ahead of the gate so its own wire response survives
    /// the refactor unchanged.
    #[tokio::test]
    async fn user_handlers_keep_their_missing_credentials_response() {
        assert_missing_credentials(
            &SetUserStatus {},
            Method::PUT,
            "/rustfs/admin/v3/set-user-status?accessKey=alice",
            "authentication required",
        )
        .await;
        assert_missing_credentials(&ListUsers {}, Method::GET, "/rustfs/admin/v3/list-users", "authentication required").await;
        assert_missing_credentials(&RemoveUser {}, Method::DELETE, "/rustfs/admin/v3/remove-user", "authentication required")
            .await;
        assert_missing_credentials(&ExportIam {}, Method::GET, "/rustfs/admin/v3/export-iam", "get cred failed").await;
    }

    #[test]
    fn user_handlers_use_the_shared_admin_gate_with_their_actions() {
        let production = include_str!("user.rs")
            .split("\n#[cfg(test)]\n")
            .next()
            .expect("production source must precede tests");

        for (handler, action, binds_credentials) in [
            ("SetUserStatus", "EnableUserAdminAction", false),
            ("ListUsers", "ListUsersAdminAction", true),
            ("RemoveUser", "DeleteUserAdminAction", true),
            ("ExportIam", "ExportIAMAction", false),
        ] {
            let block = source_block(production, &format!("impl Operation for {handler}"));
            assert_shared_gate_wiring(block, handler, &[action], binds_credentials);
        }

        // AddUser and GetUserInfo derive a per-request `deny_only` from `should_check_deny_only`, and
        // ImportIam consumes the `owner` flag while restoring service accounts. The shared gate pins
        // `deny_only = false` and hands back only the credentials, so these three keep the explicit
        // `check_key_valid` + `validate_admin_request` preamble.
        for (handler, marker) in [
            ("AddUser", "should_check_deny_only(ak, &cred)"),
            ("GetUserInfo", "should_check_deny_only(ak, &cred)"),
            ("ImportIam", "if !owner {"),
        ] {
            let block = source_block(production, &format!("impl Operation for {handler}"));
            assert!(
                block.contains(marker),
                "{handler} must keep the context that rules out the shared gate ({marker})"
            );
            assert!(
                !block.contains("authorize_admin_request("),
                "{handler} must not be routed through the shared gate"
            );
            assert!(
                block.contains("validate_admin_request("),
                "{handler} must keep its explicit validate_admin_request call"
            );
        }
    }
}
