use crate::error::{TuskError, TuskResult};
use crate::models::schema::{
    ColumnDetail, ColumnInfo, ConstraintInfo, IndexInfo, SchemaObject, TriggerInfo,
};
use crate::state::{AppState, DbFlavor};
use serde_json::Value;
use sqlx::Row;
use std::collections::HashMap;
use std::sync::Arc;
use tauri::State;

fn ch_string_literal(s: &str) -> String {
    let escaped = s.replace('\\', "\\\\").replace('\'', "\\'");
    format!("'{}'", escaped)
}

fn ch_obj_string(obj: &serde_json::Map<String, Value>, key: &str) -> Option<String> {
    obj.get(key).and_then(|v| match v {
        Value::String(s) => Some(s.clone()),
        Value::Number(n) => Some(n.to_string()),
        _ => None,
    })
}

fn ch_obj_i64(obj: &serde_json::Map<String, Value>, key: &str) -> Option<i64> {
    obj.get(key).and_then(|v| match v {
        Value::Number(n) => n.as_i64(),
        Value::String(s) => s.parse::<i64>().ok(),
        _ => None,
    })
}

pub async fn list_databases_core(state: &AppState, connection_id: &str) -> TuskResult<Vec<String>> {
    let flavor = state.get_flavor(connection_id).await;
    if matches!(flavor, DbFlavor::ClickHouse) {
        let client = state.get_ch_client(connection_id).await?;
        let rows = client
            .fetch_objects(
                "SELECT name FROM system.databases \
                 WHERE name NOT IN ('system','INFORMATION_SCHEMA','information_schema') \
                 ORDER BY name",
            )
            .await?;
        return Ok(rows
            .iter()
            .filter_map(|o| ch_obj_string(o, "name"))
            .collect());
    }

    let pool = state.get_pool(connection_id).await?;

    let rows = sqlx::query(
        "SELECT datname FROM pg_database \
         WHERE datistemplate = false \
         ORDER BY datname",
    )
    .fetch_all(&pool)
    .await
    .map_err(TuskError::Database)?;

    Ok(rows.iter().map(|r| r.get::<String, _>(0)).collect())
}

#[tauri::command]
pub async fn list_databases(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
) -> TuskResult<Vec<String>> {
    list_databases_core(&state, &connection_id).await
}

pub async fn list_schemas_core(state: &AppState, connection_id: &str) -> TuskResult<Vec<String>> {
    let flavor = state.get_flavor(connection_id).await;
    if matches!(flavor, DbFlavor::ClickHouse) {
        // ClickHouse has no schema layer — surface the active database as a virtual schema.
        let client = state.get_ch_client(connection_id).await?;
        return Ok(vec![client.database.clone()]);
    }

    let pool = state.get_pool(connection_id).await?;

    let sql = if flavor == DbFlavor::Greenplum {
        "SELECT schema_name FROM information_schema.schemata \
         WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'gp_toolkit') \
         ORDER BY schema_name"
    } else {
        "SELECT schema_name FROM information_schema.schemata \
         WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast') \
         ORDER BY schema_name"
    };

    let rows = sqlx::query(sql)
        .fetch_all(&pool)
        .await
        .map_err(TuskError::Database)?;

    Ok(rows.iter().map(|r| r.get::<String, _>(0)).collect())
}

#[tauri::command]
pub async fn list_schemas(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
) -> TuskResult<Vec<String>> {
    list_schemas_core(&state, &connection_id).await
}

