# -*- coding: utf-8 -*-
"""
Netzwerk-Scanner-Modul fuer das SAWAS Dashboard.

Features:
- Geraete im lokalen Subnetz finden (Ping-Sweep + ARP + Reverse-DNS)
- Auto-Scan alle 30 Minuten + manueller Klick
- bekannte Geraete in network_devices.json speichern
- Eigene Bezeichnung pro Geraet vergeben
- Offline-Geraete grau mit "weg seit X"
- Online-Geraete pulsieren gruen
- Pop-up zentral auf dem Desktop bei NEU entdeckten Geraeten
- Aktives WLAN + verfuegbare WLANs (netsh)
- Mit anderem WLAN verbinden (gespeichertes Profil oder neue Eingabe)
- Fallback: Windows-WLAN-Liste oeffnen
"""
import time

from PyQt5.QtCore import (
    Qt, QTimer, QPropertyAnimation, QEasingCurve, pyqtSignal, pyqtProperty,
    QSize, QUrl
)
from PyQt5.QtGui import QColor, QPainter, QBrush, QPen, QFont, QCursor, QDesktopServices
from PyQt5.QtWidgets import (
    QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QScrollArea,
    QWidget, QLineEdit, QCheckBox, QMessageBox, QInputDialog, QSizePolicy,
    QToolButton, QMenu, QAction
)

from modules.base import DashboardModule
from modules._netscan_storage import NetworkStore
from modules._netscan_core import NetworkScanner, ping_host
from modules._netscan_dialogs import (
    NewDeviceDialog, WifiConnectDialog, DeviceNotesDialog, CredentialOverlay
)
from modules import _wifi_helper as wifi


AUTO_SCAN_INTERVAL_MIN = 30
WIFI_REFRESH_INTERVAL_S = 30


# ===========================================================================
# Status-LED mit Pulse-Animation
# ===========================================================================
class StatusLED(QWidget):
    """Kleiner LED-Punkt. Gruen + pulsierend wenn online, grau statisch sonst."""

    SIZE = 14

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFixedSize(self.SIZE, self.SIZE)
        self._online = False
        self._pulse  = 1.0
        self._anim = QPropertyAnimation(self, b"pulseValue", self)
        self._anim.setDuration(1300)
        self._anim.setStartValue(0.35)
        self._anim.setEndValue(1.0)
        self._anim.setEasingCurve(QEasingCurve.InOutSine)
        self._anim.setLoopCount(-1)

    def setOnline(self, on: bool):
        if on == self._online:
            return
        self._online = on
        if on:
            self._anim.start()
        else:
            self._anim.stop()
            self._pulse = 1.0
        self.update()

    def getPulse(self) -> float:
        return self._pulse

    def setPulse(self, v: float):
        self._pulse = float(v)
        self.update()

    pulseValue = pyqtProperty(float, getPulse, setPulse)

    def paintEvent(self, e):
        p = QPainter(self)
        p.setRenderHint(QPainter.Antialiasing)
        r = self.rect().adjusted(2, 2, -2, -2)
        if self._online:
            # Aussenring (glow)
            glow = QColor(124, 255, 0, int(110 * self._pulse))
            p.setPen(Qt.NoPen)
            p.setBrush(QBrush(glow))
            p.drawEllipse(self.rect())
            # Kern
            core = QColor(124, 255, 0)
            p.setBrush(QBrush(core))
            p.drawEllipse(r)
        else:
            p.setPen(Qt.NoPen)
            p.setBrush(QBrush(QColor(110, 110, 130)))
            p.drawEllipse(r)


