// 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::{StorageError, add_object_lock_years, get_bucket_cors_config};
use crate::config::{RustFSBufferConfig, WorkloadProfile, is_buffer_profile_enabled};
use crate::error::ApiError;
use crate::server::cors;
use crate::storage::ecfs::ListObjectUnorderedQuery;
use crate::storage::storage_api::ecfs_extend_consumer::contract::bucket::{BucketOperations, BucketOptions};
use crate::storage::storage_api::ecfs_extend_consumer::contract::multipart::MAX_MULTIPART_PART_NUMBER;
use crate::storage::storage_api::ecstore_bucket::metadata_sys::{self, ObjectLockConfigState};
use http::header::{IF_MATCH, IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_UNMODIFIED_SINCE};
use http::{HeaderMap, HeaderValue, StatusCode};
use metrics::counter;
use rustfs_targets::EventName;
use rustfs_targets::arn::{TargetID, TargetIDError};
use rustfs_utils::http::{
    AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
    SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP, contains_key_str, insert_str, remove_str,
};
use s3s::dto::{
    Delimiter, LambdaFunctionConfiguration, NotificationConfigurationFilter, ObjectLockConfiguration, ObjectLockEnabled,
    ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockRetention, ObjectLockRetentionMode, QueueConfiguration,
    TopicConfiguration,
};
use s3s::{S3Error, S3ErrorCode, S3Response, S3Result};
use serde_urlencoded::from_bytes;
use std::collections::HashMap;
use std::ops::Add;
use std::sync::{Arc, OnceLock, RwLock};
use std::time::{Duration, Instant};
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
use time::{format_description::FormatItem, macros::format_description};
use tracing::{debug, warn};

use crate::storage::storage_api::ecfs_extend_consumer::StorageObjectInfo as ObjectInfo;
use crate::storage::storage_api::runtime_sources_consumer::runtime_sources;

const LOG_COMPONENT_STORAGE: &str = "storage";
const LOG_SUBSYSTEM_OBJECT_LOCK: &str = "object_lock";
const LOG_SUBSYSTEM_OBJECT_KEY: &str = "object_key";

