from __future__ import annotations

from pathlib import Path

from evdev import InputDevice, ecodes, list_devices


def is_mouse(dev: InputDevice) -> bool:
    keys = dev.capabilities().get(ecodes.EV_KEY, [])
    rels = dev.capabilities().get(ecodes.EV_REL, [])
    return ecodes.BTN_LEFT in keys and ecodes.REL_X in rels and ecodes.REL_Y in rels


def is_keyboard(dev: InputDevice) -> bool:
    caps = dev.capabilities().get(ecodes.EV_KEY, [])
    # Gaming mice (e.g. Logitech G502) commonly also advertise KEY_A/
    # KEY_SPACE (programmable buttons / media-key emulation) and would
    # otherwise get opened redundantly as both "keyboard" and "mouse" by
    # separate listeners, which was observed to make one of the duplicate
    # opens randomly drop ("Input device disconnected").
    return ecodes.KEY_A in caps and ecodes.KEY_SPACE in caps and not is_mouse(dev)


def inaccessible_keyboard_names() -> list[str]:
    """Best-effort list of device names the kernel flags as keyboards (a
    "kbd" handler in /proc/bus/input/devices) but that this process can't
    actually open.

    evdev.list_devices() silently drops any /dev/input/eventN this process
    fails an os.access(R_OK | W_OK) check on - no exception, no gap in the
    returned list, so a real keyboard blocked by permissions just vanishes
    without a trace and whatever else happens to qualify as "keyboard-like"
    (e.g. a gaming mouse's macro-key HID sub-interface) silently becomes
    the only thing listened on. Observed on a Bazzite/GNOME machine: the
    physical keyboard's device node wasn't covered by the desktop
    session's usual per-session uaccess ACL (likely stripped by a
    Steam-Input udev rule for recognized gaming-keyboard hardware IDs),
    leaving it gated by the static 'input' group alone - which only takes
    effect for the process's *next* login session, not a new terminal in
    an already-running one. /proc/bus/input/devices is a plain readable
    text file regardless of any of that, so it's used here purely to
    produce an actionable diagnostic, never to open anything.
    """
    try:
        text = Path("/proc/bus/input/devices").read_text()
    except OSError:
        return []

    accessible = {Path(p).name for p in list_devices()}
    missing: list[str] = []
    name: str | None = None
    for line in text.splitlines():
        if line.startswith("N: Name="):
            name = line.split("=", 1)[1].strip('"')
        elif line.startswith("H: Handlers="):
            handlers = line.split("=", 1)[1].split()
            if "kbd" not in handlers:
                continue
            event_handlers = [h for h in handlers if h.startswith("event")]
            if event_handlers and event_handlers[0] not in accessible:
                missing.append(name or event_handlers[0])
    return missing
