use bytes::Bytes;

use crate::cached_crate_storage::DynStorage;
use crate::storage_error::StorageError;

pub struct ToolchainStorage {
    storage: DynStorage,
}

impl ToolchainStorage {
    pub fn new(storage: DynStorage) -> Self {
        Self { storage }
    }

    pub fn storage_path(date: &str, name: &str, version: &str, target: &str) -> String {
        format!("{date}/{name}-{version}-{target}.tar.xz")
    }

    /// Storage path for an individual component archive. Same format as
    /// `storage_path`, kept as a separate method for clarity at call sites.
    pub fn component_storage_path(
        date: &str,
        component: &str,
        version: &str,
        target: &str,
    ) -> String {
        Self::storage_path(date, component, version, target)
    }

    pub async fn put(
        &self,
        date: &str,
        name: &str,
        version: &str,
        target: &str,
        archive_data: Bytes,
    ) -> Result<(String, String), StorageError> {
        let path = Self::storage_path(date, name, version, target);
        let hash = sha256::digest(&archive_data[..]);

        self.storage.put(&path, archive_data).await.map_err(|e| {
            if let StorageError::S3Error(object_store::Error::AlreadyExists { .. }) = e {
                return StorageError::ToolchainArchiveExists {
                    name: name.to_string(),
                    version: version.to_string(),
                    target: target.to_string(),
                };
            }
            StorageError::ToolchainStoreFailed {
                name: name.to_string(),
                version: version.to_string(),
                target: target.to_string(),
                reason: e.to_string(),
            }
        })?;

        Ok((path, hash))
    }

    pub async fn put_raw(&self, path: &str, data: Bytes) -> Result<(), StorageError> {
        self.storage
            .put(path, data)
            .await
            .map_err(|e| StorageError::ToolchainStoreFailed {
                name: format!("component at {path}"),
                version: String::new(),
                target: String::new(),
                reason: e.to_string(),
            })
    }

    pub async fn get(&self, path: &str) -> Result<Bytes, StorageError> {
        self.storage.get(path).await.map_err(|e| {
            if matches!(
                &e,
                StorageError::S3Error(object_store::Error::NotFound { .. })
                    | StorageError::FileDoesNotExist(_)
            ) {
                return StorageError::ToolchainNotFound {
                    path: path.to_string(),
                };
            }
            StorageError::ToolchainGetFailed {
                path: path.to_string(),
                reason: e.to_string(),
            }
        })
    }

    pub async fn delete(&self, path: &str) -> Result<(), StorageError> {
        self.storage
            .delete(path)
            .await
            .map_err(|e| StorageError::ToolchainDeleteFailed {
                path: path.to_string(),
                reason: e.to_string(),
            })
    }

    pub async fn exists(&self, path: &str) -> Result<bool, StorageError> {
        self.storage.exists(path).await
    }
}
