#!/usr/bin/env python3
"""
Port Monitor — 网卡/USB 端口变化监控（系统事件驱动）
通过 Linux netlink socket 注册内核事件回调，零轮询，精准监控。
- 网卡：NETLINK_ROUTE socket (RTMGRP_LINK) 监听接口增删改
- USB：NETLINK_KOBJECT_UEVENT socket (protocol 15) 监听 USB 热插拔
收到事件后触发快照对比并记录日志，飞行中也可工作。
"""

import json
import os
import select
import socket
import struct
import subprocess
import threading
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional

try:
    import psutil
    HAS_PSUTIL = True
except ImportError:
    HAS_PSUTIL = False


# ---------- Netlink constants ----------
RTM_NEWLINK = 16
RTM_DELLINK = 17
NLMSG_DONE = 3
IFLA_IFNAME = 3
RTMGRP_LINK = 1
NETLINK_KOBJECT_UEVENT = 15


class PortMonitor:
    """网卡和 USB 端口变化监控器（系统事件驱动）"""

    def __init__(
        self,
        log_dir: Optional[Path] = None,
        config_file: Optional[Path] = None,
    ):
        # 日志目录
        self.log_dir = log_dir or (Path(__file__).parent / "data" / "port_monitor")
        self.log_dir.mkdir(parents=True, exist_ok=True)

        # 配置文件
        self.config_file = config_file or (Path(__file__).parent / "data" / "port_monitor_config.json")
        self._config = self._load_config()

        self._enabled = self._config.get("enabled", False)
        self._lock = threading.Lock()

        # 快照
        self._last_network: Dict[str, Any] = {}
        self._last_usb: Dict[str, Any] = {}
        self._current_network: List[Dict[str, Any]] = []
        self._current_usb: List[Dict[str, Any]] = []

        # 最近变化（供前端查询）
        self._recent_changes: List[Dict[str, Any]] = []
        self._max_recent = 50

        # 事件 socket
        self._nl_sock: Optional[socket.socket] = None
        self._uevent_sock: Optional[socket.socket] = None
        self._has_netlink = False

        # 防抖计时
        self._net_debounce_timer: Optional[threading.Timer] = None
        self._usb_debounce_timer: Optional[threading.Timer] = None
        self._debounce_lock = threading.Lock()
        self._debounce_delay = 0.5

        # 线程控制
        self._thread: Optional[threading.Thread] = None
        self._stop_event = threading.Event()

    # ---------- Config ----------

    def _load_config(self) -> Dict[str, Any]:
        if self.config_file.exists():
            try:
                with open(self.config_file, "r", encoding="utf-8") as f:
                    return json.load(f)
            except Exception:
                pass
        return {"enabled": False}

    def _save_config(self) -> None:
        try:
            with open(self.config_file, "w", encoding="utf-8") as f:
                json.dump(self._config, f, indent=2)
            os.sync()
        except Exception:
            pass

    # ---------- Log ----------

    def _today_log_path(self) -> Path:
        return self.log_dir / "port_monitor_{}.jsonl".format(
            datetime.now().strftime("%Y%m%d")
        )

    def _write_log(self, event: Dict[str, Any]) -> None:
        log_path = self._today_log_path()
        line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
        try:
            with open(log_path, "a", encoding="utf-8") as f:
                f.write(line + "\n")
                f.flush()
                os.fsync(f.fileno())
        except Exception:
            pass

    def _add_recent_change(self, event: Dict[str, Any]) -> None:
        with self._lock:
            self._recent_changes.insert(0, event)
            if len(self._recent_changes) > self._max_recent:
                self._recent_changes = self._recent_changes[:self._max_recent]

    # ---------- Snapshot ----------

    def _get_network_snapshot(self) -> Dict[str, Any]:
        if not HAS_PSUTIL:
            return {}
        result: Dict[str, Any] = {}
        try:
            addrs = psutil.net_if_addrs()
            stats = psutil.net_if_stats()
            for name, addr_list in addrs.items():
                if name == "lo" or name.startswith("lo"):
                    continue
                # 过滤虚拟网卡
                skip_prefixes = ("docker", "veth", "br-", "tun", "virbr", "vmnet", "dummy", "sit")
                if any(name.startswith(p) for p in skip_prefixes):
                    continue
                macs = [a.address for a in addr_list if a.family == psutil.AF_LINK]
                ipv4s = [a.address for a in addr_list if str(a.family) == "AddressFamily.AF_INET"]
                stat = stats.get(name)
                result[name] = {
                    "name": name,
                    "mac": macs[0] if macs else "",
                    "ipv4": ipv4s,
                    "is_up": stat.isup if stat else False,
                }
        except Exception:
            pass
        return result

    def _get_usb_snapshot(self) -> Dict[str, Any]:
        result: Dict[str, Any] = {}
        try:
            output = subprocess.check_output(
                ["lsusb"], stderr=subprocess.DEVNULL, text=True, timeout=5
            )
            for line in output.strip().split("\n"):
                line = line.strip()
                if not line:
                    continue
                parts = line.split()
                if len(parts) >= 6 and parts[0] == "Bus" and parts[2] == "Device":
                    bus = parts[1]
                    device = parts[3].rstrip(":")
                    desc = " ".join(parts[6:]) if len(parts) > 6 else ""
                    key = "{}-{}".format(bus, device)
                    result[key] = {
                        "bus": bus,
                        "device": device,
                        "id": parts[5] if len(parts) > 5 else "",
                        "description": desc,
                    }
        except Exception:
            pass
        return result

    # ---------- Change detection ----------

    def _detect_changes(
        self,
        old: Dict[str, Any],
        new: Dict[str, Any],
        device_type: str,
    ) -> List[Dict[str, Any]]:
        old_keys = set(old.keys())
        new_keys = set(new.keys())
        changes: List[Dict[str, Any]] = []

        for key in new_keys - old_keys:
            event = {
                "timestamp": datetime.now().isoformat(),
                "type": device_type,
                "event": "added",
                "device": new[key],
            }
            self._write_log(event)
            self._add_recent_change(event)
            changes.append(event)

        for key in old_keys - new_keys:
            event = {
                "timestamp": datetime.now().isoformat(),
                "type": device_type,
                "event": "removed",
                "device": old[key],
            }
            self._write_log(event)
            self._add_recent_change(event)
            changes.append(event)

        for key in old_keys & new_keys:
            old_dev = old[key]
            new_dev = new[key]
            changed_fields = {}
            for field in old_dev:
                if old_dev.get(field) != new_dev.get(field):
                    changed_fields[field] = {"old": old_dev.get(field), "new": new_dev.get(field)}
            if changed_fields:
                event = {
                    "timestamp": datetime.now().isoformat(),
                    "type": device_type,
                    "event": "changed",
                    "device": new_dev,
                    "changes": changed_fields,
                }
                self._write_log(event)
                self._add_recent_change(event)
                changes.append(event)

        return changes

    def _check_network_now(self) -> List[Dict[str, Any]]:
        net_new = self._get_network_snapshot()
        changes = self._detect_changes(self._last_network, net_new, "network")
        with self._lock:
            self._last_network = net_new
            self._current_network = list(net_new.values())
        return changes

    def _check_usb_now(self) -> List[Dict[str, Any]]:
        usb_new = self._get_usb_snapshot()
        changes = self._detect_changes(self._last_usb, usb_new, "usb")
        with self._lock:
            self._last_usb = usb_new
            self._current_usb = list(usb_new.values())
        return changes

    # ---------- Debounce ----------

    def _debounced_check_network(self) -> None:
        with self._debounce_lock:
            if self._net_debounce_timer:
                self._net_debounce_timer.cancel()
            self._net_debounce_timer = threading.Timer(
                self._debounce_delay, self._do_check_network
            )
            self._net_debounce_timer.daemon = True
            self._net_debounce_timer.start()

    def _do_check_network(self) -> None:
        if not self._enabled:
            return
        try:
            self._check_network_now()
        except Exception:
            pass
        with self._debounce_lock:
            self._net_debounce_timer = None

    def _debounced_check_usb(self) -> None:
        with self._debounce_lock:
            if self._usb_debounce_timer:
                self._usb_debounce_timer.cancel()
            self._usb_debounce_timer = threading.Timer(
                self._debounce_delay, self._do_check_usb
            )
            self._usb_debounce_timer.daemon = True
            self._usb_debounce_timer.start()

    def _do_check_usb(self) -> None:
        if not self._enabled:
            return
        try:
            self._check_usb_now()
        except Exception:
            pass
        with self._debounce_lock:
            self._usb_debounce_timer = None

    # ---------- Netlink event parsing ----------

    @staticmethod
    def _parse_netlink_ifname(data: bytes) -> Optional[str]:
        """从 RTM_NEWLINK/DELLINK netlink 消息中提取接口名"""
        offset = 0
        while offset + 16 <= len(data):
            nlmsg_len, nlmsg_type, nlmsg_flags, nlmsg_seq, nlmsg_pid = struct.unpack_from(
                "IHHII", data, offset
            )
            if nlmsg_len < 16 or offset + nlmsg_len > len(data):
                break
            if nlmsg_type in (RTM_NEWLINK, RTM_DELLINK):
                msg_offset = offset + 16
                if msg_offset + 16 <= len(data):
                    attr_offset = msg_offset + 16
                    while attr_offset + 4 <= offset + nlmsg_len:
                        rta_len, rta_type = struct.unpack_from("HH", data, attr_offset)
                        if rta_len < 4:
                            break
                        if rta_type == IFLA_IFNAME:
                            raw = data[attr_offset + 4 : attr_offset + rta_len]
                            return raw.split(b"\x00")[0].decode("utf-8", errors="replace")
                        attr_offset += ((rta_len + 3) // 4) * 4
            offset += ((nlmsg_len + 3) // 4) * 4
            if nlmsg_len == 0:
                break
        return None

    @staticmethod
    def _parse_uevent(data: bytes) -> Dict[str, str]:
        """解析 uevent 消息为 key-value 字典"""
        result: Dict[str, str] = {}
        try:
            text = data.decode("utf-8", errors="replace")
            for part in text.split("\x00"):
                if "=" in part:
                    k, v = part.split("=", 1)
                    result[k] = v
        except Exception:
            pass
        return result

    @staticmethod
    def _is_usb_uevent(data: bytes) -> bool:
        """判断 uevent 是否为 USB 相关"""
        info = PortMonitor._parse_uevent(data)
        return info.get("SUBSYSTEM") == "usb" or "usb" in info.get("DEVPATH", "").lower()

    # ---------- Socket setup ----------

    def _setup_sockets(self) -> bool:
        """创建并绑定 netlink socket。返回是否成功。"""
        try:
            self._nl_sock = socket.socket(socket.AF_NETLINK, socket.SOCK_DGRAM, socket.NETLINK_ROUTE)
            self._nl_sock.bind((os.getpid(), RTMGRP_LINK))
            self._nl_sock.setblocking(False)
        except Exception:
            self._nl_sock = None

        try:
            self._uevent_sock = socket.socket(socket.AF_NETLINK, socket.SOCK_DGRAM, NETLINK_KOBJECT_UEVENT)
            self._uevent_sock.bind((os.getpid(), 0xFFFFFFFF))
            self._uevent_sock.setblocking(False)
        except Exception:
            self._uevent_sock = None

        self._has_netlink = self._nl_sock is not None or self._uevent_sock is not None
        return self._has_netlink

    def _close_sockets(self) -> None:
        if self._nl_sock:
            try:
                self._nl_sock.close()
            except Exception:
                pass
            self._nl_sock = None
        if self._uevent_sock:
            try:
                self._uevent_sock.close()
            except Exception:
                pass
            self._uevent_sock = None

    # ---------- Listen loop ----------

    def _listen_loop(self) -> None:
        """事件监听主循环：select 阻塞等待内核事件，零轮询。"""
        if not self._setup_sockets():
            return

        # 初始化基线快照（不记录日志）
        self._last_network = self._get_network_snapshot()
        self._last_usb = self._get_usb_snapshot()
        with self._lock:
            self._current_network = list(self._last_network.values())
            self._current_usb = list(self._last_usb.values())

        socks: List[socket.socket] = []
        if self._nl_sock:
            socks.append(self._nl_sock)
        if self._uevent_sock:
            socks.append(self._uevent_sock)

        while not self._stop_event.is_set():
            if not self._enabled:
                self._stop_event.wait(1.0)
                continue

            try:
                readable, _, _ = select.select(socks, [], [], 2.0)
            except (OSError, ValueError):
                break

            if not readable:
                continue

            for sock in readable:
                try:
                    data = sock.recv(8192)
                except (BlockingIOError, OSError):
                    continue

                if sock is self._nl_sock:
                    ifname = self._parse_netlink_ifname(data)
                    if ifname:
                        self._debounced_check_network()
                elif sock is self._uevent_sock:
                    if self._is_usb_uevent(data):
                        self._debounced_check_usb()

        self._close_sockets()

    # ---------- Public API ----------

    def start(self) -> None:
        if self._thread is not None and self._thread.is_alive():
            return
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._listen_loop, daemon=True)
        self._thread.start()

    def stop(self) -> None:
        self._stop_event.set()
        if self._thread is not None:
            self._thread.join(timeout=3)
            self._thread = None
        self._close_sockets()
        with self._debounce_lock:
            if self._net_debounce_timer:
                self._net_debounce_timer.cancel()
                self._net_debounce_timer = None
            if self._usb_debounce_timer:
                self._usb_debounce_timer.cancel()
                self._usb_debounce_timer = None

    @property
    def enabled(self) -> bool:
        with self._lock:
            return self._enabled

    def set_enabled(self, value: bool) -> None:
        with self._lock:
            self._enabled = value
            self._config["enabled"] = value
        self._save_config()

    def check_now(self) -> Dict[str, Any]:
        """由 API 触发一次强制检测（对比快照并返回变化）"""
        net_changes = self._check_network_now() if self._enabled else []
        usb_changes = self._check_usb_now() if self._enabled else []
        return {
            "enabled": self._enabled,
            "network": self._current_network,
            "usb": self._current_usb,
            "changes": net_changes + usb_changes,
            "timestamp": datetime.now().isoformat(),
        }

    def get_status(self) -> Dict[str, Any]:
        with self._lock:
            return {
                "enabled": self._enabled,
                "network": self._current_network,
                "usb": self._current_usb,
                "timestamp": datetime.now().isoformat(),
            }

    def list_logs(self) -> List[Dict[str, Any]]:
        logs = []
        try:
            for f in sorted(self.log_dir.glob("port_monitor_*.jsonl"), reverse=True):
                stat = f.stat()
                logs.append({
                    "filename": f.name,
                    "size": stat.st_size,
                    "mtime": datetime.fromtimestamp(stat.st_mtime).isoformat(),
                    "path": str(f),
                })
        except Exception:
            pass
        return logs
