//! S3 API router using Axum

use crate::config::{S3Config, S3PerformanceConfig};
use axum::{
    body::{Body, Bytes},
    extract::{Request, State},
    http::{HeaderMap, Method, StatusCode},
    middleware,
    response::{IntoResponse, Response},
    Router,
};
use bytes::BytesMut;
use chrono::Utc;
use futures::{stream, stream::FuturesUnordered, StreamExt};
use http_body_util::BodyExt;
use md5::{Digest, Md5};
use serde::Deserialize;
use sha2::Sha256;
use std::io;
use std::sync::Arc;
use tokio::task::JoinHandle;

use crate::metadata::MetadataStore;
use crate::tenant::TenantContext;
use lightningstor_storage::StorageBackend;
use lightningstor_types::{
    Bucket, BucketName, MultipartUpload, Object, ObjectKey, ObjectMetadata, ObjectVersion, Part,
    PartNumber,
};

use super::auth::{AuthState, VerifiedBodyBytes, VerifiedPayloadHash, VerifiedTenantContext};
use super::xml::{
    BucketEntry, ErrorResponse, ListAllMyBucketsResult, ListBucketResult, ListBucketResultV2,
    ObjectEntry,
};

/// S3 API state
#[derive(Clone)]
pub struct S3State {
    pub storage: Arc<dyn StorageBackend>,
    pub metadata: Arc<MetadataStore>,
    pub performance: S3PerformanceConfig,
}

impl S3State {
    pub fn new(storage: Arc<dyn StorageBackend>, metadata: Arc<MetadataStore>) -> Self {
        Self::new_with_config(storage, metadata, S3PerformanceConfig::default())
    }

    pub fn new_with_config(
        storage: Arc<dyn StorageBackend>,
        metadata: Arc<MetadataStore>,
        performance: S3PerformanceConfig,
    ) -> Self {
        Self {
            storage,
            metadata,
            performance,
        }
    }
}

/// Create the S3-compatible HTTP router with storage and metadata backends
pub fn create_router_with_state(
    storage: Arc<dyn StorageBackend>,
    metadata: Arc<MetadataStore>,
) -> Router {
    create_router_with_auth_state(
        storage,
        metadata,
        Arc::new(AuthState::new(None)),
        S3PerformanceConfig::default(),
    )
}

/// Create the S3-compatible HTTP router with auth and storage backends
pub fn create_router_with_auth(
    storage: Arc<dyn StorageBackend>,
    metadata: Arc<MetadataStore>,
    iam_endpoint: Option<String>,
) -> Router {
    create_router_with_auth_config(storage, metadata, iam_endpoint, S3Config::default())
}

pub fn create_router_with_auth_config(
    storage: Arc<dyn StorageBackend>,
    metadata: Arc<MetadataStore>,
    iam_endpoint: Option<String>,
    config: S3Config,
) -> Router {
    create_router_with_auth_state(
        storage,
        metadata,
        Arc::new(AuthState::new_with_config(iam_endpoint, &config.auth)),
        config.performance,
    )
}

fn create_router_with_auth_state(
    storage: Arc<dyn StorageBackend>,
    metadata: Arc<MetadataStore>,
    auth_state: Arc<AuthState>,
    performance: S3PerformanceConfig,
) -> Router {
    let state = Arc::new(S3State::new_with_config(storage, metadata, performance));

    Router::new()
        // Catch-all route for ALL operations (including root /)
        // We use fallback to bypass matchit routing completely
        .fallback(dispatch_global)
        .layer(middleware::from_fn(move |request, next| {
            let auth_state = auth_state.clone();
            async move { super::auth::sigv4_auth_middleware(auth_state, request, next).await }
        }))
        .with_state(state)
}

/// Create a router without state (for backwards compatibility, returns stub responses)
pub fn create_router() -> Router {
    // Create a minimal router that returns NotImplemented for all operations
    Router::new().fallback(|| async {
        error_response(
            StatusCode::SERVICE_UNAVAILABLE,
            "ServiceUnavailable",
            "Storage not configured",
        )
    })
}

/// Query parameters for ListObjects
#[derive(Debug, Deserialize, Default)]
#[serde(default)]
pub struct ListObjectsQuery {
    prefix: Option<String>,
    delimiter: Option<String>,
    #[serde(rename = "max-keys")]
    max_keys: Option<u32>,
    marker: Option<String>,
    #[serde(rename = "start-after")]
    start_after: Option<String>,
    #[serde(rename = "continuation-token")]
    continuation_token: Option<String>,
    #[serde(rename = "list-type")]
    list_type: Option<String>,
}

fn request_tenant(extensions: &axum::http::Extensions) -> TenantContext {
    extensions
        .get::<VerifiedTenantContext>()
        .map(|tenant| tenant.0.clone())
        .unwrap_or_else(|| TenantContext {
            org_id: "default".to_string(),
            project_id: "default".to_string(),
        })
}

fn streaming_put_threshold_bytes(state: &Arc<S3State>) -> usize {
    state.performance.streaming_put_threshold_bytes
}

fn inline_put_max_bytes(state: &Arc<S3State>) -> usize {
    state.performance.inline_put_max_bytes
}

fn multipart_put_concurrency(state: &Arc<S3State>) -> usize {
    state.performance.multipart_put_concurrency
}

fn request_content_length(headers: &HeaderMap) -> Option<usize> {
    headers
        .get("content-length")
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.parse::<usize>().ok())
}

// =============================================================================
// Global Dispatcher
// =============================================================================

async fn dispatch_global(State(state): State<Arc<S3State>>, request: Request) -> Response {
    let full_path = request.uri().path().trim_start_matches('/').to_string();

    // Check if path is effectively root (empty)
    if full_path.is_empty() {
        if request.method() == Method::GET {
            let tenant = request_tenant(request.extensions());
            return list_buckets(state, tenant).await.into_response();
        } else {
            return error_response(
                StatusCode::METHOD_NOT_ALLOWED,
                "MethodNotAllowed",
                "Method not allowed on root",
            )
            .into_response();
        }
    }

    let (bucket, key) = match full_path.find('/') {
        Some(idx) => (
            full_path[..idx].to_string(),
            full_path[idx + 1..].to_string(),
        ),
        None => (full_path, String::new()),
    };

    let method = request.method().clone();
    let (parts, body) = request.into_parts();
    let headers = parts.headers;
    let uri = parts.uri;
    let verified_body = parts
        .extensions
        .get::<VerifiedBodyBytes>()
        .map(|payload| payload.0.clone());
    let verified_payload_hash = parts
        .extensions
        .get::<VerifiedPayloadHash>()
        .map(|payload| payload.0.clone());
    let tenant = request_tenant(&parts.extensions);

    // Dispatch based on method and key presence
    if method == Method::PUT {
        if key.is_empty() {
            create_bucket(state, tenant, bucket).await.into_response()
        } else {
            put_object(
                state,
                tenant,
                bucket,
                key,
                headers,
                body,
                verified_body,
                verified_payload_hash,
            )
            .await
            .into_response()
        }
    } else if method == Method::GET {
        if key.is_empty() {
            // Parse query params
            let query_str = uri.query().unwrap_or("");
            let params: ListObjectsQuery =
                serde_urlencoded::from_str(query_str).unwrap_or_default();
            list_objects(state, tenant, bucket, params)
                .await
                .into_response()
        } else {
            get_object(state, tenant, bucket, key).await.into_response()
        }
    } else if method == Method::DELETE {
        if key.is_empty() {
            delete_bucket(state, tenant, bucket).await.into_response()
        } else {
            delete_object(state, tenant, bucket, key)
                .await
                .into_response()
        }
    } else if method == Method::HEAD {
        if key.is_empty() {
            head_bucket(state, tenant, bucket).await.into_response()
        } else {
            head_object(state, tenant, bucket, key)
                .await
                .into_response()
        }
    } else {
        error_response(
            StatusCode::METHOD_NOT_ALLOWED,
            "MethodNotAllowed",
            "Method not allowed",
        )
        .into_response()
    }
}

