//! Real S3 implementation backed by the AWS SDK.

use std::future::Future;
use std::pin::Pin;

use aws_sdk_s3::Client;
use tracing::debug;

use crate::error::S3boltError;
use crate::s3::ops::S3Operations;
use crate::types::{ObjectInfo, S3Uri};

/// Real S3 implementation using `aws-sdk-s3`.
#[derive(Debug, Clone)]
pub struct RealS3 {
    /// Client used for listing and reading (source).
    pub source: Client,
    /// Client used for writing (destination). May be the same as source.
    pub dest: Client,
}

impl S3Operations for RealS3 {
    fn list_objects(
        &self,
        uri: &S3Uri,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<ObjectInfo>, S3boltError>> + Send + '_>> {
        let uri = uri.clone();
        Box::pin(async move {
            let mut objects = Vec::new();
            let mut paginator = self
                .source
                .list_objects_v2()
                .bucket(&uri.bucket)
                .prefix(&uri.key)
                .into_paginator()
                .send();

            while let Some(page) = paginator.next().await {
                let page = page.map_err(|e| S3boltError::S3Api {
                    operation: "ListObjectsV2".to_owned(),
                    message: e.to_string(),
                    is_retryable: true,
                })?;

                if let Some(contents) = page.contents {
                    for obj in contents {
                        objects.push(ObjectInfo {
                            key: obj.key.unwrap_or_default(),
                            size: obj.size.unwrap_or(0) as u64,
                            e_tag: obj.e_tag,
                            last_modified: obj.last_modified.map(|t| {
                                chrono::DateTime::from_timestamp(t.secs(), t.subsec_nanos())
                                    .unwrap_or_default()
                            }),
                            storage_class: obj.storage_class.map(|s| s.to_string()),
                        });
                    }
                }
            }

            debug!(count = objects.len(), "listed objects");
            Ok(objects)
        })
    }

    fn copy_object(
        &self,
        source: &S3Uri,
        destination: &S3Uri,
        storage_class: Option<&str>,
    ) -> Pin<Box<dyn Future<Output = Result<String, S3boltError>> + Send + '_>> {
        let source = source.clone();
        let destination = destination.clone();
        let storage_class = storage_class.map(|s| s.to_owned());

        Box::pin(async move {
            crate::s3::copier::copy_single(
                &self.dest,
                &source,
                &destination,
                storage_class.as_deref(),
            )
            .await
        })
    }

    fn copy_object_multipart(
        &self,
        source: &S3Uri,
        destination: &S3Uri,
        object_size: u64,
        part_size: u64,
        storage_class: Option<&str>,
    ) -> Pin<Box<dyn Future<Output = Result<String, S3boltError>> + Send + '_>> {
        let source = source.clone();
        let destination = destination.clone();
        let storage_class = storage_class.map(|s| s.to_owned());

        Box::pin(async move {
            crate::s3::copier::copy_multipart(
                &self.dest,
                &source,
                &destination,
                object_size,
                part_size,
                storage_class.as_deref(),
            )
            .await
        })
    }

    fn head_object(
        &self,
        uri: &S3Uri,
    ) -> Pin<Box<dyn Future<Output = Result<Option<(String, u64)>, S3boltError>> + Send + '_>> {
        let uri = uri.clone();

        Box::pin(async move {
            let resp = self
                .dest
                .head_object()
                .bucket(&uri.bucket)
                .key(&uri.key)
                .send()
                .await;

            match resp {
                Ok(output) => {
                    let e_tag = output.e_tag().unwrap_or("").to_owned();
                    let size = output.content_length().unwrap_or(0) as u64;
                    Ok(Some((e_tag, size)))
                }
                Err(e) => {
                    let msg = e.to_string();
                    if msg.contains("404") || msg.contains("NoSuchKey") || msg.contains("NotFound")
                    {
                        Ok(None)
                    } else {
                        Err(S3boltError::S3Api {
                            operation: "HeadObject".to_owned(),
                            message: msg,
                            is_retryable: false,
                        })
                    }
                }
            }
        })
    }
}
