#!/usr/bin/env python3

import sys
import subprocess
import time
import math
import os
import re
from datetime import datetime, timedelta
import select
import psutil

from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QWizard, QInputDialog, QWizardPage, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QProgressBar, QApplication, QMessageBox, QLineEdit, QFileDialog, QListWidget, QListWidgetItem
from PyQt5.QtCore import QThread, pyqtSignal, QProcess, QStorageInfo
from PyQt5.QtCore import Qt, QTimer

sys.path.append(os.path.dirname(__file__) + '/Resources')

import selectdisk # bundled

# NOTE: The return type of a QThread in Qt is void.
# A QThread does not return any values directly.
# Instead, a QThread can emit signals or communicate with other threads or objects
# using thread-safe mechanisms such as Qt's signals and slots mechanism
# or by using a thread-safe queue

class BackupThread(QThread):
    progress_update = pyqtSignal(int)
    operationSucceeded = pyqtSignal(bool)

    def __init__(self, dataset, destination_pool_name):
        super().__init__()
        self.destination_pool_name = destination_pool_name
        self.dataset = dataset

    def run(self):

        # Get the size of the dataset to be backed up
        command = ['zfs', 'get', '-H', '-p', '-o', 'value', 'used', self.dataset]
        print(' '.join(command))
        size_output = subprocess.check_output(command)
        size = int(float(size_output.strip()))
        print("Size: {}".format(size))

        # Check if snapshot already exists
        snapshot_exists = False
        try:
            subprocess.check_output(['zfs', 'list', '-H', '-t', 'snapshot', self.dataset + '@keepthisforincrementalbackup'])
            snapshot_exists = True
            print("Source snapshot '%s' already exists" % (self.dataset + '@keepthisforincrementalbackup'))
        except subprocess.CalledProcessError:
            print("Source snapshot '%s' does not exist" % (self.dataset + '@keepthisforincrementalbackup'))
            snapshot_exists = False

        if snapshot_exists:
            # Destroy the existing snapshot
            print("Destroying snapshot '%s'" % self.dataset + '@keepthisforincrementalbackup')
            command = ['sudo', '-A', '-E', 'zfs', 'destroy', self.dataset + '@keepthisforincrementalbackup']
            print(' '.join(command))
            try:
                result = subprocess.check_output(command)
                print(result)
            except:
                print("Error destroying snapshot '%s'" % (self.dataset + '@keepthisforincrementalbackup'))
                self.operationSucceeded.emit(False)


        # Make a snapshot of the dataset so that we can send it while it is mounted
        print("Creating snapshot '%s'" % self.dataset + '@keepthisforincrementalbackup')
        try:
            command = ['sudo', '-A', '-E', 'zfs', 'snapshot', self.dataset + '@keepthisforincrementalbackup']
            print(' '.join(command))
            result = subprocess.check_output(command)
            print(result)
        except subprocess.CalledProcessError as e:
            print("Error creating snapshot '%s'" % (self.dataset + '@keepthisforincrementalbackup'))
            self.operationSucceeded.emit(False)


        # Start the send process using zfs send
        zfs_command = ['sudo', '-A', '-E', 'zfs', 'send', '-c', self.dataset + '@keepthisforincrementalbackup']
        print(' '.join(zfs_command))
        send_proc = subprocess.Popen(zfs_command, stdout=subprocess.PIPE)      

        # Start the receive process using zfs receive
        destination_dataset_path = self.dataset.replace('/', '_')
        print("Destination dataset path: %s" % destination_dataset_path)
        destination_dataset_name = f"{self.destination_pool_name}/{destination_dataset_path}@{datetime.now().strftime('%Y%m%d_%H%M%S')}"
        print("Destination dataset name: %s" % destination_dataset_name)
        zfs_receive_command = ['sudo', '-A', '-E', "zfs", "receive", "-v", "-F", destination_dataset_name]
        print(' '.join(zfs_receive_command))
        receive_proc = subprocess.Popen(zfs_receive_command, stdin=send_proc.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        count = 0
        start_time = time.time()
        # Read the output of zfs send and parse it to get the progress
        while True:
            # Check if the send process has finished
            if send_proc.poll() is not None:
                # The send process has finished
                break

            # Check if the send process has outputted any data
            if select.select([send_proc.stdout], [], [], 0.0)[0]:
                # The send process has outputted data
                # Read the data
                data = send_proc.stdout.readline()
                # Parse the data
                # UnicodeDecodeError: 'utf-8' codec can't decode byte 0xac in position 8: invalid start byte
                match = re.match(r'^.*\s(\d+)%.*$', str(data))
                if match:
                    # We have found a progress update
                    # Get the progress
                    progress = int(match.group(1))
                    # Update the progress bar
                    self.progress_update.emit(progress)
                    print("Progress: %d%%" % progress)
                    # Calculate the time remaining
                    elapsed_time = time.time() - start_time
                    if elapsed_time > 0:
                        time_remaining = (elapsed_time / progress) * (100 - progress)
                    else:
                        time_remaining = 0
                    print("Time remaining: %d seconds" % time_remaining)
                    # Calculate the transfer rate
                    transfer_rate = (count / elapsed_time) / 1024 / 1024
                    print("Transfer rate: %d MB/s" % transfer_rate)
                    # Calculate the estimated size of the backup
                    estimated_size = (size / progress) * 100
                    print("Estimated size: %d bytes" % estimated_size)
                    # Calculate the estimated datetime of completion
                    estimated_completion = datetime.now() + timedelta(seconds=time_remaining)
                    print("Estimated completion: %s" % estimated_completion.strftime('%Y-%m-%d %H:%M:%S'))

        # Check if the send process has finished
        if send_proc.poll() is None:
            # The send process has not finished
            # Kill the send process
            send_proc.kill()
            # Wait for the send process to finish
            send_proc.wait()

        # Check if the send process exited successfully
        if send_proc.returncode != 0:
            print("Error sending dataset")
            self.operationSucceeded.emit(False)

        # TODO: Delete the snapshot on the source?
        # This would mean that the next incremental backup would be larger
        # Delete the snapshot
        # command = ['sudo', '-A', '-E', 'zfs', 'destroy', self.dataset + '@keepthisforincrementalbackup']

        self.operationSucceeded.emit(True)
        
class BackupWizard(QWizard):
    def __init__(self):
        super().__init__()

        # Quit the application when the wizard is closed
        self.finished.connect(self.close)

        self.dataset = "zroot/var/log"
        self.destination_pool_name = None

        self.setWizardStyle(QWizard.MacStyle)
        self.setPixmap(QWizard.BackgroundPixmap, QPixmap(os.path.dirname(__file__) + '/Resources/background.png'))

        self.setWindowTitle("Backup Wizard")
        # Instantiate the pages with this wizard as the parent
        welcome_page = WelcomePage(self)
        progress_page = ProgressPage(self)
        summary_page = SummaryPage(self)
        
        self.addPage(WelcomePage())
        self.addPage(ExplanationPage())
        # self.addPage(SaveFilePage())
        self.addPage(DestinationPoolPage())
        self.addPage(ProgressPage())
        self.addPage(SummaryPage())

        self.setFixedSize(600, 400)


class WelcomePage(QWizardPage):
    def __init__(self, parent=None):
        super().__init__(parent)
        
    def initializePage(self):
        self.setTitle("Welcome to the ZFS Backup Wizard")
        layout = QVBoxLayout()
        self.setLayout(layout)

        # Check if the dataset exists and how much space it takes up
        try:
            command = ['zfs', 'get', '-H', '-p', '-o', 'value', 'used', self.wizard().dataset]
            print(' '.join(command))
            size_output = subprocess.check_output(command)
            size = int(float(size_output.strip()))
            print("Size: {}".format(size))
            self.wizard().disk_size_needed = size
        except subprocess.CalledProcessError as e:
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("The '%s' dataset dataset does not exist." % self.wizard().dataset)
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            sys.exit(1)

        label = QLabel("This wizard will help you create a backup of the '%s' ZFS dataset." % self.wizard().dataset)
        label.setWordWrap(True)
        layout.addWidget(label)

        gb_needed = self.wizard().disk_size_needed / 1024 / 1024 / 1024
        gb_needed = math.ceil(gb_needed)
        label = QLabel("You need a disk with at least %s GB of free space." % str(gb_needed))
        layout.addWidget(label)

        intro_label = QLabel()
        intro_text = "To protect your valuable data it is essential to have multiple copies of your data to ensure that you can recover it in the event of a failure. Since all storage media can fail, the recommended strategy is the 3-2-1 backup method, which involves having at least three copies of your data, with two local copies on different drives and one off-site backup, such as a drive stored in a secure location or a cloud backup service. Using a spinning drive for backups and avoiding SSDs is recommended due to the risk of data loss when the disk is not attached to power for a prolonged amount of time. A USB drive is an excellent option for local backups as it is easy to use and has good forward compatibility."
        intro_label.setText(intro_text)
        intro_label.setWordWrap(True)
        layout.addWidget(intro_label)

        # Check if the output of "mount" contains "/media/.uzip" as an indication that we are running from a Live system
        proc = QProcess()
        command = 'mount'
        args = []
        print("Starting %s %s" % (command, args))
        proc.start(command, args)
        proc.waitForFinished()
        if "/media/.uzip" in str(proc.readAllStandardOutput()):
            print("Live system detected")
            # Show error message in a QMessageBox
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("This wizard is only available on helloSystem Live ISO.")
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            sys.exit(1)

class DestinationPoolPage(QWizardPage):
    def __init__(self, parent=None):
        super().__init__(parent)

    def initializePage(self):

        # Disable the Next button
        self.wizard().button(QWizard.NextButton).setEnabled(False)

        # List all pools and their available space and let the user choose one
        self.setTitle("Choose a destination pool")
        layout = QVBoxLayout()
        self.setLayout(layout)

        # Add an explanation text
        label = QLabel("Choose a pool to store the backup on.")
        label.setWordWrap(True)
        layout.addWidget(label)

        # Show a list of all available pools to choose as a destination
        self.pool_listwidget = QListWidget()
        layout.addWidget(self.pool_listwidget)

        # Create a QLabel to show the available space on the selected pool
        self.pool_space = QLabel()
        if self.pool_listwidget.currentItem():
            self.updatePoolSpace()
        layout.addWidget(self.pool_space)

        # When the user selects a different pool, update the available space label
        self.pool_listwidget.currentItemChanged.connect(self.onSelectionChanged)

        # Add a button to create a new pool
        self.create_pool_button = QPushButton("Create new pool")
        self.create_pool_button.clicked.connect(self.createPool)
        layout.addWidget(self.create_pool_button)

        # zfs import all pools; we can only backup to a pool that is already imported
        try:
            command = ['sudo', '-A', '-E', 'zpool', 'import', '-a']
            print(' '.join(command))
            subprocess.check_output(command)
        except subprocess.CalledProcessError as e:
            print("Error importing pools")
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("Error importing pools.")
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()

        # Periodically check for new pools
        self.pool_listwidget_timer = QTimer()
        self.pool_listwidget_timer.timeout.connect(self.showAvailablePools)
        self.pool_listwidget_timer.start(1000)
        self.showAvailablePools()

    # Stop the timer when the user leaves this page and
    # prevent the user from leaving this page if no pool is selected
    def validatePage(self):
        if self.pool_listwidget.currentItem():
            self.wizard().destination_pool_name = self.pool_listwidget.currentItem().text()
            self.pool_listwidget_timer.stop()
            return True
        return False
        
    def showAvailablePools(self):

        # Get the name of the currently selected pool
        current_pool_name = None
        if self.pool_listwidget.currentItem():
            current_pool_name = self.pool_listwidget.currentItem().text()

        # Get a list of all pools
        try:
            command = ['zpool', 'list', '-H', '-o', 'name']
            print(' '.join(command))
            pool_list_output = subprocess.check_output(command)
            pool_list = pool_list_output.decode('utf-8').splitlines()
            print("Pools: {}".format(pool_list))
        except subprocess.CalledProcessError as e:
            print("Error getting list of pools")
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("Error getting list of pools.")
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            
        # Get the available space on each pool
        pool_list_with_space = []
        for pool in pool_list:
            try:
                command = ['zfs', 'get', '-H', '-p', '-o', 'value', 'available', pool]
                print(' '.join(command))
                available_output = subprocess.check_output(command)
                available = int(float(available_output.strip()))
                print("Available: {}".format(available))
                pool_list_with_space.append((pool, available))
            except subprocess.CalledProcessError as e:
                print("Error getting available space on pool '%s'" % pool)
                error_dialog = QMessageBox()
                error_dialog.setIcon(QMessageBox.Critical)
                error_dialog.setText("Error getting available space on pool '%s'." % pool)
                error_dialog.setWindowTitle("Error")
                error_dialog.exec_()

        # # Check if we can do an incremental send to the destination pool
        # # First, see whether the destination pool has any snapshots and if so, get the latest one
        # try:
        #     command = ['zfs', 'get', '-H', '-p', '-o', 'value', 'creation', '-t', 'snapshot', self.wizard().destination_pool_name]
        #     print(' '.join(command))
        #     snapshots = subprocess.check_output(command).strip().splitlines()
        #     if snapshots:
        #         latest_snapshot = snapshots[-1]
        #     else:
        #         latest_snapshot = None
        # except subprocess.CalledProcessError as e:
        #     print("Error getting snapshots on pool '%s'" % self.wizard().destination_pool_name)
        #     # Show error message in a QMessageBox
        #     error_dialog = QMessageBox()
        #     error_dialog.setIcon(QMessageBox.Critical)
        #     error_dialog.setText("Error getting snapshots on pool '%s'." % self.wizard().destination_pool_name)
        #     error_dialog.setWindowTitle("Error")
        #     error_dialog.exec_()
        #     # Prevent the wizard from continuing
        #     self.wizard().button(QWizard.NextButton).setEnabled(False)
        #     return
        #
        # # Check if the latest snapshot is older than the oldest snapshot on the source pool
        # if latest_snapshot:
        #     try:
        #         command = ['zfs', 'get', '-H', '-p', '-o', 'value', 'creation', '-t', 'snapshot', self.wizard().source_pool_name]
        #         print(' '.join(command))
        #         snapshots = subprocess.check_output(command).strip().splitlines()
        #         if snapshots:
        #             oldest_snapshot = snapshots[0]
        #         else:
        #             oldest_snapshot = None
        #     except subprocess.CalledProcessError as e:
        #         print("Error getting snapshots on pool '%s'" % self.wizard().source_pool_name)
        #         # Show error message in a QMessageBox
        #         error_dialog = QMessageBox()
        #         error_dialog.setIcon(QMessageBox.Critical)
        #         error_dialog.setText("Error getting snapshots on pool '%s'." % self.wizard().source_pool_name)
        #         error_dialog.setWindowTitle("Error")
        #         error_dialog.exec_()
        #         # Prevent the wizard from continuing
        #         self.wizard().button(QWizard.NextButton).setEnabled(False)
        #         return

        #     if oldest_snapshot:
        #         try:
        #             command = ['zfs', 'get', '-H', '-p', '-o', 'value', 'creation', '-t', 'snapshot', latest_snapshot]
        #             print(' '.join(command))
        #             latest_snapshot_creation = subprocess.check_output(command).strip()
        #             command = ['zfs', 'get', '-H', '-p', '-o', 'value', 'creation', '-t', 'snapshot', oldest_snapshot]
        #             print(' '.join(command))
        #             oldest_snapshot_creation = subprocess.check_output(command).strip()
        #             if latest_snapshot_creation < oldest_snapshot_creation:
        #                 # The latest snapshot is older than the oldest snapshot on the source pool, so we can do an incremental send
        #                 self.wizard().incremental_send = True
        #                 self.wizard().latest_snapshot = latest_snapshot
        #             else:
        #                 self.wizard().incremental_send = False
        #                 self.wizard().latest_snapshot = None
        #         except subprocess.CalledProcessError as e:
        #             print("Error getting creation time of snapshots")
        #             # Show error message in a QMessageBox
        #             error_dialog = QMessageBox()
        #             error_dialog.setIcon(QMessageBox.Critical)
        #             error_dialog.setText("Error getting creation time of snapshots.")
        #             error_dialog.setWindowTitle("Error")
        #             error_dialog.exec_()
        #             # Prevent the wizard from continuing
        #             self.wizard().button(QWizard.NextButton).setEnabled(False)
        #             return
        #     else:
        #         self.wizard().incremental_send = False
        #         self.wizard().latest_snapshot = None

        # Add the pools to the list widget
        self.pool_listwidget.clear()
        for pool in pool_list_with_space:
            item = QListWidgetItem(pool[0])
            item.setData(Qt.UserRole, pool[1])
            self.pool_listwidget.addItem(item)

        # Disable pools in which the source is located
        for i in range(self.pool_listwidget.count()):
            if self.pool_listwidget.item(i).text() == self.wizard().dataset.split('/')[0]:
                self.pool_listwidget.item(i).setFlags(Qt.NoItemFlags)
                self.pool_listwidget.item(i).setToolTip("The pool contains the source dataset.")
        
        # Disable pools that do not have enough space
        for i in range(self.pool_listwidget.count()):
            if self.pool_listwidget.item(i).data(Qt.UserRole) < self.wizard().disk_size_needed:
                self.pool_listwidget.item(i).setFlags(Qt.NoItemFlags)
        
        # Disable pools that are already mounted
        for i in range(self.pool_listwidget.count()):
            try:
                command = ['zfs', 'get', '-H', '-p', '-o', 'value', 'mounted', self.pool_listwidget.item(i).text()]
                print(' '.join(command))
                mounted_output = subprocess.check_output(command)
                mounted = mounted_output.strip()
                print("Mounted: {}".format(mounted))
                if mounted == b'true':
                    self.pool_listwidget.item(i).setDisabled(True)
                    self.pool_listwidget.item(i).setToolTip("The pool is already mounted.")
            except subprocess.CalledProcessError as e:
                print("Error getting mount status of pool '%s'" % self.pool_listwidget.item(i).text())
                error_dialog = QMessageBox()
                error_dialog.setIcon(QMessageBox.Critical)
                error_dialog.setText("Error getting mount status of pool '%s'." % self.pool_listwidget.item(i).text())
                error_dialog.setWindowTitle("Error")
                error_dialog.exec_()

        # Select the pool that was selected before
        if current_pool_name != None:
            for i in range(self.pool_listwidget.count()):
                if self.pool_listwidget.item(i).text() == current_pool_name:
                    self.pool_listwidget.setCurrentRow(i)
                    break

    def createPool(self):

        self.pool_listwidget_timer.stop()

        # Show a dialog to select a disk
        w = selectdisk.DiskSelectionWidget()

        # Show the dialog and wait for the user to select a disk
        w.show()
        while w.selected_disk == None:
            QApplication.processEvents()
        
        # Print the selected disk to stdout
        selected_device = None
        if w.selected_disk != None:
            print("/dev/%s" % w.selected_disk)
            selected_device = "/dev/%s" % w.selected_disk
        else:
            print("No disk selected")
            return

        print("Selected device: {}".format(selected_device))

        # Ask the user for a name for the new pool
        new_pool_name, ok = QInputDialog.getText(self, 'Create new pool', 'Enter a name for the new pool:')
        if not ok:
            return

        # Check if the pool name is valid
        if not re.match(r'^[a-zA-Z0-9_]+$', new_pool_name):
            print("Invalid pool name '%s'" % new_pool_name)
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("Invalid pool name '%s'." % new_pool_name)
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            return

        # Check if the pool name is already in use
        try:
            command = ['zpool', 'list', '-H', '-o', 'name']
            print(' '.join(command))
            pool_list_output = subprocess.check_output(command)
            pool_list = pool_list_output.decode('utf-8').splitlines()
            print("Pools: {}".format(pool_list))
        except subprocess.CalledProcessError as e:
            print("Error getting list of pools")
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("Error getting list of pools.")
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            return

        if new_pool_name in pool_list:
            print("Pool name '%s' is already in use" % new_pool_name)
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("Pool name '%s' is already in use." % new_pool_name)
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            return

        # Get a list of all ZFS pools and their devices

        # Create a list of device:pool tuples
        device_pool_list = []
                
        # Run the "zpool status" command and capture the output
        output = subprocess.check_output(["zpool", "status"])
        lines = output.decode("utf-8").split("\n")
        current_pool = None
        current_devices = []
        for line in lines:
            # If the line starts with "pool:", it's a new pool
            if line.startswith("  pool:"):
                # If we were already processing a pool, print its devices
                if current_pool is not None:
                    print(current_pool + ": " + ", ".join(current_devices))
                # Start processing the new pool
                current_pool = line.split()[1].strip()
                print("Current pool: {}".format(current_pool))
                current_devices = []
            # If the line starts with whitespace and a device name, add it to the list
            elif current_pool is not None:
                # Skip if stripped line is empty
                if not line.strip():
                    continue
                # Skip if the first field in the stripped line equals the current pool name
                if line.strip().split()[0] == current_pool:
                    continue
                # Check if stripped line ends with a number
                if line.strip()[-1].isdigit():
                    # split the line on whitespace and add the first element to the list
                    current_devices.append(line.split()[0])
                    # Add to the list of device:pool tuples
                    device_pool_list.append((line.split()[0], current_pool))

        # Print the list of device:pool tuples
        print("Device:pool list: {}".format(device_pool_list))

        # List of all pools that use the selected device
        pools_using_selected_device = []
        for device, pool in device_pool_list:
            if device == selected_device.replace("/dev/", ""):
                pools_using_selected_device.append(pool)
        
        # If the selected device is already in use, ask the user if they want to destroy the existing pool
        if pools_using_selected_device:
            # Show a dialog to ask the user if they want to destroy the existing pool
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Warning)
            msg.setText("The selected device is already in use by the following pools:")
            msg.setInformativeText(", ".join(pools_using_selected_device) + "\n\nDo you want to destroy the existing pool(s)? This will delete all data on the pool(s) and cannot be undone.")
            msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
            msg.setDefaultButton(QMessageBox.No)
            ret = msg.exec_()
            if ret == QMessageBox.No:
                print("Not destroying existing pool")
                return
            else:
                print("Destroying existing pool")
                # Destroy the existing pool
                for pool in pools_using_selected_device:
                    command = ['sudo', '-A', '-E', 'zpool', 'destroy', pool]
                    print(' '.join(command))
                    try:
                        subprocess.check_output(command, stderr=subprocess.STDOUT)
                    except subprocess.CalledProcessError as e:
                        print("Error destroying pool '%s'" % pool)
                        error_dialog = QMessageBox()
                        error_dialog.setIcon(QMessageBox.Critical)
                        error_dialog.setText("Error destroying pool '%s'." % pool)
                        # Set informative text to the error message from stderr
                        error_dialog.setInformativeText(e.output.decode('utf-8'))
                        error_dialog.setWindowTitle("Error")
                        error_dialog.exec_()
                        return
          
        # Destroy any zfs metadata on the disk
        try:
            # Check stderr for errors
            command = ['sudo', '-A', '-E', 'zpool', 'labelclear', '-f', selected_device]
            print(' '.join(command))
            subprocess.check_output(command, stderr=subprocess.STDOUT)
        except subprocess.CalledProcessError as e:
            print("Error destroying zfs metadata on '%s'" % selected_device)
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("Error destroying zfs metadata on '%s'." % selected_device)
            # Set informative text to the error message from stderr
            error_dialog.setInformativeText(e.output.decode('utf-8'))
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            return

        # Create the new pool
        try:
            # Create a pool that is not mounted by default and has no mountpoint
            # Check stderr for errors
            command = ['sudo', '-A', '-E', 'zpool', 'create', '-f', '-m', 'none', new_pool_name, selected_device]
            print(' '.join(command))
            subprocess.check_output(command, stderr=subprocess.STDOUT)
        except subprocess.CalledProcessError as e:
            print("Error creating pool '%s'" % new_pool_name)
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("Error creating pool '%s'." % new_pool_name)
            # Set informative text to the error message from stderr
            error_dialog.setInformativeText(e.output.decode('utf-8'))
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            return

        self.pool_listwidget_timer.start()

    def onSelectionChanged(self):
        self.wizard().button(QWizard.NextButton).setEnabled(True)
        self.updatePoolSpace()
        
    def updatePoolSpace(self):
        if self.pool_listwidget.currentItem():
            available_space_in_gb = round(self.pool_listwidget.currentItem().data(Qt.UserRole) / 1024 / 1024 / 1024)
            self.pool_space.setText("Available space: {} GB".format(available_space_in_gb))
        else:
            self.pool_space.setText("")

    def checkDestinationPool(self):

        # Check available space on the destination pool
        try:
            command = ['zfs', 'get', '-H', '-p', '-o', 'value', 'available', self.wizard().destination_pool_name]
            print(' '.join(command))
            available = 0
            available_output = subprocess.check_output(command)
            available = int(float(available_output.strip()))
            print("Available: {}".format(available))
        except subprocess.CalledProcessError as e:
            print("Error getting available space on pool '%s'" % self.wizard().destination_pool_name)
            # Show error message in a QMessageBox
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("Error getting available space on pool '%s'." % self.wizard().destination_pool_name)
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            # Prevent the wizard from continuing
            self.wizard().button(QWizard.NextButton).setEnabled(False)
            return     

        if available < self.wizard().disk_size_needed:
            available_gb = math.floor(available / 1024 / 1024 / 1024)
            size_gb = math.floor(self.wizard().disk_size_needed / 1024 / 1024 / 1024)
            print("Not enough space on pool '%s'" % self.wizard().destination_pool_name)
            print("Available: %d GB" % available_gb)
            print("Size: %d GB" % size_gb)
            # Show error message in a QMessageBox
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("Not enough space on pool '%s'." % self.wizard().destination_pool_name)
            error_dialog.setInformativeText("Available: %d GB, Size: %d GB" % (available_gb, size_gb))
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            # Prevent the wizard from continuing
            self.wizard().button(QWizard.NextButton).setEnabled(False)

