#![allow(clippy::missing_errors_doc)]
#![allow(clippy::struct_excessive_bools)]
//! Concurrent HTTP transport with per-host rate limiting, connection pooling,
//! proxy support, and configurable request/response timeouts.

mod client;
/// Cookie store + per-host expiry tracking. Made `pub` so consumers
/// outside the http_transport module (e.g. karyx-transport, which
/// used to maintain its own near-identical copy with a 4-line diff)
/// can reuse the canonical implementation instead of forking it.
pub mod cookie;
mod dns;
pub(crate) mod execute;
/// SSRF target guard (`is_internal_ip`, `ensure_external_target`), split out of
/// `execute` to keep that module under the modularity limit.
pub(crate) mod target_guard;
#[cfg(test)]
mod tests;

use crate::adaptive_rate::AdaptiveRateLimiter;
use crate::camouflage::HeaderConfig;
use dashmap::DashMap;
use rustc_hash::FxHashMap;
use secir::{Error, Result};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, RwLock};
use std::time::Duration;
use tokio::sync::{Mutex as AsyncMutex, Semaphore};

use self::client::{ClientBuildConfig, ClientPair};
use self::cookie::CookieJar;
#[cfg(test)]
use self::cookie::{CookieScope, host_key, parse_set_cookie};
use self::dns::CachedDnsResolver;
use self::execute::RequestCoordinator;
#[cfg(test)]
use self::execute::is_internal_ip;

tokio::task_local! {
    pub(super) static CURRENT_MAX_REDIRECTS: u32;
}

pub(super) const DEFAULT_MAX_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
pub(super) const GLOBAL_CONCURRENCY_LIMIT: usize = 50;
pub(super) const PER_HOST_CONCURRENCY_LIMIT: usize = 10;
pub(super) const CLIENT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
pub(super) const CLIENT_POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(90);

/// Configuration options for the `HttpTransport`.
#[derive(Debug, Clone)]
pub struct HttpTransportOptions {
    /// Optional global rate limit in requests per second.
    pub rate_limit_rps: Option<u32>,
    /// Optional fixed delay applied between requests globally.
    pub fixed_delay: Option<Duration>,
    /// Optional maximum allowed response body size.
    pub max_response_size: Option<usize>,
    /// Whether to allow requests to internal/private IP addresses.
    pub allow_internal: bool,
    /// Whether to accept invalid TLS certificates.
    pub accept_invalid_certs: bool,
    /// Optional single proxy URL to route traffic through.
    pub proxy_url: Option<String>,
    /// Optional list of proxy URLs to rotate requests among.
    pub proxy_urls: Vec<String>,
    /// Optional `(username, password)` for proxy authentication.
    pub proxy_auth: Option<(String, String)>,
    /// Number of retries for failed requests.
    pub retries: usize,
    /// Optional connection timeout for the underlying client in seconds.
    pub dialer_timeout: Option<u64>,
    /// Optional TCP keep-alive interval in seconds.
    pub dialer_keep_alive: Option<u64>,
    /// Whether to automatically follow redirects across hosts.
    pub follow_host_redirects: bool,
    /// Whether to completely disable following redirects.
    pub disable_redirects: bool,
    /// Optional custom SNI string for TLS handshakes.
    pub sni: Option<String>,
    /// Optional browser profile name for header camouflage.
    ///
    /// Names use the shared `stealth-profiles` catalog aliases such as
    /// `chrome`, `chrome-android`, `firefox-windows`, `safari`, `edge`,
    /// and `ie11`. Unknown names keep the legacy deterministic rotation.
    pub browser_profile: Option<String>,
    /// Whether to dynamically mimic browser headers and behavior.
    pub camouflage: bool,
    /// Whether to attempt WAF evasion techniques (e.g. injecting bypass headers).
    pub evasion: bool,
    /// Maximum additional randomized delay (jitter) to add to requests in milliseconds.
    pub jitter_max_ms: u64,
}

