use crate::db::clickhouse::ChClient;
use crate::error::{TuskError, TuskResult};
use crate::models::connection::ConnectionConfig;
use crate::state::{AppState, DbFlavor};
use serde::Serialize;
use sqlx::PgPool;
use sqlx::Row;
use std::fs;
use std::sync::Arc;
use tauri::{AppHandle, Manager, State};

#[derive(Debug, Clone, Serialize)]
pub struct ConnectResult {
    pub version: String,
    pub flavor: DbFlavor,
}

pub(crate) fn get_connections_path(app: &AppHandle) -> TuskResult<std::path::PathBuf> {
    let dir = app
        .path()
        .app_data_dir()
        .map_err(|e| TuskError::Config(e.to_string()))?;
    fs::create_dir_all(&dir)?;
    Ok(dir.join("connections.json"))
}

/// Read all saved connection configs from disk.
pub(crate) fn load_all_connections(app: &AppHandle) -> TuskResult<Vec<ConnectionConfig>> {
    let path = get_connections_path(app)?;
    if !path.exists() {
        return Ok(vec![]);
    }
    let data = fs::read_to_string(&path)?;
    let connections: Vec<ConnectionConfig> = serde_json::from_str(&data)?;
    Ok(connections)
}

/// Look up a single saved connection by id. Used by tools that need credentials
/// (e.g. switch_database from inside the chat agent loop) but only have the id in scope.
pub(crate) fn load_connection_config(
    app: &AppHandle,
    connection_id: &str,
) -> TuskResult<ConnectionConfig> {
    load_all_connections(app)?
        .into_iter()
        .find(|c| c.id == connection_id)
        .ok_or_else(|| {
            TuskError::Custom(format!(
                "Connection '{}' not found in connections.json",
                connection_id
            ))
        })
}

#[tauri::command]
pub async fn get_connections(app: AppHandle) -> TuskResult<Vec<ConnectionConfig>> {
    let path = get_connections_path(&app)?;
    if !path.exists() {
        return Ok(vec![]);
    }
    let data = fs::read_to_string(&path)?;
    let connections: Vec<ConnectionConfig> = serde_json::from_str(&data)?;
    Ok(connections)
}

#[tauri::command]
pub async fn save_connection(app: AppHandle, config: ConnectionConfig) -> TuskResult<()> {
    let path = get_connections_path(&app)?;
    let mut connections = if path.exists() {
        let data = fs::read_to_string(&path)?;
        serde_json::from_str::<Vec<ConnectionConfig>>(&data)?
    } else {
        vec![]
    };

    if let Some(pos) = connections.iter().position(|c| c.id == config.id) {
        connections[pos] = config;
    } else {
        connections.push(config);
    }

    let data = serde_json::to_string_pretty(&connections)?;
    fs::write(&path, data)?;
    Ok(())
}

async fn close_connection(state: &AppState, id: &str) {
    let mut pools = state.pools.write().await;
    if let Some(pool) = pools.remove(id) {
        pool.close().await;
    }
    drop(pools);
    let mut clients = state.ch_clients.write().await;
    clients.remove(id);
    drop(clients);
    let mut ro = state.read_only.write().await;
    ro.remove(id);
    drop(ro);
    let mut flavors = state.db_flavors.write().await;
    flavors.remove(id);
    drop(flavors);
    state.gp_majors.write().await.remove(id);
    state.invalidate_chat_caches_for(id).await;
}

#[tauri::command]
pub async fn delete_connection(
    app: AppHandle,
    state: State<'_, Arc<AppState>>,
    id: String,
) -> TuskResult<()> {
    let path = get_connections_path(&app)?;
    if path.exists() {
        let data = fs::read_to_string(&path)?;
        let mut connections: Vec<ConnectionConfig> = serde_json::from_str(&data)?;
        connections.retain(|c| c.id != id);
        let data = serde_json::to_string_pretty(&connections)?;
        fs::write(&path, data)?;
    }
    close_connection(&state, &id).await;
    // Best-effort cleanup of per-connection persisted state; errors are logged
    // but don't block the deletion (the connections.json is the source of truth).
    if let Err(e) = crate::commands::memory::delete_memory_core(&app, &id) {
        log::warn!("failed to delete memory file for {}: {}", id, e);
    }
    if let Err(e) =
        crate::commands::saved_queries::delete_by_connection_core(&app, &id).await
    {
        log::warn!("failed to clean saved queries for {}: {}", id, e);
    }
    Ok(())
}

#[tauri::command]
pub async fn test_connection(config: ConnectionConfig) -> TuskResult<String> {
    match config.db_flavor {
        DbFlavor::ClickHouse => {
            let client = ChClient::new(
                &config.host,
                config.port,
                config.secure,
                &config.user,
                &config.password,
                &config.database,
            );
            client.ping().await
        }
        _ => {
            let pool = PgPool::connect(&config.connection_url())
                .await
                .map_err(TuskError::Database)?;
            let row = sqlx::query("SELECT version()")
                .fetch_one(&pool)
                .await
                .map_err(TuskError::Database)?;
            let version: String = row.get(0);
            pool.close().await;
            Ok(version)
        }
    }
}

