Skip to content
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};

use serde::{Deserialize, Serialize};

/// Connection metadata that is persisted to disk (no password).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionMeta {
    pub id: String,
    pub name: String,
    pub host: String,
    pub port: u16,
    pub user: String,
}

/// A full connection with password (in-memory only).
#[derive(Debug, Clone)]
pub struct Connection {
    pub id: String,
    pub name: String,
    pub host: String,
    pub port: u16,
    pub user: String,
    pub password: String,
}

impl Connection {
    /// Build the full HTTP URL for ClickHouse.
    /// Uses HTTPS when the port is 8443 or 443, otherwise plain HTTP.
    pub fn url(&self) -> String {
        let scheme = if self.port == 8443 || self.port == 443 {
            "https"
        } else {
            "http"
        };
        format!("{}://{}:{}", scheme, self.host, self.port)
    }

    /// Build a `clickhouse::Client` from this connection's settings.
    pub fn build_client(&self) -> clickhouse::Client {
        let mut client = clickhouse::Client::default()
            .with_url(self.url())
            .with_user(&self.user);
        if !self.password.is_empty() {
            client = client.with_password(&self.password);
        }
        client
    }
}

/// Serialisable state persisted to disk (passwords are NOT included).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct StoredState {
    connections: Vec<ConnectionMeta>,
    active_id: Option<String>,
}

/// Thread-safe store of saved connections.
/// Passwords are kept in memory only, not persisted to disk.
#[derive(Debug, Clone)]
pub struct ConnectionStore {
    inner: Arc<RwLock<StoredState>>,
    /// In-memory only storage for passwords (not persisted).
    passwords: Arc<RwLock<HashMap<String, String>>>,
    path: PathBuf,
}

impl ConnectionStore {
    /// Load from the given file path, creating it (and parent dirs) if absent.
    /// Passwords will need to be provided separately via `set_password()`.
    pub fn load(path: PathBuf) -> Self {
        let state = if path.exists() {
            std::fs::read_to_string(&path)
                .ok()
                .and_then(|s| serde_json::from_str::<StoredState>(&s).ok())
                .unwrap_or_default()
        } else {
            StoredState::default()
        };

        Self {
            inner: Arc::new(RwLock::new(state)),
            passwords: Arc::new(RwLock::new(HashMap::new())),
            path,
        }
    }

    /// Default location: `$HOME/.chtmx/connections.json`
    pub fn default_path() -> PathBuf {
        let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
        PathBuf::from(home).join(".chtmx").join("connections.json")
    }

    /// Persist current state to disk (metadata only, passwords stay in memory).
    fn save(&self) {
        let state = self.inner.read().expect("store read lock poisoned").clone();
        if let Some(parent) = self.path.parent() {
            let _ = std::fs::create_dir_all(parent);
        }
        match serde_json::to_string_pretty(&state) {
            Ok(json) => {
                if let Err(e) = std::fs::write(&self.path, json) {
                    log::error!("Failed to persist connections: {}", e);
                }
            }
            Err(e) => log::error!("Failed to serialise connections: {}", e),
        }
    }

    /// Return a snapshot of all connection metadata (without passwords).
    pub fn all_meta(&self) -> Vec<ConnectionMeta> {
        self.inner
            .read()
            .expect("store read lock poisoned")
            .connections
            .clone()
    }

    /// Return a snapshot of all connections (with passwords from memory).
    pub fn all(&self) -> Vec<Connection> {
        let state = self.inner.read().expect("store read lock poisoned");
        let passwords = self.passwords.read().expect("passwords read lock poisoned");
        state
            .connections
            .iter()
            .map(|meta| Connection {
                id: meta.id.clone(),
                name: meta.name.clone(),
                host: meta.host.clone(),
                port: meta.port,
                user: meta.user.clone(),
                password: passwords.get(&meta.id).cloned().unwrap_or_default(),
            })
            .collect()
    }

