# 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.utils.misc import reverse_dict
from libcloud.common.cloudstack import CloudStackDriverMixIn
from libcloud.loadbalancer.base import DEFAULT_ALGORITHM, Driver, Member, Algorithm, LoadBalancer
from libcloud.loadbalancer.types import State, Provider


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().__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)