impl Default for HttpTransportOptions {
    fn default() -> Self {
        Self {
            rate_limit_rps: None,
            fixed_delay: None,
            max_response_size: Some(DEFAULT_MAX_RESPONSE_SIZE),
            allow_internal: false,
            accept_invalid_certs: false,
            proxy_url: None,
            proxy_urls: Vec::new(),
            proxy_auth: None,
            retries: 1,
            dialer_timeout: None,
            dialer_keep_alive: None,
            follow_host_redirects: false,
            disable_redirects: false,
            sni: None,
            browser_profile: None,
            camouflage: false,
            evasion: false,
            jitter_max_ms: 0,
        }
    }
}

/// A robust, configurable HTTP transport layer built for scanning tasks.
#[derive(Clone)]
pub struct HttpTransport {
    client: reqwest::Client,
    redirect_client: reqwest::Client,
    concurrency: usize,
    max_response_size: usize,
    allow_internal: bool,
    proxy_url: Option<String>,
    proxy_urls: Vec<String>,
    proxy_auth: Option<(String, String)>,
    rotating_clients: Vec<ClientPair>,
    rate_limit: Option<Arc<RateLimiter>>,
    fixed_delay: Option<Arc<RateLimiter>>,
    retries: usize,
    cookie_jar: Arc<CookieJar>,
    request_coordinator: Arc<RequestCoordinator>,
    dns_resolver: Arc<CachedDnsResolver>,
    global_semaphore: Arc<Semaphore>,
    host_semaphores: Arc<DashMap<String, Arc<Semaphore>>>,
    adaptive_rate_limiter: Arc<AdaptiveRateLimiter>,
    header_config: Arc<HeaderConfig>,
    host_error_counts: Arc<DashMap<String, AtomicUsize>>,
    max_host_errors: usize,
    host_error_tracking_enabled: bool,
}

pub(super) struct RateLimiter {
    pub(super) interval: Duration,
    last_request: AsyncMutex<std::time::Instant>,
}

impl RateLimiter {
    fn new(requests_per_second: u32) -> Self {
        Self {
            interval: Duration::from_secs_f64(1.0 / f64::from(requests_per_second)),
            last_request: AsyncMutex::new(std::time::Instant::now()),
        }
    }

    pub(super) async fn acquire(&self) {
        let mut last = self.last_request.lock().await;
        let now = std::time::Instant::now();
        let elapsed = now.duration_since(*last);
        if elapsed < self.interval {
            let wait = self.interval.saturating_sub(elapsed);
            *last = now + wait;
            drop(last);
            tokio::time::sleep(wait).await;
        } else {
            *last = now;
        }
    }
}

impl HttpTransport {
    /// Creates a new `HttpTransport` with the default configuration.
    #[must_use = "transport construction can fail; handle the returned transport or error"]
    pub fn new(concurrency: usize, timeout: Duration) -> Result<Self> {
        Self::with_transport_options(concurrency, timeout, HttpTransportOptions::default())
    }

    /// Returns the primary proxy URL, if configured.
    #[must_use]
    pub fn proxy_url(&self) -> Option<&str> {
        self.proxy_url.as_deref()
    }

    /// Returns the proxy authentication credentials, if configured.
    #[must_use]
    pub fn proxy_auth(&self) -> Option<(&str, &str)> {
        self.proxy_auth
            .as_ref()
            .map(|(username, password)| (username.as_str(), password.as_str()))
    }

    /// Returns the list of configured proxy URLs for rotation.
    #[must_use]
    pub fn proxy_urls(&self) -> &[String] {
        &self.proxy_urls
    }

    /// Determines the correct proxy URL to use for a specific request index.
    pub fn proxy_url_for_request_index(&self, index: usize) -> Option<&str> {
        if self.proxy_urls.is_empty() {
            self.proxy_url()
        } else {
            self.proxy_urls
                .get(index % self.proxy_urls.len())
                .map(String::as_str)
        }
    }

    /// Returns the configured retry count.
    #[must_use]
    pub fn retries(&self) -> usize {
        self.retries
    }

    /// Returns the maximum configured response size.
    #[must_use]
    pub fn max_response_size(&self) -> usize {
        self.max_response_size
    }

