#!/usr/bin/env python3

# Copyright 2016-2020  Jay Flood, SP, Brasil
# All rights reserved.
#
# Redistribution and use of this script, with or without modification, is
# permitted provided that the following conditions are met:
#
# 1. Redistributions of this script must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# Author: brokenman@porteus.org
#
# Python port of Porteus bundles downloader (GUI + console modes).
# Run with --console for terminal mode, otherwise GTK3 GUI is used.

import os, sys, re, hashlib, shutil, tempfile, subprocess
import urllib.request, threading
from pathlib import Path

# ----------------------------------------------------------------------
# Internationalisation (i18n) – easily extensible
# ----------------------------------------------------------------------
# Translation dictionaries. Add new languages as new entries.
# Keys are the original English strings; values are the translated strings.
TRANSLATIONS = {
    'ru': {
        'Porteus bundles downloader': 'Загрузчик бандлов Porteus',
        'Module': 'Бандл',
        'Cancel': 'Отмена',
        'OK': 'ОК',
        'Could not download the bundle list from:\n{url}':
            'Не удалось загрузить список бандлов с:\n{url}',
        'Bundle list does not contain any .xzm modules at:\n{url}':
            'Список бандлов не содержит .xzm модулей по адресу:\n{url}',
        'No bundles found for current architecture and desktop.':
            'Для текущей архитектуры и рабочего стола бандлов не найдено.',
        'You must select a bundle to download.': 'Необходимо выбрать бандл для загрузки.',
        'Download failed': 'Ошибка загрузки',
        'Checksum match. Module verified.': 'Контрольная сумма совпадает. Бандл проверен.',
        'Checksum mismatch! Possible corrupt module.':
            'Несовпадение контрольной суммы! Возможно бандл повреждён.',
        'Module activation': 'Активация бандла',
        'Would you like to activate the module now?':
            'Активировать бандл сейчас?',
        'Module activated': 'Бандл активирован',
        'has been activated.': 'активирован.',
        'Activation failed': 'Ошибка активации',
        'This script can only run on Porteus.':
            'Этот скрипт может работать только в Porteus.',
        'Only root can run this script. (console mode)':
            'Этот скрипт может запускать только root. (консольный режим)',
        'Welcome to the Porteus bundle manager':
            'Добро пожаловать в менеджер бандлов Porteus',
        'Updating list of packages ...': 'Обновление списка пакетов ...',
        'Available bundles:': 'Доступные бандлы:',
        'Enter number of the bundle to download (0 to quit):':
            'Введите номер бандла для загрузки (0 для выхода):',
        'Invalid selection.': 'Неверный выбор.',
        'Downloading': 'Загрузка',
        'Verifying download integrity...': 'Проверка целостности загрузки...',
        'Success. File saved to:': 'Готово. Файл сохранён в:',
        'Target directory does not exist or is not writable.':
            'Целевая папка не существует или недоступна для записи.',
        'Enter download path:': 'Укажите путь для сохранения:',
        'Checksum verification skipped:': 'Проверка контрольной суммы пропущена:',
        '  Porteus bundles downloader': '  Загрузчик бандлов Porteus',
    },
    # Add other languages here, e.g.:
    # 'de': { ... },
}

def _(text, **kwargs):
    """
    Return translated string if available for the current locale,
    otherwise return the original English text.
    Supports Python's str.format() placeholders.
    """
    lang = os.environ.get('LANG', '')
    # Use only the first two characters to match the language code
    lang_code = lang[:2] if len(lang) >= 2 else 'en'
    # Get translation dictionary for the language, fallback to English
    lang_dict = TRANSLATIONS.get(lang_code, {})
    translated = lang_dict.get(text, text)
    # Apply any formatting arguments
    if kwargs:
        return translated.format(**kwargs)
    return translated

# ----------------------------------------------------------------------
# Porteus helpers (no bash needed)
# ----------------------------------------------------------------------
def is_porteus():
    return os.path.isfile('/etc/porteus-version')

def is_writable(path):
    return os.access(path, os.W_OK)

def get_desktop():
    """Get current desktop name in lowercase."""
    try:
        res = subprocess.run(['bash', '-c',
            '. /usr/share/porteus/porteus-functions; get_desktop'],
            capture_output=True, text=True, timeout=5)
        if res.returncode == 0 and res.stdout.strip():
            return res.stdout.strip().lower()
    except:
        pass
    desktop = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()
    if not desktop:
        try:
            with open('/etc/porteus/desktop') as f:
                desktop = f.read().strip().lower()
        except:
            pass
    return desktop

