//! Connection pooling and HTTP client building.

use std::{sync::Arc, time::Duration};

use hickory_resolver::TokioResolver;
use proxywire::{ProxyAuth, ProxyEndpoint, ProxyRoute, apply_reqwest_route};
use reqwest::{
    Url,
    header::{HeaderName, HeaderValue, USER_AGENT},
};

use crate::camouflage::{BrowserHeaderMapError, default_browser_navigation_header_map};
use crate::{Error, HttpConfig, Result};

const MAX_TIMEOUT_SECS: u64 = 86_400;
const MAX_RETRY_DELAY_MS: u64 = 60_000;

/// Resolves DNS queries utilizing hickory-resolver.
pub struct HickoryResolver(pub Arc<TokioResolver>);

impl reqwest::dns::Resolve for HickoryResolver {
    fn resolve(
        &self,
        name: reqwest::dns::Name,
    ) -> std::pin::Pin<
        Box<
            dyn std::future::Future<
                    Output = std::result::Result<
                        Box<dyn Iterator<Item = std::net::SocketAddr> + Send>,
                        Box<dyn std::error::Error + Send + Sync>,
                    >,
                > + Send,
        >,
    > {
        let resolver = Arc::clone(&self.0);
        Box::pin(async move {
            let lookup = resolver
                .lookup_ip(name.as_str())
                .await
                .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
            let addrs: Box<dyn Iterator<Item = std::net::SocketAddr> + Send> = Box::new(
                lookup
                    .into_iter()
                    .map(|ip| std::net::SocketAddr::new(ip, 0)),
            );
            Ok(addrs)
        })
    }
}

/// Builds a `reqwest::Client` customized for [`ScanClient`](crate::ScanClient).
///
/// Redirect following is disabled at the reqwest layer; [`ScanClient`] applies
/// SSRF-checked redirects manually via [`HttpConfig::max_redirects`].
pub fn build_client(config: &HttpConfig, resolver: Arc<TokioResolver>) -> Result<reqwest::Client> {
    build_client_with_redirect(config, resolver, reqwest::redirect::Policy::none())
}

/// Builds a pooled `reqwest::Client` for fleet crates that rely on reqwest's
/// native redirect policy (e.g. gossan recon) instead of [`ScanClient`]'s
/// manual redirect loop.
pub fn build_client_with_redirect(
    config: &HttpConfig,
    resolver: Arc<TokioResolver>,
    redirect: reqwest::redirect::Policy,
) -> Result<reqwest::Client> {
    validate_timeouts(config)?;
    validate_tls_config(config)?;
    validate_user_agent(config)?;
    validate_proxy_auth(config)?;
    let proxy_route = build_proxy_route(config)?;
    let mut headers = default_browser_navigation_header_map(&config.user_agent)
        .map_err(map_browser_header_error)?;

    let mut sorted_headers: Vec<(&String, &String)> = config.custom_headers.iter().collect();
    sorted_headers.sort_unstable_by(|(left_name, _), (right_name, _)| left_name.cmp(right_name));

    for (name, value) in sorted_headers {
        let header_name = HeaderName::from_bytes(name.as_bytes())
            .map_err(|_| Error::InvalidHeaderName { name: name.clone() })?;
        let header_value = HeaderValue::from_str(value)
            .map_err(|_| Error::InvalidHeaderValue { name: name.clone() })?;
        headers.insert(header_name, header_value);
    }

    let mut builder = reqwest::Client::builder()
        .dns_resolver(Arc::new(HickoryResolver(resolver)))
        .default_headers(headers)
        .timeout(Duration::from_secs(config.timeout_secs))
        .connect_timeout(Duration::from_secs(config.connect_timeout_secs))
        .redirect(redirect)
        .danger_accept_invalid_certs(config.tls_accept_invalid_certs)
        .danger_accept_invalid_hostnames(config.tls_accept_invalid_hostnames);

    let proxy_display = config.proxy.as_deref().unwrap_or_default();
    builder = apply_reqwest_route(builder, &proxy_route)
        .map_err(|_| Error::InvalidProxy(sanitize_proxy_for_display(proxy_display)))?;

    builder.build().map_err(Error::ClientBuild)
}

