//! Scanning HTTP client tuned for security tooling.

use std::fmt;

use reqwest::{Method, RequestBuilder, header::LOCATION};
use tokio::time::sleep;
use tower::{Service, ServiceExt};

use crate::metrics::{RateGate, build_rate_gate};
use crate::request::{build_redirect_request, ensure_safe_redirect_target};
use crate::retry::{
    backoff_delay, retry_delay_for_status, should_retry_error, should_retry_status,
};
use crate::{Error, HttpConfig, Result, ScanResponse};

use hickory_resolver::TokioResolver;
use std::sync::Arc;

/// A high-performance, retry-aware asynchronous HTTP client optimized for security scanning.
#[derive(Clone)]
pub struct ScanClient {
    client: reqwest::Client,
    /// Configuration used to build this client.
    pub config: HttpConfig,
    /// Rate limiter, if configured.
    pub rate_gate: Option<RateGate>,
}

impl fmt::Debug for ScanClient {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ScanClient")
            .field("config", &self.config)
            .finish_non_exhaustive()
    }
}

impl ScanClient {
    /// Instantiate the client from a predefined `HttpConfig` and shared resolver.
    pub fn new(config: HttpConfig, resolver: Arc<TokioResolver>) -> Result<Self> {
        let client = crate::pool::build_client(&config, Arc::clone(&resolver))?;
        let rate_gate = build_rate_gate(config.rate_limit_per_sec)?;

        Ok(Self {
            client,
            config,
            rate_gate,
        })
    }

    /// Instantiate the client from a predefined `HttpConfig` using a default resolver.
    ///
    /// # Deprecation
    ///
    /// Prefer [`ScanClient::new`] with a shared [`TokioResolver`] when building
    /// multiple clients in the same process: a shared resolver reuses the DNS
    /// cache and avoids creating a separate thread-pool per client.
    ///
    /// This convenience constructor is correct and safe - it builds an ephemeral
    /// system-defaults resolver on every call and is suitable for one-shot tools
    /// and tests. The cost is that each call allocates its own background resolver
    /// thread.
    ///
    /// # Errors
    ///
    /// Returns `Error::ResolverInit` if the system DNS resolver cannot be
    /// initialised, or any other config-validation error from [`Self::new`].
    pub fn from_config(config: HttpConfig) -> Result<Self> {
        let resolver = Arc::new(
            TokioResolver::builder_tokio()
                .map_err(|e| Error::ResolverInit {
                    message: e.to_string(),
                })?
                .build(),
        );
        Self::new(config, resolver)
    }

    /// Perform an asynchronous GET request to the specified URL.
    ///
    /// Example:
    /// ```rust,no_run
    /// use scanclient::{HttpConfig, ScanClient};
    ///
    /// #[tokio::main]
    /// async fn main() -> scanclient::Result<()> {
    ///     let client = ScanClient::from_config(HttpConfig::default())?;
    ///     let _response = client.get("https://example.com").await?;
    ///     Ok(())
    /// }
    /// ```
    pub async fn get(&self, url: &str) -> Result<ScanResponse> {
        self.execute(self.client.get(url)).await
    }

    /// Perform an asynchronous HEAD request to the specified URL.
    ///
    /// HEAD requests are useful for checking resource metadata without
    /// downloading the full response body.
    ///
    /// # Parameters
    /// - `url`: The target URL to request.
    ///
    /// # Returns
    /// A `ScanResponse` containing the response status and headers.
    ///
    /// # Errors
    /// Returns an error if the request fails or all retry attempts are exhausted.
    ///
    /// # Example
    /// ```rust,no_run
    /// use scanclient::{HttpConfig, ScanClient};
    ///
    /// #[tokio::main]
    /// async fn main() -> scanclient::Result<()> {
    ///     let client = ScanClient::from_config(HttpConfig::default())?;
    ///     let response = client.head("https://example.com/resource").await?;
    ///     println!("Content-Length: {:?}", response.header_value("content-length"));
    ///     Ok(())
    /// }
    /// ```
    pub async fn head(&self, url: &str) -> Result<ScanResponse> {
        self.execute(self.client.head(url)).await
    }

