//! TCP-over-rendezvous HTTP fallback. The client wraps any `/api/*` request
//! in an `HttpProxyRequest` protobuf and ships it over the rendezvous TCP
//! connection (already encrypted via secure_tcp) when
//! `OPTION_USE_RAW_TCP_FOR_API=Y`. We dispatch the wrapped request through
//! the **same** axum `Router` the HTTPS listener uses, so every existing
//! handler — auth, AB, audit, OIDC, … — is reachable through this path
//! with zero per-route plumbing.

use axum::body::Body;
use axum::Router;
use hbb_common::log;
use hbb_common::rendezvous_proto::{HeaderEntry, HttpProxyRequest, HttpProxyResponse};
use http::header::{HeaderMap, HeaderName, HeaderValue};
use http::{Method, Request};
use once_cell::sync::Lazy;
use std::convert::TryFrom;
use std::sync::Mutex;
use tower::ServiceExt;

/// Shared router. Populated by [`api::serve`] before the HTTPS listener
/// starts, so that the rendezvous TCP path can reach the same handlers.
/// `Mutex` because `Router` isn't `Sync` even though it is `Send + Clone`;
/// we never hold the lock across an await — we clone out, drop the guard,
/// and call `oneshot` on the clone.
static ROUTER: Lazy<Mutex<Option<Router>>> = Lazy::new(|| Mutex::new(None));

pub fn install_router(r: Router) {
    *ROUTER.lock().unwrap() = Some(r);
}

pub async fn dispatch(req: HttpProxyRequest) -> HttpProxyResponse {
    let router = match ROUTER.lock().unwrap().as_ref() {
        Some(r) => r.clone(),
        None => return error_response(503, "router not initialized"),
    };

    let http_req = match build_request(&req) {
        Ok(r) => r,
        Err(msg) => return error_response(400, &msg),
    };

    let response = match router.oneshot(http_req).await {
        Ok(r) => r,
        Err(e) => {
            log::warn!("http_proxy: router error: {}", e);
            return error_response(500, &format!("router: {}", e));
        }
    };

    let status = response.status().as_u16() as i32;
    let headers = serialize_headers(response.headers());
    let body = match collect_body(response.into_body()).await {
        Ok(b) => b,
        Err(msg) => return error_response(500, &msg),
    };

    HttpProxyResponse {
        status,
        headers,
        body: body.into(),
        error: String::new(),
        ..Default::default()
    }
}

fn build_request(req: &HttpProxyRequest) -> Result<Request<Body>, String> {
    let method = if req.method.is_empty() {
        Method::GET
    } else {
        Method::try_from(req.method.as_bytes())
            .map_err(|e| format!("invalid method {:?}: {}", req.method, e))?
    };
    let uri = if req.path.is_empty() {
        "/".to_string()
    } else if req.path.starts_with('/') {
        req.path.clone()
    } else {
        format!("/{}", req.path)
    };
    let body_bytes: Vec<u8> = req.body.to_vec();
    let mut builder = Request::builder().method(method).uri(uri);
    let headers_map = builder
        .headers_mut()
        .ok_or_else(|| "request builder produced no headers map".to_string())?;
    let mut saw_content_type = false;
    for h in &req.headers {
        if h.name.is_empty() {
            continue;
        }
        let lower = h.name.to_ascii_lowercase();
        // Drop hop-by-hop / framing headers we'll set ourselves to match
        // the actual body length axum sees.
        if matches!(
            lower.as_str(),
            "host" | "content-length" | "connection" | "transfer-encoding"
        ) {
            continue;
        }
        if lower == "content-type" {
            saw_content_type = true;
        }
        let name = HeaderName::try_from(h.name.as_bytes())
            .map_err(|e| format!("bad header name {:?}: {}", h.name, e))?;
        let value = HeaderValue::try_from(h.value.as_bytes())
            .map_err(|e| format!("bad header value for {:?}: {}", h.name, e))?;
        headers_map.append(name, value);
    }
    // Default to JSON if the client forgot — every /api/* handler expects
    // JSON unless the route reads `body` as raw bytes (only /api/record),
    // which doesn't care about content-type.
    if !saw_content_type {
        headers_map.insert(
            HeaderName::from_static("content-type"),
            HeaderValue::from_static("application/json"),
        );
    }
    builder
        .body(Body::from(body_bytes))
        .map_err(|e| format!("build request: {}", e))
}

fn serialize_headers(map: &HeaderMap) -> Vec<HeaderEntry> {
    map.iter()
        .map(|(k, v)| HeaderEntry {
            name: k.as_str().to_string(),
            value: v.to_str().unwrap_or("").to_string(),
            ..Default::default()
        })
        .collect()
}

/// Collect any `http_body::Body` whose chunks are buffer-like into a `Vec<u8>`.
/// Works against both the request `Body` we build and axum's
/// `UnsyncBoxBody<Bytes, axum::Error>` response body.
async fn collect_body<B>(mut body: B) -> Result<Vec<u8>, String>
where
    B: http_body::Body + Unpin,
    B::Data: hbb_common::bytes::Buf,
    B::Error: std::fmt::Display,
{
    use hbb_common::bytes::Buf;
    let mut buf = Vec::new();
    while let Some(chunk) = body.data().await {
        let mut chunk = chunk.map_err(|e| format!("body read: {}", e))?;
        while chunk.has_remaining() {
            let s = chunk.chunk();
            buf.extend_from_slice(s);
            let n = s.len();
            chunk.advance(n);
        }
    }
    Ok(buf)
}

fn error_response(status: i32, msg: &str) -> HttpProxyResponse {
    HttpProxyResponse {
        status,
        body: msg.as_bytes().to_vec().into(),
        error: msg.to_string(),
        ..Default::default()
    }
}
