from __future__ import absolute_import
import requests
from requests.exceptions import Timeout
from warnings import warn
import configparser
import json
from os import environ, path
from getpass import getpass
import base64
import six


def get_token(username, password=None, token_type='patric', timeout=30):
    """ Get an authentication token for SEED web services.

        The authentication token is also stored in the .patric_config file in
        the user's home directory. The SeedClient object retrieves the
        authentication token from the file so the user does not need to keep
        getting a new token.

    Parameters
    ----------
    username : str
        User name
    password : str, optional
        Password or None to prompt and enter password
    token_type : {'patric', 'rast'}, optional
        Type of authentication token
    timeout : integer
        Number of seconds to wait for response

    Returns
    -------
    str
        User ID (which can be different than user name)
    """

    # Prompt for a password if not specified.
    if password is None:
        password = getpass(prompt='{0} password: '.format(token_type))

    # Get an authentication token from the specified web service.
    if token_type == 'patric':
        url = 'https://user.patricbrc.org/authenticate'
        request_data = {'username': username, 'password': password}
        try:
            response = requests.post(url, data=request_data, timeout=timeout)
        except Timeout as e:
            warn('The PATRIC authentication service did not return a response within {0} seconds. '
                 'Try again with a larger timeout value. (Details: {1})'.format(timeout, e))
            return None
        if response.status_code != requests.codes.OK:
            response.raise_for_status()
        token = response.text
        user_id = token.split('|')[0].replace('un=', '')

    elif token_type == 'rast':
        url = 'http://rast.nmpdr.org/goauth/token?grant_type=client_credentials&client_id={0}'.format(username)
        headers = dict()
        headers['Content-Type'] = 'application/json'
        headers['Authorization'] = 'Basic {0}'\
            .format(base64.urlsafe_b64encode(six.b(username + ':' + password)).decode("ascii"))
        try:
            response = requests.get(url, headers=headers, timeout=timeout)
        except Timeout as e:
            warn('The RAST authentication service did not return a response within {0} seconds. '
                 'Try again with a larger timeout value. (Details: {1})'.format(timeout, e))
            return None
        if response.status_code != requests.codes.OK:
            response.raise_for_status()
        data = json.loads(response.text)
        token = data['access_token']
        user_id = data['client_id']

    else:
        raise ValueError('Token token_type {0} is not valid'.format(token_type))

    # Save the authentication data in config file.
    config_file = path.join(environ['HOME'], '.patric_config')
    config = configparser.ConfigParser()
    config.read(config_file)
    if not config.has_section('authentication'):
        config.add_section('authentication')
    config.set('authentication', 'token', token)
    config.set('authentication', 'user_id', user_id)
    config.write(open(config_file, 'w'))

    return user_id


class ServerError(Exception):
    """ Exception raised when server returns an error. """

    def __init__(self, message, name='JSONRPCError', code=-32603, data=None, error=None):
        # The name is always 'JSONRPCError and the code is always -32603 because it
        # is hard-coded in the server.
        self.name = name
        self.code = code

        if message is None:
            self.data = data or error or ''
            self.message = self.data
        else:
            # Separate the lines in the message and store as an array of lines.
            self.data = message.split('\n')

            # If the first line has the _ERROR_ delimiter, extract the message.
            if self.data[0].startswith('_ERROR_'):
                self.message = self.data[0][7:-7]

            # If the second line has the _ERROR_ delimiter, extract the message.
            elif self.data[0] == 'JSONRPC error:' and len(self.data) > 1 and self.data[1].startswith('_ERROR_'):
                self.message = self.data[1][7:-7]

            # If the first line is a JSONRPC error and there is a second line, extract the message.
            elif self.data[0] == 'JSONRPC error:' and len(self.data) > 1:
                self.message = self.data[1]

            # Otherwise, use the first line as the message.
            else:
                self.message = self.data[0]

    def traceback(self):
        output = ''
        for line in self.data:
            output += line + '\n'
        return output

    def __str__(self):
        return self.message


class ObjectNotFoundError(Exception):
    """ Exception raised when an object cannot be retrieved from workspace. """

    def __init__(self, message, data):
        self.message = message
        self.data = data

    def traceback(self):
        output = ''
        for line in self.data:
            output += line + '\n'
        return output

    def __str__(self):
        return self.message