pub async fn list_tables_core(
    state: &AppState,
    connection_id: &str,
    schema: &str,
) -> TuskResult<Vec<SchemaObject>> {
    let flavor = state.get_flavor(connection_id).await;
    if matches!(flavor, DbFlavor::ClickHouse) {
        let client = state.get_ch_client(connection_id).await?;
        let escaped = ch_string_literal(schema);
        let sql = format!(
            "SELECT name, total_rows, total_bytes FROM system.tables \
             WHERE database = {} AND engine NOT LIKE '%View' \
             ORDER BY name",
            escaped
        );
        let rows = client.fetch_objects(&sql).await?;
        return Ok(rows
            .iter()
            .map(|o| SchemaObject {
                name: ch_obj_string(o, "name").unwrap_or_default(),
                object_type: "table".to_string(),
                schema: schema.to_string(),
                row_count: ch_obj_i64(o, "total_rows"),
                size_bytes: ch_obj_i64(o, "total_bytes"),
            })
            .collect());
    }

    let pool = state.get_pool(connection_id).await?;

    let rows = sqlx::query(
        "SELECT t.table_name, \
                c.reltuples::bigint as row_count, \
                pg_total_relation_size(c.oid)::bigint as size_bytes \
         FROM information_schema.tables t \
         LEFT JOIN pg_class c ON c.relname = t.table_name \
            AND c.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = $1) \
         WHERE t.table_schema = $1 AND t.table_type = 'BASE TABLE' \
         ORDER BY t.table_name",
    )
    .bind(schema)
    .fetch_all(&pool)
    .await
    .map_err(TuskError::Database)?;

    Ok(rows
        .iter()
        .map(|r| SchemaObject {
            name: r.get(0),
            object_type: "table".to_string(),
            schema: schema.to_string(),
            row_count: r.get::<Option<i64>, _>(1),
            size_bytes: r.get::<Option<i64>, _>(2),
        })
        .collect())
}

#[tauri::command]
pub async fn list_tables(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
    schema: String,
) -> TuskResult<Vec<SchemaObject>> {
    list_tables_core(&state, &connection_id, &schema).await
}

#[tauri::command]
pub async fn list_views(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
    schema: String,
) -> TuskResult<Vec<SchemaObject>> {
    let flavor = state.get_flavor(&connection_id).await;
    if matches!(flavor, DbFlavor::ClickHouse) {
        let client = state.get_ch_client(&connection_id).await?;
        let sql = format!(
            "SELECT name FROM system.tables \
             WHERE database = {} AND engine LIKE '%View' \
             ORDER BY name",
            ch_string_literal(&schema)
        );
        let rows = client.fetch_objects(&sql).await?;
        return Ok(rows
            .iter()
            .map(|o| SchemaObject {
                name: ch_obj_string(o, "name").unwrap_or_default(),
                object_type: "view".to_string(),
                schema: schema.clone(),
                row_count: None,
                size_bytes: None,
            })
            .collect());
    }

    let pool = state.get_pool(&connection_id).await?;

    let rows = sqlx::query(
        "SELECT table_name FROM information_schema.views \
         WHERE table_schema = $1 \
         ORDER BY table_name",
    )
    .bind(&schema)
    .fetch_all(&pool)
    .await
    .map_err(TuskError::Database)?;

    Ok(rows
        .iter()
        .map(|r| SchemaObject {
            name: r.get(0),
            object_type: "view".to_string(),
            schema: schema.clone(),
            row_count: None,
            size_bytes: None,
        })
        .collect())
}

#[tauri::command]
pub async fn list_functions(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
    schema: String,
) -> TuskResult<Vec<SchemaObject>> {
    let flavor = state.get_flavor(&connection_id).await;
    if matches!(flavor, DbFlavor::ClickHouse) {
        // ClickHouse functions are global, not schema-scoped — surface empty here.
        return Ok(vec![]);
    }
    let pool = state.get_pool(&connection_id).await?;

    let rows = sqlx::query(
        "SELECT routine_name FROM information_schema.routines \
         WHERE routine_schema = $1 AND routine_type = 'FUNCTION' \
         ORDER BY routine_name",
    )
    .bind(&schema)
    .fetch_all(&pool)
    .await
    .map_err(TuskError::Database)?;

    Ok(rows
        .iter()
        .map(|r| SchemaObject {
            name: r.get(0),
            object_type: "function".to_string(),
            schema: schema.clone(),
            row_count: None,
            size_bytes: None,
        })
        .collect())
}

