package openstack

import (
	"fmt"
	"log"
	"net"

	"github.com/google/uuid"
	"github.com/gophercloud/gophercloud"
	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces"
	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external"
	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
	"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
	"github.com/gophercloud/gophercloud/pagination"
)

// CheckFloatingIP gets a floating IP by its ID and checks if it is already
// associated with any internal interface.
// It returns floating IP if it can be used.
func CheckFloatingIP(client *gophercloud.ServiceClient, id string) (*floatingips.FloatingIP, error) {
	floatingIP, err := floatingips.Get(client, id).Extract()
	if err != nil {
		return nil, err
	}
	if floatingIP.PortID != "" {
		return nil, fmt.Errorf("provided floating IP '%s' is already associated with port '%s'",
			id, floatingIP.PortID)
	}

	return floatingIP, nil
}

// FindFreeFloatingIP returns free unassociated floating IP.
// It will return first floating IP if there are many.
func FindFreeFloatingIP(client *gophercloud.ServiceClient) (*floatingips.FloatingIP, error) {
	var freeFloatingIP *floatingips.FloatingIP

	pager := floatingips.List(client, floatingips.ListOpts{
		Status: "DOWN",
	})
	err := pager.EachPage(func(page pagination.Page) (bool, error) {
		candidates, err := floatingips.ExtractFloatingIPs(page)
		if err != nil {
			return false, err // stop and throw error out
		}

		for _, candidate := range candidates {
			if candidate.PortID != "" {
				continue // this floating IP is associated with port, move to next in list
			}

			// Floating IP is able to be allocated.
			freeFloatingIP = &candidate
			return false, nil // stop iterating over pages
		}
		return true, nil // try the next page
	})
	if err != nil {
		return nil, err
	}
	if freeFloatingIP == nil {
		return nil, fmt.Errorf("no free floating IPs found")
	}

	return freeFloatingIP, nil
}

// GetInstancePortID returns internal port of the instance that can be used for
// the association of a floating IP.
// It will return an ID of a first port if there are many.
func GetInstancePortID(client *gophercloud.ServiceClient, id string, instance_float_net string) (string, error) {

	selected_interface := 0

	interfacesPage, err := attachinterfaces.List(client, id).AllPages()
	if err != nil {
		return "", err
	}
	interfaces, err := attachinterfaces.ExtractInterfaces(interfacesPage)
	if err != nil {
		return "", err
	}
	if len(interfaces) == 0 {
		return "", fmt.Errorf("instance '%s' has no interfaces", id)
	}

	for i := 0; i < len(interfaces); i++ {
		log.Printf("Instance interface: %v: %+v\n", i, interfaces[i])
		if interfaces[i].NetID == instance_float_net {
			log.Printf("Found preferred interface: %v\n", i)
			selected_interface = i
			log.Printf("Using interface value: %v", selected_interface)
		}
	}

	return interfaces[selected_interface].PortID, nil
}

// CheckFloatingIPNetwork checks provided network reference and returns a valid
// Networking service ID.
func CheckFloatingIPNetwork(client *gophercloud.ServiceClient, networkRef string) (string, error) {
	if _, err := uuid.Parse(networkRef); err != nil {
		return GetFloatingIPNetworkIDByName(client, networkRef)
	}

	return networkRef, nil
}

// ExternalNetwork is a network with external router.
type ExternalNetwork struct {
	networks.Network
	external.NetworkExternalExt
}

// GetFloatingIPNetworkIDByName searches for the external network ID by the provided name.
func GetFloatingIPNetworkIDByName(client *gophercloud.ServiceClient, networkName string) (string, error) {
	var externalNetworks []ExternalNetwork

	allPages, err := networks.List(client, networks.ListOpts{
		Name: networkName,
	}).AllPages()
	if err != nil {
		return "", err
	}

	if err := networks.ExtractNetworksInto(allPages, &externalNetworks); err != nil {
		return "", err
	}

	if len(externalNetworks) == 0 {
		return "", fmt.Errorf("can't find external network %s", networkName)
	}
	// Check and return the first external network.
	if !externalNetworks[0].External {
		return "", fmt.Errorf("network %s is not external", networkName)
	}

	return externalNetworks[0].ID, nil
}

// DiscoverProvisioningNetwork finds the first network whose subnet matches the given network ranges.
func DiscoverProvisioningNetwork(client *gophercloud.ServiceClient, cidrs []string) (string, error) {
	allPages, err := subnets.List(client, subnets.ListOpts{}).AllPages()
	if err != nil {
		return "", err
	}

	allSubnets, err := subnets.ExtractSubnets(allPages)
	if err != nil {
		return "", err
	}

	for _, subnet := range allSubnets {
		_, tenantIPNet, err := net.ParseCIDR(subnet.CIDR)
		if err != nil {
			return "", err
		}

		for _, cidr := range cidrs {
			_, candidateIPNet, err := net.ParseCIDR(cidr)
			if err != nil {
				return "", err
			}

			if containsNet(candidateIPNet, tenantIPNet) {
				return subnet.NetworkID, nil
			}
		}
	}

	return "", fmt.Errorf("failed to discover a provisioning network")
}

// containsNet returns true whenever IPNet `a` contains IPNet `b`
func containsNet(a *net.IPNet, b *net.IPNet) bool {
	aMask, _ := a.Mask.Size()
	bMask, _ := b.Mask.Size()
	return a.Contains(b.IP) && aMask <= bMask
}