mod auth;
mod forest_client;
mod routes;
mod state;
mod templates;

use std::net::SocketAddr;
use std::sync::Arc;

use axum::Router;
use forage_core::session::{FileSessionStore, SessionStore};
use forage_db::PgSessionStore;
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer;
use tracing_subscriber::EnvFilter;

use crate::forest_client::GrpcForestClient;
use crate::state::AppState;
use crate::templates::TemplateEngine;

pub fn build_router(state: AppState) -> Router {
    Router::new()
        .merge(routes::router())
        .nest_service("/static", ServeDir::new("static"))
        .layer(TraceLayer::new_for_http())
        .with_state(state)
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
        .init();

    let forest_endpoint =
        std::env::var("FOREST_SERVER_URL").unwrap_or_else(|_| "http://localhost:4040".into());
    tracing::info!("connecting to forest-server at {forest_endpoint}");

    let forest_client = GrpcForestClient::connect_lazy(&forest_endpoint)?;
    let template_engine = TemplateEngine::new()?;

    // Session store: PostgreSQL if DATABASE_URL is set, otherwise in-memory
    let sessions: Arc<dyn SessionStore> = if let Ok(database_url) = std::env::var("DATABASE_URL") {
        tracing::info!("using PostgreSQL session store");
        let pool = sqlx::PgPool::connect(&database_url).await?;
        forage_db::migrate(&pool).await?;

        let pg_store = Arc::new(PgSessionStore::new(pool));

        // Session reaper for PostgreSQL
        let reaper = pg_store.clone();
        tokio::spawn(async move {
            let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
            loop {
                interval.tick().await;
                match reaper.reap_expired(30).await {
                    Ok(n) if n > 0 => tracing::info!("session reaper: removed {n} expired sessions"),
                    Err(e) => tracing::warn!("session reaper error: {e}"),
                    _ => {}
                }
            }
        });

        pg_store
    } else {
        let session_dir = std::env::var("SESSION_DIR").unwrap_or_else(|_| "target/sessions".into());
        tracing::info!("using file session store at {session_dir} (set DATABASE_URL for PostgreSQL)");
        let file_store = Arc::new(FileSessionStore::new(&session_dir).expect("failed to create session dir"));

        let reaper = file_store.clone();
        tokio::spawn(async move {
            let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
            loop {
                interval.tick().await;
                reaper.reap_expired();
                tracing::debug!("session reaper: {} active sessions", reaper.session_count());
            }
        });

        file_store
    };

    let forest_client = Arc::new(forest_client);
    let state = AppState::new(template_engine, forest_client.clone(), forest_client, sessions);
    let app = build_router(state);

    let port: u16 = std::env::var("PORT")
        .ok()
        .and_then(|p| p.parse().ok())
        .unwrap_or(3000);
    let addr = SocketAddr::from(([0, 0, 0, 0], port));
    tracing::info!("listening on {}", addr);

    let listener = tokio::net::TcpListener::bind(addr).await?;
    axum::serve(listener, app).await?;

    Ok(())
}

#[cfg(test)]
mod test_support;
#[cfg(test)]
mod tests;
