Skip to content
use actix_web::Result as AwResult;
use actix_web::{HttpRequest, get, web};
use maud::{PreEscaped, html};

use crate::{ConnectionStore, db};

/// Renders a filter icon button with an optional active indicator dot
fn filter_icon_button(
    column_name: &str,
    database: &str,
    table: &str,
    has_filter: bool,
) -> maud::Markup {
    html! {
        button
            class="bn bg-transparent pointer pa1 ml2 filter-trigger relative"
            onclick={"toggleFilter('" (column_name) "', '" (database) "', '" (table) "')"}
            title="Filter column"
            {
            svg class={"hover-white " @if has_filter { "white" } @else { "white-70" }} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" style="width: 1.25rem; height: 1.25rem;" {
                path d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z" {}
            }
            @if has_filter {
                span class="absolute bg-orange br-100" style="width: 7px; height: 7px; top: 2px; right: 2px;" {}
            }
        }
    }
}

#[get("/databases")]
pub async fn databases_page(
    req: HttpRequest,
    store: web::Data<ConnectionStore>,
) -> AwResult<maud::Markup> {
    let Some(ch) = store.active_client() else {
        let content = html! {
            div class="w-100 flex flex-column items-center justify-center" style="height: 100%;" {
                div class="bg-near-black ba b--white-20 br2 pa4 mw6 w-100 tc" {
                    p class="f4 white-70 mb3" { "No active database connection" }
                    a href="/connections"
                      hx-get="/connections"
                      hx-target="#feature"
                      hx-swap="innerHTML"
                      hx-push-url="true"
                      class="dib bg-orange white br2 ph3 pv2 no-underline f6 fw6 hover-bg-dark-orange" {
                        "Configure a connection"
                    }
                }
            }
        };
        return if req.headers().get("HX-Request").is_some() {
            Ok(content)
        } else {
            Ok(super::render_layout(&content))
        };
    };

    let databases = db::all_databases(ch).await;

    let content = html! {
        div class="w-100 flex flex-column" style="height: 100%;" {
            div class="bg-black-70 pa3" {
                div class="flex items-end" {
                    div class="mr2" {
                        select
                            id="database-select"
                            name="database"
                            class="input-reset ba b--white-30 ph2 pv1 br2 f7 bg-white-10 white"
                            style="color: white;"
                            hx-get="/databases/tables"
                            hx-include="[name='database']"
                            hx-target="#table-select-container"
                            hx-swap="innerHTML"
                            hx-trigger="change" {
                            option value="" selected disabled style="background-color: #1a1a1a; color: #ccc;" { "Database..." }
                            @for database in &databases {
                                option value=(database.name) style="background-color: #1a1a1a; color: white;" { (database.name) }
                            }
                        }
                    }

                    div id="table-select-container" {}

                    div id="toggle-controls-container" class="ml3" {}
                }
            }

            div class="w-100 ph3 mt4 flex-auto flex flex-column" {
                h1 id="db-heading" class="f4 fw6 white-90 mb3 lh-title" { "Select a database" }

                div id="db-content" class="db-content br3 flex-auto flex flex-column" style="overflow: hidden;" {
                    p class="white-70 f6 i tc pa4" {
                        "Select a database and table to view details"
                    }
                }
            }
        }
    };

    if req.headers().get("HX-Request").is_some() {
        Ok(content)
    } else {
        Ok(super::render_layout(&content))
    }
}

