# edited by glg
import ctypes
import sys
import time
from collections import deque

from PySide6.QtCore import QAbstractNativeEventFilter, QCoreApplication

from pypos.core.utils.scanner_device_utils import extract_device_match_token, normalize_device_name


class RAWINPUTDEVICE(ctypes.Structure):
    _fields_ = [
        ("usUsagePage", ctypes.c_ushort),
        ("usUsage", ctypes.c_ushort),
        ("dwFlags", ctypes.c_uint),
        ("hwndTarget", ctypes.c_void_p),
    ]


class RAWINPUTHEADER(ctypes.Structure):
    _fields_ = [
        ("dwType", ctypes.c_uint),
        ("dwSize", ctypes.c_uint),
        ("hDevice", ctypes.c_void_p),
        ("wParam", ctypes.c_void_p),
    ]


class RAWKEYBOARD(ctypes.Structure):
    _fields_ = [
        ("MakeCode", ctypes.c_ushort),
        ("Flags", ctypes.c_ushort),
        ("Reserved", ctypes.c_ushort),
        ("VKey", ctypes.c_ushort),
        ("Message", ctypes.c_uint),
        ("ExtraInformation", ctypes.c_uint),
    ]


class RAWINPUTUNION(ctypes.Union):
    _fields_ = [
        ("keyboard", RAWKEYBOARD),
        ("_padding", ctypes.c_ubyte * 24),
    ]


class RAWINPUT(ctypes.Structure):
    _fields_ = [
        ("header", RAWINPUTHEADER),
        ("data", RAWINPUTUNION),
    ]


class POINT(ctypes.Structure):
    _fields_ = [
        ("x", ctypes.c_long),
        ("y", ctypes.c_long),
    ]


class MSG(ctypes.Structure):
    _fields_ = [
        ("hwnd", ctypes.c_void_p),
        ("message", ctypes.c_uint),
        ("wParam", ctypes.c_void_p),
        ("lParam", ctypes.c_void_p),
        ("time", ctypes.c_uint),
        ("pt", POINT),
    ]


