//! Types and traits governing whether requests should procede

use std::future::Future;
use url::Url;

/// Describes types that track requests
///
/// The Session type can be included by downstream client implementations as a generic middleware
/// interface for deciding when requests should or should not continue.
///
/// An included [`RequestCountSession`] type is included, which simply counts how many requests the
/// session has observed and indicates future requests shouldn't be executed after it's limit is
/// hit.
pub trait Session {
    /// Returns false if a request to the given URL should not procede
    fn should_procede(&mut self, url: &Url) -> bool;

    /// Update the session from a successful request
    fn mark_success(&mut self, url: &Url);

    /// Update the session from a failed request
    fn mark_failure(&mut self, url: &Url);
}

/// Describes types that can produce Sessions
pub trait SessionFactory {
    /// The Session being produced
    type Session: Session;

    /// Produce the session
    fn build_session(&self) -> Self::Session;
}

/// A Session that limits the total number of requests allowed to procede
///
/// This type is useful in the case of resolving dependencies of an object. A limit can be set that
/// decides when the resolver has recursed too far.
pub struct RequestCountSession {
    count: usize,
    max_requests: usize,
}

/// A simple error type that occurs when a session decides not to procede with a request
#[derive(Clone, Debug)]
pub struct SessionError;

/// A simple guard method that can be used by downstream client and repo implementations for
/// guarding their requests
pub async fn guard<Fut, T, E, S>(fut: Fut, url: &Url, mut session: S) -> Result<T, E>
where
    Fut: Future<Output = Result<T, E>>,
    E: From<SessionError>,
    S: Session,
{
    if !session.should_procede(url) {
        return Err(SessionError.into());
    }

    match fut.await {
        Ok(t) => {
            session.mark_success(url);
            Ok(t)
        }
        Err(e) => {
            session.mark_failure(url);
            Err(e)
        }
    }
}

impl RequestCountSession {
    /// Create a new RequestCountSession with a maximum number of allowed requests
    ///
    /// Generically, this session should be created new for each set of client requests
    pub fn max(max_requests: usize) -> Self {
        Self {
            count: 0,
            max_requests,
        }
    }
}

impl std::fmt::Display for SessionError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "SessionError")
    }
}

impl std::error::Error for SessionError {}

impl Session for RequestCountSession {
    fn should_procede(&mut self, _: &Url) -> bool {
        self.count < self.max_requests
    }

    fn mark_success(&mut self, _: &Url) {
        self.count += 1;
    }

    fn mark_failure(&mut self, _: &Url) {
        self.count += 1;
    }
}

impl Session for () {
    fn should_procede(&mut self, _: &Url) -> bool {
        true
    }

    fn mark_success(&mut self, _: &Url) {}
    fn mark_failure(&mut self, _: &Url) {}
}

impl<'a, T> Session for &'a mut T
where
    T: Session,
{
    fn should_procede(&mut self, url: &Url) -> bool {
        T::should_procede(self, url)
    }

    fn mark_success(&mut self, url: &Url) {
        T::mark_success(self, url)
    }

    fn mark_failure(&mut self, url: &Url) {
        T::mark_failure(self, url)
    }
}

impl<T> Session for Box<T>
where
    T: Session,
{
    fn should_procede(&mut self, url: &Url) -> bool {
        T::should_procede(self, url)
    }

    fn mark_success(&mut self, url: &Url) {
        T::mark_success(self, url)
    }

    fn mark_failure(&mut self, url: &Url) {
        T::mark_failure(self, url)
    }
}

impl<T, U> Session for (T, U)
where
    T: Session,
    U: Session,
{
    fn should_procede(&mut self, url: &Url) -> bool {
        self.0.should_procede(url) && self.1.should_procede(url)
    }

    fn mark_success(&mut self, url: &Url) {
        self.0.mark_success(url);
        self.1.mark_success(url);
    }

    fn mark_failure(&mut self, url: &Url) {
        self.0.mark_failure(url);
        self.1.mark_failure(url);
    }
}

