// 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 crate::storage::s3_api::common::{rustfs_initiator, rustfs_owner};
use crate::storage::storage_api::effective_storage_class;
use crate::storage::storage_api::s3_api_consumer::multipart::contract::multipart::{
    ListMultipartsInfo, ListPartsInfo, MAX_MULTIPART_PART_NUMBER,
};
use crate::storage::storage_api::s3_api_consumer::multipart::to_s3s_etag;
use s3s::dto::{CommonPrefix, ListMultipartUploadsOutput, ListPartsOutput, MultipartUpload, Part, Timestamp};
use s3s::{S3Error, S3ErrorCode};

const MAX_MULTIPART_UPLOADS_LIST: i32 = 1000;

#[derive(Debug, PartialEq, Eq)]
pub(crate) struct ListPartsParams {
    pub part_number_marker: Option<usize>,
    pub max_parts: usize,
}

#[derive(Debug, PartialEq, Eq)]
pub(crate) struct ListMultipartUploadsParams {
    pub prefix: String,
    pub key_marker: Option<String>,
    pub max_uploads: usize,
}

pub(crate) fn build_list_parts_output(res: ListPartsInfo) -> ListPartsOutput {
    let owner = rustfs_owner();
    let initiator = rustfs_initiator();
    let transformed_parts = rustfs_utils::http::contains_key_str(&res.user_defined, rustfs_utils::http::SUFFIX_COMPRESSION)
        || res
            .user_defined
            .keys()
            .any(|key| rustfs_utils::http::is_object_encryption_marker(key));

    ListPartsOutput {
        bucket: Some(res.bucket),
        key: Some(res.object),
        upload_id: Some(res.upload_id),
        parts: Some(
            res.parts
                .into_iter()
                .map(|p| Part {
                    e_tag: p.etag.map(|etag| to_s3s_etag(&etag)),
                    last_modified: p.last_mod.map(Timestamp::from),
                    part_number: p.part_num.try_into().ok(),
                    // Compressed parts store fewer bytes than the client sent; S3
                    // semantics report the uploaded (logical) size, matching
                    // GetObjectAttributes ObjectParts.
                    size: if p.actual_size > 0 || (transformed_parts && p.actual_size == 0) {
                        Some(p.actual_size)
                    } else {
                        p.size.try_into().ok()
                    },
                    ..Default::default()
                })
                .collect(),
        ),
        owner: Some(owner),
        initiator: Some(initiator),
        is_truncated: Some(res.is_truncated),
        next_part_number_marker: res.next_part_number_marker.try_into().ok(),
        max_parts: res.max_parts.try_into().ok(),
        part_number_marker: res.part_number_marker.try_into().ok(),
        storage_class: Some(
            effective_storage_class((!res.storage_class.is_empty()).then_some(res.storage_class.as_str()), None)
                .to_string()
                .into(),
        ),
        ..Default::default()
    }
}

pub(crate) fn parse_list_parts_params(
    part_number_marker: Option<i32>,
    max_parts: Option<i32>,
) -> Result<ListPartsParams, S3Error> {
    let part_number_marker = match part_number_marker {
        Some(marker) => {
            if marker < 0 {
                return Err(S3Error::with_message(
                    S3ErrorCode::InvalidArgument,
                    "part-number-marker must be non-negative".to_string(),
                ));
            }
            Some(marker as usize)
        }
        None => None,
    };
    let max_parts = match max_parts {
        Some(parts) => {
            if !(1..=1000).contains(&parts) {
                return Err(S3Error::with_message(
                    S3ErrorCode::InvalidArgument,
                    "max-parts must be between 1 and 1000".to_string(),
                ));
            }
            parts as usize
        }
        None => 1000,
    };

    Ok(ListPartsParams {
        part_number_marker,
        max_parts,
    })
}

pub(crate) fn parse_upload_part_number(part_number: i32) -> Result<usize, S3Error> {
    if !(1..=MAX_MULTIPART_PART_NUMBER).contains(&part_number) {
        return Err(S3Error::with_message(
            S3ErrorCode::InvalidArgument,
            format!("partNumber must be between 1 and {MAX_MULTIPART_PART_NUMBER}"),
        ));
    }

    Ok(part_number as usize)
}