// =============================================================================
// Service Operations
// =============================================================================

async fn list_buckets(state: Arc<S3State>, tenant: TenantContext) -> impl IntoResponse {
    match state
        .metadata
        .list_buckets(&tenant.org_id, Some(&tenant.project_id))
        .await
    {
        Ok(buckets) => {
            let bucket_entries: Vec<BucketEntry> = buckets
                .iter()
                .map(|b| BucketEntry {
                    name: b.name.as_str().to_string(),
                    creation_date: b.created_at.to_rfc3339(),
                })
                .collect();

            let result = ListAllMyBucketsResult {
                owner: super::xml::Owner {
                    id: tenant.org_id.clone(),
                    display_name: tenant.project_id.clone(),
                },
                buckets: super::xml::Buckets {
                    bucket: bucket_entries,
                },
            };

            match super::xml::to_xml(&result) {
                Ok(xml) => Response::builder()
                    .status(StatusCode::OK)
                    .header("Content-Type", "application/xml")
                    .body(Body::from(xml))
                    .unwrap(),
                Err(_) => error_response(
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "InternalError",
                    "Failed to serialize response",
                ),
            }
        }
        Err(e) => error_response(
            StatusCode::INTERNAL_SERVER_ERROR,
            "InternalError",
            &e.to_string(),
        ),
    }
}

// =============================================================================
// Bucket Operations
// =============================================================================

async fn create_bucket(
    state: Arc<S3State>,
    tenant: TenantContext,
    bucket: String,
) -> impl IntoResponse {
    tracing::info!(bucket = %bucket, "CreateBucket request");
    let region = "default";

    // Validate bucket name
    let bucket_name = match BucketName::new(&bucket) {
        Ok(name) => name,
        Err(e) => return error_response(StatusCode::BAD_REQUEST, "InvalidBucketName", e),
    };

    // Check if bucket already exists
    match state
        .metadata
        .load_bucket(&tenant.org_id, &tenant.project_id, &bucket)
        .await
    {
        Ok(Some(_)) => {
            return error_response(
                StatusCode::CONFLICT,
                "BucketAlreadyExists",
                "Bucket already exists",
            );
        }
        Ok(None) => {} // Bucket does not exist, proceed
        Err(e) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &e.to_string(),
            );
        }
    }

    // Create bucket
    let new_bucket = Bucket::new(bucket_name, &tenant.org_id, &tenant.project_id, region);

    match state.metadata.save_bucket(&new_bucket).await {
        Ok(_) => {
            tracing::info!(bucket = %bucket, "Bucket created successfully");
            Response::builder()
                .status(StatusCode::OK)
                .header("Location", format!("/{}", bucket))
                .body(Body::empty())
                .unwrap()
        }
        Err(e) => error_response(
            StatusCode::INTERNAL_SERVER_ERROR,
            "InternalError",
            &e.to_string(),
        ),
    }
}

async fn delete_bucket(
    state: Arc<S3State>,
    tenant: TenantContext,
    bucket: String,
) -> impl IntoResponse {
    tracing::info!(bucket = %bucket, "DeleteBucket request");

    // Load bucket
    let bucket_obj = match state
        .metadata
        .load_bucket(&tenant.org_id, &tenant.project_id, &bucket)
        .await
    {
        Ok(Some(b)) => b,
        Ok(None) => {
            return error_response(
                StatusCode::NOT_FOUND,
                "NoSuchBucket",
                "The specified bucket does not exist",
            )
        }
        Err(e) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &e.to_string(),
            )
        }
    };

    // Ensure bucket is empty before deleting to avoid data loss
    match state.metadata.has_objects(&bucket_obj.id).await {
        Ok(true) => {
            return error_response(
                StatusCode::CONFLICT,
                "BucketNotEmpty",
                "The bucket you tried to delete is not empty",
            );
        }
        Ok(false) => {}
        Err(e) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &e.to_string(),
            );
        }
    }

    // Delete bucket
    match state.metadata.delete_bucket(&bucket_obj).await {
        Ok(_) => {
            tracing::info!(bucket = %bucket, "Bucket deleted successfully");
            Response::builder()
                .status(StatusCode::NO_CONTENT)
                .body(Body::empty())
                .unwrap()
        }
        Err(e) => error_response(
            StatusCode::INTERNAL_SERVER_ERROR,
            "InternalError",
            &e.to_string(),
        ),
    }
}

async fn head_bucket(
    state: Arc<S3State>,
    tenant: TenantContext,
    bucket: String,
) -> impl IntoResponse {
    tracing::info!(bucket = %bucket, "HeadBucket request");

    match state
        .metadata
        .load_bucket(&tenant.org_id, &tenant.project_id, &bucket)
        .await
    {
        Ok(Some(b)) => Response::builder()
            .status(StatusCode::OK)
            .header("x-amz-bucket-region", &b.region)
            .body(Body::empty())
            .unwrap(),
        Ok(None) => error_response(
            StatusCode::NOT_FOUND,
            "NoSuchBucket",
            "The specified bucket does not exist",
        ),
        Err(e) => error_response(
            StatusCode::INTERNAL_SERVER_ERROR,
            "InternalError",
            &e.to_string(),
        ),
    }
}

/// Compute common prefixes for hierarchical folder-like browsing
fn compute_common_prefixes(
    contents: &[ObjectEntry],
    prefix: &str,
    delimiter: &str,
) -> (Vec<ObjectEntry>, Vec<super::xml::CommonPrefix>) {
    use std::collections::HashSet;

    let mut prefixes: HashSet<String> = HashSet::new();
    let mut filtered: Vec<ObjectEntry> = Vec::new();

    for obj in contents {
        // Remove the prefix from the key to get the relative path
        let relative_key = if obj.key.starts_with(prefix) {
            &obj.key[prefix.len()..]
        } else {
            &obj.key
        };

        // Find the first delimiter in the relative path
        if let Some(delim_pos) = relative_key.find(delimiter) {
            // This object is in a "subdirectory" - extract the common prefix
            let common_prefix = format!("{}{}{}", prefix, &relative_key[..delim_pos], delimiter);
            prefixes.insert(common_prefix);
        } else {
            // No delimiter - this object is at the current level
            filtered.push(obj.clone());
        }
    }

    let mut common_prefixes: Vec<super::xml::CommonPrefix> = prefixes
        .into_iter()
        .map(|p| super::xml::CommonPrefix { prefix: p })
        .collect();

    // Sort for consistent output
    common_prefixes.sort_by(|a, b| a.prefix.cmp(&b.prefix));

    (filtered, common_prefixes)
}

fn extract_user_metadata(headers: &HeaderMap) -> std::collections::HashMap<String, String> {
    let mut metadata = std::collections::HashMap::new();
    for (name, value) in headers.iter() {
        let name_str = name.as_str();
        if let Some(stripped) = name_str.strip_prefix("x-amz-meta-") {
            if let Ok(val_str) = value.to_str() {
                metadata.insert(stripped.to_string(), val_str.to_string());
            }
        }
    }
    metadata
}

