# Internal Library
import aepp
from aepp import connector
from copy import deepcopy
import pandas as pd
import logging


class Profile:
    """
    A class containing the different methods exposed on Customer Profile API.
    The API documentation is available:
    https://www.adobe.io/apis/experienceplatform/home/api-reference.html#!acpdr/swagger-specs/real-time-customer-profile.yaml
    """

    ## logging capability
    loggingEnabled = False
    logger = None

    def __init__(
        self,
        config: dict = aepp.config.config_object,
        header=aepp.config.header,
        loggingObject: dict = None,
        **kwargs,
    ):
        """
            This will instantiate the profile class
            Arguments:
                config : OPTIONAL : config object in the config module.
                header : OPTIONAL : header object  in the config module.
                loggingObject : OPTIONAL : logging object to log messages.
        kwargs:
            kwargsvaluewillupdatetheheader
        """
        if loggingObject is not None and sorted(
            ["level", "stream", "format", "filename", "file"]
        ) == sorted(list(loggingObject.keys())):
            self.loggingEnabled = True
            self.logger = logging.getLogger(f"{__name__}")
            self.logger.setLevel(loggingObject["level"])
            formatter = logging.Formatter(loggingObject["format"])
            if loggingObject["file"]:
                fileHandler = logging.FileHandler(loggingObject["filename"])
                fileHandler.setFormatter(formatter)
                self.logger.addHandler(fileHandler)
            if loggingObject["stream"]:
                streamHandler = logging.StreamHandler()
                streamHandler.setFormatter(formatter)
                self.logger.addHandler(streamHandler)
        self.connector = connector.AdobeRequest(
            config_object=config,
            header=header,
            loggingEnabled=self.loggingEnabled,
            logger=self.logger,
        )
        self.header = self.connector.header
        self.header["Accept"] = "application/vnd.adobe.xdm+json"
        self.header.update(**kwargs)
        self.sandbox = self.connector.config["sandbox"]
        # same endpoint than segmentation
        self.endpoint = (
            aepp.config.endpoints["global"] + aepp.config.endpoints["segmentation"]
        )

    def getEntity(
        self,
        schema_name: str = "_xdm.context.profile",
        entityId: str = None,
        entityIdNS: str = None,
        mergePoliciyId: str = None,
        **kwargs,
    ) -> dict:
        """
        Returns an entity by ID or Namespace.
        Arguments:
            schema_name : REQUIRED : class name of the schema to be retrieved. default : _xdm.context.profile
            entityId : OPTIONAL : identity ID
            entityIdNS : OPTIONAL : Identity Namespace code. Required if entityId is used (except for native identity)
            mergePoliciyId : OPTIONAL : Id of the merge policy.
        Possible kwargs:
            fields : path of the elements to be retrieved, separated by comma. Ex : "person.name.firstName,person.name.lastName"
            relatedSchema_name : If schema.name is "_xdm.context.experienceevent", this value must specify the schema for the profile entity that the time series events are related to.
            relatedEntityId : ID of the entity that the ExperienceEvents are associated with. Used when looking up ExperienceEvents. For Native XID lookup, use relatedEntityId=<XID> and leave relatedEntityIdNS absent;
            For ID:NS lookup, use both relatedEntityId and relatedEntityIdNS fields.
            relatedEntityIdNS : Identity Namespace code of the related entity ID of ExperienceEvent. Used when looking up ExperienceEvents. If this field is used, entityId cannot be empty.
            startTime : Start time of Time range filter for ExperienceEvents. Should be at millisecond granularity. Included. Default: From beginning.
            endTime : End time of Time range filter for ExperienceEvents. Should be at millisecond granularity. Excluded. Default: To the end.
            limit : Number of records to return from the result. Only for time-series objects. Default: 1000
        """
        if self.loggingEnabled:
            self.logger.debug(f"Starting getEntity")
        path = "/access/entities"
        params = {
            "schema.name": schema_name,
            "entityId": entityId,
            "entityIdNS": entityIdNS,
            "mergePoliciyId": mergePoliciyId,
        }
        if schema_name == "_xdm.context.experienceevent":
            params["relatedSchema.name"] = kwargs.get(
                "relatedSchema_name", "_xdm.context.profile"
            )
            params["relatedEntityId"] = kwargs.get("relatedEntityId", entityId)
            params["relatedEntityIdNS"] = kwargs.get("relatedEntityIdNS", None)
            params["limit"] = kwargs.get("limit", 1000)
            params["startTime"] = kwargs.get("startTime", None)
            params["endTime"] = kwargs.get("endTime", None)
        params["fields"] = kwargs.get("fields", None)
        res = self.connector.getData(
            self.endpoint + path, params=params, headers=self.header
        )
        return res

    def getEntities(self, request_data: dict = None) -> dict:
        """
        Get a number of different identities from ID or namespaces.
        Argument:
            request_data : Required : A dictionary that contains fields for the search.
            Example
            {
                "schema": {
                    "name": "_xdm.context.profile"
                },
                "relatedSchema": {
                    "name": "_xdm.context.profile"
                },
                "fields": [
                    "person.name.firstName"
                ],
                "identities": [
                    {
                    "entityId": "69935279872410346619186588147492736556",
                    "entityIdNS": {
                        "code": "CRMID"
                    }
                    }
                ],
                "timeFilter": {
                    "startTime": 1539838505,
                    "endTime": 1539838510
                },
                "limit": 10,
                "orderby": "-timestamp",
                "withCA": True
            }
        """
        if self.loggingEnabled:
            self.logger.debug(f"Starting getEntities")
        path = "/access/entities"
        if request_data is None or type(request_data) != dict:
            raise Exception("Expected a dictionary to fetch entities")
        res = self.connector.postData(
            self.endpoint + path, data=request_data, headers=self.header
        )
        return res

    def deleteEntity(
        self, schema_name: str = None, entityId: str = None, entityIdNS: str = None
    ) -> str:
        """
        Delete a specific entity
        Arguments:
            schema_name : REQUIRED : Name of the associated XDM schema.
            entityId : REQUIRED : entity ID
            entityIdNS : OPTIONAL : entity ID Namespace
        """
        path = "/access/entities"
        params = {}
        if schema_name is None:
            raise Exception("Expected a schema name")
        else:
            params["schema.name"] = schema_name
        if entityId is not None:
            params["entityId"] = entityId
        if entityIdNS is not None:
            params["entityIdNS"] = entityIdNS
        if self.loggingEnabled:
            self.logger.debug(f"Starting deleteEntity")
        res = self.connector.deleteData(
            self.endpoint + path, params=params, headers=self.header
        )

        return res

    def getMergePolicies(self, limit: int = 100) -> dict:
        """
        Returns the list of merge policies hosted in this instance.
        Arguments:
            limit : OPTIONAL : amount of merge policies returned by pages.
        """
        if self.loggingEnabled:
            self.logger.debug(f"Starting getMergePolicies")
        path = "/config/mergePolicies"
        params = {"limit": limit}
        res = self.connector.getData(
            self.endpoint + path, params=params, headers=self.header
        )
        data = res["children"]
        nextPage = res["_links"]["next"].get("href", "")
        while nextPage != "":
            path = "/config/mergePolicies?" + nextPage.split("?")[1]
            res = self.connector.getData(
                self.endpoint + path, params=params, headers=self.header
            )
            data += res["children"]
            nextPage = res["_links"]["next"].get("href", "")
        return data

    def getMergePolicy(self, policy_id: str = None) -> dict:
        """
        Return a specific merge policy.
        Arguments:
            policy_id : REQUIRED : id of the the policy id to be returned.
        """
        if policy_id is None:
            raise Exception("Missing the policy id")
        if self.loggingEnabled:
            self.logger.debug(f"Starting getMergePolicy")
        path = f"/config/mergePolicies/{policy_id}"
        res = self.connector.getData(self.endpoint + path, headers=self.header)
        return res

    def createMergePolicy(self, policy: dict = None) -> dict:
        """
        Arguments:
            policy: REQUIRED : The dictionary defining the policy
        Refer to the documentation : https://experienceleague.adobe.com/docs/experience-platform/profile/api/merge-policies.html
        Example of JSON:
        {
            "name": "real-time-customer-profile-default",
            "imsOrgId": "1BD6382559DF0C130A49422D@AdobeOrg",
            "schema": {
                "name": "_xdm.context.profile"
            },
            "default": False,
            "identityGraph": {
                "type": "pdg"
            },
            "attributeMerge": {
                "type": "timestampOrdered",
                "order": [
                "string"
                ]
            },
            "updateEpoch": 1234567890
        }
        """
        path = "/config/mergePolicies"
        if policy is None:
            raise ValueError("Require a dictionary")
        if self.loggingEnabled:
            self.logger.debug(f"Starting createMergePolicy")
        res = self.connector.postData(
            self.endpoint + path, data=policy, headers=self.header
        )
        return res

    def updateMergePolicy(self, mergePolicyId: str = None, policy: dict = None) -> dict:
        """
        Update a merge policy by replacing its definition. (PUT method)
        Arguments:
            mergePolicyId : REQUIRED : The merge Policy Id
            policy : REQUIRED : a dictionary giving the definition of the merge policy
            Refer to the documentation : https://experienceleague.adobe.com/docs/experience-platform/profile/api/merge-policies.html
        Example of JSON:
        {
            "name": "real-time-customer-profile-default",
            "imsOrgId": "1BD6382559DF0C130A49422D@AdobeOrg",
            "schema": {
                "name": "_xdm.context.profile"
            },
            "default": False,
            "identityGraph": {
                "type": "pdg"
            },
            "attributeMerge": {
                "type": "timestampOrdered",
                "order": [
                "string"
                ]
            },
            "updateEpoch": 1234567890
        }
        attributeMerge can be "dataSetPrecedence" or "timestampOrdered". Please provide a list of dataSetId on "order" when using the first option.
        """
        if mergePolicyId is None:
            raise ValueError("Require a mergePolicyId")
        if policy is None or type(policy) != dict:
            raise ValueError("Require a dictionary to update the merge policy")
        if self.loggingEnabled:
            self.logger.debug(f"Starting updateMergePolicy")
        path = f"/config/mergePolicies/{mergePolicyId}"
        res = self.connector.putData(
            self.endpoint + path, data=policy, headers=self.header
        )
        return res

    def patchMergePolicy(
        self, mergePolicyId: str = None, operations: list = None
    ) -> str:
        """
        Update a merge policy by replacing its definition.
        Arguments:
            mergePolicyId : REQUIRED : The merge Policy Id
            operations : REQUIRED : a list of operations to realize on the merge policy
            Refer to the documentation : https://experienceleague.adobe.com/docs/experience-platform/profile/api/merge-policies.html
        Example of operation:
        [
            {
                "op": "add",
                "path": "/identityGraph.type",
                "value": "pdg"
            }
        ]
        """
        if mergePolicyId is None:
            raise ValueError("Require a mergePolicyId")
        if operations is None or type(operations) != list:
            raise ValueError("Require a dictionary to update the merge policy")
        if self.loggingEnabled:
            self.logger.debug(f"Starting patchMergePolicy")
        path = f"/config/mergePolicies/{mergePolicyId}"
        res = self.connector.patchData(
            self.endpoint + path, data=operations, headers=self.header
        )
        return res

    def deleteMergePolicy(self, mergePolicyId: str = None) -> str:
        """
        Delete a merge policy by its ID.
        Arguments:
            mergePolicyId : REQUIRED : The merge Policy to be deleted
        """
        if mergePolicyId is None:
            raise ValueError("Require a mergePolicyId")
        if self.loggingEnabled:
            self.logger.debug(f"Starting deleteMergePolicy")
        path = f"/config/mergePolicies/{mergePolicyId}"
        res = self.connector.deleteData(self.endpoint + path)
        return res

    def getPreviewStatus(self) -> dict:
        """
        View the details for the last successful sample job that was run for the IMS Organization.
        """
        if self.loggingEnabled:
            self.logger.debug(f"Starting getPreviewStatus")
        path = "/previewsamplestatus"
        privateHeader = deepcopy(self.header)
        privateHeader["Accept"] = "application/json"
        res = self.connector.getData(self.endpoint + path, headers=privateHeader)
        return res

    def getPreviewDataSet(self, date: str = None, output: str = "raw") -> dict:
        """
        View a report showing the distribution of profiles by dataset.
        Arguments:
            date : OPTIONAL : Format: YYYY-MM-DD.
                If multiple reports were run on the date, the most recent report for that date will be returned.
                If a report does not exist for the specified date, a 404 error will be returned.
                If no date is specified, the most recent report will be returned.
                Example: date=2024-12-31
            output : OPTIONAL : if you want to have a dataframe returns. Use "df", default "raw"
        """
        if self.loggingEnabled:
            self.logger.debug(f"Starting getPreviewDataSet")
        path = "/previewsamplestatus/report/dataset"
        params = {}
        if date is not None:
            params["date"] = date
        privateHeader = deepcopy(self.header)
        privateHeader["Accept"] = "application/json"
        res = self.connector.getData(self.endpoint + path, headers=privateHeader)
        if output == "df":
            df = pd.DataFrame(res["data"])
            return df
        return res

    def getPreviewDataSetOverlap(self, date: str = None, output: str = "raw") -> dict:
        """
        Method to find the overlap of identities with datasets.
        Arguments:
            date : OPTIONAL : Format: YYYY-MM-DD.
                If multiple reports were run on the date, the most recent report for that date will be returned.
                If a report does not exist for the specified date, a 404 error will be returned.
                If no date is specified, the most recent report will be returned.
                Example: date=2024-12-31
            output : OPTIONAL : if you want to have a dataframe returns. Use "df", default "raw"
        """
        if self.loggingEnabled:
            self.logger.debug(f"Starting getPreviewDataSetOverlap")
        path = "/previewsamplestatus/report/dataset/overlap"
        params = {}
        if date is not None:
            params["date"] = date
        privateHeader = deepcopy(self.header)
        privateHeader["Accept"] = "application/json"
        privateHeader["x-model-name"] = "_xdm.context.profile"
        res = self.connector.getData(self.endpoint + path, headers=privateHeader)
        if output == "df":
            df = pd.DataFrame(res["data"])
            return df
        return res

    def getPreviewNamespace(self, date: str = None, output: str = "raw") -> dict:
        """
        View a report showing the distribution of profiles by namespace.
        Arguments:
            date : OPTIONAL : Format: YYYY-MM-DD.
                If multiple reports were run on the date, the most recent report for that date will be returned.
                If a report does not exist for the specified date, a 404 error will be returned.
                If no date is specified, the most recent report will be returned.
                Example: date=2024-12-31
            output : OPTIONAL : if you want to have a dataframe returns. Use "df", default "raw"
        """
        if self.loggingEnabled:
            self.logger.debug(f"Starting getPreviewNamespace")
        path = "/previewsamplestatus/report/namespace"
        params = {}
        if date is not None:
            params["date"] = date
        privateHeader = deepcopy(self.header)
        privateHeader["Accept"] = "application/json"
        res = self.connector.getData(self.endpoint + path, headers=privateHeader)
        if output == "df":
            df = pd.DataFrame(res["data"])
            return df
        return res

    def createDeleteSystemJob(self, dataSetId: str = None, batchId: str = None) -> dict:
        """
        Delete all the data for a batch or a dataSet based on their ids.
        Note: you cannot delete batch from record type dataset. You can overwrite them to correct the issue.
        Only Time Series and record type datasets can be deleted.
        Arguments:
            dataSetId : REQUIRED : dataSetId to be deleted
            batchId : REQUIRED : batchId to be deleted.
        More info: https://www.adobe.io/apis/experienceplatform/home/api-reference.html#/Profile_System_Jobs/createDeleteRequest
        """
        if self.loggingEnabled:
            self.logger.debug(f"Starting createDeleteSystemJob")
        path = "/system/jobs"
        if dataSetId is not None:
            obj = {"dataSetId": dataSetId}
            res = self.connector.postData(self.endpoint + path, data=obj)
            return res
        elif batchId is not None:
            obj = {"batchId": batchId}
            res = self.connector.postData(self.endpoint + path, data=obj)
            return res
        else:
            raise ValueError("Require a dataSetId or a batchId")

    def getDeleteSystemJobs(
        self, page: int = 0, limit: int = 100, n_results: int = 100
    ) -> dict:
        """
        Retrieve a list of all delete requests (Profile System Jobs) created by your organization.
        Arguments:
            page : OPTIONAL : Return a specific page of results, as per the create time of the request. For example, page=0
            limit : OPTIONAL : Limit response to a specific number of objects. Must be a positive number. For example, limit=10
            n_results : OPTIONAL : Number of total result to retrieve.
        """
        if self.loggingEnabled:
            self.logger.debug(f"Starting getDeleteSystemJobs")
        path = "/system/jobs"
        params = {"page": page, "limit": limit}
        res = self.connector.getData(self.endpoint + path, params=params)
        data = res["children"]
        count = len(data)
        nextPage = res["_page"].get("next", "")
        while nextPage != "" and count < n_results:
            page += 1
            params = {"page": page, "limit": limit}
            res = self.connector.getData(
                self.endpoint + path, params=params, headers=self.header
            )
            count += len(res["children"])
            data += res["children"]
            nextPage = res["_page"].get("next", "")
        return data

    def getDeleteSystemJob(self, jobId: str = None) -> dict:
        """
        Get a specific delete system job by its ID.
        Arguments:
            jobId : REQUIRED : Job ID to be retrieved.
        """
        if jobId is None:
            raise ValueError("Require a system Job ID")
        if self.loggingEnabled:
            self.logger.debug(f"Starting getDeleteSystemJob")
        path = f"/system/jobs/{jobId}"
        res = self.connector.getData(self.endpoint + path, headers=self.header)
        return res

    def deleteDeleteSystemJob(self, jobId: str = None) -> dict:
        """
        Delete a specific delete system job by its ID.
        Arguments:
            jobId : REQUIRED : Job ID to be deleted.
        """
        if jobId is None:
            raise ValueError("Require a system Job ID")
        if self.loggingEnabled:
            self.logger.debug(f"Starting deleteDeleteSystemJob")
        path = f"/system/jobs/{jobId}"
        res = self.connector.deleteData(self.endpoint + path, headers=self.header)
        return res

    def getComputedAttributes(self) -> list:
        """
        Retrieve the list of computed attributes set up in your organization.
        """
        if self.loggingEnabled:
            self.logger.debug(f"Starting getComputedAttributes")
        path = "/config/computedAttributes"
        res = self.connector.getData(self.endpoint + path)
        data = res["children"]
        nextPage = res["_page"].get("next", "")
        # while nextPage != "":
        #     res = self.connector.getData(self.endpoint+path,
        #                     params=params, headers=self.header)
        #     data += res['children']
        #     nextPage = res['_page'].get('next','')
        return res

    def getComputedAttribute(self, attributeId: str = None) -> dict:
        """
        Returns a single computed attribute.
        Arguments:
            attributeId : REQUIRED : The computed attribute ID.
        """
        if attributeId is None:
            raise ValueError("Require a computed attribute ID")
        if self.loggingEnabled:
            self.logger.debug(f"Starting getComputedAttribute")
        path = f"/config/computedAttributes/{attributeId}"
        res = self.connector.getData(self.endpoint + path)
        return res

    def deleteComputedAttribute(self, attributeId: str = None) -> dict:
        """
        Delete a specific computed attribute.
        Arguments:
            attributeId : REQUIRED : The computed attribute ID to be deleted.
        """
        if attributeId is None:
            raise ValueError("Require a computed attribute ID")
        if self.loggingEnabled:
            self.logger.debug(f"Starting deleteComputedAttributes")
        path = f"/config/computedAttributes/{attributeId}"
        res = self.connector.deleteData(self.endpoint + path)
        return res

    def getDestinations(self) -> dict:
        """
        Retrieve a list of edge projection destinations. The latest definitions are returned.
        """
        if self.loggingEnabled:
            self.logger.debug(f"Starting getDestinations")
        path = "/config/destinations"
        res = self.connector.getData(self.endpoint + path)
        return res

    def createDestination(self, destinationConfig: dict = None) -> dict:
        """
        Create a new edge projection destination. Assume that there is time between creation and propagation of this information to the edge.
        Arguments:
            destinationConfig : REQUIRED : the destination configuration
        """
        if self.loggingEnabled:
            self.logger.debug(f"Starting createDestination")
        privateHeader = deepcopy(self.header)
        privateHeader[
            "Content-Type"
        ] = "application/vnd.adobe.platform.projectionDestination+json; version=1"
        if destinationConfig is None:
            raise ValueError("Require a destination configuration object")
        path = "/config/destinations"
        res = self.connector.postData(
            self.endpoint + path, data=destinationConfig, headers=privateHeader
        )
        return res

    def getDestination(self, destinationId: str = None) -> dict:
        """
        Get a specific destination based on its ID.
        Arguments:
            destinationId : REQUIRED : The destination ID to be retrieved
        """
        if destinationId is None:
            raise ValueError("Require a destination ID")
        if self.loggingEnabled:
            self.logger.debug(f"Starting getDestination")
        path = f"/config/destinations/{destinationId}"
        res = self.connector.getData(self.endpoint + path)
        return res

    def deleteDestination(self, destinationId: str = None) -> dict:
        """
        Delete a specific destination based on its ID.
        Arguments:
            destinationId : REQUIRED : The destination ID to be deleted
        """
        if destinationId is None:
            raise ValueError("Require a destination ID")
        if self.loggingEnabled:
            self.logger.debug(f"Starting deleteDestination")
        path = f"/config/destinations/{destinationId}"
        res = self.connector.deleteData(self.endpoint + path)
        return res

    def updateDestination(
        self, destinationId: str = None, destinationConfig: dict = None
    ) -> dict:
        """
        Update a specific destination based on its ID. (PUT request)
        Arguments:
            destinationId : REQUIRED : The destination ID to be updated
            destinationConfig : REQUIRED : the destination config object to replace the old one.
        """
        if destinationId is None:
            raise ValueError("Require a destination ID")
        if destinationConfig is None:
            raise ValueError("Require a dictionation for updating the destination")
        if self.loggingEnabled:
            self.logger.debug(f"Starting updateDestination")
        privateHeader = deepcopy(self.header)
        privateHeader[
            "Content-Type"
        ] = "application/vnd.adobe.platform.projectionDestination+json"
        path = f"/config/destinations/{destinationId}"
        res = self.connector.putData(
            self.endpoint + path, data=destinationConfig, headers=privateHeader
        )
        return res

    def getProjections(self, schemaName: str = None, name: str = None) -> dict:
        """
        Retrieve a list of edge projection configurations. The latest definitions are returned.
        Arguments:
            schemaName : OPTIONAL : The name of the schema class associated with the projection configuration you want to access.
                example : _xdm.context.profile
            name : OPTIONAL : The name of the projection configuration you want to access.
                if name is specified, schemaName is also required.
        """
        path = "/config/projections"
        params = {}
        if name is not None and schemaName is None:
            raise AttributeError(
                "You must specify a schema name when setting a projection name"
            )
        if self.loggingEnabled:
            self.logger.debug(f"Starting getProjections")
        if schemaName is not None:
            params["schemaName"] = schemaName
        if name is not None:
            params["name"] = name
        res = self.connector.getData(
            self.endpoint + path, params=params, headers=self.headers
        )
        return res

    def getProjection(self, projectionId: str = None) -> dict:
        """
        Retrieve a single projection based on its ID.
        Arguments:
            projectionId : REQUIRED : the projection ID to be retrieved.
        """
        if projectionId is None:
            raise ValueError("Require a projection ID")
        if self.loggingEnabled:
            self.logger.debug(f"Starting getProjection")
        path = f"/config/projections/{projectionId}"
        res = self.connector.getData(self.endpoint + path)
        return res

    def deleteProjection(self, projectionId: str = None) -> dict:
        """
        Delete a single projection based on its ID.
        Arguments:
            projectionId : REQUIRED : the projection ID to be deleted.
        """
        if projectionId is None:
            raise ValueError("Require a projection ID")
        if self.loggingEnabled:
            self.logger.debug(f"Starting deleteProjection")
        path = f"/config/projections/{projectionId}"
        res = self.connector.deleteData(self.endpoint + path)
        return res

    def createProjection(
        self, schemaName: str = None, projectionConfig: dict = None
    ) -> dict:
        """
        Create a projection
        Arguments:
            schemaName : REQUIRED : XDM schema namess
            projectionConfig : REQUIRED : the object definiing the projection
        """
        if schemaName is None:
            raise ValueError("Require a schema name specified")
        if projectionConfig is None:
            raise ValueError("Require a projection configuration")
        if self.loggingEnabled:
            self.logger.debug(f"Starting createProjection")
        path = "/config/projections"
        params = {"schemaName": schemaName}
        privateHeader = deepcopy(self.header)
        privateHeader[
            "Content-Type"
        ] = "application/vnd.adobe.platform.projectionConfig+json; version=1"
        res = self.connector.postData(
            self.endpoint + path,
            params=params,
            data=projectionConfig,
            privateHeader=privateHeader,
        )
        return res

    def updateProjection(
        self, projectionId: str = None, projectionConfig: dict = None
    ) -> dict:
        """
        Update a projection based on its ID.(PUT request)
        Arguments:
            projectionId : REQUIRED : The ID of the projection to be updated.
            projectionConfig : REQUIRED : the object definiing the projection
        """
        if projectionId is None:
            raise ValueError("Require a projectionId")
        if projectionConfig is None:
            raise ValueError("Require a projection Configuration object")
        if self.loggingEnabled:
            self.logger.debug(f"Starting updateProjection")
        privateHeader = deepcopy(self.header)
        privateHeader[
            "Content-Type"
        ] = "application/vnd.adobe.platform.projectionConfig+json"
        path = f"/config/projections/{projectionId}"
        res = self.connector.putData(
            self.endpoint + path, data=projectionConfig, headers=privateHeader
        )
        return res
