use std::sync::Arc;
use std::time::Instant;

use bytes::Bytes;
use http::{HeaderMap, Method, StatusCode, header};
use http_body_util::Full;
use hyper::{Request, Response, body::Incoming};
use serde_json::json;

use super::GatewayInner;
use crate::health::HealthHandle;

type Body = Full<Bytes>;

const JSON: &str = "application/json; charset=utf-8";
const TEXT: &str = "text/plain; charset=utf-8";
const PROMETHEUS: &str = "text/plain; version=0.0.4; charset=utf-8";

// Control-plane routes never need request bodies
const MAX_CONTENT_LENGTH: u64 = 64 * 1024;

// Request bodies are never read; oversized declared bodies are rejected up front
pub async fn handle(req: Request<Incoming>, state: Arc<GatewayInner>) -> Response<Body> {
    let started = Instant::now();
    let (route, response) = if declares_oversized_body(req.headers()) {
        let response = text(StatusCode::PAYLOAD_TOO_LARGE, "request body too large\n");
        ("payload_too_large", response)
    } else {
        route(req.method(), req.uri().path(), &state)
    };
    if state.metrics_attached() {
        crate::metrics::record_request(route, response.status().as_u16(), started.elapsed());
    }
    response
}

fn declares_oversized_body(headers: &HeaderMap) -> bool {
    headers
        .get(header::CONTENT_LENGTH)
        .and_then(|value| value.to_str().ok())
        .and_then(|raw| raw.parse::<u64>().ok())
        .is_some_and(|length| length > MAX_CONTENT_LENGTH)
}

pub fn timeout() -> Response<Body> {
    text(
        StatusCode::GATEWAY_TIMEOUT,
        "telemetry gateway request timed out\n",
    )
}

fn route(method: &Method, path: &str, state: &GatewayInner) -> (&'static str, Response<Body>) {
    match (method, path) {
        (&Method::GET, "/health/live") => ("health_live", health_live(&state.health)),
        (&Method::GET, "/health/ready") => ("health_ready", health_ready(&state.health)),
        (&Method::GET, "/health/startup") => ("health_startup", health_startup(&state.health)),
        (&Method::GET, "/health") => (
            "health",
            json_response(StatusCode::OK, &state.health.report()),
        ),
        (&Method::GET, "/version") => ("version", version(state)),
        (&Method::GET, "/metrics") => ("metrics", metrics(state)),
        #[cfg(feature = "alloc-metrics")]
        (&Method::GET, "/debug/alloc/mimalloc/stats") => ("alloc_stats_text", alloc_stats_text()),
        #[cfg(feature = "alloc-metrics")]
        (&Method::GET, "/debug/alloc/mimalloc/stats.json") => {
            ("alloc_stats_json", alloc_stats_json())
        }
        #[cfg(feature = "alloc-metrics")]
        (&Method::POST, "/debug/alloc/mimalloc/collect") => ("alloc_collect", alloc_collect()),
        (
            _,
            "/health/live" | "/health/ready" | "/health/startup" | "/health" | "/version"
            | "/metrics",
        ) => (
            "method_not_allowed",
            text(StatusCode::METHOD_NOT_ALLOWED, ""),
        ),
        #[cfg(feature = "alloc-metrics")]
        (
            _,
            "/debug/alloc/mimalloc/stats"
            | "/debug/alloc/mimalloc/stats.json"
            | "/debug/alloc/mimalloc/collect",
        ) => (
            "method_not_allowed",
            text(StatusCode::METHOD_NOT_ALLOWED, ""),
        ),
        _ => ("not_found", text(StatusCode::NOT_FOUND, "")),
    }
}

fn health_live(health: &HealthHandle) -> Response<Body> {
    if health.is_live() {
        status_json(StatusCode::OK, "live")
    } else {
        status_json(StatusCode::SERVICE_UNAVAILABLE, "dead")
    }
}

fn health_ready(health: &HealthHandle) -> Response<Body> {
    let status = if health.is_ready() {
        StatusCode::OK
    } else {
        StatusCode::SERVICE_UNAVAILABLE
    };
    json_response(status, &health.report())
}

fn health_startup(health: &HealthHandle) -> Response<Body> {
    if health.is_started() {
        status_json(StatusCode::OK, "started")
    } else {
        status_json(StatusCode::SERVICE_UNAVAILABLE, "starting")
    }
}

fn version(state: &GatewayInner) -> Response<Body> {
    json_response(
        StatusCode::OK,
        &json!({
            "service": {
                "name": state.service.name.as_str(),
                "version": state.service.version.as_str(),
            },
            "build": state.input.build(),
        }),
    )
}

fn metrics(state: &GatewayInner) -> Response<Body> {
    if !state.metrics_route.is_enabled() {
        return text(StatusCode::NOT_FOUND, "");
    }
    match state.metrics_body() {
        Ok(Some(body)) => body_response(StatusCode::OK, PROMETHEUS, body),
        Ok(None) => text(StatusCode::SERVICE_UNAVAILABLE, "metrics not attached\n"),
        Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
    }
}

#[cfg(feature = "alloc-metrics")]
fn alloc_stats_text() -> Response<Body> {
    match efdn_alloc::stats_text() {
        Ok(body) => body_response(StatusCode::OK, TEXT, body),
        Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
    }
}

#[cfg(feature = "alloc-metrics")]
fn alloc_stats_json() -> Response<Body> {
    match efdn_alloc::stats_json() {
        Ok(body) => body_response(StatusCode::OK, JSON, body),
        Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
    }
}

#[cfg(feature = "alloc-metrics")]
fn alloc_collect() -> Response<Body> {
    efdn_alloc::collect();
    // 204 responses carry no content metadata
    Response::builder()
        .status(StatusCode::NO_CONTENT)
        .body(Full::new(Bytes::new()))
        .expect("gateway response builder uses a valid status")
}

fn status_json(status: StatusCode, value: &'static str) -> Response<Body> {
    json_response(status, &json!({ "status": value }))
}

fn json_response(status: StatusCode, value: &impl serde::Serialize) -> Response<Body> {
    match serde_json::to_vec(value) {
        Ok(body) => body_response(status, JSON, body),
        Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
    }
}

fn text(status: StatusCode, body: &str) -> Response<Body> {
    body_response(status, TEXT, body.as_bytes().to_vec())
}

fn body_response(
    status: StatusCode,
    content_type: &'static str,
    body: impl Into<Bytes>,
) -> Response<Body> {
    Response::builder()
        .status(status)
        .header(header::CONTENT_TYPE, content_type)
        .body(Full::new(body.into()))
        .expect("gateway response builder uses valid status and headers")
}
