// 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 rustfs_s3_types::EventName;

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(usize)]
pub enum S3Operation {
    AbortMultipartUpload,
    CompleteMultipartUpload,
    CopyObject,
    CreateBucket,
    CreateMultipartUpload,
    DeleteBucket,
    DeleteBucketCors,
    DeleteBucketEncryption,
    DeleteBucketLifecycle,
    DeleteBucketPolicy,
    DeleteBucketReplication,
    DeleteBucketTagging,
    DeleteObject,
    DeleteObjectTagging,
    DeleteObjects,
    DeletePublicAccessBlock,
    GetBucketAcl,
    GetBucketCors,
    GetBucketEncryption,
    GetBucketLifecycleConfiguration,
    GetBucketLocation,
    GetBucketLogging,
    GetBucketNotificationConfiguration,
    GetBucketPolicy,
    GetBucketPolicyStatus,
    GetBucketReplication,
    GetBucketTagging,
    GetBucketVersioning,
    GetObject,
    GetObjectAcl,
    GetObjectAttributes,
    GetObjectLegalHold,
    GetObjectLockConfiguration,
    GetObjectRetention,
    GetObjectTagging,
    GetObjectTorrent,
    GetPublicAccessBlock,
    HeadBucket,
    HeadObject,
    ListBuckets,
    ListMultipartUploads,
    ListObjectVersions,
    ListObjects,
    ListObjectsV2,
    ListParts,
    PutBucketAcl,
    PutBucketCors,
    PutBucketEncryption,
    PutBucketLifecycleConfiguration,
    PutBucketLogging,
    PutBucketNotificationConfiguration,
    PutBucketPolicy,
    PutBucketReplication,
    PutBucketTagging,
    PutBucketVersioning,
    PutObject,
    PutObjectAcl,
    PutObjectLegalHold,
    PutObjectLockConfiguration,
    PutObjectRetention,
    PutObjectTagging,
    PutPublicAccessBlock,
    RestoreObject,
    SelectObjectContent,
    UploadPart,
    UploadPartCopy,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EventMapping {
    None,
    Single(EventName),
    PutObject,
    DeleteObject,
    DeleteObjects,
}

impl EventMapping {
    #[inline]
    fn primary_event(self) -> Option<EventName> {
        match self {
            Self::None => None,
            Self::Single(event_name) => Some(event_name),
            Self::PutObject => Some(EventName::ObjectCreatedPut),
            Self::DeleteObject => Some(EventName::ObjectRemovedDelete),
            Self::DeleteObjects => Some(EventName::ObjectRemovedDeleteObjects),
        }
    }

