use std::fmt::{Display, Formatter};
use std::ops::Deref;
use std::path::PathBuf;
use std::time::{Duration, Instant};

use async_http_range_reader::AsyncHttpRangeReaderError;
use async_zip::error::ZipError;
use reqwest::Response;
use serde::Deserialize;
use tracing::warn;

use uv_cache::Error as CacheError;
use uv_distribution_filename::{WheelFilename, WheelFilenameError};
use uv_normalize::PackageName;
use uv_redacted::DisplaySafeUrl;

use crate::middleware::OfflineError;
use crate::{FlatIndexError, html};

/// RFC 9457 Problem Details for HTTP APIs
///
/// This structure represents the standard format for machine-readable details
/// of errors in HTTP response bodies as defined in RFC 9457.
#[derive(Debug, Clone, Deserialize)]
pub struct ProblemDetails {
    /// A URI reference that identifies the problem type.
    /// When dereferenced, it SHOULD provide human-readable documentation for the problem type.
    #[serde(rename = "type", default = "default_problem_type")]
    pub problem_type: String,

    /// A short, human-readable summary of the problem type.
    pub title: Option<String>,

    /// The HTTP status code generated by the origin server for this occurrence of the problem.
    pub status: Option<u16>,

    /// A human-readable explanation specific to this occurrence of the problem.
    pub detail: Option<String>,

    /// A URI reference that identifies the specific occurrence of the problem.
    pub instance: Option<String>,
}

/// Default problem type URI as per RFC 9457
#[inline]
fn default_problem_type() -> String {
    "about:blank".to_string()
}

impl ProblemDetails {
    /// The content type for RFC 9457 Problem Details responses.
    pub const CONTENT_TYPE: &str = "application/problem+json";

    /// Try to parse a response body as RFC 9457 Problem Details.
    ///
    /// Returns `None` if parsing fails. The caller is responsible for checking
    /// that the content type is `application/problem+json` before calling this.
    pub fn try_from_response_body(body: &[u8]) -> Option<Self> {
        match serde_json::from_slice(body) {
            Ok(details) => Some(details),
            Err(err) => {
                warn!("Failed to parse problem details: {err}");
                None
            }
        }
    }

    /// Try to extract RFC 9457 Problem Details from an HTTP response.
    ///
    /// Returns `None` if the content type is not `application/problem+json`
    /// or if parsing fails. Only consumes the response body if the content
    /// type matches.
    pub async fn try_from_response(response: Response) -> Option<Self> {
        let is_problem = response
            .headers()
            .get(reqwest::header::CONTENT_TYPE)
            .and_then(|ct| ct.to_str().ok())
            .is_some_and(|ct| ct == Self::CONTENT_TYPE);
        if !is_problem {
            return None;
        }
        match response.bytes().await {
            Ok(bytes) => Self::try_from_response_body(&bytes),
            Err(err) => {
                warn!("Failed to read response body for problem details: {err}");
                None
            }
        }
    }

    /// Get a human-readable description of the problem
    pub fn description(&self) -> Option<String> {
        match self {
            Self {
                title: Some(title),
                detail: Some(detail),
                ..
            } => Some(format!("Server message: {title}, {detail}")),
            Self {
                title: Some(title), ..
            } => Some(format!("Server message: {title}")),
            Self {
                detail: Some(detail),
                ..
            } => Some(format!("Server message: {detail}")),
            Self {
                status: Some(status),
                ..
            } => Some(format!("HTTP error {status}")),
            _ => None,
        }
    }
}

#[derive(Debug)]
pub struct Error {
    kind: Box<ErrorKind>,
    retries: u32,
    duration: Duration,
}

impl Display for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        if self.retries > 0 {
            write!(
                f,
                "Request failed after {retries} {subject} in {duration:.1}s",
                retries = self.retries,
                subject = if self.retries > 1 { "retries" } else { "retry" },
                duration = self.duration.as_secs_f32(),
            )
        } else {
            Display::fmt(&self.kind, f)
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        if self.retries > 0 {
            Some(&self.kind)
        } else {
            self.kind.source()
        }
    }
}