pub(crate) fn parse_list_multipart_uploads_params(
    prefix: Option<String>,
    key_marker: Option<String>,
    max_uploads: Option<i32>,
) -> Result<ListMultipartUploadsParams, S3Error> {
    let prefix = prefix.unwrap_or_default();
    let max_uploads = match max_uploads {
        Some(value) => {
            if !(1..=MAX_MULTIPART_UPLOADS_LIST).contains(&value) {
                return Err(S3Error::with_message(
                    S3ErrorCode::InvalidArgument,
                    format!("max-uploads must be between 1 and {}", MAX_MULTIPART_UPLOADS_LIST),
                ));
            }

            value as usize
        }
        None => MAX_MULTIPART_UPLOADS_LIST as usize,
    };

    if let Some(key_marker) = &key_marker
        && !key_marker.starts_with(prefix.as_str())
    {
        return Err(S3Error::with_message(S3ErrorCode::InvalidArgument, "Invalid key marker".to_string()));
    }

    Ok(ListMultipartUploadsParams {
        prefix,
        key_marker,
        max_uploads,
    })
}

pub(crate) fn build_list_multipart_uploads_output(
    bucket: String,
    prefix: String,
    result: ListMultipartsInfo,
) -> ListMultipartUploadsOutput {
    let owner = rustfs_owner();
    let initiator = rustfs_initiator();

    ListMultipartUploadsOutput {
        bucket: Some(bucket),
        prefix: Some(prefix),
        delimiter: result.delimiter,
        key_marker: result.key_marker,
        upload_id_marker: result.upload_id_marker,
        next_key_marker: result.next_key_marker,
        next_upload_id_marker: result.next_upload_id_marker,
        max_uploads: Some(result.max_uploads as i32),
        is_truncated: Some(result.is_truncated),
        uploads: Some(
            result
                .uploads
                .into_iter()
                .map(|u| MultipartUpload {
                    key: Some(u.object),
                    upload_id: Some(u.upload_id),
                    initiated: u.initiated.map(Timestamp::from),
                    owner: Some(owner.clone()),
                    initiator: Some(initiator.clone()),
                    ..Default::default()
                })
                .collect(),
        ),
        common_prefixes: Some(
            result
                .common_prefixes
                .into_iter()
                .map(|c| CommonPrefix { prefix: Some(c) })
                .collect(),
        ),
        ..Default::default()
    }
}

#[cfg(test)]
mod tests {
    use super::{
        MAX_MULTIPART_UPLOADS_LIST, build_list_multipart_uploads_output, build_list_parts_output,
        parse_list_multipart_uploads_params, parse_list_parts_params, parse_upload_part_number,
    };
    use crate::storage::s3_api::common::{rustfs_initiator, rustfs_owner};
    use crate::storage::storage_api::s3_api_consumer::multipart::contract::multipart::{
        ListMultipartsInfo, ListPartsInfo, MultipartInfo, PartInfo,
    };
    use crate::storage::storage_api::s3_api_consumer::multipart::to_s3s_etag;
    use s3s::S3ErrorCode;
    use s3s::dto::Timestamp;
    use time::OffsetDateTime;

