#!/usr/bin/env python3
# vim:et:sta:sts=4:sw=4:ts=8:tw=79:

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib
from enum import Enum
import subprocess
import threading
import argparse
import logging
import pexpect
import os

# Internationalization
import locale
import gettext
locale.setlocale(locale.LC_ALL, '')
locale.bindtextdomain("salix-update-notifier", "/usr/share/locale")
gettext.bindtextdomain("salix-update-notifier", "/usr/share/locale")
gettext.textdomain("salix-update-notifier")
_ = gettext.gettext

SLAPT_GET_BIN = "/usr/sbin/slapt-get"
FLATPAK_BIN = "/usr/bin/flatpak"
SLAPT_GET_ICON = "package"
FLATPAK_ICON = "flatpak"

if os.geteuid() == 0:
    FLATPAK_INSTALLATION="--system"
else:
    FLATPAK_INSTALLATION="--user"

# Create a module-level logger (no output until configured)
logger = logging.getLogger(__name__)

#
# slapt-get-logic
#
def _get_slapt_get_upgrades():
    """
    Get a list of packages that can be upgraded along with their respective versions.

    Returns:
        list: A list of dictionaries containing the package name, old version, 
        and new version.
    """
    # slapt-get is only available if you're root
    if os.geteuid() != 0:
        return []
    # Run the command and capture its output
    command = f"{SLAPT_GET_BIN} --upgrade --simulate"
    new_env = dict(subprocess.os.environ)
    new_env['LANG'] = 'C.utf8'
    output = subprocess.check_output(command, shell=True, env=new_env).decode('utf-8')

    # Split the output into lines
    lines = output.splitlines()

    # Initialize an empty list to store the packages to be upgraded
    upgrades = []

    # Initialize a flag to indicate when we're parsing the upgrade lines
    parsing_upgrades = False

    # Iterate over each line in the output
    for line in lines:
        # Check if we're starting to parse the upgrades
        if line.startswith('After unpacking'):
            parsing_upgrades = True
            continue
        if line == "Done":
            break
        # If we're parsing upgrades and the line is not empty
        if parsing_upgrades and line.strip():
            # If the line contains 'is to be upgraded to version'
            if 'is to be upgraded to version' in line:
                # Extract the package name, old version, and new version
                package_info = line.split('is to be upgraded to version')
                package_name_version = package_info[0].strip().split('-')
                package_name = '-'.join(package_name_version[:-3])
                old_version = package_name_version[-3]
                new_version = package_info[1].strip().split('-')[0]
                upgrades.append({
                    'name': package_name,
                    'old_version': old_version,
                    'new_version': new_version,
                    'new_dep': False
                })
            elif line.endswith(' is to be installed'):
                package_info = line.split('is to be installed')
                package_name_version = package_info[0].strip().split('-')
                package_name = '-'.join(package_name_version[:-3])
                old_version = "(" +_("new") + ")"
                new_version = package_name_version[-3]
                upgrades.append({
                    'name': package_name,
                    'old_version': old_version,
                    'new_version': new_version,
                    'new_dep': True
                })
    return upgrades

#
# flatpak logic
#
def _get_installed_flatpaks():
    """
    Get a dictionary of all installed flatpaks. The key is a tuple of
    (flatpak_id, branch).
    """
    command = [FLATPAK_BIN, "list", FLATPAK_INSTALLATION, "--all", "--columns=application,branch,version"]
    new_env = dict(subprocess.os.environ)
    new_env['LANG'] = 'C.utf8'
    output = subprocess.check_output(command, env=new_env).decode("utf-8")
    lines = output.splitlines()
    installed_flatpaks = {}
    for line in lines:
        columns = line.split('\t')
        flatpak_id = columns[0]
        branch = columns[1]
        if len(columns) == 2:
            version = ''
        else:
            version = columns[2]
        installed_flatpaks[(flatpak_id, branch)] = {'old_version': version}
    return installed_flatpaks

