use std::time::Instant;

use actix_web::dev::Service;
use actix_web::{App, HttpResponse, HttpServer, dev::Server, web};
use cache::AppCache;
use db::database::AppDatabase;

const REQUEST_LOG_EXCLUDED_PATHS: &[&str] = &[
    "/health",
    "/live",
    "/ready",
    "/metrics",
    "/favicon.ico",
    "/robots.txt",
];

fn should_log_request(path: &str) -> bool {
    !REQUEST_LOG_EXCLUDED_PATHS.contains(&path)
}

async fn health(
    db: web::Data<AppDatabase>,
    cache: web::Data<AppCache>,
) -> HttpResponse {
    let db_ok = sqlx::query("SELECT 1").execute(db.reader()).await.is_ok();
    let cache_ok = cache.ping_cluster().await.is_ok();

    if db_ok && cache_ok {
        HttpResponse::Ok().json(serde_json::json!({
            "status": "ok",
            "db": "ok",
            "cache": "ok",
        }))
    } else {
        HttpResponse::ServiceUnavailable().json(serde_json::json!({
            "status": "unhealthy",
            "db": if db_ok { "ok" } else { "error" },
            "cache": if cache_ok { "ok" } else { "error" },
        }))
    }
}

pub fn start_health(
    port: u16,
    db: AppDatabase,
    cache: AppCache,
) -> anyhow::Result<Server> {
    tracing::info!("health endpoint starting on 0.0.0.0:{}", port);

    let srv = HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(db.clone()))
            .app_data(web::Data::new(cache.clone()))
            .wrap_fn(|req, srv| {
                let should_log = should_log_request(req.path());
                let method = req.method().clone();
                let path = req.path().to_owned();
                let peer_addr =
                    req.connection_info().peer_addr().map(str::to_owned);
                let started_at = Instant::now();
                let fut = srv.call(req);

                async move {
                    match fut.await {
                        Ok(res) => {
                            if should_log {
                                tracing::info!(
                                    method = %method,
                                    path = %path,
                                    status = res.status().as_u16(),
                                    elapsed_ms = started_at.elapsed().as_millis(),
                                    peer_addr = peer_addr.as_deref().unwrap_or("-"),
                                    "http request"
                                );
                            }
                            Ok(res)
                        }
                        Err(err) => {
                            if should_log {
                                tracing::warn!(
                                    method = %method,
                                    path = %path,
                                    elapsed_ms = started_at.elapsed().as_millis(),
                                    peer_addr = peer_addr.as_deref().unwrap_or("-"),
                                    error = %err,
                                    "http request failed"
                                );
                            }
                            Err(err)
                        }
                    }
                }
            })
            .route("/health", web::get().to(health))
    })
    .bind(format!("0.0.0.0:{}", port))?;

    Ok(srv.run())
}
