import gi
import re
import json
import os
import shutil
import subprocess
import threading
from typing import List, Dict, Any, Optional, Tuple

gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, Gdk, GLib

# Technische Begriffe, nicht übersetzen
FS_CHOICES = ["ext4", "btrfs", "xfs"]
BTRFS_DEFAULT_SUBVOLS = [
    ("@", "/", True),
    ("@home", "/home", True),
    ("@snapshots", "/.snapshots", True),
    ("@var_log", "/var/log", True),
]

MODE_ERASE = "erase"
MODE_FREE = "free"
MODE_EXISTING = "existing"


class PartitioningView(Gtk.Box):
    # Page to select hard drive, file system etc.
    def __init__(self):
        super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        for s in (
            self.set_margin_top,
            self.set_margin_bottom,
            self.set_margin_start,
            self.set_margin_end,
        ):
            s(6)
        self.is_uefi = os.path.isdir("/sys/firmware/efi")

        # --- State ---
        self.disks: List[Dict[str, Any]] = []
        self.selected_disk: Optional[str] = None
        self.mode: str = MODE_ERASE
        self.fs_choice: str = "ext4"
        self.subvol_rows = []

        # auto layout options
        self.use_separate_home: bool = False
        self.home_size_percent: int = 50
        self.use_swap_partition: bool = False
        self.swap_partition_gib: int = 8
        self.esp_size_mib: int = 512

        # free-space mode state
        self.free_segments: List[Tuple[float, float, float]] = []
        self.free_best_size_mib: float = 0.0
        self.root_size_gib: int = 30

        # existing partition mode state
        self.partitions: List[Dict[str, Any]] = []
        self.selected_partition: Optional[str] = None

        title = Gtk.Label.new(_("Festplatten-Partitionierung"))
        title.add_css_class("title-1")
        title.set_halign(Gtk.Align.START)
        self.append(title)
        
        subtitle = Gtk.Label.new(_("Wähle aus, wie Void Linux installiert werden soll."))
        subtitle.set_halign(Gtk.Align.START)
        subtitle.set_opacity(0.8)
        self.append(subtitle)

        sc = Gtk.ScrolledWindow()
        sc.set_vexpand(True)
        sc.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        self.append(sc)

        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
        sc.set_child(main_box)

        # --- Disk selection & Tools ---
        disk_frame = Gtk.Frame(label=_("1. Ziel-Datenträger"))
        disk_frame.add_css_class("card")
        main_box.append(disk_frame)

        disk_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        disk_vbox.set_margin_top(8)
        disk_vbox.set_margin_bottom(8)
        disk_vbox.set_margin_start(12)
        disk_vbox.set_margin_end(12)
        disk_frame.set_child(disk_vbox)

        # Row 1: Selection
        disk_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        disk_row.append(Gtk.Label.new(_("Festplatte:")))
        self.device_combo = Gtk.ComboBoxText()
        self.device_combo.set_hexpand(True)
        self.device_combo.connect("changed", self._on_device_changed)
        disk_row.append(self.device_combo)
        disk_vbox.append(disk_row)

        # Row 2: Tools (GParted + Refresh)
        tools_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        
        self.btn_refresh = Gtk.Button.new_from_icon_name("view-refresh-symbolic")
        self.btn_refresh.set_tooltip_text(_("Festplatten & Partitionen neu einlesen"))
        self.btn_refresh.connect("clicked", self._on_refresh_clicked)
        tools_row.append(self.btn_refresh)

        tools_label = Gtk.Label.new(_("Werkzeuge:"))
        tools_label.add_css_class("dim-label")
        tools_row.append(tools_label)

        self.btn_gparted = Gtk.Button.new_with_label(_("GParted öffnen"))
        self.btn_gparted.set_tooltip_text(_("Partitionen sicher verkleinern oder verschieben"))
        self.btn_gparted.connect("clicked", self._on_open_gparted)
        tools_row.append(self.btn_gparted)
        
        # Spinner for installation progress
        self.gparted_spinner = Gtk.Spinner()
        tools_row.append(self.gparted_spinner)

        hint_gparted = Gtk.Label.new(_("Nutze GParted, um bestehende Partitionen zu verkleinern."))
        hint_gparted.add_css_class("dim-label")
        hint_gparted.set_wrap(True)
        tools_row.append(hint_gparted)

        disk_vbox.append(tools_row)

        # --- Mode selection ---
        mode_frame = Gtk.Frame(label=_("2. Partitionierungs-Modus"))
        mode_frame.add_css_class("card")
        main_box.append(mode_frame)

        mode_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        mode_box.set_margin_top(8)
        mode_box.set_margin_bottom(8)
        mode_box.set_margin_start(12)
        mode_box.set_margin_end(12)
        mode_frame.set_child(mode_box)

        self.rb_erase = Gtk.CheckButton.new_with_label(
            _("Gesamte Festplatte verwenden (Löscht alle Daten!)")
        )
        self.rb_free = Gtk.CheckButton.new_with_label(
            _("Freien Speicherplatz verwenden (Neben anderem System installieren)")
        )
        self.rb_existing = Gtk.CheckButton.new_with_label(
            _("Vorhandene Partition manuell zuweisen")
        )

        self.rb_free.set_group(self.rb_erase)
        self.rb_existing.set_group(self.rb_erase)

        self.rb_erase.set_active(True)

        self.rb_erase.connect("toggled", self._on_mode_changed)
        self.rb_free.connect("toggled", self._on_mode_changed)
        self.rb_existing.connect("toggled", self._on_mode_changed)

        mode_box.append(self.rb_erase)
        mode_box.append(self.rb_free)
        mode_box.append(self.rb_existing)

        # --- Mode specific boxes (below mode selection) ---
        self.mode_detail_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        main_box.append(self.mode_detail_box)

        # Existing partition picker
        self.existing_frame = Gtk.Frame(label=_("Ausgewählte Partition"))
        self.existing_frame.add_css_class("card")
        self.mode_detail_box.append(self.existing_frame)

        ex_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        ex_box.set_margin_top(10)
        ex_box.set_margin_bottom(10)
        ex_box.set_margin_start(12)
        ex_box.set_margin_end(12)
        self.existing_frame.set_child(ex_box)

        ex_box.append(Gtk.Label.new(_("Partition:")))

        self.partition_paths: List[str] = []
        self.partition_model = Gtk.StringList.new([])  # type: ignore
        self.partition_dropdown = Gtk.DropDown.new(self.partition_model, None)
        self.partition_dropdown.set_enable_search(True)
        self.partition_dropdown.connect("notify::selected", self._on_partition_selected)
        self.partition_dropdown.set_sensitive(False)
        ex_box.append(self.partition_dropdown)

        ex_warn = Gtk.Label.new(_("⚠ Diese Partition wird für / verwendet und formatiert."))
        ex_warn.set_wrap(True)
        ex_warn.add_css_class("dim-label")
        ex_box.append(ex_warn)

        # Free space info + root size
        self.free_frame = Gtk.Frame(label=_("Freier Speicher"))
        self.free_frame.add_css_class("card")
        self.mode_detail_box.append(self.free_frame)

        free_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        free_box.set_margin_top(10)
        free_box.set_margin_bottom(10)
        free_box.set_margin_start(12)
        free_box.set_margin_end(12)
        self.free_frame.set_child(free_box)

        self.lbl_free_info = Gtk.Label.new(_("Freier Platz: —"))
        self.lbl_free_info.set_halign(Gtk.Align.START)
        self.lbl_free_info.add_css_class("dim-label")
        free_box.append(self.lbl_free_info)

        row_root = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        row_root.append(Gtk.Label.new(_("Größe für Root (/) in GiB:")))
        self.spin_root_gib = Gtk.SpinButton.new_with_range(10, 2048, 1)
        self.spin_root_gib.set_value(self.root_size_gib)
        self.spin_root_gib.connect("value-changed", self._on_root_size_changed)
        row_root.append(self.spin_root_gib)
        free_box.append(row_root)

        free_hint = Gtk.Label.new(
            _("Es werden neue Partitionen im nicht zugewiesenen Bereich erstellt. "
            "Falls zu wenig Platz ist: Nutze GParted (oben), um Partitionen zu verkleinern.")
        )
        free_hint.set_wrap(True)
        free_hint.set_halign(Gtk.Align.START)
        free_hint.add_css_class("dim-label")
        free_box.append(free_hint)

        # --- Auto options (erase/free) ---
        self.auto_options_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        main_box.append(self.auto_options_box)

        layout_frame = Gtk.Frame(label=_("3. Layout-Optionen"))
        layout_frame.add_css_class("card")
        self.auto_options_box.append(layout_frame)

        layout_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        layout_box.set_margin_top(8)
        layout_box.set_margin_bottom(8)
        layout_box.set_margin_start(12)
        layout_box.set_margin_end(12)
        layout_frame.set_child(layout_box)

        row_fs = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        row_fs.append(Gtk.Label.new(_("Dateisystem:")))
        self.fs_combo = Gtk.ComboBoxText()
        for fs in FS_CHOICES:
            self.fs_combo.append_text(fs)
        self.fs_combo.set_active(FS_CHOICES.index("ext4"))
        self.fs_combo.connect("changed", self._on_fs_changed)
        row_fs.append(self.fs_combo)
        layout_box.append(row_fs)

        layout_box.append(
            Gtk.Separator(
                orientation=Gtk.Orientation.HORIZONTAL, margin_top=5, margin_bottom=5
            )
        )

        self.home_options_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        self.cb_home = Gtk.CheckButton.new_with_label(_("Eigene /home Partition anlegen"))
        self.cb_home.connect("toggled", self._on_home_toggled)
        self.home_options_box.append(self.cb_home)

        row_home_size = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        row_home_size.append(Gtk.Label.new(_("Größe für /home (% des freien Platzes):")))
        self.spin_home_percent = Gtk.SpinButton.new_with_range(5, 95, 5)
        self.spin_home_percent.set_value(self.home_size_percent)
        self.spin_home_percent.connect("value-changed", self._on_home_percent_changed)
        row_home_size.append(self.spin_home_percent)
        self.home_options_box.append(row_home_size)

        layout_box.append(self.home_options_box)

        self.cb_swap_part = Gtk.CheckButton.new_with_label(_("Swap-Partition anlegen"))
        self.cb_swap_part.connect("toggled", self._on_swap_part_toggled)
        layout_box.append(self.cb_swap_part)

        self.row_swap_size = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        self.row_swap_size.append(Gtk.Label.new(_("Größe der Swap-Partition (GiB):")))
        self.spin_swap_part_gib = Gtk.SpinButton.new_with_range(1, 128, 1)
        self.spin_swap_part_gib.set_value(self.swap_partition_gib)
        self.spin_swap_part_gib.connect("value-changed", self._on_swap_gib_changed)
        self.row_swap_size.append(self.spin_swap_part_gib)
        layout_box.append(self.row_swap_size)

        self.subvol_frame = Gtk.Frame(label=_("Btrfs-Subvolumes"))
        self.subvol_frame.add_css_class("card")
        self.auto_options_box.append(self.subvol_frame)

        subvol_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        subvol_box.set_margin_top(8)
        subvol_box.set_margin_bottom(8)
        subvol_box.set_margin_start(12)
        subvol_box.set_margin_end(12)
        self.subvol_frame.set_child(subvol_box)

        self.subvol_list_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
        subvol_box.append(self.subvol_list_box)

        # --- DECORATIVE ICON ---
        spacer = Gtk.Box()
        spacer.set_vexpand(True)
        main_box.append(spacer)

        icon_box = Gtk.Box(spacing=24)
        icon_box.set_halign(Gtk.Align.CENTER)
        icon_box.set_valign(Gtk.Align.END)
        icon_box.set_margin_bottom(12)

        icon_disk = Gtk.Image.new_from_icon_name("drive-harddisk-symbolic")
        icon_disk.set_pixel_size(120)
        icon_disk.add_css_class("page-illustration")
        
        icon_box.append(icon_disk)
        main_box.append(icon_box)

        # Initialize
        self._load_disks()
        self._build_subvols_ui()
        
        # Initialen Status korrekt setzen
        self._update_mode_visibility()
        self._update_fs_options_visibility()

    def _on_open_gparted(self, button):
        # Check if gparted is installed
        if shutil.which("gparted"):
            self._launch_gparted()
        else:
            # Install gparted first
            self.btn_gparted.set_sensitive(False)
            self.btn_gparted.set_label(_("Installiere GParted..."))
            self.gparted_spinner.start()
            
            # Install in background thread to keep UI responsive
            thread = threading.Thread(target=self._install_gparted_thread)
            thread.daemon = True
            thread.start()

    def _install_gparted_thread(self):
        try:
            # Update repos and install gparted
            print("Installiere GParted...")
            subprocess.run(["xbps-install", "-S", "-y", "gparted"], check=True)
            success = True
            error_msg = None
        except Exception as e:
            print(f"Fehler bei GParted Installation: {e}")
            success = False
            error_msg = str(e)

        # Update UI in main thread
        GLib.idle_add(self._on_gparted_install_finished, success, error_msg)

    def _on_gparted_install_finished(self, success, error_msg):
        self.gparted_spinner.stop()
        self.btn_gparted.set_sensitive(True)
        self.btn_gparted.set_label(_("GParted öffnen"))
        
        if success:
            self._launch_gparted()
        else:
            # Show error dialog
            root_win = self.get_root()
            dialog = Gtk.MessageDialog(
                transient_for=root_win if isinstance(root_win, Gtk.Window) else None,
                modal=True,
                message_type=Gtk.MessageType.ERROR,
                buttons=Gtk.ButtonsType.CLOSE,
                text=_("Installation fehlgeschlagen")
            )
            dialog.format_secondary_text(
                f"{_('GParted konnte nicht installiert werden.')}\n\n{_('Fehler')}: {error_msg}\n\n"
                f"{_('Bitte prüfe deine Internetverbindung.')}"
            )
            dialog.connect("response", lambda d, r: d.destroy())
            dialog.present()

    def _launch_gparted(self):
        try:
            subprocess.Popen(["gparted"])
        except Exception as e:
            print(f"Fehler beim Starten von GParted: {e}")

    def _on_refresh_clicked(self, button):
        self._load_disks()
        # Auch den freien Speicher neu berechnen
        self._refresh_free_space()

    def _run_lsblk(self) -> Optional[List[Dict]]:
        try:
            out = subprocess.check_output(
                ["lsblk", "-p", "-J", "-o", "NAME,SIZE,FSTYPE,TYPE,PATH,MOUNTPOINT"],
                text=True,
            )
            return json.loads(out).get("blockdevices", [])
        except Exception as e:
            print(f"Fehler bei lsblk: {e}")
            return None

    def _load_disks(self):
        # Merke aktuelle Auswahl
        active_id = self.device_combo.get_active_text()
        current_disk = active_id.split(" ")[0] if active_id else None

        self.device_combo.remove_all()
        self.disks.clear()
        devices = self._run_lsblk()
        if not devices:
            return
        
        active_idx = 0
        idx = 0
        for dev_info in devices:
            if dev_info.get("type") == "disk":
                self.disks.append(dev_info)
                path = dev_info['path']
                self.device_combo.append_text(f"{path} ({dev_info['size']})")
                if current_disk and path == current_disk:
                    active_idx = idx
                idx += 1
                
        if self.disks:
            self.device_combo.set_active(active_idx)

    def _on_device_changed(self, combo):
        active_text = combo.get_active_text()
        self.selected_disk = active_text.split(" ")[0] if active_text else None
        self._refresh_partitions()
        self._refresh_free_space()

    def _on_partition_selected(self, dropdown, _pspec=None):
        idx = dropdown.get_selected()
        if idx is None or idx < 0 or idx >= len(self.partition_paths):
            self.selected_partition = None
            return
        self.selected_partition = self.partition_paths[idx]

    def _on_mode_changed(self, radio_button):
        if not radio_button.get_active():
            return
        if self.rb_erase.get_active():
            self.mode = MODE_ERASE
        elif self.rb_free.get_active():
            self.mode = MODE_FREE
        elif self.rb_existing.get_active():
            self.mode = MODE_EXISTING
        self._update_mode_visibility()
        self._update_fs_options_visibility()
        self._refresh_partitions()
        self._refresh_free_space()

    def _on_fs_changed(self, combo):
        self.fs_choice = combo.get_active_text() or "ext4"
        self._update_fs_options_visibility()

    def _on_home_toggled(self, checkbox):
        self.use_separate_home = checkbox.get_active()
        self._update_home_controls()

    def _on_swap_part_toggled(self, checkbox):
        self.use_swap_partition = checkbox.get_active()
        self._update_swap_controls()

    def _on_home_percent_changed(self, spin):
        self.home_size_percent = int(spin.get_value())

    def _on_swap_gib_changed(self, spin):
        self.swap_partition_gib = int(spin.get_value())

    def _on_root_size_changed(self, spin):
        self.root_size_gib = int(spin.get_value())

    def _update_mode_visibility(self):
        # detail frames
        self.existing_frame.set_visible(self.mode == MODE_EXISTING)
        self.free_frame.set_visible(self.mode == MODE_FREE)

        # layout options visible for erase + free + existing (FS selection is still useful)
        self.auto_options_box.set_visible(True)

    def _update_fs_options_visibility(self):
        # Ermitteln, welche Optionen sichtbar sein sollen
        is_btrfs = (self.fs_choice == "btrfs")
        is_existing = (self.mode == MODE_EXISTING)
        
        # 1. Btrfs Subvolumes Frame
        # Nur sichtbar, wenn Btrfs gewählt ist
        self.subvol_frame.set_visible(is_btrfs)

        # 2. Swap Option
        # Verfügbar für alle Dateisysteme (Ext4, XFS, Btrfs), 
        # außer im Modus "Vorhandene Partition nutzen" (da partitionieren wir nicht)
        can_partition = not is_existing
        self.cb_swap_part.set_visible(can_partition)
        # Die Größe nur zeigen, wenn Swap generell möglich UND angehakt ist
        self.row_swap_size.set_visible(can_partition and self.use_swap_partition)

        # 3. Separate /home Partition
        # Verfügbar für Ext4/XFS im Partitionierungs-Modus.
        # Bei Btrfs verstecken wir es, weil wir Subvolumes nutzen.
        show_home_option = can_partition and (not is_btrfs)
        self.home_options_box.set_visible(show_home_option)

        # Logik-Update für Variablen, falls nötig
        if is_btrfs and can_partition:
            # Bei Btrfs deaktivieren wir die Checkbox visuell (da unsichtbar), 
            # setzen aber intern die Logik für Subvolumes.
            pass 

    def _update_home_controls(self):
        self.spin_home_percent.set_sensitive(self.use_separate_home)

    def _update_swap_controls(self):
        # Swap-Größe nur anzeigen, wenn Partitionierungsmodus aktiv UND Swap angehakt
        is_existing = (self.mode == MODE_EXISTING)
        self.row_swap_size.set_visible(self.use_swap_partition and not is_existing)

    def _build_subvols_ui(self):
        self.subvol_rows.clear()
        child = self.subvol_list_box.get_first_child()
        while child:
            self.subvol_list_box.remove(child)
            child = self.subvol_list_box.get_first_child()
        for name, mnt, checked in BTRFS_DEFAULT_SUBVOLS:
            cb = Gtk.CheckButton.new_with_label(f"{name}  →  {mnt}")
            cb.set_active(checked)
            self.subvol_list_box.append(cb)
            self.subvol_rows.append((name, mnt, cb))

    def _refresh_partitions(self):
        self.partition_paths = []
        self.partition_model = Gtk.StringList.new([])  # type: ignore
        self.partition_dropdown.set_model(self.partition_model)
        self.partition_dropdown.set_selected(Gtk.INVALID_LIST_POSITION)
        self.partition_dropdown.set_sensitive(False)
        self.selected_partition = None

        if not self.selected_disk:
            return

        devices = self._run_lsblk() or []
        disk_node = None
        for dev in devices:
            if dev.get("path") == self.selected_disk:
                disk_node = dev
                break

        if not disk_node:
            return

        parts = []
        for child in disk_node.get("children", []) or []:
            if child.get("type") == "part":
                parts.append(child)

        for p in parts:
            name = p.get("path") or p.get("name")
            size = p.get("size", "")
            fstype = p.get("fstype", "") or "unbekannt"
            mnt = p.get("mountpoint")

            if mnt:
                continue

            if name:
                self.partition_paths.append(name)
                self.partition_model.append(f"{name} ({size}, {fstype})")

        if self.partition_paths:
            self.partition_dropdown.set_sensitive(True)
            self.partition_dropdown.set_selected(0)
            self.selected_partition = self.partition_paths[0]


    def _refresh_free_space(self):
        self.free_segments.clear()
        self.free_best_size_mib = 0.0
        if not self.selected_disk or self.mode != MODE_FREE:
            self.lbl_free_info.set_text(_("Freier Platz: —"))
            return

        try:
            out = subprocess.check_output(
                ["parted", "-m", self.selected_disk, "unit", "MiB", "print", "free"],
                text=True,
                stderr=subprocess.DEVNULL,
            )
        except Exception as e:
            self.lbl_free_info.set_text(_("Freier Platz: (konnte nicht ermittelt werden)"))
            return

        for line in out.splitlines():
            line = line.strip()
            if not line or line.startswith("BYT;") or line.startswith(self.selected_disk):
                continue
            parts = line.split(":")
            if len(parts) < 5:
                continue
            _nr, start, end, size, typ = parts[:5]
            if typ != "free":
                continue

            def mib(v: str) -> float:
                v = v.strip()
                if v.endswith("MiB"):
                    return float(v[:-3])
                if v.endswith("GiB"):
                    return float(v[:-3]) * 1024.0
                if v.endswith("KiB"):
                    return float(v[:-3]) / 1024.0
                return float(re.sub(r"[^0-9.]", "", v) or 0.0)

            s = mib(start)
            e = mib(end)
            z = mib(size)
            if z > 1.0:
                self.free_segments.append((s, e, z))

        if not self.free_segments:
            self.lbl_free_info.set_text(_("Freier Platz: 0 GiB (kein unzugewiesener Bereich gefunden)"))
            self.spin_root_gib.set_sensitive(False)
            return

        best = max(self.free_segments, key=lambda t: t[2])
        self.free_best_size_mib = best[2]
        best_gib = best[2] / 1024.0
        self.lbl_free_info.set_text(f"{_('Freier Platz (größter Block)')}: {best_gib:.1f} GiB")

        max_root_gib = max(10, int(best_gib) - (2 if self.use_swap_partition else 0))
        self.spin_root_gib.set_sensitive(True)
        self.spin_root_gib.set_range(10, max_root_gib if max_root_gib > 10 else 10)
        if self.root_size_gib > max_root_gib and max_root_gib >= 10:
            self.root_size_gib = max_root_gib
            self.spin_root_gib.set_value(self.root_size_gib)

    def get_plan(self) -> Dict[str, Any]:
        plan: Dict[str, Any] = {"device": self.selected_disk, "mode": self.mode, "uefi": self.is_uefi}

        plan["auto_layout"] = {
            "filesystem": self.fs_choice,
            "use_separate_home": (
                self.use_separate_home if self.fs_choice != "btrfs" else False
            ),
            "home_size_percent": self.home_size_percent,
            "use_swap_partition": self.use_swap_partition if self.mode != MODE_EXISTING else False,
            "swap_partition_gib": self.swap_partition_gib,
            "esp_size_mib": self.esp_size_mib,
        }

        if self.fs_choice == "btrfs":
            plan["auto_layout"]["subvolumes"] = [
                name for name, _, cb in self.subvol_rows if cb.get_active()
            ]

        if self.mode == MODE_FREE:
            plan["free_space"] = {
                "root_size_gib": self.root_size_gib,
            }

        if self.mode == MODE_EXISTING:
            plan["existing_partition"] = self.selected_partition
            plan["format_existing"] = True

        return plan

    def validate_plan(self):
        if not self.selected_disk:
            raise ValueError(_("Kein Ziel-Datenträger ausgewählt."))

        if self.mode == MODE_FREE:
            if not self.free_segments:
                raise ValueError(
                    _("Kein freier (nicht zugewiesener) Speicherplatz auf diesem Datenträger gefunden.\n"
                    "Bitte nutze den GParted-Button, um Platz zu schaffen.")
                )
            if self.root_size_gib < 10:
                raise ValueError(_("Root-Größe muss mindestens 10 GiB sein."))
            best_gib = self.free_best_size_mib / 1024.0 if self.free_best_size_mib else 0.0
            if self.root_size_gib > best_gib:
                raise ValueError(_("Root-Größe ist größer als der verfügbare freie Speicher."))

        if self.mode == MODE_EXISTING:
            if not self.selected_partition:
                raise ValueError(_("Bitte wähle eine vorhandene Partition aus."))