#!/usr/bin/env python3
"""AnduinOS OOBE — First-run wizard and Welcome Center.
   Named "AnduinOS 欢迎中心" (Welcome to AnduinOS)."""

import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
gi.require_version('Gio', '2.0')
from gi.repository import Gtk, Adw, Gio, GLib, Gdk
import subprocess
import os
import sys
import threading
import importlib.util
import urllib.request
import time
import shlex
import tempfile
from importlib.machinery import SourceFileLoader
from concurrent.futures import ThreadPoolExecutor, as_completed

def _get_appearance_module():
    paths = [
        os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../anduinos-appearance/src/anduinos-appearance'),
        '/usr/bin/anduinos-appearance'
    ]
    for p in paths:
        if os.path.exists(p):
            try:
                app_mod = SourceFileLoader("anduinos_appearance", p).load_module()
                return app_mod
            except Exception:
                pass
    return None
import re
import json
import gettext
import warnings

warnings.filterwarnings('ignore', category=DeprecationWarning, module='gi')

# ── i18n ───────────────────────────────────────────────────────────
_LOCALE_DIR = '/usr/share/locale'
try:
    _ = gettext.translation('anduinos-oobe', _LOCALE_DIR, fallback=True).gettext
except Exception:
    _ = lambda s: s

MARKER_FILE = os.path.expanduser("~/.config/anduinos-oobe-done")

# ── Icon helper ────────────────────────────────────────────────────
_ICON_BASES = [
    os.path.join(os.path.dirname(os.path.abspath(__file__)),
                 '..', 'resources', 'icons'),      # dev path
    '/usr/share/anduinos-oobe/icons',               # installed path
]

def _icon(name):
    """Return path to a bundled Fluent icon, or fall back to a system icon name."""
    for base in _ICON_BASES:
        path = os.path.join(base, name)
        if os.path.isfile(path):
            return path
    # Fallback: try system icon name
    fallbacks = {
        'theme': 'preferences-desktop-theme-global',
        'layout': 'preferences-desktop-display',
        'firewall': 'preferences-security-firewall',
        'nvidia': 'nvidia',
        'wine': 'wine',
        'keyboard': 'input-keyboard',
        'appstore': 'gnome-software',
        'privacy': 'preferences-system-privacy',
        'nas': 'network-server',
        'disk': 'drive-harddisk',
        'chrome': 'google-chrome',
        'steam': 'steam',
        'discord': 'discord',
        'blender': 'blender',
        'vlc': 'vlc',
        'wechat': 'wechat',
        'qq': 'qq',
        'wps-office': 'wps-office-wpsmain',
        'onlyoffice': 'libreoffice-writer',
        'spark-store': 'spark-store',
        'github': 'github',
    }
    return fallbacks.get(name.split('.')[0], name)

def _create_image(name):
    """Creates a Gtk.Image handling either a bundled file or a fallback icon name."""
    icon_path = _icon(name)
    img = Gtk.Image()
    if os.path.isfile(icon_path):
        img.set_from_file(icon_path)
    else:
        img.set_from_icon_name(icon_path)
    return img

# ═══════════════════════════════════════════════════════════════════
# HELPERS
# ═══════════════════════════════════════════════════════════════════

def _read_os_pretty_name():
    """Read PRETTY_NAME from /etc/os-release, fallback to NAME=."""
    try:
        with open('/etc/os-release', 'r') as f:
            pretty = None
            name = None
            for line in f:
                line = line.strip()
                if line.startswith('PRETTY_NAME='):
                    pretty = line.split('=', 1)[1].strip().strip('"').strip("'")
                elif line.startswith('NAME='):
                    name = line.split('=', 1)[1].strip().strip('"').strip("'")
            return pretty or name or 'AnduinOS'
    except Exception:
        return 'AnduinOS'


def has_nvidia_gpu():
    if os.environ.get('NVIDIA_PAGE') == 'true':
        return True
    try:
        r = subprocess.run(['lspci'], capture_output=True, text=True, timeout=10)
        if 'NVIDIA' in r.stdout or 'nvidia' in r.stdout.lower():
            return True
    except Exception:
        pass
    return False


def is_arm64():
    """True on aarch64 platforms (Snapdragon, Apple Silicon, etc.)."""
    return os.uname().machine == 'aarch64'


def is_virtual_machine():
    if os.environ.get('XBOX_PAGE') == 'true':
        return False
    try:
        r = subprocess.run(['systemd-detect-virt'], capture_output=True, text=True, timeout=5)
        output = r.stdout.strip().lower()
        return output != 'none' and output != ''
    except Exception:
        return False


def has_second_disk():
    try:
        r = subprocess.run(['lsblk', '-ndo', 'NAME,TYPE'], capture_output=True, text=True, timeout=10)
        count = 0
        for line in r.stdout.strip().split('\n'):
            parts = line.split()
            if len(parts) >= 2 and parts[1] == 'disk' and not parts[0].startswith('loop'):
                count += 1
        return count > 1
    except Exception:
        return False


def is_bottles_installed():
    sys_path = "/var/lib/flatpak/app/com.usebottles.bottles"
    user_path = os.path.expanduser("~/.local/share/flatpak/app/com.usebottles.bottles")
    return os.path.isdir(sys_path) or os.path.isdir(user_path)


def is_chinese_locale():
    lang = os.environ.get('LANGUAGE', os.environ.get('LANG', ''))
    return lang.startswith('zh_CN')


def _get_flathub_url():
    """Return the current Flathub remote URL, or empty string on failure."""
    try:
        r = subprocess.run(['flatpak', 'remotes', '--system', '--show-details'],
                           capture_output=True, text=True, timeout=10)
        for line in r.stdout.splitlines():
            cols = line.split('\t')
            if len(cols) >= 2 and cols[0] == 'flathub':
                return cols[2] if len(cols) > 2 else ''
    except Exception:
        pass
    return ''


def _is_flathub_china_mirror():
    """True if Flathub is already pointing at a Chinese mirror."""
    url = _get_flathub_url()
    return 'ustc.edu.cn' in url or 'tuna.tsinghua.edu.cn' in url


def _is_package_installed(pkg):
    """Return True if the given dpkg package is installed."""
    try:
        r = subprocess.run(['dpkg', '-s', pkg], capture_output=True, timeout=5)
        return r.returncode == 0
    except Exception:
        return False


def is_live_environment():
    """Return True only if all four conditions are met:
    1. casper package is installed
    2. ubiquity package is installed
    3. /cdrom directory exists
    4. Kernel cmdline contains boot=casper
    """
    if not _is_package_installed('casper'):
        return False
    if not _is_package_installed('ubiquity'):
        return False
    if not os.path.isdir('/cdrom'):
        return False
    try:
        with open('/proc/cmdline', 'r') as f:
            cmdline = f.read()
        if 'boot=casper' not in cmdline:
            return False
    except Exception:
        return False
    return True


def _apply_theme(dark):
    scheme = 'prefer-dark' if dark else 'prefer-light'
    settings = Gio.Settings.new('org.gnome.desktop.interface')
    settings.set_string('color-scheme', scheme)


def _apply_layout(style):
    """Apply taskbar layout via dconf. style in ('modern', 'classic')."""
    ARC = '/org/gnome/shell/extensions/arcmenu'
    DTP = '/org/gnome/shell/extensions/dash-to-panel'

    # Map to internal style names used by the appearance code
    internal = 'eleven' if style == 'modern' else 'classic'
    pos = 'bottom'

    MENU_CONFIG = {
        ('classic', 'bottom'): ('arcmenu', 'BottomLeft'),
        ('eleven', 'bottom'): ('11', 'BottomCentered'),
    }

    POSITIONS = {'bottom': 'BOTTOM'}

    if internal == 'eleven':
        elements = [
            {"element": "activitiesButton", "visible": True, "position": "stackedTL"},
            {"element": "showAppsButton", "visible": False, "position": "stackedTL"},
            {"element": "leftBox", "visible": True, "position": "stackedTL"},
            {"element": "centerBox", "visible": True, "position": "stackedBR"},
            {"element": "taskbar", "visible": True, "position": "centerMonitor"},
            {"element": "rightBox", "visible": True, "position": "stackedBR"},
            {"element": "systemMenu", "visible": True, "position": "stackedBR"},
            {"element": "dateMenu", "visible": True, "position": "stackedBR"},
            {"element": "desktopButton", "visible": True, "position": "stackedBR"},
        ]
    else:
        elements = [
            {"element": "centerBox", "visible": True, "position": "stackedTL"},
            {"element": "taskbar", "visible": True, "position": "stackedTL"},
            {"element": "showAppsButton", "visible": False, "position": "stackedTL"},
            {"element": "activitiesButton", "visible": True, "position": "stackedBR"},
            {"element": "leftBox", "visible": True, "position": "stackedBR"},
            {"element": "rightBox", "visible": True, "position": "stackedBR"},
            {"element": "systemMenu", "visible": True, "position": "stackedBR"},
            {"element": "dateMenu", "visible": True, "position": "stackedBR"},
            {"element": "desktopButton", "visible": True, "position": "stackedBR"},
        ]

    monitors = ["0", "1", "2"]
    pep = json.dumps({m: elements for m in monitors})
    pp = json.dumps({m: 'BOTTOM' for m in monitors})
    ps = json.dumps({m: 48 for m in monitors})
    menu_layout, force_menu = MENU_CONFIG[(internal, pos)]

    try:
        subprocess.run(['dconf', 'write', f'{DTP}/dot-position', "'BOTTOM'"], check=True)
        subprocess.run(['dconf', 'write', f'{DTP}/panel-positions', f"'{pp}'"], check=True)
        subprocess.run(['dconf', 'write', f'{DTP}/panel-sizes', f"'{ps}'"], check=True)
        subprocess.run(['dconf', 'write', f'{DTP}/panel-element-positions', f"'{pep}'"], check=True)
        subprocess.run(['dconf', 'write', f'{ARC}/force-menu-location', f"'{force_menu}'"], check=True)
        subprocess.run(['dconf', 'write', f'{ARC}/menu-layout', f"'{menu_layout}'"], check=True)
        if internal == 'eleven':
            subprocess.run(['dconf', 'write', f'{DTP}/group-apps', 'true'], check=True)
            subprocess.run(['dconf', 'write', f'{DTP}/group-apps-use-launchers', 'true'], check=True)
    except subprocess.CalledProcessError:
        pass
    subprocess.run(['dconf', 'update'], check=False)


# ═══════════════════════════════════════════════════════════════════
# BREATHING LOGO WIDGET
# ═══════════════════════════════════════════════════════════════════

class BreathingLogo(Gtk.Box):
    """The official AnduinOS logo with a breathing pulse animation."""

    def __init__(self, size=128):
        super().__init__(orientation=Gtk.Orientation.VERTICAL)
        self.set_halign(Gtk.Align.CENTER)
        self.set_valign(Gtk.Align.CENTER)
        # Try to load the official logo from the installed path first,
        # then fall back to the resources directory
        logo_paths = [
            '/usr/share/icons/hicolor/scalable/apps/anduinos-oobe.svg',
            os.path.join(os.path.dirname(os.path.abspath(__file__)),
                         '..', 'resources', 'anduinos-oobe.svg'),
        ]
        logo_path = None
        for p in logo_paths:
            if os.path.isfile(p):
                logo_path = p
                break

        self._picture = Gtk.Picture()
        self._picture.set_size_request(size, size)
        self._picture.set_content_fit(Gtk.ContentFit.CONTAIN)
        self._picture.set_can_shrink(True)
        if logo_path:
            self._picture.set_filename(logo_path)
        else:
            # Fallback: show the app icon
            self._picture.set_icon_name('anduinos-oobe')

        # CSS for opacity animation
        self._css_provider = Gtk.CssProvider()
        css = b"""
        .breathing-logo {
            transition: opacity 2s ease-in-out;
            opacity: 1.0;
        }
        .breathing-logo.dimmed {
            opacity: 0.55;
        }
        """
        self._css_provider.load_from_data(css)
        Gtk.StyleContext.add_provider_for_display(
            Gdk.Display.get_default(),
            self._css_provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )
        self._picture.add_css_class('breathing-logo')

        self.append(self._picture)
        
        self._dimmed = False
        self._pulse()
        GLib.timeout_add(2000, self._pulse)

    def _pulse(self):
        self._dimmed = not self._dimmed
        if self._dimmed:
            self._picture.add_css_class('dimmed')
        else:
            self._picture.remove_css_class('dimmed')
        return True


# ═══════════════════════════════════════════════════════════════════
# PAGE 1: WELCOME — 启航
# ═══════════════════════════════════════════════════════════════════

