/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use interrupt_support::Interrupted;

/// This enum is to discriminate `StorageHttpError`, and not used as an error.
#[cfg(feature = "sync-client")]
#[derive(Debug, Clone)]
pub enum ErrorResponse {
    NotFound { route: String },
    // 401
    Unauthorized { route: String },
    // 412
    PreconditionFailed { route: String },
    // 5XX
    ServerError { route: String, status: u16 }, // TODO: info for "retry-after" and backoff handling etc here.
    // Other HTTP responses.
    RequestFailed { route: String, status: u16 },
}

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[cfg(feature = "crypto")]
    #[error("Key {0} had wrong length, got {1}, expected {2}")]
    BadKeyLength(&'static str, usize, usize),

    #[cfg(feature = "crypto")]
    #[error("SHA256 HMAC Mismatch error")]
    HmacMismatch,

    #[cfg(feature = "crypto")]
    #[error("Crypto/NSS error: {0}")]
    CryptoError(#[from] rc_crypto::Error),

    #[cfg(feature = "crypto")]
    #[error("Base64 decode error: {0}")]
    Base64Decode(#[from] base64::DecodeError),

    #[error("JSON error: {0}")]
    JsonError(#[from] serde_json::Error),

    #[error("Bad cleartext UTF8: {0}")]
    BadCleartextUtf8(#[from] std::string::FromUtf8Error),

    #[cfg(feature = "crypto")]
    #[error("HAWK error: {0}")]
    HawkError(#[from] rc_crypto::hawk::Error),

    //
    // Errors specific to this module.
    //
    #[cfg(feature = "sync-client")]
    #[error("HTTP status {0} when requesting a token from the tokenserver")]
    TokenserverHttpError(u16),

    #[cfg(feature = "sync-client")]
    #[error("HTTP storage error: {0:?}")]
    StorageHttpError(ErrorResponse),

    #[cfg(feature = "sync-client")]
    #[error("Server requested backoff. Retry after {0:?}")]
    BackoffError(std::time::SystemTime),

    #[cfg(feature = "sync-client")]
    #[error("Outgoing record is too large to upload")]
    RecordTooLargeError,

    // Do we want to record the concrete problems?
    #[cfg(feature = "sync-client")]
    #[error("Not all records were successfully uploaded")]
    RecordUploadFailed,

    /// Used for things like a node reassignment or an unexpected syncId
    /// implying the app needs to "reset" its understanding of remote storage.
    #[cfg(feature = "sync-client")]
    #[error("The server has reset the storage for this account")]
    StorageResetError,

    #[cfg(feature = "sync-client")]
    #[error("Unacceptable URL: {0}")]
    UnacceptableUrl(String),

    #[cfg(feature = "sync-client")]
    #[error("Missing server timestamp header in request")]
    MissingServerTimestamp,

    #[cfg(feature = "sync-client")]
    #[error("Unexpected server behavior during batch upload: {0}")]
    ServerBatchProblem(&'static str),

    #[cfg(feature = "sync-client")]
    #[error("It appears some other client is also trying to setup storage; try again later")]
    SetupRace,

    #[cfg(feature = "sync-client")]
    #[error("Client upgrade required; server storage version too new")]
    ClientUpgradeRequired,

    // This means that our global state machine needs to enter a state (such as
    // "FreshStartNeeded", but the allowed_states don't include that state.)
    // It typically means we are trying to do a "fast" or "read-only" sync.
    #[cfg(feature = "sync-client")]
    #[error("Our storage needs setting up and we can't currently do it")]
    SetupRequired,

    #[cfg(feature = "sync-client")]
    #[error("Store error: {0}")]
    StoreError(#[from] anyhow::Error),

    #[cfg(feature = "sync-client")]
    #[error("Network error: {0}")]
    RequestError(#[from] viaduct::Error),

    #[cfg(feature = "sync-client")]
    #[error("Unexpected HTTP status: {0}")]
    UnexpectedStatus(#[from] viaduct::UnexpectedStatus),

    #[cfg(feature = "sync-client")]
    #[error("URL parse error: {0}")]
    MalformedUrl(#[from] url::ParseError),

    #[error("The operation was interrupted.")]
    Interrupted(#[from] Interrupted),
}

#[cfg(feature = "sync-client")]
impl Error {
    pub(crate) fn get_backoff(&self) -> Option<std::time::SystemTime> {
        if let Error::BackoffError(time) = self {
            Some(*time)
        } else {
            None
        }
    }
}
