# Copyright (C) 2020-2022 HERE Global B.V. and its affiliate(s).
# All rights reserved.
#
# This software and other materials contain proprietary information
# controlled by HERE and are protected by applicable copyright legislation.
# Any use and utilization of this software and other materials and
# disclosure to any third parties is conditional upon having a separate
# agreement with HERE for the access, use, utilization or disclosure of this
# software. In the absence of such agreement, the use of the software is not
# allowed.
"""HERE platform module
"""
import json
import logging
import time
import uuid
from typing import Any, Dict, Iterator, List, Optional, cast

from here.platform.adapter import Adapter
from here.platform.adapter_default import DefaultAdapter
from here.platform.api.aaa_authorization_api import AAAAuthorizationApi
from here.platform.api.aaa_oauth2_api import AAAOauth2Api
from here.platform.api.artifact_api import AccessType, ArtifactApi
from here.platform.api.base_api import BaseApi
from here.platform.api.data_config_api import DataConfigApi
from here.platform.api.factory.apifactory import API, APIFactory
from here.platform.api.lookup_api import LookupApi
from here.platform.api.map_matching_api import MapMatchingApi
from here.platform.api.registry import LookupApiRegistry
from here.platform.api.service_registry_api import ServiceRegistryApi
from here.platform.artifact import Artifact, ArtifactConfiguration
from here.platform.auth import Auth
from here.platform.catalog import Catalog
from here.platform.config.application_config import ApplicationConfig
from here.platform.config.platform_config import PlatformConfig
from here.platform.credentials import CredentialsType, PlatformCredentials
from here.platform.environment import Environment
from here.platform.exceptions import AuthenticationException
from here.platform.layer import Layer
from here.platform.map_matcher import MapMatcher
from here.platform.model.subscription import (
    InteractiveMapSubscription,
    InteractiveMapSubscriptionStatus,
    InteractiveMapSubscriptionType,
    InteractiveMapUnsubscribe,
)
from here.platform.project import Project
from here.platform.schema import SchemaRegistry
from here.platform.schema_api import Schema, SchemaConfiguration
from here.platform.service import Service, ServiceConfiguration

logger = logging.getLogger(__name__)


