import os

from qgis.core import QgsApplication, QgsAuthMethodConfig, QgsSettings
from qgis.PyQt import uic
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QInputDialog, QMessageBox, QWidget

from openlog.datamodel.connection.openlog_connection import Connection
from openlog.toolbelt import PlgTranslator

BASE_SETTINGS_KEY = "/OpenLog/gui/connection/"
LAST_USED_KEY = "/last_used"
LIST_KEY = "/list"
DB_KEY = "/db"
USER_KEY = "/user"
HOST_KEY = "/host"
PORT_KEY = "/port"
PASSWORD_KEY = "/password"
QGS_CONFIG_ID_KEY = "/qgs_config_id"
SERVICE_KEY = "/service"
OPTIONAL_PARAMS_KEY = "/optional_params"


class ConnectionWidget(QWidget):
    class InvalidConnection(Exception):
        pass

    class InvalidInterface(Exception):
        pass

    def __init__(self, parent=None) -> None:
        """
        Database connection widget.
        Can be used to store a psycog2 connection

        Args:
            parent: parent widget
        """
        super().__init__(parent)

        # translation
        self.tr = PlgTranslator().tr

        uic.loadUi(
            os.path.dirname(os.path.realpath(__file__)) + "/connection_widget.ui", self
        )

        self.save_connection_button.setIcon(
            QIcon(QgsApplication.iconPath("mActionFileSave.svg"))
        )
        self.save_connection_button.clicked.connect(self.save_current_connection)

        self.add_connection_button.setIcon(
            QIcon(QgsApplication.iconPath("mActionAdd.svg"))
        )
        self.add_connection_button.clicked.connect(self.add_connection)

        self.remove_connection_button.setIcon(
            QIcon(QgsApplication.iconPath("mActionRemove.svg"))
        )
        self.remove_connection_button.clicked.connect(self._remove_current_connection)

        self.connection_cb.currentIndexChanged.connect(self._set_connection_from_cb)

        self._load_connection()
        self.test_button.clicked.connect(self.test_connection)

    def database_creation_use(self, template_database_name: str = "postgres"):
        """
        Define widget for database creation use (no database name edition)

        Args:
            template_database_name: default database name (optional, default 'postgres')
        """
        self.database_label.setText(self.tr("Default database"))
        self.database_edit.setEnabled(False)
        self.database_edit.setText(template_database_name)

    def _get_connection_type_name(self) -> str:
        """
        Returns connection type name for save in QgsSettings

        Returns: 'no_type' by default, to be defined in interface

        """
        return "no_type"

    def get_connection_from_settings(self, name: str) -> Connection:
        """
        Return openlog connection from user QgsSettings

        Args:
            name: openlog connection name

        Returns: openlog connection

        """
        base_key = BASE_SETTINGS_KEY + self._get_connection_type_name() + "/" + name

        settings = QgsSettings()
        connection = Connection(
            name=name,
            host=settings.value(base_key + HOST_KEY),
            port=settings.value(base_key + PORT_KEY),
            database=settings.value(base_key + DB_KEY),
            user=settings.value(base_key + USER_KEY),
            qgs_config_id=settings.value(base_key + QGS_CONFIG_ID_KEY),
            password=settings.value(base_key + PASSWORD_KEY),
            service=settings.value(base_key + SERVICE_KEY),
        )

        optional_params = settings.value(base_key + OPTIONAL_PARAMS_KEY + LIST_KEY, [])
        for param in optional_params:
            connection.optional_params[param] = settings.value(
                base_key + OPTIONAL_PARAMS_KEY + "/" + param, ""
            )

        return connection

    def save_connection_to_settings(self, connection: Connection) -> None:
        """
        Save openlog connection to user QgsSettings

        Args:
            connection: openlog connection
        """
        settings = QgsSettings()
        save_connection = settings.value(
            BASE_SETTINGS_KEY + self._get_connection_type_name() + "/" + LIST_KEY, []
        )
        if connection.name not in save_connection:
            save_connection.append(connection.name)
            settings.setValue(
                BASE_SETTINGS_KEY + self._get_connection_type_name() + "/" + LIST_KEY,
                save_connection,
            )

        base_key = (
            BASE_SETTINGS_KEY + self._get_connection_type_name() + "/" + connection.name
        )
        settings.setValue(base_key + HOST_KEY, connection.host)
        settings.setValue(base_key + DB_KEY, connection.database)
        settings.setValue(base_key + PORT_KEY, connection.port)
        settings.setValue(base_key + USER_KEY, connection.user)
        settings.setValue(base_key + PASSWORD_KEY, connection.password)
        settings.setValue(base_key + QGS_CONFIG_ID_KEY, connection.qgs_config_id)
        settings.setValue(base_key + SERVICE_KEY, connection.service)
        if connection.optional_params:
            settings.setValue(
                base_key + OPTIONAL_PARAMS_KEY + LIST_KEY,
                list(connection.optional_params.keys()),
            )
            for param in connection.optional_params:
                settings.setValue(
                    base_key + OPTIONAL_PARAMS_KEY + "/" + param,
                    connection.optional_params[param],
                )

    def get_connection(self):
        """
        Create a connection depending on current interface (psycog2, sqlalchemy, ...).
        Raises InvalidConnection if connection invalid

        Returns: psycog or sqlalchemy connection

        """

        settings = QgsSettings()
        settings.setValue(
            BASE_SETTINGS_KEY + self._get_connection_type_name() + "/" + LAST_USED_KEY,
            self.connection_cb.currentText(),
        )
        return self.get_connection_object(self.get_connection_model())

    def get_connection_object(self, connection: Connection):
        """
        Get connection object depending on current interface (psycog2, sqlalchemy, ...).

        Raises ConnectionWidget.InvalidInterface if not implemented

        Args:
            connection: connection datamodel
        """
        raise ConnectionWidget.InvalidInterface()

    def get_connection_model(self) -> Connection:
        """
        Return openlog connection from widget

        Returns: openlog connnection

        """
        username, password = self._get_user_name_password()
        result = Connection(
            name=self.connection_cb.currentText(),
            user=username,
            database=self.database_edit.text(),
            host=self.host_edit.text(),
            port=self.port_edit.text(),
            qgs_config_id=self.auth_settings.configId(),
            password=password,
            service=self.service_edit.text(),
        )

        return result

    def set_connection_model(self, connection: Connection) -> None:
        """
        Define current widget parameters from openlog connection

        Args:
            connection: openlog connection
        """
        self.connection_cb.setCurrentText(connection.name)
        self.host_edit.setText(connection.host)
        self.port_edit.setText(str(connection.port))
        # Don't change database if it's not enabled, user won't be able to change it
        if self.database_edit.isEnabled():
            self.database_edit.setText(connection.database)
        if connection.qgs_config_id:
            self.auth_settings.setConfigId(connection.qgs_config_id)
        else:
            self.auth_settings.setUsername(connection.user)
            self.auth_settings.setPassword(connection.password)
        self.service_edit.setText(connection.service)

    def _get_user_name_password(self) -> (str, str):
        """
        Return current username and password

        Returns: (username,password)

        """
        username = self.auth_settings.username()
        password = self.auth_settings.password()
        if self.auth_settings.configurationTabIsSelected():
            auth_mgr = QgsApplication.authManager()
            auth_cfg = QgsAuthMethodConfig()
            auth_mgr.loadAuthenticationConfig(
                self.auth_settings.configId(), auth_cfg, True
            )
            if "username" in auth_cfg.configMap().keys():
                username = auth_cfg.configMap()["username"]
            if "password" in auth_cfg.configMap().keys():
                password = auth_cfg.configMap()["password"]
        return username, password

    def test_connection(self) -> bool:
        """
        Test current defined connection.
        Display a warning message if connection invalid

        Returns: True if connection is valid, False otherwise

        """
        valid = True
        try:
            conn = self.get_connection()
            conn.close()
        except ConnectionWidget.InvalidConnection as exc:
            valid = False
            QMessageBox.warning(self, self.tr("Connection failed"), str(exc))
        return valid

    def _load_connection(self) -> None:
        """
        Load openlog connection from user QgsSettings

        """
        settings = QgsSettings()
        save_connection = settings.value(
            BASE_SETTINGS_KEY + self._get_connection_type_name() + "/" + LIST_KEY
        )
        if save_connection:
            for connection in save_connection:
                self.connection_cb.addItem(connection)
            last_used = settings.value(
                BASE_SETTINGS_KEY
                + self._get_connection_type_name()
                + "/"
                + LAST_USED_KEY
            )
            if last_used and last_used in save_connection:
                self.connection_cb.setCurrentText(last_used)

    def add_connection(self) -> None:
        """
        Add a openlog connection from current widget to user QgsSettings

        """
        name, ok = QInputDialog.getText(
            self, self.tr("New connection"), self.tr("Name")
        )
        if ok and name:
            self.connection_cb.blockSignals(True)
            self.connection_cb.addItem(name)
            self.connection_cb.setCurrentText(name)
            self.connection_cb.blockSignals(False)
            self.save_current_connection()

    def _set_connection_from_cb(self) -> None:
        """
        Define current connection from selected combobox value

        """
        name = self.connection_cb.currentText()
        self.set_connection_model(self.get_connection_from_settings(name))

    def save_current_connection(self) -> None:
        """
        Save current connection to user QgsSettings (if name defined)

        """
        name = self.connection_cb.currentText()
        if name:
            connection_model = self.get_connection_model()
            self.save_connection_to_settings(connection_model)

    def _remove_current_connection(self) -> None:
        """
        Remove current connection for user QgsSettings

        """
        name = self.connection_cb.currentText()
        if name:
            settings = QgsSettings()
            save_connection = settings.value(
                BASE_SETTINGS_KEY + self._get_connection_type_name() + "/" + LIST_KEY,
                [],
            )
            save_connection.remove(name)
            settings.setValue(
                BASE_SETTINGS_KEY + self._get_connection_type_name() + "/" + LIST_KEY,
                save_connection,
            )
            settings.remove(
                BASE_SETTINGS_KEY + self._get_connection_type_name() + "/" + name
            )
            self.connection_cb.removeItem(self.connection_cb.findText(name))
