//! HTTP response wrappers.
//!
//! Provides [`ScanResponse`], a high-level wrapper around HTTP responses
//! with convenient methods for accessing status, headers, and body content.

use reqwest::{
    StatusCode,
    header::{HeaderMap, HeaderName},
};

/// Final normalized HTTP response returned by `ScanClient` queries.
#[derive(Debug, Clone)]
pub struct ScanResponse {
    status: StatusCode,
    headers: HeaderMap,
    body: Vec<u8>,
}

impl ScanResponse {
    pub(crate) async fn from_response(
        mut response: reqwest::Response,
        max_body_size: usize,
    ) -> crate::Result<Self> {
        let status = response.status();
        let headers = response.headers().clone();
        if let Some(content_length) = response.content_length() {
            if content_length > max_body_size as u64 {
                return Err(crate::Error::ResponseBodyTooLarge {
                    limit: max_body_size,
                    observed: content_length,
                });
            }
        }

        let mut body = Vec::new();
        while let Some(chunk) = response.chunk().await? {
            let observed = body.len().saturating_add(chunk.len());
            if observed > max_body_size {
                return Err(crate::Error::ResponseBodyTooLarge {
                    limit: max_body_size,
                    observed: observed as u64,
                });
            }
            body.extend_from_slice(&chunk);
        }

        Ok(Self {
            status,
            headers,
            body,
        })
    }

    /// Returns the HTTP status code.
    ///
    /// Example:
    /// ```rust
    /// use reqwest::StatusCode;
    /// # let response = scanclient::ScanResponse::from_parts(StatusCode::OK, reqwest::header::HeaderMap::new(), b"ok".to_vec());
    /// assert_eq!(response.status(), StatusCode::OK);
    /// ```
    pub fn status(&self) -> StatusCode {
        self.status
    }

    /// Access the HTTP response headers.
    ///
    /// Returns a reference to the `HeaderMap` containing all response headers.
    /// Use `header_value()` for a convenient way to access specific headers by name.
    ///
    /// # Returns
    /// A reference to the `HeaderMap` containing all response headers.
    ///
    /// # Example
    /// ```rust
    /// use reqwest::StatusCode;
    /// # let response = scanclient::ScanResponse::from_parts(
    /// #     StatusCode::OK,
    /// #     reqwest::header::HeaderMap::new(),
    /// #     b"ok".to_vec()
    /// # );
    /// let headers = response.headers();
    /// for (name, value) in headers.iter() {
    ///     println!("{}: {:?}", name, value);
    /// }
    /// ```
    pub fn headers(&self) -> &HeaderMap {
        &self.headers
    }

    /// Access the raw response body as bytes.
    ///
    /// Returns a byte slice containing the full response body.
    /// For text responses, consider using `body_text()` instead.
    ///
    /// # Returns
    /// A byte slice (`&[u8]`) containing the response body.
    ///
    /// # Example
    /// ```rust
    /// use reqwest::StatusCode;
    /// # let response = scanclient::ScanResponse::from_parts(
    /// #     StatusCode::OK,
    /// #     reqwest::header::HeaderMap::new(),
    /// #     b"Hello, World!".to_vec()
    /// # );
    /// let bytes = response.body_bytes();
    /// assert_eq!(bytes, b"Hello, World!");
    /// ```
    pub fn body_bytes(&self) -> &[u8] {
        &self.body
    }

    /// Parse the response body as UTF-8 text.
    ///
    /// Attempts to interpret the response body as a UTF-8 encoded string.
    /// Returns an error if the body contains invalid UTF-8 sequences.
    /// For lossy conversion, use `String::from_utf8_lossy()` on `body_bytes()`.
    ///
    /// # Returns
    /// - `Ok(&str)` if the body is valid UTF-8.
    /// - `Err(Utf8Error)` if the body contains invalid UTF-8 sequences.
    ///
    /// # Example
    /// ```rust
    /// use reqwest::StatusCode;
    /// # let response = scanclient::ScanResponse::from_parts(
    /// #     StatusCode::OK,
    /// #     reqwest::header::HeaderMap::new(),
    /// #     b"Hello, World!".to_vec()
    /// # );
    /// match response.body_text() {
    ///     Ok(text) => println!("Body: {}", text),
    ///     Err(e) => println!("Invalid UTF-8: {}", e),
    /// }
    /// ```
    pub fn body_text(&self) -> std::result::Result<&str, std::str::Utf8Error> {
        std::str::from_utf8(&self.body)
    }

    /// Check if the response body contains a specific substring.
    ///
    /// Performs a case-sensitive substring search on the response body.
    /// If the body is valid UTF-8, it searches the text directly.
    /// Otherwise, it falls back to lossy UTF-8 conversion for the search.
    ///
    /// # Parameters
    /// - `needle`: The substring to search for.
    ///
    /// # Returns
    /// `true` if the body contains the substring, `false` otherwise.
    ///
    /// # Example
    /// ```rust
    /// use reqwest::StatusCode;
    /// # let response = scanclient::ScanResponse::from_parts(
    /// #     StatusCode::OK,
    /// #     reqwest::header::HeaderMap::new(),
    /// #     b"Error: Invalid API key".to_vec()
    /// # );
    /// if response.contains("Error") {
    ///     println!("Response contains an error message");
    /// }
    /// ```
    pub fn contains(&self, needle: &str) -> bool {
        match self.body_text() {
            Ok(text) => text.contains(needle),
            Err(_) => String::from_utf8_lossy(&self.body).contains(needle),
        }
    }

    /// Get the value of a specific response header.
    ///
    /// Retrieves a header value by its name. The search is case-insensitive
    /// as per HTTP specification. Returns `None` if the header is not present
    /// or if the header value contains non-UTF-8 bytes.
    ///
    /// # Parameters
    /// - `name`: The name of the header to retrieve (case-insensitive).
    ///
    /// # Returns
    /// - `Some(&str)` with the header value if found and valid UTF-8.
    /// - `None` if the header is not present or not valid UTF-8.
    ///
    /// # Example
    /// ```rust
    /// use reqwest::StatusCode;
    /// # let response = scanclient::ScanResponse::from_parts(
    /// #     StatusCode::OK,
    /// #     reqwest::header::HeaderMap::new(),
    /// #     b"ok".to_vec()
    /// # );
    /// if let Some(content_type) = response.header_value("Content-Type") {
    ///     println!("Content-Type: {}", content_type);
    /// }
    /// ```
    pub fn header_value(&self, name: &str) -> Option<&str> {
        let name = HeaderName::from_bytes(name.as_bytes()).ok()?;
        self.headers.get(name)?.to_str().ok()
    }

    /// Construct a response from already-materialized parts.
    ///
    /// This is mainly useful in tests and adapters that already have the body bytes.
    pub fn from_parts(status: StatusCode, headers: HeaderMap, body: Vec<u8>) -> Self {
        Self {
            status,
            headers,
            body,
        }
    }
}
