#!/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 urllib.request
import stat
import re
import json
import gettext
import warnings

# The installed anduinos-appearance package owns all taskbar layout behavior.
# Keep a source-tree fallback for local development and repository tests.
try:
    from anduinos_appearance import (
        apply_style_and_position,
        detect_current,
        draw_preview,
    )
except ModuleNotFoundError:
    _appearance_src = os.path.abspath(os.path.join(
        os.path.dirname(__file__), '..', '..',
        'anduinos-appearance', 'src'))
    if _appearance_src not in sys.path:
        sys.path.insert(0, _appearance_src)
    from anduinos_appearance import (
        apply_style_and_position,
        detect_current,
        draw_preview,
    )

# Scaling backend ships beside the installed helpers, with an assets fallback.
_scaling_lib = os.path.dirname(os.path.abspath(__file__))
if not os.path.isfile(os.path.join(_scaling_lib, 'display_scaling.py')):
    _scaling_lib = '/usr/lib/anduinos-oobe'
sys.path.insert(0, _scaling_lib)
import display_scaling as _display_scaling
import agents as _agents

OOBE_LAYOUT_STYLES = {
    'modern': 'eleven',
    'classic': 'classic',
}

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

# ── i18n ───────────────────────────────────────────────────────────
_LOCALE_DIR = '/usr/share/locale'
_source_locale_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'locale'))
if os.path.isdir(_source_locale_dir):
    _LOCALE_DIR = _source_locale_dir
try:
    _ = gettext.translation('anduinos-oobe', _LOCALE_DIR, fallback=True).gettext
except Exception:
    _ = lambda s: s

MARKER_FILE = os.path.expanduser("~/.config/anduinos-oobe-done")
FLATHUB_REMOTE = 'flathub'
FLATHUB_OFFICIAL_REPO = 'https://dl.flathub.org/repo/flathub.flatpakrepo'
FLATHUB_OFFICIAL_URL = 'https://dl.flathub.org/repo'
FLATHUB_USTC_URL = 'https://mirrors.ustc.edu.cn/flathub'
DRIVER_CENTER_EXECUTABLE = '/usr/bin/anduinos-driver-center'
# ── 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 is_arm64():
    """True on aarch64 platforms (Snapdragon, Apple Silicon, etc.)."""
    return os.uname().machine == 'aarch64'


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.replace('-', '_').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',
                            '--columns=name,url'],
                           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_REMOTE:
                return cols[1]
    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_live_environment():
    """Return True only for the Dracut-established AnduinOS Live contract."""
    return (
        os.path.isfile('/run/anduinos-live/environment')
        and os.path.isdir('/cdrom')
        and os.path.isfile('/run/anduinos-live/rootfs.squashfs')
    )


def internet_connection_ready(monitor=None):
    """Return True only for complete Internet access without a captive portal."""
    try:
        monitor = monitor or Gio.NetworkMonitor.get_default()
        return monitor.get_connectivity() == Gio.NetworkConnectivity.FULL
    except Exception:
        # Detection failures must keep the network page available.
        return False


def _split_nmcli_terse(line):
    """Split an escaped nmcli terse record without losing literal colons."""
    fields = []
    current = []
    escaped = False
    for character in line.rstrip('\n'):
        if escaped:
            current.append(character)
            escaped = False
        elif character == '\\':
            escaped = True
        elif character == ':':
            fields.append(''.join(current))
            current = []
        else:
            current.append(character)
    if escaped:
        current.append('\\')
    fields.append(''.join(current))
    return fields


def _scan_wifi_networks():
    """Return visible NetworkManager Wi-Fi networks, grouped by SSID."""
    result = subprocess.run(
        ['nmcli', '--terse', '--escape', 'yes', '--fields',
         'IN-USE,SSID,SIGNAL,SECURITY', 'device', 'wifi', 'list',
         '--rescan', 'yes'],
        capture_output=True, text=True, timeout=20)
    if result.returncode != 0:
        detail = (result.stderr or result.stdout).strip()
        raise RuntimeError(detail or _('NetworkManager could not scan Wi-Fi.'))

    networks = {}
    for line in result.stdout.splitlines():
        fields = _split_nmcli_terse(line)
        if len(fields) != 4:
            continue
        active, ssid, signal_text, security = fields
        ssid = ssid.strip()
        if not ssid:
            continue
        try:
            signal = max(0, min(100, int(signal_text)))
        except ValueError:
            signal = 0
        candidate = {
            'ssid': ssid,
            'signal': signal,
            'security': security.strip() or '--',
            'active': active.strip() == '*',
        }
        previous = networks.get(ssid)
        if previous is None or (
            candidate['active'], candidate['signal']
        ) > (previous['active'], previous['signal']):
            networks[ssid] = candidate

    return sorted(
        networks.values(),
        key=lambda network: (
            not network['active'],
            -network['signal'],
            network['ssid'].casefold(),
        ))


def _parse_network_devices(output):
    """Group NetworkManager devices by hardware type."""
    devices = {'wifi': [], 'ethernet': []}
    for line in output.splitlines():
        fields = _split_nmcli_terse(line)
        if len(fields) != 3:
            continue
        name, device_type, state = (field.strip() for field in fields)
        if name and device_type in devices:
            devices[device_type].append({
                'name': name,
                'state': state.lower(),
            })
    return devices


