use crate::db::clickhouse::ChClient;
use crate::error::{TuskError, TuskResult};
use crate::models::ai::AiSettings;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::{watch, RwLock};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DbFlavor {
    PostgreSQL,
    Greenplum,
    ClickHouse,
}


#[derive(Clone)]
pub struct CachedString {
    pub value: String,
    pub cached_at: Instant,
}

#[derive(Clone)]
pub struct CachedVec<T: Clone> {
    pub value: Vec<T>,
    pub cached_at: Instant,
}

pub struct AppState {
    pub pools: RwLock<HashMap<String, PgPool>>,
    pub ch_clients: RwLock<HashMap<String, Arc<ChClient>>>,
    pub read_only: RwLock<HashMap<String, bool>>,
    pub db_flavors: RwLock<HashMap<String, DbFlavor>>,
    /// Greenplum major version (6 or 7), tracked separately because GP6 and GP7
    /// expose very different system catalogs (GP6 = PG9.4 base, GP7 = PG14 base).
    pub gp_majors: RwLock<HashMap<String, u8>>,
    /// Chat agent caches: lite overview per connection.
    pub overview_cache: RwLock<HashMap<String, CachedString>>,
    /// Chat agent caches: list of tables per (connection_id, db_name) — used for
    /// list_tables on a non-active PG database via temporary pool.
    pub tables_by_db_cache: RwLock<HashMap<(String, String), CachedVec<String>>>,
    pub mcp_shutdown_tx: watch::Sender<bool>,
    pub mcp_running: RwLock<bool>,
    pub ai_settings: RwLock<Option<AiSettings>>,
}

impl AppState {
    pub fn new() -> Self {
        let (mcp_shutdown_tx, _) = watch::channel(false);
        Self {
            pools: RwLock::new(HashMap::new()),
            ch_clients: RwLock::new(HashMap::new()),
            read_only: RwLock::new(HashMap::new()),
            db_flavors: RwLock::new(HashMap::new()),
            gp_majors: RwLock::new(HashMap::new()),
            overview_cache: RwLock::new(HashMap::new()),
            tables_by_db_cache: RwLock::new(HashMap::new()),
            mcp_shutdown_tx,
            mcp_running: RwLock::new(false),
            ai_settings: RwLock::new(None),
        }
    }

    /// Drop every chat-agent cache entry tied to this connection.
    /// Called by switch_database_core, disconnect, and on connection delete.
    pub async fn invalidate_chat_caches_for(&self, connection_id: &str) {
        self.overview_cache.write().await.remove(connection_id);
        self.tables_by_db_cache
            .write()
            .await
            .retain(|(cid, _), _| cid != connection_id);
    }

    pub async fn get_pool(&self, connection_id: &str) -> TuskResult<PgPool> {
        let pools = self.pools.read().await;
        pools
            .get(connection_id)
            .cloned()
            .ok_or_else(|| TuskError::NotConnected(connection_id.to_string()))
    }

    pub async fn get_ch_client(&self, connection_id: &str) -> TuskResult<Arc<ChClient>> {
        let clients = self.ch_clients.read().await;
        clients
            .get(connection_id)
            .cloned()
            .ok_or_else(|| TuskError::NotConnected(connection_id.to_string()))
    }

    pub async fn is_read_only(&self, id: &str) -> bool {
        let map = self.read_only.read().await;
        map.get(id).copied().unwrap_or(true)
    }

    pub async fn get_flavor(&self, id: &str) -> DbFlavor {
        let map = self.db_flavors.read().await;
        map.get(id).copied().unwrap_or(DbFlavor::PostgreSQL)
    }

    /// Returns the Greenplum major version (6 or 7) for a connection, or None
    /// for non-GP connections / when the version string couldn't be parsed.
    pub async fn get_gp_major(&self, id: &str) -> Option<u8> {
        self.gp_majors.read().await.get(id).copied()
    }
}