    #[inline]
    fn matches_event(self, event_name: EventName) -> bool {
        match self {
            Self::None => false,
            Self::Single(mapped_event_name) => mapped_event_name == event_name,
            Self::PutObject => matches!(event_name, EventName::ObjectCreatedPut | EventName::ObjectCreatedPost),
            Self::DeleteObject => matches!(
                event_name,
                EventName::ObjectRemovedDelete
                    | EventName::ObjectRemovedDeleteMarkerCreated
                    | EventName::ObjectRemovedDeleteAllVersions
            ),
            Self::DeleteObjects => {
                matches!(event_name, EventName::ObjectRemovedDeleteObjects | EventName::ObjectRemovedDelete)
            }
        }
    }
}

impl S3Operation {
    pub const ALL: [Self; 66] = [
        Self::AbortMultipartUpload,
        Self::CompleteMultipartUpload,
        Self::CopyObject,
        Self::CreateBucket,
        Self::CreateMultipartUpload,
        Self::DeleteBucket,
        Self::DeleteBucketCors,
        Self::DeleteBucketEncryption,
        Self::DeleteBucketLifecycle,
        Self::DeleteBucketPolicy,
        Self::DeleteBucketReplication,
        Self::DeleteBucketTagging,
        Self::DeleteObject,
        Self::DeleteObjectTagging,
        Self::DeleteObjects,
        Self::DeletePublicAccessBlock,
        Self::GetBucketAcl,
        Self::GetBucketCors,
        Self::GetBucketEncryption,
        Self::GetBucketLifecycleConfiguration,
        Self::GetBucketLocation,
        Self::GetBucketLogging,
        Self::GetBucketNotificationConfiguration,
        Self::GetBucketPolicy,
        Self::GetBucketPolicyStatus,
        Self::GetBucketReplication,
        Self::GetBucketTagging,
        Self::GetBucketVersioning,
        Self::GetObject,
        Self::GetObjectAcl,
        Self::GetObjectAttributes,
        Self::GetObjectLegalHold,
        Self::GetObjectLockConfiguration,
        Self::GetObjectRetention,
        Self::GetObjectTagging,
        Self::GetObjectTorrent,
        Self::GetPublicAccessBlock,
        Self::HeadBucket,
        Self::HeadObject,
        Self::ListBuckets,
        Self::ListMultipartUploads,
        Self::ListObjectVersions,
        Self::ListObjects,
        Self::ListObjectsV2,
        Self::ListParts,
        Self::PutBucketAcl,
        Self::PutBucketCors,
        Self::PutBucketEncryption,
        Self::PutBucketLifecycleConfiguration,
        Self::PutBucketLogging,
        Self::PutBucketNotificationConfiguration,
        Self::PutBucketPolicy,
        Self::PutBucketReplication,
        Self::PutBucketTagging,
        Self::PutBucketVersioning,
        Self::PutObject,
        Self::PutObjectAcl,
        Self::PutObjectLegalHold,
        Self::PutObjectLockConfiguration,
        Self::PutObjectRetention,
        Self::PutObjectTagging,
        Self::PutPublicAccessBlock,
        Self::RestoreObject,
        Self::SelectObjectContent,
        Self::UploadPart,
        Self::UploadPartCopy,
    ];

    #[inline]
    pub const fn metric_index(self) -> usize {
        self as usize
    }