def _get_network_devices():
    """Return physical Wi-Fi and Ethernet devices known to NetworkManager."""
    command_env = os.environ.copy()
    command_env['LC_ALL'] = 'C'
    result = subprocess.run(
        ['nmcli', '--terse', '--escape', 'yes', '--fields',
         'DEVICE,TYPE,STATE', 'device', 'status'],
        capture_output=True, text=True, timeout=10, env=command_env)
    if result.returncode != 0:
        detail = (result.stderr or result.stdout).strip()
        raise RuntimeError(
            detail or _('Could not inspect network adapters.'))
    return _parse_network_devices(result.stdout)


def _wifi_radio_enabled():
    result = subprocess.run(
        ['nmcli', 'radio', 'wifi'],
        capture_output=True, text=True, timeout=10)
    if result.returncode != 0:
        detail = (result.stderr or result.stdout).strip()
        raise RuntimeError(detail or _('Could not read the Wi-Fi radio state.'))
    state = result.stdout.strip().lower()
    if state == 'enabled':
        return True
    if state == 'disabled':
        return False
    raise RuntimeError(_('Unknown Wi-Fi radio state: {}').format(
        state or _('empty')))


def _set_wifi_radio(enabled):
    result = subprocess.run(
        ['nmcli', 'radio', 'wifi', 'on' if enabled else 'off'],
        capture_output=True, text=True, timeout=20)
    if result.returncode != 0:
        detail = (result.stderr or result.stdout).strip()
        raise RuntimeError(detail or _('Could not change the Wi-Fi radio state.'))


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)


# ═══════════════════════════════════════════════════════════════════
# 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


# ═══════════════════════════════════════════════════════════════════
# NETWORK — shown immediately after Welcome when startup is offline
# ═══════════════════════════════════════════════════════════════════

