#!/usr/bin/python3
# -*- coding: utf-8 -*-

import html
import os
import re
import signal
import sys
import time
from pathlib import Path
from typing import Any

from PyQt5 import QtCore, QtGui, QtWidgets  # pylint: disable=import-error

from sun.__metadata__ import (__author__, __copyright__, __email__,
                              __license__, __license_link__, __prgnam__,
                              __version__, __website__)
from sun.configs import TRAY_PID_FILE, configs_load
from sun.daemon import LOG_FILE
from sun.manager import Manager
from sun.utils import Utilities


def get_theme_colors() -> tuple[str, str]:
    """Get background and text colors from the active Qt palette.

    Returns:
        tuple[str, str]: (bg_color, text_color) as hex strings.
    """
    palette = QtWidgets.QApplication.palette()
    bg_color = palette.color(palette.Base).name()
    text_color = palette.color(palette.Text).name()
    return bg_color, text_color


class TrayIcon(QtWidgets.QSystemTrayIcon):  # type: ignore[misc]  # pylint: disable=too-many-instance-attributes

    """SUN system tray icon application.

    Attributes:
        configs (Configs): Application configuration and system information.
        menu (QtWidgets.QMenu): System tray context menu.
        msg_box (QtWidgets.QMessageBox): Message box for displaying information dialogs.
        tool (Manager): Manager instance for daemon control and update checks.
    """

    ACTION_COLORS_DARK: dict[str, str] = {
        'Upgraded': '#f0c040',
        'Rebuilt': '#00bcd4',
        'Added': '#4caf50',
        'Removed': '#f44336',
    }

    ACTION_COLORS_LIGHT: dict[str, str] = {
        'Upgraded': '#b8860b',
        'Rebuilt': '#0077aa',
        'Added': '#2e7d32',
        'Removed': '#c62828',
    }

    def __init__(self) -> None:  # pylint: disable=[R0915,too-many-locals]
        super().__init__()
        self.configs = configs_load
        self.tool = Manager()
        self.icon_path: str = f"{self.configs.data_configs['icon_path']}/{__prgnam__}"
        self.msg_box: QtWidgets.QMessageBox

        # Icon definition
        self.setIcon(QtGui.QIcon(f'{self.icon_path}/{__prgnam__}.png'))
        self.setToolTip('SUN (Slackware Update Notifier)')  # Tooltip

        # Create a main menu
        self.menu = QtWidgets.QMenu()

        # Adding submenu
        submenu = self.menu.addMenu('Daemon')
        submenu.setIcon(QtGui.QIcon(f'{self.icon_path}/daemon.png'))

        # Adding items to the submenu
        start = submenu.addAction('Start')
        start.setIcon(QtGui.QIcon(f'{self.icon_path}/start.png'))
        start.triggered.connect(self.daemon_start)

        stop = submenu.addAction('Stop')
        stop.setIcon(QtGui.QIcon(f'{self.icon_path}/stop.png'))
        stop.triggered.connect(self.daemon_stop)

        restart = submenu.addAction('Restart')
        restart.setIcon(QtGui.QIcon(f'{self.icon_path}/restart.png'))
        restart.triggered.connect(self.daemon_restart)

        status = submenu.addAction('Status')
        status.setIcon(QtGui.QIcon(f'{self.icon_path}/status.png'))
        status.triggered.connect(self.daemon_status)

        # Adding a simple item to the menu
        check_updates = self.menu.addAction('Check Updates')
        check_updates.setIcon(QtGui.QIcon(f'{self.icon_path}/check.png'))
        check_updates.triggered.connect(self.show_check_updates)

        pending_upgrades = self.menu.addAction('Pending Upgrades')
        pending_upgrades.setIcon(QtGui.QIcon(f'{self.icon_path}/pending.png'))
        pending_upgrades.triggered.connect(self.show_pending_upgrades)

        os_info = self.menu.addAction('Os Info')
        os_info.setIcon(QtGui.QIcon(f'{self.icon_path}/info.png'))
        os_info.triggered.connect(self.show_os_info)

        changelog = self.menu.addAction('ChangeLog')
        changelog.setIcon(QtGui.QIcon(f'{self.icon_path}/changelog.png'))
        changelog.triggered.connect(self.show_changelog)

        log = self.menu.addAction('SUN Log')
        log.setIcon(QtGui.QIcon(f'{self.icon_path}/log.png'))
        log.triggered.connect(self.show_log)

        config = self.menu.addAction('Config')
        config.setIcon(QtGui.QIcon(f'{self.icon_path}/config.png'))
        config.triggered.connect(self.show_config)

        # Adding separator
        self.menu.addSeparator()

        # Adding 'Reload app'
        reload_action = self.menu.addAction('Reload app')
        reload_action.setIcon(QtGui.QIcon(f'{self.icon_path}/reload.png'))
        reload_action.triggered.connect(self.reload_app)

        # Add the 'About' option
        about = self.menu.addAction('About')
        about.setIcon(QtGui.QIcon(f'{self.icon_path}/about.png'))
        about.triggered.connect(self.show_about)

        # Adding 'Exit'
        exit_action = self.menu.addAction('Exit')
        exit_action.setIcon(QtGui.QIcon(f'{self.icon_path}/exit.png'))
        exit_action.triggered.connect(self.exit_app)

        # Setting the menu in the tray icon
        self.setContextMenu(self.menu)

        # Event connection
        self.activated.connect(self.on_tray_icon_activated)

        # Silent badge timer: checks for updates in the background at
        # BADGE_INTERVAL (independent of the daemon's INTERVAL).
        self._badge_timer = QtCore.QTimer(self)
        self._badge_timer.timeout.connect(self._update_badge_silent)
        self._badge_timer.start(60 * 1000 * self.configs.badge_interval)
        # Immediate daemon state icon: no HTTP, just PID check so the icon
        # reflects offline state instantly without waiting for delay_load.
        QtCore.QTimer.singleShot(
            0, lambda: self._apply_badge(0, 0, self.tool.daemon_status())
        )
        # Full badge check (with HTTP) after startup delay.
        QtCore.QTimer.singleShot(
            self.configs.delay_load * 1000, self._update_badge_silent
        )

        # Watch the local ChangeLog and the package manager data file for changes.
        # Also watch /var/log/packages so the badge refreshes when packages are
        # installed or removed. All signals route to the same _update_badge_silent
        # callback so no parallel logic is needed.
        self._local_log = str(QtCore.QDir.cleanPath(
            f'{self.configs.repo_log_path}/{self.configs.repo_log_file}'
        ))
        self._data_file = self.tool.pending.data_file
        watched_files = [self._local_log]
        if self._data_file:
            watched_files.append(self._data_file)
        self._log_watcher = QtCore.QFileSystemWatcher(watched_files, self)
        self._log_watcher.fileChanged.connect(self._update_badge_silent)
        self._log_watcher.addPath(str(Path('/var/log/packages')))
        self._log_watcher.directoryChanged.connect(self._update_badge_silent)

    def _make_offline_icon(self) -> QtGui.QIcon:
        """Return a greyscale version of the tray icon to indicate daemon is offline.

        Converts each pixel to its luminance value while preserving the original
        alpha channel, so the icon remains transparent where expected.
        """
        base = QtGui.QPixmap(f'{self.icon_path}/{__prgnam__}.png')
        image = base.toImage().convertToFormat(QtGui.QImage.Format_ARGB32)
        for y in range(image.height()):
            for x in range(image.width()):
                pixel = image.pixel(x, y)
                alpha = (pixel >> 24) & 0xFF
                r = (pixel >> 16) & 0xFF
                g = (pixel >> 8) & 0xFF
                b = pixel & 0xFF
                gray = int(r * 0.299 + g * 0.587 + b * 0.114)
                image.setPixel(x, y, QtGui.QColor(gray, gray, gray, alpha).rgba())
        return QtGui.QIcon(QtGui.QPixmap.fromImage(image))

    def _apply_badge(self, update_count: int, pending_count: int = 0,
                     daemon_running: bool = True) -> None:
        """Update the tray icon and tooltip to reflect the current update/pending state.

        Red badge takes priority over yellow: show red when changelog updates are
        available (run slpkg update), yellow when packages are pending upgrade
        (run slpkg upgrade), and no badge when everything is up to date.

        Args:
            update_count (int): Number of changelog updates available.
            pending_count (int): Number of packages pending upgrade.
            daemon_running (bool): If False, show a greyed-out icon.
        """
        if not daemon_running:
            self.setIcon(self._make_offline_icon())
            self.setToolTip('SUN: daemon is not running')
        elif update_count > 0:
            self.setIcon(self._make_badge_icon(update_count, '#f44336'))
            self.setToolTip(f'SUN: {update_count} update(s) available')
        elif pending_count > 0:
            self.setIcon(self._make_badge_icon(pending_count, '#f0c040'))
            self.setToolTip(f'SUN: {pending_count} package(s) pending upgrade')
        else:
            self.setIcon(QtGui.QIcon(f'{self.icon_path}/{__prgnam__}.png'))
            self.setToolTip('SUN (Slackware Update Notifier)')

    def _update_badge_silent(self) -> None:
        """Silently check for updates and pending upgrades, then refresh the badge.

        Re-adds watched files to the watcher after each call, because package
        managers often do atomic writes (write + rename), which causes
        QFileSystemWatcher to lose track of the new file inode.
        """
        daemon_running = self.tool.daemon_status()
        _, update_pkgs = self.tool.check_updates()
        _, pending_pkgs = self.tool.check_pending()
        self._apply_badge(len(update_pkgs), len(pending_pkgs), daemon_running)
        if self._local_log not in self._log_watcher.files():
            self._log_watcher.addPath(self._local_log)
        if self._data_file and self._data_file not in self._log_watcher.files():
            self._log_watcher.addPath(self._data_file)
        if '/var/log/packages' not in self._log_watcher.directories():
            self._log_watcher.addPath('/var/log/packages')

    def reload_app(self) -> None:
        """Reload tray app.
        """
        QtWidgets.qApp.quit()
        os.execl(sys.executable, sys.executable, *sys.argv)

    def on_tray_icon_activated(self, reason: QtWidgets.QSystemTrayIcon.ActivationReason) -> None:
        """Left click.

        Args:
            reason (QtWidgets.QSystemTrayIcon.ActivationReason): Check the reason.
        """
        if reason == QtWidgets.QSystemTrayIcon.Trigger:
            self.show_check_updates()

    def show_message(self, data: str, title: str, min_width: int = 0) -> None:  # pylint: disable=too-many-locals
        """Displays a styled QMessageBox dialog with the given content.

        Args:
            data (str): Text message to display.
            title (str): Window title of the dialog.
            min_width (int): Minimum dialog width in pixels (0 = auto).
        """
        icon = QtGui.QIcon(f"{self.icon_path}/{__prgnam__}")
        icon_about = QtGui.QIcon(f"{self.icon_path}/about.png")
        icon_osinfo = QtGui.QIcon(f"{self.icon_path}/info.png")
        icon_config = QtGui.QIcon(f"{self.icon_path}/config.png")
        icon_check = QtGui.QIcon(f"{self.icon_path}/check.png")
        icon_pending = QtGui.QIcon(f"{self.icon_path}/pending.png")

        icon_start = QtGui.QIcon(f"{self.icon_path}/start.png")
        icon_stop = QtGui.QIcon(f"{self.icon_path}/stop.png")
        icon_restart = QtGui.QIcon(f"{self.icon_path}/restart.png")
        icon_status = QtGui.QIcon(f"{self.icon_path}/status.png")

        self.msg_box = QtWidgets.QMessageBox()

        bg_color, text_color = get_theme_colors()

        self.msg_box.setStyleSheet(f"""
            QWidget {{
                background-color: {bg_color};
                color: {text_color};
                font-family: monospace;
            }}
        """)

        self.msg_box.setWindowTitle(title)
        self.msg_box.setWindowIcon(icon)
        self.msg_box.setText(data)
        daemon_acted = (title.endswith(('Start', 'Stop', 'Restart'))
                        and not data.startswith(('FAILED', 'SUN is not running', 'SUN is already running')))
        if daemon_acted:
            self.msg_box.setStandardButtons(QtWidgets.QMessageBox.NoButton)
        else:
            self.msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok)

        icon_title: dict[str, Any] = {
            'SUN - About': icon_about.pixmap(42, 42),
            'SUN - OS Info': icon_osinfo.pixmap(42, 42),
            'SUN - Config': icon_config.pixmap(42, 42),
            'SUN - Check Updates': icon_check.pixmap(72, 72),
            'SUN - Pending Upgrades': icon_pending.pixmap(72, 72),
            'SUN - Daemon Start': icon_start.pixmap(72, 72),
            'SUN - Daemon Stop': icon_stop.pixmap(72, 72),
            'SUN - Daemon Restart': icon_restart.pixmap(72, 72),
            'SUN - Daemon Status': icon_status.pixmap(72, 72)
        }
        self.msg_box.setIconPixmap(icon_title[title])

        if min_width > 0:
            spacer = QtWidgets.QSpacerItem(
                min_width, 0,
                QtWidgets.QSizePolicy.Minimum,
                QtWidgets.QSizePolicy.Expanding
            )
            self.msg_box.layout().addItem(spacer, self.msg_box.layout().rowCount(), 0, 1,
                                          self.msg_box.layout().columnCount())

        self.msg_box.setModal(False)
        self.msg_box.show()

        if daemon_acted:
            QtCore.QTimer.singleShot(1000, self.show_ok_button)

    def show_ok_button(self) -> None:
        """Adds the OK button to the message box after the action delay.
        """
        self.msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok)

    def _make_badge_icon(self, count: int, color: str = '#f44336') -> QtGui.QIcon:
        """Create tray icon with a numeric badge overlay in the top-right corner.

        Draws the count on a colored circle over the base sun.png icon using
        QPainter. The badge size scales with the icon dimensions. Shows '99+' if
        count exceeds 99.

        Args:
            count (int): Number to display on the badge.
            color (str): Badge background color as a hex string (default red).

        Returns:
            QtGui.QIcon: Icon with colored badge and white count label.
        """
        base = QtGui.QPixmap(f'{self.icon_path}/{__prgnam__}.png')
        result = QtGui.QPixmap(base.size())
        result.fill(QtCore.Qt.transparent)

        painter = QtGui.QPainter(result)
        painter.drawPixmap(0, 0, base)

        label = str(count) if count <= 99 else '99+'
        w, h = base.width(), base.height()
        badge_size = max(w // 2, 10)
        badge_x = w - badge_size
        badge_y = 0

        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        painter.setBrush(QtGui.QBrush(QtGui.QColor(color)))
        painter.setPen(QtCore.Qt.NoPen)
        painter.drawEllipse(badge_x, badge_y, badge_size, badge_size)

        font = painter.font()
        font.setPixelSize(max(badge_size - 4, 6))
        font.setBold(True)
        painter.setFont(font)
        painter.setPen(QtGui.QColor('white'))
        painter.drawText(QtCore.QRect(badge_x, badge_y, badge_size, h // 2),
                         QtCore.Qt.AlignCenter, label)
        painter.end()

        return QtGui.QIcon(result)

    def show_check_updates(self) -> None:  # pylint: disable=too-many-locals
        """Show message updates.
        """
        title: str = 'SUN - Check Updates'
        data, packages = self.tool.check_updates()
        count: int = len(packages)
        _, pending_pkgs = self.tool.check_pending()
        self._apply_badge(count, len(pending_pkgs))
        warning_html: str = ''
        if not self.configs.ignore_mirror_warning:
            w = self.tool.utils.mirror_version_warning()
            if w:
                warning_html = f'<p style="color:#f0a000;">{w}</p>'
        if count > 0:
            max_pkg: int = self.configs.max_packages
            packages = packages[:max_pkg]
            max_len = max(len(p.split('  ', 1)[0]) for p in packages)
            aligned = []
            for pkg in packages:
                parts = pkg.split('  ', 1)
                aligned.append(f'{parts[0].ljust(max_len)}  {parts[1]}' if len(parts) == 2 else pkg)
            if count > max_pkg:
                aligned += ['and more...']
            if self.configs.colored_output:
                bg_color, _ = get_theme_colors()
                is_dark = QtGui.QColor(bg_color).lightness() < 128
                action_colors = self.ACTION_COLORS_DARK if is_dark else self.ACTION_COLORS_LIGHT
                lines = []
                for line in aligned:
                    for action, color in action_colors.items():
                        if f'  {action}.' in line:
                            line = line.replace(f'  {action}.',
                                                f'  <span style="color:{color};">{action}.</span>')
                            break
                    lines.append(line)
            else:
                lines = aligned
            fm = QtWidgets.QApplication.fontMetrics()
            min_width = max(fm.horizontalAdvance(line) for line in aligned) + 120
            packages_html = '<pre>' + '\n'.join(lines) + '</pre>'
            self.show_message(f'{warning_html}<h3>{data.strip()}</h3>{packages_html}', title, min_width)
        else:
            self._apply_badge(0, len(pending_pkgs))
            self.show_message(f'{warning_html}{data}' if warning_html else data, title)

    def show_pending_upgrades(self) -> None:
        """Show packages that have a newer version available but are not yet installed.
        """
        title: str = 'SUN - Pending Upgrades'
        message, packages = self.tool.check_pending()
        count: int = len(packages)
        if count > 0:
            max_pkg: int = self.configs.max_packages
            displayed = packages[:max_pkg]
            lines = [p.replace('.txz', '') for p in displayed]
            if count > max_pkg:
                lines.append('and more...')
            fm = QtWidgets.QApplication.fontMetrics()
            min_width = max(fm.horizontalAdvance(line) for line in lines) + 120
            packages_html = '<pre>' + '\n'.join(lines) + '</pre>'
            self.show_message(f'<h3>{message.strip()}</h3>{packages_html}', title, min_width)
        else:
            self.show_message(message, title)

    def show_changelog(self) -> None:
        """Shows the remote Slackware ChangeLog in a scrollable dialog.
        """
        changelog = self.tool.utils.read_repo_text_file()
        if changelog:
            changelog = '\n'.join(changelog.splitlines()[:1000])

        bg_color, text_color = get_theme_colors()

        dialog = QtWidgets.QDialog()
        dialog.setWindowTitle('SUN - ChangeLog')
        dialog.setWindowIcon(QtGui.QIcon(f'{self.icon_path}/{__prgnam__}.png'))
        dialog.resize(800, 600)
        dialog.setStyleSheet(f'background-color: {bg_color}; color: {text_color};')

        text_edit = QtWidgets.QTextEdit()
        text_edit.setReadOnly(True)
        text_edit.setFont(QtGui.QFont('Monospace', self.configs.log_font_size))

        if changelog and self.configs.colored_output:
            is_dark = QtGui.QColor(bg_color).lightness() < 128
            date_color = '#f0c040' if is_dark else '#b8860b'
            comment_color = '#88aabb' if is_dark else '#5a7080'
            lines = []
            for line in changelog.splitlines():
                escaped = html.escape(line)
                if line and line[0] in (' ', '\t'):
                    lines.append(f'<span style="color:{comment_color};">{escaped}</span>')
                elif re.match(r'^[A-Z][a-z]{2}\s', line):
                    lines.append(f'<span style="color:{date_color};font-weight:bold;">{escaped}</span>')
                else:
                    lines.append(escaped)
            text_edit.setHtml(
                f'<pre style="font-family: monospace; font-size: {self.configs.log_font_size}pt;">'
                + '\n'.join(lines) + '</pre>'
            )
        else:
            text_edit.setPlainText(changelog or 'SUN: Error: Failed to fetch ChangeLog.')

        ok_button = QtWidgets.QPushButton('OK')
        ok_button.clicked.connect(dialog.accept)
        btn_layout = QtWidgets.QHBoxLayout()
        btn_layout.addStretch()
        btn_layout.addWidget(ok_button)
        btn_layout.addStretch()

        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(text_edit)
        layout.addLayout(btn_layout)
        dialog.setLayout(layout)

        dialog.exec_()

    def show_log(self) -> None:
        """Shows the daemon log file in a scrollable dialog.
        """
        bg_color, text_color = get_theme_colors()

        dialog = QtWidgets.QDialog()
        dialog.setWindowTitle('SUN - Log')
        dialog.setWindowIcon(QtGui.QIcon(f'{self.icon_path}/{__prgnam__}.png'))
        dialog.resize(800, 500)
        dialog.setStyleSheet(f'background-color: {bg_color}; color: {text_color};')

        text_edit = QtWidgets.QTextEdit()
        text_edit.setReadOnly(True)
        text_edit.setFont(QtGui.QFont('Monospace', self.configs.log_font_size))

        if LOG_FILE.is_file():
            text_edit.setPlainText(LOG_FILE.read_text(encoding='utf-8'))
            text_edit.moveCursor(QtGui.QTextCursor.End)
        else:
            text_edit.setPlainText('SUN: Log file not found.')

        ok_button = QtWidgets.QPushButton('OK')
        ok_button.clicked.connect(dialog.accept)
        btn_layout = QtWidgets.QHBoxLayout()
        btn_layout.addStretch()
        btn_layout.addWidget(ok_button)
        btn_layout.addStretch()

        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(text_edit)
        layout.addLayout(btn_layout)
        dialog.setLayout(layout)

        dialog.exec_()

    def show_os_info(self) -> None:
        """Show message OS info.
        """
        title: str = 'SUN - OS Info'
        data: str = self.tool.utils.os_info_html()
        if not self.configs.colored_output:
            data = re.sub(r'<span[^>]*>(.*?)</span>', r'\1', data)
        self.show_message(data, title)

    def show_config(self) -> None:
        """Show sun.conf configuration dialog.
        """
        title: str = 'SUN - Config'
        data: str = self.tool.utils.config_html()
        if not self.configs.colored_output:
            data = re.sub(r'<span[^>]*>(.*?)</span>', r'\1', data)
        self.show_message(data, title)

    def daemon_start(self) -> None:
        """Show message and start the daemon.
        """
        title: str = 'SUN - Daemon Start'
        data: str = 'SUN daemon starting...'
        data = self.tool.daemon_process('start', data)
        if data == 'SUN daemon starting...':
            for _ in range(10):
                pid = self.tool.utils.get_daemon_pid()
                if pid:
                    data = f'SUN daemon starting... (PID {pid})'
                    break
                time.sleep(0.1)
            QtCore.QTimer.singleShot(3000, self._update_badge_silent)
        elif data == 'SUN is already running':
            pid = self.tool.utils.get_daemon_pid()
            if pid:
                data = f'SUN is already running (PID {pid})'
        self.show_message(data, title)

    def daemon_stop(self) -> None:
        """Show message and stop the daemon.
        """
        title: str = 'SUN - Daemon Stop'
        pid = self.tool.utils.get_daemon_pid()
        if pid:
            pid = f' (PID {pid})'
        data: str = f'SUN daemon stopping...{pid}'
        data = self.tool.daemon_process('stop', data)
        if 'stopping' in data:
            QtCore.QTimer.singleShot(0, self._update_badge_silent)
        self.show_message(data, title)

    def daemon_restart(self) -> None:
        """Show message and restart the daemon.
        """
        title: str = 'SUN - Daemon Restart'
        data: str = 'SUN daemon restarting...'
        data = self.tool.daemon_process('restart', data)
        if data == 'SUN daemon restarting...':
            for _ in range(10):
                pid = self.tool.utils.get_daemon_pid()
                if pid:
                    data = f'SUN daemon restarting... (PID {pid})'
                    break
                time.sleep(0.1)
            QtCore.QTimer.singleShot(3000, self._update_badge_silent)
        self.show_message(data, title)

    def daemon_status(self) -> None:
        """Show message status about the daemon.
        """
        title: str = 'SUN - Daemon Status'
        pid = self.tool.utils.get_daemon_pid()
        data: str = (f'SUN is running as PID {pid}\t' if self.tool.daemon_status()
                     else 'SUN is not running\t')
        self.show_message(data, title)

    def show_about(self) -> None:
        """Show about dialog box.
        """
        bg_color, text_color = get_theme_colors()

        dialog = QtWidgets.QDialog()
        dialog.setWindowTitle('SUN - About')
        dialog.setWindowIcon(QtGui.QIcon(f'{self.icon_path}/{__prgnam__}.png'))
        dialog.setFixedSize(480, 380)
        dialog.setStyleSheet(f'background-color: {bg_color}; color: {text_color};')

        # Header: icon + name/version centered
        icon_label = QtWidgets.QLabel()
        icon_label.setPixmap(QtGui.QPixmap(f'{self.icon_path}/{__prgnam__}_about.png'))
        icon_label.setAlignment(QtCore.Qt.AlignCenter)

        title_label = QtWidgets.QLabel(f'<h2>SUN {__version__}</h2>Slackware Update Notifier')
        title_label.setAlignment(QtCore.Qt.AlignCenter)

        header_layout = QtWidgets.QVBoxLayout()
        header_layout.addWidget(icon_label)
        header_layout.addWidget(title_label)

        # Content: description + links centered
        content = QtWidgets.QTextBrowser()
        content.setOpenExternalLinks(True)
        content.setFrameShape(QtWidgets.QFrame.NoFrame)
        content.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        content.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        content.setHtml(f"""
            <p align='center'>Tray notification applet and daemon for informing about package
            updates in Slackware. Also serves as a CLI tool.</p>
            <p align='center'>
            <b>Author:</b> {__author__} &lt;<a href='mailto:{__email__}'>{__email__}</a>&gt;<br>
            <b>Copyright:</b> {__copyright__} © Dimitris Zlatanidis<br>
            <b>License:</b> <a href='{__license_link__}'>{__license__}</a><br>
            <b>Website:</b> <a href='{__website__}'>{__website__}</a>
            </p>
            <p align='center'><small>
            <a href='http://www.slackware.com/'>Slackware®</a> is a Registered Trademark of Patrick Volkerding.<br>
            <a href='https://www.kernel.org/'>Linux®</a> is a Registered Trademark of Linus Torvalds.
            </small></p>
        """)
        content.setStyleSheet(f'background-color: {bg_color}; color: {text_color};')

        # Close button
        close_btn = QtWidgets.QPushButton('Close')
        close_btn.clicked.connect(dialog.accept)
        btn_layout = QtWidgets.QHBoxLayout()
        btn_layout.addStretch()
        btn_layout.addWidget(close_btn)
        btn_layout.addStretch()

        # Main layout
        main_layout = QtWidgets.QVBoxLayout()
        main_layout.addLayout(header_layout)
        main_layout.addWidget(content)
        main_layout.addLayout(btn_layout)
        dialog.setLayout(main_layout)

        dialog.exec_()

    def exit_app(self) -> None:
        """Exit the app.
        """
        print('Exiting...')
        QtWidgets.qApp.quit()


def main() -> None:
    """Initializes the Qt application and starts the system tray icon.
    """
    app = QtWidgets.QApplication(sys.argv)

    # Don't close the application when the last window is closed
    app.setQuitOnLastWindowClosed(False)

    tray_icon = TrayIcon()
    tray_icon.show()

    # Write tray PID file so sun-daemon can send SIGUSR1 to trigger badge updates.
    TRAY_PID_FILE.parent.mkdir(exist_ok=True)
    TRAY_PID_FILE.write_text(str(os.getpid()))
    app.aboutToQuit.connect(lambda: TRAY_PID_FILE.unlink(missing_ok=True))

    # Timer for checking SIGINT/SIGUSR1
    timer = QtCore.QTimer()
    timer.start(500)  # Check every 500 ms
    timer.timeout.connect(lambda: None)

    # Allow exiting with ctrl-c
    signal.signal(signal.SIGINT, lambda sig, frame: app.quit())
    # SIGUSR1 from sun-daemon: defer badge update to Qt event loop for safety.
    signal.signal(signal.SIGUSR1, lambda sig, frame: QtCore.QTimer.singleShot(0, tray_icon._update_badge_silent))  # pylint: disable=protected-access

    sys.exit(app.exec_())


def is_xserver_ready() -> str:
    """Check for DISPLAY variable on OS environment.

    Returns:
        str: DISPLAY environment value.
    """
    return os.environ.get('DISPLAY', '')


def sun_tray_autostart() -> None:
    """Check and run tray app if X server is up.
    """
    print('SUN: Checking for X server... ', flush=True, end='')
    while True:
        if is_xserver_ready():
            dl = configs_load.delay_load
            time.sleep(dl)
            print('\nSUN: Tray icon app is running...')
            break
        time.sleep(1)
    kill_duplicate_processes('sun-tray_autostart')
    main()


def kill_duplicate_processes(process_name: str) -> None:
    """It keeps the first PID and kills the rest.

    Args:
        process_name (str): The name of process.
    """
    utils = Utilities()
    pids = utils.process_pids(process_name)

    if len(pids) > 1:
        for pid in pids[1:]:
            try:
                os.kill(pid, signal.SIGTERM)  # Sends SIGTERM to terminate
                print(f"Process with PID {pid} terminated.")
            except ProcessLookupError:
                print(f"Process with PID {pid} not found.")
            except PermissionError:
                print(f"I do not have permission to terminate the process with PID {pid}.")


if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "sun_tray_autostart":
        sun_tray_autostart()