    pub fn as_str(self) -> &'static str {
        match self {
            Self::AbortMultipartUpload => "s3:AbortMultipartUpload",
            Self::CompleteMultipartUpload => "s3:CompleteMultipartUpload",
            Self::CopyObject => "s3:CopyObject",
            Self::CreateBucket => "s3:CreateBucket",
            Self::CreateMultipartUpload => "s3:CreateMultipartUpload",
            Self::DeleteBucket => "s3:DeleteBucket",
            Self::DeleteBucketCors => "s3:DeleteBucketCors",
            Self::DeleteBucketEncryption => "s3:DeleteBucketEncryption",
            Self::DeleteBucketLifecycle => "s3:DeleteBucketLifecycle",
            Self::DeleteBucketPolicy => "s3:DeleteBucketPolicy",
            Self::DeleteBucketReplication => "s3:DeleteBucketReplication",
            Self::DeleteBucketTagging => "s3:DeleteBucketTagging",
            Self::DeleteObject => "s3:DeleteObject",
            Self::DeleteObjectTagging => "s3:DeleteObjectTagging",
            Self::DeleteObjects => "s3:DeleteObjects",
            Self::DeletePublicAccessBlock => "s3:DeletePublicAccessBlock",
            Self::GetBucketAcl => "s3:GetBucketAcl",
            Self::GetBucketCors => "s3:GetBucketCors",
            Self::GetBucketEncryption => "s3:GetBucketEncryption",
            Self::GetBucketLifecycleConfiguration => "s3:GetBucketLifecycleConfiguration",
            Self::GetBucketLocation => "s3:GetBucketLocation",
            Self::GetBucketLogging => "s3:GetBucketLogging",
            Self::GetBucketNotificationConfiguration => "s3:GetBucketNotificationConfiguration",
            Self::GetBucketPolicy => "s3:GetBucketPolicy",
            Self::GetBucketPolicyStatus => "s3:GetBucketPolicyStatus",
            Self::GetBucketReplication => "s3:GetBucketReplication",
            Self::GetBucketTagging => "s3:GetBucketTagging",
            Self::GetBucketVersioning => "s3:GetBucketVersioning",
            Self::GetObject => "s3:GetObject",
            Self::GetObjectAcl => "s3:GetObjectAcl",
            Self::GetObjectAttributes => "s3:GetObjectAttributes",
            Self::GetObjectLegalHold => "s3:GetObjectLegalHold",
            Self::GetObjectLockConfiguration => "s3:GetObjectLockConfiguration",
            Self::GetObjectRetention => "s3:GetObjectRetention",
            Self::GetObjectTagging => "s3:GetObjectTagging",
            Self::GetObjectTorrent => "s3:GetObjectTorrent",
            Self::GetPublicAccessBlock => "s3:GetPublicAccessBlock",
            Self::HeadBucket => "s3:HeadBucket",
            Self::HeadObject => "s3:HeadObject",
            Self::ListBuckets => "s3:ListBuckets",
            Self::ListMultipartUploads => "s3:ListMultipartUploads",
            Self::ListObjectVersions => "s3:ListObjectVersions",
            Self::ListObjects => "s3:ListObjects",
            Self::ListObjectsV2 => "s3:ListObjectsV2",
            Self::ListParts => "s3:ListParts",
            Self::PutBucketAcl => "s3:PutBucketAcl",
            Self::PutBucketCors => "s3:PutBucketCors",
            Self::PutBucketEncryption => "s3:PutBucketEncryption",
            Self::PutBucketLifecycleConfiguration => "s3:PutBucketLifecycleConfiguration",
            Self::PutBucketLogging => "s3:PutBucketLogging",
            Self::PutBucketNotificationConfiguration => "s3:PutBucketNotificationConfiguration",
            Self::PutBucketPolicy => "s3:PutBucketPolicy",
            Self::PutBucketReplication => "s3:PutBucketReplication",
            Self::PutBucketTagging => "s3:PutBucketTagging",
            Self::PutBucketVersioning => "s3:PutBucketVersioning",
            Self::PutObject => "s3:PutObject",
            Self::PutObjectAcl => "s3:PutObjectAcl",
            Self::PutObjectLegalHold => "s3:PutObjectLegalHold",
            Self::PutObjectLockConfiguration => "s3:PutObjectLockConfiguration",
            Self::PutObjectRetention => "s3:PutObjectRetention",
            Self::PutObjectTagging => "s3:PutObjectTagging",
            Self::PutPublicAccessBlock => "s3:PutPublicAccessBlock",
            Self::RestoreObject => "s3:RestoreObject",
            Self::SelectObjectContent => "s3:SelectObjectContent",
            Self::UploadPart => "s3:UploadPart",
            Self::UploadPartCopy => "s3:UploadPartCopy",
        }
    }