def get_server():
    try:
        with open('/etc/porteus.conf') as f:
            for line in f:
                if line.startswith('SERVER='):
                    return line.split('=', 1)[1].strip()
    except:
        pass
    return 'http://dl.porteus.org'

# ----------------------------------------------------------------------
# Core download logic (shared between GUI and console)
# ----------------------------------------------------------------------
def fetch_bundle_list(server, arch, pver, desktop, tmpdir):
    """Download index.html and return list of .xzm names matching arch & desktop."""
    url = f"{server}/{arch}/{pver}/bundles/"
    idx_path = os.path.join(tmpdir, 'index.html')
    try:
        urllib.request.urlretrieve(url, idx_path)
    except Exception as e:
        raise RuntimeError(
            _('Could not download the bundle list from:\n{url}', url=url) + f'\n{e}'
        )

    if not os.path.isfile(idx_path):
        raise RuntimeError(
            _('Could not download the bundle list from:\n{url}', url=url)
        )

    with open(idx_path, 'r', errors='ignore') as f:
        content = f.read()

    if '.xzm' not in content:
        raise RuntimeError(
            _('Bundle list does not contain any .xzm modules at:\n{url}', url=url)
        )

    # Extract href links, filter by arch and desktop
    links = re.findall(r'<a\s+href="([^"]+\.xzm)"', content, re.IGNORECASE)
    filtered = [m for m in links
                if (arch in m or 'alldesktops' in m)
                and (desktop in m or 'alldesktops' in m)]
    if not filtered:
        raise RuntimeError(
            _('No bundles found for current architecture and desktop.')
        )
    return sorted(filtered)

def download_file(url, destination, progress_callback=None, cancel_check=None):
    """Download a file with optional progress (0..1) and cancel support."""
    try:
        resp = urllib.request.urlopen(url)
        total = int(resp.headers.get('Content-Length', 0))
        downloaded = 0
        with open(destination, 'wb') as f:
            while True:
                if cancel_check and cancel_check():
                    raise Exception("Cancelled")
                chunk = resp.read(8192)
                if not chunk:
                    break
                f.write(chunk)
                downloaded += len(chunk)
                if progress_callback and total > 0:
                    progress_callback(downloaded / total)
        if total > 0 and downloaded < total:
            raise Exception("Download incomplete")
    except:
        if os.path.isfile(destination):
            os.unlink(destination)
        raise

def md5_verify(filepath, module_name, server, arch, pver):
    """Compare md5sum with server's md5sums.txt. Returns True if match, raises on error."""
    md5_url = f"{server}/{arch}/{pver}/bundles/md5sums.txt"
    try:
        with urllib.request.urlopen(md5_url) as resp:
            data = resp.read().decode('utf-8', errors='ignore')
        expected = None
        for line in data.splitlines():
            if module_name in line:
                expected = line.split()[0]
                break
        if not expected:
            raise ValueError("md5sum not found for this module")
    except Exception as e:
        raise RuntimeError(f"{_('Checksum verification skipped:')} {e}")

    h = hashlib.md5()
    with open(filepath, 'rb') as f:
        while chunk := f.read(8192):
            h.update(chunk)
    if h.hexdigest() != expected:
        raise ValueError(_('Checksum mismatch! Possible corrupt module.'))
    return True