    #[test]
    fn test_list_parts_output_maps_parts_and_owner() {
        let input = ListPartsInfo {
            bucket: "bucket-a".to_string(),
            object: "obj-a".to_string(),
            upload_id: "upload-a".to_string(),
            storage_class: "STANDARD".to_string(),
            part_number_marker: 1,
            next_part_number_marker: 2,
            max_parts: 1000,
            is_truncated: true,
            parts: vec![PartInfo {
                part_num: 1,
                size: 11,
                etag: Some("etag-1".to_string()),
                ..Default::default()
            }],
            ..Default::default()
        };

        let output = build_list_parts_output(input);
        let parts = output.parts.as_ref().expect("parts should be present");

        assert_eq!(output.bucket.as_deref(), Some("bucket-a"));
        assert_eq!(output.key.as_deref(), Some("obj-a"));
        assert_eq!(output.upload_id.as_deref(), Some("upload-a"));
        assert_eq!(output.storage_class.as_ref().map(|v| v.as_str()), Some("STANDARD"));
        assert_eq!(output.part_number_marker, Some(1));
        assert_eq!(output.next_part_number_marker, Some(2));
        assert_eq!(output.max_parts, Some(1000));
        assert_eq!(output.is_truncated, Some(true));
        assert_eq!(parts.len(), 1);
        assert_eq!(parts[0].part_number, Some(1));
        assert_eq!(parts[0].size, Some(11));
        assert_eq!(parts[0].e_tag, Some(to_s3s_etag("etag-1")));
        assert_eq!(output.owner, Some(rustfs_owner()));
        assert_eq!(output.initiator, Some(rustfs_initiator()));
    }

    #[test]
    fn test_list_parts_output_reports_logical_size_for_compressed_parts() {
        let input = ListPartsInfo {
            bucket: "bucket-a".to_string(),
            object: "obj-a".to_string(),
            upload_id: "upload-a".to_string(),
            parts: vec![PartInfo {
                part_num: 1,
                // Stored (compressed) bytes on disk vs. the logical size the client uploaded.
                size: 1_024,
                actual_size: 8_388_608,
                ..Default::default()
            }],
            ..Default::default()
        };

        let output = build_list_parts_output(input);
        let parts = output.parts.as_ref().expect("parts should be present");

        assert_eq!(parts.len(), 1);
        assert_eq!(
            parts[0].size,
            Some(8_388_608),
            "compressed parts must report the uploaded logical size, not the stored size"
        );
    }

    #[test]
    fn test_list_parts_output_reports_zero_logical_size_for_compressed_parts() {
        let mut user_defined = std::collections::HashMap::new();
        rustfs_utils::http::insert_str(&mut user_defined, rustfs_utils::http::SUFFIX_COMPRESSION, "S2".to_string());
        let input = ListPartsInfo {
            user_defined,
            parts: vec![PartInfo {
                part_num: 1,
                // Legacy SSE writes an 8-byte end record for an empty part.
                size: 8,
                actual_size: 0,
                ..Default::default()
            }],
            ..Default::default()
        };

        let output = build_list_parts_output(input);
        let parts = output.parts.as_ref().expect("parts should be present");

        assert_eq!(parts[0].size, Some(0));
    }

    #[test]
    fn test_list_parts_output_reports_zero_logical_size_for_encrypted_parts() {
        let input = ListPartsInfo {
            user_defined: std::collections::HashMap::from([(
                rustfs_utils::http::AMZ_SERVER_SIDE_ENCRYPTION.to_string(),
                "AES256".to_string(),
            )]),
            parts: vec![
                PartInfo {
                    part_num: 1,
                    size: 8,
                    actual_size: 0,
                    ..Default::default()
                },
                PartInfo {
                    part_num: 2,
                    size: 8,
                    actual_size: -1,
                    ..Default::default()
                },
            ],
            ..Default::default()
        };

        let output = build_list_parts_output(input);
        let parts = output.parts.as_ref().expect("parts should be present");

        assert_eq!(parts[0].size, Some(0));
        assert_eq!(parts[1].size, Some(8));
    }

    #[test]
    fn test_list_parts_output_falls_back_to_stored_size_when_actual_size_unknown() {
        let input = ListPartsInfo {
            parts: vec![
                PartInfo {
                    part_num: 1,
                    size: 1_024,
                    // Uncompressed parts leave actual_size unset.
                    actual_size: 0,
                    ..Default::default()
                },
                PartInfo {
                    part_num: 2,
                    size: 1_024,
                    // Legacy/unknown sentinel must not leak a negative size to clients.
                    actual_size: -1,
                    ..Default::default()
                },
            ],
            ..Default::default()
        };

        let output = build_list_parts_output(input);
        let parts = output.parts.as_ref().expect("parts should be present");

        assert_eq!(parts.len(), 2);
        assert_eq!(parts[0].size, Some(1024));
        assert_eq!(parts[1].size, Some(1024));
    }