class ObjectTypeError(Exception):
    """ Exception raised when an object is the wrong type for the operation. """

    def __init__(self, message, data):
        self.message = message
        self.data = data

    def traceback(self):
        output = ''
        for line in self.data:
            output += line + '\n'
        return output

    def __str__(self):
        return self.message


class DuplicateGapfillError(Exception):
    """ Exception raised when a gap fill solution is already available. """
    pass


class JobError(Exception):
    """ Exception raised when a job submitted to app service ended in error. """
    pass


class AuthenticationError(Exception):
    """ Exception raised when there is a problem with authentication token. """


def handle_server_error(e, references=None):
    """ Handle an error returned by a PATRIC service server.

    Parameters
    ----------
    e : Exception
        Exception returned by server
    references : list
        List of workspace references in input parameters of server method

    Returns
    -------
    ObjectNotFoundError
        When server method had a problem with a workspace reference
    DuplicateGapfillError
        When a gap fill solution already exists for a media condition
    ObjectTypeError
        When a workspace reference refers to an object of the wrong type
    Exception
        Source exception for all other cases
    """

    # Map a generic server error to a specific exception.
    if isinstance(e, ServerError):
        # Errors returned by ModelSEED server do not indicate which workspace reference had
        # a problem. One of the references in the list is the one that caused the problem.
        reference_string = ''
        if references is not None:
            reference_string = '"{0}"'.format('" or "'.join(references))
        if 'Object not found!' in e.message or 'Owner not specified in deletion!' in e.message:
            msg = 'An object was not found in workspace: {0}'.format(reference_string)
            return ObjectNotFoundError(msg, e.data)

        if 'does not include at least a top level directory!' in e.message:
            msg = 'An object reference is missing a top level directory: {0}'.format(reference_string)
            return ObjectNotFoundError(msg, e.data)

        if 'Path does not point to folder or object:' in e.message:
            msg = 'An object was not found in workspace: {0}'.format(reference_string)
            return ObjectNotFoundError(msg, e.data)

        if 'User lacks permission to / for requested action!' in e.message:
            msg = 'User does not have permission to a directory in an object reference: {0}'.format(reference_string)
            return ObjectNotFoundError(msg, e.data)

        if 'is not a valid object path!' in e.message:
            msg = 'An object reference is not a valid path: {0}'.format(reference_string)
            return ObjectNotFoundError(msg, e.data)

        if 'Invalid date format:' in e.message:
            msg = 'An object was not found in workspace: {0}'.format(reference_string)
            return ObjectNotFoundError(msg, e.data)

        if 'No gap filling needed on specified condition' in e.message:
            return DuplicateGapfillError('Gap fill solution already available for specified media')

        if 'does not match specified type' in e.message:
            return ObjectTypeError(e.message, e.data)

    # Return the input exception.
    return e


class SeedClient(object):
    """ Client for SEED web services """

    def __init__(self, url, name, token=None):
        """ Initialize object.

        Parameters
        ----------
        url : str
            URL of service endpoint
        name : str
            Name of service
        token : str, optional
            Authentication token for SEED web services, when None get the
            token from the .patric_config file when calling a method
        """

        self.url = url
        self.name = name
        self.username = None
        if token is not None:
            self.username = token.split('|')[0].replace('un=', '')

        # Create the headers for the request to the server.
        self.headers = dict()
        self.headers['AUTHORIZATION'] = token

        return

    def call(self, method, params, timeout=1800):
        """ Call a server method and wait for the response.

        Parameters
        ----------
        method: str
            Name of server method
        params : dict or list
            Input parameters for method
        timeout : integer
            Number of seconds to wait for response

        Returns
        -------
        data
            Output of method in JSON format

        Raises
        ------
        ServerError
            When server returns an error response
        """

        # If needed, look for the authentication token in the Patric config file.
        if self.headers['AUTHORIZATION'] is None:
            self.set_authentication_token()

        # Create the body of the request for the specified method.
        request_data = dict()
        request_data['method'] = self.name + '.' + method
        request_data['params'] = params
        request_data['version'] = '1.1'
        request_data['id'] = '1'
        # Send the request to the server and get back a response.
        response = requests.post(self.url, data=json.dumps(request_data), headers=self.headers, timeout=timeout, verify=False)

        if response.status_code == requests.codes.server_error:
            if 'content-type' in response.headers and response.headers['content-type'] == 'application/json':
                err = json.loads(response.text)
                if 'error' in err:
                    raise ServerError(**err['error'])
                else:
                    raise ServerError(response.text)
            else:
                raise ServerError(response.text)

        if response.status_code != requests.codes.OK:
            response.raise_for_status()
        return json.loads(response.text)['result'][0]  # Get the output from the method in the response

    def set_authentication_token(self):
        """ Set the authentication token from the config file.

        Raises
        ------
        AuthenticationError
            When there is a problem with the authentication section in the config file
        """

        config = configparser.ConfigParser()
        config.read(path.join(environ['HOME'], '.patric_config'))
        try:
            self.headers['AUTHORIZATION'] = config.get('authentication', 'token')
            self.username = config.get('authentication', 'user_id')
        except (configparser.NoSectionError, configparser.NoOptionError):
            self.headers['AUTHORIZATION'] = None
            raise AuthenticationError('Call get_token() to obtain an authentication token')

