//! Reverse proxy runtime — forwards HTTP requests to localhost:{port}.
//!
//! Used by the `proxy_pass!` macro. Not intended to be used directly.

use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};

#[doc(hidden)]
pub use reqwest::Client as __ProxyHttpClient;

// ── Pattern matching for tunnel proxy ──────────────────────────

/// Compiled URL patterns for proxy routing.
///
/// Converts patterns like `"/api/instances/{instance_id}/"` into regexes
/// with named capture groups for parameter extraction.
pub struct CompiledPatterns {
    regexes: Vec<regex::Regex>,
}

impl CompiledPatterns {
    /// Compile URL patterns into regex matchers.
    ///
    /// `{param_name}` in patterns is converted to named capture group `(?P<param_name>[^/]+)`.
    pub fn compile(patterns: &[&str]) -> Self {
        let regexes = patterns
            .iter()
            .map(|pattern| {
                let regex_str = pattern_to_regex(pattern);
                regex::Regex::new(&regex_str)
                    .unwrap_or_else(|e| panic!("invalid proxy pattern '{}': {}", pattern, e))
            })
            .collect();
        Self { regexes }
    }

    /// Extract the first named capture group from the first matching pattern.
    ///
    /// Returns `None` if no pattern matches.
    pub fn extract_first(&self, path: &str) -> Option<String> {
        for regex in &self.regexes {
            if let Some(caps) = regex.captures(path) {
                for name in regex.capture_names().flatten() {
                    if let Some(m) = caps.name(name) {
                        return Some(m.as_str().to_string());
                    }
                }
            }
        }
        None
    }
}

/// Convert a URL pattern to a regex string.
///
/// `{param}` → `(?P<param>[^/]+)`, literal parts are regex-escaped.
#[allow(clippy::string_slice)]
fn pattern_to_regex(pattern: &str) -> String {
    let mut result = String::from("^");
    let mut rest = pattern;
    while !rest.is_empty() {
        if let Some(start) = rest.find('{') {
            // Literal part before '{'
            result.push_str(&regex::escape(&rest[..start]));
            rest = &rest[start + 1..];
            // Find closing '}'
            if let Some(end) = rest.find('}') {
                let name = &rest[..end];
                result.push_str(&format!("(?P<{}>[^/]+)", name));
                rest = &rest[end + 1..];
            } else {
                // Unclosed '{', treat rest as literal
                result.push_str(&regex::escape(&rest));
                break;
            }
        } else {
            result.push_str(&regex::escape(rest));
            break;
        }
    }
    result
}

// ── ProxyResolve trait ─────────────────────────────────────────

/// Trait for resolving proxy routing decisions.
///
/// Implement this on your state type to enable tunnel proxy middleware
/// generated by `proxy_pass!`.
pub trait ProxyResolve: Send + Sync + 'static {
    /// Check if the extracted parameter identifies a remote target that should be proxied.
    ///
    /// Called before consuming the request body. Return `false` to pass the request
    /// through to the next middleware.
    fn should_proxy(
        &self,
        param: &str,
    ) -> impl std::future::Future<Output = bool> + Send;

    /// Forward a packed HTTP request to the remote target via tunnel.
    ///
    /// Called after `should_proxy` returns `true`. Return `None` on forwarding failure.
    fn proxy_forward(
        &self,
        param: &str,
        request: crate::tunnel_proxy::HttpTunnelRequest,
    ) -> impl std::future::Future<Output = Option<crate::tunnel_proxy::HttpTunnelResponse>> + Send;
}

const PROXY_PORT_MIN: u16 = 1024;

/// Parse rest_path "{port}/{path...}" into (port, target_path).
/// Returns None if port is invalid or < PROXY_PORT_MIN.
#[allow(clippy::string_slice)]
fn parse_proxy_target(rest: &str) -> Option<(u16, &str)> {
    let (port_str, target_path) = match rest.find('/') {
        Some(pos) => (&rest[..pos], &rest[pos..]),
        None => (rest, "/"),
    };
    let port: u16 = port_str.parse().ok()?;
    if port >= PROXY_PORT_MIN {
        Some((port, target_path))
    } else {
        None
    }
}

/// Convert axum Method to reqwest Method.
fn to_reqwest_method(method: &axum::http::Method) -> reqwest::Method {
    match method.as_str() {
        "GET" => reqwest::Method::GET,
        "POST" => reqwest::Method::POST,
        "PUT" => reqwest::Method::PUT,
        "DELETE" => reqwest::Method::DELETE,
        "PATCH" => reqwest::Method::PATCH,
        "HEAD" => reqwest::Method::HEAD,
        "OPTIONS" => reqwest::Method::OPTIONS,
        _ => reqwest::Method::GET,
    }
}

/// Forward an HTTP request through the reverse proxy.
///
/// - `rest_path`: path after the proxy prefix, format `"{port}/{path...}"`
/// - `query`: optional query string
/// - `headers`: original request headers (hop-by-hop filtered automatically)
/// - `body`: request body bytes
/// - `cookie_name`: if Some, extracts this cookie from request headers and injects it
///
/// Handles: path parsing, port validation, URL construction, header forwarding,
/// host rewrite, cookie injection, response header rewriting (location, set-cookie,
/// hop-by-hop filtering).
#[doc(hidden)]
pub async fn __forward_impl(
    client: &reqwest::Client,
    method: &axum::http::Method,
    rest_path: &str,
    query: Option<&str>,
    headers: &HeaderMap,
    body: &[u8],
    cookie_name: Option<&str>,
) -> Response {
    // 1. Parse port and target path
    let (port, target_path) = match parse_proxy_target(rest_path) {
        Some(result) => result,
        None => return crate::bad_request("Invalid port (must be >= 1024)"),
    };

    // 2. Build target URL
    let target_base = format!("http://localhost:{}", port);
    let target_url = crate::build_proxy_url(&target_base, target_path, query);

    // 3. Convert method
    let req_method = to_reqwest_method(method);

    // 4. Build request
    let mut req = client.request(req_method, &target_url);

    // 5. Forward headers (filter hop-by-hop) + rewrite host
    let forwarded = crate::forward_request_headers(headers);
    for (name, value) in forwarded.iter() {
        req = req.header(name, value);
    }
    req = req.header("host", format!("localhost:{}", port));

    // 6. Cookie injection: extract from request headers and inject
    if let Some(cn) = cookie_name {
        if let Some(cv) = crate::extract_cookie_value(headers, cn) {
            req = req.header("cookie", format!("{}={}", cn, cv));
        }
    }

    // 7. Body
    if !body.is_empty() {
        req = req.body(body.to_vec());
    }

    // 8. Send
    let proxy_prefix = format!("/proxy/{}", port);
    match req.send().await {
        Ok(resp) => {
            let status = StatusCode::from_u16(resp.status().as_u16())
                .unwrap_or(StatusCode::BAD_GATEWAY);
            let mut out_headers = HeaderMap::new();

            for (name, value) in resp.headers().iter() {
                if let Ok(val_str) = value.to_str() {
                    match crate::process_proxy_response_header(
                        name.as_str(),
                        val_str,
                        &proxy_prefix,
                    ) {
                        None => {} // hop-by-hop, strip
                        Some(rewritten) => {
                            if let Ok(hv) = rewritten.parse() {
                                out_headers.append(name.clone(), hv);
                            }
                        }
                    }
                }
            }

            let resp_body = resp.bytes().await.unwrap_or_default();
            (status, out_headers, resp_body).into_response()
        }
        Err(e) => crate::json_error_response(
            StatusCode::BAD_GATEWAY,
            &format!("Proxy error: {}", e),
        ),
    }
}