def _get_remote_flatpaks():
    """
    Get a dictionary of all available flatpaks at the remote. The key is a
    tuple of (flatpak_id, branch).
    """
    command = [FLATPAK_BIN, "remote-ls", FLATPAK_INSTALLATION, "--all", "--columns=application,name,version,branch"]
    new_env = dict(subprocess.os.environ)
    new_env['LANG'] = 'C.utf8'
    output = subprocess.check_output(command, env=new_env).decode("utf-8")
    lines = output.splitlines()
    available_updates = {}
    for line in lines:
        columns = line.split('\t')
        available_updates[(columns[0], columns[3])] = {
                'name': columns[1],
                'new_version': columns[2]
                }
    return available_updates

def _get_flatpak_upgrades():
    """
    Get a list of flatpak apps that will be upgraded (or installed as
    dependencies.
    """
    new_env = dict(subprocess.os.environ)
    new_env['LANG'] = 'C.utf8'
    child = pexpect.spawn(f"{FLATPAK_BIN} update {FLATPAK_INSTALLATION}",
            env=new_env, encoding="utf-8", timeout=None)
    output_lines = []
    available_updates = []
    while True:
        try:
            i = child.expect([
                r"Do you want to install it\?.*",
                r"Proceed with these changes.*",
                pexpect.EOF,
                pexpect.TIMEOUT
            ])
            if i == 0:
                # flatpak asks "Do you want to install it?"
                child.sendline("y")
            elif i == 1:
                # flatpak asks "Proceed with these changes?"
                child.sendline("n")
            elif i == 2:
                # End of process
                break
            elif i == 3:
                # Timeout — optional handling
                break
            output_lines.extend(child.before.splitlines())
        except pexpect.exceptions.EOF:
            break
        except pexpect.exceptions.TIMEOUT:
            break
    # Combine all collected lines
    output = [line.strip() for line in output_lines if line.strip()]
    # Parse numbered entries
    for line in output:
        parts = line.split(None)
        if parts and parts[0].rstrip('.').isdigit():
            last_number = int(parts[0].rstrip('.'))
            available_updates.append({
                'flatpak_id': parts[1],
                'branch': parts[2],
                'action': parts[3]
            })
    installed_flatpaks = _get_installed_flatpaks()
    remote_flatpaks = _get_remote_flatpaks()
    for flatpak in available_updates:
        try:
            flatpak['old_version'] = installed_flatpaks[(flatpak['flatpak_id'], flatpak['branch'])]['old_version']
        except KeyError:
            flatpak['old_version'] = ''
        flatpak['name'] = remote_flatpaks[(flatpak['flatpak_id'], flatpak['branch'])]['name']
        if flatpak['action'] == 'i':
            flatpak['new_version'] = "(" + _("new") + ")"
        else:
            flatpak['new_version'] = remote_flatpaks[(flatpak['flatpak_id'], flatpak['branch'])]['new_version']
    return available_updates

#
# Just a helper enum
#
class PkgType(Enum):
    SLACK = 1
    FLATPAK = 2

