#!/usr/bin/python
#
# Author: ncmprhnsbl
#
# This gtkdialog script opens a window with some options to obtain and update various applications
# for Porteus Linux, it is a gui for  update-*-live scripts

import sys
import logging
from os import getenv, getuid, path
from subprocess import run, Popen, CalledProcessError
from platform import machine

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Vte', '2.91')
from gi.repository import Gtk, Vte, GLib, GdkPixbuf


## Make sure we're in X
def ensure_x_session():
    if getenv('DISPLAY') is None:
        logging.error("This program requires an X session!")
        sys.exit(1)


## prompt root password(gui psu) and re-execute? probly a bit dirty..
## using subprocess rather than os.system() to be script agnostic..
def ensure_root():
    if getuid() == 0:
        return

    logging.info("Not root, trying to elevate privileges via psu...")
    this_script = path.abspath(__file__)
    try:
        run(['psu', this_script], check=True)
    except FileNotFoundError:
        logging.error("psu not found. Please run this script as root manually.")
        sys.exit(1)
    except CalledProcessError:
        logging.error("psu failed. Possibly wrong password or user abort.")
        sys.exit(1)
    sys.exit(0)


class ModuleCentreWindow(Gtk.Window):
    ICON_NAMES = {
        'firefox':        ['firefox', 'firefox-esr', 'web-browser'],
        'chrome':         ['google-chrome', 'chrome', 'web-browser'],
        'palemoon':       ['palemoon', 'web-browser'],
        'opera':          ['opera', 'opera-browser', 'web-browser'],
        'chromium-gost':  ['chromium-gost', 'chromium-browser', 'web-browser'],
        'vivaldi':        ['vivaldi', 'vivaldi-stable', 'web-browser'],
        'telegram':       ['telegram', 'telegram-desktop', 'im-message'],
        'max':            ['/usr/share/pixmaps/max.png', 'max', 'chat', 'im-message'],
        'anydesk':        ['anydesk', 'anydesk-icon', 'remote-desktop'],
        'assistant':      ['assistant', 'remote-desktop', 'preferences-desktop-remote-desktop'],
        'rustdesk':       ['rustdesk', 'rustdesk-icon', 'remote-desktop'],
        'remmina':        ['org.remmina.Remmina', 'remmina', 'remmina-icon', 'remote-desktop'],
        'chromium':       ['chromium', 'chromium-browser', 'web-browser'],
        'netsurf':        ['netsurf', 'netsurf-icon', 'web-browser'],
        'yt-dlp':         ['youtube-dl', 'download', 'folder-download', 'emblem-downloads'],
        'gimp':           ['gimp', 'gimp-icon', 'applications-graphics'],
        'kdenlive':       ['/usr/share/pixmaps/kdenlive.png', 'kdenlive', 'applications-multimedia', 'video-x-generic'],
        'shotcut':        ['/usr/share/pixmaps/shotcut.png', 'shotcut', 'applications-multimedia', 'video-x-generic'],
        'libreoffice':    ['libreoffice-startcenter', 'libreoffice', 'office', 'x-office-document'],
        'onlyoffice':     ['onlyoffice-desktopeditors', 'onlyoffice', 'office'],
        'wps-office':     ['wps-office', 'wps-office2019', 'office', 'x-office-document'],
        'printing':       ['printer', 'printer-config', 'printing'],
        'thunderbird':    ['thunderbird', 'thunderbird-icon', 'mail', 'emblem-mail'],
        'vbox':           ['/usr/share/pixmaps/porteus/vbox.png', 'virtualbox', 'virtualbox-icon', 'system'],
        'naps2':          ['com.naps2.Naps2', 'naps2', 'scanner', 'scan', 'accessories-scan'],
        'csp':            ['/opt/cprocsp/share/icons/cptools.png', 'security-high', 'changes-prevent'],
        'csp-plugin':     ['/opt/cprocsp/share/icons/cptools.png', 'security-high'],
        'csp-qset':       ['/opt/cprocsp/share/icons/cptools.png', 'security-high'],
        'gosplugin':      ['flag-ru', 'flag', 'emblem-nation', 'government', 'applications-system'],
        'nr-plugin':      ['/opt/nr-plugin/share/nr_plugin.ico', 'network', 'application-x-executable'],
        'recordsupport':  ['/opt/nr-plugin/share/nr_plugin.ico', 'media-record', 'audio'],
    }

    def __init__(self, original_user):
        # Determine language BEFORE super() call so _tr works
        lang = getenv('LANG', '')
        self.is_russian = lang.startswith('ru')

        super().__init__(
            title=self._tr("Porteus Module Centre", "Центр модулей Porteus"),
            border_width=5,
            default_height=480,
            default_width=650,
            icon_name="cdr"
        )
        self.set_position(Gtk.WindowPosition.CENTER)  # Center window on start
        self.original_user = original_user
        self.active_terminal = None

        vbox = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL)
        self.add(vbox)

        label_header = Gtk.Label()
        label_header.set_markup(
            '<span size="x-large" weight="bold">' +
            self._tr("Choose a Module to Install or to Update",
                     "Выберите модуль для установки или обновления") +
            '</span>'
        )
        vbox.pack_start(label_header, False, False, 5)

        vbox.pack_start(Gtk.Separator(), False, False, 5)

        scrolled = Gtk.ScrolledWindow(hexpand=True, vexpand=True)
        vbox.pack_start(scrolled, True, True, 5)

        vbox_inner = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL)

        # --- Network ---
        vbox_net = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL)
        lbl_net = Gtk.Label()
        lbl_net.set_markup('<span size="large" weight="bold">\t' +
                           self._tr("Network:", "Сеть:") + '</span>')
        lbl_net.set_halign(Gtk.Align.START)
        vbox_net.pack_start(lbl_net, False, False, 5)

        flowbox_net = Gtk.FlowBox(max_children_per_line=5, row_spacing=25, homogeneous=True)
        self._add_button("Firefox", "Mozilla's web browser", "firefox", flowbox_net)
        if machine() == "x86_64":
            self._add_button("Chrome", "Google's web browser", "chrome", flowbox_net)
            self._add_button("Palemoon", "Lightweight web browser based on older Firefox", "palemoon", flowbox_net)
            self._add_button("Opera", "Chromium based web browser", "opera", flowbox_net)
            self._add_button("Chromium-gost", "Chromium based web browser for Russian banking", "chromium-gost", flowbox_net)
            self._add_button("Vivaldi", "Chromium based web browser", "vivaldi", flowbox_net)
            self._add_button("Telegram", "Desktop Messaging Client", "telegram", flowbox_net)
            self._add_button("Max", "Desktop Messaging Client", "max", flowbox_net)
            self._add_button("AnyDesk", "Remote Desktop Access", "anydesk", flowbox_net)
            self._add_button(
                self._tr("Assistant", "Ассистент"),
                self._tr("Remote Desktop Access", "Удалённый доступ к рабочему столу"),
                "assistant",
                flowbox_net
            )
            self._add_button("RustDesk", "Remote Desktop Access", "rustdesk", flowbox_net)
            self._add_button("Remmina", "Remote Desktop Access (requires 05-devel.xzm)", "remmina", flowbox_net)
        self._add_button("Chromium", "The open source web browser with google elements removed", "chromium", flowbox_net)
        self._add_button("Netsurf", "Very lightweight Web Browser", "netsurf", flowbox_net)
        self._add_button("Yt-dlp", "(Console)Download Videos from the Web", "yt-dlp", flowbox_net)
        vbox_net.pack_start(flowbox_net, False, False, 5)

        # --- Graphics, audio and video ---
        vbox_gfx = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL)
        lbl_gfx = Gtk.Label()
        lbl_gfx.set_markup('<span size="large" weight="bold">\t' +
                           self._tr("Graphics, audio and video:", "Графика, аудио и видео:") + '</span>')
        lbl_gfx.set_halign(Gtk.Align.START)
        vbox_gfx.pack_start(lbl_gfx, False, False, 5)

        flowbox_gfx = Gtk.FlowBox(max_children_per_line=5, row_spacing=25, homogeneous=True)
        self._add_button("Gimp", "Gnu Image Manipulation Program", "gimp", flowbox_gfx)
        self._add_button("Kdenlive", self._tr("Video Editor", "Видеоредактор"), "kdenlive", flowbox_gfx)
        self._add_button("Shotcut", self._tr("Video Editor", "Видеоредактор"), "shotcut", flowbox_gfx)
        vbox_gfx.pack_start(flowbox_gfx, False, False, 5)

        # --- Office ---
        vbox_off = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL)
        lbl_off = Gtk.Label()
        lbl_off.set_markup('<span size="large" weight="bold">\t' +
                           self._tr("Office:", "Офис:") + '</span>')
        lbl_off.set_halign(Gtk.Align.START)
        vbox_off.pack_start(lbl_off, False, False, 5)

        flowbox_off = Gtk.FlowBox(max_children_per_line=5, row_spacing=25, homogeneous=True)
        if machine() == "x86_64":
            self._add_button("LibreOffice", "Office Suite", "libreoffice", flowbox_off)
            self._add_button("OnlyOffice", "Office Suite", "onlyoffice", flowbox_off)
        self._add_button("WPS Office", "Office Suite", "wps-office", flowbox_off)
        self._add_button(
            self._tr("Printing", "Печать"),
            self._tr("Printing Tools", "Инструменты печати"),
            "printing", flowbox_off
        )
        self._add_button(
            self._tr("Printing Extra", "Печать (доп.)"),
            self._tr("Printing Tools (requires 05-devel.xzm)", "Инструменты печати (требуется 05-devel.xzm)"),
            "printing", flowbox_off
        )
        self._add_button("Thunderbird", "Mozilla's email client", "thunderbird", flowbox_off)
        self._add_button("Naps2", "Scanning software (requires 05-devel.xzm)", "naps2", flowbox_off)
        vbox_off.pack_start(flowbox_off, False, False, 5)

        vbox_inner.pack_start(vbox_net, True, True, 5)
        vbox_inner.pack_start(vbox_gfx, True, True, 5)
        vbox_inner.pack_start(vbox_off, True, True, 5)

        if machine() == "x86_64":
            vbox_virt = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL)
            lbl_virt = Gtk.Label()
            lbl_virt.set_markup('<span size="large" weight="bold">\t' +
                                self._tr("Virtualization:", "Визуализация:") + '</span>')
            lbl_virt.set_halign(Gtk.Align.START)
            vbox_virt.pack_start(lbl_virt, False, False, 5)

            flowbox_virt = Gtk.FlowBox(max_children_per_line=5, row_spacing=25, homogeneous=True)
            self._add_button("VirtualBox", "Virtual Computer (requires 05-devel.xzm)", "vbox", flowbox_virt)
            vbox_virt.pack_start(flowbox_virt, False, False, 5)
            vbox_inner.pack_start(vbox_virt, True, True, 5)

        # --- Russian Services ---
        vbox_rus = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL)
        lbl_rus = Gtk.Label()
        lbl_rus.set_markup('<span size="large" weight="bold">\t' +
                           self._tr("Russian Services:", "Российские сервисы:") + '</span>')
        lbl_rus.set_halign(Gtk.Align.START)
        vbox_rus.pack_start(lbl_rus, False, False, 5)

        flowbox_rus = Gtk.FlowBox(max_children_per_line=5, row_spacing=25, homogeneous=True)
        self._add_button(
            self._tr("CryptoPro CSP", "КриптоПро CSP"),
            self._tr("CryptoPro CSP", "КриптоПро CSP"),
            "csp", flowbox_rus
        )
        self._add_button(
            self._tr("CryptoPro EDS Browser plug-in", "КриптоПро ЭЦП Browser plug-in"),
            self._tr("CryptoPro EDS Browser plug-in", "КриптоПро ЭЦП Browser plug-in"),
            "csp-plugin", flowbox_rus
        )
        self._add_button(
            self._tr("Auto setup CryptoPro CSP", "Авто настройка КриптоПро CSP"),
            self._tr("Auto setup CryptoPro CSP", "Авто настройка КриптоПро CSP"),
            "csp-qset", flowbox_rus
        )
        self._add_button(
            self._tr("Gosplugin", "Госплагин"),
            self._tr("Plugin for government portals", "Плагин для государственных порталов"),
            "gosplugin", flowbox_rus
        )
        self._add_button(
            self._tr("NR Plugin", "Плагин ГИС НР"),
            self._tr("NR Plugin", "Плагин ГИС НР (ЕИС и ГИС Торги)"),
            "nr-plugin", flowbox_rus
        )
        self._add_button(
            self._tr("Record Support", "Плагин ФДП"),
            self._tr("Plugin for recording user actions", "Плагин фиксации действий пользователя (ЕИС и ГИС Торги)"),
            "recordsupport", flowbox_rus
        )
        vbox_rus.pack_start(flowbox_rus, False, False, 5)
        vbox_inner.pack_start(vbox_rus, True, True, 5)

        scrolled.add(vbox_inner)

        vbox.pack_start(Gtk.Separator(), False, False, 10)

        # Bottom panel
        hbox_bottom = Gtk.Box(spacing=5, homogeneous=False)

        btn_close = Gtk.Button.new_with_label(self._tr("Close", "Закрыть"))
        btn_close.connect("clicked", self._on_close)
        hbox_bottom.pack_end(btn_close, False, False, 2)

        img_lynx = self._safe_image_from_icons(["browser", "web-browser"], Gtk.IconSize.BUTTON)
        btn_lynx = Gtk.Button.new_with_label(self._tr("Run Lynx", "Запустить Lynx"))
        btn_lynx.set_image(img_lynx)
        btn_lynx.set_always_show_image(True)
        btn_lynx.connect("clicked", self._on_lynx)
        hbox_bottom.pack_end(btn_lynx, False, False, 2)

        img_help = self._safe_image_from_icons(["help-about", "help-browser"], Gtk.IconSize.BUTTON)
        btn_help = Gtk.Button.new_with_label(self._tr("Help", "Справка"))
        btn_help.set_image(img_help)
        btn_help.set_always_show_image(True)
        btn_help.connect("clicked", self._on_help)
        hbox_bottom.pack_end(btn_help, False, False, 2)

        img_file = self._safe_image_from_icons(["cdr", "system-file-manager", "file-manager"], Gtk.IconSize.BUTTON)
        btn_file = Gtk.Button.new_with_label(self._tr("File", "Файл"))
        btn_file.set_image(img_file)
        btn_file.set_always_show_image(True)
        btn_file.connect("clicked", self._on_file)
        hbox_bottom.pack_end(btn_file, False, False, 2)

        vbox.pack_start(hbox_bottom, False, False, 5)

    def _tr(self, english_text, russian_text):
        return russian_text if self.is_russian else english_text

    def _safe_image_from_icons(self, icon_list, size, pixel_size=None):
        for entry in icon_list:
            if path.isfile(entry):
                try:
                    if pixel_size is not None:
                        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
                            entry, pixel_size, pixel_size, True
                        )
                        image = Gtk.Image.new_from_pixbuf(pixbuf)
                    else:
                        image = Gtk.Image.new_from_file(entry)
                    return image
                except Exception as e:
                    logging.debug(f"Failed to load icon from file '{entry}': {e}")
                    continue
            else:
                try:
                    image = Gtk.Image.new_from_icon_name(entry, size)
                    if pixel_size is not None:
                        image.set_pixel_size(pixel_size)
                    return image
                except Exception as e:
                    logging.debug(f"Icon name '{entry}' not loaded: {e}")
                    continue
        logging.warning(f"None of the icons {icon_list} could be loaded, using fallback.")
        icon_theme = Gtk.IconTheme.get_default()
        fallback = "image-missing" if icon_theme.has_icon("image-missing") else "gtk-missing-image"
        image = Gtk.Image.new_from_icon_name(fallback, size)
        if pixel_size is not None:
            image.set_pixel_size(pixel_size)
        return image

    def _add_button(self, text, tooltip, option_name, flowbox):
        icon_list = self.ICON_NAMES.get(option_name, [option_name])
        img = self._safe_image_from_icons(icon_list, Gtk.IconSize.DND, pixel_size=48)
        button = Gtk.Button.new_with_label(text)
        button.set_image(img)
        button.set_image_position(Gtk.PositionType.TOP)
        button.set_always_show_image(True)
        button.set_tooltip_text(tooltip)
        button.set_relief(Gtk.ReliefStyle.NONE)
        button.set_focus_on_click(False)
        button.connect("clicked", self._on_module_button, text, option_name)
        flowbox.add(button)

    def _on_module_button(self, button, display_name, option):
        update_script = '/opt/porteus-scripts/update'
        if not path.isfile(update_script):
            logging.error(f"Update script not found: {update_script}")
            self._show_error(
                self._tr("Update script is missing. Please check your installation.",
                         "Скрипт обновления не найден. Проверьте установку.")
            )
            return

        if self.active_terminal is not None:
            logging.warning("Another update process is already running.")
            return

        dialog = UpdateTerminalDialog(
            self,
            self._tr(f"Updating {display_name}", f"Обновление {display_name}"),
            option
        )
        dialog.connect("response", self._on_terminal_response)
        dialog.show_all()
        self.active_terminal = dialog

    def _on_terminal_response(self, dialog, response_id):
        self.active_terminal = None
        dialog.destroy()

    def _on_close(self, button):
        Gtk.main_quit()

    def _on_lynx(self, button):
        if not self.original_user:
            logging.error("Original user unknown, cannot launch Lynx.")
            return
        try:
            Popen(['su', '--login', self.original_user, 'vterm', '/usr/bin/lynx'],
                  stdin=None, stdout=None, stderr=None, close_fds=True)
        except FileNotFoundError as e:
            logging.error(f"Failed to launch Lynx: {e}")
            self._show_error(
                self._tr(f"Could not run Lynx: {e}", f"Не удалось запустить Lynx: {e}")
            )

    def _on_help(self, button):
        help_dlg = HelpDialog(self, self._tr)
        help_dlg.run()
        help_dlg.destroy()

    def _on_file(self, button):
        try:
            Popen(['dbus-run-session', '/opt/porteus-scripts/xorg/fmanager', '/tmp'],
                  stdin=None, stdout=None, stderr=None, close_fds=True)
        except FileNotFoundError as e:
            logging.error(f"Failed to launch file manager: {e}")
            self._show_error(
                self._tr(f"Could not run file manager: {e}",
                         f"Не удалось запустить файловый менеджер: {e}")
            )

    def _show_error(self, message):
        dlg = Gtk.MessageDialog(transient_for=self,
                                flags=0,
                                message_type=Gtk.MessageType.ERROR,
                                buttons=Gtk.ButtonsType.OK,
                                text=message)
        dlg.run()
        dlg.destroy()