class SaveFilePage(QWizardPage):
    def __init__(self, parent=None):
        super().__init__(parent)

    def initializePage(self):
        
        self.setTitle("Save backup file")
        self.setSubTitle("Specify the location to save the backup file.")
                
        layout = QHBoxLayout()
        self.setLayout(layout)
        
        self.file_edit = QLineEdit()
        self.browse_button = QPushButton("Browse...")
        self.browse_button.clicked.connect(self.browse)
        
        layout.addWidget(self.file_edit)
        layout.addWidget(self.browse_button)
        
        self.registerField("file_path*", self.file_edit)
    
    def browse(self):
        # Set QFileDialog::AnyFile
        dialog = QFileDialog()
        dialog.setFileMode(QFileDialog.AnyFile)
        file_path, _ = dialog.getSaveFileName(self, "Save ZFS backup image file", "", "ZFS image files (*.zfs)")
        if file_path:
            self.file_edit.setText(file_path)

    def validatePage(self):
        # Path must not be stored in self.dataset
        file_path = self.field("file_path")
        # Get absolute normalized path
        file_path = os.path.abspath(os.path.normpath(file_path))
        print("User selected file path: {}".format(file_path))
        # From the zroot, remove everything before the first slash
        forbidden_path = "/" + "/".join(self.wizard().dataset.split("/")[1:])
        print("Forbidden path: {}".format(forbidden_path))
        if (file_path.startswith(forbidden_path)):
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("You cannot save the backup file in the dataset '%s' which is about to being backed up." % self.wizard().dataset)
            error_dialog.setInformativeText("Please choose another disk.")
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            return False
            
        # If the path already exists, it must not be a directory
        if os.path.exists(file_path):
            if not os.path.isfile(file_path):
                error_dialog = QMessageBox()
                error_dialog.setIcon(QMessageBox.Critical)
                error_dialog.setText("The backup file must not be an existing directory")
                error_dialog.setWindowTitle("Error")
                error_dialog.exec_()
                return False

        # Get the size of the destination disk
        try:
            # Find the mountpoint of the partition that contains the file using Qt
            mountpoint = QStorageInfo(file_path).rootPath()
            
            # Using Qt, find out the free space on the destination disk
            disk_size_available = QStorageInfo(mountpoint).bytesAvailable()
            print("Size: {}".format(disk_size_available))
            self.wizard().disk_size_available = disk_size_available

        except subprocess.CalledProcessError as e:
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("Could not determine the size of the destination disk.")
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            return False
        
        # Check if the destination disk is large enough to hold the backup file
        if self.wizard().disk_size_available < self.wizard().disk_size_needed:
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("The destination disk is not large enough to hold the backup file.")
            gb_needed = self.wizard().disk_size_needed / 1024 / 1024 / 1024
            gb_needed = math.ceil(gb_needed)
            # Find out how much space is available using Qt
            gb_available = self.wizard().disk_size_available / 1024 / 1024 / 1024
            gb_available = math.floor(gb_available)
            # Set additional details
            error_dialog.setInformativeText("The backup file needs %s GB, but the destination disk has only %s GB." % (gb_needed, gb_available))
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            return False

        return True
    
