# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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 base64

try:
    import simplejson as json
except:
    import json

from libcloud.utils.py3 import httplib
from libcloud.utils.py3 import b

from libcloud.common.base import JsonResponse, ConnectionUserAndKey

from libcloud.container.base import (Container, ContainerDriver,
                                     ContainerImage)

from libcloud.container.providers import Provider
from libcloud.container.types import ContainerState

VALID_RESPONSE_CODES = [httplib.OK, httplib.ACCEPTED, httplib.CREATED,
                        httplib.NO_CONTENT]


class RancherResponse(JsonResponse):

    def parse_error(self):
        parsed = super(RancherResponse, self).parse_error()
        if 'fieldName' in parsed:
            return "Field %s is %s: %s - %s" % (parsed['fieldName'],
                                                parsed['code'],
                                                parsed['message'],
                                                parsed['detail'])
        else:
            return "%s - %s" % (parsed['message'], parsed['detail'])

    def success(self):
        return self.status in VALID_RESPONSE_CODES


class RancherException(Exception):

    def __init__(self, code, message):
        self.code = code
        self.message = message
        self.args = (code, message)

    def __str__(self):
        return "%s %s" % (self.code, self.message)

    def __repr__(self):
        return "RancherException %s %s" % (self.code, self.message)


class RancherConnection(ConnectionUserAndKey):

    responseCls = RancherResponse
    timeout = 30

    def add_default_headers(self, headers):
        """
        Add parameters that are necessary for every request
        If user and password are specified, include a base http auth
        header
        """
        headers['Content-Type'] = 'application/json'
        headers['Accept'] = 'application/json'
        if self.key and self.user_id:
            user_b64 = base64.b64encode(b('%s:%s' % (self.user_id, self.key)))
            headers['Authorization'] = 'Basic %s' % (user_b64.decode('utf-8'))
        return headers


