# 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 contains a :class:`DataMetadataApi` class to perform API operations.

The HERE API reference documentation used in this module can be found here:
|metadata_api_reference|

.. |metadata_api_reference| raw:: html

   <a href="https://developer.here.com/documentation/data-api/api-reference-metadata.html" target="_blank">Metadata API Reference</a>  # noqa E501
"""

from typing import Iterable, Optional, cast

from here.platform.api.base_api import BaseApi
from here.platform.auth import Auth
from here.platform.config import ApplicationConfig, PlatformConfig
from here.platform.exceptions import PlatformException


class DataMetadataApi(BaseApi):
    """
    This class provides access to HERE platform Metadata APIs.

    The metadata service provides a way to get information (metadata) about layers and partitions
    stored in a catalog. This service exposes the metadata for all the partitions or all the
    changed partitions. For a catalog with versioned layers, you can retrieve metadata for a
    particular version or version range of the catalog.
    """

    def __init__(
        self,
        base_url: str,
        auth: Optional[Auth],
        platform_config: PlatformConfig,
        application_config: ApplicationConfig,
        proxies: Optional[dict] = None,
    ):
        """
        Instantiate DataMetadataApi object.

        :param base_url: a Metadata API Url obtained from Lookup API for a given hrn.
        :param platform_config: a mandatory :class:`PlatformConfig` object to provide
            configuration information for the API.
        :param application_config: a mandatory :class:`ApplicationConfig` object to provide
            configuration information for the API.
        :param auth: an Authentication instance.
        :param proxies: an optional proxy configuration. Defaults to the environment proxy
        configuration.
        """
        super(DataMetadataApi, self).__init__(
            platform_config=platform_config,
            application_config=application_config,
            auth=auth,
            proxies=proxies,
        )
        self.base_url = base_url

    def get_layers_version(self, version: int, billing_tag: Optional[str] = None) -> dict:
        """
        Return information about layer versions for the catalog version.

        :param version: Catalog version.
        :param billing_tag: Billing Tag is an optional free-form tag which is used for grouping
            billing records together.
        :return: response from the API.
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = "/layerVersions"
        url = self.format_url(self.base_url, path)
        params = {"version": version, "billingTag": billing_tag}
        resp = self.get(url=url, params=params)
        if resp.status_code == 200:
            resp_json: dict = resp.json()
            return resp_json
        else:
            raise PlatformException(resp)

    def get_changes(
        self,
        layer_id: str,
        start_version: Optional[int] = None,
        end_version: Optional[int] = None,
        since_time: Optional[int] = None,
        part: Optional[str] = None,
        additional_fields: Optional[list] = ["dataSize", "checksum", "compressedDataSize", "crc"],
        range_header: Optional[str] = None,
        billing_tag: Optional[str] = None,
    ) -> dict:
        """
        Get the latest partition metadata in a version range for a versioned layer or a time
        range for a volatile layer. For versioned layers the range is expressed as a start and
        end version and might not return all changes for the partitions which were added and
        removed in between the specified start and end versions.

        :param layer_id: Unique layer id. Content of this parameter refers to a valid layer ID.
        :param start_version: Available/Required for versioned layers only; the beginning of the
            range of versions you want to get (exclusive). By convention -1 indicates the initial
            version before the first publication. After the first publication,the catalog version
            is 0
        :param end_version: Available/Required for versioned layers only; the end of the range of
            versions you want to get (inclusive). This must be a valid catalog version greater
            than the startVersion.
        :param since_time: Available/Required for volatile layers only; will return partitions
            whose data has been modified since this time, in milliseconds since epoch, inclusive.
        :param part: Available/Required for versioned layers only; indicates which part of the
            layer shall be queried.
        :param additional_fields: Available values : dataSize, checksum, compressedDataSize, crc
        :param range_header: an optional Range parameter to resume download of a large response
            for versioned layers when there is a connection issue between the client and server.
            Specify a single byte range offset like this: Range: bytes=10-. This parameter is
            compliant with RFC 7233, but note that this parameter only supports a single byte
            range. The range parameter can also be specified as a query parameter, i.e.
            range=bytes=10-. For volatile layers use the pagination links returned in the response
            body.
        :param billing_tag: Billing Tag is an optional free-form tag which is used for grouping
            billing records together.
        :return: response from the API.
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = f"/layers/{layer_id}/changes"
        url = self.format_url(self.base_url, path)
        additional_fields_str = (
            ",".join(cast(Iterable[str], additional_fields))
            if type(additional_fields) == list
            else ""
        )
        params = {
            "startVersion": start_version,
            "endVersion": end_version,
            "sinceTime": since_time,
            "part": part,
            "additionalFields": additional_fields_str,
            "billingTag": billing_tag,
        }
        headers = self.headers
        headers["Range"] = range_header
        resp = self.get(url, params=params, headers=headers)
        if resp.status_code in [200, 206]:
            resp_json: dict = resp.json()
            return resp_json
        else:
            raise PlatformException(resp)

    def get_changes_parts(self, layer_id: str, num_requested_parts: str) -> dict:
        """
        Return a list of Part Ids which represent the parts that can be used to limit the scope
        of queries of changes for the version range. This allows to run parallel queries with
        multiple parts. The user has to provide the desired number of parts and the service will
        return a list of Part Ids. Please note in some cases the requested number of parts will
        make them too small and in this case the service might return lesser amount of the parts
        than requested.

        :param layer_id: Unique layer id. Content of this parameter refers to a valid layer ID.
        :param num_requested_parts: Indicates requested number of parts.
        :return: response from the API.
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = f"/layers/{layer_id}/changes/parts"
        url = self.format_url(self.base_url, path)
        params = {"numRequestedParts": num_requested_parts}
        resp = self.get(url=url, params=params)
        if resp.status_code == 200:
            resp_json: dict = resp.json()
            return resp_json
        else:
            raise PlatformException(resp)

    def get_partitions(
        self,
        layer_id: str,
        version: Optional[int] = None,
        part: Optional[str] = None,
        additional_fields: Optional[list] = ["dataSize", "checksum", "compressedDataSize", "crc"],
        range_header: Optional[str] = None,
        billing_tag: Optional[str] = None,
    ) -> dict:
        """
        Get the metadata for all partitions in a specific layer.

        :param layer_id: Unique layer id. Content of this parameter refers to a valid layer ID.
        :param version: If you are getting metadata from a versioned layer, specify the version
            of the layer you want. This parameter is required for versioned layers. If you are
            getting metadata from another layer type, do not specify this parameter.
        :param part: Available/Required for versioned layers only; indicates which part of the
            layer shall be queried.
        :param additional_fields: Available values : dataSize, checksum, compressedDataSize, crc
        :param range_header: an optional Range parameter to resume download of a large response
            for versioned layers when there is a connection issue between the client and server.
            Specify a single byte range offset like this: Range: bytes=10-. This parameter is
            compliant with RFC 7233, but note that this parameter only supports a single byte
            range. The range parameter can also be specified as a query parameter, i.e.
            range=bytes=10-. For volatile layers use the pagination links returned in the response
            body.
        :param billing_tag: Billing Tag is an optional free-form tag which is used for grouping
            billing records together.
        :return: response from the API.
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = f"/layers/{layer_id}/partitions"
        url = self.format_url(self.base_url, path)
        additional_fields_str = (
            ",".join(cast(Iterable[str], additional_fields))
            if type(additional_fields) == list
            else ""
        )
        params = {
            "version": version,
            "part": part,
            "additionalFields": additional_fields_str,
            "billingTag": billing_tag,
        }
        headers = self.headers
        headers["Range"] = range_header
        resp = self.get(url, params=params, headers=headers)
        if resp.status_code in [200, 206]:
            resp_json: dict = resp.json()
            return resp_json
        else:
            raise PlatformException(resp)

    def get_partitions_parts(self, layer_id: str, num_requested_parts: str) -> dict:
        """
        Return a list of Part Ids which represent the parts that can be used to limit the scope
        of queries of the metadata for all partitions in a specific layer. This allows to run
        parallel queries with multiple parts. The user has to provide the desired number of parts
        and the service will return a list of Part Ids. Please note in some cases the requested
        number of parts will make them too small and in this case the service might return lesser
        amount of the parts than requested.

        :param layer_id: Unique layer id. Content of this parameter refers to a valid layer ID.
        :param num_requested_parts: Indicates requested number of parts.
        :return: response from the API.
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = f"/layers/{layer_id}/partitions/parts"
        url = self.format_url(self.base_url, path)
        params = {"numRequestedParts": num_requested_parts}
        resp = self.get(url=url, params=params)
        if resp.status_code == 200:
            resp_json: dict = resp.json()
            return resp_json
        else:
            raise PlatformException(resp)

    def list_versions(
        self,
        start_version: int,
        end_version: int,
        billing_tag: Optional[str] = None,
    ) -> dict:
        """
        Return information about specific catalog version(s). If the catalog doesn't contain any
        versions, an empty dictionary is returned. Maximum number of versions to be returned
        per call is 1000 versions. If requested range is bigger than 1000 versions,
        400 Bad Request will be returned.

        :param start_version: The version number after which list of returned versions will begin.
            Minimum value -1 will return list starting with version 0.
        :param end_version: The end of the range of versions you want to get (inclusive). This
            must be a valid catalog version greater than the startVersion. The maximum value for
            this parameter is returned from the /versions/latest endpoint. If this version does
            not exist, 400 Bad Request is returned
        :param billing_tag: Billing Tag is an optional free-form tag which is used for grouping
            billing records together.
        :return: response from the API.
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = "/versions"
        url = self.format_url(self.base_url, path)
        params = {
            "startVersion": start_version,
            "endVersion": end_version,
            "billingTag": billing_tag,
        }
        resp = self.get(url, params=params)
        if resp.status_code == 200:
            resp_json: dict = resp.json()
            return resp_json
        elif resp.status_code == 404:
            return {}
        else:
            raise PlatformException(resp)

    def compatible_versions(
        self, catalog_dependencies: dict, limit: Optional[int] = 1, next: Optional[str] = None
    ) -> dict:
        """
        Given a list of HRNs and versions provided by the user, returns a list of versions of this
        catalog for which the listed HRN are either present in the direct or indirect dependencies
        with the same version, or are not present. Please note that versions that don't depend on
        any of the provided catalog HRNs, are also considered compatible. The compatible versions
        are returned in reverse order, from the newest to the oldest. When there is no compatible
        version the service returns an empty list. When the catalog has no version a 404 is
        returned.

        :param catalog_dependencies: The catalog dependencies we want to search for
        :param limit: The numbers of items to return per page
        :param next: The next url where the iteration will continue.
        :return: response from the API.
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = "/versions/compatibles"
        url = next if next else self.format_url(self.base_url, path)
        params = {
            "limit": limit,
        }
        resp = self.post(url, data=catalog_dependencies, params=params)
        if resp.status_code == 200:
            resp_json: dict = resp.json()
            return resp_json
        else:
            raise PlatformException(resp)

    def get_latest_version(
        self, start_version=-1, billing_tag: Optional[str] = None
    ) -> Optional[int]:
        """
        Return information about the latest version for the given catalog. If the catalog doesn't
        contain any versions None will be returned.

        :param start_version: The catalog version returned from a prior request to
            /versions/latest. You should save the version from each request so that you can use it
             in the startVersion parameter of subsequent requests. If you don't have the version
             from a prior request, set the parameter to -1.
        :param billing_tag: Billing Tag is an optional free-form tag which is used for grouping
            billing records together.
        :return: latest version number of the catalog if version exist else None
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = "/versions/latest"
        url = self.format_url(self.base_url, path)
        params = {
            "startVersion": start_version,
            "billingTag": billing_tag,
        }
        resp = self.get(url, params=params)
        if resp.status_code == 200:
            return int(resp.json().get("version"))
        elif resp.status_code == 404:
            return None
        else:
            raise PlatformException(resp)

    def get_minimum_version(self, billing_tag: Optional[str] = None) -> Optional[int]:
        """
        Return minimum version for the given catalog. If the catalog doesn't
        contain any versions None will be returned.

        :param billing_tag: Billing Tag is an optional free-form tag which is used for grouping
            billing records together.
        :return: an int representing minimum version, if any
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = "/versions/minimum"
        url = self.format_url(self.base_url, path)
        params = {"billingTag": billing_tag}
        resp = self.get(url, params=params)
        if resp.status_code == 200:
            return int(resp.json().get("version"))
        elif resp.status_code == 404:
            return None
        else:
            raise PlatformException(resp)

    def set_minimum_version(self, version_obj: dict, billing_tag: Optional[str] = None):
        """
        Sets minimum version for the given catalog.

        :param version_obj: A version object that contains new minimum version.
        :param billing_tag: Billing Tag is an optional free-form tag which is used for grouping
            billing records together.
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = "/versions/minimum"
        url = self.format_url(self.base_url, path)
        params = {"billingTag": billing_tag}
        resp = self.post(url, data=version_obj, params=params)
        if resp.status_code == 204:
            return
        else:
            raise PlatformException(resp)
