#!/usr/bin/env python3
"""
Main application for Debian Software Manager

References:
Gtk4: https://docs.gtk.org/gtk4/
Apt: https://apt-team.pages.debian.net/python-apt/

Author: Arjen Balfoort 
Date: 2026-03-26
"""

import os
import sys
import json
import shutil
import glob
import subprocess
import queue
import threading
import argparse
import gettext
import logging

from logging.handlers import TimedRotatingFileHandler
import apt

from apt_handler import AptPackageHandler
from flatpak_handler import FlatpakPackageHandler
from app_shared import (
    AppStreamPackageInfo,
    PackageAction,
    FilterKind,
    PackageState,
    LogLevel,
    show_dialog,
    DialogIcon,
    LogLevel_To_DialogIcon,
    html_to_pango
)
from app_downloads import DownloadsHandler
from app_info import SoftwareManager
from image import ImageHandler

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

_ = gettext.translation('deb-software', fallback=True).gettext

# Application name
APP_NAME = "Debian Software Manager"

# Configure logging
# Disable PIL debug logging
logging.getLogger("PIL").setLevel(logging.WARNING)
SAVE_DIR = os.path.expanduser("~/.config/deb-software")
os.makedirs(SAVE_DIR, exist_ok=True)

# Create a timed rotating file handler that keeps logs for 10 days
handler = TimedRotatingFileHandler(
    f"{SAVE_DIR}/deb-software.log",
    when='midnight',  # Rotate at midnight
    interval=1,       # Rotate every 1 day
    backupCount=10,   # Keep 10 days of logs
)

# Set the logging level and format
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s',
                              datefmt='%Y-%m-%d %H:%M:%S')
handler.setFormatter(formatter)

# Rotate logs immediately on startup
handler.doRollover()
logger = logging.getLogger()
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)

# Global exception handler
def handle_exception(exc_type, exc_value, exc_traceback):
    """
    Global exception handler to log unexpected errors.
    """
    logging.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
    show_dialog(
        APP_NAME,
        _("An unexpected error occurred.\nPlease check the log file for details."),
        icon=DialogIcon.ERROR
    )

# Set the global exception handler
sys.excepthook = handle_exception

