//! S3/Garage storage service for media uploads

use bytes::Bytes;
use s3::creds::Credentials;
use s3::error::S3Error as RustS3Error;
use s3::{Bucket, Region};
use thiserror::Error;

use crate::config::Config;

/// S3 service errors
#[derive(Error, Debug)]
pub enum S3Error {
    #[error("S3 upload failed: {0}")]
    UploadFailed(String),

    #[error("S3 delete failed: {0}")]
    DeleteFailed(String),

    #[error("S3 get failed: {0}")]
    GetFailed(String),

    #[error("S3 not configured")]
    NotConfigured,

    #[error("S3 credentials error: {0}")]
    CredentialsError(String),

    #[error("S3 bucket error: {0}")]
    BucketError(String),
}

impl From<RustS3Error> for S3Error {
    fn from(e: RustS3Error) -> Self {
        S3Error::BucketError(e.to_string())
    }
}

/// S3/Garage storage service
#[derive(Clone)]
pub struct S3Service {
    bucket: Box<Bucket>,
    bucket_name: String,
    endpoint: String,
}

impl S3Service {
    /// Create a new S3 service from configuration
    pub async fn new(config: &Config) -> Result<Self, S3Error> {
        if config.s3_endpoint.is_empty() || config.s3_access_key.is_empty() {
            return Err(S3Error::NotConfigured);
        }

        let credentials = Credentials::new(
            Some(&config.s3_access_key),
            Some(&config.s3_secret_key),
            None,
            None,
            None,
        )
        .map_err(|e| S3Error::CredentialsError(e.to_string()))?;

        let region = Region::Custom {
            region: "garage".to_string(),
            endpoint: config.s3_endpoint.clone(),
        };

        let bucket = Bucket::new(&config.s3_bucket, region, credentials)
            .map_err(|e| S3Error::BucketError(e.to_string()))?
            .with_path_style();

        Ok(Self {
            bucket,
            bucket_name: config.s3_bucket.clone(),
            endpoint: config.s3_endpoint.clone(),
        })
    }

    /// Upload a file to S3
    ///
    /// Returns the key (path) where the file was stored
    pub async fn upload_file(
        &self,
        key: &str,
        data: Bytes,
        content_type: &str,
    ) -> Result<String, S3Error> {
        self.bucket
            .put_object_with_content_type(key, &data, content_type)
            .await
            .map_err(|e| S3Error::UploadFailed(e.to_string()))?;

        Ok(key.to_string())
    }

    /// Delete a file from S3
    pub async fn delete_file(&self, key: &str) -> Result<(), S3Error> {
        self.bucket
            .delete_object(key)
            .await
            .map_err(|e| S3Error::DeleteFailed(e.to_string()))?;

        Ok(())
    }

    /// Get a file from S3
    ///
    /// Returns the file bytes and content type
    pub async fn get_file(&self, key: &str) -> Result<(Bytes, String), S3Error> {
        let response = self
            .bucket
            .get_object(key)
            .await
            .map_err(|e| S3Error::GetFailed(e.to_string()))?;

        let content_type = response
            .headers()
            .get("content-type")
            .map(|s| s.to_string())
            .unwrap_or_else(|| "application/octet-stream".to_string());

        Ok((Bytes::from(response.to_vec()), content_type))
    }

    /// Check if a file exists in S3
    pub async fn file_exists(&self, key: &str) -> bool {
        self.bucket.head_object(key).await.is_ok()
    }

    /// Get the bucket name
    pub fn bucket(&self) -> &str {
        &self.bucket_name
    }

    /// Get the endpoint URL
    pub fn endpoint(&self) -> &str {
        &self.endpoint
    }
}

impl std::fmt::Debug for S3Service {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("S3Service")
            .field("bucket", &self.bucket_name)
            .field("endpoint", &self.endpoint)
            .finish()
    }
}
