# printer_status_circle.py
# PATCH: Widget PySide6 untuk ikon status printer (on/off) mengikuti setting printer default dan polling status hardware thermal
# edited by glg

import os
import time
import threading
from PySide6.QtWidgets import QLabel
from PySide6.QtGui import QPixmap, QPainter
from PySide6.QtCore import Qt, Signal
# // edited by glg (15:20 WIB, 2026-02-11)
# // change: Gunakan helper resource writable eksplisit untuk ikon runtime.
# // technical rationale: Ikon status dipakai dari APPDATA/resources agar konsisten dengan mekanisme seed/update aset.
from pypos.core.utils.path_utils import get_app_data_resource_dir
# // edited by glg (19:35 WIB, 2026-02-06)
# // change: Ganti load_config menjadi read_config.
# // technical rationale: load_config dihapus; konfigurasi wajib dibaca dari config.json.
from pypos.core.utils.config_utils import read_config
from pypos.core.utils.ui_image_asset_utils import load_best_scaled_pixmap
from pypos.core.utils.ui_scale_runtime import register_ui_scale_listener, scale_ui_px
from pypos.core.utils.worker_pool_utils import submit_ui_periodic_task
from pypos.modules.printer.services.printer_settings_facade_service import PrinterSettingsFacadeService

from PySide6.QtPrintSupport import QPrinterInfo

