#!/usr/bin/env python3
"""
Packages tab for SolydXK System

Author: Arjen Balfoort
Date: 2026-02-22
"""

import os
import gettext
import queue
import threading

from packages import (
    AptAction,
    PackageFilter,
    HeldBackPackages,
    RemovablePackages,
    filter_packages
)
from utils import (
    clear_listbox,
    show_dialog,
    DialogIcon,
    is_dpkg_locked,
    SudoCommandThread,
    LogLevel,
    DebSoftwareApp,
    LogLevel_To_DialogIcon
)

import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, GLib


# i18n: http://docs.python.org/3/library/gettext.html
_ = gettext.translation('solydxk-system', fallback=True).gettext


class TabPackages:
    """
    Packages tab for SolydXK System
    """
    def __init__(self, app: DebSoftwareApp) -> None:
        self.app: DebSoftwareApp = app
        self.held_back_packages_class: HeldBackPackages = HeldBackPackages(self.app)
        self.removable_packages_class: RemovablePackages = RemovablePackages(self.app)
        self.held_back_packages: list[str] = []
        self.installed_packages: list[str] = []
        self.removable_packages: list[str] = []

    def get_data(self):
        """ Get held back and installed packages """
        if not self.installed_packages:
            self.installed_packages = self.held_back_packages_class.get_installed_packages()
            # Get removable packages takes a while
            self._set_removable_packages()
        if not self.held_back_packages:
            self.held_back_packages = self.held_back_packages_class.get_held_back_packages()

    def setup_packages_tab(self) -> None:
        """
        Set up the packages tab.
        """
        # Reset the progress bar
        self.app.progressbar.set_fraction(0)

        # Start the spinner
        self.app.spinner.set_visible(True)
        self.app.spinner.start()

        # Get data
        data = threading.Thread(target=self.get_data)
        data.start()

        def check_thread_done():
            if data.is_alive():
                return True  # Keep checking
            else:
                # Thread is done, stop spinner and update UI
                self.app.spinner.set_visible(False)
                self.app.spinner.stop()

                # Connect to the 'toggled' signal of the select all checkbox
                self.app.chk_select_all.connect('toggled',
                                                        self.on_select_all_toggled)

                # Fill the cleanup listbox
                self._fill_listbox(self.app.listbox_cleanup,
                                   self.removable_packages,
                                   "cleanup")
                self._fill_listbox(self.app.listbox_hold_back,
                                   self.held_back_packages,
                                   "hold_back")

                # Attach completion to the entry
                self._update_list_store()
                self.app.package_search.connect('icon-release',
                                                        self.on_clear_package_search)

                # Add save buttons
                self.app.mark_button.connect("clicked",
                                                     self.on_mark_clicked)

                self.app.unmark_button.connect("clicked",
                                                       self.on_unmark_clicked)

                self.app.cleanup_button.connect("clicked",
                                                        self.on_cleanup_clicked,)

                return False  # Stop checking

        GLib.idle_add(check_thread_done)

    @staticmethod
    def on_clear_package_search(entry: Gtk.Entry,
                                icon_position: Gtk.EntryIconPosition) -> None:
        """
        Handle the icon-release event for the package search entry.
        """
        if icon_position == Gtk.EntryIconPosition.PRIMARY:
            return
        entry.set_text("")

    def on_mark_clicked(self, _button: Gtk.Button) -> None:
        """
        Handle the click event for the hold back button.
        """
        package = self.app.package_search.get_text()

        # Check if package is valid
        if not package or package not in self.installed_packages:
            show_dialog(_("Packages"),
                        _("Package %s not found", package),
                        self.app.window,
                        DialogIcon.ERROR)
            return

        # Hold back package
        self.app.logging.info("Hold back package: %s", package)
        self.held_back_packages_class.hold_back_package(package)

        # Remove package from removable_packages
        # and update the listbox
        if package in self.removable_packages:
            self.app.logging.info("Remove from removable packages: %s", package)
            self.removable_packages.remove(package)
            self.app.logging.debug("on_mark_clicked.self.removable_packages = %s",
                                           ', '.join(self.removable_packages))
            self._fill_listbox(self.app.listbox_cleanup, self.removable_packages, "cleanup")

        # Add package to held_back_packages and update the listbox
        self.held_back_packages.append(package)
        self.app.logging.debug("on_mark_clicked.self.held_back_packages = %s",
                                       ', '.join(self.held_back_packages))
        self._fill_listbox(self.app.listbox_hold_back, self.held_back_packages, "hold_back")

        # Update the list store for the package search
        self._update_list_store()

    def on_unmark_clicked(self, _button: Gtk.Button) -> None:
        """
        Handle the click event for the remove held back button.
        """
        packages = []
        row: Gtk.ListBoxRow | None = self.app.listbox_hold_back.get_first_child()
        while row is not None:
            checkbox: Gtk.CheckButton = row.get_child()
            if checkbox.get_active():
                self.app.logging.info("Unmark held-back package: %s", checkbox.package)
                packages.append(checkbox.package)
                self.held_back_packages_class.remove_held_back_package(checkbox.package)
            row = row.get_next_sibling()

        # Add package to installed_packages
        # Remove package from held_back_packages
        for package in packages:
            self.held_back_packages.remove(package)
        self.app.logging.debug("on_unmark_clicked.self.held_back_packages = %s",
                                       self.held_back_packages)

        # Update the held back list box
        self._update_list_store()
        self._fill_listbox(self.app.listbox_hold_back, self.held_back_packages, "hold_back")

        # Update the removable packages list box
        self._set_removable_packages()
        self._fill_listbox(self.app.listbox_cleanup, self.removable_packages, "cleanup")

    def on_select_all_toggled(self,
                              button: Gtk.CheckButton) -> None:
        """
        Handle the toggled event for the select all checkbox.
        """
        row: Gtk.ListBoxRow | None = self.app.listbox_cleanup.get_first_child()
        while row is not None:
            checkbox: Gtk.CheckButton = row.get_child()
            checkbox.set_active(button.get_active())
            row = row.get_next_sibling()

    def on_cleanup_clicked(self,
                           _button: Gtk.Button) -> None:
        """
        Handle the click event for the cleanup button.
        """
        message = ''
        cleanup_packages = []

        if is_dpkg_locked():
            self.app.logging.error("dpkg is locked by another process.")
            return

        row: Gtk.ListBoxRow | None = self.app.listbox_cleanup.get_first_child()
        while row is not None:
            checkbox: Gtk.CheckButton = row.get_child()
            if checkbox.get_active():
                cleanup_packages.append(checkbox.package)
            row = row.get_next_sibling()

        if not cleanup_packages:
            show_dialog(_("Packages"),
                        _("No packages selected for removal."),
                        self.app.window,
                        DialogIcon.WARNING)
            return

        self.app.logging.debug("Cleanup packages: %s", ', '.join(cleanup_packages))

        # Filter the packages to only include installed packages
        packages = filter_packages(cleanup_packages,
                                   PackageFilter.INSTALLED,
                                   self.app.logging)

        # pkexec will prompt for root password via polkit
        command = ["pkexec",
                   "--disable-internal-agent",
                   "python3",
                   os.path.join(self.app.script_dir, "packages_sudo.py"),
                   AptAction.REMOVE.value] + packages

        # Install selected packages and dependencies
        result_queue = queue.Queue()
        thread = SudoCommandThread(command,
                                   _("Packages"),
                                   result_queue,
                                   self.app.logging)
        thread.start()

        # Use a timeout to update the progress bar
        def update_progress():
            nonlocal message
            if not result_queue.empty():
                dict_info = result_queue.get()
                level = dict_info['level']
                message = dict_info.get('message')

                if level == LogLevel.ERROR:
                    # An error occurred
                    show_dialog(_("Packages"),
                                message,
                                self.app.window,
                                DialogIcon.ERROR)
                    self.app.progressbar.set_fraction(0)
                    self.app.progressbar_label.set_text("")
                    return False

                self.app.progressbar.set_fraction(dict_info['progress'] / 100)
                self.app.progressbar_label.set_text(message)

            if not thread.is_alive() and result_queue.empty():
                if level != LogLevel.CANCELED:
                    # Update listbox
                    for package in cleanup_packages:
                        if package in self.removable_packages:
                            self.removable_packages.remove(package)
                    self._fill_listbox(self.app.listbox_cleanup, self.removable_packages, "cleanup")
                    self.app.chk_select_all.set_active(False)

                # Show dialog
                if level == LogLevel.SUCCESS:
                    message = _("Packages have been removed successfully.")
                show_dialog(_("Packages"),
                            message,
                            self.app.window,
                            LogLevel_To_DialogIcon.get(level, DialogIcon.INFO))
                self.app.progressbar_label.set_text("")

                # All done
                return False

            # Keep going
            return True

        # Add the timeout callback (runs every 100ms)
        GLib.timeout_add(100, update_progress)

    def _set_removable_packages(self) -> list[str]:
        self.removable_packages = self.removable_packages_class.get_removable_packages()

    def _update_list_store(self) -> None:
        # Create a ListStore and populate it
        list_store = Gtk.ListStore(str)
        for item in self.installed_packages:
            list_store.append([item])

        # Set up completion
        # NOTE: Gtk.EntryCompletion is deprecated in GTK4.10+ and will be removed in GTK5.
        #       For GTK5, replace with Gtk.DropDown + Gtk.SearchEntry or a new API.
        completion = Gtk.EntryCompletion()
        completion.set_model(list_store)
        completion.set_text_column(0)
        completion.set_inline_completion(True)
        completion.set_popup_completion(True)

        self.app.package_search.set_completion(completion)

    def _fill_listbox(self,
                      listbox: Gtk.ListBox,
                      packages: list[str],
                      listbox_name: str) -> None:
        """
        Fill the listbox with the given data.

        Args:
            listbox (Gtk.ListBox): The listbox to fill.
            data (list[str]): The data to fill the listbox with.
            listbox_name (str): Name of the listbox. Used for logging.
        """
        clear_listbox(listbox)

        # Create a row for each package name
        for package in packages:
            self.app.logging.debug("Add package to %s: %s", listbox_name, package)
            row = Gtk.ListBoxRow()

            # Add checkbox
            checkbox = Gtk.CheckButton()
            checkbox.package = package
            checkbox.set_label(package)
            row.set_child(checkbox)

            listbox.append(row)