#[get("/databases/tables")]
pub async fn get_tables(
    database: web::Query<std::collections::HashMap<String, String>>,
    store: web::Data<ConnectionStore>,
) -> AwResult<maud::Markup> {
    let db_name = database.get("database").map(|s| s.as_str()).unwrap_or("");

    if db_name.is_empty() {
        return Ok(html! {});
    }

    let Some(ch) = store.active_client() else {
        return Ok(html! { p class="white-70 f6" { "No active connection." } });
    };

    let tables = db::all_tables(ch, db_name).await;

    Ok(html! {
     h1 id="db-heading"
        class="f4 fw6 white-90 mb3 lh-title"
        hx-swap-oob="true" {
        (db_name)
     }

     div class="mr2" {
        select
            id="table-select"
            name="table"
            class="input-reset ba b--white-30 ph2 pv1 br2 f7 bg-white-10 white"
            style="color: white;"
            hx-get="/database/tables/table"
            hx-include="[name='database']"
            hx-target="#db-content"
            hx-swap="innerHTML"
            hx-trigger="change"
            {
            option
            value=""
            selected
            disabled
            style="background-color: #1a1a1a; color: #ccc;"
            { "Table..." }
            @for table in &tables {
                option value=(table.name) style="background-color: #1a1a1a; color: white;" { (table.name) }
            }
        }
        }
    })
}

#[get("database/tables/table")]
pub async fn get_table(
    req: HttpRequest,
    store: web::Data<ConnectionStore>,
) -> AwResult<maud::Markup> {
    let query_str = req.query_string();

    let mut db_name = String::new();
    let mut table_name = String::new();
    let mut show_types = false;
    let mut filters: std::collections::HashMap<String, Vec<String>> =
        std::collections::HashMap::new();

    for pair in query_str.split('&') {
        if let Some((key, value)) = pair.split_once('=') {
            let value = value.replace("%20", " ").replace("+", " ");

            match key {
                "database" => db_name = value,
                "table" => table_name = value,
                "show_types" => show_types = value == "true" || value == "1",
                k if k.starts_with("filter_") => {
                    if !value.is_empty() {
                        let col = k.strip_prefix("filter_").unwrap();
                        filters.entry(col.to_string()).or_default().push(value);
                    }
                }
                _ => {}
            }
        }
    }

    let Some(conn) = store.active() else {
        return Ok(html! { p class="white-70 f6 pa3" { "No active connection." } });
    };

    let table_html = get_table_as_html(&conn, &db_name, &table_name, 0, show_types, &filters)
        .await
        .unwrap();

    Ok(html! {
        h1 id="db-heading"
           class="f4 fw6 white-90 mb3 lh-title"
           hx-swap-oob="true" {
            (db_name) span class="white-50" { " / " } (table_name)
        }

        div id="toggle-controls-container"
            class="ml3"
            hx-swap-oob="true" {
            label class="flex items-center pointer" {
                input
                    type="checkbox"
                    id="show-types-toggle"
                    class="mr2"
                    checked[show_types]
                    hx-get={"/database/tables/table?database=" (&db_name) "&table=" (&table_name) "&show_types=" (!show_types)
                        @for (col, vals) in &filters {
                            @for val in vals {
                                "&filter_" (col) "=" (val)
                            }
                        }
                    }
                    hx-trigger="change"
                    hx-target="#table-container"
                    hx-swap="outerHTML"
                    hx-push-url="true";
                span class="f6 white-90 fw6" { "Show Types" }
            }
        }

        (table_html)
    })
}

#[get("database/tables/table/rows")]
pub async fn get_table_rows(
    req: HttpRequest,
    store: web::Data<ConnectionStore>,
) -> AwResult<maud::Markup> {
    let query_str = req.query_string();

    let mut db_name = String::new();
    let mut table_name = String::new();
    let mut offset = 0;
    let mut show_types = false;
    let mut filters: std::collections::HashMap<String, Vec<String>> =
        std::collections::HashMap::new();

    for pair in query_str.split('&') {
        if let Some((key, value)) = pair.split_once('=') {
            let value = value.replace("%20", " ").replace("+", " ");

            match key {
                "database" => db_name = value,
                "table" => table_name = value,
                "offset" => offset = value.parse::<usize>().unwrap_or(0),
                "show_types" => show_types = value == "true" || value == "1",
                k if k.starts_with("filter_") => {
                    if !value.is_empty() {
                        let col = k.strip_prefix("filter_").unwrap();
                        filters.entry(col.to_string()).or_default().push(value);
                    }
                }
                _ => {}
            }
        }
    }

    let Some(conn) = store.active() else {
        return Ok(html! { p class="white-70 f6 pa3" { "No active connection." } });
    };

    let table_html =
        get_table_rows_html(&conn, &db_name, &table_name, offset, show_types, &filters)
            .await
            .unwrap();

    Ok(table_html)
}

