use std::sync::Arc;

use tauri::Emitter;
use tokio::sync::{mpsc, Mutex};
use tokio::task::JoinHandle;

use lumotia_hotkey::{EvdevHotkeyListener, HotkeyCombo, HotkeyEvent};

/// A live evdev listener and the JoinHandle for the forwarder task that
/// drains its event channel into Tauri's event bus. They are stored as a
/// pair so a reconfigure can join BOTH before installing a replacement —
/// the previous implementation leaked the forwarder on every reconfigure
/// (atomiser Race-2), causing duplicate Pressed/Released emits to the
/// frontend.
struct ActiveListener {
    listener: EvdevHotkeyListener,
    forwarder: JoinHandle<()>,
}

/// Managed state for the evdev hotkey listener.
pub struct HotkeyState {
    active: Arc<Mutex<Option<ActiveListener>>>,
}

impl HotkeyState {
    pub fn new() -> Self {
        Self {
            active: Arc::new(Mutex::new(None)),
        }
    }
}

/// Check whether the current session is running on Wayland.
#[tauri::command]
pub fn is_wayland_session() -> bool {
    std::env::var("WAYLAND_DISPLAY").is_ok()
        || std::env::var("XDG_SESSION_TYPE")
            .map(|v| v == "wayland")
            .unwrap_or(false)
}

/// Check whether evdev hotkey capture is available (user in `input` group, etc.).
#[tauri::command]
pub fn check_hotkey_access() -> Result<(), String> {
    lumotia_hotkey::check_evdev_access()
}

/// Tear down an `ActiveListener`. Stops the underlying evdev listener
/// first (which drops every internal `event_tx` clone via the supervisor
/// shutdown path) and then awaits the forwarder JoinHandle. Bounded by a
/// timeout so a stuck forwarder cannot block the caller.
async fn shutdown_active(active: ActiveListener) {
    active.listener.stop().await;
    // After listener.stop() returns, every device-listener task has
    // dropped its event_tx clone. The forwarder's mpsc receiver therefore
    // returns None and the task exits naturally. Bound the wait so a
    // misbehaving forwarder cannot wedge the caller.
    match tokio::time::timeout(std::time::Duration::from_secs(2), active.forwarder).await {
        Ok(Ok(())) => {}
        Ok(Err(e)) => {
            tracing::warn!(
                target: "lumotia_hotkey",
                error = %e,
                "hotkey forwarder task panicked"
            );
        }
        Err(_) => {
            tracing::warn!(
                target: "lumotia_hotkey",
                "hotkey forwarder did not drain within 2s; detaching"
            );
        }
    }
}

/// Start the evdev global hotkey listener. Emits "lumotia:hotkey-pressed" and
/// "lumotia:hotkey-released" events to the frontend.
///
/// If a listener is already running, it is stopped first AND its
/// forwarder is joined before the replacement is installed. This is the
/// fix for atomiser Race-2: the previous implementation spawned a fresh
/// forwarder on every reconfigure without ever joining the old one, so
/// every hotkey change leaked a permanent task that received duplicate
/// events from the (also-leaked) old device listeners.
#[tauri::command]
pub async fn start_evdev_hotkey(
    app: tauri::AppHandle,
    state: tauri::State<'_, HotkeyState>,
    hotkey: String,
) -> Result<(), String> {
    let combo = HotkeyCombo::from_tauri_str(&hotkey)
        .ok_or_else(|| format!("Cannot parse hotkey: {hotkey}"))?;

    // Stop existing listener (and join its forwarder) if any. Done
    // BEFORE creating the new listener so the old event_tx is fully
    // dropped and the new event_rx is the only live receiver.
    let mut guard = state.active.lock().await;
    if let Some(existing) = guard.take() {
        // Release the lock for the await so other commands aren't held
        // up if shutdown_active takes the full 2s timeout. Reacquire
        // after.
        drop(guard);
        shutdown_active(existing).await;
        guard = state.active.lock().await;
    }

    let (event_tx, mut event_rx) = mpsc::channel::<HotkeyEvent>(64);
    let listener = EvdevHotkeyListener::start(combo, event_tx).await;

    // Forward evdev events to Tauri event bus. The JoinHandle is stored
    // alongside the listener so the next reconfigure (or explicit stop)
    // can join it cleanly.
    let app_clone = app.clone();
    let forwarder = tokio::spawn(async move {
        while let Some(event) = event_rx.recv().await {
            match event {
                HotkeyEvent::Pressed => {
                    let _ = app_clone.emit("lumotia:hotkey-pressed", ());
                }
                HotkeyEvent::Released => {
                    let _ = app_clone.emit("lumotia:hotkey-released", ());
                }
            }
        }
        tracing::debug!(
            target: "lumotia_hotkey",
            "forwarder exiting; all event_tx senders dropped"
        );
    });

    *guard = Some(ActiveListener {
        listener,
        forwarder,
    });

    Ok(())
}

/// Update the hotkey combo on a running listener.
#[tauri::command]
pub async fn update_evdev_hotkey(
    state: tauri::State<'_, HotkeyState>,
    hotkey: String,
) -> Result<(), String> {
    let combo = HotkeyCombo::from_tauri_str(&hotkey)
        .ok_or_else(|| format!("Cannot parse hotkey: {hotkey}"))?;

    let guard = state.active.lock().await;
    if let Some(ref active) = *guard {
        active.listener.set_hotkey(combo);
        Ok(())
    } else {
        Err("Hotkey listener not running".to_string())
    }
}

/// Stop the evdev hotkey listener.
#[tauri::command]
pub async fn stop_evdev_hotkey(state: tauri::State<'_, HotkeyState>) -> Result<(), String> {
    let mut guard = state.active.lock().await;
    if let Some(active) = guard.take() {
        drop(guard);
        shutdown_active(active).await;
    }
    Ok(())
}
