# 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.

"""
This module manages single instance of APIs and Registries.
"""

import importlib
from collections import namedtuple
from enum import Enum
from typing import Any, Dict, Optional, Union

from here.platform.api.aaa_authorization_api import AAAAuthorizationApi
from here.platform.api.artifact_api import ArtifactApi
from here.platform.api.lookup_api import LookupApi
from here.platform.api.registry import LookupApiRegistry
from here.platform.api.service_registry_api import ServiceRegistryApi
from here.platform.auth import Auth
from here.platform.config.application_config import ApplicationConfig
from here.platform.config.platform_config import PlatformConfig

# Namedtuple to store APIs' required info that'll be used in their instance creation
ApiData = namedtuple("ApiData", ["name", "module_name", "class_name", "api_version_name"])


class API(Enum):
    """
    Enum of APIs carrying information required during API instance creation.
    """

    BASE = ApiData("base_api", "here.platform.api.base_api", "BaseApi", None)
    DATA_CONFIG = ApiData(
        "data_config_api", "here.platform.api.data_config_api", "DataConfigApi", None
    )
    LOOKUP = ApiData("lookup_api", "here.platform.api.lookup_api", "LookupApi", None)
    ARTIFACT = ApiData("artifact_api", "here.platform.api.artifact_api", "ArtifactApi", None)
    AAA_AUTH = ApiData(
        "aaa_auth_api", "here.platform.api.aaa_authorization_api", "AAAAuthorizationApi", None
    )
    SERVICE_REGISTRY = ApiData(
        "service_registry_api",
        "here.platform.api.service_registry_api",
        "ServiceRegistryApi",
        None,
    )
    DATA_BLOB = ApiData("blob_api", "here.platform.api.data_blob_api", "DataBlobApi", "blob-v1")
    DATA_VOLATILE_BLOB = ApiData(
        "data_volatile_blob_api",
        "here.platform.api.data_volatile_blob_api",
        "DataVolatileBlobApi",
        "volatile-blob-v1",
    )
    DATA_METADATA = ApiData(
        "datametadeta_api", "here.platform.api.data_metadata_api", "DataMetadataApi", "metadata-v1"
    )
    DATA_PUBLISH = ApiData(
        "data_publish_api", "here.platform.api.data_publish_api", "DataPublishApi", "publish-v2"
    )
    DATA_QUERY = ApiData(
        "data_query_api", "here.platform.api.data_query_api", "DataQueryApi", "query-v1"
    )
    DATA_INDEX = ApiData(
        "data_index_api", "here.platform.api.data_index_api", "DataIndexApi", "index-v1"
    )
    DATA_STREAM = ApiData(
        "data_stream_api", "here.platform.api.data_stream_api", "DataStreamApi", "stream-v2"
    )
    DATA_INGEST = ApiData(
        "data_ingest_api", "here.platform.api.data_ingest_api", "DataIngestApi", "ingest-v1"
    )
    DATA_INTERACTIVE = ApiData(
        "data_interactive_api",
        "here.platform.api.data_interactive_api",
        "DataInteractiveApi",
        "interactive-v1",
    )
    DATA_OBJECT_BLOB = ApiData(
        "data_object_blob_api",
        "here.platform.api.data_object_blob_api",
        "DataObjectBlobApi",
        "blob-v2",
    )
    DATA_STATISTICS = ApiData(
        "data_statistics_api",
        "here.platform.api.data_statistics_api",
        "DataStatisticsApi",
        "statistics-v1",
    )
    SERVICE = ApiData("service_api", "here.platform.api.service_api", "ServiceApi", None)
    MAP_MATCHING = ApiData(
        "map_matching_api", "here.platform.api.map_matching_api", "MapMatchingApi", None
    )