pub async fn get_table_as_html(
    conn: &crate::connections::Connection,
    database: &str,
    table: &str,
    offset: usize,
    show_types: bool,
    filters: &std::collections::HashMap<String, Vec<String>>,
) -> Result<maud::Markup, Box<dyn std::error::Error>> {
    const PAGE_SIZE: usize = 40;

    let dyn_table = db::get_dyn_table(conn, database, table, PAGE_SIZE, offset, filters).await?;

    let next_offset = offset + PAGE_SIZE;
    let has_more_rows = dyn_table.row_count() == PAGE_SIZE;

    let markup = html! {
        div id="table-container" class="overflow-auto flex-auto relative" style="min-height: 400px;" {
            @if !filters.is_empty() {
                div class="pa2 bb b--white-20 flex items-center justify-between bg-near-black" {
                    div class="f6 white-70" {
                        "Active filters: "
                        @for (idx, (col, vals)) in filters.iter().enumerate() {
                            @if idx > 0 { ", " }
                            span class="white-90 fw6" { (col) }
                            " (" (vals.len()) ")"
                        }
                    }
                    button
                        class="bn bg-orange white br2 ph3 pv2 pointer hover-bg-dark-orange f6 fw6"
                        hx-get={"/database/tables/table?database=" (database) "&table=" (table) @if show_types { "&show_types=true" }}
                        hx-trigger="click"
                        hx-target="#table-container"
                        hx-swap="outerHTML"
                        hx-push-url="true"
                        { "Clear All Filters" }
                }
            }
            table class="f6 w-100" cellspacing="0" style="min-width: 800px;" {
                thead {
                    tr {
                        @for column in &dyn_table.columns {
                            th class="fw6 bb b--white-20 pv3 pr4 pl3 white-90 bg-dark-orange relative" style="position: sticky; top: 0; z-index: 10; min-width: 120px;" {
                                div class="flex items-center justify-between" {
                                    div class="flex items-center" {
                                        span { (column.name) }
                                        @if show_types {
                                            span class="f7 white-70 fw4 ml2" { "(" (column.clickhouse_type()) ")" }
                                        }
                                    }
                                    @let has_filter = filters.contains_key(&column.name);
                                    (filter_icon_button(&column.name, database, table, has_filter))
                                }
                                div id={"filter-flyout-" (column.name)} class="absolute dn bg-near-black ba b--white-20 br2 shadow-3" style="top: 100%; right: 0; width: 280px; max-height: 400px; z-index: 200; margin-top: 0.5rem;" {
                                    div class="pa3 bb b--white-20 flex items-center justify-between" {
                                        h4 class="f6 fw6 white-90 ma0" { "Filter: " (column.name) }
                                        button
                                            class="bn bg-transparent white-70 hover-white pointer f4 pa0"
                                            onclick={"document.getElementById('filter-flyout-" (column.name) "').classList.add('dn')"}
                                            { "×" }
                                    }
                                    div class="pa2 bb b--white-20" {
                                        input
                                            type="text"
                                            id={"search_" (column.name)}
                                            name="search"
                                            placeholder="Search values..."
                                            class="input-reset pa2 w-100 f6 ba b--white-30 br2 white-80 bg-black-20"
                                            hx-get={"/database/tables/table/column/values?database=" (database) "&table=" (table) "&column=" (column.name)
                                                @for (col, vals) in filters {
                                                    @for val in vals {
                                                        "&selected_" (col) "=" (val)
                                                    }
                                                }
                                            }
                                            hx-trigger="keyup changed delay:300ms"
                                            hx-target={"#filter-values-" (column.name)}
                                            hx-include="this"
                                            hx-swap="innerHTML";
                                    }
                                    div class="pa2 bb b--white-20 flex gap2" {
                                        button
                                            type="button"
                                            class="bn bg-transparent white-70 hover-white pointer f7 pa0 underline"
                                            onclick={"selectAllFilters('" (column.name) "')"} {
                                            "Select All"
                                        }
                                        span class="white-30" { " / " }
                                        button
                                            type="button"
                                            class="bn bg-transparent white-70 hover-white pointer f7 pa0 underline"
                                            onclick={"deselectAllFilters('" (column.name) "')"} {
                                            "Deselect All"
                                        }
                                    }
                                    div id={"filter-values-" (column.name)} class="overflow-y-auto pa2" style="max-height: 250px;"
                                        hx-get={"/database/tables/table/column/values?database=" (database) "&table=" (table) "&column=" (column.name)
                                            @for (col, vals) in filters {
                                                @for val in vals {
                                                    "&selected_" (col) "=" (val)
                                                }
                                            }
                                        }
                                        hx-trigger="load"
                                        hx-swap="innerHTML" {}
                                    div class="pa2 bt b--white-20" {
                                        button
                                            class="bn bg-orange white br2 pa2 w-100 pointer hover-bg-dark-orange f6 fw6"
                                            hx-get={"/database/tables/table?database=" (database) "&table=" (table) @if show_types { "&show_types=true" }}
                                            hx-trigger="click"
                                            hx-target="#table-container"
                                            hx-include="[name^='filter_']:checked"
                                            hx-swap="outerHTML"
                                            hx-push-url="true"
                                            { "Apply Filter" }
                                    }
                                }
                            }
                        }
                    }
                }
                tbody id="table-body-container" class="lh-copy" {
                    @for row_idx in 0..dyn_table.row_count() {
                        tr class="hover-bg-orange-10" {
                            @for col_idx in 0..dyn_table.column_count() {
                                td class="pv3 pr4 pl3 bb b--white-10 white-80 tl" { (dyn_table.get_value_as_string(row_idx, col_idx)) }
                            }
                        }
                    }
                    @if has_more_rows {
                        tr
                            hx-get={"/database/tables/table/rows?database=" (database) "&table=" (table) "&offset=" (next_offset) @if show_types { "&show_types=true" }}
                            hx-trigger="intersect once"
                            hx-include="[name^='filter_']"
                            hx-swap="outerHTML" {
                            td colspan="999" class="tc pv3 white-50" {
                                "Loading more..."
                            }
                        }
                    }
                }
            }
        }
        (PreEscaped(r#"<script>
            function toggleFilter(columnName, database, table) {
                const allFlyouts = document.querySelectorAll('[id^="filter-flyout-"]');
                allFlyouts.forEach(flyout => {
                    if (flyout.id !== 'filter-flyout-' + columnName) {
                        flyout.classList.add('dn');
                    }
                });
                const flyout = document.getElementById('filter-flyout-' + columnName);
                if (flyout) {
                    flyout.classList.toggle('dn');
                }
            }
            
            function selectAllFilters(columnName) {
                const container = document.getElementById('filter-values-' + columnName);
                if (container) {
                    const checkboxes = container.querySelectorAll('input[type="checkbox"]');
                    checkboxes.forEach(cb => cb.checked = true);
                }
            }
            
            function deselectAllFilters(columnName) {
                const container = document.getElementById('filter-values-' + columnName);
                if (container) {
                    const checkboxes = container.querySelectorAll('input[type="checkbox"]');
                    checkboxes.forEach(cb => cb.checked = false);
                }
            }
        </script>"#))
    };

    Ok(markup)
}

pub async fn get_table_rows_html(
    conn: &crate::connections::Connection,
    database: &str,
    table: &str,
    offset: usize,
    show_types: bool,
    filters: &std::collections::HashMap<String, Vec<String>>,
) -> Result<maud::Markup, Box<dyn std::error::Error>> {
    const PAGE_SIZE: usize = 40;

    let dyn_table = db::get_dyn_table(conn, database, table, PAGE_SIZE, offset, filters).await?;

    let next_offset = offset + PAGE_SIZE;
    let has_more_rows = dyn_table.row_count() == PAGE_SIZE;

    let markup = html! {
        @for row_idx in 0..dyn_table.row_count() {
            tr class="hover-bg-orange-10" {
                @for col_idx in 0..dyn_table.column_count() {
                    td class="pv3 pr4 pl3 bb b--white-10 white-80 tl" { (dyn_table.get_value_as_string(row_idx, col_idx)) }
                }
            }
        }
        @if has_more_rows {
            tr
                hx-get={"/database/tables/table/rows?database=" (database) "&table=" (table) "&offset=" (next_offset) @if show_types { "&show_types=true" }}
                hx-trigger="intersect once"
                hx-include="[name^='filter_']"
                hx-swap="outerHTML" {
                td colspan="999" class="tc pv3 white-50" {
                    "Loading more..."
                }
            }
        }
    };

    Ok(markup)
}

#[get("database/tables/table/column/values")]
pub async fn get_column_values(
    req: HttpRequest,
    store: web::Data<ConnectionStore>,
) -> AwResult<maud::Markup> {
    let query_str = req.query_string();

    let mut db_name = String::new();
    let mut table = String::new();
    let mut column = String::new();
    let mut offset = 0;
    let mut search = None;
    let mut selected_values: Vec<String> = Vec::new();
    let mut filters: std::collections::HashMap<String, Vec<String>> =
        std::collections::HashMap::new();

    for pair in query_str.split('&') {
        if let Some((key, value)) = pair.split_once('=') {
            let value = value.replace("%20", " ").replace("+", " ");

            match key {
                "database" => db_name = value,
                "table" => table = value,
                "column" => column = value,
                "offset" => offset = value.parse::<usize>().unwrap_or(0),
                "search" => search = Some(value),
                k if k.starts_with("selected_") => {
                    if let Some(col) = k.strip_prefix("selected_") {
                        if col == column && !value.is_empty() {
                            selected_values.push(value.clone());
                        }
                        if !value.is_empty() {
                            filters.entry(col.to_string()).or_default().push(value);
                        }
                    }
                }
                _ => {}
            }
        }
    }

    let Some(conn) = store.active() else {
        return Ok(html! { p class="white-70 f6 pa2" { "No active connection." } });
    };

    const PAGE_SIZE: usize = 40;

    let dyn_table = db::get_distinct_column_values(
        &conn,
        &db_name,
        &table,
        &column,
        PAGE_SIZE,
        offset,
        search.as_deref(),
        &filters,
    )
    .await
    .unwrap();

    let next_offset = offset + PAGE_SIZE;
    let has_more = dyn_table.row_count() == PAGE_SIZE;

    let markup = html! {
        @for row_idx in 0..dyn_table.row_count() {
            @let value = dyn_table.get_value_as_string(row_idx, 0);
            @let is_checked = selected_values.contains(&value);
            label class="db pv2 ph2 hover-bg-white-10 pointer tl" {
                input
                    type="checkbox"
                    name={"filter_" (column)}
                    value={(value)}
                    class="mr2"
                    checked[is_checked];
                span class="white-80" { (value) }
            }
        }
        @if has_more {
            div
                hx-get={"/database/tables/table/column/values?database=" (&db_name) "&table=" (&table) "&column=" (&column) "&offset=" (next_offset)
                    @if let Some(s) = &search { "&search=" (s) }
                    @for val in &selected_values { "&selected_" (column) "=" (val) }
                }
                hx-trigger="intersect once"
                hx-swap="outerHTML"
                class="tc pv2 white-50 f7" {
                "Load more..."
            }
        }
    };

    Ok(markup)
}