#! python3  # noqa: E265

"""Plugin settings."""

# standard
from dataclasses import asdict, dataclass, fields
from pathlib import Path
from typing import Any, Optional, Type

# PyQGIS
from qgis.core import Qgis, QgsApplication, QgsAuthMethodConfig, QgsSettings

import oslandia.toolbelt.log_handler as log_hdlr
from oslandia.__about__ import __title__, __version__

# package
from oslandia.constants import NEWSFEED_URL_FR, NEWSFEED_URL_US
from oslandia.datamodels import oauth2_configuration
from oslandia.toolbelt.application_folder import get_app_dir
from oslandia.toolbelt.env_var_parser import EnvVarParser
from oslandia.toolbelt.qt_utils import get_locale


# -- GLOBALS --
CFG_AUTH_NAME = "oslandia_plugin_cfg"
PREFIX_ENV_VARIABLE = "QGIS_PLG_OSLANDIA_"

# ############################################################################
# ########## Classes ###############
# ##################################


@dataclass
class PlgEnvVariableSettings:
    """Plugin settings from environment variable"""

    def env_variable_used(self, attribute: str, default_from_name: bool = True) -> str:
        """Get environment variable used for environment variable settings

        :param attribute: attribute to check
        :type attribute: str
        :param default_from_name: define default environment value from attribute name
            PREFIX_ENV_VARIABLE_<upper case attribute>
        :type default_from_name: bool
        :return: environment variable used
        :rtype: str
        """
        settings_env_variable = asdict(self)
        env_variable = settings_env_variable.get(attribute, "")
        if not env_variable and default_from_name:
            env_variable = f"{PREFIX_ENV_VARIABLE}{attribute}".upper()
        return env_variable