    /// Returns the configured fixed delay between requests.
    #[must_use]
    pub fn fixed_delay(&self) -> Option<Duration> {
        self.fixed_delay.as_ref().map(|limiter| limiter.interval)
    }

    /// Checks whether internal IP addresses are permitted targets.
    #[must_use]
    pub fn allows_internal(&self) -> bool {
        self.allow_internal
    }

    /// Configures the maximum allowed consecutive errors for a host before it is temporarily blocked.
    #[must_use]
    pub fn with_max_host_errors(mut self, max_errors: usize, enabled: bool) -> Self {
        self.max_host_errors = max_errors;
        self.host_error_tracking_enabled = enabled;
        self
    }

    /// Checks if a given host has exceeded its error threshold and is blocked.
    #[must_use]
    pub fn is_host_blocked(&self, host: &str) -> bool {
        if !self.host_error_tracking_enabled {
            return false;
        }
        self.host_error_counts
            .get(host)
            .is_some_and(|entry| entry.load(Ordering::Relaxed) >= self.max_host_errors)
    }

    /// Records an error event for a specific host, contributing to its error threshold.
    pub fn record_host_error(&self, host: &str) {
        if !self.host_error_tracking_enabled {
            return;
        }
        self.host_error_counts
            .entry(host.to_string())
            .or_insert_with(|| AtomicUsize::new(0))
            .fetch_add(1, Ordering::Relaxed);
    }

    /// Creates a new `HttpTransport` with the specified granular options.
    #[must_use = "transport construction can fail; handle the returned transport or error"]
    pub fn with_options(
        concurrency: usize,
        timeout: Duration,
        rate_limit_rps: Option<u32>,
        max_response_size: usize,
        accept_invalid_certs: bool,
        proxy_url: Option<String>,
    ) -> Result<Self> {
        Self::with_options_and_retries(
            concurrency,
            timeout,
            rate_limit_rps,
            max_response_size,
            accept_invalid_certs,
            proxy_url,
            1,
        )
    }

    /// Creates a new `HttpTransport` with granular options and a custom retry limit.
    #[must_use = "transport construction can fail; handle the returned transport or error"]
    pub fn with_options_and_retries(
        concurrency: usize,
        timeout: Duration,
        rate_limit_rps: Option<u32>,
        max_response_size: usize,
        accept_invalid_certs: bool,
        proxy_url: Option<String>,
        retries: usize,
    ) -> Result<Self> {
        Self::with_transport_options(
            concurrency,
            timeout,
            HttpTransportOptions {
                rate_limit_rps,
                max_response_size: Some(max_response_size),
                accept_invalid_certs,
                proxy_url,
                retries,
                ..HttpTransportOptions::default()
            },
        )
    }