pub const RFC1123: &[FormatItem<'_>] =
    format_description!("[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT");

fn format_object_lock_timestamp(timestamp: OffsetDateTime) -> String {
    timestamp.format(&Rfc3339).unwrap_or_default()
}

fn has_object_lock_retention_metadata(metadata: &HashMap<String, String>) -> bool {
    metadata.contains_key(AMZ_OBJECT_LOCK_MODE_LOWER) || metadata.contains_key(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER)
}

pub(crate) fn remove_object_lock_retention_metadata(metadata: &mut HashMap<String, String>) -> bool {
    let removed_mode = metadata.remove(AMZ_OBJECT_LOCK_MODE_LOWER).is_some();
    let removed_retain_until_date = metadata.remove(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER).is_some();
    let removed_timestamp = contains_key_str(metadata, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP);
    remove_str(metadata, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP);

    removed_mode || removed_retain_until_date || removed_timestamp
}

fn remove_object_lock_legal_hold_metadata(metadata: &mut HashMap<String, String>) -> bool {
    let removed_legal_hold = metadata.remove(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER).is_some();
    let removed_timestamp = contains_key_str(metadata, SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP);
    remove_str(metadata, SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP);

    removed_legal_hold || removed_timestamp
}

pub(crate) fn remove_object_lock_metadata_for_copy(metadata: &mut HashMap<String, String>) -> bool {
    let removed_retention = remove_object_lock_retention_metadata(metadata);
    let removed_legal_hold = remove_object_lock_legal_hold_metadata(metadata);

    removed_retention || removed_legal_hold
}

/// Apply bucket default Object Lock retention to object metadata if no explicit retention is set.
pub(crate) fn apply_lock_retention(object_lock_config: Option<&ObjectLockConfiguration>, metadata: &mut HashMap<String, String>) {
    if has_object_lock_retention_metadata(metadata) {
        return;
    }

    let Some(config) = object_lock_config else { return };

    if config.object_lock_enabled.as_ref().map(|e| e.as_str()) != Some(ObjectLockEnabled::ENABLED) {
        return;
    }

    let Some(default_retention) = config.rule.as_ref().and_then(|r| r.default_retention.as_ref()) else {
        return;
    };
    let Some(mode) = default_retention.mode.as_ref() else { return };

    let now = OffsetDateTime::now_utc();
    let retain_until = match (default_retention.days, default_retention.years) {
        (Some(days), _) => now.saturating_add(time::Duration::days(days as i64)),
        (None, Some(years)) => add_object_lock_years(now, years),
        _ => return,
    };

    if let Ok(date_str) = retain_until.format(&Rfc3339) {
        metadata.insert(AMZ_OBJECT_LOCK_MODE_LOWER.to_string(), mode.as_str().to_string());
        metadata.insert(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER.to_string(), date_str);
        insert_str(metadata, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP, format_object_lock_timestamp(now));
    }
}

pub(crate) fn apply_default_lock_retention_metadata(
    object_lock_configuration: Option<&ObjectLockConfiguration>,
    metadata: &mut HashMap<String, String>,
) -> bool {
    if has_object_lock_retention_metadata(metadata) {
        return false;
    }

    let mut default_retention_metadata = HashMap::new();
    apply_lock_retention(object_lock_configuration, &mut default_retention_metadata);
    if default_retention_metadata.is_empty() {
        return false;
    }

    metadata.extend(default_retention_metadata);
    true
}

pub(crate) async fn load_bucket_object_lock_config_state(bucket: &str) -> S3Result<ObjectLockConfigState> {
    map_bucket_object_lock_config_state(bucket, metadata_sys::get_object_lock_config_state(bucket).await)
}

pub(crate) fn map_bucket_object_lock_config_state(
    bucket: &str,
    result: Result<ObjectLockConfigState, StorageError>,
) -> S3Result<ObjectLockConfigState> {
    match result {
        Ok(ObjectLockConfigState::Fabricated) => {
            warn!(
                component = LOG_COMPONENT_STORAGE,
                subsystem = LOG_SUBSYSTEM_OBJECT_LOCK,
                event = "object_lock_config_not_authoritative",
                bucket = %bucket,
                "Bucket Object Lock configuration is not authoritative"
            );
            Err(S3Error::with_message(
                S3ErrorCode::InternalError,
                "Failed to load Object Lock configuration".to_string(),
            ))
        }
        Ok(state) => Ok(state),
        Err(err) => {
            warn!(
                component = LOG_COMPONENT_STORAGE,
                subsystem = LOG_SUBSYSTEM_OBJECT_LOCK,
                event = "object_lock_config_load_failed",
                bucket = %bucket,
                error = ?err,
                "Failed to load bucket object lock configuration"
            );
            Err(S3Error::with_message(
                S3ErrorCode::InternalError,
                "Failed to load Object Lock configuration".to_string(),
            ))
        }
    }
}

pub(crate) fn apply_bucket_default_lock_retention(
    bucket: &str,
    state: &ObjectLockConfigState,
    metadata: &mut HashMap<String, String>,
    has_explicit_retention: bool,
) -> S3Result<()> {
    let object_lock_configuration = match state {
        ObjectLockConfigState::Configured { config, .. } => Some(config),
        ObjectLockConfigState::ConfirmedAbsent => None,
        ObjectLockConfigState::Fabricated => {
            warn!(
                component = LOG_COMPONENT_STORAGE,
                subsystem = LOG_SUBSYSTEM_OBJECT_LOCK,
                event = "object_lock_config_not_authoritative",
                bucket = %bucket,
                "Bucket Object Lock configuration is not authoritative"
            );
            return Err(S3Error::with_message(
                S3ErrorCode::InternalError,
                "Failed to load Object Lock configuration".to_string(),
            ));
        }
    };

    if has_explicit_retention || has_object_lock_retention_metadata(metadata) {
        return Ok(());
    }

    apply_default_lock_retention_metadata(object_lock_configuration, metadata);
    Ok(())
}

/// Calculate adaptive buffer size with workload profile support.
///
/// This enhanced version supports different workload profiles for optimal performance
/// across various use cases (AI/ML, web workloads, secure storage, etc.).
///
/// # Arguments
/// * `file_size` - The size of the file in bytes, or -1 if unknown
/// * `profile` - Optional workload profile. If None, uses auto-detection or GeneralPurpose
///
/// # Returns
/// Optimal buffer size in bytes based on the workload profile and file size
///
/// # Examples
/// ```ignore
/// // Use general purpose profile (default)
/// let buffer_size = get_adaptive_buffer_size_with_profile(1024 * 1024, None);
///
/// // Use AI training profile for large model files
/// let buffer_size = get_adaptive_buffer_size_with_profile(
///     500 * 1024 * 1024,
///     Some(WorkloadProfile::AiTraining)
/// );
///
/// // Use secure storage profile for compliance scenarios
/// let buffer_size = get_adaptive_buffer_size_with_profile(
///     10 * 1024 * 1024,
///     Some(WorkloadProfile::SecureStorage)
/// );
///
/// // Use custom profile for specialized requirements
/// let custom_profile = WorkloadProfile::custom(
///     32 * 1024,      // min_size: 32KB
///     2 * 1024 * 1024, // max_size: 2MB
///     256 * 1024,     // default_unknown: 256KB
///     vec![
///         (1024 * 1024, 64 * 1024),           // < 1MB: 64KB
///         (10 * 1024 * 1024, 128 * 1024),     // 1MB-10MB: 128KB
///         (i64::MAX, 512 * 1024),             // >= 10MB: 512KB
///     ],
/// );
/// let buffer_size = get_adaptive_buffer_size_with_profile(
///     5 * 1024 * 1024,
///     Some(custom_profile)
/// );
/// ```
///
#[allow(
    dead_code,
    reason = "exercised by ecfs_test; the lib target cannot see test-only consumers (backlog#1823)"
)]
pub(crate) fn get_adaptive_buffer_size_with_profile(file_size: i64, profile: Option<WorkloadProfile>) -> usize {
    let config = match profile {
        Some(p) => RustFSBufferConfig::new(p),
        None => {
            // Auto-detect OS environment or use general purpose
            RustFSBufferConfig::with_auto_detect()
        }
    };

    config.get_buffer_size(file_size)
}