class _Factory:
    """
    Parent class for API factories
    """

    def __init__(
        self,
        platform_config: PlatformConfig,
        application_config: ApplicationConfig,
        auth: Auth,
        proxies: Dict,
        local_environment: Optional[bool] = False,
        lookup_api_registry: Optional[LookupApiRegistry] = LookupApiRegistry(),
    ):
        """
        Instantiates factory for API instances.

        :param platform_config: the properties of Platform Configuration.
        :param application_config: an instance of ApplicationConfig.
        :param auth: an Authentication instance.
        :param proxies: a proxy configuration.
        :param local_environment: boolean value to indicate whether
            environment is local_environment.
        :param lookup_api_registry: LookupApiRegistry instance.
        """
        self._platform_config = platform_config
        self._application_config = application_config
        self._auth = auth
        self._proxies = proxies
        self._local_environment = local_environment
        self._lookup_api_registry = lookup_api_registry

        self._api_cache: Dict[API, Any] = {}
        self._api_args = {
            "platform_config": self._platform_config,
            "application_config": self._application_config,
            "auth": self._auth,
            "proxies": self._proxies,
        }

    def get_api(self, api: API) -> Any:
        """
        Gets API against :param:`api`.

        :param api: API enum value for which API instance is needed.
        :return: API instance agaisnt the given :param:`api`.
        """
        if api not in self._api_cache:
            self._create_api(api)
        return self._api_cache[api]

    def _create_api(self, api: API) -> None:
        """
        Skeleton function overridden in child classes to create
        instance for given :param:`api`.

        :param api: API enum value for which API instance is needed.
        """
        pass


class _PlatformFactory(_Factory):
    """
    Factory class for Platform API instances
    """

    def __init__(
        self,
        platform_config: PlatformConfig,
        application_config: ApplicationConfig,
        auth: Auth,
        proxies: Dict,
        local_environment: Optional[bool] = False,
        lookup_api_registry: Optional[LookupApiRegistry] = LookupApiRegistry(),
    ):
        """
        Instantiates factory for API instances.

        :param platform_config: the properties of Platform Configuration.
        :param application_config: an instance of ApplicationConfig.
        :param auth: an Authentication instance.
        :param proxies: a proxy configuration.
        :param local_environment: boolean value to indicate whether
            environment is local_environment.
        :param lookup_api_registry: LookupApiRegistry instance.
        """
        super().__init__(
            platform_config,
            application_config,
            auth,
            proxies,
            local_environment=local_environment,
            lookup_api_registry=lookup_api_registry,
        )

    def _get_artifact_base_url(self) -> str:
        """
        Gets Artifact URL from the Lookup API.

        :return: Artifact URL.
        """
        lookup_api: LookupApi = self.get_api(API.LOOKUP)
        return str(lookup_api.get_impl_platform_api_list()["artifact"]["baseURL"])

    def _get_account_url(self) -> str:
        """
        Gets Account URL.

        :return: Account URL.
        :raises ValueError: If account_url is not present in the configuration.
        """
        if self._platform_config.account_url is None:
            raise ValueError("here_account_api_url is not present in the configuration.")
        account_url: str = self._platform_config.account_url + "/authorization/v1.1"
        return account_url

    def _location_service_registry(self) -> str:
        """
        Gets Location Service Registry URL from the Lookup API.

        :return: Location Service Registry URL.
        """
        lookup_api: LookupApi = self.get_api(API.LOOKUP)
        return str(lookup_api.get_impl_platform_api_list()["location-service-registry"]["baseURL"])

    def _create_api(self, api: API) -> None:
        """
        Creates API instance for the given :param:`api`.

        :param api: Enum type of the API to be created.
        :raises NotImplementedError: If called when LOCAL environment (LDS endpoint) is active.
        """

        if api in [API.AAA_AUTH, API.SERVICE_REGISTRY]:
            if self._local_environment:
                raise NotImplementedError(f"{api.name} API is not available in LOCAL environment")

        if api == API.LOOKUP:
            self._api_cache[api] = LookupApi(
                lookup_api_registry=self._lookup_api_registry,  # type: ignore
                **self._api_args,  # type: ignore
            )
            return
        elif api == API.ARTIFACT:
            self._api_cache[api] = ArtifactApi(
                base_url=self._get_artifact_base_url(),
                **self._api_args,  # type: ignore
            )
            return
        elif api == API.AAA_AUTH:
            self._api_cache[api] = AAAAuthorizationApi(
                base_url=self._get_account_url(),
                **self._api_args,  # type: ignore
            )
            return
        elif api == API.SERVICE_REGISTRY:
            self._api_cache[api] = ServiceRegistryApi(
                base_url=self._location_service_registry(),
                **self._api_args,  # type: ignore
            )
            return

        api_class = getattr(importlib.import_module(api.value.module_name), api.value.class_name)
        self._api_cache[api] = api_class(**self._api_args)


