# Copyright (C) 2021-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:`DataVolatileBlobApi` class to perform API operations.

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

.. |volatile_blob_api_reference| raw:: html

   By Key <a href="https://developer.here.com/documentation/data-api/api-reference-blob-v2.html" target="_blank">Object Blob API Reference By Key</a>  # noqa E501
   By Handle <a href="https://development-architecture.pages.gitlab.in.here.com/olp-apis/?urls.primaryName=data-blob-v2-dev" target="_blank">Object Blob API Reference By Handle</a>  # noqa E501
   
"""

from typing import Any, Optional

from here.platform.api.base_api import BaseApi
from here.platform.auth import Auth
from here.platform.config.application_config import ApplicationConfig
from here.platform.config.platform_config import PlatformConfig
from here.platform.exceptions import PlatformException
from requests import Response


class DataObjectBlobApi(BaseApi):
    """
    This class provides access to HERE platform Data Blob APIs v2.

    The data-blob-api-v2 service supports the upload and retrieval of objects stored in
    ObjectStore layers. Two types of uploads can be done with this layer - Single and Multipart.
    The single part upload can be used for objects up to 192 MB.
    Use multipart upload for any data objects larger than 192 MB. Every multipart upload uses
    the token from previous multipart upload response. Before doing any multipart upload,
    wait for the response from previous multipart upload, if any.
    Also, while reading, objects can be read in part by passing a `Range` Header.
    Example, `Range: bytes=10-`
    """

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

        :param base_url: base_url for the request
        :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().__init__(
            platform_config=platform_config,
            application_config=application_config,
            auth=auth,
            proxies=proxies,
        )
        self.base_url = base_url

    def key_exists(self, layer_id: str, key: str, billing_tag: Optional[str] = None) -> bool:
        """
        Checks whether the object with :param:`key` exists in the object store layer.
        """
        path = f"/layers/{layer_id}/keys/{key}"
        params = {"billingTag": billing_tag}
        url = "{}{}".format(self.base_url, path)
        resp = self.head(url, params=params)
        return resp.status_code in [200, 204]

    def get_object(
        self, layer_id: str, key: str, only_meta: bool, billing_tag: Optional[str] = None
    ) -> Response:
        """
        Fetches the object against a :param:`key` in layer with ID :param:`layer_id`.

        :param layer_id: ID of the object store layer
        :param key: key for the object to be fetched
        :param only_meta: fetch only metadata
        :param billing_tag: A string to represent a grouping of billing records
        :returns: object data against given :param:`key`
        :raises PlatformException: If platform responds with an HTTP error
        """
        path = f"/layers/{layer_id}/keys/{key}"
        params = {"billingTag": billing_tag}
        url = "{}{}".format(self.base_url, path)
        resp = self.request("HEAD" if only_meta else "GET", url, params=params)
        if resp.status_code in [200, 204]:
            return resp
        else:
            raise PlatformException(resp)

    def put_object(
        self,
        layer_id: str,
        key: str,
        object_data: bytes = None,
        headers: dict = None,
        source: str = None,
        billing_tag: Optional[str] = None,
    ) -> Any:
        """
        Uploads the :param:`object_data` against given :param:`key`
        in layer with ID :param:`layer_id`.

        :param layer_id: ID of the object store layer
        :param key: key for the object to be uploaded
        :param object_data: object to be uploaded
        :param headers: additional request header fields like content-length,
         and content-type which can be provided while uploading object.
        :param source: source to copy an existing object, instead of uploading a new one,
            If this parameter is present, the object_data must be empty.
        :param billing_tag: A string to represent a grouping of billing records
        :returns: status response after putting the object
        :raises PlatformException: If platform responds with an HTTP error
        """
        path = f"/layers/{layer_id}/keys/{key}"
        params = {"billingTag": billing_tag}
        if source:
            params["source"] = source
        url = "{}{}".format(self.base_url, path)
        complete_headers = {**self.headers}
        if headers:
            complete_headers = {**self.headers, **headers}
        resp = self.put(url, data=object_data, params=params, headers=complete_headers)
        if resp.status_code in [200, 204]:
            return resp.content
        else:
            raise PlatformException(resp)

    def delete_object(
        self,
        layer_id: str,
        key: str,
        billing_tag: Optional[str] = None,
    ):
        """
        Deletes the object with :param:`key` from the object store layer.

        :param layer_id: ID of the object store layer.
        :param key: key for the object to be deleted.
        :param billing_tag: A string to represent a grouping of billing records
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = f"/layers/{layer_id}/keys/{key}"
        params = {"billingTag": billing_tag}
        url = "{}{}".format(self.base_url, path)
        resp = self.delete(url, params=params)
        if resp.status_code == 202:
            return
        else:
            raise PlatformException(resp)

    def list_keys(self, layer_id: str, params: dict) -> Any:
        """
        List all the keys present in the object store layer.
        """
        path = f"/layers/{layer_id}/keys"
        url = "{}{}".format(self.base_url, path)
        resp = self.get(url, params=params)
        if resp.status_code in [200, 204]:
            return resp.json()
        else:
            raise PlatformException(resp)

    def start_multipart_upload(
        self,
        layer_id: str,
        key: str,
        content_type: str,
        billing_tag: Optional[str] = None,
        content_encoding: Optional[str] = None,
    ):
        """
        The multipart upload start is to be followed by the individual parts upload and
         completed with a call to complete the upload

        :param layer_id: ID of the object store layer.
        :param key:The key identifies a specific blob so that you can get that blob's contents
        :param content_type: A standard MIME type describing the format of the blob data.
        :param billing_tag: A string to represent a grouping of billing records
        :param content_encoding: Content-encoding of the object. This field is optional.
            For more information, see https://tools.ietf.org/html/rfc2616#section-14.11
        :return: returns multipart token
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = f"/layers/{layer_id}/keys/{key}"
        params = {"billingTag": billing_tag}
        url = "{}{}".format(self.base_url, path)
        data = dict()
        data["contentType"] = content_type
        if content_encoding:
            data["contentEncoding"] = content_encoding
        resp = self.post(url, data=data, params=params)
        if resp.status_code == 200:
            return resp.json()
        else:
            raise PlatformException(resp)

    def get_multipart_upload_status(
        self,
        layer_id,
        multipart_token,
        billing_tag: Optional[str] = None,
    ):
        """
        Gets the status of a multipart upload.
         The status can be received only when the upload has been completed.

        :param layer_id: ID of the object store layer.
        :param multipart_token: The identifier of the multipart upload (token).
            This token is returned when the multipart upload is initiated.
        :param billing_tag: A string to represent a grouping of billing records
        :return: returns status of multipart upload.
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = f"/layers/{layer_id}/keysMultipart/{multipart_token}"
        params = {"billingTag": billing_tag}
        url = "{}{}".format(self.base_url, path)
        resp = self.get(url, params=params)
        if resp.status_code == 200:
            return resp.json()
        else:
            raise PlatformException(resp)

    def cancel_multipart_upload(
        self,
        layer_id,
        multipart_token,
        billing_tag: Optional[str] = None,
    ):
        """
        Cancels an entire multipart upload operation. You can only cancel
         a multipart upload before
        it has been completed.

        :param layer_id: ID of the object store layer.
        :param multipart_token: The identifier of the multipart upload (token).
            This token is returned when the multipart upload is initiated.
        :param billing_tag: A string to represent a grouping of billing records
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = f"/layers/{layer_id}/keysMultipart/{multipart_token}"
        params = {"billingTag": billing_tag}
        url = "{}{}".format(self.base_url, path)
        resp = self.delete(url, params=params)
        if resp.status_code == 202:
            return
        else:
            raise PlatformException(resp)

    async def upload_blob_part(
        self,
        layer_id: str,
        multipartToken: str,
        part_number: int,
        object_part: bytes,
        content_type: str,
        billing_tag: Optional[str] = None,
    ):
        """
        The multipart upload start is to be followed by the individual parts upload and
         completed with a call to complete the upload

        :param layer_id: ID of the object store layer.
        :param multipartToken: The identifier of the multipart upload (token).
            This token is returned when the multipart upload is initiated.
        :param part_number: The number of the part for the multi part upload. The numbers of the
            upload parts must start from 1, be no greater than 10,000 and be consecutive. Parts
            uploaded with the same partNumber are overridden.
        :param object_part: object part to be uploaded
        :param content_type: A standard MIME type describing the format of the blob data. For more
            information, see RFC 2616, section 14.17: Content-Type. The value of this header must
            match the content type specified in the contentType field when the multipart upload
            was initialized, and this content type must also match the content type specified in
            the layer's configuration.
        :param billing_tag: A string to represent a grouping of billing records
        :return: returns multipart token
        :raises PlatformException: If platform responds with an HTTP error.
        """
        path = f"/layers/{layer_id}/keysMultipart/{multipartToken}/parts"
        url = "{}{}".format(self.base_url, path)
        params = {
            "partNumber": part_number,
            "billingTag": billing_tag,
        }
        headers = self.headers
        headers["Content-Type"] = content_type
        response = self.post(url, data=object_part, params=params, headers=headers)
        if response.status_code == 200:
            return response.content
        else:
            raise PlatformException(response)

    def complete_multipart_upload(
        self,
        layer_id: str,
        multipart_token: str,
        data: dict,
        billing_tag: Optional[str] = None,
    ):
        """
        Completes a multipart upload by handle.

        :param layer_id: ID of the object store layer.
        :param multipart_token: The identifier of the multipart upload (token).
            This token is returned when the multipart upload is initiated.
        :param data: It is list of dict object which holds id of uploaded parts and part_number
        :param billing_tag: A string which is used for grouping billing records.
        :raises PlatformException: If platform responds with an HTTP error.
        """

        path = f"/layers/{layer_id}/keysMultipart/{multipart_token}"
        params = {"billingTag": billing_tag}
        url = "{}{}".format(self.base_url, path)
        resp = self.put(url, data=data, params=params)
        if resp.status_code in [200, 204]:
            # Note: 200 is returned only by LDS
            return
        else:
            raise PlatformException(resp)