#
# GUI
#
class SalixUpdateManager:

    def _add_listbox_entry(self, pkg, pkg_type):
        # Create row container
        row_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
        row_box.set_margin_start(8)
        row_box.set_margin_end(8)
        row_box.set_margin_top(4)
        row_box.set_margin_bottom(4)
        # Create icon - you can choose different sizes:
        # Gtk.IconSize.MENU (16px), Gtk.IconSize.SMALL_TOOLBAR (16px),
        # Gtk.IconSize.LARGE_TOOLBAR (24px), Gtk.IconSize.DND (32px)
        if pkg_type == PkgType.SLACK:
            icon = Gtk.Image.new_from_icon_name(SLAPT_GET_ICON, Gtk.IconSize.DND)
            label_text = f"{pkg['name']} {pkg['old_version']} -> {pkg['new_version']}"
        elif pkg_type == PkgType.FLATPAK:
            icon = Gtk.Image.new_from_icon_name(FLATPAK_ICON, Gtk.IconSize.DND)
            if pkg['old_version'] == '':
                label_text = f"{pkg['name']} ({pkg['flatpak_id']}) [{pkg['branch']}] {pkg['new_version']}"
            else:
                label_text = f"{pkg['name']} ({pkg['flatpak_id']}) {pkg['old_version']} ({pkg['branch']}) -> {pkg['new_version']}"
        else:
            return
        label = Gtk.Label(label=label_text)
        label.set_xalign(0)
        label.set_hexpand(True)
        # Assemble row
        row_box.pack_start(icon, False, False, 0)
        row_box.pack_start(label, True, True, 0)
        row = Gtk.ListBoxRow()
        row.add(row_box)
        self.list_box.add(row)

    def gtk_main_quit(self, widget, data=None):
        Gtk.main_quit()

    def on_button_OK_clicked(self, widget, data=None):
        self.stack.set_visible_child_name("page1")
        self.button_OK.set_sensitive(False)
        self.updates_task_running = False
        self.progressbar_pulse.pulse()
        self.pulse_timeout_id = GLib.timeout_add(100,
                self.update_progress)
        self.updates_task_running = True
        self.updates_task = threading.Thread(target=self._perform_updates)
        self.updates_task.daemon = True
        self.updates_task.start()

    def on_button_cancel_clicked(self, widget, data=None):
        Gtk.main_quit()

    def on_button_reading_cancel_clicked(self, widget, data=None):
        # stop background task and...
        Gtk.main_quit()

    def on_button_no_updates_exit_clicked(self, widget, data=None):
        Gtk.main_quit()

    def on_button_error_exit_clicked(self, widget, data=None):
        Gtk.main_quit()

    def on_button_success_OK_clicked(self, widget, data=None):
        Gtk.main_quit()

    def update_progress(self):
        if self.updates_task_running:
            self.progressbar_pulse.pulse()
            return True
        return False
    
    def update_progressbar_reading(self):
        if self.reading_task_running:
            self.progressbar_reading.pulse()
            return True
        return False

    def _perform_updates(self):
        # This runs in a worker thread: do only blocking I/O & parsing here.
        # All GTK actions should be added with GLib.idle_add
        len_slapt = len(self.slapt_get_upgrades)
        try:
            len_flat = self.len_flat
        except AttributeError: # when flatpak is not installed
            len_flat = 0
        # steps = (download + install) packages + (download + install) flatpaks
        # + remove unused flatpak runtimes + update any remaining flatpaks
        steps = 2*len_slapt + 2*len_flat + 2
        # download packages with slapt-get and then install them
        if os.geteuid() == 0:
            GLib.idle_add(self.img_package_type.set_from_icon_name, SLAPT_GET_ICON,
                    Gtk.IconSize.DIALOG)
            self._perform_updates_download_slapt(steps)
            current_pct_frac = len_slapt / steps
            current_pct = round(100 * current_pct_frac)
            GLib.idle_add(self.progressbar_pct.set_fraction, current_pct_frac)
            GLib.idle_add(self.label_pct.set_text, _("%d %% complete") % current_pct)
            # now install slapt-get packages that have been downloaded
            current_step = len_slapt
            self._perform_updates_install_slapt(current_step, steps)
            current_pct_frac = 2*len_slapt / steps
            current_pct = round(100 * current_pct_frac)
            GLib.idle_add(self.progressbar_pct.set_fraction, current_pct_frac)
            GLib.idle_add(self.label_pct.set_text, _("%d %% complete") % current_pct)
        # now for flatpak updates, just download first
        if os.access(FLATPAK_BIN, os.X_OK) and len_flat > 0:
            GLib.idle_add(self.img_package_type.set_from_icon_name, FLATPAK_ICON,
                    Gtk.IconSize.DIALOG)
            current_step = 2*len_slapt
            max_next_step = 2*len_slapt + len_flat
            self._perform_updates_download_flatpaks(current_step, steps)
            current_pct_frac = (max_next_step) / steps
            current_pct = round(100 * current_pct_frac)
            GLib.idle_add(self.progressbar_pct.set_fraction, current_pct_frac)
            GLib.idle_add(self.label_pct.set_text, _("%d %% complete") % current_pct)
            # and update the flatpak updates now
            current_step = 2*len_slapt + len_flat
            max_next_step = 2*len_slapt + 2*len_flat
            self._perform_updates_install_flatpaks(current_step, max_next_step, steps)
            current_pct_frac = (max_next_step) / steps
            current_pct = round(100 * current_pct_frac)
            GLib.idle_add(self.progressbar_pct.set_fraction, current_pct_frac)
            GLib.idle_add(self.label_pct.set_text, _("%d %% complete") % current_pct)
            # remove unused flatpak runtimes
            current_step = 2*len_slapt + 2*len_flat
            self._perform_updates_remove_unused_flatpaks(current_step, steps)
            current_pct_frac = (2*len_slapt + 2*len_flat + 1) / steps
            current_pct = round(100 * current_pct_frac)
            GLib.idle_add(self.progressbar_pct.set_fraction, current_pct_frac)
            GLib.idle_add(self.label_pct.set_text, _("%d %% complete") % current_pct)
            # update remaining flatpaks (are there any at this point? Just to be
            # certain)
            current_step = 2*len_slapt + 2*len_flat + 1
            self._perform_updates_update_rest_of_flatpaks(current_step, steps)
        GLib.idle_add(self.img_package_type.set_from_icon_name, 'gtk-ok',
                Gtk.IconSize.DIALOG)
        GLib.idle_add(self.progressbar_pct.set_fraction, 1)
        GLib.idle_add(self.label_pct.set_text, _("%d %% complete") % 100)

        self.updates_task_running = False
        GLib.idle_add(self.label_upgrade.set_text, _("All updates have completed successfully."))
        GLib.idle_add(self.progressbar_pct.set_fraction, 1)
        GLib.idle_add(self.label_pct.set_text, _("%d %% complete") % 100)
        GLib.idle_add(self.dialog_success.show)
        return False # stop the idle handler

    def _perform_updates_download_slapt(self, steps):
        # This runs in a worker thread: do only blocking I/O & parsing here.
        # All GTK actions should be added with GLib.idle_add
        try:
            env = os.environ.copy()
            env["LANG"] = "C.utf8"
            process = subprocess.Popen(
                [SLAPT_GET_BIN, "--upgrade", "--download-only", "--no-prompt"],
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                universal_newlines=True,
                bufsize=1,
                env=env
            )
            parsing_progress = False
            for line in process.stdout:
                line = line.strip()
                logger.debug(line)
                if line.startswith("Need to get"):
                    parsing_progress = True
                    continue
                if line == "Done":
                    parsing_progress = False
                    break
                if parsing_progress and line != "":
                    current_step = int(line.split(" ")[0].split("/")[0])
                    current_pkg = line.split(" ")[3]
                    current_pkg_ver = line.split(" ")[4]
                    GLib.idle_add(self.label_upgrade.set_text, _("Downloading %s %s") %
                            (current_pkg, current_pkg_ver))
                    current_pct_frac = current_step / steps
                    current_pct = round(100 * current_pct_frac)
                    GLib.idle_add(self.progressbar_pct.set_fraction, current_pct_frac)
                    GLib.idle_add(self.label_pct.set_text, _("%d %% complete") % current_pct)
            process.wait()
            if process.returncode != 0:
                label_text = _("Package download failed.") + " "
                label_text = label_text + _("Please try again or check your network connection and available disk space if the issue persists.")
                GLib.idle_add(self.label_error.set_text, label_text)
                GLib.idle_add(self.dialog_error.show)
        except Exception as e:
            label_text = _("Exception") + "\n" + _("Package download failed.") + " "
            label_text = label_text + _("Please try again or check your network connection and available disk space if the issue persists.")
            label_text = label_text + '\n\n' + e
            GLib.idle_add(self.label_error.set_text, label_text)
            GLib.idle_add(self.dialog_error.show)

    def _perform_updates_install_slapt(self, current_step, steps):
        # This runs in a worker thread: do only blocking I/O & parsing here.
        # All GTK actions should be added with GLib.idle_add
        try:
            env = os.environ.copy()
            env["LANG"] = "C.utf8"
            process = subprocess.Popen(
                [SLAPT_GET_BIN, "--upgrade", "--no-prompt"],
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                universal_newlines=True,
                bufsize=1,
                env=env
            )
            parsing_progress = False
            for line in process.stdout:
                line = line.strip()
                logger.debug(line)
                if line.startswith("Need to get"):
                    parsing_progress = True
                    continue
                if line == "Done":
                    parsing_progress = False
                    break
                if parsing_progress and line.startswith("Upgrading package "):
                    current_step = current_step + 1
                    current_pkg = line.split(" ")[4]
                    GLib.idle_add(self.label_upgrade.set_text, _("Updating %s") % current_pkg)
                    current_pct_frac = current_step / steps
                    current_pct = round(100 * current_pct_frac)
                    GLib.idle_add(self.progressbar_pct.set_fraction, current_pct_frac)
                    GLib.idle_add(self.label_pct.set_text, _("%d %% complete") % current_pct)
                if parsing_progress and line.startswith("Installing package "):
                    current_step = current_step + 1
                    current_pkg = line.split(" ")[2]
                    GLib.idle_add(self.label_upgrade.set_text, _("Installing %s") % current_pkg)
                    current_pct_frac = current_step / steps
                    current_pct = round(100 * current_pct_frac)
                    GLib.idle_add(self.progressbar_pct.set_fraction, current_pct_frac)
                    GLib.idle_add(self.label_pct.set_text, _("%d %% complete") % current_pct)
            process.wait()
            if process.returncode != 0:
                label_text = _("Package installation failed.") + " "
                label_text = label_text + _("Please try again or check your network connection and available disk space if the issue persists.")
                GLib.idle_add(self.label_error.set_text, label_text)
                GLib.idle_add(self.dialog_error.show)
        except Exception as e:
            label_text = _("Exception") + _("Package installation failed.") + " "
            label_text = label_text + _("Please try again or check your network connection and available disk space if the issue persists.")
            label_text = label_text + '\n\n' + e
            GLib.idle_add(self.label_error.set_text, label_text)
            GLib.idle_add(self.dialog_error.show)

    def _perform_updates_download_flatpaks(self, current_step, steps):
        # This runs in a worker thread: do only blocking I/O & parsing here.
        # All GTK actions should be added with GLib.idle_add
        try:
            env = os.environ.copy()
            env["LANG"] = "C.utf8"
            process = subprocess.Popen(
                [FLATPAK_BIN, "update", FLATPAK_INSTALLATION, "--no-deploy", "--noninteractive", "-y"],
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                universal_newlines=True,
                bufsize=1,
                env=env
            )
            parsing_progress = False
            for line in process.stdout:
                line = line.strip()
                logger.debug(line)
                # "Updating" shows when downloading a flatpak that was
                # previously installed.
                # "Installing" shows when a new runtime is installed as a
                # dependency.
                if line.startswith("Updating ") or line.startswith("Installing "):
                    current_step = current_step + 1
                    current_flatpak_id = line.split(" ")[1].split("/")[1]
                    GLib.idle_add(self.label_upgrade.set_text, _("Downloading %s") %
                            (current_flatpak_id))
                    current_pct_frac = current_step / steps
                    current_pct = round(100 * current_pct_frac)
                    GLib.idle_add(self.progressbar_pct.set_fraction, current_pct_frac)
                    GLib.idle_add(self.label_pct.set_text, _("%d %% complete") % current_pct)
            process.wait()
            if process.returncode != 0:
                label_text = _("Flatpak download failed.") + " "
                label_text = label_text + _("Please try again or check your network connection and available disk space if the issue persists.")
                GLib.idle_add(self.label_error.set_text, label_text)
                GLib.idle_add(self.dialog_error.show)
        except Exception as e:
            label_text = _("Exception") + "\n" + _("Flatpak download failed.") + " "
            label_text = label_text + _("Please try again or check your network connection and available disk space if the issue persists.")
            label_text = label_text + '\n\n' + e
            GLib.idle_add(self.label_error.set_text, label_text)
            GLib.idle_add(self.dialog_error.show)

    def _perform_updates_install_flatpaks(self, current_step, max_next_step, steps):
        # This runs in a worker thread: do only blocking I/O & parsing here.
        # All GTK actions should be added with GLib.idle_add
        try:
            env = os.environ.copy()
            env["LANG"] = "C.utf8"
            process = subprocess.Popen(
                [FLATPAK_BIN, "update", FLATPAK_INSTALLATION, "--no-pull", "--noninteractive", "-y"],
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                universal_newlines=True,
                bufsize=1,
                env=env
            )
            parsing_progress = False
            for line in process.stdout:
                line = line.strip()
                logger.debug(line)
                # "Updating" shows when installing a flatpak that was
                # previously installed.
                # "Installing" shows when a new runtime is installed as a
                # dependency.
                if line.startswith("Updating ") or line.startswith("Installing "):
                    # There may be more flatpaks installed that we can tell
                    # before hand. That includes new flatpaks that are
                    # installed as dependencies. So instead of counting up by
                    # 1, we'll count up by 0.5 and we'll also make sure that we
                    # don't go over the threshold (max_next_step) so
                    # percentages on the GUI don't freak out and go back.
                    current_step = current_step + 0.5
                    if current_step > max_next_step:
                        current_step = max_next_step
                    current_flatpak_id = line.split(" ")[1].split("/")[1]
                    GLib.idle_add(self.label_upgrade.set_text, _("Installing %s") % current_flatpak_id)
                    current_pct_frac = current_step / steps
                    current_pct = round(100 * current_pct_frac)
                    GLib.idle_add(self.progressbar_pct.set_fraction, current_pct_frac)
                    GLib.idle_add(self.label_pct.set_text, _("%d %% complete") % current_pct)
            process.wait()
            if process.returncode != 0:
                label_text = _("Flatpak installation failed.") + " "
                label_text = label_text + _("Please try again or check your network connection and available disk space if the issue persists.")
                GLib.idle_add(self.label_error.set_text, label_text)
                GLib.idle_add(self.dialog_error.show)
        except Exception as e:
            label_text = _("Exception") + "\n" + _("Flatpak installation failed.") + " "
            label_text = label_text + _("Please try again or check your network connection and available disk space if the issue persists.")
            label_text = label_text + '\n\n' + e
            GLib.idle_add(self.label_error.set_text, label_text)
            GLib.idle_add(self.dialog_error.show)

    def _perform_updates_remove_unused_flatpaks(self, current_step, steps):
        # This runs in a worker thread: do only blocking I/O & parsing here.
        # All GTK actions should be added with GLib.idle_add
        try:
            GLib.idle_add(self.label_upgrade.set_text, _("Removing unused flatpaks"))
            env = os.environ.copy()
            env["LANG"] = "C.utf8"
            process = subprocess.Popen(
                [FLATPAK_BIN, "remove", FLATPAK_INSTALLATION, "--unused", "--noninteractive", "-y"],
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                universal_newlines=True,
                bufsize=1,
                env=env
            )
            for line in process.stdout:
                line = line.strip()
                logger.debug(line)
            process.wait()
            if process.returncode != 0:
                label_text = _("Unused flatpak removal failed.")
                GLib.idle_add(self.label_error.set_text, label_text)
                GLib.idle_add(self.dialog_error.show)
        except Exception as e:
            label_text = _("Exception") + "\n" + _("Unused flatpak removal failed.") + " "
            GLib.idle_add(self.label_error.set_text, label_text)
            GLib.idle_add(self.dialog_error.show)

    def _perform_updates_update_rest_of_flatpaks(self, current_step, steps):
        # This runs in a worker thread: do only blocking I/O & parsing here.
        # All GTK actions should be added with GLib.idle_add
        try:
            GLib.idle_add(self.label_upgrade.set_text, _("Finalizing updates"))
            env = os.environ.copy()
            env["LANG"] = "C.utf8"
            process = subprocess.Popen(
                [FLATPAK_BIN, "update", FLATPAK_INSTALLATION, "--noninteractive", "-y"],
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                universal_newlines=True,
                bufsize=1,
                env=env
            )
            for line in process.stdout:
                line = line.strip()
                logger.debug(line)
            process.wait()
            if process.returncode != 0:
                label_text = _("Finalizing flatpak updates failed.")
                GLib.idle_add(self.label_error.set_text, label_text)
                GLib.idle_add(self.dialog_error.show)
        except Exception as e:
            label_text = _("Exception") + "\n" + _("Finalizing flatpak updates failed.")
            GLib.idle_add(self.label_error.set_text, label_text)
            GLib.idle_add(self.dialog_error.show)

    def _on_updates_parsed(self, slapt_upgrades, flatpak_upgrades):
        # This runs on the main (GTK) thread, no need for GLib.idle_add()
        self.slapt_get_upgrades = slapt_upgrades
        self.flatpak_upgrades = flatpak_upgrades

        for pkg in self.slapt_get_upgrades:
            self._add_listbox_entry(pkg, PkgType.SLACK)
        for flatpak in self.flatpak_upgrades:
            self._add_listbox_entry(flatpak, PkgType.FLATPAK)

        self.list_box.show_all()
        self.reading_task_running = False
        self.dialog_reading.hide()

        len_slapt = len(self.slapt_get_upgrades)
        len_slapt_new = 0
        for package in self.slapt_get_upgrades:
            if package['new_dep']:
                len_slapt_new = len_slapt_new + 1
        len_slapt_updates = len_slapt - len_slapt_new
        try:
            len_flat_new = self.len_flat_new
            len_flat_updates = self.len_flat_updates
        except AttributeError: # when flatpak is not installed
            len_flat_new = 0
            len_flat_updates = 0
        len_flat = len_flat_new + len_flat_updates

        if len_slapt == 0 and len_flat == 0:
            self.dialog_no_updates.show()
        else:
            summary = ""
            if len_slapt == 1:
                summary = _("1 package will be updated.")
            elif len_slapt > 1:
                summary = (_("%d packages will be updated.") %
                        len_slapt_updates)
            if len_slapt_new == 1:
                summary = summary + "\n" + _("1 new package will be installed as a dependency.")
            elif len_slapt_new > 1:
                summary = summary + "\n" + (_("%d new packages will be installed as dependencies.") %
                        len_slapt_new)
            if len_slapt > 0:
                summary = summary + "\n"
            if len_flat_updates == 1:
                summary = summary + _("1 flatpak will be updated.")
            elif len_flat_updates > 1:
                summary = summary + (_("%d flatpaks will be updated.") %
                        len_flat_updates)
            if len_flat_new > 0:
                summary = summary + "\n"
            if len_flat_new == 1:
                summary = summary + _("1 new flatpak runtime will be installed as a dependency.")
            elif len_flat_new > 1:
                summary = summary + (_("%d new flatpak runtimes will be installed as dependencies.") %
                        len_flat_new)
            self.label_update_summary.set_text(summary)
            self.label_question.set_text(_("Do you want to proceed with these software updates?"))

        return False  # stop the idle handler

    def _populate_updates_list(self):
        # This runs in a worker thread: do only blocking I/O & parsing here.
        # All GTK actions should be added with GLib.idle_add
        self.reading_task_running = True
        try:
            slapt_upgrades = _get_slapt_get_upgrades()
        except Exception as e:
            slapt_upgrades = []
            label_text = _("Exception") + "\n" + _("Error getting slapt-get updates:") + " "
            label_text = label_text + _("Please try again or check your network connection and available disk space if the issue persists.")
            GLib.idle_add(self.label_error.set_text, label_text)
            GLib.idle_add(self.dialog_error.show)
        if os.access(FLATPAK_BIN, os.X_OK):
            try:
                flatpak_upgrades = _get_flatpak_upgrades()
                self.len_flat = len(flatpak_upgrades)
                self.len_flat_new = 0
                for flatpak in flatpak_upgrades:
                    if flatpak['action'] == 'i':
                        self.len_flat_new = self.len_flat_new + 1
                self.len_flat_updates = self.len_flat - self.len_flat_new
            except Exception as e:
                flatpak_upgrades = []
                self.len_flat = 0
                self.len_flat_new = 0
                self.len_flat_updates = 0
                label_text = _("Exception") + "\n" + _("Error getting flatpak updates:") + " "
                label_text = label_text + _("Please try again or check your network connection and available disk space if the issue persists.")
                label_text = label_text + '\n\n' + e
                GLib.idle_add(self.label_error.set_text, label_text)
                GLib.idle_add(self.dialog_error.show)
        else:
            flatpak_upgrades = []
        GLib.idle_add(self._on_updates_parsed, slapt_upgrades,
                flatpak_upgrades)

    def __init__(self):
        builder = Gtk.Builder()
        builder.set_translation_domain("salix-update-notifier")
        if os.path.exists('salix-update-manager.ui'):
            builder.add_from_file('salix-update-manager.ui')
        elif os.path.exists('/usr/share/salix-update-notifier/salix-update-manager.ui'):
            builder.add_from_file(
                '/usr/share/salix-update-notifier/salix-update-manager.ui')

        self.window = builder.get_object('salix-update-manager')
        self.list_box = builder.get_object('list_box')
        self.stack = builder.get_object('stack')
        self.button_OK = builder.get_object('button_OK')
        self.label_upgrade = builder.get_object('label_upgrade')
        self.progressbar_pulse = builder.get_object('progressbar_pulse')
        self.label_pct = builder.get_object('label_pct')
        self.progressbar_pct = builder.get_object('progressbar_pct')
        self.img_package_type = builder.get_object('img_package_type')

        self.dialog_reading = builder.get_object('dialog_reading')
        self.progressbar_reading = builder.get_object('progressbar_reading')
        self.button_reading_cancel = builder.get_object('button_reading_cancel')
        self.label_update_summary = builder.get_object('label_update_summary')
        self.label_question = builder.get_object('label_question')

        self.dialog_no_updates = builder.get_object('dialog_no_updates')
        self.button_no_updates_exit = builder.get_object('button_no_updates_exit')

        self.dialog_error = builder.get_object('dialog_error')
        self.label_error = builder.get_object('label_error')
        self.button_error_exit = builder.get_object('button_error_exit')

        self.dialog_success = builder.get_object('dialog_success')
        self.button_success_OK = builder.get_object('button_success_OK')

        builder.connect_signals(self)

        self.dialog_reading.show()
        self.reading_task_running = False
        # Start pulsating progressbar
        self.progressbar_reading.pulse()
        self.pulse_timeout_id = GLib.timeout_add(100,
                self.update_progressbar_reading)
        # Start background task
        self.populate_updates_task = threading.Thread(target=self._populate_updates_list)
        self.populate_updates_task.daemon = True
        self.populate_updates_task.start()
        self.updates_complete = False

def main(argv=None):
    """Main entry point for Salix Update Manager."""
    parser = argparse.ArgumentParser(
        description="Salix Update Manager"
    )
    parser.add_argument(
        "--debug",
        action="store_true",
        help="Enable debug output (shows detailed logging in console)."
    )

    args, unknown_args = parser.parse_known_args(argv)

    if args.debug:
        logging.basicConfig(
            level=logging.DEBUG,
            format="%(asctime)s [%(levelname)s] %(message)s",
            handlers=[logging.StreamHandler()]
        )
        logger.debug("Debug mode enabled.")

    app = SalixUpdateManager()
    app.window.show()
    Gtk.main()

if __name__ == "__main__":
    main()