#[tauri::command]
pub async fn connect(
    state: State<'_, Arc<AppState>>,
    config: ConnectionConfig,
) -> TuskResult<ConnectResult> {
    match config.db_flavor {
        DbFlavor::ClickHouse => {
            let client = ChClient::new(
                &config.host,
                config.port,
                config.secure,
                &config.user,
                &config.password,
                &config.database,
            );
            let version = client.ping().await?;
            let arc = Arc::new(client);
            state.ch_clients.write().await.insert(config.id.clone(), arc);
            state.read_only.write().await.insert(config.id.clone(), true);
            state
                .db_flavors
                .write()
                .await
                .insert(config.id.clone(), DbFlavor::ClickHouse);
            Ok(ConnectResult {
                version,
                flavor: DbFlavor::ClickHouse,
            })
        }
        _ => {
            let pool = PgPool::connect(&config.connection_url())
                .await
                .map_err(TuskError::Database)?;
            sqlx::query("SELECT 1")
                .execute(&pool)
                .await
                .map_err(TuskError::Database)?;
            let row = sqlx::query("SELECT version()")
                .fetch_one(&pool)
                .await
                .map_err(TuskError::Database)?;
            let version: String = row.get(0);
            let flavor = if version.to_lowercase().contains("greenplum") {
                DbFlavor::Greenplum
            } else {
                DbFlavor::PostgreSQL
            };
            // Pull the GP major (6 or 7) from a string like
            // "PostgreSQL 14.4 (Greenplum Database 7.1.0 ...)".
            // GP6 and GP7 expose very different system catalogs, so downstream
            // schema-introspection code branches on this.
            let gp_major: Option<u8> = if flavor == DbFlavor::Greenplum {
                version
                    .split("Greenplum Database ")
                    .nth(1)
                    .and_then(|tail| tail.split('.').next())
                    .and_then(|major| major.parse::<u8>().ok())
            } else {
                None
            };
            state.pools.write().await.insert(config.id.clone(), pool);
            state.read_only.write().await.insert(config.id.clone(), true);
            state
                .db_flavors
                .write()
                .await
                .insert(config.id.clone(), flavor);
            if let Some(m) = gp_major {
                state.gp_majors.write().await.insert(config.id.clone(), m);
            } else {
                state.gp_majors.write().await.remove(&config.id);
            }
            Ok(ConnectResult { version, flavor })
        }
    }
}

/// Core implementation of switching the active database for a connection.
/// Reusable from both the Tauri command (frontend-driven) and the chat agent
/// loop (model-driven via the switch_database tool).
pub(crate) async fn switch_database_core(
    state: &AppState,
    config: &ConnectionConfig,
    database: &str,
) -> TuskResult<()> {
    let mut switched = config.clone();
    switched.database = database.to_string();

    let result: TuskResult<()> = match config.db_flavor {
        DbFlavor::ClickHouse => {
            let client = ChClient::new(
                &switched.host,
                switched.port,
                switched.secure,
                &switched.user,
                &switched.password,
                &switched.database,
            );
            client.ping().await?;
            state
                .ch_clients
                .write()
                .await
                .insert(config.id.clone(), Arc::new(client));
            Ok(())
        }
        _ => {
            let pool = PgPool::connect(&switched.connection_url())
                .await
                .map_err(TuskError::Database)?;
            sqlx::query("SELECT 1")
                .execute(&pool)
                .await
                .map_err(TuskError::Database)?;
            let mut pools = state.pools.write().await;
            if let Some(old_pool) = pools.remove(&config.id) {
                old_pool.close().await;
            }
            pools.insert(config.id.clone(), pool);
            Ok(())
        }
    };

    // Drop every cache that's bound to this connection's previous database.
    state.invalidate_chat_caches_for(&config.id).await;

    result
}

#[tauri::command]
pub async fn switch_database(
    state: State<'_, Arc<AppState>>,
    config: ConnectionConfig,
    database: String,
) -> TuskResult<()> {
    switch_database_core(&state, &config, &database).await
}

#[tauri::command]
pub async fn disconnect(state: State<'_, Arc<AppState>>, id: String) -> TuskResult<()> {
    close_connection(&state, &id).await;
    Ok(())
}

#[tauri::command]
pub async fn set_read_only(
    state: State<'_, Arc<AppState>>,
    connection_id: String,
    read_only: bool,
) -> TuskResult<()> {
    let mut map = state.read_only.write().await;
    map.insert(connection_id, read_only);
    Ok(())
}

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

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