from __future__ import annotations

import argparse
import os
import logging
import threading

# Environment variable HDF5_USE_FILE_LOCKING must be set *before* importing HDF5 libraries
# This prevents file locking issues, especially in shared filesystems (e.g., NFS).
os.environ["HDF5_USE_FILE_LOCKING"] = "FALSE"

from .hdf5widget import Hdf5TreeView  # noqa
from .utils import (  # noqa
    generateInputs,
    extractScanNumber,
    generateUniquePath,
    getScanEnergy,
    FilenameCompleterLineEdit,
)
from ewoks.bindings import execute_graph  # noqa
from ewoks.bindings import submit_graph  # noqa

import PyQt5.QtCore  # noqa
from silx.gui import qt, icons  # noqa
from silx.gui.utils.concurrent import submitToQtMainThread  # noqa

from ..utils import FLATFIELD_DEFAULT_DIR, NEWFLAT_FILENAME, OLDFLAT_FILENAME  # noqa

_logger = logging.getLogger(__name__)
_FILE_EXTENSIONS = ".h5", ".hdf5", ".hdf", ".nx", ".nxs", ".nx5", ".nexus"


class Id31FAIEwoksMainWindow(qt.QMainWindow):
    """
    This GUI is designed for reprocessing HDF5 scans data with fast azimuthal
    integration using Ewoks workflows (ewoksXRPD)
    """

    _SETTINGS_VERSION_STR: str = "1"
    _PYFAI_METHODS: tuple[str] = (
        "no_csr_cython",
        "bbox_csr_cython",
        "full_csr_cython",
        "no_csr_ocl_gpu",
        "bbox_csr_ocl_gpu",
        "full_csr_ocl_gpu",
        "no_histogram_cython",
        "bbox_histogram_cython",
        "full_histogram_ocl_gpu",
    )
    _MONITOR_NAMES: tuple[str] = "mondio", "scaled_detdio", "scaled_mondio", "scur"

    _WORKFLOW: str = "integrate_with_saving_with_flat.json"
    _WORKFLOW_LOAD_OPTIONS: dict = {"root_module": "ewoksid31.workflows"}
    _HOSTS: tuple[str] = "Local", "Ewoks worker"

    def __init__(self, parent: qt.QWidget | None = None):
        super().__init__(parent)
        self.setWindowTitle("pyFAI with EWOKS - ID31")
        self.resize(1000, 800)

        self.statusBar = qt.QStatusBar()
        self.setStatusBar(self.statusBar)

        # Set paths
        self._defaultDirectoryRaw = ""
        self._flatFieldDirName = FLATFIELD_DEFAULT_DIR

        # Central Layout setup
        centralWidget = qt.QWidget(self)
        self.setCentralWidget(centralWidget)
        mainLayout = qt.QVBoxLayout(centralWidget)

        # HDF5 Viewer
        self._hdf5Widget = Hdf5TreeView()
        mainLayout.addWidget(self._hdf5Widget)

        # Menu and Toolbar setup
        self._setupMenuAndToolBar()

        # pyFAI Config Section
        pyFaiGroupBox = qt.QGroupBox("pyFAI config")
        pyFaiLayout = qt.QGridLayout()

        self._configFileLineEdit = FilenameCompleterLineEdit()
        self._configFileLineEdit.setPlaceholderText("/path/to/pyfai_config.json")

        loadConfigFileButton = qt.QPushButton("Open...")
        loadConfigFileButton.setIcon(icons.getQIcon("document-open"))
        loadConfigFileButton.clicked.connect(self._loadConfigFileButtonClicked)

        pyFaiLayout.addWidget(self._configFileLineEdit, 0, 0, 1, 1)
        pyFaiLayout.addWidget(loadConfigFileButton, 0, 1, 1, 1)

        pyFaiGroupBox.setLayout(pyFaiLayout)
        mainLayout.addWidget(pyFaiGroupBox)

        # pyFAI Options Section
        pyFaiOptionsGroupBox = qt.QGroupBox("pyFAI options")
        pyFaiOptionsLayout = qt.QGridLayout()

        spacer = qt.QSpacerItem(
            20, 10, qt.QSizePolicy.Expanding, qt.QSizePolicy.Minimum
        )
        pyFaiOptionsLayout.addItem(spacer, 0, 2, 2, 1)

        self._sigmaClipCheckBox = qt.QCheckBox("Sigma clipping threshold:")
        self._sigmaClipCheckBox.setChecked(False)
        self._sigmaClipCheckBox.setToolTip(
            "Check to enable sigma clipping and set threshold."
        )

        self._sigmaClipThresholdSpinBox = qt.QDoubleSpinBox()
        self._sigmaClipThresholdSpinBox.setSingleStep(0.1)
        self._sigmaClipThresholdSpinBox.setRange(0.1, 10.0)
        self._sigmaClipThresholdSpinBox.setValue(3.0)
        self._sigmaClipThresholdSpinBox.setDecimals(1)
        self._sigmaClipThresholdSpinBox.setEnabled(False)

        self._sigmaClipCheckBox.toggled.connect(
            self._sigmaClipThresholdSpinBox.setEnabled
        )
        self._sigmaClipCheckBox.toggled.connect(self._sigmaClipCheckBoxToggled)

        pyFaiOptionsLayout.addWidget(
            self._sigmaClipCheckBox, 0, 0, 1, 1, qt.Qt.AlignLeft
        )
        pyFaiOptionsLayout.addWidget(
            self._sigmaClipThresholdSpinBox, 0, 1, 1, 1, qt.Qt.AlignLeft
        )

        pyFaiMethodLabel = qt.QLabel("Integration method:")
        self._pyFaiMethodComboBox = qt.QComboBox()
        self._pyFaiMethodComboBox.addItems(self._PYFAI_METHODS)
        self._pyFaiMethodComboBox.setCurrentText("no_histogram_cython")
        self._pyFaiMethodComboBox.setToolTip("Select the pyFAI integration method.")

        pyFaiOptionsLayout.addWidget(pyFaiMethodLabel, 1, 0, 1, 1, qt.Qt.AlignLeft)
        pyFaiOptionsLayout.addWidget(
            self._pyFaiMethodComboBox, 1, 1, 1, 1, qt.Qt.AlignLeft
        )

        monitorNameLabel = qt.QLabel("Monitor name:")
        self._monitorNameComboBox = qt.QComboBox()
        self._monitorNameComboBox.addItems(self._MONITOR_NAMES)
        self._monitorNameComboBox.setCurrentText("scaled_mondio")
        self._monitorNameComboBox.setToolTip(
            "Select the monitor name for normalization."
        )

        pyFaiOptionsLayout.addWidget(monitorNameLabel, 2, 0, 1, 1, qt.Qt.AlignLeft)
        pyFaiOptionsLayout.addWidget(self._monitorNameComboBox, 2, 1, 1, 1)

        pyFaiOptionsGroupBox.setLayout(pyFaiOptionsLayout)
        mainLayout.addWidget(pyFaiOptionsGroupBox)

        # Output Directory Section
        outputGroupBox = qt.QGroupBox("Output directory")
        outputLayout = qt.QHBoxLayout()
        self._outputLineEdit = FilenameCompleterLineEdit()
        self._outputLineEdit.setPlaceholderText("/path/to/output/directory")

        outputDirButton = qt.QPushButton("Select...")
        outputDirButton.setToolTip("Define a new output directory.")
        outputDirButton.setIcon(icons.getQIcon("folder"))
        outputDirButton.clicked.connect(self._outputDirButtonClicked)

        outputLayout.addWidget(self._outputLineEdit)
        outputLayout.addWidget(outputDirButton)

        outputGroupBox.setLayout(outputLayout)
        mainLayout.addWidget(outputGroupBox)

        # Run Section
        executionModeLayout = qt.QHBoxLayout()

        executionModeLabel = qt.QLabel("Host:")
        self._executionModeComboBox = qt.QComboBox()
        self._executionModeComboBox.addItems(self._HOSTS)
        self._executionModeComboBox.setCurrentText("Local")
        self._executionModeComboBox.setToolTip(
            "Select where the process will be executed: locally or on an Ewoks worker."
        )

        runButton = qt.QPushButton("Run")
        runButton.setIcon(icons.getQIcon("next"))
        runButton.clicked.connect(self._runButtonClicked)
        runButton.setToolTip("Run the processing workflow on the selected host.")
        runButton.setMinimumSize(runButton.sizeHint() * 2)

        executionModeLayout.addStretch()
        executionModeLayout.addWidget(runButton)
        executionModeLayout.addWidget(executionModeLabel)
        executionModeLayout.addWidget(self._executionModeComboBox)
        executionModeLayout.addStretch()

        mainLayout.addLayout(executionModeLayout)

    def _showHelpDialogClicked(self):
        """
        Display the Help dialog when the Help button is clicked.
        """
        helpDialog = qt.QMessageBox(self)
        helpDialog.setWindowTitle("Help")
        helpDialog.setIcon(qt.QMessageBox.Information)
        helpDialog.setText("Welcome to the pyFAI with EWOKS application help.")
        helpDialog.setInformativeText(
            "How to get started?\n\n"
            f"- Load raw data files {_FILE_EXTENSIONS}.\n"
            "- Load the pyFAI configuration file.\n"
            "- Select an output directory to save the processed data.\n"
            "- Click Run to execute the workflow."
        )
        helpDialog.setStandardButtons(qt.QMessageBox.Ok)
        helpDialog.exec_()

    def _setupMenuAndToolBar(
        self,
    ) -> None:
        """
        Setup of the main menu and toolbar with actions for file handling.
        """
        menuBar = self.menuBar()
        fileMenu = menuBar.addMenu("File")

        openAction = qt.QAction(icons.getQIcon("document-open"), "Open...", self)
        openAction.triggered.connect(self._openActionTriggered)
        openAction.setShortcut(qt.QKeySequence.StandardKey.Open)
        reloadAction = qt.QAction(icons.getQIcon("view-refresh"), "Reload", self)
        reloadAction.triggered.connect(self._hdf5Widget.reloadSelected)
        reloadAction.setShortcut(qt.QKeySequence.StandardKey.Refresh)
        clearAction = qt.QAction(icons.getQIcon("remove"), "Clear", self)
        clearAction.triggered.connect(self._hdf5Widget.clearFiles)
        clearAction.setShortcut(qt.QKeySequence.StandardKey.Delete)

        fileMenu.addActions([openAction, reloadAction, clearAction])

        toolBar = self.addToolBar("File Actions")
        toolBar.addActions([openAction, reloadAction, clearAction])
        toolBar.setMovable(False)
        self.setContextMenuPolicy(qt.Qt.PreventContextMenu)
        toolBar.setFloatable(False)

        spacer = qt.QWidget(self)
        spacer.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)
        toolBar.addWidget(spacer)

        helpAction = qt.QAction(
            qt.QIcon.fromTheme("help-about"), "?", self
        )  # How TODO add an icon that is not existing in silx icons ???
        font = helpAction.font()
        font.setBold(True)
        helpAction.setFont(font)
        helpAction.setToolTip("About this app")
        helpAction.triggered.connect(self._showHelpDialogClicked)
        toolBar.addAction(helpAction)

    def addRawDataFile(self, fileName: str) -> None:
        """
        Proxy method to add a new file to the HDF5 tree viewer.
        """
        if not fileName or not os.path.isfile(fileName):
            qt.QMessageBox.warning(
                self, "Invalid File Format", "The selected file does not exist."
            )
            return
        if not fileName.endswith(_FILE_EXTENSIONS):
            qt.QMessageBox.warning(
                self,
                "Invalid File Format",
                f"Please select a valid HDF5 or NEXUS file {_FILE_EXTENSIONS}.",
            )
            return
        self._hdf5Widget.addFile(fileName)
        self._defaultDirectoryRaw = os.path.dirname(fileName)

    def _openActionTriggered(self) -> None:
        """
        Add Raw data as HDF5 file without cleaning the tree viewer.
        """
        nameFilter = " ".join(f"*{ext}" for ext in _FILE_EXTENSIONS)
        fileName, _ = qt.QFileDialog.getOpenFileName(
            self,
            "Add RAW data file",
            self._defaultDirectoryRaw,
            f"HDF5 files ({nameFilter});;All files (*)",
        )
        if fileName:
            self.addRawDataFile(fileName)

    def _loadConfigFileButtonClicked(self) -> None:
        """
        Choose and import JSON or PONI config file.
        """
        currentDir = os.path.dirname(self.getConfigFilePath()) or os.getcwd()
        filePath, _ = qt.QFileDialog.getOpenFileName(
            self,
            "Open config file",
            currentDir,
            "JSON or PONI files (*.json *.poni);;All files (*)",
        )
        if filePath:
            self.setConfigFilePath(filePath)
        else:
            self.setConfigFilePath("")
            _logger.info("No config file chosen.")

    def getFlatFieldDirName(self) -> str:
        """Returns the flat-field directory where to find flats.mat and oldflats.mat"""
        return self._flatFieldDirName

    def setFlatFieldDirName(self, path: str):
        """Set the directory where to find flats.mat and oldflats.mat"""
        self._flatFieldDirName = os.path.abspath(path)

    def getConfigFilePath(self) -> str:
        """
        Returns the current configuration file path from the line edit.
        """
        return self._configFileLineEdit.text().strip()

    def setConfigFilePath(self, path: str) -> None:
        """
        Update the configuration file path in the line edit.
        """
        self._configFileLineEdit.setText(path)

    def getOutputDirName(self) -> str:
        """
        Returns the current output directory path from the line edit.
        """
        return self._outputLineEdit.text().strip()

    def setOutputDirName(self, path: str) -> None:
        """
        Update the output directory path in the line edit.
        """
        self._outputLineEdit.setText(os.path.abspath(path))

    def _outputDirButtonClicked(self) -> None:
        """
        Choose an output directory using a dialog and set it in the line edit.
        """
        currentPath = self.getOutputDirName()
        if currentPath and os.path.exists(currentPath):
            pass
        else:
            currentPath = os.getcwd()
            _logger.info(f"Path not found. Falling back to: {currentPath}")

        newPath = qt.QFileDialog.getExistingDirectory(
            self, "Choose output directory", currentPath
        )
        if newPath:
            self.setOutputDirName(newPath)

    def _sigmaClipCheckBoxToggled(self, toggled: bool) -> None:
        """
        Update integration methods combo box according to sigma clipping.

        - Change selected method if not supported by sigma clipping
        - Enable/Disable pixel splitting methods depending on sigma clipping
        """
        sigma_clipping_compatible_prefix = "no_csr"

        if toggled and not self._pyFaiMethodComboBox.currentText().startswith(
            sigma_clipping_compatible_prefix
        ):
            self._pyFaiMethodComboBox.setCurrentText("no_csr_ocl_gpu")

        model = self._pyFaiMethodComboBox.model()
        for row in range(self._pyFaiMethodComboBox.count()):
            item = model.item(row)
            if not item.text().startswith(sigma_clipping_compatible_prefix):
                flags = item.flags()
                if toggled:
                    item.setFlags(flags & ~qt.Qt.ItemIsEnabled)
                    item.setToolTip("Not available with sigma clipping")
                else:
                    item.setFlags(flags | qt.Qt.ItemIsEnabled)
                    item.setToolTip("")

    def _getSigmaClippingThreshold(self) -> float | None:
        """
        Retrieve Sigma Clipping threshold from the user interface.

        Returns:
            The Sigma Clipping threshold if enabled.
        """
        if self._sigmaClipCheckBox.isChecked():
            return float(self._sigmaClipThresholdSpinBox.value())
        return None

    def _getParameters(self, datasetFilename: str, scanNumber: int) -> dict | None:
        """
        Generates parameters to execute workflow for a given scan.

        Args:
            datasetFilename: Filename of the HDF5 file containing the scan
            scanNumber: Number of the scan.

        Returns:
            Dictionnary of parameters for the workflow, or None if failed.
        """
        detectorName = "p3"

        energy = getScanEnergy(datasetFilename, scanNumber)

        if energy is not None:
            _logger.info(f"Energy for scan {scanNumber}: {energy}")
        else:
            _logger.error(
                f"Unable to retrieve energy for scan {scanNumber} in file {datasetFilename}."
            )
            return None

        outputDirectory = self.getOutputDirName()

        inputParameters = generateInputs(
            newFlat=os.path.join(self.getFlatFieldDirName(), NEWFLAT_FILENAME),
            oldFlat=os.path.join(self.getFlatFieldDirName(), OLDFLAT_FILENAME),
            energy=energy,
            pyfaiConfig=self.getConfigFilePath(),
            pyfaiMethod=self._pyFaiMethodComboBox.currentText(),
            datasetFilename=datasetFilename,
            scanNumber=scanNumber,
            monitorName=self._monitorNameComboBox.currentText(),
            referenceCounts=1,
            detectorName=detectorName,
            outputDirectory=outputDirectory,
            sigmaClippingThreshold=self._getSigmaClippingThreshold(),
        )
        scanName = f"{os.path.basename(datasetFilename)}::{scanNumber}"
        inputParameters["scanName"] = scanName

        return inputParameters

    def _processScans(self, scans: list[dict], local: bool = True) -> None:
        """
        Execute workflow and save HDF5 data and JSON workflow ewoks file from a single selected scan.

        Executing in a separate thread.

        Args:
            scans: Selected scan nodes
            local: Whether to execute locally or submit to Ewoks worker.
        """
        inputParameters = self._prepareScans(scans)

        if local:
            self._updateStatusBar("Processing scans locally...")
            thread = threading.Thread(
                target=self._executeWorkflowForParams, args=(inputParameters,)
            )
            thread.start()
        else:
            self._updateStatusBar("Submitting scans to Ewoks worker...")
            self._submitWorkflowForParams(inputParameters)

    def _prepareScans(self, scans) -> list[dict]:
        """
        Prepares parameters for each selected scan.
        """
        uniqueScans = set()
        for scan in scans:
            scanNumber = extractScanNumber(scan.physical_name)
            if scanNumber == -1:
                _logger.warning(
                    f"Skipping scan: Invalid scan number in {scan.physical_name}"
                )
                continue
            uniqueScans.add((scan.physical_filename, scanNumber))

        inputParameters = list()
        for rawDataFilename, scanNumber in uniqueScans:
            workflowParameters = self._getParameters(rawDataFilename, scanNumber)
            if not workflowParameters:
                _logger.warning(
                    f"Skipping scan {rawDataFilename}::{scanNumber} due to missing parameters."
                )
                continue

            inputParameters.append(workflowParameters)

        return inputParameters

    def _showErrorInStatusBar(self, message: str) -> None:
        """
        Display an error message in the status bar and log it.

        Args:
            message: The error message to display.
        """
        _logger.error(message)
        submitToQtMainThread(self._updateStatusBar, message, error=True)

    def _executeWorkflowForParams(self, params) -> None:
        """
        Workflow execution for multiple scans in a single thread (locally).

        Args:
            params: List of workflow input parameters for each scan.
        """
        for workflowParameters in params:
            scanName = workflowParameters.pop("scanName", "Unknown scan")
            _logger.info(f"Processing workflow for parameters: {scanName}")
            try:
                execute_graph(
                    self._WORKFLOW,
                    load_options=self._WORKFLOW_LOAD_OPTIONS,
                    **workflowParameters,
                )
                _logger.info(f"Successfully processed scan: {scanName}")
            except Exception as e:
                errorMsg = f"Error processing scan {scanName}: {e}"
                _logger.error(errorMsg)
                submitToQtMainThread(self._updateStatusBar, errorMsg, error=True)
                raise e

        submitToQtMainThread(
            self._updateStatusBar, "Processing completed successfully."
        )

    def _runButtonClicked(self) -> None:
        """
        Handle the run button click. Validate inputs, adjust output directory and process scans based on the selected execution mode.
        """
        if not self._validateInputParameters():
            return
        pass

        selectedScanNodes = list(self._hdf5Widget.getSelectedNodes())
        self._adjustOutputDirectory()

        executionMode = self._executionModeComboBox.currentText()

        if executionMode == "Local":
            self._processScans(selectedScanNodes, local=True)
        elif executionMode == "Ewoks worker":
            self._processScans(selectedScanNodes, local=False)
        else:
            self._updateStatusBar(
                f"Unknown execution mode: {executionMode}.", error=True
            )

    def _processFutures(self, futures: list[tuple]) -> None:
        """
        Process the futures in a separate thread.

        Args:
            futures: A list of tuples containing -> (future, scanName).
        """
        failed = list()
        for future, scanName in futures:
            try:
                future.get()
            except Exception as e:
                _logger.error(f"Workflow failed for {scanName}: {e}")
                failed.append(scanName)

        if failed:
            errorMsg = f"Workflows completed with errors for scans: {', '.join(failed)}"
            submitToQtMainThread(self._updateStatusBar, errorMsg, error=True)

        else:
            successMsg = "All workflows completed successfully."
            submitToQtMainThread(self._updateStatusBar, successMsg)

    def _submitWorkflowForParams(self, params) -> None:
        """
        Submit workflow for multiple scans to the Ewoks worker (remotely).

        Args:
            params: List of workflow input parameters for each scan.
        """
        futures = list()

        for workflowParameters in params:
            scanName = workflowParameters.pop("scanName", "Unknown scan")
            _logger.info(f"Submitting workflow for parameters: {scanName}")

            try:
                future = submit_graph(
                    self._WORKFLOW,
                    load_options=self._WORKFLOW_LOAD_OPTIONS,
                    **workflowParameters,
                )
                futures.append((future, scanName))
                _logger.info(f"Successfully submitted scan: {scanName}")
            except Exception as e:
                error_msg = f"Error submitting scan {scanName}: {e}"
                _logger.error(error_msg)
                self._updateStatusBar(error_msg, error=True)

        thread = threading.Thread(target=self._processFutures, args=(futures,))
        thread.start()

    def _submitButtonClicked(self) -> None:
        """
        Handle the submit button click. Validate inputs, adjust output directory and submit scans to the worker.
        """

        if not self._validateInputParameters():
            return
        selectedScanNodes = list(self._hdf5Widget.getSelectedNodes())
        self._adjustOutputDirectory()
        self._processScans(selectedScanNodes, local=False)

    def _getValidationErrors(
        self, rawDataLoaded: bool, selectedScanNodes: list, configFileLoaded: bool
    ) -> list[str]:
        """
        Generate a list of validation error messages based on the current state.

        Args:
            rawDataLoaded: Whether raw data is loaded.
            selectedScanNodes: List of selected scan nodes.
            configFileLoaded: Whether a configuration file is loaded.

        Returns:
            A list of error messages if validation fails, otherwise an empty list.
        """
        errors = list()
        if not rawDataLoaded:
            errors.append("❌ No raw data file loaded.")
        if not selectedScanNodes:
            errors.append("❌ No scan selected.")
        if not configFileLoaded:
            errors.append("❌ No pyFAI config file loaded.")
        return errors

    def _showWarningMessage(self, errorMessages: list[str]) -> None:
        """
        Show a warning message box if prerequisites for processing are not met.
        """
        warningMessageBox = qt.QMessageBox(self)
        warningMessageBox.setWindowTitle("Workflow cannot be excecuted")
        warningMessageBox.setIcon(qt.QMessageBox.Warning)
        warningMessageBox.setText("\n".join(errorMessages))
        warningMessageBox.exec_()

    def _validateInputParameters(self) -> bool:
        """
        Validates that all necessary inputs are present before starting processing.

        Returns:
            True if inputs are valid, False otherwise.
        """
        rawDataLoaded = not self._hdf5Widget.isEmpty()
        selectedScanNodes = self._hdf5Widget.getSelectedNodes()
        configFileLoaded = bool(self.getConfigFilePath())

        errorMessages = self._getValidationErrors(
            rawDataLoaded, selectedScanNodes, configFileLoaded
        )

        if errorMessages:
            self._updateStatusBar(" ".join(errorMessages), error=True)
            _logger.warning(f"Processing cannot start: {' '.join(errorMessages)}")
            self._showWarningMessage(errorMessages)
            return False

        for node in selectedScanNodes:
            _logger.info(
                f"Selected node for processing: {node.physical_filename}::{node.physical_name}"
            )
        return True

    def _adjustOutputDirectory(self) -> None:
        """
        Adjusts the output directory if it already exists.
        """
        currentOutputDir = self.getOutputDirName()
        if os.path.exists(currentOutputDir):
            newOutputDirectory = generateUniquePath(currentOutputDir)

            if not self._showConfirmationMessage(newOutputDirectory):
                _logger.info("Process canceled by the user.")
                return

            os.makedirs(newOutputDirectory, exist_ok=True)
            self.setOutputDirName(newOutputDirectory)
            _logger.info(f"New output directory created: {newOutputDirectory}")

        else:
            self.setOutputDirName(currentOutputDir)
            _logger.info(f"Output directory set to: {currentOutputDir}")

    def _showConfirmationMessage(self, newOutputDirectory: str) -> bool:
        """Show a confirmation message box when creating a new directory for output file.

        Returns True is the user confirms, otherwise False.
        """
        confirmationMessageBox = qt.QMessageBox(self)
        confirmationMessageBox.setWindowTitle("Output Directory Warning")
        confirmationMessageBox.setIcon(qt.QMessageBox.Warning)
        confirmationMessageBox.setText("The output directory is not empty.")
        confirmationMessageBox.setInformativeText(
            f"A new directory will be created:\n{newOutputDirectory}"
        )
        confirmationMessageBox.setStandardButtons(
            qt.QMessageBox.Ok | qt.QMessageBox.Cancel
        )
        userResponse = confirmationMessageBox.exec_()
        return userResponse == qt.QMessageBox.Ok

    def _updateStatusBar(self, message: str, error: bool = False):
        """
        Updates the status bar with a message.

        If 'error' is True, displays the message in red.
        """
        self.statusBar.setStyleSheet("color: red;" if error else "")
        self.statusBar.showMessage(message)

    def loadSettings(self) -> None:
        """
        Load user settings.
        """
        settings = qt.QSettings()

        if settings.value("version") != self._SETTINGS_VERSION_STR:
            _logger.info("Setting version mismatch. Clearing settings.")
            settings.clear()
            return

        geometry = settings.value("mainWindow/geometry")
        if geometry:
            self.restoreGeometry(geometry)

        self._defaultDirectoryRaw = settings.value("parameters/inputDirectory", "")
        self.setConfigFilePath(settings.value("parameters/configFile", ""))
        outputDirectory = settings.value("parameters/outputDirectory", "")
        self.setOutputDirName(outputDirectory)

        self._sigmaClipCheckBox.setChecked(
            settings.value("sigmaClipping/enabled", False, type=bool)
        )
        self._sigmaClipThresholdSpinBox.setValue(
            settings.value("sigmaClipping/threshold", 3, type=float)
        )

        integrationMethod = settings.value("pyFAI/integrationMethod", None)
        if integrationMethod in self._PYFAI_METHODS:
            self._pyFaiMethodComboBox.setCurrentText(integrationMethod)

        monitorName = settings.value("pyFAI/monitorName", None)
        if monitorName in self._MONITOR_NAMES:
            self._monitorNameComboBox.setCurrentText(monitorName)

        executionMode = settings.value("runSection/executionMode", None)
        if executionMode in self._HOSTS:
            self._executionModeComboBox.setCurrentText(executionMode)

    def closeEvent(self, event: qt.QCloseEvent) -> None:
        """
        Save user settings on application close.
        """
        settings = qt.QSettings()

        settings.setValue("version", self._SETTINGS_VERSION_STR)
        settings.setValue("mainWindow/geometry", self.saveGeometry())
        settings.setValue("parameters/inputDirectory", self._defaultDirectoryRaw)
        settings.setValue("parameters/configFile", self.getConfigFilePath())
        settings.setValue("parameters/outputDirectory", self.getOutputDirName())
        settings.setValue("sigmaClipping/enabled", self._sigmaClipCheckBox.isChecked())
        settings.setValue(
            "sigmaClipping/threshold", float(self._sigmaClipThresholdSpinBox.value())
        )
        settings.setValue(
            "pyFAI/integrationMethod", self._pyFaiMethodComboBox.currentText()
        )
        settings.setValue("pyFAI/monitorName", self._monitorNameComboBox.currentText())

        settings.setValue(
            "runSection/executionMode", self._executionModeComboBox.currentText()
        )

        event.accept()


