"""
/***************************************************************************
 Class CDB4AdminDialog

        This is a QGIS plugin for the CityGML 3D City Database.
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2021-09-30
        git sha              : $Format:%H$
        author(s)            : Giorgio Agugiaro
                               Konstantinos Pantelios
        email                : g.agugiaro@tudelft.nl
                               konstantinospantelios@yahoo.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
   Copyright 2021 Giorgio Agugiaro, Konstantinos Pantelios

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 *                                                                         *
 ***************************************************************************/
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
     from ...cdb_tools_main import CDBToolsMain
     from ..gui_db_connector.other_classes import DBConnectionInfo

import os
from psycopg2.extensions import connection as pyconn

from qgis.core import Qgis, QgsMessageLog
from qgis.gui import QgsMessageBar
from qgis.PyQt import uic
from qgis.PyQt.QtCore import Qt, QThread
from qgis.PyQt.QtWidgets import QDialog, QMessageBox, QProgressBar, QVBoxLayout

from ..gui_db_connector.db_connector_dialog import DBConnectorDialog
from ..gui_db_connector.functions import conn_functions as conn_f
from ..shared.dataTypes import CDBPrivType
from ..shared.functions import sql as sh_sql
from ..shared.functions import general_functions as gen_f  
from .other_classes import DefaultSettings, DialogChecks, FeatureType
from .functions import sql
from .functions import tab_install_functions as ti_f
from .functions import tab_install_widget_functions as ti_wf
from .functions import tab_settings_widget_functions as ts_wf
from .functions import threads as thr
from . import admin_constants as c

# This loads the .ui file so that PyQt can populate the plugin with the elements from Qt Designer
FORM_CLASS, _ = uic.loadUiType(os.path.join(os.path.dirname(__file__), "ui", "cdb4_admin_dialog.ui"))

class CDB4AdminDialog(QDialog, FORM_CLASS):
    """Administrator Dialog class of the plugin. The GUI is imported from an external .ui xml
    """

    def __init__(self, cdbMain: CDBToolsMain, parent=None):
        """Constructor."""
        super(CDB4AdminDialog, self).__init__(parent)
        # Set up the user interface from Designer through FORM_CLASS.
        # After self.setupUi() you can access any designer object by doing
        # self.<objectname>, and you can use autoconnect slots
        self.setupUi(self)

        ############################################################
        ## "Standard" variables or constants
        ############################################################

        # Variable to store the plugin name
        self.PLUGIN_NAME: str = cdbMain.PLUGIN_NAME
        # Variable to store the qgis_pkg
        self.QGIS_PKG_SCHEMA: str = cdbMain.QGIS_PKG_SCHEMA

        # Variable to store the name of this dialog (same as the label in the menu)
        self.DLG_NAME_LABEL: str = cdbMain.MENU_LABEL_ADMIN
        # Variable to store the name of this dialog
        # self.DLG_NAME: str = cdbMain.DLG_NAME_ADMIN

        # Variable to store the qgis_pkg_usrgroup_* associated to the current database.
        self.GROUP_NAME: str = None
        # Variable to store the selected cdb_schema name.
        # self.CDB_SCHEMA: str = None
        # Variable to store the selected usr_schema name.
        self.USR_SCHEMA: str = None
        # Variable to store the existing ADEs in the database.
        self.ADEs: list[str] = []

        # Variable to store the current open connection of a database.
        self.conn: pyconn = None
        # Variable to store the existing connection parameters.
        self.DB: DBConnectionInfo = None

        self.msg_bar: QgsMessageBar
        self.bar: QProgressBar
        self.thread: QThread

        # Initialize classes containing requirements, settings, etc.
        self.settings = DefaultSettings()
        self.checks = DialogChecks()

        ############################################################
        ## From here you can add your variables or constants
        ############################################################

        # Registry to store the existing feature types
        self.FeatureTypesRegistry: dict[str, FeatureType] = {}

        # Initialize label values. This is used in order to revert to the original state 
        # in reset operations when original text has already changed.
        # Connection button labels
        self.btnConnectToDb.init_text = c.btnConnectToDbC_t
        
        # Main installation button labels
        self.btnMainInst.init_text = c.btnMainInst_t
        self.btnMainUninst.init_text=  c.btnMainUninst_t
        
        #- SIGNALS  (start)  ################################################################

        #### 'Admin Connection' tab

        # 'Connection' group box signals
        # self.cbxExistingConn.currentIndexChanged.connect(lambda: self.evt_cbxExistingConn_changed(cdbMain))
        self.cbxExistingConn.currentIndexChanged.connect(self.evt_cbxExistingConn_changed)
        self.btnNewConn.clicked.connect(self.evt_btnNewConn_clicked)
        self.btnConnectToDb.clicked.connect(self.evt_btnConnectToDb_clicked)

        # 'Main Installation' group box signals
        self.btnMainInst.clicked.connect(self.evt_btnMainInst_clicked)
        self.btnMainUninst.clicked.connect(self.evt_btnMainUninst_clicked)

        # 'User Installation' group box signals
        self.btnAddUserToGrp.clicked.connect(self.evt_btnAddUserToGrp_clicked)

        self.cbxUser.currentIndexChanged.connect(self.evt_cbxPluginUser_changed)
        self.btnRemoveUserFromGrp.clicked.connect(self.evt_btnRemoveUserFromGrp_clicked)
        self.btnUsrInst.clicked.connect(self.evt_btnUsrInst_clicked)
        self.btnUsrUninst.clicked.connect(self.evt_btnUsrUninst_clicked)

        self.ckbUserRO.toggled.connect(self.evt_ckbUserRO_toggled)
        self.btnSetPriv.clicked.connect(self.evt_btnSetPriv_clicked)

        # Close connection button
        self.btnCloseConn.clicked.connect(self.evt_btnCloseConn_clicked)

        #### 'Installation Settings' tab
        self.ckbSelAllCDBSch.toggled.connect(self.evt_ckbSelAllCDBSch_toggled)
        self.ckbUserRW.toggled.connect(self.evt_ckbUserRW_toggled)

        self.btnResetToDefault.clicked.connect(self.evt_btnResetToDefault_clicked)

        #-SIGNALS  (end)  ################################################################

    ### Required functions BEGIN ############################

    def dlg_reset_all(self) -> None:
        """ Function that resets the all dialog.
        """
        ts_wf.tabSettings_reset(dlg=self)
        ti_wf.tabInstall_reset(dlg=self)

        return None


    def create_progress_bar(self, layout: QVBoxLayout, position: int) -> None:
        """Function that creates a QProgressBar embedded into a QgsMessageBar, in a specific position in the GUI.

        *   :param layout: QLayout of the gui where the bar is to be
                assigned.
            :type layout: QBoxLayout

        *   :param position: The place (index) in the layout to place
                the progress bar
            :type position: int
        """
        # Create QgsMessageBar instance.
        self.msg_bar = QgsMessageBar()

        # Add the message bar into the input layer and position.
        layout.insertWidget(position, self.msg_bar)

        # Create QProgressBar instance into QgsMessageBar.
        self.bar = QProgressBar(parent=self.msg_bar)

        # Setup progress bar.
        self.bar.setAlignment(Qt.AlignLeft|Qt.AlignVCenter)
        self.bar.setStyleSheet("text-align: left;")

        # Show progress bar in message bar.
        self.msg_bar.pushWidget(widget=self.bar, level=Qgis.MessageLevel.Info)


    def evt_update_bar(self, step: int, text: str) -> None:
        """Function to setup the progress bar upon update. Important: Progress Bar needs to be already created
        in cdbMain.admin_gui.msg_bar: QgsMessageBar and cdbMain.admin_gui.bar: QProgressBar.
        This event is not linked to any widet_setup function as it isn't responsible for changes in different 
        widgets in the gui.

        *   :param dialog: The dialog to hold the bar.
            e.g. "admin_dlg" or "loader_dlg"
            :type step: str

        *   :param step: Current value of the progress
            :type step: int

        *   :param text: Text to display on the bar
            :type text: str
        """
        # Show text instead of completed percentage.
        if text:
            self.bar.setFormat(text)

        # Update progress with current step
        self.bar.setValue(step)

    ### Required functions END ############################

    # EVENT FUNCTIONS (begin)  #####################################################################

    #### Events for 'connection' TAB

    # 'Connection' group box events
    def evt_cbxExistingConn_changed(self) -> None:
        """Event that is called when the 'Existing Connection' comboBox (cbxExistingConn) current index changes.
        This function runs every time the current selection of 'Existing Connection' changes.
        """
        # Close the (previous?) open connection.
        if self.conn:
            self.conn.close()

        # Set the current database connection object variable
        self.DB: DBConnectionInfo = self.cbxExistingConn.currentData()

        # Check that the connection parameters were set
        if not self.DB:
            return None # Exit

        # Reset the Database Administration tab from previous runs
        # All tabs are also disabled first.
        ti_wf.tabInstall_reset(dlg=self)
        ts_wf.tabSettings_reset(dlg=self)

        # Enable Install tab.
        self.tabInstall.setDisabled(False)

        # Enable button to connect.
        self.btnConnectToDb.setText(self.btnConnectToDb.init_text.format(db=self.DB.database_name))
        self.btnConnectToDb.setDisabled(False)

    
    def evt_btnNewConn_clicked(self) -> None:
        """Event that is called when the 'New Connection' pushButton (btnNewConn) is pressed.
        Responsible to add VALID new connection to the 'Existing connections'.
        """
        # Bypass the input blockade for the connector dialogue.
        self.setWindowModality(Qt.WindowModal)

        # Create/Show/Execute additional dialog for the new connection
        dlgConnector = DBConnectorDialog()
        dlgConnector.setWindowModality(Qt.ApplicationModal)
        dlgConnector.show()
        dlgConnector.exec_()

        # Add new connection to the Existing connections
        if dlgConnector.conn_params:
            self.cbxExistingConn.addItem(f"{dlgConnector.conn_params.connection_name}", dlgConnector.conn_params)
            # dlgConnector.close()

        # Re-set the input blockage
        self.setWindowModality(Qt.ApplicationModal)


    def evt_btnConnectToDb_clicked(self) -> None:
        """Event that is called when the current 'Connect to {db}' pushButton (btnConnectToDb) is pressed.
        """
        # Variable to store the plugin main dialog.
        db = self.DB

        msg: str = None

        # Enable 'Connection Status' group box.
        self.gbxConnStatus.setDisabled(False)

        # -------------------------------------------------------------------------------------------
        # Series of tests to be carried out when I connect as admin.
        #
        # 1) Can I connect to the database?
        #        if yes: Is the PostgreSQL version >= the minimum required one (e.g. 10)? 
        #           If yes, continue
        #           if no, exit
        #        If yes: is the user a superuser?
        #            if yes, continue
        #            if no, exit
        #        if no, exit
        # 2) Is the 3DCityDB installed? 
        #       If yes, is it v. 4?
        #           If yes, continue
        #           iF no, exit
        #       If no, exit
        # 3) Is the QGIS Package already installed?
        #       if yes: Is it a compatible version?
        #                if yes, continue
        #                if no, inform the user that an upgrade is required
        #       if no: continue and offer to install
        # -------------------------------------------------------------------------------------------

        # 1) Can I connect to the database? If yes, continue
        # Attempt to connect to the database
        self.conn = conn_f.open_db_connection(db_connection=self.DB, app_name = self.PLUGIN_NAME)

        if self.conn:
            self.DB.pg_server_version = conn_f.get_posgresql_server_version(dlg=self)
            # Set/update the status check variable
            self.checks.is_conn_successful = True

            # Show database name in the Connection Status
            self.lblConnToDb_out.setText(c.success_html.format(text=" AS ".join([self.DB.db_toc_node_label, self.DB.username])))
            # self.lblConnToDb_out.setText(c.success_html.format(text=db.database_name))

            # Enable 'Close current connection' button.
            self.btnCloseConn.setDisabled(False)
            
            # Get the PosgreSQL version (i.e. v. 10.0)
            postgres_major_version = int(db.pg_server_version.split(".")[0])

            #######################################
            # Only for debugging purposes
            # postgres_major_version: int = 9
            #######################################

            if postgres_major_version >= c.PG_MIN_VERSION:
                # Show PostgreSQL version in green and continue (all fine)
                self.lblPostInst_out.setText(c.success_html.format(text=db.pg_server_version))
            else:
                # Show database name we are connected to
                self.lblConnToDb_out.setText(c.success_html.format(text=db.database_name))
                # Show unsupported version of PostgreSQL
                self.lblPostInst_out.setText(c.failure_html.format(text=c.PG_VERSION_UNSUPPORTED_MSG))

                # Inform the user
                msg = f"You are connecting to PostgreSQL v. {db.pg_server_version}. This version is not supported anymore, you need v. {c.PG_MIN_VERSION} or higher."
                QMessageBox.critical(self, "Unsupported PostgreSQL version", msg)

                ti_wf.tabInstall_reset(dlg=self)
                ts_wf.tabSettings_reset(dlg=self)
                if self.conn:
                    self.conn.close()

                return None # Exit

            # Check whether the user is an admin.
            is_superuser = sql.is_superuser(dlg=self, usr_name=self.DB.username)

            if is_superuser:
                # Update the label in the Connection status
                self.lblUserPrivileges_out.setText(c.success_html.format(text=c.USER_PRIV_MSG.format(privs=" superuser")))
            else:
                # Update the label in the Connection status
                self.lblUserPrivileges_out.setText(c.failure_html.format(text=c.USER_PRIV_MSG.format(privs="no superuser")))

                # Inform the user
                msg = f"User '{db.username}' is not a database superuser.\n\nPlease contact your database administrator."
                QMessageBox.critical(self, "Insufficient user privileges", msg)

                ti_wf.tabInstall_reset(dlg=self)
                ts_wf.tabSettings_reset(dlg=self)
                if self.conn:
                    self.conn.close()

                return None # Exit

        else: # Connection failed!
            # Set/update the status check variable
            self.checks.is_conn_successful = False
            
            # Update the message in the Connection Status
            self.lblConnToDb_out.setText(c.failure_html.format(text=c.CONN_FAIL_MSG))

            # Inform the user
            msg = "The selected connection to the PostgreSQL server cannot be established.\n\nPlease check whether it is still valid: the connection parameters may have to be updated!"
            QMessageBox.warning(self, "Connection error", msg)

            ti_wf.tabInstall_reset(dlg=self)
            ts_wf.tabSettings_reset(dlg=self)
            if self.conn:
                self.conn.close()

            return None # Exit

        # 2) Is the 3DCityDB installed?
        # Store version into the connection object.
        self.DB.citydb_version = sh_sql.get_3dcitydb_version(dlg=self)

        if self.DB.citydb_version:
            # Set/update the status check variable
            self.checks.is_3dcitydb_installed = True

            citydb_version_major = int(db.citydb_version.split(".")[0])

            #####################################
            # Only for debugging purposes
            # citydb_version_major: int = 3
            #####################################

            if citydb_version_major == c.CDB_MIN_VERSION_MAJOR:
                # Set/update the status check variable
                self.checks.is_3dcitydb_supported = True

                # Show 3DCityDB version
                self.lbl3DCityDBInst_out.setText(c.success_html.format(text=db.citydb_version))
            else:
                # Set/update the status check variable
                self.checks.is_3dcitydb_supported = False

                # Set the label in the Connection Groupbox (Missing citydb installation)
                self.lbl3DCityDBInst_out.setText(c.crit_warning_html.format(text=f"{db.citydb_version} (required v. {c.CDB_MIN_VERSION_MAJOR}.x)"))

                msg = f"The 3D City Database installed in this database is v. {db.citydb_version} and is not supported. You need 3D City Database v. {c.CDB_MIN_VERSION_MAJOR}.x."
                QMessageBox.critical(self, "Unsupported 3D City Database version", msg)

                ti_wf.tabInstall_reset(dlg=self)
                ts_wf.tabSettings_reset(dlg=self)
                if self.conn:
                    self.conn.close()

                return None

        else:
            # Set/update the status check variable
            self.checks.is_3dcitydb_installed = False

            # Set the label in the Connection Groupbox (Missing citydb installation)
            self.lbl3DCityDBInst_out.setText(c.failure_html.format(text=c.CDB_FAIL_MSG))

            # Inform the user
            msg = "The 3D City Database is not installed in this database."
            QMessageBox.critical(self, "No 3DCityDB found", msg)

            ti_wf.tabInstall_reset(dlg=self)
            ts_wf.tabSettings_reset(dlg=self)
            if self.conn:
                self.conn.close()

            return None # Exit

        # From here on, no need to exit the function anymore with return None.

        # 3) Is the QGIS Package already installed?

        # Fill out the labels of the buttons, no matter what.
        self.btnMainInst.setText(self.btnMainInst.init_text.format(db=db.database_name))
        self.btnMainUninst.setText(self.btnMainUninst.init_text.format(db=db.database_name))

        # Enable 'Main installation' group box.
        self.gbxMainInst.setDisabled(False)

        # Check if the qgis_pkg schema (main installation) is installed in database.
        is_qgis_pkg_installed = sh_sql.is_qgis_pkg_installed(dlg=self)

        ##########################################
        # Only for debugging purposes
        # is_qgis_pkg_installed: bool = False
        ##########################################

        if is_qgis_pkg_installed:
            # Set/update the status check variable
            self.checks.is_qgis_pkg_installed = True
           
            # Get the current qgis_pkg version and check that it is compatible.
            # Named tuple: full_version, major_version, minor_version, minor_revision, code_name, release_date
            qgis_pkg_curr_version = sh_sql.get_qgis_pkg_version(dlg=self)
            # print(f"Loaded QGIS Package version: {qgis_pkg_curr_version.version}")

            qgis_pkg_curr_version_txt      : str = qgis_pkg_curr_version.version         # e.g. 0.9.1
            qgis_pkg_curr_version_major    : int = qgis_pkg_curr_version.major_version   # e.g. 0
            qgis_pkg_curr_version_minor    : int = qgis_pkg_curr_version.minor_version   # e.g. 9
            qgis_pkg_curr_version_minor_rev: int = qgis_pkg_curr_version.minor_revision  # e.g. 1

            ###########################################################
            # Only for testing purposes
            # qgis_pkg_curr_version_major: int     = 0
            # qgis_pkg_curr_version_minor: int     = 9
            # qgis_pkg_curr_version_minor_rev: int = 5
            # qgis_pkg_curr_version_txt: str = ".".join([str(qgis_pkg_curr_version_major), str(qgis_pkg_curr_version_minor), str(qgis_pkg_curr_version_minor_rev)])
            ###########################################################

            # Check that the QGIS Package version is >= than the minimum required for this version of the plugin (see cdb4_constants.py)
            if all((qgis_pkg_curr_version_major == c.QGIS_PKG_MIN_VERSION_MAJOR,
                    qgis_pkg_curr_version_minor == c.QGIS_PKG_MIN_VERSION_MINOR,
                    qgis_pkg_curr_version_minor_rev >= c.QGIS_PKG_MIN_VERSION_MINOR_REV,
                    )):

                # Set/update the status check variable
                self.checks.is_qgis_pkg_supported = True

                # Show message in Connection Status the Qgis Package is installed (and version)
                self.lblMainInst_out.setText(c.success_html.format(text=c.INST_SUCC_MSG + " (v. " + qgis_pkg_curr_version_txt + ")").format(pkg=self.QGIS_PKG_SCHEMA))

                # Enable the close connection button
                self.btnCloseConn.setDisabled(False)

                # Initialize the FeatureTypeRegistry
                ti_f.initialize_feature_type_registry(dlg=self)

                # Get and assign the variable with the group name
                # group name (qgis_pkg_usrgroup_*) assigned to self.GROUP_NAME
                sql.create_qgis_pkg_usrgroup_name(dlg=self)
                # print('self.GROUP_NAME:', self.GROUP_NAME)

                # Set the current user schema name for the current db admin user (e.g. postgres)
                self.USR_SCHEMA = sh_sql.create_qgis_usr_schema_name(dlg=self)
                # print('self.USR_SCHEMA:', self.USR_SCHEMA)

                # Finish setting up the GUI
                ti_wf.setup_post_qgis_pkg_installation(dlg=self)

            elif any((qgis_pkg_curr_version_major > c.QGIS_PKG_MIN_VERSION_MAJOR,
                      all((qgis_pkg_curr_version_major == c.QGIS_PKG_MIN_VERSION_MAJOR, qgis_pkg_curr_version_minor > c.QGIS_PKG_MIN_VERSION_MINOR))
                    )):
                # This is a future, most likely incompatible version of the QGIS Package.
                # For example: 1.x.x or 0.11.x, if the current is 0.10.x)

                # Set/update the status check variable
                self.checks.is_qgis_pkg_supported = False

                # Update the Connection Status label 
                self.lblMainInst_out.setText(c.warning_html.format(text=c.INST_FAIL_VERSION_MSG))

                # Deactivate the Main Installation Group box
                self.gbxMainInst.setDisabled(True)
                # Deactivate the User Installation Group box
                self.gbxUserInstCont.setDisabled(False)

                # Inform the user
                msg = f"The QGIS Package (v. {qgis_pkg_curr_version_txt}) installed in the database you are connecting to is more recent than the one supported by this plug-in (v. {c.QGIS_PKG_MIN_VERSION_MAJOR}.{c.QGIS_PKG_MIN_VERSION_MINOR}.x).\n\nPlease upgrade your plug-in to a more recent version or contact the database administrator."
                QMessageBox.warning(self, "Unsupported QGIS Package version", msg)

                return None # Exit

            else: # Wrong (outdated) version of QGIS Package
                # Set/update the status check variable
                self.checks.is_qgis_pkg_supported = False

                # Update the Connection Status label 
                self.lblMainInst_out.setText(c.warning_html.format(text=c.INST_FAIL_VERSION_MSG))
                # Dectivate the install button
                self.btnMainInst.setDisabled(True)
                # Activate the uninstall button
                self.btnMainUninst.setDisabled(False)

                # Inform the user
                msg = f"The QGIS Package (v. {qgis_pkg_curr_version_txt}) installed in this database is not supported.\nPlease uninstall it and replace it with the one (v. {c.QGIS_PKG_MIN_VERSION_TXT}) provided herewith."
                QMessageBox.warning(self, "Unsupported QGIS Package version", msg)

        else:  # QGIS Package is not installed
            # Set/update the status check variable
            self.checks.is_qgis_pkg_installed = False

            # Update the label in the Connection status box (QGIS Package is not installed)
            self.lblMainInst_out.setText(c.crit_warning_html.format(text=c.INST_FAIL_MSG.format(pkg=self.QGIS_PKG_SCHEMA)))

            # Fisish setting up the GUI
            ti_wf.setup_post_qgis_pkg_uninstallation(dlg=self)

            
    def evt_btnMainInst_clicked(self) -> None:
        """Event that is called when the 'Install to database' pushButton (btnMainInst) is pressed.
        """
        msg = f"Any previous installation of '{self.QGIS_PKG_SCHEMA}' will be replaced!\n\nDo you want to proceed?"
        res = QMessageBox.question(self, "Installation", msg)
        if res == QMessageBox.Yes:
            # Set the label to the "ongoing" message
            # Upon successful layer creation, the label will be set accordingly.
            self.lblMainInst_out.setText(c.ongoing_html.format(text=c.INST_ONGOING_MSG.format(pkg=self.QGIS_PKG_SCHEMA)))
            # Run the thread
            thr.run_install_qgis_pkg_thread(dlg=self, sql_scripts_path=c.PG_SCRIPTS_INST_PATH, qgis_pkg_schema=self.QGIS_PKG_SCHEMA)

        return None


    def evt_btnMainUninst_clicked(self) -> None:
        """Event that is called when the 'Uninstall from database' pushButton (btnMainUninst) is pressed.
        """
        msg = f"Uninstalling '{self.QGIS_PKG_SCHEMA}'!\n\nDo you want to proceed?"
        res = QMessageBox.question(self, "Uninstallation", msg)
        if res == QMessageBox.Yes:
            # Set the label to the "ongoing" message
            # Upon successful layer creation, the label will be set accordingly.
            self.lblMainInst_out.setText(c.ongoing_html.format(text=c.UNINST_ONGOING_MSG.format(pkg=self.QGIS_PKG_SCHEMA)))
            # Run the thread
            thr.run_uninstall_qgis_pkg_thread(dlg=self)

        return None


    def evt_btnAddUserToGrp_clicked(self) -> None:
        """ Event that is called when the button 'Add to qgis_user_group' is pressed (btnAddUserToGrp).
        """
        usr_name: str = self.cbxSelUser4Grp.currentText()
        index = self.cbxSelUser4Grp.currentIndex()
        sql.add_user_to_qgis_pkg_usrgroup(dlg=self, usr_name=usr_name)

        # Remove the entry from the combox
        self.cbxSelUser4Grp.removeItem(index)

        if self.cbxSelUser4Grp.count() == 0:
            ti_wf.fill_database_users_box(dlg=self, usr_names=None) 
            self.cbxSelUser4Grp.setDisabled(True)
            self.btnAddUserToGrp.setDisabled(True)

        # Update the list of the users that are part of the group in the Installation tab
        usr_names = sql.list_qgis_pkg_usrgroup_members(dlg=self)

        # Refill the combobox of the plugin users
        ti_wf.fill_plugin_users_box(dlg=self, usr_names=usr_names)

        # Inform user
        msg = f"User '{usr_name}' has been succesfully added to the database group '{self.GROUP_NAME}'."
        QgsMessageLog.logMessage(msg, self.PLUGIN_NAME, level=Qgis.MessageLevel.Info)

        return None


    def evt_cbxPluginUser_changed(self) -> None:
        """Event that is called when the 'Selected User' comboBox (cbxUser) current index changes.
        """
        # Retrieve current user from combobox
        usr_name: str = self.cbxUser.currentText()
        # print("current usr_name", usr_name)

        if not usr_name or usr_name == "None available":
            # Disable the Create usr_schema button
            self.btnUsrInst.setDisabled(True)
            # Disable the Drop usr_schema button
            self.btnUsrUninst.setDisabled(True)
            # Clean the label of the user scheama installation.
            self.lblUserInst_out.setText(None)

            return None # Exit, as there is nothing to do.
        
        # Create the corresponding usr_schema, add the usr_name to the group (if it is not yet member), 
        # set it to the self.USR_SCHEMA
        self.USR_SCHEMA = sh_sql.create_qgis_usr_schema_name(dlg=self, usr_name=usr_name)
    
        # Set the labels of the buttons
        # self.btnUsrInst.setText(self.btnUsrInst.init_text.format(usr=usr_name))
        # self.btnUsrUninst.setText(self.btnUsrUninst.init_text.format(usr=usr_name))
        
        # Check if usr_schema is already installed in the database.
        is_usr_pkg_installed = sh_sql.is_usr_schema_installed(dlg=self)

        if is_usr_pkg_installed:
            # Update the lable in the connection status box
            self.lblUserInst_out.setText(c.success_html.format(text=c.INST_SUCC_MSG.format(pkg=self.USR_SCHEMA)))

            # Disable the remove from group button
            self.btnRemoveUserFromGrp.setDisabled(True)
            # Disable the Create usr_schema button
            self.btnUsrInst.setDisabled(True)
            # Enable the Drop usr_schema button
            self.btnUsrUninst.setDisabled(False)

            # 3) User privileges settings

            # Enable privileges groupbox
            self.gbxPriv.setDisabled(False)

            # Retrieve the list of cdb_schemas with their privileges status
            # Function returns a list of named tuples (cdb_schema, co_number, priv_type)
            cdb_schemas = sql.list_cdb_schemas_privs(dlg=self, usr_name=usr_name)
            # Fill the combobox of the cdb_schemas in the groupbox
            ti_wf.fill_cdb_schemas_privs_box(dlg=self, cdb_schemas_with_priv=cdb_schemas)

        else:
            # Update the label in the connection status box
            self.lblUserInst_out.setText(c.crit_warning_html.format(text=c.INST_FAIL_MSG.format(pkg=self.USR_SCHEMA)))
 
            # Enable the remove from group button
            self.btnRemoveUserFromGrp.setDisabled(False)
            # Enable the create usr_schema button
            self.btnUsrInst.setDisabled(False)
            # Disable the create usr_schema button
            self.btnUsrUninst.setDisabled(True)

            # Reset and disable the user privileges groupbox
            ti_wf.gbxPriv_reset(dlg=self) # this also disables it.


    def evt_btnRemoveUserFromGrp_clicked(self) -> None:
        """ Event that is called when the button 'Remove from group' is pressed (btnRemoveUserFromGrp).
        """
        usr_name = self.cbxUser.currentText()
        index = self.cbxUser.currentIndex()

        # Remove user from the group in the database.
        sql.remove_user_from_qgis_pkg_usrgroup(dlg=self, usr_name=usr_name)

        # Remove the entry from the combox and update the current combobox
        # It automatically fires the event self.evt_cbxPluginUser_changed())
        self.cbxUser.removeItem(index)

        # Update the current in case 
        if self.cbxUser.count() == 0:
            # Update the plugin users combobox
            ti_wf.fill_plugin_users_box(dlg=self, usr_names=None) # this also disables it

            # Disable the remove from group button
            self.btnRemoveUserFromGrp.setDisabled(True)
            # Disable the User schema Install button
            self.btnUsrInst.setDisabled(True)
            # Disable the User schema Uninstall button
            self.btnUsrUninst.setDisabled(True)
            # Reset and disable the user privileges groupbox
            ti_wf.gbxPriv_reset(dlg=self) # this also disables it.

        # Update the combobox with the database usr_names

        # Update the list of the users that are part of the group in the Installation tab
        usr_names = sql.list_qgis_pkg_non_usrgroup_members(dlg=self)
        # Refill the combobox of the plugin users
        ti_wf.fill_database_users_box(dlg=self, usr_names=usr_names)

        # Inform user
        msg = f"User '{usr_name}' has been succesfully removed from group '{self.GROUP_NAME}'"
        QgsMessageLog.logMessage(msg, self.PLUGIN_NAME, level=Qgis.MessageLevel.Info)

        return None


    def evt_btnUsrInst_clicked(self) -> None:
        """Event that is called when the 'Create schema for user' pushButton (btnUsrInst) is pressed.
        """
        msg = f"Any previous installation of '{self.USR_SCHEMA}' will be replaced!\n\nDo you want to proceed?"
        res = QMessageBox.question(self, "Installation", msg)
        if res == QMessageBox.Yes:
            # Set the label to the "ongoing" message
            # Upon successful layer creation, the label will be set accordingly.
            self.lblUserInst_out.setText(c.ongoing_html.format(text=c.INST_ONGOING_MSG.format(pkg=self.USR_SCHEMA)))
            # Run the thread
            sql.create_qgis_usr_schema(dlg=self)
        if not res: # Query was canceled by user, or error occurred.
            return None

        # Create QgsMessageBar instance.
        self.msg_bar = QgsMessageBar()
        # Add the message bar into the input layer and position.
        self.vLayoutUserInstGroup.insertWidget(2, self.msg_bar)
        
        if sh_sql.is_usr_schema_installed(dlg=self):

            # Retrieve current user from combobox
            usr_name: str = self.cbxUser.currentText()
            # print("current usr_name", usr_name)

            # Disable the remove from group button
            self.btnRemoveUserFromGrp.setDisabled(True)
            # Disable the create usr_schema button
            self.btnUsrInst.setDisabled(True)
            # Enable the drop usr_schema button
            self.btnUsrUninst.setDisabled(False)

            # Enable the User Privileges Group
            self.gbxPriv.setDisabled(False)

            # Retrieve the list of cdb_schemas with their privileges status
            # Function returns a list of named tuples (cdb_schema, co_number, priv_type)
            cdb_schemas  = sql.list_cdb_schemas_privs(dlg=self, usr_name=usr_name)
            # Fill the combobox of the cdb_schemas in the groupbox
            ti_wf.fill_cdb_schemas_privs_box(dlg=self, cdb_schemas_with_priv=cdb_schemas)

            # Replace with Success msg.
            msg = self.msg_bar.createMessage(c.INST_SUCC_MSG.format(pkg=self.USR_SCHEMA))
            self.msg_bar.pushWidget(widget=msg, level=Qgis.MessageLevel.Success, duration=5)

            # Inform user
            self.lblUserInst_out.setText(c.success_html.format(text=c.INST_SUCC_MSG.format(pkg=self.USR_SCHEMA)))
            QgsMessageLog.logMessage(message=c.INST_SUCC_MSG.format(pkg=self.USR_SCHEMA), tag=self.PLUGIN_NAME, level=Qgis.MessageLevel.Success, notifyUser=True)
        else:
            # Enable the remove from group button
            self.btnRemoveUserFromGrp.setDisabled(False)
            # Enable the create usr_schema button
            self.btnUsrInst.setDisabled(False)
            # Disable the create usr_schema button
            self.btnUsrUninst.setDisabled(True)

            # Reset and disable the user privileges groupbox
            ti_wf.gbxPriv_reset(dlg=self) # this also disables it.

            # Replace with Failure msg.
            msg = self.msg_bar.createMessage(c.INST_FAIL_MSG.format(pkg=self.USR_SCHEMA))
            self.msg_bar.pushWidget(widget=msg, level=Qgis.MessageLevel.Critical, duration=5)

            # Inform user
            self.lblUserInst_out.setText(c.crit_warning_html.format(text=c.INST_FAIL_MSG.format(pkg=self.USR_SCHEMA)))
            QgsMessageLog.logMessage(message=c.INST_FAIL_MSG.format(pkg=self.USR_SCHEMA), tag=self.PLUGIN_NAME, level=Qgis.MessageLevel.Critical, notifyUser=True)


    def evt_btnUsrUninst_clicked(self) -> None:
        """Event that is called when the 'Drop schema for user' pushButton
        (btnUsrUninst) is pressed.
        """
        msg = f"Uninstalling user schema '{self.USR_SCHEMA}'!\n\nDo you want to proceed?"
        res = QMessageBox.question(self, "Uninstallation", msg)
        if res == QMessageBox.Yes:
            # Set the label to the "ongoing" message
            # Upon successful layer creation, the label will be set accordingly.
            self.lblUserInst_out.setText(c.ongoing_html.format(text=c.UNINST_ONGOING_MSG.format(pkg=self.USR_SCHEMA)))
            # Run the thread
            thr.run_drop_usr_schema_thread(dlg=self)
        else: # Query was cancelled by user, or error occurred.
            return None


    def evt_ckbSelAllCDBSch_toggled(self) -> None:
        """ Toggles the status of the ALL selection checkbox. 
        If checked, it will turn all entries to checked.
        If unchecked, it will set all entries to unchecked.
        """
        status: bool = self.ckbSelAllCDBSch.isChecked()

        if status: # Selected/Checked
            # Select all items in combobox with cdb_schemas, set status to 2 (Checked)
            for i in range(self.ccbSelCDBSch.count()):
                 self.ccbSelCDBSch.setItemCheckState(i, Qt.Checked)
            # Disable the drop down menu
            self.ccbSelCDBSch.setDisabled(True)
        else:
            # Unselect all items in combobox with cdb_schemas, set status to 0 (Unchecked)
            for i in range(self.ccbSelCDBSch.count()):
                 self.ccbSelCDBSch.setItemCheckState(i, Qt.Unchecked)
            # Enable the drop down menu
            self.ccbSelCDBSch.setDisabled(False)
            
        return None


    def evt_btnSetPriv_clicked(self) -> None:
        """ Event that is called when the button 'Set (privileges)' is pressed (btnSetPriv).
        """
        # Get the selected plugin user
        sel_usr_name: str = self.cbxUser.currentText()
        # print ("Selected user:", sel_usr_name)
        if not sel_usr_name:
            return None # Exit, something went wrong

        # Store all available cdb_schemas, regardless of the selection
        # At least the default citydb should exist.
        all_cdb_schemas: list[str] = []
        sel_cdb_schemas: Optional[list[str]] = []

        for i in range(0, self.ccbSelCDBSch.count()):
            all_cdb_schemas.append(self.ccbSelCDBSch.itemData(i))
        # print("all_cdb_schemas:", all_cdb_schemas)

        if self.ckbSelAllCDBSch.isChecked():
            sel_cdb_schemas = None
        else:
            # Get the selected cdb_schemas
            sel_cdb_schemas = gen_f.get_checkedItemsData(self.ccbSelCDBSch)
            # print ("Selected cdb_schemas:", sel_cdb_schemas)

            # Check that at least a cdb_schema has been selected
            if len(sel_cdb_schemas) == 0:
                msg = "You must select at least one citydb schema!"
                QMessageBox.warning(self, "Invalid selection", msg)
                return None # Exit and do nothing

        selected_priv = self.cbxSelPriv.currentText()
        # print ("Selected priv:", selected_priv)

        if selected_priv == "Revoke ALL privileges":
            sql.revoke_qgis_usr_privileges(dlg=self, usr_name=sel_usr_name, cdb_schemas=sel_cdb_schemas)
            if not sel_cdb_schemas:
                msg = f"For user '{sel_usr_name}', privileges revoked from ALL citydb schemas."            
            else: 
                msg = f"For user '{sel_usr_name}', privileges revoked from citydb schemas: {sel_cdb_schemas}."  

        elif selected_priv == "Grant Read-only":
            sql.grant_qgis_usr_privileges(self, usr_name=sel_usr_name, priv_type=CDBPrivType.READ_ONLY, cdb_schemas=sel_cdb_schemas)
            if not sel_cdb_schemas:
                msg = f"For user '{sel_usr_name}', read-only privileges granted to ALL citydb schemas."            
            else: 
                msg = f"For user '{sel_usr_name}', read-only privileges granted to citydb schemas: {sel_cdb_schemas}."  

        elif selected_priv == "Grant Read & Write":
            sql.grant_qgis_usr_privileges(self, usr_name=sel_usr_name, priv_type=CDBPrivType.READ_WRITE, cdb_schemas=sel_cdb_schemas)
            if not sel_cdb_schemas:
                msg = f"For user '{sel_usr_name}', read & write privileges granted to ALL citydb schemas."            
            else: 
                msg = f"For user '{sel_usr_name}', read & write privileges granted to citydb schemas: {sel_cdb_schemas}."  


        # Retrieve again the list of cdb_schemas and their privileges status
        # Function returns a list of named tuples (cdb_schema, co_number, priv_type)
        cdb_schemas = sql.list_cdb_schemas_privs(dlg=self, usr_name=sel_usr_name)
        # Fill the combobox of the cdb_schemas in the groupbox
        ti_wf.fill_cdb_schemas_privs_box(dlg=self, cdb_schemas_with_priv=cdb_schemas)

        # Inform the user
        QMessageBox.information(self, "Setting privileges", msg)
        QgsMessageLog.logMessage(msg, self.PLUGIN_NAME, level=Qgis.MessageLevel.Info)

        return None


    def evt_btnCloseConn_clicked(self) -> None:
        """Event that is called when the 'Close current connection' pushButton
        (btnCloseConn) is pressed.
        """
        ts_wf.tabSettings_reset(dlg=self)
        ti_wf.tabInstall_reset(dlg=self)
        
        # Close the current open connection.
        if self.conn is not None:
            self.conn.close()

        return None

    ####  Events for 'Settings' TAB

    def evt_ckbUserRO_toggled(self) -> None:
        """Toggles the status of the checkbox to install the default RO user on off.
        If off, it also disables the child checkbox regarding schema installation and privileges.
        """
        status: bool = self.ckbUserRO.isChecked()
        if status:
            self.ckbUserROAccess.setDisabled(False)
        else:
            self.ckbUserROAccess.setChecked(False)
            self.ckbUserROAccess.setDisabled(True)

        return None


    def evt_ckbUserRW_toggled(self) -> None:
        """Toggles the status of the checkbox to install the default RW user on off.
        If off, it also disables the child checkbox regarding schema installation and privileges.
        """
        status: bool = self.ckbUserRW.isChecked()
        if status:
            self.ckbUserRWAccess.setDisabled(False)
        else:
            self.ckbUserRWAccess.setChecked(False)
            self.ckbUserRWAccess.setDisabled(True)

        return None


    def evt_btnResetToDefault_clicked(self) -> None:
        """Event that is called when the button 'Reset to default values' is clicked
        """
        # Reset and enables the Default Users groupbox
        ts_wf.gbxDefaultUsers_reset(self) # also disables
        self.gbxDefaultUsers.setDisabled(False)

        return None

    #-EVENT FUNCTIONS (end) #####################################################################