Skip to content
use actix_web::{App, HttpResponse, HttpServer, Responder, get, web};
use env_logger::Env;
use log::info;

mod assets;
mod config;
mod connections;
mod db;
mod upload;
mod view;

pub use connections::ConnectionStore;

#[get("/health")]
async fn health() -> impl Responder {
    HttpResponse::Ok()
}

#[get("/health/db")]
async fn health_db(store: web::Data<ConnectionStore>) -> impl Responder {
    let Some(ch) = store.active_client() else {
        return HttpResponse::ServiceUnavailable().body("No active database connection configured");
    };
    match ch.query("SELECT 1").fetch_one::<u8>().await {
        Ok(_) => HttpResponse::Ok().body("Database connection OK"),
        Err(e) => {
            log::error!("Database health check failed: {}", e);
            HttpResponse::ServiceUnavailable().body(format!("Database connection failed: {}", e))
        }
    }
}

#[get("/health/db/status")]
async fn health_db_status(store: web::Data<ConnectionStore>) -> impl Responder {
    use maud::html;

    let (status_class, status_text, icon) = match store.active_client() {
        None => ("bg-dark-gray white", "Not configured", "โ—‹"),
        Some(ch) => match ch.query("SELECT 1").fetch_one::<u8>().await {
            Ok(_) => ("bg-dark-green white", "Healthy", "โ—"),
            Err(e) => {
                log::error!("Database health check failed: {}", e);
                ("bg-dark-red white", "Unhealthy", "โ—")
            }
        },
    };

    let markup = html! {
        div id="db-status"
            class=(format!("flex items-center pa2 br2 dib {}", status_class))
            hx-get="/health/db/status"
            hx-trigger="every 10s"
            hx-swap="outerHTML" {
            span class="f6 mr2" { (icon) }
            span class="f6 b" { "Database: " (status_text) }
        }
    };

    maud::PreEscaped(markup.into_string())
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let config = config::from_env();

    env_logger::Builder::from_env(Env::default().default_filter_or(config.log_level())).init();

    info!("{config}");

    // Load persisted connections from disk.
    let store = ConnectionStore::load(ConnectionStore::default_path());

    // Load connections from environment variables.
    // Supports: CLICKHOUSE_URL, CLICKHOUSE_USER, CLICKHOUSE_PASSWORD (legacy)
    // And numbered variants: CLICKHOUSE_URL_1, CLICKHOUSE_USER_1, etc.
    let env_connections = load_connections_from_env();

    if store.all_meta().is_empty() && env_connections.is_empty() {
        info!(
            "No connections configured. Add connections via the web UI or environment variables."
        );
    } else if store.all_meta().is_empty() {
        // Seed from environment variables if no persisted connections exist.
        for (idx, (name, host, port, user, password)) in env_connections.iter().enumerate() {
            store.add(
                name.clone(),
                host.clone(),
                *port,
                user.clone(),
                password.clone(),
            );
            if idx == 0 {
                info!(
                    "Seeded default connection '{}' from environment variables",
                    name
                );
            } else {
                info!("Seeded connection '{}' from environment variables", name);
            }
        }
    } else {
        // Connections exist on disk - try to match passwords from env for connections
        // that have the same host/port/user as env-configured ones.
        for (name, host, port, user, password) in &env_connections {
            let meta_list = store.all_meta();
            let matching = meta_list
                .iter()
                .find(|m| m.host == *host && m.port == *port && m.user == *user);
            if let Some(meta) = matching {
                // Set password from env for this existing connection
                store.set_password(&meta.id, password.clone());
                info!(
                    "Updated password from env for connection '{}' ({}@{}:{})",
                    meta.name, user, host, port
                );
            } else {
                // Add as new connection
                store.add(
                    name.clone(),
                    host.clone(),
                    *port,
                    user.clone(),
                    password.clone(),
                );
                info!("Added new connection '{}' from environment variables", name);
            }
        }
    }

    let store_data = web::Data::new(store);
    let config_data = web::Data::new(config);

    let bind_address = config_data.address();

    HttpServer::new(move || {
        App::new()
            .app_data(store_data.clone())
            .app_data(config_data.clone())
            .service(health)
            .service(health_db)
            .service(health_db_status)
            .service(assets::assets)
            .service(view::index)
            .service(view::home::home_page)
            .service(view::about::about_page)
            .service(view::how_it_works::how_it_works_page)
            .service(view::upload::upload_page)
            .service(view::databases::databases_page)
            .service(view::databases::get_tables)
            .service(view::databases::get_table)
            .service(view::databases::get_table_rows)
            .service(view::databases::get_column_values)
            .service(view::ag_grid::ag_grid_page)
            .service(view::ag_grid::ag_grid_get_tables)
            .service(view::ag_grid::ag_grid_get_grid)
            .service(view::ag_grid::ag_grid_column_defs)
            .service(view::ag_grid::ag_grid_set_filter_values)
            .service(view::ag_grid::ag_grid_pivot_column_defs)
            .service(view::ag_grid::ag_grid_pivot_auto_config)
            .service(view::ag_grid::ag_grid_rows)
            .service(view::ag_grid::ag_grid_pivot_rows)
            .service(view::charts::charts_page)
            .service(view::charts::charts_get_tables)
            .service(view::charts::charts_view)
            .service(view::charts::charts_render)
            .service(view::test_data::test_data_page)
            .service(view::test_data::generate)
            .service(upload::upload_csv)
            .service(view::connections::connections_page)
            .service(view::connections::add_connection)
            .service(view::connections::delete_connection)
            .service(view::connections::set_active_connection)
            .service(view::connections::set_connection_password)
    })
    .bind(bind_address)?
    .run()
    .await
}