    /// Return the currently active connection ID, if any.
    pub fn active_id(&self) -> Option<String> {
        self.inner
            .read()
            .expect("store read lock poisoned")
            .active_id
            .clone()
    }

    /// Check if a connection has a password set in memory.
    pub fn has_password(&self, id: &str) -> bool {
        let passwords = self.passwords.read().expect("passwords read lock poisoned");
        passwords.get(id).is_some_and(|p| !p.is_empty())
    }

    /// Get the password for a connection (if set in memory).
    pub fn get_password(&self, id: &str) -> Option<String> {
        self.passwords
            .read()
            .expect("passwords read lock poisoned")
            .get(id)
            .cloned()
    }

    /// Set or update the password for a connection (in memory only).
    pub fn set_password(&self, id: &str, password: String) {
        let mut passwords = self
            .passwords
            .write()
            .expect("passwords write lock poisoned");
        passwords.insert(id.to_string(), password);
    }

    /// Return the currently active connection metadata, if any.
    pub fn active_meta(&self) -> Option<ConnectionMeta> {
        let state = self.inner.read().expect("store read lock poisoned");
        state
            .active_id
            .as_ref()
            .and_then(|id| state.connections.iter().find(|c| &c.id == id).cloned())
    }

    /// Return the currently active connection with password, if any.
    pub fn active(&self) -> Option<Connection> {
        let meta = self.active_meta()?;
        let passwords = self.passwords.read().expect("passwords read lock poisoned");
        Some(Connection {
            id: meta.id.clone(),
            name: meta.name.clone(),
            host: meta.host.clone(),
            port: meta.port,
            user: meta.user.clone(),
            password: passwords.get(&meta.id).cloned().unwrap_or_default(),
        })
    }

    /// Build a ClickHouse client for the active connection.
    /// Returns `None` when no active connection is set or no password in memory.
    pub fn active_client(&self) -> Option<clickhouse::Client> {
        self.active().map(|c| c.build_client())
    }

    /// Add a new connection. Returns the new connection's id.
    /// The password is stored in memory only.
    pub fn add(
        &self,
        name: String,
        host: String,
        port: u16,
        user: String,
        password: String,
    ) -> String {
        let id = uuid::Uuid::new_v4().to_string();
        let meta = ConnectionMeta {
            id: id.clone(),
            name,
            host,
            port,
            user,
        };
        let mut state = self.inner.write().expect("store write lock poisoned");
        // If this is the first connection, make it active automatically.
        if state.connections.is_empty() {
            state.active_id = Some(id.clone());
        }
        state.connections.push(meta);
        drop(state);

        // Store password in memory only
        let mut passwords = self
            .passwords
            .write()
            .expect("passwords write lock poisoned");
        passwords.insert(id.clone(), password);
        drop(passwords);

        self.save();
        id
    }

    /// Remove a connection by id. If it was active, clear the active id.
    pub fn remove(&self, id: &str) {
        let mut state = self.inner.write().expect("store write lock poisoned");
        state.connections.retain(|c| c.id != id);
        if state.active_id.as_deref() == Some(id) {
            state.active_id = None;
        }
        drop(state);

        // Also remove password from memory
        let mut passwords = self
            .passwords
            .write()
            .expect("passwords write lock poisoned");
        passwords.remove(id);
        drop(passwords);

        self.save();
    }

    /// Set the active connection by id. No-op if the id does not exist.
    pub fn set_active(&self, id: &str) {
        let mut state = self.inner.write().expect("store write lock poisoned");
        if state.connections.iter().any(|c| c.id == id) {
            state.active_id = Some(id.to_string());
        }
        drop(state);
        self.save();
    }

    /// Add a connection from environment variables (metadata to disk, password to memory).
    pub fn add_from_env(
        &self,
        name: String,
        host: String,
        port: u16,
        user: String,
        password: String,
    ) -> String {
        self.add(name, host, port, user, password)
    }
}