class SoftwareGtk(Gtk.Application):
    """
    Main application class.
    """
    def __init__(self):
        super().__init__(
            application_id='org.solydxk.deb_software',
            flags=Gio.ApplicationFlags.HANDLES_OPEN | Gio.ApplicationFlags.HANDLES_COMMAND_LINE
        )

        # Directories
        self.save_dir: str = SAVE_DIR
        self.cache_dir: str = os.path.expanduser("~/.cache/deb-software")
        os.makedirs(self.cache_dir, exist_ok=True)
        self.script_dir: str = os.path.abspath(os.path.dirname(__file__))
        self.data_dir: str = os.path.join(self.script_dir, "data")
        self.images_dir: str = os.path.join(self.script_dir, "images")
        self.log_path: str = os.path.join(self.save_dir, "deb-software.log")

        # Load configuration from json
        with open(file=os.path.join(self.data_dir, "config.json"),
                  mode="r",
                  encoding="utf-8") as f:
            self.config = json.load(f)

        # Log start date and time
        self.logging: logging.Logger = logging
        self.logging.info(f"{APP_NAME} started")

        # Get current desktop (kde or xfce)
        self.desktop: str = os.getenv('XDG_CURRENT_DESKTOP').lower()

        # Properties
        self.downloads_handler: DownloadsHandler = DownloadsHandler(self)
        self.software_manager: SoftwareManager = None
        self.image_handler: ImageHandler = None
        self.favorites: list = []
        self.pending_files: list = []
        self.parser_args: argparse.Namespace = None

        # Load the UI file
        self.builder: Gtk.Builder = Gtk.Builder.new_from_file(f'{self.script_dir}/deb-software.ui')
        self.window: Gtk.Window = self.builder.get_object('main_window')
        self.progressbar: Gtk.ProgressBar = self.builder.get_object('progressbar')
        self.notebook: Gtk.Notebook = self.builder.get_object('notebook')
        self.summary_sw: Gtk.ScrolledWindow = self.builder.get_object('summary_sw')
        self.summary_listbox: Gtk.ListBox = self.builder.get_object('summary_listbox')
        self.uptodate_box: Gtk.Box = self.builder.get_object('uptodate_box')
        self.updates_label: Gtk.Label = self.builder.get_object('updates_label')
        self.btn_favorites: Gtk.Button = self.builder.get_object('btn_favorites')

        # Add a loading spinner
        self.overlay: Gtk.Overlay = self.builder.get_object("main_overlay")
        self.spinner = Gtk.Spinner()
        self.spinner.set_halign(Gtk.Align.CENTER)
        self.spinner.set_valign(Gtk.Align.CENTER)
        self.spinner.set_visible(False)
        # Add spinner as overlay child
        self.overlay.add_overlay(self.spinner)

        # Add a label OVER the progressbar
        self.progress_overlay: Gtk.Overlay = self.builder.get_object("progress_overlay")
        self.progress_label: Gtk.Label = Gtk.Label()
        self.progress_label.set_halign(Gtk.Align.START)
        self.progress_label.set_valign(Gtk.Align.CENTER)
        self.progress_label.set_css_classes(["progress_label"])
        self.progress_label.set_ellipsize(Pango.EllipsizeMode.END)
        self.progress_overlay.add_overlay(self.progress_label)

        # Save the selected package and screenshot
        self.selected_package: AppStreamPackageInfo = None
        self.selected_screenshot: int = 0

        # Summary page
        self.sum_update: Gtk.Button = self.builder.get_object('sum_update')
        self.sum_update.set_visible(False)
        self.sum_title: Gtk.Label = self.builder.get_object('sum_title')

        # Detail page buttons
        self.app_back: Gtk.Button = self.builder.get_object('app_back')
        self.app_install: Gtk.Button = self.builder.get_object('app_install')
        self.app_update: Gtk.Button = self.builder.get_object('app_update')
        self.update_all_label: Gtk.Label = self.builder.get_object('update_all_label')
        self.app_remove: Gtk.Button = self.builder.get_object('app_remove')
        self.app_launch: Gtk.Button = self.builder.get_object('app_launch')

        # Detail page
        self.app_header: Gtk.Box = self.builder.get_object('app_header')
        self.app_logo: Gtk.Picture = self.builder.get_object('app_logo')
        self.app_name: Gtk.Label = self.builder.get_object('app_name')
        self.app_installed: Gtk.Label = self.builder.get_object('app_installed')
        self.app_candidate: Gtk.Label = self.builder.get_object('app_candidate')
        self.app_size: Gtk.Label = self.builder.get_object('app_size')
        self.app_origin: Gtk.Label = self.builder.get_object('app_origin')
        self.app_screenshot: Gtk.Picture = self.builder.get_object('app_screenshot')
        self.app_summary: Gtk.Label = self.builder.get_object('app_summary')
        self.app_homepage: Gtk.Label = self.builder.get_object('app_homepage')
        self.lbl_packages: Gtk.Label = self.builder.get_object('lbl_packages')
        self.app_packages: Gtk.Label = self.builder.get_object('app_packages')

        # Description can be HTML
        self.app_description: Gtk.Label = self.builder.get_object('app_description')

        # Save the current action
        self.saved_action: PackageAction = None

        # Get current icon theme
        self.display: Gdk.Display = Gdk.Display.get_default()
        self.icon_theme: Gtk.IconTheme = Gtk.IconTheme.get_for_display(self.display)

        # Set the application name and icon by ID
        self.window.set_title(APP_NAME)
        self.window.set_icon_name(self.get_application_id())

        # Load CSS
        css_path = os.path.join(self.script_dir, "style", f"{self.desktop}.css")
        # First, check if there is a css file in the user directory
        css_paths = glob.glob(f"{self.save_dir}/*.css")
        if css_paths:
            css_path = css_paths[0]
        if not os.path.exists(css_path):
            # Default to kde.css
            css_path = os.path.join(self.script_dir, "style", "kde.css")
        css_provider = Gtk.CssProvider()
        self.logging.info(f"Loading CSS from {css_path}")
        css_provider.load_from_path(css_path)
        Gtk.StyleContext.add_provider_for_display(self.display,
                                                  css_provider,
                                                  Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

    def do_activate(self, *args, **kwargs) -> None:
        """
        Called when the application is activated.
        Overrides Gtk.Application.do_activate.
        """
        # Get the main window and show it
        self.window.set_application(self)
        self.window.present()

        # Show spinner immediately
        self.spinner.set_visible(True)
        self.spinner.start()

        # Start heavy work AFTER GTK draws the spinner
        GLib.idle_add(self._start_background_thread, self._initialize_app_thread)

    # Ignore the Lintian warning: it is a false positve
    # https://pylint.pycqa.org/en/latest/user_guide/messages/message_control.html
    def do_command_line(self, command_line, *args):  # pylint: disable=arguments-differ
        # Get all arguments
        all_args = command_line.get_arguments()[1:]  # Skip script name

        # Parse known flags with argparse
        parser = argparse.ArgumentParser()
        parser.add_argument("-d", "--debug",
                            action="store_true",
                            help="Enable debug mode")
        parser.add_argument("-c", "--category",
                            help="Show packages for a specific category")
        parser.add_argument("-u", "--update",
                            action="store_true",
                            help="Show packages for updates. Short for '--category=updates'")
        parser_args, remaining_args = parser.parse_known_args(all_args)

        # Store for later use
        self.parser_args = parser_args
        self.pending_files = remaining_args

        self.activate()
        return 0

    # =========================================
    # Event handlers
    # =========================================
    def on_button_clicked(self,
                          button: Gtk.Button,
                          category: str
                          ) -> None:
        """
        Show category apps.
        """
        self.logging.info(f"Show category: {category}")
        self.sum_update.set_visible(False)
        self.sum_title.set_label(self._get_button_text(button))
        self.notebook.set_current_page(0)
        self.summary_listbox.remove_all()
        if category == 'favorites':
            # Load favorites
            for pk_info in self.favorites:
                self._add_app_summary(pk_info)
        elif category == 'installed':
            # Load installed
            for pk_info in self.software_manager.get_installed_packages_info(
                    components_filter=FilterKind.APP
                ):
                self._add_app_summary(pk_info)
        elif category == 'updates':
            # Load updates
            if self.software_manager.updates:
                for pk_info in self.software_manager.updates:
                    self._add_app_summary(pk_info)
                self.sum_update.set_visible(True)
            else:
                self._show_no_updates()
        elif category == 'addons':
            # Load addons
            for pk_info in self.software_manager.addons:
                self._add_app_summary(pk_info)
        else:
            # Load category apps
            for pk_info in self.software_manager.filter_components(components_filter=FilterKind.APP,
                                                                   category_filter=category):
                self._add_app_summary(pk_info)

        # Reset scroll position to the top
        vadjustment = self.summary_sw.get_vadjustment()
        vadjustment.set_value(0)

        # Focus the button
        button.grab_focus()

    def on_update_all_clicked(self, _button: Gtk.Button) -> None:
        """
        Update all packages.
        """
        apt_packages = []
        flatpak_packages = []
        result_queue = queue.Queue()
        self.selected_package = None

        for package in self.software_manager.updates:
            if "flatpak" in package.origin:
                flatpak_packages.append(package)
            else:
                apt_packages.append(package)

        if apt_packages:
            thread = AptPackageHandler(app=self,
                                       action=PackageAction.SYSTEM_UPGRADE,
                                       packages=apt_packages,
                                       result_queue=result_queue)
            self._thread_starter(thread=thread,
                                 result_queue=result_queue)

        if flatpak_packages:
            thread = FlatpakPackageHandler(app=self,
                                           action=PackageAction.UPDATE,
                                           packages=flatpak_packages,
                                           result_queue=result_queue)
            self._thread_starter(thread=thread,
                                 result_queue=result_queue)

    def on_search_activate(self, entry: Gtk.Entry) -> None:
        """
        Show search results.
        """
        self.sum_update.set_visible(False)
        self.sum_title.set_label(_("Search results"))
        self.notebook.set_current_page(0)
        self.summary_listbox.remove_all()
        query = entry.get_text()
        results = self.software_manager.search_packages(query)
        self.logging.info(f"Show search results: {query} - {len(results)} results")
        for result in results:
            self._add_app_summary(result)

    def on_clear_search(self,
                        entry: Gtk.Entry,
                        icon_position: Gtk.EntryIconPosition) -> None:
        """
        Handle the icon-release event for the package search entry.
        """
        if icon_position == Gtk.EntryIconPosition.PRIMARY:
            self.on_search_activate(entry)
        else:
            entry.set_text('')

    def on_screenshot_clicked(self, _gesture, _n_press, _x, _y) -> None:
        """
        Show screenshot.
        """
        if len(self.selected_package.screenshots) <= 1:
            return

        self.selected_screenshot = self.selected_screenshot + 1
        if self.selected_screenshot == len(self.selected_package.screenshots):
            self.selected_screenshot = 0

        # Get the image from the URL
        img_url = self.selected_package.screenshots[self.selected_screenshot]
        self.logging.info(f"Show screenshot: {img_url}")
        img = self.image_handler.load_image_from_url(self.cache_dir,img_url)
        if img is not None:
            self.app_screenshot.set_paintable(img)

    def on_summary_clicked(self, _gesture, _n_press, _x, _y, pk_info) -> None:
        """
        Show package details.
        """
        self.logging.info(
            f"Show package details: {pk_info.package_id} with origin: {pk_info.origin}"
        )

        # A downloaded package already has all the info
        self.selected_package = pk_info

        if not pk_info.download_path:
            # Get the package details for repo packages
            self.selected_package = self.software_manager.get_package_details(
                pk_info.package_id, pk_info.origin
            )

        # Set buttons
        if self.selected_package.is_installed:
            self.set_button_state(PackageState.INSTALLED)
        else:
            self.set_button_state(PackageState.AVAILABLE)

        self.image_handler.set_picture(self.app_logo, self.selected_package.icon, 128)
        self.app_name.set_label(self.selected_package.name)
        self.app_installed.set_label(str(self.selected_package.version))
        self.app_candidate.set_label(self.selected_package.candidate_version)
        self.app_size.set_label(self.selected_package.installed_size)
        self.app_origin.set_label(self.selected_package.origin)

        # Homepage
        self.app_homepage.set_visible(False)
        if self.selected_package.homepage:
            self.app_homepage.set_visible(True)
            self.app_homepage.set_markup(
                f"<a href='{self.selected_package.homepage}'>{self.selected_package.homepage}</a>")
            self.app_homepage.set_use_markup(True)
            self.app_homepage.connect("activate-link", self.on_activate_link)

        # Set background color
        self.image_handler.set_widget_background_color_from_image(
            widget=self.app_header,
            image_path=self.selected_package.icon,
            transparency=0.1)

        # Screenshot is an URL
        self.app_screenshot.set_visible(False)
        for screenshot_url in self.selected_package.screenshots:
            img = self.image_handler.load_image_from_url(self.cache_dir, screenshot_url)
            if img is not None:
                # GtkPicture
                self.app_screenshot.set_visible(True)
                self.app_screenshot.set_paintable(img)
                # Create a hand cursor
                if len(self.selected_package.screenshots) > 1:
                    hand_cursor = Gdk.Cursor.new_from_name("pointer", None)
                    self.app_screenshot.set_cursor(hand_cursor)
                break

        self.app_summary.set_label(self.selected_package.summary)

        buffer = Gtk.TextBuffer()
        buffer.set_text(html_to_pango(self.selected_package.description))
        self.app_description.set_buffer(buffer)

        # Show the list of packages that are being installed
        if self.selected_package.is_installed:
            self.lbl_packages.set_visible(False)
            self.app_packages.set_visible(False)
        else:
            self.lbl_packages.set_visible(True)
            self.app_packages.set_visible(True)
            self.app_packages.set_label('\n'.join([
                self.selected_package.package_name or self.selected_package.package_id
            ] + self.selected_package.additional_packages))

        self.notebook.set_current_page(1)

    # Connect the 'activate-link' signal to open the URL in the default browser
    def on_activate_link(self, _label, uri):
        """ Open the URL in the default browser (the Gtk4 way) """
        launcher = Gtk.UriLauncher(uri=uri)
        # Launch asynchronously
        launcher.launch(self.window)
        return True

    def on_app_back_clicked(self, _button: Gtk.Button) -> None:
        """
        Go back to the summary page.
        """
        self.notebook.set_current_page(0)

    def on_app_install_clicked(self, _button: Gtk.Button) -> None:
        """
        Install the package.
        """
        self._single_package_action(PackageAction.INSTALL)

    def on_app_remove_clicked(self, _button: Gtk.Button) -> None:
        """
        Remove the package.
        """
        self._single_package_action(PackageAction.REMOVE)

    def on_app_update_clicked(self, _button: Gtk.Button) -> None:
        """
        Update the package.
        """
        self._single_package_action(PackageAction.UPDATE)

    def on_app_launch_clicked(self, _button: Gtk.Button) -> None:
        """
        Launch the package.
        """
        command = [self.selected_package.executable]
        origin = self.selected_package.origin
        if origin == 'flatpak':
            command = ['flatpak', 'run', self.selected_package.package_id]

        try:
            with subprocess.Popen(
                command,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True
            ) as process:
                _stdout, stderr = process.communicate()
                if process.returncode != 0:
                    self.logging.error(
                        f"Command failed: {' '.join(command)}\n"
                        f"Exit code: {process.returncode}\n"
                        f"stderr: {stderr.strip()}"
                    )
                else:
                    self.logging.info(f"Run command: {' '.join(command)}.")
        except Exception as e:  # pylint: disable=broad-except
            self.logging.error(f"Failed to start command: {' '.join(command)}: {e}")

    def on_button_log_clicked(self, _button: Gtk.Button) -> None:
        """
        Open the log file in a text editor.
        """
        if not os.path.exists(self.log_path):
            self.logging.warning(f"Log file not found: {self.log_path}")
            show_dialog(APP_NAME,
                        _("Log file not found"),
                        self.window,
                        DialogIcon.WARNING)
            return
        os.system(f"xdg-open {self.log_path}")

    def on_button_help_clicked(self, _button: Gtk.Button) -> None:
        """
        Open the help file in the default browser.
        """
        help_path = os.path.join(self.script_dir, "help.html")
        if not os.path.exists(help_path):
            return
        os.system(f"xdg-open {help_path}")

    def on_button_about_clicked(self, _button: Gtk.Button) -> None:
        """
        Show the about window.
        """
        try:
            version = apt.Cache()['deb-software'].versions[0].source_version
        except KeyError:
            version = ''

        about = Gtk.AboutDialog(
            transient_for=self.window,
            modal=True,
            program_name=APP_NAME,
            version=version,
            copyright=f"© 2026 {self.config.get('authors')[0]}",
            license_type=Gtk.License.GPL_3_0,
            website=self.config.get("home"),
            website_label="Website",
            authors=self.config.get("authors"),
            logo_icon_name=self.get_application_id()
        )

        about.present()

    def on_button_refresh_clicked(self, _button: Gtk.Button) -> None:
        """
        Refresh apt and appstream data.
        Clear the cache dir.
        """
        self.logging.info("Refresh the list of packages.")
        # Clear cache dir
        shutil.rmtree(self.cache_dir, ignore_errors=True)
        os.makedirs(self.cache_dir, exist_ok=True)

        # Do an apt update
        result_queue = queue.Queue()
        thread = AptPackageHandler(app=self,
                                   action=PackageAction.REFRESH_CACHE,
                                   result_queue=result_queue)
        self._thread_starter(thread=thread,
                             result_queue=result_queue)

        # Refresh updates
        self._refresh_updates()

    # =========================================
    # Global methods
    # =========================================
    def set_updates_label(self):
        """
        Set the number of updates available.
        """
        updates_text = self._get_category_translation("updates")
        self.updates_label.set_text(
            f"{updates_text} ({self.software_manager.upgradable_count})"
        )
        self.update_all_label.set_text(_("Update all"))
        self.sum_title.set_label(updates_text)

    def set_button_state(self, state: PackageState) -> None:
        """ Set the button state. """
        if state in (PackageState.INSTALLED, PackageState.UPDATED):
            self.app_install.set_visible(False)
            self.app_remove.set_visible(True)
            if self.selected_package is not None:
                if self.selected_package.executable or \
                self.selected_package.origin == 'flatpak':
                    self.app_launch.set_visible(True)
                else:
                    self.app_launch.set_visible(False)
                if self.selected_package.version != self.selected_package.candidate_version and \
                   state != PackageState.UPDATED:
                    self.app_update.set_visible(True)
                else:
                    self.app_update.set_visible(False)
        else:
            self.app_install.set_visible(True)
            self.app_remove.set_visible(False)
            self.app_update.set_visible(False)
            self.app_launch.set_visible(False)

    # =========================================
    # Private methods
    # =========================================
    def _start_background_thread(self, function) -> bool:
        # Now start the heavy lifting in a worker thread
        threading.Thread(
            target=function,
            daemon=True
        ).start()
        return False

    def _initialize_app_thread(self):
        """
        Initialize the application in a worker thread.
        """
        # Heavy work here
        software_manager = SoftwareManager(self)
        image_handler = ImageHandler(self)

        # Load favorites
        favorites = []
        with open(os.path.join(self.data_dir, "favorites.json"), "r", encoding="utf-8") as f:
            for key, packages in json.load(f).items():
                if key in ['all', self.desktop]:
                    for package in packages:
                        info = software_manager.get_package_info(package, "debian")
                        if info:
                            favorites.append(info)

        # When done, update UI on main thread
        GLib.idle_add(self._initialize_app_finish,
                    software_manager,
                    image_handler,
                    favorites)

    def _initialize_app_finish(self, software_manager, image_handler, favorites):
        """
        Update UI on main thread.
        """
        self.software_manager = software_manager
        self.image_handler = image_handler
        self.favorites = favorites

        # Build UI elements that depend on app_software
        self._connect_buttons()
        self._build_buttons()
        self.set_updates_label()

        # Process any pending files from command line
        if self.pending_files:
            # Get package info
            pk_info_list = self.downloads_handler.get_packages_info(self.pending_files)
            if not pk_info_list:
                self.pending_files = []
            else:
                # Only show downloaded packages, even if there are also repo updates
                self.software_manager.updates = pk_info_list
                self.software_manager.upgradable_count = len(pk_info_list)
                self.updates_label.set_text(_("Downloads"))
                self.update_all_label.set_text(_("Install all"))
                self.on_button_clicked(self.builder.get_object('btn_updates'), 'updates')

        # Hide spinner
        self.spinner.stop()
        self.spinner.set_visible(False)

        return False

    def _single_package_action(self, action: PackageAction) -> None:
        """ 
        Perform a single package action.
        """
        self.saved_action = action
        result_queue = queue.Queue()

        if action not in (PackageAction.INSTALL, PackageAction.REMOVE, PackageAction.UPDATE):
            self.logging.error(f"Unknown action: {action}")
            return

        if "flatpak" not in self.selected_package.origin:
            thread = AptPackageHandler(app=self,
                                       action=action,
                                       packages=[self.selected_package],
                                       result_queue=result_queue)
            self._thread_starter(thread=thread,
                                 result_queue=result_queue)
        else:
            thread = FlatpakPackageHandler(app=self,
                                           action=action,
                                           packages=[self.selected_package],
                                           result_queue=result_queue)
            self._thread_starter(thread=thread,
                                 result_queue=result_queue)

    def _thread_starter(self,
                        thread: threading.Thread,
                        result_queue: queue.Queue) -> None:
        """ Start a thread and check its progress. """
        message = ''
        state = PackageState.INSTALLED
        subject = _("Installing")

        if thread.action == PackageAction.REMOVE:
            state = PackageState.REMOVED
            subject = _("Removing")
        elif thread.action == PackageAction.UPDATE:
            state = PackageState.UPDATED
            subject = _("Updating")
        elif thread.action == PackageAction.REFRESH_CACHE:
            state = None
            subject = _("Updating cache")

        # Disable buttons while running apt
        self.sum_update.set_sensitive(False)
        self.app_install.set_sensitive(False)
        self.app_remove.set_sensitive(False)
        self.app_update.set_sensitive(False)

        # Start the thread
        thread.start()

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

                if level == LogLevel.ERROR:
                    show_dialog(subject, message, self.window, DialogIcon.ERROR)
                    self.progressbar.set_fraction(0)
                    self.progress_label.set_text("")
                    # Enable buttons
                    self.sum_update.set_sensitive(True)
                    self.app_install.set_sensitive(True)
                    self.app_remove.set_sensitive(True)
                    self.app_update.set_sensitive(True)
                    return False

                # Update the progressbar
                self.progressbar.set_fraction(dict_info['progress'] / 100)
                if not level == LogLevel.SUCCESS:
                    # Success message is shown in dialog below
                    self.progress_label.set_text(message)

            # Only stop when BOTH conditions are true:
            # 1. thread is dead
            # 2. queue is empty
            if not thread.is_alive() and result_queue.empty():
                # Show dialog
                show_dialog(
                    subject,
                    message,
                    self.window,
                    LogLevel_To_DialogIcon.get(level, DialogIcon.INFO)
                )
                self.progress_label.set_text("")

                # Update button state
                if state is not None and level != LogLevel.CANCELED:
                    self.set_button_state(state)

                # Enable buttons
                self.sum_update.set_sensitive(True)
                self.app_install.set_sensitive(True)
                self.app_remove.set_sensitive(True)
                self.app_update.set_sensitive(True)

                # Refresh updates if not canceled
                if level != LogLevel.CANCELED:
                    GLib.idle_add(self._refresh_updates)

                # All done
                return False

            # Keep going
            return True

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

    def _show_no_updates(self) -> None:
        # This is just a message to the user
        # and should not be selectable: no background color on hover.
        # Create a CSS provider
        css_provider = Gtk.CssProvider()
        css = """
        .no-hover:hover {
            background-color: transparent;
        }
        """
        css_provider.load_from_data(css.encode())
        Gtk.StyleContext.add_provider_for_display(
            Gdk.Display.get_default(),
            css_provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

        # Create a box
        box = Gtk.Box()
        box.set_orientation(Gtk.Orientation.VERTICAL)
        box.set_spacing(24)
        box.set_css_classes(['uptodate_box'])

        # Add icon
        icon_path = os.path.join(self.images_dir, "uptodate.svg")
        icon = Gtk.Picture()
        self.image_handler.set_picture(icon, icon_path, 64)
        icon.set_content_fit(Gtk.ContentFit.SCALE_DOWN)
        icon.set_valign(Gtk.Align.CENTER)
        box.append(icon)

        # Add label
        update_label = Gtk.Label()
        update_label.set_label(_("Your system is up to date"))
        update_label.set_halign(Gtk.Align.CENTER)
        update_label.set_valign(Gtk.Align.CENTER)
        box.append(update_label)

        # Create a ListBoxRow and add the box to it
        listbox_row = Gtk.ListBoxRow()
        listbox_row.set_child(box)
        listbox_row.add_css_class("no-hover")

        # Append the row to the listbox
        self.summary_listbox.append(listbox_row)

    def _refresh_updates(self) -> None:
        """
        Refresh updates.
        """
        self.sum_update.set_visible(False)
        self.summary_listbox.remove_all()
        self.software_manager.refresh_data()
        if self.software_manager.updates:
            for pk_info in self.software_manager.updates:
                self._add_app_summary(pk_info)
            self.sum_update.set_visible(True)
        else:
            self._show_no_updates()
        self.set_updates_label()

    def _add_app_summary(self, pk_info: AppStreamPackageInfo) -> None:
        """
        Add a package summary to the right pane.
        """
        package_box = self.software_manager.get_gtk_box_summary(pk_info)
        # Create a gesture for the box
        click_gesture = Gtk.GestureClick()
        click_gesture.connect("pressed", self.on_summary_clicked, pk_info)
        package_box.add_controller(click_gesture)
        self.summary_listbox.append(package_box)

    def _connect_buttons(self):
        """ Connect buttons. """
        self.btn_favorites.connect('clicked', self.on_button_clicked, 'favorites')

        btn_installed = self.builder.get_object('btn_installed')
        btn_installed.connect('clicked', self.on_button_clicked, 'installed')

        btn_updates = self.builder.get_object('btn_updates')
        btn_updates.connect('clicked', self.on_button_clicked, 'updates')

        btn_accessibility = self.builder.get_object('btn_accessibility')
        btn_accessibility.connect('clicked', self.on_button_clicked, 'accessibility')

        btn_addons = self.builder.get_object('btn_addons')
        btn_addons.connect('clicked', self.on_button_clicked, 'addons')

        btn_log = self.builder.get_object('btn_log')
        btn_log.connect('clicked', self.on_button_log_clicked)

        btn_help = self.builder.get_object('btn_help')
        btn_help.connect('clicked', self.on_button_help_clicked)

        btn_about = self.builder.get_object('btn_about')
        btn_about.connect('clicked', self.on_button_about_clicked)

        btn_refresh = self.builder.get_object('btn_refresh')
        btn_refresh.connect('clicked', self.on_button_refresh_clicked)

        search_entry = self.builder.get_object('search_entry')
        search_entry.connect("activate", self.on_search_activate)
        search_entry.connect('icon-release', self.on_clear_search)

        self.sum_update.connect('clicked', self.on_update_all_clicked)
        self.app_back.connect('clicked', self.on_app_back_clicked)
        self.app_install.connect('clicked', self.on_app_install_clicked)
        self.app_remove.connect('clicked', self.on_app_remove_clicked)
        self.app_update.connect('clicked', self.on_app_update_clicked)
        self.app_launch.connect('clicked', self.on_app_launch_clicked)

    def _build_buttons(self):
        """ Build the category buttons and select the initial category. """
        left_pane = self.builder.get_object('left_pane')
        sibling = self.builder.get_object('separator')

        # Select the initial category.
        args_category = self.parser_args.category.strip().lower() \
            if self.parser_args.category else ''

        button_action = None
        for category in self.software_manager.categories:
            button = Gtk.Button()
            button.connect('clicked', self.on_button_clicked, category)

            if args_category == category.lower():
                button_action = (button, category)

            box = Gtk.Box(spacing=10)
            icon = Gtk.Picture()
            icon.set_can_shrink(False)
            icon.set_content_fit(Gtk.ContentFit.SCALE_DOWN)
            icon_path = f"/usr/lib/deb-software/images/menu/{category.split(' ')[0].lower()}.svg"
            if os.path.exists(icon_path):
                self.image_handler.set_picture(icon, icon_path, 16)
            else:
                self.image_handler.set_picture(
                    icon, f"applications-{category.split(' ')[0].lower()}", 16
                )

            box.append(icon)
            box.append(Gtk.Label(label=self._get_category_translation(category)))
            button.set_child(box)

            left_pane.insert_child_after(button, sibling)
            sibling = button

        if button_action:
            self.on_button_clicked(*button_action)
        elif args_category == 'updates' or self.parser_args.update:
            self.on_button_clicked(self.builder.get_object('btn_updates'), 'updates')
        elif args_category == 'installed':
            self.on_button_clicked(self.builder.get_object('btn_installed'), 'installed')
        else:
            self.on_button_clicked(self.builder.get_object('btn_favorites'), 'favorites')

    @staticmethod
    def _get_category_translation(category: str) -> str:
        cat = category.lower()
        if 'accessor' in cat:
            return _('Accessories')
        if 'develop' in cat:
            return _('Development')
        if 'education' in cat:
            return _('Education')
        if 'games' in cat:
            return _('Games')
        if 'graphic' in cat:
            return _('Graphics')
        if 'internet' in cat:
            return _('Internet')
        if 'multimedia' in cat:
            return _('Multimedia')
        if 'office' in cat:
            return _('Office')
        if 'science' in cat:
            return _('Science')
        if 'system' in cat:
            return _('System')
        if 'update' in cat:
            return _('Updates')
        return category.capitalize()

    @staticmethod
    def _get_button_text(button: Gtk.Button) -> str:
        # Get the box
        box = button.get_child()
        if box is None:
            label = button.get_label()
            return label if label else ''

        # Iterate over the children of the box
        child = box.get_first_child()
        while child is not None:
            if isinstance(child, Gtk.Label):
                return child.get_text()
            child = child.get_next_sibling()

        return ''

if __name__ == '__main__':
    # Start the app
    app = SoftwareGtk()
    app.run(sys.argv)
