use std::path::PathBuf;

use thiserror::Error;

use crate::RequestError;

/// Quickly implement `From<>` the "big 3" generic error types
/// ([`JsonFileError`], [`JsonDownloadError`], [`DownloadFileError`])
/// for your error types.
///
/// Pass in the name of your error `enum` and the specific variants in the correct order.
/// The JRI stands for the order of arguments: "Json, Request, Io".
///
/// Usage:
///
/// ```no_run
/// use ql_core::{impl_3_errs_jri, IoError, JsonError, RequestError};
///
/// enum MyError {
///     IoErr(IoError),
///     Req(RequestError),
///     Json(JsonError)
/// }
///
/// impl_3_errs_jri!(MyError, Json, Req, IoErr);
/// // impl From<ql_core::JsonDownloadError> for MyError
/// // impl From<ql_core::JsonFileError> for MyError
/// // impl From<ql_core::DownloadFileError> for MyError
/// ```
#[macro_export]
macro_rules! impl_3_errs_jri {
    ($target:ident, $json_variant:ident, $request_variant:ident, $io_variant:ident) => {
        impl From<$crate::JsonFileError> for $target {
            fn from(value: $crate::JsonFileError) -> Self {
                match value {
                    $crate::JsonFileError::SerdeError(err) => Self::$json_variant(err),
                    $crate::JsonFileError::Io(err) => Self::$io_variant(err),
                }
            }
        }
        impl From<$crate::JsonDownloadError> for $target {
            fn from(value: $crate::JsonDownloadError) -> Self {
                match value {
                    $crate::JsonDownloadError::RequestError(err) => Self::$request_variant(err),
                    $crate::JsonDownloadError::SerdeError(err) => Self::$json_variant(err),
                }
            }
        }
        impl From<$crate::DownloadFileError> for $target {
            fn from(value: $crate::DownloadFileError) -> Self {
                match value {
                    $crate::DownloadFileError::Request(err) => Self::$request_variant(err),
                    $crate::DownloadFileError::Io(err) => Self::$io_variant(err),
                }
            }
        }
    };
}

#[derive(Debug, Error)]
pub enum IoError {
    #[error("at path {path:?}, error: {error}")]
    Io {
        error: std::io::Error,
        path: PathBuf,
    },
    #[error("at path: {path}\nfrom url: {url}\n\nerror: {error}")]
    FromUrl {
        error: std::io::Error,
        path: PathBuf,
        url: String,
    },
    #[error("couldn't read directory {parent:?}, error {error}")]
    ReadDir { error: String, parent: PathBuf },
    #[error(".local/share or AppData directory not found")]
    LauncherDirNotFound,
    #[error("directory is outside parent directory. POTENTIAL SECURITY RISK AVOIDED")]
    DirEscapeAttack,
}

/// Converts any `std::io::Result<T>` into
/// `Result<T, IoError>`.
///
/// This allows you to use our [`IoError`] type
/// which has more context.
///
/// # Example
///
/// ```no_run
/// # use std::path::Path;
/// let p = Path::from("some_file.txt");
/// std::fs::write(p, "hi").path(p)?;
/// // Here, if this fails, the error message
/// // will tell you what path it tried writing to.
/// # Ok(())
/// ```
pub trait IntoIoError<T = ()> {
    type Output;
    #[allow(clippy::missing_errors_doc)]
    fn path(self, p: impl Into<PathBuf>) -> Self::Output;
    #[allow(clippy::missing_errors_doc)]
    fn dir(self, p: impl Into<PathBuf>) -> Self::Output;
}

impl<T> IntoIoError<T> for std::io::Result<T> {
    type Output = Result<T, IoError>;
    fn path(self, p: impl Into<PathBuf>) -> Result<T, IoError> {
        self.map_err(|error| IoError::Io {
            error,
            path: p.into().clone(),
        })
    }

    fn dir(self, p: impl Into<PathBuf>) -> Result<T, IoError> {
        self.map_err(|err| IoError::ReadDir {
            error: err.to_string(),
            parent: p.into(),
        })
    }
}

impl IntoIoError for std::io::Error {
    type Output = IoError;
    fn path(self, p: impl Into<PathBuf>) -> IoError {
        IoError::Io {
            error: self,
            path: p.into().clone(),
        }
    }

    fn dir(self, p: impl Into<PathBuf>) -> IoError {
        IoError::ReadDir {
            error: self.to_string(),
            parent: p.into(),
        }
    }
}

pub trait IntoStringError<T> {
    #[allow(clippy::missing_errors_doc)]
    fn strerr(self) -> Result<T, String>;
}

impl<T, E: ToString> IntoStringError<T> for Result<T, E> {
    fn strerr(self) -> Result<T, String> {
        self.map_err(|err| err.to_string())
    }
}

#[derive(Debug, Error)]
pub enum JsonDownloadError {
    #[error(transparent)]
    RequestError(#[from] RequestError),
    #[error(transparent)]
    SerdeError(#[from] JsonError),
}

impl From<reqwest::Error> for JsonDownloadError {
    fn from(value: reqwest::Error) -> Self {
        Self::RequestError(RequestError::ReqwestError(value))
    }
}

#[derive(Debug, Error)]
pub enum DownloadFileError {
    #[error(transparent)]
    Request(#[from] RequestError),
    #[error(transparent)]
    Io(#[from] IoError),
}

impl From<reqwest::Error> for DownloadFileError {
    fn from(value: reqwest::Error) -> Self {
        Self::Request(RequestError::ReqwestError(value))
    }
}

#[derive(Debug, Error)]
pub enum JsonFileError {
    #[error(transparent)]
    SerdeError(#[from] JsonError),
    #[error(transparent)]
    Io(#[from] IoError),
}

const JSON_ERR_PREFIX: &str = "could not parse JSON (this is a bug! please report):\n";

#[derive(Debug, Error)]
pub enum JsonError {
    #[error("{JSON_ERR_PREFIX}while parsing JSON:\n{error}\n\n{json}")]
    From {
        error: serde_json::Error,
        json: String,
    },
    #[error("{JSON_ERR_PREFIX}while converting object to JSON:\n{error}")]
    To { error: serde_json::Error },
}

pub trait IntoJsonError<T> {
    #[allow(clippy::missing_errors_doc)]
    fn json(self, p: String) -> Result<T, JsonError>;
    #[allow(clippy::missing_errors_doc)]
    fn json_to(self) -> Result<T, JsonError>;
}

impl<T> IntoJsonError<T> for Result<T, serde_json::Error> {
    fn json(self, json: String) -> Result<T, JsonError> {
        self.map_err(|error: serde_json::Error| JsonError::From { error, json })
    }

    fn json_to(self) -> Result<T, JsonError> {
        self.map_err(|error: serde_json::Error| JsonError::To { error })
    }
}
