#!/usr/bin/env python3

import os
import sys

# Must run as root – refuse otherwise
if os.geteuid() != 0:
    msg = (
        'Error: Onix Updater requires root privileges.\n'
        'Please run with: sudo onix-updater\n'
    )
    print(msg, file=sys.stderr)

    # Try to show a graphical message as well
    for notify in ('/usr/bin/zenity', '/usr/bin/kdialog'):
        if os.path.exists(notify):
            break
    else:
        notify = None

    if notify and 'DISPLAY' in os.environ:
        import subprocess
        subprocess.run(
            [notify, '--error', '--title', 'Onix Updater',
             '--text', 'This application requires root privileges.\nPlease run with: sudo onix-updater'],
            check=False
        )

    sys.exit(1)

import subprocess
import threading
from pathlib import Path

# Set display environment for Wayland/X11 (only if not already set)
if 'WAYLAND_DISPLAY' in os.environ and 'GDK_BACKEND' not in os.environ:
    os.environ['GDK_BACKEND'] = 'wayland'
elif 'GDK_BACKEND' not in os.environ:
    os.environ['GDK_BACKEND'] = 'x11'

import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, GLib

GLib.set_prgname('onix-updater')
GLib.set_application_name('Onix Updater')

# ── Constants ──────────────────────────────────────────────────────────────────

APP_ID = 'org.onix.updater'
WINDOW_TITLE = 'Onix Updater'
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 600

UPDATE_WINDOW_WIDTH = 800
UPDATE_WINDOW_HEIGHT = 600

EDITOR_WINDOW_WIDTH = 900
EDITOR_WINDOW_HEIGHT = 700

PACMAN_CONF_PATH = Path('/etc/pacman.conf')
PACMAN_BIN = '/usr/bin/pacman'
FLATPAK_BIN = Path('/usr/bin/flatpak')
REFLECTOR_BIN = Path('/usr/bin/reflector')
NOTIFY_SEND_BIN = Path('/usr/bin/notify-send')

EXTERNAL_MANAGER_PATHS = {
    'snap': '/usr/bin/snap',
    'yay': '/usr/bin/yay',
    'paru': '/usr/bin/paru',
}

PAGE_MARGIN = 20


# ── Application ───────────────────────────────────────────────────────────────

class OnixUpdater(Adw.Application):
    def __init__(self):
        super().__init__(application_id=APP_ID)
        self.connect('activate', self.on_activate)

        self.updates_available: list[str] = []
        self.external_managers: list[str] = []
        self.update_thread: threading.Thread | None = None
        self.process: subprocess.Popen | None = None

    def on_activate(self, app):
        self.win = MainWindow(application=app)
        self.win.present()
        self.win.check_for_updates()


# ── Main Window ───────────────────────────────────────────────────────────────

