mod context;
mod shutdown;

use std::time::Instant;

use actix_web::{App, dev::Service};
use context::AppContext;
use service::ai::sync::spawn_model_sync_loop;

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

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let ctx = AppContext::init().await?;

    let api_port = ctx.config.api_port()?;
    tracing::info!("GitDataAI API service starting on 0.0.0.0:{}", api_port);

    let service = ctx.service.clone();
    let session_store = ctx.session_store.clone();
    let session_key = ctx.session_key.clone();
    let channel_bus = ctx.channel_bus.clone();

    let srv = actix_web::HttpServer::new(move || {
        let session_middleware = session::SessionMiddleware::builder(
            session_store.clone(),
            session_key.clone(),
        )
        .cookie_secure(false)
        .cookie_name("id".to_string())
        .session_lifecycle(
            session::config::PersistentSession::default()
                .session_ttl(actix_web::cookie::time::Duration::days(30)),
        )
        .build();

        App::new()
            .app_data(actix_web::web::Data::new(service.clone()))
            .app_data(actix_web::web::Data::new(channel_bus.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)
                        }
                    }
                }
            })
            .wrap(session_middleware)
            .configure(|cfg| api::configure(cfg, channel_bus.clone()))
    })
    .bind(format!("0.0.0.0:{}", api_port))?;

    spawn_model_sync_loop(ctx.service.clone());

    let server = srv.run();
    tracing::info!("API server is running");

    tokio::select! {
        _ = server => {
            tracing::info!("API server stopped");
        }
        _ = shutdown::wait_for_shutdown_signal() => {
            tracing::info!("shutdown signal received, stopping gitdata API service");
        }
    }

    Ok(())
}
