#![allow(clippy::implicit_hasher)]
//! Header normalization and request camouflage to match real browsers.

use crate::config::DEFAULT_HTTP_PROFILE;
use crate::header_injection::{cloudflare_bypass_headers, generic_bypass_headers};
use crate::tls_profiles::rotate;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT};
use std::collections::HashMap;
use std::time::Duration;
use guise_pacing::Jitter;
use guise_profiles::{
    canonical_navigation_header_name, default_profile_request_headers_without_compression,
    named_profile, profile_request_headers, BrowserRequestHeaders, BrowserRequestKind,
};
use thiserror::Error;

/// Errors from building typed browser header maps.
#[derive(Debug, Error)]
pub(crate) enum BrowserHeaderMapError {
    /// Shared profile catalog emitted an invalid header name.
    #[error(
        "invalid shared stealth browser header name `{name}`. Fix: repair `stealth-profiles` before building scanner clients."
    )]
    InvalidProfileHeaderName {
        /// Invalid profile header name.
        name: &'static str,
    },
    /// Shared profile catalog emitted an invalid header value.
    #[error(
        "invalid shared stealth browser header value for `{name}`. Fix: repair `stealth-profiles` before building scanner clients."
    )]
    InvalidProfileHeaderValue {
        /// Profile header name whose value was invalid.
        name: &'static str,
    },
    /// Configured User-Agent is not valid as an HTTP header value.
    #[error(
        "invalid default browser User-Agent header. Fix: set `HttpConfig.user_agent` to an HTTP-safe browser-shaped value or repair the shared stealth profile catalog."
    )]
    InvalidUserAgent,
}

/// Configuration for request camouflage.
#[derive(Debug, Clone)]
pub struct HeaderConfig {
    /// Whether to apply browser-like headers.
    pub use_browser_headers: bool,
    /// The specific browser profile to use (if any).
    pub profile_name: Option<String>,
    /// Maximum jitter to apply to requests in milliseconds.
    pub jitter_max_ms: u64,
    /// Whether to include WAF bypass headers.
    pub enable_waf_bypass: bool,
    /// Internal IP to use for WAF bypass.
    pub waf_bypass_ip: Option<String>,
}

impl HeaderConfig {
    /// Returns a configuration enabling browser-like headers.
    #[must_use]
    pub fn browser_compat() -> Self {
        Self {
            use_browser_headers: true,
            profile_name: None,
            jitter_max_ms: 0,
            enable_waf_bypass: false,
            waf_bypass_ip: None,
        }
    }

    /// Returns a configuration with camouflage disabled.
    #[must_use]
    pub fn disabled() -> Self {
        Self {
            use_browser_headers: false,
            profile_name: None,
            jitter_max_ms: 0,
            enable_waf_bypass: false,
            waf_bypass_ip: None,
        }
    }
}

/// Applies browser-like headers to the given request builder directly, avoiding intermediate allocations.
pub fn apply_browser_headers_to_request(
    mut req: reqwest::RequestBuilder,
    user_headers: &HashMap<String, String>,
    config: &HeaderConfig,
    index: usize,
) -> reqwest::RequestBuilder {
    if config.enable_waf_bypass {
        let ip = config.waf_bypass_ip.as_deref().unwrap_or("127.0.0.1");

        for (k, v) in cloudflare_bypass_headers() {
            if !contains_header(user_headers, &k) {
                req = req.header(&k, &v);
            }
        }

        for (k, v) in generic_bypass_headers(ip) {
            if !contains_header(user_headers, &k) {
                req = req.header(&k, &v);
            }
        }
    }

    if config.use_browser_headers {
        for header in selected_browser_headers(config.profile_name.as_deref(), index).as_slice() {
            if !contains_header(user_headers, header.name) {
                req = req.header(header.name, header.value);
            }
        }
    }

    req
}

