#[cfg(coverage)]
impl KeyboardAggregator {
    fn fetch_events(&mut self) -> Vec<InputEvent> {
        self.dev
            .fetch_events()
            .map(|it| it.collect::<Vec<InputEvent>>())
            .unwrap_or_default()
    }
}

#[cfg(not(coverage))]
impl KeyboardAggregator {
    fn fetch_events(&mut self) -> Vec<InputEvent> {
        match self.dev.fetch_events() {
            Ok(it) => it.collect(),
            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Vec::new(),
            Err(e) => {
                if self.dev_mode {
                    error!("⌨️❌ read error: {e}");
                }
                Vec::new()
            }
        }
    }
}

pub fn build_keyboard_report<I>(pressed_keys: I) -> [u8; 8]
where
    I: IntoIterator<Item = KeyCode>,
{
    let mut out = [0u8; 8];
    let mut mods = 0u8;
    let mut keys = Vec::new();

    for kc in pressed_keys {
        if let Some(m) = is_modifier(kc) {
            mods |= m;
            continue;
        }
        if let Some(u) = keycode_to_usage(kc) {
            keys.push(u);
        }
    }

    keys.sort_unstable();
    keys.dedup();
    out[0] = mods;
    for (i, k) in keys.into_iter().take(6).enumerate() {
        out[2 + i] = k;
    }
    out
}

pub fn emit_live_keyboard_report(
    tx: &Sender<KeyboardReport>,
    code: KeyCode,
    value: i32,
    report: [u8; 8],
) {
    if should_stage_modifier_report(code, value, report) {
        send_keyboard_report(tx, modifier_only_report(report[0]));
        let delay = live_modifier_delay();
        if !delay.is_zero() {
            std::thread::sleep(delay);
        }
    }
    send_keyboard_report(tx, report);
}

pub fn send_keyboard_report(tx: &Sender<KeyboardReport>, report: [u8; 8]) {
    let _ = tx.send(KeyboardReport {
        data: report.to_vec(),
    });
}

fn paste_rpc_enabled_from_env() -> bool {
    let rpc_enabled = std::env::var("LESAVKA_PASTE_RPC")
        .map(|v| v != "0")
        .unwrap_or(true);
    let have_key = paste_key_available_from_env();
    let enabled = paste_rpc_enabled(rpc_enabled, have_key);
    #[cfg(not(coverage))]
    if rpc_enabled && !have_key {
        tracing::info!("📋 paste key missing; disabling paste RPC and using HID paste fallback");
    }
    enabled
}

fn paste_rpc_enabled(rpc_enabled: bool, have_key: bool) -> bool {
    rpc_enabled && have_key
}

fn paste_key_available_from_env() -> bool {
    if std::env::var("LESAVKA_PASTE_KEY")
        .map(|value| !value.trim().is_empty())
        .unwrap_or(false)
    {
        return true;
    }
    if let Ok(path) = std::env::var("LESAVKA_PASTE_KEY_FILE") {
        return std::path::Path::new(path.trim()).is_file();
    }
    std::env::var_os("HOME").is_some_and(|home| {
        let mut path = std::path::PathBuf::from(home);
        path.push(".config/lesavka/paste-key");
        path.is_file()
    })
}

fn is_paste_modifier(code: KeyCode) -> bool {
    matches!(
        code,
        KeyCode::KEY_LEFTCTRL
            | KeyCode::KEY_RIGHTCTRL
            | KeyCode::KEY_LEFTALT
            | KeyCode::KEY_RIGHTALT
    )
}

fn should_stage_modifier_report(code: KeyCode, value: i32, report: [u8; 8]) -> bool {
    value == 1
        && is_modifier(code).is_none()
        && report[0] != 0
        && report[2..].iter().any(|b| *b != 0)
}

fn modifier_only_report(modifiers: u8) -> [u8; 8] {
    [modifiers, 0, 0, 0, 0, 0, 0, 0]
}

fn live_modifier_delay() -> Duration {
    std::env::var("LESAVKA_LIVE_MODIFIER_DELAY_MS")
        .ok()
        .and_then(|value| value.parse::<u64>().ok())
        .map(Duration::from_millis)
        .unwrap_or_else(|| Duration::from_millis(24))
}

#[cfg(coverage)]
fn read_clipboard_text() -> Option<String> {
    if let Ok(cmd) = std::env::var("LESAVKA_CLIPBOARD_CMD") {
        if let Ok(out) = std::process::Command::new("sh")
            .arg("-lc")
            .arg(cmd)
            .output()
        {
            let text = String::from_utf8_lossy(&out.stdout).to_string();
            if out.status.success() && !text.is_empty() {
                return Some(text);
            }
        }
    }

    for args in [
        vec!["--no-newline", "--type", "text/plain"],
        vec!["--no-newline"],
        vec![],
    ] {
        if let Ok(out) = std::process::Command::new("wl-paste").args(&args).output()
            && out.status.success()
        {
            return Some(String::from_utf8_lossy(&out.stdout).to_string());
        }
    }

    None
}

#[cfg(not(coverage))]
fn read_clipboard_text() -> Option<String> {
    if let Ok(cmd) = std::env::var("LESAVKA_CLIPBOARD_CMD") {
        if let Ok(out) = std::process::Command::new("sh")
            .arg("-lc")
            .arg(cmd.clone())
            .output()
        {
            if out.status.success() {
                let text = String::from_utf8_lossy(&out.stdout).to_string();
                if !text.is_empty() {
                    return Some(text);
                }
                tracing::warn!("📋 clipboard command returned empty");
            } else {
                let err = String::from_utf8_lossy(&out.stderr);
                tracing::warn!("📋 clipboard command failed: {cmd} ({err})");
            }
        } else {
            tracing::warn!("📋 clipboard command failed to spawn: {cmd}");
        }
        // fall through to auto-detect if custom command fails
    }

    let candidates: &[(&str, &[&str])] = &[
        ("wl-paste", &["--no-newline", "--type", "text/plain"]),
        ("wl-paste", &["--no-newline"]),
        ("wl-paste", &[]),
        ("xclip", &["-selection", "clipboard", "-o"]),
        ("xsel", &["-b", "-o"]),
    ];

    for (cmd, args) in candidates {
        if let Ok(out) = std::process::Command::new(cmd).args(*args).output()
            && out.status.success() {
                return Some(String::from_utf8_lossy(&out.stdout).to_string());
            }
    }
    None
}

impl Drop for KeyboardAggregator {
    fn drop(&mut self) {
        let _ = self.dev.ungrab();
        let _ = self.tx.send(KeyboardReport {
            data: [0; 8].into(),
        });
    }
}