impl<T, U, V> Session for (T, U, V)
where
    T: Session,
    U: Session,
    V: Session,
{
    fn should_procede(&mut self, url: &Url) -> bool {
        self.0.should_procede(url) && self.1.should_procede(url) && self.2.should_procede(url)
    }

    fn mark_success(&mut self, url: &Url) {
        self.0.mark_success(url);
        self.1.mark_success(url);
        self.2.mark_success(url);
    }

    fn mark_failure(&mut self, url: &Url) {
        self.0.mark_failure(url);
        self.1.mark_failure(url);
        self.2.mark_failure(url);
    }
}

impl<T, U, V, W> Session for (T, U, V, W)
where
    T: Session,
    U: Session,
    V: Session,
    W: Session,
{
    fn should_procede(&mut self, url: &Url) -> bool {
        self.0.should_procede(url)
            && self.1.should_procede(url)
            && self.2.should_procede(url)
            && self.3.should_procede(url)
    }

    fn mark_success(&mut self, url: &Url) {
        self.0.mark_success(url);
        self.1.mark_success(url);
        self.2.mark_success(url);
        self.3.mark_success(url);
    }

    fn mark_failure(&mut self, url: &Url) {
        self.0.mark_failure(url);
        self.1.mark_failure(url);
        self.2.mark_failure(url);
        self.3.mark_failure(url);
    }
}

impl<T, U, V, W, X> Session for (T, U, V, W, X)
where
    T: Session,
    U: Session,
    V: Session,
    W: Session,
    X: Session,
{
    fn should_procede(&mut self, url: &Url) -> bool {
        self.0.should_procede(url)
            && self.1.should_procede(url)
            && self.2.should_procede(url)
            && self.3.should_procede(url)
            && self.4.should_procede(url)
    }

    fn mark_success(&mut self, url: &Url) {
        self.0.mark_success(url);
        self.1.mark_success(url);
        self.2.mark_success(url);
        self.3.mark_success(url);
        self.4.mark_success(url);
    }

    fn mark_failure(&mut self, url: &Url) {
        self.0.mark_failure(url);
        self.1.mark_failure(url);
        self.2.mark_failure(url);
        self.3.mark_failure(url);
        self.4.mark_failure(url);
    }
}

impl<T, U, V, W, X, Y> Session for (T, U, V, W, X, Y)
where
    T: Session,
    U: Session,
    V: Session,
    W: Session,
    X: Session,
    Y: Session,
{
    fn should_procede(&mut self, url: &Url) -> bool {
        self.0.should_procede(url)
            && self.1.should_procede(url)
            && self.2.should_procede(url)
            && self.3.should_procede(url)
            && self.4.should_procede(url)
            && self.5.should_procede(url)
    }

    fn mark_success(&mut self, url: &Url) {
        self.0.mark_success(url);
        self.1.mark_success(url);
        self.2.mark_success(url);
        self.3.mark_success(url);
        self.4.mark_success(url);
        self.5.mark_success(url);
    }

    fn mark_failure(&mut self, url: &Url) {
        self.0.mark_failure(url);
        self.1.mark_failure(url);
        self.2.mark_failure(url);
        self.3.mark_failure(url);
        self.4.mark_failure(url);
        self.5.mark_failure(url);
    }
}

impl<T, U, V, W, X, Y, Z> Session for (T, U, V, W, X, Y, Z)
where
    T: Session,
    U: Session,
    V: Session,
    W: Session,
    X: Session,
    Y: Session,
    Z: Session,
{
    fn should_procede(&mut self, url: &Url) -> bool {
        self.0.should_procede(url)
            && self.1.should_procede(url)
            && self.2.should_procede(url)
            && self.3.should_procede(url)
            && self.4.should_procede(url)
            && self.5.should_procede(url)
            && self.6.should_procede(url)
    }

    fn mark_success(&mut self, url: &Url) {
        self.0.mark_success(url);
        self.1.mark_success(url);
        self.2.mark_success(url);
        self.3.mark_success(url);
        self.4.mark_success(url);
        self.5.mark_success(url);
        self.6.mark_success(url);
    }

    fn mark_failure(&mut self, url: &Url) {
        self.0.mark_failure(url);
        self.1.mark_failure(url);
        self.2.mark_failure(url);
        self.3.mark_failure(url);
        self.4.mark_failure(url);
        self.5.mark_failure(url);
        self.6.mark_failure(url);
    }
}