from __future__ import absolute_import
import requests
from requests.exceptions import Timeout
from warnings import warn
import configparser
import json
from os import environ, path
from getpass import getpass
import base64
import six


def get_token(username, password=None, token_type='patric', timeout=30):
    """ Get an authentication token for SEED web services.

        The authentication token is also stored in the .patric_config file in
        the user's home directory. The SeedClient object retrieves the
        authentication token from the file so the user does not need to keep
        getting a new token.

    Parameters
    ----------
    username : str
        User name
    password : str, optional
        Password or None to prompt and enter password
    token_type : {'patric', 'rast'}, optional
        Type of authentication token
    timeout : integer
        Number of seconds to wait for response

    Returns
    -------
    str
        User ID (which can be different than user name)
    """

    # Prompt for a password if not specified.
    if password is None:
        password = getpass(prompt='{0} password: '.format(token_type))

    # Get an authentication token from the specified web service.
    if token_type == 'patric':
        url = 'https://user.patricbrc.org/authenticate'
        request_data = {'username': username, 'password': password}
        try:
            response = requests.post(url, data=request_data, timeout=timeout)
        except Timeout as e:
            warn('The PATRIC authentication service did not return a response within {0} seconds. '
                 'Try again with a larger timeout value. (Details: {1})'.format(timeout, e))
            return None
        if response.status_code != requests.codes.OK:
            response.raise_for_status()
        token = response.text
        user_id = token.split('|')[0].replace('un=', '')

    elif token_type == 'rast':
        url = 'http://rast.nmpdr.org/goauth/token?grant_type=client_credentials&client_id={0}'.format(username)
        headers = dict()
        headers['Content-Type'] = 'application/json'
        headers['Authorization'] = 'Basic {0}'\
            .format(base64.urlsafe_b64encode(six.b(username + ':' + password)).decode("ascii"))
        try:
            response = requests.get(url, headers=headers, timeout=timeout)
        except Timeout as e:
            warn('The RAST authentication service did not return a response within {0} seconds. '
                 'Try again with a larger timeout value. (Details: {1})'.format(timeout, e))
            return None
        if response.status_code != requests.codes.OK:
            response.raise_for_status()
        data = json.loads(response.text)
        token = data['access_token']
        user_id = data['client_id']

    else:
        raise ValueError('Token token_type {0} is not valid'.format(token_type))

    # Save the authentication data in config file.
    config_file = path.join(environ['HOME'], '.patric_config')
    config = configparser.ConfigParser()
    config.read(config_file)
    if not config.has_section('authentication'):
        config.add_section('authentication')
    config.set('authentication', 'token', token)
    config.set('authentication', 'user_id', user_id)
    config.write(open(config_file, 'w'))

    return user_id


class ServerError(Exception):
    """ Exception raised when server returns an error. """

    def __init__(self, message, name='JSONRPCError', code=-32603, data=None, error=None):
        # The name is always 'JSONRPCError and the code is always -32603 because it
        # is hard-coded in the server.
        self.name = name
        self.code = code

        if message is None:
            self.data = data or error or ''
            self.message = self.data
        else:
            # Separate the lines in the message and store as an array of lines.
            self.data = message.split('\n')

            # If the first line has the _ERROR_ delimiter, extract the message.
            if self.data[0].startswith('_ERROR_'):
                self.message = self.data[0][7:-7]

            # If the second line has the _ERROR_ delimiter, extract the message.
            elif self.data[0] == 'JSONRPC error:' and len(self.data) > 1 and self.data[1].startswith('_ERROR_'):
                self.message = self.data[1][7:-7]

            # If the first line is a JSONRPC error and there is a second line, extract the message.
            elif self.data[0] == 'JSONRPC error:' and len(self.data) > 1:
                self.message = self.data[1]

            # Otherwise, use the first line as the message.
            else:
                self.message = self.data[0]

    def traceback(self):
        output = ''
        for line in self.data:
            output += line + '\n'
        return output

    def __str__(self):
        return self.message


