//! Linux evdev-based global hotkey listener.
//!
//! Reads raw input events from `/dev/input/event*` devices. Works on both
//! X11 and Wayland because it operates at the kernel level, bypassing the
//! display server entirely.
//!
//! Key patterns stolen from oddlama/whisper-overlay:
//! - Device hotplug via `notify` watching `/dev/input/`
//! - Retry loop for udev permission propagation on new devices
//! - Per-device async event streams

use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use evdev::{Device, InputEventKind, Key};
use notify::{recommended_watcher, EventKind, RecursiveMode, Watcher};
use tokio::sync::{mpsc, watch, Mutex};

use crate::HotkeyCombo;

/// Events emitted by the hotkey listener.
#[derive(Debug, Clone)]
pub enum HotkeyEvent {
    /// The configured hotkey was pressed.
    Pressed,
    /// The configured hotkey was released (useful for push-to-talk).
    Released,
}

/// Manages evdev device listeners and hotplug detection.
pub struct EvdevHotkeyListener {
    /// Send a new hotkey config to all listener tasks.
    hotkey_tx: watch::Sender<Option<HotkeyCombo>>,
    /// Signals all tasks to shut down.
    shutdown_tx: mpsc::Sender<()>,
}

impl EvdevHotkeyListener {
    /// Start the hotkey listener. Returns the listener handle and a receiver
    /// for hotkey events.
    ///
    /// The listener spawns:
    /// 1. One async task per input device that has the target key
    /// 2. A watcher task that detects new devices via inotify on `/dev/input/`
    pub fn start(combo: HotkeyCombo, event_tx: mpsc::Sender<HotkeyEvent>) -> Self {
        let (hotkey_tx, hotkey_rx) = watch::channel(Some(combo));
        let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);

        let tracked = Arc::new(Mutex::new(HashSet::<PathBuf>::new()));

        // Spawn initial device listeners
        let hotkey_rx_clone = hotkey_rx.clone();
        let event_tx_clone = event_tx.clone();
        let tracked_clone = tracked.clone();
        tokio::spawn(async move {
            scan_and_attach(&hotkey_rx_clone, &event_tx_clone, &tracked_clone).await;
        });

        // Spawn hotplug watcher
        let hotkey_rx_hotplug = hotkey_rx.clone();
        let event_tx_hotplug = event_tx.clone();
        let tracked_hotplug = tracked.clone();
        tokio::spawn(async move {
            let (notify_tx, mut notify_rx) = mpsc::channel::<PathBuf>(32);

            // notify watcher runs on a blocking thread internally.
            // If inotify itself is unavailable (rare: minimal containers,
            // some BSDs misconfigured as Linux) we degrade to "no
            // hotplug detection" rather than panicking the task — the
            // initial scan_and_attach pass above still picks up all
            // devices that exist at startup.
            let _watcher = {
                let notify_tx = notify_tx.clone();
                let watcher = recommended_watcher(move |res: Result<notify::Event, _>| {
                    if let Ok(event) = res {
                        if matches!(event.kind, EventKind::Create(_)) {
                            for path in event.paths {
                                if is_event_device(&path) {
                                    let _ = notify_tx.blocking_send(path);
                                }
                            }
                        }
                    }
                });
                match watcher {
                    Ok(mut w) => {
                        match w.watch(Path::new("/dev/input"), RecursiveMode::NonRecursive) {
                            Ok(()) => Some(w),
                            Err(e) => {
                                eprintln!(
                                    "[kon-hotkey] cannot watch /dev/input ({e}); \
                                 hotplug detection disabled, devices present \
                                 at startup still work",
                                );
                                None
                            }
                        }
                    }
                    Err(e) => {
                        eprintln!(
                            "[kon-hotkey] cannot create inotify watcher ({e}); \
                             hotplug detection disabled",
                        );
                        None
                    }
                }
            };

            loop {
                tokio::select! {
                    Some(path) = notify_rx.recv() => {
                        // Retry opening with backoff — udev permissions propagate
                        // asynchronously after device creation (whisper-overlay pattern)
                        let hotkey_rx = hotkey_rx_hotplug.clone();
                        let event_tx = event_tx_hotplug.clone();
                        let tracked = tracked_hotplug.clone();
                        tokio::spawn(async move {
                            for attempt in 0..5 {
                                if attempt > 0 {
                                    tokio::time::sleep(
                                        std::time::Duration::from_secs(1)
                                    ).await;
                                }
                                if try_attach_device(
                                    &path, &hotkey_rx, &event_tx, &tracked,
                                ).await {
                                    break;
                                }
                            }
                        });
                    }
                    _ = shutdown_rx.recv() => break,
                }
            }
        });

        Self {
            hotkey_tx,
            shutdown_tx,
        }
    }

    /// Update the hotkey combination. All device listeners pick up the
    /// change via the watch channel.
    pub fn set_hotkey(&self, combo: HotkeyCombo) {
        let _ = self.hotkey_tx.send(Some(combo));
    }

    /// Stop all listeners and clean up.
    pub async fn stop(&self) {
        let _ = self.hotkey_tx.send(None);
        let _ = self.shutdown_tx.send(()).await;
    }
}