/// Get adaptive buffer size using global workload profile configuration.
///
/// This is the primary buffer sizing function that uses the workload profile
/// system configured at startup to provide optimal buffer sizes for different scenarios.
///
/// The function automatically selects buffer sizes based on:
/// - Configured workload profile (default: GeneralPurpose)
/// - File size characteristics
/// - Optional performance metrics collection
///
/// # Arguments
/// * `file_size` - The size of the file in bytes, or -1 if unknown
///
/// # Returns
/// Optimal buffer size in bytes based on the configured workload profile
///
/// # Performance Metrics
/// When compiled with the `metrics` feature flag, this function tracks:
/// - Buffer size distribution
/// - Selection frequency
/// - Buffer-to-file size ratios
///
/// # Examples
/// ```ignore
/// // Uses configured profile (default: GeneralPurpose)
/// let buffer_size = get_buffer_size_opt_in(file_size);
/// ```
pub(crate) fn get_buffer_size_opt_in(file_size: i64) -> usize {
    let buffer_size = if is_buffer_profile_enabled() {
        // Use the AppContext-owned profile when available, with global fallback during migration.
        let config = runtime_sources::current_buffer_config();
        config.get_buffer_size(file_size)
    } else {
        // Opt-out mode: Use GeneralPurpose profile for consistent behavior
        let config = RustFSBufferConfig::new(WorkloadProfile::GeneralPurpose);
        config.get_buffer_size(file_size)
    };

    // Optional performance metrics collection for monitoring and optimization
    {
        use metrics::histogram;
        histogram!("rustfs_buffer_size_bytes").record(buffer_size as f64);
        counter!("rustfs_buffer_size_selections_total").increment(1);

        if file_size > 0 {
            let ratio = buffer_size as f64 / file_size as f64;
            histogram!("rustfs_buffer_to_file_ratio").record(ratio);
        }
    }

    buffer_size
}

/// Validate object key for control characters and log special characters
///
/// This function:
/// 1. Rejects keys containing control characters (null bytes, newlines, carriage returns)
/// 2. Logs debug information for keys containing spaces, plus signs, or percent signs
///
/// The s3s library handles URL decoding, so keys are already decoded when they reach this function.
/// This validation ensures that invalid characters that could cause issues are rejected early.
pub(crate) fn validate_object_key(key: &str, operation: &str) -> S3Result<()> {
    // Validate object key doesn't contain control characters
    if key.contains(['\0', '\n', '\r']) {
        return Err(S3Error::with_message(
            S3ErrorCode::InvalidArgument,
            format!("Object key contains invalid control characters: {key:?}"),
        ));
    }

    // Log debug info for keys with special characters to help diagnose encoding issues
    if key.contains([' ', '+', '%']) {
        debug!(
            component = LOG_COMPONENT_STORAGE,
            subsystem = LOG_SUBSYSTEM_OBJECT_KEY,
            event = "object_key_special_characters",
            operation,
            key = ?key,
            "Object key contains special characters"
        );
    }

    Ok(())
}

/// Validate that 'allow-unordered' parameter is not used with a delimiter
///
/// This function:
/// 1. Checks if a delimiter is specified in the ListObjects request
/// 2. Parses the query string to check for the 'allow-unordered' parameter
/// 3. Rejects the request if both 'delimiter' and 'allow-unordered=true' are present
///
/// According to S3 compatibility requirements, unordered listing cannot be combined with
/// hierarchical directory traversal (delimited listing). This validation ensures
/// conflicting parameters are caught before processing the request.
pub(crate) fn validate_list_object_unordered_with_delimiter(
    delimiter: Option<&Delimiter>,
    query_string: Option<&str>,
) -> S3Result<()> {
    if delimiter.is_none() {
        return Ok(());
    }

    let Some(query) = query_string else {
        return Ok(());
    };

    if let Ok(params) = from_bytes::<ListObjectUnorderedQuery>(query.as_bytes())
        && params.allow_unordered.as_deref() == Some("true")
    {
        return Err(S3Error::with_message(
            S3ErrorCode::InvalidArgument,
            "The allow-unordered parameter cannot be used when delimiter is specified.".to_string(),
        ));
    }

    Ok(())
}

pub(crate) fn parse_object_lock_retention(retention: Option<ObjectLockRetention>) -> S3Result<HashMap<String, String>> {
    let mut eval_metadata = HashMap::new();

    if let Some(v) = retention {
        let mode = match v.mode {
            Some(mode) => match mode.as_str() {
                ObjectLockRetentionMode::COMPLIANCE | ObjectLockRetentionMode::GOVERNANCE => mode.as_str().to_string(),
                _ => {
                    return Err(S3Error::with_message(
                        S3ErrorCode::MalformedXML,
                        "The XML you provided was not well-formed or did not validate against our published schema".to_string(),
                    ));
                }
            },
            None => String::default(),
        };

        let now = OffsetDateTime::now_utc();
        // Validate retain_until_date is in the future (S3 requirement)
        // Only validate when both mode and date are provided (not clearing retention)
        let retain_until_date = if let Some(date) = v.retain_until_date {
            let retain_until = OffsetDateTime::from(date);
            // Only validate future date when mode is set (not clearing retention)
            if !mode.is_empty() && retain_until <= now {
                return Err(S3Error::with_message(
                    S3ErrorCode::InvalidArgument,
                    "The retain until date must be in the future".to_string(),
                ));
            }
            retain_until.format(&Rfc3339).map_err(|_| {
                S3Error::with_message(
                    S3ErrorCode::InvalidArgument,
                    "The retain until date is not a supported RFC3339 timestamp".to_string(),
                )
            })?
        } else {
            String::default()
        };

        // This is intentional behavior. Empty string represents "retention cleared" which is different from "retention never set". Consistent with minio
        eval_metadata.insert(AMZ_OBJECT_LOCK_MODE_LOWER.to_string(), mode);
        eval_metadata.insert(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER.to_string(), retain_until_date);
        insert_str(
            &mut eval_metadata,
            SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP,
            format_object_lock_timestamp(now),
        );
    }
    Ok(eval_metadata)
}

