use std::{sync::Arc, time::Instant};

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

pub mod action;
pub mod auth;
pub mod handler;
pub mod lfs;
pub mod lfs_routes;
pub mod rate_limit;
pub mod routes;
pub mod utils;

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)
}

#[derive(Clone)]
pub struct HttpAppState {
    pub db: AppDatabase,
    pub cache: AppCache,
    pub sync: crate::sync::ReceiveSyncService,
    pub rate_limiter: Arc<rate_limit::RateLimiter>,
    pub config: AppConfig,
    pub git_state: crate::AppGitState,
}

async fn robots(state: web::Data<HttpAppState>) -> HttpResponse {
    let sitemap_url = state
        .config
        .git_http_domain()
        .map(|d| format!("{}/sitemap.xml", d.trim_end_matches('/')))
        .unwrap_or_default();

    let body = if sitemap_url.is_empty() {
        "User-agent: *\nDisallow: /\n".to_string()
    } else {
        format!("User-agent: *\nDisallow: /\n\nSitemap: {sitemap_url}\n")
    };

    HttpResponse::Ok()
        .content_type("text/plain; charset=utf-8")
        .body(body)
}

async fn health(state: web::Data<HttpAppState>) -> HttpResponse {
    let db_ok = sqlx::query("SELECT 1")
        .execute(state.db.reader())
        .await
        .is_ok();
    let cache_ok = state.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 git_http_cfg(cfg: &mut web::ServiceConfig) {
    cfg.route("/robots.txt", web::get().to(robots))
        .route("/health", web::get().to(health))
        .route(
            "/{namespace}/{repo_name}.git/action",
            web::get().to(action::action_poll),
        )
        .route(
            "/{namespace}/{repo_name}.git/info/refs",
            web::get().to(routes::info_refs),
        )
        .route(
            "/{namespace}/{repo_name}.git/git-upload-pack",
            web::post().to(routes::upload_pack),
        )
        .route(
            "/{namespace}/{repo_name}.git/git-receive-pack",
            web::post().to(routes::receive_pack),
        )
        .route(
            "/{namespace}/{repo_name}.git/info/lfs/objects/batch",
            web::post().to(lfs_routes::lfs_batch),
        )
        .route(
            "/{namespace}/{repo_name}.git/info/lfs/objects/{oid}",
            web::put().to(lfs_routes::lfs_upload),
        )
        .route(
            "/{namespace}/{repo_name}.git/info/lfs/objects/{oid}",
            web::get().to(lfs_routes::lfs_download),
        )
        .route(
            "/{namespace}/{repo_name}.git/info/lfs/locks",
            web::post().to(lfs_routes::lfs_lock_create),
        )
        .route(
            "/{namespace}/{repo_name}.git/info/lfs/locks",
            web::get().to(lfs_routes::lfs_lock_list),
        )
        .route(
            "/{namespace}/{repo_name}.git/info/lfs/locks/{id}",
            web::get().to(lfs_routes::lfs_lock_get),
        )
        .route(
            "/{namespace}/{repo_name}.git/info/lfs/locks/{id}",
            web::delete().to(lfs_routes::lfs_lock_delete),
        )
        .route(
            "/{namespace}/{repo_name}.git/graphql",
            web::post().to(crate::graphql::graphql_handle),
        );
}

pub async fn run_http(
    config: AppConfig,
    db: AppDatabase,
    cache: AppCache,
    redis_pool: deadpool_redis::cluster::Pool,
) -> anyhow::Result<()> {
    let sync = crate::sync::ReceiveSyncService::new(redis_pool);

    let rate_limiter = Arc::new(rate_limit::RateLimiter::new(
        rate_limit::RateLimitConfig::default(),
    ));
    let _cleanup = rate_limiter.clone().start_cleanup();

    let git_state = crate::AppGitState {
        cache: cache.clone(),
        db: db.clone(),
    };

    let state = HttpAppState {
        db: db.clone(),
        cache: cache.clone(),
        sync,
        rate_limiter,
        config: config.clone(),
        git_state,
    };

    let http_port = config.git_http_port()?;
    tracing::info!("Starting git HTTP server on 0.0.0.0:{}", http_port);

    let server = HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(state.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)
                        }
                    }
                }
            })
            .configure(git_http_cfg)
    })
    .bind(format!("0.0.0.0:{}", http_port))?
    .run();

    let result = server.await;
    if let Err(e) = result {
        tracing::error!("HTTP server error: {}", e);
    }

    tracing::info!("Git HTTP server stopped");
    Ok(())
}