    /// Perform an asynchronous POST request with a body payload.
    ///
    /// POST requests are typically used to submit data to a server,
    /// such as form submissions or API requests.
    ///
    /// # Parameters
    /// - `url`: The target URL to send the POST request to.
    /// - `body`: The request body payload. Can be a string, bytes, or any type
    ///   implementing `Into<reqwest::Body>`.
    ///
    /// # Returns
    /// A `ScanResponse` containing the server's response.
    ///
    /// # Errors
    /// Returns an error if the request fails or all retry attempts are exhausted.
    ///
    /// # Example
    /// ```rust,no_run
    /// use scanclient::{HttpConfig, ScanClient};
    ///
    /// #[tokio::main]
    /// async fn main() -> scanclient::Result<()> {
    ///     let client = ScanClient::from_config(HttpConfig::default())?;
    ///     let response = client.post(
    ///         "https://api.example.com/data",
    ///         r#"{"key": "value"}"#
    ///     ).await?;
    ///     println!("Status: {}", response.status());
    ///     Ok(())
    /// }
    /// ```
    pub async fn post<B>(&self, url: &str, body: B) -> Result<ScanResponse>
    where
        B: Into<reqwest::Body>,
    {
        self.execute(self.client.post(url).body(body)).await
    }

    /// Create a custom `RequestBuilder` utilizing the internal connection pool.
    ///
    /// This method allows building custom HTTP requests with full control over
    /// headers, query parameters, and body content using reqwest's `RequestBuilder`.
    ///
    /// # Parameters
    /// - `method`: The HTTP method (GET, POST, PUT, DELETE, etc.).
    /// - `url`: The target URL for the request.
    ///
    /// # Returns
    /// A `reqwest::RequestBuilder` configured with the client's default headers
    /// and settings.
    ///
    /// # Example
    /// ```rust,no_run
    /// use scanclient::{HttpConfig, ScanClient};
    /// use reqwest::Method;
    ///
    /// #[tokio::main]
    /// async fn main() -> scanclient::Result<()> {
    ///     let client = ScanClient::from_config(HttpConfig::default())?;
    ///     let request = client.request(Method::PUT, "https://api.example.com/items/1")
    ///         .header("Content-Type", "application/json")
    ///         .body(r#"{"name": "updated"}"#);
    ///     let response = client.execute(request).await?;
    ///     println!("Status: {}", response.status());
    ///     Ok(())
    /// }
    /// ```
    pub fn request(&self, method: Method, url: &str) -> RequestBuilder {
        self.client.request(method, url)
    }

    /// Execute a constructed request through the retry and rate-limiting pipeline.
    ///
    /// This method takes a `RequestBuilder` (created via `request()` or from
    /// the underlying reqwest client) and executes it with automatic retries
    /// and rate limiting as configured in `HttpConfig`.
    ///
    /// # Parameters
    /// - `builder`: A `RequestBuilder` configured with the desired request settings.
    ///
    /// # Returns
    /// A `ScanResponse` containing the server's response after any retries.
    ///
    /// # Errors
    /// - `Error::UnclonableRequest` if the request body cannot be cloned for retries.
    /// - `Error::RetryExhausted` if all retry attempts fail.
    /// - `Error::Request` for other request failures.
    ///
    /// # Example
    /// ```rust,no_run
    /// use scanclient::{HttpConfig, ScanClient};
    ///
    /// #[tokio::main]
    /// async fn main() -> scanclient::Result<()> {
    ///     let client = ScanClient::from_config(HttpConfig::default())?;
    ///     let request = client.request(reqwest::Method::PATCH, "https://api.example.com/items/1")
    ///         .header("Content-Type", "application/json")
    ///         .body(r#"{"field": "value"}"#);
    ///     let response = client.execute(request).await?;
    ///     println!("Status: {}", response.status());
    ///     Ok(())
    /// }
    /// ```
    pub async fn execute(&self, builder: RequestBuilder) -> Result<ScanResponse> {
        let response = self.execute_raw(builder).await?;
        ScanResponse::from_response(response, self.config.max_body_size).await
    }

