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

from libcloud.utils.py3 import httplib

try:
    import simplejson as json
except ImportError:
    import json

from libcloud.utils.misc import reverse_dict
from libcloud.common.types import LibcloudError
from libcloud.common.gogrid import GoGridConnection, GoGridResponse,\
    BaseGoGridDriver
from libcloud.loadbalancer.base import LoadBalancer, Member, Driver, Algorithm
from libcloud.loadbalancer.base import DEFAULT_ALGORITHM
from libcloud.loadbalancer.types import State, LibcloudLBImmutableError


class GoGridLBResponse(GoGridResponse):
    def success(self):
        if self.status == httplib.INTERNAL_SERVER_ERROR:
            # Hack, but at least this error message is more useful than
            # "unexpected server error"
            body = json.loads(self.body)
            if body['method'] == '/grid/loadbalancer/add' and \
                len(body['list']) >= 1 and \
                body['list'][0]['message'].find(
                    'unexpected server error') != -1:
                raise LibcloudError(
                    value='You mostly likely tried to add a member with an IP'
                          ' address not assigned to your account', driver=self)
        return super(GoGridLBResponse, self).success()


class GoGridLBConnection(GoGridConnection):
    """
    Connection class for the GoGrid load-balancer driver.
    """
    responseCls = GoGridLBResponse


class GoGridLBDriver(BaseGoGridDriver, Driver):
    connectionCls = GoGridLBConnection
    api_name = 'gogrid_lb'
    name = 'GoGrid LB'
    website = 'http://www.gogrid.com/'

    LB_STATE_MAP = {'On': State.RUNNING,
                    'Unknown': State.UNKNOWN}
    _VALUE_TO_ALGORITHM_MAP = {
        'round robin': Algorithm.ROUND_ROBIN,
        'least connect': Algorithm.LEAST_CONNECTIONS
    }
    _ALGORITHM_TO_VALUE_MAP = reverse_dict(_VALUE_TO_ALGORITHM_MAP)

    def __init__(self, *args, **kwargs):
        """
        @inherits: :class:`Driver.__init__`
        """
        super(GoGridLBDriver, self).__init__(*args, **kwargs)

    def list_protocols(self):
        # GoGrid only supports http
        return ['http']

    def list_balancers(self):
        return self._to_balancers(
            self.connection.request('/api/grid/loadbalancer/list').object)

    def ex_create_balancer_nowait(self, name, members, protocol='http',
                                  port=80, algorithm=DEFAULT_ALGORITHM):
        """
        @inherits: :class:`Driver.create_balancer`
        """
        algorithm = self._algorithm_to_value(algorithm)

        params = {'name': name,
                  'loadbalancer.type': algorithm,
                  'virtualip.ip': self._get_first_ip(),
                  'virtualip.port': port}
        params.update(self._members_to_params(members))

        resp = self.connection.request('/api/grid/loadbalancer/add',
                                       method='GET',
                                       params=params)
        return self._to_balancers(resp.object)[0]

    def create_balancer(self, name, members, protocol='http', port=80,
                        algorithm=DEFAULT_ALGORITHM):
        balancer = self.ex_create_balancer_nowait(name, members, protocol,
                                                  port, algorithm)

        timeout = 60 * 20
        waittime = 0
        interval = 2 * 15

        if balancer.id is not None:
            return balancer
        else:
            while waittime < timeout:
                balancers = self.list_balancers()

                for i in balancers:
                    if i.name == balancer.name and i.id is not None:
                        return i

                waittime += interval
                time.sleep(interval)

        raise Exception('Failed to get id')

    def destroy_balancer(self, balancer):
        try:
            resp = self.connection.request(
                '/api/grid/loadbalancer/delete', method='POST',
                params={'id': balancer.id})
        except Exception as e:
            if "Update request for LoadBalancer" in str(e):
                raise LibcloudLBImmutableError(
                    "Cannot delete immutable object", GoGridLBDriver)
            else:
                raise

        return resp.status == 200

    def get_balancer(self, **kwargs):
        params = {}

        try:
            params['name'] = kwargs['ex_balancer_name']
        except KeyError:
            balancer_id = kwargs['balancer_id']
            params['id'] = balancer_id

        resp = self.connection.request('/api/grid/loadbalancer/get',
                                       params=params)

        return self._to_balancers(resp.object)[0]

    def balancer_attach_member(self, balancer, member):
        members = self.balancer_list_members(balancer)
        members.append(member)

        params = {"id": balancer.id}

        params.update(self._members_to_params(members))

        resp = self._update_balancer(params)
        return [m for m in
                self._to_members(resp.object["list"][0]["realiplist"],
                                 balancer)
                if m.ip == member.ip][0]

    def balancer_detach_member(self, balancer, member):
        members = self.balancer_list_members(balancer)

        remaining_members = [n for n in members if n.id != member.id]

        params = {"id": balancer.id}
        params.update(self._members_to_params(remaining_members))

        resp = self._update_balancer(params)

        return resp.status == 200

    def balancer_list_members(self, balancer):
        resp = self.connection.request('/api/grid/loadbalancer/get',
                                       params={'id': balancer.id})
        return self._to_members(resp.object["list"][0]["realiplist"], balancer)

    def _update_balancer(self, params):
        try:
            return self.connection.request('/api/grid/loadbalancer/edit',
                                           method='POST',
                                           params=params)
        except Exception as e:
            if "Update already pending" in str(e):
                raise LibcloudLBImmutableError(
                    "Balancer is immutable", GoGridLBDriver)

            raise LibcloudError(value='Exception: %s' % str(e), driver=self)

    def _members_to_params(self, members):
        """
        Helper method to convert list of :class:`Member` objects
        to GET params.

        """

        params = {}

        i = 0
        for member in members:
            params["realiplist.%s.ip" % i] = member.ip
            params["realiplist.%s.port" % i] = member.port
            i += 1

        return params

    def _to_balancers(self, object):
        return [self._to_balancer(el) for el in object["list"]]

    def _to_balancer(self, el):
        lb = LoadBalancer(id=el.get("id"),
                          name=el["name"],
                          state=self.LB_STATE_MAP.get(
                              el["state"]["name"], State.UNKNOWN),
                          ip=el["virtualip"]["ip"]["ip"],
                          port=el["virtualip"]["port"],
                          driver=self.connection.driver)
        return lb

    def _to_members(self, object, balancer=None):
        return [self._to_member(el, balancer) for el in object]

    def _to_member(self, el, balancer=None):
        member = Member(id=el["ip"]["id"],
                        ip=el["ip"]["ip"],
                        port=el["port"],
                        balancer=balancer)
        return member