fn map_browser_header_error(error: BrowserHeaderMapError) -> Error {
    match error {
        BrowserHeaderMapError::InvalidProfileHeaderName { name } => Error::InvalidHeaderName {
            name: name.to_string(),
        },
        BrowserHeaderMapError::InvalidProfileHeaderValue { name } => Error::InvalidHeaderValue {
            name: name.to_string(),
        },
        BrowserHeaderMapError::InvalidUserAgent => Error::InvalidHeaderValue {
            name: USER_AGENT.as_str().to_string(),
        },
    }
}

fn validate_tls_config(config: &HttpConfig) -> Result<()> {
    if config.tls_verify {
        if config.tls_accept_invalid_certs || config.tls_accept_invalid_hostnames {
            return Err(Error::InvalidTlsConfig);
        }
        return Ok(());
    }

    if config.tls_accept_invalid_certs || config.tls_accept_invalid_hostnames {
        return Ok(());
    }

    Err(Error::InvalidTlsConfig)
}

fn validate_timeouts(config: &HttpConfig) -> Result<()> {
    for (field, value) in [
        ("timeout_secs", config.timeout_secs),
        ("connect_timeout_secs", config.connect_timeout_secs),
    ] {
        if value == 0 || value > MAX_TIMEOUT_SECS {
            return Err(Error::InvalidTimeout {
                field,
                value,
                max: MAX_TIMEOUT_SECS,
            });
        }
    }

    if config.retry_delay_ms == 0 || config.retry_delay_ms > MAX_RETRY_DELAY_MS {
        return Err(Error::InvalidRetryDelay {
            value: config.retry_delay_ms,
            max: MAX_RETRY_DELAY_MS,
        });
    }

    if config.max_body_size == 0 {
        return Err(Error::InvalidMaxBodySize {
            value: config.max_body_size,
        });
    }

    Ok(())
}

fn validate_user_agent(config: &HttpConfig) -> Result<()> {
    if config.user_agent.trim().is_empty() {
        return Err(Error::InvalidUserAgent);
    }

    Ok(())
}

fn validate_proxy_auth(config: &HttpConfig) -> Result<()> {
    if config.proxy.is_none() {
        if config.proxy_user.is_some() || config.proxy_password.is_some() {
            return Err(Error::InvalidProxyAuth);
        }
        return Ok(());
    }

    if config.proxy_password.is_some() && config.proxy_user.is_none() {
        return Err(Error::InvalidProxyAuth);
    }

    Ok(())
}

fn sanitize_proxy_for_display(proxy: &str) -> String {
    // Try to parse the proxy as a URL and strip userinfo if present.
    if let Ok(url) = Url::parse(proxy) {
        let scheme = url.scheme();
        let host = url.host_str().unwrap_or_default();
        let port = url.port().map(|p| format!(":{p}")).unwrap_or_default();
        return format!("{scheme}://{host}{port}");
    }
    // Fallback: if userinfo present (user:pass@host), strip everything up to the last '@'.
    if let Some(at) = proxy.rfind('@') {
        return proxy[(at + 1)..].to_string();
    }
    proxy.to_string()
}

fn validate_proxy_url(proxy: &str) -> Result<()> {
    ProxyEndpoint::from_url(proxy)
        .map(|_| ())
        .map_err(|_| Error::InvalidProxy(sanitize_proxy_for_display(proxy)))
}

fn build_proxy_route(config: &HttpConfig) -> Result<ProxyRoute> {
    let Some(proxy) = &config.proxy else {
        return Ok(ProxyRoute::Direct);
    };
    validate_proxy_url(proxy)?;
    let mut endpoint = ProxyEndpoint::from_url(proxy)
        .map_err(|_| Error::InvalidProxy(sanitize_proxy_for_display(proxy)))?;
    if let Some(proxy_user) = &config.proxy_user {
        endpoint.auth = Some(ProxyAuth::basic(
            proxy_user.clone(),
            config.proxy_password.clone().unwrap_or_default(),
        ));
    }
    Ok(ProxyRoute::Chain(vec![endpoint]))
}
