package openstack

import (
	"context"
	"fmt"

	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
	"github.com/hashicorp/packer/helper/multistep"
	"github.com/hashicorp/packer/packer"
)

type StepAllocateIp struct {
	FloatingIPNetwork string
	FloatingIP        string
	ReuseIPs          bool
}

func (s *StepAllocateIp) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
	ui := state.Get("ui").(packer.Ui)
	config := state.Get("config").(*Config)
	server := state.Get("server").(*servers.Server)

	var instanceIP floatingips.FloatingIP

	// This is here in case we error out before putting instanceIp into the
	// statebag below, because it is requested by Cleanup()
	state.Put("access_ip", &instanceIP)

	if s.FloatingIP == "" && !s.ReuseIPs && s.FloatingIPNetwork == "" {
		ui.Message("Floating IP not required")
		return multistep.ActionContinue
	}

	// We need the v2 compute client
	computeClient, err := config.computeV2Client()
	if err != nil {
		err = fmt.Errorf("Error initializing compute client: %s", err)
		state.Put("error", err)
		return multistep.ActionHalt
	}

	// We need the v2 network client
	networkClient, err := config.networkV2Client()
	if err != nil {
		err = fmt.Errorf("Error initializing network client: %s", err)
		state.Put("error", err)
		return multistep.ActionHalt
	}

	// Try to Use the OpenStack floating IP by checking provided parameters in
	// the following order:
	//  - try to use "FloatingIP" ID directly if it's provided
	//  - try to find free floating IP in the project if "ReuseIPs" is set
	//  - create a new floating IP if "FloatingIPNetwork" is provided (it can be
	//    ID or name of the network).
	if s.FloatingIP != "" {
		// Try to use FloatingIP if it was provided by the user.
		freeFloatingIP, err := CheckFloatingIP(networkClient, s.FloatingIP)
		if err != nil {
			err := fmt.Errorf("Error using provided floating IP '%s': %s", s.FloatingIP, err)
			state.Put("error", err)
			ui.Error(err.Error())
			return multistep.ActionHalt
		}

		instanceIP = *freeFloatingIP
		ui.Message(fmt.Sprintf("Selected floating IP: '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
		state.Put("floatingip_istemp", false)
	} else if s.ReuseIPs {
		// If ReuseIPs is set to true and we have a free floating IP, use it rather
		// than creating one.
		ui.Say(fmt.Sprint("Searching for unassociated floating IP"))
		freeFloatingIP, err := FindFreeFloatingIP(networkClient)
		if err != nil {
			err := fmt.Errorf("Error searching for floating IP: %s", err)
			state.Put("error", err)
			ui.Error(err.Error())
			return multistep.ActionHalt
		}

		instanceIP = *freeFloatingIP
		ui.Message(fmt.Sprintf("Selected floating IP: '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
		state.Put("floatingip_istemp", false)
	} else if s.FloatingIPNetwork != "" {
		// Lastly, if FloatingIPNetwork was provided by the user, we need to use it
		// to allocate a new floating IP and associate it to the instance.
		floatingNetwork, err := CheckFloatingIPNetwork(networkClient, s.FloatingIPNetwork)
		if err != nil {
			err := fmt.Errorf("Error using the provided floating_ip_network: %s", err)
			state.Put("error", err)
			ui.Error(err.Error())
			return multistep.ActionHalt
		}

		ui.Say(fmt.Sprintf("Creating floating IP using network %s ...", floatingNetwork))
		newIP, err := floatingips.Create(networkClient, floatingips.CreateOpts{
			FloatingNetworkID: floatingNetwork,
		}).Extract()
		if err != nil {
			err := fmt.Errorf("Error creating floating IP from floating network '%s': %s", floatingNetwork, err)
			state.Put("error", err)
			ui.Error(err.Error())
			return multistep.ActionHalt
		}

		instanceIP = *newIP
		ui.Message(fmt.Sprintf("Created floating IP: '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
		state.Put("floatingip_istemp", true)
	}

	// Assoctate a floating IP if it was obtained in the previous steps.
	if instanceIP.ID != "" {
		ui.Say(fmt.Sprintf("Associating floating IP '%s' (%s) with instance port...",
			instanceIP.ID, instanceIP.FloatingIP))

		portID, err := GetInstancePortID(computeClient, server.ID)
		if err != nil {
			err := fmt.Errorf("Error getting interfaces of the instance '%s': %s", server.ID, err)
			state.Put("error", err)
			ui.Error(err.Error())
			return multistep.ActionHalt
		}

		_, err = floatingips.Update(networkClient, instanceIP.ID, floatingips.UpdateOpts{
			PortID: &portID,
		}).Extract()
		if err != nil {
			err := fmt.Errorf(
				"Error associating floating IP '%s' (%s) with instance port '%s': %s",
				instanceIP.ID, instanceIP.FloatingIP, portID, err)
			state.Put("error", err)
			ui.Error(err.Error())
			return multistep.ActionHalt
		}

		ui.Message(fmt.Sprintf(
			"Added floating IP '%s' (%s) to instance!", instanceIP.ID, instanceIP.FloatingIP))
	}

	state.Put("access_ip", &instanceIP)
	return multistep.ActionContinue
}

func (s *StepAllocateIp) Cleanup(state multistep.StateBag) {
	config := state.Get("config").(*Config)
	ui := state.Get("ui").(packer.Ui)
	instanceIP := state.Get("access_ip").(*floatingips.FloatingIP)

	// Don't clean up if unless required
	if instanceIP.ID == "" && instanceIP.FloatingIP == "" {
		return
	}

	// Don't delete pool addresses we didn't allocate
	if state.Get("floatingip_istemp") == false {
		return
	}

	// We need the v2 network client
	client, err := config.networkV2Client()
	if err != nil {
		ui.Error(fmt.Sprintf(
			"Error deleting temporary floating IP '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
		return
	}

	if instanceIP.ID != "" {
		if err := floatingips.Delete(client, instanceIP.ID).ExtractErr(); err != nil {
			ui.Error(fmt.Sprintf(
				"Error deleting temporary floating IP '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
			return
		}

		ui.Say(fmt.Sprintf("Deleted temporary floating IP '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
	}
}
