//! S3 copy operations: single-request and multipart server-side copy.

use aws_sdk_s3::Client;
use tracing::{debug, warn};

use crate::error::S3boltError;
use crate::types::S3Uri;

/// Perform a single-request server-side copy (`CopyObject`).
///
/// Suitable for objects ≤ 5 GiB.
pub async fn copy_single(
    client: &Client,
    source: &S3Uri,
    destination: &S3Uri,
    storage_class: Option<&str>,
) -> Result<String, S3boltError> {
    let copy_source = format!("{}/{}", source.bucket, source.key);

    let mut req = client
        .copy_object()
        .copy_source(&copy_source)
        .bucket(&destination.bucket)
        .key(&destination.key);

    if let Some(sc) = storage_class {
        req = req.storage_class(
            sc.parse()
                .map_err(|_| S3boltError::ConfigError(format!("invalid storage class: {sc}")))?,
        );
    }

    let resp = req.send().await.map_err(|e| S3boltError::S3Api {
        operation: "CopyObject".to_owned(),
        message: e.to_string(),
        is_retryable: is_retryable_error(&e.to_string()),
    })?;

    let e_tag = resp
        .copy_object_result()
        .and_then(|r| r.e_tag())
        .unwrap_or("")
        .to_owned();

    debug!(
        src = %source,
        dst = %destination,
        e_tag = %e_tag,
        "copy complete"
    );

    Ok(e_tag)
}

/// Perform a multipart server-side copy (`UploadPartCopy`).
///
/// Required for objects > 5 GiB. Each part is copied server-side in parallel.
pub async fn copy_multipart(
    client: &Client,
    source: &S3Uri,
    destination: &S3Uri,
    object_size: u64,
    part_size: u64,
    storage_class: Option<&str>,
) -> Result<String, S3boltError> {
    let copy_source = format!("{}/{}", source.bucket, source.key);

    // 1. Initiate multipart upload.
    let mut create_req = client
        .create_multipart_upload()
        .bucket(&destination.bucket)
        .key(&destination.key);

    if let Some(sc) = storage_class {
        create_req = create_req.storage_class(
            sc.parse()
                .map_err(|_| S3boltError::ConfigError(format!("invalid storage class: {sc}")))?,
        );
    }

    let create_resp = create_req.send().await.map_err(|e| S3boltError::S3Api {
        operation: "CreateMultipartUpload".to_owned(),
        message: e.to_string(),
        is_retryable: false,
    })?;

    let upload_id = create_resp
        .upload_id()
        .ok_or_else(|| S3boltError::MultipartFailed {
            size: object_size,
            reason: "no upload_id returned".to_owned(),
        })?
        .to_owned();

    // 2. Upload parts in parallel.
    let num_parts = object_size.div_ceil(part_size);
    let mut part_futures = Vec::with_capacity(num_parts as usize);

    for part_num in 0..num_parts {
        let start = part_num * part_size;
        let end = std::cmp::min(start + part_size - 1, object_size - 1);
        let range = format!("bytes={start}-{end}");

        let client = client.clone();
        let copy_source = copy_source.clone();
        let bucket = destination.bucket.clone();
        let key = destination.key.clone();
        let upload_id = upload_id.clone();
        let part_number = (part_num + 1) as i32; // 1-based

        part_futures.push(tokio::spawn(async move {
            let resp = client
                .upload_part_copy()
                .copy_source(&copy_source)
                .copy_source_range(&range)
                .bucket(&bucket)
                .key(&key)
                .upload_id(&upload_id)
                .part_number(part_number)
                .send()
                .await
                .map_err(|e| S3boltError::S3Api {
                    operation: "UploadPartCopy".to_owned(),
                    message: e.to_string(),
                    is_retryable: is_retryable_error(&e.to_string()),
                })?;

            let e_tag = resp
                .copy_part_result()
                .and_then(|r| r.e_tag())
                .unwrap_or("")
                .to_owned();

            Ok::<(i32, String), S3boltError>((part_number, e_tag))
        }));
    }

    // Collect part results.
    let mut completed_parts = Vec::with_capacity(num_parts as usize);
    for fut in part_futures {
        match fut.await {
            Ok(Ok((part_number, e_tag))) => {
                completed_parts.push(
                    aws_sdk_s3::types::CompletedPart::builder()
                        .part_number(part_number)
                        .e_tag(e_tag)
                        .build(),
                );
            }
            Ok(Err(e)) => {
                // Abort the multipart upload on failure.
                abort_multipart(client, destination, &upload_id).await;
                return Err(e);
            }
            Err(join_err) => {
                abort_multipart(client, destination, &upload_id).await;
                return Err(S3boltError::MultipartFailed {
                    size: object_size,
                    reason: join_err.to_string(),
                });
            }
        }
    }

    // Sort by part number.
    completed_parts.sort_by_key(|p| p.part_number());

    // 3. Complete multipart upload.
    let complete_resp = client
        .complete_multipart_upload()
        .bucket(&destination.bucket)
        .key(&destination.key)
        .upload_id(&upload_id)
        .multipart_upload(
            aws_sdk_s3::types::CompletedMultipartUpload::builder()
                .set_parts(Some(completed_parts))
                .build(),
        )
        .send()
        .await
        .map_err(|e| S3boltError::S3Api {
            operation: "CompleteMultipartUpload".to_owned(),
            message: e.to_string(),
            is_retryable: false,
        })?;

    let e_tag = complete_resp.e_tag().unwrap_or("").to_owned();

    debug!(
        src = %source,
        dst = %destination,
        parts = num_parts,
        e_tag = %e_tag,
        "multipart copy complete"
    );

    Ok(e_tag)
}

/// Abort a multipart upload (best-effort cleanup on failure).
async fn abort_multipart(client: &Client, destination: &S3Uri, upload_id: &str) {
    let result = client
        .abort_multipart_upload()
        .bucket(&destination.bucket)
        .key(&destination.key)
        .upload_id(upload_id)
        .send()
        .await;

    if let Err(e) = result {
        warn!(
            upload_id,
            dst = %destination,
            error = %e,
            "failed to abort multipart upload"
        );
    }
}

/// Heuristic to detect retryable S3 errors from error message.
fn is_retryable_error(msg: &str) -> bool {
    let lower = msg.to_lowercase();
    lower.contains("slowdown")
        || lower.contains("503")
        || lower.contains("500")
        || lower.contains("timeout")
        || lower.contains("connection reset")
        || lower.contains("broken pipe")
}