def create_network_page(continue_offline, update_nav=None):
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)
    page._hide_next = True
    page._suggest_next = False

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

    icon = Gtk.Image.new_from_icon_name('network-wireless-symbolic')
    icon.set_pixel_size(72)
    icon.set_halign(Gtk.Align.CENTER)
    center.append(icon)

    title = Gtk.Label(label=_('Connect to the Internet'))
    title.add_css_class('title-1')
    title.set_halign(Gtk.Align.CENTER)
    title.set_wrap(True)
    title.set_justify(Gtk.Justification.CENTER)
    center.append(title)

    subtitle = Gtk.Label(
        label=_('Connect now so AnduinOS can safely check for updates, drivers, and apps.'))
    subtitle.add_css_class('dim-label')
    subtitle.set_halign(Gtk.Align.CENTER)
    subtitle.set_wrap(True)
    subtitle.set_justify(Gtk.Justification.CENTER)
    center.append(subtitle)

    status_group = Adw.PreferencesGroup()
    status_row = Adw.ActionRow(title=_('Internet Connection'))
    status_icon = Gtk.Image(pixel_size=24)
    status_row.add_prefix(status_icon)
    status_group.add(status_row)
    center.append(status_group)

    hardware_group = Adw.PreferencesGroup()
    hardware_group.set_title(_('Network Hardware'))
    hardware_row = Adw.ActionRow()
    hardware_icon = Gtk.Image(pixel_size=24)
    hardware_row.add_prefix(hardware_icon)
    hardware_group.add(hardware_row)
    center.append(hardware_group)

    networks_group = Adw.PreferencesGroup()
    networks_group.set_title(_('Available Wi-Fi Networks'))

    controls_row = Adw.ActionRow(title=_('Wi-Fi'))
    refresh_btn = Gtk.Button(icon_name='view-refresh-symbolic')
    refresh_btn.set_valign(Gtk.Align.CENTER)
    refresh_btn.set_tooltip_text(_('Scan Again'))
    wifi_switch = Gtk.Switch(valign=Gtk.Align.CENTER, sensitive=False)
    controls_row.add_suffix(refresh_btn)
    controls_row.add_suffix(wifi_switch)
    networks_group.add(controls_row)

    wifi_rows = []
    center.append(networks_group)

    settings_btn = Gtk.Button(label=_('Open Network Settings'))
    settings_btn.set_halign(Gtk.Align.CENTER)
    settings_state = {'panel': 'network'}

    def _open_settings(panel='network'):
        try:
            subprocess.Popen(['gnome-control-center', panel])
        except OSError as error:
            status_row.set_subtitle(
                _('Could not open network settings: {}').format(error))

    def _open_wifi_settings(*_args):
        _open_settings('wifi')

    settings_btn.connect(
        'clicked',
        lambda _button: _open_settings(settings_state['panel']))
    center.append(settings_btn)

    offline_btn = Gtk.Button(label=_('Continue Offline'))
    offline_btn.set_halign(Gtk.Align.CENTER)
    offline_btn.add_css_class('flat')
    offline_btn.set_tooltip_text(
        _('Skip updates, online drivers, apps, and account setup.'))
    offline_btn.connect('clicked', lambda _button: continue_offline())
    center.append(offline_btn)

    monitor = Gio.NetworkMonitor.get_default()

    def _render_connectivity():
        online = internet_connection_ready(monitor)
        page._hide_next = not online
        page._suggest_next = online
        page._block_carousel = not online
        if online:
            status_icon.set_from_icon_name('network-transmit-receive-symbolic')
            status_icon.remove_css_class('warning')
            status_icon.add_css_class('success')
            status_row.set_subtitle(
                _('Internet connection is ready. Choose Next to continue.'))
            offline_btn.set_visible(False)
        else:
            status_icon.set_from_icon_name('network-offline-symbolic')
            status_icon.remove_css_class('success')
            status_icon.add_css_class('warning')
            status_row.set_subtitle(
                _('Connect to Wi-Fi or a wired network to continue.'))
            offline_btn.set_visible(True)
        if update_nav:
            update_nav()

    def _clear_wifi_rows():
        for row in wifi_rows:
            networks_group.remove(row)
        wifi_rows.clear()

    def _replace_wifi_rows(devices, enabled, found):
        wifi_devices = devices['wifi']
        ethernet_devices = devices['ethernet']
        has_wifi = bool(wifi_devices)
        has_ethernet = bool(ethernet_devices)
        ethernet_connected = any(
            device['state'] in ('connected', 'connecting')
            for device in ethernet_devices
        )

        networks_group.set_visible(has_wifi)
        if not has_wifi and not has_ethernet:
            hardware_icon.set_from_icon_name('network-offline-symbolic')
            hardware_row.set_title(_('No network adapters detected.'))
            hardware_row.set_subtitle(
                _('Connect a network adapter or continue offline.'))
            settings_btn.set_label(_('Open Network Settings'))
            settings_state['panel'] = 'network'
        elif has_ethernet and not has_wifi:
            hardware_icon.set_from_icon_name('network-wired-symbolic')
            hardware_row.set_title(_('Wired Network'))
            hardware_row.set_subtitle(
                _('Cable connected.') if ethernet_connected
                else _('Plug in a network cable to connect.'))
            settings_btn.set_label(_('Open Network Settings'))
            settings_state['panel'] = 'network'
        elif has_wifi and not has_ethernet:
            hardware_icon.set_from_icon_name('network-wireless-symbolic')
            hardware_row.set_title(_('Wireless Network'))
            hardware_row.set_subtitle(_('Wi-Fi adapter detected.'))
            settings_btn.set_label(_('Open Wi-Fi Settings'))
            settings_state['panel'] = 'wifi'
        else:
            hardware_icon.set_from_icon_name('network-wired-symbolic')
            hardware_row.set_title(_('Wired and Wireless Networks'))
            hardware_row.set_subtitle(
                _('Cable connected and Wi-Fi available.')
                if ethernet_connected else
                _('Wi-Fi is available; the network cable is disconnected.'))
            settings_btn.set_label(_('Open Network Settings'))
            settings_state['panel'] = 'network'

        wifi_switch.handler_block(radio_handler)
        wifi_switch.set_active(enabled)
        wifi_switch.set_state(enabled)
        wifi_switch.handler_unblock(radio_handler)
        wifi_switch.set_sensitive(True)
        _clear_wifi_rows()

        if not enabled:
            row = Adw.ActionRow(title=_('Wi-Fi is turned off.'))
            networks_group.add(row)
            wifi_rows.append(row)
        elif not found:
            row = Adw.ActionRow(title=_('No Wi-Fi networks found.'))
            networks_group.add(row)
            wifi_rows.append(row)
        else:
            for network in found:
                detail = '{}%'.format(network['signal'])
                if network['security'] != '--':
                    detail += ' · ' + network['security']
                row = Adw.ActionRow(
                    title=network['ssid'], subtitle=detail, activatable=True)
                signal = network['signal']
                signal_icon = Gtk.Image.new_from_icon_name(
                    'network-wireless-signal-excellent-symbolic'
                    if signal >= 70 else
                    'network-wireless-signal-good-symbolic'
                    if signal >= 40 else
                    'network-wireless-signal-weak-symbolic')
                row.add_prefix(signal_icon)
                if network['active']:
                    row.add_suffix(
                        Gtk.Image.new_from_icon_name('emblem-ok-symbolic'))
                row.connect('activated', _open_wifi_settings)
                networks_group.add(row)
                wifi_rows.append(row)
        refresh_btn.set_sensitive(True)

    def _show_scan_error(error):
        _clear_wifi_rows()
        row = Adw.ActionRow(
            title=_('Wi-Fi unavailable: {}').format(error))
        networks_group.add(row)
        wifi_rows.append(row)
        wifi_switch.set_sensitive(True)
        refresh_btn.set_sensitive(True)

    def _scan_wifi(*_args):
        refresh_btn.set_sensitive(False)

        def _worker():
            try:
                devices = _get_network_devices()
                has_wifi = bool(devices['wifi'])
                enabled = _wifi_radio_enabled() if has_wifi else False
                found = (
                    _scan_wifi_networks() if has_wifi and enabled else []
                )
            except Exception as error:
                GLib.idle_add(_show_scan_error, error)
            else:
                GLib.idle_add(
                    _replace_wifi_rows, devices, enabled, found)

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

    def _finish_radio_change(enabled, error):
        if error is not None:
            _show_scan_error(error)
        else:
            wifi_switch.handler_block(radio_handler)
            wifi_switch.set_active(enabled)
            wifi_switch.set_state(enabled)
            wifi_switch.handler_unblock(radio_handler)
        _scan_wifi()

    def _on_radio_state_set(_switch, enabled):
        wifi_switch.set_sensitive(False)
        refresh_btn.set_sensitive(False)

        def _worker():
            error = None
            try:
                _set_wifi_radio(enabled)
            except Exception as exception:
                error = exception
            GLib.idle_add(_finish_radio_change, enabled, error)

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

    radio_handler = wifi_switch.connect('state-set', _on_radio_state_set)
    refresh_btn.connect('clicked', _scan_wifi)
    monitor.connect(
        'network-changed',
        lambda _monitor, _available: (_render_connectivity(), _scan_wifi()))

    _render_connectivity()
    _scan_wifi()
    return page


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

