#![allow(clippy::needless_pass_by_value)]
use super::execute::is_internal_ip;
use super::{
    CLIENT_CONNECT_TIMEOUT, CLIENT_POOL_IDLE_TIMEOUT, HttpTransport, PER_HOST_CONCURRENCY_LIMIT,
};
use crate::camouflage::default_browser_navigation_header_map;
use secir::{Error, Result};
use std::net::IpAddr;
use std::time::Duration;

fn redirect_target_is_safe(url: &reqwest::Url) -> bool {
    let Some(host) = url.host_str() else {
        return false;
    };
    if let Ok(ip) = host.parse::<IpAddr>() {
        return !is_internal_ip(ip);
    }
    true
}

#[derive(Clone)]
pub(super) struct ClientPair {
    pub(super) client: reqwest::Client,
    pub(super) redirect_client: reqwest::Client,
}

#[derive(Clone)]
pub(super) struct ClientBuildConfig<'a> {
    pub(super) timeout: Duration,
    pub(super) accept_invalid_certs: bool,
    pub(super) proxy_url: Option<&'a str>,
    pub(super) proxy_auth: Option<&'a (String, String)>,
    pub(super) connect_timeout: Option<Duration>,
    pub(super) keep_alive: Option<u64>,
    pub(super) disable_redirects: bool,
    pub(super) follow_host_redirects: bool,
}

impl HttpTransport {
    pub(super) fn build_client_pair(config: ClientBuildConfig<'_>) -> Result<ClientPair> {
        let ClientBuildConfig {
            timeout,
            accept_invalid_certs,
            proxy_url,
            proxy_auth,
            connect_timeout,
            keep_alive,
            disable_redirects,
            follow_host_redirects,
        } = config;
        let make_builder = || -> Result<reqwest::ClientBuilder> {
            let default_headers =
                default_browser_navigation_header_map(&crate::config::default_user_agent())
                    .map_err(|e| Error::Transport {
                        url: String::new(),
                        message: format!("failed to build default browser headers: {e}"),
                        source: Some(Box::new(e)),
                    })?;
            let builder = reqwest::Client::builder()
                .timeout(timeout)
                .connect_timeout(connect_timeout.unwrap_or(CLIENT_CONNECT_TIMEOUT))
                .pool_max_idle_per_host(PER_HOST_CONCURRENCY_LIMIT)
                .pool_idle_timeout(CLIENT_POOL_IDLE_TIMEOUT)
                .tcp_keepalive(keep_alive.map_or(Duration::from_secs(30), Duration::from_secs))
                .tcp_keepalive_interval(
                    keep_alive.map_or(Duration::from_secs(30), Duration::from_secs),
                )
                .tcp_nodelay(true)
                .danger_accept_invalid_certs(accept_invalid_certs)
                // HTTP/2 is automatically negotiated via ALPN during TLS handshake.
                // For HTTPS targets (the vast majority), reqwest + rustls will use
                // HTTP/2 when the server supports it, falling back to HTTP/1.1.
                // This gives us multiplexing  -  ALL requests to the same host over
                // ONE TCP connection with multiplexed streams.
                .default_headers(default_headers);

            if let Some(proxy_url) = proxy_url {
                let mut proxy = reqwest::Proxy::all(proxy_url).map_err(|e| Error::Transport {
                    url: String::new(),
                    message: format!("failed to configure proxy: {e}"),
                    source: Some(Box::new(e)),
                })?;
                if let Some((username, password)) = proxy_auth {
                    proxy = proxy.basic_auth(username, password);
                }
                return Ok(builder.proxy(proxy));
            }
            Ok(builder)
        };

        let client_redirect = reqwest::redirect::Policy::none();
        let redirect_client_redirect = if disable_redirects {
            reqwest::redirect::Policy::none()
        } else if follow_host_redirects {
            reqwest::redirect::Policy::custom(|attempt| {
                let max = super::CURRENT_MAX_REDIRECTS.try_with(|&m| m).unwrap_or(10) as usize;
                if attempt.previous().len() >= max {
                    return attempt.stop();
                }
                // Only follow redirects to the same host as the original request
                if let (Some(original), Some(current)) = (
                    attempt
                        .previous()
                        .first()
                        .and_then(|u| u.host_str().map(str::to_string)),
                    attempt.url().host_str(),
                ) && original != current
                {
                    return attempt.stop();
                }
                attempt.follow()
            })
        } else {
            reqwest::redirect::Policy::custom(|attempt| {
                let max = super::CURRENT_MAX_REDIRECTS.try_with(|&m| m).unwrap_or(10) as usize;
                if attempt.previous().len() >= max {
                    return attempt.stop();
                }
                if redirect_target_is_safe(attempt.url()) {
                    attempt.follow()
                } else {
                    attempt.stop()
                }
            })
        };

        let client = make_builder()?
            .redirect(client_redirect)
            .build()
            .map_err(|e| Error::Transport {
                url: String::new(),
                message: format!("failed to build HTTP client: {e}"),
                source: Some(Box::new(e)),
            })?;

        let redirect_client = make_builder()?
            .redirect(redirect_client_redirect)
            .build()
            .map_err(|e| Error::Transport {
                url: String::new(),
                message: format!("failed to build redirect HTTP client: {e}"),
                source: Some(Box::new(e)),
            })?;

        Ok(ClientPair {
            client,
            redirect_client,
        })
    }
}
