//! Fast CLI query daemon for the provisioning system.
//! Serves command registry lookups via Unix socket with in-memory cache.
//! Idle-shuts down after 60s of inactivity — the bash wrapper restarts it on demand.

mod cache;
mod error;
mod registry;
mod socket;

use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tracing::info;

struct Config {
    socket_path: PathBuf,
    lock_path: PathBuf,
    cache_json: PathBuf,
    idle_timeout: Duration,
}

impl Config {
    fn from_env() -> Self {
        let xdg_data = std::env::var("XDG_DATA_HOME")
            .unwrap_or_else(|_| format!("{}/.local/share", env!("HOME")));
        let xdg_cache = std::env::var("XDG_CACHE_HOME")
            .unwrap_or_else(|_| format!("{}/.cache", env!("HOME")));

        let socket_path = std::env::var("PRVNG_CLI_SOCKET")
            .map(PathBuf::from)
            .unwrap_or_else(|_| {
                PathBuf::from(&xdg_data).join("provisioning").join("cli.sock")
            });

        let lock_path = socket_path.with_extension("lock");

        let cache_json = std::env::var("PRVNG_CLI_REGISTRY_CACHE")
            .map(PathBuf::from)
            .unwrap_or_else(|_| {
                PathBuf::from(&xdg_cache)
                    .join("provisioning")
                    .join("commands-registry.json")
            });

        let idle_secs: u64 = std::env::var("PRVNG_CLI_IDLE_SECS")
            .ok()
            .and_then(|v| v.parse().ok())
            .unwrap_or(60);

        Self {
            socket_path,
            lock_path,
            cache_json,
            idle_timeout: Duration::from_secs(idle_secs),
        }
    }
}

// ── Singleton enforcement via flock(2) ───────────────────────────────────────
//
// flock(LOCK_EX | LOCK_NB) is atomic and the kernel releases it automatically
// when the process exits for any reason — no cleanup needed on the Rust side.
// Using extern "C" avoids adding a libc dependency.

#[cfg(unix)]
mod singleton {
    use std::fs::OpenOptions;
    use std::os::unix::io::IntoRawFd;
    use std::path::Path;

    extern "C" {
        fn flock(fd: i32, operation: i32) -> i32;
    }
    const LOCK_EX: i32 = 2;
    const LOCK_NB: i32 = 4;

    /// Acquire an exclusive advisory lock on `lock_path`.
    /// Returns the locked file descriptor (must be kept alive for the lock to hold).
    /// Returns `None` if another process already holds the lock.
    pub fn acquire(lock_path: &Path) -> Option<i32> {
        let file = OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(false)
            .open(lock_path)
            .ok()?;
        let fd = file.into_raw_fd();
        let ret = unsafe { flock(fd, LOCK_EX | LOCK_NB) };
        if ret == 0 {
            Some(fd)
        } else {
            #[allow(unused_unsafe)]
            unsafe { close(fd) };
            None
        }
    }

    extern "C" { fn close(fd: i32) -> i32; }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(
            std::env::var("RUST_LOG")
                .unwrap_or_else(|_| "prvng_cli=info".into()),
        )
        .init();

    let cfg = Config::from_env();

    if let Some(parent) = cfg.socket_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    if let Some(parent) = cfg.cache_json.parent() {
        std::fs::create_dir_all(parent)?;
    }

    // Acquire singleton lock before doing any real work.
    // If another instance holds the lock, exit silently — it is already serving.
    #[cfg(unix)]
    let _lock_fd = match singleton::acquire(&cfg.lock_path) {
        Some(fd) => fd,
        None => {
            info!("another prvng-cli instance is running (lock held); exiting");
            return Ok(());
        }
    };

    let shared_cache = cache::new_shared();

    // Load registry from JSON cache if it exists; missing cache is non-fatal —
    // the bash wrapper rebuilds it via `nickel export` before invoking prvng-cli.
    if cfg.cache_json.exists() {
        cache::reload(&shared_cache, &cfg.cache_json).await?;
    } else {
        tracing::warn!(
            "registry cache not found at {}; lookups will return not_found until populated",
            cfg.cache_json.display()
        );
    }

    // Watch the cache file for changes and reload in-memory index automatically.
    cache::spawn_watcher(shared_cache.clone(), cfg.cache_json.clone());

    info!(
        socket = %cfg.socket_path.display(),
        idle_timeout = ?cfg.idle_timeout,
        "prvng-cli started"
    );

    let shutdown = Arc::new(RwLock::new(false));
    socket::serve(&cfg.socket_path, shared_cache, cfg.idle_timeout, shutdown).await?;

    Ok(())
}