def create_display_scaling_group():
    group = Adw.PreferencesGroup()
    group.set_margin_start(24)
    group.set_margin_end(24)
    group.set_margin_bottom(28)
    row = Adw.ActionRow(title=_('Display Scaling'))
    row.set_subtitle(_('Recommendation unavailable for this display.'))
    row.set_subtitle_lines(2)
    row.set_use_markup(False)
    dropdown = Gtk.DropDown(model=Gtk.StringList.new([]))
    dropdown.set_valign(Gtk.Align.CENTER)
    dropdown.set_sensitive(False)
    dropdown.set_tooltip_text(_('Display Scaling'))
    row.add_suffix(dropdown)
    row.set_activatable_widget(dropdown)
    group.add(row)

    status = {'info': None, 'busy': False, 'updating': False, 'timer': 0,
              'error': False}

    def render(info):
        status['updating'] = True
        status['info'] = info
        supported = list(info['supported'])
        if not any(_display_scaling.same_scale(info['scale'], s) for s in supported):
            supported.append(info['scale'])
            supported.sort()
        status['choices'] = supported
        labels = [(_('%(scale)s (Recommended)') % {'scale': _display_scaling.format_scale(s)})
                  if _display_scaling.same_scale(s, info['recommended'])
                  else _display_scaling.format_scale(s) for s in supported]
        dropdown.set_model(Gtk.StringList.new(labels))
        dropdown.set_selected(next(i for i, s in enumerate(supported)
                                   if _display_scaling.same_scale(s, info['scale'])))
        recommended = info['recommended']
        subtitle = (_('Primary display: %(display)s · Recommended: %(scale)s') % {
            'display': info['name'], 'scale': _display_scaling.format_scale(recommended)
        }) if recommended is not None else _('Recommendation unavailable for this display.')
        row.set_subtitle(_('Could not change display scaling.') if status['error'] else subtitle)
        dropdown.set_sensitive(len(info['supported']) > 1)
        status['updating'] = False

    def finished(info, error, applying):
        status['busy'] = False
        if applying:
            status['error'] = error is not None
        if info is not None:
            # Do not rebuild an open dropdown every time the monitor is polled.
            if applying or info != status['info']:
                render(info)
        else:
            status['info'] = None
            dropdown.set_sensitive(False)
            row.set_subtitle(_('Could not change display scaling.') if applying
                             else _('Recommendation unavailable for this display.'))
        if error is not None:
            print(f'Display scaling: {error}', file=sys.stderr)
        return GLib.SOURCE_REMOVE

    def refresh(scale=None):
        if status['busy']:
            return
        status['busy'] = True
        applying = scale is not None
        expected_key = status['info']['key'] if applying else None
        if applying:
            dropdown.set_sensitive(False)

        def worker():
            info, error = None, None
            try:
                client = _display_scaling.DisplayClient()
                info = (client.apply(scale, expected_key) if applying
                        else _display_scaling.primary_info(client.state()))
            except Exception as exc:
                error = exc
                # Reflect the actual scale after rejection or a concurrent change.
                if applying:
                    try:
                        info = _display_scaling.primary_info(client.state())
                    except Exception:
                        pass
            GLib.idle_add(finished, info, error, applying)

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

    def selected(widget, _param):
        if status['updating'] or status['busy'] or status['info'] is None:
            return
        index = widget.get_selected()
        if index >= len(status['choices']):
            return
        value = status['choices'][index]
        if not _display_scaling.same_scale(value, status['info']['scale']):
            refresh(value)

    def poll():
        refresh()
        return GLib.SOURCE_CONTINUE

    def mapped(_widget):
        refresh()
        if not status['timer']:
            status['timer'] = GLib.timeout_add_seconds(3, poll)

    def unmapped(_widget):
        if status['timer']:
            GLib.source_remove(status['timer'])
            status['timer'] = 0

    dropdown.connect('notify::selected', selected)
    group.connect('map', mapped)
    group.connect('unmap', unmapped)
    return group


