#! python3  # noqa: E265

"""Authentication dialog logic."""

# standard
from __future__ import annotations

import json
from pathlib import Path
from typing import TYPE_CHECKING, Dict, Optional

# PyQGIS
from qgis.core import Qgis, QgsApplication, QgsAuthMethodConfig
from qgis.PyQt import uic
from qgis.PyQt.QtCore import pyqtSignal
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QWidget

# Plugin
from oslandia.__about__ import DIR_PLUGIN_ROOT
from oslandia.constants import OAUTH_DECLARED_REDIRECT_PORTS
from oslandia.custom_exceptions import (
    InvalidOAuthPortError,
    UnavailablePortError,
    UnavailableUserError,
)
from oslandia.datamodels.oauth2_configuration import OAuth2Configuration
from oslandia.gui.stylesheet import WidgetStyleSheet
from oslandia.helpdesk.user import UserRequestManager
from oslandia.toolbelt import PlgLogger, PlgOptionsManager
from oslandia.toolbelt.network_manager import NetworkRequestsManager


# only for type checking
if TYPE_CHECKING:
    from qgis.core import QgsAuthManager


class AuthenticationWidget(WidgetStyleSheet):
    # Signal to indicate user is connected
    user_connected = pyqtSignal()

    def __init__(self, parent: Optional[QWidget] = None):
        """Dialog to define current connection as authentication config.

        :param parent: parent widget, defaults to None
        :type parent: QObject, optional
        """
        # init module and ui
        super().__init__(parent)
        uic.loadUi(Path(__file__).parent / f"{Path(__file__).stem}.ui", self)
        self.auth_manager: Optional[QgsAuthManager] = None

        # toolbelt
        self.log = PlgLogger().log
        self.ntwk_requester_blk = NetworkRequestsManager()
        self.plg_settings_mngr = PlgOptionsManager()
        self.plg_settings = self.plg_settings_mngr.get_plg_settings()

        # widgets connect
        self.btn_log_in.setIcon(
            QIcon(
                str(
                    DIR_PLUGIN_ROOT.joinpath(
                        "resources/images/oslandia_favicon_gitlab.svg"
                    )
                )
            )
        )
        self.btn_log_in.clicked.connect(self.connect)

        # Copy clipboard
        self.clipboard = QgsApplication.instance().clipboard()
        self.lbl_clipboard_information.setStyleSheet(
            """
            font-style: italic;
            color: #3c72ac;
            qproperty-alignment: 'AlignCenter';
        """
        )
        self.lbl_clipboard_information.setText("")

    def connect(self) -> None:
        """Check connection parameter and define current geotuileur authentication config
        if connection is valid.
        """
        # Create new authentication config and add it to manager
        self.auth_manager = QgsApplication.authManager()
        new_auth = self.plg_settings.create_auth_config()

        if not isinstance(new_auth, QgsAuthMethodConfig):
            self.log(
                message=self.tr("Error while creating authentication configuration."),
                log_level=Qgis.MessageLevel.Critical,
                push=True,
                duration=30,
            )
            return

        auth_created = self.auth_manager.storeAuthenticationConfig(new_auth, True)
        if not auth_created[0]:
            self.log(
                message=self.tr(
                    "Error while storing authentication configuration {} in QGIS "
                    "Authentication Manager.".format(new_auth)
                ),
                log_level=Qgis.MessageLevel.Critical,
                push=True,
                duration=30,
                parent_location=self,
                nb_parent_max_for_message_bar=0,
            )
            return

        self.log(
            message="Credentials saved into QGIS Authentication Manager as "
            f" {new_auth.name()} ({new_auth.id()})",
            log_level=Qgis.MessageLevel.NoLevel,
        )

        # If connection valid, remove previous config and use new created config,
        # otherwise remove created config
        if self.check_connection(new_auth):
            if self.plg_settings.authentification_config_id:
                self.auth_manager.removeAuthenticationConfig(
                    self.plg_settings.authentification_config_id
                )
            self.plg_settings.authentification_config_id = new_auth.id()
            self.plg_settings_mngr.save_from_object(self.plg_settings)

            # Validate dialog
            self.user_connected.emit()
        else:
            self.log(
                message="Authentication configuration {} has been removed.".format(
                    new_auth.id()
                ),
                log_level=Qgis.MessageLevel.NoLevel,
                push=False,
            )
            self.auth_manager.removeAuthenticationConfig(new_auth.id())

    def check_connection(
        self,
        qgis_auth_config: QgsAuthMethodConfig,
    ) -> bool:
        """Check if connection is valid for a given QgsAuthMethodConfig.

        Display a message box with user name and last name if connection valid, or
        message with error message otherwise.

        :param qgis_auth_config: qgis authentication configuration to use
        :type QgsAuthMethodConfig: str


        :return: True if connection is valid for qgis_auth_config, False otherwise
        :rtype: bool
        """

        res = True

        # Check connection to API by getting token user information
        try:
            manager = UserRequestManager()
            manager.plg_settings.authentification_config_id = qgis_auth_config.id()

            # check if an acceptable port is available
            self.get_available_redirect_port(qgis_auth_config)
            self.auth_manager.storeAuthenticationConfig(qgis_auth_config, True)

            self.copy_url_to_clipboard(qgis_auth_config)

            # try to get authenticated user informations
            user = manager.get_current_user()
            self.log(
                message=self.tr("Welcome {}!".format(user.name)),
                log_level=Qgis.MessageLevel.Success,
                push=True,
                duration=5,
            )

        except UnavailableUserError as exc:
            self.log(
                message=self.tr("Authentication failed. Trace: {}".format(exc)),
                log_level=Qgis.MessageLevel.Critical,
                push=True,
                duration=30,
                parent_location=self,
                nb_parent_max_for_message_bar=0,
            )
            res = False

        except UnavailablePortError:
            self.log(
                self.tr(
                    "None of the usable port {} is available. Please see requirements."
                ).format(OAUTH_DECLARED_REDIRECT_PORTS),
                log_level=Qgis.MessageLevel.Critical,
                push=True,
                duration=30,
                parent_location=self,
                nb_parent_max_for_message_bar=0,
            )
            res = False

            self.qgrp_requirements.setCollapsed(False)

        return res

    def get_available_redirect_port(
        self,
        qgis_auth_config: QgsAuthMethodConfig,
    ) -> bool:
        """Check if one of declared oAuth port is available for callback redirection.

        :param qgis_auth_config: qgis authentication configuration to use
        :type QgsAuthMethodConfig: str

        :return: True if port is available, False otherwise
        :rtype: bool
        """
        oauth2cfg: OAuth2Configuration | None = OAuth2Configuration.from_config_map(
            qgis_auth_config.configMap()
        )

        # check if port is in declared ports
        if oauth2cfg.redirectPort not in OAUTH_DECLARED_REDIRECT_PORTS:
            self.log(
                InvalidOAuthPortError(
                    "Port {} is not in the list of declared ports: {}. It could lead to "
                    "unexpect behavior.".format(
                        oauth2cfg.redirectPort,
                        OAUTH_DECLARED_REDIRECT_PORTS,
                    )
                ),
                log_level=Qgis.MessageLevel.Warning,
                parent_location=self,
                nb_parent_max_for_message_bar=0,
            )

        # if port is available
        if self.ntwk_requester_blk.is_port_available(
            port=oauth2cfg.redirectPort,
        ):
            self.log(
                message=self.tr(
                    "Port {} is available and will be used to perform oAuth2 related "
                    "operations.".format(
                        oauth2cfg.redirectPort,
                    )
                ),
                push=True,
                duration=5,
                parent_location=self,
                nb_parent_max_for_message_bar=0,
            )
            return True

        # if port is not available, parse the list of declared ports and check if one
        # of them is available
        self.log(
            message=self.tr(
                "Port {} is not available. Trying another port among declared ones: {}.".format(
                    oauth2cfg.redirectPort,
                    OAUTH_DECLARED_REDIRECT_PORTS,
                )
            ),
            log_level=Qgis.MessageLevel.Warning,
            push=True,
            duration=5,
            parent_location=self,
            nb_parent_max_for_message_bar=0,
        )
        for possible_port in OAUTH_DECLARED_REDIRECT_PORTS:
            if possible_port == oauth2cfg.redirectPort:
                self.log(
                    message="Port {} has already been tested. Skipping.".format(
                        possible_port
                    ),
                    log_level=Qgis.MessageLevel.NoLevel,
                )
                continue
            # check if the port is available
            if self.ntwk_requester_blk.is_port_available(
                port=possible_port,
            ):
                # if available, set it in the configuration
                oauth2cfg.redirectPort = possible_port
                qgis_auth_config.setConfigMap(
                    {"oauth2config": oauth2cfg.as_qgis_str_config_map()},
                )
                return self.get_available_redirect_port(
                    qgis_auth_config,
                )

        # no more port available
        raise UnavailablePortError

    def get_authent_url(self, auth_config: QgsAuthMethodConfig) -> str:
        """Build the authentication URL to open in browser for user authentication.

        :param auth_config: Authentication configuration
        :type auth_config: QgsAuthMethodConfig

        :return: Authentication URL
        :rtype: str
        """
        config_map: Dict[str, str] = auth_config.configMap()

        oauth_config = json.loads(config_map.get("oauth2config", "{}"))

        request_url = oauth_config.get("requestUrl")
        client_id = oauth_config.get("clientId")
        scope = oauth_config.get("scope")
        port = oauth_config.get("redirectPort")

        response_type = "code"
        redirect_uri: str = f"http://127.0.0.1:{port}/callback"

        return (
            f"{request_url}"
            f"?response_type={response_type}"
            f"&client_id={client_id}"
            f"&redirect_uri={redirect_uri}"
            f"&scope={scope}"
        )

    def copy_url_to_clipboard(self, auth_config: QgsAuthMethodConfig) -> None:
        """Copy authentication URL to clipboard."""
        self.clipboard.setText(self.get_authent_url(auth_config))
        self.lbl_clipboard_information.setText(
            self.tr(
                "If your browser did not open correctly, the authentication URL has been "
                "stored in your clipboard."
            )
        )