    async fn execute_raw(&self, builder: RequestBuilder) -> Result<reqwest::Response> {
        let max_attempts = self.config.max_retries.saturating_add(1);
        let request = builder.build().map_err(Error::Request)?;
        let method = request.method().clone();
        let retryable_method =
            self.config.retry_non_idempotent_methods || is_idempotent_method(&method);

        let mut attempt = 0;
        loop {
            self.acquire_rate_slot().await?;

            let request_attempt = request.try_clone().ok_or(Error::UnclonableRequest)?;
            match self.send_with_redirects(request_attempt).await {
                Ok(response)
                    if retryable_method
                        && should_retry_status(response.status())
                        && attempt + 1 < max_attempts =>
                {
                    sleep(retry_delay_for_status(
                        self.config.retry_delay_ms,
                        attempt,
                        response.status(),
                        response.headers(),
                    ))
                    .await;
                }
                Ok(response) => return Ok(response),
                Err(Error::Request(error))
                    if retryable_method
                        && should_retry_error(&error)
                        && attempt + 1 < max_attempts =>
                {
                    sleep(backoff_delay(self.config.retry_delay_ms, attempt)).await;
                }
                Err(Error::Request(error)) if retryable_method && should_retry_error(&error) => {
                    return Err(Error::RetryExhausted { source: error });
                }
                Err(error) => return Err(error),
            }
            attempt += 1;
        }
    }

    async fn send_with_redirects(
        &self,
        mut request: reqwest::Request,
    ) -> Result<reqwest::Response> {
        let mut redirect_count = 0_usize;

        loop {
            let request_snapshot = request.try_clone().ok_or(Error::UnclonableRequest)?;
            let response = self.client.execute(request).await.map_err(Error::Request)?;

            // If not a redirect, return the response
            if !response.status().is_redirection() {
                return Ok(response);
            }

            // If we've reached the redirect limit, return an explicit error
            if redirect_count >= self.config.max_redirects {
                return Err(Error::RedirectLimitExceeded {
                    url: response.url().to_string(),
                    max: self.config.max_redirects,
                });
            }

            let Some(location) = response.headers().get(LOCATION) else {
                return Ok(response);
            };
            let location = location
                .to_str()
                .map_err(|_| Error::InvalidRedirectLocation {
                    location: String::from("<non-utf8>"),
                })?;
            let next_url =
                response
                    .url()
                    .join(location)
                    .map_err(|_| Error::InvalidRedirectLocation {
                        location: location.to_string(),
                    })?;

            // Disallow automatic redirect following when a proxy is configured,
            // because the proxy performs DNS resolution which makes local checks
            // ineffective and may result in SSRF.
            if self.config.proxy.is_some() {
                return Err(Error::RedirectBlocked {
                    url: next_url.to_string(),
                });
            }

            ensure_safe_redirect_target(&next_url).await?;
            request = build_redirect_request(&request_snapshot, response.status(), next_url)?;
            redirect_count += 1;
        }
    }

    async fn acquire_rate_slot(&self) -> Result<()> {
        let Some(rate_gate) = &self.rate_gate else {
            return Ok(());
        };

        let mut gate = rate_gate.clone();
        gate.ready().await.map_err(Error::RateLimiter)?;
        gate.call(()).await.map_err(Error::RateLimiter)?;
        Ok(())
    }
}

fn is_idempotent_method(method: &Method) -> bool {
    matches!(
        *method,
        Method::GET | Method::HEAD | Method::OPTIONS | Method::TRACE | Method::PUT | Method::DELETE
    )
}