    /// Creates a new `HttpTransport` using a predefined `HttpTransportOptions` bundle.
    #[must_use = "transport construction can fail; handle the returned transport or error"]
    pub fn with_transport_options(
        concurrency: usize,
        timeout: Duration,
        options: HttpTransportOptions,
    ) -> Result<Self> {
        let HttpTransportOptions {
            rate_limit_rps,
            fixed_delay,
            max_response_size,
            allow_internal,
            accept_invalid_certs,
            proxy_url,
            proxy_urls,
            proxy_auth,
            retries,
            dialer_timeout,
            dialer_keep_alive,
            follow_host_redirects,
            disable_redirects,
            sni,
            browser_profile,
            camouflage,
            evasion,
            jitter_max_ms,
        } = options;
        let default_proxy_url = if proxy_urls.is_empty() {
            proxy_url.clone()
        } else {
            None
        };
        if let Some(sni) = sni.as_deref() {
            return Err(Error::Transport {
                url: String::new(),
                message: format!(
                    "custom SNI override '{sni}' is not supported by the current reqwest transport"
                ),
                source: None,
            });
        }

        let dns_cache = Arc::new(RwLock::new(FxHashMap::default()));
        let dns_resolver = Arc::new(CachedDnsResolver::new(dns_cache));
        let base_client_config = Arc::new(ClientBuildConfig {
            timeout,
            accept_invalid_certs,
            proxy_url: default_proxy_url.as_deref(),
            proxy_auth: proxy_auth.as_ref(),
            connect_timeout: dialer_timeout.map(Duration::from_secs),
            keep_alive: dialer_keep_alive,
            disable_redirects,
            follow_host_redirects,
        });
        let default_clients = Self::build_client_pair((*base_client_config).clone())?;
        let rotating_clients = proxy_urls
            .iter()
            .map(|proxy| {
                Self::build_client_pair(ClientBuildConfig {
                    proxy_url: Some(proxy.as_str()),
                    ..(*base_client_config).clone()
                })
            })
            .collect::<Result<Vec<_>>>()?;

        let rate_limit = rate_limit_rps.map(|rps| Arc::new(RateLimiter::new(rps)));
        let retries = retries.max(1);
        let max_response_size = max_response_size.unwrap_or(DEFAULT_MAX_RESPONSE_SIZE);
        let header_config = Arc::new(HeaderConfig {
            use_browser_headers: camouflage || browser_profile.is_some(),
            profile_name: browser_profile,
            jitter_max_ms,
            enable_waf_bypass: evasion,
            waf_bypass_ip: None,
        });

        Ok(Self {
            client: default_clients.client,
            redirect_client: default_clients.redirect_client,
            concurrency: concurrency.max(1),
            max_response_size,
            allow_internal,
            proxy_url: default_proxy_url,
            proxy_urls,
            proxy_auth,
            rotating_clients,
            rate_limit,
            fixed_delay: fixed_delay.map(|delay| {
                Arc::new(RateLimiter {
                    interval: delay,
                    last_request: AsyncMutex::new(std::time::Instant::now()),
                })
            }),
            retries,
            cookie_jar: Arc::new(CookieJar::default()),
            request_coordinator: Arc::new(RequestCoordinator::default()),
            dns_resolver,
            global_semaphore: Arc::new(Semaphore::new(GLOBAL_CONCURRENCY_LIMIT)),
            host_semaphores: Arc::new(DashMap::default()),
            adaptive_rate_limiter: Arc::new(AdaptiveRateLimiter::default()),
            header_config,
            host_error_counts: Arc::new(DashMap::default()),
            max_host_errors: 30,
            host_error_tracking_enabled: true,
        })
    }

    /// Pre-resolves DNS records for a batch of target URLs to eliminate per-request resolution latency.
    pub async fn pre_resolve_targets(&self, targets: &[secir::transport::TargetUrl]) {
        let hosts = targets
            .iter()
            .filter_map(|target| {
                url::Url::parse(target.as_str())
                    .ok()
                    .and_then(|url| url.host_str().map(str::to_ascii_lowercase))
            })
            .collect::<std::collections::HashSet<_>>()
            .into_iter()
            .collect::<Vec<_>>();
        self.dns_resolver.pre_resolve_hosts(&hosts).await;
    }
}

/// Ensure TCP Fast Open is enabled for outbound connections.
/// On Linux, TFO client mode is controlled by /`proc/sys/net/ipv4/tcp_fastopen`.
/// Bit 0 (value 1) = client mode enabled. We check and warn if not available.
pub fn ensure_tcp_fastopen() {
    #[cfg(target_os = "linux")]
    {
        match std::fs::read_to_string("/proc/sys/net/ipv4/tcp_fastopen") {
            Ok(val) => {
                let flags: u32 = val.trim().parse().unwrap_or(0);
                if flags & 1 == 0 {
                    tracing::warn!(
                        "TCP Fast Open client mode not enabled (tcp_fastopen={}). \
                         Enable with: sysctl -w net.ipv4.tcp_fastopen=3",
                        flags
                    );
                } else {
                    tracing::debug!("TCP Fast Open client mode enabled (tcp_fastopen={})", flags);
                }
            }
            Err(e) => {
                tracing::debug!("could not read tcp_fastopen: {e}");
            }
        }
    }
}