    #[inline]
    fn event_mapping(self) -> EventMapping {
        match self {
            Self::AbortMultipartUpload => EventMapping::Single(EventName::ObjectRemovedAbortMultipartUpload),
            Self::CompleteMultipartUpload => EventMapping::Single(EventName::ObjectCreatedCompleteMultipartUpload),
            Self::CopyObject => EventMapping::Single(EventName::ObjectCreatedCopy),
            Self::CreateBucket => EventMapping::Single(EventName::BucketCreated),
            Self::CreateMultipartUpload => EventMapping::Single(EventName::ObjectCreatedCreateMultipartUpload),
            Self::DeleteBucket => EventMapping::Single(EventName::BucketRemoved),
            Self::DeleteObjectTagging => EventMapping::Single(EventName::ObjectTaggingDelete),
            Self::DeleteObject => EventMapping::DeleteObject,
            Self::DeleteObjects => EventMapping::DeleteObjects,
            Self::GetObject => EventMapping::Single(EventName::ObjectAccessedGet),
            Self::GetObjectAttributes => EventMapping::Single(EventName::ObjectAccessedAttributes),
            Self::GetObjectLegalHold => EventMapping::Single(EventName::ObjectAccessedGetLegalHold),
            Self::GetObjectRetention => EventMapping::Single(EventName::ObjectAccessedGetRetention),
            Self::HeadObject => EventMapping::Single(EventName::ObjectAccessedHead),
            Self::PutObject => EventMapping::PutObject,
            Self::PutObjectAcl => EventMapping::Single(EventName::ObjectAclPut),
            Self::PutObjectLegalHold => EventMapping::Single(EventName::ObjectCreatedPutLegalHold),
            Self::PutObjectRetention => EventMapping::Single(EventName::ObjectCreatedPutRetention),
            Self::PutObjectTagging => EventMapping::Single(EventName::ObjectTaggingPut),
            Self::RestoreObject => EventMapping::Single(EventName::ObjectRestorePost),
            Self::SelectObjectContent => EventMapping::Single(EventName::ObjectAccessedGet),
            Self::DeleteBucketCors
            | Self::DeleteBucketEncryption
            | Self::DeleteBucketLifecycle
            | Self::DeleteBucketPolicy
            | Self::DeleteBucketReplication
            | Self::DeleteBucketTagging
            | Self::DeletePublicAccessBlock
            | Self::GetBucketAcl
            | Self::GetBucketCors
            | Self::GetBucketEncryption
            | Self::GetBucketLifecycleConfiguration
            | Self::GetBucketLocation
            | Self::GetBucketLogging
            | Self::GetBucketNotificationConfiguration
            | Self::GetBucketPolicy
            | Self::GetBucketPolicyStatus
            | Self::GetBucketReplication
            | Self::GetBucketTagging
            | Self::GetBucketVersioning
            | Self::GetObjectAcl
            | Self::GetObjectLockConfiguration
            | Self::GetObjectTagging
            | Self::GetObjectTorrent
            | Self::GetPublicAccessBlock
            | Self::HeadBucket
            | Self::ListBuckets
            | Self::ListMultipartUploads
            | Self::ListObjectVersions
            | Self::ListObjects
            | Self::ListObjectsV2
            | Self::ListParts
            | Self::PutBucketAcl
            | Self::PutBucketCors
            | Self::PutBucketEncryption
            | Self::PutBucketLifecycleConfiguration
            | Self::PutBucketLogging
            | Self::PutBucketNotificationConfiguration
            | Self::PutBucketPolicy
            | Self::PutBucketReplication
            | Self::PutBucketTagging
            | Self::PutBucketVersioning
            | Self::PutObjectLockConfiguration
            | Self::PutPublicAccessBlock
            | Self::UploadPart
            | Self::UploadPartCopy => EventMapping::None,
        }
    }

    pub fn to_event_name(self) -> Option<EventName> {
        self.event_mapping().primary_event()
    }
}

pub fn event_name_to_s3_operation(event_name: EventName) -> Option<S3Operation> {
    match event_name {
        EventName::BucketCreated => Some(S3Operation::CreateBucket),
        EventName::BucketRemoved => Some(S3Operation::DeleteBucket),
        EventName::ObjectAccessedGet => Some(S3Operation::GetObject),
        EventName::ObjectAccessedGetRetention => Some(S3Operation::GetObjectRetention),
        EventName::ObjectAccessedGetLegalHold => Some(S3Operation::GetObjectLegalHold),
        EventName::ObjectAccessedHead => Some(S3Operation::HeadObject),
        EventName::ObjectAccessedAttributes => Some(S3Operation::GetObjectAttributes),
        EventName::ObjectCreatedCompleteMultipartUpload => Some(S3Operation::CompleteMultipartUpload),
        EventName::ObjectCreatedCopy => Some(S3Operation::CopyObject),
        EventName::ObjectCreatedPost => Some(S3Operation::PutObject),
        EventName::ObjectCreatedPut => Some(S3Operation::PutObject),
        EventName::ObjectCreatedPutRetention => Some(S3Operation::PutObjectRetention),
        EventName::ObjectCreatedPutLegalHold => Some(S3Operation::PutObjectLegalHold),
        EventName::ObjectTaggingPut => Some(S3Operation::PutObjectTagging),
        EventName::ObjectTaggingDelete => Some(S3Operation::DeleteObjectTagging),
        EventName::ObjectAclPut => Some(S3Operation::PutObjectAcl),
        EventName::ObjectRemovedDelete => Some(S3Operation::DeleteObject),
        EventName::ObjectRemovedDeleteMarkerCreated => Some(S3Operation::DeleteObject),
        EventName::ObjectRemovedDeleteAllVersions => Some(S3Operation::DeleteObject),
        EventName::ObjectRestorePost => Some(S3Operation::RestoreObject),
        EventName::ObjectRemovedAbortMultipartUpload => Some(S3Operation::AbortMultipartUpload),
        EventName::ObjectCreatedCreateMultipartUpload => Some(S3Operation::CreateMultipartUpload),
        EventName::ObjectRemovedDeleteObjects => Some(S3Operation::DeleteObjects),
        _ => None,
    }
}

