use dioxus::prelude::*;
use serde::{Deserialize, Serialize};

fn api_base() -> String {
    if let Some(window) = web_sys::window() {
        if let Ok(hostname) = window.location().hostname() {
            return format!("http://{}:3100", hostname);
        }
    }
    "http://localhost:3100".to_string()
}

fn main() {
    dioxus::launch(App);
}

// --- API Types ---

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
struct Dataset {
    id: String,
    name: String,
    schema_fingerprint: String,
    objects: Vec<ObjRef>,
    created_at: String,
    updated_at: String,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
struct ObjRef {
    bucket: String,
    key: String,
    size_bytes: u64,
    created_at: String,
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
struct QueryResponse {
    columns: Vec<ColumnInfo>,
    rows: serde_json::Value,
    row_count: usize,
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
struct ColumnInfo {
    name: String,
    data_type: String,
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
struct GenerateResponse {
    text: String,
    model: String,
    tokens_evaluated: Option<u64>,
    tokens_generated: Option<u64>,
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
struct EmbedResponse {
    embeddings: Vec<Vec<f64>>,
    model: String,
    dimensions: usize,
}

// --- API Calls ---

async fn fetch_datasets() -> Result<Vec<Dataset>, String> {
    let resp = reqwest::get(&format!("{}/catalog/datasets", api_base()))
        .await.map_err(|e| e.to_string())?;
    resp.json().await.map_err(|e| e.to_string())
}

async fn run_sql(sql: &str) -> Result<QueryResponse, String> {
    let client = reqwest::Client::new();
    let resp = client.post(&format!("{}/query/sql", api_base()))
        .json(&serde_json::json!({"sql": sql}))
        .send().await.map_err(|e| e.to_string())?;
    if !resp.status().is_success() {
        return Err(resp.text().await.unwrap_or_default());
    }
    resp.json().await.map_err(|e| e.to_string())
}

async fn ai_generate(prompt: &str, max_tokens: u32) -> Result<GenerateResponse, String> {
    let client = reqwest::Client::new();
    let resp = client.post(&format!("{}/ai/generate", api_base()))
        .json(&serde_json::json!({"prompt": prompt, "max_tokens": max_tokens, "temperature": 0.2}))
        .send().await.map_err(|e| e.to_string())?;
    if !resp.status().is_success() {
        return Err(resp.text().await.unwrap_or_default());
    }
    resp.json().await.map_err(|e| e.to_string())
}

async fn ai_embed(texts: Vec<String>) -> Result<EmbedResponse, String> {
    let client = reqwest::Client::new();
    let resp = client.post(&format!("{}/ai/embed", api_base()))
        .json(&serde_json::json!({"texts": texts}))
        .send().await.map_err(|e| e.to_string())?;
    if !resp.status().is_success() {
        return Err(resp.text().await.unwrap_or_default());
    }
    resp.json().await.map_err(|e| e.to_string())
}

async fn fetch_health(path: &str) -> Result<String, String> {
    let resp = reqwest::get(&format!("{}{}", api_base(), path))
        .await.map_err(|e| e.to_string())?;
    resp.text().await.map_err(|e| e.to_string())
}

/// Cached schema context — built once, reused across questions.
static SCHEMA_CACHE: std::sync::OnceLock<std::sync::Mutex<Option<String>>> = std::sync::OnceLock::new();

async fn get_schema_context_cached(datasets: &[Dataset]) -> String {
    // Check cache first
    {
        let cache = SCHEMA_CACHE.get_or_init(|| std::sync::Mutex::new(None));
        if let Ok(guard) = cache.lock() {
            if let Some(ref cached) = *guard {
                return cached.clone();
            }
        }
    }
    // Build and cache
    let ctx = get_schema_context(datasets).await;
    if let Some(cache) = SCHEMA_CACHE.get() {
        if let Ok(mut guard) = cache.lock() {
            *guard = Some(ctx.clone());
        }
    }
    ctx
}

/// Get schema context for datasets (used for AI SQL generation).
async fn get_schema_context(datasets: &[Dataset]) -> String {
    let core_tables = ["candidates", "clients", "job_orders", "placements", "timesheets", "call_log", "email_log"];
    let mut ctx = String::from("DATABASE SCHEMA:\n\n");
    for ds in datasets.iter().filter(|d| core_tables.contains(&d.name.as_str())) {
        let desc = run_sql(&format!("DESCRIBE {}", ds.name)).await;
        match desc {
            Ok(resp) => {
                ctx.push_str(&format!("TABLE: {}\n  Columns:\n", ds.name));
                if let Some(rows) = resp.rows.as_array() {
                    for row in rows {
                        let col = row.get("column_name").and_then(|v| v.as_str()).unwrap_or("?");
                        let dt = row.get("data_type").and_then(|v| v.as_str()).unwrap_or("?");
                        ctx.push_str(&format!("    {}.{} ({})\n", ds.name, col, dt));
                    }
                }
                ctx.push('\n');
            }
            Err(_) => {
                ctx.push_str(&format!("TABLE: {} (schema unavailable)\n\n", ds.name));
            }
        }
    }
    // Add relationship hints so the model knows how to JOIN
    ctx.push_str("RELATIONSHIPS:\n");
    ctx.push_str("  candidates.candidate_id = placements.candidate_id\n");
    ctx.push_str("  candidates.candidate_id = timesheets.candidate_id\n");
    ctx.push_str("  candidates.candidate_id = call_log.candidate_id\n");
    ctx.push_str("  candidates.candidate_id = email_log.candidate_id\n");
    ctx.push_str("  clients.client_id = job_orders.client_id\n");
    ctx.push_str("  clients.client_id = placements.client_id\n");
    ctx.push_str("  clients.client_id = timesheets.client_id\n");
    ctx.push_str("  job_orders.job_order_id = placements.job_order_id\n");
    ctx.push_str("  placements.placement_id = timesheets.placement_id\n");
    ctx.push_str("\nNOTE: 'vertical' is only in candidates, clients, and job_orders. To get vertical for timesheets or placements, JOIN to those tables.\n");
    ctx
}

// --- Tabs ---
#[derive(Clone, PartialEq)]
enum Tab {
    Dashboard,
    Ask,
    Explore,
    Sql,
    Ingest,
    Playbook,
    Status,
}

// --- Playbook memory types (Phase 19) ---

#[derive(Debug, Clone, Deserialize, PartialEq)]
struct PlaybookStats {
    entries: usize,
    entries_with_embeddings: usize,
    #[serde(default)]
    total_names_endorsed: usize,
    #[serde(default)]
    sample: Vec<PlaybookSample>,
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
struct PlaybookSample {
    id: String,
    operation: String,
    #[serde(default)]
    city: Option<String>,
    #[serde(default)]
    state: Option<String>,
    #[serde(default)]
    endorsed: Vec<String>,
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
struct HybridResp {
    #[serde(default)]
    sql_matches: usize,
    #[serde(default)]
    vector_reranked: usize,
    #[serde(default)]
    method: String,
    #[serde(default)]
    duration_ms: u64,
    #[serde(default)]
    answer: Option<String>,
    #[serde(default)]
    sources: Vec<HybridSource>,
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
struct HybridSource {
    doc_id: String,
    chunk_text: String,
    score: f32,
    #[serde(default)]
    sql_verified: bool,
    #[serde(default)]
    playbook_boost: f32,
    #[serde(default)]
    playbook_citations: Vec<String>,
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
struct IndexInfo {
    index_name: String,
    source: String,
    #[serde(default)]
    chunk_count: usize,
    #[serde(default)]
    vector_backend: String,
}

async fn fetch_playbook_stats() -> Result<PlaybookStats, String> {
    let resp = reqwest::get(&format!("{}/vectors/playbook_memory/stats", api_base()))
        .await.map_err(|e| e.to_string())?;
    if !resp.status().is_success() {
        return Err(format!("HTTP {}: {}", resp.status(), resp.text().await.unwrap_or_default()));
    }
    resp.json().await.map_err(|e| e.to_string())
}

async fn rebuild_playbook_memory() -> Result<serde_json::Value, String> {
    let client = reqwest::Client::new();
    let resp = client.post(&format!("{}/vectors/playbook_memory/rebuild", api_base()))
        .json(&serde_json::json!({}))
        .send().await.map_err(|e| e.to_string())?;
    if !resp.status().is_success() {
        return Err(format!("HTTP {}: {}", resp.status(), resp.text().await.unwrap_or_default()));
    }
    resp.json().await.map_err(|e| e.to_string())
}

async fn fetch_indexes() -> Result<Vec<IndexInfo>, String> {
    let resp = reqwest::get(&format!("{}/vectors/indexes", api_base()))
        .await.map_err(|e| e.to_string())?;
    resp.json().await.map_err(|e| e.to_string())
}

async fn hybrid_search(index_name: &str, question: &str, use_playbook: bool, top_k: usize) -> Result<HybridResp, String> {
    let client = reqwest::Client::new();
    let resp = client.post(&format!("{}/vectors/hybrid", api_base()))
        .json(&serde_json::json!({
            "index_name": index_name,
            "question": question,
            "top_k": top_k,
            "generate": false,
            "use_playbook_memory": use_playbook,
        }))
        .send().await.map_err(|e| e.to_string())?;
    if !resp.status().is_success() {
        return Err(format!("HTTP {}: {}", resp.status(), resp.text().await.unwrap_or_default()));
    }
    resp.json().await.map_err(|e| e.to_string())
}

// --- App ---

#[component]
fn App() -> Element {
    let mut active_tab = use_signal(|| Tab::Ask);
    let mut datasets = use_signal(Vec::<Dataset>::new);
    let mut ds_loading = use_signal(|| true);

    use_effect(move || {
        spawn(async move {
            ds_loading.set(true);
            if let Ok(ds) = fetch_datasets().await {
                datasets.set(ds);
            }
            ds_loading.set(false);
        });
    });

    let refresh = move |_| {
        spawn(async move {
            ds_loading.set(true);
            if let Ok(ds) = fetch_datasets().await {
                datasets.set(ds);
            }
            ds_loading.set(false);
        });
    };

    rsx! {
        div { class: "app",
            div { class: "header",
                h1 { "LAKEHOUSE" }
                div { class: "tabs",
                    button {
                        class: if *active_tab.read() == Tab::Dashboard { "tab active" } else { "tab" },
                        onclick: move |_| active_tab.set(Tab::Dashboard),
                        "Dashboard"
                    }
                    button {
                        class: if *active_tab.read() == Tab::Ask { "tab active" } else { "tab" },
                        onclick: move |_| active_tab.set(Tab::Ask),
                        "Ask"
                    }
                    button {
                        class: if *active_tab.read() == Tab::Explore { "tab active" } else { "tab" },
                        onclick: move |_| active_tab.set(Tab::Explore),
                        "Explore"
                    }
                    button {
                        class: if *active_tab.read() == Tab::Sql { "tab active" } else { "tab" },
                        onclick: move |_| active_tab.set(Tab::Sql),
                        "SQL"
                    }
                    button {
                        class: if *active_tab.read() == Tab::Ingest { "tab active" } else { "tab" },
                        onclick: move |_| active_tab.set(Tab::Ingest),
                        "Ingest"
                    }
                    button {
                        class: if *active_tab.read() == Tab::Playbook { "tab active" } else { "tab" },
                        onclick: move |_| active_tab.set(Tab::Playbook),
                        "Playbook"
                    }
                    button {
                        class: if *active_tab.read() == Tab::Status { "tab active" } else { "tab" },
                        onclick: move |_| active_tab.set(Tab::Status),
                        "System"
                    }
                }
                div { class: "header-right",
                    span { class: "ds-count",
                        {if *ds_loading.read() { "...".to_string() } else { format!("{} datasets", datasets.read().len()) }}
                    }
                    button { class: "refresh-btn", onclick: refresh, "↻" }
                }
            }

            div { class: "content-full",
                match *active_tab.read() {
                    Tab::Dashboard => rsx! { DashboardPanel {} },
                    Tab::Ask => rsx! { AskPanel { datasets: datasets.read().clone() } },
                    Tab::Explore => rsx! { ExplorePanel { datasets: datasets.read().clone() } },
                    Tab::Sql => rsx! { SqlPanel {} },
                    Tab::Ingest => rsx! { IngestPanel {} },
                    Tab::Playbook => rsx! { PlaybookPanel {} },
                    Tab::Status => rsx! { StatusPanel {} },
                }
            }
        }
    }
}

// === ASK — Natural language → SQL → Results ===

#[component]
fn AskPanel(datasets: Vec<Dataset>) -> Element {
    let mut question = use_signal(|| String::new());
    let mut generated_sql = use_signal(|| None::<String>);
    let mut result = use_signal(|| None::<Result<QueryResponse, String>>);
    let mut thinking = use_signal(|| false);
    let mut step = use_signal(|| String::new());

    let datasets_clone = datasets.clone();

    let ask = move |_| {
        let q = question.read().clone();
        if q.trim().is_empty() { return; }
        let ds = datasets_clone.clone();
        spawn(async move {
            thinking.set(true);
            generated_sql.set(None);
            result.set(None);

            // Step 1: Get schema context
            step.set("reading schemas...".into());
            let schema_ctx = get_schema_context_cached(&ds).await;

            // Step 2: Generate SQL
            step.set("writing SQL...".into());
            let prompt = format!(
                "You are a SQL assistant. You write Apache DataFusion SQL (PostgreSQL-compatible).\n\n\
                CRITICAL: You MUST only use column names that appear in the schema below. Do NOT invent or guess column names.\n\n\
                {schema_ctx}\n\
                User question: {q}\n\n\
                Rules:\n\
                - ONLY use table and column names listed above\n\
                - If unsure about a column name, pick the closest match from the schema\n\
                - Output ONLY the SQL query. No markdown, no explanation, no backticks."
            );

            match ai_generate(&prompt, 512).await {
                Ok(resp) => {
                    let sql = clean_sql(&resp.text);
                    generated_sql.set(Some(sql.clone()));

                    // Step 3: Execute
                    step.set("running query...".into());
                    let query_result = run_sql(&sql).await;

                    // Step 3b: If schema error, retry with the error as feedback
                    if let Err(ref err) = query_result {
                        if err.contains("error") {
                            step.set("fixing SQL...".into());
                            let retry_prompt = format!(
                                "The SQL you wrote had an error:\n{err}\n\n\
                                {schema_ctx}\n\
                                Original question: {q}\n\n\
                                Write a CORRECTED SQL query using ONLY the columns listed in the schema. Output ONLY SQL."
                            );
                            if let Ok(retry_resp) = ai_generate(&retry_prompt, 512).await {
                                let retry_sql = clean_sql(&retry_resp.text);
                                generated_sql.set(Some(retry_sql.clone()));
                                step.set("running corrected query...".into());
                                let retry_result = run_sql(&retry_sql).await;
                                result.set(Some(retry_result));
                            } else {
                                result.set(Some(query_result));
                            }
                        } else {
                            result.set(Some(query_result));
                        }
                    } else {
                        result.set(Some(query_result));
                    }
                }
                Err(e) => {
                    result.set(Some(Err(format!("AI error: {e}"))));
                }
            }

            step.set(String::new());
            thinking.set(false);
        });
    };

    rsx! {
        div { class: "panel ask-panel",
            div { class: "ask-hero",
                h2 { "Ask your data anything" }
                p { class: "subtitle", "Natural language → SQL → Results. Powered by local AI over the staffing dataset." }
            }

            div { class: "ask-input-row",
                input {
                    class: "ask-input",
                    value: "{question}",
                    placeholder: "e.g. Which clients placed the most candidates last quarter?",
                    oninput: move |e| question.set(e.value()),
                    onkeydown: move |e| {
                        if e.key() == Key::Enter {
                            let q = question.read().clone();
                            if q.trim().is_empty() { return; }
                            let ds = datasets.clone();
                            spawn(async move {
                                thinking.set(true);
                                generated_sql.set(None);
                                result.set(None);
                                step.set("reading schemas...".into());
                                let schema_ctx = get_schema_context_cached(&ds).await;
                                step.set("writing SQL...".into());
                                let prompt = format!(
                                    "You are a SQL assistant. You write Apache DataFusion SQL (PostgreSQL-compatible).\n\n\
                                    CRITICAL: You MUST only use column names that appear in the schema below. Do NOT invent or guess column names.\n\n\
                                    {schema_ctx}\n\
                                    User question: {q}\n\n\
                                    Rules:\n\
                                    - ONLY use table and column names listed above\n\
                                    - If unsure about a column name, pick the closest match from the schema\n\
                                    - Output ONLY the SQL query. No markdown, no explanation, no backticks."
                                );
                                match ai_generate(&prompt, 512).await {
                                    Ok(resp) => {
                                        let sql = clean_sql(&resp.text);
                                        generated_sql.set(Some(sql.clone()));
                                        step.set("running query...".into());
                                        let query_result = run_sql(&sql).await;
                                        if let Err(ref err) = query_result {
                                            if err.contains("error") {
                                                step.set("fixing SQL...".into());
                                                let retry_prompt = format!(
                                                    "The SQL you wrote had an error:\n{err}\n\n{schema_ctx}\n\nOriginal question: {q}\n\nWrite a CORRECTED SQL query using ONLY the columns listed. Output ONLY SQL."
                                                );
                                                if let Ok(rr) = ai_generate(&retry_prompt, 512).await {
                                                    let rsql = clean_sql(&rr.text);
                                                    generated_sql.set(Some(rsql.clone()));
                                                    step.set("running corrected query...".into());
                                                    result.set(Some(run_sql(&rsql).await));
                                                } else {
                                                    result.set(Some(query_result));
                                                }
                                            } else {
                                                result.set(Some(query_result));
                                            }
                                        } else {
                                            result.set(Some(query_result));
                                        }
                                    }
                                    Err(e) => {
                                        result.set(Some(Err(format!("AI error: {e}"))));
                                    }
                                }
                                step.set(String::new());
                                thinking.set(false);
                            });
                        }
                    },
                }
                button {
                    class: "btn btn-ask",
                    disabled: *thinking.read(),
                    onclick: ask,
                    {if *thinking.read() { step.read().clone() } else { "Ask".to_string() }}
                }
            }

            div { class: "ask-examples",
                "Try: "
                button { class: "example-btn", onclick: move |_| question.set("How many candidates do we have by city?".into()), "candidates by city" }
                button { class: "example-btn", onclick: move |_| question.set("Top 10 clients by total placements".into()), "top clients by placements" }
                button { class: "example-btn", onclick: move |_| question.set("Open job orders ordered by bill rate descending".into()), "open jobs by rate" }
                button { class: "example-btn", onclick: move |_| question.set("Recruiters with the highest placement count".into()), "top recruiters" }
                button { class: "example-btn", onclick: move |_| question.set("Total billed hours per client last month".into()), "hours per client" }
                button { class: "example-btn", onclick: move |_| question.set("Cold leads: candidates we called more than 5 times but never placed".into()), "cold leads" }
            }

            if let Some(sql) = generated_sql.read().as_ref() {
                div { class: "generated-sql",
                    span { class: "sql-label", "Generated SQL" }
                    pre { class: "sql-code", "{sql}" }
                }
            }

            match result.read().as_ref() {
                None => rsx! {},
                Some(Err(e)) => rsx! { div { class: "error", "{e}" } },
                Some(Ok(resp)) => rsx! { ResultsTable { response: resp.clone() } },
            }
        }
    }
}

// === EXPLORE — Dataset overview + AI summary ===

#[component]
fn ExplorePanel(datasets: Vec<Dataset>) -> Element {
    let mut selected = use_signal(|| None::<String>);
    let mut schema_info = use_signal(|| None::<Result<QueryResponse, String>>);
    let mut preview = use_signal(|| None::<Result<QueryResponse, String>>);
    let mut summary = use_signal(|| None::<Result<String, String>>);
    let mut loading = use_signal(|| false);

    let mut select_ds = move |name: String| {
        selected.set(Some(name.clone()));
        spawn(async move {
            loading.set(true);
            schema_info.set(None);
            preview.set(None);
            summary.set(None);

            // Get schema
            let desc = run_sql(&format!("DESCRIBE {name}")).await;
            schema_info.set(Some(desc));

            // Get preview
            let prev = run_sql(&format!("SELECT * FROM {name} LIMIT 5")).await;

            // Generate AI summary
            if let Ok(ref data) = prev {
                let data_json = serde_json::to_string(&data.rows).unwrap_or_default();
                let cols: Vec<String> = data.columns.iter()
                    .map(|c| format!("{} ({})", c.name, c.data_type))
                    .collect();
                let prompt = format!(
                    "You are a data analyst. Describe this dataset in 2-3 sentences. Be specific about what the data contains and what insights it could provide.\n\n\
                    Table: {name}\n\
                    Columns: {}\n\
                    Sample data (first 5 rows): {data_json}\n\n\
                    Description:",
                    cols.join(", ")
                );
                let summ = ai_generate(&prompt, 256).await.map(|r| r.text);
                summary.set(Some(summ));
            }

            preview.set(Some(prev));
            loading.set(false);
        });
    };

    rsx! {
        div { class: "panel explore-panel",
            div { class: "explore-grid",
                // Dataset cards
                div { class: "ds-cards",
                    h3 { "Datasets" }
                    if datasets.is_empty() {
                        div { class: "empty", "No datasets registered" }
                    }
                    for ds in datasets.iter() {
                        {
                            let name = ds.name.clone();
                            let name2 = ds.name.clone();
                            let is_active = selected.read().as_ref() == Some(&ds.name);
                            let obj_count = ds.objects.len();
                            let total_bytes: u64 = ds.objects.iter().map(|o| o.size_bytes).sum();
                            rsx! {
                                div {
                                    class: if is_active { "ds-card active" } else { "ds-card" },
                                    onclick: move |_| select_ds(name.clone()),
                                    div { class: "ds-card-name", "{name2}" }
                                    div { class: "ds-card-meta", "{obj_count} file(s) · {total_bytes} bytes" }
                                }
                            }
                        }
                    }
                }

                // Detail view
                div { class: "ds-detail",
                    if *loading.read() {
                        div { class: "loading", "analyzing dataset..." }
                    } else if selected.read().is_none() {
                        div { class: "empty", "select a dataset to explore" }
                    } else {
                        if let Some(ref name) = *selected.read() {
                            h3 { "{name}" }
                        }

                        // AI Summary
                        if let Some(result) = summary.read().as_ref() {
                            div { class: "summary-box",
                                span { class: "summary-label", "AI Summary" }
                                match result {
                                    Ok(text) => rsx! { p { class: "summary-text", "{text}" } },
                                    Err(e) => rsx! { p { class: "error-inline", "{e}" } },
                                }
                            }
                        }

                        // Schema
                        if let Some(Ok(schema)) = schema_info.read().as_ref() {
                            div { class: "schema-box",
                                span { class: "section-label", "Schema" }
                                ResultsTable { response: schema.clone() }
                            }
                        }

                        // Preview
                        if let Some(Ok(prev)) = preview.read().as_ref() {
                            div { class: "preview-box",
                                span { class: "section-label", "Preview (5 rows)" }
                                ResultsTable { response: prev.clone() }
                            }
                        }
                    }
                }
            }
        }
    }
}

// === SQL — Raw SQL editor ===

#[component]
fn SqlPanel() -> Element {
    let mut query_text = use_signal(|| String::from("SELECT candidate_id, first_name, last_name, city, status FROM candidates LIMIT 10"));
    let mut result = use_signal(|| None::<Result<QueryResponse, String>>);
    let mut loading = use_signal(|| false);

    let run = move |_| {
        let sql = query_text.read().clone();
        if sql.trim().is_empty() { return; }
        spawn(async move {
            loading.set(true);
            result.set(Some(run_sql(&sql).await));
            loading.set(false);
        });
    };

    rsx! {
        div { class: "panel",
            div { class: "sql-editor",
                textarea {
                    class: "sql-textarea",
                    value: "{query_text}",
                    placeholder: "SELECT * FROM dataset LIMIT 100",
                    oninput: move |e| query_text.set(e.value()),
                    onkeydown: move |e| {
                        if e.key() == Key::Enter && e.modifiers().ctrl() {
                            let sql = query_text.read().clone();
                            if !sql.trim().is_empty() {
                                spawn(async move {
                                    loading.set(true);
                                    result.set(Some(run_sql(&sql).await));
                                    loading.set(false);
                                });
                            }
                        }
                    },
                }
                div { class: "sql-actions",
                    button {
                        class: "btn",
                        disabled: *loading.read(),
                        onclick: run,
                        if *loading.read() { "running..." } else { "Run (Ctrl+Enter)" }
                    }
                }
            }
            div { class: "results-area",
                match result.read().as_ref() {
                    None => rsx! { div { class: "empty", "run a query to see results" } },
                    Some(Err(e)) => rsx! { div { class: "error", "{e}" } },
                    Some(Ok(resp)) => rsx! { ResultsTable { response: resp.clone() } },
                }
            }
        }
    }
}

// === DASHBOARD — System overview ===

#[component]
fn DashboardPanel() -> Element {
    let mut stats = use_signal(|| None::<serde_json::Value>);
    let mut loading = use_signal(|| false);

    let do_load = move || {
        spawn(async move {
            loading.set(true);
            let client = reqwest::Client::new();

            let datasets = fetch_datasets().await.ok().map(|d| d.len()).unwrap_or(0);
            let row_count = run_sql("SELECT (SELECT COUNT(*) FROM candidates) + (SELECT COUNT(*) FROM clients) + (SELECT COUNT(*) FROM job_orders) + (SELECT COUNT(*) FROM placements) + (SELECT COUNT(*) FROM timesheets) + (SELECT COUNT(*) FROM call_log) + (SELECT COUNT(*) FROM email_log) as total").await.ok().and_then(|r| r.rows.as_array()?.first()?.get("total")?.as_i64()).unwrap_or(0);

            let cache: Option<serde_json::Value> = match client.get(&format!("{}/query/cache/stats", api_base())).send().await {
                Ok(r) => r.json().await.ok(),
                Err(_) => None,
            };

            let indexes: Vec<serde_json::Value> = match client.get(&format!("{}/vectors/indexes", api_base())).send().await {
                Ok(r) => r.json().await.unwrap_or_default(),
                Err(_) => vec![],
            };

            let tools: Vec<serde_json::Value> = match client.get(&format!("{}/tools", api_base())).send().await {
                Ok(r) => r.json().await.unwrap_or_default(),
                Err(_) => vec![],
            };

            let jobs: Vec<serde_json::Value> = match client.get(&format!("{}/vectors/jobs", api_base())).send().await {
                Ok(r) => r.json().await.unwrap_or_default(),
                Err(_) => vec![],
            };

            let hnsw: Vec<String> = match client.get(&format!("{}/vectors/hnsw/list", api_base())).send().await {
                Ok(r) => r.json().await.unwrap_or_default(),
                Err(_) => vec![],
            };

            let journal: Option<serde_json::Value> = match client.get(&format!("{}/journal/stats", api_base())).send().await {
                Ok(r) => r.json().await.ok(),
                Err(_) => None,
            };

            stats.set(Some(serde_json::json!({
                "datasets": datasets,
                "total_rows": row_count,
                "cache": cache,
                "vector_indexes": indexes.len(),
                "vector_total_chunks": indexes.iter().filter_map(|i| i.get("chunk_count")?.as_i64()).sum::<i64>(),
                "hnsw_loaded": hnsw.len(),
                "tools": tools.len(),
                "jobs_total": jobs.len(),
                "jobs_running": jobs.iter().filter(|j| j.get("status").and_then(|s| s.as_str()) == Some("running")).count(),
                "journal": journal,
            })));
            loading.set(false);
        });
    };

    // Auto-load on mount
    // Load on first render (user clicks Dashboard tab)
    let mut loaded = use_signal(|| false);
    use_effect(move || {
        if !*loaded.read() {
            loaded.set(true);
            do_load();
        }
    });

    rsx! {
        div { class: "panel",
            div { class: "dashboard-hero",
                h2 { "Lakehouse" }
                p { class: "subtitle", "Rust-first data platform — SQL + AI + RAG over object storage" }
            }

            if let Some(s) = stats.read().as_ref() {
                div { class: "stat-grid",
                    div { class: "stat-card",
                        div { class: "stat-value", "{s[\"datasets\"]}" }
                        div { class: "stat-label", "Datasets" }
                    }
                    div { class: "stat-card",
                        div { class: "stat-value", {format!("{:.1}M", s["total_rows"].as_i64().unwrap_or(0) as f64 / 1_000_000.0)} }
                        div { class: "stat-label", "Total Rows" }
                    }
                    div { class: "stat-card",
                        div { class: "stat-value", {format!("{}K", s["vector_total_chunks"].as_i64().unwrap_or(0) / 1000)} }
                        div { class: "stat-label", "Embeddings" }
                    }
                    div { class: "stat-card accent",
                        div { class: "stat-value", "{s[\"hnsw_loaded\"]}" }
                        div { class: "stat-label", "HNSW Indexes Loaded" }
                    }
                    div { class: "stat-card",
                        div { class: "stat-value", "{s[\"tools\"]}" }
                        div { class: "stat-label", "Governed Tools" }
                    }
                    div { class: "stat-card",
                        div { class: "stat-value", {
                            s.get("cache")
                                .and_then(|c| c.get("datasets"))
                                .and_then(|d| d.as_i64())
                                .map(|n| format!("{n}"))
                                .unwrap_or("0".into())
                        } }
                        div { class: "stat-label", "Cached Datasets" }
                    }
                }

                div { class: "architecture-section",
                    h3 { "Architecture" }
                    div { class: "arch-grid",
                        div { class: "arch-card",
                            div { class: "arch-title", "Ingest" }
                            div { class: "arch-items", "CSV · JSON · PDF (+OCR) · Text · Postgres · MySQL · Inbox watcher · Cron schedules" }
                        }
                        div { class: "arch-card",
                            div { class: "arch-title", "Storage" }
                            div { class: "arch-items", "Parquet on Object Storage · Delta writes · Compaction · Tombstones · Multi-bucket federation + rescue" }
                        }
                        div { class: "arch-card",
                            div { class: "arch-title", "Query" }
                            div { class: "arch-items", "DataFusion SQL · MemCache (9.8× hot) · Merge-on-read · AI-safe views" }
                        }
                        div { class: "arch-card",
                            div { class: "arch-title", "AI / Vector" }
                            div { class: "arch-items", "Ollama (local) · Embed/Generate/RAG · HNSW (Parquet) · Lance IVF_PQ · Hybrid SQL+vector · Profile-scoped" }
                        }
                        div { class: "arch-card",
                            div { class: "arch-title", "Learning loop" }
                            div { class: "arch-items", "Playbook memory · Endorsement boost · Multi-agent orchestrator · Autotune agent (Pareto-promote)" }
                        }
                        div { class: "arch-card",
                            div { class: "arch-title", "Governance" }
                            div { class: "arch-items", "Event journal · PII detection · Tool registry · Access control · Audit log · Catalog v2 metadata" }
                        }
                    }
                }

                div { class: "phases-section",
                    h3 { "Build Progression" }
                    div { class: "phase-list",
                        {rsx! {
                            PhaseItem { num: "0-5", name: "Foundation", detail: "Storage · Catalog · DataFusion · Ollama · UI · gRPC" }
                            PhaseItem { num: "6", name: "Ingest Pipeline", detail: "CSV · JSON · PDF · Text · auto-schema · dedupe" }
                            PhaseItem { num: "7", name: "Vector + RAG", detail: "Embed · brute-force cosine · LLM grounded answers" }
                            PhaseItem { num: "8", name: "Hot Cache + Deltas", detail: "MemTable LRU · 9.8× speedup · merge-on-read · compaction" }
                            PhaseItem { num: "8.5", name: "Agent Workspaces", detail: "Per-contract · daily/weekly/monthly tiers · zero-copy handoff" }
                            PhaseItem { num: "9", name: "Event Journal", detail: "Append-only mutation log · time-travel · audit" }
                            PhaseItem { num: "10", name: "Rich Catalog v2", detail: "PII auto-detection · lineage · freshness SLA · sensitivity" }
                            PhaseItem { num: "11", name: "Embedding Versioning", detail: "Per-index model+version · A/B · incremental re-embed" }
                            PhaseItem { num: "12", name: "Tool Registry", detail: "Governed actions · param validation · audit · MCP-ready" }
                            PhaseItem { num: "13", name: "Access Control", detail: "Roles · field-level sensitivity · column masking · query audit" }
                            PhaseItem { num: "14", name: "Schema Evolution", detail: "Diff detection · AI migration prompts · versioned schemas" }
                            PhaseItem { num: "15", name: "HNSW + Trials", detail: "100K vectors · p50 873µs · trial journal · eval harness" }
                            PhaseItem { num: "16", name: "Hot-swap + Autotune", detail: "Promotion registry · rollback · ε-greedy agent · Pareto winner" }
                            PhaseItem { num: "17", name: "Model Profiles + VRAM", detail: "ModelProfile manifests · scoped search · sequential model swap" }
                            PhaseItem { num: "18", name: "Lance hybrid backend", detail: "IVF_PQ build 14× faster · random fetch 112× · S3-native · per-profile routing" }
                            PhaseItem { num: "19", name: "Playbook memory", detail: "Feedback loop · endorsement boost (cap 0.25) · orchestrator write-through · citations" }
                            PhaseItem { num: "+", name: "Federation + Schedules", detail: "Multi-bucket · rescue fallback · error journal · MySQL · PDF OCR · cron ingest · catalog dedupe" }
                        }}
                    }
                }
            } else if *loading.read() {
                div { class: "loading", "loading dashboard..." }
            }

            button { class: "btn", onclick: move |_| do_load(), "Refresh" }
        }
    }
}

#[component]
fn PhaseItem(num: String, name: String, detail: String) -> Element {
    rsx! {
        div { class: "phase-item",
            span { class: "phase-num", "{num}" }
            span { class: "phase-name", "{name}" }
            span { class: "phase-detail", "{detail}" }
        }
    }
}

// === PLAYBOOK — Phase 19 meta-index feedback loop ===

#[component]
fn PlaybookPanel() -> Element {
    let mut stats = use_signal(|| None::<Result<PlaybookStats, String>>);
    let mut indexes = use_signal(Vec::<IndexInfo>::new);
    let mut rebuild_status = use_signal(|| None::<Result<String, String>>);
    let mut rebuilding = use_signal(|| false);
    let mut loaded = use_signal(|| false);

    // Comparison state
    let mut selected_index = use_signal(|| String::new());
    let mut question = use_signal(|| String::from("reliable assembler in Detroit"));
    let mut top_k = use_signal(|| 10usize);
    let mut compare_loading = use_signal(|| false);
    let mut hits_off = use_signal(|| None::<Result<HybridResp, String>>);
    let mut hits_on = use_signal(|| None::<Result<HybridResp, String>>);

    let load_all = move || {
        spawn(async move {
            stats.set(Some(fetch_playbook_stats().await));
            if let Ok(ix) = fetch_indexes().await {
                if selected_index.read().is_empty() {
                    if let Some(default) = ix.iter().find(|i| i.source == "workers_500k").or_else(|| ix.first()) {
                        selected_index.set(default.index_name.clone());
                    }
                }
                indexes.set(ix);
            }
        });
    };

    use_effect(move || {
        if !*loaded.read() {
            loaded.set(true);
            load_all();
        }
    });

    let do_rebuild = move |_| {
        spawn(async move {
            rebuilding.set(true);
            rebuild_status.set(None);
            match rebuild_playbook_memory().await {
                Ok(v) => rebuild_status.set(Some(Ok(format!("rebuild ok — {}", v)))),
                Err(e) => rebuild_status.set(Some(Err(e))),
            }
            // Refresh stats afterward
            stats.set(Some(fetch_playbook_stats().await));
            rebuilding.set(false);
        });
    };

    let do_compare = move |_| {
        let idx = selected_index.read().clone();
        let q = question.read().clone();
        let k = *top_k.read();
        if idx.is_empty() || q.trim().is_empty() { return; }
        spawn(async move {
            compare_loading.set(true);
            hits_off.set(None);
            hits_on.set(None);
            // Run both sequentially so the embedding cache is shared
            hits_off.set(Some(hybrid_search(&idx, &q, false, k).await));
            hits_on.set(Some(hybrid_search(&idx, &q, true, k).await));
            compare_loading.set(false);
        });
    };

    rsx! {
        div { class: "panel",
            div { class: "ask-hero",
                h2 { "Playbook Memory" }
                p { class: "subtitle",
                    "Phase 19 feedback loop: past successful playbooks boost future search rankings. \
                     Endorsed workers from semantically similar past operations re-rank toward the top, \
                     with citations back to the playbook that endorsed them."
                }
            }

            // Stats card
            div { class: "panel-section",
                match stats.read().as_ref() {
                    None => rsx! { div { class: "loading", "loading playbook stats..." } },
                    Some(Err(e)) => rsx! { div { class: "error", "stats: {e}" } },
                    Some(Ok(s)) => rsx! {
                        div { class: "stat-grid",
                            div { class: "stat-card",
                                div { class: "stat-value", "{s.entries}" }
                                div { class: "stat-label", "Playbooks in Memory" }
                            }
                            div { class: "stat-card",
                                div { class: "stat-value", "{s.entries_with_embeddings}" }
                                div { class: "stat-label", "Embedded" }
                            }
                            div { class: "stat-card accent",
                                div { class: "stat-value", "{s.total_names_endorsed}" }
                                div { class: "stat-label", "Endorsed Worker-Tags" }
                            }
                        }
                    }
                }
                div { class: "sql-actions",
                    button {
                        class: "btn",
                        disabled: *rebuilding.read(),
                        onclick: do_rebuild,
                        if *rebuilding.read() { "rebuilding from successful_playbooks..." } else { "Rebuild from successful_playbooks" }
                    }
                }
                if let Some(s) = rebuild_status.read().as_ref() {
                    match s {
                        Ok(msg) => rsx! { div { class: "result-box", "{msg}" } },
                        Err(e) => rsx! { div { class: "error", "{e}" } },
                    }
                }
            }

            // Sample playbooks
            if let Some(Ok(s)) = stats.read().as_ref() {
                if !s.sample.is_empty() {
                    div { class: "panel-section",
                        h3 { "Sample playbooks" }
                        div { class: "table-wrap",
                            table {
                                thead { tr {
                                    th { "ID" }
                                    th { "Operation" }
                                    th { "Location" }
                                    th { "Endorsed" }
                                } }
                                tbody {
                                    for pb in s.sample.iter() {
                                        {
                                            let loc = match (&pb.city, &pb.state) {
                                                (Some(c), Some(st)) => format!("{c}, {st}"),
                                                _ => "—".into(),
                                            };
                                            let endorsed = if pb.endorsed.is_empty() {
                                                "—".to_string()
                                            } else {
                                                pb.endorsed.join(", ")
                                            };
                                            let pid = pb.id.clone();
                                            let op = pb.operation.clone();
                                            rsx! {
                                                tr {
                                                    td { class: "mono-cell", title: "{pid}", "{pid}" }
                                                    td { "{op}" }
                                                    td { "{loc}" }
                                                    td { "{endorsed}" }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            // Side-by-side comparison: boost OFF vs ON
            div { class: "panel-section",
                h3 { "See the boost — search compared" }
                p { class: "hint",
                    "Run the same query against the same index twice — once with playbook boost OFF and once ON. \
                     Hits with non-zero playbook_boost and citations are workers that past similar playbooks endorsed."
                }
                div { class: "form-row",
                    label { "Index" }
                    select {
                        value: "{selected_index}",
                        onchange: move |e| selected_index.set(e.value()),
                        for ix in indexes.read().iter() {
                            option { value: "{ix.index_name}", "{ix.index_name} ({ix.source}, {ix.chunk_count} chunks, {ix.vector_backend})" }
                        }
                    }
                }
                div { class: "form-row",
                    label { "Question" }
                    input {
                        value: "{question}",
                        oninput: move |e| question.set(e.value()),
                        placeholder: "e.g. reliable assembler in Detroit"
                    }
                }
                div { class: "form-row",
                    label { "Top K" }
                    input {
                        r#type: "number",
                        value: "{top_k}",
                        oninput: move |e| {
                            if let Ok(n) = e.value().parse::<usize>() { top_k.set(n.clamp(1, 50)); }
                        }
                    }
                }
                button {
                    class: "btn btn-ask",
                    disabled: *compare_loading.read(),
                    onclick: do_compare,
                    if *compare_loading.read() { "running both queries..." } else { "Run comparison" }
                }

                div { class: "explore-grid",
                    div { class: "ds-detail",
                        h3 { "Boost OFF (vanilla)" }
                        match hits_off.read().as_ref() {
                            None => rsx! { div { class: "empty", "—" } },
                            Some(Err(e)) => rsx! { div { class: "error", "{e}" } },
                            Some(Ok(r)) => rsx! { HybridHitTable { resp: r.clone() } },
                        }
                    }
                    div { class: "ds-detail",
                        h3 { "Boost ON (Phase 19)" }
                        match hits_on.read().as_ref() {
                            None => rsx! { div { class: "empty", "—" } },
                            Some(Err(e)) => rsx! { div { class: "error", "{e}" } },
                            Some(Ok(r)) => rsx! { HybridHitTable { resp: r.clone() } },
                        }
                    }
                }
            }
        }
    }
}

#[component]
fn HybridHitTable(resp: HybridResp) -> Element {
    rsx! {
        div { class: "results-info",
            "{resp.sources.len()} hits · {resp.duration_ms}ms · method={resp.method}"
        }
        if resp.sources.is_empty() {
            div { class: "empty-sm", "no hits" }
        } else {
            div { class: "table-wrap",
                table {
                    thead { tr {
                        th { "#" }
                        th { "Doc" }
                        th { "Score" }
                        th { "Boost" }
                        th { "Citations" }
                        th { "Snippet" }
                    } }
                    tbody {
                        for (i, h) in resp.sources.iter().enumerate() {
                            {
                                let snippet: String = h.chunk_text.chars().take(120).collect();
                                let cites = if h.playbook_citations.is_empty() {
                                    "—".to_string()
                                } else {
                                    h.playbook_citations.join(", ")
                                };
                                let row_class = if h.playbook_boost > 0.0 { "boosted-row" } else { "" };
                                let rank = i + 1;
                                let did = h.doc_id.clone();
                                let score = format!("{:.3}", h.score);
                                let boost = if h.playbook_boost > 0.0 { format!("+{:.3}", h.playbook_boost) } else { "—".into() };
                                rsx! {
                                    tr { class: "{row_class}",
                                        td { "{rank}" }
                                        td { class: "mono-cell", "{did}" }
                                        td { "{score}" }
                                        td { "{boost}" }
                                        td { class: "mono-cell", title: "{cites}", "{cites}" }
                                        td { "{snippet}" }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

// === INGEST — Data on-ramp ===

#[component]
fn IngestPanel() -> Element {
    let mut pg_host = use_signal(|| "127.0.0.1".to_string());
    let mut pg_db = use_signal(|| "knowledge_base".to_string());
    let mut pg_tables = use_signal(|| None::<Vec<String>>);
    let mut pg_result = use_signal(|| None::<Result<serde_json::Value, String>>);
    let mut pg_loading = use_signal(|| false);

    let list_tables = move |_| {
        let host = pg_host.read().clone();
        let db = pg_db.read().clone();
        spawn(async move {
            pg_loading.set(true);
            let client = reqwest::Client::new();
            let resp = client.post(&format!("{}/ingest/postgres/tables", api_base()))
                .json(&serde_json::json!({"host": host, "port": 5432, "database": db, "user": "postgres", "password": ""}))
                .send().await;
            match resp {
                Ok(r) => {
                    if let Ok(tables) = r.json::<Vec<String>>().await {
                        pg_tables.set(Some(tables));
                    }
                }
                Err(_) => pg_tables.set(None),
            }
            pg_loading.set(false);
        });
    };

    let import_table = move |table: String| {
        let host = pg_host.read().clone();
        let db = pg_db.read().clone();
        spawn(async move {
            pg_result.set(None);
            let client = reqwest::Client::new();
            let resp = client.post(&format!("{}/ingest/postgres/import", api_base()))
                .json(&serde_json::json!({"host": host, "port": 5432, "database": db, "user": "postgres", "password": "", "table": table}))
                .send().await;
            match resp {
                Ok(r) => {
                    match r.json::<serde_json::Value>().await {
                        Ok(v) => pg_result.set(Some(Ok(v))),
                        Err(e) => pg_result.set(Some(Err(e.to_string()))),
                    }
                }
                Err(e) => pg_result.set(Some(Err(e.to_string()))),
            }
        });
    };

    rsx! {
        div { class: "panel",
            h2 { "Data Ingest" }
            p { class: "subtitle", "Bring data in from files, databases, or the auto-watch inbox" }

            div { class: "panel-section",
                h3 { "File Upload" }
                p { class: "hint", "POST a file to /ingest/file — or drop it in ./inbox/ for auto-ingest" }
                div { class: "arch-card",
                    div { class: "arch-title", "Supported Formats" }
                    div { class: "arch-items", "CSV (auto-schema) | JSON (nested flattening) | PDF (text extraction) | Text/SMS" }
                }
                div { class: "arch-card",
                    div { class: "arch-title", "Auto-Watch Inbox" }
                    div { class: "arch-items", "Drop files in ./inbox/ → auto-detected → Parquet → queryable in <15s" }
                }
            }

            div { class: "panel-section",
                h3 { "PostgreSQL Import" }
                div { class: "form-row",
                    label { "Host" }
                    input { value: "{pg_host}", oninput: move |e| pg_host.set(e.value()) }
                }
                div { class: "form-row",
                    label { "Database" }
                    input { value: "{pg_db}", oninput: move |e| pg_db.set(e.value()) }
                }
                button { class: "btn", disabled: *pg_loading.read(), onclick: list_tables, "List Tables" }

                if let Some(tables) = pg_tables.read().as_ref() {
                    div { class: "table-list",
                        for table in tables.iter() {
                            {
                                let t = table.clone();
                                let t2 = table.clone();
                                rsx! {
                                    div { class: "table-item",
                                        span { "{t}" }
                                        button { class: "btn btn-sm", onclick: move |_| import_table(t2.clone()), "Import" }
                                    }
                                }
                            }
                        }
                    }
                }

                if let Some(result) = pg_result.read().as_ref() {
                    match result {
                        Ok(v) => rsx! {
                            div { class: "result-box",
                                pre { {serde_json::to_string_pretty(v).unwrap_or_default()} }
                            }
                        },
                        Err(e) => rsx! { div { class: "error", "{e}" } },
                    }
                }
            }
        }
    }
}

// === STATUS — System health ===

#[component]
fn StatusPanel() -> Element {
    let mut results = use_signal(Vec::<(String, String, Result<String, String>)>::new);
    let mut checking = use_signal(|| false);

    let run_checks = move |_| {
        spawn(async move {
            checking.set(true);
            results.set(vec![]);
            let mut checks = vec![];

            let r = fetch_health("/health").await;
            checks.push(("Gateway".into(), "HTTP ingress".into(), r));

            let r = fetch_health("/storage/health").await;
            checks.push(("Storage".into(), "Object store (Parquet files)".into(), r));

            let r = fetch_health("/catalog/health").await;
            checks.push(("Catalog".into(), "Dataset registry".into(), r));

            let r = fetch_health("/query/health").await;
            checks.push(("Query Engine".into(), "DataFusion SQL".into(), r));

            let r = fetch_health("/ai/health").await;
            checks.push(("AI Bridge".into(), "Ollama sidecar".into(), r));

            let r = fetch_datasets().await.map(|ds| format!("{} datasets registered", ds.len()));
            checks.push(("Catalog Data".into(), "Dataset count".into(), r));

            let r = run_sql("SELECT 1 + 1 as result").await
                .map(|q| format!("1+1 = {} ({} row)", q.rows.as_array().and_then(|a| a.first()).and_then(|r| r.get("result")).map(|v| v.to_string()).unwrap_or("?".into()), q.row_count));
            checks.push(("SQL Execution".into(), "DataFusion compute".into(), r));

            let r = ai_embed(vec!["health check".into()]).await
                .map(|e| format!("{}d vector from {}", e.dimensions, e.model));
            checks.push(("Embeddings".into(), "nomic-embed-text via Ollama".into(), r));

            let r = ai_generate("Say OK", 8).await
                .map(|g| format!("\"{}\" via {}", g.text.trim(), g.model));
            checks.push(("Generation".into(), "LLM via Ollama".into(), r));

            results.set(checks);
            checking.set(false);
        });
    };

    rsx! {
        div { class: "panel",
            div { class: "status-hero",
                h2 { "System Status" }
                p { class: "subtitle", "Verify every layer: Rust gateway → object storage → DataFusion → Ollama" }
                button {
                    class: "btn btn-ask",
                    disabled: *checking.read(),
                    onclick: run_checks,
                    if *checking.read() { "checking..." } else { "Run All Checks" }
                }
            }

            if !results.read().is_empty() {
                div { class: "check-grid",
                    for (name, desc, result) in results.read().iter() {
                        {
                            let name = name.clone();
                            let desc = desc.clone();
                            let (class, icon, text) = match result {
                                Ok(msg) => ("check-card pass", "✓", msg.clone()),
                                Err(e) => ("check-card fail", "✗", e.clone()),
                            };
                            rsx! {
                                div { class: "{class}",
                                    div { class: "check-icon", "{icon}" }
                                    div { class: "check-info",
                                        div { class: "check-name", "{name}" }
                                        div { class: "check-desc", "{desc}" }
                                        div { class: "check-msg", "{text}" }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

// === Results Table ===

#[component]
fn ResultsTable(response: QueryResponse) -> Element {
    let rows = response.rows.as_array();
    rsx! {
        div { class: "results-info",
            "{response.row_count} row(s) · {response.columns.len()} column(s)"
        }
        if response.row_count == 0 {
            div { class: "empty-sm", "no rows returned" }
        } else if let Some(rows) = rows {
            if rows.len() > 500 {
                div { class: "results-info", "Showing first 500 of {response.row_count} rows (use SQL tab with LIMIT for larger)" }
            }
            div { class: "table-wrap",
                table {
                    thead {
                        tr {
                            for col in response.columns.iter() {
                                th { title: "{col.data_type}", "{col.name}" }
                            }
                        }
                    }
                    tbody {
                        for row in rows.iter().take(500) {
                            tr {
                                for col in response.columns.iter() {
                                    td { {format_cell(row.get(&col.name))} }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

/// Clean AI-generated SQL: extract only the SQL query, strip everything else.
fn clean_sql(raw: &str) -> String {
    let s = raw.trim();

    // Strategy 1: If there's a ```sql...``` block, extract just that
    if let Some(start) = s.find("```sql") {
        let after = &s[start + 6..];
        if let Some(end) = after.find("```") {
            return after[..end].trim().to_string();
        }
    }
    if let Some(start) = s.find("```") {
        let after = &s[start + 3..];
        if let Some(end) = after.find("```") {
            let inner = after[..end].trim();
            // Skip leading "sql" keyword
            let inner = inner.strip_prefix("sql").map(|s| s.trim_start()).unwrap_or(inner);
            return inner.to_string();
        }
    }

    // Strategy 2: Find the first SELECT/WITH/INSERT/UPDATE/DELETE statement
    let upper = s.to_uppercase();
    for keyword in &["SELECT", "WITH", "INSERT", "UPDATE", "DELETE"] {
        if let Some(pos) = upper.find(keyword) {
            let sql_part = &s[pos..];
            // Take up to the first semicolon (or end)
            let end = sql_part.find(';').map(|p| p + 1).unwrap_or(sql_part.len());
            return sql_part[..end].trim().to_string();
        }
    }

    // Strategy 3: Strip leading "sql" and clean up
    let mut result = s.to_string();
    let lines: Vec<&str> = result.lines().collect();
    if let Some(first) = lines.first() {
        if first.trim().eq_ignore_ascii_case("sql") {
            result = lines[1..].join("\n").trim().to_string();
        }
    }
    result
}

fn format_cell(val: Option<&serde_json::Value>) -> String {
    match val {
        None | Some(serde_json::Value::Null) => "—".to_string(),
        Some(serde_json::Value::String(s)) => s.clone(),
        Some(serde_json::Value::Number(n)) => n.to_string(),
        Some(serde_json::Value::Bool(b)) => b.to_string(),
        Some(other) => other.to_string(),
    }
}