pub(crate) fn parse_object_lock_legal_hold(legal_hold: Option<ObjectLockLegalHold>) -> S3Result<HashMap<String, String>> {
    let mut eval_metadata = HashMap::new();
    if let Some(v) = legal_hold {
        let status = match v.status {
            Some(status) => match status.as_str() {
                ObjectLockLegalHoldStatus::OFF | ObjectLockLegalHoldStatus::ON => status.as_str().to_string(),
                _ => {
                    return Err(S3Error::with_message(
                        S3ErrorCode::MalformedXML,
                        "The XML you provided was not well-formed or did not validate against our published schema".to_string(),
                    ));
                }
            },
            None => String::default(),
        };
        let now = OffsetDateTime::now_utc();
        // This is intentional behavior. Empty string represents "status cleared" which is different from "status never set".
        eval_metadata.insert(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER.to_string(), status);
        insert_str(
            &mut eval_metadata,
            SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP,
            format_object_lock_timestamp(now),
        );
    }
    Ok(eval_metadata)
}

pub(crate) async fn validate_bucket_object_lock_enabled(bucket: &str) -> S3Result<()> {
    let state = load_bucket_object_lock_config_state(bucket).await?;
    validate_bucket_object_lock_enabled_state(bucket, &state)
}

pub(crate) fn validate_bucket_object_lock_enabled_state(bucket: &str, state: &ObjectLockConfigState) -> S3Result<()> {
    match state {
        ObjectLockConfigState::Configured { config, .. }
            if config.object_lock_enabled == Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)) =>
        {
            Ok(())
        }
        ObjectLockConfigState::Configured { .. } => Err(S3Error::with_message(
            S3ErrorCode::InvalidRequest,
            "Object Lock is not enabled for this bucket".to_string(),
        )),
        ObjectLockConfigState::ConfirmedAbsent => Err(S3Error::with_message(
            S3ErrorCode::InvalidRequest,
            "Bucket is missing ObjectLockConfiguration".to_string(),
        )),
        ObjectLockConfigState::Fabricated => {
            warn!(
                component = LOG_COMPONENT_STORAGE,
                subsystem = LOG_SUBSYSTEM_OBJECT_LOCK,
                event = "object_lock_config_not_authoritative",
                bucket = %bucket,
                "Bucket Object Lock configuration is not authoritative"
            );
            Err(S3Error::with_message(
                S3ErrorCode::InternalError,
                "Failed to get bucket ObjectLockConfiguration".to_string(),
            ))
        }
    }
}

/// Validates HTTP conditional request headers for a single object according to
/// RFC 7232 (HTTP/1.1 conditional requests) and S3 API semantics.
///
/// This function evaluates the following headers, if present, in the standard
/// conditional request order:
/// - `If-Match`
/// - `If-None-Match`
/// - `If-Modified-Since`
/// - `If-Unmodified-Since`
///
/// The `headers` parameter provides the incoming HTTP request headers, and
/// `info` supplies the object's current metadata (ETag and modification time)
/// used to evaluate those preconditions.
///
/// The function returns `Ok(())` if either no relevant conditional headers are
/// set or all specified preconditions are satisfied. If any precondition fails,
/// it returns an `Err(S3Error)` with an appropriate S3 error code (for example,
/// `PreconditionFailed` for failed `If-Match` / `If-Unmodified-Since`, or
/// `NotModified` for `If-None-Match` / `If-Modified-Since` when the resource
/// has not changed), allowing the caller to translate this into the correct
/// HTTP response.
pub(crate) fn check_preconditions(headers: &HeaderMap, info: &ObjectInfo) -> S3Result<()> {
    let mod_time = info.mod_time;
    let etag = info.etag.as_deref();
    let if_match = non_empty_header_value(headers, IF_MATCH);
    let if_none_match = non_empty_header_value(headers, IF_NONE_MATCH);
    let if_modified_since = non_empty_header_value(headers, IF_MODIFIED_SINCE);
    let if_unmodified_since = non_empty_header_value(headers, IF_UNMODIFIED_SINCE);

    if mod_time.is_none() && etag.is_none() {
        return Ok(());
    }

    // If-Match: requires ETag to exist
    if let Some(if_match_val) = if_match {
        match etag {
            Some(e) if is_etag_equal(e, if_match_val) => {}
            _ => return Err(S3Error::new(S3ErrorCode::PreconditionFailed)),
        }
    }

    // If-Unmodified-Since (only when If-Match is absent)
    if if_match.is_none()
        && let Some(t) = mod_time
        && let Some(if_unmodified_since) = if_unmodified_since
        && let Ok(given_time) = time::PrimitiveDateTime::parse(if_unmodified_since, &RFC1123).map(|dt| dt.assume_utc())
        && t > given_time.add(time::Duration::seconds(1))
    {
        return Err(S3Error::new(S3ErrorCode::PreconditionFailed));
    }

    // If-None-Match
    if let Some(if_none_match) = if_none_match
        && let Some(e) = etag
        && is_etag_equal(e, if_none_match)
    {
        let mut error_headers = HeaderMap::new();
        if let Ok(etag_header) = parse_etag(e) {
            error_headers.insert("etag", etag_header);
        }
        if let Some(t) = mod_time
            && let Ok(last_modified_str) = t.format(&RFC1123)
            && let Ok(last_modified_header) = HeaderValue::from_str(&last_modified_str)
        {
            error_headers.insert("last-modified", last_modified_header);
        }

        let mut s3_error = S3Error::new(S3ErrorCode::NotModified);
        s3_error.set_message("Not Modified".to_string());
        s3_error.set_status_code(StatusCode::NOT_MODIFIED);
        s3_error.set_headers(error_headers);
        return Err(s3_error);
    }

    // If-Modified-Since (only when If-None-Match is absent — semantics per RFC 7232; dates use RFC 1123 format)
    if if_none_match.is_none()
        && let Some(t) = mod_time
        && let Some(if_modified_since) = if_modified_since
        && let Ok(given_time) = time::PrimitiveDateTime::parse(if_modified_since, &RFC1123).map(|dt| dt.assume_utc())
        && t < given_time.add(time::Duration::seconds(1))
    {
        let mut error_headers = HeaderMap::new();
        if let Some(e) = etag
            && let Ok(etag_header) = parse_etag(e)
        {
            error_headers.insert("etag", etag_header);
        }
        if let Ok(last_modified_str) = t.format(&RFC1123)
            && let Ok(last_modified_header) = HeaderValue::from_str(&last_modified_str)
        {
            error_headers.insert("last-modified", last_modified_header);
        }

        let mut s3_error = S3Error::new(S3ErrorCode::NotModified);
        s3_error.set_message("Not Modified".to_string());
        s3_error.set_status_code(StatusCode::NOT_MODIFIED);
        s3_error.set_headers(error_headers);

        return Err(s3_error);
    }

    Ok(())
}