# ===========================================================================
# Geraete-Karte (eine Zeile pro Host)
# ===========================================================================
class DeviceCard(QFrame):
    pingRequested   = pyqtSignal(str)   # IP
    renameRequested = pyqtSignal(str)   # key
    removeRequested = pyqtSignal(str)   # key
    notesRequested  = pyqtSignal(str)   # key
    webRequested    = pyqtSignal(str)   # key

    def __init__(self, record: dict, parent=None):
        super().__init__(parent)
        self._key  = record["_key"]
        self._ip   = record["ip"]
        self._web_url: str | None = None
        self.setFrameShape(QFrame.NoFrame)
        self.setStyleSheet("""
            DeviceCard {
                background: rgba(255,255,255,0.025);
                border: 1px solid rgba(0,229,255,0.15);
                border-radius: 8px;
            }
            DeviceCard[online="true"] {
                border-color: rgba(124,255,0,0.45);
                background: rgba(124,255,0,0.06);
            }
            QLabel { color: #E8F4FF; }
            QLabel#name   { font-weight: 700; font-size: 10pt; color: #FFFFFF; }
            QLabel#meta   { color: #9EE6FF; font-size: 8pt; }
            QLabel#offline-since { color: #FF9F66; font-size: 8pt; }
            QLabel[offline="true"]#name { color: #909090; }
            QToolButton {
                color: #C0E8F2; background: transparent; border: none;
                font-size: 11pt; padding: 0 4px;
            }
            QToolButton:hover { color: #FFFFFF; }
            QToolButton#web-on  { color: #7CFF00; }
            QToolButton#web-on:hover  { color: #FFFFFF; }
            QToolButton#web-off { color: #6FA9C0; }
            QToolButton#web-off:hover { color: #00E5FF; }
        """)
        lay = QHBoxLayout(self)
        lay.setContentsMargins(8, 6, 6, 6)
        lay.setSpacing(8)

        self.led = StatusLED()
        lay.addWidget(self.led, 0, Qt.AlignTop | Qt.AlignVCenter)

        info_col = QVBoxLayout()
        info_col.setSpacing(0)
        info_col.setContentsMargins(0, 0, 0, 0)

        self.lbl_name = QLabel()
        self.lbl_name.setObjectName("name")
        info_col.addWidget(self.lbl_name)

        self.lbl_meta = QLabel()
        self.lbl_meta.setObjectName("meta")
        info_col.addWidget(self.lbl_meta)

        lay.addLayout(info_col, 1)

        self.btn_web = QToolButton()
        self.btn_web.setObjectName("web-off")
        self.btn_web.setText("↗")
        self.btn_web.setToolTip("Im Browser oeffnen (http://IP versuchen)")
        self.btn_web.setCursor(QCursor(Qt.PointingHandCursor))
        self.btn_web.clicked.connect(self._open_web)
        lay.addWidget(self.btn_web)

        self.btn_ping = QToolButton()
        self.btn_ping.setText("⟳")
        self.btn_ping.setToolTip("Jetzt anpingen")
        self.btn_ping.setCursor(QCursor(Qt.PointingHandCursor))
        self.btn_ping.clicked.connect(lambda: self.pingRequested.emit(self._ip))
        lay.addWidget(self.btn_ping)

        self.btn_menu = QToolButton()
        self.btn_menu.setText("⋯")
        self.btn_menu.setToolTip("Mehr")
        self.btn_menu.setCursor(QCursor(Qt.PointingHandCursor))
        self.btn_menu.clicked.connect(self._show_menu)
        lay.addWidget(self.btn_menu)

        self.update_from_record(record)

    def _show_menu(self):
        m = QMenu(self)
        a_rename = QAction("Bezeichnung ändern …", self)
        a_rename.triggered.connect(lambda: self.renameRequested.emit(self._key))
        m.addAction(a_rename)
        a_notes = QAction("Zugangsdaten / Notizen …", self)
        a_notes.triggered.connect(lambda: self.notesRequested.emit(self._key))
        m.addAction(a_notes)
        m.addSeparator()
        a_remove = QAction("Aus Liste entfernen", self)
        a_remove.triggered.connect(lambda: self.removeRequested.emit(self._key))
        m.addAction(a_remove)
        m.exec_(self.btn_menu.mapToGlobal(self.btn_menu.rect().bottomLeft()))

    def _open_web(self):
        # Das Modul-Objekt entscheidet, ob es das Credential-Overlay zeigt -
        # daher nur das Signal senden, statt selbst zu oeffnen.
        self.webRequested.emit(self._key)

    def setWebUrl(self, url: str | None):
        self._web_url = url
        if url:
            self.btn_web.setObjectName("web-on")
            self.btn_web.setText("🌐")
            self.btn_web.setToolTip(f"Webseite oeffnen: {url}")
        else:
            self.btn_web.setObjectName("web-off")
            self.btn_web.setText("↗")
            self.btn_web.setToolTip("Im Browser oeffnen (http://IP versuchen)")
        # Style neu anwenden, weil setObjectName den Selector aendert
        self.btn_web.style().unpolish(self.btn_web)
        self.btn_web.style().polish(self.btn_web)

    def update_from_record(self, rec: dict):
        self._ip = rec["ip"]
        online = bool(rec.get("is_online"))
        self.led.setOnline(online)
        self.setWebUrl(rec.get("web_url"))

        name = rec.get("custom_name") or rec.get("hostname") or rec["ip"]
        self.lbl_name.setText(name)
        self.lbl_name.setProperty("offline", "false" if online else "true")
        self.lbl_name.style().unpolish(self.lbl_name)
        self.lbl_name.style().polish(self.lbl_name)

        meta_parts = []
        if name != rec["ip"]:
            meta_parts.append(rec["ip"])
        if rec.get("hostname") and rec.get("hostname") != name:
            meta_parts.append(rec["hostname"])
        if rec.get("mac"):
            meta_parts.append(rec["mac"])
        if not online:
            since = rec.get("last_seen_online", 0)
            meta_parts.append("offline seit " + _humanize_since(since))
        self.lbl_meta.setText(" · ".join(meta_parts))

        self.setProperty("online", "true" if online else "false")
        self.style().unpolish(self)
        self.style().polish(self)