/// Parse a ClickHouse HTTP URL like `http://host:port` into `(host, port)`.
fn parse_clickhouse_url(url: &str) -> (String, u16) {
    let without_scheme = url
        .strip_prefix("http://")
        .or_else(|| url.strip_prefix("https://"))
        .unwrap_or(url);
    if let Some(colon) = without_scheme.rfind(':') {
        let host = without_scheme[..colon].to_string();
        let port = without_scheme[colon + 1..].parse::<u16>().unwrap_or(8123);
        (host, port)
    } else {
        (without_scheme.to_string(), 8123)
    }
}

/// Load connections from environment variables.
/// Supports:
/// - Legacy: CLICKHOUSE_URL, CLICKHOUSE_USER, CLICKHOUSE_PASSWORD
/// - Numbered: CLICKHOUSE_URL_1, CLICKHOUSE_USER_1, CLICKHOUSE_PASSWORD_1, etc.
///
/// Returns Vec of (name, host, port, user, password).
fn load_connections_from_env() -> Vec<(String, String, u16, String, String)> {
    let mut connections = Vec::new();

    // First, check for numbered connections (CLICKHOUSE_URL_1, etc.)
    let mut idx = 1;
    loop {
        let url_var = format!("CLICKHOUSE_URL_{}", idx);
        let user_var = format!("CLICKHOUSE_USER_{}", idx);
        let pass_var = format!("CLICKHOUSE_PASSWORD_{}", idx);

        if let Ok(url) = std::env::var(&url_var) {
            let (host, port) = parse_clickhouse_url(&url);
            let user = std::env::var(&user_var).unwrap_or_else(|_| "default".to_string());
            let password = std::env::var(&pass_var).unwrap_or_default();
            let name = format!("Connection {} (from env)", idx);
            connections.push((name, host, port, user, password));
            idx += 1;
        } else {
            // No more numbered connections
            break;
        }
    }

    // Also check for the legacy un-numbered variables (treated as connection 0)
    if let Ok(url) = std::env::var("CLICKHOUSE_URL") {
        let (host, port) = parse_clickhouse_url(&url);
        let user = std::env::var("CLICKHOUSE_USER").unwrap_or_else(|_| "default".to_string());
        let password = std::env::var("CLICKHOUSE_PASSWORD").unwrap_or_default();
        let name = if connections.is_empty() {
            "Default (from env)".to_string()
        } else {
            "Legacy (from env)".to_string()
        };
        connections.push((name, host, port, user, password));
    }

    connections
}