//! Declare an error type for tor-circmgr

use std::sync::Arc;

use futures::task::SpawnError;
use retry_error::RetryError;
use thiserror::Error;

use tor_error::{Bug, ErrorKind, HasKind};
use tor_linkspec::OwnedChanTarget;

/// An error returned while looking up or building a circuit
#[derive(Error, Debug, Clone)]
#[non_exhaustive]
pub enum Error {
    /// We started building a circuit on a guard, but later decided not
    /// to use that guard.
    //
    // TODO: We shouldn't count this as an error for the purposes of the number
    // of allowable failures of a circuit request.
    #[error("Discarded circuit because of speculative guard selection")]
    GuardNotUsable,

    /// We were waiting on a pending circuit, but it failed to report
    #[error("Pending circuit(s) failed without reporting status")]
    PendingCanceled,

    /// A circuit succeeded, but was cancelled before it could be used.
    ///
    /// Circuits can be cancelled either by a call to
    /// `retire_all_circuits()`, or by a configuration change that
    /// makes old paths unusable.
    //
    // TODO: We shouldn't count this as an error for the purposes of the number
    // of allowable failures of a circuit request.
    #[error("Circuit canceled")]
    CircCanceled,

    /// A circuit build took too long to finish.
    #[error("Circuit took too long to build")]
    CircTimeout,

    /// A request spent too long waiting for a circuit
    #[error("Spent too long waiting for a circuit to build")]
    RequestTimeout,

    /// No suitable relays for a request
    #[error("Can't build path for circuit: {0}")]
    NoPath(String),

    /// No suitable exit relay for a request.
    #[error("Can't find exit for circuit: {0}")]
    NoExit(String),

    /// Problem creating or updating a guard manager.
    #[error("Problem creating or updating guards list: {0}")]
    GuardMgr(#[source] tor_guardmgr::GuardMgrError),

    /// Problem selecting a guard relay.
    #[error("Unable to select a guard relay: {0}")]
    Guard(#[from] tor_guardmgr::PickGuardError),

    /// Unable to get or build a circuit, despite retrying.
    #[error("{0}")]
    RequestFailed(RetryError<Box<Error>>),

    /// Problem with channel
    #[error("Problem with channel to {peer}")]
    Channel {
        /// Which relay we were trying to connect to
        peer: OwnedChanTarget,

        /// What went wrong
        #[source]
        cause: tor_chanmgr::Error,
    },

    /// Protocol issue while building a circuit.
    #[error("Problem building a circuit with {peer:?}")]
    Protocol {
        /// The peer that created the protocol error.
        ///
        /// This is set to None if we can't blame a single party.
        peer: Option<OwnedChanTarget>,
        /// The underlying error.
        #[source]
        error: tor_proto::Error,
    },

    /// We have an expired consensus
    #[error("Consensus is expired")]
    ExpiredConsensus,

    /// Unable to spawn task
    #[error("unable to spawn {spawning}")]
    Spawn {
        /// What we were trying to spawn
        spawning: &'static str,
        /// What happened when we tried to spawn it.
        #[source]
        cause: Arc<SpawnError>,
    },

    /// Problem loading or storing persistent state.
    #[error("Problem loading or storing state: {0}")]
    State(#[from] tor_persist::Error),

    /// An error caused by a programming issue . or a failure in another
    /// library that we can't work around.
    #[error("Programming issue: {0}")]
    Bug(#[from] Bug),
}

impl From<futures::channel::oneshot::Canceled> for Error {
    fn from(_: futures::channel::oneshot::Canceled) -> Error {
        Error::PendingCanceled
    }
}

impl From<tor_rtcompat::TimeoutError> for Error {
    fn from(_: tor_rtcompat::TimeoutError) -> Error {
        Error::CircTimeout
    }
}

impl From<tor_guardmgr::GuardMgrError> for Error {
    fn from(err: tor_guardmgr::GuardMgrError) -> Error {
        match err {
            tor_guardmgr::GuardMgrError::State(e) => Error::State(e),
            _ => Error::GuardMgr(err),
        }
    }
}

impl HasKind for Error {
    fn kind(&self) -> ErrorKind {
        use Error as E;
        use ErrorKind as EK;
        match self {
            E::Channel { cause, .. } => cause.kind(),
            E::Bug(e) => e.kind(),
            E::NoPath(_) => EK::NoPath,
            E::NoExit(_) => EK::NoExit,
            E::PendingCanceled => EK::ReactorShuttingDown,
            E::CircTimeout => EK::TorNetworkTimeout,
            E::GuardNotUsable => EK::TransientFailure,
            E::RequestTimeout => EK::TorNetworkTimeout,
            E::RequestFailed(e) => e
                .sources()
                .max_by_key(|e| e.severity())
                .map(|e| e.kind())
                .unwrap_or(EK::Internal),
            E::CircCanceled => EK::TransientFailure,
            E::Protocol { error, .. } => error.kind(),
            E::State(e) => e.kind(),
            E::GuardMgr(e) => e.kind(),
            E::Guard(e) => e.kind(),
            E::ExpiredConsensus => EK::DirectoryExpired,
            E::Spawn { cause, .. } => cause.kind(),
        }
    }
}

impl Error {
    /// Construct a new `Error` from a `SpawnError`.
    pub(crate) fn from_spawn(spawning: &'static str, err: SpawnError) -> Error {
        Error::Spawn {
            spawning,
            cause: Arc::new(err),
        }
    }

    /// Return an integer representing the relative severity of this error.
    ///
    /// Used to determine which error to use when determining the kind of a retry error.
    fn severity(&self) -> usize {
        use Error as E;
        match self {
            E::GuardNotUsable => 10,
            E::PendingCanceled => 20,
            E::CircCanceled => 20,
            E::CircTimeout => 30,
            E::RequestTimeout => 30,
            E::NoPath(_) => 40,
            E::NoExit(_) => 40,
            E::GuardMgr(_) => 40,
            E::Guard(_) => 40,
            E::RequestFailed(_) => 40,
            E::Channel { .. } => 40,
            E::Protocol { .. } => 45,
            E::ExpiredConsensus => 50,
            E::Spawn { .. } => 90,
            E::State(_) => 90,
            E::Bug(_) => 100,
        }
    }

    /// Return a list of the peers to "blame" for this error, if there are any.
    pub fn peers(&self) -> Vec<&OwnedChanTarget> {
        match self {
            Error::RequestFailed(errors) => errors.sources().flat_map(|e| e.peers()).collect(),
            Error::Channel { peer, .. } => vec![peer],
            Error::Protocol {
                peer: Some(peer), ..
            } => vec![peer],
            _ => vec![],
        }
    }
}