async fn list_objects(
    state: Arc<S3State>,
    tenant: TenantContext,
    bucket: String,
    params: ListObjectsQuery,
) -> impl IntoResponse {
    tracing::info!(bucket = %bucket, ?params, "ListObjects request");

    // Load bucket to verify it exists
    let bucket_obj = match state
        .metadata
        .load_bucket(&tenant.org_id, &tenant.project_id, &bucket)
        .await
    {
        Ok(Some(b)) => b,
        Ok(None) => {
            return error_response(
                StatusCode::NOT_FOUND,
                "NoSuchBucket",
                "The specified bucket does not exist",
            )
        }
        Err(e) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &e.to_string(),
            )
        }
    };

    let prefix = params.prefix.unwrap_or_default();
    let max_keys = params.max_keys.unwrap_or(1000);
    let delimiter = params.delimiter.as_deref();
    let is_v2 = params.list_type.as_deref() == Some("2");
    let page_marker = if is_v2 {
        params
            .continuation_token
            .clone()
            .or_else(|| params.start_after.clone())
    } else {
        params.marker.clone().or_else(|| params.start_after.clone())
    };

    // List objects
    match state
        .metadata
        .list_objects_page(&bucket_obj.id, &prefix, page_marker.as_deref(), max_keys)
        .await
    {
        Ok((objects, has_more)) => {
            let contents: Vec<ObjectEntry> = objects
                .iter()
                .filter(|o| !o.is_delete_marker)
                .map(|o| ObjectEntry {
                    key: o.key.as_str().to_string(),
                    last_modified: o.last_modified.to_rfc3339(),
                    etag: format!("\"{}\"", o.etag.as_str()),
                    size: o.size,
                    storage_class: o.storage_class.clone(),
                })
                .collect();

            // Compute CommonPrefixes if delimiter is provided
            let (filtered_contents, common_prefixes) = if let Some(delim) = delimiter {
                compute_common_prefixes(&contents, &prefix, delim)
            } else {
                (contents.clone(), vec![])
            };

            let key_count = (filtered_contents.len() + common_prefixes.len()) as u32;
            let is_truncated = has_more;
            let next_token = if is_truncated {
                objects.last().map(|object| object.key.as_str().to_string())
            } else {
                None
            };

            let xml = if is_v2 {
                // ListObjectsV2 response
                let result = ListBucketResultV2 {
                    name: bucket,
                    prefix: prefix.clone(),
                    delimiter: params.delimiter,
                    max_keys,
                    key_count,
                    is_truncated,
                    continuation_token: params.continuation_token,
                    next_continuation_token: next_token,
                    start_after: params.start_after,
                    contents: filtered_contents,
                    common_prefixes,
                };

                super::xml::to_xml(&result)
            } else {
                // ListObjects (v1) response
                let result = ListBucketResult {
                    name: bucket,
                    prefix,
                    marker: params.marker,
                    delimiter: params.delimiter,
                    max_keys,
                    is_truncated,
                    next_marker: next_token,
                    contents: filtered_contents,
                    common_prefixes,
                };

                super::xml::to_xml(&result)
            };

            match xml {
                Ok(xml) => Response::builder()
                    .status(StatusCode::OK)
                    .header("Content-Type", "application/xml")
                    .body(Body::from(xml))
                    .unwrap(),
                Err(_) => error_response(
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "InternalError",
                    "Failed to serialize response",
                ),
            }
        }
        Err(e) => error_response(
            StatusCode::INTERNAL_SERVER_ERROR,
            "InternalError",
            &e.to_string(),
        ),
    }
}

// =============================================================================
// Object Operations
// =============================================================================

async fn put_object(
    state: Arc<S3State>,
    tenant: TenantContext,
    bucket: String,
    key: String,
    headers: HeaderMap,
    body: Body,
    verified_body: Option<Bytes>,
    verified_payload_hash: Option<String>,
) -> impl IntoResponse {
    tracing::debug!(bucket = %bucket, key = %key, "PutObject request");

    // Load bucket
    let bucket_obj = match state
        .metadata
        .load_bucket(&tenant.org_id, &tenant.project_id, &bucket)
        .await
    {
        Ok(Some(b)) => b,
        Ok(None) => {
            return error_response(
                StatusCode::NOT_FOUND,
                "NoSuchBucket",
                "The specified bucket does not exist",
            )
        }
        Err(e) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &e.to_string(),
            )
        }
    };

    // Validate object key
    let object_key = match ObjectKey::new(&key) {
        Ok(k) => k,
        Err(e) => return error_response(StatusCode::BAD_REQUEST, "InvalidArgument", e),
    };

    // Extract content type from headers
    let content_type = headers
        .get("content-type")
        .and_then(|v| v.to_str().ok())
        .map(|s| s.to_string());

    // Create object metadata
    let user_metadata = extract_user_metadata(&headers);
    let metadata = ObjectMetadata {
        content_type: content_type.clone(),
        content_encoding: headers
            .get("content-encoding")
            .and_then(|v| v.to_str().ok())
            .map(|s| s.to_string()),
        content_disposition: headers
            .get("content-disposition")
            .and_then(|v| v.to_str().ok())
            .map(|s| s.to_string()),
        content_language: headers
            .get("content-language")
            .and_then(|v| v.to_str().ok())
            .map(|s| s.to_string()),
        cache_control: headers
            .get("cache-control")
            .and_then(|v| v.to_str().ok())
            .map(|s| s.to_string()),
        user_metadata,
    };

    let content_length = request_content_length(&headers);
    let (body_len, etag, multipart_upload, inline_body) = match verified_body {
        Some(body_bytes) => {
            let body_len = body_bytes.len() as u64;
            let (actual_payload_hash, etag) = match verified_payload_hash.as_deref() {
                Some(expected_payload_hash) if expected_payload_hash != "UNSIGNED-PAYLOAD" => (
                    expected_payload_hash.to_string(),
                    calculate_etag(&body_bytes),
                ),
                _ => calculate_payload_hashes(&body_bytes),
            };
            if let Some(expected_payload_hash) = verified_payload_hash {
                if expected_payload_hash != "UNSIGNED-PAYLOAD"
                    && actual_payload_hash != expected_payload_hash
                {
                    return error_response(
                        StatusCode::FORBIDDEN,
                        "SignatureDoesNotMatch",
                        "x-amz-content-sha256 does not match the request body",
                    );
                }
            }

            (body_len, etag, None, Some(body_bytes))
        }
        None => {
            let prepared = if let Some(content_length) = content_length
                .filter(|content_length| *content_length <= inline_put_max_bytes(&state))
            {
                read_inline_put_body(
                    body,
                    verified_payload_hash.as_deref(),
                    inline_put_max_bytes(&state),
                    Some(content_length),
                )
                .await
            } else {
                stream_put_body(
                    &state,
                    &bucket_obj.id.to_string(),
                    object_key.clone(),
                    metadata.clone(),
                    body,
                    verified_payload_hash.as_deref(),
                )
                .await
            };

            match prepared {
                Ok(prepared) => (
                    prepared.body_len,
                    prepared.etag,
                    prepared.multipart_upload,
                    prepared.inline_body,
                ),
                Err(response) => return response,
            }
        }
    };

    // Create object
    let mut object = Object::new(
        bucket_obj.id.to_string(),
        object_key,
        etag.clone(),
        body_len,
        content_type,
    );
    object.metadata = metadata;

    // Handle versioning
    if bucket_obj.versioning == lightningstor_types::Versioning::Enabled {
        object.version = ObjectVersion::new();
    }

    let has_inline_body = inline_body.is_some();
    if let Some(body_bytes) = inline_body {
        if let Err(e) = state.storage.put_object(&object.id, body_bytes).await {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &format!("Failed to store object: {}", e),
            );
        }
    }

    if let Some(upload) = &multipart_upload {
        if let Err(e) = state
            .metadata
            .save_object_multipart_upload(&object.id, upload)
            .await
        {
            let _ = delete_multipart_parts(&state, upload).await;
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &format!("Failed to save multipart manifest: {}", e),
            );
        }
    }

    // Save object metadata
    if let Err(e) = state.metadata.save_object(&object).await {
        if has_inline_body {
            let _ = state.storage.delete_object(&object.id).await;
        }
        if let Some(upload) = &multipart_upload {
            let _ = state
                .metadata
                .delete_object_multipart_upload(&object.id)
                .await;
            let _ = delete_multipart_parts(&state, upload).await;
        }
        return error_response(
            StatusCode::INTERNAL_SERVER_ERROR,
            "InternalError",
            &e.to_string(),
        );
    }

    tracing::debug!(bucket = %bucket, key = %key, etag = %etag.as_str(), "Object stored successfully");

    Response::builder()
        .status(StatusCode::OK)
        .header("ETag", format!("\"{}\"", etag.as_str()))
        .header("x-amz-version-id", object.version.as_str())
        .body(Body::empty())
        .unwrap()
}

