//! PID-based lock file for toggle-on/toggle-off behavior.
//!
//! The first invocation creates a lock file and runs the overlay.
//! A second invocation sees the lock, sends SIGUSR1 to the running
//! instance, and exits immediately.

use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};

use anyhow::{Context, Result};
use nix::sys::signal::{self, SaFlags, SigAction, SigHandler, SigSet, Signal};
use nix::unistd::Pid;

static SHOULD_EXIT: AtomicBool = AtomicBool::new(false);

extern "C" fn handle_sigusr1(_: nix::libc::c_int) {
    SHOULD_EXIT.store(true, Ordering::Relaxed);
}

pub enum ToggleResult {
    RunNew,
    SignaledExisting,
}

/// Check if another instance is running and signal it, or indicate we should run.
pub fn try_toggle() -> Result<ToggleResult> {
    let path = lock_file_path();

    let contents = match fs::read_to_string(&path) {
        Ok(c) => c,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(ToggleResult::RunNew),
        Err(e) => return Err(e).context("read lock file"),
    };

    let pid: i32 = contents
        .trim()
        .parse()
        .context("parse PID from lock file")?;
    let pid = Pid::from_raw(pid);

    // Check if the process is alive.
    match signal::kill(pid, None) {
        Ok(()) => {}
        Err(nix::errno::Errno::ESRCH) => {
            // Stale lock — process is gone.
            let _ = fs::remove_file(&path);
            return Ok(ToggleResult::RunNew);
        }
        Err(e) => return Err(e).context("check if PID is alive"),
    }

    // Process is alive — send SIGUSR1 to tell it to exit.
    match signal::kill(pid, Signal::SIGUSR1) {
        Ok(()) => Ok(ToggleResult::SignaledExisting),
        Err(nix::errno::Errno::ESRCH) => {
            // Raced — process died between check and signal.
            let _ = fs::remove_file(&path);
            Ok(ToggleResult::RunNew)
        }
        Err(e) => Err(e).context("send SIGUSR1"),
    }
}

/// Returns true if SIGUSR1 has been received.
pub fn should_exit() -> bool {
    SHOULD_EXIT.load(Ordering::Relaxed)
}

/// Create the lock file and install the SIGUSR1 handler. Returns a guard
/// that removes the lock file on drop.
pub fn create_lock() -> Result<LockGuard> {
    let path = lock_file_path();

    // Install SIGUSR1 handler before writing the lock file.
    // No SA_RESTART — let the signal interrupt poll() so the event loop
    // notices should_exit() promptly instead of waiting for the timeout.
    let action = SigAction::new(
        SigHandler::Handler(handle_sigusr1),
        SaFlags::empty(),
        SigSet::empty(),
    );
    unsafe { signal::sigaction(Signal::SIGUSR1, &action) }.context("install SIGUSR1 handler")?;

    let pid = std::process::id();
    fs::write(&path, pid.to_string()).context("write lock file")?;

    Ok(LockGuard { path })
}

pub struct LockGuard {
    path: PathBuf,
}

impl Drop for LockGuard {
    fn drop(&mut self) {
        let _ = fs::remove_file(&self.path);
    }
}

fn lock_file_path() -> PathBuf {
    if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR") {
        PathBuf::from(dir).join("ringa.lock")
    } else {
        PathBuf::from("/tmp/ringa.lock")
    }
}
