//! Configures libcurl's http handles.

use std::path::PathBuf;
use std::str;
use std::time::Duration;

use anyhow::bail;
use curl::easy::Easy;
use curl::easy::Easy2;
use curl::easy::InfoType;
use curl::easy::SslOpt;
use curl::easy::SslVersion;
use tracing::debug;
use tracing::trace;

use crate::CargoResult;
use crate::GlobalContext;
use crate::util::context::SslVersionConfig;
use crate::util::context::SslVersionConfigRange;

/// Creates a new HTTP handle with appropriate global configuration for cargo.
pub fn http_handle(gctx: &GlobalContext) -> CargoResult<Easy> {
    let (mut handle, timeout) = http_handle_and_timeout(gctx)?;
    timeout.configure(&mut handle)?;
    Ok(handle)
}

pub fn http_handle_and_timeout(gctx: &GlobalContext) -> CargoResult<(Easy, HttpTimeout)> {
    // The timeout option for libcurl by default times out the entire transfer,
    // but we probably don't want this. Instead we only set timeouts for the
    // connect phase as well as a "low speed" timeout so if we don't receive
    // many bytes in a large-ish period of time then we time out.
    let mut handle = Easy::new();
    let timeout = configure_http_handle(gctx, &mut handle)?;
    Ok((handle, timeout))
}

// Only use a custom transport if any HTTP options are specified,
// such as proxies or custom certificate authorities.
//
// The custom transport, however, is not as well battle-tested.
pub fn needs_custom_http_transport(gctx: &GlobalContext) -> CargoResult<bool> {
    Ok(super::proxy::http_proxy_exists(gctx.http_config()?, gctx)
        || *gctx.http_config()? != Default::default()
        || gctx.get_env_os("HTTP_TIMEOUT").is_some())
}
/// Configure a libcurl http handle with the defaults options for Cargo
pub struct HandleConfiguration {
    proxy: Option<String>,
    cainfo: Option<PathBuf>,
    proxy_cainfo: Option<String>,
    ssl_options: Option<SslOpt>,
    useragent: String,
    ssl_version: Option<SslVersion>,
    ssl_min_max_version: Option<(SslVersion, SslVersion)>,
    pub timeout: HttpTimeout,
    pub verbose: bool,
    pub multiplexing: bool,
}

pub fn configure_http_handle(gctx: &GlobalContext, handle: &mut Easy) -> CargoResult<HttpTimeout> {
    let configuration = HandleConfiguration::new(gctx)?;
    configuration.configure(handle)?;
    Ok(configuration.timeout)
}