fn calculate_payload_hashes(body: &[u8]) -> (String, lightningstor_types::ETag) {
    let mut sha256 = Sha256::new();
    sha256.update(body);

    let sha256_hex = hex::encode(sha256.finalize());
    (sha256_hex, calculate_etag(body))
}

struct PreparedPutBody {
    body_len: u64,
    etag: lightningstor_types::ETag,
    inline_body: Option<Bytes>,
    multipart_upload: Option<MultipartUpload>,
}

async fn read_inline_put_body(
    mut body: Body,
    expected_payload_hash: Option<&str>,
    max_bytes: usize,
    expected_len: Option<usize>,
) -> Result<PreparedPutBody, Response<Body>> {
    let verify_payload_hash = expected_payload_hash
        .filter(|expected_payload_hash| *expected_payload_hash != "UNSIGNED-PAYLOAD");
    let initial_capacity = expected_len.unwrap_or(0).min(max_bytes);
    let mut buffered = BytesMut::with_capacity(initial_capacity);
    let mut full_md5 = Md5::new();
    let mut full_sha256 = verify_payload_hash.map(|_| Sha256::new());
    let mut body_len = 0usize;

    while let Some(frame) = body.frame().await {
        let frame = match frame {
            Ok(frame) => frame,
            Err(e) => {
                return Err(error_response(
                    StatusCode::BAD_REQUEST,
                    "InvalidRequest",
                    &e.to_string(),
                ));
            }
        };
        let Ok(data) = frame.into_data() else {
            continue;
        };
        if data.is_empty() {
            continue;
        }
        body_len = body_len.saturating_add(data.len());
        if body_len > max_bytes {
            return Err(error_response(
                StatusCode::PAYLOAD_TOO_LARGE,
                "EntityTooLarge",
                "request body exceeded the inline upload limit",
            ));
        }
        full_md5.update(&data);
        if let Some(full_sha256) = full_sha256.as_mut() {
            full_sha256.update(&data);
        }
        buffered.extend_from_slice(&data);
    }

    if let (Some(expected_payload_hash), Some(full_sha256)) = (verify_payload_hash, full_sha256) {
        let actual_payload_hash = hex::encode(full_sha256.finalize());
        if expected_payload_hash != actual_payload_hash {
            return Err(error_response(
                StatusCode::FORBIDDEN,
                "SignatureDoesNotMatch",
                "x-amz-content-sha256 does not match the request body",
            ));
        }
    }

    let md5_hash: [u8; 16] = full_md5.finalize().into();
    Ok(PreparedPutBody {
        body_len: body_len as u64,
        etag: lightningstor_types::ETag::from_md5(&md5_hash),
        inline_body: Some(buffered.freeze()),
        multipart_upload: None,
    })
}

type StreamingPartUploadHandle = JoinHandle<Result<Part, Response<Body>>>;

async fn stream_put_body(
    state: &Arc<S3State>,
    bucket_id: &str,
    object_key: ObjectKey,
    metadata: ObjectMetadata,
    mut body: Body,
    expected_payload_hash: Option<&str>,
) -> Result<PreparedPutBody, Response<Body>> {
    let verify_payload_hash = expected_payload_hash
        .filter(|expected_payload_hash| *expected_payload_hash != "UNSIGNED-PAYLOAD");
    let threshold = streaming_put_threshold_bytes(state);
    let mut buffered = BytesMut::with_capacity(threshold);
    let mut full_md5 = Some(Md5::new());
    let mut full_sha256 = verify_payload_hash.map(|_| Sha256::new());
    let mut body_len = 0u64;
    let mut next_part_number = 1u32;
    let mut multipart_upload: Option<MultipartUpload> = None;
    let mut scheduled_part_numbers = Vec::new();
    let mut completed_parts = Vec::new();
    let mut in_flight_uploads = FuturesUnordered::new();
    let max_in_flight_uploads = multipart_put_concurrency(state);

    while let Some(frame) = body.frame().await {
        let frame = match frame {
            Ok(frame) => frame,
            Err(e) => {
                if let Some(upload) = &multipart_upload {
                    cleanup_incomplete_streaming_upload(
                        state,
                        upload.upload_id.as_str(),
                        &scheduled_part_numbers,
                        &mut in_flight_uploads,
                    )
                    .await;
                }
                return Err(error_response(
                    StatusCode::BAD_REQUEST,
                    "InvalidRequest",
                    &e.to_string(),
                ));
            }
        };
        let Ok(data) = frame.into_data() else {
            continue;
        };
        if data.is_empty() {
            continue;
        }
        body_len += data.len() as u64;
        if let Some(full_md5) = full_md5.as_mut() {
            full_md5.update(&data);
        }
        if let Some(full_sha256) = full_sha256.as_mut() {
            full_sha256.update(&data);
        }
        buffered.extend_from_slice(&data);

        while buffered.len() >= threshold {
            let chunk = buffered.split_to(threshold).freeze();
            if let Err(response) = queue_streaming_part_upload(
                state,
                &mut multipart_upload,
                bucket_id,
                &object_key,
                &metadata,
                &mut next_part_number,
                chunk,
                &mut scheduled_part_numbers,
                &mut in_flight_uploads,
            ) {
                if let Some(upload) = &multipart_upload {
                    cleanup_incomplete_streaming_upload(
                        state,
                        upload.upload_id.as_str(),
                        &scheduled_part_numbers,
                        &mut in_flight_uploads,
                    )
                    .await;
                }
                return Err(response);
            }
            full_md5 = None;

            if in_flight_uploads.len() >= max_in_flight_uploads {
                if let Err(response) =
                    collect_next_streaming_part(&mut in_flight_uploads, &mut completed_parts).await
                {
                    if let Some(upload) = &multipart_upload {
                        cleanup_incomplete_streaming_upload(
                            state,
                            upload.upload_id.as_str(),
                            &scheduled_part_numbers,
                            &mut in_flight_uploads,
                        )
                        .await;
                    }
                    return Err(response);
                }
            }
        }
    }

    if let (Some(expected_payload_hash), Some(full_sha256)) = (verify_payload_hash, full_sha256) {
        let actual_payload_hash = hex::encode(full_sha256.finalize());
        if expected_payload_hash != actual_payload_hash {
            if let Some(upload) = &multipart_upload {
                cleanup_incomplete_streaming_upload(
                    state,
                    upload.upload_id.as_str(),
                    &scheduled_part_numbers,
                    &mut in_flight_uploads,
                )
                .await;
            }
            return Err(error_response(
                StatusCode::FORBIDDEN,
                "SignatureDoesNotMatch",
                "x-amz-content-sha256 does not match the request body",
            ));
        }
    }

    if multipart_upload.is_some() {
        if !buffered.is_empty() {
            let chunk = buffered.freeze();
            if let Err(response) = queue_streaming_part_upload(
                state,
                &mut multipart_upload,
                bucket_id,
                &object_key,
                &metadata,
                &mut next_part_number,
                chunk,
                &mut scheduled_part_numbers,
                &mut in_flight_uploads,
            ) {
                if let Some(upload) = &multipart_upload {
                    cleanup_incomplete_streaming_upload(
                        state,
                        upload.upload_id.as_str(),
                        &scheduled_part_numbers,
                        &mut in_flight_uploads,
                    )
                    .await;
                }
                return Err(response);
            }
        }

        if let Err(response) =
            collect_all_streaming_parts(&mut in_flight_uploads, &mut completed_parts).await
        {
            if let Some(upload) = &multipart_upload {
                cleanup_incomplete_streaming_upload(
                    state,
                    upload.upload_id.as_str(),
                    &scheduled_part_numbers,
                    &mut in_flight_uploads,
                )
                .await;
            }
            return Err(response);
        }

        completed_parts.sort_by_key(|part| part.part_number.as_u32());
        let multipart_etags = completed_parts
            .iter()
            .map(|part| part.etag.clone())
            .collect::<Vec<_>>();
        let etag = lightningstor_types::ETag::multipart(&multipart_etags, completed_parts.len());
        if let Some(upload) = multipart_upload.as_mut() {
            upload.parts = completed_parts;
        }

        return Ok(PreparedPutBody {
            body_len,
            etag,
            inline_body: None,
            multipart_upload,
        });
    }

    let md5_hash: [u8; 16] = full_md5
        .expect("inline PUT bodies retain the aggregate MD5")
        .finalize()
        .into();
    let etag = lightningstor_types::ETag::from_md5(&md5_hash);

    Ok(PreparedPutBody {
        body_len,
        etag,
        inline_body: Some(buffered.freeze()),
        multipart_upload: None,
    })
}