class _CatalogApiFactory(_Factory):
    """
    Factory class for Catalog API instances.
    """

    def __init__(
        self,
        catalog_hrn: str,
        platform_factory: _PlatformFactory,
        platform_config: PlatformConfig,
        application_config: ApplicationConfig,
        auth: Auth,
        proxies: Dict,
        local_environment: Optional[bool] = False,
    ):
        """
        Instantiates factory for API instances.

        :param catalog_hrn: HRN of the catalog for which :class:`_CatalogFactory` is created.
        :param platform_factory: instance of :class:`_PlatformFactory`.
        :param platform_config: the properties of Platform Configuration.
        :param application_config: an instance of ApplicationConfig.
        :param auth: an Authentication instance.
        :param proxies: a proxy configuration.
        :param local_environment: boolean value to indicate whether
            environment is local_environment.
        """
        super().__init__(
            platform_config, application_config, auth, proxies, local_environment=local_environment
        )
        self._catalog_hrn = catalog_hrn
        self._platform_factory = platform_factory
        self._resource_apis: dict = {}

    def _get_resource_apis(self) -> dict:
        """
        Gets the collection of reource APIs for the current Catalog.
        """
        if self._resource_apis is None or len(self._resource_apis) == 0:
            if self._catalog_hrn is None:
                raise ValueError("Catalog HRN is not set in the APIFactory")
            lookup_api: LookupApi = self._platform_factory.get_api(API.LOOKUP)
            self._resource_apis: dict = lookup_api.get_resource_api_list(  # type: ignore
                self._catalog_hrn
            )
        return self._resource_apis

    def _create_api(self, api: API) -> None:
        """
        Creates API instance for the given :param:`api`.

        :param api: Enum type of the API to be created.
        :raises NotImplementedError: If called when LOCAL environment (LDS endpoint) is active.
        """

        if api in [
            API.DATA_INGEST,
            API.DATA_INTERACTIVE,
            API.DATA_OBJECT_BLOB,
            API.DATA_STATISTICS,
        ]:
            if self._local_environment:
                raise NotImplementedError(f"{api.name} API is not available in LOCAL environment")

        api_class = getattr(importlib.import_module(api.value.module_name), api.value.class_name)
        self._api_cache[api] = api_class(
            self._get_resource_apis()[api.value.api_version_name]["baseURL"], **self._api_args
        )


class _MapMatchingFactory(_Factory):
    def __init__(
        self,
        map_matching_base_url: str,
        platform_factory: _PlatformFactory,
        platform_config: PlatformConfig,
        application_config: ApplicationConfig,
        auth: Auth,
        proxies: Dict,
        local_environment: Optional[bool] = False,
    ):
        super().__init__(platform_config, application_config, auth, proxies, local_environment)
        self._map_matching_base_url = map_matching_base_url
        self._platform_factory = platform_factory

    def _create_api(self, api: API) -> None:
        """
        Creates API instance for the given :param:`api`.

        :param api: Enum type of the API to be created.
        :raises NotImplementedError: If called when LOCAL environment (LDS endpoint) is active.
        """

        if api in [API.SERVICE]:
            if self._local_environment:
                raise NotImplementedError(f"{api.name} API is not available in LOCAL environment")

        api_class = getattr(importlib.import_module(api.value.module_name), api.value.class_name)
        self._api_cache[api] = api_class(self._map_matching_base_url, **self._api_args)


class _ServiceFactory(_Factory):
    """
    Factory class for Service API instances.
    """

    def __init__(
        self,
        service_base_url: str,
        platform_factory: _PlatformFactory,
        platform_config: PlatformConfig,
        application_config: ApplicationConfig,
        auth: Auth,
        proxies: Dict,
        local_environment: Optional[bool] = False,
    ):
        """
        Instantiates factory for API instances.

        :param service_base_url: base URL of the service for which
            :class:`_ServiceFactory` is created.
        :param platform_factory: instance of :class:`_PlatformFactory`.
        :param platform_config: the properties of Platform Configuration.
        :param application_config: an instance of ApplicationConfig.
        :param auth: an Authentication instance.
        :param proxies: a proxy configuration.
        :param local_environment: boolean value to indicate whether
            environment is local_environment.
        """
        super().__init__(platform_config, application_config, auth, proxies, local_environment)
        self._service_base_url = service_base_url
        self._platform_factory = platform_factory

    def _create_api(self, api: API) -> None:
        """
        Creates API instance for the given :param:`api`.

        :param api: Enum type of the API to be created.
        :raises NotImplementedError: If called when LOCAL environment (LDS endpoint) is active.
        """

        if api in [API.SERVICE]:
            if self._local_environment:
                raise NotImplementedError(f"{api.name} API is not available in LOCAL environment")

        api_class = getattr(importlib.import_module(api.value.module_name), api.value.class_name)
        self._api_cache[api] = api_class(self._service_base_url, **self._api_args)


