## Internal modules
from aepp import config, configs

## External module
import json
import os
from typing import Dict, Optional, Union
from copy import deepcopy
import time
import requests
from pathlib import Path
import jwt


class AdobeRequest:
    """
    Handle request to Audience Manager and taking care that the request have a valid token set each time.
    """

    loggingEnabled = False

    def __init__(
        self,
        config_object: dict = config.config_object,
        header: dict = config.header,
        verbose: bool = False,
        loggingEnabled: bool = False,
        logger: object = None,
        retry: int = 0,
        **kwargs,
    ) -> None:
        """
        Set the connector to be used for handling request to AAM
        Arguments:
            config_object : OPTIONAL : Require the importConfig file to have been used.
            header : OPTIONAL : Header that you are already using.
            verbose : OPTIONAL : display comment while running
            loggingEnabled : OPTIONAL : if the logging is enable for that instance.
            logger : OPTIONAL : instance of the logger created
            retry : OPTIONAL : When GET request fails, if set to an int, it will retry this number of time
        """
        if config_object["org_id"] == "":
            raise Exception(
                "You have to upload the configuration file with importConfigFile or configure method."
            )
        self.config = deepcopy(config_object)
        self.header = deepcopy(header)
        self.loggingEnabled = loggingEnabled
        self.logger = logger
        self.retry = retry
        if self.config["token"] == "" or time.time() > self.config["date_limit"]:
            if "aepScope" in kwargs.keys() and "privacyScope" in kwargs.keys():
                token_with_expiry = self.get_token_and_expiry_for_config(
                    config=self.config,
                    verbose=verbose,
                    aepScope=kwargs.get("aepScope"),
                    privacyScope=kwargs.get("privacyScope"),
                )
                self.token = token_with_expiry["token"]
                self.config["token"] = self.token
                self.config["date_limit"] = (
                    time.time() + token_with_expiry["expiry"] / 1000 - 500
                )
                self.header.update({"Authorization": f"Bearer {self.token}"})
            else:
                token_with_expiry = self.get_token_and_expiry_for_config(
                    config=self.config,
                    verbose=verbose,
                    aepScope=kwargs.get("aepScope"),
                    privacyScope=kwargs.get("privacyScope"),
                )
                self.token = token_with_expiry["token"]
                self.config["token"] = self.token
                self.config["date_limit"] = (
                    time.time() + token_with_expiry["expiry"] / 1000 - 500
                )
                self.header.update({"Authorization": f"Bearer {self.token}"})

    def _find_path(self, path: str) -> Optional[Path]:
        """Checks if the file denoted by the specified `path` exists and returns the Path object
        for the file.

        If the file under the `path` does not exist and the path denotes an absolute path, tries
        to find the file by converting the absolute path to a relative path.

        If the file does not exist with either the absolute and the relative path, returns `None`.
        """
        if Path(path).exists():
            return Path(path)
        elif path.startswith("/") and Path("." + path).exists():
            return Path("." + path)
        elif path.startswith("\\") and Path("." + path).exists():
            return Path("." + path)
        else:
            return None

    def get_token_and_expiry_for_config(
        self, config: dict, verbose: bool = False, save: bool = False, **kwargs
    ) -> Dict[str, str]:
        """
        Retrieve the token by using the information provided by the user during the import importConfigFile function.
        ArgumentS :
            verbose : OPTIONAL : Default False. If set to True, print information.
            save : OPTIONAL : Default False. If set to True, save the toke in the .
        """
        private_key = configs.get_private_key_from_config(config)
        header_jwt = {
            "cache-control": "no-cache",
            "content-type": "application/x-www-form-urlencoded",
        }
        now_plus_24h = int(time.time()) + 24 * 60 * 60
        jwt_payload = {
            "exp": now_plus_24h,
            "iss": config["org_id"],
            "sub": config["tech_id"],
            "https://ims-na1.adobelogin.com/s/ent_dataservices_sdk": True,
            "aud": f'https://ims-na1.adobelogin.com/c/{config["client_id"]}',
        }
        # privacy topic
        if kwargs.get("privacyScope", False):
            jwt_payload["https://ims-na1.adobelogin.com/s/ent_gdpr_sdk"] = True
        if kwargs.get("aepScope", True) == False:
            del jwt_payload["https://ims-na1.adobelogin.com/s/ent_dataservices_sdk"]
        encoded_jwt = self._get_jwt(payload=jwt_payload, private_key=private_key)

        payload = {
            "client_id": config["client_id"],
            "client_secret": config["secret"],
            "jwt_token": encoded_jwt,
        }
        response = requests.post(
            config["tokenEndpoint"], headers=header_jwt, data=payload
        )
        json_response = response.json()
        try:
            self.token = json_response["access_token"]
            self.config["token"] = self.token
        except KeyError:
            print("Issue retrieving token")
            print(json_response)
        expiry = json_response["expires_in"] - 500
        if save:
            with open("token.txt", "w") as f:
                f.write(self.token)
            print(f"token has been saved here: {os.getcwd()}{os.sep}token.txt")
        if self.loggingEnabled:
            self.logger.debug(f"token retrieved: {self.token}")
        if verbose:
            print("token valid till : " + time.ctime(time.time() + expiry / 1000))
        if self.loggingEnabled:
            self.logger.debug(
                f"token valid till : {time.ctime(time.time() + expiry / 1000)}"
            )
        return {"token": self.token, "expiry": expiry}

    def _get_jwt(self, payload: dict, private_key: str) -> str:
        """
        Ensure that jwt enconding return the same type (str) as versions < 2.0.0 returned bytes and >2.0.0 return strings.
        """
        token: Union[str, bytes] = jwt.encode(payload, private_key, algorithm="RS256")
        if isinstance(token, bytes):
            return token.decode("utf-8")
        return token

    def _checkingDate(self) -> None:
        """
        Checking if the token is still valid
        """
        now = time.time()
        if now > self.config["date_limit"]:
            if self.loggingEnabled:
                self.logger.warning("token expired. Trying to retrieve a new token")
            token_with_expiry = self.get_token_and_expiry_for_config(config=self.config)
            self.token = token_with_expiry["token"]
            self.config["token"] = self.token
            if self.loggingEnabled:
                self.logger.info("new token retrieved : {self.token}")
            self.header.update({"Authorization": f"Bearer {self.token}"})
            self.config["date_limit"] = (
                time.time() + token_with_expiry["expiry"] / 1000 - 500
            )

    def updateSandbox(self, sandbox: str) -> None:
        """
        Update the sandbox used for the request
        Arguments:
            sandbox : REQUIRED : the sandbox to use for the requests
        """
        if not sandbox:
            raise Exception("require a sandbox")
        self.header["x-sandbox-name"] = sandbox

    def getData(
        self,
        endpoint: str,
        params: dict = None,
        data: dict = None,
        headers: dict = None,
        *args,
        **kwargs,
    ):
        """
        Abstraction for getting data
        """
        self._checkingDate()
        if headers is None:
            headers = self.header
        if self.loggingEnabled:
            self.logger.debug(
                f"Start GET request to {endpoint} with header: {json.dumps(headers)}"
            )
        if params is None and data is None:
            res = requests.get(endpoint, headers=headers)
        elif params is not None and data is None:
            if self.loggingEnabled:
                self.logger.debug(f"params: {json.dumps(params)}")
            res = requests.get(endpoint, headers=headers, params=params)
        elif params is None and data is not None:
            if self.loggingEnabled:
                self.logger.debug(f"data: {json.dumps(data)}")
            res = requests.get(endpoint, headers=headers, data=data)
        elif params is not None and data is not None:
            if self.loggingEnabled:
                self.logger.debug(f"params: {json.dumps(params)}")
                self.logger.debug(f"data: {json.dumps(data)}")
            res = requests.get(endpoint, headers=headers, params=params, data=data)
        if self.loggingEnabled:
            self.logger.debug(f"endpoint used: {res.request.url}")
            self.logger.debug(f"endpoint used: {res.request.url}")
            self.logger.debug(f"params used: {params}")
        try:
            if kwargs.get("format", "json") == "json":
                res_json = res.json()
            if kwargs.get("format", "json") == "txt":
                res_json = res.text
            elif kwargs.get("format", "json") == "raw":
                res_json = res
            else:
                res_json = res.json()
        except:
            if kwargs.get("verbose", False):
                print(res.text)
            if self.loggingEnabled:
                self.logger.warning(f"error: {res.text}")
            res_json = {"error": "Request Error"}
            if self.retry > 0:
                if self.loggingEnabled:
                    self.logger.info(f"starting retry: {self.retry} to do")
                for each in range(self.retry):
                    if "error" in res_json.keys():
                        time.sleep(5)
                        res_json = self.getData(
                            endpoint, params, data, headers, **kwargs
                        )
        try:  ## sometimes list is being returned
            if type(res_json) == dict:
                if "errorMessage" in res_json.keys():
                    if self.loggingEnabled:
                        self.logger.error(
                            f"GET method failed: {res.status_code}, {res['errorMessage']}"
                        )
                    print(f"status code : {res.status_code}")
                    print(f"error message : {res['errorMessage']}")
        except:
            pass
        return res_json

    def headData(
        self, endpoint: str, params: dict = None, headers: dict = None, *args, **kwargs
    ):
        """
        Abstraction for the head method.
        """
        self._checkingDate()
        if headers is None:
            headers = self.header
        if self.loggingEnabled:
            self.logger.debug(
                f"Start GET request to {endpoint} with header: {json.dumps(headers)}"
            )
        if params is None:
            res = requests.head(endpoint, headers=headers)
        if params is not None:
            res = requests.head(endpoint, headers=headers, params=params)
        try:
            res_header = res.headers()
        except:
            if kwargs.get("verbose", False):
                print("error generating the JSON response")
                print(f"status: {res.status_code}")
                print(res.text)
            if res.status_code != 200:
                res_header = res.headers()
            else:
                res_header = {}
        return res_header

    def postData(
        self,
        endpoint: str,
        params: dict = None,
        data: dict = None,
        bytesData: bytes = None,
        headers: dict = None,
        *args,
        **kwargs,
    ):
        """
        Abstraction for posting data
        """
        self._checkingDate()
        if headers is None:
            headers = self.header
        if self.loggingEnabled:
            self.logger.debug(
                f"Start POST request to {endpoint} with header: {json.dumps(headers)}"
            )
        if params is None and data is None:
            res = requests.post(endpoint, headers=headers)
        elif params is not None and data is None:
            if self.loggingEnabled:
                self.logger.debug(f"params: {json.dumps(params)}")
            res = requests.post(endpoint, headers=headers, params=params)
        elif params is None and data is not None:
            if self.loggingEnabled:
                self.logger.debug(f"data: {json.dumps(data)}")
            res = requests.post(endpoint, headers=headers, data=json.dumps(data))
        elif params is not None and data is not None:
            if self.loggingEnabled:
                self.logger.debug(f"params: {json.dumps(params)}")
                self.logger.debug(f"data: {json.dumps(data)}")
            res = requests.post(
                endpoint, headers=headers, params=params, data=json.dumps(data)
            )
        elif bytesData is not None:
            if self.loggingEnabled:
                self.logger.debug(f"bytes data used")
            res = requests.post(
                endpoint, headers=headers, params=params, data=bytesData
            )
        try:
            res_json = res.json()
        except:
            if kwargs.get("verbose", False):
                print("error generating the JSON response")
                print(f"status: {res.status_code}")
                print(res.text)
            if res.status_code != 200:
                res_json = {"error": "Request Error"}
            else:
                res_json = {}
        try:  ## sometimes list is being returned
            if "errorMessage" in res_json.keys():
                print(f"status code : {res.status_code}")
        except:
            pass
        return res_json

    def patchData(
        self,
        endpoint: str,
        params: dict = None,
        data=None,
        headers: dict = None,
        *args,
        **kwargs,
    ):
        """
        Abstraction for deleting data
        """
        self._checkingDate()
        if headers is None:
            headers = self.header
        if self.loggingEnabled:
            self.logger.debug(
                f"Start PATCH request to {endpoint} with header: {json.dumps(headers)}"
            )
        if params is not None and data is None:
            if self.loggingEnabled:
                self.logger.debug(f"params: {json.dumps(params)}")
            res = requests.patch(endpoint, headers=headers, params=params)
        elif params is None and data is not None:
            if self.loggingEnabled:
                self.logger.debug(f"data: {json.dumps(data)}")
            res = requests.patch(endpoint, headers=headers, data=json.dumps(data))
        elif params is not None and data is not None:
            if self.loggingEnabled:
                self.logger.debug(f"params: {json.dumps(params)}")
                self.logger.debug(f"data: {json.dumps(data)}")
            res = requests.patch(
                endpoint, headers=headers, params=params, data=json.dumps(data)
            )
        try:
            res_json = res.json()
        except:
            if kwargs.get("verbose", False):
                print("error generating the JSON response")
                print(f"status: {res.status_code}")
                print(res.text)
            if self.loggingEnabled:
                self.logger.error(
                    f"error with the response {res.status_code}: {res.text}"
                )
            if res.status_code != 200:
                res_json = {"error": res.text}
            else:
                res_json = {}
        return res_json

    def putData(
        self,
        endpoint: str,
        params: dict = None,
        data=None,
        headers: dict = None,
        *args,
        **kwargs,
    ):
        """
        Abstraction for deleting data
        """
        self._checkingDate()
        if headers is None:
            headers = self.header
        if self.loggingEnabled:
            self.logger.debug(
                f"Start PUT request to {endpoint} with header: {json.dumps(headers)}"
            )
        if params is not None and data is None:
            if self.loggingEnabled:
                self.logger.debug(f"params: {json.dumps(params)}")
            res = requests.put(endpoint, headers=headers, params=params)
        elif params is None and data is not None:
            if self.loggingEnabled:
                self.logger.debug(f"data: {json.dumps(data)}")
            res = requests.put(endpoint, headers=headers, data=json.dumps(data))
        elif params is not None and data is not None:
            if self.loggingEnabled:
                self.logger.debug(f"params: {json.dumps(params)}")
                self.logger.debug(f"data: {json.dumps(data)}")
            res = requests.put(
                endpoint, headers=headers, params=params, data=json.dumps(data)
            )
        try:
            res_json = res.json()
        except:
            if kwargs.get("verbose", False):
                print("error generating the JSON response")
                print(f"status: {res.status_code}")
                print(res.text)
            if self.loggingEnabled:
                self.logger.error(
                    f"error with the response {res.status_code}: {res.text}"
                )
            if res.status_code != 200:
                res_json = {"error": res.text}
            else:
                res_json = {}
        return res_json

    def deleteData(
        self, endpoint: str, params: dict = None, headers: dict = None, *args, **kwargs
    ):
        """
        Abstraction for deleting data
        """
        self._checkingDate()
        if headers is None:
            headers = self.header
        if self.loggingEnabled:
            self.logger.debug(
                f"Start PUT request to {endpoint} with header: {json.dumps(headers)}"
            )
        if params is None:
            res = requests.delete(endpoint, headers=headers)
        elif params is not None:
            res = requests.delete(endpoint, headers=headers, params=params)
        try:
            status_code = res.status_code
            if status_code >= 400:
                if self.loggingEnabled:
                    self.logger.error(
                        f"error with the response {res.status_code}: {res.text}"
                    )
                return res.json()
        except:
            if kwargs.get("verbose", False):
                print(res.text)
            status_code = res.status_code
        return status_code
