//! Request building and manipulation logic.

use std::net::IpAddr;

use reqwest::{
    Method, StatusCode, Url,
    header::{CONTENT_LENGTH, CONTENT_TYPE, TRANSFER_ENCODING},
};
use tokio::net::lookup_host;

use crate::{Error, Result};

const DEFAULT_HTTP_PORT: u16 = 80;
const DEFAULT_HTTPS_PORT: u16 = 443;

/// Builds a redirect request.
pub fn build_redirect_request(
    request: &reqwest::Request,
    status: StatusCode,
    next_url: Url,
) -> Result<reqwest::Request> {
    let mut next_request = request.try_clone().ok_or(Error::UnclonableRequest)?;
    *next_request.url_mut() = next_url;

    if should_redirect_to_get(status, next_request.method()) {
        *next_request.method_mut() = Method::GET;
        *next_request.body_mut() = None;
        next_request.headers_mut().remove(CONTENT_LENGTH);
        next_request.headers_mut().remove(CONTENT_TYPE);
        next_request.headers_mut().remove(TRANSFER_ENCODING);
    }

    Ok(next_request)
}

fn should_redirect_to_get(status: StatusCode, method: &Method) -> bool {
    matches!(status, StatusCode::SEE_OTHER)
        || (matches!(status, StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND)
            && *method == Method::POST)
}

/// Ensures the redirect target is safe (not local).
pub async fn ensure_safe_redirect_target(url: &Url) -> Result<()> {
    if !matches!(url.scheme(), "http" | "https") {
        return Err(Error::RedirectBlocked {
            url: url.to_string(),
        });
    }

    let Some(host) = url.host_str() else {
        return Err(Error::RedirectBlocked {
            url: url.to_string(),
        });
    };

    if let Ok(ip) = host.parse::<IpAddr>() {
        if crate::http_transport::execute::is_internal_ip(ip) {
            return Err(Error::RedirectBlocked {
                url: url.to_string(),
            });
        }
        return Ok(());
    }

    let port = url.port_or_known_default().unwrap_or(match url.scheme() {
        "https" => DEFAULT_HTTPS_PORT,
        _ => DEFAULT_HTTP_PORT,
    });
    let addresses =
        lookup_host((host, port))
            .await
            .map_err(|source| Error::RedirectResolution {
                host: host.to_string(),
                source,
            })?;

    for address in addresses {
        if crate::http_transport::execute::is_internal_ip(address.ip()) {
            return Err(Error::RedirectBlocked {
                url: url.to_string(),
            });
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::ensure_safe_redirect_target;
    use reqwest::Url;

    #[tokio::test]
    async fn verify_scanclient_incomplete_ip_blocklist() {
        let cases = vec![
            ("http://0.0.0.0/", "0.0.0.0/8"),
            ("http://100.64.0.1/", "100.64.0.0/10 CGN"),
            ("http://198.18.0.1/", "198.18.0.0/15 benchmark"),
            ("http://[64:ff9b::1]/", "64:ff9b::/96 NAT64"),
            ("http://[2001:db8::1]/", "2001:db8::/32 documentation"),
            ("http://[ff02::1]/", "ff00::/8 multicast"),
        ];

        for (url, desc) in cases {
            let parsed = Url::parse(url).unwrap();
            let result = ensure_safe_redirect_target(&parsed).await;
            assert!(
                result.is_err(),
                "Redirect to {} ({}) should be blocked but was allowed",
                url,
                desc
            );
        }
    }
}