    #[test]
    fn test_list_parts_output_normalizes_legacy_storage_class_and_handles_overflow_markers() {
        let input = ListPartsInfo {
            storage_class: "STANDARD_IA".to_string(),
            part_number_marker: usize::MAX,
            next_part_number_marker: usize::MAX,
            max_parts: usize::MAX,
            parts: vec![PartInfo {
                part_num: usize::MAX,
                size: usize::MAX,
                ..Default::default()
            }],
            ..Default::default()
        };

        let output = build_list_parts_output(input);
        let parts = output.parts.as_ref().expect("parts should be present");

        assert_eq!(output.storage_class.as_ref().map(|value| value.as_str()), Some("STANDARD"));
        assert_eq!(output.part_number_marker, None);
        assert_eq!(output.next_part_number_marker, None);
        assert_eq!(output.max_parts, None);
        assert_eq!(parts.len(), 1);
        assert_eq!(parts[0].part_number, None);
        assert_eq!(parts[0].size, None);

        let output = build_list_parts_output(ListPartsInfo::default());
        assert_eq!(
            output.storage_class.as_ref().map(|value| value.as_str()),
            Some("STANDARD"),
            "legacy uploads without a stored class must report their effective STANDARD layout"
        );
    }

    #[test]
    fn test_list_multipart_uploads_output_maps_uploads_and_prefixes() {
        let result = ListMultipartsInfo {
            delimiter: Some("/".to_string()),
            key_marker: Some("key-marker".to_string()),
            upload_id_marker: Some("upload-id-marker".to_string()),
            next_key_marker: Some("next-key-marker".to_string()),
            next_upload_id_marker: Some("next-upload-id-marker".to_string()),
            max_uploads: 1000,
            is_truncated: true,
            uploads: vec![MultipartInfo {
                object: "obj-a".to_string(),
                upload_id: "upload-a".to_string(),
                initiated: Some(OffsetDateTime::UNIX_EPOCH),
                ..Default::default()
            }],
            common_prefixes: vec!["prefix-a/".to_string(), "prefix-b/".to_string()],
            ..Default::default()
        };

        let output = build_list_multipart_uploads_output("bucket-a".to_string(), "root/".to_string(), result);

        let uploads = output.uploads.as_ref().expect("uploads should be present");
        let common_prefixes = output.common_prefixes.as_ref().expect("common prefixes should be present");

        assert_eq!(output.bucket.as_deref(), Some("bucket-a"));
        assert_eq!(output.prefix.as_deref(), Some("root/"));
        assert_eq!(output.delimiter.as_deref(), Some("/"));
        assert_eq!(output.key_marker.as_deref(), Some("key-marker"));
        assert_eq!(output.upload_id_marker.as_deref(), Some("upload-id-marker"));
        assert_eq!(output.next_key_marker.as_deref(), Some("next-key-marker"));
        assert_eq!(output.next_upload_id_marker.as_deref(), Some("next-upload-id-marker"));
        assert_eq!(output.max_uploads, Some(1000));
        assert_eq!(output.is_truncated, Some(true));

        assert_eq!(uploads.len(), 1);
        assert_eq!(uploads[0].key.as_deref(), Some("obj-a"));
        assert_eq!(uploads[0].upload_id.as_deref(), Some("upload-a"));
        assert_eq!(uploads[0].initiated, Some(Timestamp::from(OffsetDateTime::UNIX_EPOCH)));
        assert_eq!(uploads[0].owner, Some(rustfs_owner()));
        assert_eq!(uploads[0].initiator, Some(rustfs_initiator()));

        assert_eq!(common_prefixes.len(), 2);
        assert_eq!(common_prefixes[0].prefix.as_deref(), Some("prefix-a/"));
        assert_eq!(common_prefixes[1].prefix.as_deref(), Some("prefix-b/"));
    }