#[tauri::command]
pub async fn list_indexes(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
    schema: String,
) -> TuskResult<Vec<SchemaObject>> {
    let flavor = state.get_flavor(&connection_id).await;
    if matches!(flavor, DbFlavor::ClickHouse) {
        return Ok(vec![]);
    }
    let pool = state.get_pool(&connection_id).await?;

    let rows = sqlx::query(
        "SELECT indexname FROM pg_indexes \
         WHERE schemaname = $1 \
         ORDER BY indexname",
    )
    .bind(&schema)
    .fetch_all(&pool)
    .await
    .map_err(TuskError::Database)?;

    Ok(rows
        .iter()
        .map(|r| SchemaObject {
            name: r.get(0),
            object_type: "index".to_string(),
            schema: schema.clone(),
            row_count: None,
            size_bytes: None,
        })
        .collect())
}

#[tauri::command]
pub async fn list_sequences(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
    schema: String,
) -> TuskResult<Vec<SchemaObject>> {
    let flavor = state.get_flavor(&connection_id).await;
    if matches!(flavor, DbFlavor::ClickHouse) {
        return Ok(vec![]);
    }
    let pool = state.get_pool(&connection_id).await?;

    let rows = sqlx::query(
        "SELECT sequence_name FROM information_schema.sequences \
         WHERE sequence_schema = $1 \
         ORDER BY sequence_name",
    )
    .bind(&schema)
    .fetch_all(&pool)
    .await
    .map_err(TuskError::Database)?;

    Ok(rows
        .iter()
        .map(|r| SchemaObject {
            name: r.get(0),
            object_type: "sequence".to_string(),
            schema: schema.clone(),
            row_count: None,
            size_bytes: None,
        })
        .collect())
}

pub async fn get_table_columns_core(
    state: &AppState,
    connection_id: &str,
    schema: &str,
    table: &str,
) -> TuskResult<Vec<ColumnInfo>> {
    let flavor = state.get_flavor(connection_id).await;
    if matches!(flavor, DbFlavor::ClickHouse) {
        let client = state.get_ch_client(connection_id).await?;
        let sql = format!(
            "SELECT name, type, default_expression, is_in_primary_key, comment, position \
             FROM system.columns WHERE database = {} AND table = {} \
             ORDER BY position",
            ch_string_literal(schema),
            ch_string_literal(table)
        );
        let rows = client.fetch_objects(&sql).await?;
        return Ok(rows
            .iter()
            .map(|o| {
                let type_str = ch_obj_string(o, "type").unwrap_or_default();
                let is_nullable = type_str.starts_with("Nullable(");
                ColumnInfo {
                    name: ch_obj_string(o, "name").unwrap_or_default(),
                    data_type: type_str,
                    is_nullable,
                    column_default: ch_obj_string(o, "default_expression"),
                    ordinal_position: ch_obj_i64(o, "position").unwrap_or(0) as i32,
                    character_maximum_length: None,
                    is_primary_key: ch_obj_i64(o, "is_in_primary_key").unwrap_or(0) != 0,
                    comment: ch_obj_string(o, "comment"),
                }
            })
            .collect());
    }

    let pool = state.get_pool(connection_id).await?;

    let rows = sqlx::query(
        "SELECT \
            c.column_name, \
            c.data_type, \
            c.is_nullable, \
            c.column_default, \
            c.ordinal_position::int, \
            c.character_maximum_length::int, \
            COALESCE(( \
                SELECT true FROM information_schema.table_constraints tc \
                JOIN information_schema.key_column_usage kcu \
                    ON tc.constraint_name = kcu.constraint_name \
                    AND tc.table_schema = kcu.table_schema \
                WHERE tc.constraint_type = 'PRIMARY KEY' \
                    AND tc.table_schema = $1 \
                    AND tc.table_name = $2 \
                    AND kcu.column_name = c.column_name \
                LIMIT 1 \
            ), false) as is_pk, \
            col_description( \
                (SELECT oid FROM pg_class \
                 JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace \
                 WHERE relname = $2 AND nspname = $1), \
                c.ordinal_position \
            ) as col_comment \
         FROM information_schema.columns c \
         WHERE c.table_schema = $1 AND c.table_name = $2 \
         ORDER BY c.ordinal_position",
    )
    .bind(schema)
    .bind(table)
    .fetch_all(&pool)
    .await
    .map_err(TuskError::Database)?;

    Ok(rows
        .iter()
        .map(|r| ColumnInfo {
            name: r.get::<String, _>(0),
            data_type: r.get::<String, _>(1),
            is_nullable: r.get::<String, _>(2) == "YES",
            column_default: r.get::<Option<String>, _>(3),
            ordinal_position: r.get::<i32, _>(4),
            character_maximum_length: r.get::<Option<i32>, _>(5),
            is_primary_key: r.get::<bool, _>(6),
            comment: r.get::<Option<String>, _>(7),
        })
        .collect())
}