impl Error {
    /// Create a new [`Error`] with the given [`ErrorKind`] and number of retries.
    pub fn new(kind: ErrorKind, retries: u32, duration: Duration) -> Self {
        Self {
            kind: Box::new(kind),
            retries,
            duration,
        }
    }

    /// Return the number of retries that were attempted before this error was returned.
    pub fn retries(&self) -> u32 {
        self.retries
    }

    /// Return the time taken for network requests, including retries, backoff and jitter,
    /// before this error was returned.
    pub fn duration(&self) -> Duration {
        self.duration
    }

    /// Convert this error into an [`ErrorKind`].
    pub fn into_kind(self) -> ErrorKind {
        *self.kind
    }

    /// Return the [`ErrorKind`] of this error.
    pub fn kind(&self) -> &ErrorKind {
        &self.kind
    }

    pub(crate) fn with_retries(mut self, retries: u32) -> Self {
        self.retries = retries;
        self
    }

    /// Create a new error from a JSON parsing error.
    pub(crate) fn from_json_err(err: serde_json::Error, url: DisplaySafeUrl) -> Self {
        ErrorKind::BadJson { source: err, url }.into()
    }

    /// Create a new error from an HTML parsing error.
    pub(crate) fn from_html_err(err: html::Error, url: DisplaySafeUrl) -> Self {
        ErrorKind::BadHtml { source: err, url }.into()
    }

    /// Create a new error from a `MessagePack` parsing error.
    pub(crate) fn from_msgpack_err(err: rmp_serde::decode::Error, url: DisplaySafeUrl) -> Self {
        ErrorKind::BadMessagePack { source: err, url }.into()
    }

    /// Create an [`Error`] from a [`reqwest_middleware::Error`].
    pub(crate) fn from_reqwest_middleware(
        url: DisplaySafeUrl,
        err: reqwest_middleware::Error,
        start: Instant,
    ) -> Self {
        if let reqwest_middleware::Error::Middleware(ref underlying) = err {
            if let Some(offline_err) = underlying.downcast_ref::<OfflineError>() {
                return ErrorKind::Offline(offline_err.url().to_string()).into();
            }
            if let Some(reqwest_retry::RetryError::WithRetries { retries, .. }) =
                underlying.downcast_ref::<reqwest_retry::RetryError>()
            {
                let retries = *retries;
                return Self::new(
                    ErrorKind::WrappedReqwestError(url, WrappedReqwestError::from(err)),
                    retries,
                    start.elapsed(),
                );
            }
        }
        Self::from(ErrorKind::WrappedReqwestError(
            url,
            WrappedReqwestError::from(err),
        ))
    }

    /// Returns `true` if this error corresponds to an offline error.
    pub(crate) fn is_offline(&self) -> bool {
        matches!(&*self.kind, ErrorKind::Offline(_))
    }

    /// Returns `true` if this error corresponds to an I/O "not found" error.
    pub(crate) fn is_file_not_exists(&self) -> bool {
        let ErrorKind::Io(err) = &*self.kind else {
            return false;
        };
        matches!(err.kind(), std::io::ErrorKind::NotFound)
    }

    /// Returns `true` if the error is due to an SSL error.
    pub fn is_ssl(&self) -> bool {
        matches!(&*self.kind, ErrorKind::WrappedReqwestError(.., err) if err.is_ssl())
    }