class UpdateTerminalDialog(Gtk.Dialog):
    def __init__(self, parent, header_text, option):
        super().__init__(title="Porteus Terminal", transient_for=parent, flags=0)
        self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)

        self.set_default_size(450, 550)

        content_area = self.get_content_area()

        label = Gtk.Label()
        label.set_markup(f'<span size="x-large" weight="bold">{header_text}</span>')
        content_area.add(label)

        self.terminal = Vte.Terminal()
        self.terminal.spawn_async(
            Vte.PtyFlags.DEFAULT,
            None,
            ['/opt/porteus-scripts/update', option],
            None,
            GLib.SpawnFlags.DO_NOT_REAP_CHILD,
            None, None, -1, None,
            self._on_spawn_complete
        )

        scrolled = Gtk.ScrolledWindow()
        scrolled.add(self.terminal)
        content_area.pack_start(scrolled, True, True, 5)

    def _on_spawn_complete(self, terminal, task):
        try:
            terminal.spawn_async_finish(task)
        except GLib.Error as e:
            logging.error(f"Failed to spawn update process: {e.message}")
            error_dlg = Gtk.MessageDialog(transient_for=self,
                                          flags=0,
                                          message_type=Gtk.MessageType.ERROR,
                                          buttons=Gtk.ButtonsType.OK,
                                          text=f"Failed to start update:\n{e.message}")
            error_dlg.run()
            error_dlg.destroy()
            self.response(Gtk.ResponseType.OK)