# ----------------------------------------------------------------------
# Console mode
# ----------------------------------------------------------------------
def console_mode(output_dir=None):
    """Interactive terminal version."""
    if os.geteuid() != 0:
        print(_('Only root can run this script. (console mode)'))
        sys.exit(1)

    server = get_server()
    arch = 'x86_64' if os.uname().machine == 'x86_64' else 'i586'
    with open('/etc/porteus-version') as f:
        pver = f.read().strip()
    desktop = get_desktop()
    tmpdir = tempfile.mkdtemp(prefix='.bundle.')

    print()
    print(_('Welcome to the Porteus bundle manager'))
    print(_('Updating list of packages ...'))

    try:
        modules = fetch_bundle_list(server, arch, pver, desktop, tmpdir)
    except RuntimeError as e:
        print(e)
        shutil.rmtree(tmpdir, ignore_errors=True)
        sys.exit(1)

    print(_('Available bundles:'))
    for i, mod in enumerate(modules, 1):
        print(f"{i:3d}. {mod}")

    # User selection
    while True:
        try:
            choice = input(_('Enter number of the bundle to download (0 to quit): '))
            choice = int(choice)
            if choice == 0:
                shutil.rmtree(tmpdir, ignore_errors=True)
                return
            if 1 <= choice <= len(modules):
                selected = modules[choice - 1]
                break
        except (ValueError, IndexError):
            pass
        print(_('Invalid selection.'))

    # Target directory
    if output_dir:
        target_dir = output_dir
    else:
        target_dir = input(_('Enter download path: ') + ' ').strip()
    if not target_dir or not os.path.isdir(target_dir) or not os.access(target_dir, os.W_OK):
        print(_('Target directory does not exist or is not writable.'))
        shutil.rmtree(tmpdir, ignore_errors=True)
        sys.exit(1)

    # Download
    url = f"{server}/{arch}/{pver}/bundles/{selected}"
    dest = os.path.join(target_dir, selected)
    print(f"\n{_('Downloading')} {selected} ...")
    last_pct = 0
    def progress(p):
        nonlocal last_pct
        pct = int(p * 100)
        if pct != last_pct:
            print(f"\rProgress: {pct:3d}%", end='', flush=True)
            last_pct = pct
    try:
        download_file(url, dest, progress_callback=progress)
        print()
    except Exception as e:
        print(f"\n{_('Download failed')}: {e}")
        shutil.rmtree(tmpdir, ignore_errors=True)
        sys.exit(1)

    # Verify
    print(_('Verifying download integrity...'))
    try:
        md5_verify(dest, selected, server, arch, pver)
        print(_('Checksum match. Module verified.'))
    except (ValueError, RuntimeError) as e:
        print(e)
        shutil.rmtree(tmpdir, ignore_errors=True)
        sys.exit(1)

    print(_('Success. File saved to:'), dest)
    shutil.rmtree(tmpdir, ignore_errors=True)

