# coding: utf-8

# Copyright 2019 IBM All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
from http.cookiejar import CookieJar
import json as json_import
from os.path import basename
import platform
import sys
from typing import Dict, List, Optional, Tuple, Union

import requests
from requests.structures import CaseInsensitiveDict
from ibm_cloud_sdk_core.authenticators import Authenticator
from .version import __version__
from .utils import has_bad_first_or_last_char, remove_null_values, cleanup_values, read_external_sources
from .detailed_response import DetailedResponse
from .api_exception import ApiException
from .jwt_token_manager import JWTTokenManager

# Uncomment this to enable http debugging
# import http.client as http_client
# http_client.HTTPConnection.debuglevel = 1

class BaseService:
    """Common functionality shared by generated service classes.

    The base service authenticates requests via its authenticator, stores cookies, and
    wraps responses from the service endpoint in DetailedResponse or APIException objects.

    Keyword Arguments:
        service_url: Url to the service endpoint. Defaults to None.
        authenticator: Adds authentication data to service requests. Defaults to None.
        disable_ssl_verification: A flag that indicates whether verification of the server's SSL
            certificate should be disabled or not. Defaults to False.

    Attributes:
        service_url (str): Url to the service endpoint.
        authenticator (Authenticator): Adds authentication data to service requests.
        disable_ssl_verification (bool): A flag that indicates whether verification of
            the server's SSL certificate should be disabled or not.
        default_headers (dict): A dictionary of headers to be sent with every HTTP request to the service endpoint.
        jar (http.cookiejar.CookieJar): Stores cookies received from the service.
        http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests.
    Raises:
        ValueError: If Authenticator is not provided or invalid type.
    """
    SDK_NAME = 'ibm-python-sdk-core'
    ERROR_MSG_DISABLE_SSL = 'The connection failed because the SSL certificate is not valid. To use a self-signed '\
                            'certificate, disable verification of the server\'s SSL certificate by invoking the '\
                            'set_disable_ssl_verification(True) on your service instance and/ or use the '\
                            'disable_ssl_verification option of the authenticator.'

    def __init__(self,
                 service_url: str = None,
                 authenticator: Authenticator = None,
                 disable_ssl_verification: bool = False):
        self.service_url = service_url
        self.http_config = {}
        self.jar = CookieJar()
        self.authenticator = authenticator
        self.disable_ssl_verification = disable_ssl_verification
        self.default_headers = None
        self._set_user_agent_header(self._build_user_agent())
        if not self.authenticator:
            raise ValueError('authenticator must be provided')
        if not isinstance(self.authenticator, Authenticator):
            raise ValueError(
                'authenticator should be of type Authenticator')

    @staticmethod
    def _get_system_info():
        return '{0} {1} {2}'.format(
            platform.system(),  # OS
            platform.release(),  # OS version
            platform.python_version()  # Python version
        )

    def _build_user_agent(self):
        return '{0}-{1} {2}'.format(self.SDK_NAME, __version__,
                                    self._get_system_info())

    def configure_service(self, service_name: str):
        """Look for external configuration of a service. Set service properties.

        Try to get config from external sources, with the following priority:
        1. Credentials file(ibm-credentials.env)
        2. Environment variables
        3. VCAP Services(Cloud Foundry)

        Args:
            service_name: The service name

        Raises:
            ValueError: If service_name is not a string.
        """
        if not isinstance(service_name, str):
            raise ValueError('Service_name must be of type string.')

        config = read_external_sources(service_name)
        if config.get('URL'):
            self.set_service_url(config.get('URL'))
        if config.get('DISABLE_SSL'):
            self.set_disable_ssl_verification(
                bool(config.get('DISABLE_SSL'))
            )

    def _set_user_agent_header(self, user_agent_string=None):
        self.user_agent_header = {'User-Agent': user_agent_string}

    def set_http_config(self, http_config: dict):
        """Sets the http config dictionary.

        The dictionary can contain values that control the timeout, proxies, and etc of HTTP requests.

        Arguments:
            http_config: Configuration values to customize HTTP behaviors.

        Raises:
            TypeError: http_config is not a dict.
        """
        if isinstance(http_config, dict):
            self.http_config = http_config
            if (self.authenticator and hasattr(self.authenticator, 'token_manager') and
                    isinstance(self.authenticator.token_manager, JWTTokenManager)):
                self.authenticator.token_manager.http_config = http_config
        else:
            raise TypeError("http_config parameter must be a dictionary")

    def set_disable_ssl_verification(self, status: bool = False):
        """Set the flag that indicates whether verification of
        the server's SSL certificate should be disabled or not.

        Keyword Arguments:
            status: set to true to disable ssl verification (default: {False})
        """
        self.disable_ssl_verification = status

    def set_service_url(self, service_url: str):
        """Set the url the service will make HTTP requests too.

        Arguments:
            service_url: The WHATWG URL standard origin ex. https://example.service.com

        Raises:
            ValueError: Improperly formatted service_url
        """
        if has_bad_first_or_last_char(service_url):
            raise ValueError(
                'The service url shouldn\'t start or end with curly brackets or quotes. '
                'Be sure to remove any {} and \" characters surrounding your service url'
            )
        self.service_url = service_url

    def get_authenticator(self) -> Authenticator:
        """Get the authenticator currently used by the service.

        Returns:
            The authenticator currently used by the service.
        """
        return self.authenticator

    def set_default_headers(self, headers: Dict[str, str]):
        """Set http headers to be sent in every request.

        Arguments:
            headers: A dictionary of headers
        """
        if isinstance(headers, dict):
            self.default_headers = headers
        else:
            raise TypeError("headers parameter must be a dictionary")

    def send(self, request: requests.Request, **kwargs) -> DetailedResponse:
        """Send a request and wrap the response in a DetailedResponse or APIException.

        Args:
            request: The request to send to the service endpoint.

        Raises:
            ApiException: The exception from the API.

        Returns:
            The response from the request.
        """
        # Use a one minute timeout when our caller doesn't give a timeout.
        # http://docs.python-requests.org/en/master/user/quickstart/#timeouts
        kwargs = dict({"timeout": 60}, **kwargs)
        kwargs = dict(kwargs, **self.http_config)

        if self.disable_ssl_verification:
            kwargs['verify'] = False

        try:
            response = requests.request(**request, cookies=self.jar, **kwargs)

            if 200 <= response.status_code <= 299:
                if response.status_code == 204 or request['method'] == 'HEAD':
                    # There is no body content for a HEAD request or a 204 response
                    result = None
                elif not response.text:
                    result = None
                else:
                    try:
                        result = response.json()
                    except:
                        result = response
                return DetailedResponse(result, response.headers,
                                        response.status_code)

            error_message = None
            if response.status_code == 401:
                error_message = 'Unauthorized: Access is denied due to ' \
                                'invalid credentials'
            raise ApiException(
                response.status_code, error_message, http_response=response)
        except requests.exceptions.SSLError:
            logging.exception(self.ERROR_MSG_DISABLE_SSL)
            raise
        except ApiException as err:
            logging.exception(err.message)
            raise
        except:
            logging.exception('Error in service call')
            raise


    def prepare_request(self,
                        method: str,
                        url: str,
                        headers: Optional[dict] = None,
                        params: Optional[dict] = None,
                        data: Optional[Union[str, dict]] = None,
                        files: Optional[Union[
                            Dict[str, Tuple[str]],
                            List[Tuple[str, Tuple[str, ...]]]
                        ]] = None,
                        **kwargs) -> dict:
        """Build a dict that represents an HTTP service request.

        Clean up headers, add default http configuration, convert data
        into json, process files, and merge all into a single request dict.

        Args:
            method: The HTTP method of the request ex. GET, POST, etc.
            url: The origin + pathname according to WHATWG spec.

        Keyword Arguments:
            headers: Headers of the request.
            params: Querystring data to be appended to the url.
            data: The request body. Converted to json if a dict.
            files: 'files' can be a dictionary (i.e { '<part-name>': (<tuple>)}),
                or a list of tuples [ (<part-name>, (<tuple>))... ]

        Returns:
            Prepared request dictionary.
        """
        # pylint: disable=unused-argument; necessary for kwargs
        request = {'method': method}

        # validate the service url is set
        if not self.service_url:
            raise ValueError('The service_url is required')
        request['url'] = self.service_url + url

        headers = remove_null_values(headers) if headers else {}
        headers = cleanup_values(headers)
        headers = CaseInsensitiveDict(headers)
        if self.default_headers is not None:
            headers.update(self.default_headers)
        if not any(key in headers for key in ['user-agent', 'User-Agent']):
            headers.update(self.user_agent_header)
        request['headers'] = headers

        params = remove_null_values(params)
        params = cleanup_values(params)
        request['params'] = params

        if sys.version_info >= (3, 0) and isinstance(data, str):
            data = data.encode('utf-8')

        if data and isinstance(data, dict):
            data = remove_null_values(data)
            headers.update({'content-type': 'application/json'})
            data = json_import.dumps(data)
        request['data'] = data

        self.authenticator.authenticate(request)

        # Next, we need to process the 'files' argument to try to fill in
        # any missing filenames where possible.
        # 'files' can be a dictionary (i.e { '<part-name>': (<tuple>)} )
        # or a list of tuples [ (<part-name>, (<tuple>))... ]
        # If 'files' is a dictionary we'll convert it to a list of tuples.
        new_files = []
        if files is not None:
            # If 'files' is a dictionary, transform it into a list of tuples.
            if isinstance(files, dict):
                files = remove_null_values(files)
                files = files.items()
            # Next, fill in any missing filenames from file tuples.
            for part_name, file_tuple in files:
                if file_tuple and len(file_tuple) == 3 and file_tuple[0] is None:
                    file = file_tuple[1]
                    if file and hasattr(file, 'name'):
                        filename = basename(file.name)
                        file_tuple = (filename, file_tuple[1], file_tuple[2])
                new_files.append((part_name, file_tuple))
        request['files'] = new_files
        return request

    @staticmethod
    def encode_path_vars(*args: str) -> List[str]:
        """Encode path variables to be substituted into a URL path.

        Arguments:
            args: A list of strings to be URL path encoded

        Returns:
            A list of encoded strings that are safe to substitute into a URL path.
        """
        return (requests.utils.quote(x, safe='') for x in args)

    # The methods below are kept for compatibility and should be removed
    # in the next major release.

    # pylint: disable=protected-access

    @staticmethod
    def _convert_model(val):
        if isinstance(val, str):
            val = json_import.loads(val)
        if hasattr(val, "_to_dict"):
            return val._to_dict()
        return val

    @staticmethod
    def _convert_list(val):
        if isinstance(val, list):
            return ",".join(val)
        return val

    @staticmethod
    def _encode_path_vars(*args):
        return (requests.utils.quote(x, safe='') for x in args)
