// 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 std::io::{Error, ErrorKind};

use http::HeaderMap;

const X_AMZ_VERSION_ID: &str = "x-amz-version-id";
const X_OSS_VERSION_ID: &str = "x-oss-version-id";
const X_COS_VERSION_ID: &str = "x-cos-version-id";
const X_OBS_VERSION_ID: &str = "x-obs-version-id";
const MAX_REMOTE_VERSION_ID_LEN: usize = 1024;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[allow(dead_code, reason = "bucket versioning states kept as a complete vocabulary (backlog#1823)")]
pub enum BucketVersioningState {
    Unknown,
    Disabled,
    Suspended,
    Enabled,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RemoteVersion {
    Unknown,
    Disabled,
    SuspendedNull,
    Exact(String),
}

impl RemoteVersion {
    pub fn exact_id(&self) -> Option<&str> {
        match self {
            Self::SuspendedNull => Some("null"),
            Self::Exact(version_id) => Some(version_id),
            Self::Unknown | Self::Disabled => None,
        }
    }

    #[allow(dead_code, reason = "MinIO-parity accessor with no caller in this port (backlog#1823)")]
    pub fn exact_request_id(&self) -> Result<Option<&str>, Error> {
        match self {
            Self::Unknown => Err(Error::new(
                ErrorKind::InvalidData,
                "remote object version is unknown; exact version routing is unsafe",
            )),
            Self::Disabled => Ok(None),
            Self::SuspendedNull => Ok(Some("null")),
            Self::Exact(version_id) => Ok(Some(version_id)),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConditionalCreateCapability {
    Unsupported,
    IfNoneMatchStar,
    GenerationMatchZero,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ProviderVersionCapabilities {
    raw_version_header: Option<&'static str>,
    pub bucket_versioning_state: bool,
    pub list_object_versions: bool,
    pub conditional_create: ConditionalCreateCapability,
    pub exact_get_delete: bool,
}

impl ProviderVersionCapabilities {
    pub fn for_tier_type(tier_type: &str) -> Self {
        if tier_type.eq_ignore_ascii_case("s3")
            || tier_type.eq_ignore_ascii_case("rustfs")
            || tier_type.eq_ignore_ascii_case("minio")
            || tier_type.eq_ignore_ascii_case("r2")
            || tier_type.eq_ignore_ascii_case("wasabi")
        {
            let list_object_versions = tier_type.eq_ignore_ascii_case("s3")
                || tier_type.eq_ignore_ascii_case("rustfs")
                || tier_type.eq_ignore_ascii_case("minio")
                || tier_type.eq_ignore_ascii_case("r2");
            Self {
                raw_version_header: Some(X_AMZ_VERSION_ID),
                bucket_versioning_state: list_object_versions,
                list_object_versions,
                conditional_create: if tier_type.eq_ignore_ascii_case("s3") || tier_type.eq_ignore_ascii_case("r2") {
                    ConditionalCreateCapability::IfNoneMatchStar
                } else {
                    ConditionalCreateCapability::Unsupported
                },
                exact_get_delete: true,
            }
        } else if tier_type.eq_ignore_ascii_case("aliyun") {
            Self {
                raw_version_header: Some(X_OSS_VERSION_ID),
                bucket_versioning_state: false,
                list_object_versions: false,
                conditional_create: ConditionalCreateCapability::Unsupported,
                exact_get_delete: true,
            }
        } else if tier_type.eq_ignore_ascii_case("tencent") {
            Self {
                raw_version_header: Some(X_COS_VERSION_ID),
                bucket_versioning_state: false,
                list_object_versions: false,
                conditional_create: ConditionalCreateCapability::Unsupported,
                exact_get_delete: true,
            }
        } else if tier_type.eq_ignore_ascii_case("huaweicloud") {
            Self {
                raw_version_header: Some(X_OBS_VERSION_ID),
                bucket_versioning_state: false,
                list_object_versions: false,
                conditional_create: ConditionalCreateCapability::Unsupported,
                exact_get_delete: true,
            }
        } else if tier_type.eq_ignore_ascii_case("gcs") {
            Self {
                raw_version_header: None,
                bucket_versioning_state: false,
                list_object_versions: false,
                conditional_create: ConditionalCreateCapability::GenerationMatchZero,
                exact_get_delete: false,
            }
        } else {
            Self {
                raw_version_header: None,
                bucket_versioning_state: false,
                list_object_versions: false,
                conditional_create: ConditionalCreateCapability::Unsupported,
                exact_get_delete: false,
            }
        }
    }

    pub fn raw_version_id(self, headers: &HeaderMap) -> Result<Option<&str>, Error> {
        let Some(header_name) = self.raw_version_header else {
            return Ok(None);
        };
        let Some(value) = headers.get(header_name) else {
            return Ok(None);
        };
        let value = value
            .to_str()
            .map_err(|_| Error::new(ErrorKind::InvalidData, "remote object version id is not valid ASCII"))?;
        validate_remote_version_id(value)?;
        Ok(Some(value))
    }

    pub fn remote_version(self, headers: &HeaderMap, versioning: BucketVersioningState) -> Result<RemoteVersion, Error> {
        let Some(value) = self.raw_version_id(headers)? else {
            return Ok(match versioning {
                BucketVersioningState::Disabled => RemoteVersion::Disabled,
                BucketVersioningState::Unknown | BucketVersioningState::Suspended | BucketVersioningState::Enabled => {
                    RemoteVersion::Unknown
                }
            });
        };
        if value == "null" {
            return Ok(RemoteVersion::SuspendedNull);
        }
        Ok(RemoteVersion::Exact(value.to_string()))
    }
}

pub fn validate_remote_version_id(version_id: &str) -> Result<(), Error> {
    if version_id.is_empty() {
        return Err(Error::new(
            ErrorKind::InvalidData,
            "remote tier returned an empty object version id header",
        ));
    }
    if version_id.len() > MAX_REMOTE_VERSION_ID_LEN {
        return Err(Error::new(
            ErrorKind::InvalidData,
            "remote tier returned an oversized object version id header",
        ));
    }
    if version_id.chars().any(char::is_control) {
        return Err(Error::new(
            ErrorKind::InvalidData,
            "remote tier returned an object version id containing control characters",
        ));
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{BucketVersioningState, ConditionalCreateCapability, ProviderVersionCapabilities, RemoteVersion};
    use http::{HeaderMap, HeaderValue};

    #[test]
    fn provider_version_header_matrix_preserves_opaque_versions() {
        for (tier_type, header_name) in [
            ("s3", "x-amz-version-id"),
            ("S3", "x-amz-version-id"),
            ("rustfs", "x-amz-version-id"),
            ("RustFS", "x-amz-version-id"),
            ("minio", "x-amz-version-id"),
            ("MinIO", "x-amz-version-id"),
            ("r2", "x-amz-version-id"),
            ("R2", "x-amz-version-id"),
            ("wasabi", "x-amz-version-id"),
            ("Wasabi", "x-amz-version-id"),
            ("aliyun", "x-oss-version-id"),
            ("Aliyun", "x-oss-version-id"),
            ("tencent", "x-cos-version-id"),
            ("Tencent", "x-cos-version-id"),
            ("huaweicloud", "x-obs-version-id"),
            ("Huaweicloud", "x-obs-version-id"),
        ] {
            let mut headers = HeaderMap::new();
            headers.insert(header_name, HeaderValue::from_static("opaque.version_01"));
            let capabilities = ProviderVersionCapabilities::for_tier_type(tier_type);

            assert_eq!(capabilities.raw_version_id(&headers).expect("raw version"), Some("opaque.version_01"));
            assert_eq!(
                capabilities
                    .remote_version(&headers, BucketVersioningState::Enabled)
                    .expect("remote version"),
                RemoteVersion::Exact("opaque.version_01".to_string())
            );
        }
    }

    #[test]
    fn provider_version_header_matrix_does_not_cross_read_sibling_headers() {
        let mut headers = HeaderMap::new();
        headers.insert("x-amz-version-id", HeaderValue::from_static("aws-version"));
        headers.insert("x-cos-version-id", HeaderValue::from_static("cos-version"));

        assert_eq!(
            ProviderVersionCapabilities::for_tier_type("s3")
                .raw_version_id(&headers)
                .expect("aws raw version"),
            Some("aws-version")
        );
        assert_eq!(
            ProviderVersionCapabilities::for_tier_type("tencent")
                .raw_version_id(&headers)
                .expect("cos raw version"),
            Some("cos-version")
        );
    }

    #[test]
    fn provider_version_missing_header_is_unknown_until_bucket_state_is_known() {
        let headers = HeaderMap::new();
        let capabilities = ProviderVersionCapabilities::for_tier_type("aliyun");

        assert_eq!(
            capabilities
                .remote_version(&headers, BucketVersioningState::Unknown)
                .expect("unknown versioning"),
            RemoteVersion::Unknown
        );
        assert_eq!(
            capabilities
                .remote_version(&headers, BucketVersioningState::Disabled)
                .expect("disabled versioning"),
            RemoteVersion::Disabled
        );
    }

    #[test]
    fn provider_capability_matrix_is_conservative_and_provider_specific() {
        for (tier_type, state, list, conditional_create, exact_get_delete) in [
            ("s3", true, true, ConditionalCreateCapability::IfNoneMatchStar, true),
            ("rustfs", true, true, ConditionalCreateCapability::Unsupported, true),
            ("minio", true, true, ConditionalCreateCapability::Unsupported, true),
            ("r2", true, true, ConditionalCreateCapability::IfNoneMatchStar, true),
            ("wasabi", false, false, ConditionalCreateCapability::Unsupported, true),
            ("aliyun", false, false, ConditionalCreateCapability::Unsupported, true),
            ("tencent", false, false, ConditionalCreateCapability::Unsupported, true),
            ("huaweicloud", false, false, ConditionalCreateCapability::Unsupported, true),
            ("gcs", false, false, ConditionalCreateCapability::GenerationMatchZero, false),
            ("azure", false, false, ConditionalCreateCapability::Unsupported, false),
            ("unsupported", false, false, ConditionalCreateCapability::Unsupported, false),
        ] {
            let capabilities = ProviderVersionCapabilities::for_tier_type(tier_type);
            assert_eq!(capabilities.bucket_versioning_state, state, "{tier_type} versioning state");
            assert_eq!(capabilities.list_object_versions, list, "{tier_type} version listing");
            assert_eq!(capabilities.conditional_create, conditional_create, "{tier_type} conditional create");
            assert_eq!(capabilities.exact_get_delete, exact_get_delete, "{tier_type} exact routing");
        }
    }

    #[test]
    fn remote_version_states_preserve_unknown_disabled_suspended_and_exact() {
        let capabilities = ProviderVersionCapabilities::for_tier_type("s3");
        let empty = HeaderMap::new();
        let mut null = HeaderMap::new();
        null.insert("x-amz-version-id", HeaderValue::from_static("null"));
        let mut exact = HeaderMap::new();
        exact.insert("x-amz-version-id", HeaderValue::from_static("opaque.generation-7"));

        for (headers, state, expected) in [
            (&empty, BucketVersioningState::Unknown, RemoteVersion::Unknown),
            (&empty, BucketVersioningState::Disabled, RemoteVersion::Disabled),
            (&empty, BucketVersioningState::Suspended, RemoteVersion::Unknown),
            (&empty, BucketVersioningState::Enabled, RemoteVersion::Unknown),
            (&null, BucketVersioningState::Suspended, RemoteVersion::SuspendedNull),
            (
                &exact,
                BucketVersioningState::Enabled,
                RemoteVersion::Exact("opaque.generation-7".to_string()),
            ),
        ] {
            assert_eq!(
                capabilities
                    .remote_version(headers, state)
                    .expect("version state should normalize"),
                expected
            );
        }
    }

    #[test]
    fn exact_request_routing_fails_closed_for_unknown_versions() {
        for (version, expected) in [
            (RemoteVersion::Disabled, None),
            (RemoteVersion::SuspendedNull, Some("null")),
            (RemoteVersion::Exact("opaque-v1".to_string()), Some("opaque-v1")),
        ] {
            assert_eq!(version.exact_request_id().expect("known version state"), expected);
        }
        assert!(RemoteVersion::Unknown.exact_request_id().is_err());
    }

    #[test]
    fn provider_version_rejects_empty_or_oversized_headers() {
        let oversized = "v".repeat(1025);
        for bad in ["", oversized.as_str()] {
            let mut headers = HeaderMap::new();
            headers.insert("x-oss-version-id", HeaderValue::from_str(bad).expect("test header value"));

            assert!(
                ProviderVersionCapabilities::for_tier_type("aliyun")
                    .raw_version_id(&headers)
                    .is_err()
            );
        }
    }
}
