mod auth_guard;
mod bouncer_watcher;
mod business_metrics;
mod ergo_admin;
mod events;
mod flash;
mod htmx;
mod irccloud;
mod keycloak;
mod k8s;
mod page;
mod routes;
mod soju_migrate;
mod state;
mod stripe_util;

use std::sync::{Arc, OnceLock};

use axum::{Router, routing::{get, post}};

static PROM_HANDLE: OnceLock<metrics_exporter_prometheus::PrometheusHandle> = OnceLock::new();

async fn metrics_handler() -> String {
    PROM_HANDLE.get().map(|h| h.render()).unwrap_or_default()
}
use tower_http::services::ServeDir;
use openid::DiscoveredClient;
use state::AppState;
use tower_sessions::{ExpiredDeletion, SessionManagerLayer, Expiry, cookie::SameSite};
use tower_sessions_sqlx_store::PostgresStore;
use irc_now_common::auth::OidcConfig;
use irc_now_common::db::DbConfig;

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

async fn sync_shared_bouncer_limits(kube: kube::Client, namespace: String) {
    let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
    loop {
        interval.tick().await;
        match soju_migrate::connect_soju_db(&kube, &namespace, "soju-shared-db").await {
            Ok(client) => {
                match client
                    .execute(
                        r#"UPDATE "User" SET max_networks = 1 WHERE max_networks != 1 OR max_networks IS NULL"#,
                        &[],
                    )
                    .await
                {
                    Ok(n) => {
                        if n > 0 {
                            tracing::info!("synced max_networks=1 for {n} shared bouncer users");
                        }
                    }
                    Err(e) => tracing::warn!("shared bouncer max_networks sync failed: {e}"),
                }
            }
            Err(e) => tracing::debug!("shared bouncer db not available: {e}"),
        }
    }
}

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

    let db = DbConfig::from_env("ACCOUNTS").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 stripe_webhook_secret =
        std::env::var("STRIPE_WEBHOOK_SECRET").unwrap_or_default();
    let stripe_secret_key =
        std::env::var("STRIPE_SECRET_KEY").unwrap_or_default();
    let stripe_price_id =
        std::env::var("STRIPE_PRICE_ID").unwrap_or_default();
    let stripe_client = stripe::Client::new(&stripe_secret_key);

    let kube_client = kube::Client::try_default()
        .await
        .expect("kube client init failed");
    let namespace =
        std::env::var("NAMESPACE").unwrap_or_else(|_| "irc-josie-cloud".to_string());

    let http_client = reqwest::Client::new();

    let announcement = std::env::var("ANNOUNCEMENT").ok().filter(|s| !s.is_empty());

    let txt_url = std::env::var("TXT_INTERNAL_URL").unwrap_or_else(|_| "http://txt:8080".to_string());
    let pics_url = std::env::var("PICS_INTERNAL_URL").unwrap_or_else(|_| "http://pics:8080".to_string());
    let bot_url = std::env::var("BOT_INTERNAL_URL").unwrap_or_else(|_| "http://bot:8080".to_string());

    let state = AppState {
        db,
        oidc,
        oidc_client: Arc::new(oidc_client),
        stripe_webhook_secret,
        stripe_client,
        stripe_price_id,
        kube: kube_client,
        namespace,
        http_client,
        announcement,
        txt_url,
        pics_url,
        bot_url,
    };

    let handle = metrics_exporter_prometheus::PrometheusBuilder::new()
        .install_recorder()
        .expect("prometheus recorder install failed");
    PROM_HANDLE.set(handle).expect("prometheus handle already set");

    let session_store = PostgresStore::new(state.db.clone());
    session_store.migrate().await.expect("session store migration failed");

    let _deletion_task = tokio::task::spawn(
        session_store
            .clone()
            .continuously_delete_expired(tokio::time::Duration::from_secs(3600)),
    );

    let session_layer = SessionManagerLayer::new(session_store)
        .with_same_site(SameSite::Lax)
        .with_expiry(Expiry::OnInactivity(time::Duration::days(7)));

    let internal_app = Router::new()
        .route("/metrics", get(metrics_handler))
        .route("/health", get(health));

    let app = Router::new()
        .route("/", get(|| async { axum::response::Redirect::to("/dashboard") }))
        .route("/health", get(health))
        .route("/auth/login", get(routes::auth::login))
        .route("/auth/callback", get(routes::auth::callback))
        .route("/auth/logout", get(routes::auth::logout))
        .route("/auth/error", get(routes::auth::error_page))
        .route("/dashboard", get(routes::dashboard::index))
        .route("/bouncers", get(routes::bouncer::list))
        .route("/bouncers/create", post(routes::bouncer::create))
        .route("/bouncers/{name}", get(routes::bouncer::detail))
        .route("/bouncers/{name}/delete", post(routes::bouncer::delete))
        .route("/bouncers/{name}/downgrade", get(routes::bouncer::downgrade_form).post(routes::bouncer::downgrade))
        .route("/bouncers/{name}/status", get(routes::bouncer::status))
        .route("/bots", get(routes::bot::list))
        .route("/bots/create", get(routes::bot::create_form).post(routes::bot::create))
        .route("/bots/{id}", get(routes::bot::detail))
        .route("/bots/{id}/update", post(routes::bot::update))
        .route("/bots/{id}/delete", post(routes::bot::delete))
        .route("/bots/{id}/start", post(routes::bot::start))
        .route("/bots/{id}/stop", post(routes::bot::stop))
        .route("/bots/{id}/scripts/create", get(routes::bot::script_create_form).post(routes::bot::script_create))
        .route("/bots/{id}/scripts/{sid}", get(routes::bot::script_edit))
        .route("/bots/{id}/scripts/{sid}/update", post(routes::bot::script_update))
        .route("/bots/{id}/scripts/{sid}/delete", post(routes::bot::script_delete))
        .route("/bots/{id}/logs", get(routes::bot::logs))
        .route("/bots/{id}/kv", get(routes::bot::kv_list))
        .route("/bots/{id}/kv/set", post(routes::bot::kv_set))
        .route("/bots/{id}/kv/{key}/delete", post(routes::bot::kv_delete))
        .route("/networks", get(routes::network::list))
        .route("/networks/create", post(routes::network::create))
        .route("/networks/{name}", get(routes::network::detail))
        .route("/networks/{name}/delete", post(routes::network::delete))
        .route("/networks/{name}/status", get(routes::network::status))
        .route("/billing", get(routes::billing::index))
        .route("/billing/checkout", post(routes::billing::checkout))
        .route("/billing/portal", get(routes::billing::portal))
        .route("/billing/webhook", post(routes::billing::webhook))
        .route("/migrate", get(routes::migrate::form))
        .route("/migrate", post(routes::migrate::run_migration))
        .route("/profile", get(routes::profile::index))
        .route("/profile/update", post(routes::profile::update))
        .route("/report", get(routes::report::form).post(routes::report::submit))
        .route("/admin/reports", get(routes::admin::reports))
        .route("/admin/reports/{id}", get(routes::admin::report_detail))
        .route("/admin/reports/{id}/action", post(routes::admin::report_action))
        .route("/admin/users", get(routes::admin::users))
        .route("/admin/users/{sub}", get(routes::admin::user_detail))
        .route("/admin/users/{sub}/suspend", post(routes::admin::suspend_user))
        .route("/admin/users/{sub}/unsuspend", post(routes::admin::unsuspend_user))
        .route("/admin/audit", get(routes::admin::audit_log))
        .route("/admin/stats", get(routes::admin::stats))
        .nest_service("/static", ServeDir::new("static"))
        .layer(session_layer)
        .with_state(state.clone());

    tokio::spawn(bouncer_watcher::watch_bouncers(state.clone()));
    tokio::spawn(sync_shared_bouncer_limits(state.kube.clone(), state.namespace.clone()));
    tokio::spawn(business_metrics::record_event_metrics(state.db.clone()));
    tokio::spawn(business_metrics::record_soju_metrics(state.kube.clone(), state.namespace.clone()));
    tokio::spawn(business_metrics::record_stripe_metrics(state.stripe_client.clone()));

    let internal_listener = tokio::net::TcpListener::bind("0.0.0.0:9090").await.unwrap();
    tracing::info!("metrics listening on {}", internal_listener.local_addr().unwrap());
    tokio::spawn(async move {
        axum::serve(internal_listener, internal_app).await.unwrap();
    });

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