    /// Returns `true` if the error is due to the server not supporting HTTP range requests.
    pub fn is_http_range_requests_unsupported(&self) -> bool {
        match &*self.kind {
            // The server doesn't support range requests (as reported by the `HEAD` check).
            ErrorKind::AsyncHttpRangeReader(
                _,
                AsyncHttpRangeReaderError::HttpRangeRequestUnsupported,
            ) => {
                return true;
            }

            // The server doesn't support range requests (it doesn't return the necessary headers).
            ErrorKind::AsyncHttpRangeReader(
                _,
                AsyncHttpRangeReaderError::ContentLengthMissing
                | AsyncHttpRangeReaderError::ContentRangeMissing,
            ) => {
                return true;
            }

            // The server returned a "Method Not Allowed" error, indicating it doesn't support
            // HEAD requests, so we can't check for range requests.
            ErrorKind::WrappedReqwestError(_, err) => {
                if let Some(status) = err.status() {
                    // If the server doesn't support HEAD requests, we can't check for range
                    // requests.
                    if status == reqwest::StatusCode::METHOD_NOT_ALLOWED {
                        return true;
                    }

                    // In some cases, registries return a 404 for HEAD requests when they're not
                    // supported. In the worst case, we'll now just proceed to attempt to stream the
                    // entire file, so it's fine to be somewhat lenient here.
                    if status == reqwest::StatusCode::NOT_FOUND {
                        return true;
                    }

                    // In some cases, registries (like PyPICloud) return a 403 for HEAD requests
                    // when they're not supported. Again, it's better to be lenient here.
                    if status == reqwest::StatusCode::FORBIDDEN {
                        return true;
                    }

                    // In some cases, registries (like Alibaba Cloud) return a 400 for HEAD requests
                    // when they're not supported. Again, it's better to be lenient here.
                    if status == reqwest::StatusCode::BAD_REQUEST {
                        return true;
                    }
                }
            }

            // The server doesn't support range requests, but we only discovered this while
            // unzipping due to erroneous server behavior.
            ErrorKind::Zip(_, ZipError::UpstreamReadError(err)) => {
                if let Some(inner) = err.get_ref() {
                    if let Some(inner) = inner.downcast_ref::<AsyncHttpRangeReaderError>() {
                        if matches!(
                            inner,
                            AsyncHttpRangeReaderError::HttpRangeRequestUnsupported
                        ) {
                            return true;
                        }
                    }
                }
            }

            _ => {}
        }

        false
    }

    /// Returns `true` if the error is due to the server not supporting HTTP streaming. Most
    /// commonly, this is due to serving ZIP files with features that are incompatible with
    /// streaming, like data descriptors.
    pub fn is_http_streaming_unsupported(&self) -> bool {
        matches!(
            &*self.kind,
            ErrorKind::Zip(_, ZipError::FeatureNotSupported(_))
        )
    }
}

