mod auth_guard;
mod routes;
mod state;
mod storage;

use std::sync::Arc;

use axum::{Router, routing::{get, post}};
use tower_http::services::ServeDir;
use openid::DiscoveredClient;
use state::AppState;
use storage::Storage;
use tower_sessions::{MemoryStore, SessionManagerLayer, cookie::SameSite};
use irc_now_common::auth::OidcConfig;
use irc_now_common::db::DbConfig;

async fn health() -> &'static str {
    "ok"
}

async fn cleanup_expired_images(db: sqlx::PgPool, storage: storage::Storage) {
    let mut interval = tokio::time::interval(std::time::Duration::from_secs(3600));
    loop {
        interval.tick().await;
        let rows: Vec<(String, String)> = match sqlx::query_as(
            "SELECT id, filename FROM images WHERE expires_at IS NOT NULL AND expires_at < now()",
        )
        .fetch_all(&db)
        .await
        {
            Ok(r) => r,
            Err(e) => {
                tracing::error!("expired image query failed: {e}");
                continue;
            }
        };

        if rows.is_empty() {
            continue;
        }

        let count = rows.len();
        for (id, filename) in &rows {
            let original_key = format!("{id}/{filename}");
            let thumb_key = format!("{id}/thumb.webp");
            let _ = storage.delete(&original_key).await;
            let _ = storage.delete(&thumb_key).await;
        }

        match sqlx::query("DELETE FROM images WHERE expires_at IS NOT NULL AND expires_at < now()")
            .execute(&db)
            .await
        {
            Ok(_) => tracing::info!("cleaned up {count} expired images"),
            Err(e) => tracing::error!("expired image delete failed: {e}"),
        }
    }
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    let db = DbConfig::from_env("PICS").connect().await.unwrap();
    let oidc = OidcConfig::from_env();

    let issuer_url = url::Url::parse(&oidc.issuer_url).expect("invalid OIDC issuer URL");

    tracing::info!("discovering OIDC provider at {issuer_url}");
    let oidc_client = DiscoveredClient::discover(
        oidc.client_id.clone(),
        oidc.client_secret.clone(),
        Some(oidc.redirect_url.clone()),
        issuer_url,
    )
    .await
    .expect("OIDC discovery failed");

    tracing::info!("OIDC provider discovered");

    let s3_endpoint = std::env::var("S3_ENDPOINT").expect("S3_ENDPOINT required");
    let s3_bucket = std::env::var("S3_BUCKET").unwrap_or_else(|_| "pics".to_string());
    let s3_access_key = std::env::var("S3_ACCESS_KEY").expect("S3_ACCESS_KEY required");
    let s3_secret_key = std::env::var("S3_SECRET_KEY").expect("S3_SECRET_KEY required");

    let storage = Storage::new(&s3_endpoint, &s3_bucket, &s3_access_key, &s3_secret_key)
        .expect("S3 storage init failed");

    let state = AppState {
        db: db.clone(),
        oidc,
        oidc_client: Arc::new(oidc_client),
        storage: storage.clone(),
    };

    tokio::spawn(cleanup_expired_images(db, storage));

    let session_store = MemoryStore::default();
    let session_layer = SessionManagerLayer::new(session_store).with_same_site(SameSite::Lax);

    let app = Router::new()
        .route("/health", get(health))
        .route("/", get(routes::image::index))
        .route("/upload", post(routes::image::upload))
        .route("/my", get(routes::image::my_images))
        .route("/api/usage/{sub}", get(routes::usage::user_usage))
        .route("/{id}", get(routes::image::view))
        .route("/{id}/raw", get(routes::image::raw))
        .route("/{id}/thumb", get(routes::image::thumb))
        .route("/{id}/delete", post(routes::image::delete))
        .route("/auth/login", get(routes::auth::login))
        .route("/auth/callback", get(routes::auth::callback))
        .route("/auth/logout", get(routes::auth::logout))
        .nest_service("/static", ServeDir::new("static"))
        .layer(session_layer)
        .with_state(state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    tracing::info!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await.unwrap();
}