class RawInputScannerService(QAbstractNativeEventFilter):
    _WM_INPUT = 0x00FF
    _RID_INPUT = 0x10000003
    _RIDI_DEVICENAME = 0x20000007
    _RIM_TYPEKEYBOARD = 1
    _RI_KEY_BREAK = 0x0001

    _VK_RETURN = 0x0D
    _VK_0 = 0x30
    _VK_9 = 0x39
    _VK_A = 0x41
    _VK_Z = 0x5A
    _VK_NUMPAD0 = 0x60
    _VK_NUMPAD9 = 0x69
    _VK_OEM_MINUS = 0xBD
    _VK_SUBTRACT = 0x6D
    _VK_OEM_PLUS = 0xBB
    _VK_ADD = 0x6B
    _VK_OEM_PERIOD = 0xBE
    _VK_DECIMAL = 0x6E
    _VK_OEM_SLASH = 0xBF
    _VK_DIVIDE = 0x6F
    _DISCOVERED_MAX = 64
    _DISCOVERED_TTL_MS = 30 * 60 * 1000

    _VALID_BACKEND_MODE = {"auto", "wedge", "rawinput"}

    def __init__(self, settings=None, log_warning=None, log_debug=None):
        super().__init__()
        self._log_warning = log_warning or (lambda msg: None)
        self._log_debug = log_debug or (lambda msg: None)
        self._is_windows = sys.platform.startswith("win")
        self._user32 = ctypes.windll.user32 if self._is_windows else None
        self._installed = False
        self._active = False
        self._queue = deque()
        self._buffer_by_device = {}
        self._device_name_cache = {}
        self._discovered_device_map = {}
        self._last_error = ""
        self._settings = self._normalize_settings(settings or {})

    def _current_ms(self):
        return int(time.perf_counter() * 1000)

    # edited by glg
    def _to_bool(self, value, default=False):
        if value is None:
            return bool(default)
        if isinstance(value, bool):
            return value
        text = str(value).strip().lower()
        if text in {"1", "true", "yes", "on"}:
            return True
        if text in {"0", "false", "no", "off"}:
            return False
        try:
            return int(float(value)) > 0
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return bool(default)

    # edited by glg
    def _to_int(self, value, default=0, min_value=None):
        try:
            parsed = int(float(value))
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            parsed = int(default)
        if min_value is not None and parsed < int(min_value):
            return int(default)
        return int(parsed)

    def _normalize_mode(self, mode):
        parsed = str(mode or "auto").strip().lower()
        if parsed not in self._VALID_BACKEND_MODE:
            return "auto"
        return parsed

    def _normalize_whitelist(self, raw):
        if not isinstance(raw, list):
            return []
        values = []
        seen = set()
        for entry in raw:
            if isinstance(entry, dict):
                text = str(entry.get("match") or entry.get("value") or "").strip().lower()
            else:
                text = str(entry or "").strip().lower()
            if not text or text in seen:
                continue
            values.append(text)
            seen.add(text)
        return values

    def _normalize_settings(self, settings):
        cfg = settings if isinstance(settings, dict) else {}
        return {
            "enabled": self._to_bool(cfg.get("enabled"), False),
            "max_inter_char_ms": max(1, self._to_int(cfg.get("max_inter_char_ms", 50), 50, min_value=1)),
            "min_length": max(1, self._to_int(cfg.get("min_length", 5), 5, min_value=1)),
            "require_enter_suffix": self._to_bool(cfg.get("require_enter_suffix"), True),
            "backend_mode": self._normalize_mode(cfg.get("backend_mode")),
            "rawinput_enabled": self._to_bool(cfg.get("rawinput_enabled"), True),
            "rawinput_whitelist": self._normalize_whitelist(cfg.get("rawinput_whitelist", [])),
        }

    def apply_settings(self, settings):
        self._settings = self._normalize_settings(settings)
        if self._installed:
            self.stop()
            self.start()

    def is_active(self):
        return bool(self._active)

    def is_whitelist_ready(self):
        whitelist = self._settings.get("rawinput_whitelist") or []
        return bool(len(whitelist) > 0)

    def is_capture_enabled(self):
        return bool(self.is_active() and self.is_whitelist_ready())

    def _should_activate(self):
        if not self._settings.get("enabled"):
            return False
        if not self._settings.get("rawinput_enabled"):
            return False
        mode = self._settings.get("backend_mode", "auto")
        if mode == "wedge":
            return False
        if not self._is_windows:
            return False
        return True

    def start(self):
        if not self._should_activate():
            self._active = False
            return False
        app = QCoreApplication.instance()
        if app is None:
            self._active = False
            return False
        if not self._register_raw_input():
            self._active = False
            return False
        try:
            app.installNativeEventFilter(self)
            self._installed = True
            self._active = True
            return True
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError) as e:
            self._installed = False
            self._active = False
            self._last_error = str(e)
            self._log_warning(f"RawInput install filter gagal: {e}")
            return False

    def stop(self):
        if self._installed:
            app = QCoreApplication.instance()
            if app is not None:
                try:
                    app.removeNativeEventFilter(self)
                except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                    pass
        self._installed = False
        self._active = False
        self._queue.clear()
        self._buffer_by_device.clear()
        self._discovered_device_map.clear()
        self._device_name_cache.clear()

    def _register_raw_input(self):
        try:
            rid = RAWINPUTDEVICE()
            rid.usUsagePage = 0x01
            rid.usUsage = 0x06
            rid.dwFlags = 0
            rid.hwndTarget = None
            ok = self._user32.RegisterRawInputDevices(
                ctypes.byref(rid), 1, ctypes.sizeof(RAWINPUTDEVICE)
            )
            if not ok:
                self._last_error = "RegisterRawInputDevices gagal"
                self._log_warning("RegisterRawInputDevices gagal.")
                return False
            return True
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError) as e:
            self._last_error = str(e)
            self._log_warning(f"Registrasi RawInput gagal: {e}")
            return False

    def get_status(self):
        return {
            "active": bool(self._active),
            "capture_enabled": bool(self.is_capture_enabled()),
            "installed": bool(self._installed),
            "is_windows": bool(self._is_windows),
            "enabled": bool(self._settings.get("enabled", False)),
            "backend_mode": str(self._settings.get("backend_mode", "wedge")),
            "rawinput_enabled": bool(self._settings.get("rawinput_enabled", True)),
            "whitelist_count": len(self._settings.get("rawinput_whitelist") or []),
            "whitelist_ready": bool(self.is_whitelist_ready()),
            "device_cache_size": len(self._device_name_cache),
            "buffer_device_count": len(self._buffer_by_device),
            "queue_size": len(self._queue),
            "discovered_devices": self.get_discovered_devices(),
            "last_error": str(self._last_error or ""),
        }

    def get_discovered_devices(self, limit=20):
        try:
            max_limit = max(1, int(limit or 20))
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            max_limit = 20
        self._prune_discovered_devices(self._current_ms())
        rows = []
        for item in self._discovered_device_map.values():
            if not isinstance(item, dict):
                continue
            name = normalize_device_name(item.get("name"))
            if not name:
                continue
            token = str(item.get("token") or "")
            last_seen_ms = int(item.get("last_seen_ms") or 0)
            rows.append(
                {
                    "name": name,
                    "token": token,
                    "last_seen_ms": last_seen_ms,
                }
            )
        rows.sort(key=lambda row: int(row.get("last_seen_ms") or 0), reverse=True)
        return rows[:max_limit]

    def _mark_device_seen(self, device_name):
        name = normalize_device_name(device_name)
        if not name:
            return
        now_ms = self._current_ms()
        token = extract_device_match_token(name)
        self._discovered_device_map[name.lower()] = {
            "name": name,
            "token": token,
            "last_seen_ms": now_ms,
        }
        self._prune_discovered_devices(now_ms)

    def _prune_discovered_devices(self, now_ms):
        cutoff = int(now_ms) - int(self._DISCOVERED_TTL_MS)
        drop_keys = []
        for key, item in self._discovered_device_map.items():
            last_seen_ms = int((item or {}).get("last_seen_ms") or 0)
            if last_seen_ms < cutoff:
                drop_keys.append(key)
        for key in drop_keys:
            self._discovered_device_map.pop(key, None)
        if len(self._discovered_device_map) <= int(self._DISCOVERED_MAX):
            return
        ordered = sorted(
            self._discovered_device_map.items(),
            key=lambda pair: int((pair[1] or {}).get("last_seen_ms") or 0),
            reverse=True,
        )
        keep = ordered[: int(self._DISCOVERED_MAX)]
        self._discovered_device_map = {k: v for k, v in keep}

    def _device_name(self, device_handle):
        key = int(device_handle or 0)
        if key in self._device_name_cache:
            return self._device_name_cache[key]
        if key == 0:
            self._device_name_cache[key] = ""
            return ""
        try:
            size = ctypes.c_uint(0)
            self._user32.GetRawInputDeviceInfoW(
                ctypes.c_void_p(key),
                self._RIDI_DEVICENAME,
                None,
                ctypes.byref(size),
            )
            if size.value == 0:
                self._device_name_cache[key] = ""
                return ""
            buf = ctypes.create_unicode_buffer(size.value)
            result = self._user32.GetRawInputDeviceInfoW(
                ctypes.c_void_p(key),
                self._RIDI_DEVICENAME,
                buf,
                ctypes.byref(size),
            )
            if result == 0xFFFFFFFF:
                self._device_name_cache[key] = ""
                return ""
            name = str(buf.value or "")
            self._device_name_cache[key] = name
            return name
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            self._device_name_cache[key] = ""
            return ""

    def _is_device_allowed(self, device_name):
        whitelist = self._settings.get("rawinput_whitelist") or []
        if not whitelist:
            return False
        normalized = str(device_name or "").lower()
        return any(token in normalized for token in whitelist)

    def _vkey_to_char(self, vkey):
        if self._VK_0 <= vkey <= self._VK_9:
            return chr(vkey)
        if self._VK_NUMPAD0 <= vkey <= self._VK_NUMPAD9:
            return chr(ord("0") + (vkey - self._VK_NUMPAD0))
        if self._VK_A <= vkey <= self._VK_Z:
            return chr(vkey)
        if vkey in (self._VK_OEM_MINUS, self._VK_SUBTRACT):
            return "-"
        if vkey in (self._VK_OEM_PLUS, self._VK_ADD):
            return "+"
        if vkey in (self._VK_OEM_PERIOD, self._VK_DECIMAL):
            return "."
        if vkey in (self._VK_OEM_SLASH, self._VK_DIVIDE):
            return "/"
        return None

    def _state_for_device(self, device_key):
        state = self._buffer_by_device.get(device_key)
        if state is None:
            state = {"buffer": [], "last_ms": None}
            self._buffer_by_device[device_key] = state
        return state

    def _flush_buffer(self, state):
        barcode = "".join(state.get("buffer") or []).strip()
        state["buffer"] = []
        state["last_ms"] = None
        if len(barcode) < int(self._settings.get("min_length", 5)):
            return None
        return barcode

    def _push_char(self, state, char):
        now_ms = self._current_ms()
        max_ms = int(self._settings.get("max_inter_char_ms", 50))
        if state["last_ms"] is not None and (now_ms - state["last_ms"]) > max_ms:
            if not bool(self._settings.get("require_enter_suffix", True)):
                barcode = self._flush_buffer(state)
                if barcode:
                    self._queue.append(barcode)
            else:
                state["buffer"] = []
        state["buffer"].append(str(char))
        state["last_ms"] = now_ms

    def poll_pending_barcode(self):
        if self._queue:
            return self._queue.popleft()
        if bool(self._settings.get("require_enter_suffix", True)):
            return None
        now_ms = self._current_ms()
        max_ms = int(self._settings.get("max_inter_char_ms", 50))
        for state in self._buffer_by_device.values():
            last_ms = state.get("last_ms")
            if last_ms is None:
                continue
            if (now_ms - last_ms) <= max_ms:
                continue
            barcode = self._flush_buffer(state)
            if barcode:
                return barcode
        return None

    def nativeEventFilter(self, event_type, message):
        if not self._active:
            return False, 0
        try:
            if event_type not in ("windows_generic_MSG", "windows_dispatcher_MSG"):
                return False, 0
            msg = MSG.from_address(int(message))
            if int(msg.message) != self._WM_INPUT:
                return False, 0

            size = ctypes.c_uint(0)
            header_size = ctypes.sizeof(RAWINPUTHEADER)
            self._user32.GetRawInputData(
                ctypes.c_void_p(int(msg.lParam)),
                self._RID_INPUT,
                None,
                ctypes.byref(size),
                header_size,
            )
            if int(size.value) <= 0:
                return False, 0

            buf = (ctypes.c_ubyte * int(size.value))()
            result = self._user32.GetRawInputData(
                ctypes.c_void_p(int(msg.lParam)),
                self._RID_INPUT,
                ctypes.byref(buf),
                ctypes.byref(size),
                header_size,
            )
            if int(result) == -1:
                return False, 0

            raw = ctypes.cast(ctypes.byref(buf), ctypes.POINTER(RAWINPUT)).contents
            if int(raw.header.dwType) != self._RIM_TYPEKEYBOARD:
                return False, 0

            device_handle = int(raw.header.hDevice or 0)
            device_name = self._device_name(device_handle)
            self._mark_device_seen(device_name)
            if not self._is_device_allowed(device_name):
                return False, 0

            keyboard = raw.data.keyboard
            flags = int(keyboard.Flags)
            if flags & self._RI_KEY_BREAK:
                return False, 0

            vkey = int(keyboard.VKey)
            state = self._state_for_device(device_handle)
            if vkey == self._VK_RETURN:
                barcode = self._flush_buffer(state)
                if barcode:
                    self._queue.append(barcode)
                return False, 0

            char = self._vkey_to_char(vkey)
            if char:
                self._push_char(state, char)
            return False, 0
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError) as e:
            self._last_error = str(e)
            return False, 0