@dataclass
class PlgSettingsStructure:
    """Plugin settings structure and defaults values."""

    # global
    debug_mode: bool = False
    version: str = __version__
    # since Qt is not able to store Path object,
    # we use a getter/setter to manage conversion from/to str
    # see: https://gitlab.com/Oslandia/qgis/oslandia/-/issues/83
    _local_app_folder: str = f"{get_app_dir(dir_name='cache')}"

    # News feed
    rss_source: str = NEWSFEED_URL_FR
    if get_locale().name() != "fr_FR":
        rss_source: str = NEWSFEED_URL_US

    rss_category_filters: str = ""
    rss_latest_content_guid: str = ""
    rss_notify_push_duration: int = 30
    rss_notify_push_info: bool = True
    rss_poll_frequency_hours: int = 24
    rss_read_items: str = ""
    rss_view_only_unread: bool = False

    # Helpdesk
    authentification_config_id: str | None = None
    helpdesk_api_url_base: str = "https://api.oslandia.net/"
    gitlab_url_instance: str = "https://git.oslandia.net/"
    gitlab_api_version: str = "4"

    # GitLab group
    gitlab_group_id: str = "519,5"

    # Minimum access level for archived display, see [GitLab API](https://docs.gitlab.com/api/group_members/#update-a-group-member) for value definition
    # Possible values: 0 (No access), 5 (Minimal access), 10 (Guest), 15 (Planner), 20 (Reporter), 30 (Developer), 40 (Maintainer), 50 (Owner), 60 (Admin). Default: 30.
    min_access_level_for_archived: int = 40

    # issue creation option
    allow_qgis_info_add_in_new_issue: bool = False
    allow_plugin_info_add_in_new_issue: bool = False
    excluded_assignee_list: str = "aahyen,cpretre,oslauto,root,webapps"

    # Projets browser (index 0 = cards / index 1 = list)
    project_browser_last_view: int = 0

    # Last selected project
    issue_view_last_project_id: int | None = None
    issue_creation_last_project_id: int | None = None

    # Notification
    enable_notification: bool = False
    notification_frequency: int = 1800
    excluded_username_list: str = ""

    use_gitlab_markdown_api: bool = False
    use_gitlab_markdown_format: bool = False

    # Experimental features
    enable_user_selection_screenshot: bool = False

    # Showcase

    # Onboarding
    run_onboarding: bool = True

    # UTM
    enable_utm: bool = True

    # Pagination for request
    nb_item_by_page: int = 5
    nb_parallel_task: int = 5

    @property
    def excluded_username_list_values(self) -> list[str]:
        """Get list of excluded username.
        Split settings excluded_username_list with ',' and drop empty entries.

        :return: list of excluded username
        :rtype: list[str]
        """
        return [
            username.strip()
            for username in self.excluded_username_list.split(",")
            if username.strip()
        ]

    @property
    def excluded_assignee_list_values(self) -> list[str]:
        """Get list of excluded assignee.
        Split settings excluded_assignee_list with , and drop empty entries.

        :return: list of excluded assigne
        :rtype: list[str]
        """
        return [
            assignee.strip()
            for assignee in self.excluded_assignee_list.split(",")
            if assignee.strip()
        ]

    @property
    def gitlab_api_url_base(self) -> str:
        """Get GitLab instance API base URL.

        :return: https://{instance}/api/v{api_version}
        :rtype: str
        """
        return f"{self.gitlab_url_instance}api/v{self.gitlab_api_version}/"

    @property
    def gitlab_group_list_values(self) -> list[int]:
        """Get list gitlab group.
        Split settings gitlab_group_id with ,

        :return: list of gitlab group
        :rtype: List[int]
        """
        result = []
        for group_str in self.gitlab_group_id.split(","):
            try:
                result.append(int(group_str))
            except ValueError:
                continue
        return result

    @property
    def gitlab_graphql_url(self) -> str:
        """Get GitLab GraphQL URL.

        :return: https://{instance}/api/graphql
        :rtype: str
        """
        return f"{self.gitlab_url_instance}api/graphql"

    @property
    def local_app_folder(self) -> Path:
        """Get local application folder.

        :return: local application folder
        :rtype: Path
        """
        if self._local_app_folder:
            return Path(self._local_app_folder).resolve()
        else:
            return get_app_dir(dir_name="cache")

    @local_app_folder.setter
    def local_app_folder(self) -> None:
        """Set local application folder."""
        self._local_app_folder = f"{get_app_dir(dir_name='cache')}"

    def create_auth_config(self) -> QgsAuthMethodConfig | None:
        """Create QgsAuthMethodConfig for OAuth2 authentification.

        :return: created configuration. Warning: config must be added to QgsApplication.authManager() before use
        :rtype: Optional[QgsAuthMethodConfig]
        """
        new_auth_config = QgsAuthMethodConfig(method="OAuth2", version=1)
        new_auth_config.setId(QgsApplication.authManager().uniqueConfigId())
        new_auth_config.setName(CFG_AUTH_NAME)

        # load OAuth2 configuration from JSON file
        auth_config_obj = oauth2_configuration.OAuth2Configuration.from_json()
        if not isinstance(auth_config_obj, oauth2_configuration.OAuth2Configuration):
            log_hdlr.PlgLogger.log(
                message="Error while loading authentication configuration.",
                log_level=Qgis.MessageLevel.Critical,
                push=True,
            )
            return

        # We need to use a string for config_map
        auth_config_as_str = auth_config_obj.as_qgis_str_config_map()
        config_map = {"oauth2config": auth_config_as_str}
        new_auth_config.setConfigMap(config_map)

        if not new_auth_config.isValid():
            log_hdlr.PlgLogger.log(
                message="Error while creating authentication configuration NOT VALID.",
                log_level=Qgis.MessageLevel.Critical,
                push=True,
            )
            return

        log_hdlr.PlgLogger.log(
            message=f"Authentication configuration created with ID: {new_auth_config.id()} "
            f"({new_auth_config.name()})",
            log_level=Qgis.MessageLevel.Success,
        )

        return new_auth_config

    def get_filters_list(self, lower: bool = False) -> list[str]:
        """Get user filters as a lower str list.

        :param lower: To tell if we want the list as lowercase, defaults to False
        :type lower: bool, optional

        :return: the user's filters list
        :rtype: list[str]
        """
        user_filters = [
            filter_cat.strip() for filter_cat in self.rss_category_filters.split(",")
        ]
        user_filters = list(filter(None, user_filters))
        lowercase_user_filters = [cat.lower() for cat in user_filters]
        if lower:
            return lowercase_user_filters
        else:
            return user_filters


