"""
    Provides a simple class to build Oasis API clients.
"""
import logging

from requests import RequestException
from six.moves import urllib

from ..model_execution.files import TAR_FILE
from ..model_execution.bin import create_binary_tar_file, create_binary_files, check_inputs_directory, \
    cleanup_bin_directory, check_conversion_tools
from ..utils.exceptions import OasisException

__all__ = [
    'OasisAPIClient'
]


# Python 2 standard imports
import os
import io
import time

# Python 3rd party imports
import requests

from requests_toolbelt.multipart.encoder import MultipartEncoder

# Oasis imports
from ..utils.log import oasis_log
from ..utils.status import STATUS_PENDING, STATUS_SUCCESS, STATUS_FAILURE


class OasisAPIClient(object):
    """
    Class for interacting with the oasis api server

    :param oasis_api_url: The root URL for the API. This should
        include the scheme and port (eg. http://localhost:8001)
    :type oasis_api_url: str

    :param logger: The logger to use for message logging. If None
        the root logger is used.
    :type logger: Logger
    """
    #: The chunk size to use when streaming data from the server
    DOWNLOAD_CHUCK_SIZE_IN_BYTES = 1024

    def __init__(self, oasis_api_url, logger=None):
        """
        Construct the client.
        """

        self._oasis_api_url = oasis_api_url
        self._logger = logger or logging.getLogger()

    def build_uri(self, path):
        """
        Builds the uri for the requested resource

        :param path: The path to the resource
        :type path: str

        :return: The fully qualified uri for the resource
        """
        return urllib.parse.urljoin(self._oasis_api_url, path)

    @oasis_log
    def upload_inputs_from_directory(self, directory, bin_directory=None, do_il=False, do_build=False, do_clean=False):
        """
        Upload the CSV files from a specified directory.

        :param directory: the directory containing the CSVs.
        :type directory: str

        :param bin_directory: the directory to build the binary files in, if not set ``directory`` is used
        :type bin_directory: str

        :param do_il: if True, require files for insured loss (IL) calculation.
        :type do_il: bool

        :param do_build: if True, the input tar will be built
        :type do_build: bool

        :param do_clean: if True, remove the tar and bin files
        :type do_clean: bool

        :return: The location of the uploaded inputs.
        """
        bin_directory = bin_directory or directory

        try:
            if do_build:
                check_inputs_directory(directory, do_il=do_il)
                check_conversion_tools(do_il=do_il)
                create_binary_files(directory, bin_directory, do_il=do_il)
                create_binary_tar_file(bin_directory)

            self._logger.debug("Uploading inputs")
            inputs_tar_to_upload = os.path.join(bin_directory, TAR_FILE)

            with io.open(inputs_tar_to_upload, 'rb') as f:
                inputs_multipart_data = MultipartEncoder(
                    fields={
                        'file': (TAR_FILE, f, 'text/plain')
                    }
                )

                response = requests.post(
                    self.build_uri('/exposure'),
                    data=inputs_multipart_data,
                    headers={'Content-Type': inputs_multipart_data.content_type}
                )

            if not response.ok:
                self._logger.error(
                    "POST {} failed: {}, {}".format(response.request.url, str(response.status_code), str(response.json))
                )
                raise OasisException("Failed to save exposure.")

            exposure_location = response.json()['exposures'][0]['location']
            self._logger.debug("Uploaded exposure. Location: " + exposure_location)

            # Return the location of the uploaded inputs
            return exposure_location
        finally:
            # Tidy up
            if do_clean:
                cleanup_bin_directory(bin_directory)

    @oasis_log
    def run_analysis(self, analysis_settings_json, input_location):
        """
        Starts an analysis running. Calling code will need to poll for
        completion or failure, and handle results.

        :param analysis_setting_json: The analysis settings encoded as JSON.
        :type analysis_setting_json: str

        :param input_location: The location of the inputs resource.
        :type input_location: str

        :return: The location of analysis status, to poll.
        """
        response = requests.post(
            self.build_uri("/analysis/" + input_location),
            json=analysis_settings_json,
        )

        if not response.ok:
            self._logger.error("POST {} failed: {}".format(response.request.url, str(response.status_code)))
            raise OasisException("Failed to start analysis")

        analysis_status_location = response.json()['location']
        self._logger.info("Analysis started")

        return analysis_status_location

    def get_analysis_status(self, analysis_status_location):
        """
        Fetches the analysis status for the requested analysis.

        :param analysis_status_location: The location the analysis status.
            This is the value returned from ``run_analysis``
        :type analysis_status_location: str

        :raises OasisException: If the analysis fails.
        :raises OasisException: If the http request is not successful

        :return: A 2 tuple of the status and output location. If the
            status is pending the output location is empty.
        """
        response = requests.get(self.build_uri('/analysis_status/' + analysis_status_location))
        if response.status_code != 200:
            raise OasisException("GET analysis status failed: {}".format(response.status_code))

        self._logger.debug("Response: {}".format(response.json()))
        status = response.json()['status']
        self._logger.debug("Analysis status: " + status)

        if status == STATUS_FAILURE:
            error_message = "Analysis failed: {}".format(response.json()['message'])
            self._logger.error(error_message)
            raise OasisException(error_message)
        elif status == STATUS_SUCCESS:
            return status, response.json()['outputs_location']
        else:
            return status, ''

    @oasis_log
    def run_analysis_and_poll(self, analysis_settings_json, input_location, outputs_directory, analysis_poll_interval=5):
        """
        Run an analysis to completion or failure. resources on the server are
        cleaned up once the analysis is complete.

        :param analysis_setting_json: The analysis settings encoded as JSON.
        :type analysis_settings_json: str

        :param input_location: The local directory containing the inputs resources.
        :type input_location: str

        :param outputs_directory: The local directory to save the outputs to.
        :type outputs_directory: str

        :param analysis_poll_interval: The interval time to wait between status checks
        :type analysis_poll_interval: int

        :raises OasisException: If the analysis fails.
        :raises OasisException: If a http request is not successful
        """
        analysis_status_location = self.run_analysis(analysis_settings_json, input_location)

        self._logger.info("Analysis started")

        status, outputs_location = self.get_analysis_status(analysis_status_location)
        # Poll until the analysis has completed successfully.
        # If the analysis fails then an exception will be raised.
        while status != STATUS_SUCCESS:
            time.sleep(analysis_poll_interval)
            status, outputs_location = self.get_analysis_status(analysis_status_location)

        self._logger.debug("Analysis completed")

        self._logger.debug("Downloading outputs")
        outputs_file = os.path.join(outputs_directory, outputs_location + ".tar.gz")
        self.download_outputs(outputs_location, outputs_file)
        self._logger.debug("Downloaded outputs")

        # cleanup
        self.delete_exposure(input_location)
        self.delete_outputs(outputs_location)

    def delete_resource(self, path):
        """
        Cleans up a resource on the server

        :param path: The path to the resource to delete
        :type path: str
        """
        response = requests.delete(self.build_uri(path))
        if response.status_code != 200:
            self._logger.warning("DELETE {} failed: {}".format(response.request.url, response.status_code))
        else:
            self._logger.info("Deleted {}".format(response.request.url))

    @oasis_log
    def delete_exposure(self, input_location):
        """
        Cleans up an exposure on the server

        :param input_location: The input location for the analysis
        :type input_location: str
        """
        self._logger.info("Deleting exposure")
        self.delete_resource('/exposure/' + input_location)

    @oasis_log
    def delete_outputs(self, outputs_location):
        """
        Cleans up ouptut files on the server

        :param output_location: The output location for the analysis
        :type output_location: str
        """
        self._logger.info("Deleting outputs")
        self.delete_resource('/outputs/' + outputs_location)

    def download_resource(self, path, localfile):
        """
        Streams a resource from the server to a local file

        :param path: The path of the resource to download
        :type path: str

        :param localfile: The path to the local file to download the
            resource to.
        :type localfile: str
        """
        if os.path.exists(localfile):
            error_message = 'Local file alreday exists: {}'.format(localfile)
            self._logger.error(error_message)
            raise OasisException(error_message)

        response = requests.get(self.build_uri(path), stream=True)
        if not response.ok:
            exception_message = 'GET {} failed: {}'.format(response.request.url, response.status_code)
            self._logger.error(exception_message)
            raise OasisException(exception_message)

        with io.open(localfile, 'wb') as f:
            for chunk in response.iter_content(chunk_size=self.DOWNLOAD_CHUCK_SIZE_IN_BYTES):
                f.write(chunk)

    @oasis_log
    def download_exposure(self, exposure_location, localfile):
        """
        Download exposure data to a specified local file.

        :param exposure_location: The location of the exposure resource.
        :type exposure_location: str

        :param localfile: The path to the local file to download the
            exposure to.
        :type localfile: str
        """
        self.download_resource('/exposure/' + exposure_location, localfile)

    @oasis_log
    def download_outputs(self, outputs_location, localfile):
        """
        Download outputs data to a specified local file.

        :param outputs_location: The location of the ouptuts resource.
        :type outputs_location: str

        :param localfile: The path to the local file to download the
            exposure to.
        :type localfile: str
        """
        self.download_resource('/outputs/' + outputs_location, localfile)

    @oasis_log
    def health_check(self, poll_attempts=1, retry_delay=5):
        """
        Checks the health of the server.

        :param poll_attempts: The maximum number of checks to make
        :type poll_attempts: int

        :param retry_delay: The amount of time to wait between retry attempts
        :type retry_delay: int

        :return: True If the server is healthy, otherwise False
        """
        for attempt in range(poll_attempts):
            try:
                if attempt > 0:
                    time.sleep(retry_delay)

                resp = requests.get('{}/healthcheck'.format(self._oasis_api_url))
                if resp.status_code == 200:
                    return True
            except RequestException:
                pass
        else:
            self._logger.error(
                'Could not connect to the api server after {} attempts. Check it is running and try again later.'.format(
                    poll_attempts,
                )
            )
            return False