class HelpDialog(Gtk.Dialog):
    def __init__(self, parent, tr_func):
        super().__init__(title=tr_func("Help", "Справка"), transient_for=parent, flags=0)
        self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
        self.set_default_size(550, 500)

        content_area = self.get_content_area()

        label_header = Gtk.Label()
        label_header.set_markup('<span size="x-large" weight="bold">' +
                                tr_func("Porteus Module Centre Help",
                                        "Справка Центра модулей Porteus") + '</span>')
        content_area.add(label_header)

        scrolled = Gtk.ScrolledWindow(hexpand=True, vexpand=True)
        content_area.pack_start(scrolled, True, True, 5)

        help_text = tr_func(
            "Porteus Module Centre is to help you get or update selected software.\n"
            "Choose by clicking a button and follow the y/n prompts in the terminal window that opens.\n"
            "Some option offer a choice, either to download a premade module from our server or to build it from official packages.\n\n"
            "If you choose the latter, for Firefox, Chrome, Chromium, or Opera you can choose your preferred language and "
            "set a custom homepage (the default homepage is the Porteus forum: https://forum.porteus.org).\n\n"
            "At the end, the chosen module(s) can be found in /tmp directory. Multiple modules can be chosen (one at a time).\n\n"
            "The Run Lynx button launches a terminal with the included Lynx text based Web browser.\n\n"
            "You can use the File button below to open a Privileged (Root user) Filemanager or you can use the normal (Guest user) Filemanager, "
            "to move the module to your porteus modules directory or somewhere else outside the live filesystem to survive reboot. "
            "Don't forget to activate it (double click). The last activated browser will become the icon visible on the toolbar.",

            "Центр модулей Porteus помогает получить или обновить выбранное программное обеспечение.\n"
            "Нажмите кнопку и следуйте инструкциям y/n в открывшемся окне терминала.\n"
            "Некоторые опции предлагают выбор: загрузить готовый модуль с нашего сервера или собрать из официальных пакетов.\n\n"
            "При выборе сборки для Firefox, Chrome, Chromium или Opera можно указать предпочитаемый язык и "
            "задать домашнюю страницу (по умолчанию — форум Porteus: https://forum.porteus.org).\n\n"
            "Собранные модули будут находиться в каталоге /tmp. Можно выбрать несколько модулей (по очереди).\n\n"
            "Кнопка «Запустить Lynx» открывает терминал со встроенным текстовым браузером Lynx.\n\n"
            "Кнопка «Файл» открывает файловый менеджер с правами root (или гостевой), "
            "чтобы переместить модуль в папку модулей Porteus или другое место за пределами live-системы для сохранения после перезагрузки. "
            "Не забудьте активировать модуль (двойной щелчок). Последний активированный браузер станет значком на панели инструментов."
        )

        textview = Gtk.TextView(border_width=20,
                                editable=False,
                                wrap_mode=Gtk.WrapMode.WORD,
                                justification=Gtk.Justification.FILL,
                                cursor_visible=False)
        textview.get_buffer().set_text(help_text)

        scrolled.add(textview)

        self.show_all()


def main():
    logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

    ensure_x_session()
    original_user = getenv('SUDO_USER') or getenv('USER')
    ensure_root()
    win = ModuleCentreWindow(original_user)
    win.connect("destroy", Gtk.main_quit)
    win.show_all()
    Gtk.main()


if __name__ == '__main__':
    main()
