# 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.

from libcloud.common.cloudstack import CloudStackDriverMixIn
from libcloud.loadbalancer.base import LoadBalancer, Member, Driver, Algorithm
from libcloud.loadbalancer.base import DEFAULT_ALGORITHM
from libcloud.loadbalancer.types import Provider
from libcloud.loadbalancer.types import State
from libcloud.utils.misc import reverse_dict


class CloudStackLBDriver(CloudStackDriverMixIn, Driver):
    """Driver for CloudStack load balancers."""

    api_name = 'cloudstack_lb'
    name = 'CloudStack'
    website = 'http://cloudstack.org/'
    type = Provider.CLOUDSTACK

    _VALUE_TO_ALGORITHM_MAP = {
        'roundrobin': Algorithm.ROUND_ROBIN,
        'leastconn': Algorithm.LEAST_CONNECTIONS
    }
    _ALGORITHM_TO_VALUE_MAP = reverse_dict(_VALUE_TO_ALGORITHM_MAP)

    LB_STATE_MAP = {
        'Active': State.RUNNING,
    }

    def __init__(self, key, secret=None, secure=True, host=None,
                 path=None, port=None, *args, **kwargs):
        """
        @inherits: :class:`Driver.__init__`
        """
        host = host if host else self.host
        path = path if path else self.path

        if path is not None:
            self.path = path

        if host is not None:
            self.host = host

        if (self.type == Provider.CLOUDSTACK) and (not host or not path):
            raise Exception('When instantiating CloudStack driver directly ' +
                            'you also need to provide host and path argument')

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

    def list_protocols(self):
        """
        We don't actually have any protocol awareness beyond TCP.

        :rtype: ``list`` of ``str``
        """
        return ['tcp']

    def list_balancers(self):
        balancers = self._sync_request(command='listLoadBalancerRules',
                                       method='GET')
        balancers = balancers.get('loadbalancerrule', [])
        return [self._to_balancer(balancer) for balancer in balancers]

    def get_balancer(self, balancer_id):
        balancer = self._sync_request(command='listLoadBalancerRules',
                                      params={'id': balancer_id},
                                      method='GET')
        balancer = balancer.get('loadbalancerrule', [])
        if not balancer:
            raise Exception("no such load balancer: " + str(balancer_id))
        return self._to_balancer(balancer[0])

    def create_balancer(self, name, members, protocol='http', port=80,
                        algorithm=DEFAULT_ALGORITHM, location=None,
                        private_port=None, network_id=None, vpc_id=None):
        """
        @inherits: :class:`Driver.create_balancer`

        :param location: Location
        :type  location: :class:`NodeLocation`

        :param private_port: Private port
        :type  private_port: ``int``

        :param network_id: The guest network this rule will be created for.
        :type  network_id: ``str``
        """

        args = {}
        ip_args = {}

        if location is None:
            locations = self._sync_request(command='listZones', method='GET')
            location = locations['zone'][0]['id']
        else:
            location = location.id
        if private_port is None:
            private_port = port

        if network_id is not None:
            args['networkid'] = network_id
            ip_args['networkid'] = network_id

        if vpc_id is not None:
            ip_args['vpcid'] = vpc_id

        ip_args.update({'zoneid': location,
                        'networkid': network_id,
                        'vpc_id': vpc_id})

        result = self._async_request(command='associateIpAddress',
                                     params=ip_args,
                                     method='GET')
        public_ip = result['ipaddress']

        args.update({'algorithm': self._ALGORITHM_TO_VALUE_MAP[algorithm],
                     'name': name,
                     'privateport': private_port,
                     'publicport': port,
                     'publicipid': public_ip['id']})

        result = self._sync_request(
            command='createLoadBalancerRule',
            params=args,
            method='GET')

        listbalancers = self._sync_request(
            command='listLoadBalancerRules',
            params=args,
            method='GET')

        listbalancers = [rule for rule in listbalancers['loadbalancerrule'] if
                         rule['id'] == result['id']]
        if len(listbalancers) != 1:
            return None

        balancer = self._to_balancer(listbalancers[0])

        for member in members:
            balancer.attach_member(member)

        return balancer

    def destroy_balancer(self, balancer):
        self._async_request(command='deleteLoadBalancerRule',
                            params={'id': balancer.id},

                            method='GET')
        self._async_request(command='disassociateIpAddress',
                            params={'id': balancer.ex_public_ip_id},
                            method='GET')

    def balancer_attach_member(self, balancer, member):
        member.port = balancer.ex_private_port
        self._async_request(command='assignToLoadBalancerRule',
                            params={'id': balancer.id,
                                    'virtualmachineids': member.id},
                            method='GET')
        return True

    def balancer_detach_member(self, balancer, member):
        self._async_request(command='removeFromLoadBalancerRule',
                            params={'id': balancer.id,
                                    'virtualmachineids': member.id},
                            method='GET')
        return True

    def balancer_list_members(self, balancer):
        members = self._sync_request(command='listLoadBalancerRuleInstances',
                                     params={'id': balancer.id},
                                     method='GET')
        members = members['loadbalancerruleinstance']
        return [self._to_member(m, balancer.ex_private_port, balancer)
                for m in members]

    def _to_balancer(self, obj):
        balancer = LoadBalancer(
            id=obj['id'],
            name=obj['name'],
            state=self.LB_STATE_MAP.get(obj['state'], State.UNKNOWN),
            ip=obj['publicip'],
            port=obj['publicport'],
            driver=self.connection.driver
        )
        balancer.ex_private_port = obj['privateport']
        balancer.ex_public_ip_id = obj['publicipid']
        return balancer

    def _to_member(self, obj, port, balancer):
        return Member(
            id=obj['id'],
            ip=obj['nic'][0]['ipaddress'],
            port=port,
            balancer=balancer
        )
