use base64::{alphabet, engine::{self, general_purpose}, Engine};
use minio::s3::{
    args::{BucketExistsArgs, MakeBucketArgs},
    client::ClientBuilder,
    creds::StaticProvider,
    error::Error,
    http::BaseUrl,
    Client,
};
use tracing::{instrument, trace, warn};
use url::Url;

use crate::Config;
        
const CUSTOM_ENGINE: engine::GeneralPurpose = engine::GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::NO_PAD);

#[derive(Clone)]
pub struct S3 {
    bucket_name: String,
    client: Client,
}

impl S3 {
    #[instrument(skip_all, name = "S3")]
    pub async fn connect(config: &Config<'_>) -> Result<Self, Error> {
        let base_url = config
            .s3_url
            .parse::<BaseUrl>()
            .expect("Failed to parse url into BaseUrl");

        let static_provider =
            StaticProvider::new(&config.s3_access_key, &config.s3_secret_key, None);

        let client = ClientBuilder::new(base_url)
            .provider(Some(Box::new(static_provider)))
            .build()?;

        trace!("Checking bucket...");
        let exists = client
            .bucket_exists(
                &BucketExistsArgs::new(&config.s3_bucket)
                    .expect("Failed to check if bucket exists"),
            )
            .await?;

        if !exists {
            trace!("Creating bucket...");
            client
                .make_bucket(
                    &MakeBucketArgs::new(&config.s3_bucket).expect("Failed to create bucket!"),
                )
                .await?;
        }

        trace!("Connection successful");

        Ok(Self {
            bucket_name: config.s3_bucket.to_owned(),
            client: client,
        })
    }

    #[instrument(skip_all)]
    pub async fn store(&self, data: &str, url: &Url) {
        if let Some(domain) = url.domain() {
            let filename = domain.to_owned() + url.path();

            trace!("Created filename: {filename} from raw: {}", url.to_string());

            let _ = match &self
                .client
                .put_object_content(&self.bucket_name, &filename, data.to_owned())
                .send()
                .await {
                    Ok(_) => {},
                    Err(err) => {
                        match err {
                            Error::InvalidObjectName(_) => {

                                warn!("Tried storing invalid object name, retrying with Base64 encoding. Last try.");
                                
                                let filename: String = domain.to_owned() + &CUSTOM_ENGINE.encode(url.path());

                                let _ = &self
                                    .client
                                    .put_object_content(&self.bucket_name, &filename, data.to_owned())
                                    .send()
                                    .await
                                    .unwrap();
                            },
                            _ => {},
                       }
                    },
                };
        }
    }
}
