# --------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------

import logging
from azure.iot.device.common.evented_callback import EventedCallback
from azure.iot.device.common.pipeline import (
    pipeline_nucleus,
    pipeline_stages_base,
    pipeline_ops_base,
    pipeline_stages_http,
)

from azure.iot.device.iothub.pipeline import exceptions as pipeline_exceptions

from . import (
    pipeline_ops_iothub_http,
    pipeline_stages_iothub_http,
)

logger = logging.getLogger(__name__)


class HTTPPipeline(object):
    """Pipeline to communicate with Edge.
    Uses HTTP.
    """

    def __init__(self, pipeline_configuration):
        """
        Constructor for instantiating a pipeline adapter object.

        :param auth_provider: The authentication provider
        :param pipeline_configuration: The configuration generated based on user inputs
        """
        # NOTE: This pipeline DOES NOT handle SasToken management!
        # (i.e. using a SasTokenStage)
        # It instead relies on the parallel MQTT pipeline to handle that.
        #
        # Because they share a pipeline configuration, and MQTT has renewal logic we can be sure
        # that the SasToken in the pipeline configuration is valid.
        #
        # Furthermore, because HTTP doesn't require constant connections or long running tokens,
        # there's no need to reauthorize connections, so we can just pass the token from the config
        # when needed for auth.
        #
        # This is not an ideal solution, but it's the simplest one for the time being.

        # Contains data and information shared globally within the pipeline
        self._nucleus = pipeline_nucleus.PipelineNucleus(pipeline_configuration)

        self._pipeline = (
            pipeline_stages_base.PipelineRootStage(self._nucleus)
            .append_stage(pipeline_stages_iothub_http.IoTHubHTTPTranslationStage())
            .append_stage(pipeline_stages_http.HTTPTransportStage())
        )

        callback = EventedCallback()

        op = pipeline_ops_base.InitializePipelineOperation(callback=callback)

        self._pipeline.run_op(op)
        callback.wait_for_completion()

    def invoke_method(self, device_id, method_params, callback, module_id=None):
        """
        Send a request to the service to invoke a method on a target device or module.

        :param device_id: The target device id
        :param method_params: The method parameters to be invoked on the target client
        :param callback: callback which is called when request has been fulfilled.
            On success, this callback is called with the error=None.
            On failure, this callback is called with error set to the cause of the failure.
        :param module_id: The target module id

        The following exceptions are not "raised", but rather returned via the "error" parameter
            when invoking "callback":

        :raises: :class:`azure.iot.device.iothub.pipeline.exceptions.ProtocolClientError`
        """
        logger.debug("HTTPPipeline invoke_method called")
        if not self._nucleus.pipeline_configuration.method_invoke:
            # If this parameter is not set, that means that the pipeline was not generated by the edge environment. Method invoke only works for clients generated using the edge environment.
            error = pipeline_exceptions.PipelineRuntimeError(
                "invoke_method called, but it is only supported on module clients generated from an edge environment. If you are not using a module generated from an edge environment, you cannot use invoke_method"
            )
            return callback(error=error)

        def on_complete(op, error):
            callback(error=error, invoke_method_response=op.method_response)

        self._pipeline.run_op(
            pipeline_ops_iothub_http.MethodInvokeOperation(
                target_device_id=device_id,
                target_module_id=module_id,
                method_params=method_params,
                callback=on_complete,
            )
        )

    def get_storage_info_for_blob(self, blob_name, callback):
        """
        Sends a POST request to the IoT Hub service endpoint to retrieve an object that contains information for uploading via the Storage SDK.

        :param blob_name: The name of the blob that will be uploaded via the Azure Storage SDK.
        :param callback: callback which is called when request has been fulfilled.
            On success, this callback is called with the error=None, and the storage_info set to the information JSON received from the service.
            On failure, this callback is called with error set to the cause of the failure, and the storage_info=None.

        The following exceptions are not "raised", but rather returned via the "error" parameter
            when invoking "callback":

        :raises: :class:`azure.iot.device.iothub.pipeline.exceptions.ProtocolClientError`
        """
        logger.debug("HTTPPipeline get_storage_info_for_blob called")
        if not self._nucleus.pipeline_configuration.blob_upload:
            # If this parameter is not set, that means this is not a device client. Upload to blob is not supported on module clients.
            error = pipeline_exceptions.PipelineRuntimeError(
                "get_storage_info_for_blob called, but it is only supported for use with device clients. Ensure you are using a device client."
            )
            return callback(error=error)

        def on_complete(op, error):
            callback(error=error, storage_info=op.storage_info)

        self._pipeline.run_op(
            pipeline_ops_iothub_http.GetStorageInfoOperation(
                blob_name=blob_name, callback=on_complete
            )
        )

    def notify_blob_upload_status(
        self, correlation_id, is_success, status_code, status_description, callback
    ):
        """
        Sends a POST request to a IoT Hub service endpoint to notify the status of the Storage SDK call for a blob upload.

        :param str correlation_id: Provided by IoT Hub on get_storage_info_for_blob request.
        :param bool is_success: A boolean that indicates whether the file was uploaded successfully.
        :param int status_code: A numeric status code that is the status for the upload of the file to storage.
        :param str status_description: A description that corresponds to the status_code.

        :param callback: callback which is called when request has been fulfilled.
            On success, this callback is called with the error=None.
            On failure, this callback is called with error set to the cause of the failure.


        The following exceptions are not "raised", but rather returned via the "error" parameter
            when invoking "callback":

        :raises: :class:`azure.iot.device.iothub.pipeline.exceptions.ProtocolClientError`
        """
        logger.debug("HTTPPipeline notify_blob_upload_status called")
        if not self._nucleus.pipeline_configuration.blob_upload:
            # If this parameter is not set, that means this is not a device client. Upload to blob is not supported on module clients.
            error = pipeline_exceptions.PipelineRuntimeError(
                "notify_blob_upload_status called, but it is only supported for use with device clients. Ensure you are using a device client."
            )
            return callback(error=error)

        def on_complete(op, error):
            callback(error=error)

        self._pipeline.run_op(
            pipeline_ops_iothub_http.NotifyBlobUploadStatusOperation(
                correlation_id=correlation_id,
                is_success=is_success,
                status_code=status_code,
                status_description=status_description,
                callback=on_complete,
            )
        )
