import json
import os
import re
import subprocess
import ast
from typing import List, Tuple

from config import SetupConfig
from logging_config import get_logger

logger = get_logger()


GNOME_LAYOUTS = [
    "biggnome",
    "desk-ux",
    "hybrid",
    "g-unity",
    "classic",
    "minimal",
]

GNOME_LAYOUT_DISPLAY_NAMES = {
    "biggnome": "BigGnome",
    "desk-ux": "Desk UX",
    "hybrid": "Hybrid",
    "g-unity": "G-Unity",
    "classic": "Classic",
    "minimal": "Minimal",
}

GNOME_DTP_MONITOR_KEYS = {
    "panel-anchors",
    "panel-element-positions",
    "panel-lengths",
    "panel-positions",
    "panel-sizes",
}

GNOME_LIGHT_STYLE_UUID = "light-style@gnome-shell-extensions.gcampax.github.com"
GNOME_USER_THEME_UUID = "user-theme@gnome-shell-extensions.gcampax.github.com"


class SystemService:
    def __init__(self, test_mode: bool = False):
        self.test_mode = test_mode
        if self.test_mode:
            logger.info(
                "--- RUNNING IN TEST MODE: No system changes will be applied. ---"
            )

        # Base paths from the original scripts
        self.base_themes_path = "/usr/share/biglinux/biglinux-themes-gui"
        self.desktop_list_script = os.path.join(
            self.base_themes_path, "list-desktops.sh"
        )
        self.desktop_apply_script = os.path.join(
            self.base_themes_path, "apply-desktop.sh"
        )
        self.theme_list_script = os.path.join(self.base_themes_path, "list-themes.sh")
        self.theme_apply_script = os.path.join(self.base_themes_path, "apply-theme.sh")

        # Image paths
        self.desktop_image_path = os.path.join(self.base_themes_path, "img/{}.svg")
        self.theme_image_path = os.path.join(self.base_themes_path, "img/{}.png")
        self.assets_path = os.path.abspath(
            os.path.join(os.path.dirname(__file__), "assets")
        )
        # GNOME layout definitions (.txt) and previews (.svg) come straight from
        # the layout-switcher package — single source of truth, no longer
        # duplicated under assets/gnome-layouts/. The GNOME layout chooser only
        # runs on the GNOME profile, where layout-switcher is installed, so this
        # is not a hard dependency of biglinux-livecd (KDE/XFCE ISOs don't use it).
        self.gnome_layouts_path = "/usr/share/layout-switcher/layouts"
        self.gnome_layouts_icons_path = "/usr/share/layout-switcher/icons"

        # Temp files for live session - Calamares will copy these to /etc/big-default-config/
        self.tmp_lang_file = "/tmp/big_language"
        self.tmp_keyboard_file = "/tmp/big_keyboard"
        self.tmp_desktop_file = "/tmp/big_desktop_changed"
        self.tmp_gnome_layout_file = "/tmp/big_gnome_layout"
        self.tmp_gnome_settings_file = "/tmp/big_gnome_settings"
        self.tmp_theme_file = "/tmp/big_desktop_theme"
        self.tmp_jamesdsp_file = "/tmp/big_enable_jamesdsp"
        self.tmp_display_profile_file = "/tmp/big_improve_display"

    def _run_command(
        self,
        command: List[str],
        as_root: bool = False,
        read_only: bool = False,
        wait_for_completion: bool = False,
    ) -> Tuple[bool, str]:
        """
        Helper to run external commands.
        - If wait_for_completion or read_only is True, it runs synchronously and returns output.
        - Otherwise, it runs in the background (asynchronously) and detaches.
        """
        if self.test_mode and not read_only:
            logger.debug(
                f"[TEST MODE] Suppressed command: {'sudo ' if as_root else ''}{' '.join(command)}"
            )
            return True, ""

        if read_only:
            wait_for_completion = True

        if as_root:
            command.insert(0, "sudo")

        try:
            if wait_for_completion:
                # Run synchronously and capture output (for read-only commands)
                result = subprocess.run(
                    command,
                    capture_output=True,
                    text=True,
                    check=True,
                    encoding="utf-8",
                )
                return True, result.stdout.strip()
            else:
                # Run in the background and detach
                # Redirect stdout/stderr to /dev/null to prevent terminal clutter
                subprocess.Popen(
                    command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
                )
                return True, ""  # Assume success on launch
        except FileNotFoundError:
            err_msg = f"Command not found: {command[0]}"
            logger.error(err_msg)
            return False, err_msg
        except subprocess.CalledProcessError as e:
            err_msg = f"Error running command '{' '.join(e.cmd)}': {e.stderr}"
            logger.error(err_msg)
            return False, e.stderr.strip()
        except Exception as e:
            err_msg = f"An unexpected error occurred with command '{' '.join(command)}': {e}"
            logger.error(err_msg)
            return False, str(e)


    def _write_tmp_file(self, filepath: str, content: str):
        """Helper to write to a temp file."""
        if self.test_mode:
            logger.debug(
                f"[TEST MODE] Suppressed write to {filepath}: '{content[:50]}{'...' if len(content) > 50 else ''}'"
            )
            return True
        try:
            with open(filepath, "w", encoding="utf-8") as f:
                f.write(content)
            return True
        except IOError as e:
            logger.error(f"Error writing to {filepath}: {e}")
            return False

    def _write_user_config_file(self, filepath: str, content: str):
        """Helper to write a file in the user's home directory."""
        if self.test_mode:
            logger.debug(
                f"[TEST MODE] Suppressed write to {filepath}: '{content[:50]}{'...' if len(content) > 50 else ''}'"
            )
            return True
        try:
            os.makedirs(os.path.dirname(filepath), exist_ok=True)
            tmp_path = f"{filepath}.tmp"
            with open(tmp_path, "w", encoding="utf-8") as f:
                f.write(content)
            os.replace(tmp_path, filepath)
            return True
        except IOError as e:
            logger.error(f"Error writing to {filepath}: {e}")
            return False

    def _update_ini_settings(self, filepath: str, section: str, settings: dict):
        """Update key/value pairs in a simple INI file."""
        if self.test_mode:
            logger.debug(f"[TEST MODE] Would update {filepath}: {settings}")
            return

        lines = []
        if os.path.exists(filepath):
            try:
                with open(filepath, "r", encoding="utf-8") as f:
                    lines = f.readlines()
            except OSError as e:
                logger.error(f"Error reading {filepath}: {e}")
                return

        modified_lines = []
        in_section = False
        section_found = False
        updated_keys = set()

        def append_missing_keys():
            for key, value in settings.items():
                if key not in updated_keys:
                    modified_lines.append(f"{key}={value}\n")
                    updated_keys.add(key)

        for line in lines:
            stripped = line.strip()
            if stripped.startswith("[") and stripped.endswith("]"):
                if in_section:
                    append_missing_keys()
                in_section = stripped[1:-1] == section
                section_found = section_found or in_section
                modified_lines.append(line)
                continue

            if in_section and "=" in stripped and not stripped.startswith("#"):
                key = stripped.split("=", 1)[0].strip()
                if key in settings:
                    modified_lines.append(f"{key}={settings[key]}\n")
                    updated_keys.add(key)
                    continue

            modified_lines.append(line)

        if in_section:
            append_missing_keys()
        elif not section_found:
            if modified_lines and modified_lines[-1].strip():
                modified_lines.append("\n")
            modified_lines.append(f"[{section}]\n")
            append_missing_keys()

        self._write_user_config_file(filepath, "".join(modified_lines))

    def _apply_gtk_settings_ini(self, dark: bool, icon_theme: str):
        """Keep GTK settings.ini in sync for XFCE/Cinnamon sessions."""
        home = os.path.expanduser("~")
        gtk_theme = "adw-gtk3-dark" if dark else "adw-gtk3"
        prefer_dark = "true" if dark else "false"
        settings = {
            "gtk-application-prefer-dark-theme": prefer_dark,
            "gtk-theme-name": gtk_theme,
            "gtk-icon-theme-name": icon_theme,
        }

        for gtk_dir in ("gtk-3.0", "gtk-4.0"):
            settings_path = os.path.join(home, ".config", gtk_dir, "settings.ini")
            self._update_ini_settings(settings_path, "Settings", settings)

    @staticmethod
    def _simple_icon_theme(desktop_env: str, dark: bool) -> str:
        if dark:
            return "bigicons-papient-dark"
        if desktop_env == "XFCE":
            return "bigicons-papient-light"
        return "bigicons-papient"

    def apply_language_settings(self, lang_code: str, timezone: str):
        """Applies language, locale, and timezone settings."""
        logger.info(f"Setting language to {lang_code} and timezone to {timezone}")
        self._write_tmp_file(self.tmp_lang_file, lang_code)

        self._run_command(["timedatectl", "set-timezone", timezone], as_root=True)
        self._run_command(["timedatectl", "set-ntp", "1"], as_root=True)
        self._run_command(
            ["localectl", "set-locale", f"LANG={lang_code}.UTF-8"], as_root=True
        )
        self._run_command(
            ["/usr/bin/biglinux-verify-md5sum", "--no-gui"], as_root=False
        )

    def apply_keyboard_layout(self, layout: str):
        """Applies the selected keyboard layout."""
        logger.info(f"Setting keyboard layout to: {layout}")
        layout_cleaned = layout.replace("\\", "")
        self._write_tmp_file(self.tmp_keyboard_file, layout_cleaned)
        self._run_command(["setxkbmap", layout_cleaned])
        self._run_command(
            ["localectl", "set-x11-keymap", layout_cleaned, "pc105+inet", "", "terminate:ctrl_alt_bksp"],
            as_root=True
        )

        home = os.path.expanduser("~")
        desktop_env = self.get_desktop_environment()

        if desktop_env == "Cinnamon":
            # Configure keyboard layout via dconf settings file for Cinnamon
            settings_file = self._get_settings_file_path(desktop_env)
            if settings_file:
                sources_value = f"[('xkb', '{layout_cleaned}')]"
                self._modify_settings_file(settings_file, {
                    "org/cinnamon/desktop/input-sources": {
                        "sources": sources_value
                    },
                    "org/gnome/desktop/input-sources": {
                        "sources": sources_value
                    }
                })
                logger.info(f"Configured keyboard layout '{layout_cleaned}' in settings.cinnamon")
        else:
            # KDE/Plasma uses kxkbrc
            kxkbrc_path = os.path.join(home, ".config", "kxkbrc")
            kxkbrc_content = f"[Layout]\nLayoutList={layout_cleaned}\nUse=true\n"
            self._write_user_config_file(kxkbrc_path, kxkbrc_content)

    def get_available_desktops(self) -> List[str]:
        """Returns a list of available desktop layout names."""
        if self.get_desktop_environment() == "GNOME":
            layouts = [
                layout
                for layout in GNOME_LAYOUTS
                if os.path.exists(self._get_gnome_layout_file_path(layout))
            ]
            if not layouts:
                logger.warning(f"No GNOME layouts found at {self.gnome_layouts_path}")
            return layouts

        if not os.path.exists(self.desktop_list_script):
            logger.warning(f"Desktop script not found at {self.desktop_list_script}")
            return []
        success, output = self._run_command([self.desktop_list_script], read_only=True)
        return output.splitlines() if success else []

    def apply_desktop_layout(self, layout: str):
        """Applies the selected desktop layout."""
        logger.info(f"Applying desktop layout: {layout}")
        if self.get_desktop_environment() == "GNOME":
            self.apply_gnome_desktop_layout(layout)
            return

        self._write_tmp_file(self.tmp_desktop_file, layout)
        self._run_command([self.desktop_apply_script, layout, "quiet"])

    def apply_gnome_desktop_layout(self, layout: str):
        """Prepare the selected GNOME layout for startgnome-community."""
        layout_file = self._get_gnome_layout_file_path(layout)
        if not layout_file or not os.path.exists(layout_file):
            logger.error(f"GNOME layout file not found for: {layout}")
            return

        try:
            with open(layout_file, "r", encoding="utf-8") as f:
                layout_text = f.read()
        except OSError as e:
            logger.error(f"Failed to read GNOME layout {layout_file}: {e}")
            return

        settings_text = self._normalize_gnome_layout_text(layout_text)
        settings_file = self._get_settings_file_path("GNOME")
        if not self._write_user_config_file(settings_file, settings_text):
            return

        self._write_tmp_file(self.tmp_desktop_file, layout)
        self._write_tmp_file(self.tmp_gnome_layout_file, layout)
        self._write_tmp_file(self.tmp_gnome_settings_file, settings_text)
        logger.info(f"Prepared GNOME layout '{layout}' in {settings_file}")

    def get_available_themes(self) -> List[str]:
        """Returns a list of available theme names."""
        if not os.path.exists(self.theme_list_script):
            logger.warning(f"Theme script not found at {self.theme_list_script}")
            return []
        success, output = self._run_command([self.theme_list_script], read_only=True)
        return output.splitlines() if success else []

    def apply_theme(self, theme: str):
        """Applies the selected theme."""
        logger.info(f"Applying theme: {theme}")
        self._write_tmp_file(self.tmp_theme_file, theme)
        self._run_command([self.theme_apply_script, theme])

    def apply_simple_theme(self, theme: str):
        """
        Applies light or dark theme for simplified environments (GNOME/XFCE/Cinnamon).
        Modifies the settings file directly instead of using dconf write.

        Args:
            theme: Either "light" or "dark"
        """
        logger.info(f"Applying simple theme: {theme}")
        self._write_tmp_file(self.tmp_theme_file, theme)

        desktop_env = self.get_desktop_environment()

        if desktop_env == "GNOME":
            self._ensure_gnome_settings_file()

        if theme == "dark":
            self._apply_dark_theme(desktop_env)
        else:
            self._apply_light_theme(desktop_env)

        if desktop_env == "GNOME":
            self._sync_gnome_settings_tmp()

    def _apply_dark_theme(self, desktop_env: str):
        """Applies dark theme configuration by modifying settings file directly."""
        logger.info("Applying dark theme configuration")

        home = os.path.expanduser("~")
        settings_file = self._get_settings_file_path(desktop_env)

        if desktop_env == "Cinnamon":
            logger.info("Setting Cinnamon themes to dark mode")
            self._modify_settings_file(settings_file, {
                "org/gnome/desktop/interface": {
                    "color-scheme": "'prefer-dark'",
                    "gtk-theme": "'adw-gtk3-dark'",
                    "icon-theme": "'bigicons-papient-dark'"
                },
                "org/cinnamon/desktop/interface": {
                    "gtk-theme": "'adw-gtk3-dark'",
                    "icon-theme": "'bigicons-papient-dark'"
                },
                "org/cinnamon/theme": {
                    "name": "'Big-Orange'"
                }
            })
        elif desktop_env == "GNOME":
            logger.info("Setting GNOME themes to dark mode")
            modifications = {
                "org/gnome/desktop/interface": {
                    "color-scheme": "'prefer-dark'",
                    "gtk-theme": "'adw-gtk3-dark'",
                    "icon-theme": "'bigicons-papient-dark'"
                },
                "org/gnome/shell/extensions/user-theme": {
                    "name": "'Big-Blue'"
                }
            }
            modifications.update(self._gnome_shell_theme_extensions(settings_file, dark=True))
            self._modify_settings_file(settings_file, modifications)
        elif desktop_env == "XFCE":
            logger.info("Setting XFCE themes to dark mode")
            self._modify_settings_file(settings_file, {
                "org/gnome/desktop/interface": {
                    "color-scheme": "'prefer-dark'",
                    "gtk-theme": "'adw-gtk3-dark'",
                    "icon-theme": "'bigicons-papient-dark'"
                }
            })
            # Apply theme immediately via xfconf-query
            self._run_command(["xfconf-query", "-c", "xsettings", "-p", "/Net/ThemeName", "-s", "adw-gtk3-dark"])
            self._run_command(["xfconf-query", "-c", "xsettings", "-p", "/Net/IconThemeName", "-s", "bigicons-papient-dark"])
            self._run_command(["xfconf-query", "-c", "xfwm4", "-p", "/general/theme", "-s", "adw-gtk3-dark"])

        self._apply_gtk_settings_ini(
            dark=True,
            icon_theme=self._simple_icon_theme(desktop_env, dark=True),
        )

        # Configure Kvantum theme
        kvantum_dir = os.path.join(home, ".config", "Kvantum")
        kvantum_conf = os.path.join(kvantum_dir, "kvantum.kvconfig")
        kvantum_content = "[General]\ntheme=BigAdwaitaRoundGtkDark\n"
        self._write_user_config_file(kvantum_conf, kvantum_content)

        # Copy kdeglobals for dark theme
        kdeglobals_source = "/usr/share/sync-kde-and-gtk-places/biglinux-dark"
        kdeglobals_dest = os.path.join(home, ".config", "kdeglobals")
        if os.path.exists(kdeglobals_source):
            self._run_command(["cp", "-f", kdeglobals_source, kdeglobals_dest])
        else:
            logger.warning(f"Dark theme kdeglobals not found at {kdeglobals_source}")

    def _apply_light_theme(self, desktop_env: str):
        """Applies light theme configuration by modifying settings file directly."""
        logger.info("Applying light theme configuration")

        home = os.path.expanduser("~")
        settings_file = self._get_settings_file_path(desktop_env)

        if desktop_env == "Cinnamon":
            logger.info("Setting Cinnamon themes to light mode")
            self._modify_settings_file(settings_file, {
                "org/gnome/desktop/interface": {
                    "color-scheme": "'default'",
                    "gtk-theme": "'adw-gtk3'",
                    "icon-theme": "'bigicons-papient'"
                },
                "org/cinnamon/desktop/interface": {
                    "gtk-theme": "'adw-gtk3'",
                    "icon-theme": "'bigicons-papient'"
                },
                "org/cinnamon/theme": {
                    "name": "'Big-Orange-Light'"
                }
            })
        elif desktop_env == "GNOME":
            logger.info("Setting GNOME themes to light mode")
            modifications = {
                "org/gnome/desktop/interface": {
                    "color-scheme": "'default'",
                    "gtk-theme": "'adw-gtk3'",
                    "icon-theme": "'bigicons-papient'"
                },
                "org/gnome/shell/extensions/user-theme": {
                    "name": "'Big-Blue'"
                }
            }
            modifications.update(self._gnome_shell_theme_extensions(settings_file, dark=False))
            self._modify_settings_file(settings_file, modifications)
        elif desktop_env == "XFCE":
            logger.info("Setting XFCE themes to light mode")
            self._modify_settings_file(settings_file, {
                "org/gnome/desktop/interface": {
                    "color-scheme": "'default'",
                    "gtk-theme": "'adw-gtk3'",
                    "icon-theme": "'bigicons-papient-light'"
                }
            })
            # Apply theme immediately via xfconf-query
            self._run_command(["xfconf-query", "-c", "xsettings", "-p", "/Net/ThemeName", "-s", "adw-gtk3"])
            self._run_command(["xfconf-query", "-c", "xsettings", "-p", "/Net/IconThemeName", "-s", "bigicons-papient-light"])
            self._run_command(["xfconf-query", "-c", "xfwm4", "-p", "/general/theme", "-s", "adw-gtk3"])

        self._apply_gtk_settings_ini(
            dark=False,
            icon_theme=self._simple_icon_theme(desktop_env, dark=False),
        )

        # Configure Kvantum theme
        kvantum_dir = os.path.join(home, ".config", "Kvantum")
        kvantum_conf = os.path.join(kvantum_dir, "kvantum.kvconfig")
        kvantum_content = "[General]\ntheme=BigAdwaitaRoundGtk\n"
        self._write_user_config_file(kvantum_conf, kvantum_content)

        # Copy kdeglobals for light theme
        kdeglobals_source = "/usr/share/sync-kde-and-gtk-places/biglinux"
        kdeglobals_dest = os.path.join(home, ".config", "kdeglobals")
        if os.path.exists(kdeglobals_source):
            self._run_command(["cp", "-f", kdeglobals_source, kdeglobals_dest])
        else:
            logger.warning(f"Light theme kdeglobals not found at {kdeglobals_source}")

    def _get_settings_file_path(self, desktop_env: str) -> str:
        """Returns the path to the settings file for the given desktop environment."""
        home = os.path.expanduser("~")
        dconf_dir = os.path.join(home, ".config", "dconf")

        if desktop_env == "Cinnamon":
            return os.path.join(dconf_dir, "settings.cinnamon")
        elif desktop_env == "GNOME":
            return os.path.join(dconf_dir, "settings.gnome")
        elif desktop_env == "XFCE":
            return os.path.join(dconf_dir, "settings.xfce")
        else:
            return ""

    def _modify_settings_file(self, settings_file: str, modifications: dict):
        """
        Modifies a dconf settings file with the given key-value pairs.

        Args:
            settings_file: Path to the settings file
            modifications: Dict of {section: {key: value}}
                          Example: {"org/cinnamon/theme": {"name": "'Big-Orange'"}}
        """
        if self.test_mode:
            logger.debug(f"[TEST MODE] Would modify {settings_file} with: {modifications}")
            return

        if not os.path.exists(settings_file):
            logger.error(f"Settings file does not exist: {settings_file}")
            return

        logger.info(f"Modifying settings file: {settings_file}")

        # Read current content
        try:
            with open(settings_file, 'r', encoding='utf-8') as f:
                lines = f.readlines()
        except Exception as e:
            logger.error(f"Failed to read settings file: {e}")
            return

        # Process modifications
        modified_lines = []
        current_section = None
        sections_found = set()

        i = 0
        while i < len(lines):
            line = lines[i]
            stripped = line.strip()

            # Check if this is a section header
            if stripped.startswith('[') and stripped.endswith(']'):
                current_section = stripped[1:-1]
                modified_lines.append(line)

                # If this section needs modifications, apply them
                if current_section in modifications:
                    sections_found.add(current_section)
                    # Look ahead to see which keys already exist in this section
                    section_keys = set()
                    j = i + 1
                    while j < len(lines):
                        next_line = lines[j].strip()
                        if next_line.startswith('['):
                            break
                        if '=' in next_line and not next_line.startswith('#'):
                            key = next_line.split('=')[0].strip()
                            section_keys.add(key)
                        j += 1

                    # Process lines in this section
                    i += 1
                    while i < len(lines):
                        line = lines[i]
                        stripped = line.strip()

                        # Next section starts
                        if stripped.startswith('['):
                            i -= 1  # Go back one line
                            break

                        # Empty line or comment
                        if not stripped or stripped.startswith('#'):
                            modified_lines.append(line)
                            i += 1
                            continue

                        # Key-value line
                        if '=' in stripped:
                            key = stripped.split('=')[0].strip()
                            if key in modifications[current_section]:
                                # Replace with new value
                                new_value = modifications[current_section][key]
                                modified_lines.append(f"{key}={new_value}\n")
                                logger.debug(f"Updated [{current_section}] {key} = {new_value}")
                            else:
                                # Keep original
                                modified_lines.append(line)
                        else:
                            modified_lines.append(line)

                        i += 1

                    # Add any keys that didn't exist in the section
                    for key, value in modifications[current_section].items():
                        if key not in section_keys:
                            modified_lines.append(f"{key}={value}\n")
                            logger.debug(f"Added [{current_section}] {key} = {value}")
            else:
                modified_lines.append(line)

            i += 1

        # Add sections that didn't exist
        for section, keys in modifications.items():
            if section not in sections_found:
                modified_lines.append(f"\n[{section}]\n")
                for key, value in keys.items():
                    modified_lines.append(f"{key}={value}\n")
                    logger.debug(f"Added new section [{section}] {key} = {value}")

        # Write modified content
        try:
            with open(settings_file, 'w', encoding='utf-8') as f:
                f.writelines(modified_lines)
            logger.info(f"Successfully modified {settings_file}")
        except Exception as e:
            logger.error(f"Failed to write settings file: {e}")

    @staticmethod
    def _parse_settings_list(value: str) -> List[str]:
        try:
            parsed = ast.literal_eval(value.strip())
        except (ValueError, SyntaxError):
            return []
        if not isinstance(parsed, list):
            return []
        return [item for item in parsed if isinstance(item, str) and item]

    @staticmethod
    def _settings_key_values(settings_file: str, section_name: str) -> dict:
        values = {}
        current_section = ""
        try:
            with open(settings_file, "r", encoding="utf-8") as f:
                for raw in f:
                    stripped = raw.strip()
                    if stripped.startswith("[") and stripped.endswith("]"):
                        current_section = stripped[1:-1]
                        continue
                    if current_section != section_name or "=" not in stripped:
                        continue
                    key, _, value = stripped.partition("=")
                    values[key.strip()] = value.strip()
        except OSError as e:
            logger.error(f"Failed to read settings file: {e}")
        return values

    def _gnome_shell_theme_extensions(self, settings_file: str, dark: bool) -> dict:
        values = self._settings_key_values(settings_file, "org/gnome/shell")
        enabled = self._parse_settings_list(values.get("enabled-extensions", "[]"))
        disabled = self._parse_settings_list(values.get("disabled-extensions", "[]"))

        def add_once(items: List[str], uuid: str):
            if uuid not in items:
                items.append(uuid)

        if dark:
            enabled = [uuid for uuid in enabled if uuid != GNOME_LIGHT_STYLE_UUID]
            disabled = [uuid for uuid in disabled if uuid != GNOME_USER_THEME_UUID]
            add_once(enabled, GNOME_USER_THEME_UUID)
            add_once(disabled, GNOME_LIGHT_STYLE_UUID)
        else:
            enabled = [uuid for uuid in enabled if uuid != GNOME_USER_THEME_UUID]
            disabled = [uuid for uuid in disabled if uuid != GNOME_LIGHT_STYLE_UUID]
            add_once(enabled, GNOME_LIGHT_STYLE_UUID)
            add_once(disabled, GNOME_USER_THEME_UUID)

        return {
            "org/gnome/shell": {
                "enabled-extensions": repr(enabled),
                "disabled-extensions": repr(disabled),
            }
        }

    def finalize_setup(self, config: SetupConfig):
        """
        Performs final setup steps, including creating flag files.

        All config files are saved to /tmp during the live session.
        Calamares will copy them to /etc/big-default-config/ on the installed system.
        """
        logger.info("Finalizing setup...")

        # JamesDSP configuration - save flag to /tmp
        if config.enable_jamesdsp:
            logger.info("JamesDSP enabled, creating flag file.")
            self._run_command(["touch", self.tmp_jamesdsp_file], as_root=False)
            # Configure JamesDSP in live session
            home = os.path.expanduser("~")
            jamesdsp_conf = os.path.join(home, ".config/jamesdsp/application.conf")
            if os.path.exists(jamesdsp_conf):
                self._run_command(
                    [
                        "sed",
                        "-i",
                        "s|AutoStartEnabled=false|AutoStartEnabled=true|g",
                        jamesdsp_conf,
                    ],
                    as_root=False,
                )
                # Restart the systemd service so JamesDSP actually starts
                self._run_command(
                    ["systemctl", "--user", "restart", "jamesdsp-autostart.service"],
                    as_root=False,
                )
        else:
            logger.info("JamesDSP not enabled, removing flag file if it exists.")
            self._run_command(["rm", "-f", self.tmp_jamesdsp_file], as_root=False)
            home = os.path.expanduser("~")
            jamesdsp_conf = os.path.join(home, ".config/jamesdsp/application.conf")
            if os.path.exists(jamesdsp_conf):
                self._run_command(
                    [
                        "sed",
                        "-i",
                        "s|AutoStartEnabled=true|AutoStartEnabled=false|g",
                        jamesdsp_conf,
                    ],
                    as_root=False,
                )
                # Stop the systemd service so JamesDSP actually stops
                self._run_command(
                    ["systemctl", "--user", "stop", "jamesdsp-autostart.service"],
                    as_root=False,
                )

        # Display profile/ICC configuration - save flag to /tmp
        if config.enable_enhanced_contrast:
            logger.info("Enhanced contrast enabled, creating flag file.")
            self._run_command(["touch", self.tmp_display_profile_file], as_root=False)
            self._run_command(
                ["/usr/bin/icc_profile_apply", "enable"],
                as_root=False,
            )
        else:
            logger.info(
                "Enhanced contrast not enabled, removing flag file if it exists."
            )
            self._run_command(
                ["rm", "-f", self.tmp_display_profile_file], as_root=False
            )
            self._run_command(
                ["/usr/bin/icc_profile_apply", "disable"],
                as_root=False,
            )

        self._run_command(["killall", "kwin_wayland"])

    def get_desktop_image_path(self, layout_name: str) -> str:
        if self.get_desktop_environment() == "GNOME":
            return os.path.join(self.gnome_layouts_icons_path, f"{layout_name}.svg")
        return self.desktop_image_path.format(layout_name)

    def get_theme_image_path(self, theme_name: str) -> str:
        return self.theme_image_path.format(theme_name)

    def get_desktop_display_name(self, layout_name: str) -> str:
        return GNOME_LAYOUT_DISPLAY_NAMES.get(layout_name, layout_name)

    def apply_jamesdsp_settings(self, enabled: bool):
        """
        Applies JamesDSP configuration immediately.
        This is called when a theme is selected, based on the switch state.
        """
        home = os.path.expanduser("~")
        jamesdsp_conf = os.path.join(home, ".config/jamesdsp/application.conf")
        
        if enabled:
            logger.info("Applying JamesDSP enabled settings...")
            self._run_command(["touch", self.tmp_jamesdsp_file], as_root=False)
            if os.path.exists(jamesdsp_conf):
                self._run_command(
                    [
                        "sed",
                        "-i",
                        "s|AutoStartEnabled=false|AutoStartEnabled=true|g",
                        jamesdsp_conf,
                    ],
                    as_root=False,
                )
                # Restart the systemd service so JamesDSP actually starts
                self._run_command(
                    ["systemctl", "--user", "restart", "jamesdsp-autostart.service"],
                    as_root=False,
                )
        else:
            logger.info("Applying JamesDSP disabled settings...")
            self._run_command(["rm", "-f", self.tmp_jamesdsp_file], as_root=False)
            if os.path.exists(jamesdsp_conf):
                self._run_command(
                    [
                        "sed",
                        "-i",
                        "s|AutoStartEnabled=true|AutoStartEnabled=false|g",
                        jamesdsp_conf,
                    ],
                    as_root=False,
                )
                # Stop the systemd service so JamesDSP actually stops
                self._run_command(
                    ["systemctl", "--user", "stop", "jamesdsp-autostart.service"],
                    as_root=False,
                )

    def apply_icc_profile_settings(self, enabled: bool):
        """
        Applies ICC profile configuration immediately.
        This is called when a theme is selected, based on the switch state.
        """
        if enabled:
            logger.info("Applying ICC profile enabled settings...")
            self._run_command(["touch", self.tmp_display_profile_file], as_root=False)
            self._run_command(
                ["/usr/bin/icc_profile_apply", "enable"],
                as_root=False,
            )
        else:
            logger.info("Applying ICC profile disabled settings...")
            self._run_command(
                ["rm", "-f", self.tmp_display_profile_file], as_root=False
            )
            self._run_command(
                ["/usr/bin/icc_profile_apply", "disable"],
                as_root=False,
            )

    def check_jamesdsp_availability(self) -> bool:
        """Checks if JamesDSP executable exists."""
        return os.path.exists("/usr/bin/jamesdsp")

    def check_enhanced_contrast_availability(self) -> bool:
        """Checks for the AppleRGB ICC profile and if running on Wayland."""
        icc_profile_exists = os.path.exists("/usr/share/color/icc/colord/ECI-RGBv1.icc")
        logger.debug(f"ICC profile exists: {icc_profile_exists}")

        # Check if running on Wayland (works for GNOME, KDE, etc)
        wayland_running = False

        # Method 1: Check XDG_SESSION_TYPE environment variable
        session_type = os.environ.get("XDG_SESSION_TYPE", "").lower()
        logger.debug(f"XDG_SESSION_TYPE: {session_type}")
        if session_type == "wayland":
            wayland_running = True

        # Method 2: Check if WAYLAND_DISPLAY is set
        wayland_display = os.environ.get("WAYLAND_DISPLAY", "")
        logger.debug(f"WAYLAND_DISPLAY: {wayland_display}")
        if not wayland_running and wayland_display:
            wayland_running = True

        # Method 3: Check for compositor processes (fallback)
        if not wayland_running:
            try:
                # Check for kwin_wayland (KDE) or gnome-shell on Wayland
                result = subprocess.run(
                    ["pgrep", "-x", "kwin_wayland"], capture_output=True, check=False
                )
                wayland_running = result.returncode == 0
            except FileNotFoundError:
                pass

        logger.info(f"Enhanced contrast availability: ICC={icc_profile_exists}, Wayland={wayland_running}, Result={icc_profile_exists and wayland_running}")
        return icc_profile_exists and wayland_running

    def get_total_memory_gb(self) -> float:
        """Gets total system memory in Gigabytes from /proc/meminfo."""
        try:
            with open("/proc/meminfo", "r", encoding="utf-8") as f:
                meminfo = f.read()
            match = re.search(r"MemTotal:\s+(\d+)\s+kB", meminfo)
            if match:
                kb = int(match.group(1))
                gb = kb / (1024 * 1024)
                return gb
        except (FileNotFoundError, ValueError, IndexError) as e:
            logger.warning(f"Could not read or parse /proc/meminfo: {e}")
        return 0.0  # Safe fallback

    def is_virtual_machine(self) -> bool:
        """Checks if the system is running inside a virtual machine using systemd-detect-virt."""
        try:
            # systemd-detect-virt returns 0 if in a VM, 1 otherwise.
            result = subprocess.run(
                ["systemd-detect-virt"], capture_output=True, check=False
            )
            return result.returncode == 0
        except FileNotFoundError:
            # Fallback if the command doesn't exist. Assume not a VM for safety.
            return False

    def get_desktop_environment(self) -> str:
        """Detects the desktop environment by checking specific startup files."""
        if os.path.exists("/usr/bin/startgnome-community"):
            return "GNOME"
        elif os.path.exists("/usr/bin/startcinnamon-community"):
            return "Cinnamon"
        elif os.path.exists("/usr/bin/startxfce-community"):
            return "XFCE"
        else:
            return "other"

    def is_simplified_environment(self) -> bool:
        """Checks if we should use simplified UI (GNOME/XFCE/Cinnamon)."""
        desktop_env = self.get_desktop_environment()
        return desktop_env in ["GNOME", "XFCE", "Cinnamon"]

    def uses_simple_theme_selector(self) -> bool:
        """Checks if the theme step should be light/dark only."""
        return self.get_desktop_environment() in ["GNOME", "XFCE", "Cinnamon"]

    def has_desktop_layout_step(self) -> bool:
        """Checks if the wizard should show the desktop layout step."""
        desktop_env = self.get_desktop_environment()
        return desktop_env == "GNOME" or not self.is_simplified_environment()

    def _get_gnome_layout_file_path(self, layout: str) -> str:
        if layout not in GNOME_LAYOUTS:
            return ""
        return os.path.join(self.gnome_layouts_path, f"{layout}.txt")

    def _ensure_gnome_settings_file(self):
        settings_file = self._get_settings_file_path("GNOME")
        if os.path.exists(settings_file):
            return
        default_layout = GNOME_LAYOUTS[0]
        logger.info(f"Creating GNOME settings from default layout: {default_layout}")
        self.apply_gnome_desktop_layout(default_layout)

    def _sync_gnome_settings_tmp(self):
        settings_file = self._get_settings_file_path("GNOME")
        if not os.path.exists(settings_file):
            return
        try:
            with open(settings_file, "r", encoding="utf-8") as f:
                self._write_tmp_file(self.tmp_gnome_settings_file, f.read())
        except OSError as e:
            logger.error(f"Failed to sync GNOME settings temp file: {e}")

    def _normalize_gnome_layout_text(self, text: str) -> str:
        """Keep bundled GNOME layout dumps portable across monitors."""
        out_lines = []
        for line in text.splitlines():
            if line.startswith("preferred-monitor-by-connector="):
                out_lines.append("preferred-monitor-by-connector='primary'")
                continue
            if line.startswith("primary-monitor="):
                out_lines.append("primary-monitor=''")
                continue
            if "=" not in line:
                out_lines.append(line)
                continue

            key, _, value = line.partition("=")
            if key not in GNOME_DTP_MONITOR_KEYS:
                out_lines.append(line)
                continue

            try:
                data = json.loads(value.strip().strip("'"))
            except (TypeError, ValueError):
                out_lines.append(line)
                continue

            if data:
                normalized = {"0": next(iter(data.values()))}
            else:
                normalized = {}
            new_value = json.dumps(normalized, separators=(",", ":"))
            out_lines.append(f"{key}='{new_value}'")

        normalized_text = "\n".join(out_lines)
        if text.endswith("\n"):
            normalized_text += "\n"
        return normalized_text

    # XivaStudio detection with caching
    _xivastudio_cache: bool | None = None

    # XivaStudio custom logo paths
    XIVASTUDIO_LOGO_PNG = "/usr/share/pixmaps/icon-logo-xivastudio.png"
    XIVASTUDIO_LOGO_GIF = "/usr/share/pixmaps/icon-logo-xivastudio.gif"

    def is_xivastudio(self) -> bool:
        """
        Checks if running on XivaStudio variant.
        Result is cached after first check.
        """
        if SystemService._xivastudio_cache is None:
            SystemService._xivastudio_cache = os.path.exists(
                self.XIVASTUDIO_LOGO_PNG
            ) or os.path.exists(self.XIVASTUDIO_LOGO_GIF)
        return SystemService._xivastudio_cache

    def get_xivastudio_logo_path(self) -> str | None:
        """
        Returns XivaStudio logo path if available.
        PNG is preferred over GIF.
        """
        if os.path.exists(self.XIVASTUDIO_LOGO_PNG):
            return self.XIVASTUDIO_LOGO_PNG
        if os.path.exists(self.XIVASTUDIO_LOGO_GIF):
            return self.XIVASTUDIO_LOGO_GIF
        return None