class APIFactory:
    """
    Factory that is exposed to accept request for API creation.
    """

    def __init__(
        self,
        platform_config: PlatformConfig,
        application_config: ApplicationConfig,
        auth: Auth,
        proxies: Dict,
        local_environment: Optional[bool] = False,
        lookup_api_registry: Optional[LookupApiRegistry] = LookupApiRegistry(),
    ):
        """
        Initializes APIFactory with required params.

        :param platform_config: the properties of Platform Configuration.
        :param application_config: an instance of ApplicationConfig.
        :param auth: an Authentication instance.
        :param proxies: a proxy configuration.
        :param local_environment: boolean value to indicate whether
            environment is local_environment.
        :param lookup_api_registry: LookupApiRegistry instance.
        """

        self.api_args = {
            "platform_config": platform_config,
            "application_config": application_config,
            "auth": auth,
            "proxies": proxies,
            "local_environment": local_environment,
        }

        self._platform_factory: _PlatformFactory = _PlatformFactory(
            lookup_api_registry=lookup_api_registry, **self.api_args  # type: ignore
        )
        self._catalog_factories: Dict[Union[str, Optional[str]], _CatalogApiFactory] = {}
        self._service_factories: Dict[Union[str, Optional[str]], _ServiceFactory] = {}
        self._map_matching_factories: Dict[Union[str, Optional[str]], _MapMatchingFactory] = {}

    def remove_catalog_apis(self, catalog_hrn: str) -> None:
        """
        Removes Factory of APIs against :param:`catalog_hrn`.

        :param catalog_hrn: HRN of the catalog.
        """
        if catalog_hrn in self._catalog_factories:
            del self._catalog_factories[catalog_hrn]

    def get_api(
        self,
        api: API,
        catalog_hrn: Optional[str] = None,
        service_base_url: Optional[str] = None,
        map_matching_base_url: Optional[str] = None,
    ) -> Any:
        """
        Gets API against :param:`api`.

        :param api: Name of the API to be fetched.
        :param catalog_hrn: (Optional) HRN of the catalog for which
            API need to be fetched.
        :param service_base_url: (Optional) Base URL of the service
            for whichAPI need to be fetched.
        :param map_matching_base_url: Map matching base url
        :return: API instance against the given :param:`api`.
        :raises ValueError: If an invalid API value passed.
        """
        if api in [
            API.BASE,
            API.DATA_CONFIG,
            API.LOOKUP,
            API.ARTIFACT,
            API.AAA_AUTH,
            API.SERVICE_REGISTRY,
        ]:
            return self._platform_factory.get_api(api)
        elif api in [
            API.DATA_BLOB,
            API.DATA_VOLATILE_BLOB,
            API.DATA_METADATA,
            API.DATA_PUBLISH,
            API.DATA_QUERY,
            API.DATA_INDEX,
            API.DATA_STREAM,
            API.DATA_INGEST,
            API.DATA_INTERACTIVE,
            API.DATA_OBJECT_BLOB,
            API.DATA_STATISTICS,
        ]:
            if catalog_hrn not in self._catalog_factories:
                self._catalog_factories[catalog_hrn] = _CatalogApiFactory(
                    catalog_hrn=catalog_hrn,  # type: ignore
                    platform_factory=self._platform_factory,
                    **self.api_args,  # type: ignore
                )
            return self._catalog_factories[catalog_hrn].get_api(api)
        elif api in [API.SERVICE]:
            if service_base_url not in self._service_factories:
                self._service_factories[service_base_url] = _ServiceFactory(
                    service_base_url=service_base_url,  # type: ignore
                    platform_factory=self._platform_factory,
                    **self.api_args,  # type: ignore
                )
            return self._service_factories[service_base_url].get_api(api)
        elif api in [API.MAP_MATCHING]:
            if map_matching_base_url not in self._map_matching_factories:
                self._map_matching_factories[map_matching_base_url] = _MapMatchingFactory(
                    map_matching_base_url=map_matching_base_url,  # type: ignore
                    platform_factory=self._platform_factory,
                    **self.api_args,  # type: ignore
                )
            return self._map_matching_factories[map_matching_base_url].get_api(api)
        else:
            raise ValueError("Invalid API value passed")