fn queue_streaming_part_upload(
    state: &Arc<S3State>,
    upload: &mut Option<MultipartUpload>,
    bucket_id: &str,
    object_key: &ObjectKey,
    metadata: &ObjectMetadata,
    next_part_number: &mut u32,
    chunk: Bytes,
    scheduled_part_numbers: &mut Vec<u32>,
    in_flight_uploads: &mut FuturesUnordered<StreamingPartUploadHandle>,
) -> Result<(), Response<Body>> {
    let upload = upload.get_or_insert_with(|| {
        let mut upload = MultipartUpload::new(bucket_id.to_string(), object_key.clone());
        upload.metadata = metadata.clone();
        upload
    });

    let part_number = match PartNumber::new(*next_part_number) {
        Ok(part_number) => part_number,
        Err(e) => {
            return Err(error_response(StatusCode::BAD_REQUEST, "InvalidRequest", e));
        }
    };
    *next_part_number += 1;
    scheduled_part_numbers.push(part_number.as_u32());
    in_flight_uploads.push(start_streaming_part_upload(
        Arc::clone(state),
        upload.upload_id.to_string(),
        part_number,
        chunk,
    ));
    Ok(())
}

fn start_streaming_part_upload(
    state: Arc<S3State>,
    upload_id: String,
    part_number: PartNumber,
    chunk: Bytes,
) -> StreamingPartUploadHandle {
    tokio::spawn(async move {
        let part_etag = calculate_etag(&chunk);
        let part_size = chunk.len() as u64;
        state
            .storage
            .put_part(upload_id.as_str(), part_number.as_u32(), chunk)
            .await
            .map_err(|e| {
                error_response(
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "InternalError",
                    &format!("Failed to store object part: {}", e),
                )
            })?;
        Ok(Part {
            part_number,
            etag: part_etag,
            size: part_size,
            last_modified: Utc::now(),
        })
    })
}

async fn collect_next_streaming_part(
    in_flight_uploads: &mut FuturesUnordered<StreamingPartUploadHandle>,
    completed_parts: &mut Vec<Part>,
) -> Result<(), Response<Body>> {
    match in_flight_uploads.next().await {
        Some(Ok(Ok(part))) => {
            completed_parts.push(part);
            Ok(())
        }
        Some(Ok(Err(response))) => Err(response),
        Some(Err(join_error)) => Err(error_response(
            StatusCode::INTERNAL_SERVER_ERROR,
            "InternalError",
            &format!("Multipart upload task failed: {}", join_error),
        )),
        None => Ok(()),
    }
}

async fn collect_all_streaming_parts(
    in_flight_uploads: &mut FuturesUnordered<StreamingPartUploadHandle>,
    completed_parts: &mut Vec<Part>,
) -> Result<(), Response<Body>> {
    while !in_flight_uploads.is_empty() {
        collect_next_streaming_part(in_flight_uploads, completed_parts).await?;
    }
    Ok(())
}

async fn cleanup_incomplete_streaming_upload(
    state: &Arc<S3State>,
    upload_id: &str,
    scheduled_part_numbers: &[u32],
    in_flight_uploads: &mut FuturesUnordered<StreamingPartUploadHandle>,
) {
    while let Some(_result) = in_flight_uploads.next().await {}
    for part_number in scheduled_part_numbers {
        let _ = state.storage.delete_part(upload_id, *part_number).await;
    }
    let _ = state.storage.delete_upload_parts(upload_id).await;
}

fn multipart_object_body(state: Arc<S3State>, object: &Object, upload: MultipartUpload) -> Body {
    let storage = Arc::clone(&state.storage);
    let object_size = object.size;
    let object_id = object.id;
    let stream = stream::try_unfold(
        (storage, upload, 0usize, 0u64),
        move |(storage, upload, next_part_index, consumed)| async move {
            if consumed >= object_size {
                return Ok::<_, io::Error>(None);
            }

            let mut idx = next_part_index;
            let mut offset = consumed;
            while idx < upload.parts.len() {
                let part = &upload.parts[idx];
                let part_start = offset;
                let part_end = part_start + part.size;
                idx += 1;
                offset = part_end;

                if object_size <= part_start {
                    break;
                }

                let bytes = storage
                    .get_part(upload.upload_id.as_str(), part.part_number.as_u32())
                    .await
                    .map_err(|e| {
                        io::Error::other(format!(
                            "failed to retrieve multipart object {object_id} part {}: {e}",
                            part.part_number.as_u32()
                        ))
                    })?;
                let body_end = (object_size.min(part_end) - part_start) as usize;
                if body_end > bytes.len() {
                    return Err(io::Error::other(format!(
                        "multipart object {object_id} part {} is inconsistent: stored={} expected={}",
                        part.part_number.as_u32(),
                        bytes.len(),
                        body_end
                    )));
                }

                return Ok(Some((
                    bytes.slice(0..body_end),
                    (storage, upload, idx, offset),
                )));
            }

            Ok(None)
        },
    );

    Body::from_stream(stream)
}

async fn delete_multipart_parts(
    state: &Arc<S3State>,
    upload: &MultipartUpload,
) -> lightningstor_types::Result<()> {
    for part in &upload.parts {
        state
            .storage
            .delete_part(upload.upload_id.as_str(), part.part_number.as_u32())
            .await?;
    }
    state
        .storage
        .delete_upload_parts(upload.upload_id.as_str())
        .await?;
    Ok(())
}