impl HandleConfiguration {
    pub fn new(gctx: &GlobalContext) -> CargoResult<Self> {
        if let Some(offline_flag) = gctx.offline_flag() {
            bail!(
                "attempting to make an HTTP request, but {offline_flag} was \
                specified"
            )
        }

        let http = gctx.http_config()?;
        let timeout = HttpTimeout::new(gctx)?;
        let useragent = if let Some(user_agent) = http.user_agent.clone() {
            user_agent
        } else {
            format!("cargo/{}", crate::version())
        };
        let multiplexing = http.multiplexing.unwrap_or(true);
        let mut handle = HandleConfiguration {
            proxy: None,
            cainfo: None,
            proxy_cainfo: None,
            ssl_options: None,
            useragent,
            ssl_version: None,
            ssl_min_max_version: None,
            verbose: false,
            timeout,
            multiplexing,
        };
        if let Some(proxy) = super::proxy::http_proxy(http) {
            handle.proxy = Some(proxy);
        }
        if let Some(cainfo) = &http.cainfo {
            let cainfo = cainfo.resolve_path(gctx);
            handle.cainfo = Some(cainfo);
        }
        // Use `proxy_cainfo` if explicitly set; otherwise, fall back to `cainfo` as curl does #15376.
        if let Some(proxy_cainfo) = http.proxy_cainfo.as_ref().or(http.cainfo.as_ref()) {
            let proxy_cainfo = proxy_cainfo.resolve_path(gctx);
            handle.proxy_cainfo = Some(format!("{}", proxy_cainfo.display()));
        }
        if let Some(check) = http.check_revoke {
            let mut v = SslOpt::new();
            v.no_revoke(!check);
            handle.ssl_options = Some(v);
        }

        fn to_ssl_version(s: &str) -> CargoResult<SslVersion> {
            let version = match s {
                "default" => SslVersion::Default,
                "tlsv1" => SslVersion::Tlsv1,
                "tlsv1.0" => SslVersion::Tlsv10,
                "tlsv1.1" => SslVersion::Tlsv11,
                "tlsv1.2" => SslVersion::Tlsv12,
                "tlsv1.3" => SslVersion::Tlsv13,
                _ => bail!(
                    "Invalid ssl version `{s}`,\
                    choose from 'default', 'tlsv1', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3'."
                ),
            };
            Ok(version)
        }

        if let Some(ssl_version) = &http.ssl_version {
            match ssl_version {
                SslVersionConfig::Single(s) => {
                    let version = to_ssl_version(s.as_str())?;
                    handle.ssl_version = Some(version);
                }
                SslVersionConfig::Range(SslVersionConfigRange { min, max }) => {
                    let min_version = min
                        .as_ref()
                        .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
                    let max_version = max
                        .as_ref()
                        .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
                    handle.ssl_min_max_version = Some((min_version, max_version));
                }
            }
        } else if cfg!(windows) {
            // This is a temporary workaround for some bugs with libcurl and
            // schannel and TLS 1.3.
            //
            // Our libcurl on Windows is usually built with schannel.
            // On Windows 11 (or Windows Server 2022), libcurl recently (late
            // 2022) gained support for TLS 1.3 with schannel, and it now defaults
            // to 1.3. Unfortunately there have been some bugs with this.
            // https://github.com/curl/curl/issues/9431 is the most recent. Once
            // that has been fixed, and some time has passed where we can be more
            // confident that the 1.3 support won't cause issues, this can be
            // removed.
            //
            // Windows 10 is unaffected. libcurl does not support TLS 1.3 on
            // Windows 10. (Windows 10 sorta had support, but it required enabling
            // an advanced option in the registry which was buggy, and libcurl
            // does runtime checks to prevent it.)
            handle.ssl_min_max_version = Some((SslVersion::Default, SslVersion::Tlsv12));
        }

        if let Some(true) = http.debug {
            handle.verbose = true;
        }

        Ok(handle)
    }

    pub fn configure(&self, handle: &mut Easy) -> Result<(), curl::Error> {
        if let Some(v) = &self.proxy {
            handle.proxy(&v)?;
        }
        if let Some(v) = &self.cainfo {
            handle.cainfo(&v)?;
        }
        if let Some(v) = &self.proxy_cainfo {
            handle.proxy_cainfo(&v)?;
        }
        if let Some(v) = &self.ssl_options {
            handle.ssl_options(&v)?;
        }
        handle.useragent(&self.useragent)?;
        // Empty string accept encoding expands to the encodings supported by the current libcurl.
        handle.accept_encoding("")?;
        if let Some(v) = &self.ssl_version {
            handle.ssl_version(v.clone())?;
        }
        if let Some((min, max)) = &self.ssl_min_max_version {
            handle.ssl_min_max_version(min.clone(), max.clone())?;
        }
        if self.verbose {
            handle.verbose(true)?;
            tracing::debug!(target: "network", "{:#?}", curl::Version::get());
            handle.debug_function(debug)?;
        }
        Ok(())
    }

    pub fn configure2<T>(&self, handle: &mut Easy2<T>) -> Result<(), curl::Error> {
        if let Some(v) = &self.proxy {
            handle.proxy(&v)?;
        }
        if let Some(v) = &self.cainfo {
            handle.cainfo(&v)?;
        }
        if let Some(v) = &self.proxy_cainfo {
            handle.proxy_cainfo(&v)?;
        }
        if let Some(v) = &self.ssl_options {
            handle.ssl_options(&v)?;
        }
        handle.useragent(&self.useragent)?;
        // Empty string accept encoding expands to the encodings supported by the current libcurl.
        handle.accept_encoding("")?;
        if let Some(v) = &self.ssl_version {
            handle.ssl_version(v.clone())?;
        }
        if let Some((min, max)) = &self.ssl_min_max_version {
            handle.ssl_min_max_version(min.clone(), max.clone())?;
        }
        if self.verbose {
            handle.verbose(true)?;
            tracing::debug!(target: "network", "{:#?}", curl::Version::get());
        }

        // Enable HTTP/2 if possible.
        crate::try_old_curl_http2_pipewait!(self.multiplexing, handle);
        Ok(())
    }
}