/// Check whether the user has access to evdev devices.
pub fn check_access() -> Result<(), String> {
    let input_dir = Path::new("/dev/input");
    if !input_dir.exists() {
        return Err("/dev/input does not exist".to_string());
    }

    // Try to open any event device
    let entries =
        std::fs::read_dir(input_dir).map_err(|e| format!("Cannot read /dev/input: {e}"))?;

    for entry in entries.flatten() {
        let path = entry.path();
        if is_event_device(&path) {
            match Device::open(&path) {
                Ok(_) => return Ok(()),
                Err(e) => {
                    if e.kind() == std::io::ErrorKind::PermissionDenied {
                        return Err(format!(
                            "Permission denied reading {}. \
                             Add your user to the 'input' group: \
                             sudo usermod -aG input $USER \
                             (then log out and back in)",
                            path.display()
                        ));
                    }
                }
            }
        }
    }

    Err("No input devices found in /dev/input".to_string())
}

/// Scan all `/dev/input/event*` devices and attach listeners to any
/// that support the target key.
async fn scan_and_attach(
    hotkey_rx: &watch::Receiver<Option<HotkeyCombo>>,
    event_tx: &mpsc::Sender<HotkeyEvent>,
    tracked: &Arc<Mutex<HashSet<PathBuf>>>,
) {
    let input_dir = Path::new("/dev/input");
    let entries = match std::fs::read_dir(input_dir) {
        Ok(e) => e,
        Err(e) => {
            log::error!("Cannot read /dev/input: {e}");
            return;
        }
    };

    for entry in entries.flatten() {
        let path = entry.path();
        if is_event_device(&path) {
            try_attach_device(&path, hotkey_rx, event_tx, tracked).await;
        }
    }
}

/// Try to open a device and start listening if it supports the target key.
/// Returns true if the device was successfully attached.
async fn try_attach_device(
    path: &Path,
    hotkey_rx: &watch::Receiver<Option<HotkeyCombo>>,
    event_tx: &mpsc::Sender<HotkeyEvent>,
    tracked: &Arc<Mutex<HashSet<PathBuf>>>,
) -> bool {
    let mut tracked_set = tracked.lock().await;
    if tracked_set.contains(path) {
        return true;
    }

    let device = match Device::open(path) {
        Ok(d) => d,
        Err(e) => {
            log::debug!("Cannot open {}: {e}", path.display());
            return false;
        }
    };

    // Check if this device has the keys we need
    let supported = device.supported_keys();
    let has_keys = supported.map_or(false, |keys| {
        // Must support at least some keyboard keys
        keys.contains(Key::KEY_A) || keys.contains(Key::KEY_R)
    });

    if !has_keys {
        return false;
    }

    let device_name = device.name().unwrap_or("unknown").to_string();
    log::info!(
        "Attached hotkey listener to: {} ({})",
        device_name,
        path.display()
    );

    tracked_set.insert(path.to_path_buf());
    drop(tracked_set);

    // Spawn a listener task for this device
    let hotkey_rx = hotkey_rx.clone();
    let event_tx = event_tx.clone();
    let path_owned = path.to_path_buf();
    let tracked = tracked.clone();

    tokio::spawn(async move {
        if let Err(e) = device_listener(device, hotkey_rx, event_tx).await {
            log::warn!("Device listener for {} ended: {e}", path_owned.display());
        }
        // Remove from tracked set so hotplug can re-attach if reconnected
        tracked.lock().await.remove(&path_owned);
    });

    true
}

/// Listen for events on a single device. Tracks modifier state and fires
/// hotkey events when the combo matches.
async fn device_listener(
    device: Device,
    mut hotkey_rx: watch::Receiver<Option<HotkeyCombo>>,
    event_tx: mpsc::Sender<HotkeyEvent>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let mut stream = device.into_event_stream()?;

    // Track modifier state
    let mut ctrl_held = false;
    let mut shift_held = false;
    let mut alt_held = false;
    let mut super_held = false;

    loop {
        tokio::select! {
            result = stream.next_event() => {
                let event = result?;

                if let InputEventKind::Key(key) = event.kind() {
                    let pressed = event.value() == 1; // 1 = press, 0 = release, 2 = repeat
                    let released = event.value() == 0;

                    // Update modifier state
                    match key {
                        Key::KEY_LEFTCTRL | Key::KEY_RIGHTCTRL => {
                            ctrl_held = pressed || (!released && ctrl_held);
                        }
                        Key::KEY_LEFTSHIFT | Key::KEY_RIGHTSHIFT => {
                            shift_held = pressed || (!released && shift_held);
                        }
                        Key::KEY_LEFTALT | Key::KEY_RIGHTALT => {
                            alt_held = pressed || (!released && alt_held);
                        }
                        Key::KEY_LEFTMETA | Key::KEY_RIGHTMETA => {
                            super_held = pressed || (!released && super_held);
                        }
                        trigger_key => {
                            let combo = hotkey_rx.borrow().clone();
                            if let Some(ref combo) = combo {
                                let code = trigger_key.code();
                                if code == combo.key_code
                                    && ctrl_held == combo.ctrl
                                    && shift_held == combo.shift
                                    && alt_held == combo.alt
                                    && super_held == combo.super_key
                                {
                                    if pressed {
                                        let _ = event_tx.send(HotkeyEvent::Pressed).await;
                                    } else if released {
                                        let _ = event_tx.send(HotkeyEvent::Released).await;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            _ = hotkey_rx.changed() => {
                // Hotkey config changed — if set to None, shut down
                if hotkey_rx.borrow().is_none() {
                    break;
                }
            }
        }
    }

    Ok(())
}

fn is_event_device(path: &Path) -> bool {
    path.file_name()
        .and_then(|n| n.to_str())
        .map_or(false, |n| n.starts_with("event"))
}