class ObjectNotFoundError(Exception):
    """ Exception raised when an object cannot be retrieved from workspace. """

    def __init__(self, message, data):
        self.message = message
        self.data = data

    def traceback(self):
        output = ''
        for line in self.data:
            output += line + '\n'
        return output

    def __str__(self):
        return self.message


class ObjectTypeError(Exception):
    """ Exception raised when an object is the wrong type for the operation. """

    def __init__(self, message, data):
        self.message = message
        self.data = data

    def traceback(self):
        output = ''
        for line in self.data:
            output += line + '\n'
        return output

    def __str__(self):
        return self.message


class DuplicateGapfillError(Exception):
    """ Exception raised when a gap fill solution is already available. """
    pass


class JobError(Exception):
    """ Exception raised when a job submitted to app service ended in error. """
    pass


class AuthenticationError(Exception):
    """ Exception raised when there is a problem with authentication token. """


def handle_server_error(e, references=None):
    """ Handle an error returned by a PATRIC service server.

    Parameters
    ----------
    e : Exception
        Exception returned by server
    references : list
        List of workspace references in input parameters of server method

    Returns
    -------
    ObjectNotFoundError
        When server method had a problem with a workspace reference
    DuplicateGapfillError
        When a gap fill solution already exists for a media condition
    ObjectTypeError
        When a workspace reference refers to an object of the wrong type
    Exception
        Source exception for all other cases
    """

    # Map a generic server error to a specific exception.
    if isinstance(e, ServerError):
        # Errors returned by ModelSEED server do not indicate which workspace reference had
        # a problem. One of the references in the list is the one that caused the problem.
        reference_string = ''
        if references is not None:
            reference_string = '"{0}"'.format('" or "'.join(references))
        if 'Object not found!' in e.message or 'Owner not specified in deletion!' in e.message:
            msg = 'An object was not found in workspace: {0}'.format(reference_string)
            return ObjectNotFoundError(msg, e.data)

        if 'does not include at least a top level directory!' in e.message:
            msg = 'An object reference is missing a top level directory: {0}'.format(reference_string)
            return ObjectNotFoundError(msg, e.data)

        if 'Path does not point to folder or object:' in e.message:
            msg = 'An object was not found in workspace: {0}'.format(reference_string)
            return ObjectNotFoundError(msg, e.data)

        if 'User lacks permission to / for requested action!' in e.message:
            msg = 'User does not have permission to a directory in an object reference: {0}'.format(reference_string)
            return ObjectNotFoundError(msg, e.data)

        if 'is not a valid object path!' in e.message:
            msg = 'An object reference is not a valid path: {0}'.format(reference_string)
            return ObjectNotFoundError(msg, e.data)

        if 'Invalid date format:' in e.message:
            msg = 'An object was not found in workspace: {0}'.format(reference_string)
            return ObjectNotFoundError(msg, e.data)

        if 'No gap filling needed on specified condition' in e.message:
            return DuplicateGapfillError('Gap fill solution already available for specified media')

        if 'does not match specified type' in e.message:
            return ObjectTypeError(e.message, e.data)

    # Return the input exception.
    return e