def main():
    logging.basicConfig(level=logging.INFO)

    parser = argparse.ArgumentParser(
        description="Perform azimuthal integration of selected scans"
    )
    parser.add_argument(
        "-f",
        "--fresh",
        action="store_true",
        help="Start without loading previous user preferences",
    )
    parser.add_argument(
        "-i",
        "--input",
        type=str,
        required=False,
        help="Dataset file to process (HDF5 format)",
        default="",
        metavar="FILE",
    )
    parser.add_argument(
        "-o",
        "--output-dir",
        type=str,
        required=False,
        help="Folder where to store the results",
        default="",
        metavar="FOLDER",
    )
    parser.add_argument(
        "-c",
        "--pyfai-config",
        type=str,
        default=None,
        help="PyFAI config file (.json)",
        metavar="FILE",
    )
    parser.add_argument(
        "--flat-dir",
        type=str,
        required=False,
        help=f"Folder containing flat-field files: flats.mat and old_flats.mat (default: {FLATFIELD_DEFAULT_DIR})",
        default=FLATFIELD_DEFAULT_DIR,
        metavar="FOLDER",
    )

    args = parser.parse_args()

    app = qt.QApplication([])
    app.setOrganizationName("ESRF")
    app.setOrganizationDomain("esrf.fr")
    app.setApplicationName("id31pyfaiewoks")

    window = Id31FAIEwoksMainWindow()
    window.setAttribute(qt.Qt.WA_DeleteOnClose)

    window.setFlatFieldDirName(args.flat_dir)

    if not args.fresh:
        _logger.info(
            "Launching application in default mode. Loading previous settings."
        )
        window.loadSettings()
    if args.input:
        raw_data = os.path.abspath(args.input)
        if os.path.isfile(raw_data) and raw_data.endswith(_FILE_EXTENSIONS):
            window.addRawDataFile(raw_data)
        else:
            _logger.error(f"Invalid raw data file path or format: {raw_data}")

    if args.output_dir:
        window.setOutputDirName(args.output_dir)

    if args.pyfai_config:
        config_file = os.path.abspath(args.pyfai_config)
        if os.path.isfile(config_file):
            window.setConfigFilePath(config_file)
        else:
            _logger.error(f"Invalid config file path: {config_file}")

    window.show()
    app.exec_()
