# Copyright 2015 LIP - INDIGO-DataCloud
#
# 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.

from ooi.api import helpers
from ooi import exception
from ooi.log import log as logging
from ooi.openstack import helpers as os_helpers

LOG = logging.getLogger(__name__)


class OpenStackNeutron(helpers.BaseHelper):
    """Class to interact with the neutron API."""

    def __init__(self, neutron_endpoint):
        super(OpenStackNeutron, self).__init__(None, None)
        self.neutron_endpoint = neutron_endpoint

    @staticmethod
    def _build_link(net_id, compute_id, ip, mac=None, pool=None,
                    state='ACTIVE'):
        link = {}
        link['mac'] = mac
        link['pool'] = pool
        link['network_id'] = net_id
        link['compute_id'] = compute_id
        link['ip'] = ip
        link['state'] = os_helpers.network_status(state)
        return link

    @staticmethod
    def _build_networks(networks):
        ooi_net_list = []
        for net in networks:
            ooi_net = {}
            status = net.get("status", None)
            ooi_net["state"] = os_helpers.network_status(status)
            public = net.get('router:external', None)
            if public:
                ooi_net["id"] = os_helpers.PUBLIC_NETWORK
                # TODO(jorgesece): include info about pools
            else:
                ooi_net["id"] = net["id"]
            ooi_net["name"] = net.get("name", None)
            if "subnet_info" in net:
                sub = net["subnet_info"]
                ooi_net["address"] = sub.get("cidr", None)
                ooi_net["ip_version"] = sub.get("ip_version", None)
                ooi_net["gateway"] = sub.get("gateway_ip", None)
            ooi_net_list.append(ooi_net)
        return ooi_net_list

    def _get_public_network(self, req):
        """Get public network

        This method get public network id

        :param req: the incoming request
        """
        att_public = {"router:external": True}
        net_public = self.list_resources(req,
                                         'networks',
                                         att_public)
        return net_public[0]["id"]

    def list_resources(self, req, resource, parameters=None):
        """List resources.

        It returns json code from the server

        :param req: the incoming request
        :param resource: network resource to manage
        :param parameters: query parameters
        :param tenant: include tenant in the query parameters
        """
        path = "/%s" % resource
        os_req = self._make_get_request(req, path, parameters)
        response = os_req.get_response()
        return self.get_from_response(response, resource, [])

    def get_resource(self, req, resource, id):
        """Get information from a resource.

        :param req: the incoming request
        :param resource: network resource to manage
        :param id: subnet identification
        """
        path = "/%s/%s" % (resource, id)
        req = self._make_get_request(req, path)
        response = req.get_response()
        single_resource = resource[:-1]
        return self.get_from_response(response, single_resource, {})

    def create_resource(self, req, resource, parameters):
        """Create a resource.

        :param req: the incoming request
        :param resource: network resource to manage
        :param parameters: parameters with values for the new network
        """
        single_resource = resource[:-1]
        req_subnet = self._make_create_request(req, resource, parameters)
        response_subnet = req_subnet.get_response()
        json_response = self.get_from_response(
            response_subnet, single_resource, {})
        return json_response

    def delete_resource(self, req, resource, id):
        """Delete resource. It returns empty array

        :param req: the incoming request
        :param parameters: conain id
        """
        path = "/%s" % resource
        req = self._make_delete_request(req, path, id)
        response = req.get_response()
        return self.get_from_response(response, None, [])

    def _add_router_interface(self, req, router_id, subnet_id):
        """Add interface.

        :param req: the incoming request
        :param router_id: router identification
        :param subnet_id: router identification
        """
        path = "/routers/%s/add_router_interface" % router_id
        parameters = {'subnet_id': subnet_id}
        os_req = self._make_put_request(req, path, parameters)
        response = os_req.get_response()
        json_response = self.get_from_response(
            response, None, {})
        return json_response

    def _remove_router_interface(self, req, router_id, port_id):
        """Remove interface.

        :param req: the incoming request
        :param router_id: router identification
        :param subnet_id: router identification
        """
        path = "/routers/%s/remove_router_interface" % router_id
        parameters = {'port_id': port_id}
        os_req = self._make_put_request(req, path, parameters)
        response = os_req.get_response()
        json_response = self.get_from_response(
            response, None, {})
        return json_response

    def create_port(self, req, network_id, device_id):
        """Add a port to the subnet

        Returns the port information

        :param req: the incoming network
        :param network_id: network id
        :param device_id: device id
        """
        attributes_port = {
            'device_owner': 'compute:nova',
            'network_id': network_id,
            'device_id': device_id
        }
        p = self.create_resource(req,
                                 'ports',
                                 attributes_port)
        link = self._build_link(
            p["network_id"],
            p['device_id'],
            p["fixed_ips"][0]["ip_address"],
            mac=p["mac_address"],
            state=p["status"])
        return link

    def delete_port(self, req, mac):
        """Delete a port to the subnet

        Returns the port information

        :param req: the incoming network
        :param mac: link mac
        """
        attributes_port = {
            "mac_address": mac
        }
        ports = self.list_resources(
            req,
            'ports', attributes_port
        )
        if ports.__len__() == 0:
            raise exception.LinkNotFound(
                "Interface %s not found" % mac
            )
        out = self.delete_resource(req,
                                   'ports',
                                   ports[0]['id'])
        return out

    def get_network_id(self, req, mac, server_id=None):
        """Get the Network ID from the mac port

        :param req: the incoming network
        :param mac: mac port
        :param server_id: id not use in neutron
        """
        try:
            attributes_port = {
                "mac_address": mac
            }
            ports = self.list_resources(
                req,
                'ports', attributes_port
            )
            id = ports[0]['network_id']
        except Exception:
            raise exception.NetworkNotFound
        return id

    def _add_floating_ip(self, req, public_net_id, port_id):
        """Add floating to the public network and a port

        Creates the floating IP and asign it to the port
        of the device.

        :param req: the incoming network
        :param public_net_id: public network id
        :param port_id: port id of the device
        """
        attributes_port = {
            "floating_network_id": public_net_id,
            "port_id": port_id
        }
        floating_ip = self.create_resource(req,
                                           'floatingips',
                                           attributes_port)
        return floating_ip

    def _remove_floating_ip(self, req, public_net_id, ip):
        """Delete floating to the public network and a port

        Delete the floating IP and asign it to the port
        of the device.

        :param req: the incoming network
        :param public_net_id: network id
        :param ip: floating ip to remove
        """
        attributes_port = {
            "floating_network_id": public_net_id,
            "floating_ip_address": ip
        }
        try:
            floating_ip = self.list_resources(req,
                                              'floatingips',
                                              attributes_port)
            response = self.delete_resource(req,
                                            'floatingips',
                                            floating_ip[0]['id'])
        except Exception:
            raise exception.NotFound
        return response

    def get_network_details(self, req, id):
        """Get info from a network.

        It returns json code from the server

        :param req: the incoming network
        :param id: net identification
        """
        if id == os_helpers.PUBLIC_NETWORK:
            id = self._get_public_network(req)
        path = "/networks/%s" % id
        req = self._make_get_request(req, path)
        response = req.get_response()
        net = self.get_from_response(response, "network", {})
        # subnet
        if "subnets" in net:
            path = "/subnets/%s" % net["subnets"][0]
            req_subnet = self._make_get_request(req, path)
            response_subnet = req_subnet.get_response()
            net["subnet_info"] = self.get_from_response(
                response_subnet, "subnet", {})

        ooi_networks = self._build_networks([net])

        return ooi_networks[0]

    def list_networks(self, req):
        """List networks.

        It returns json code from the server

        :param req: the incoming request
        :param parameters: query parameters
        """
        networks = self.list_resources(req,
                                       'networks'
                                       )
        ooi_networks = self._build_networks(networks)
        return ooi_networks

    def create_network(self, req, name, cidr,
                       gateway=None, ip_version=None):
        """Create a full neutron network.

        It creates a private network conected to the public one.
        It creates a full network objects stack:
        network, subnet, port, and router.
        In case of error, the objects already created are deleted.

        :param req: the incoming request
        :param name: network resource to manage
        :param cidr: parameters with values
        :param gateway: gateway ip
        :param ip_version: ip version
        """
        # NETWORK
        net_param = {'name': name}
        net = self.create_resource(req,
                                   'networks',
                                   net_param)
        # SUBNETWORK
        try:
            if not ip_version:
                ip_version = 4
            subnet_param = {'network_id': net["id"],
                            'cidr': cidr,
                            'ip_version': ip_version
                            }
            if gateway:
                subnet_param.update(
                    {'gateway_ip': gateway}
                )

            if "ip_version" not in subnet_param:
                subnet_param['ip_version'] = 4
            net["subnet_info"] = self.create_resource(
                req, 'subnets', subnet_param)

        # INTERFACE and ROUTER information is agnostic to the user
            router = None
            router_list = self.list_resources(req, "routers")
            for r in router_list:
                if r["external_gateway_info"]:
                    router = r
                    break
            if not router:
                net_public = self._get_public_network(req)
                attributes_router = {"external_gateway_info": {
                    "network_id": net_public}
                }
                router = self.create_resource(req,
                                              'routers',
                                              attributes_router)

            # create interface to the network
            self._add_router_interface(req,
                                       router['id'],
                                       net['subnet_info']['id']
                                       )
        except Exception as ex:
            self.delete_resource(req,
                                 'networks', net['id'])
            raise ex
        ooi_net = self._build_networks([net])
        return ooi_net[0]

    def delete_network(self, req, id):
        """Delete a full network.

        :param req: the incoming request
        :param id: net identification
        """
        param = {"network_id": id}
        ports = self.list_resources(req, 'ports', param)
        for port in ports:
            if port['device_owner'] == "network:router_interface":
                self._remove_router_interface(req,
                                              port['device_id'],
                                              port['id'],
                                              )
            else:
                self.delete_resource(req,
                                     'ports', port["id"])
        response = self.delete_resource(req,
                                        'networks',
                                        id)
        return response

    def assign_floating_ip(self, req, device_id):
        """assign floating ip to a server

        :param req: the incoming request
        :param device_id: device id
        """
        # net_id it is not needed if
        # there is just one port of the VM
        attributes_port = {'device_id': device_id}
        try:
            net_public = self._get_public_network(req)
        except Exception:
            raise exception.NetworkNotFound()
        try:
            ports = self.list_resources(req, 'ports', attributes_port)
            port_id = ports[0]['id']
            # subnet_id = ports[0]['fixed_ips'][0]['subnet_id']
        except Exception:
            raise exception.NotFound()
        response = self._add_floating_ip(req, net_public, port_id)
        try:
            link_public = self._build_link(
                ports[0]['network_id'],
                attributes_port['device_id'],
                response['floating_ip_address'],
                pool=response['floating_network_id'])
        except Exception:
            raise exception.OCCIInvalidSchema()
        return link_public

    def release_floating_ip(self, req, iface):
        """release floating ip from a server

        :param req: the incoming request
        :param iface: link information
        """
        # net_id it is not needed if there is just one port of the VM
        try:
            net_public = self._get_public_network(req)
        except Exception:
            raise exception.NetworkNotFound()
        response = self._remove_floating_ip(req, net_public, iface['ip'])

        return response

    def list_compute_net_links(self, req, network_id,
                               device_id):
        """List the network and compute links

        It lists every private and public ip related to
        the servers of the tenant

        :param req: the incoming request
        :param network_id: id network
        :param device_id: id device
        """

        param_port = {'device_owner': 'compute:nova',
                      'device_id': device_id,
                      'network_id': network_id
                      }
        link_list = []
        try:
            ports = self.list_resources(req, 'ports', param_port)
            for port in ports:
                link_private = self._build_link(
                    port["network_id"],
                    port['device_id'],
                    port["fixed_ips"][0]["ip_address"],
                    mac=port["mac_address"],
                    state=os_helpers.network_status(port["status"]))
                link_list.append(link_private)
                # Query public links associated to the port
                floating_ips = self.list_resources(req,
                                                   'floatingips',
                                                   {"port_id": port['id']})
                for f_ip in floating_ips:
                    link_public = self._build_link(
                        port["network_id"],
                        port['device_id'],
                        f_ip['floating_ip_address'],
                        mac=port["mac_address"],
                        pool=f_ip['floating_network_id'])

                    link_list.append(link_public)
        except Exception:
            raise exception.NotFound()
        return link_list

    def get_compute_net_link(self, req, compute_id, network_id,
                             ip):
        """Get a specific network/server link

        It shows a specific link (either private or public ip)

        :param req: the incoming request
        :param compute_id: server id
        :param network_id: network id
        :param ip: ip connected
        """
        try:
            if network_id == os_helpers.PUBLIC_NETWORK:
                param = {'floating_ip_address': ip}
                flo_ips = self.list_resources(req,
                                              'floatingips',
                                              param)
                for f_ip in flo_ips:
                    link_public = self._build_link(
                        network_id,
                        compute_id,
                        f_ip['floating_ip_address'],
                        pool=f_ip['floating_network_id'])
                    return link_public
            else:
                param_ports = {'device_id': compute_id,
                               'network_id': network_id}
                ports = self.list_resources(req, 'ports', param_ports)
                for p in ports:
                    if ip == p["fixed_ips"][0]["ip_address"]:
                        link_private = self._build_link(
                            p["network_id"],
                            p['device_id'],
                            p["fixed_ips"][0]["ip_address"],
                            mac=p["mac_address"],
                            state=p["status"])
                        return link_private
            raise exception.NotFound()
        except Exception:
            raise exception.NotFound()

    def run_action(self, req, action, net_id):
        """Run an action on a network.

        :param req: the incoming request
        :param action: the action to run
        :param net_id: server id to delete
        """
        os_req = self._make_action_reques(req, action, id)
        response = os_req.get_response()
        if response.status_int != 202:
            raise helpers.exception_from_response(response)

    def get_security_group_details(self, req, sec_id):
        """Get info about a security group.

        :param req: the incoming request
        :param sec_id: security group id to show
        """
        try:
            secgroup = self.get_resource(req, 'security-groups', sec_id,
                                         response_resource="security_group")
            ooi_sec = os_helpers.build_security_group_from_neutron(
                [secgroup]
            )
            return ooi_sec[0]
        except Exception:
            raise exception.NotFound()

    def list_security_groups(self, req):
        """List security groups

        :param req: the incoming request
        """
        try:
            secgroup = self.list_resources(req, 'security-groups',
                                           response_resource="security_groups")
            ooi_sec = os_helpers.build_security_group_from_neutron(
                secgroup
            )
            return ooi_sec
        except Exception:
            raise exception.NotFound()

    def create_security_group(self, req, name, description, rules):
        """Create security group

        :param req: the incoming request
        :param name: security group name
        :param description: security group description
        :param rules: security group rules
        """
        try:
            tenant_id = self.tenant_from_req(req)
            param_group = {"tenant_id": tenant_id,
                           "description": description,
                           "name": name,
                           }
            secgroup = self.create_resource(
                req, 'security-groups', param_group,
                response_resource="security_group")
            sec_id = secgroup["id"]
            secgroup["security_group_rules"] = []
            for rule in rules:
                port_min, port_max = os_helpers.security_group_rule_port(
                    rule["port"]
                )
                param_rule = {
                    "ethertype": rule.get("ipversion", "IPv4"),
                    "port_range_max": port_max,
                    "port_range_min": port_min,
                    "direction": os_helpers.security_group_rule_type(
                        rule["type"]),
                    "remote_ip_prefix": rule.get("range", "0.0.0.0/0"),
                    "protocol": rule["protocol"],
                    "security_group_id": sec_id,
                }
                secrule = self.create_resource(
                    req,
                    'security-group-rules', param_rule,
                    response_resource="security_group_rule")
                secgroup["security_group_rules"].append(secrule)
            ooi_sec = os_helpers.build_security_group_from_neutron(
                [secgroup]
            )
            return ooi_sec[0]
        except Exception as ex:
            raise ex

    def delete_security_group(self, req, sec_id):
        """Delete info about a security group.

        :param req: the incoming request
        :param sec_id: security group id to delete
        """
        try:
            secgroup = self.delete_resource(req, 'security-groups', sec_id)
            return secgroup
        except Exception:
            raise exception.NotFound()