def create_appearance_page():
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page._oobe_page_name = 'appearance'
    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)

    center.append(create_display_scaling_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)

    # OOBE presents Eleven as Modern and keeps Classic as Classic.
    detected_style, _detected_position = detect_current()
    current_style = 'classic' if detected_style == 'classic' else 'modern'

    state = {'style': current_style}

    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):
        draw_preview(
            area, cr, w, h,
            OOBE_LAYOUT_STYLES[state['style']], '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 apply_style_and_position(OOBE_LAYOUT_STYLES[style], 'bottom'):
                if style == 'modern':
                    btn_classic.set_active(False)
                else:
                    btn_modern.set_active(False)
                state['style'] = style
                preview.queue_draw()
            else:
                btn.set_active(False)
        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'
        result = 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 result.returncode == 0:
            style, position = detect_current()
            apply_style_and_position(style, position)

    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)

    # ── Network Security Card ──
    fw_card = Adw.PreferencesGroup()
    fw_card.set_title(_('Network Security'))
    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 When Idle'))

    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 4: HARDWARE DRIVERS — Driver Center entry point
# ═══════════════════════════════════════════════════════════════════

def _open_driver_center():
    """Launch Driver Center without elevation."""
    try:
        subprocess.Popen([DRIVER_CENTER_EXECUTABLE])
    except Exception as error:
        return _('Could not open AnduinOS Driver Center: {}').format(error)
    return None


def _skip_hardware_drivers(navigate_next):
    navigate_next()


def create_hardware_drivers_page(navigate_next, update_nav=None):
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page._suggest_next = False
    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_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=24)
    icon_box.set_halign(Gtk.Align.CENTER)
    for icon_name in ('nvidia.svg', 'input-gaming.svg'):
        icon = _create_image(icon_name)
        icon.set_pixel_size(72)
        icon_box.append(icon)
    center.append(icon_box)

    title = Gtk.Label(label=_('Configure Hardware Drivers?'))
    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=_(
            'Open AnduinOS Driver Center now to manage graphics cards, '
            'Xbox controllers, and other hardware.'
        )
    )
    body.add_css_class('dim-label')
    body.set_halign(Gtk.Align.CENTER)
    body.set_wrap(True)
    body.set_justify(Gtk.Justification.CENTER)
    body.set_margin_top(12)
    center.append(body)

    status_label = Gtk.Label()
    status_label.add_css_class('error')
    status_label.set_halign(Gtk.Align.CENTER)
    status_label.set_wrap(True)
    status_label.set_justify(Gtk.Justification.CENTER)
    status_label.set_margin_top(12)
    center.append(status_label)

    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.connect(
        'clicked', lambda _button: _skip_hardware_drivers(navigate_next)
    )

    open_btn = Gtk.Button(label=_('Open Driver Center'))
    open_btn.add_css_class('suggested-action')

    def _on_open(_button):
        error = _open_driver_center()
        status_label.set_label(error or '')

    open_btn.connect('clicked', _on_open)
    btn_box.append(skip_btn)
    btn_box.append(open_btn)
    center.append(btn_box)

    return page


def create_exe_sandbox_page(navigate_next):
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page._requires_internet = True
    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 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_overlay_scrolling(False)
    scroll.set_min_content_height(150)
    scroll.set_policy(Gtk.PolicyType.NEVER, 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_overlay_scrolling(False)
    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)

    if is_chinese_locale():
        rime_row = Adw.ActionRow()
        rime_row.set_title(_('Open Chinese Input Method Settings'))
        rime_row.set_subtitle(_('Switch to Chinese (Rime) first to choose Pinyin or Shuangpin.'))
        rime_key = Gtk.Label()
        rime_key.set_markup('<b><span foreground="#9141ac"> F4 </span></b>')
        rime_key.add_css_class('heading')
        rime_row.add_suffix(rime_key)
        featured_group.add(rime_row)

    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._requires_internet = True
    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_checked(command, timeout):
        """Run a command and preserve its useful error output."""
        try:
            proc = subprocess.run(
                command, capture_output=True, text=True, timeout=timeout)
        except subprocess.TimeoutExpired as e:
            raise RuntimeError(
                f'{os.path.basename(command[0])} timed out') from e
        except OSError as e:
            raise RuntimeError(str(e)) from e

        if proc.returncode != 0:
            detail = (proc.stderr or proc.stdout).strip()
            if not detail:
                detail = (
                    f'{os.path.basename(command[0])} exited with status '
                    f'{proc.returncode}'
                )
            raise RuntimeError(detail)
        return proc

    def _system_flathub_exists():
        proc = _run_checked(
            ['flatpak', 'remotes', '--system', '--show-disabled',
             '--columns=name'], 10)
        return FLATHUB_REMOTE in {
            line.strip() for line in proc.stdout.splitlines()
        }

    def _test_ustc_repository():
        # Probe actual OSTree metadata instead of the mirror's HTML index.
        for metadata in ('config', 'summary.idx'):
            request = urllib.request.Request(
                f'{FLATHUB_USTC_URL}/{metadata}',
                headers={
                    'Range': 'bytes=0-0',
                    'User-Agent': 'anduinos-oobe/2.0',
                })
            try:
                with urllib.request.urlopen(request, timeout=10) as response:
                    response.read(1)
            except Exception as e:
                raise RuntimeError(
                    f'USTC mirror metadata is unreachable: {metadata}: {e}'
                ) from e

    def _ensure_system_flathub():
        if _system_flathub_exists():
            return
        _run_checked(
            ['pkexec', 'flatpak', '--system', 'remote-add',
             '--if-not-exists', FLATHUB_REMOTE, FLATHUB_OFFICIAL_REPO],
            300)

    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():
        spinner.stop()
        # 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_ustc_repository()
                _ensure_system_flathub()

                # Switch
                _run_checked(
                    ['pkexec', 'flatpak', '--system', 'remote-modify',
                     FLATHUB_REMOTE, '--enable',
                     f'--url={FLATHUB_USTC_URL}'],
                    300)

                # The remote switch is already complete. AppStream refresh is
                # an optimization and must not turn a successful switch into a
                # false failure when the catalog is temporarily unavailable.
                try:
                    _run_checked(
                        ['flatpak', 'update', '--system', '--appstream'], 60)
                except Exception as e:
                    print(
                        f'Warning: Flathub AppStream refresh failed: {e}',
                        file=sys.stderr,
                    )
            except Exception as e:
                GLib.idle_add(_on_error, str(e), _switch_to_ustc)
            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:
                _ensure_system_flathub()
                _run_checked(
                    ['pkexec', 'flatpak', '--system', 'remote-modify',
                     FLATHUB_REMOTE, '--enable',
                     f'--url={FLATHUB_OFFICIAL_URL}'],
                    300)
            except Exception as e:
                GLib.idle_add(_on_error, str(e), _switch_back)
            else:
                GLib.idle_add(_update_ustc_state)

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

    def _on_error(msg, retry_callback):
        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', retry_callback))

    _update_ustc_state()
    return page


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