class ProgressPage(QWizardPage):
    def __init__(self, parent=None):
        super().__init__()

        self.setTitle("Backup Progress")
        self.setSubTitle("Please wait while the backup is in progress...")

        layout = QVBoxLayout()

        self.progress_label = QLabel()
        layout.addWidget(self.progress_label)

        self.progress_bar = QProgressBar()
        layout.addWidget(self.progress_bar)

        self.setLayout(layout)

        self.backup_thread = None
    
    def initializePage(self):
        self.start_backup()

    def start_backup(self):
        self.backup_thread = BackupThread(self.wizard().dataset, self.wizard().destination_pool_name)
        self.backup_thread.progress_update.connect(self.update_progress)
        # self.backup_thread.operationSucceeded.connect(self.on_thread_finished) # AttributeError: 'PyQt5.QtCore.pyqtSignal' object has no attribute 'connect
        # The correct way to connect a signal to a slot is to use the connect() method of the signal.
        # Instead, do this:
        # Connect the signal to the slot
        self.backup_thread.operationSucceeded.connect(self.on_thread_finished) # This errors as shown above
        # Connect the signal to the slot

        
        # Disallow going back to previous pages while the backup is in progress
        self.backup_thread.started.connect(lambda: self.wizard().setOption(QWizard.NoBackButtonOnStartPage, True))
        self.backup_thread.operationSucceeded.connect(lambda: self.wizard().setOption(QWizard.NoBackButtonOnStartPage, False))
                       
        self.backup_thread.start()

    def update_progress(self, percent):
        self.progress_bar.setValue(percent)
        self.progress_label.setText("Backup in progress: {}%".format(percent))

    def on_thread_finished(self, success):
        if success:
            # The backup was successful
            print("Backup successful")
            self.wizard().next()
        else:
            # The backup failed
            print("Backup failed")
            # Show an error message
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("The backup failed.")
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            # Exit the wizard
            self.wizard().reject()