class MainWindow(Adw.ApplicationWindow):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.set_title(WINDOW_TITLE)
        self.set_default_size(WINDOW_WIDTH, WINDOW_HEIGHT)
        self.set_icon_name('onix')

        self.external_managers: list[str] = []
        self.updates_available: list[str] = []

        self._build_ui()

    def _build_ui(self):
        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.set_content(main_box)

        self.header_bar = Adw.HeaderBar()
        icon_img = Gtk.Image.new_from_icon_name('onix')
        icon_img.set_pixel_size(24)
        icon_img.set_margin_end(6)
        self.header_bar.pack_start(icon_img)
        main_box.append(self.header_bar)

        self.stack = Gtk.Stack()
        self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
        main_box.append(self.stack)

        self.switcher = Gtk.StackSwitcher()
        self.switcher.set_stack(self.stack)
        self.header_bar.set_title_widget(self.switcher)

        self._create_updates_page()
        self._create_repositories_page()
        self._create_settings_page()

    # ── Updates Page ──────────────────────────────────────────────────────

    def _create_updates_page(self):
        page = self._make_page()

        # Status group
        status_group = Adw.PreferencesGroup()
        status_group.set_title('System Status')

        self.status_row = Adw.ActionRow()
        self.status_row.set_title('Status')
        self.status_row.set_subtitle('Checking for updates…')
        status_group.add(self.status_row)

        self.check_button = Gtk.Button(label='Check for Updates')
        self.check_button.connect('clicked', self.on_check_updates)
        self.check_button.add_css_class('suggested-action')
        status_group.add(self.check_button)

        page.append(status_group)

        # Available updates group
        self.updates_group = Adw.PreferencesGroup()
        self.updates_group.set_title('Available Updates')
        self.updates_group.set_description('Packages that can be updated')

        self.updates_listbox = Gtk.ListBox()
        self.updates_listbox.set_selection_mode(Gtk.SelectionMode.NONE)

        scrolled_updates = Gtk.ScrolledWindow()
        scrolled_updates.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled_updates.set_min_content_height(300)
        scrolled_updates.set_child(self.updates_listbox)
        self.updates_group.add(scrolled_updates)

        page.append(self.updates_group)

        self.update_all_button = Gtk.Button(label='Update All')
        self.update_all_button.connect('clicked', self.on_update_all)
        self.update_all_button.add_css_class('destructive-action')
        self.update_all_button.set_margin_top(10)
        page.append(self.update_all_button)

        self.stack.add_titled(page, 'updates', 'Updates')

    def _make_page(self):
        page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        page.set_margin_top(PAGE_MARGIN)
        page.set_margin_bottom(PAGE_MARGIN)
        page.set_margin_start(PAGE_MARGIN)
        page.set_margin_end(PAGE_MARGIN)
        return page

    def check_for_updates(self):
        def check_thread():
            try:
                result = subprocess.run(
                    [PACMAN_BIN, '-Qu'],
                    capture_output=True, text=True
                )
                if result.stdout and result.stdout.strip():
                    updates = result.stdout.strip().split('\n')
                    GLib.idle_add(self._update_updates_list, updates)
                else:
                    GLib.idle_add(self._show_no_updates)
            except Exception as e:
                GLib.idle_add(self._show_status_error, str(e))

        thread = threading.Thread(target=check_thread, daemon=True)
        thread.start()

    def _update_updates_list(self, updates: list[str]):
        self._clear_listbox(self.updates_listbox)
        self.updates_available.clear()

        for update in updates:
            line = update.strip()
            if not line:
                continue

            parts = line.split()
            if len(parts) >= 2:
                package = parts[0]
                version_info = ' '.join(parts[1:])

                row = Adw.ActionRow()
                row.set_title(package)
                row.set_subtitle(version_info)
                self.updates_listbox.append(row)
                self.updates_available.append(package)

        if self.updates_available:
            self.status_row.set_subtitle(
                f'{len(self.updates_available)} updates available'
            )
            self.update_all_button.set_visible(True)
        else:
            self._show_no_updates()

    def _show_no_updates(self):
        self._clear_listbox(self.updates_listbox)
        self.status_row.set_subtitle('System is up to date')
        self.update_all_button.set_visible(False)

        row = Adw.ActionRow()
        row.set_title('No updates available')
        row.set_subtitle('Your system is already up to date')
        self.updates_listbox.append(row)

    def _show_status_error(self, message: str):
        self.status_row.set_subtitle(f'Error: {message}')

    def on_check_updates(self, _button):
        self.status_row.set_subtitle('Checking for updates…')
        self.check_button.set_sensitive(False)
        self.check_for_updates()
        GLib.timeout_add_seconds(1, lambda: self.check_button.set_sensitive(True))

    # ── Update All ────────────────────────────────────────────────────────

    def on_update_all(self, _button):
        self._show_full_update_dialog()

    def _show_full_update_dialog(self):
        pacman_updates = len(self.updates_available) if self.updates_available else 0
        flatpak_available = 'flatpak' in self.external_managers

        update_lines = []
        if pacman_updates > 0:
            update_lines.append(f'Pacman packages: {pacman_updates}')
        if flatpak_available:
            update_lines.append('Flatpak applications')

        if not update_lines:
            update_lines.append('No updates available')

        dialog = Gtk.Dialog()
        dialog.set_title('Update System')
        dialog.set_modal(True)
        dialog.set_transient_for(self)

        content = dialog.get_content_area()
        content.set_margin_top(PAGE_MARGIN)
        content.set_margin_bottom(PAGE_MARGIN)
        content.set_margin_start(PAGE_MARGIN)
        content.set_margin_end(PAGE_MARGIN)

        label = Gtk.Label(
            label='Update the following?\n\n' + '\n'.join(update_lines)
        )
        content.append(label)

        dialog.add_button('Cancel', Gtk.ResponseType.CANCEL)
        dialog.add_button('Update All', Gtk.ResponseType.ACCEPT)
        dialog.set_default_response(Gtk.ResponseType.ACCEPT)

        def on_response(dlg, response):
            if response == Gtk.ResponseType.ACCEPT:
                self._start_full_system_update()
            dlg.destroy()

        dialog.connect('response', on_response)
        dialog.present()

    def _start_full_system_update(self):
        win = Adw.Window()
        win.set_title('System Update')
        win.set_default_size(UPDATE_WINDOW_WIDTH, UPDATE_WINDOW_HEIGHT)
        win.set_modal(True)
        win.set_transient_for(self)

        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        main_box.set_margin_top(PAGE_MARGIN)
        main_box.set_margin_bottom(PAGE_MARGIN)
        main_box.set_margin_start(PAGE_MARGIN)
        main_box.set_margin_end(PAGE_MARGIN)
        win.set_content(main_box)

        self._update_status_label = Gtk.Label(label='Starting system update…')
        self._update_status_label.set_halign(Gtk.Align.START)
        self._update_status_label.add_css_class('title-1')
        main_box.append(self._update_status_label)

        scrolled = Gtk.ScrolledWindow()
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_min_content_height(300)
        scrolled.set_vexpand(True)

        self._terminal_text = Gtk.TextView()
        self._terminal_text.set_editable(False)
        self._terminal_text.set_monospace(True)
        self._terminal_text.set_wrap_mode(Gtk.WrapMode.WORD)
        self._terminal_text.set_vexpand(True)
        scrolled.set_child(self._terminal_text)
        main_box.append(scrolled)

        self._update_progress_bar = Gtk.ProgressBar()
        self._update_progress_bar.set_fraction(0.0)
        main_box.append(self._update_progress_bar)

        button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        button_box.set_halign(Gtk.Align.CENTER)
        button_box.set_margin_top(10)

        self._update_cancel_button = Gtk.Button(label='Cancel Update')
        self._update_cancel_button.add_css_class('destructive-action')
        self._update_cancel_button.connect('clicked', self._cancel_update)
        button_box.append(self._update_cancel_button)

        main_box.append(button_box)

        # Store references
        self._update_window = win
        self._update_cancelled = False

        win.present()

        thread = threading.Thread(target=self._full_update_thread, daemon=True)
        thread.start()

    def _cancel_update(self, _button=None):
        self._update_cancelled = True

        if hasattr(self, '_current_process') and self._current_process:
            self._current_process.terminate()

        self._idle_call(lambda: self._update_status_label.set_text('Update cancelled by user'))
        self._idle_call(lambda: self._update_progress_bar.set_fraction(0.0))
        self._idle_call(lambda: self._update_cancel_button.set_sensitive(False))

        GLib.timeout_add_seconds(2, lambda: self._close_update_window() or False)

    def _close_update_window(self):
        if hasattr(self, '_update_window') and self._update_window:
            self._update_window.close()
            self._update_window = None
        return False

    def _update_terminal(self, text: str):
        if self._update_cancelled:
            return
        buffer = self._terminal_text.get_buffer()
        end_iter = buffer.get_end_iter()
        buffer.insert(end_iter, text + '\n')
        self._terminal_text.scroll_to_iter(buffer.get_end_iter(), 0.0, False, 0.0, 0.0)

    def _idle_call(self, callback):
        GLib.idle_add(callback)

    def _run_sudo_command(self, cmd: list[str]) -> tuple[int, str]:
        try:
            # Already running as root – no pkexec needed
            process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                bufsize=1
            )
            self._current_process = process

            output_lines: list[str] = []
            while True:
                if self._update_cancelled:
                    process.terminate()
                    break

                line = process.stdout.readline()
                if not line and process.poll() is not None:
                    break
                if line:
                    line_stripped = line.rstrip('\n')
                    output_lines.append(line_stripped)
                    GLib.idle_add(self._update_terminal, line_stripped)

            return_code = process.wait()
            self._current_process = None
            return return_code, '\n'.join(output_lines)
        except Exception as e:
            self._current_process = None
            return -1, str(e)

    def _full_update_thread(self):
        self._update_cancelled = False

        try:
            # Pacman update
            self._idle_call(lambda: self._update_status_label.set_text('Updating pacman packages…'))
            self._idle_call(lambda: self._update_progress_bar.set_fraction(0.1))

            return_code, output = self._run_sudo_command([
                PACMAN_BIN, '-Syu', '--noconfirm', '--needed'
            ])

            if return_code != 0:
                self._idle_call(lambda: self._update_status_label.set_text('Pacman update failed'))
                self._idle_call(lambda: self._update_progress_bar.set_fraction(0.0))
                self._idle_call(lambda: self._update_cancel_button.set_sensitive(False))
                GLib.timeout_add_seconds(3, self._close_update_window)
                return

            self._idle_call(lambda: self._update_progress_bar.set_fraction(0.5))

            # Flatpak update
            if 'flatpak' in self.external_managers:
                self._idle_call(lambda: self._update_status_label.set_text('Updating flatpak applications…'))
                self._idle_call(lambda: self._update_progress_bar.set_fraction(0.6))

                # Flatpak update does not need pkexec – runs as user
                return_code, output = self._run_flatpak_update()

                if return_code != 0:
                    self._idle_call(self._update_terminal, f'Flatpak update warning: {output}')
                    # Non-fatal – continue

            self._idle_call(lambda: self._update_progress_bar.set_fraction(1.0))
            self._idle_call(lambda: self._update_status_label.set_text('System update completed successfully'))
            self._idle_call(lambda: self._update_cancel_button.set_sensitive(False))
            self._idle_call(self._send_update_notification)

            GLib.timeout_add_seconds(2, self._close_update_window)

        except Exception as e:
            self._idle_call(lambda: self._update_status_label.set_text(f'Update failed: {e}'))
            self._idle_call(lambda: self._update_progress_bar.set_fraction(0.0))
            self._idle_call(lambda: self._update_cancel_button.set_sensitive(False))
            GLib.timeout_add_seconds(3, self._close_update_window)

    def _run_flatpak_update(self) -> tuple[int, str]:
        try:
            process = subprocess.Popen(
                ['flatpak', 'update', '-y'],
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                bufsize=1
            )

            output_lines: list[str] = []
            while True:
                if self._update_cancelled:
                    process.terminate()
                    break

                line = process.stdout.readline()
                if not line and process.poll() is not None:
                    break
                if line:
                    line_stripped = line.rstrip('\n')
                    output_lines.append(line_stripped)
                    GLib.idle_add(self._update_terminal, line_stripped)

            return_code = process.wait()
            return return_code, '\n'.join(output_lines)
        except Exception as e:
            return -1, str(e)

    def _send_update_notification(self):
        if hasattr(self, 'enable_notifications') and self.enable_notifications.get_active():
            self._send_notification('System Updated', 'Package updates completed successfully')

    def _send_notification(self, title: str, message: str):
        try:
            subprocess.run([str(NOTIFY_SEND_BIN), title, message], check=True)
        except Exception:
            pass

    # ── Repositories Page ─────────────────────────────────────────────────

    def _create_repositories_page(self):
        page = self._make_page()

        pacman_group = Adw.PreferencesGroup()
        pacman_group.set_title('Pacman Configuration')

        edit_pacman_button = Gtk.Button(label='Edit pacman.conf')
        edit_pacman_button.connect('clicked', self.on_edit_pacman_conf)
        pacman_group.add(edit_pacman_button)

        mirrorlist_button = Gtk.Button(label='Update Mirrorlist')
        mirrorlist_button.connect('clicked', self.on_update_mirrorlist)
        pacman_group.add(mirrorlist_button)

        page.append(pacman_group)

        self.repos_group = Adw.PreferencesGroup()
        self.repos_group.set_title('Active Repositories')
        page.append(self.repos_group)

        self._load_repositories()

        self.stack.add_titled(page, 'repositories', 'Repositories')

    def on_edit_pacman_conf(self, _button):
        dialog = PacmanConfDialog(transient_for=self)
        dialog.present()

    def on_update_mirrorlist(self, _button):
        def mirror_thread():
            try:
                if REFLECTOR_BIN.exists():
                    subprocess.run(
                        ['reflector', '--latest', '20', '--sort', 'rate',
                         '--save', '/etc/pacman.d/mirrorlist'],
                        check=True
                    )
                    GLib.idle_add(self._show_mirrorlist_success)
                else:
                    GLib.idle_add(self._show_status_error, 'reflector is not installed')
            except Exception as e:
                GLib.idle_add(self._show_status_error, str(e))

        thread = threading.Thread(target=mirror_thread, daemon=True)
        thread.start()

    def _show_mirrorlist_success(self):
        dialog = Gtk.Dialog()
        dialog.set_title('Mirrorlist Updated')
        dialog.set_modal(True)
        dialog.set_transient_for(self)

        content = dialog.get_content_area()
        content.set_margin_top(PAGE_MARGIN)
        content.set_margin_bottom(PAGE_MARGIN)
        content.set_margin_start(PAGE_MARGIN)
        content.set_margin_end(PAGE_MARGIN)

        label = Gtk.Label(label='Package mirrorlist has been successfully updated')
        content.append(label)

        dialog.add_button('OK', Gtk.ResponseType.OK)
        dialog.connect('response', lambda d, _r: d.destroy())
        dialog.present()

    def _load_repositories(self):
        def load_thread():
            try:
                repos: list[str] = []
                with open(PACMAN_CONF_PATH) as f:
                    for line in f:
                        line = line.strip()
                        if line.startswith('[') and line.endswith(']') and line != '[options]':
                            repos.append(line[1:-1])
                GLib.idle_add(self._update_repos_list, repos)
            except Exception as e:
                print(f'Error loading repositories: {e}')

        thread = threading.Thread(target=load_thread, daemon=True)
        thread.start()

    def _update_repos_list(self, repos: list[str]):
        self._clear_action_rows(self.repos_group)

        if not repos:
            row = Adw.ActionRow()
            row.set_title('No repositories found')
            self.repos_group.add(row)
            return

        for repo in repos:
            row = Adw.ActionRow()
            row.set_title(repo)
            row.set_subtitle('Active repository')
            self.repos_group.add(row)

    # ── Settings Page ─────────────────────────────────────────────────────

    def _create_settings_page(self):
        page = self._make_page()

        notification_group = Adw.PreferencesGroup()
        notification_group.set_title('Notifications')

        self.enable_notifications = Gtk.Switch()
        self.enable_notifications.set_active(True)
        self.enable_notifications.set_valign(Gtk.Align.CENTER)

        notification_row = Adw.ActionRow()
        notification_row.set_title('Enable Notifications')
        notification_row.set_subtitle('Show desktop notifications for updates')
        notification_row.add_suffix(self.enable_notifications)
        notification_group.add(notification_row)

        page.append(notification_group)

        self.external_group = Adw.PreferencesGroup()
        self.external_group.set_title('External Package Managers')
        page.append(self.external_group)

        self._check_external_managers()

        self.stack.add_titled(page, 'settings', 'Settings')

    def _check_external_managers(self):
        self._clear_action_rows(self.external_group)

        self.external_managers.clear()

        if FLATPAK_BIN.exists():
            row = Adw.ActionRow()
            row.set_title('Flatpak')
            row.set_subtitle('Flatpak package manager detected')

            switch = Gtk.Switch()
            switch.set_active(True)
            switch.set_valign(Gtk.Align.CENTER)
            switch.connect('notify::active', self._on_external_toggled)
            row.add_suffix(switch)

            self.external_group.add(row)
            self.external_managers.append('flatpak')

        for name, path in EXTERNAL_MANAGER_PATHS.items():
            if Path(path).exists():
                row = Adw.ActionRow()
                row.set_title(name.capitalize())
                row.set_subtitle(f'{name.capitalize()} package manager detected')

                switch = Gtk.Switch()
                switch.set_active(True)
                switch.set_valign(Gtk.Align.CENTER)
                row.add_suffix(switch)

                self.external_group.add(row)
                self.external_managers.append(name)

    def _on_external_toggled(self, switch, _param, manager_name: str = 'flatpak'):
        if switch.get_active():
            if manager_name not in self.external_managers:
                self.external_managers.append(manager_name)
        else:
            if manager_name in self.external_managers:
                self.external_managers.remove(manager_name)

    # ── Helpers ───────────────────────────────────────────────────────────

    @staticmethod
    def _clear_listbox(listbox: Gtk.ListBox):
        child = listbox.get_first_child()
        while child is not None:
            nxt = child.get_next_sibling()
            listbox.remove(child)
            child = nxt

    @staticmethod
    def _clear_action_rows(group: Adw.PreferencesGroup):
        child = group.get_first_child()
        while child is not None:
            nxt = child.get_next_sibling()
            if isinstance(child, Adw.ActionRow):
                group.remove(child)
            child = nxt

    def show_error(self, message: str):
        dialog = Gtk.Dialog()
        dialog.set_title('Error')
        dialog.set_modal(True)
        dialog.set_transient_for(self)

        content = dialog.get_content_area()
        content.set_margin_top(PAGE_MARGIN)
        content.set_margin_bottom(PAGE_MARGIN)
        content.set_margin_start(PAGE_MARGIN)
        content.set_margin_end(PAGE_MARGIN)

        label = Gtk.Label(label=message)
        content.append(label)

        dialog.add_button('OK', Gtk.ResponseType.OK)
        dialog.connect('response', lambda d, _r: d.destroy())
        dialog.present()