class PlgOptionsManager:
    """Plugin settings manager.

    This class is used to manage the plugin settings. It provides methods to
    load, save and update the settings. It also provides methods to get and set
    the settings values.
    """

    @staticmethod
    def get_plg_settings() -> PlgSettingsStructure:
        """Load and return plugin settings as a dictionary. \
        Useful to get user preferences across plugin logic.

        :return: plugin settings
        :rtype: PlgSettingsStructure
        """
        # get dataclass fields definition
        settings_fields = fields(PlgSettingsStructure)
        env_variable_settings = PlgEnvVariableSettings()

        # retrieve settings from QGIS/Qt
        settings = QgsSettings()
        settings.beginGroup(__title__)

        # map settings values to preferences object
        li_settings_values = []
        for i in settings_fields:
            try:
                value = settings.value(key=i.name, defaultValue=i.default, type=i.type)
                # If environnement variable used, get value from environnement variable
                env_variable = env_variable_settings.env_variable_used(i.name)
                if env_variable:
                    value = EnvVarParser.get_env_var(env_variable, value)
                li_settings_values.append(value)
            except TypeError:
                li_settings_values.append(
                    settings.value(key=i.name, defaultValue=i.default)
                )

        # instanciate new settings object
        options = PlgSettingsStructure(*li_settings_values)

        settings.endGroup()

        return options

    @staticmethod
    def get_value_from_key(
        key: str,
        default: Any = None,
        expected_type: Optional[Type[Any]] = None,
    ) -> Any:
        """Return the value of a plugin setting by its key.

        :param key: setting key, must be an attribute of PlgSettingsStructure
        :type key: str
        :param default: fallback value if the key is not found, defaults to None
        :type default: Any, optional
        :param expected_type: expected type for deserialization by QgsSettings, defaults to None
        :type expected_type: type or None, optional

        :return: setting value, or None if the key is invalid or an error occurred
        :rtype: Any
        """
        if not hasattr(PlgSettingsStructure, key):
            log_hdlr.PlgLogger.log(
                message="Bad settings key. Must be one of: {}".format(
                    ",".join(f.name for f in fields(PlgSettingsStructure))
                ),
                log_level=Qgis.MessageLevel.Warning,
            )
            return None

        settings = QgsSettings()
        settings.beginGroup(__title__)

        try:
            out_value = settings.value(
                key=key, defaultValue=default, type=expected_type
            )
        except Exception as err:
            log_hdlr.PlgLogger.log(
                message=f"Error occurred trying to get settings: {key}.Trace: {err}",
                log_level=Qgis.MessageLevel.Warning,
                push=False,
            )
            out_value = None

        settings.endGroup()

        return out_value

    @classmethod
    def set_value_from_key(cls, key: str, value: Any) -> bool:
        """Set plugin setting value using the key.

        :param key: setting key, must be an attribute of PlgSettingsStructure
        :type key: str
        :param value: value to set
        :type value: Any

        :return: True if the value was saved successfully, False otherwise
        :rtype: bool
        """
        if not hasattr(PlgSettingsStructure, key):
            log_hdlr.PlgLogger.log(
                message="Bad settings key: {}. Must be one of: {}".format(
                    key, ",".join(f.name for f in fields(PlgSettingsStructure))
                ),
                log_level=Qgis.MessageLevel.Critical,
            )
            return False

        settings = QgsSettings()
        settings.beginGroup(__title__)

        try:
            settings.setValue(key, value)
            out_value = True
            log_hdlr.PlgLogger.log(
                message=f"Setting `{key}` saved with value `{value}`",
                log_level=Qgis.MessageLevel.NoLevel,
            )
        except Exception as err:
            log_hdlr.PlgLogger.log(
                message=f"Error occurred trying to set settings: {key}.Trace: {err}",
                log_level=Qgis.MessageLevel.Warning,
                push=False,
            )
            out_value = False

        settings.endGroup()

        return out_value

    @classmethod
    def save_from_object(cls, plugin_settings_obj: PlgSettingsStructure) -> None:
        """Save plugin settings from a PlgSettingsStructure object.

        :param plugin_settings_obj: settings object to persist
        :type plugin_settings_obj: PlgSettingsStructure
        """
        for k, v in asdict(plugin_settings_obj).items():
            cls.set_value_from_key(k, v)
