//! Hyper ↔ Ruxno conversion utilities
//!
//! This module provides lossless conversion between Hyper's HTTP types
//! and Ruxno's domain models. It handles:
//!
//! - Request conversion (method, URI, headers, body buffering)
//! - Response conversion (status, headers, body types)
//! - Query parameter parsing
//! - Header mapping (zero-copy where possible)
//!
//! # Design (item 13)
//!
//! Conversions that require extra parameters (`max_body_size`, `max_headers`)
//! are encapsulated in [`RequestConverter`] rather than exposed as free
//! functions. This follows AGENTS.md: "use built-in conversion traits instead
//! of custom conversion methods". The response direction is parameter-free so
//! it is implemented as `From<Response>` directly.
//!
//! # Examples
//!
//! ```rust,ignore
//! use ruxno::http::convert::RequestConverter;
//!
//! let converter = RequestConverter::new(1024 * 1024, 100); // 1 MB, 100 headers
//! let domain_req = converter.convert(hyper_req).await?;
//!
//! // Response conversion via From trait
//! let hyper_res: hyper::Response<_> = domain_res.into();
//! ```

use crate::core::CoreError;
use crate::domain::{Headers, Query, Request, Response};
use bytes::Bytes;
use http_body_util::BodyExt;
use hyper::body::Incoming;

// ── RequestConverter ─────────────────────────────────────────────────────────

/// Converts Hyper requests into domain [`Request`] objects.
///
/// Encapsulates the `max_body_size` and `max_headers` limits so callers don't
/// have to thread them through every call site. `RuxnoService` constructs one
/// converter at startup and reuses it for every request.
///
/// # Examples
///
/// ```rust,ignore
/// let converter = RequestConverter::new(1024 * 1024, 100);
/// let domain_req = converter.convert(hyper_req).await?;
/// let domain_req = converter.convert_for_upgrade(hyper_req).await?;
/// ```
pub struct RequestConverter {
    max_body_size: usize,
    max_headers: usize,
}

impl RequestConverter {
    /// Create a new converter with the given limits.
    pub fn new(max_body_size: usize, max_headers: usize) -> Self {
        Self {
            max_body_size,
            max_headers,
        }
    }

    /// Convert a normal Hyper request, buffering the body.
    ///
    /// Validates header count and body size, then buffers the full body into
    /// memory. Use [`convert_for_upgrade`] for WebSocket/SSE requests where
    /// the body must not be consumed.
    pub async fn convert(&self, req: hyper::Request<Incoming>) -> Result<Request, CoreError> {
        from_hyper_request(req, self.max_body_size, self.max_headers).await
    }

    /// Convert a Hyper request for protocol upgrades (WebSocket, SSE).
    ///
    /// Does **not** buffer the body. Stores the original hyper request in the
    /// domain `Request` so the upgrade handler can take it later.
    pub async fn convert_for_upgrade(
        &self,
        req: hyper::Request<Incoming>,
    ) -> Result<Request, CoreError> {
        from_hyper_request_for_upgrade(req, self.max_headers).await
    }
}

// ── Free functions (kept for internal use and back-compat) ───────────────────

/// Convert Hyper request to domain request with body size limits.
///
/// Prefer [`RequestConverter`] at call sites that already hold the limits.
/// This free function is kept for use inside `RequestConverter::convert` and
/// for tests that construct requests directly.
pub async fn from_hyper_request(
    req: hyper::Request<Incoming>,
    max_body_size: usize,
    max_headers: usize,
) -> Result<Request, CoreError> {
    let (parts, body) = req.into_parts();

    let header_count = parts.headers.len();
    if header_count > max_headers {
        return Err(CoreError::request_header_fields_too_large(format!(
            "Request has {} headers, exceeds maximum allowed {} headers",
            header_count, max_headers
        )));
    }

    if let Some(content_length) = parts.headers.get(hyper::header::CONTENT_LENGTH) {
        if let Ok(length_str) = content_length.to_str() {
            if let Ok(length) = length_str.parse::<usize>() {
                if length > max_body_size {
                    return Err(CoreError::payload_too_large(format!(
                        "Request body size {} bytes exceeds maximum allowed size {} bytes",
                        length, max_body_size
                    )));
                }
            }
        }
    }

    let limited_body = http_body_util::Limited::new(body, max_body_size);
    let body_bytes = limited_body
        .collect()
        .await
        .map(|collected| collected.to_bytes())
        .map_err(|e| {
            if e.to_string().contains("length limit exceeded") {
                CoreError::payload_too_large(format!(
                    "Request body exceeds maximum allowed size of {} bytes",
                    max_body_size
                ))
            } else {
                CoreError::bad_request(format!("Failed to read request body: {}", e))
            }
        })?;

    let query = parse_query_params(parts.uri.query());
    let headers = Headers::from(parts.headers);

    Ok(Request::new(
        parts.method,
        parts.uri,
        parts.version,
        query,
        headers,
        body_bytes,
    ))
}

