use axum::{
    body::{to_bytes, Body, Bytes},
    extract::Request,
    middleware::Next,
    response::Response,
};
use std::time::Instant;

#[derive(Clone, Debug)]
pub struct RequestId(pub String);

pub fn truncate_string(value: &str, max: usize) -> String {
    if value.chars().count() > max {
        value.chars().take(max).collect::<String>() + "....."
    } else {
        value.to_string()
    }
}
fn truncate_json(value: &mut serde_json::Value, max: usize) {
    match value {
        serde_json::Value::String(v) => *v = truncate_string(v, max),
        serde_json::Value::Array(v) => v.iter_mut().for_each(|v| truncate_json(v, max)),
        serde_json::Value::Object(v) => v.values_mut().for_each(|v| truncate_json(v, max)),
        _ => {}
    }
}
fn pretty(bytes: &Bytes) -> String {
    let raw = String::from_utf8_lossy(bytes);
    match serde_json::from_str::<serde_json::Value>(&raw) {
        Ok(mut value) => {
            truncate_json(&mut value, 50);
            serde_json::to_string_pretty(&value).unwrap_or_else(|_| raw.into_owned())
        }
        Err(_) => truncate_string(&raw, 50),
    }
}

pub async fn request_logging_middleware(mut req: Request<Body>, next: Next) -> Response {
    let started = Instant::now();
    let id = uuid::Uuid::new_v4().to_string();
    let method = req.method().clone();
    let uri = req.uri().clone();
    req.extensions_mut().insert(RequestId(id.clone()));
    let (parts, body) = req.into_parts();
    let request_bytes = to_bytes(body, usize::MAX).await.unwrap_or_default();
    tracing::info!(request_id=%id, %method, %uri, body=%pretty(&request_bytes), "request started");
    let response = next
        .run(Request::from_parts(parts, Body::from(request_bytes)))
        .await;
    let status = response.status();
    let (parts, body) = response.into_parts();
    let response_bytes = to_bytes(body, usize::MAX).await.unwrap_or_default();
    tracing::info!(request_id=%id, status=%status, elapsed_ms=started.elapsed().as_millis(), body=%pretty(&response_bytes), "request completed");
    Response::from_parts(parts, Body::from(response_bytes))
}

pub fn log_info<T: std::fmt::Debug>(request_id: &RequestId, label: &str, data: T) {
    tracing::info!(request_id=%request_id.0, %label, data=?data);
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn truncates_at_character_boundary() {
        assert_eq!(truncate_string("中文测试", 2), "中文.....");
    }
}