def create_welcome_page(navigate_next):
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)

    # Center content
    center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
    center.set_valign(Gtk.Align.CENTER)
    center.set_halign(Gtk.Align.CENTER)
    center.set_vexpand(True)
    center.set_margin_start(48)
    center.set_margin_end(48)
    center.set_margin_bottom(24)

    # Breathing logo
    logo = BreathingLogo(96)
    logo.set_halign(Gtk.Align.CENTER)
    logo.set_margin_bottom(32)
    center.append(logo)

    # Main title
    title = Gtk.Label(label=_('Welcome Home.'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_justify(Gtk.Justification.CENTER)
    title.set_wrap(True)
    title.set_margin_bottom(12)
    center.append(title)

    # Subtitle
    subtitle = Gtk.Label(
        label=_('AnduinOS is ready. Just a few steps to shape it') + '\n' + _('into the perfect system tailored to your habits.')
    )
    subtitle.add_css_class('title-4')
    subtitle.set_halign(Gtk.Align.CENTER)
    subtitle.set_justify(Gtk.Justification.CENTER)
    subtitle.set_wrap(True)
    subtitle.set_margin_bottom(36)
    center.append(subtitle)

    # Start button (only useful in OOBE mode)
    start_btn = Gtk.Button(label=_('  Start Setup  '))
    start_btn.add_css_class('suggested-action')
    start_btn.add_css_class('pill')
    start_btn.set_halign(Gtk.Align.CENTER)
    start_btn.set_size_request(200, 48)
    if navigate_next:
        start_btn.connect('clicked', lambda b: navigate_next())
    else:
        start_btn.set_visible(False)
    
    center.append(start_btn)

    # Version info
    version = Gtk.Label(label=_read_os_pretty_name())
    version.add_css_class('dim-label')
    version.add_css_class('caption')
    version.set_halign(Gtk.Align.CENTER)
    version.set_margin_top(24)
    center.append(version)

    page.append(center)
    return page


# ═══════════════════════════════════════════════════════════════════
# PAGE 2: APPEARANCE — 外观与秩序
# ═══════════════════════════════════════════════════════════════════

def create_appearance_page():
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)
    page.set_spacing(0)

    center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    center.set_valign(Gtk.Align.CENTER)
    center.set_vexpand(True)
    page.append(center)

    # Title
    title = Gtk.Label(label=_('Define Your Visual Order'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_wrap(True)
    title.set_justify(Gtk.Justification.CENTER)
    title.set_margin_bottom(48)
    center.append(title)

    # ── Theme Section ──
    theme_group = Adw.PreferencesGroup()
    theme_group.set_title(_('Light and Shadow'))
    theme_group.set_description(_('Choose a crisp light theme, or an immersive dark experience.'))
    theme_group.set_margin_bottom(36)
    theme_group.set_margin_start(24)
    theme_group.set_margin_end(24)

    # Detect current
    settings = Gio.Settings.new('org.gnome.desktop.interface')
    is_dark = 'dark' in settings.get_string('color-scheme').lower()

    # Big Radio Cards for Theme
    theme_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=24)
    theme_box.set_halign(Gtk.Align.CENTER)
    theme_box.set_margin_top(12)

    btn_light = Gtk.ToggleButton()
    light_content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
    light_icon = _create_image('preferences-desktop-display-randr.svg')
    light_icon.set_pixel_size(48)
    light_content.append(light_icon)
    light_content.append(Gtk.Label(label=_('Light')))
    light_content.set_margin_top(24)
    light_content.set_margin_bottom(24)
    light_content.set_margin_start(48)
    light_content.set_margin_end(48)
    btn_light.set_child(light_content)

    btn_dark = Gtk.ToggleButton()
    dark_content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
    dark_icon = _create_image('preferences-desktop-display-nightcolor.svg')
    dark_icon.set_pixel_size(48)
    dark_content.append(dark_icon)
    dark_content.append(Gtk.Label(label=_('Dark')))
    dark_content.set_margin_top(24)
    dark_content.set_margin_bottom(24)
    dark_content.set_margin_start(48)
    dark_content.set_margin_end(48)
    btn_dark.set_child(dark_content)

    btn_light.set_active(not is_dark)
    btn_dark.set_active(is_dark)

    def _on_theme_click(btn, dark):
        if btn.get_active():
            if dark:
                btn_light.set_active(False)
            else:
                btn_dark.set_active(False)
            _apply_theme(dark)
        # Ensure at least one is active
        elif not btn_light.get_active() and not btn_dark.get_active():
            btn.set_active(True)

    btn_light.connect('toggled', _on_theme_click, False)
    btn_dark.connect('toggled', _on_theme_click, True)

    theme_box.append(btn_light)
    theme_box.append(btn_dark)
    theme_group.add(theme_box)
    center.append(theme_group)

    # ── Layout Section ──
    layout_title_group = Adw.PreferencesGroup()
    layout_title_group.set_title(_('Workflow Layout'))
    layout_title_group.set_description(_('Prefer modern centered alignment, or classic left-docked?'))
    layout_title_group.set_margin_start(24)
    layout_title_group.set_margin_end(24)
    layout_title_group.set_margin_bottom(12)
    center.append(layout_title_group)

    # Detect current layout
    try:
        r = subprocess.run(['dconf', 'read', '/org/gnome/shell/extensions/arcmenu/menu-layout'],
                           capture_output=True, text=True, timeout=5)
        ml = r.stdout.strip()
        current_style = 'modern'
        if 'arcmenu' in ml:
            r2 = subprocess.run(
                ['dconf', 'read',
                 '/org/gnome/shell/extensions/dash-to-panel/panel-element-positions'],
                capture_output=True, text=True, timeout=5)
            current_style = 'classic' if 'centerMonitor' not in r2.stdout else 'modern'
    except Exception:
        current_style = 'modern'

    state = {'style': current_style}

    # Optional Preview from anduinos-appearance
    app_mod = _get_appearance_module()
    preview = None
    if app_mod and hasattr(app_mod, 'draw_preview'):
        preview_card = Gtk.Frame()
        preview_card.add_css_class('card')
        preview_card.set_margin_bottom(24)
        preview_card.set_margin_start(24)
        preview_card.set_margin_end(24)
        preview = Gtk.DrawingArea()
        preview.set_size_request(340, 110)
        
        def _do_draw(area, cr, w, h):
            style_str = 'eleven' if state['style'] == 'modern' else 'classic'
            app_mod.draw_preview(area, cr, w, h, style_str, 'bottom')
            
        preview.set_draw_func(_do_draw)
        preview_card.set_child(preview)
        center.append(preview_card)

    layout_group = Adw.PreferencesGroup()
    layout_group.set_margin_start(24)
    layout_group.set_margin_end(24)

    layout_row = Adw.ActionRow()
    layout_row.set_title(_('Taskbar Layout'))
    layout_row.set_subtitle(_('Choose your workflow.'))
    layout_icon = _create_image('dock.svg')
    layout_icon.set_pixel_size(32)
    layout_row.add_prefix(layout_icon)

    layout_btns = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
    layout_btns.add_css_class('linked')
    layout_btns.set_valign(Gtk.Align.CENTER)
    
    btn_modern = Gtk.ToggleButton(label=_(' Modern '))
    btn_classic = Gtk.ToggleButton(label=_(' Classic '))
    btn_modern.set_active(current_style == 'modern')
    btn_classic.set_active(current_style == 'classic')
    
    layout_btns.append(btn_classic)
    layout_btns.append(btn_modern)
    layout_row.add_suffix(layout_btns)
    layout_row.set_activatable_widget(btn_modern)

    def _on_layout_click(btn, style):
        if btn.get_active():
            if style == 'modern':
                btn_classic.set_active(False)
            else:
                btn_modern.set_active(False)
            state['style'] = style
            if preview:
                preview.queue_draw()
            _apply_layout(style)
        elif not btn_modern.get_active() and not btn_classic.get_active():
            btn.set_active(True)

    btn_modern.connect('toggled', _on_layout_click, 'modern')
    btn_classic.connect('toggled', _on_layout_click, 'classic')

    layout_group.add(layout_row)

    activities_row = Adw.SwitchRow()
    activities_row.set_title(_('Show Virtual Desktop Switch Button'))
    activities_row.set_subtitle(_('Display the Activities button on the taskbar'))
    
    act_icon = _create_image('window-duplicate.svg')
    act_icon.set_pixel_size(32)
    activities_row.add_prefix(act_icon)
    
    try:
        r_act = subprocess.run(['dconf', 'read', '/org/gnome/shell/extensions/arcmenu/show-activities-button'],
                               capture_output=True, text=True, timeout=5)
        is_activities_shown = 'false' not in r_act.stdout.lower()
    except Exception:
        is_activities_shown = True
        
    activities_row.set_active(is_activities_shown)
    
    def _on_activities_toggle(switch, pspec):
        v = 'true' if switch.get_active() else 'false'
        subprocess.run(['dconf', 'write', '/org/gnome/shell/extensions/arcmenu/show-activities-button', v])
        # Force Dash-to-Panel to refresh so the change takes effect immediately
        if app_mod and hasattr(app_mod, 'apply_style_and_position'):
            style = 'eleven' if state['style'] == 'modern' else state['style']
            app_mod.apply_style_and_position(style, 'bottom')

    activities_row.connect('notify::active', _on_activities_toggle)
    layout_group.add(activities_row)

    center.append(layout_group)

    return page


# ═══════════════════════════════════════════════════════════════════
# PAGE 3: SECURITY — 边界与隐私
# ═══════════════════════════════════════════════════════════════════

def create_security_page():
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)
    page.set_spacing(0)

    center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    center.set_valign(Gtk.Align.CENTER)
    center.set_vexpand(True)
    page.append(center)

    title = Gtk.Label(label=_('Digital Sovereignty, Under Your Control'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_wrap(True)
    title.set_justify(Gtk.Justification.CENTER)
    title.set_margin_bottom(8)
    center.append(title)

    sub = Gtk.Label(label=_('We deeply value privacy. Here, you hold absolute authority.'))
    sub.add_css_class('dim-label')
    sub.set_halign(Gtk.Align.CENTER)
    sub.set_wrap(True)
    sub.set_justify(Gtk.Justification.CENTER)
    sub.set_margin_bottom(48)
    center.append(sub)

    # ── Firewall Card ──
    fw_card = Adw.PreferencesGroup()
    fw_card.set_title(_('Firewall'))
    fw_card.set_description(
        _('Enable low-level network protection — silently blocks all unauthorized inbound connections.')
    )
    fw_card.set_margin_bottom(24)

    fw_row = Adw.SwitchRow()
    fw_row.set_title(_('Network Firewall (UFW)'))

    # Shield icon that glows green
    shield_icon = _create_image('preferences-security-firewall.svg')
    shield_icon.set_pixel_size(32)
    fw_row.add_prefix(shield_icon)

    # Read UFW status from world-readable config file — no root needed
    try:
        with open('/etc/ufw/ufw.conf', 'r') as f:
            for line in f:
                line = line.strip()
                if line.startswith('ENABLED='):
                    fw_active = line.split('=', 1)[1].strip().lower() == 'yes'
                    break
            else:
                fw_active = False
    except Exception:
        fw_active = False
    fw_row.set_active(fw_active)

    def _on_fw_toggle(switch, pspec):
        enable = switch.get_active()
        arg = 'enable' if enable else 'disable'

        def _done(returncode, stdout, stderr, sw, expected):
            if returncode != 0:
                GLib.idle_add(lambda: sw.set_active(not expected))
            else:
                icon_name = 'preferences-security-firewall.svg' if enable else 'preferences-security.svg'
                path = _icon(icon_name)
                def _update_icon():
                    if os.path.isfile(path):
                        shield_icon.set_from_file(path)
                    else:
                        shield_icon.set_from_icon_name(path)
                GLib.idle_add(_update_icon)

        def _run_fw():
            try:
                proc = subprocess.run(['pkexec', 'ufw', '--force', arg],
                                      capture_output=True, text=True, timeout=30)
                GLib.idle_add(lambda: _done(proc.returncode, proc.stdout, proc.stderr,
                                            switch, enable))
            except Exception as e:
                GLib.idle_add(lambda e=e: _done(-1, '', str(e), switch, enable))

        threading.Thread(target=_run_fw, daemon=True).start()

    fw_row.connect('notify::active', _on_fw_toggle)
    fw_card.add(fw_row)
    center.append(fw_card)

    # ── Location Card ──
    loc_card = Adw.PreferencesGroup()
    loc_card.set_title(_('Location Services'))
    loc_card.set_description(
        _('Allow apps like weather and clock to access your approximate geographic location.')
    )

    loc_row = Adw.SwitchRow()
    loc_row.set_title(_('Location Access'))

    loc_icon = _create_image('gnome-maps.svg')
    loc_icon.set_pixel_size(32)
    loc_row.add_prefix(loc_icon)

    settings_loc = Gio.Settings.new('org.gnome.system.location')
    loc_row.set_active(settings_loc.get_boolean('enabled'))

    def _on_loc_toggle(switch, pspec):
        settings_loc.set_boolean('enabled', switch.get_active())

    loc_row.connect('notify::active', _on_loc_toggle)
    loc_card.add(loc_row)
    center.append(loc_card)

    # ── Power Card ──
    power_card = Adw.PreferencesGroup()
    power_card.set_title(_('Power Settings'))
    power_card.set_description(_('Manage idle behavior to balance energy saving and convenience.'))
    power_card.set_margin_top(24)

    power_row = Adw.SwitchRow()
    power_row.set_title(_('Auto Sleep'))

    power_icon = _create_image('gnome-power-manager.svg')
    power_icon.set_pixel_size(32)
    power_row.add_prefix(power_icon)

    settings_power = Gio.Settings.new('org.gnome.settings-daemon.plugins.power')
    sleep_active = settings_power.get_string('sleep-inactive-ac-type') == 'suspend' or settings_power.get_string('sleep-inactive-battery-type') == 'suspend'

    power_row.set_active(sleep_active)

    def _on_power_toggle(switch, pspec):
        if switch.get_active():
            settings_power.set_int('sleep-inactive-ac-timeout', 1800)
            settings_power.set_int('sleep-inactive-battery-timeout', 1200)
            settings_power.set_string('sleep-inactive-ac-type', 'suspend')
            settings_power.set_string('sleep-inactive-battery-type', 'suspend')
        else:
            settings_power.set_int('sleep-inactive-ac-timeout', 0)
            settings_power.set_int('sleep-inactive-battery-timeout', 0)
            settings_power.set_string('sleep-inactive-ac-type', 'nothing')
            settings_power.set_string('sleep-inactive-battery-type', 'nothing')

    power_row.connect('notify::active', _on_power_toggle)
    power_card.add(power_row)
    center.append(power_card)

    return page


# ═══════════════════════════════════════════════════════════════════
# PAGE 3.5: UPDATE SYSTEM
# ═══════════════════════════════════════════════════════════════════

def create_update_page(navigate_next, update_nav=None):
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)
    page.set_spacing(0)
    
    page._suggest_next = False

    center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    center.set_valign(Gtk.Align.CENTER)
    center.set_vexpand(True)
    page.append(center)

    icon = _create_image('yast-upgrade.svg')
    icon.set_pixel_size(72)
    icon.set_halign(Gtk.Align.CENTER)
    center.append(icon)

    title = Gtk.Label(label=_('Keep Your System Up to Date'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_wrap(True)
    title.set_justify(Gtk.Justification.CENTER)
    title.set_margin_top(12)
    center.append(title)

    sub = Gtk.Label(
        label=_('Security patches and the latest features are just a click away.')
    )
    sub.add_css_class('dim-label')
    sub.set_halign(Gtk.Align.CENTER)
    sub.set_wrap(True)
    sub.set_justify(Gtk.Justification.CENTER)
    sub.set_margin_top(12)
    center.append(sub)

    # Advanced Output Expander (Terminal)
    expander = Gtk.Expander(label=_('Terminal Output'))
    expander.set_margin_top(24)
    expander.set_halign(Gtk.Align.FILL)
    
    scroll = Gtk.ScrolledWindow()
    scroll.set_min_content_height(150)
    scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
    
    textview = Gtk.TextView()
    textview.set_editable(False)
    textview.set_cursor_visible(False)
    textview.add_css_class('card')
    textview.set_monospace(True)
    textview.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
    buffer = textview.get_buffer()
    
    scroll.set_child(textview)
    expander.set_child(scroll)
    center.append(expander)

    status_label = Gtk.Label()
    status_label.add_css_class('dim-label')
    status_label.set_halign(Gtk.Align.CENTER)
    status_label.set_wrap(True)
    status_label.set_margin_top(12)
    status_label.set_margin_bottom(12)
    center.append(status_label)

    progress_bar = Gtk.ProgressBar()
    progress_bar.set_visible(False)
    center.append(progress_bar)

    btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
    btn_box.set_halign(Gtk.Align.CENTER)
    btn_box.set_margin_top(12)

    skip_btn = Gtk.Button(label=_('  Do It Later  '))
    skip_btn.connect('clicked', lambda b: navigate_next())

    mirror_btn = Gtk.Button(label=_('  Find Fastest Mirror  '))
    mirror_btn.add_css_class('suggested-action')

    action_btn = Gtk.Button(label=_('  Check for Updates  '))

    page._state = 0 

    def _on_action(btn):
        btn.set_sensitive(False)
        skip_btn.set_sensitive(False)
        mirror_btn.set_sensitive(False)
        progress_bar.set_visible(True)
        progress_bar.pulse()
        expander.set_expanded(True)

        def _pulse():
            if progress_bar.is_visible():
                progress_bar.pulse()
                return True
            return False
        GLib.timeout_add(100, _pulse)

        def _do_work():
            def __append_text(t):
                end_iter = buffer.get_end_iter()
                buffer.insert(end_iter, t)
                mark = buffer.create_mark(None, buffer.get_end_iter(), False)
                textview.scroll_to_mark(mark, 0.0, True, 0.0, 1.0)

            def __cleanup():
                progress_bar.set_visible(False)
                action_btn.set_sensitive(True)
                skip_btn.set_sensitive(True)
                mirror_btn.set_sensitive(True)

            if page._state == 0:
                try:
                    GLib.idle_add(lambda: status_label.set_label(_('Checking for updates…')))
                    bash_cmd = 'while fuser /var/lib/dpkg/lock >/dev/null 2>&1 || fuser /var/lib/apt/lists/lock >/dev/null 2>&1 || fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do echo "Waiting for other package managers to finish..."; sleep 3; done; rm -f /var/lib/apt/lists/*_InRelease /var/lib/apt/lists/*_Release /var/lib/apt/lists/*_Packages /var/lib/apt/lists/*_Sources /var/lib/apt/lists/*_Translation*; apt-get update'
                    proc1 = subprocess.Popen(
                        ['pkexec', 'sh', '-c', bash_cmd],
                        stdout=subprocess.PIPE,
                        stderr=subprocess.STDOUT,
                        text=True,
                        bufsize=1
                    )
                    for line in iter(proc1.stdout.readline, ''):
                        GLib.idle_add(lambda l=line: __append_text(l))
                    proc1.wait()

                    if proc1.returncode != 0:
                        raise Exception("apt-get update failed")
                    
                    proc2 = subprocess.run(
                        ['apt-get', '-s', 'upgrade'],
                        capture_output=True, text=True
                    )
                    
                    has_updates = False
                    for line in proc2.stdout.split('\n'):
                        if 'upgraded, ' in line:
                            parts = line.split()
                            if parts[0] != '0':
                                has_updates = True
                                GLib.idle_add(lambda l=line: __append_text(l + '\n'))
                            break
                    
                    if has_updates:
                        GLib.idle_add(lambda: (
                            status_label.set_label(_('Updates are available.')),
                            action_btn.set_label(_('  Install Updates  ')),
                            action_btn.add_css_class('suggested-action'),
                            page.__setattr__('_state', 1),
                            __cleanup()
                        ))
                    else:
                        def _set_ready():
                            status_label.set_label(_('✓ System is up to date.'))
                            action_btn.set_visible(False)
                            skip_btn.set_label(_('  Continue  '))
                            page._suggest_next = True
                            if update_nav: update_nav()
                            __cleanup()
                        GLib.idle_add(_set_ready)
                except Exception as e:
                    GLib.idle_add(lambda e=e: (
                        status_label.set_label(_('✗ Check failed: ') + str(e)),
                        __cleanup()
                    ))
            
            elif page._state == 1:
                try:
                    GLib.idle_add(lambda: status_label.set_label(_('Installing updates…')))
                    bash_cmd = 'while fuser /var/lib/dpkg/lock >/dev/null 2>&1 || fuser /var/lib/apt/lists/lock >/dev/null 2>&1 || fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do echo "Waiting for other package managers to finish..."; sleep 3; done; DEBIAN_FRONTEND=noninteractive apt-get dist-upgrade -y'
                    proc = subprocess.Popen(
                        ['pkexec', 'sh', '-c', bash_cmd],
                        stdout=subprocess.PIPE,
                        stderr=subprocess.STDOUT,
                        text=True,
                        bufsize=1
                    )
                    for line in iter(proc.stdout.readline, ''):
                        GLib.idle_add(lambda l=line: __append_text(l))
                    proc.wait()

                    if proc.returncode == 0:
                        def _set_done():
                            status_label.set_label(_('✓ Updates installed successfully!'))
                            action_btn.set_visible(False)
                            skip_btn.set_label(_('  Continue  '))
                            page._suggest_next = True
                            if update_nav: update_nav()
                            __cleanup()
                        GLib.idle_add(_set_done)
                    else:
                        raise Exception("apt-get dist-upgrade failed")
                except Exception as e:
                    GLib.idle_add(lambda e=e: (
                        status_label.set_label(_('✗ Installation failed: ') + str(e)),
                        __cleanup()
                    ))

        threading.Thread(target=_do_work, daemon=True).start()

    action_btn.connect('clicked', _on_action)

    # ── Mirror speed-test handler ─────────────────────────────────
    def _on_mirror(btn):
        btn.set_sensitive(False)
        skip_btn.set_sensitive(False)
        action_btn.set_sensitive(False)
        progress_bar.set_visible(True)
        progress_bar.pulse()
        expander.set_expanded(True)

        def _pulse():
            if progress_bar.is_visible():
                progress_bar.pulse()
                return True
            return False
        GLib.timeout_add(100, _pulse)

        def _append_text(t):
            end_iter = buffer.get_end_iter()
            buffer.insert(end_iter, t)
            mark = buffer.create_mark(None, buffer.get_end_iter(), False)
            textview.scroll_to_mark(mark, 0.0, True, 0.0, 1.0)

        def _mirror_cleanup():
            progress_bar.set_visible(False)
            action_btn.set_sensitive(True)
            skip_btn.set_sensitive(True)
            mirror_btn.set_sensitive(True)

        def _mirror_work():
            try:
                GLib.idle_add(lambda: (
                    status_label.set_label(_('Testing mirrors…')),
                    _append_text(_('=== Testing mirror speeds ===') + '\n')
                ))

                codename = subprocess.run(
                    ['lsb_release', '-cs'],
                    capture_output=True, text=True
                ).stdout.strip()

                mirrors = [
                    # ── Default ─────────────────────────────────
                    "https://archive.ubuntu.com/ubuntu/",
                    # ── Oceania ──────────────────────────────────
                    "https://mirror.aarnet.edu.au/pub/ubuntu/archive/",
                    "https://mirror.fsmg.org.nz/ubuntu/",
                    "https://mirror.2degrees.nz/ubuntu/",
                    "https://ubuntu.lagoon.nc/ubuntu/",
                    # ── East Asia ────────────────────────────────
                    "https://mirror.xtom.com.hk/ubuntu/",
                    "https://mirror.01link.hk/ubuntu/",
                    "https://ftp.udx.icscoe.jp/Linux/ubuntu/",
                    "https://ftp.kaist.ac.kr/ubuntu/",
                    "http://jp.archive.ubuntu.com/ubuntu/",
                    "http://kr.archive.ubuntu.com/ubuntu/",
                    "http://tw.archive.ubuntu.com/ubuntu/",
                    "https://mirror.twds.com.tw/ubuntu/",
                    # ── China ────────────────────────────────────
                    "http://mirrors.ustc.edu.cn/ubuntu/",
                    "http://ftp.sjtu.edu.cn/ubuntu/",
                    "http://mirrors.tuna.tsinghua.edu.cn/ubuntu/",
                    "http://mirrors.aliyun.com/ubuntu/",
                    "http://mirrors.cloud.tencent.com/ubuntu/",
                    "http://mirrors.huaweicloud.com/ubuntu/",
                    "http://mirrors.zju.edu.cn/ubuntu/",
                    "http://mirrors.cn99.com/ubuntu/",
                    "https://mirror.nju.edu.cn/ubuntu/",
                    "https://mirrors.bfsu.edu.cn/ubuntu/",
                    # ── Southeast Asia ───────────────────────────
                    "http://sg.archive.ubuntu.com/ubuntu/",
                    "http://ossmirror.mycloud.services/os/linux/ubuntu/",
                    "https://mirror.sg.gs/ubuntu/",
                    "https://mirror.kku.ac.th/ubuntu/",
                    "https://mirror.bizflycloud.vn/ubuntu/",
                    # ── South Asia ───────────────────────────────
                    "https://mirrors.nxtgen.com/ubuntu-mirror/ubuntu/",
                    # ── Middle East ──────────────────────────────
                    "https://ubuntu.mobinhost.com/ubuntu/",
                    "https://mirror.iranserver.com/ubuntu/",
                    "https://mirror.maeen.sa/apt-mirror/",
                    # ── Europe: North ────────────────────────────
                    "https://mirrors.dotsrc.org/ubuntu/",
                    "https://mirrors.nic.funet.fi/ubuntu/",
                    "https://ftp.acc.umu.se/ubuntu/",
                    "https://mirrors.xtom.ee/ubuntu/",
                    # ── Europe: West ─────────────────────────────
                    "https://mirror.ubuntu.ikoula.com/",
                    "https://ftp.uni-stuttgart.de/ubuntu/",
                    "https://mirror.i3d.net/pub/ubuntu/",
                    "https://mirrors.xtom.nl/ubuntu/",
                    "https://mirror.init7.net/ubuntu/",
                    "https://mirror.cov.ukservers.com/ubuntu/",
                    "https://mirrors.ukfast.co.uk/sites/archive.ubuntu.com/",
                    # ── Europe: South ────────────────────────────
                    "https://ubuntu.mirror.garr.it/ubuntu/",
                    "https://mirror.raiolanetworks.com/ubuntu/",
                    "https://mirrors.up.pt/ubuntu/",
                    "https://mirror.alastyr.com/ubuntu/ubuntu-archive/",
                    # ── Europe: East ─────────────────────────────
                    "https://mirrors.neterra.net/ubuntu/archive/",
                    "https://ftp.icm.edu.pl/pub/Linux/ubuntu/",
                    "https://ftp.psnc.pl/linux/ubuntu/",
                    "https://ubuntu.anexia.at/ubuntu/",
                    "https://mirror.team-host.ru/ubuntu/",
                    # ── Americas ─────────────────────────────────
                    # Canada
                    "https://mirror.csclub.uwaterloo.ca/ubuntu/",
                    # USA — high bandwidth
                    "https://mirrors.iu13.net/ubuntu/",
                    "https://mirror.tzulo.com/ubuntu/",
                    "https://mirror.pilotfiber.com/ubuntu/",
                    "https://mirror.us.mirhosting.net/ubuntu/",
                    "http://mirror.math.princeton.edu/pub/ubuntu/",
                    "http://mirror.pit.teraswitch.com/ubuntu/",
                    # USA — academic
                    "https://mirror.fcix.net/ubuntu/",
                    "https://mirror.its.umich.edu/ubuntu/",
                    "http://mirrors.mit.edu/ubuntu/",
                    "http://www.gtlib.gatech.edu/pub/ubuntu/",
                    "http://ubuntu.osuosl.org/ubuntu/",
                    # USA — official / cloud
                    "http://us.archive.ubuntu.com/ubuntu/",
                    "http://azure.archive.ubuntu.com/ubuntu/",
                    # Brazil
                    "https://mirror.uepg.br/ubuntu/",
                ]

                results = []

                def test_mirror(mirror):
                    url = f"{mirror}dists/{codename}/Release"
                    try:
                        start = time.monotonic()
                        req = urllib.request.Request(url, method='HEAD')
                        resp = urllib.request.urlopen(req, timeout=3)
                        elapsed_ms = (time.monotonic() - start) * 1000
                        if resp.status == 200:
                            return (mirror, elapsed_ms)
                    except Exception:
                        pass
                    return (mirror, None)

                total_mirrors = len(mirrors)
                tested_count = 0

                with ThreadPoolExecutor(max_workers=12) as executor:
                    futures = {executor.submit(test_mirror, m): m for m in mirrors}
                    for future in as_completed(futures):
                        mirror, elapsed = future.result()
                        tested_count += 1
                        if elapsed is not None:
                            results.append((mirror, elapsed))
                            GLib.idle_add(
                                lambda m=mirror, e=elapsed, c=tested_count: (
                                _append_text(
                                    f"  ✓ {m} — {e:.0f} ms\n"),
                                status_label.set_label(
                                    _('Testing mirrors… ({c}/{t})').format(
                                        c=c, t=total_mirrors))
                            ))
                        else:
                            GLib.idle_add(
                                lambda m=mirror, c=tested_count: (
                                _append_text(f"  ✗ {m}\n"),
                                status_label.set_label(
                                    _('Testing mirrors… ({c}/{t})').format(
                                        c=c, t=total_mirrors))
                            ))

                results.sort(key=lambda x: x[1])

                if not results:
                    GLib.idle_add(lambda: (
                        _append_text('\n' + _('No mirrors reachable.') + '\n'),
                        status_label.set_label(
                            _('✗ No mirrors reachable. Check your connection.')),
                        _mirror_cleanup()
                    ))
                    return

                # ── Phase 2: bandwidth test top 5 by latency ──────────
                top5 = results[:5]
                GLib.idle_add(lambda: _append_text(
                    '\n' + _('=== Testing bandwidth of top 5 mirrors ') + \
                      _('(3 s each) ===') + '\n'
                ))

                def _test_bandwidth(mirror):
                    url = f"{mirror}dists/{codename}/Contents-amd64.gz"
                    try:
                        start = time.monotonic()
                        req = urllib.request.Request(url)
                        resp = urllib.request.urlopen(req, timeout=5)
                        if resp.status != 200:
                            resp.close()
                            return (mirror, 0.0)
                        bytes_read = 0
                        while True:
                            elapsed = time.monotonic() - start
                            if elapsed >= 3.0:
                                break
                            chunk = resp.read(65536)
                            if not chunk:
                                break
                            bytes_read += len(chunk)
                        resp.close()
                        elapsed = time.monotonic() - start
                        if elapsed > 0 and bytes_read > 0:
                            speed_mbps = ((bytes_read * 8)
                                          / (elapsed * 1024 * 1024))
                            return (mirror, speed_mbps)
                    except Exception:
                        pass
                    return (mirror, 0.0)

                bw_map = {}
                with ThreadPoolExecutor(max_workers=5) as executor:
                    futures = {
                        executor.submit(_test_bandwidth, m): m
                        for m, _ in top5
                    }
                    for future in as_completed(futures):
                        mirror, speed = future.result()
                        bw_map[mirror] = speed
                        GLib.idle_add(lambda m=mirror, s=speed: (
                            _append_text(
                                f"  {m} — {s:.1f} Mbps\n"
                            )
                        ))

                # Pick best by bandwidth (fall back to latency winner)
                best_mirror = max(bw_map, key=bw_map.get)
                best_speed = bw_map[best_mirror]
                best_latency = next(
                    t for m, t in top5 if m == best_mirror
                )

                GLib.idle_add(lambda: _append_text(
                    f"\n>>> {_('Best mirror by bandwidth')}: "
                    f"{best_mirror}\n"
                    f"    {_('Latency')}: {best_latency:.0f} ms | "
                    f"{_('Bandwidth')}: {best_speed:.1f} Mbps\n"
                ))

                # Read current mirror for comparison
                current_mirror = ''
                try:
                    with open(
                        '/etc/apt/sources.list.d/ubuntu.sources'
                    ) as f:
                        for line in f:
                            if line.startswith('URIs:'):
                                current_mirror = line.split(':', 1)[1].strip()
                                break
                except Exception:
                    pass

                def _show_dialog():
                    if current_mirror and current_mirror == best_mirror:
                        body = (
                            f"{best_mirror}\n"
                            f"{_('Latency')}: {best_latency:.0f} ms | "
                            f"{_('Bandwidth')}: {best_speed:.1f} Mbps\n\n"
                            f"{_('This is already your current mirror.')}"
                        )
                    else:
                        body = (
                            f"{best_mirror}\n"
                            f"{_('Latency')}: {best_latency:.0f} ms | "
                            f"{_('Bandwidth')}: {best_speed:.1f} Mbps"
                        )
                        if current_mirror:
                            body += (
                                f"\n\n{_('Current mirror')}: {current_mirror}"
                            )
                        body += (
                            "\n\n" +
                            _('Switch to this mirror and run apt update?')
                        )

                    dialog = Adw.MessageDialog.new(
                        page.get_root(),
                        _('Fastest Mirror Found'),
                        body
                    )
                    dialog.add_response('keep', _('Keep Current'))
                    dialog.add_response('switch', _('Switch'))
                    dialog.set_response_appearance(
                        'switch', Adw.ResponseAppearance.SUGGESTED)
                    dialog.set_default_response('switch')
                    dialog.set_close_response('keep')

                    def _on_dialog_response(d, response):
                        if response == 'switch':
                            _do_switch(best_mirror, codename)
                        else:
                            _mirror_cleanup()
                        d.destroy()

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

                GLib.idle_add(_show_dialog)

            except Exception as e:
                err_msg = str(e)
                GLib.idle_add(lambda msg=err_msg: (
                    status_label.set_label(
                        _('✗ Mirror test failed: ') + msg),
                    _mirror_cleanup()
                ))

        def _do_switch(mirror, codename):
            GLib.idle_add(lambda: (
                status_label.set_label(_('Switching mirror…')),
                _append_text('\n' + _('=== Applying mirror switch ===') + '\n')
            ))

            sources = (
                f"Types: deb\n"
                f"URIs: {mirror}\n"
                f"Suites: {codename}\n"
                f"Components: main restricted universe multiverse\n"
                f"Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n"
                f"\n"
                f"Types: deb\n"
                f"URIs: {mirror}\n"
                f"Suites: {codename}-updates\n"
                f"Components: main restricted universe multiverse\n"
                f"Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n"
                f"\n"
                f"Types: deb\n"
                f"URIs: {mirror}\n"
                f"Suites: {codename}-backports\n"
                f"Components: main restricted universe multiverse\n"
                f"Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n"
                f"\n"
                f"Types: deb\n"
                f"URIs: {mirror}\n"
                f"Suites: {codename}-security\n"
                f"Components: main restricted universe multiverse\n"
                f"Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n"
            )

            with tempfile.NamedTemporaryFile(
                mode='w', delete=False, suffix='.sources'
            ) as f:
                f.write(sources)
                temp_path = f.name

            def _apply():
                try:
                    bash_cmd = (
                        f"set -e\n"
                        f"cp /etc/apt/sources.list.d/ubuntu.sources"
                        f" /etc/apt/sources.list.d/ubuntu.sources.bak"
                        f" 2>/dev/null || true\n"
                        f"truncate -s 0 /etc/apt/sources.list "
                        f"2>/dev/null || true\n"
                        f"cat {shlex.quote(temp_path)}"
                        f" > /etc/apt/sources.list.d/ubuntu.sources\n"
                        f"if [ ! -f /etc/apt/sources.list.d/anduinos.sources ]; then\n"
                        f"  echo 'Creating /etc/apt/sources.list.d/anduinos.sources'\n"
                        f"  cat > /etc/apt/sources.list.d/anduinos.sources <<'EOF'\n"
                        f"Types: deb\n"
                        f"URIs: https://packages.anduinos.com/artifacts/anduinos/\n"
                        f"Suites: {codename}-addon {codename}-webapps\n"
                        f"Components: main\n"
                        f"Architectures: amd64\n"
                        f"Signed-By: /usr/share/keyrings/anduinos-archive-keyring.gpg\n"
                        f"EOF\n"
                        f"fi\n"
                        f"rm {shlex.quote(temp_path)}\n"
                        f"rm -f /var/lib/apt/lists/*_InRelease /var/lib/apt/lists/*_Release /var/lib/apt/lists/*_Packages /var/lib/apt/lists/*_Sources /var/lib/apt/lists/*_Translation*\n"
                        f"apt-get update"
                    )
                    proc = subprocess.Popen(
                        ['pkexec', 'sh', '-c', bash_cmd],
                        stdout=subprocess.PIPE,
                        stderr=subprocess.STDOUT,
                        text=True,
                        bufsize=1
                    )
                    for line in iter(proc.stdout.readline, ''):
                        GLib.idle_add(lambda l=line: _append_text(l))
                    proc.wait()

                    if proc.returncode == 0:
                        GLib.idle_add(lambda: (
                            _append_text(
                                '\n' + _('✓ Mirror switched and system updated.') + '\n'
                            ),
                            status_label.set_label(
                                _('✓ Switched to ') + mirror),
                            mirror_btn.remove_css_class('suggested-action'),
                            action_btn.add_css_class('suggested-action'),
                            _mirror_cleanup()
                        ))
                    else:
                        raise Exception(
                            f"pkexec exited with code {proc.returncode}"
                        )
                except Exception as e:
                    err_msg = str(e)
                    GLib.idle_add(lambda msg=err_msg: (
                        status_label.set_label(
                            _('✗ Switch failed: ') + msg),
                        _mirror_cleanup()
                    ))

            threading.Thread(target=_apply, daemon=True).start()

        threading.Thread(target=_mirror_work, daemon=True).start()

    mirror_btn.connect('clicked', _on_mirror)

    btn_box.append(skip_btn)
    btn_box.append(mirror_btn)
    btn_box.append(action_btn)
    center.append(btn_box)

    return page


# ═══════════════════════════════════════════════════════════════════
# PAGE 3.8: SECURE BOOT — 信任链条 (conditional)
# ═══════════════════════════════════════════════════════════════════

def create_secureboot_page(navigate_next, update_nav=None):
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)
    page.set_spacing(0)

    center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    center.set_valign(Gtk.Align.CENTER)
    center.set_vexpand(True)
    page.append(center)

    chip_icon = _create_image('secureboot-chip.svg')
    chip_icon.set_pixel_size(72)
    chip_icon.set_halign(Gtk.Align.CENTER)
    chip_icon.set_margin_bottom(12)
    center.append(chip_icon)

    title = Gtk.Label(label=_('Secure Boot Configuration'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_margin_bottom(8)
    center.append(title)
    
    desc = Gtk.Label(label=_('Secure Boot is a motherboard security standard that ensures only trusted software loads at startup.') + '\n' + _('It protects your system from low-level malware and rootkits.'))
    desc.add_css_class('dim-label')
    desc.set_halign(Gtk.Align.CENTER)
    desc.set_justify(Gtk.Justification.CENTER)
    desc.set_margin_bottom(24)
    center.append(desc)
    
    # State verification
    key_path = '/var/lib/shim-signed/mok/MOK.der'
    has_cert = False
    is_trusted = False

    def _refresh_sb_checks():
        nonlocal has_cert, is_trusted
        has_cert = os.path.exists(key_path)
        is_trusted = False
        if has_cert:
            try:
                r = subprocess.run(['mokutil', '--test-key', key_path], capture_output=True, text=True, timeout=5)
                if 'is already enrolled' in r.stdout or 'is already enrolled' in r.stderr:
                    is_trusted = True
            except Exception:
                pass

    _refresh_sb_checks()

    list_group = Adw.PreferencesGroup()
    list_group.set_title(_("System Trust Status"))
    list_group.set_margin_bottom(24)
    center.append(list_group)

    r1 = Adw.ActionRow(title=_("Secure Boot Enabled"))
    r1.set_subtitle(_("Motherboard hardware protection is active"))
    icon1 = _create_image('emblem-ok-symbolic')
    icon1.add_css_class('success')
    icon1.set_pixel_size(16)
    r1.add_suffix(icon1)
    list_group.add(r1)

    r2 = Adw.ActionRow(title=_("Local MOK Certificate"))
    icon2 = _create_image('emblem-ok-symbolic' if has_cert else 'dialog-warning-symbolic')
    if has_cert:
        r2.set_subtitle(_("Certificate generated locally"))
        icon2.add_css_class('success')
    else:
        r2.set_subtitle(_("Missing local certificate"))
        icon2.add_css_class('warning')
    icon2.set_pixel_size(16)
    r2.add_suffix(icon2)
    list_group.add(r2)

    r3 = Adw.ActionRow(title=_("UEFI Firmware Trust"))
    icon3 = _create_image('emblem-ok-symbolic' if is_trusted else 'dialog-error-symbolic')
    if is_trusted:
        r3.set_subtitle(_("Certificate is trusted by motherboard"))
        icon3.add_css_class('success')
    else:
        r3.set_subtitle(_("Pending enrollment in blue screen (MOKManager)"))
        icon3.add_css_class('error')
    icon3.set_pixel_size(16)
    r3.add_suffix(icon3)
    list_group.add(r3)

    r4 = Adw.ActionRow(title=_("Third-party Drivers"))
    r4.set_subtitle(_("Scanning for installed drivers..."))
    r4_icon = Gtk.Spinner()
    r4_icon.start()
    r4_icon.set_margin_top(8)
    r4_icon.set_margin_bottom(8)
    r4.add_suffix(r4_icon)
    list_group.add(r4)

    def _check_drivers():
        found = False
        try:
            cert_serial = subprocess.check_output(
                ['openssl', 'x509', '-in', key_path, '-inform', 'DER', '-noout', '-serial'],
                text=True, timeout=5
            ).strip().split('=')[1]
            formatted_serial = ':'.join(cert_serial[i:i+2] for i in range(0, len(cert_serial), 2)).lower()

            dkms_dir = f"/lib/modules/{os.uname().release}/updates/dkms"
            if os.path.isdir(dkms_dir):
                for f in os.listdir(dkms_dir):
                    if f.endswith('.ko') or f.endswith('.ko.zst') or f.endswith('.ko.xz'):
                        mod_path = os.path.join(dkms_dir, f)
                        modinfo_out = subprocess.check_output(['modinfo', mod_path], text=True, timeout=5).lower()
                        if formatted_serial in modinfo_out:
                            found = True
                            break
        except Exception:
            pass

        def _update_ui():
            r4.remove(r4_icon)
            if found:
                icon4 = _create_image('emblem-ok-symbolic')
                r4.set_subtitle(_("Drivers are signed and ready to load"))
                icon4.add_css_class('success')
                icon4.set_pixel_size(16)
                r4.add_suffix(icon4)
            else:
                r4.set_subtitle(_("No signed third-party drivers detected"))

        GLib.idle_add(_update_ui)

    if has_cert:
        threading.Thread(target=_check_drivers, daemon=True).start()
    else:
        r4.remove(r4_icon)
        icon4 = _create_image('dialog-error-symbolic')
        icon4.add_css_class('error')
        icon4.set_pixel_size(16)
        r4.add_suffix(icon4)
        r4.set_subtitle(_("Cannot sign drivers without a certificate"))

    # Status message + action area (always present, toggled on refresh)
    sb_status = Gtk.Label()
    sb_status.set_justify(Gtk.Justification.CENTER)
    sb_status.set_halign(Gtk.Align.CENTER)

    # Enroll button container (visibility toggled based on is_trusted)
    sb_action_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
    sb_action_box.set_halign(Gtk.Align.CENTER)

    if is_trusted:
        sb_status.set_label(_('System Trust Established. Third-party drivers will load securely.'))
        sb_status.set_margin_top(12)
        sb_status.set_margin_bottom(0)
        sb_status.add_css_class('title-4')
        sb_action_box.set_visible(False)
    else:
        sb_status.set_label(_('The local trust certificate is missing or not yet enrolled.') + '\n' + _('You must configure this to use third-party drivers like NVIDIA.'))
        sb_status.set_margin_top(0)
        sb_status.set_margin_bottom(24)
        sb_action_box.set_visible(True)

    center.append(sb_status)

    btn = Gtk.Button()
    btn.set_halign(Gtk.Align.CENTER)
    btn.add_css_class('suggested-action')
    btn.add_css_class('pill')
    btn.set_size_request(240, 48)
    btn.set_margin_top(16)

    spinner = Gtk.Spinner()
    enroll_btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
    enroll_btn_box.set_halign(Gtk.Align.CENTER)
    btn_lbl = Gtk.Label(label=_('Create & Enroll Certificate'))
    enroll_btn_box.append(spinner)
    enroll_btn_box.append(btn_lbl)
    btn.set_child(enroll_btn_box)
    sb_action_box.append(btn)

    def _on_enroll_clicked(button):
        pwd = "123456"
        button.set_sensitive(False)
        spinner.start()
        btn_lbl.set_label(_("Generating & Signing..."))

        def _worker():
            import shlex
            p = shlex.quote(pwd)
            # 强行接管 DKMS 签名配置，只用 Key 路径，不写 sign_file —
            # DKMS 会自动找当前内核的 scripts/sign-file
            conf_script = """
mkdir -p /etc/dkms/framework.conf.d
cat << 'EOF' > /etc/dkms/framework.conf.d/anduinos-sb-sign.conf
mok_signing_key="/var/lib/shim-signed/mok/MOK.priv"
mok_certificate="/var/lib/shim-signed/mok/MOK.der"
EOF
"""
            # Use set -e instead of && chaining — bash 5.3+ rejects
            # a bare && at the start of a new line after a heredoc.
            cmd = (f"set -e\n"
                   f"update-secureboot-policy --new-key\n"
                   f"printf '%s\\n%s\\n' {p} {p} | mokutil --import /var/lib/shim-signed/mok/MOK.der\n"
                   f"{conf_script}\n"
                   f"dkms autoinstall")
            proc = subprocess.run(['pkexec', 'bash', '-c', cmd])

            def _update_ui():
                spinner.stop()
                if proc.returncode == 0:
                    dialog = Adw.MessageDialog.new(
                        page.get_root(),
                        _("Certificate Created"),
                        _("Success! When you reboot, a blue screen will appear.") + '\n' + _("Select 'Enroll MOK' → 'Continue' → 'Yes', and enter password: 123456")
                    )
                    dialog.add_response("ok", _("OK"))
                    dialog.set_default_response("ok")

                    def _on_dialog_response(dlg, response):
                        page._hide_next = True
                        if update_nav:
                            update_nav()

                        _refresh_sb_checks()
                        icon2.set_from_icon_name('emblem-ok-symbolic')
                        icon2.remove_css_class('warning')
                        icon2.add_css_class('success')
                        r2.set_subtitle(_("Certificate generated locally"))
                        icon3.set_from_icon_name('emblem-ok-symbolic' if is_trusted else 'dialog-error-symbolic')
                        icon3.remove_css_class('error')
                        icon3.remove_css_class('success')
                        icon3.add_css_class('success' if is_trusted else 'error')
                        r3.set_subtitle(_("Certificate is trusted by motherboard") if is_trusted else _("Pending enrollment in blue screen (MOKManager)"))
                        if is_trusted:
                            sb_status.set_label(_('System Trust Established. Third-party drivers will load securely.'))
                            sb_status.set_margin_top(12)
                            sb_status.set_margin_bottom(0)
                            sb_status.add_css_class('title-4')
                            sb_action_box.set_visible(False)
                        else:
                            sb_status.set_label(_('The local trust certificate is missing or not yet enrolled.') + '\n' + _('You must configure this to use third-party drivers like NVIDIA.'))
                            sb_status.set_margin_top(0)
                            sb_status.set_margin_bottom(24)
                            sb_status.remove_css_class('title-4')
                            sb_action_box.set_visible(True)
                        sb_refresh_btn.set_visible(not (has_cert and is_trusted))

                        sb_status.set_visible(False)
                        btn.set_visible(False)

                        rb_lbl = Gtk.Label(label=_("Note: Use password <b>123456</b> after rebooting."))
                        rb_lbl.set_use_markup(True)
                        rb_lbl.set_margin_bottom(12)
                        center.append(rb_lbl)

                        rb_btn = Gtk.Button(label=_("Reboot & Configure Secure Boot"))
                        rb_btn.add_css_class('suggested-action')
                        rb_btn.add_css_class('pill')
                        rb_btn.set_size_request(280, 48)
                        rb_btn.set_halign(Gtk.Align.CENTER)
                        center.append(rb_btn)

                        def _on_reboot_clicked(b):
                            r_dlg = Adw.MessageDialog.new(
                                page.get_root(),
                                _("Reboot Required"),
                                _("Please trust the certificate upon reboot using password 123456.")
                            )
                            r_dlg.add_response("cancel", _("Cancel"))
                            r_dlg.add_response("reboot", _("Reboot"))
                            r_dlg.set_response_appearance("reboot", Adw.ResponseAppearance.DESTRUCTIVE)

                            def _on_reboot_response(d, resp):
                                if resp == "reboot":
                                    subprocess.run(['gnome-session-quit', '--reboot', '--no-prompt'])
                            r_dlg.connect('response', _on_reboot_response)
                            r_dlg.present()

                        rb_btn.connect('clicked', _on_reboot_clicked)

                    dialog.connect('response', _on_dialog_response)
                    dialog.present()
                else:
                    sb_status.set_label(_("Configuration failed. Please try again."))
                    button.set_sensitive(True)
                    btn_lbl.set_label(_('Create & Enroll Certificate'))
            GLib.idle_add(_update_ui)

        threading.Thread(target=_worker, daemon=True).start()

    btn.connect('clicked', _on_enroll_clicked)
    center.append(sb_action_box)

    # ── "Check Again" refresh button ──
    sb_refresh_btn = Gtk.Button(label=_('  Check Again  '))
    sb_refresh_btn.set_halign(Gtk.Align.CENTER)
    sb_refresh_btn.set_margin_top(8)

    def _on_sb_refresh(button):
        button.set_sensitive(False)
        button.set_label(_('  Checking...  '))
        def _worker():
            _refresh_sb_checks()
            def _apply():
                icon2.set_from_icon_name('emblem-ok-symbolic' if has_cert else 'dialog-warning-symbolic')
                icon2.remove_css_class('success')
                icon2.remove_css_class('warning')
                icon2.add_css_class('success' if has_cert else 'warning')
                r2.set_subtitle(_("Certificate generated locally") if has_cert else _("Missing local certificate"))
                icon3.set_from_icon_name('emblem-ok-symbolic' if is_trusted else 'dialog-error-symbolic')
                icon3.remove_css_class('success')
                icon3.remove_css_class('error')
                icon3.add_css_class('success' if is_trusted else 'error')
                r3.set_subtitle(_("Certificate is trusted by motherboard") if is_trusted else _("Pending enrollment in blue screen (MOKManager)"))
                if is_trusted:
                    sb_status.set_label(_('System Trust Established. Third-party drivers will load securely.'))
                    sb_status.set_margin_top(12)
                    sb_status.set_margin_bottom(0)
                    sb_status.add_css_class('title-4')
                    sb_action_box.set_visible(False)
                else:
                    sb_status.set_label(_('The local trust certificate is missing or not yet enrolled.') + '\n' + _('You must configure this to use third-party drivers like NVIDIA.'))
                    sb_status.set_margin_top(0)
                    sb_status.set_margin_bottom(24)
                    sb_status.remove_css_class('title-4')
                    sb_action_box.set_visible(True)
                sb_refresh_btn.set_visible(not (has_cert and is_trusted))
                button.set_sensitive(True)
                button.set_label(_('  Check Again  '))
            GLib.idle_add(_apply)
        threading.Thread(target=_worker, daemon=True).start()

    sb_refresh_btn.connect('clicked', _on_sb_refresh)
    sb_refresh_btn.set_visible(not (has_cert and is_trusted))
    center.append(sb_refresh_btn)

    return page


# ═══════════════════════════════════════════════════════════════════
# PAGE 4: NVIDIA — 性能释放 (conditional)
# ═══════════════════════════════════════════════════════════════════

def create_nvidia_page(navigate_next, update_nav=None):
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)
    page.set_spacing(0)
    
    page._suggest_next = False

    center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    center.set_valign(Gtk.Align.CENTER)
    center.set_vexpand(True)
    page.append(center)

    # Green-tinted icon
    icon = _create_image('nvidia.svg')
    icon.set_pixel_size(72)
    icon.set_halign(Gtk.Align.CENTER)
    center.append(icon)

    title = Gtk.Label(label=_('Unleash Your Graphics Hardware'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_wrap(True)
    title.set_justify(Gtk.Justification.CENTER)
    title.set_margin_top(12)
    center.append(title)

    driver_version = None
    try:
        r = subprocess.run(['nvidia-smi', '--query-gpu=driver_version', '--format=csv,noheader'],
                           capture_output=True, text=True, timeout=5)
        if r.returncode == 0:
            driver_version = r.stdout.strip().split('\n')[0]
    except Exception:
        pass

    if driver_version:
        sub = Gtk.Label(
            label=_('NVIDIA proprietary driver is already installed. (Version: {})').format(driver_version)
        )
    else:
        sub = Gtk.Label(
            label=_('An NVIDIA dedicated graphics card was detected in your system.')
        )
    sub.add_css_class('dim-label')
    sub.set_halign(Gtk.Align.CENTER)
    sub.set_wrap(True)
    sub.set_justify(Gtk.Justification.CENTER)
    sub.set_margin_top(12)
    center.append(sub)

    body = Gtk.Label(
        label=_('For ultimate smoothness in gaming, 3D rendering, and high-framerate desktop') + '\n' + _('experiences, we recommend installing the proprietary driver.')
    )
    body.set_halign(Gtk.Align.CENTER)
    body.set_wrap(True)
    body.set_justify(Gtk.Justification.CENTER)
    body.set_margin_top(24)
    body.set_margin_bottom(24)
    center.append(body)

    # Status
    status_label = Gtk.Label()
    status_label.add_css_class('dim-label')
    status_label.set_halign(Gtk.Align.CENTER)
    status_label.set_wrap(True)
    status_label.set_margin_bottom(12)
    center.append(status_label)

    progress_bar = Gtk.ProgressBar()
    progress_bar.set_visible(False)
    progress_bar.set_margin_start(48)
    progress_bar.set_margin_end(48)
    center.append(progress_bar)

    # Buttons
    btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
    btn_box.set_halign(Gtk.Align.CENTER)
    btn_box.set_margin_top(24)

    skip_btn = Gtk.Button(label=_('  Do It Later  '))
    skip_btn.connect('clicked', lambda b: navigate_next())

    if driver_version:
        install_btn = Gtk.Button(label=_('  Check for Driver Updates  '))
    else:
        install_btn = Gtk.Button(label=_('  One-Click NVIDIA Driver Install  '))
    install_btn.add_css_class('suggested-action')

    # Advanced Output Expander
    expander = Gtk.Expander(label=_('Advanced Output'))
    expander.set_margin_top(12)
    expander.set_margin_start(48)
    expander.set_margin_end(48)
    expander.set_halign(Gtk.Align.FILL)
    
    scroll = Gtk.ScrolledWindow()
    scroll.set_min_content_height(150)
    scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
    
    textview = Gtk.TextView()
    textview.set_editable(False)
    textview.set_cursor_visible(False)
    textview.add_css_class('card')
    textview.set_monospace(True)
    textview.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
    buffer = textview.get_buffer()
    
    scroll.set_child(textview)
    expander.set_child(scroll)
    center.append(expander)

    def _on_install(btn):
        btn.set_sensitive(False)
        skip_btn.set_sensitive(False)
        progress_bar.set_visible(True)
        progress_bar.pulse()
        status_label.set_label(_('Installing NVIDIA driver…') + '\n' + _('This may take several minutes.'))

        def _pulse():
            if progress_bar.is_visible():
                progress_bar.pulse()
                return True
            return False

        GLib.timeout_add(100, _pulse)

        def _do_install():
            try:
                bash_cmd = 'rm -f /var/lib/apt/lists/*_InRelease /var/lib/apt/lists/*_Release /var/lib/apt/lists/*_Packages /var/lib/apt/lists/*_Sources /var/lib/apt/lists/*_Translation*; apt-get update; ubuntu-drivers install || true; NVIDIA_PKGS=$(dpkg-query -f '"'"'${Package}\n'"'"' -W 2>/dev/null | grep -E "nvidia|linux-modules-nvidia"); if [ -n "$NVIDIA_PKGS" ]; then DEBIAN_FRONTEND=noninteractive apt-get install --only-upgrade -y $NVIDIA_PKGS; fi'
                proc = subprocess.Popen(
                    ['pkexec', 'sh', '-c', bash_cmd],
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    text=True,
                    bufsize=1
                )
                
                def _append_text(t):
                    end_iter = buffer.get_end_iter()
                    buffer.insert(end_iter, t)
                    mark = buffer.create_mark(None, buffer.get_end_iter(), False)
                    textview.scroll_to_mark(mark, 0.0, True, 0.0, 1.0)
                    
                for line in iter(proc.stdout.readline, ''):
                    GLib.idle_add(lambda l=line: _append_text(l))
                proc.stdout.close()
                proc.wait()
                
                if proc.returncode == 0:
                    def _set_success():
                        status_label.set_label(
                            _('✓ Driver installed successfully!') + '\n' + _('A reboot is recommended to apply changes.'))
                        progress_bar.set_visible(False)
                        btn.set_visible(False)
                        skip_btn.set_visible(False)
                        page._suggest_next = True
                        if update_nav: update_nav()
                    GLib.idle_add(_set_success)
                else:
                    def _set_fail():
                        status_label.set_label(
                            _('✗ Installation encountered an issue.') + '\n' + _('You can install drivers later from Settings.'))
                        progress_bar.set_visible(False)
                        btn.set_sensitive(True)
                        skip_btn.set_sensitive(True)
                        page._suggest_next = True
                        if update_nav: update_nav()
                    GLib.idle_add(_set_fail)
            except Exception as e:
                def _set_ex():
                    status_label.set_label(_('✗ Installation failed: ') + str(e))
                    progress_bar.set_visible(False)
                    btn.set_sensitive(True)
                    skip_btn.set_sensitive(True)
                    page._suggest_next = True
                    if update_nav: update_nav()
                GLib.idle_add(_set_ex)

        threading.Thread(target=_do_install, daemon=True).start()

    install_btn.connect('clicked', _on_install)
    btn_box.append(skip_btn)
    btn_box.append(install_btn)
    center.append(btn_box)

    return page


# ═══════════════════════════════════════════════════════════════════
# PAGE 5: EXE SANDBOX — 无缝兼容
# ═══════════════════════════════════════════════════════════════════

def create_xbox_page(navigate_next, update_nav=None):
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)
    page.set_spacing(0)
    
    page._suggest_next = False

    center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    center.set_valign(Gtk.Align.CENTER)
    center.set_vexpand(True)
    page.append(center)

    icon = _create_image('input-gaming.svg')
    icon.set_pixel_size(72)
    icon.set_halign(Gtk.Align.CENTER)
    center.append(icon)

    title = Gtk.Label(label=_('Xbox Controller Support'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_wrap(True)
    title.set_justify(Gtk.Justification.CENTER)
    title.set_margin_top(12)
    center.append(title)

    is_installed = False
    sb_enabled = False
    has_cert = False
    is_trusted = False
    current_cert_serial = None
    modinfo_sig_key = None
    is_loaded = False

    mok_priv = '/var/lib/shim-signed/mok/MOK.priv'
    mok_der = '/var/lib/shim-signed/mok/MOK.der'

    def _refresh_xbox_checks():
        nonlocal is_installed, sb_enabled, has_cert, is_trusted, current_cert_serial, modinfo_sig_key, is_loaded

        is_installed = False
        try:
            r = subprocess.run(['dpkg', '-l', 'anduinos-xbox-controller-driver'],
                               capture_output=True, text=True, timeout=5)
            is_installed = (r.returncode == 0 and 'ii  anduinos-xbox-controller-driver' in r.stdout)
        except Exception:
            pass

        sb_enabled = False
        try:
            r = subprocess.run(['mokutil', '--sb-state'], capture_output=True, text=True, timeout=5)
            if 'SecureBoot enabled' in r.stdout:
                sb_enabled = True
        except Exception:
            pass

        has_cert = os.path.isfile(mok_priv)
        is_trusted = False
        if has_cert:
            try:
                res = subprocess.run(['mokutil', '--test-key', mok_der],
                                     capture_output=True, text=True, timeout=5)
                if 'is already enrolled' in res.stdout:
                    is_trusted = True
            except Exception:
                pass
        else:
            is_trusted = not sb_enabled

        current_cert_serial = None
        if has_cert:
            try:
                cert_serial = subprocess.check_output(
                    ['openssl', 'x509', '-in', mok_der, '-inform', 'DER', '-noout', '-serial'],
                    text=True, timeout=5
                ).strip().split('=')[1]
                current_cert_serial = ':'.join(cert_serial[i:i+2] for i in range(0, len(cert_serial), 2)).lower()
            except Exception:
                pass

        modinfo_sig_key = None
        if is_installed:
            try:
                out = subprocess.check_output(['modinfo', 'hid-xpadneo'], text=True, timeout=5)
                for line in out.splitlines():
                    if line.startswith('sig_key:'):
                        modinfo_sig_key = line.split(':', 1)[1].strip().lower()
                        break
            except Exception:
                pass

        is_loaded = False
        if is_installed:
            try:
                r = subprocess.run(['lsmod'], capture_output=True, text=True, timeout=5)
                if 'xpadneo' in r.stdout:
                    is_loaded = True
            except Exception:
                pass

    _refresh_xbox_checks()

    r3 = None
    icon3 = None

    if is_installed:
        sub = Gtk.Label(
            label=_('Xbox controller driver (xpadneo) is already installed.')
        )
    else:
        sub = Gtk.Label(
            label=_('Enjoy seamless Xbox controller support with rumble and accurate inputs.')
        )
    sub.add_css_class('dim-label')
    sub.set_halign(Gtk.Align.CENTER)
    sub.set_wrap(True)
    sub.set_justify(Gtk.Justification.CENTER)
    sub.set_margin_top(12)
    center.append(sub)

    if is_installed:
        body_text = _('AnduinOS provides native support for Xbox controllers via Bluetooth.') + '\n' + \
                    _('The advanced xpadneo driver is already set up and ready for gaming.')
    else:
        body_text = _('AnduinOS provides native support for Xbox controllers via Bluetooth.') + '\n' + \
                    _('For advanced features like rumble and accurate mapping, we recommend installing the xpadneo driver.')

    body = Gtk.Label(label=body_text)
    body.set_halign(Gtk.Align.CENTER)
    body.set_wrap(True)
    body.set_justify(Gtk.Justification.CENTER)
    body.set_margin_top(24)
    body.set_margin_bottom(12)
    center.append(body)

    list_group = Adw.PreferencesGroup()
    list_group.set_margin_top(12)
    list_group.set_margin_bottom(12)
    list_group.set_margin_start(48)
    list_group.set_margin_end(48)
    center.append(list_group)

    r1 = Adw.ActionRow(title=_("Xbox Controller Driver Installed"))
    icon1 = _create_image('emblem-ok-symbolic' if is_installed else 'dialog-error-symbolic')
    if is_installed:
        r1.set_subtitle(_("The xpadneo driver is currently installed"))
        icon1.add_css_class('success')
    else:
        r1.set_subtitle(_("The driver is not installed"))
        icon1.add_css_class('error')
    icon1.set_pixel_size(16)
    r1.add_suffix(icon1)
    list_group.add(r1)

    if sb_enabled:
        r2 = Adw.ActionRow(title=_("Driver Signature Trusted"))
        icon2 = _create_image('dialog-error-symbolic')
        icon2.set_pixel_size(16)
        
        if not is_installed:
            r2.set_subtitle(_("Driver not installed, signature missing"))
            icon2.add_css_class('error')
        else:
            if not modinfo_sig_key:
                r2.set_subtitle(_("Driver is NOT signed"))
                icon2.add_css_class('error')
            else:
                if modinfo_sig_key == current_cert_serial:
                    if is_trusted:
                        r2.set_subtitle(_("Signed with current trusted certificate"))
                        icon2.set_from_icon_name('emblem-ok-symbolic')
                        icon2.add_css_class('success')
                    else:
                        r2.set_subtitle(_("Signed with current certificate, but not enrolled"))
                        icon2.set_from_icon_name('dialog-warning-symbolic')
                        icon2.add_css_class('warning')
                else:
                    r2.set_subtitle(_("Signed with an unknown or outdated certificate"))
                    icon2.set_from_icon_name('dialog-warning-symbolic')
                    icon2.add_css_class('warning')
                    
        r2.add_suffix(icon2)
        list_group.add(r2)

    if is_installed:
        r3 = Adw.ActionRow(title=_("Controller Status"))

        icon3 = _create_image('emblem-ok-symbolic')
        icon3.set_pixel_size(16)

        if is_loaded:
            r3.set_subtitle(_("Driver loaded and active. Ready to play!"))
            icon3.add_css_class('success')
        else:
            is_blocked = False
            if sb_enabled and (not is_trusted or modinfo_sig_key != current_cert_serial):
                is_blocked = True
            
            if is_blocked:
                r3.set_subtitle(_("Driver cannot load due to missing signature trust"))
                icon3.set_from_icon_name('dialog-error-symbolic')
                icon3.add_css_class('error')
            else:
                r3.set_subtitle(_("Driver standing by. Waiting for controller connection..."))
                icon3.add_css_class('success')

        r3.add_suffix(icon3)
        list_group.add(r3)

    status_label = Gtk.Label()
    status_label.add_css_class('dim-label')
    status_label.set_halign(Gtk.Align.CENTER)
    status_label.set_wrap(True)
    status_label.set_margin_bottom(12)
    center.append(status_label)

    progress_bar = Gtk.ProgressBar()
    progress_bar.set_visible(False)
    progress_bar.set_margin_start(48)
    progress_bar.set_margin_end(48)
    center.append(progress_bar)

    btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
    btn_box.set_halign(Gtk.Align.CENTER)
    btn_box.set_margin_top(24)

    tools_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
    tools_box.set_halign(Gtk.Align.CENTER)
    tools_box.set_margin_top(12)
    
    pair_btn = Gtk.Button(label=_('Pair your controller'))
    pair_btn.connect('clicked', lambda b: subprocess.Popen(['gnome-control-center', 'bluetooth']))
    test_btn = Gtk.Button(label=_('Test your controller'))
    def _open_tester(b):
        import shutil
        if shutil.which('firefox'):
            subprocess.Popen(['firefox', 'https://hardwaretester.com/gamepad'])
        else:
            subprocess.Popen(['xdg-open', 'https://hardwaretester.com/gamepad'])
    test_btn.connect('clicked', _open_tester)
    
    tools_box.append(pair_btn)
    tools_box.append(test_btn)
    
    is_ready_to_play = False
    if is_installed:
        if sb_enabled:
            if is_trusted and modinfo_sig_key == current_cert_serial:
                is_ready_to_play = True
        else:
            is_ready_to_play = True
    tools_box.set_visible(is_ready_to_play)



    install_btn = Gtk.Button(label=_('  Install Xbox Driver  '))
    install_btn.add_css_class('suggested-action')

    reinstall_btn = Gtk.Button(label=_('  Fix & Reinstall Driver  '))
    reinstall_btn.add_css_class('suggested-action')
    reinstall_btn.set_visible(False)

    # ── Smart button visibility ──
    install_btn.set_visible(False)
    reinstall_btn.set_visible(False)
    page._suggest_next = True
    status_label.set_label('')

    if not is_installed:
        if sb_enabled and not is_trusted:
            install_btn.set_visible(True)
            install_btn.set_sensitive(False)
            status_label.set_markup('<span foreground="#ffcc00">' + _('⚠️ Secure Boot is enabled but the custom certificate is not yet enrolled.') + '\n' + _('Please configure Secure Boot in the previous step before installing this driver.') + '</span>')
            page._suggest_next = False
        else:
            install_btn.set_visible(True)
            install_btn.set_sensitive(True)
            page._suggest_next = False
    else:
        if sb_enabled and is_trusted and modinfo_sig_key != current_cert_serial:
            reinstall_btn.set_visible(True)
            status_label.set_markup('<span foreground="#ffcc00">' + _('⚠️ The driver signature does not match your system certificate. Click Fix & Reinstall to repair the driver.') + '</span>')
            page._suggest_next = False

    expander = Gtk.Expander(label=_('Advanced Output'))
    expander.set_margin_top(12)
    expander.set_margin_start(48)
    expander.set_margin_end(48)
    expander.set_halign(Gtk.Align.FILL)
    
    scroll = Gtk.ScrolledWindow()
    scroll.set_min_content_height(150)
    scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
    
    textview = Gtk.TextView()
    textview.set_editable(False)
    textview.set_cursor_visible(False)
    textview.add_css_class('card')
    textview.set_monospace(True)
    textview.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
    buffer = textview.get_buffer()
    
    scroll.set_child(textview)
    expander.set_child(scroll)
    center.append(expander)

    def _on_install(btn):
        btn.set_sensitive(False)
        progress_bar.set_visible(True)
        progress_bar.pulse()
        status_label.set_label(_('Installing Xbox controller driver…') + "\n" + _('This may take a minute.'))

        def _pulse():
            if progress_bar.is_visible():
                progress_bar.pulse()
                return True
            return False

        GLib.timeout_add(100, _pulse)

        def _do_install():
            try:
                conf_script = f"""
rm -f /var/lib/apt/lists/*_InRelease /var/lib/apt/lists/*_Release /var/lib/apt/lists/*_Packages /var/lib/apt/lists/*_Sources /var/lib/apt/lists/*_Translation*
apt update
apt install -y anduinos-xbox-controller-driver
"""
                proc = subprocess.Popen(
                    ['pkexec', 'bash', '-c', conf_script],
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    text=True,
                    bufsize=1
                )
                
                def _append_text(t):
                    end_iter = buffer.get_end_iter()
                    buffer.insert(end_iter, t)
                    mark = buffer.create_mark(None, buffer.get_end_iter(), False)
                    textview.scroll_to_mark(mark, 0.0, True, 0.0, 1.0)
                    
                for line in iter(proc.stdout.readline, ''):
                    GLib.idle_add(lambda l=line: _append_text(l))
                proc.stdout.close()
                proc.wait()
                
                if proc.returncode == 0:
                    def _set_success():
                        nonlocal r3, icon3, is_installed
                        is_installed = True
                        status_label.set_label(_('✓ Xbox controller driver installed successfully!'))
                        r1.set_subtitle(_("The xpadneo driver is currently installed"))
                        icon1.set_from_icon_name('emblem-ok-symbolic')
                        icon1.remove_css_class('error')
                        icon1.add_css_class('success')

                        if sb_enabled:
                            r2.set_subtitle(_("Signed with current trusted certificate"))
                            icon2.set_from_icon_name('emblem-ok-symbolic')
                            icon2.remove_css_class('error')
                            icon2.add_css_class('success')

                        if r3 is None:
                            r3 = Adw.ActionRow(title=_("Controller Status"))
                            icon3 = _create_image('emblem-ok-symbolic')
                            icon3.set_pixel_size(16)
                            r3.add_suffix(icon3)
                            list_group.add(r3)
                        icon3.remove_css_class('error')
                        icon3.add_css_class('success')
                        icon3.set_from_icon_name('emblem-ok-symbolic')
                        r3.set_subtitle(_("Driver standing by. Waiting for controller connection...") + "\n" + _("(Reboot recommended)"))

                        progress_bar.set_visible(False)
                        btn.set_visible(False)
                        tools_box.set_visible(True)
                        page._suggest_next = True
                        xb_refresh_btn.set_visible(False)
                        if update_nav: update_nav()
                    GLib.idle_add(_set_success)
                else:
                    def _set_fail():
                        status_label.set_label(_('✗ Installation encountered an issue.') + "\n" + _('Please check the advanced output.'))
                        progress_bar.set_visible(False)
                        btn.set_sensitive(True)
                        page._suggest_next = True
                        if update_nav: update_nav()
                    GLib.idle_add(_set_fail)
            except Exception as e:
                def _set_ex():
                    status_label.set_label(_('✗ Installation failed: ') + str(e))
                    progress_bar.set_visible(False)
                    btn.set_sensitive(True)
                    page._suggest_next = True
                    if update_nav: update_nav()
                GLib.idle_add(_set_ex)

        threading.Thread(target=_do_install, daemon=True).start()

    install_btn.connect('clicked', _on_install)

    def _on_reinstall(btn):
        btn.set_sensitive(False)
        progress_bar.set_visible(True)
        progress_bar.pulse()
        status_label.set_label(_('Repairing driver signature…') + "\n" + _('This may take a minute.'))

        def _pulse():
            if progress_bar.is_visible():
                progress_bar.pulse()
                return True
            return False

        GLib.timeout_add(100, _pulse)

        def _do_reinstall():
            try:
                # 先补 DKMS 签名配置，再重装驱动
                conf_script = """
if [ ! -f /etc/dkms/framework.conf.d/anduinos-sb-sign.conf ] && [ -f /var/lib/shim-signed/mok/MOK.priv ]; then
    mkdir -p /etc/dkms/framework.conf.d
    cat << 'EOF' > /etc/dkms/framework.conf.d/anduinos-sb-sign.conf
mok_signing_key="/var/lib/shim-signed/mok/MOK.priv"
mok_certificate="/var/lib/shim-signed/mok/MOK.der"
EOF
fi
apt update && apt reinstall -y anduinos-xbox-controller-driver
"""
                proc = subprocess.Popen(
                    ['pkexec', 'bash', '-c', conf_script],
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    text=True,
                    bufsize=1
                )

                def _append_text(t):
                    end_iter = buffer.get_end_iter()
                    buffer.insert(end_iter, t)
                    mark = buffer.create_mark(None, buffer.get_end_iter(), False)
                    textview.scroll_to_mark(mark, 0.0, True, 0.0, 1.0)

                for line in iter(proc.stdout.readline, ''):
                    GLib.idle_add(lambda l=line: _append_text(l))
                proc.stdout.close()
                proc.wait()

                if proc.returncode == 0:
                    GLib.idle_add(lambda: (
                        progress_bar.set_visible(False),
                        _on_xb_refresh(xb_refresh_btn)
                    ))
                else:
                    GLib.idle_add(lambda: (
                        status_label.set_label(_('✗ Repair failed.') + "\n" + _('Please check the advanced output.')),
                        progress_bar.set_visible(False),
                        btn.set_sensitive(True)
                    ))
            except Exception as e:
                GLib.idle_add(lambda e=e: (
                    status_label.set_label(_('✗ Error: ') + str(e)),
                    progress_bar.set_visible(False),
                    btn.set_sensitive(True)
                ))

        threading.Thread(target=_do_reinstall, daemon=True).start()

    reinstall_btn.connect('clicked', _on_reinstall)
    btn_box.append(install_btn)
    btn_box.append(reinstall_btn)
    center.append(btn_box)
    center.append(tools_box)

    # ── "Check Again" refresh button ──
    xb_refresh_btn = Gtk.Button(label=_('  Check Again  '))
    xb_refresh_btn.set_halign(Gtk.Align.CENTER)
    xb_refresh_btn.set_margin_top(12)

    def _on_xb_refresh(button):
        button.set_sensitive(False)
        button.set_label(_('  Checking...  '))
        def _worker():
            _refresh_xbox_checks()
            def _apply():
                nonlocal r3, icon3

                # sub label
                if is_installed:
                    sub.set_label(_('Xbox controller driver (xpadneo) is already installed.'))
                else:
                    sub.set_label(_('Enjoy seamless Xbox controller support with rumble and accurate inputs.'))

                # body label
                if is_installed:
                    body.set_label(_('AnduinOS provides native support for Xbox controllers via Bluetooth.') + '\n' +
                                   _('The advanced xpadneo driver is already set up and ready for gaming.'))
                else:
                    body.set_label(_('AnduinOS provides native support for Xbox controllers via Bluetooth.') + '\n' +
                                   _('For advanced features like rumble and accurate mapping, we recommend installing the xpadneo driver.'))

                # r1 — Driver Installed
                icon1.set_from_icon_name('emblem-ok-symbolic' if is_installed else 'dialog-error-symbolic')
                icon1.remove_css_class('success')
                icon1.remove_css_class('error')
                if is_installed:
                    r1.set_subtitle(_("The xpadneo driver is currently installed"))
                    icon1.add_css_class('success')
                else:
                    r1.set_subtitle(_("The driver is not installed"))
                    icon1.add_css_class('error')

                # r2 — Signature Trusted (only if sb_enabled)
                if sb_enabled:
                    icon2.remove_css_class('success')
                    icon2.remove_css_class('error')
                    icon2.remove_css_class('warning')
                    if not is_installed:
                        r2.set_subtitle(_("Driver not installed, signature missing"))
                        icon2.set_from_icon_name('dialog-error-symbolic')
                        icon2.add_css_class('error')
                    elif not modinfo_sig_key:
                        r2.set_subtitle(_("Driver is NOT signed"))
                        icon2.set_from_icon_name('dialog-error-symbolic')
                        icon2.add_css_class('error')
                    elif modinfo_sig_key == current_cert_serial:
                        if is_trusted:
                            r2.set_subtitle(_("Signed with current trusted certificate"))
                            icon2.set_from_icon_name('emblem-ok-symbolic')
                            icon2.add_css_class('success')
                        else:
                            r2.set_subtitle(_("Signed with current certificate, but not enrolled"))
                            icon2.set_from_icon_name('dialog-warning-symbolic')
                            icon2.add_css_class('warning')
                    else:
                        r2.set_subtitle(_("Signed with an unknown or outdated certificate"))
                        icon2.set_from_icon_name('dialog-warning-symbolic')
                        icon2.add_css_class('warning')

                # r3 — Controller Status (lazy-create if first install after page load)
                if is_installed:
                    if r3 is None:
                        r3 = Adw.ActionRow(title=_("Controller Status"))
                        icon3 = _create_image('emblem-ok-symbolic')
                        icon3.set_pixel_size(16)
                        r3.add_suffix(icon3)
                        list_group.add(r3)
                    icon3.remove_css_class('success')
                    icon3.remove_css_class('error')
                    if is_loaded:
                        r3.set_subtitle(_("Driver loaded and active. Ready to play!"))
                        icon3.set_from_icon_name('emblem-ok-symbolic')
                        icon3.add_css_class('success')
                    else:
                        is_blocked = False
                        if sb_enabled and (not is_trusted or modinfo_sig_key != current_cert_serial):
                            is_blocked = True
                        if is_blocked:
                            r3.set_subtitle(_("Driver cannot load due to missing signature trust"))
                            icon3.set_from_icon_name('dialog-error-symbolic')
                            icon3.add_css_class('error')
                        else:
                            r3.set_subtitle(_("Driver standing by. Waiting for controller connection..."))
                            icon3.set_from_icon_name('emblem-ok-symbolic')
                            icon3.add_css_class('success')

                # is_ready_to_play
                _ready = False
                if is_installed:
                    if sb_enabled:
                        if is_trusted and modinfo_sig_key == current_cert_serial:
                            _ready = True
                    else:
                        _ready = True
                tools_box.set_visible(_ready)

                # install / reinstall button visibility
                install_btn.set_visible(False)
                reinstall_btn.set_visible(False)
                page._suggest_next = True
                status_label.set_label('')

                if not is_installed:
                    if sb_enabled and not is_trusted:
                        install_btn.set_visible(True)
                        install_btn.set_sensitive(False)
                        status_label.set_markup('<span foreground="#ffcc00">' + _('⚠️ Secure Boot is enabled but the custom certificate is not yet enrolled.') + '\n' + _('Please reboot to enroll the certificate before installing this driver.') + '</span>')
                        page._suggest_next = False
                    else:
                        install_btn.set_visible(True)
                        install_btn.set_sensitive(True)
                        page._suggest_next = False
                elif sb_enabled and is_trusted and modinfo_sig_key != current_cert_serial:
                    reinstall_btn.set_visible(True)
                    status_label.set_markup('<span foreground="#ffcc00">' + _('⚠️ The driver signature does not match your system certificate. Click Fix & Reinstall to repair the driver.') + '</span>')
                    page._suggest_next = False

                xb_refresh_btn.set_visible(not _ready)
                button.set_sensitive(True)
                button.set_label(_('  Check Again  '))
            GLib.idle_add(_apply)
        threading.Thread(target=_worker, daemon=True).start()

    xb_refresh_btn.connect('clicked', _on_xb_refresh)
    xb_refresh_btn.set_visible(not is_ready_to_play)
    center.append(xb_refresh_btn)

    return page

def create_exe_sandbox_page(navigate_next):
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)
    page.set_spacing(0)
    center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    center.set_valign(Gtk.Align.CENTER)
    center.set_vexpand(True)
    page.append(center)

    # Windows/Wine icon from Fluent theme
    icon = _create_image('wine.svg')
    icon.set_pixel_size(72)
    icon.set_halign(Gtk.Align.CENTER)
    center.append(icon)

    title = Gtk.Label(label=_('Run Windows Apps, with ease'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_wrap(True)
    title.set_justify(Gtk.Justification.CENTER)
    title.set_margin_top(12)
    center.append(title)

    body = Gtk.Label(
        label=_('Powered by Wine and Bottles technology, running classic Windows applications') + '\n' + _('is now easier than ever.')
    )
    body.set_halign(Gtk.Align.CENTER)
    body.set_wrap(True)
    body.set_justify(Gtk.Justification.CENTER)
    body.set_margin_top(24)
    body.set_margin_bottom(12)
    center.append(body)

    # Status
    status_label = Gtk.Label()
    status_label.add_css_class('dim-label')
    status_label.set_halign(Gtk.Align.CENTER)
    status_label.set_wrap(True)
    status_label.set_margin_bottom(12)
    if is_bottles_installed():
        status_label.set_label(_('✓ Windows compatibility sandbox is already installed.'))
    center.append(status_label)

    progress_bar = Gtk.ProgressBar()
    progress_bar.set_visible(False)
    progress_bar.set_margin_start(48)
    progress_bar.set_margin_end(48)
    center.append(progress_bar)

    # Buttons — REVERSE PSYCHOLOGY:
    # Blue "Skip" = suggested-action (users who mindlessly click Next will skip)
    # Gray "Enable" = plain button (only users who actually want it will click)
    btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
    btn_box.set_halign(Gtk.Align.CENTER)
    btn_box.set_margin_top(24)

    skip_btn = Gtk.Button(label=_('  Skip  '))
    skip_btn.add_css_class('suggested-action')
    skip_btn.connect('clicked', lambda b: navigate_next())

    install_btn = Gtk.Button(label=_('  Enable Windows Compatibility Sandbox  '))

    # Config button (Configure Bottles)
    config_btn = Gtk.Button()
    config_btn.add_css_class('suggested-action')
    config_btn.add_css_class('pill')
    
    config_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
    config_icon = _create_image('com.usebottles.bottles.svg')
    config_icon.set_pixel_size(24)
    config_label = Gtk.Label(label=_('Configure Bottles'))
    config_box.append(config_icon)
    config_box.append(config_label)
    config_btn.set_child(config_box)
    
    config_btn.connect('clicked', lambda b: subprocess.Popen(['flatpak', 'run', 'com.usebottles.bottles']))

    if is_bottles_installed():
        install_btn.set_visible(False)
        skip_btn.set_visible(False)
        config_btn.set_visible(True)
    else:
        config_btn.set_visible(False)

    # Advanced Output Expander
    expander = Gtk.Expander(label=_('Advanced Output'))
    expander.set_margin_top(12)
    expander.set_margin_start(48)
    expander.set_margin_end(48)
    expander.set_halign(Gtk.Align.FILL)
    
    scroll = Gtk.ScrolledWindow()
    scroll.set_min_content_height(150)
    scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
    
    textview = Gtk.TextView()
    textview.set_editable(False)
    textview.set_cursor_visible(False)
    textview.add_css_class('card')
    textview.set_monospace(True)
    textview.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
    buffer = textview.get_buffer()
    
    scroll.set_child(textview)
    expander.set_child(scroll)
    center.append(expander)

    def _on_install(btn):
        btn.set_sensitive(False)
        skip_btn.set_sensitive(False)
        progress_bar.set_visible(True)
        progress_bar.pulse()
        status_label.set_label(
            _('Deploying compatibility sandbox via Flatpak…') + '\n' + _('This may take a few minutes.')
        )

        def _pulse():
            if progress_bar.is_visible():
                progress_bar.pulse()
                return True
            return False

        GLib.timeout_add(100, _pulse)

        def _do_install():
            try:
                proc = subprocess.Popen(
                    ['flatpak', 'install', '--system', '-y', 'flathub', 'com.usebottles.bottles'],
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    text=True,
                    bufsize=1
                )
                
                def _append_text(t):
                    end_iter = buffer.get_end_iter()
                    buffer.insert(end_iter, t)
                    mark = buffer.create_mark(None, buffer.get_end_iter(), False)
                    textview.scroll_to_mark(mark, 0.0, True, 0.0, 1.0)
                    
                for line in iter(proc.stdout.readline, ''):
                    GLib.idle_add(lambda l=line: _append_text(l))
                proc.stdout.close()
                proc.wait()
                
                if proc.returncode == 0:
                    GLib.idle_add(lambda: (
                        status_label.set_label(
                            _('✓ Sandbox deployed successfully!') + '\n' + _('You can now double-click .exe files to run them.')),
                        progress_bar.set_visible(False),
                        btn.set_visible(False),
                        config_btn.set_visible(True),
                        skip_btn.set_visible(False)
                    ))
                else:
                    GLib.idle_add(lambda: (
                        status_label.set_label(
                            _('✗ Deployment failed.') + '\n' + _('You can install Bottles later from the App Store.')),
                        progress_bar.set_visible(False),
                        btn.set_sensitive(True),
                        skip_btn.set_sensitive(True),
                        expander.set_expanded(True)
                    ))
            except Exception as e:
                GLib.idle_add(lambda e=e: (
                    status_label.set_label(_('✗ Error: ') + str(e)),
                    progress_bar.set_visible(False),
                    btn.set_sensitive(True),
                    skip_btn.set_sensitive(True)
                ))

        threading.Thread(target=_do_install, daemon=True).start()

    install_btn.connect('clicked', _on_install)
    btn_box.append(skip_btn)
    btn_box.append(install_btn)
    btn_box.append(config_btn)
    center.append(btn_box)

    return page


# ═══════════════════════════════════════════════════════════════════
# PAGE 6: SHORTCUTS — 指尖魔法
# ═══════════════════════════════════════════════════════════════════

def create_shortcuts_page():
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)
    page.set_spacing(0)

    title = Gtk.Label(label=_('The Magic at Your Fingertips'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_wrap(True)
    title.set_justify(Gtk.Justification.CENTER)
    title.set_margin_bottom(8)
    page.append(title)

    sub = Gtk.Label(label=_('Skip tedious clicking. Capture inspiration with elegance.'))
    sub.add_css_class('dim-label')
    sub.set_halign(Gtk.Align.CENTER)
    sub.set_wrap(True)
    sub.set_justify(Gtk.Justification.CENTER)
    sub.set_margin_bottom(24)
    page.append(sub)

    # Scrollable content area — allows window to shrink while content remains accessible
    scroll = Gtk.ScrolledWindow()
    scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
    scroll.set_vexpand(True)
    scroll.set_propagate_natural_height(True)

    scroll_content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)

    # Featured Shortcuts
    featured_group = Adw.PreferencesGroup()
    featured_group.set_margin_bottom(24)

    # 1. Screenshot
    row1 = Adw.ActionRow()
    row1.set_title(_('Screenshot'))
    row1.set_subtitle(_('Select, annotate, copy — all in one motion'))
    kbd1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
    kbd1.set_valign(Gtk.Align.CENTER)
    for key_text, color in [('Super', '#3584e4'), ('Shift', '#9141ac'), ('S', '#e5a50a')]:
        keycap = Gtk.Label()
        keycap.set_markup(f'<b><span foreground="{color}"> {key_text} </span></b>')
        keycap.add_css_class('heading')
        kbd1.append(keycap)
        plus = Gtk.Label(label='+')
        plus.add_css_class('dim-label')
        kbd1.append(plus)
    last = kbd1.get_last_child()
    if last: kbd1.remove(last)
    row1.add_suffix(kbd1)
    featured_group.add(row1)

    # 2. Screen Recording
    row2 = Adw.ActionRow()
    row2.set_title(_('Screen Recording'))
    row2.set_subtitle(_('Seamless start, instant capture'))
    kbd2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
    kbd2.set_valign(Gtk.Align.CENTER)
    for key_text, color in [('Super', '#3584e4'), ('G', '#33d17a')]:
        keycap = Gtk.Label()
        keycap.set_markup(f'<b><span foreground="{color}"> {key_text} </span></b>')
        keycap.add_css_class('heading')
        kbd2.append(keycap)
        plus = Gtk.Label(label='+')
        plus.add_css_class('dim-label')
        kbd2.append(plus)
    last = kbd2.get_last_child()
    if last: kbd2.remove(last)
    row2.add_suffix(kbd2)
    featured_group.add(row2)

    scroll_content.append(featured_group)

    # More shortcuts in a compact list
    more_group = Adw.PreferencesGroup()
    more_group.set_title(_('More Essential Shortcuts'))

    more_shortcuts = [
        ('Super + E', _('Open File Manager')),
        ('Ctrl + Alt + T', _('Open Terminal')),
        ('Ctrl + Shift + Esc', _('Task Manager')),
        ('Super + V', _('View Clipboard History')),
        ('Super + L', _('Lock Screen')),
        ('Super + A', _('Action Center')),
        ('Super + N', _('Notification Center')),
        ('Super + U', _('Toggle Network Stats Display')),
        ('Super + Tab', _('Switch Between Windows')),
        ('Alt + F4', _('Close Current Window')),
        ('Super + ↑/↓/←/→', _('Snap Windows')),
        ('Ctrl + Super + ←/→', _('Switch Virtual Desktop')),
        ('Super + D', _('Show Desktop')),
        ('Super + I', _('System Settings')),
        ('Super + ;', _('Emoji and Characters')),
        ('Super + M', _('Minimize Window')),
    ]

    for key_text, desc_text in more_shortcuts:
        row = Adw.ActionRow()
        row.set_title(desc_text)
        lbl = Gtk.Label(label=key_text)
        lbl.add_css_class('dim-label')
        row.add_suffix(lbl)
        more_group.add(row)

    scroll_content.append(more_group)

    scroll.set_child(scroll_content)
    page.append(scroll)

    # Tip
    tip = Gtk.Label(
        label=_('You can view and customize all shortcuts anytime in Settings → Keyboard.')
    )
    tip.add_css_class('dim-label')
    tip.add_css_class('caption')
    tip.set_halign(Gtk.Align.CENTER)
    tip.set_wrap(True)
    tip.set_margin_top(12)
    page.append(tip)

    return page


# ═══════════════════════════════════════════════════════════════════
# PAGE 7: FLATHUB MIRROR — 中国源加速
# ═══════════════════════════════════════════════════════════════════

def create_flathub_mirror_page():
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)

    center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    center.set_valign(Gtk.Align.CENTER)
    center.set_vexpand(True)
    page.append(center)

    flag = Gtk.Label(label='<span size="48000">🇨🇳</span>')
    flag.set_use_markup(True)
    flag.set_halign(Gtk.Align.CENTER)
    flag.set_margin_bottom(12)
    center.append(flag)

    title = Gtk.Label(label=_('In China? Use USTC mirror for App Store!'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_margin_bottom(8)
    center.append(title)

    sub = Gtk.Label(label=_('Flathub access can be slow from China. Switch to the USTC mirror for faster downloads.'))
    sub.set_halign(Gtk.Align.CENTER)
    sub.set_wrap(True)
    sub.set_max_width_chars(60)
    sub.set_margin_bottom(24)
    center.append(sub)

    status_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    status_box.set_halign(Gtk.Align.CENTER)
    status_box.set_spacing(8)
    center.append(status_box)

    status_label = Gtk.Label()
    status_label.set_halign(Gtk.Align.CENTER)
    status_label.set_wrap(True)
    status_box.append(status_label)

    btn_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    btn_box.set_halign(Gtk.Align.CENTER)
    btn_box.set_spacing(8)
    btn_box.set_margin_top(16)
    center.append(btn_box)

    spinner = Gtk.Spinner()

    def _run_btn(label, css_class, callback):
        btn = Gtk.Button(label=label)
        if css_class:
            btn.add_css_class(css_class)
        btn.add_css_class('pill')
        btn.set_size_request(320, 48)
        btn.set_halign(Gtk.Align.CENTER)
        btn.connect('clicked', lambda b: callback())
        return btn

    def _update_ustc_state():
        # Clear previous
        while True:
            c = btn_box.get_first_child()
            if c is None:
                break
            btn_box.remove(c)
        while True:
            c = status_box.get_first_child()
            if c is None:
                break
            status_box.remove(c)
        status_box.append(status_label)

        on_china = _is_flathub_china_mirror()

        if on_china:
            status_label.set_label(_('✓ Already using Chinese mirror — downloads are fast.'))
            status_label.add_css_class('success')
            btn_box.append(_run_btn(
                _('Switch Back to International'), None,
                lambda: _switch_back()))
        else:
            status_label.set_label(_('Flathub is using the international server.'))
            status_label.remove_css_class('success')
            btn_box.append(_run_btn(
                _('🚀 Switch to USTC Mirror'), 'suggested-action',
                lambda: _switch_to_ustc()))

    def _switch_to_ustc():
        # Show spinner
        while True:
            c = btn_box.get_first_child()
            if c is None:
                break
            btn_box.remove(c)
        spin_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        spin_box.set_halign(Gtk.Align.CENTER)
        spin_box.append(spinner)
        spin_lbl = Gtk.Label(label=_('Testing USTC mirror...'))
        spin_box.append(spin_lbl)
        btn_box.append(spin_box)
        spinner.start()

        def _worker():
            try:
                # Test connectivity
                test = subprocess.run(
                    ['curl', '-sI', '--connect-timeout', '5', '--max-time', '10',
                     'https://mirrors.ustc.edu.cn/flathub/'],
                    capture_output=True, timeout=15)
                if test.returncode != 0:
                    raise Exception('USTC mirror unreachable')

                # Switch
                subprocess.run(
                    ['sudo', 'flatpak', 'remote-modify', 'flathub',
                     '--url=https://mirrors.ustc.edu.cn/flathub'],
                    capture_output=True, check=True, timeout=30)

                # Refresh app list
                subprocess.run(
                    ['flatpak', 'update', '--appstream'],
                    capture_output=True, timeout=60)
            except Exception as e:
                GLib.idle_add(_on_error, str(e))
            else:
                GLib.idle_add(_update_ustc_state)

        threading.Thread(target=_worker, daemon=True).start()

    def _switch_back():
        while True:
            c = btn_box.get_first_child()
            if c is None:
                break
            btn_box.remove(c)
        spin_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        spin_box.set_halign(Gtk.Align.CENTER)
        spin_box.append(spinner)
        spin_lbl = Gtk.Label(label=_('Switching back...'))
        spin_box.append(spin_lbl)
        btn_box.append(spin_box)
        spinner.start()

        def _worker():
            try:
                subprocess.run(
                    ['sudo', 'flatpak', 'remote-modify', 'flathub',
                     '--url=https://dl.flathub.org/repo'],
                    capture_output=True, check=True, timeout=30)
            except Exception as e:
                GLib.idle_add(_on_error, str(e))
            else:
                GLib.idle_add(_update_ustc_state)

        threading.Thread(target=_worker, daemon=True).start()

    def _on_error(msg):
        spinner.stop()
        while True:
            c = btn_box.get_first_child()
            if c is None:
                break
            btn_box.remove(c)
        status_label.set_label(_('✗ Failed: ') + msg)
        status_label.remove_css_class('success')
        btn_box.append(_run_btn(_('Retry'), 'suggested-action', lambda: _switch_to_ustc()))

    _update_ustc_state()
    return page


# ═══════════════════════════════════════════════════════════════════
# PAGE 8: APPS — 第一天的高效
# ═══════════════════════════════════════════════════════════════════

def create_apps_page():
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)
    page.set_spacing(0)

    center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    center.set_valign(Gtk.Align.CENTER)
    center.set_vexpand(True)
    page.append(center)

    title = Gtk.Label(label=_('Productive From Day One'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_wrap(True)
    title.set_justify(Gtk.Justification.CENTER)
    title.set_margin_bottom(8)
    center.append(title)

    sub = Gtk.Label(
        label=_('Essential productivity and entertainment tools, hand-picked for you.') + '\n' + _('Click any app to open its details in the App Store.')
    )
    sub.add_css_class('dim-label')
    sub.set_halign(Gtk.Align.CENTER)
    sub.set_wrap(True)
    sub.set_justify(Gtk.Justification.CENTER)
    sub.set_margin_bottom(24)
    center.append(sub)

    # Extra (name, app_id, description, icon) to prepend for specific locales.
    REGIONAL_APPS = {
        'zh_CN': [
            (_('WeChat'), 'com.tencent.WeChat',
             _('Most popular messaging app in China'), 'wechat.svg'),
            (_('QQ'), 'com.qq.QQ',
             _('Tencent QQ instant messenger'), 'qq.svg'),
            (_('WPS Office'), 'com.wps.Office',
             _('Best office suite for Chinese documents'), 'wps-office.svg'),
        ],
    }

    apps = [
        (_('Google Chrome'), 'com.google.Chrome',
         _("The world's most popular web browser"), 'chrome.svg'),
        (_('Visual Studio Code'), 'com.visualstudio.code',
         _('Code editing. Redefined.'), 'visualstudiocode.svg'),
        (_('Steam'), 'com.valvesoftware.Steam',
         _('Your portal to the gaming universe'), 'steam.svg'),
        (_('Discord'), 'com.discordapp.Discord',
         _('Chat, voice, and video for communities'), 'discord.svg'),
        (_('Spotify'), 'com.spotify.Client',
         _('Music for everyone'), 'spotify-client.svg'),
        (_('OnlyOffice'), 'org.onlyoffice.desktopeditors',
         _('Best Microsoft Office-compatible suite'), 'asc-de.svg'),
        (_('Thunderbird'), 'org.mozilla.thunderbird',
         _('Secure, fast, and easy email client'), 'thunderbird.svg'),
        (_('Blender'), 'org.blender.Blender',
         _('Professional 3D creation suite'), 'blender.svg'),
        (_('GNOME Boxes'), 'org.gnome.Boxes',
         _('Virtualization made simple'), 'gnome-boxes.svg'),
        (_('VLC'), 'org.videolan.VLC',
         _('The versatile media player that plays everything'), 'vlc.svg'),
        (_('Krita'), 'org.kde.krita',
         _('Professional free and open source painting program'), 'gnome-paint.svg'),
        (_('OBS Studio'), 'com.obsproject.Studio',
         _('Free and open source software for video recording and live streaming'), 'obs.svg'),
    ]

    # Prepend regional apps for the current locale.
    lang = os.environ.get('LANGUAGE', os.environ.get('LANG', ''))
    for locale_prefix, extras in REGIONAL_APPS.items():
        if lang.startswith(locale_prefix):
            apps = extras + apps
            break

    installed_apps = set()
    try:
        proc = subprocess.run(['flatpak', 'list', '--app', '--columns=application'], capture_output=True, text=True)
        if proc.returncode == 0:
            installed_apps = set(proc.stdout.splitlines())
    except Exception:
        pass

    # App grid
    flow = Gtk.FlowBox()
    flow.set_selection_mode(Gtk.SelectionMode.NONE)
    flow.set_max_children_per_line(3)
    flow.set_row_spacing(12)
    flow.set_column_spacing(12)
    flow.set_margin_top(12)
    flow.set_margin_bottom(12)
    flow.set_margin_start(12)
    flow.set_margin_end(12)
    flow.set_homogeneous(True)

    for name, app_id, desc_text, icon_name, *rest in apps:
        card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        card.set_margin_start(10)
        card.set_margin_end(10)
        card.set_margin_top(14)
        card.set_margin_bottom(14)
        card.set_size_request(180, 130)

        # App icon from Fluent theme
        app_icon = _create_image(icon_name)
        app_icon.set_pixel_size(48)
        app_icon.set_halign(Gtk.Align.CENTER)
        card.append(app_icon)

        name_label = Gtk.Label(label=name)
        name_label.add_css_class('heading')
        name_label.set_halign(Gtk.Align.CENTER)
        name_label.set_wrap(True)
        name_label.set_justify(Gtk.Justification.CENTER)
        card.append(name_label)

        desc_label = Gtk.Label(label=desc_text)
        desc_label.add_css_class('dim-label')
        desc_label.add_css_class('caption')
        desc_label.set_halign(Gtk.Align.CENTER)
        desc_label.set_wrap(True)
        desc_label.set_justify(Gtk.Justification.CENTER)
        desc_label.set_max_width_chars(20)
        card.append(desc_label)

        # Install status
        if app_id in installed_apps:
            status = Gtk.Label(label='✓ ' + _('Installed'))
        else:
            status = Gtk.Label(label=_('Click to open in App Store'))
        status.add_css_class('caption-heading')
        status.set_halign(Gtk.Align.CENTER)
        card.append(status)

        click = Gtk.GestureClick()
        click.connect('released',
                      lambda g, n, x, y, fid=app_id, s=status:
                      (_open_software_center(fid), s.set_label('✓ ' + _('Opening…'))))
        card.add_controller(click)

        child = Gtk.FlowBoxChild()
        child.add_css_class('card')
        child.set_child(card)
        flow.append(child)

    scroll = Gtk.ScrolledWindow()
    scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
    scroll.set_min_content_height(440)
    scroll.set_max_content_height(440)
    scroll.set_propagate_natural_height(True)
    scroll.set_child(flow)
    center.append(scroll)

    # Queue hint
    queue_hint = Gtk.Label(
        label=_('Pro tip: You can explore these apps in the App Store,') + '\n' + _('and continue with the system setup while they download.')
    )
    queue_hint.add_css_class('dim-label')
    queue_hint.add_css_class('caption')
    queue_hint.set_halign(Gtk.Align.CENTER)
    queue_hint.set_wrap(True)
    queue_hint.set_margin_top(12)
    center.append(queue_hint)

    # Browse Store button
    store_btn = Gtk.Button(label=_('  Browse Store  '))
    store_btn.set_halign(Gtk.Align.CENTER)
    store_btn.set_margin_top(18)
    store_btn.connect('clicked',
        lambda b: subprocess.Popen(['gnome-software']))
    center.append(store_btn)

    return page


def _open_software_center(app_id):
    try:
        subprocess.Popen(['gnome-software', f'--details={app_id}'])
    except Exception as e:
        print(f"Failed to open software center: {e}")


# ═══════════════════════════════════════════════════════════════════
# PAGE 7.5: SYNC — Connect & Protect Your Data
# ═══════════════════════════════════════════════════════════════════

def create_sync_page():
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)
    page.set_spacing(0)

    title = Gtk.Label(label=_('Connect & Protect Your Data'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_wrap(True)
    title.set_justify(Gtk.Justification.CENTER)
    title.set_margin_bottom(24)
    page.append(title)

    # Horizontal split
    hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=24)
    hbox.set_homogeneous(True)
    hbox.set_vexpand(True)
    hbox.set_valign(Gtk.Align.CENTER)

    # ── Left: Online Accounts ──
    cloud_card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
    cloud_card.add_css_class('card')

    cloud_icon = _create_image('online-account.svg')
    cloud_icon.set_pixel_size(48)
    cloud_icon.set_halign(Gtk.Align.CENTER)
    cloud_icon.set_margin_top(16)
    cloud_card.append(cloud_icon)

    cloud_title = Gtk.Label(label=_('Online Accounts'))
    cloud_title.add_css_class('title-3')
    cloud_title.set_halign(Gtk.Align.CENTER)
    cloud_card.append(cloud_title)

    cloud_desc = Gtk.Label(
        label=_('Connect your Google, Microsoft,') + '\n' + _('or Nextcloud accounts to sync') + '\n' + \
                _('files, contacts, and calendar.')
    )
    cloud_desc.set_halign(Gtk.Align.CENTER)
    cloud_desc.set_wrap(True)
    cloud_desc.set_justify(Gtk.Justification.CENTER)
    cloud_desc.set_margin_start(12)
    cloud_desc.set_margin_end(12)
    cloud_desc.set_margin_bottom(12)
    cloud_card.append(cloud_desc)

    cloud_btn = Gtk.Button(label=_('Connect Accounts'))
    cloud_btn.set_halign(Gtk.Align.CENTER)
    cloud_btn.set_margin_bottom(16)
    cloud_btn.connect('clicked', lambda b: subprocess.Popen(['gnome-control-center', 'online-accounts']))
    cloud_card.append(cloud_btn)

    hbox.append(cloud_card)

    # ── Right: Local Backups ──
    backup_card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
    backup_card.add_css_class('card')

    backup_icon = _create_image('deja-dup.svg')
    backup_icon.set_pixel_size(48)
    backup_icon.set_halign(Gtk.Align.CENTER)
    backup_icon.set_margin_top(16)
    backup_card.append(backup_icon)

    backup_title = Gtk.Label(label=_('System Backup'))
    backup_title.add_css_class('title-3')
    backup_title.set_halign(Gtk.Align.CENTER)
    backup_card.append(backup_title)

    backup_desc = Gtk.Label(
        label=_('Use Deja Dup to protect your') + '\n' + _('important files with automated,') + '\n' + \
                _('secure local or remote backups.')
    )
    backup_desc.set_halign(Gtk.Align.CENTER)
    backup_desc.set_wrap(True)
    backup_desc.set_justify(Gtk.Justification.CENTER)
    backup_desc.set_margin_start(12)
    backup_desc.set_margin_end(12)
    backup_desc.set_margin_bottom(12)
    backup_card.append(backup_desc)

    backup_btn = Gtk.Button(label=_('Get Deja Dup'))
    backup_btn.set_halign(Gtk.Align.CENTER)
    backup_btn.set_margin_bottom(16)
    backup_btn.connect('clicked', lambda b: subprocess.Popen(['gnome-software', '--details=org.gnome.DejaDup.desktop']))
    backup_card.append(backup_btn)

    hbox.append(backup_card)
    page.append(hbox)

    return page


# ═══════════════════════════════════════════════════════════════════
# PAGE 8: PRIVACY — 绝不妥协的承诺
# ═══════════════════════════════════════════════════════════════════

def create_privacy_page():
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(60)
    page.set_margin_end(60)
    page.set_margin_top(36)
    page.set_margin_bottom(36)
    page.set_spacing(0)

    # Shield icon
    icon = _create_image('privacy.svg')
    icon.set_pixel_size(72)
    icon.set_halign(Gtk.Align.CENTER)
    icon.set_margin_bottom(24)
    page.append(icon)

    # Bold manifesto — typography-driven
    headline = Gtk.Label(label=_('Your Data. Your Rules. Period.'))
    headline.add_css_class('title-1')
    headline.set_halign(Gtk.Align.CENTER)
    headline.set_wrap(True)
    headline.set_justify(Gtk.Justification.CENTER)
    headline.set_margin_bottom(24)
    page.append(headline)

    body = Gtk.Label(
        label=_('AnduinOS disables all system-level telemetry by default.') + '\n' + _('We do not collect your usage habits.') + '\n' + \
                _('We do not upload your search history.') + '\n' + _('There are no pervasive advertisements.')
    )
    body.add_css_class('title-4')
    body.set_halign(Gtk.Align.CENTER)
    body.set_wrap(True)
    body.set_justify(Gtk.Justification.CENTER)
    body.set_margin_bottom(36)
    page.append(body)

    # Signature
    sig = Gtk.Label(
        label=_('In an era ruled by algorithms and surveillance,') + '\n' + _('this is our firmest commitment to you.')
    )
    sig.add_css_class('body')
    sig.set_halign(Gtk.Align.CENTER)
    sig.set_wrap(True)
    sig.set_justify(Gtk.Justification.CENTER)
    sig.set_margin_bottom(24)
    page.append(sig)

    # Divider
    divider = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
    divider.set_margin_start(48)
    divider.set_margin_end(48)
    divider.set_margin_bottom(24)
    page.append(divider)

    # Pledge items
    pledges = [
        _('✓ Zero telemetry — your activity is yours alone'),
        _('✓ No data collection — your files stay on your device'),
        _('✓ No ads in your operating system'),
        _('✓ Full root access — you truly own your machine'),
        _('✓ UFW firewall — your digital perimeter, defended'),
        _('✓ Open source — the code is yours to inspect and modify'),
    ]

    for pledge in pledges:
        row = Gtk.Label(label=pledge)
        row.set_halign(Gtk.Align.START)
        row.set_margin_start(24)
        row.set_margin_top(4)
        row.set_margin_bottom(4)
        page.append(row)

    return page




# ═══════════════════════════════════════════════════════════════════
# PAGE 10: SUPPORT — 你不是孤岛
# ═══════════════════════════════════════════════════════════════════

def create_support_page():
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)
    page.set_spacing(0)

    center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    center.set_valign(Gtk.Align.CENTER)
    center.set_vexpand(True)
    page.append(center)

    icon = _create_image('anduinos-oobe.svg')
    icon.set_pixel_size(72)
    icon.set_halign(Gtk.Align.CENTER)
    icon.set_margin_bottom(12)
    center.append(icon)

    title = Gtk.Label(label=_('Welcome to the AnduinOS Community'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_wrap(True)
    title.set_justify(Gtk.Justification.CENTER)
    title.set_margin_bottom(12)
    center.append(title)

    body = Gtk.Label(
        label=_("Whether you're a Windows refugee seeking shelter,") + '\n' + \
                _("or a hardcore geek developer — you'll find your tribe here.") + '\n' + \
                _('Found a bug? Want a feature? Tell us anytime.')
    )
    body.set_halign(Gtk.Align.CENTER)
    body.set_wrap(True)
    body.set_justify(Gtk.Justification.CENTER)
    body.set_margin_bottom(36)
    center.append(body)

    # Link cards
    links_group = Adw.PreferencesGroup()

    links = [
        (_('Official Documentation'), 'https://docs.anduinos.com/',
         _('Comprehensive guides and troubleshooting — covers everything you need'),
         'open-book-symbolic.svg'),
        (_('Community Forums'), 'https://github.com/Anduin2017/AnduinOS/discussions',
         _('Discuss with other AnduinOS users and developers'),
         'system-users-symbolic.svg'),
        (_('Discord Server'), 'https://discord.gg/YGBzvmyBrR',
         _('Join our official Discord server for real-time chat and support'),
         'discord.svg'),
        (_('Report an Issue'), 'https://github.com/AiursoftWeb/AnduinOS-Packages/issues',
         _('Found a bug or have a feature request? Let us know!'),
         'bug-symbolic.svg'),
        (_('Buy AnduinOS Enterprise Edition'), 'https://www.aiursoft.com/anduinos',
         _('Get enterprise-grade support and advanced features for your organization'),
         'anduinos-oobe.svg'),
        (_('Ask Ubuntu'), 'https://askubuntu.com/',
         _('Almost all Ubuntu solutions apply directly to AnduinOS'),
         'help-about-symbolic.svg'),
        (_('Source Code'), 'https://github.com/aiursoftweb/anduinos-2',
         _('AnduinOS is open-source. Explore the code and contribute!'),
         'github.svg'),
    ]

    for name, url, desc_text, link_icon in links:
        row = Adw.ActionRow()
        row.set_title(name)
        row.set_subtitle(desc_text)
        icon = _create_image(link_icon)
        icon.set_pixel_size(24)
        row.add_prefix(icon)

        open_btn = Gtk.Button(label=_('Open'))
        open_btn.set_valign(Gtk.Align.CENTER)
        open_btn.connect('clicked', lambda b, u=url: subprocess.Popen(['xdg-open', u]))
        row.add_suffix(open_btn)
        links_group.add(row)

    center.append(links_group)

    # Footer note
    note = Gtk.Label(
        label=_("💡 AnduinOS is compatible with Ubuntu's entire ecosystem.") + '\n' + \
                _("You can rely on Ubuntu's vast documentation and expertise.")
    )
    note.add_css_class('dim-label')
    note.set_halign(Gtk.Align.CENTER)
    note.set_wrap(True)
    note.set_justify(Gtk.Justification.CENTER)
    note.set_margin_top(18)
    center.append(note)

    return page


# ═══════════════════════════════════════════════════════════════════
# PAGE 11: FINISH — 旅程开始
# ═══════════════════════════════════════════════════════════════════

def create_finish_page(on_finish):
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_valign(Gtk.Align.FILL)
    page.set_halign(Gtk.Align.FILL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(36)
    page.set_margin_bottom(36)
    page.set_spacing(0)

    # Big green checkmark
    check = Gtk.Label()
    check.set_markup('<span foreground="#33d17a" font="96"><b>✓</b></span>')
    check.set_halign(Gtk.Align.CENTER)
    check.set_margin_bottom(12)
    page.append(check)

    title = Gtk.Label(label=_('All Set.'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_wrap(True)
    title.set_justify(Gtk.Justification.CENTER)
    title.set_margin_bottom(48)
    page.append(title)

    # Start button
    start_btn = Gtk.Button(label=_('  Start Your AnduinOS Journey  '))
    start_btn.add_css_class('suggested-action')
    start_btn.add_css_class('pill')
    start_btn.set_halign(Gtk.Align.CENTER)
    start_btn.set_size_request(280, 48)

    def _on_start(btn):
        # Fade out animation
        window = btn.get_root()
        if window and hasattr(window, 'oobe_finish'):
            window.oobe_finish()

    start_btn.connect('clicked', lambda b: _on_start(b))
    page.append(start_btn)

    return page


# ═══════════════════════════════════════════════════════════════════
# MAIN WINDOW
# ═══════════════════════════════════════════════════════════════════

class OobeWindow(Adw.ApplicationWindow):
    def __init__(self, app, is_oobe):
        super().__init__(application=app)
        self.app = app
        self.is_oobe = is_oobe

        if is_oobe:
            self.set_title(_('AnduinOS Setup'))
            self.set_default_size(780, 910)
        else:
            self.set_title(_('AnduinOS Welcome Center'))
            self.set_default_size(780, 910)

        self.set_icon_name('anduinos-oobe')

        # ── ToolbarView ──
        toolbar = Adw.ToolbarView()
        self.set_content(toolbar)

        # Header
        header = Adw.HeaderBar()
        if not is_oobe:
            header.set_show_title(True)
        toolbar.add_top_bar(header)

        # OOBE wizard: carousel with dots and nav
        self.carousel = Adw.Carousel()
        self.carousel.set_interactive(True)
        self.carousel.set_allow_long_swipes(True)
        self.carousel.set_allow_mouse_drag(True)
        self.carousel.set_allow_scroll_wheel(False)

        nav = self  # navigation context
        nav._pages = []

        def _nav_next():
            idx = int(self.carousel.get_position())
            if idx < self.carousel.get_n_pages() - 1:
                self.carousel.scroll_to(
                    self.carousel.get_nth_page(idx + 1), True
                )
            else:
                self._finish_oobe()

        for factory in self._get_page_factories(navigate_next=_nav_next):
            p = factory()
            scroll = Gtk.ScrolledWindow()
            scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
            scroll.set_child(p)
            clamp = Adw.Clamp()
            clamp.set_maximum_size(650)
            clamp.set_child(scroll)
            nav._pages.append(clamp)
            self.carousel.append(clamp)

        # Wrap carousel in a clamp so adjacent pages don't peek
        # through on ultrawide screens
        carousel_clamp = Adw.Clamp()
        carousel_clamp.set_maximum_size(680)
        carousel_clamp.set_child(self.carousel)
        toolbar.set_content(carousel_clamp)

        # ── Bottom bar ──
        bottom = Gtk.CenterBox()
        bottom.set_margin_start(36)
        bottom.set_margin_end(36)
        bottom.set_margin_top(12)
        bottom.set_margin_bottom(24)

        self.back_btn = Gtk.Button(label=_('← Back'))
        self.back_btn.set_visible(False)
        self.back_btn.connect('clicked', lambda b: self._nav_back())
        bottom.set_start_widget(self.back_btn)

        self.dots = Adw.CarouselIndicatorDots()
        self.dots.set_carousel(self.carousel)
        bottom.set_center_widget(self.dots)

        self.next_btn = Gtk.Button(label=_('Next →'))
        self.next_btn.add_css_class('suggested-action')
        self.next_btn.connect('clicked', lambda b: _nav_next())
        bottom.set_end_widget(self.next_btn)

        self.carousel.connect('page-changed', self._on_page_changed)
        toolbar.add_bottom_bar(bottom)

        self._update_nav_buttons()

    def _get_page_factories(self, navigate_next=None):
        """Return list of page factory functions in order.
        If navigate_next is None, pages are for Welcome Center (no nav)."""

        def _nav():
            if navigate_next:
                navigate_next()

        def _update_nav():
            self._update_nav_buttons()

        factories = [
            lambda: create_welcome_page(_nav if self.is_oobe else None),
            lambda: create_appearance_page(),
            lambda: create_security_page(),
            lambda: create_update_page(_nav, _update_nav),
        ]

        try:
            r = subprocess.run(['mokutil', '--sb-state'], capture_output=True, text=True, timeout=5)
            if 'SecureBoot enabled' in r.stdout:
                factories.append(lambda: create_secureboot_page(_nav, _update_nav))
        except Exception:
            pass

        if has_nvidia_gpu():
            factories.append(lambda: create_nvidia_page(_nav, _update_nav))
        if not is_virtual_machine():
            factories.append(lambda: create_xbox_page(_nav, _update_nav))
        # Bottles (Wine) is x86-only — no Flatpak on aarch64 and emulation
        # of x86 Windows apps on ARM is not "with ease".
        if not is_arm64():
            factories.append(lambda: create_exe_sandbox_page(_nav))
        if is_chinese_locale():
            factories.append(lambda: create_flathub_mirror_page())

        factories.extend([
            lambda: create_shortcuts_page(),
            lambda: create_apps_page(),
            lambda: create_sync_page(),
            lambda: create_privacy_page(),

            lambda: create_support_page(),
            lambda: create_finish_page(lambda: self._finish_oobe()),
        ])

        return factories

    def _on_page_changed(self, carousel, index):
        self._update_nav_buttons()

    def _update_nav_buttons(self):
        idx = int(self.carousel.get_position())
        n_pages = self.carousel.get_n_pages()

        clamp = self.carousel.get_nth_page(idx)
        page = clamp.get_child() if clamp else None

        hide_next = getattr(page, '_hide_next', False) if page else False

        self.back_btn.set_visible(idx > 0)
        if idx == 0 and self.is_oobe:
            self.next_btn.set_visible(False)
        else:
            self.next_btn.set_visible(not hide_next)
        
        if idx == 1 and self.is_oobe:
            # Mark OOBE as seen once user reaches the Appearance page.
            # No need to nag them again even if they close the window here.
            os.makedirs(os.path.dirname(MARKER_FILE), exist_ok=True)
            open(MARKER_FILE, 'a').close()

        if idx >= n_pages - 1:
            self.next_btn.set_label(_('Finish'))
        else:
            self.next_btn.set_label(_('Next →'))

        if page:
            if getattr(page, '_suggest_next', True):
                self.next_btn.add_css_class('suggested-action')
            else:
                self.next_btn.remove_css_class('suggested-action')

    def _nav_back(self):
        idx = int(self.carousel.get_position())
        if idx > 0:
            self.carousel.scroll_to(self.carousel.get_nth_page(idx - 1), True)

    def _finish_oobe(self):
        """Mark OOBE done and close."""
        os.makedirs(os.path.dirname(MARKER_FILE), exist_ok=True)
        open(MARKER_FILE, 'a').close()
        self.destroy()

    def oobe_finish(self):
        self._finish_oobe()


# ═══════════════════════════════════════════════════════════════════
# APPLICATION
# ═══════════════════════════════════════════════════════════════════

class OobeApp(Adw.Application):
    def __init__(self):
        super().__init__(application_id='com.anduinos.Oobe')
        self.is_oobe = False

    def do_startup(self):
        Adw.Application.do_startup(self)

        about_action = Gio.SimpleAction.new('about', None)
        about_action.connect('activate', self._on_about)
        self.add_action(about_action)

    def _on_about(self, action, param):
        dialog = Adw.AboutDialog()
        dialog.set_application_name(_('AnduinOS Welcome'))
        dialog.set_application_icon('anduinos-oobe')
        dialog.set_developer_name(_('AnduinOS Team'))
        dialog.set_website('https://www.anduinos.com')
        dialog.set_issue_url('https://github.com/AiursoftWeb/AnduinOS-Packages/issues')
        dialog.set_license_type(Gtk.License.GPL_3_0)
        dialog.set_version('1.0.0')
        dialog.present(self.get_active_window())

    def do_activate(self):
        if hasattr(self, '_win'):
            self._win.present()
            return
        self._win = OobeWindow(self, self.is_oobe)
        self._win.connect('destroy', lambda w: self.quit())
        self._win.present()


def main():
    is_oobe = '--oobe' in sys.argv

    if is_oobe:
        # Do not run on Live CD / Installation Media.
        # All four conditions must be satisfied to consider the system as live:
        # - casper package installed
        # - ubiquity package installed
        # - /cdrom directory exists
        # - kernel boot parameter includes boot=casper
        if is_live_environment():
            sys.exit(0)

        # Do not run if already completed
        if os.path.exists(MARKER_FILE):
            sys.exit(0)

    # Remove --oobe from argv before passing to GTK
    clean_argv = [a for a in sys.argv if a != '--oobe']
    sys.argv = clean_argv

    app = OobeApp()
    app.is_oobe = is_oobe
    return app.run(None)


if __name__ == '__main__':
    raise SystemExit(main())