fn calculate_etag(body: &[u8]) -> lightningstor_types::ETag {
    let mut md5 = Md5::new();
    md5.update(body);
    let md5_hash: [u8; 16] = md5.finalize().into();
    lightningstor_types::ETag::from_md5(&md5_hash)
}

async fn get_object(
    state: Arc<S3State>,
    tenant: TenantContext,
    bucket: String,
    key: String,
) -> impl IntoResponse {
    tracing::debug!(bucket = %bucket, key = %key, "GetObject request");

    // Load bucket
    let bucket_obj = match state
        .metadata
        .load_bucket(&tenant.org_id, &tenant.project_id, &bucket)
        .await
    {
        Ok(Some(b)) => b,
        Ok(None) => {
            return error_response(
                StatusCode::NOT_FOUND,
                "NoSuchBucket",
                "The specified bucket does not exist",
            )
        }
        Err(e) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &e.to_string(),
            )
        }
    };

    // Load object metadata
    let object = match state.metadata.load_object(&bucket_obj.id, &key, None).await {
        Ok(Some(o)) => o,
        Ok(None) => {
            return error_response(
                StatusCode::NOT_FOUND,
                "NoSuchKey",
                "The specified key does not exist",
            )
        }
        Err(e) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &e.to_string(),
            )
        }
    };

    if object.is_delete_marker {
        return error_response(
            StatusCode::NOT_FOUND,
            "NoSuchKey",
            "The specified key does not exist",
        );
    }

    let multipart_upload = match state
        .metadata
        .load_object_multipart_upload(&object.id)
        .await
    {
        Ok(upload) => upload,
        Err(e) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &e.to_string(),
            )
        }
    };
    let (body, content_length) = if let Some(upload) = multipart_upload {
        (
            multipart_object_body(Arc::clone(&state), &object, upload),
            object.size as usize,
        )
    } else {
        let data = match state.storage.get_object(&object.id).await {
            Ok(data) => data,
            Err(e) => {
                return error_response(
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "InternalError",
                    &format!("Failed to retrieve object: {}", e),
                )
            }
        };
        let len = data.len();
        (Body::from(data), len)
    };

    let mut response = Response::builder()
        .status(StatusCode::OK)
        .header("Content-Length", content_length)
        .header("ETag", format!("\"{}\"", object.etag.as_str()))
        .header("Last-Modified", object.last_modified.to_rfc2822())
        .header("x-amz-version-id", object.version.as_str());

    if let Some(ct) = &object.metadata.content_type {
        response = response.header("Content-Type", ct);
    }
    if let Some(ce) = &object.metadata.content_encoding {
        response = response.header("Content-Encoding", ce);
    }
    if let Some(cd) = &object.metadata.content_disposition {
        response = response.header("Content-Disposition", cd);
    }
    if let Some(cl) = &object.metadata.content_language {
        response = response.header("Content-Language", cl);
    }
    if let Some(cc) = &object.metadata.cache_control {
        response = response.header("Cache-Control", cc);
    }

    response.body(body).unwrap()
}

async fn delete_object(
    state: Arc<S3State>,
    tenant: TenantContext,
    bucket: String,
    key: String,
) -> impl IntoResponse {
    tracing::debug!(bucket = %bucket, key = %key, "DeleteObject request");

    // Load bucket
    let bucket_obj = match state
        .metadata
        .load_bucket(&tenant.org_id, &tenant.project_id, &bucket)
        .await
    {
        Ok(Some(b)) => b,
        Ok(None) => {
            return error_response(
                StatusCode::NOT_FOUND,
                "NoSuchBucket",
                "The specified bucket does not exist",
            )
        }
        Err(e) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &e.to_string(),
            )
        }
    };

    // Load object metadata
    let object = match state.metadata.load_object(&bucket_obj.id, &key, None).await {
        Ok(Some(o)) => o,
        Ok(None) => {
            // S3 returns 204 even if object doesn't exist
            return Response::builder()
                .status(StatusCode::NO_CONTENT)
                .body(Body::empty())
                .unwrap();
        }
        Err(e) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &e.to_string(),
            )
        }
    };

    match state
        .metadata
        .load_object_multipart_upload(&object.id)
        .await
    {
        Ok(Some(upload)) => {
            if let Err(e) = delete_multipart_parts(&state, &upload).await {
                tracing::warn!(bucket = %bucket, key = %key, error = %e, "Failed to delete multipart object parts");
            }
            if let Err(e) = state
                .metadata
                .delete_object_multipart_upload(&object.id)
                .await
            {
                tracing::warn!(bucket = %bucket, key = %key, error = %e, "Failed to delete multipart manifest");
            }
        }
        Ok(None) => {
            if let Err(e) = state.storage.delete_object(&object.id).await {
                tracing::warn!(bucket = %bucket, key = %key, error = %e, "Failed to delete object data");
            }
        }
        Err(e) => {
            tracing::warn!(bucket = %bucket, key = %key, error = %e, "Failed to inspect multipart manifest during delete");
        }
    }

    // Delete from metadata store
    if let Err(e) = state
        .metadata
        .delete_object(&bucket_obj.id, &key, None)
        .await
    {
        return error_response(
            StatusCode::INTERNAL_SERVER_ERROR,
            "InternalError",
            &e.to_string(),
        );
    }

    tracing::debug!(bucket = %bucket, key = %key, "Object deleted successfully");

    Response::builder()
        .status(StatusCode::NO_CONTENT)
        .header("x-amz-version-id", object.version.as_str())
        .body(Body::empty())
        .unwrap()
}

async fn head_object(
    state: Arc<S3State>,
    tenant: TenantContext,
    bucket: String,
    key: String,
) -> impl IntoResponse {
    tracing::debug!(bucket = %bucket, key = %key, "HeadObject request");

    // Load bucket
    let bucket_obj = match state
        .metadata
        .load_bucket(&tenant.org_id, &tenant.project_id, &bucket)
        .await
    {
        Ok(Some(b)) => b,
        Ok(None) => {
            return error_response(
                StatusCode::NOT_FOUND,
                "NoSuchBucket",
                "The specified bucket does not exist",
            )
        }
        Err(e) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &e.to_string(),
            )
        }
    };

    // Load object metadata
    let object = match state.metadata.load_object(&bucket_obj.id, &key, None).await {
        Ok(Some(o)) => o,
        Ok(None) => {
            return error_response(
                StatusCode::NOT_FOUND,
                "NoSuchKey",
                "The specified key does not exist",
            )
        }
        Err(e) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalError",
                &e.to_string(),
            )
        }
    };

    if object.is_delete_marker {
        return error_response(
            StatusCode::NOT_FOUND,
            "NoSuchKey",
            "The specified key does not exist",
        );
    }

    let mut response = Response::builder()
        .status(StatusCode::OK)
        .header("Content-Length", object.size)
        .header("ETag", format!("\"{}\"", object.etag.as_str()))
        .header("Last-Modified", object.last_modified.to_rfc2822())
        .header("x-amz-version-id", object.version.as_str());

    if let Some(ct) = &object.metadata.content_type {
        response = response.header("Content-Type", ct);
    }
    if let Some(ce) = &object.metadata.content_encoding {
        response = response.header("Content-Encoding", ce);
    }
    if let Some(cd) = &object.metadata.content_disposition {
        response = response.header("Content-Disposition", cd);
    }
    if let Some(cl) = &object.metadata.content_language {
        response = response.header("Content-Language", cl);
    }
    if let Some(cc) = &object.metadata.cache_control {
        response = response.header("Cache-Control", cc);
    }

    response.body(Body::empty()).unwrap()
}

