use metrics::counter;
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::{db::Website, Config, Timer};

const S3_ROUND_TRIP_METRIC: &'static str = "s3_trips";

#[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?;
        counter!(S3_ROUND_TRIP_METRIC).increment(1);

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

        trace!("Connection successful");

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

    #[instrument(name = "s3_store", skip_all)]
    pub async fn store(&self, data: &str, url: &Url) {
        let counter = counter!(S3_ROUND_TRIP_METRIC);
        let t = Timer::start("Stored page");
        let _ = t; // prevent compiler drop

        let filename = Website::get_url_as_string(url);
        trace!("Storing {} as {filename}", url.to_string());

        counter.increment(1);
        let _ = match &self
            .client
            .put_object_content(&self.bucket_name, &filename, data.to_owned())
            .send()
            .await
        {
            Ok(_) => {}
            Err(err) => match err {
                Error::InvalidObjectName(_) => {
                    // This code will really only run if the url has non-english chars
                    warn!("Tried storing invalid object name, retrying with Base64 encoding. Last try.");

                    let filename: String = Website::get_url_as_b64_path(url);

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