#[tauri::command]
pub async fn get_table_columns(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
    schema: String,
    table: String,
) -> TuskResult<Vec<ColumnInfo>> {
    get_table_columns_core(&state, &connection_id, &schema, &table).await
}

#[tauri::command]
pub async fn get_table_constraints(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
    schema: String,
    table: String,
) -> TuskResult<Vec<ConstraintInfo>> {
    let flavor = state.get_flavor(&connection_id).await;
    if matches!(flavor, DbFlavor::ClickHouse) {
        return Ok(vec![]);
    }
    let pool = state.get_pool(&connection_id).await?;

    let rows = sqlx::query(
        "SELECT \
            c.conname AS constraint_name, \
            CASE c.contype \
                WHEN 'p' THEN 'PRIMARY KEY' \
                WHEN 'f' THEN 'FOREIGN KEY' \
                WHEN 'u' THEN 'UNIQUE' \
                WHEN 'c' THEN 'CHECK' \
                WHEN 'x' THEN 'EXCLUDE' \
            END AS constraint_type, \
            ARRAY( \
                SELECT a.attname FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord) \
                JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum \
                ORDER BY k.ord \
            )::text[] AS columns, \
            ref_ns.nspname AS referenced_schema, \
            ref_cl.relname AS referenced_table, \
            CASE WHEN c.confrelid > 0 THEN ARRAY( \
                SELECT a.attname FROM unnest(c.confkey) WITH ORDINALITY AS k(attnum, ord) \
                JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.attnum \
                ORDER BY k.ord \
            )::text[] ELSE NULL END AS referenced_columns, \
            CASE c.confupdtype \
                WHEN 'a' THEN 'NO ACTION' \
                WHEN 'r' THEN 'RESTRICT' \
                WHEN 'c' THEN 'CASCADE' \
                WHEN 'n' THEN 'SET NULL' \
                WHEN 'd' THEN 'SET DEFAULT' \
                ELSE NULL \
            END AS update_rule, \
            CASE c.confdeltype \
                WHEN 'a' THEN 'NO ACTION' \
                WHEN 'r' THEN 'RESTRICT' \
                WHEN 'c' THEN 'CASCADE' \
                WHEN 'n' THEN 'SET NULL' \
                WHEN 'd' THEN 'SET DEFAULT' \
                ELSE NULL \
            END AS delete_rule \
         FROM pg_constraint c \
         JOIN pg_class cl ON cl.oid = c.conrelid \
         JOIN pg_namespace ns ON ns.oid = cl.relnamespace \
         LEFT JOIN pg_class ref_cl ON ref_cl.oid = c.confrelid \
         LEFT JOIN pg_namespace ref_ns ON ref_ns.oid = ref_cl.relnamespace \
         WHERE ns.nspname = $1 AND cl.relname = $2 \
         ORDER BY c.contype, c.conname",
    )
    .bind(&schema)
    .bind(&table)
    .fetch_all(&pool)
    .await
    .map_err(TuskError::Database)?;

    Ok(rows
        .iter()
        .map(|r| ConstraintInfo {
            name: r.get::<String, _>(0),
            constraint_type: r.get::<String, _>(1),
            columns: r.get::<Vec<String>, _>(2),
            referenced_schema: r.get::<Option<String>, _>(3),
            referenced_table: r.get::<Option<String>, _>(4),
            referenced_columns: r.get::<Option<Vec<String>>, _>(5),
            update_rule: r.get::<Option<String>, _>(6),
            delete_rule: r.get::<Option<String>, _>(7),
        })
        .collect())
}