    #[test]
    fn test_parse_list_parts_params_defaults_and_valid_values() {
        let parsed = parse_list_parts_params(Some(5), Some(100)).expect("expected valid params");
        assert_eq!(parsed.part_number_marker, Some(5));
        assert_eq!(parsed.max_parts, 100);

        let parsed = parse_list_parts_params(None, None).expect("expected default params");
        assert_eq!(parsed.part_number_marker, None);
        assert_eq!(parsed.max_parts, 1000);
    }

    #[test]
    fn test_parse_list_parts_params_rejects_invalid_max_parts() {
        let err = parse_list_parts_params(None, Some(0)).expect_err("expected invalid max_parts");
        assert_eq!(*err.code(), S3ErrorCode::InvalidArgument);

        let err = parse_list_parts_params(None, Some(1001)).expect_err("expected invalid max_parts");
        assert_eq!(*err.code(), S3ErrorCode::InvalidArgument);
    }

    #[test]
    fn test_parse_list_parts_params_rejects_negative_part_number_marker() {
        let err = parse_list_parts_params(Some(-1), None).expect_err("expected invalid part_number_marker");
        assert_eq!(*err.code(), S3ErrorCode::InvalidArgument);
        assert_eq!(err.message(), Some("part-number-marker must be non-negative"));
    }

    #[test]
    fn test_parse_upload_part_number_accepts_s3_range() {
        assert_eq!(parse_upload_part_number(1).expect("part 1 should be valid"), 1);
        assert_eq!(parse_upload_part_number(10000).expect("part 10000 should be valid"), 10000);
    }

    #[test]
    fn test_parse_upload_part_number_rejects_out_of_s3_range() {
        for part_number in [-1, 0, 10001] {
            let err = parse_upload_part_number(part_number).expect_err("expected invalid part number");
            assert_eq!(*err.code(), S3ErrorCode::InvalidArgument);
            assert_eq!(err.message(), Some("partNumber must be between 1 and 10000"));
        }
    }

    #[test]
    fn test_parse_list_multipart_uploads_params_defaults_and_valid_values() {
        let parsed =
            parse_list_multipart_uploads_params(Some("prefix/".to_string()), Some("prefix/key-marker".to_string()), Some(100))
                .expect("expected valid params");
        assert_eq!(parsed.prefix, "prefix/");
        assert_eq!(parsed.key_marker.as_deref(), Some("prefix/key-marker"));
        assert_eq!(parsed.max_uploads, 100);

        let parsed = parse_list_multipart_uploads_params(None, None, None).expect("expected default params");
        assert_eq!(parsed.prefix, "");
        assert_eq!(parsed.key_marker, None);
        assert_eq!(parsed.max_uploads, MAX_MULTIPART_UPLOADS_LIST as usize);
    }

    #[test]
    fn test_parse_list_multipart_uploads_params_rejects_invalid_key_marker() {
        let err = parse_list_multipart_uploads_params(Some("prefix/".to_string()), Some("other/key-marker".to_string()), None)
            .expect_err("expected invalid key marker");

        assert_eq!(*err.code(), S3ErrorCode::InvalidArgument);
        assert_eq!(err.message(), Some("Invalid key marker"));
    }

    #[test]
    fn test_parse_list_multipart_uploads_params_rejects_invalid_max_uploads() {
        let err = parse_list_multipart_uploads_params(Some("prefix/".to_string()), None, Some(-1))
            .expect_err("expected invalid max_uploads");
        assert_eq!(*err.code(), S3ErrorCode::InvalidArgument);

        let err = parse_list_multipart_uploads_params(Some("prefix/".to_string()), None, Some(0))
            .expect_err("expected invalid max_uploads");
        assert_eq!(*err.code(), S3ErrorCode::InvalidArgument);

        let err = parse_list_multipart_uploads_params(Some("prefix/".to_string()), None, Some(MAX_MULTIPART_UPLOADS_LIST + 1))
            .expect_err("expected invalid max_uploads");
        assert_eq!(*err.code(), S3ErrorCode::InvalidArgument);
    }
}