def _humanize_since(ts: float) -> str:
    if not ts:
        return "unbekannt"
    delta = max(0, int(time.time() - ts))
    if delta < 60:    return f"{delta}s"
    if delta < 3600:  return f"{delta // 60} min"
    if delta < 86400: return f"{delta // 3600} h"
    return f"{delta // 86400} Tage"


# ===========================================================================
# Haupt-Modul
# ===========================================================================
class NetworkScannerModule(DashboardModule):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.store = NetworkStore()
        self._cards: dict[str, DeviceCard] = {}   # key -> Card
        self._scanner: NetworkScanner | None = None
        self._scan_running = False
        self._initial_scan_done = False
        self._cred_overlay: CredentialOverlay | None = None

        root = QVBoxLayout(self)
        root.setContentsMargins(2, 2, 2, 2)
        root.setSpacing(6)

        # ----- Kopfzeile: Subnet-Info + Scan-Knopf --------------------------
        head = QHBoxLayout()
        head.setSpacing(6)
        self.lbl_subnet = QLabel("Bereit zum Scan")
        self.lbl_subnet.setStyleSheet("color: #9EE6FF; font-size: 8pt; letter-spacing: .5px;")
        head.addWidget(self.lbl_subnet, 1)

        self.btn_scan = QPushButton("⟳  Scan")
        self.btn_scan.setCursor(QCursor(Qt.PointingHandCursor))
        self.btn_scan.setStyleSheet(self._btn_css())
        self.btn_scan.clicked.connect(self.start_scan)
        head.addWidget(self.btn_scan)
        root.addLayout(head)

        # ----- Such-/Filterzeile -------------------------------------------
        flt = QHBoxLayout()
        flt.setSpacing(6)
        self.edit_search = QLineEdit()
        self.edit_search.setPlaceholderText("Suchen … (Name, IP, Hostname)")
        self.edit_search.setStyleSheet("""
            QLineEdit {
                background: rgba(0,0,0,0.30);
                color: #E8F4FF;
                border: 1px solid rgba(0,229,255,0.25);
                border-radius: 6px;
                padding: 4px 8px;
                font-size: 9pt;
            }
            QLineEdit:focus { border-color: rgba(0,229,255,0.65); }
        """)
        self.edit_search.textChanged.connect(self._refresh_list)
        flt.addWidget(self.edit_search, 1)

        self.chk_online = QCheckBox("nur online")
        self.chk_online.setStyleSheet("color: #C0E8F2; font-size: 8pt;")
        self.chk_online.stateChanged.connect(self._refresh_list)
        flt.addWidget(self.chk_online)
        root.addLayout(flt)

        # ----- Geraete-Liste (Scroll) --------------------------------------
        self.scroll = QScrollArea()
        self.scroll.setWidgetResizable(True)
        self.scroll.setFrameShape(QFrame.NoFrame)
        self.scroll.setStyleSheet("""
            QScrollArea, QScrollArea > QWidget > QWidget { background: transparent; }
            QScrollBar:vertical { background: transparent; width: 8px; }
            QScrollBar::handle:vertical {
                background: rgba(0,229,255,0.25);
                border-radius: 4px;
                min-height: 30px;
            }
            QScrollBar::handle:vertical:hover { background: rgba(0,229,255,0.45); }
            QScrollBar::add-line, QScrollBar::sub-line { height: 0; }
        """)
        self.list_host = QWidget()
        self.list_lay = QVBoxLayout(self.list_host)
        self.list_lay.setContentsMargins(0, 0, 0, 0)
        self.list_lay.setSpacing(4)
        # Platzhalter, wenn nichts in der Liste ist
        self.lbl_empty = QLabel('Noch keine Geräte erfasst. Klick „Scan" oben rechts.')
        self.lbl_empty.setAlignment(Qt.AlignCenter)
        self.lbl_empty.setWordWrap(True)
        self.lbl_empty.setStyleSheet("color: #6FA9C0; padding: 18px 8px; font-size: 9pt;")
        self.list_lay.addWidget(self.lbl_empty)
        self.list_lay.addStretch(1)
        self.scroll.setWidget(self.list_host)
        self.scroll.setMinimumHeight(220)
        root.addWidget(self.scroll, 1)

        # ----- WLAN-Bereich -------------------------------------------------
        self.wifi_section = self._build_wifi_section()
        root.addWidget(self.wifi_section)

        # ----- Bestehende Geraete einsortieren ------------------------------
        self._refresh_list()

        # ----- Auto-Scan-Timer ---------------------------------------------
        self._auto_timer = QTimer(self)
        self._auto_timer.setInterval(AUTO_SCAN_INTERVAL_MIN * 60 * 1000)
        self._auto_timer.timeout.connect(self.start_scan)
        self._auto_timer.start()

        # Offline-Anzeige-Refresh (humanize_since aktualisieren)
        self._tick_timer = QTimer(self)
        self._tick_timer.setInterval(30_000)
        self._tick_timer.timeout.connect(self._refresh_offline_labels)
        self._tick_timer.start()

        # Erst-Scan kurz nach Modul-Start
        QTimer.singleShot(1500, self.start_scan)

        # WLAN-Status alle 30 s
        self._wifi_timer = QTimer(self)
        self._wifi_timer.setInterval(WIFI_REFRESH_INTERVAL_S * 1000)
        self._wifi_timer.timeout.connect(self._refresh_wifi_status)
        self._wifi_timer.start()
        QTimer.singleShot(400, self._refresh_wifi_status)

    # -----------------------------------------------------------------------
    # WLAN-Bereich aufbauen
    # -----------------------------------------------------------------------
    def _build_wifi_section(self) -> QFrame:
        wrap = QFrame()
        wrap.setStyleSheet("""
            QFrame { background: rgba(0,229,255,0.04);
                     border: 1px solid rgba(0,229,255,0.18);
                     border-radius: 8px; }
            QLabel { color: #E8F4FF; }
            QLabel#wifi-title { color: #9EE6FF; font-size: 8pt; letter-spacing: 1px; }
            QLabel#wifi-active { color: #FFFFFF; font-weight: 700; font-size: 10pt; }
            QLabel#wifi-meta   { color: #9EE6FF; font-size: 8pt; }
            QToolButton { color: #C0E8F2; background: transparent; border: none; padding: 0 6px; }
            QToolButton:hover { color: #FFFFFF; }
        """)
        lay = QVBoxLayout(wrap)
        lay.setContentsMargins(10, 8, 8, 8)
        lay.setSpacing(4)

        head = QHBoxLayout()
        head.setSpacing(6)
        title = QLabel("WLAN")
        title.setObjectName("wifi-title")
        head.addWidget(title, 1)

        self.btn_wifi_more = QToolButton()
        self.btn_wifi_more.setText("Verfuegbare ▾")
        self.btn_wifi_more.setCursor(QCursor(Qt.PointingHandCursor))
        self.btn_wifi_more.clicked.connect(self._toggle_wifi_list)
        head.addWidget(self.btn_wifi_more)

        self.btn_wifi_settings = QToolButton()
        self.btn_wifi_settings.setText("⚙")
        self.btn_wifi_settings.setToolTip("Windows-WLAN-Liste oeffnen")
        self.btn_wifi_settings.setCursor(QCursor(Qt.PointingHandCursor))
        self.btn_wifi_settings.clicked.connect(wifi.open_windows_wifi_flyout)
        head.addWidget(self.btn_wifi_settings)
        lay.addLayout(head)

        self.lbl_wifi_active = QLabel("Nicht verbunden")
        self.lbl_wifi_active.setObjectName("wifi-active")
        lay.addWidget(self.lbl_wifi_active)

        self.lbl_wifi_meta = QLabel("")
        self.lbl_wifi_meta.setObjectName("wifi-meta")
        lay.addWidget(self.lbl_wifi_meta)

        # ausklappbare Liste verfuegbarer Netze
        self.wifi_list = QWidget()
        self.wifi_list.setVisible(False)
        self.wifi_list_lay = QVBoxLayout(self.wifi_list)
        self.wifi_list_lay.setContentsMargins(0, 4, 0, 0)
        self.wifi_list_lay.setSpacing(2)
        lay.addWidget(self.wifi_list)

        return wrap

    def _toggle_wifi_list(self):
        if self.wifi_list.isVisible():
            self.wifi_list.setVisible(False)
            self.btn_wifi_more.setText("Verfuegbare ▾")
        else:
            self._populate_wifi_list()
            self.wifi_list.setVisible(True)
            self.btn_wifi_more.setText("Verfuegbare ▴")

    def _populate_wifi_list(self):
        # alte Eintraege weg
        while self.wifi_list_lay.count():
            it = self.wifi_list_lay.takeAt(0)
            w = it.widget()
            if w:
                w.deleteLater()

        nets = wifi.available_wifis()
        if not nets:
            lbl = QLabel("Keine Netze gefunden.")
            lbl.setStyleSheet("color: #9EE6FF; font-size: 8pt; padding: 4px 0;")
            self.wifi_list_lay.addWidget(lbl)
            return

        active = wifi.current_wifi()
        active_ssid = active["ssid"] if active else None
        profiles = wifi.saved_profiles()

        for n in nets[:12]:
            row = QFrame()
            row.setStyleSheet("""
                QFrame { background: rgba(255,255,255,0.02);
                         border: 1px solid rgba(0,229,255,0.10);
                         border-radius: 6px; }
                QLabel { color: #E8F4FF; font-size: 9pt; }
                QPushButton {
                    color: #001821;
                    background: qlineargradient(x1:0,y1:0, x2:1,y2:0,
                        stop:0 #00E5FF, stop:1 #7CFF00);
                    border: none; border-radius: 6px;
                    padding: 3px 10px; font-size: 8pt; font-weight: 700;
                }
                QPushButton:hover { color: #000; }
                QPushButton[active="true"] {
                    background: rgba(124,255,0,0.20);
                    color: #7CFF00;
                }
            """)
            rlay = QHBoxLayout(row)
            rlay.setContentsMargins(6, 4, 6, 4)
            rlay.setSpacing(6)

            name = QLabel(n["ssid"])
            rlay.addWidget(name, 1)

            sig = QLabel(f"{n['signal']}%")
            sig.setStyleSheet(f"color: {_signal_color(n['signal'])}; font-size: 8pt;")
            rlay.addWidget(sig)

            btn = QPushButton()
            if n["ssid"] == active_ssid:
                btn.setText("verbunden")
                btn.setProperty("active", "true")
                btn.setEnabled(False)
            else:
                btn.setText("verbinden")
                btn.clicked.connect(lambda _, s=n["ssid"]: self._connect_wifi(s, profiles))
            rlay.addWidget(btn)
            self.wifi_list_lay.addWidget(row)

    def _connect_wifi(self, ssid: str, profiles: set):
        # Wenn Profil bekannt: direkt verbinden
        if ssid in profiles:
            ok, msg = wifi.connect_known(ssid)
            if ok:
                self._toast(f"Verbinde mit {ssid} …")
            else:
                QMessageBox.warning(self, "WLAN", msg or "Verbindung fehlgeschlagen.")
            QTimer.singleShot(2500, self._refresh_wifi_status)
            return

        # Sonst Passwort abfragen
        dlg = WifiConnectDialog(ssid, parent=self)
        if dlg.exec_() != dlg.Accepted:
            return
        pw = dlg.password()
        ok, msg = wifi.connect_new(ssid, pw)
        if ok:
            self._toast(f"Verbinde mit {ssid} …")
        else:
            QMessageBox.warning(self, "WLAN", msg or "Verbindung fehlgeschlagen.")
        QTimer.singleShot(2500, self._refresh_wifi_status)

    def _refresh_wifi_status(self):
        cur = wifi.current_wifi()
        if cur:
            self.lbl_wifi_active.setText("● " + cur["ssid"])
            self.lbl_wifi_active.setStyleSheet("color: #7CFF00; font-weight: 700; font-size: 10pt;")
            meta = []
            if cur.get("signal") is not None:
                meta.append(f"Signal {cur['signal']} %")
            if cur.get("radio"):
                meta.append(cur["radio"])
            if cur.get("channel"):
                meta.append(f"Kanal {cur['channel']}")
            self.lbl_wifi_meta.setText(" · ".join(meta))
        else:
            self.lbl_wifi_active.setText("● Nicht verbunden")
            self.lbl_wifi_active.setStyleSheet("color: #FF6F66; font-weight: 700; font-size: 10pt;")
            self.lbl_wifi_meta.setText("")
        if self.wifi_list.isVisible():
            self._populate_wifi_list()

    # -----------------------------------------------------------------------
    # Scan-Steuerung
    # -----------------------------------------------------------------------
    def start_scan(self):
        if self._scan_running:
            return
        self._scan_running = True
        self.btn_scan.setText("⟳  …")
        self.btn_scan.setEnabled(False)
        self.lbl_subnet.setText("Scanne …")

        # Vor dem Scan: alle Geraete als "offline" markieren, der Scanner
        # frischt online-Eintraege wieder auf.
        known_ips = []
        for rec in self.store.all_devices():
            self.store.mark_offline(rec["_key"])
            ip = rec.get("ip")
            if ip:
                known_ips.append(ip)

        # Bekannte IPs (z.B. Kameras, die kein ICMP beantworten) zusaetzlich
        # per Web-Check pruefen lassen.
        self._scanner = NetworkScanner(also_check_ips=known_ips, parent=self)
        self._scanner.progress.connect(self._on_progress)
        self._scanner.deviceFound.connect(self._on_device_found)
        self._scanner.webChecked.connect(self._on_web_checked)
        self._scanner.scanFinished.connect(self._on_scan_finished)
        self._scanner.scanError.connect(self._on_scan_error)
        self._scanner.start()

    def _on_progress(self, done: int, total: int):
        self.lbl_subnet.setText(f"Scanne … {done}/{total}")

    def _on_device_found(self, ip, mac, hostname):
        rec, is_new = self.store.upsert(ip, mac, hostname, online=True)
        self._upsert_card(rec)
        # Erst nach dem ersten Komplett-Scan poppen "neue" Geraete als Popup auf,
        # sonst wuerde der allererste Scan-Lauf ein Pop-up pro Host abfeuern.
        if is_new and self._initial_scan_done:
            self._show_new_device_popup(rec)

    def _on_web_checked(self, ip, url):
        """Webserver-Erkennung nach dem Ping. Setzt die URL pro Geraet."""
        self.store.set_web_url(ip, url)
        for rec in self.store.all_devices():
            if rec["ip"] == ip:
                card = self._cards.get(rec["_key"])
                if card:
                    card.setWebUrl(url)
                break

    def _on_scan_finished(self, online_ips: list, subnet_str: str):
        self._scan_running = False
        self.btn_scan.setText("⟳  Scan")
        self.btn_scan.setEnabled(True)
        if subnet_str:
            self.lbl_subnet.setText(f"{len(online_ips)} online · {subnet_str}")
        else:
            self.lbl_subnet.setText("Einzel-Ping abgeschlossen")
        self._refresh_list()
        self.store.save()
        self._initial_scan_done = True

    def _on_scan_error(self, msg: str):
        self._scan_running = False
        self.btn_scan.setText("⟳  Scan")
        self.btn_scan.setEnabled(True)
        self.lbl_subnet.setText(msg)

    # -----------------------------------------------------------------------
    # Einzel-Ping (Klick auf ⟳ in einer Karte)
    # -----------------------------------------------------------------------
    def _single_ping(self, ip: str):
        # In Thread auslagern, damit das UI nicht blockt
        scan = NetworkScanner(single_ip=ip, parent=self)
        scan.deviceFound.connect(self._on_device_found)
        scan.webChecked.connect(self._on_web_checked)
        scan.scanFinished.connect(lambda online, _s: self._on_single_ping_done(ip, online))
        scan.start()
        # Halten, sonst wird das QThread-Objekt gleich GC'd
        self._single_scans = getattr(self, "_single_scans", [])
        self._single_scans.append(scan)

    def _on_single_ping_done(self, ip: str, online_ips: list):
        if not online_ips:
            # Offline markieren
            for rec in self.store.all_devices():
                if rec["ip"] == ip:
                    self.store.mark_offline(rec["_key"])
                    self._upsert_card(rec)
                    break
            self.store.save()
        self._refresh_list()

    # -----------------------------------------------------------------------
    # Karten-Verwaltung
    # -----------------------------------------------------------------------
    def _make_card(self, rec: dict) -> DeviceCard:
        card = DeviceCard(rec)
        card.pingRequested.connect(self._single_ping)
        card.renameRequested.connect(self._rename_device)
        card.removeRequested.connect(self._remove_device)
        card.notesRequested.connect(self._edit_notes)
        card.webRequested.connect(self._open_device_web)
        return card

    def _upsert_card(self, rec: dict):
        """
        Wird waehrend des Scans pro gefundenem Geraet aufgerufen.
        Legt eine Karte an oder aktualisiert sie. Position spielt hier keine
        Rolle - _refresh_list() ordnet am Ende des Scans sauber neu an.
        """
        key = rec["_key"]
        card = self._cards.get(key)
        if card:
            card.update_from_record(rec)
            # Falls die Karte aus irgendeinem Grund nicht (mehr) im Layout sitzt,
            # wieder einfuegen.
            if card.parent() is not self.list_host:
                self.list_lay.insertWidget(self.list_lay.count() - 1, card)
                card.setVisible(True)
            return
        card = self._make_card(rec)
        self._cards[key] = card
        # Vor Empty-Label + Stretch (die zwei letzten Layout-Items)
        self.list_lay.insertWidget(self.list_lay.count() - 2, card)
        # Empty-Label ausblenden, sobald ueberhaupt was sichtbar ist
        self.lbl_empty.setVisible(False)

    def _refresh_list(self):
        """
        Sortiert die Karten neu und wendet Filter (Suche, „nur online") an.
        Vorhandene Karten werden NICHT zerstoert, nur deren Position und
        Sichtbarkeit angepasst - das vermeidet Flackern und den frueheren Bug,
        dass Karten nach dem Scan aus dem Layout verschwanden.
        """
        records = self.store.all_devices()
        records.sort(key=lambda r: (
            not r.get("is_online"),
            (r.get("custom_name") or r.get("hostname") or r["ip"]).lower(),
        ))

        search      = self.edit_search.text().strip().lower()
        only_online = self.chk_online.isChecked()
        existing    = {r["_key"] for r in records}

        # 1) Karten loeschen, deren Record im Store nicht mehr existiert
        for key in list(self._cards.keys()):
            if key not in existing:
                c = self._cards.pop(key)
                self.list_lay.removeWidget(c)
                c.deleteLater()

        # 2) Alle bestehenden Karten aus dem Layout nehmen (Reihenfolge neu)
        for card in self._cards.values():
            self.list_lay.removeWidget(card)

        # 3) Karten in sortierter Reihenfolge wieder einfuegen + Filter
        visible_count = 0
        for i, rec in enumerate(records):
            key = rec["_key"]
            card = self._cards.get(key)
            if card is None:
                card = self._make_card(rec)
                self._cards[key] = card
            else:
                card.update_from_record(rec)

            # Filter pruefen
            visible = True
            if only_online and not rec.get("is_online"):
                visible = False
            if visible and search:
                hay = " ".join([
                    rec.get("custom_name", "") or "",
                    rec.get("hostname", "") or "",
                    rec.get("mac", "") or "",
                    rec["ip"],
                ]).lower()
                if search not in hay:
                    visible = False

            card.setVisible(visible)
            self.list_lay.insertWidget(i, card)
            if visible:
                visible_count += 1

        # 4) Empty-Label nur zeigen, wenn wirklich nichts sichtbar ist
        if visible_count == 0:
            if not records:
                self.lbl_empty.setText('Noch keine Geräte erfasst. Klick „Scan" oben rechts.')
            else:
                self.lbl_empty.setText("Keine Treffer für aktuellen Filter.")
            self.lbl_empty.setVisible(True)
        else:
            self.lbl_empty.setVisible(False)

    def _refresh_offline_labels(self):
        """Aktualisiert nur die 'offline seit X'-Texte, ohne neu zu scannen."""
        for rec in self.store.all_devices():
            if not rec.get("is_online"):
                card = self._cards.get(rec["_key"])
                if card:
                    card.update_from_record(rec)

    def _rename_device(self, key: str):
        rec = next((r for r in self.store.all_devices() if r["_key"] == key), None)
        if not rec:
            return
        current = rec.get("custom_name") or ""
        text, ok = QInputDialog.getText(
            self, "Bezeichnung",
            f"Bezeichnung fuer {rec['ip']}:",
            text=current
        )
        if ok:
            self.store.set_custom_name(key, text)
            rec["custom_name"] = text.strip()
            card = self._cards.get(key)
            if card:
                card.update_from_record(rec)

    def _remove_device(self, key: str):
        self.store.remove(key)
        card = self._cards.pop(key, None)
        if card:
            card.deleteLater()

    def _edit_notes(self, key: str):
        rec = next((r for r in self.store.all_devices() if r["_key"] == key), None)
        if not rec:
            return
        label = rec.get("custom_name") or rec.get("hostname") or rec["ip"]
        dlg = DeviceNotesDialog(label, rec["ip"], rec, parent=self)
        if dlg.exec_() == dlg.Accepted:
            user, pw, notes = dlg.values()
            self.store.set_credentials(key, user, pw, notes)

    def _open_device_web(self, key: str):
        """Knopf in der Karte: Webseite oeffnen + ggf. Credential-Overlay zeigen."""
        rec = next((r for r in self.store.all_devices() if r["_key"] == key), None)
        if not rec:
            return
        url = rec.get("web_url") or f"http://{rec['ip']}/"
        QDesktopServices.openUrl(QUrl(url))

        user  = (rec.get("username") or "").strip()
        pw    = rec.get("password") or ""
        notes = (rec.get("notes")    or "").strip()
        if user or pw or notes:
            if self._cred_overlay is None:
                self._cred_overlay = CredentialOverlay()
            label = rec.get("custom_name") or rec.get("hostname") or rec["ip"]
            self._cred_overlay.show_for(label, rec["ip"], user, pw, notes)

    # -----------------------------------------------------------------------
    # Popup fuer neue Geraete
    # -----------------------------------------------------------------------
    def _show_new_device_popup(self, rec: dict):
        dlg = NewDeviceDialog(
            rec["ip"], rec.get("mac"), rec.get("hostname"),
            parent=self
        )
        if dlg.exec_() == dlg.Accepted:
            name = dlg.custom_name()
            if name:
                self.store.set_custom_name(rec["_key"], name)
                rec["custom_name"] = name
                card = self._cards.get(rec["_key"])
                if card:
                    card.update_from_record(rec)

    # -----------------------------------------------------------------------
    # Helper
    # -----------------------------------------------------------------------
    def _btn_css(self) -> str:
        return ("QPushButton {"
                "  color: #001821;"
                "  background: qlineargradient(x1:0,y1:0, x2:1,y2:0,"
                "    stop:0 #00E5FF, stop:1 #7CFF00);"
                "  border: none; border-radius: 8px;"
                "  padding: 5px 14px; font-size: 9pt; font-weight: 700;"
                "}"
                "QPushButton:hover { color: #000; }"
                "QPushButton:disabled { background: rgba(255,255,255,0.05); color: #666; }")

    def _toast(self, text: str):
        self.lbl_subnet.setText(text)


def _signal_color(sig: int) -> str:
    if sig >= 70: return "#7CFF00"
    if sig >= 40: return "#FFD166"
    return "#FF6F66"


# ---------------------------------------------------------------------------
MODULE_TITLE = "Netzwerk-Scanner"
MODULE_CLASS = NetworkScannerModule
