//! Tunnel HTTP proxy — serialization/deserialization for HTTP-over-tunnel forwarding.
//!
//! When a reverse proxy target has no public IP, HTTP requests are serialized,
//! sent through a bidirectional WebSocket tunnel, and deserialized on the other end.
//!
//! Host side: `pack_request()` → tunnel RPC → `unpack_response()`
//! Slave side: `unpack_request()` → local forward → `pack_response()`

use std::collections::HashMap;

use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
use axum::response::{IntoResponse, Response};
use base64::Engine as _;
use serde::{Deserialize, Serialize};

const BASE64: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD;

/// Default body size limit for `pack_response()` (10 MB).
const DEFAULT_BODY_LIMIT: usize = 10 * 1024 * 1024;

/// HTTP request serialized for tunnel transport.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpTunnelRequest {
    pub method: String,
    pub path: String,
    pub headers: HashMap<String, String>,
    /// Base64-encoded body (`None` if empty).
    pub body: Option<String>,
}

/// HTTP response serialized for tunnel transport.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpTunnelResponse {
    pub status: u16,
    pub headers: HashMap<String, String>,
    /// Base64-encoded body (`None` if empty).
    pub body: Option<String>,
}

/// Pack an axum HTTP request into tunnel transport format.
///
/// Combines path and query into a single string.
/// Base64-encodes the body if non-empty.
pub fn pack_request(
    method: &Method,
    path: &str,
    query: Option<&str>,
    headers: &HeaderMap,
    body: &[u8],
) -> HttpTunnelRequest {
    let full_path = match query {
        Some(q) if !q.is_empty() => format!("{}?{}", path, q),
        _ => path.to_string(),
    };

    let header_map: HashMap<String, String> = headers
        .iter()
        .filter_map(|(name, value)| {
            value.to_str().ok().map(|v| (name.to_string(), v.to_string()))
        })
        .collect();

    HttpTunnelRequest {
        method: method.to_string(),
        path: full_path,
        headers: header_map,
        body: if body.is_empty() {
            None
        } else {
            Some(BASE64.encode(body))
        },
    }
}

/// Unpack a tunnel response into an axum `Response`.
///
/// Parses status code, rebuilds headers, base64-decodes body.
pub fn unpack_response(resp: HttpTunnelResponse) -> Response {
    let status = StatusCode::from_u16(resp.status).unwrap_or(StatusCode::BAD_GATEWAY);

    let mut headers = HeaderMap::new();
    for (k, v) in &resp.headers {
        if let (Ok(name), Ok(val)) = (
            HeaderName::from_bytes(k.as_bytes()),
            HeaderValue::from_str(v),
        ) {
            headers.insert(name, val);
        }
    }

    let body = resp
        .body
        .and_then(|b| BASE64.decode(&b).ok())
        .unwrap_or_default();

    (status, headers, body).into_response()
}

/// Unpack a tunnel request into HTTP parts.
///
/// Returns `(method, path, headers, body_bytes)`.
/// Method defaults to GET if the string cannot be parsed.
pub fn unpack_request(req: HttpTunnelRequest) -> (Method, String, HeaderMap, Vec<u8>) {
    let method = req.method.parse::<Method>().unwrap_or(Method::GET);

    let mut headers = HeaderMap::new();
    for (name, value) in &req.headers {
        if let (Ok(hn), Ok(hv)) = (
            name.parse::<HeaderName>(),
            value.parse::<HeaderValue>(),
        ) {
            headers.insert(hn, hv);
        }
    }

    let body = req
        .body
        .and_then(|b| BASE64.decode(&b).ok())
        .unwrap_or_default();

    (method, req.path, headers, body)
}

/// Pack an axum `Response` into tunnel transport format (10 MB body limit).
pub async fn pack_response(resp: Response) -> HttpTunnelResponse {
    pack_response_with_limit(resp, DEFAULT_BODY_LIMIT).await
}

/// Pack an axum `Response` into tunnel transport format with explicit body size limit.
pub async fn pack_response_with_limit(resp: Response, body_limit: usize) -> HttpTunnelResponse {
    let status = resp.status().as_u16();

    let resp_headers: HashMap<String, String> = resp
        .headers()
        .iter()
        .filter_map(|(name, value)| {
            value.to_str().ok().map(|v| (name.to_string(), v.to_string()))
        })
        .collect();

    let body_bytes = axum::body::to_bytes(resp.into_body(), body_limit)
        .await
        .unwrap_or_default();

    let body = if body_bytes.is_empty() {
        None
    } else {
        Some(BASE64.encode(&body_bytes))
    };

    HttpTunnelResponse {
        status,
        headers: resp_headers,
        body,
    }
}
/// Prepare a tunnel request for local forwarding.
///
/// Unpacks the request, injects an authentication cookie, and constructs
/// the forward path as `{port}/{path}` (with leading `/` trimmed).
///
/// Returns `(method, forward_path, headers, body)` ready for proxy forwarding.
pub fn prepare_tunnel_forward(
    request: HttpTunnelRequest,
    port: u16,
    cookie_name: &str,
    cookie_value: &str,
) -> (Method, String, HeaderMap, Vec<u8>) {
    let (method, path, mut headers, body) = unpack_request(request);

    // Inject authentication cookie
    let cookie_str = format!("{}={}", cookie_name, cookie_value);
    if let Ok(hv) = cookie_str.parse::<HeaderValue>() {
        headers.insert(axum::http::header::COOKIE, hv);
    }

    // Build forward path: {port}/{path} with leading / trimmed
    let trimmed = path.trim_start_matches('/');
    let forward_path = format!("{}/{}", port, trimmed);

    (method, forward_path, headers, body)
}