/// Applies browser-like headers to a mutable header map.
///
/// Existing user-supplied headers win case-insensitively, matching
/// `apply_browser_headers_to_request`.
pub fn apply_browser_headers(
    headers: &mut HashMap<String, String>,
    config: &HeaderConfig,
    index: usize,
) {
    if config.enable_waf_bypass {
        let ip = config.waf_bypass_ip.as_deref().unwrap_or("127.0.0.1");

        for (k, v) in cloudflare_bypass_headers() {
            insert_if_absent_case_insensitive(headers, k, v);
        }

        for (k, v) in generic_bypass_headers(ip) {
            insert_if_absent_case_insensitive(headers, k, v);
        }
    }

    if !config.use_browser_headers {
        return;
    }

    for header in selected_browser_headers(config.profile_name.as_deref(), index).as_slice() {
        insert_if_absent_case_insensitive(
            headers,
            canonical_navigation_header_name(header.name),
            header.value,
        );
    }
}

/// Build the default browser navigation headers for pooled scanner clients.
///
/// Compression negotiation is intentionally excluded: the default reqwest build
/// for `scanclient` does not enable transparent gzip/br decompression.
pub(crate) fn default_browser_navigation_header_map(
    user_agent: &str,
) -> Result<HeaderMap, BrowserHeaderMapError> {
    let mut headers = HeaderMap::new();
    for header in
        default_profile_request_headers_without_compression(BrowserRequestKind::Navigation)
            .as_slice()
    {
        let name = HeaderName::from_bytes(header.name.as_bytes())
            .map_err(|_| BrowserHeaderMapError::InvalidProfileHeaderName { name: header.name })?;
        let value = if name == USER_AGENT {
            HeaderValue::from_str(user_agent)
                .map_err(|_| BrowserHeaderMapError::InvalidUserAgent)?
        } else {
            HeaderValue::from_str(header.value).map_err(|_| {
                BrowserHeaderMapError::InvalidProfileHeaderValue { name: header.name }
            })?
        };
        headers.insert(name, value);
    }
    Ok(headers)
}

fn selected_browser_headers(profile_name: Option<&str>, index: usize) -> BrowserRequestHeaders {
    if let Some(profile) = profile_name.and_then(named_profile) {
        return profile_request_headers(profile, BrowserRequestKind::Navigation);
    }
    let profile = named_profile(rotate(index).name).unwrap_or(DEFAULT_HTTP_PROFILE);
    profile_request_headers(profile, BrowserRequestKind::Navigation)
}

fn contains_header(user_headers: &HashMap<String, String>, name: &str) -> bool {
    user_headers
        .keys()
        .any(|existing| existing.eq_ignore_ascii_case(name))
}

fn insert_if_absent_case_insensitive(
    headers: &mut HashMap<String, String>,
    name: impl AsRef<str>,
    value: impl Into<String>,
) {
    let name = name.as_ref();
    if contains_header(headers, name) {
        return;
    }
    headers.insert(name.to_string(), value.into());
}

/// Generates a random delay up to `max_ms`.
#[must_use]
pub fn random_delay(max_ms: u64) -> Duration {
    Jitter::up_to(max_ms).sample_thread()
}

#[cfg(test)]
mod tests {
    use super::default_browser_navigation_header_map;
    use reqwest::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, USER_AGENT};

    #[test]
    fn default_navigation_header_map_uses_shared_default_profile_without_compression() {
        let headers =
            default_browser_navigation_header_map("Operator-UA/1.0").expect("default headers");
        let facts = guise_profiles::default_profile_facts();

        assert_eq!(
            headers
                .get(USER_AGENT)
                .and_then(|value| value.to_str().ok()),
            Some("Operator-UA/1.0")
        );
        assert_eq!(
            headers.get(ACCEPT).and_then(|value| value.to_str().ok()),
            Some(facts.accept)
        );
        assert_eq!(
            headers
                .get(ACCEPT_LANGUAGE)
                .and_then(|value| value.to_str().ok()),
            Some(facts.accept_language)
        );
        assert_eq!(
            headers
                .get("Sec-Fetch-Mode")
                .and_then(|value| value.to_str().ok()),
            Some("navigate")
        );
        assert!(headers.get(ACCEPT_ENCODING).is_none());
    }
}
