#!/usr/bin/env python3
"""
Robust Apt/Dpkg monitor.
Triggers on:
- dpkg status changes
- dpkg transaction files
- apt lists updates
Debounced to fire once per update.
"""

import os
import time
import threading
from typing import Callable
import psutil
from logging_config import logger
from gi.repository import GLib, Gio

_active_monitors = []


class AptCacheMonitor:
    """Monitor dpkg + apt lists and call on_change once per update."""

    # Dedup state
    LOG_DEDUP_MS = 1000  # 1 second window

    # dpkg debouncer
    DPKG_LOG_WINDOW_MS = 1500  # 1.5 seconds

    DEBOUNCE_MS = 1500  # 1.5 seconds

    def __init__(self, on_change: Callable[[], None]):
        self.on_change = on_change
        self._monitor_handles = []
        self._last_trigger = 0

        self._last_log = {}
        self._last_dpkg_log = 0

        self.paths = [
            "/var/lib/dpkg/",       # Watch ALL dpkg files (status, info/, updates/, etc.)
            "/var/lib/apt/lists/",  # Watch ALL apt list files
        ]

        self._waiting_for_lock = False

    # -----------------------------
    # Event handler
    # -----------------------------
    @staticmethod
    def _apt_is_locked() -> bool:
        """Check if dpkg, apt, or apt-get are running."""
        for proc in psutil.process_iter(['name']):
            try:
                if proc.info['name'] in ('dpkg', 'apt', 'apt-get'):
                    return True
            except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                continue
        return False

    def _on_changed(self, _monitor, file, _other_file, event_type):
        path = file.get_path()
        if not path:
            return

        # Accept only meaningful events
        if event_type not in (
            Gio.FileMonitorEvent.CHANGES_DONE_HINT,
            Gio.FileMonitorEvent.CREATED,
            Gio.FileMonitorEvent.DELETED,
        ):
            # Allow apt lists ATTRIBUTE_CHANGED
            if not (path.startswith("/var/lib/apt/lists/") and
                    event_type == Gio.FileMonitorEvent.ATTRIBUTE_CHANGED):
                return

        # Already waiting → ignore
        if self._waiting_for_lock:
            return

        self._waiting_for_lock = True
        logger.info("APT/DPKG change detected → waiting for transaction to finish")

        def wait_for_idle():
            try:
                while self._apt_is_locked():
                    time.sleep(0.5)
                time.sleep(1.0)
                GLib.idle_add(self.on_change)
            finally:
                self._waiting_for_lock = False

        threading.Thread(target=wait_for_idle, daemon=True).start()

    # -----------------------------
    # Start monitoring
    # -----------------------------
    def start(self):
        """ Start monitoring. """
        for path in self.paths:
            file_obj = Gio.File.new_for_path(path)

            # File or directory?
            if os.path.isdir(path):
                monitor = file_obj.monitor_directory(Gio.FileMonitorFlags.NONE, None)
            else:
                monitor = file_obj.monitor_file(Gio.FileMonitorFlags.NONE, None)

            logger.info("Monitoring %s", path)
            _active_monitors.append(monitor)
            monitor.connect("changed", self._on_changed)
            self._monitor_handles.append(monitor)

    # -----------------------------
    # Stop monitoring
    # -----------------------------
    def stop(self):
        """ Stop monitoring. """
        for m in self._monitor_handles:
            try:
                _active_monitors.remove(m)
            except ValueError:
                pass
        self._monitor_handles.clear()

    def _log_once_file(self, path, message):
        now = time.time() * 1000
        last = self._last_log.get(path, 0)

        if now - last > self.LOG_DEDUP_MS:
            logger.debug(message)
            self._last_log[path] = now

    def _log_first_dpkg_event(self, path, ev_name):
        now = time.time() * 1000
        if now - self._last_dpkg_log > self.DPKG_LOG_WINDOW_MS:
            logger.debug("GIO event: %s path=%s", ev_name, path)
            self._last_dpkg_log = now