/// Returns whether an S3 operation is semantically compatible with an event name.
///
/// Some S3 operations intentionally map to multiple event variants:
/// - `PutObject` can emit both `ObjectCreatedPut` and `ObjectCreatedPost`.
/// - `DeleteObject` can emit delete/delete-marker/all-versions variants.
/// - `DeleteObjects` can emit per-object delete events in addition to the
///   internal batch-delete event.
pub fn operation_matches_event_name(op: S3Operation, event_name: EventName) -> bool {
    op.event_mapping().matches_event(event_name)
}

/// Resolves the object-delete notification event name from delete-marker state.
#[inline]
pub fn delete_event_name_for_marker(delete_marker: bool) -> EventName {
    if delete_marker {
        EventName::ObjectRemovedDeleteMarkerCreated
    } else {
        EventName::ObjectRemovedDelete
    }
}

/// Resolves the object-create notification event name from POST-object mode.
#[inline]
pub fn put_event_name_for_post_object(is_post_object: bool) -> EventName {
    if is_post_object {
        EventName::ObjectCreatedPost
    } else {
        EventName::ObjectCreatedPut
    }
}

/// Returns `true` when the event is one of object-remove notification variants
/// that should omit size/etag metadata.
#[inline]
pub fn is_object_removed_event(event_name: EventName) -> bool {
    event_name.is_removed()
}