// =============================================================================
// Helpers
// =============================================================================

fn error_response(status: StatusCode, code: &str, message: &str) -> Response<Body> {
    let error = ErrorResponse {
        code: code.to_string(),
        message: message.to_string(),
        resource: "".to_string(),
        request_id: uuid::Uuid::new_v4().to_string(),
    };

    Response::builder()
        .status(status)
        .header("Content-Type", "application/xml")
        .body(Body::from(error.to_xml()))
        .unwrap()
}

#[cfg(test)]
mod tests {
    use super::*;
    use async_trait::async_trait;
    use axum::body::to_bytes;
    use axum::http::Request;
    use lightningstor_storage::{LocalFsBackend, StorageBackend, StorageError, StorageResult};
    use lightningstor_types::BucketId;
    use std::collections::HashMap;
    use std::str::FromStr;
    use std::sync::atomic::{AtomicUsize, Ordering};
    use tempfile::{tempdir, TempDir};
    use tokio::sync::Mutex;
    use tokio::time::{sleep, Duration};
    use tower::ServiceExt;

    async fn test_router_with_metadata() -> (Router, Arc<MetadataStore>) {
        let tempdir = tempdir().unwrap();
        let storage = Arc::new(LocalFsBackend::new(tempdir.path(), false).await.unwrap());
        let metadata = Arc::new(MetadataStore::new_in_memory());
        let router = create_router_with_auth_state(
            storage,
            metadata.clone(),
            Arc::new(AuthState::disabled()),
            S3PerformanceConfig::default(),
        );
        std::mem::forget(tempdir);
        (router, metadata)
    }

    async fn test_router() -> Router {
        test_router_with_metadata().await.0
    }

    fn request_with_tenant(
        method: Method,
        uri: &str,
        body: Body,
        org_id: &str,
        project_id: &str,
    ) -> Request<Body> {
        let mut request = Request::builder()
            .method(method)
            .uri(uri)
            .body(body)
            .unwrap();
        request
            .extensions_mut()
            .insert(VerifiedTenantContext(TenantContext {
                org_id: org_id.to_string(),
                project_id: project_id.to_string(),
            }));
        request
    }

