use axum::extract::Request;
use axum::http::Method;
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use metrics::{counter, histogram};
use std::time::Instant;

/// HTTP 请求指标中间件。
///
/// 记录两个 Prometheus 指标：
/// - `http_requests_total` — 计数器，标签: method, path, status
/// - `http_request_duration_seconds` — 直方图，标签: method, path, status
pub async fn metrics_middleware(req: Request, next: Next) -> Response {
    let method = method_label(req.method());
    let path = path_label(req.uri().path());

    let start = Instant::now();
    let resp = next.run(req).await;
    let elapsed = start.elapsed();

    let status = resp.status().as_u16().to_string();

    let labels = [
        ("method", method.clone()),
        ("path", path.clone()),
        ("status", status.clone()),
    ];

    counter!("http_requests_total", &labels).increment(1);
    histogram!("http_request_duration_seconds", &labels).record(elapsed.as_secs_f64());

    resp
}

fn method_label(method: &Method) -> String {
    method.as_str().to_owned()
}

/// 归一化路径：将 UUID 段替换为 `:id`，避免高基数。
fn path_label(path: &str) -> String {
    let parts: Vec<&str> = path
        .split('/')
        .filter(|s| !s.is_empty())
        .map(|s| if looks_like_uuid(s) { ":id" } else { s })
        .collect();
    if parts.is_empty() {
        "/".to_string()
    } else {
        format!("/{}", parts.join("/"))
    }
}

fn looks_like_uuid(s: &str) -> bool {
    s.len() == 36
        && s.chars().filter(|c| *c == '-').count() == 4
        && s.chars().all(|c| c.is_ascii_hexdigit() || c == '-')
}

/// 在独立端口启动 Prometheus exporter。
pub fn start_metrics_server(port: u16) {
    let builder = metrics_exporter_prometheus::PrometheusBuilder::new();
    let recorder = builder.build_recorder();
    let handle = recorder.handle();

    if let Err(e) = metrics::set_global_recorder(recorder) {
        tracing::error!(error = %e, "Failed to install Prometheus recorder");
        return;
    }

    tokio::spawn(async move {
        let app = axum::Router::new()
            .route(
                "/metrics",
                axum::routing::get(move || {
                    let handle = handle.clone();
                    async move {
                        let body = handle.render();
                        axum::response::IntoResponse::into_response((
                            [(
                                axum::http::header::CONTENT_TYPE,
                                "text/plain; version=0.0.4",
                            )],
                            body,
                        ))
                    }
                }),
            )
            .fallback(|| async { axum::http::StatusCode::NOT_FOUND.into_response() as Response });

        let addr = format!("0.0.0.0:{port}");
        match tokio::net::TcpListener::bind(&addr).await {
            Ok(listener) => {
                tracing::info!(addr = %addr, "Prometheus metrics server listening");
                if let Err(e) = axum::serve(listener, app).await {
                    tracing::error!(error = %e, "Metrics server error");
                }
            }
            Err(e) => {
                tracing::error!(error = %e, addr = %addr, "Failed to bind metrics server");
            }
        }
    });
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn path_label_normalizes_uuids() {
        assert_eq!(path_label("/api/v1/users"), "/api/v1/users");
        assert_eq!(
            path_label("/api/v1/users/01234567-89ab-cdef-0123-456789abcdef/posts"),
            "/api/v1/users/:id/posts"
        );
        assert_eq!(path_label("/"), "/");
        assert_eq!(path_label(""), "/");
    }

    #[test]
    fn is_uuid_checks_format() {
        assert!(looks_like_uuid("01234567-89ab-cdef-0123-456789abcdef"));
        assert!(!looks_like_uuid("not-a-uuid"));
        assert!(!looks_like_uuid("short"));
    }
}