impl From<ErrorKind> for Error {
    fn from(kind: ErrorKind) -> Self {
        Self {
            kind: Box::new(kind),
            retries: 0,
            duration: Duration::default(),
        }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum ErrorKind {
    #[error(transparent)]
    InvalidUrl(#[from] uv_distribution_types::ToUrlError),

    #[error(transparent)]
    Flat(#[from] FlatIndexError),

    #[error("Expected a file URL, but received: {0}")]
    NonFileUrl(DisplaySafeUrl),

    #[error("Expected an index URL, but received non-base URL: {0}")]
    CannotBeABase(DisplaySafeUrl),

    #[error("Failed to read metadata: `{0}`")]
    Metadata(String, #[source] uv_metadata::Error),

    #[error("{0} isn't available locally, but making network requests to registries was banned")]
    NoIndex(String),

    /// The package was not found in the registry.
    ///
    /// Make sure the package name is spelled correctly and that you've
    /// configured the right registry to fetch it from.
    #[error("Package `{0}` was not found in the registry")]
    RemotePackageNotFound(PackageName),

    /// The package was not found in the local (file-based) index.
    #[error("Package `{0}` was not found in the local index")]
    LocalPackageNotFound(PackageName),

    /// The root was not found in the local (file-based) index.
    #[error("Local index not found at: `{}`", _0.display())]
    LocalIndexNotFound(PathBuf),

    /// The metadata file could not be parsed.
    #[error("Couldn't parse metadata of {0} from {1}")]
    MetadataParseError(
        WheelFilename,
        String,
        #[source] Box<uv_pypi_types::MetadataError>,
    ),

    /// An error that happened while making a request or in a reqwest middleware.
    #[error("Failed to fetch: `{0}`")]
    WrappedReqwestError(DisplaySafeUrl, #[source] WrappedReqwestError),

    #[error("Received some unexpected JSON from {}", url)]
    BadJson {
        source: serde_json::Error,
        url: DisplaySafeUrl,
    },

    #[error("Received some unexpected HTML from {}", url)]
    BadHtml {
        source: html::Error,
        url: DisplaySafeUrl,
    },

    #[error("Received some unexpected MessagePack from {}", url)]
    BadMessagePack {
        source: rmp_serde::decode::Error,
        url: DisplaySafeUrl,
    },

    #[error("Failed to read zip with range requests: `{0}`")]
    AsyncHttpRangeReader(DisplaySafeUrl, #[source] AsyncHttpRangeReaderError),

    #[error("{0} is not a valid wheel filename")]
    WheelFilename(#[source] WheelFilenameError),

    #[error("Package metadata name `{metadata}` does not match given name `{given}`")]
    NameMismatch {
        given: PackageName,
        metadata: PackageName,
    },

    #[error("Failed to unzip wheel: {0}")]
    Zip(WheelFilename, #[source] ZipError),

    #[error("Failed to write to the client cache")]
    CacheWrite(#[source] std::io::Error),

    #[error("Failed to acquire lock on the client cache")]
    CacheLock(#[source] CacheError),

    #[error(transparent)]
    Io(std::io::Error),

    #[error("Cache deserialization failed")]
    Decode(#[source] rmp_serde::decode::Error),

    #[error("Cache serialization failed")]
    Encode(#[source] rmp_serde::encode::Error),

    #[error("Missing `Content-Type` header for {0}")]
    MissingContentType(DisplaySafeUrl),

    #[error("Invalid `Content-Type` header for {0}")]
    InvalidContentTypeHeader(DisplaySafeUrl, #[source] http::header::ToStrError),

    #[error("Unsupported `Content-Type` \"{1}\" for {0}. Expected JSON or HTML.")]
    UnsupportedMediaType(DisplaySafeUrl, String),

    #[error("Reading from cache archive failed: {0}")]
    ArchiveRead(String),

    #[error("Writing to cache archive failed: {0}")]
    ArchiveWrite(String),

    #[error(
        "Network connectivity is disabled, but the requested data wasn't found in the cache for: `{0}`"
    )]
    Offline(String),
}

impl ErrorKind {
    /// Create an [`ErrorKind`] from a [`reqwest::Error`].
    pub(crate) fn from_reqwest(url: DisplaySafeUrl, error: reqwest::Error) -> Self {
        Self::WrappedReqwestError(url, WrappedReqwestError::from(error))
    }

    /// Create an [`ErrorKind`] from a [`reqwest::Error`] with problem details.
    pub(crate) fn from_reqwest_with_problem_details(
        url: DisplaySafeUrl,
        error: reqwest::Error,
        problem_details: Option<ProblemDetails>,
    ) -> Self {
        Self::WrappedReqwestError(
            url,
            WrappedReqwestError::with_problem_details(error.into(), problem_details),
        )
    }
}

/// Handle the case with no internet by explicitly telling the user instead of showing an obscure
/// DNS error.
///
/// Wraps a [`reqwest_middleware::Error`] instead of an [`reqwest::Error`] since the actual reqwest
/// error may be below some context in the [`anyhow::Error`].
#[derive(Debug)]
pub struct WrappedReqwestError {
    error: reqwest_middleware::Error,
    problem_details: Option<Box<ProblemDetails>>,
}

impl WrappedReqwestError {
    /// Create a new `WrappedReqwestError` with optional problem details
    pub fn with_problem_details(
        error: reqwest_middleware::Error,
        problem_details: Option<ProblemDetails>,
    ) -> Self {
        Self {
            error: Self::filter_retries_from_error(error),
            problem_details: problem_details.map(Box::new),
        }
    }

    /// Drop `RetryError::WithRetries` to avoid reporting the number of retries twice.
    ///
    /// We attach the number of errors outside by adding the retry counts from the retry middleware
    /// and from uv's outer retry loop for streaming bodies. Stripping the inner count from the
    /// error context avoids showing two numbers.
    fn filter_retries_from_error(error: reqwest_middleware::Error) -> reqwest_middleware::Error {
        match error {
            reqwest_middleware::Error::Middleware(error) => {
                match error.downcast::<reqwest_retry::RetryError>() {
                    Ok(
                        reqwest_retry::RetryError::WithRetries { err, .. }
                        | reqwest_retry::RetryError::Error(err),
                    ) => err,
                    Err(error) => reqwest_middleware::Error::Middleware(error),
                }
            }
            error @ reqwest_middleware::Error::Reqwest(_) => error,
        }
    }

    /// Return the inner [`reqwest::Error`] from the error chain, if it exists.
    pub fn inner(&self) -> Option<&reqwest::Error> {
        match &self.error {
            reqwest_middleware::Error::Reqwest(err) => Some(err),
            reqwest_middleware::Error::Middleware(err) => err.chain().find_map(|err| {
                if let Some(err) = err.downcast_ref::<reqwest::Error>() {
                    Some(err)
                } else if let Some(reqwest_middleware::Error::Reqwest(err)) =
                    err.downcast_ref::<reqwest_middleware::Error>()
                {
                    Some(err)
                } else {
                    None
                }
            }),
        }
    }

    /// Check if the error chain contains a `reqwest` error that looks like this:
    /// * error sending request for url (...)
    /// * client error (Connect)
    /// * dns error: failed to lookup address information: Name or service not known
    /// * failed to lookup address information: Name or service not known
    fn is_likely_offline(&self) -> bool {
        if let Some(reqwest_err) = self.inner() {
            if !reqwest_err.is_connect() {
                return false;
            }
            // Self is "error sending request for url", the first source is "error trying to connect",
            // the second source is "dns error". We have to check for the string because hyper errors
            // are opaque.
            if std::error::Error::source(&reqwest_err)
                .and_then(|err| err.source())
                .is_some_and(|err| err.to_string().starts_with("dns error: "))
            {
                return true;
            }
        }
        false
    }

    /// Check if the error chain contains a `reqwest` error that looks like this:
    /// * invalid peer certificate: `UnknownIssuer`
    fn is_ssl(&self) -> bool {
        if let Some(reqwest_err) = self.inner() {
            if !reqwest_err.is_connect() {
                return false;
            }
            // Self is "error sending request for url", the first source is "error trying to connect",
            // the second source is "dns error". We have to check for the string because hyper errors
            // are opaque.
            if std::error::Error::source(&reqwest_err)
                .and_then(|err| err.source())
                .is_some_and(|err| err.to_string().starts_with("invalid peer certificate: "))
            {
                return true;
            }
        }
        false
    }
}

impl From<reqwest::Error> for WrappedReqwestError {
    fn from(error: reqwest::Error) -> Self {
        Self {
            // No need to filter retries as this error does not have retries.
            error: error.into(),
            problem_details: None,
        }
    }
}

impl From<reqwest_middleware::Error> for WrappedReqwestError {
    fn from(error: reqwest_middleware::Error) -> Self {
        Self {
            error: Self::filter_retries_from_error(error),
            problem_details: None,
        }
    }
}

impl Deref for WrappedReqwestError {
    type Target = reqwest_middleware::Error;

    fn deref(&self) -> &Self::Target {
        &self.error
    }
}

impl Display for WrappedReqwestError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        if self.is_likely_offline() {
            // Insert an extra hint, we'll show the wrapped error through `source`
            f.write_str("Could not connect, are you offline?")
        } else if let Some(problem_details) = &self.problem_details {
            // Show problem details if available
            match problem_details.description() {
                None => Display::fmt(&self.error, f),
                Some(message) => f.write_str(&message),
            }
        } else {
            // Show the wrapped error
            Display::fmt(&self.error, f)
        }
    }
}

impl std::error::Error for WrappedReqwestError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        if self.is_likely_offline() {
            // `Display` is inserting an extra message, so we need to show the wrapped error
            Some(&self.error)
        } else if self.problem_details.is_some() {
            // `Display` is showing problem details, so show the wrapped error as source
            Some(&self.error)
        } else {
            // `Display` is showing the wrapped error, continue with its source
            self.error.source()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_problem_details_parsing() {
        let json = r#"{
            "type": "https://example.com/probs/out-of-credit",
            "title": "You do not have enough credit.",
            "detail": "Your current balance is 30, but that costs 50.",
            "status": 403,
            "instance": "/account/12345/msgs/abc"
        }"#;

        let problem_details: ProblemDetails = serde_json::from_slice(json.as_bytes()).unwrap();
        assert_eq!(
            problem_details.problem_type,
            "https://example.com/probs/out-of-credit"
        );
        assert_eq!(
            problem_details.title,
            Some("You do not have enough credit.".to_string())
        );
        assert_eq!(
            problem_details.detail,
            Some("Your current balance is 30, but that costs 50.".to_string())
        );
        assert_eq!(problem_details.status, Some(403));
        assert_eq!(
            problem_details.instance,
            Some("/account/12345/msgs/abc".to_string())
        );
    }

    #[test]
    fn test_problem_details_default_type() {
        let json = r#"{
            "detail": "Something went wrong",
            "status": 500
        }"#;

        let problem_details: ProblemDetails = serde_json::from_slice(json.as_bytes()).unwrap();
        assert_eq!(problem_details.problem_type, "about:blank");
        assert_eq!(
            problem_details.detail,
            Some("Something went wrong".to_string())
        );
        assert_eq!(problem_details.status, Some(500));
    }

    #[test]
    fn test_problem_details_description() {
        let json = r#"{
            "detail": "Detailed error message",
            "title": "Error Title",
            "status": 400
        }"#;

        let problem_details: ProblemDetails = serde_json::from_slice(json.as_bytes()).unwrap();
        assert_eq!(
            problem_details.description().unwrap(),
            "Server message: Error Title, Detailed error message"
        );

        let json_no_detail = r#"{
            "title": "Error Title",
            "status": 400
        }"#;

        let problem_details: ProblemDetails =
            serde_json::from_slice(json_no_detail.as_bytes()).unwrap();
        assert_eq!(
            problem_details.description().unwrap(),
            "Server message: Error Title"
        );

        let json_minimal = r#"{
            "status": 400
        }"#;

        let problem_details: ProblemDetails =
            serde_json::from_slice(json_minimal.as_bytes()).unwrap();
        assert_eq!(problem_details.description().unwrap(), "HTTP error 400");
    }

    #[test]
    fn test_problem_details_with_extensions() {
        let json = r#"{
            "type": "https://example.com/probs/out-of-credit",
            "title": "You do not have enough credit.",
            "detail": "Your current balance is 30, but that costs 50.",
            "status": 403,
            "balance": 30,
            "accounts": ["/account/12345", "/account/67890"]
        }"#;

        let problem_details: ProblemDetails = serde_json::from_slice(json.as_bytes()).unwrap();
        assert_eq!(
            problem_details.title,
            Some("You do not have enough credit.".to_string())
        );
    }

    #[test]
    fn test_try_from_response_body() {
        let body = r#"{"type": "about:blank", "status": 400, "title": "Bad Request", "detail": "Missing required field `name`"}"#;
        let problem = ProblemDetails::try_from_response_body(body.as_bytes())
            .expect("should parse problem details");
        assert_eq!(
            problem.description().unwrap(),
            "Server message: Bad Request, Missing required field `name`"
        );
    }
}