    #[tokio::test]
    async fn bucket_and_object_roundtrip_via_router() {
        let router = test_router().await;

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::PUT)
                    .uri("/test-bucket")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::PUT)
                    .uri("/test-bucket/folder/hello.txt")
                    .header("content-type", "text/plain")
                    .header("x-amz-meta-owner", "qa")
                    .body(Body::from("hello world"))
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        assert!(response.headers().contains_key("ETag"));

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::GET)
                    .uri("/test-bucket?list-type=2&delimiter=/")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        let list_body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap();
        let list_xml = String::from_utf8(list_body.to_vec()).unwrap();
        assert!(list_xml.contains("<Prefix>folder/</Prefix>"));

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::HEAD)
                    .uri("/test-bucket/folder/hello.txt")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        assert_eq!(response.headers().get("Content-Length").unwrap(), "11");
        assert_eq!(
            response.headers().get("Content-Type").unwrap(),
            "text/plain"
        );

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::GET)
                    .uri("/test-bucket/folder/hello.txt")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        let body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap();
        assert_eq!(body.as_ref(), b"hello world");

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::DELETE)
                    .uri("/test-bucket/folder/hello.txt")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::NO_CONTENT);

        let response = router
            .oneshot(
                Request::builder()
                    .method(Method::DELETE)
                    .uri("/test-bucket")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::NO_CONTENT);
    }

    #[tokio::test]
    async fn list_objects_v2_paginates_with_continuation_token() {
        let router = test_router().await;

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::PUT)
                    .uri("/paged-bucket")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        for key in ["a.txt", "b.txt", "c.txt"] {
            let response = router
                .clone()
                .oneshot(
                    Request::builder()
                        .method(Method::PUT)
                        .uri(format!("/paged-bucket/{key}"))
                        .header("content-type", "text/plain")
                        .body(Body::from(key.to_string()))
                        .unwrap(),
                )
                .await
                .unwrap();
            assert_eq!(response.status(), StatusCode::OK);
        }

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::GET)
                    .uri("/paged-bucket?list-type=2&max-keys=2")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        let first_body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap();
        let first_xml = String::from_utf8(first_body.to_vec()).unwrap();
        assert!(first_xml.contains("<Key>a.txt</Key>"));
        assert!(first_xml.contains("<Key>b.txt</Key>"));
        assert!(first_xml.contains("<IsTruncated>true</IsTruncated>"));
        assert!(first_xml.contains("<NextContinuationToken>b.txt</NextContinuationToken>"));

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::GET)
                    .uri("/paged-bucket?list-type=2&max-keys=2&continuation-token=b.txt")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        let second_body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap();
        let second_xml = String::from_utf8(second_body.to_vec()).unwrap();
        assert!(!second_xml.contains("<Key>a.txt</Key>"));
        assert!(!second_xml.contains("<Key>b.txt</Key>"));
        assert!(second_xml.contains("<Key>c.txt</Key>"));
        assert!(second_xml.contains("<IsTruncated>false</IsTruncated>"));
    }

    #[tokio::test]
    async fn identical_bucket_names_are_isolated_per_tenant() {
        let router = test_router().await;

        let response = router
            .clone()
            .oneshot(request_with_tenant(
                Method::PUT,
                "/shared-bucket",
                Body::empty(),
                "org-a",
                "project-a",
            ))
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let response = router
            .clone()
            .oneshot(request_with_tenant(
                Method::PUT,
                "/shared-bucket",
                Body::empty(),
                "org-b",
                "project-b",
            ))
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let response = router
            .clone()
            .oneshot(request_with_tenant(
                Method::PUT,
                "/shared-bucket/object.txt",
                Body::from("from-a"),
                "org-a",
                "project-a",
            ))
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let response = router
            .clone()
            .oneshot(request_with_tenant(
                Method::PUT,
                "/shared-bucket/object.txt",
                Body::from("from-b"),
                "org-b",
                "project-b",
            ))
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let response = router
            .clone()
            .oneshot(request_with_tenant(
                Method::GET,
                "/shared-bucket/object.txt",
                Body::empty(),
                "org-a",
                "project-a",
            ))
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        let body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap();
        assert_eq!(body.as_ref(), b"from-a");

        let response = router
            .clone()
            .oneshot(request_with_tenant(
                Method::GET,
                "/shared-bucket/object.txt",
                Body::empty(),
                "org-b",
                "project-b",
            ))
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        let body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap();
        assert_eq!(body.as_ref(), b"from-b");

        let response = router
            .clone()
            .oneshot(request_with_tenant(
                Method::GET,
                "/",
                Body::empty(),
                "org-a",
                "project-a",
            ))
            .await
            .unwrap();
        let list_body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap();
        let list_xml = String::from_utf8(list_body.to_vec()).unwrap();
        assert_eq!(list_xml.matches("<Name>shared-bucket</Name>").count(), 1);
    }

    #[tokio::test]
    async fn large_put_uses_streamed_multipart_manifest() {
        let (router, metadata) = test_router_with_metadata().await;

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::PUT)
                    .uri("/stream-bucket")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let threshold = S3PerformanceConfig::default().streaming_put_threshold_bytes;
        let body = vec![b'x'; threshold + 1024];
        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::PUT)
                    .uri("/stream-bucket/large.bin")
                    .header("content-type", "application/octet-stream")
                    .body(Body::from(body.clone()))
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let bucket = metadata
            .load_bucket("default", "default", "stream-bucket")
            .await
            .unwrap()
            .unwrap();
        let bucket_id = BucketId::from_str(&bucket.id.to_string()).unwrap();
        let object = metadata
            .load_object(&bucket_id, "large.bin", None)
            .await
            .unwrap()
            .unwrap();
        let manifest = metadata
            .load_object_multipart_upload(&object.id)
            .await
            .unwrap();
        assert!(manifest.is_some());

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::GET)
                    .uri("/stream-bucket/large.bin")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        let fetched = to_bytes(response.into_body(), (body.len() * 2) as usize)
            .await
            .unwrap();
        assert_eq!(fetched.as_ref(), body.as_slice());

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::DELETE)
                    .uri("/stream-bucket/large.bin")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::NO_CONTENT);

        let manifest = metadata
            .load_object_multipart_upload(&object.id)
            .await
            .unwrap();
        assert!(manifest.is_none());
    }

    struct DelayedMultipartStorage {
        _tempdir: TempDir,
        inline_objects: Mutex<HashMap<String, Bytes>>,
        multipart_parts: Mutex<HashMap<(String, u32), Bytes>>,
        put_part_delay: Duration,
        in_flight_put_parts: AtomicUsize,
        max_in_flight_put_parts: AtomicUsize,
    }

    impl DelayedMultipartStorage {
        fn new(put_part_delay: Duration) -> Self {
            Self {
                _tempdir: tempdir().unwrap(),
                inline_objects: Mutex::new(HashMap::new()),
                multipart_parts: Mutex::new(HashMap::new()),
                put_part_delay,
                in_flight_put_parts: AtomicUsize::new(0),
                max_in_flight_put_parts: AtomicUsize::new(0),
            }
        }

        fn max_in_flight_put_parts(&self) -> usize {
            self.max_in_flight_put_parts.load(Ordering::SeqCst)
        }

        fn note_put_part_started(&self) {
            let current = self.in_flight_put_parts.fetch_add(1, Ordering::SeqCst) + 1;
            self.max_in_flight_put_parts
                .fetch_max(current, Ordering::SeqCst);
        }

        fn note_put_part_finished(&self) {
            self.in_flight_put_parts.fetch_sub(1, Ordering::SeqCst);
        }
    }

    #[async_trait]
    impl StorageBackend for DelayedMultipartStorage {
        async fn put_object(
            &self,
            object_id: &lightningstor_types::ObjectId,
            data: Bytes,
        ) -> StorageResult<()> {
            self.inline_objects
                .lock()
                .await
                .insert(object_id.to_string(), data);
            Ok(())
        }

        async fn get_object(
            &self,
            object_id: &lightningstor_types::ObjectId,
        ) -> StorageResult<Bytes> {
            self.inline_objects
                .lock()
                .await
                .get(&object_id.to_string())
                .cloned()
                .ok_or_else(|| StorageError::NotFound(*object_id))
        }

        async fn delete_object(
            &self,
            object_id: &lightningstor_types::ObjectId,
        ) -> StorageResult<()> {
            self.inline_objects
                .lock()
                .await
                .remove(&object_id.to_string());
            Ok(())
        }

        async fn object_exists(
            &self,
            object_id: &lightningstor_types::ObjectId,
        ) -> StorageResult<bool> {
            Ok(self
                .inline_objects
                .lock()
                .await
                .contains_key(&object_id.to_string()))
        }

        async fn object_size(
            &self,
            object_id: &lightningstor_types::ObjectId,
        ) -> StorageResult<u64> {
            self.inline_objects
                .lock()
                .await
                .get(&object_id.to_string())
                .map(|bytes| bytes.len() as u64)
                .ok_or_else(|| StorageError::NotFound(*object_id))
        }

        async fn put_part(
            &self,
            upload_id: &str,
            part_number: u32,
            data: Bytes,
        ) -> StorageResult<()> {
            self.note_put_part_started();
            sleep(self.put_part_delay).await;
            self.multipart_parts
                .lock()
                .await
                .insert((upload_id.to_string(), part_number), data);
            self.note_put_part_finished();
            Ok(())
        }

        async fn get_part(&self, upload_id: &str, part_number: u32) -> StorageResult<Bytes> {
            self.multipart_parts
                .lock()
                .await
                .get(&(upload_id.to_string(), part_number))
                .cloned()
                .ok_or_else(|| {
                    StorageError::Backend(format!(
                        "missing multipart part {upload_id}:{part_number}"
                    ))
                })
        }

        async fn delete_part(&self, upload_id: &str, part_number: u32) -> StorageResult<()> {
            self.multipart_parts
                .lock()
                .await
                .remove(&(upload_id.to_string(), part_number));
            Ok(())
        }

        async fn delete_upload_parts(&self, upload_id: &str) -> StorageResult<()> {
            self.multipart_parts
                .lock()
                .await
                .retain(|(existing_upload_id, _), _| existing_upload_id != upload_id);
            Ok(())
        }
    }

    #[tokio::test]
    async fn large_put_streams_multipart_parts_with_parallel_uploads() {
        let storage = Arc::new(DelayedMultipartStorage::new(Duration::from_millis(25)));
        let metadata = Arc::new(MetadataStore::new_in_memory());
        let performance = S3PerformanceConfig::default();
        let router = create_router_with_auth_state(
            storage.clone(),
            metadata,
            Arc::new(AuthState::disabled()),
            performance.clone(),
        );

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::PUT)
                    .uri("/parallel-stream-bucket")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let body = vec![b'x'; (performance.streaming_put_threshold_bytes * 2) + 4096];
        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::PUT)
                    .uri("/parallel-stream-bucket/large.bin")
                    .header("content-type", "application/octet-stream")
                    .body(Body::from(body))
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        let etag = response
            .headers()
            .get("ETag")
            .and_then(|value| value.to_str().ok())
            .unwrap();
        assert!(
            etag.contains('-'),
            "expected multipart ETag for streamed upload, got {etag}"
        );
        assert!(
            storage.max_in_flight_put_parts() >= 2,
            "expected multipart uploads to overlap, saw max concurrency {}",
            storage.max_in_flight_put_parts()
        );
    }

    #[tokio::test]
    async fn moderate_put_with_content_length_stays_inline() {
        let storage = Arc::new(DelayedMultipartStorage::new(Duration::from_millis(25)));
        let metadata = Arc::new(MetadataStore::new_in_memory());
        let performance = S3PerformanceConfig::default();
        let router = create_router_with_auth_state(
            storage.clone(),
            metadata.clone(),
            Arc::new(AuthState::disabled()),
            performance.clone(),
        );

        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::PUT)
                    .uri("/inline-bucket")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let body = vec![b'y'; performance.streaming_put_threshold_bytes + 4096];
        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::PUT)
                    .uri("/inline-bucket/moderate.bin")
                    .header("content-type", "application/octet-stream")
                    .header("content-length", body.len().to_string())
                    .body(Body::from(body.clone()))
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        assert_eq!(storage.max_in_flight_put_parts(), 0);

        let bucket = metadata
            .load_bucket("default", "default", "inline-bucket")
            .await
            .unwrap()
            .unwrap();
        let bucket_id = BucketId::from_str(&bucket.id.to_string()).unwrap();
        let object = metadata
            .load_object(&bucket_id, "moderate.bin", None)
            .await
            .unwrap()
            .unwrap();
        assert!(metadata
            .load_object_multipart_upload(&object.id)
            .await
            .unwrap()
            .is_none());
        let stored = storage.get_object(&object.id).await.unwrap();
        assert_eq!(stored.as_ref(), body.as_slice());
    }
}