#[tauri::command]
pub async fn get_table_indexes(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
    schema: String,
    table: String,
) -> TuskResult<Vec<IndexInfo>> {
    let flavor = state.get_flavor(&connection_id).await;
    if matches!(flavor, DbFlavor::ClickHouse) {
        return Ok(vec![]);
    }
    let pool = state.get_pool(&connection_id).await?;

    let rows = sqlx::query(
        "SELECT \
            i.relname as index_name, \
            pg_get_indexdef(i.oid) as index_def, \
            ix.indisunique as is_unique, \
            ix.indisprimary as is_primary \
         FROM pg_index ix \
         JOIN pg_class i ON i.oid = ix.indexrelid \
         JOIN pg_class t ON t.oid = ix.indrelid \
         JOIN pg_namespace n ON n.oid = t.relnamespace \
         WHERE n.nspname = $1 AND t.relname = $2 \
         ORDER BY i.relname",
    )
    .bind(&schema)
    .bind(&table)
    .fetch_all(&pool)
    .await
    .map_err(TuskError::Database)?;

    Ok(rows
        .iter()
        .map(|r| IndexInfo {
            name: r.get::<String, _>(0),
            definition: r.get::<String, _>(1),
            is_unique: r.get::<bool, _>(2),
            is_primary: r.get::<bool, _>(3),
        })
        .collect())
}

#[tauri::command]
pub async fn get_completion_schema(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
) -> TuskResult<HashMap<String, HashMap<String, Vec<String>>>> {
    let flavor = state.get_flavor(&connection_id).await;
    if matches!(flavor, DbFlavor::ClickHouse) {
        let client = state.get_ch_client(&connection_id).await?;
        let sql = format!(
            "SELECT database, table, name FROM system.columns \
             WHERE database = {} \
             ORDER BY database, table, position",
            ch_string_literal(&client.database)
        );
        let rows = client.fetch_objects(&sql).await?;
        let mut result: HashMap<String, HashMap<String, Vec<String>>> = HashMap::new();
        for row in rows {
            let db = ch_obj_string(&row, "database").unwrap_or_default();
            let table = ch_obj_string(&row, "table").unwrap_or_default();
            let column = ch_obj_string(&row, "name").unwrap_or_default();
            result.entry(db).or_default().entry(table).or_default().push(column);
        }
        return Ok(result);
    }

    let pool = state.get_pool(&connection_id).await?;

    let sql = if flavor == DbFlavor::Greenplum {
        "SELECT table_schema, table_name, column_name \
         FROM information_schema.columns \
         WHERE table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'gp_toolkit') \
         ORDER BY table_schema, table_name, ordinal_position"
    } else {
        "SELECT table_schema, table_name, column_name \
         FROM information_schema.columns \
         WHERE table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast') \
         ORDER BY table_schema, table_name, ordinal_position"
    };

    let rows = sqlx::query(sql)
        .fetch_all(&pool)
        .await
        .map_err(TuskError::Database)?;

    let mut result: HashMap<String, HashMap<String, Vec<String>>> = HashMap::new();
    for row in &rows {
        let schema: String = row.get(0);
        let table: String = row.get(1);
        let column: String = row.get(2);

        result
            .entry(schema)
            .or_default()
            .entry(table)
            .or_default()
            .push(column);
    }

    Ok(result)
}