/// Convert Hyper request to domain request for protocol upgrades.
///
/// Prefer [`RequestConverter::convert_for_upgrade`] at call sites that already
/// hold the limits.
pub async fn from_hyper_request_for_upgrade(
    req: hyper::Request<Incoming>,
    max_headers: usize,
) -> Result<Request, CoreError> {
    let header_count = req.headers().len();
    if header_count > max_headers {
        return Err(CoreError::request_header_fields_too_large(format!(
            "Request has {} headers, exceeds maximum allowed {} headers",
            header_count, max_headers
        )));
    }

    let method = req.method().clone();
    let uri = req.uri().clone();
    let version = req.version();
    let headers_map = req.headers().clone();

    let query = parse_query_params(uri.query());
    let headers = Headers::from(headers_map);

    let domain_req = Request::new(method, uri, version, query, headers, Bytes::new());
    *domain_req.raw_request().lock().await = Some(req);

    Ok(domain_req)
}

/// Convert domain response to Hyper response.
///
/// Prefer the `From<Response>` impl (`domain_res.into()`) at call sites.
/// This free function is kept for back-compat with existing call sites.
pub fn to_hyper_response(res: Response) -> hyper::Response<crate::http::ResponseHyperBody> {
    res.into()
}

// ── From<Response> ───────────────────────────────────────────────────────────

impl From<Response> for hyper::Response<crate::http::ResponseHyperBody> {
    /// Convert a domain [`Response`] into a Hyper response.
    ///
    /// This is the idiomatic conversion path per AGENTS.md. The free function
    /// [`to_hyper_response`] delegates here for back-compat.
    fn from(res: Response) -> Self {
        use crate::domain::ResponseBody;
        use crate::http::body::{box_full, box_stream};
        use http_body_util::BodyExt;

        let status = res.status();
        let headers = res.headers().clone();
        let body = res.into_body();

        // Upgrade responses carry hyper extensions that must not be lost.
        if let ResponseBody::Upgrade(hyper_response) = body {
            let (parts, full_body) = hyper_response.into_parts();
            let boxed_body = BodyExt::map_err(
                full_body,
                |never: std::convert::Infallible| -> std::io::Error { match never {} },
            )
            .boxed();
            return hyper::Response::from_parts(parts, boxed_body);
        }

        let mut builder = hyper::Response::builder().status(status);
        for (name, value) in headers.iter() {
            builder = builder.header(name, value);
        }

        let hyper_body = match body {
            ResponseBody::Empty => box_full(Bytes::new()),
            ResponseBody::Bytes(bytes) => box_full(bytes),
            ResponseBody::Stream(stream) => box_stream(stream),
            ResponseBody::Upgrade(_) => unreachable!("handled above"),
        };

        match builder.body(hyper_body) {
            Ok(response) => response,
            Err(e) => {
                eprintln!("⚠️  Failed to build response: {}", e);
                let mut response = hyper::Response::new(box_full(Bytes::from(
                    r#"{"error":"Internal Server Error","message":"Failed to build response"}"#,
                )));
                *response.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR;
                response.headers_mut().insert(
                    hyper::header::CONTENT_TYPE,
                    hyper::header::HeaderValue::from_static("application/json"),
                );
                response
            }
        }
    }
}

// ── Query parsing ─────────────────────────────────────────────────────────────

/// Parse query parameters from URI query string into a [`Query`] newtype.
fn parse_query_params(query: Option<&str>) -> Query {
    query
        .map(|q| {
            q.split('&')
                .filter_map(|pair| {
                    let mut parts = pair.splitn(2, '=');
                    let key = parts.next()?;
                    let value = parts.next().unwrap_or("");

                    let key = urlencoding::decode(key).ok()?.into_owned();
                    let value = urlencoding::decode(value).ok()?.into_owned();

                    if !is_valid_query_param(&key, &value) {
                        return None;
                    }

                    Some((key, value))
                })
                .collect()
        })
        .unwrap_or_default()
}

fn is_valid_query_param(key: &str, value: &str) -> bool {
    const MAX_KEY_LENGTH: usize = 256;
    const MAX_VALUE_LENGTH: usize = 4096;

    if key.len() > MAX_KEY_LENGTH || value.len() > MAX_VALUE_LENGTH {
        return false;
    }
    if key.contains('\0') || value.contains('\0') {
        return false;
    }
    if contains_path_traversal(value) {
        return false;
    }
    true
}