pub(crate) fn debug(kind: InfoType, data: &[u8]) {
    enum LogLevel {
        Debug,
        Trace,
    }
    use LogLevel::*;
    let (prefix, level) = match kind {
        InfoType::Text => ("*", Debug),
        InfoType::HeaderIn => ("<", Debug),
        InfoType::HeaderOut => (">", Debug),
        InfoType::DataIn => ("{", Trace),
        InfoType::DataOut => ("}", Trace),
        InfoType::SslDataIn | InfoType::SslDataOut => return,
        _ => return,
    };
    let starts_with_ignore_case = |line: &str, text: &str| -> bool {
        let line = line.as_bytes();
        let text = text.as_bytes();
        line[..line.len().min(text.len())].eq_ignore_ascii_case(text)
    };
    match str::from_utf8(data) {
        Ok(s) => {
            for mut line in s.lines() {
                if starts_with_ignore_case(line, "authorization:") {
                    line = "Authorization: [REDACTED]";
                } else if starts_with_ignore_case(line, "h2h3 [authorization:") {
                    line = "h2h3 [Authorization: [REDACTED]]";
                } else if starts_with_ignore_case(line, "set-cookie") {
                    line = "set-cookie: [REDACTED]";
                }
                match level {
                    Debug => debug!(target: "network", "http-debug: {prefix} {line}"),
                    Trace => trace!(target: "network", "http-debug: {prefix} {line}"),
                }
            }
        }
        Err(_) => {
            let len = data.len();
            match level {
                Debug => {
                    debug!(target: "network", "http-debug: {prefix} ({len} bytes of data)")
                }
                Trace => {
                    trace!(target: "network", "http-debug: {prefix} ({len} bytes of data)")
                }
            }
        }
    }
}

#[must_use]
#[derive(Clone)]
pub struct HttpTimeout {
    pub dur: Duration,
    pub low_speed_limit: u32,
}

impl HttpTimeout {
    pub fn new(gctx: &GlobalContext) -> CargoResult<HttpTimeout> {
        let http_config = gctx.http_config()?;
        let low_speed_limit = http_config.low_speed_limit.unwrap_or(10);
        let seconds = http_config
            .timeout
            .or_else(|| {
                gctx.get_env("HTTP_TIMEOUT")
                    .ok()
                    .and_then(|s| s.parse().ok())
            })
            .unwrap_or(30);
        Ok(HttpTimeout {
            dur: Duration::new(seconds, 0),
            low_speed_limit,
        })
    }

    pub fn configure(&self, handle: &mut Easy) -> CargoResult<()> {
        // The timeout option for libcurl by default times out the entire
        // transfer, but we probably don't want this. Instead we only set
        // timeouts for the connect phase as well as a "low speed" timeout so
        // if we don't receive many bytes in a large-ish period of time then we
        // time out.
        handle.connect_timeout(self.dur)?;
        handle.low_speed_time(self.dur)?;
        handle.low_speed_limit(self.low_speed_limit)?;
        Ok(())
    }

    pub fn configure2<T>(&self, handle: &mut Easy2<T>) -> Result<(), curl::Error> {
        // The timeout option for libcurl by default times out the entire
        // transfer, but we probably don't want this. Instead we only set
        // timeouts for the connect phase as well as a "low speed" timeout so
        // if we don't receive many bytes in a large-ish period of time then we
        // time out.
        handle.connect_timeout(self.dur)?;
        handle.low_speed_time(self.dur)?;
        handle.low_speed_limit(self.low_speed_limit)?;
        Ok(())
    }
}