class Platform:
    """
    This class is responsible for interacting with the HERE platform.

    It requires PlatformCredentials, ApplicationConfig, Environment and proxies configuration.
    If not provided, there are defined using default values or standard configuration files.
    """

    def __init__(
        self,
        credentials: Optional[PlatformCredentials] = None,
        application_config: Optional[ApplicationConfig] = None,
        environment: Optional[Environment] = None,
        proxies: Optional[dict] = None,
        billing_tag: Optional[str] = None,
        adapter: Optional[Adapter] = None,
        project_hrn: Optional[str] = None,
        platform_config: Optional[PlatformConfig] = None,
    ):
        """
        Instantiate Platform.

        :param credentials: an instance of PlatformCredentials
        :param application_config: an instance of ApplicationConfig
        :param environment: an instance of Environment
        :param proxies: dictionary with proxy configuration per protocol,
                        e.g. ``{"http": "http://...", "https": "https://..."}``,
                        ``None`` to obtain proxy configuration from environment variables.
        :param adapter: the ``Adapter`` to transform data in different representation.
          A default one is provided in case ``None`` is specified.
        :param billing_tag: A string to represent a grouping of billing records.
        :param project_hrn: A string representing a project.
        :param platform_config: the properties of Platform Configuration.

        For more information about proxy configuration, please see:
        https://requests.readthedocs.io/en/master/user/advanced/#proxies
        """

        self._environment = environment or Environment.from_default()
        self._local_environment = self._environment == Environment.LOCAL
        self.credentials: Optional[PlatformCredentials] = None
        if not self._local_environment:
            # Get credentials only when non-local environment
            self.credentials = credentials or PlatformCredentials.from_default()
        self.application_config = application_config or ApplicationConfig.from_default()
        self._platform_config = platform_config or PlatformConfig.from_environment(
            self._environment
        )
        self._polling_wait = (
            0.1 if self._local_environment else self.application_config.polling_wait
        )
        self._retry_max_time = (
            5 if self._local_environment else self.application_config.retry_max_time
        )

        self.proxies = proxies

        self.default_adapter: Adapter = DefaultAdapter()
        self.adapter: Adapter = adapter or self.default_adapter
        self.lookup_api_registry = LookupApiRegistry()
        self.billing_tag = billing_tag
        self._project_hrn = project_hrn

        if self._platform_config.account_url is None:
            self.auth = None
        else:
            if (
                self.credentials is not None
                and self.credentials.cred_type == CredentialsType.Credentials
            ):
                aaa_oauth2_api = AAAOauth2Api(
                    base_url=self.credentials.cred_properties["endpoint"],
                    platform_config=self.platform_config,
                    application_config=self.application_config,
                    proxies=proxies,
                )
                self.auth = Auth(self.credentials, aaa_oauth2_api=aaa_oauth2_api)
            elif (
                self.credentials is not None
                and self.credentials.cred_type == CredentialsType.Token
            ):
                assert self.credentials
                self.auth = Auth(self.credentials)
            else:
                # Unexpected credentials type
                assert False

        if self._project_hrn and self.auth is not None:
            self.auth.set_scope(self._project_hrn)

        self.api_factory: APIFactory = APIFactory(
            platform_config=self.platform_config,
            application_config=self.application_config,
            auth=cast(Auth, self.auth),
            proxies=cast(dict, self.proxies),
            local_environment=self._local_environment,
            lookup_api_registry=self.lookup_api_registry,
        )

        self._schema_reg: SchemaRegistry = None  # type: ignore

    @property
    def base_api(self) -> BaseApi:
        """
        Lazy loads the service BaseApi.

        :return: BaseApi instance.
        """
        return cast(BaseApi, self.api_factory.get_api(API.BASE))

    @property
    def data_config_api(self) -> DataConfigApi:
        """
        Lazy loads the service DataConfig API.

        :return: DataConfigApi instance.
        """
        return cast(DataConfigApi, self.api_factory.get_api(API.DATA_CONFIG))

    @property
    def lookup_api(self) -> LookupApi:
        """
        Lazy loads the service Lookup API.

        :return: LookupApi instance.
        """
        return cast(LookupApi, self.api_factory.get_api(API.LOOKUP))

    @property
    def artifact_api(self) -> ArtifactApi:
        """
        Lazy loads the service Artifact API.

        :return: ArtifactApi instance.
        """
        return cast(ArtifactApi, self.api_factory.get_api(API.ARTIFACT))

    @property
    def schema_registry(self) -> SchemaRegistry:
        """
        Lazy loads the service SchemaRegistry API.

        :return: singleton SchemaRegistry instance.
        """
        if self._schema_reg is None:
            self._schema_reg = SchemaRegistry(
                self.platform_config, self.api_factory.get_api(API.ARTIFACT)
            )
        return self._schema_reg

    @property
    def aaa_auth_api(self) -> AAAAuthorizationApi:
        """
        Lazy loads the service AAAAuthorization API.

        This API is not implemented in the Local Data Service.

        :return: AAAAuthorization instance.
        """
        return cast(AAAAuthorizationApi, self.api_factory.get_api(API.AAA_AUTH))

    @property
    def service_registry_api(self) -> ServiceRegistryApi:
        """
        Lazy loads the service registry API.

        This API is not implemented in the Local Data Service.

        :return: ServiceRegistryApi instance.
        """
        return cast(ServiceRegistryApi, self.api_factory.get_api(API.SERVICE_REGISTRY))

    @property
    def map_matching_api(self) -> MapMatchingApi:
        """
        Lazy loads the service registry API.

        This API is not implemented in the Local Data Service.

        :return: ServiceRegistryApi instance.
        """
        return cast(MapMatchingApi, self.api_factory.get_api(API.MAP_MATCHING))

    @property
    def environment(self) -> Environment:
        """
        Return the platform environment.

        :return: environment
        :rtype: Environment
        """
        return self._environment

    @environment.setter
    def environment(self, environment):
        """
        Set the platform environment and platform config based on environment.

        :param environment: environment
        """
        self._environment = environment
        self._platform_config = PlatformConfig.from_environment(environment)

    @property
    def platform_config(self) -> PlatformConfig:
        """
        Return the platform config.

        :return: Platform configuration
        :rtype: PlatformConfig
        """
        return self._platform_config

    def get_status(self, as_type="json", event_type="incident"):
        """
        Get platform status

        :param as_type: format of the response
        :param event_type: type of event
        :return: Platform status in the desired format
        :raises ValueError: Incorrect parameter value
        """
        platform_status_url = self.platform_config.platform_status_url
        if platform_status_url is None:
            raise ValueError("platform_status_url is not present in configuration.")
        if as_type not in ["json", "xml"]:
            msg = 'Parameter as_type must be "xml" or "json" (the default).'
            raise ValueError(msg)
        params = {"format": as_type, "event": event_type}
        resp = self.base_api.get(url=platform_status_url, params=params)
        if as_type == "xml":
            status = resp.text
        else:
            status = resp.json()
        return status

    def get_catalog(
        self, hrn: str, adapter: Optional[Adapter] = None, billing_tag: Optional[str] = None
    ) -> Catalog:
        """
        Return a :class:`Catalog` object for the given HRN.

        :param hrn: a string representing a *HERE Resource Name*.
        :param adapter: the ``Adapter`` to transform data between different representations.
          The platform adapter is used case ``None`` is specified.
        :param billing_tag: A string to represent a grouping of billing records.
                            If None, platform billing tag will be used,if present.

        :return: :class:`Catalog` object
        :raises ValueError: if the catalog does not exist or credentials
                            don't provide access to it

        Usage::

          >>> from here.platform import Platform
          >>> platform = Platform()
          >>> platform.get_catalog("hrn:here:data::olp-here:oma-3") #doctest: +ELLIPSIS
          <here.platform.catalog.Catalog object at 0x...>
        """
        billing_tag = billing_tag or self.billing_tag
        if self.data_config_api.catalog_exists(catalog_hrn=hrn):
            return Catalog(hrn=hrn, platform=self, adapter=adapter, billing_tag=billing_tag)
        else:
            raise ValueError(
                f"Catalog {hrn} does not exist or credentials don't provide access to it"
            )

    def catalog_exists(self, hrn: str) -> bool:
        """
        Check whether a catalog with the specified HRN exists.

        :param hrn: a *HERE Resource Name*
        :return: a boolean value indicating if a catalog exists
        """
        return self.data_config_api.catalog_exists(catalog_hrn=hrn, billing_tag=self.billing_tag)

    def create_catalog(
        self,
        id: str,
        name: str,
        summary: str,
        description: str,
        billing_tag: Optional[str] = None,
        **details,
    ) -> Catalog:
        """
        Create a catalog and return a :class:`Catalog` object for the given metadata.

        :param id: An identifier unique within the realm, used to construct the catalog HRN
        :param name: The short name for the catalog
        :param summary: A one-sentence summary of the catalog
        :param description: A detailed description of the catalog and what it contains
        :param details: Optional catalog details - tags, layers, version, notifications and replication.
                        See the documentation for the catalog configuration for more informations
        :param billing_tag: A string to represent a grouping of billing records.
                        If None, platform billing tag will be used,if present.
        :return: a :class:`Catalog` object

        Usage::

          >>> from here.platform import Platform
          >>> platform = Platform()
          >>> catalog = platform.create_catalog(id="create-catalog", name="name", # doctest: +SKIP
                            summary="summary", description="description", tags=["tag1", "tag2"]) # doctest: +SKIP # noqa: E501
        """
        data = details.copy()
        data["id"] = id
        data["name"] = name
        data["summary"] = summary
        data["description"] = description

        response = self.data_config_api.create_catalog(data, billing_tag=self.billing_tag)
        status_response, complete = self.data_config_api.get_catalog_status(
            response["href"], self.billing_tag
        )
        while not complete:
            logging.debug(f"status polling wait {self._polling_wait} sec.")
            time.sleep(self._polling_wait)
            status_response, complete = self.data_config_api.get_catalog_status(
                response["href"], self.billing_tag
            )
        hrn = status_response.get("hrn")
        if not hrn:
            # LocalDataServer returns a different response than does Platform
            hrn = status_response["item"]["href"].rsplit("/", 1)[-1]
        return Catalog(hrn, platform=self, billing_tag=billing_tag)

    def list_catalogs(self, **filters: str) -> List[Catalog]:
        """
        List all the catalogs accessible on the HERE platform.

        Optionally, search and return only catalogs specified by some filter criteria.
        It does not return catalog that credentials don't provide access to.

        :param filters: keywords to search for
        :return: a list of :class:`Catalog` objects

        Usage::

          >>> from here.platform import Platform
          >>> platform = Platform()
          >>> platform.list_catalogs(coverage="CN") # doctest: +ELLIPSIS
          [<here.platform.catalog.Catalog object at ...]
        """
        catalogs_info = self.data_config_api.get_catalogs(**filters)
        try:
            catalog_items = catalogs_info["results"]["items"]
        except KeyError:
            catalog_items = []
        catalogs = []
        for cat in catalog_items:
            try:
                catalogs.append(Catalog(cat["hrn"], platform=self))
            except AuthenticationException:
                logger.warning("Unable to access HRN {}. Skipping.".format(cat["hrn"]))
        return catalogs

    def modify_catalog(self, hrn: str, **details):
        """
        Modify the catalog details, including name, summary, description.

        This replaces the complete details of the catalog.

        After this call succeedes, obtain a new catalog via `get_catalog`
        to have access to the modified details, including affected layers.

        :param hrn: the *HERE Resource Name* of the catalog
        :param details: Optional catalog details - tags, layers, version, notifications and replication.
                        See the documentation for the catalog configuration for more informations.

        The documentation for the API endpoint used can be found here:
        |modify_catalog|

        .. |modify_catalog| raw:: html

          <a href="https://developer.here.com/documentation/data-api/api-reference-config.html" target="_blank">Modify a Catalog</a>  # noqa E501

        Usage::

          >>> from here.platform import Platform
          >>> platform = Platform()
          >>> platform.modify_catalog(hrn="hrn:here:data::olp-here:update", name='updated-name', description='updated-description') # doctest: +SKIP
        """
        if not details:
            return
        response = self.data_config_api.patch_catalog(hrn, data=details)
        status_response, complete = self.data_config_api.get_catalog_status(
            response["href"], billing_tag=self.billing_tag
        )
        while not complete:
            logging.debug(f"status polling wait {self._polling_wait} sec.")
            time.sleep(self._polling_wait)
            status_response, complete = self.data_config_api.get_catalog_status(
                response["href"], billing_tag=self.billing_tag
            )

    def update_catalog(
        self,
        hrn: str,
        name: Optional[str] = None,
        summary: Optional[str] = None,
        description: Optional[str] = None,
        new_layers: Optional[List[Dict]] = None,
        **details,
    ):
        """
        Update the catalog details.

        This replaces the complete details of the catalog.

        After this call succeedes, obtain a new catalog via `get_catalog`
        to have access to the updated details, including affected layers.

        :param hrn: the *HERE Resource Name* of the catalog
        :param name: the optional new short name of the catalog.
        :param summary: the optional new summary of the catalog.
        :param description: the optional new detailed description of the catalog.
        :param new_layers: the optional list of new layers - each new layer needs to be a dict
                           For the format please see the documentation for the catalog configuration.
                           Please note that layers can only be added to the catalog using this function.
        :param details: Optional catalog details - tags, version, notifications and replication.
                        See the documentation for the catalog configuration for more information.

        The documentation for the API endpoint used can be found here:
        |update_catalog|

        .. |update_catalog| raw:: html

          <a href="https://developer.here.com/documentation/data-api/api-reference-config.html" target="_blank">Updates a Catalog</a>  # noqa E501

        Usage::

          >>> from here.platform import Platform
          >>> platform = Platform()
          >>> new_layer = {}  # put the configuration of a new layer here as a dict
          >>> platform.update_catalog(hrn='hrn:here:data::olp-here:update', name='updated', new_layers=[new_layer]) # doctest: +SKIP
        """
        data = details.copy()
        assert not ("layers" in data and new_layers is not None), (
            "You can either define the new_layers or overwrite"
            " the existing layers but not both at the same time."
        )
        catalog = self.get_catalog(hrn)
        catalog_conf = catalog.configuration
        # the "id" field cannot be updated however it must be in the request body
        data["id"] = catalog_conf.id
        data["name"] = name if name is not None else catalog_conf.name
        data["summary"] = summary if summary is not None else catalog_conf.summary
        data["description"] = description if description is not None else catalog_conf.description
        if "layers" not in data:
            data["layers"] = [lyr.json for lyr in catalog_conf.layers]
        if new_layers is not None:
            data["layers"] += new_layers
        response = self.data_config_api.update_catalog(
            catalog_hrn=hrn, data=data, billing_tag=self.billing_tag
        )
        status_response, complete = self.data_config_api.get_catalog_status(
            response["href"], billing_tag=self.billing_tag
        )
        while not complete:
            logging.debug(f"status polling wait {self._polling_wait} sec.")
            time.sleep(self._polling_wait)
            status_response, complete = self.data_config_api.get_catalog_status(
                response["href"], billing_tag=self.billing_tag
            )

    def delete_catalog(self, hrn: str):
        """
        Delete a catalog along with the layers it contains.

        :param hrn: the *HERE Resource Name* of the catalog

        Usage::

          >>> from here.platform import Platform
          >>> platform = Platform()
          >>> platform.delete_catalog("hrn:here:data::olp-here:delete") # doctest: +SKIP
        """
        response = self.data_config_api.delete_catalog(hrn, self.billing_tag)
        while True:
            logging.debug(f"status polling wait {self._polling_wait} sec.")
            time.sleep(self._polling_wait)
            status_response, complete = self.data_config_api.get_catalog_status(
                response["href"], billing_tag=self.billing_tag
            )
            status = status_response["status"]
            logger.debug(f"Catalog delete: {hrn} state: {status}")
            if complete:
                self.api_factory.remove_catalog_apis(hrn)
                logger.info(f"Catalog deletion for hrn: {hrn} finished with status: {status}")
                return

    def get_project(self, hrn: str) -> Project:
        """
        Return a :class:`Project` object for the given HRN.

        :param hrn: a string representing a *HERE Resource Name*.

        :return: the Project.

        Usage::

        >>> from here.platform import Platform
        >>> platform = Platform()
        >>> platform.get_project("hrn:here:authorization::olp-here:project/test")# doctest: +SKIP
        """
        return Project(hrn, platform=self)

    def create_project(self, project_id: str, project_name: str, project_desc: str) -> Project:
        """
        Create a project.

        :param project_id: A unique id for the project.
        :param project_name: The short name for the project.
        :param project_desc: A detailed description of the project and what it contains.
        :return: a :class 'Project' object.

        Usage::

        >>> from here.platform import Platform
        >>> platform = Platform()
        >>> project = platform.create_project(id="create-project", name="name",# doctest: +SKIP
        >>>                              description="description") # doctest: +SKIP
        """
        payload = {"id": project_id, "name": project_name, "description": project_desc}
        response = self.aaa_auth_api.create_project(body=payload)
        return Project(response["hrn"], platform=self)

    def list_projects(
        self,
        limit: Optional[int] = None,
        can_manage: Optional[bool] = None,
        is_member: Optional[bool] = None
        # page_token: Optional[str] = None
    ) -> List[Project]:
        """
        Get the list of projects that you are a project admin or a member based on the can_manage
        and is_member.

        :param limit: Number of entries to be returned in the response
        :param can_manage: If true returns all projects of which the
            caller(user/app) is a project admin
        :param is_member: If true returns all projects of which the caller(user/app) is a member

        :return: a list of projects.

        Usage::

        >>> from here.platform import Platform
        >>> platform = Platform()
        >>> resp = platform.list_projects(limit=10) # doctest: +SKIP
        """
        response = self.aaa_auth_api.get_project_list(
            limit=str(limit) if limit else None,
            # page_token=page_token,
            can_manage=can_manage,
            is_member=is_member,
        )

        return [Project(p["hrn"], self) for p in response["items"]]

    def update_project(self, hrn: str, project_name: str, project_desc: str):
        """
        Update the project metadata.

        :param hrn: HRN identifying the project
        :param project_name: The short name for the project.
        :param project_desc: A detailed description of the project and what it contains.
        """
        payload = {"name": project_name, "description": project_desc}
        self.aaa_auth_api.patch_project(project=hrn, body=payload)

    def delete_project(self, hrn: str):
        """
        Delete a project along with the catalogs it contains.

        :param hrn: HRN identifying the project
        """
        self.aaa_auth_api.delete_project(hrn)

    def leave_project(self, hrn: str):
        """
        Remove the caller from the specified Project.

        :param hrn: HRN identifying the project

        Usage::

        >>> from here.platform import Platform
        >>> platform = Platform()
        >>> platform.leave_project("hrn:here:authorization::" # doctest: +SKIP
                               "olp-here:project/test") # doctest: +SKIP
        """
        self.aaa_auth_api.leave_project(hrn)

    def list_layer_details(self, **filters: str) -> List[dict]:
        """
        List all the layer details accessible on the HERE platform.

        Optionally, search and return only layers specified by some filter criteria.
        It does not return layers that credentials don't provide access to.

        :param filters: keywords to search for
        :return: list of layer details

        Usage::

          >>> from here.platform import Platform
          >>> import pandas as pd
          >>> platform = Platform()
          >>> layers = platform.list_layer_details(coverage='US') # doctest: +ELLIPSIS
        """
        filters["verbose"] = "True"
        catalogs_info = self.data_config_api.get_catalogs(**filters)
        catalogs = catalogs_info["results"]["items"]
        layer_list = []
        for cat in catalogs:
            layers = cat["layers"]
            try:
                catalog = self.get_catalog(cat["hrn"])
            except AuthenticationException:
                logger.info(f"App doesn't have access to {cat['hrn']}.")
                continue
            for layer in layers:
                layer = catalog.get_layer(layer_id=layer["id"])
                layer_list.append(layer.get_details())
        return layer_list

    def list_layers(self, **filters: str) -> List[Layer]:
        """
        List all the layer details accessible on the HERE platform.

        Optionally, search and return only layers specified by some filter criteria.
        It does not return layers that credentials don't provide access to.

        :param filters: keywords to search for
        :return: dataframe with layer details

        Usage::

          >>> from here.platform import Platform
          >>> import pandas as pd
          >>> platform = Platform()
          >>> layer_df = platform.list_layers(coverage='US') # doctest: +ELLIPSIS
        """
        filters["verbose"] = "True"
        catalogs_info = self.data_config_api.get_catalogs(**filters)
        catalogs = catalogs_info["results"]["items"]
        layer_list = []
        for cat in catalogs:
            layers = cat["layers"]
            try:
                catalog = self.get_catalog(cat["hrn"])
            except AuthenticationException:
                logger.info(f"App doesn't have access to {cat['hrn']}.")
                continue
            for layer in layers:
                layer = catalog.get_layer(layer_id=layer["id"])
                layer_list.append(layer)
        return layer_list

    def list_services(self) -> List[Service]:
        """
        List all the services accessible on the HERE platform.

        :return: a list of :class:`Service` objects

        Usage::

          >>> from here.platform import Platform
          >>> platform = Platform()
          >>> platform.list_services() # doctest: +ELLIPSIS
          [<here.platform.service.Service object at ...]
        """
        services_info = self.service_registry_api.get_services()
        services = [
            Service(
                hrn=service_info["hrn"],
                configuration=ServiceConfiguration.from_dict(json_dict=service_info),
                platform=self,
            )
            for service_info in services_info
        ]
        return services

    def get_service(self, hrn: str) -> Service:
        """
        Return a :class:`Service` object for the given HRN.

        :param hrn: a string representing a *HERE Resource Name*.
        :return: :class:`Service` object

        Usage::

          >>> from here.platform import Platform
          >>> platform = Platform()
          >>> platform.get_service("hrn:here:service::olp-here:routing-8") #doctest: +ELLIPSIS
          <here.platform.service.Service object at 0x...>
        """
        service_info = self.service_registry_api.get_service(service_hrn=hrn)
        service_config = ServiceConfiguration.from_dict(json_dict=service_info)
        return Service(hrn=hrn, configuration=service_config, platform=self)

    def clone_catalog(
        self,
        source: Catalog,
        id: str,
        name: Optional[str] = None,
        summary: Optional[str] = None,
        description: Optional[str] = None,
        billing_tag: Optional[str] = None,
        layers: Optional[List] = None,
    ):
        """
        Create a catalog from a source catalog and return
        a :class:`Catalog` object. Cloning will be restricted to configuration.
        No data will be cloned or copied from source to new catalog.

        :param source: Catalog object from which new catalog need to be cloned.
        :param id: An identifier unique within the realm, used to construct the catalog HRN
        :param name: The short name for the catalog
        :param summary: A one-sentence summary of the catalog
        :param description: A detailed description of the catalog and what it contains
        :param billing_tag: A string to represent a grouping of billing records.
                        If None, platform billing tag will be used,if present.
        :param layers: A list of layer ids to be added in clone catalog.
                        These layer ids must be present in source catalog.
                        If None, all layers present in source catalog will be copied.
        :return: a :class:`Catalog` object

        """
        source_details = source.get_details()
        layer_list = []
        if layers:
            for layer in layers:
                layer = source.get_layer(layer_id=layer)
                layer_details = layer.get_details()
                layer_list.append(layer_details)
        cat = self.create_catalog(
            id=id,
            name=name if name else source_details["name"],
            summary=summary if summary else source_details["summary"],
            description=description if description else source_details["description"],
            billing_tag=billing_tag,
            layers=layer_list if layers else source_details["layers"],
        )
        return cat

    def map_matcher_map_versions(self) -> List[MapMatcher]:
        """
        List all available map matcher map versions
        """
        map_matching_info = self.map_matching_api.map_matcher_map_versions()
        map_matcher_list = []
        map_matcher_dict = json.loads(map_matching_info.replace("'", '"'))
        for map_matcher in map_matcher_dict["versions"]:
            map_matcher_obj = MapMatcher(
                version=map_matcher["version"], hrn=map_matcher["hrn"], platform=self
            )
            map_matcher_list.append(map_matcher_obj)
        return map_matcher_list

    def get_map_matcher_map_version(self, version: str = "latest") -> MapMatcher:
        """
        Gets information for a specific map matcher map version
        :param version: map version to use or `latest` to use the latest available version
        :return: a :class:`MapMatcher` object
        """
        map_version = self.map_matching_api.get_map_matcher_map_version(version)
        map_matcher_dict = json.loads(map_version.replace("'", '"'))
        return MapMatcher(
            version=map_matcher_dict["version"], hrn=map_matcher_dict["hrn"], platform=self
        )

    def list_artifacts(
        self,
        access: Optional[AccessType] = None,
        sort: Optional[str] = None,
        order: Optional[str] = None,
        from_param: Optional[str] = None,
        group_id: Optional[str] = None,
        artifact_id: Optional[str] = None,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
    ) -> List[Artifact]:
        """
        List all the artifacts accessible on the HERE platform.

        :param access: Comma separated list of access types (with OR semantic) to filter result.
            If 'default', returns artifacts user has read access to.
            If 'orgAdmin', returns artifacts
            OrgAdmin user have access to.
        :param sort: sort parameter
        :param order: order of sorting. Available values : ASC, DESC
        :param from_param: from parameter
        :param group_id: Applies filtering based on groupId
        :param artifact_id: Applies filtering based on artifactId
        :param limit: limit number of records
        :param offset: offset number of records
        :return: response from the API.
        """

        artifacts = self.artifact_api.list_artifacts(
            access=access,
            sort=sort,
            order=order,
            from_param=from_param,
            group_id=group_id,
            artifact_id=artifact_id,
            limit=limit,
            offset=offset,
        )
        artifacts_list = [
            Artifact(
                hrn=artifact["hrn"],
                configuration=ArtifactConfiguration.from_dict(json_dict=artifact),
                platform=self,
            )
            for artifact in artifacts["items"]
        ]
        return artifacts_list

    def get_artifact(self, artifact_hrn: str) -> Artifact:
        """
        Return the information about artifact (hrn, groupId, artifactId, version) and linked
        files.

        :param artifact_hrn: The HRN of the artifact.
        :return: response from the API.
        """
        artifact_details = self.artifact_api.get_artifact(artifact_hrn=artifact_hrn)
        return Artifact(
            hrn=artifact_details["artifact"]["hrn"],
            configuration=ArtifactConfiguration.from_dict(json_dict=artifact_details["artifact"]),
            platform=self,
        )

    def delete_artifact(self, artifact_hrn: str, force: Optional[bool] = None) -> bool:
        """
        Delete the artifact and related files by given HRN.

        :param artifact_hrn: The HRN of the schema.
        :param force: The flag to force the deletion even if the artifact
         is linked to a project. By default force flag is set to false.
        :return: response from the API.
        """
        return self.artifact_api.delete_artifact(artifact_hrn=artifact_hrn)

    def list_schemas(
        self,
        access: Optional[str] = None,
        sort: Optional[str] = None,
        order: Optional[str] = None,
        from_param: Optional[str] = None,
        group_id: Optional[str] = None,
        artifact_id: Optional[str] = None,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
    ) -> List[Schema]:
        """
        Return the list of available schemas.

        :param access: Comma separated list of access types (with OR semantic) to filter result.
            If 'default', returns schema user have read access to. If 'orgAdmin', returns catalogs
            OrgAdmin user have access to.
        :param sort: sort parameter
        :param order: order of sorting
        :param from_param: from parameter
        :param group_id: Applies filtering based on groupId
        :param artifact_id: Applies filtering based on artifactId
        :param limit: limit number of records
        :param offset: offset number of records
        :return: response from the API.
        """
        schemas = self.artifact_api.list_schemas(
            access=access,
            sort=sort,
            order=order,
            from_param=from_param,
            group_id=group_id,
            artifact_id=artifact_id,
            limit=limit,
            offset=offset,
        )

        schemas_list = [
            Schema(
                hrn=schema["hrn"],
                configuration=SchemaConfiguration.from_dict(json_dict=schema),
                platform=self,
            )
            for schema in schemas["items"]
        ]
        return schemas_list

    def get_schema(self, schema_hrn: str) -> Schema:
        """
        Return the information about schema (hrn, groupId, artifactId, version) and related
        artifact and variants for the given HRN.

        :param schema_hrn: The HRN of the schema.
        :return: response from the API.
        """
        schema_details = self.artifact_api.get_schema(schema_hrn=schema_hrn)
        return Schema(
            hrn=schema_details["schema"]["hrn"],
            configuration=SchemaConfiguration.from_dict(json_dict=schema_details["schema"]),
            platform=self,
        )

    def delete_schema(self, schema_hrn: str) -> bool:
        """
        Delete the schema and related artifacts by given HRN.

        :param schema_hrn: The HRN of the schema.
        :return: response from the API.
        """
        return self.artifact_api.delete_schema(schema_hrn=schema_hrn)

    def get_all_subscriptions(
        self, limit: Optional[int] = None
    ) -> Iterator[InteractiveMapSubscription]:
        """
        Lists all subscriptions that your account has access to.

        :param limit: number of records to limit per fetch.
        :yields: A :`List of InteractiveMapSubscription class` object
        """

        page_token = ""

        while True:
            filters: Dict[Any, Any] = dict()
            if limit:
                filters["limit"] = limit

            if page_token:
                filters["pageToken"] = page_token

            subscriptions = self.data_config_api.list_subscriptions(**filters)

            for item in subscriptions["items"]:
                yield InteractiveMapSubscription(item)

            page_token = subscriptions["nextPageToken"] if "nextPageToken" in subscriptions else ""

            if not page_token:
                break

    def subscription_exists(self, subscription_hrn: str) -> bool:
        """
        Checks whether a subscription with the specified HRN exists.

        :param subscription_hrn: The HERE Resource Name (HRN) of subscription
        :return: True if subscription exists.
        """

        return self.data_config_api.check_subscription_exists(subscription_hrn=subscription_hrn)

    def get_subscription(self, subscription_hrn: str) -> InteractiveMapSubscription:
        """
        To get configuration of the subscription associated with the HRN.

        :param subscription_hrn: The HERE Resource Name (HRN) of subscription
        :return: InteractiveMapSubscription object.
        """

        subscription_resp = self.data_config_api.get_subscription(
            subscription_hrn=subscription_hrn
        )
        logger.debug(f"Get Subscription Response : {subscription_resp}")
        return InteractiveMapSubscription(subscription_resp)

    def subscribe(
        self,
        subscription_name: str,
        description: str,
        source_catalog_hrn: str,
        source_layer: str,
        destination_catalog_hrn: str,
        destination_layer_id: str,
        interactive_map_subscription_type: InteractiveMapSubscriptionType,
    ) -> InteractiveMapSubscription:
        """
        Method to Subscribe to a Stream Layer from Layer's Catalog HRN.
        Source Layer is the current layer and Source Catalog is Layer's Catalog which it belongs.

        :param subscription_name: Name of the subscription.
        :param description: Description of the subscription.
        :param source_catalog_hrn: Catalog HRN of the source Catalog.
        :param source_layer: Layer id of the source Interactive Map Layer.
        :param destination_catalog_hrn: Catalog HRN of the destination Catalog.
        :param destination_layer_id: Layer id of the destination Stream Layer.
        :param interactive_map_subscription_type: InteractiveMapSubscriptionType containing type of
        subscription.
        :raises KeyError: in case statusToken in Response of createSubscription.
        :raises ValueError: in case Created Subscription Status is NOT Active after
                        multiple retry till max retry time.
        :return: InteractiveMapSubscription object containing details of the created subscription.
        """

        configuration_object: dict = dict(
            {
                "subscriptionName": subscription_name,
                "description": description,
                "sourceCatalog": source_catalog_hrn,
                "sourceLayer": source_layer,
                "destinationCatalog": destination_catalog_hrn,
                "destinationLayer": destination_layer_id,
                "interactiveMapSubscription": {"type": interactive_map_subscription_type.value},
            }
        )

        subscription_status_resp = dict({"status": ""})
        start_time = time.time()

        x_idempotency_key = str(uuid.uuid4())

        create_subscription_resp: dict = self.data_config_api.create_subscription(
            configuration_object=configuration_object, x_idempotency_key=x_idempotency_key
        )

        logger.debug(f"Create Subscription Response : {create_subscription_resp}")

        if "statusToken" not in create_subscription_resp:
            raise KeyError("Missing statusToken in Response of createSubscription.")

        time.sleep(min(0.1, self._polling_wait))

        while time.time() - start_time < self._retry_max_time:
            subscription_status_resp = self.data_config_api.get_subscription_status(
                status_token=create_subscription_resp["statusToken"]
            )
            logger.debug(f"Subscription Status Response : {subscription_status_resp}")

            if subscription_status_resp["status"] == "active":
                break

            time.sleep(self._polling_wait)

        logger.debug(
            f"Total wait time for Subscription Status Response : {time.time() - start_time}"
        )

        if "status" in subscription_status_resp and subscription_status_resp["status"] != "active":
            raise ValueError(
                f"Created Subscription Status is NOT Active \
                            for statusToken {create_subscription_resp['statusToken']}."
            )

        subscription_resp: dict = self.data_config_api.get_subscription(
            subscription_hrn=subscription_status_resp["subscriptionHrn"]
        )

        subscription_resp.update(create_subscription_resp)

        return InteractiveMapSubscription(subscription_resp)

    def subscription_status(self, status_token: str) -> InteractiveMapSubscriptionStatus:
        """
        Get the status of the subscription.

        :param status_token: Status token from create/delete subscription response.
        :return: InteractiveMapSubscriptionStatus object containing status.
        """

        subscription_status_resp = self.data_config_api.get_subscription_status(
            status_token=status_token
        )
        logger.debug(f"Subscription Status Response : {subscription_status_resp}")
        return InteractiveMapSubscriptionStatus(subscription_status_resp)

    def unsubscribe(self, subscription_hrn: str) -> InteractiveMapUnsubscribe:
        """
        Deletes a subscription associated with the HRN.

        :param subscription_hrn: The HERE Resource Name (HRN) of subscription.
        :return: InteractiveMapUnsubscribe object containing details.
        """
        x_idempotency_key = str(uuid.uuid4())
        subscription_status = dict({"status": ""})
        start_time = time.time()

        delete_subscription_resp: dict = self.data_config_api.delete_subscription(
            x_idempotency_key=x_idempotency_key, subscription_hrn=subscription_hrn
        )

        logger.debug(f"Delete Subscription response: {delete_subscription_resp}")
        time.sleep(min(0.1, self._polling_wait))

        while time.time() - start_time < self._retry_max_time:
            subscription_status = self.data_config_api.get_subscription_status(
                status_token=delete_subscription_resp["statusToken"]
            )

            logger.debug(f"Delete Subscription Status response: {subscription_status}")

            if subscription_status["status"] == "deleted":
                break

            time.sleep(self._polling_wait)

        delete_subscription_resp.update(subscription_status)

        return InteractiveMapUnsubscribe(delete_subscription_resp)