fn contains_path_traversal(s: &str) -> bool {
    if s.contains("../") || s.contains("..\\") {
        return true;
    }
    let lower = s.to_lowercase();
    if lower.contains("%2e%2e%2f") || lower.contains("%2e%2e%5c") {
        return true;
    }
    if lower.contains("..%2f") || lower.contains("..%5c") {
        return true;
    }
    false
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::StatusCode;
    use crate::domain::Response;

    #[test]
    fn test_parse_query_params_empty() {
        let query = parse_query_params(None);
        assert!(query.is_empty());
    }

    #[test]
    fn test_parse_query_params_single() {
        let query = parse_query_params(Some("page=1"));
        assert_eq!(query.get("page"), Some("1"));
    }

    #[test]
    fn test_parse_query_params_multiple() {
        let query = parse_query_params(Some("page=1&limit=10&sort=name"));
        assert_eq!(query.get("page"), Some("1"));
        assert_eq!(query.get("limit"), Some("10"));
        assert_eq!(query.get("sort"), Some("name"));
    }

    #[test]
    fn test_parse_query_params_url_encoded() {
        let query = parse_query_params(Some("name=John%20Doe&email=john%40example.com"));
        assert_eq!(query.get("name"), Some("John Doe"));
        assert_eq!(query.get("email"), Some("john@example.com"));
    }

    #[test]
    fn test_parse_query_params_no_value() {
        let query = parse_query_params(Some("flag&other=value"));
        assert_eq!(query.get("flag"), Some(""));
        assert_eq!(query.get("other"), Some("value"));
    }

    #[test]
    fn test_parse_query_params_duplicate_keys() {
        // Last value wins
        let query = parse_query_params(Some("page=1&page=2"));
        assert_eq!(query.get("page"), Some("2"));
    }

    #[test]
    fn test_parse_query_params_key_length_limit() {
        let long_key = "a".repeat(257);
        let query_str = format!("{}=value", long_key);
        let query = parse_query_params(Some(&query_str));
        assert!(query.is_empty());
    }

    #[test]
    fn test_parse_query_params_key_at_limit() {
        let key = "a".repeat(256);
        let query_str = format!("{}=value", key);
        let query = parse_query_params(Some(&query_str));
        assert_eq!(query.len(), 1);
        assert_eq!(query.get(&key), Some("value"));
    }

    #[test]
    fn test_parse_query_params_value_length_limit() {
        let long_value = "x".repeat(4097);
        let query_str = format!("key={}", long_value);
        let query = parse_query_params(Some(&query_str));
        assert!(query.is_empty());
    }

    #[test]
    fn test_parse_query_params_value_at_limit() {
        let value = "x".repeat(4096);
        let query_str = format!("key={}", value);
        let query = parse_query_params(Some(&query_str));
        assert_eq!(query.len(), 1);
        assert_eq!(query.get("key"), Some(value.as_str()));
    }

    #[test]
    fn test_parse_query_params_null_byte_in_key() {
        let query = parse_query_params(Some("key\0=value"));
        assert!(query.is_empty());
    }

    #[test]
    fn test_parse_query_params_null_byte_in_value() {
        let query = parse_query_params(Some("key=value\0"));
        assert!(query.is_empty());
    }

    #[test]
    fn test_parse_query_params_path_traversal_unix() {
        let query = parse_query_params(Some("path=../../etc/passwd"));
        assert!(query.is_empty());
    }

    #[test]
    fn test_parse_query_params_path_traversal_windows() {
        let query = parse_query_params(Some("path=..\\..\\windows\\system32"));
        assert!(query.is_empty());
    }

    #[test]
    fn test_parse_query_params_path_traversal_encoded() {
        let query = parse_query_params(Some("path=%2e%2e%2fetc%2fpasswd"));
        assert!(query.is_empty());
    }

    #[test]
    fn test_parse_query_params_path_traversal_mixed_encoding() {
        let query = parse_query_params(Some("path=..%2fetc%2fpasswd"));
        assert!(query.is_empty());
    }

    #[test]
    fn test_parse_query_params_safe_dots() {
        let query = parse_query_params(Some("file=document.pdf"));
        assert_eq!(query.get("file"), Some("document.pdf"));
    }

    #[test]
    fn test_parse_query_params_safe_path() {
        let query = parse_query_params(Some("path=/api/users/123"));
        assert_eq!(query.get("path"), Some("/api/users/123"));
    }

    #[test]
    fn test_parse_query_params_multiple_with_invalid() {
        let query = parse_query_params(Some("valid=ok&bad=../../etc&good=yes"));
        assert_eq!(query.len(), 2);
        assert_eq!(query.get("valid"), Some("ok"));
        assert_eq!(query.get("good"), Some("yes"));
        assert!(!query.contains_key("bad"));
    }

    #[test]
    fn test_parse_query_params_empty_value_valid() {
        let query = parse_query_params(Some("flag="));
        assert_eq!(query.get("flag"), Some(""));
    }

    #[test]
    fn test_contains_path_traversal_unix() {
        assert!(contains_path_traversal("../etc/passwd"));
        assert!(contains_path_traversal("../../etc/passwd"));
        assert!(contains_path_traversal("/var/log/../etc/passwd"));
    }

    #[test]
    fn test_contains_path_traversal_windows() {
        assert!(contains_path_traversal("..\\windows\\system32"));
        assert!(contains_path_traversal("..\\..\\windows"));
        assert!(contains_path_traversal("C:\\Users\\..\\Admin"));
    }

    #[test]
    fn test_contains_path_traversal_encoded() {
        assert!(contains_path_traversal("%2e%2e%2fetc"));
        assert!(contains_path_traversal("%2E%2E%2Fetc"));
        assert!(contains_path_traversal("%2e%2e%5cwindows"));
    }

    #[test]
    fn test_contains_path_traversal_mixed() {
        assert!(contains_path_traversal("..%2fetc"));
        assert!(contains_path_traversal("..%5cwindows"));
    }

    #[test]
    fn test_contains_path_traversal_safe() {
        assert!(!contains_path_traversal("document.pdf"));
        assert!(!contains_path_traversal("/api/users/123"));
        assert!(!contains_path_traversal("file.tar.gz"));
        assert!(!contains_path_traversal("version-1.2.3"));
    }

    #[test]
    fn test_is_valid_query_param_all_checks() {
        assert!(is_valid_query_param("page", "1"));
        assert!(is_valid_query_param("name", "John Doe"));
        assert!(!is_valid_query_param(&"a".repeat(257), "value"));
        assert!(!is_valid_query_param("key", &"x".repeat(4097)));
        assert!(!is_valid_query_param("key\0", "value"));
        assert!(!is_valid_query_param("key", "value\0"));
        assert!(!is_valid_query_param("path", "../../etc/passwd"));
    }

    #[tokio::test]
    async fn test_from_hyper_request_basic() {
        // Placeholder — full integration tests require a real Incoming body.
        let query = parse_query_params(Some("page=1"));
        assert_eq!(query.get("page"), Some("1"));
    }

    #[test]
    fn test_to_hyper_response_text() {
        let domain_res = Response::text("Hello, World!");
        let hyper_res = to_hyper_response(domain_res);
        assert_eq!(hyper_res.status(), crate::core::StatusCode::OK);
        let _body = hyper_res.into_body();
    }

    #[test]
    fn test_to_hyper_response_json() {
        let domain_res = Response::json(&serde_json::json!({"message": "success"})).unwrap();
        let hyper_res = to_hyper_response(domain_res);
        assert_eq!(hyper_res.status(), StatusCode::OK);
        assert_eq!(
            hyper_res.headers().get("content-type").unwrap(),
            "application/json"
        );
    }

    #[test]
    fn test_to_hyper_response_with_status() {
        let domain_res = Response::new()
            .with_status_code(StatusCode::NOT_FOUND)
            .with_body("Not found");
        let hyper_res = to_hyper_response(domain_res);
        assert_eq!(hyper_res.status(), StatusCode::NOT_FOUND);
    }

    #[test]
    fn test_to_hyper_response_with_headers() {
        let domain_res = Response::text("Hello").with_header("x-custom-header", "custom-value");
        let hyper_res = to_hyper_response(domain_res);
        assert_eq!(
            hyper_res.headers().get("x-custom-header").unwrap(),
            "custom-value"
        );
    }

    #[test]
    fn test_to_hyper_response_empty_body() {
        let domain_res = Response::new().with_status_code(StatusCode::NO_CONTENT);
        let hyper_res = to_hyper_response(domain_res);
        assert_eq!(hyper_res.status(), StatusCode::NO_CONTENT);
    }

    #[test]
    fn test_parse_query_params_header_count_edge_cases() {
        let query = parse_query_params(Some("a=1&b=2&c=3"));
        assert_eq!(query.len(), 3);
    }

    #[test]
    fn test_response_from_trait() {
        // Verify the From<Response> impl works directly
        let domain_res = Response::text("hello");
        let hyper_res: hyper::Response<crate::http::ResponseHyperBody> = domain_res.into();
        assert_eq!(hyper_res.status(), StatusCode::OK);
    }

    #[test]
    fn test_request_converter_new() {
        let converter = RequestConverter::new(1024 * 1024, 100);
        assert_eq!(converter.max_body_size, 1024 * 1024);
        assert_eq!(converter.max_headers, 100);
    }
}