# ----------------------------------------------------------------------
# GUI mode
# ----------------------------------------------------------------------
def gui_mode():
    import gi
    gi.require_version('Gtk', '3.0')
    gi.require_version('GLib', '2.0')
    from gi.repository import Gtk, GLib, GdkPixbuf

    if not is_porteus():
        print("This script can only run on Porteus.")
        sys.exit(1)

    server = get_server()
    arch = 'x86_64' if os.uname().machine == 'x86_64' else 'i586'
    with open('/etc/porteus-version') as f:
        pver = f.read().strip()
    desktop = get_desktop()
    tmpdir = tempfile.mkdtemp(prefix='.bundle.')

    # Determine default download directory
    moddir = os.environ.get('MODDIR', '')
    target_dir = moddir if (moddir and is_writable(moddir)) else '/tmp'

    class BundleWindow(Gtk.Window):
        def __init__(self):
            super().__init__(title=_('Porteus bundles downloader'))
            self.set_border_width(10)
            self.set_position(Gtk.WindowPosition.CENTER)
            try:
                self.set_icon_name('cdr')
            except:
                pass
            self.modules = []
            self.selected_module = None
            self.store = Gtk.ListStore(str)
            self.build_ui()
            threading.Thread(target=self.load_list, daemon=True).start()

        def build_ui(self):
            vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
            self.add(vbox)

            # Header
            hbox = Gtk.Box(spacing=6)
            try:
                pix = GdkPixbuf.Pixbuf.new_from_icon_name('cdr', Gtk.IconSize.DIALOG)
                img = Gtk.Image.new_from_pixbuf(pix)
            except:
                img = Gtk.Image.new_from_icon_name('cdr', Gtk.IconSize.DIALOG)
            hbox.pack_start(img, False, False, 0)
            lbl = Gtk.Label()
            lbl.set_markup('<span size="x-large">' + _('  Porteus bundles downloader') + '</span>')
            lbl.set_halign(Gtk.Align.START)
            hbox.pack_start(lbl, True, True, 0)
            vbox.pack_start(hbox, False, False, 0)
            vbox.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)

            # Scrolled list
            frame = Gtk.Frame()
            scroll = Gtk.ScrolledWindow()
            scroll.set_min_content_width(400)
            scroll.set_min_content_height(300)
            self.tree = Gtk.TreeView(model=self.store)
            renderer = Gtk.CellRendererText()
            col = Gtk.TreeViewColumn(_('Module'), renderer, text=0)
            self.tree.append_column(col)
            self.tree.set_headers_visible(False)
            scroll.add(self.tree)
            frame.add(scroll)
            vbox.pack_start(frame, True, True, 0)

            # Buttons
            btn_box = Gtk.Box(spacing=8)
            btn_box.set_halign(Gtk.Align.END)
            btn_cancel = Gtk.Button(label=_('Cancel'))
            btn_cancel.connect('clicked', self.on_cancel)
            btn_ok = Gtk.Button(label=_('OK'))
            btn_ok.connect('clicked', self.on_ok)
            btn_box.pack_start(btn_cancel, False, False, 0)
            btn_box.pack_start(btn_ok, False, False, 0)
            vbox.pack_start(btn_box, False, False, 0)
            self.show_all()

        def load_list(self):
            try:
                self.modules = fetch_bundle_list(server, arch, pver, desktop, tmpdir)
            except RuntimeError as e:
                GLib.idle_add(self.show_error, str(e))
                return
            GLib.idle_add(self.populate)

        def populate(self):
            for m in self.modules:
                self.store.append([m])
            return False

        def show_error(self, msg):
            dlg = Gtk.MessageDialog(transient_for=self, flags=0,
                                    message_type=Gtk.MessageType.ERROR,
                                    buttons=Gtk.ButtonsType.CLOSE, text=msg)
            dlg.run()
            dlg.destroy()
            self.quit()

        def on_cancel(self, w):
            self.quit()

        def on_ok(self, w):
            sel = self.tree.get_selection()
            model, it = sel.get_selected()
            if it is None:
                self.show_error(_('You must select a bundle to download.'))
                return
            self.selected_module = model[it][0]
            if not self.selected_module.endswith('.xzm'):
                self.show_error(_('You must select a bundle to download.'))
                return
            self.hide()
            threading.Thread(target=self.download_and_verify, daemon=True).start()

        def download_and_verify(self):
            url = f"{server}/{arch}/{pver}/bundles/{self.selected_module}"
            dest = os.path.join(target_dir, self.selected_module)

            # Progress dialog
            dlg_progress = Gtk.Dialog(title=f'Downloading {self.selected_module}',
                                      transient_for=self, modal=True)
            dlg_progress.set_default_size(300, 100)
            cancel_flag = [False]
            def do_cancel(b):
                cancel_flag[0] = True
            btn_cancel = Gtk.Button(label=_('Cancel'))
            btn_cancel.connect('clicked', do_cancel)
            lbl = Gtk.Label(label=f'Saving to: {self.selected_module}')
            bar = Gtk.ProgressBar()
            box = dlg_progress.get_content_area()
            box.pack_start(lbl, False, False, 4)
            box.pack_start(bar, True, True, 4)
            box.pack_start(btn_cancel, False, False, 4)
            dlg_progress.show_all()

            def update_progress(f):
                GLib.idle_add(bar.set_fraction, f)

            try:
                download_file(url, dest, progress_callback=update_progress,
                              cancel_check=lambda: cancel_flag[0])
            except Exception as e:
                GLib.idle_add(dlg_progress.destroy)
                GLib.idle_add(self.show_error, f"{_('Download failed')}\n{e}")
                return
            GLib.idle_add(dlg_progress.destroy)

            # Verify
            try:
                md5_verify(dest, self.selected_module, server, arch, pver)
            except (ValueError, RuntimeError) as e:
                GLib.idle_add(self.show_error, str(e))
                return

            # Success – ask to activate
            GLib.idle_add(self.ask_activate, dest)

        def ask_activate(self, module_path):
            dlg = Gtk.MessageDialog(transient_for=self, flags=0,
                                    message_type=Gtk.MessageType.QUESTION,
                                    buttons=Gtk.ButtonsType.YES_NO,
                                    text=_('Module activation'))
            dlg.format_secondary_text(
                _('Would you like to activate the module now?') +
                f'\n({module_path})')
            resp = dlg.run()
            dlg.destroy()
            if resp == Gtk.ResponseType.YES:
                try:
                    subprocess.run(['activate', module_path], check=True)
                    self.show_info(f"{_('Module activated')}: {os.path.basename(module_path)} {_('has been activated.')}")
                except subprocess.CalledProcessError as e:
                    self.show_error(f"{_('Activation failed')}: {e}")
            else:
                self.quit()

        def show_info(self, msg):
            dlg = Gtk.MessageDialog(transient_for=self, flags=0,
                                    message_type=Gtk.MessageType.INFO,
                                    buttons=Gtk.ButtonsType.CLOSE, text=msg)
            dlg.run()
            dlg.destroy()
            self.quit()

        def quit(self):
            shutil.rmtree(tmpdir, ignore_errors=True)
            Gtk.main_quit()

    app = BundleWindow()
    Gtk.main()

# ----------------------------------------------------------------------
# Entry point
# ----------------------------------------------------------------------
if __name__ == '__main__':
    if '--console' in sys.argv:
        # Optional --output-dir PATH argument
        out_dir = None
        try:
            idx = sys.argv.index('--output-dir')
            out_dir = sys.argv[idx + 1]
        except (ValueError, IndexError):
            pass
        console_mode(out_dir)
    else:
        gui_mode()