fn non_empty_header_value(headers: &HeaderMap, name: http::header::HeaderName) -> Option<&str> {
    headers
        .get(name)
        .and_then(|v| v.to_str().ok())
        .map(str::trim)
        .filter(|v| !v.is_empty())
}

/// Compares an object ETag with an ETag value from an HTTP header.
///
/// This helper implements HTTP ETag comparison semantics for headers such as
/// `If-Match` and `If-None-Match`:
/// - Supports the wildcard `*`, which matches any `object_etag`.
/// - Supports comma-separated ETag lists (e.g., `"etag1", "etag2"`), returning
///   `true` if any entry matches `object_etag`.
/// - Automatically trims surrounding whitespace and double quotes from both the
///   header entries and `object_etag` before comparison.
///
/// # Parameters
/// - `object_etag`: The ETag associated with the stored object.
/// - `header_etag`: The raw ETag header value received in the request, which may
///   be a wildcard, a single ETag, or a comma-separated list of ETags.
///
/// # Returns
/// `true` if the header value matches the object ETag according to the above
/// HTTP ETag comparison rules, otherwise `false`.
pub(crate) fn is_etag_equal(object_etag: &str, header_etag: &str) -> bool {
    let header_etag = header_etag.trim();
    if header_etag == "*" {
        return true;
    }
    header_etag
        .split(',')
        .map(|s| s.trim().trim_matches('"'))
        .any(|e| e == object_etag.trim_matches('"'))
}

/// Converts an object ETag string into an HTTP `HeaderValue` for use in response headers.
///
/// This function normalizes ETag values by ensuring they are wrapped in double quotes,
/// which is the standard HTTP ETag format according to RFC 7232. If the input ETag
/// already contains quotes, they are removed before adding new quotes to avoid duplication.
///
/// # Parameters
/// - `object_etag`: The ETag string, which may or may not already be wrapped in quotes.
///   Must not be empty after removing quotes.
///
/// # Returns
/// - `Ok(HeaderValue)`: The normalized ETag value wrapped in double quotes, ready for use
///   in HTTP response headers.
/// - `Err(S3Error)`: Returns `InvalidArgument` if the ETag value is empty (after removing quotes)
///   or contains characters that cannot be represented in an HTTP header value.
///
/// # Note
/// When this function returns an error, callers should log the error but continue with
/// the operation (e.g., return NotModified without the ETag header) rather than propagating
/// the InvalidArgument error, as the original operation semantics should be preserved.
pub fn parse_etag(object_etag: &str) -> Result<HeaderValue, S3Error> {
    let etag_trimmed = object_etag.trim_matches('"');

    if etag_trimmed.trim().is_empty() {
        return Err(S3Error::with_message(S3ErrorCode::InvalidArgument, "ETag cannot be empty".to_string()));
    }

    let etag_quoted = format!("\"{}\"", etag_trimmed);

    HeaderValue::from_str(&etag_quoted).map_err(|e| {
        warn!(
            "Failed to convert ETag to HeaderValue (ETag: {}): {}. The ETag header will be omitted from the response.",
            etag_quoted, e
        );
        S3Error::with_message(
            S3ErrorCode::InvalidArgument,
            format!("Invalid ETag header value (ETag: {}): {}", etag_quoted, e),
        )
    })
}

