use backon::{ExponentialBuilder, Retryable};

pub trait RetryableError {
    fn is_retryable(&self) -> bool;
}

#[derive(thiserror::Error, Debug)]
pub enum UploadError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("Invalid input: {0}")]
    InvalidInput(String),

    #[error("Backend storage error: {0}")]
    Backend(crate::storage::StorageError),
}

impl RetryableError for UploadError {
    fn is_retryable(&self) -> bool {
        match self {
            UploadError::Io(_) => false,
            UploadError::InvalidInput(_) => false,
            UploadError::Backend(e) => {
                matches!(e, crate::storage::StorageError::ServiceError(_))
            }
        }
    }
}

#[derive(thiserror::Error, Debug)]
pub enum DownloadError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("Corrupted data: {0}")]
    CorruptedData(String),

    #[error("Backend storage error: {0}")]
    Backend(crate::storage::StorageError),
}

impl RetryableError for DownloadError {
    fn is_retryable(&self) -> bool {
        match self {
            DownloadError::Io(_) => false,
            DownloadError::CorruptedData(_) => false,
            DownloadError::Backend(e) => {
                matches!(e, crate::storage::StorageError::ServiceError(_))
            }
        }
    }
}

pub async fn retry_with_backoff<F, Fut, T, E>(f: F) -> Result<T, E>
where
    F: FnMut() -> Fut,
    Fut: std::future::Future<Output = Result<T, E>>,
    E: RetryableError,
{
    let backoff = ExponentialBuilder::default()
        .with_min_delay(std::time::Duration::from_millis(200))
        .with_max_delay(std::time::Duration::from_secs(10))
        .with_max_times(5)
        .with_jitter();

    f.retry(backoff).when(|e| e.is_retryable()).await
}