# ── Pacman Configuration Dialog ────────────────────────────────────────────────

class PacmanConfDialog(Adw.Window):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.set_title('Edit pacman.conf')
        self.set_default_size(EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT)
        self.set_modal(True)

        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
        main_box.set_margin_top(PAGE_MARGIN)
        main_box.set_margin_bottom(PAGE_MARGIN)
        main_box.set_margin_start(PAGE_MARGIN)
        main_box.set_margin_end(PAGE_MARGIN)

        title_label = Gtk.Label(label='Edit pacman.conf')
        title_label.add_css_class('title-1')
        title_label.set_halign(Gtk.Align.START)
        main_box.append(title_label)

        scrolled = Gtk.ScrolledWindow()
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_min_content_height(400)
        scrolled.set_vexpand(True)

        self.text_view = Gtk.TextView()
        self.text_view.set_editable(True)
        self.text_view.set_monospace(True)
        self.text_view.set_wrap_mode(Gtk.WrapMode.WORD)
        self.text_view.set_vexpand(True)

        try:
            with open(PACMAN_CONF_PATH) as f:
                self.text_view.get_buffer().set_text(f.read())
        except Exception as e:
            self.text_view.get_buffer().set_text(f'Error loading pacman.conf: {e}')

        scrolled.set_child(self.text_view)
        main_box.append(scrolled)

        button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        button_box.set_halign(Gtk.Align.END)
        button_box.set_margin_top(10)

        save_button = Gtk.Button(label='Save')
        save_button.add_css_class('suggested-action')
        save_button.connect('clicked', self._on_save)
        button_box.append(save_button)

        cancel_button = Gtk.Button(label='Cancel')
        cancel_button.connect('clicked', lambda _btn: self.close())
        button_box.append(cancel_button)

        main_box.append(button_box)
        self.set_content(main_box)

    def _on_save(self, _button):
        buffer = self.text_view.get_buffer()
        start, end = buffer.get_bounds()
        content = buffer.get_text(start, end, False)

        # Basic validation
        if not content.strip():
            self._show_result_dialog('Error', 'Configuration cannot be empty')
            return

        try:
            import tempfile
            with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp:
                tmp.write(content)
                tmp_path = tmp.name

            subprocess.run(['cp', tmp_path, str(PACMAN_CONF_PATH)], check=True)
            os.unlink(tmp_path)

            self._show_result_dialog('Success', 'pacman.conf has been updated successfully')
        except Exception as e:
            self._show_result_dialog('Error', f'Failed to save pacman.conf: {e}')

    def _show_result_dialog(self, title: str, message: str):
        dialog = Gtk.Dialog()
        dialog.set_title(title)
        dialog.set_modal(True)
        dialog.set_transient_for(self)

        content = dialog.get_content_area()
        content.set_margin_top(PAGE_MARGIN)
        content.set_margin_bottom(PAGE_MARGIN)
        content.set_margin_start(PAGE_MARGIN)
        content.set_margin_end(PAGE_MARGIN)

        label = Gtk.Label(label=message)
        content.append(label)

        dialog.add_button('OK', Gtk.ResponseType.OK)
        dialog.connect('response', lambda d, _r: d.destroy())
        dialog.present()


# ── Entry Point ────────────────────────────────────────────────────────────────

if __name__ == '__main__':
    try:
        app = OnixUpdater()
        exit_code = app.run(sys.argv)
        sys.exit(exit_code)
    except Exception as e:
        print(f'Error starting application: {e}')
        sys.exit(1)