def create_apps_page():
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    page._requires_internet = True
    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'),
        (_('Nextcloud'), 'com.nextcloud.desktopclient.nextcloud',
         _('Sync and collaborate on your desktop or laptop'), 'nextcloud.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_overlay_scrolling(False)
    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, 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}")


def create_agents_page(update_navigation=lambda: None):
    page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
    page._requires_internet = True
    page._install_running = False
    page.set_margin_start(48)
    page.set_margin_end(48)
    page.set_margin_top(24)
    page.set_margin_bottom(24)
    page.set_valign(Gtk.Align.CENTER)

    title = Gtk.Label(label=_('Control Your Computer with Natural Language'))
    title.add_css_class('title-1')
    title.set_wrap(True)
    title.set_justify(Gtk.Justification.CENTER)
    page.append(title)
    subtitle = Gtk.Label(label=_(
        'Optional AI tools for your projects. Sign-in or an API key may be required; provider charges may apply.'))
    subtitle.set_wrap(True)
    subtitle.set_justify(Gtk.Justification.CENTER)
    subtitle.add_css_class('dim-label')
    page.append(subtitle)

    cards = Gtk.FlowBox()
    cards.set_selection_mode(Gtk.SelectionMode.NONE)
    cards.set_max_children_per_line(3)
    cards.set_row_spacing(12)
    cards.set_column_spacing(12)
    cards.set_homogeneous(True)
    cards.set_margin_top(12)
    cards.set_margin_bottom(12)
    cards.set_margin_start(12)
    cards.set_margin_end(12)
    card_scroll = Gtk.ScrolledWindow()
    card_scroll.set_overlay_scrolling(False)
    card_scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
    card_scroll.set_min_content_height(560)
    card_scroll.set_max_content_height(560)
    card_scroll.set_propagate_natural_height(True)
    card_scroll.set_child(cards)
    page.append(card_scroll)
    disclaimer = Gtk.Label(label=_(
        'Independently provided third-party tools. Trademarks belong to their owners; no affiliation or endorsement.'),
        wrap=True, justify=Gtk.Justification.CENTER)
    disclaimer.add_css_class('dim-label')
    disclaimer.add_css_class('caption')
    page.append(disclaimer)
    # Keep the original brand colors readable in both desktop themes.
    page._logo_css = Gtk.CssProvider()
    page._logo_css.load_from_data(b'''
        .agent-logo { background-color: white; border-radius: 12px; padding: 8px; }
        .agent-recommended {
            background-color: @accent_bg_color;
            color: @accent_fg_color;
            border-radius: 999px;
            padding: 2px 6px;
        }
    ''')
    Gtk.StyleContext.add_provider_for_display(
        Gdk.Display.get_default(), page._logo_css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
    status = Gtk.Label(wrap=True)
    page.append(status)
    progress = Gtk.ProgressBar(visible=False)
    page.append(progress)
    expander = Gtk.Expander(label=_('Advanced Output'))
    output = Gtk.TextView(editable=False, monospace=True, wrap_mode=Gtk.WrapMode.WORD_CHAR)
    output.add_css_class('card')
    log_scroll = Gtk.ScrolledWindow()
    log_scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
    log_scroll.set_min_content_height(150)
    log_scroll.set_max_content_height(200)
    log_scroll.set_child(output)
    expander.set_child(log_scroll)
    page.append(expander)
    entries = []
    failed = set()
    confirming = False

    def append_output(text):
        buffer = output.get_buffer()
        buffer.insert(buffer.get_end_iter(), text)
        mark = buffer.create_mark(None, buffer.get_end_iter(), False)
        output.scroll_to_mark(mark, 0.0, True, 0.0, 1.0)
        buffer.delete_mark(mark)

    def refresh():
        for agent, button, label in entries:
            installed = agent['is_installed']()
            label.set_label(_('Installation failed. Click the agent to retry.')
                            if agent['id'] in failed else
                            (_('Installed') if installed else _('Not installed')))
            button.set_sensitive(not page._install_running and not confirming)

    def set_running(running):
        page._install_running = running
        page._block_carousel = running
        progress.set_visible(running)
        root = page.get_root()
        if isinstance(root, Gtk.Window):
            root.set_deletable(not running)
        refresh()
        update_navigation()

    def pulse():
        if not page._install_running:
            return False
        progress.pulse()
        return True

    def finished(agent, returncode):
        installed = agent['is_installed']()
        if returncode == 0 and installed:
            failed.discard(agent['id'])
        else:
            failed.add(agent['id'])
        set_running(False)
        if returncode == 0 and installed:
            status.set_label(_('Installed. Click the agent to open it in a new window.'))
        else:
            status.set_label(_('Installation failed. Click the agent to retry.'))
            append_output(_('Installer exit status: {}').format(returncode) + '\n')
            if returncode == 0:
                append_output(_('The installer finished, but the executable could not be found.') + '\n')
            expander.set_expanded(True)

    def start_install(agent):
        output.get_buffer().set_text('')
        status.set_label(_('Installing {}…').format(agent['name']))
        set_running(True)
        GLib.timeout_add(100, pulse)

        def work():
            try:
                result = agent['install'](lambda text: GLib.idle_add(append_output, text))
            except Exception as error:
                GLib.idle_add(append_output, str(error) + '\n')
                result = -1
            GLib.idle_add(finished, agent, result)

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

    def clicked(_button, agent):
        nonlocal confirming
        if page._install_running or confirming:
            return
        if agent['is_installed']() and agent['id'] not in failed:
            try:
                agent['open']()
                status.set_label(_('Opening {}…').format(agent['name']))
            except Exception as error:
                status.set_label(_('Could not open {}.').format(agent['name']))
                append_output(str(error) + '\n')
                expander.set_expanded(True)
            return
        body = _(
            'Installation downloads and runs third-party software with your account permissions. It may change your user environment. '
            'Coding agents can read and modify files, run commands, and send project data to their provider. '
            'Only use them in projects you trust. Continue?')
        notice = agent['get_notice']()
        if notice['text']:
            body += '\n\n' + notice['text']
        dialog = Adw.AlertDialog(
            heading=_('Install {}?').format(agent['name']),
            body=body,
        )
        details = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        reminder = Gtk.Label(label=_(
            "Read the terms of service and privacy policy before use. Fees are determined by the provider."),
            wrap=True, max_width_chars=48, xalign=0)
        details.append(reminder)
        for name, website, terms, privacy in notice['providers']:
            links = Gtk.Label(wrap=True, max_width_chars=48, xalign=0)
            # Escape both translated labels and URLs before passing them to Pango.
            links.set_markup(' · '.join(
                '<a href="{}">{}</a>'.format(GLib.markup_escape_text(url),
                                            GLib.markup_escape_text(label))
                for label, url in ((name, website), (_('Terms of Service'), terms),
                                   (_('Privacy Policy'), privacy))))
            details.append(links)
        dialog.set_extra_child(details)
        dialog.add_response('cancel', _('Cancel'))
        dialog.add_response('install', _('Install'))
        dialog.set_response_appearance('install', Adw.ResponseAppearance.SUGGESTED)
        dialog.set_default_response('cancel')
        dialog.set_close_response('cancel')
        confirming = True
        refresh()

        def responded(_dialog, response):
            nonlocal confirming
            confirming = False
            refresh()
            if response == 'install':
                start_install(agent)

        dialog.connect('response', responded)
        dialog.present(page.get_root())

    for agent in _agents.recommendations(_, is_chinese_locale()):
        button = Gtk.Button()
        button.add_css_class('card')
        button.add_css_class('flat')
        content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        content.set_margin_start(10)
        content.set_margin_end(10)
        content.set_margin_top(14)
        content.set_margin_bottom(14)
        content.set_size_request(150, 180)
        logo = _create_image('agents/' + agent['id'] + '.svg')
        logo.set_pixel_size(48)
        logo.set_halign(Gtk.Align.CENTER)
        logo.add_css_class('agent-logo')
        content.append(logo)
        name = Gtk.Label(label=agent['name'], wrap=True)
        name.set_justify(Gtk.Justification.CENTER)
        name.add_css_class('heading')
        content.append(name)
        description = Gtk.Label(label=agent['description'], wrap=True)
        description.set_justify(Gtk.Justification.CENTER)
        description.set_max_width_chars(20)
        description.set_vexpand(True)
        description.add_css_class('caption')
        description.add_css_class('dim-label')
        content.append(description)
        installed_label = Gtk.Label(wrap=True)
        installed_label.set_justify(Gtk.Justification.CENTER)
        installed_label.set_max_width_chars(20)
        installed_label.add_css_class('caption-heading')
        content.append(installed_label)
        overlay = Gtk.Overlay()
        overlay.set_child(content)
        if agent.get('recommended'):
            badge = Gtk.Label(label=_('Recommended'))
            badge.add_css_class('caption-heading')
            badge.add_css_class('agent-recommended')
            badge.set_halign(Gtk.Align.END)
            badge.set_valign(Gtk.Align.START)
            badge.set_margin_top(4)
            badge.set_margin_end(4)
            badge.set_can_target(False)
            overlay.add_overlay(badge)
        button.set_child(overlay)
        button.connect('clicked', clicked, agent)
        cards.append(button)
        entries.append((agent, button, installed_label))

    page.connect('map', lambda _page: refresh())
    refresh()
    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, 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, 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
        self.offline_mode = False
        # Pages may request a navigation refresh while their widgets are being
        # constructed.  The navigation controls do not exist until the bottom
        # bar is complete, so defer those refreshes until then.
        self._nav_ready = False

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

        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 = []

        for factory in self._get_page_factories(
            navigate_next=self._nav_next
        ):
            p = factory()
            scroll = Gtk.ScrolledWindow()
            scroll.set_overlay_scrolling(False)
            scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
            scroll.set_child(p)
            clamp = Adw.Clamp()
            # Each carousel page must fill the viewport, even when its
            # content is narrow. Otherwise neighboring pages remain visible.
            clamp.set_hexpand(True)
            clamp.set_maximum_size(650)
            clamp.set_child(scroll)
            nav._pages.append(clamp)
            self.carousel.append(clamp)

        # Limit reading width on ultrawide screens; page wrappers still fill
        # the entire carousel viewport.
        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: self._nav_next())
        bottom.set_end_widget(self.next_btn)

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

        self._nav_ready = True
        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),
        ]

        # Decide once while the welcome screen is being created. If startup is
        # offline, Start leads to networking before any system setup page.
        if not internet_connection_ready():
            factories.append(
                lambda: create_network_page(
                    self._continue_offline, _update_nav
                )
            )

        factories.extend([
            lambda: create_appearance_page(),
            lambda: create_security_page(),
        ])

        factories.append(
            lambda: create_hardware_drivers_page(_nav, _update_nav)
        )
        # Let Chinese users switch Flathub to the local mirror before offering
        # Bottles, so the large Flatpak download uses the faster source.
        if is_chinese_locale():
            factories.append(lambda: create_flathub_mirror_page())
        # 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))

        factories.extend([
            lambda: create_shortcuts_page(),
            lambda: create_apps_page(),
            lambda: create_agents_page(_update_nav),
            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()

    @staticmethod
    def _content_page(clamp):
        """Unwrap the scroller and GTK's automatic viewport around box pages."""
        wrapper = clamp.get_child() if clamp else None
        content = (
            wrapper.get_child()
            if isinstance(wrapper, Gtk.ScrolledWindow)
            else wrapper
        )
        return content.get_child() if isinstance(content, Gtk.Viewport) else content

    def _update_nav_buttons(self):
        if not getattr(self, '_nav_ready', False):
            return

        idx = int(self.carousel.get_position())
        n_pages = self.carousel.get_n_pages()

        clamp = self.carousel.get_nth_page(idx)
        page = self._content_page(clamp)

        hide_next = getattr(page, '_hide_next', False) if page else False
        block_carousel = (
            getattr(page, '_block_carousel', False) if page else False
        )
        self.carousel.set_interactive(not block_carousel)

        self.back_btn.set_visible(idx > 0)
        installing = getattr(page, '_install_running', False) if page else False
        self.back_btn.set_sensitive(not installing)
        self.next_btn.set_sensitive(not installing)
        if idx == 0 and self.is_oobe:
            self.next_btn.set_visible(False)
        else:
            self.next_btn.set_visible(not hide_next)
        
        current_is_appearance = (
            page is not None and
            getattr(page, '_oobe_page_name', None) == 'appearance'
        )
        if current_is_appearance 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):
        if not self.back_btn.get_sensitive():
            return
        idx = int(self.carousel.get_position())
        if idx > 0:
            self.carousel.scroll_to(self.carousel.get_nth_page(idx - 1), True)

    def _nav_next(self):
        if not self.next_btn.get_sensitive():
            return
        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()

    def _continue_offline(self):
        """Remove network-only steps and continue with local setup."""
        self.offline_mode = True
        online_pages = [
            clamp for clamp in self._pages
            if getattr(
                self._content_page(clamp), '_requires_internet', False
            )
        ]
        for clamp in online_pages:
            self.carousel.remove(clamp)
            self._pages.remove(clamp)
        self._update_nav_buttons()
        self._nav_next()

    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('2.0.5')
        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.
        # The Dracut pre-pivot module owns the authoritative Live marker,
        # media mount and immutable installer source.
        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())
