use thiserror::Error;

/// generic google error message
#[derive(Error, Debug)]
#[error("failed to use fdfe ({0})")]
pub struct GoogleError(pub String);

/// ini parse error
#[derive(Error, Debug)]
pub enum PropParseError {
    #[error("failed to parse ini ({0})")]
    InvalidIni(#[from] ini::ParseError),
}

/// generic http client error
#[derive(Error, Debug)]
pub enum GenericRequestError {
    #[error("failed to parse URL ({0})")]
    BadUri(#[from] url::ParseError),

    #[error("bad header name ({0})")]
    BadHeaderName(#[from] http::header::InvalidHeaderName),

    #[error("bad header value ({0})")]
    BadHeaderValue(#[from] http::header::InvalidHeaderValue),

    #[error("google returned an invalid response ({0})")]
    BadResponseProto(#[from] prost::DecodeError),

    #[error("failed to fetch resource ({0})")]
    HttpError(#[from] reqwest::Error),
}

/// fetch aas error
#[derive(Error, Debug)]
pub enum FetchAasError {
    #[error("request failed ({0})")]
    Request(#[from] GenericRequestError),

    #[error("google returned an error ({0})")]
    Google(#[from] GoogleError),
}

/// internal checkin error
#[derive(Error, Debug)]
pub enum CheckinError {
    #[error("system clock went back in time ({0})")]
    InvalidSystemTime(#[from] std::time::SystemTimeError),

    #[error("no build was provided for device props")]
    NoBuild(),

    #[error("no device string was provided for device props")]
    NoDevice(),

    #[error("google returned an invalid response ({0})!")]
    BadResponse(#[from] prost::DecodeError),

    #[error("props error ({0})")]
    Props(#[from] PropsError),

    #[error("failed to fetch resource ({0})")]
    HttpError(#[from] reqwest::Error),

    #[error("request failed ({0})")]
    Request(#[from] GenericRequestError),
}

/// login error
#[derive(Error, Debug)]
pub enum LoginError {
    #[error("checkin failed ({0})")]
    Checkin(#[from] CheckinError),

    #[error("upload device config failed ({0})")]
    Upload(#[from] UploadDeviceError),

    #[error("auth fetch failed ({0})")]
    Auth(#[from] FetchAuthError),

    #[error("TOC fetch / TOS accept failed ({0})")]
    Toc(#[from] TocError),

    #[error("google returned no 'device_checkin_consistency_token'")]
    NoDeviceToken(),

    #[error("google returned no 'android_id'")]
    NoAndroidId(),

    #[error("google returned no 'upload_device_config_token'")]
    NoConfigToken(),
}

/// internal upload device error
#[derive(Error, Debug)]
pub enum UploadDeviceError {
    #[error("props error ({0})")]
    Props(#[from] PropsError),

    #[error("google returned an invalid response")]
    InvalidResponse(#[from] prost::DecodeError),

    #[error("google returned an error ({0})")]
    Google(#[from] GoogleError),

    #[error("request failed ({0})")]
    Request(#[from] GenericRequestError),
}

/// internal auth fetch error
#[derive(Error, Debug)]
pub enum FetchAuthError {
    #[error("google returned an error ({0})")]
    Google(#[from] GoogleError),

    #[error("google returned no expire time")]
    NoExpireTime(),

    #[error("google returned invalid expire time ({0})")]
    InvalidExpireTime(#[from] std::num::ParseIntError),

    #[error("props error ({0})")]
    Props(#[from] PropsError),

    #[error("request failed ({0})")]
    Request(#[from] GenericRequestError),
}

/// non-comprehensive generic props error
#[derive(Error, Debug)]
pub enum PropsError {
    #[error("failed to renew auth token ({0})")]
    UpdateAuth(#[from] Box<FetchAuthError>),

    #[error("no props were provided")]
    NoProps(),

    #[error("no build was provided")]
    NoBuild(),

    #[error("no build sdk version was provided")]
    NoBuildSDK(),

    #[error("no build device was provided")]
    NoBuildDevice(),

    #[error("no build model was provided")]
    NoBuildModel(),

    #[error("no build product was provided")]
    NoBuildProduct(),

    #[error("no 'Vending.versionString' was provided")]
    NoVersionString(),

    #[error("no 'Build.VERSION.RELEASE' was provided")]
    NoVersionRelease(),

    #[error("no 'Vending.version' was provided")]
    NoVersion(),

    #[error("no 'Build.ID' was provided")]
    NoBuildId(),

    #[error("no 'SimOperator' was provided")]
    NoSimOperator(),
}

/// internal terms of condition error
#[derive(Error, Debug)]
pub enum TocError {
    #[error("request failed ({0})")]
    Request(#[from] GenericRequestError),

    #[error("props error ({0})")]
    Props(#[from] PropsError),

    #[error("google returned an error ({0})")]
    Google(#[from] GoogleError),

    #[error("google returned an invalid response ({0})")]
    InvalidResponse(#[from] prost::DecodeError),
}

/// details error
#[derive(Error, Debug)]
pub enum DetailsError {
    #[error("google returned an error ({0})")]
    Google(#[from] GoogleError),

    #[error("props error ({0})")]
    Props(#[from] PropsError),

    #[error("request failed ({0})")]
    Request(#[from] GenericRequestError),
}

/// downloads error
#[derive(Error, Debug)]
pub enum DownloadsError {
    #[error("fetching latest version failed ({0})")]
    VersionFetchError(#[from] DetailsError),

    #[error("google returned an error ({0})")]
    Google(#[from] GoogleError),

    #[error("header error ({0})")]
    Props(#[from] PropsError),

    #[error("request failed ({0})")]
    Request(#[from] GenericRequestError),
}

/// category error
#[derive(Error, Debug)]
pub enum CategoryError {
    #[error("header error ({0})")]
    Props(#[from] PropsError),

    #[error("google returned an error ({0})")]
    Google(#[from] GoogleError),

    #[error("Got request error {0}!")]
    Request(#[from] GenericRequestError),
}

/// search suggestion error
#[derive(Error, Debug)]
pub enum SearchSuggestionError {
    #[error("header error ({0})")]
    Props(#[from] PropsError),

    #[error("google returned an error ({0})")]
    Google(#[from] GoogleError),

    #[error("Got request error {0}!")]
    Request(#[from] GenericRequestError),
}

/// search error
#[derive(Error, Debug)]
pub enum SearchError {
    #[error("header error ({0})")]
    Props(#[from] PropsError),

    #[error("google returned an error ({0})")]
    Google(#[from] GoogleError),

    #[error("Got request error {0}!")]
    Request(#[from] GenericRequestError),
}

/// reviews error
#[derive(Error, Debug)]
pub enum ReviewsError {
    #[error("header error ({0})")]
    Props(#[from] PropsError),

    #[error("google returned an error ({0})")]
    Google(#[from] GoogleError),

    #[error("Got request error {0}!")]
    Request(#[from] GenericRequestError),
}

pub(crate) trait IntoGoogleError<A> {
    fn into_google_error(self, response: &crate::proto::ResponseWrapper) -> Result<A, GoogleError>;
    fn into_google_error_map(self, function: impl FnOnce() -> String) -> Result<A, GoogleError>;
}

impl<A> IntoGoogleError<A> for Option<A> {
    fn into_google_error(self, response: &crate::proto::ResponseWrapper) -> Result<A, GoogleError> {
        self.map_or_else(
            || {
                Err(GoogleError(response.commands.as_ref().map_or_else(
                    || "Google returned... nothing?".to_string(),
                    |a| {
                        a.display_error_message
                            .clone()
                            .unwrap_or_else(|| "Google returned no error!".to_string())
                    },
                )))
            },
            Ok,
        )
    }

    fn into_google_error_map(self, function: impl FnOnce() -> String) -> Result<A, GoogleError> {
        self.map_or_else(|| Err(GoogleError(function())), Ok)
    }
}