class PrinterStatusCircle(QLabel):
    # edited by glg
    # Signal payload hasil probe printer dari worker thread ke UI thread.
    status_payload_ready = Signal(object)

    def __init__(self, parent=None, box_size=None, icon_size=None):
        super().__init__(parent)
        logo_dir = os.path.join(get_app_data_resource_dir(), "icons")
        self.icon_on_path = os.path.join(logo_dir, "icon-printing-on.png")
        self.icon_off_path = os.path.join(logo_dir, "icon-printing-off.png")
        # // edited by glg (14:50 WIB, 2026-02-03)
        # // change: Ambil ukuran ikon status dari config bila tidak diinjeksikan.
        # // technical rationale: Ikon printer mengikuti ukuran dashboard agar konsisten.
        config = read_config()
        if box_size is None:
            box_size = config.get("dashboard_status_icon_box", 28)
        if icon_size is None:
            icon_size = config.get("dashboard_status_icon_size", 20)
        try:
            box_size = int(box_size)
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            box_size = 28
        try:
            icon_size = int(icon_size)
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            icon_size = 20
        if box_size < 1:
            box_size = 28
        if icon_size < 1:
            icon_size = 20
        if icon_size > box_size:
            icon_size = box_size
        self._base_box_size = box_size
        self._base_icon_size = icon_size
        self._box_size = 0
        self._icon_size = 0
        # edited by glg
        # Polling status printer diperlambat untuk mengurangi beban probing OS/WMI di UI thread.
        try:
            poll_interval_ms = int(config.get("printer_status_poll_interval_ms", 10000))
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            poll_interval_ms = 10000
        self._status_poll_interval_ms = max(3000, poll_interval_ms)
        try:
            wmi_probe_every = int(config.get("printer_status_wmi_probe_every", 3))
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            wmi_probe_every = 3
        self._wmi_probe_every = max(1, wmi_probe_every)
        # Mulai dari nilai akhir agar probing WMI tetap terjadi pada siklus awal.
        self._wmi_probe_counter = self._wmi_probe_every - 1
        self.printer_settings_facade_service = PrinterSettingsFacadeService()
        # PATCH[FrontEndAgent|2025-12-02]: Container lebih besar untuk prevent clipping
        self._apply_scaled_metrics()
        self.setAlignment(Qt.AlignCenter)
        self.printer_name = None
        self._printing_on = False
        # edited by glg
        # Guard probe async agar timer status printer tidak membuat worker overlap.
        self._status_probe_lock = threading.Lock()
        self._status_probe_inflight = False
        self._polling_enabled = True
        self._last_status_signature = None
        self.status_payload_ready.connect(self._on_status_payload_ready, Qt.QueuedConnection)
        register_ui_scale_listener(self._on_runtime_scale_changed)
        self.update_status()
        # edited by glg
        # Timer polling status printer menggunakan interval konfigurasi.
        from PySide6.QtCore import QTimer
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_status)
        self.timer.start(self._status_poll_interval_ms)

    # edited by glg
    # Orchestrator ikon dashboard bisa menonaktifkan polling internal printer
    # agar WiFi/Printer/Scanner berjalan pada satu jalur timer yang sama.
    def set_polling_enabled(self, enabled):
        self._polling_enabled = bool(enabled)
        timer = getattr(self, "timer", None)
        if timer is None:
            return
        if self._polling_enabled:
            if not timer.isActive():
                timer.start(self._status_poll_interval_ms)
            return
        if timer.isActive():
            timer.stop()

    # edited by glg
    # Expose payload probe agar bisa dipanggil orchestrator eksternal.
    def get_status_payload_snapshot(self):
        return self._probe_status_payload()

    def _get_printer_online_status(self, printer_name: str):
        # // edited by glg (18:32 WIB, 2026-01-17)
        # // change: Tambahkan cek status printer via API Windows jika tersedia.
        # // technical rationale: Deteksi offline meski driver masih terpasang di OS.
        try:
            import win32print  # type: ignore
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return None
        try:
            handle = win32print.OpenPrinter(printer_name)
            info = win32print.GetPrinter(handle, 2)
            status = info.get("Status", 0)
            # // edited by glg (19:12 WIB, 2026-01-17)
            # // change: Cek atribut WORK_OFFLINE dari driver.
            # // technical rationale: Sebagian driver menandai offline lewat atribut, bukan status.
            attributes = info.get("Attributes", 0)
            win32print.ClosePrinter(handle)
            if attributes & win32print.PRINTER_ATTRIBUTE_WORK_OFFLINE:
                return False
            offline_flags = (
                win32print.PRINTER_STATUS_OFFLINE
                | win32print.PRINTER_STATUS_NOT_AVAILABLE
                | win32print.PRINTER_STATUS_ERROR
                | win32print.PRINTER_STATUS_PAPER_OUT
                | win32print.PRINTER_STATUS_PAUSED
            )
            if status & offline_flags:
                return False
            return True
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return None

    def _get_printer_wmi_status(self, printer_name: str):
        # // edited by glg (19:12 WIB, 2026-01-17)
        # // change: Tambahkan pengecekan status printer via WMI.
        # // technical rationale: WMI dapat memberi status lebih akurat dibanding spooler cache.
        try:
            import win32com.client  # type: ignore
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return None
        try:
            locator = win32com.client.Dispatch("WbemScripting.SWbemLocator")
            service = locator.ConnectServer(".", "root\\cimv2")
            safe_name = printer_name.replace("\\", "\\\\").replace("'", "''")
            query = (
                "SELECT WorkOffline, PrinterStatus "
                f"FROM Win32_Printer WHERE Name = '{safe_name}'"  # nosec B608
            )
            printers = service.ExecQuery(query)
            for printer in printers:
                work_offline = bool(getattr(printer, "WorkOffline", False))
                status = int(getattr(printer, "PrinterStatus", 0) or 0)
                if work_offline:
                    return False
                if status in (7, 8, 9, 11):
                    return False
                return True
            return None
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return None

    # edited by glg
    # Probe status printer dijalankan di worker thread agar UI thread tetap responsif.
    def _probe_status_payload(self):
        printer_name = None
        printing_on = False
        try:
            default_printer = self.printer_settings_facade_service.get_default_printer()
            printers = QPrinterInfo.availablePrinters()
            printer_names = [p.printerName() for p in printers]
            if default_printer and default_printer.get("name"):
                # // edited by glg (16:35 WIB, 2026-01-17)
                # // change: Fallback ke default OS jika default aplikasi tidak terdeteksi.
                # // technical rationale: Ikon status harus mengikuti printer aktif di sistem saat perangkat tersambung.
                printer_name = str(default_printer.get("name") or "").strip() or None
                printing_on = bool(printer_name and printer_name in printer_names)
                if not printing_on:
                    os_default = QPrinterInfo.defaultPrinter()
                    if os_default and not os_default.isNull():
                        os_name = os_default.printerName()
                        if os_name in printer_names:
                            printer_name = os_name
                            printing_on = True
            else:
                os_default = QPrinterInfo.defaultPrinter()
                if os_default and not os_default.isNull():
                    os_name = os_default.printerName()
                    if os_name in printer_names:
                        printer_name = os_name
                        printing_on = True
                else:
                    printer_name = None
                    printing_on = False
            # // edited by glg (18:32 WIB, 2026-01-17)
            # // change: Konfirmasi status printer menggunakan status OS jika tersedia.
            # // technical rationale: Printer bisa tetap terdaftar meski dimatikan.
            if printer_name and printing_on:
                online_status = self._get_printer_online_status(printer_name)
                if online_status is False:
                    printing_on = False
                else:
                    # edited by glg
                    # Validasi WMI tidak perlu di setiap tick agar polling tetap ringan.
                    self._wmi_probe_counter = (self._wmi_probe_counter + 1) % self._wmi_probe_every
                    if self._wmi_probe_counter == 0:
                        wmi_status = self._get_printer_wmi_status(printer_name)
                        if wmi_status is False:
                            printing_on = False
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            printer_name = None
            printing_on = False
        return {
            "printer_name": printer_name,
            "printing_on": bool(printing_on),
        }

    def update_status(self):
        with self._status_probe_lock:
            if self._status_probe_inflight:
                return
            self._status_probe_inflight = True
        started_event = threading.Event()

        def _worker():
            started_event.set()
            started_at = time.perf_counter()
            payload = self._probe_status_payload()
            payload["elapsed_ms"] = (time.perf_counter() - started_at) * 1000.0
            try:
                self.status_payload_ready.emit(payload)
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                with self._status_probe_lock:
                    self._status_probe_inflight = False

        # edited by glg
        # Gunakan worker pool bersama agar polling periodik tidak membuat thread baru per tick.
        try:
            future = submit_ui_periodic_task(_worker)
            if future is None:
                with self._status_probe_lock:
                    self._status_probe_inflight = False
                return

            # edited by glg
            # Fail-safe antrean penuh: pastikan inflight tidak terkunci
            # jika task probe tidak sempat dijalankan worker.
            def _on_done(_):
                if started_event.is_set():
                    return
                with self._status_probe_lock:
                    self._status_probe_inflight = False

            try:
                future.add_done_callback(_on_done)
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                if not started_event.is_set():
                    with self._status_probe_lock:
                        self._status_probe_inflight = False
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            with self._status_probe_lock:
                self._status_probe_inflight = False

    # edited by glg
    def _on_status_payload_ready(self, payload):
        with self._status_probe_lock:
            self._status_probe_inflight = False
        self.apply_status_payload(payload)

    # edited by glg
    # Apply payload dari worker internal maupun orchestrator eksternal.
    # Skip render bila state tidak berubah agar callback periodik lebih ringan.
    def apply_status_payload(self, payload):
        data = payload if isinstance(payload, dict) else {}
        printer_name = str(data.get("printer_name") or "").strip() or None
        printing_on = bool(data.get("printing_on", False))
        signature = (printer_name or "", int(printing_on))
        if signature == self._last_status_signature:
            return
        self._last_status_signature = signature
        self.printer_name = printer_name
        self._printing_on = printing_on
        self.update_icon(printing_on)

    def update_icon(self, printing_on: bool):
        self._apply_scaled_metrics()
        icon_path = self.icon_on_path if printing_on else self.icon_off_path
        scaled = load_best_scaled_pixmap(
            icon_path,
            self._icon_size,
            self._icon_size,
            keep_aspect=True,
            smooth=True,
            prefer_svg=True,
        )
        if not scaled.isNull():
            # Buat pixmap baru dengan padding transparan
            final_pixmap = QPixmap(self._box_size, self._box_size)
            final_pixmap.fill(Qt.transparent)
            
            # Gambar scaled icon di tengah dengan offset untuk centering
            painter = QPainter(final_pixmap)
            x_offset = (self._box_size - scaled.width()) // 2
            y_offset = (self._box_size - scaled.height()) // 2
            painter.drawPixmap(x_offset, y_offset, scaled)
            painter.end()
            
            self.setPixmap(final_pixmap)
        else:
            self.setText("?")
        if printing_on and self.printer_name:
            tooltip = f"Printer Aktif: {self.printer_name}"
        elif self.printer_name:
            tooltip = f"Printer Offline: {self.printer_name}"
        else:
            tooltip = "Printer Tidak Terdeteksi"
        self.setToolTip(tooltip)

    # edited by glg
    # Base-size printer icon disimpan agar bisa direkalkulasi saat runtime scale berubah.
    def _apply_scaled_metrics(self):
        scaled_box = max(1, int(scale_ui_px(self._base_box_size)))
        scaled_icon = max(1, int(scale_ui_px(self._base_icon_size)))
        if scaled_icon > scaled_box:
            scaled_icon = scaled_box
        if scaled_box == self._box_size and scaled_icon == self._icon_size:
            return
        self._box_size = scaled_box
        self._icon_size = scaled_icon
        self.setFixedSize(int(self._base_box_size), int(self._base_box_size))

    # edited by glg
    def _on_runtime_scale_changed(self, _scale):
        self.update_icon(bool(self._printing_on))
