# standard
import json
from dataclasses import asdict, dataclass, field
from os import getenv
from pathlib import Path
from typing import Optional

# PyQGIS
from qgis.core import Qgis

# plugin
import oslandia.toolbelt.log_handler as log_hdlr
from oslandia.__about__ import DIR_PLUGIN_ROOT


# -- GLOBALS --

# Required fields for a QGIS oAuth2 configuration
QGS_REQUIRED_FIELDS = [
    "accessMethod",
    "clientId",
    "clientSecret",
    "configType",
    "grantFlow",
    "persistToken",
    "redirectPort",
    "redirectUrl",
    "requestTimeout",
    "requestUrl",
    "scope",
    "tokenUrl",
    "version",
]


@dataclass
class OAuth2Configuration:
    """oAuth2 configuration object."""

    accessMethod: int = 0  # noqa: N815
    apiKey: str = ""  # noqa: N815
    clientId: str = ""  # noqa: N815
    clientSecret: str = ""  # noqa: N815
    configType: int = 1  # noqa: N815
    customHeader: str = ""  # noqa: N815
    description: str = (
        "Authentication related to the Oslandia plugin to give access to the GitLab "
        "ticketing and helpdesk to end-customers who subscribed to Oslandia's support offer ."
    )
    grantFlow: int = 0  # noqa: N815
    id: str = ""
    name: str = "oslandia_plugin_cfg"
    objectName: str = ""  # noqa: N815
    password: str = ""
    persistToken: bool = True  # noqa: N815
    queryPairs: dict = field(default_factory=dict)  # noqa: N815
    redirectPort: int = 7070  # noqa: N815
    redirectUrl: str = "callback"  # noqa: N815
    refreshTokenUrl: str = ""  # noqa: N815
    requestTimeout: int = 30  # noqa: N815
    requestUrl: str = "https://git.oslandia.net/oauth/authorize"  # noqa: N815
    scope: str = "api"
    tokenUrl: str = "https://git.oslandia.net/oauth/token"  # noqa: N815
    username: str = ""
    version: int = 1

    def as_qgis_str_config_map(self) -> str:
        """Convert the OAuth2Configuration instance to a string representation of a
        dictionary containing only the required fields for the QGIS configuration.

        :return: string representation of the configuration with only required fields.
        :rtype: str
        """
        oauth_config_dict = asdict(self)
        keys_to_remove = [
            key for key in oauth_config_dict if key not in QGS_REQUIRED_FIELDS
        ]

        for key in keys_to_remove:
            oauth_config_dict.pop(key)

        log_hdlr.PlgLogger.log(
            message="oAuth2 configuration converted as str to comply with QGIS "
            "Authentication manager.",
            log_level=Qgis.MessageLevel.Success,
            push=False,
        )

        # ' not supported by pyqgis, replace by "
        oauth_config_str = str(oauth_config_dict).replace("'", '"')

        # replace also boolean str
        oauth_config_str = oauth_config_str.replace("False", "false").replace(
            "True", "true"
        )

        return oauth_config_str

    @classmethod
    def from_config_map(
        cls, qgis_config_map: dict[str, str]
    ) -> Optional["OAuth2Configuration"]:
        """Creates an OAuth2Configuration instance from a QGIS configuration map.

        :param qgis_config_map: QGIS configuration map containing the OAuth2
        configuration.
        :type qgis_config_map: dict

        :return: instance of OAuth2Configuration populated with the data from the
        configuration map.
        :rtype: OAuth2Configuration
        """
        # check if the config map is compliant with the expected structure
        if not cls.is_json_compliant(json.loads(qgis_config_map.get("oauth2config"))):
            log_hdlr.PlgLogger.log(
                message="Configuration map does not comply with the expected "
                "structure. Please check your configuration.",
                log_level=Qgis.MessageLevel.Critical,
                push=True,
            )
            return

        log_hdlr.PlgLogger.log(
            message="oAuth2 configuration loaded from QGIS config map.",
            log_level=Qgis.MessageLevel.NoLevel,
            push=False,
        )

        return cls(**json.loads(qgis_config_map.get("oauth2config")))

    @classmethod
    def from_json(
        cls, json_filepath: Path | None = None
    ) -> Optional["OAuth2Configuration"]:
        """Loads an OAuth2Configuration instance from the given JSON file path.

        :param json_filepath: Path to the JSON file containing the configuration.
        :type json_filepath: Path

        :return: instance of OAuth2Configuration populated with the data from the JSON file.
        :rtype: OAuth2Configuration
        """
        # if no path is provided, get the default one
        if json_filepath is None:
            json_filepath = cls.get_json_path()

        # if the default path is not found, return right now
        if not isinstance(json_filepath, Path):
            return

        # if the file exist, try load the JSON data
        try:
            with json_filepath.open(mode="r", encoding="utf8") as my_json:
                json_data = json.load(my_json)
            log_hdlr.PlgLogger.log(
                message="JSON file ({}) read and data loaded successfully.".format(
                    json_filepath
                ),
                log_level=Qgis.MessageLevel.NoLevel,
                push=False,
            )
        except json.decoder.JSONDecodeError as err:
            log_hdlr.PlgLogger.log(
                message="JSON file ({}) could not be read. Error: {}".format(
                    json_filepath, err
                ),
                log_level=Qgis.MessageLevel.Critical,
                push=True,
            )
            return

        # check if the JSON data is compliant with the expected structure
        if not cls.is_json_compliant(json_data):
            return

        log_hdlr.PlgLogger.log(
            f"oAuth2 configuration loaded from {json_filepath}.",
            log_level=Qgis.MessageLevel.NoLevel,
        )

        return cls(
            **json_data,
        )

    @classmethod
    def get_json_path(cls) -> Path | None:
        """Return path to the oAuth2 JSON filepath.

        :return: path to the JSON file containing the configuration.
        :rtype: Optional[Path]
        """
        json_config_filepath = Path(
            getenv(
                "QGIS_PLG_OSLANDIA_OAUTH2_CONFIG",
                DIR_PLUGIN_ROOT.joinpath("auth/oauth2_config.json"),
            )
        )

        if not json_config_filepath.exists():
            log_hdlr.PlgLogger.log(
                message="The configuration file can't be found: {}".format(
                    json_config_filepath
                ),
                log_level=Qgis.MessageLevel.Critical,
                push=True,
            )
            return

        return json_config_filepath.resolve()

    @classmethod
    def is_json_compliant(cls, json_data: dict) -> bool:
        """Check whether the JSON configuration at the given path contains all the
        required keys for the OAuth2Configuration dataclass.

        :param json_data: Path to the JSON file containing the configuration.
        :type json_data: dict

        :return: True if the JSON data is compliant with the expected structure, False
        otherwise.
        :rtype: bool
        """
        missing_fields = [
            field for field in QGS_REQUIRED_FIELDS if field not in json_data
        ]

        if missing_fields:
            log_hdlr.PlgLogger.log(
                message="Data does not comply with the expected oAuth structure. "
                "Following fields are missing: {}.".format(missing_fields),
                log_level=Qgis.MessageLevel.Critical,
                push=True,
            )
            return False

        log_hdlr.PlgLogger.log(
            message="JSON data is compliant with the expected structure.",
            log_level=Qgis.MessageLevel.Success,
            push=False,
        )

        return True