/// Auxiliary functions: extract prefixes and suffixes
pub(crate) fn extract_prefix_suffix(filter: Option<&NotificationConfigurationFilter>) -> (String, String) {
    if let Some(filter) = filter
        && let Some(filter_rules) = &filter.key
    {
        let mut prefix = String::new();
        let mut suffix = String::new();
        if let Some(rules) = &filter_rules.filter_rules {
            for rule in rules {
                if let (Some(name), Some(value)) = (rule.name.as_ref(), rule.value.as_ref()) {
                    if name.as_str().eq_ignore_ascii_case("prefix") {
                        prefix = value.clone();
                    } else if name.as_str().eq_ignore_ascii_case("suffix") {
                        suffix = value.clone();
                    }
                }
            }
        }
        return (prefix, suffix);
    }
    (String::new(), String::new())
}

/// Auxiliary functions: Handle configuration
pub(crate) fn process_queue_configurations<F>(
    event_rules: &mut Vec<(Vec<EventName>, String, String, Vec<TargetID>)>,
    configurations: Option<Vec<QueueConfiguration>>,
    target_id_parser: F,
) -> Result<(), TargetIDError>
where
    F: Fn(&str) -> Result<TargetID, TargetIDError>,
{
    if let Some(configs) = configurations {
        for cfg in configs {
            let events = cfg.events.iter().filter_map(|e| EventName::parse(e.as_ref()).ok()).collect();
            let (prefix, suffix) = extract_prefix_suffix(cfg.filter.as_ref());
            let target_id = target_id_parser(&cfg.queue_arn)?;
            event_rules.push((events, prefix, suffix, vec![target_id]));
        }
    }
    Ok(())
}

pub(crate) fn process_topic_configurations<F>(
    event_rules: &mut Vec<(Vec<EventName>, String, String, Vec<TargetID>)>,
    configurations: Option<Vec<TopicConfiguration>>,
    target_id_parser: F,
) -> Result<(), TargetIDError>
where
    F: Fn(&str) -> Result<TargetID, TargetIDError>,
{
    if let Some(configs) = configurations {
        for cfg in configs {
            let events = cfg.events.iter().filter_map(|e| EventName::parse(e.as_ref()).ok()).collect();
            let (prefix, suffix) = extract_prefix_suffix(cfg.filter.as_ref());
            let target_id = target_id_parser(&cfg.topic_arn)?;
            event_rules.push((events, prefix, suffix, vec![target_id]));
        }
    }
    Ok(())
}

pub(crate) fn process_lambda_configurations<F>(
    event_rules: &mut Vec<(Vec<EventName>, String, String, Vec<TargetID>)>,
    configurations: Option<Vec<LambdaFunctionConfiguration>>,
    target_id_parser: F,
) -> Result<(), TargetIDError>
where
    F: Fn(&str) -> Result<TargetID, TargetIDError>,
{
    if let Some(configs) = configurations {
        for cfg in configs {
            let events = cfg.events.iter().filter_map(|e| EventName::parse(e.as_ref()).ok()).collect();
            let (prefix, suffix) = extract_prefix_suffix(cfg.filter.as_ref());
            let target_id = target_id_parser(&cfg.lambda_function_arn)?;
            event_rules.push((events, prefix, suffix, vec![target_id]));
        }
    }
    Ok(())
}

/// Bucket validation cache to avoid repeated stat_volume() calls on every GET.
///
/// Backend: `RwLock<HashMap>`. A parallel opt-in starshard backend
/// (`RUSTFS_BUCKET_CACHE_STARSHARD`) used to double-write every operation
/// here; no deployment ever set the variable and the branch was removed in
/// backlog#1832.
///
/// Entries expire after `BUCKET_VALIDATION_TTL` (checked on read).
/// Write operations (delete/make bucket) invalidate the cache explicitly.
const BUCKET_VALIDATION_TTL: Duration = Duration::from_secs(5);

static BUCKET_CACHE_SMALL: OnceLock<RwLock<HashMap<String, Instant>>> = OnceLock::new();

fn small_cache() -> &'static RwLock<HashMap<String, Instant>> {
    BUCKET_CACHE_SMALL.get_or_init(|| RwLock::new(HashMap::new()))
}

/// Get a value from the cache.
fn cache_get(bucket: &str) -> Option<Instant> {
    small_cache().read().ok()?.get(bucket).copied()
}

/// Insert a value into the cache.
fn cache_insert(bucket: String, ts: Instant) {
    if let Ok(mut map) = small_cache().write() {
        map.insert(bucket, ts);
    }
}

/// Remove a value from the cache.
fn cache_remove(bucket: &str) {
    if let Ok(mut map) = small_cache().write() {
        map.remove(bucket);
    }
}
/// Invalidate the validation cache for a specific bucket.
pub fn invalidate_bucket_validation_cache(bucket: &str) {
    cache_remove(bucket);
}
/// Helper function to get store and validate bucket exists.
///
/// Uses adaptive cache with 5s TTL to avoid repeated stat_volume() calls.
/// Returns store directly on cache hit without calling get_bucket_info().
pub(crate) async fn get_validated_store(bucket: &str) -> S3Result<Arc<super::ECStore>> {
    let Some(store) = runtime_sources::current_object_store_handle() else {
        return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
    };

    validate_bucket_exists(&store, bucket).await?;

    Ok(store)
}