class RancherContainerDriver(ContainerDriver):

    type = Provider.RANCHER
    name = 'Rancher'
    website = 'http://rancher.com'
    connectionCls = RancherConnection
    # Holding off on cluster support for now.
    # Only Environment API interaction enabled.
    supports_clusters = False
    # As in the /v1/
    version = '1'

    def __init__(self, key, secret, secure=True, host='localhost', port=443):
        """
        Rancher Container driver class.

        Example:

        >>> from libcloud.container.providers import get_driver
        >>> from libcloud.container.types import Provider

        >>> driver = get_driver(Provider.RANCHER)
        >>> connection = driver(key="ACCESS_KEY_HERE",
        secret="SECRET_KEY_HERE", host="172.30.0.100", port=8080)

        >>> image = ContainerImage("hastebin", "hastebin", "rlister/hastebin",
        "latest", driver=None)
        >>> newcontainer = connection.deploy_container("myawesomepastebin",
        image, environment={"STORAGE_TYPE": "file"})

        :param    key: API key or username to used (required)
        :type     key: ``str``

        :param    secret: Secret password to be used (required)
        :type     secret: ``str``

        :param    secure: Whether to use HTTPS or HTTP.
        :type     secure: ``bool``

        :param    host: Override hostname used for connections.
        :type     host: ``str``

        :param    port: Override port used for connections.
        :type     port: ``int``


        :return: ``None``
        """

        if host.startswith('http://'):
            secure = False

        super(RancherContainerDriver, self).__init__(key=key, secret=secret,
                                                     secure=secure, host=host,
                                                     port=port)

        # strip the prefix
        prefixes = ['http://', 'https://']
        for prefix in prefixes:
            if host.startswith(prefix):
                host = host.strip(prefix)

        # We only support environment api keys, meaning none of this:
        # self.baseuri = "/v%s/projects/%s" % (self.version, project_id)
        self.baseuri = "/v%s" % self.version

    def ex_list_stacks(self):
        """
        List all Rancher Stacks

        http://docs.rancher.com/rancher/v1.2/en/api/api-resources/environment/

        :rtype: ``list`` of ``dict``
        """

        result = self.connection.request(
            "%s/environments" % self.baseuri).object
        return result['data']

    def ex_deploy_stack(self, name, description=None, docker_compose=None,
                        environment=None, external_id=None,
                        rancher_compose=None, start=True):
        """
        Deploy a new stack.

        http://docs.rancher.com/rancher/v1.2/en/api/api-resources/environment/#create

        :param name: The desired name of the stack. (required)
        :type name: ``str``

        :param description: A desired description for the stack.
        :type description: ``str``

        :param docker_compose: The Docker Compose configuration to use.
        :type docker_compose: ``str``

        :param environment: Environment K/V specific to this stack.
        :type environment: ``dict``

        :param external_id: The externalId of the stack.
        :type external_id: ``str``

        :param rancher_compose: The Rancher Compose configuration for this env.
        :type rancher_compose: ``str``

        :param start: Whether to start this stack on creation.
        :type start: ``bool``

        :return: The newly created stack.
        :rtype: ``dict``
        """

        payload = {
            "description": description,
            "dockerCompose": docker_compose,
            "environment": environment,
            "externalId": external_id,
            "name": name,
            "rancherCompose": rancher_compose,
            "startOnCreate": start
        }
        data = json.dumps(dict((k, v) for (k, v) in payload.items()
                               if v is not None))
        result = self.connection.request('%s/environments' %
                                         self.baseuri, data=data,
                                         method='POST').object

        return result

    def ex_get_stack(self, env_id):
        """
        Get a stack by ID

        :param env_id: The stack to be obtained.
        :type env_id: ``str``

        :rtype: ``dict``
        """
        result = self.connection.request("%s/environments/%s" %
                                         (self.baseuri, env_id)).object

        return result

    def ex_search_stacks(self, search_params):
        """
        Search for stacks matching certain filters

        i.e. ``{ "name": "awesomestack"}``

        :param search_params: A collection of search parameters to use.
        :type search_params: ``dict``

        :rtype: ``list``
        """
        search_list = []
        for f, v in search_params.items():
            search_list.append(f + '=' + v)
        search_items = '&'.join(search_list)
        result = self.connection.request("%s/environments?%s" % (
            self.baseuri, search_items)).object

        return result['data']

    def ex_destroy_stack(self, env_id):
        """
        Destroy a stack by ID

        http://docs.rancher.com/rancher/v1.2/en/api/api-resources/environment/#delete

        :param env_id: The stack to be destroyed.
        :type env_id: ``str``

        :return: True if destroy was successful, False otherwise.
        :rtype: ``bool``
        """
        result = self.connection.request('%s/environments/%s' % (
                                         self.baseuri, env_id),
                                         method='DELETE')
        return result.status in VALID_RESPONSE_CODES

    def ex_activate_stack(self, env_id):
        """
        Activate Services for a stack.

        http://docs.rancher.com/rancher/v1.2/en/api/api-resources/environment/#activateservices

        :param env_id: The stack to activate services for.
        :type env_id: ``str``

        :return: True if activate was successful, False otherwise.
        :rtype: ``bool``
        """
        result = self.connection.request(
            '%s/environments/%s?action=activateservices' % (
                self.baseuri, env_id), method='POST'
        )
        return result.status in VALID_RESPONSE_CODES

    def ex_deactivate_stack(self, env_id):
        """
        Deactivate Services for a stack.

        http://docs.rancher.com/rancher/v1.2/en/api/api-resources/environment/#deactivateservices

        :param env_id: The stack to deactivate services for.
        :type env_id: ``str``

        :return: True if deactivate was successful, False otherwise.
        :rtype: ``bool``
        """

        result = self.connection.request(
            '%s/environments/%s?action=deactivateservices' % (
                self.baseuri, env_id), method='POST'
        )
        return result.status in VALID_RESPONSE_CODES

    def ex_list_services(self):
        """
        List all Rancher Services

        http://docs.rancher.com/rancher/v1.2/en/api/api-resources/service/

        :rtype: ``list`` of ``dict``
        """

        result = self.connection.request("%s/services" % self.baseuri).object
        return result['data']

    def ex_deploy_service(self, name, image, environment_id,
                          start=True, assign_service_ip_address=None,
                          service_description=None, external_id=None,
                          metadata=None, retain_ip=None, scale=None,
                          scale_policy=None, secondary_launch_configs=None,
                          selector_container=None, selector_link=None,
                          vip=None, **launch_conf):
        """
        Deploy a Rancher Service under a stack.

        http://docs.rancher.com/rancher/v1.2/en/api/api-resources/service/#create

        *Any further configuration passed applies to the ``launchConfig``*

        :param name: The desired name of the service. (required)
        :type name: ``str``

        :param image: The Image object to deploy. (required)
        :type image: :class:`libcloud.container.base.ContainerImage`

        :param environment_id: The stack ID this service is tied to. (required)
        :type environment_id: ``str``

        :param start: Whether to start the service on creation.
        :type start: ``bool``

        :param assign_service_ip_address: The IP address to assign the service.
        :type assign_service_ip_address: ``bool``

        :param service_description: The service description.
        :type service_description: ``str``

        :param external_id: The externalId for this service.
        :type external_id: ``str``

        :param metadata: K/V Metadata for this service.
        :type metadata: ``dict``

        :param retain_ip: Whether this service should retain its IP.
        :type retain_ip: ``bool``

        :param scale: The scale of containers in this service.
        :type scale: ``int``

        :param scale_policy: The scaling policy for this service.
        :type scale_policy: ``dict``

        :param secondary_launch_configs: Secondary container launch configs.
        :type secondary_launch_configs: ``list``

        :param selector_container: The selectorContainer for this service.
        :type selector_container: ``str``

        :param selector_link: The selectorLink for this service.
        :type selector_link: ``type``

        :param vip: The VIP to assign to this service.
        :type vip: ``str``

        :return: The newly created service.
        :rtype: ``dict``
        """

        launch_conf['imageUuid'] = self._degen_image(image),

        service_payload = {
            "assignServiceIpAddress": assign_service_ip_address,
            "description": service_description,
            "environmentId": environment_id,
            "externalId": external_id,
            "launchConfig": launch_conf,
            "metadata": metadata,
            "name": name,
            "retainIp": retain_ip,
            "scale": scale,
            "scalePolicy": scale_policy,
            "secondary_launch_configs": secondary_launch_configs,
            "selectorContainer": selector_container,
            "selectorLink": selector_link,
            "startOnCreate": start,
            "vip": vip
        }

        data = json.dumps(dict((k, v) for (k, v) in service_payload.items()
                               if v is not None))
        result = self.connection.request('%s/services' % self.baseuri,
                                         data=data, method='POST').object

        return result

    def ex_get_service(self, service_id):
        """
        Get a service by ID

        :param service_id: The service_id to be obtained.
        :type service_id: ``str``

        :rtype: ``dict``
        """
        result = self.connection.request("%s/services/%s" %
                                         (self.baseuri, service_id)).object

        return result

    def ex_search_services(self, search_params):
        """
        Search for services matching certain filters

        i.e. ``{ "name": "awesomesause", "environmentId": "1e2"}``

        :param search_params: A collection of search parameters to use.
        :type search_params: ``dict``

        :rtype: ``list``
        """
        search_list = []
        for f, v in search_params.items():
            search_list.append(f + '=' + v)
        search_items = '&'.join(search_list)
        result = self.connection.request("%s/services?%s" % (
            self.baseuri, search_items)).object

        return result['data']

    def ex_destroy_service(self, service_id):
        """
        Destroy a service by ID

        http://docs.rancher.com/rancher/v1.2/en/api/api-resources/service/#delete

        :param service_id: The service to be destroyed.
        :type service_id: ``str``

        :return: True if destroy was successful, False otherwise.
        :rtype: ``bool``
        """
        result = self.connection.request('%s/services/%s' % (self.baseuri,
                                         service_id), method='DELETE')
        return result.status in VALID_RESPONSE_CODES

    def ex_activate_service(self, service_id):
        """
        Activate a service.

        http://docs.rancher.com/rancher/v1.2/en/api/api-resources/service/#activate

        :param service_id: The service to activate services for.
        :type service_id: ``str``

        :return: True if activate was successful, False otherwise.
        :rtype: ``bool``
        """
        result = self.connection.request('%s/services/%s?action=activate' %
                                         (self.baseuri, service_id),
                                         method='POST')
        return result.status in VALID_RESPONSE_CODES

    def ex_deactivate_service(self, service_id):
        """
        Deactivate a service.

        http://docs.rancher.com/rancher/v1.2/en/api/api-resources/service/#deactivate

        :param service_id: The service to deactivate services for.
        :type service_id: ``str``

        :return: True if deactivate was successful, False otherwise.
        :rtype: ``bool``
        """
        result = self.connection.request('%s/services/%s?action=deactivate' %
                                         (self.baseuri, service_id),
                                         method='POST')
        return result.status in VALID_RESPONSE_CODES

    def list_containers(self):
        """
        List the deployed containers.

        http://docs.rancher.com/rancher/v1.2/en/api/api-resources/container/

        :rtype: ``list`` of :class:`libcloud.container.base.Container`
        """

        result = self.connection.request("%s/containers" % self.baseuri).object
        containers = [self._to_container(value) for value in result['data']]
        return containers

    def deploy_container(self, name, image, parameters=None, start=True,
                         **config):
        """
        Deploy a new container.

        http://docs.rancher.com/rancher/v1.2/en/api/api-resources/container/#create

        **The following is the Image format used for ``ContainerImage``**

        *For a ``imageuuid``*:

        - ``docker:<hostname>:<port>/<namespace>/<imagename>:<version>``

        *The following applies*:

        - ``id`` = ``<imagename>``
        - ``name`` = ``<imagename>``
        - ``path`` = ``<hostname>:<port>/<namespace>/<imagename>``
        - ``version`` = ``<version>``

        *Any extra configuration can also be passed i.e. "environment"*

        :param name: The desired name of the container. (required)
        :type name: ``str``

        :param image: The Image object to deploy. (required)
        :type image: :class:`libcloud.container.base.ContainerImage`

        :param parameters: Container Image parameters (unused)
        :type  parameters: ``str``

        :param start: Whether to start the container on creation(startOnCreate)
        :type start: ``bool``

        :rtype: :class:`Container`
        """

        payload = {
            "name": name,
            "imageUuid": self._degen_image(image),
            "startOnCreate": start,
        }
        config.update(payload)

        data = json.dumps(config)

        result = self.connection.request('%s/containers' % self.baseuri,
                                         data=data, method='POST').object

        return self._to_container(result)

    def get_container(self, con_id):
        """
        Get a container by ID

        :param con_id: The ID of the container to get
        :type  con_id: ``str``

        :rtype: :class:`libcloud.container.base.Container`
        """
        result = self.connection.request("%s/containers/%s" %
                                         (self.baseuri, con_id)).object

        return self._to_container(result)

    def start_container(self, container):
        """
        Start a container

        :param container: The container to be started
        :type  container: :class:`libcloud.container.base.Container`

        :return: The container refreshed with current data
        :rtype: :class:`libcloud.container.base.Container`
        """
        result = self.connection.request('%s/containers/%s?action=start' %
                                         (self.baseuri, container.id),
                                         method='POST').object

        return self._to_container(result)

    def stop_container(self, container):
        """
        Stop a container

        :param container: The container to be stopped
        :type  container: :class:`libcloud.container.base.Container`

        :return: The container refreshed with current data
        :rtype: :class:`libcloud.container.base.Container`
        """
        result = self.connection.request('%s/containers/%s?action=stop' %
                                         (self.baseuri, container.id),
                                         method='POST').object

        return self._to_container(result)

    def ex_search_containers(self, search_params):
        """
        Search for containers matching certain filters

        i.e. ``{ "imageUuid": "docker:mysql", "state": "running"}``

        :param search_params: A collection of search parameters to use.
        :type search_params: ``dict``

        :rtype: ``list``
        """
        search_list = []
        for f, v in search_params.items():
            search_list.append(f + '=' + v)
        search_items = '&'.join(search_list)
        result = self.connection.request("%s/containers?%s" % (
            self.baseuri, search_items)).object

        return result['data']

    def destroy_container(self, container):
        """
        Remove a container

        :param container: The container to be destroyed
        :type  container: :class:`libcloud.container.base.Container`

        :return: True if the destroy was successful, False otherwise.
        :rtype: ``bool``
        """
        result = self.connection.request('%s/containers/%s' % (self.baseuri,
                                         container.id), method='DELETE').object

        return self._to_container(result)

    def _gen_image(self, imageuuid):
        """
        This function converts a valid Rancher ``imageUuid`` string to a valid
        image object. Only supports docker based images hence `docker:` must
        prefix!!

        Please see the deploy_container() for details on the format.

        :param imageuuid: A valid Rancher image string
            i.e. ``docker:rlister/hastebin:8.0``
        :type imageuuid: ``str``

        :return: Converted ContainerImage object.
        :rtype: :class:`libcloud.container.base.ContainerImage`
        """
        # Obtain just the name(:version) for parsing
        if '/' not in imageuuid:
            # String looks like `docker:mysql:8.0`
            image_name_version = imageuuid.partition(':')[2]
        else:
            # String looks like `docker:oracle/mysql:8.0`
            image_name_version = imageuuid.rpartition("/")[2]
        # Parse based on ':'
        if ':' in image_name_version:
            version = image_name_version.partition(":")[2]
            id = image_name_version.partition(":")[0]
            name = id
        else:
            version = 'latest'
            id = image_name_version
            name = id
        # Get our path based on if there was a version
        if version != 'latest':
            path = imageuuid.partition(':')[2].rpartition(':')[0]
        else:
            path = imageuuid.partition(':')[2]

        return ContainerImage(
            id=id,
            name=name,
            path=path,
            version=version,
            driver=self.connection.driver,
            extra={
                "imageUuid": imageuuid
            }
        )

    def _degen_image(self, image):
        """
        Take in an image object to break down into an ``imageUuid``

        :param image:
        :return:
        """

        # Only supporting docker atm
        image_type = "docker"

        if image.version is not None:
            return image_type + ':' + image.path + ':' + image.version
        else:
            return image_type + ':' + image.path

    def _to_container(self, data):
        """
        Convert container in proper Container instance object
        ** Updating is NOT supported!!

        :param data: API data about container i.e. result.object
        :return: Proper Container object:
         see http://libcloud.readthedocs.io/en/latest/container/api.html

        """
        rancher_state = data['state']

        # A Removed container is purged after x amt of time.
        # Both of these render the container dead (can't be started later)
        terminate_condition = ["removed", "purged"]

        if 'running' in rancher_state:
            state = ContainerState.RUNNING
        elif 'stopped' in rancher_state:
            state = ContainerState.STOPPED
        elif 'restarting' in rancher_state:
            state = ContainerState.REBOOTING
        elif 'error' in rancher_state:
            state = ContainerState.ERROR
        elif any(x in rancher_state for x in terminate_condition):
            state = ContainerState.TERMINATED
        elif data['transitioning'] == 'yes':
            # Best we can do for current actions
            state = ContainerState.PENDING
        else:
            state = ContainerState.UNKNOWN

        # Everything contained in the json response is dumped in extra
        extra = data

        return Container(
            id=data['id'],
            name=data['name'],
            image=self._gen_image(data['imageUuid']),
            ip_addresses=[data['primaryIpAddress']],
            state=state,
            driver=self.connection.driver,
            extra=extra)