class SummaryPage(QWizardPage):
    def __init__(self, parent=None):
        super().__init__()

        self.setTitle("Summary")
        self.setSubTitle("The backup process has finished.")

        self.label = QLabel()
        self.layout = QVBoxLayout()
        self.layout.addWidget(self.label)
        self.setLayout(self.layout)

    def initializePage(self):
        self.label.setText("Backup was successful.")
        # Disable the Back button and don't allow the user to go back to previous pages
        self.wizard().setOption(QWizard.NoBackButtonOnLastPage, True)
        self.wizard().setOption(QWizard.NoBackButtonOnStartPage, True)
        # Show the Finish button
        self.wizard().setOption(QWizard.HaveFinishButtonOnEarlyPages, True)
        # Don't show the Next button
        self.wizard().setOption(QWizard.HaveNextButtonOnLastPage, False)
        # Dont' show the Cancel button
        self.wizard().setOption(QWizard.HaveCustomButton1, False)



class ExplanationPage(QWizardPage):
    def __init__(self, parent=None):
        super().__init__(parent)
        
    def initializePage(self):
        self.setTitle("Information")
        layout = QVBoxLayout()
        self.setLayout(layout)

        intro_label = QLabel()
        intro_text = """When performing a ZFS send/receive incremental backup, it is not necessary to keep snapshots on both the source and destination datasets. Once the incremental backup has been successfully received on the destination dataset, you can delete the snapshots on the source dataset if you no longer need them.

However, if you delete a snapshot on the source dataset that was previously used as the base for incremental backups, future incremental backups will need to use the next oldest snapshot as the base. This means that you will need to keep at least one snapshot on the source dataset that can serve as the base for future incremental backups.

Deleting snapshots on the source dataset will not affect the integrity or availability of the data on the destination dataset, but it will increase the amount of data that needs to be transferred during future incremental backups.
"""
        intro_label.setText(intro_text)
        intro_label.setWordWrap(True)
        layout.addWidget(intro_label)

if __name__ == '__main__':
    app = QApplication(sys.argv)

    # Check if another zfs operation is already running with psutil
    for process in psutil.process_iter():
        if process.name() == "zfs":
            error_dialog = QMessageBox()
            error_dialog.setIcon(QMessageBox.Critical)
            error_dialog.setText("Another ZFS operation is already running.")
            error_dialog.setInformativeText("Please wait until the other operation has finished.")
            error_dialog.setWindowTitle("Error")
            error_dialog.exec_()
            sys.exit(1)

    wizard = BackupWizard()
    wizard.show()

    sys.exit(app.exec_())