/// Validate that `bucket` exists on `store`, using the same adaptive 5s-TTL
/// positive cache as [`get_validated_store`]. Handlers that resolve their
/// store through the request-bound server context (backlog#1052 S6) use this
/// to keep bucket validation on that store instead of the process-global one.
pub(crate) async fn validate_bucket_exists(store: &super::ECStore, bucket: &str) -> S3Result<()> {
    // Check cache — TTL is checked manually.
    if let Some(inserted_at) = cache_get(bucket)
        && inserted_at.elapsed() < BUCKET_VALIDATION_TTL
    {
        return Ok(()); // Cache hit, skip validation
    }

    // Cache miss or expired, perform validation
    store
        .get_bucket_info(bucket, &BucketOptions::default())
        .await
        .map_err(ApiError::from)?;

    // Update cache
    cache_insert(bucket.to_string(), Instant::now());

    Ok(())
}

/// Quick check if CORS processing is needed (lightweight check for Origin header)
/// This avoids unnecessary function calls for non-CORS requests
#[inline]
pub(crate) fn needs_cors_processing(headers: &HeaderMap) -> bool {
    headers.contains_key(cors::standard::ORIGIN)
}

/// Apply CORS headers to response based on bucket CORS configuration and request origin
///
/// This function:
/// 1. Reads the Origin header from the request
/// 2. Retrieves the bucket's CORS configuration
/// 3. Matches the origin against CORS rules
/// 4. Validates AllowedHeaders if request headers are present
/// 5. Returns one of:
///    - `None`: bucket has no CORS config (or request has no valid `Origin`)
///    - `Some(empty headers)`: bucket CORS exists but request is denied / no rule matched
///    - `Some(non-empty headers)`: bucket CORS exists and request matched
///
/// Note: This function should only be called if `needs_cors_processing()` returns true
/// to avoid unnecessary overhead for non-CORS requests.
pub(crate) async fn apply_cors_headers(bucket: &str, method: &http::Method, headers: &HeaderMap) -> Option<HeaderMap> {
    use http::HeaderValue;

    fn is_credentialed_request(headers: &HeaderMap) -> bool {
        headers.contains_key(http::header::AUTHORIZATION)
            || headers.contains_key(http::header::COOKIE)
            || headers.contains_key("x-amz-security-token")
            || headers.contains_key("x-amz-content-sha256")
    }

    // Get Origin header from request
    let origin = headers.get(cors::standard::ORIGIN)?.to_str().ok()?;

    // Get CORS configuration for the bucket
    let cors_config = match get_bucket_cors_config(bucket).await {
        Ok((config, _)) => config,
        Err(_) => return None, // No CORS config, no headers to add
    };

    // Early return if no CORS rules configured
    if cors_config.cors_rules.is_empty() {
        return Some(HeaderMap::new());
    }

    // Check if method is supported and get its string representation
    const SUPPORTED_METHODS: &[&str] = &["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"];
    let method_str = method.as_str();
    if !SUPPORTED_METHODS.contains(&method_str) {
        return Some(HeaderMap::new());
    }

    // Use Access-Control-Request-Method if present (for preflight and non-preflight requests),
    // otherwise fall back to the actual HTTP method.
    let is_preflight = method == http::Method::OPTIONS;
    let requested_method = headers
        .get(cors::request::ACCESS_CONTROL_REQUEST_METHOD)
        .and_then(|v| v.to_str().ok())
        .unwrap_or(method_str);

    // Get requested headers from preflight request
    let requested_headers = if is_preflight {
        headers
            .get(cors::request::ACCESS_CONTROL_REQUEST_HEADERS)
            .and_then(|v| v.to_str().ok())
            .map(|h| h.split(',').map(|s| s.trim().to_lowercase()).collect::<Vec<_>>())
    } else {
        None
    };

    // Find matching CORS rule
    for rule in cors_config.cors_rules.iter() {
        // Check if origin matches
        let origin_matches = rule.allowed_origins.iter().any(|allowed_origin| {
            if allowed_origin == "*" {
                true
            } else {
                // Exact match or pattern match (support wildcards like https://*.example.com)
                allowed_origin == origin || matches_origin_pattern(allowed_origin, origin)
            }
        });

        if !origin_matches {
            continue;
        }

        // Check if method is allowed
        let method_allowed = rule
            .allowed_methods
            .iter()
            .any(|allowed_method| allowed_method.as_str() == requested_method);

        if !method_allowed {
            continue;
        }

        // Validate AllowedHeaders if present in the request
        if let Some(ref req_headers) = requested_headers {
            if let Some(ref allowed_headers) = rule.allowed_headers {
                // Check if all requested headers are allowed
                let all_headers_allowed = req_headers.iter().all(|req_header| {
                    allowed_headers.iter().any(|allowed_header| {
                        let allowed_lower = allowed_header.to_lowercase();
                        // "*" allows all headers, or exact match
                        allowed_lower == "*" || allowed_lower == *req_header
                    })
                });

                if !all_headers_allowed {
                    // If not all headers are allowed, skip this rule
                    continue;
                }
            } else if !req_headers.is_empty() {
                // If no AllowedHeaders specified but headers were requested, skip this rule
                // Unless the rule explicitly allows all headers
                continue;
            }
        }

        // Found matching rule, build response headers
        let mut response_headers = HeaderMap::new();

        // Access-Control-Allow-Origin
        // Credentials mode + wildcard allow list requires echoing the request origin.
        // Browsers reject `Access-Control-Allow-Origin: *` with `credentials: include`.
        let has_wildcard_origin = rule.allowed_origins.iter().any(|o| o == "*");
        let credentialed_request = is_credentialed_request(headers);
        let mut origin_reflected = false;

        if has_wildcard_origin {
            if credentialed_request {
                if let Ok(origin_value) = HeaderValue::from_str(origin) {
                    response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN, origin_value);
                    origin_reflected = true;
                }
            } else {
                response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
            }
        } else if let Ok(origin_value) = HeaderValue::from_str(origin) {
            response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN, origin_value);
            origin_reflected = true;
        }

        // Vary: Origin whenever origin is reflected (non-"*" allow-origin).
        // This prevents proxy/browser caches from reusing CORS headers across different origins.
        if origin_reflected {
            response_headers.insert(cors::standard::VARY, HeaderValue::from_static("Origin"));
        }

        // Credentials mode requires explicit allow-credentials.
        if credentialed_request {
            response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_CREDENTIALS, HeaderValue::from_static("true"));
        }

        // Access-Control-Allow-Methods (required for preflight)
        if is_preflight || !rule.allowed_methods.is_empty() {
            let methods_str = rule.allowed_methods.iter().map(|m| m.as_str()).collect::<Vec<_>>().join(", ");
            if let Ok(methods_value) = HeaderValue::from_str(&methods_str) {
                response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_METHODS, methods_value);
            }
        }

        // Access-Control-Allow-Headers (required for preflight if headers were requested)
        if is_preflight && let Some(ref allowed_headers) = rule.allowed_headers {
            let headers_str = allowed_headers.iter().map(|h| h.as_str()).collect::<Vec<_>>().join(", ");
            if let Ok(headers_value) = HeaderValue::from_str(&headers_str) {
                response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_HEADERS, headers_value);
            }
        }

        // Access-Control-Expose-Headers (for actual requests)
        if !is_preflight && let Some(ref expose_headers) = rule.expose_headers {
            let expose_headers_str = expose_headers.iter().map(|h| h.as_str()).collect::<Vec<_>>().join(", ");
            if let Ok(expose_value) = HeaderValue::from_str(&expose_headers_str) {
                response_headers.insert(cors::response::ACCESS_CONTROL_EXPOSE_HEADERS, expose_value);
            }
        }

        // Access-Control-Max-Age (for preflight requests)
        if is_preflight
            && let Some(max_age) = rule.max_age_seconds
            && let Ok(max_age_value) = HeaderValue::from_str(&max_age.to_string())
        {
            response_headers.insert(cors::response::ACCESS_CONTROL_MAX_AGE, max_age_value);
        }

        return Some(response_headers);
    }

    Some(HeaderMap::new()) // No matching rule found
}
/// Check if an origin matches a pattern (supports wildcards like https://*.example.com)
pub(crate) fn matches_origin_pattern(pattern: &str, origin: &str) -> bool {
    // Simple wildcard matching: * matches any sequence
    if pattern.contains('*') {
        let pattern_parts: Vec<&str> = pattern.split('*').collect();
        if pattern_parts.len() == 2 {
            origin.starts_with(pattern_parts[0]) && origin.ends_with(pattern_parts[1])
        } else {
            false
        }
    } else {
        pattern == origin
    }
}