#[tauri::command]
pub async fn get_column_details(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
    schema: String,
    table: String,
) -> TuskResult<Vec<ColumnDetail>> {
    let flavor = state.get_flavor(&connection_id).await;
    if matches!(flavor, DbFlavor::ClickHouse) {
        let columns = get_table_columns_core(&state, &connection_id, &schema, &table).await?;
        return Ok(columns
            .into_iter()
            .map(|c| ColumnDetail {
                column_name: c.name,
                data_type: c.data_type,
                is_nullable: c.is_nullable,
                column_default: c.column_default,
                is_identity: false,
            })
            .collect());
    }
    let pool = state.get_pool(&connection_id).await?;

    let sql = if flavor == DbFlavor::Greenplum {
        "SELECT c.column_name, c.data_type, \
                c.is_nullable = 'YES' as is_nullable, \
                c.column_default, \
                false as is_identity \
         FROM information_schema.columns c \
         WHERE c.table_schema = $1 AND c.table_name = $2 \
         ORDER BY c.ordinal_position"
    } else {
        "SELECT c.column_name, c.data_type, \
                c.is_nullable = 'YES' as is_nullable, \
                c.column_default, \
                c.is_identity = 'YES' as is_identity \
         FROM information_schema.columns c \
         WHERE c.table_schema = $1 AND c.table_name = $2 \
         ORDER BY c.ordinal_position"
    };

    let rows = sqlx::query(sql)
        .bind(&schema)
        .bind(&table)
        .fetch_all(&pool)
        .await
        .map_err(TuskError::Database)?;

    Ok(rows
        .iter()
        .map(|r| ColumnDetail {
            column_name: r.get::<String, _>(0),
            data_type: r.get::<String, _>(1),
            is_nullable: r.get::<bool, _>(2),
            column_default: r.get::<Option<String>, _>(3),
            is_identity: r.get::<bool, _>(4),
        })
        .collect())
}

#[tauri::command]
pub async fn get_table_triggers(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
    schema: String,
    table: String,
) -> TuskResult<Vec<TriggerInfo>> {
    let flavor = state.get_flavor(&connection_id).await;
    if matches!(flavor, DbFlavor::ClickHouse) {
        return Ok(vec![]);
    }
    let pool = state.get_pool(&connection_id).await?;

    let rows = sqlx::query(
        "SELECT \
            t.tgname AS trigger_name, \
            CASE \
                WHEN t.tgtype::int & 2 = 2 THEN 'BEFORE' \
                WHEN t.tgtype::int & 2 = 0 AND t.tgtype::int & 64 = 64 THEN 'INSTEAD OF' \
                ELSE 'AFTER' \
            END AS timing, \
            array_to_string(ARRAY[ \
                CASE WHEN t.tgtype::int & 4 = 4 THEN 'INSERT' ELSE NULL END, \
                CASE WHEN t.tgtype::int & 8 = 8 THEN 'DELETE' ELSE NULL END, \
                CASE WHEN t.tgtype::int & 16 = 16 THEN 'UPDATE' ELSE NULL END, \
                CASE WHEN t.tgtype::int & 32 = 32 THEN 'TRUNCATE' ELSE NULL END \
            ], ' OR ') AS event, \
            CASE WHEN t.tgtype::int & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END AS orientation, \
            p.proname AS function_name, \
            t.tgenabled != 'D' AS is_enabled, \
            pg_get_triggerdef(t.oid) AS definition \
         FROM pg_trigger t \
         JOIN pg_class c ON c.oid = t.tgrelid \
         JOIN pg_namespace n ON n.oid = c.relnamespace \
         JOIN pg_proc p ON p.oid = t.tgfoid \
         WHERE n.nspname = $1 AND c.relname = $2 AND NOT t.tgisinternal \
         ORDER BY t.tgname",
    )
    .bind(&schema)
    .bind(&table)
    .fetch_all(&pool)
    .await
    .map_err(TuskError::Database)?;

    Ok(rows
        .iter()
        .map(|r| TriggerInfo {
            name: r.get::<String, _>(0),
            timing: r.get::<String, _>(1),
            event: r.get::<String, _>(2),
            orientation: r.get::<String, _>(3),
            function_name: r.get::<String, _>(4),
            is_enabled: r.get::<bool, _>(5),
            definition: r.get::<String, _>(6),
        })
        .collect())
}