/// Returns the event mask that matches both PUT and POST object-created events.
#[inline]
pub fn put_object_created_event_mask() -> u64 {
    EventName::ObjectCreatedPut.mask() | EventName::ObjectCreatedPost.mask()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_s3_operation_to_event_name() {
        assert_eq!(S3Operation::PutObject.to_event_name(), Some(EventName::ObjectCreatedPut));
        assert_eq!(S3Operation::PutObjectAcl.to_event_name(), Some(EventName::ObjectAclPut));
        assert_eq!(S3Operation::PutObjectTagging.to_event_name(), Some(EventName::ObjectTaggingPut));
        assert_eq!(S3Operation::DeleteObjectTagging.to_event_name(), Some(EventName::ObjectTaggingDelete));
        assert_eq!(S3Operation::GetObject.to_event_name(), Some(EventName::ObjectAccessedGet));
        assert_eq!(S3Operation::ListBuckets.to_event_name(), None);
        assert_eq!(S3Operation::RestoreObject.to_event_name(), Some(EventName::ObjectRestorePost));
        assert_eq!(S3Operation::SelectObjectContent.to_event_name(), Some(EventName::ObjectAccessedGet));
        assert_eq!(
            S3Operation::AbortMultipartUpload.to_event_name(),
            Some(EventName::ObjectRemovedAbortMultipartUpload)
        );
    }

    #[test]
    fn test_event_name_to_s3_operation() {
        assert_eq!(event_name_to_s3_operation(EventName::ObjectCreatedPut), Some(S3Operation::PutObject));
        assert_eq!(event_name_to_s3_operation(EventName::ObjectAclPut), Some(S3Operation::PutObjectAcl));
        assert_eq!(
            event_name_to_s3_operation(EventName::ObjectTaggingPut),
            Some(S3Operation::PutObjectTagging)
        );
        assert_eq!(
            event_name_to_s3_operation(EventName::ObjectTaggingDelete),
            Some(S3Operation::DeleteObjectTagging)
        );
        assert_eq!(event_name_to_s3_operation(EventName::ObjectAccessedGet), Some(S3Operation::GetObject));
        assert_eq!(event_name_to_s3_operation(EventName::BucketCreated), Some(S3Operation::CreateBucket));
        assert_eq!(event_name_to_s3_operation(EventName::Everything), None);
        assert_eq!(event_name_to_s3_operation(EventName::ObjectRestorePost), Some(S3Operation::RestoreObject));
        assert_eq!(event_name_to_s3_operation(EventName::ObjectCreatedPost), Some(S3Operation::PutObject));
        assert_eq!(
            event_name_to_s3_operation(EventName::ObjectRemovedAbortMultipartUpload),
            Some(S3Operation::AbortMultipartUpload)
        );
    }

    #[test]
    fn test_operation_matches_event_name() {
        assert!(operation_matches_event_name(S3Operation::PutObject, EventName::ObjectCreatedPut));
        assert!(operation_matches_event_name(S3Operation::PutObject, EventName::ObjectCreatedPost));
        assert!(operation_matches_event_name(
            S3Operation::DeleteObject,
            EventName::ObjectRemovedDeleteMarkerCreated
        ));
        assert!(operation_matches_event_name(S3Operation::DeleteObjects, EventName::ObjectRemovedDelete));

        assert!(!operation_matches_event_name(S3Operation::GetObject, EventName::ObjectCreatedPut));
    }

    #[test]
    fn test_delete_event_name_for_marker() {
        assert_eq!(delete_event_name_for_marker(true), EventName::ObjectRemovedDeleteMarkerCreated);
        assert_eq!(delete_event_name_for_marker(false), EventName::ObjectRemovedDelete);
    }

    #[test]
    fn test_put_event_name_for_post_object() {
        assert_eq!(put_event_name_for_post_object(true), EventName::ObjectCreatedPost);
        assert_eq!(put_event_name_for_post_object(false), EventName::ObjectCreatedPut);
    }

    #[test]
    fn test_is_object_removed_event() {
        // Every ObjectRemoved* variant must be recognized as a removal event.
        assert!(is_object_removed_event(EventName::ObjectRemovedDelete));
        assert!(is_object_removed_event(EventName::ObjectRemovedDeleteMarkerCreated));
        assert!(is_object_removed_event(EventName::ObjectRemovedDeleteAllVersions));
        assert!(is_object_removed_event(EventName::ObjectRemovedNoOP));
        assert!(is_object_removed_event(EventName::ObjectRemovedAbortMultipartUpload));
        assert!(is_object_removed_event(EventName::ObjectRemovedDeleteObjects));

        // Non-removal events must be rejected.
        assert!(!is_object_removed_event(EventName::ObjectCreatedPut));
        assert!(!is_object_removed_event(EventName::ObjectCreatedPost));
        assert!(!is_object_removed_event(EventName::ObjectTaggingPut));
        assert!(!is_object_removed_event(EventName::ObjectTaggingDelete));
        assert!(!is_object_removed_event(EventName::ObjectAclPut));
        assert!(!is_object_removed_event(EventName::ObjectAccessedGet));
    }

    #[test]
    fn test_put_object_created_event_mask() {
        let mask = put_object_created_event_mask();
        assert_ne!(mask & EventName::ObjectCreatedPut.mask(), 0);
        assert_ne!(mask & EventName::ObjectCreatedPost.mask(), 0);
        assert_eq!(mask & EventName::ObjectRemovedDelete.mask(), 0);
    }

    #[test]
    fn test_operations_without_event_mapping_remain_unmapped() {
        let unmapped = [
            S3Operation::GetBucketAcl,
            S3Operation::ListObjectsV2,
            S3Operation::PutBucketNotificationConfiguration,
            S3Operation::UploadPart,
        ];
        for op in unmapped {
            assert_eq!(op.to_event_name(), None);
            assert!(!operation_matches_event_name(op, EventName::ObjectCreatedPut));
        }
    }
}