/// Wrap S3Response with CORS headers if needed
/// This function performs a lightweight check first to avoid unnecessary CORS processing
/// for non-CORS requests (requests without Origin header)
pub(crate) async fn wrap_response_with_cors<T>(
    bucket: &str,
    method: &http::Method,
    headers: &HeaderMap,
    output: T,
) -> S3Response<T> {
    let mut response = S3Response::new(output);

    // Quick check: only process CORS if Origin header is present
    if needs_cors_processing(headers)
        && let Some(cors_headers) = apply_cors_headers(bucket, method, headers).await
    {
        for (key, value) in cors_headers.iter() {
            response.headers.insert(key, value.clone());
        }
    }

    response
}

/// Parse part number from Option<i32> to Option<usize> with validation
/// This function checks that the part number is greater than 0 and
/// converts it to usize, returning an error if invalid
///
/// # Arguments
/// * `part_number` - The optional part number as i32
/// * `op` - The operation name for logging purposes
///
/// # Returns
/// * `Ok(Some(usize))` if part number is valid
/// * `Ok(None)` if part number is None
/// * `Err(S3Error)` if part number is invalid (0 or overflow)
#[inline]
pub(crate) fn parse_part_number_i32_to_usize(part_number: Option<i32>, op: &'static str) -> S3Result<Option<usize>> {
    match part_number {
        None => Ok(None),
        Some(n) if !(1..=MAX_MULTIPART_PART_NUMBER).contains(&n) => Err(S3Error::with_message(
            S3ErrorCode::InvalidArgument,
            format!("{op}: partNumber must be between 1 and {MAX_MULTIPART_PART_NUMBER}"),
        )),
        Some(n) => Ok(Some(n as usize)),
    }
}
