/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

//! Region providers that augment existing providers with new functionality

use aws_types::region::Region;
use std::borrow::Cow;
use std::fmt::Debug;
use tracing::Instrument;

/// Load a region by selecting the first from a series of region providers.
///
/// # Examples
///
/// ```no_run
/// # fn example() {
/// use aws_types::region::Region;
/// use std::env;
/// use aws_config::meta::region::RegionProviderChain;
///
/// // region provider that first checks the `CUSTOM_REGION` environment variable,
/// // then checks the default provider chain, then falls back to us-east-2
/// let provider = RegionProviderChain::first_try(env::var("CUSTOM_REGION").ok().map(Region::new))
///     .or_default_provider()
///     .or_else(Region::new("us-east-2"));
/// # }
/// ```
#[derive(Debug)]
pub struct RegionProviderChain {
    providers: Vec<Box<dyn ProvideRegion>>,
}

impl RegionProviderChain {
    /// Load a region from the provider chain
    ///
    /// The first provider to return a non-optional region will be selected
    pub async fn region(&self) -> Option<Region> {
        for provider in &self.providers {
            if let Some(region) = provider
                .region()
                .instrument(tracing::info_span!("load_region", provider = ?provider))
                .await
            {
                return Some(region);
            }
        }
        None
    }

    /// Create a default provider chain that starts by checking this provider.
    pub fn first_try(provider: impl ProvideRegion + 'static) -> Self {
        RegionProviderChain {
            providers: vec![Box::new(provider)],
        }
    }

    /// Add a fallback provider to the region provider chain.
    pub fn or_else(mut self, fallback: impl ProvideRegion + 'static) -> Self {
        self.providers.push(Box::new(fallback));
        self
    }

    /// Create a region provider chain that starts by checking the default provider.
    pub fn default_provider() -> Self {
        Self::first_try(crate::default_provider::region::default_provider())
    }

    /// Fallback to the default provider
    pub fn or_default_provider(mut self) -> Self {
        self.providers
            .push(Box::new(crate::default_provider::region::default_provider()));
        self
    }
}

impl ProvideRegion for Option<Region> {
    fn region(&self) -> future::ProvideRegion<'_> {
        future::ProvideRegion::ready(self.clone())
    }
}

impl ProvideRegion for RegionProviderChain {
    fn region(&self) -> future::ProvideRegion<'_> {
        future::ProvideRegion::new(RegionProviderChain::region(self))
    }
}

/// Future wrapper returned by [`ProvideRegion`]
///
/// Note: this module should only be used when implementing your own region providers.
pub mod future {
    use std::future::Future;
    use std::pin::Pin;
    use std::task::{Context, Poll};

    use aws_smithy_async::future::now_or_later::NowOrLater;

    use aws_types::region::Region;

    type BoxFuture<'a> = Pin<Box<dyn Future<Output = Option<Region>> + Send + 'a>>;
    /// Future returned by [`ProvideRegion`](super::ProvideRegion)
    ///
    /// - When wrapping an already loaded region, use [`ready`](ProvideRegion::ready).
    /// - When wrapping an asynchronously loaded region, use [`new`](ProvideRegion::new).
    #[derive(Debug)]
    pub struct ProvideRegion<'a>(NowOrLater<Option<Region>, BoxFuture<'a>>);
    impl<'a> ProvideRegion<'a> {
        /// A future that wraps the given future
        pub fn new(future: impl Future<Output = Option<Region>> + Send + 'a) -> Self {
            Self(NowOrLater::new(Box::pin(future)))
        }

        /// A future that resolves to a given region
        pub fn ready(region: Option<Region>) -> Self {
            Self(NowOrLater::ready(region))
        }
    }

    impl Future for ProvideRegion<'_> {
        type Output = Option<Region>;

        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
            Pin::new(&mut self.0).poll(cx)
        }
    }
}

/// Provide a [`Region`] to use with AWS requests
///
/// For most cases [`default_provider`](crate::default_provider::region::default_provider) will be the best option, implementing
/// a standard provider chain.
pub trait ProvideRegion: Send + Sync + Debug {
    /// Load a region from this provider
    fn region(&self) -> future::ProvideRegion<'_>;
}

impl ProvideRegion for Region {
    fn region(&self) -> future::ProvideRegion<'_> {
        future::ProvideRegion::ready(Some(self.clone()))
    }
}

impl<'a> ProvideRegion for &'a Region {
    fn region(&self) -> future::ProvideRegion<'_> {
        future::ProvideRegion::ready(Some((*self).clone()))
    }
}

impl ProvideRegion for Box<dyn ProvideRegion> {
    fn region(&self) -> future::ProvideRegion<'_> {
        self.as_ref().region()
    }
}

impl ProvideRegion for &'static str {
    fn region(&self) -> future::ProvideRegion<'_> {
        future::ProvideRegion::ready(Some(Region::new(Cow::Borrowed(*self))))
    }
}

#[cfg(test)]
mod test {
    use crate::meta::region::RegionProviderChain;
    use aws_types::region::Region;
    use futures_util::FutureExt;

    #[test]
    fn provider_chain() {
        let a = None;
        let b = Some(Region::new("us-east-1"));
        let chain = RegionProviderChain::first_try(a).or_else(b);
        assert_eq!(
            chain.region().now_or_never().expect("ready"),
            Some(Region::new("us-east-1"))
        );
    }

    #[test]
    fn empty_chain() {
        let chain = RegionProviderChain::first_try(None).or_else(None);
        assert_eq!(chain.region().now_or_never().expect("ready"), None);
    }
}