class SeedClient(object):
    """ Client for SEED web services """

    def __init__(self, url, name, token=None):
        """ Initialize object.

        Parameters
        ----------
        url : str
            URL of service endpoint
        name : str
            Name of service
        token : str, optional
            Authentication token for SEED web services, when None get the
            token from the .patric_config file when calling a method
        """

        self.url = url
        self.name = name
        self.username = None
        if token is not None:
            self.username = token.split('|')[0].replace('un=', '')

        # Create the headers for the request to the server.
        self.headers = dict()
        self.headers['AUTHORIZATION'] = token

        return

    def call(self, method, params, timeout=1800):
        """ Call a server method and wait for the response.

        Parameters
        ----------
        method: str
            Name of server method
        params : dict or list
            Input parameters for method
        timeout : integer
            Number of seconds to wait for response

        Returns
        -------
        data
            Output of method in JSON format

        Raises
        ------
        ServerError
            When server returns an error response
        """

        # If needed, look for the authentication token in the Patric config file.
        if self.headers['AUTHORIZATION'] is None:
            self.set_authentication_token()

        # Create the body of the request for the specified method.
        request_data = dict()
        request_data['method'] = self.name + '.' + method
        request_data['params'] = params
        request_data['version'] = '1.1'
        request_data['id'] = '1'
        # Send the request to the server and get back a response.
        response = requests.post(self.url, data=json.dumps(request_data), headers=self.headers, timeout=timeout, verify=False)

        if response.status_code == requests.codes.server_error:
            if 'content-type' in response.headers and response.headers['content-type'] == 'application/json':
                err = json.loads(response.text)
                if 'error' in err:
                    raise ServerError(**err['error'])
                else:
                    raise ServerError(response.text)
            else:
                raise ServerError(response.text)

        if response.status_code != requests.codes.OK:
            response.raise_for_status()
        return json.loads(response.text)['result'][0]  # Get the output from the method in the response

    def set_authentication_token(self):
        """ Set the authentication token from the config file.

        Raises
        ------
        AuthenticationError
            When there is a problem with the authentication section in the config file
        """

        config = configparser.ConfigParser()
        config.read(path.join(environ['HOME'], '.patric_config'))
        try:
            self.headers['AUTHORIZATION'] = config.get('authentication', 'token')
            self.username = config.get('authentication', 'user_id')
        except (configparser.NoSectionError, configparser.NoOptionError):
            self.headers['AUTHORIZATION'] = None
            raise AuthenticationError('Call get_token() to obtain an authentication token')

Get an authentication token for SEED web services.          The authentication token is also stored in the .patric_config file in         the user's home directory. The SeedClient object retrieves the         authentication token from the file so the user does not need to keep         getting a new token.      Parameters     ----------     username : str         User name     password : str, optional         Password or None to prompt and enter password     token_type : {'patric', 'rast'}, optional         Type of authentication token     timeout : integer         Number of seconds to wait for response      Returns     -------     str         User ID (which can be different than user name) # Prompt for a password if not specified. # Get an authentication token from the specified web service. # Save the authentication data in config file. Exception raised when server returns an error. # The name is always 'JSONRPCError and the code is always -32603 because it # is hard-coded in the server. # Separate the lines in the message and store as an array of lines. # If the first line has the _ERROR_ delimiter, extract the message. # If the second line has the _ERROR_ delimiter, extract the message. # If the first line is a JSONRPC error and there is a second line, extract the message. # Otherwise, use the first line as the message. Exception raised when an object cannot be retrieved from workspace. Exception raised when an object is the wrong type for the operation. Exception raised when a gap fill solution is already available. Exception raised when a job submitted to app service ended in error. Exception raised when there is a problem with authentication token. Handle an error returned by a PATRIC service server.      Parameters     ----------     e : Exception         Exception returned by server     references : list         List of workspace references in input parameters of server method      Returns     -------     ObjectNotFoundError         When server method had a problem with a workspace reference     DuplicateGapfillError         When a gap fill solution already exists for a media condition     ObjectTypeError         When a workspace reference refers to an object of the wrong type     Exception         Source exception for all other cases # Map a generic server error to a specific exception. # Errors returned by ModelSEED server do not indicate which workspace reference had # a problem. One of the references in the list is the one that caused the problem. # Return the input exception. Client for SEED web services Initialize object.          Parameters         ----------         url : str             URL of service endpoint         name : str             Name of service         token : str, optional             Authentication token for SEED web services, when None get the             token from the .patric_config file when calling a method # Create the headers for the request to the server. Call a server method and wait for the response.          Parameters         ----------         method: str             Name of server method         params : dict or list             Input parameters for method         timeout : integer             Number of seconds to wait for response          Returns         -------         data             Output of method in JSON format          Raises         ------         ServerError             When server returns an error response # If needed, look for the authentication token in the Patric config file. # Create the body of the request for the specified method. # Send the request to the server and get back a response. # Get the output from the method in the response Set the authentication token from the config file.          Raises         ------         AuthenticationError             When there is a problem with the authentication section in the config file