package ecs

import (
	"context"
	"fmt"

	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/responses"
	"github.com/aliyun/alibaba-cloud-sdk-go/services/ecs"
	"github.com/hashicorp/packer/common/uuid"
	"github.com/hashicorp/packer/helper/multistep"
	"github.com/hashicorp/packer/packer"
)

type stepConfigAlicloudVSwitch struct {
	VSwitchId   string
	ZoneId      string
	isCreate    bool
	CidrBlock   string
	VSwitchName string
}

var createVSwitchRetryErrors = []string{
	"TOKEN_PROCESSING",
}

var deleteVSwitchRetryErrors = []string{
	"IncorrectVSwitchStatus",
	"DependencyViolation",
	"DependencyViolation.HaVip",
	"IncorrectRouteEntryStatus",
	"TaskConflict",
}

func (s *stepConfigAlicloudVSwitch) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
	client := state.Get("client").(*ClientWrapper)
	ui := state.Get("ui").(packer.Ui)
	vpcId := state.Get("vpcid").(string)
	config := state.Get("config").(*Config)

	if len(s.VSwitchId) != 0 {
		describeVSwitchesRequest := ecs.CreateDescribeVSwitchesRequest()
		describeVSwitchesRequest.VpcId = vpcId
		describeVSwitchesRequest.VSwitchId = s.VSwitchId
		describeVSwitchesRequest.ZoneId = s.ZoneId

		vswitchesResponse, err := client.DescribeVSwitches(describeVSwitchesRequest)
		if err != nil {
			return halt(state, err, "Failed querying vswitch")
		}

		vswitch := vswitchesResponse.VSwitches.VSwitch
		if len(vswitch) > 0 {
			state.Put("vswitchid", vswitch[0].VSwitchId)
			s.isCreate = false
			return multistep.ActionContinue
		}

		s.isCreate = false
		return halt(state, fmt.Errorf("The specified vswitch {%s} doesn't exist.", s.VSwitchId), "")
	}

	if s.ZoneId == "" {
		describeZonesRequest := ecs.CreateDescribeZonesRequest()
		describeZonesRequest.RegionId = config.AlicloudRegion

		zonesResponse, err := client.DescribeZones(describeZonesRequest)
		if err != nil {
			return halt(state, err, "Query for available zones failed")
		}

		var instanceTypes []string
		zones := zonesResponse.Zones.Zone
		for _, zone := range zones {
			isVSwitchSupported := false
			for _, resourceType := range zone.AvailableResourceCreation.ResourceTypes {
				if resourceType == "VSwitch" {
					isVSwitchSupported = true
				}
			}

			if isVSwitchSupported {
				for _, instanceType := range zone.AvailableInstanceTypes.InstanceTypes {
					if instanceType == config.InstanceType {
						s.ZoneId = zone.ZoneId
						break
					}
					instanceTypes = append(instanceTypes, instanceType)
				}
			}
		}

		if s.ZoneId == "" {
			if len(instanceTypes) > 0 {
				ui.Say(fmt.Sprintf("The instance type %s isn't available in this region."+
					"\n You can either change the instance to one of following: %v \n"+
					"or choose another region.", config.InstanceType, instanceTypes))

				state.Put("error", fmt.Errorf("The instance type %s isn't available in this region."+
					"\n You can either change the instance to one of following: %v \n"+
					"or choose another region.", config.InstanceType, instanceTypes))
				return multistep.ActionHalt
			} else {
				ui.Say(fmt.Sprintf("The instance type %s isn't available in this region."+
					"\n You can change to other regions.", config.InstanceType))

				state.Put("error", fmt.Errorf("The instance type %s isn't available in this region."+
					"\n You can change to other regions.", config.InstanceType))
				return multistep.ActionHalt
			}
		}
	}

	if config.CidrBlock == "" {
		s.CidrBlock = DefaultCidrBlock //use the default CirdBlock
	}

	ui.Say("Creating vswitch...")

	createVSwitchRequest := s.buildCreateVSwitchRequest(state)
	createVSwitchResponse, err := client.WaitForExpected(&WaitForExpectArgs{
		RequestFunc: func() (responses.AcsResponse, error) {
			return client.CreateVSwitch(createVSwitchRequest)
		},
		EvalFunc: client.EvalCouldRetryResponse(createVSwitchRetryErrors, EvalRetryErrorType),
	})
	if err != nil {
		return halt(state, err, "Error Creating vswitch")
	}

	vSwitchId := createVSwitchResponse.(*ecs.CreateVSwitchResponse).VSwitchId

	describeVSwitchesRequest := ecs.CreateDescribeVSwitchesRequest()
	describeVSwitchesRequest.VpcId = vpcId
	describeVSwitchesRequest.VSwitchId = vSwitchId

	_, err = client.WaitForExpected(&WaitForExpectArgs{
		RequestFunc: func() (responses.AcsResponse, error) {
			return client.DescribeVSwitches(describeVSwitchesRequest)
		},
		EvalFunc: func(response responses.AcsResponse, err error) WaitForExpectEvalResult {
			if err != nil {
				return WaitForExpectToRetry
			}

			vSwitchesResponse := response.(*ecs.DescribeVSwitchesResponse)
			vSwitches := vSwitchesResponse.VSwitches.VSwitch
			if len(vSwitches) > 0 {
				for _, vSwitch := range vSwitches {
					if vSwitch.Status == VSwitchStatusAvailable {
						return WaitForExpectSuccess
					}
				}
			}

			return WaitForExpectToRetry
		},
		RetryTimes: shortRetryTimes,
	})

	if err != nil {
		return halt(state, err, "Timeout waiting for vswitch to become available")
	}

	ui.Message(fmt.Sprintf("Created vswitch: %s", vSwitchId))
	state.Put("vswitchid", vSwitchId)
	s.isCreate = true
	s.VSwitchId = vSwitchId
	return multistep.ActionContinue
}

func (s *stepConfigAlicloudVSwitch) Cleanup(state multistep.StateBag) {
	if !s.isCreate {
		return
	}

	cleanUpMessage(state, "vSwitch")

	client := state.Get("client").(*ClientWrapper)
	ui := state.Get("ui").(packer.Ui)

	_, err := client.WaitForExpected(&WaitForExpectArgs{
		RequestFunc: func() (responses.AcsResponse, error) {
			request := ecs.CreateDeleteVSwitchRequest()
			request.VSwitchId = s.VSwitchId
			return client.DeleteVSwitch(request)
		},
		EvalFunc:   client.EvalCouldRetryResponse(deleteVSwitchRetryErrors, EvalRetryErrorType),
		RetryTimes: shortRetryTimes,
	})

	if err != nil {
		ui.Error(fmt.Sprintf("Error deleting vswitch, it may still be around: %s", err))
	}
}

func (s *stepConfigAlicloudVSwitch) buildCreateVSwitchRequest(state multistep.StateBag) *ecs.CreateVSwitchRequest {
	vpcId := state.Get("vpcid").(string)

	request := ecs.CreateCreateVSwitchRequest()
	request.ClientToken = uuid.TimeOrderedUUID()
	request.CidrBlock = s.CidrBlock
	request.ZoneId = s.ZoneId
	request.VpcId = vpcId
	request.VSwitchName = s.VSwitchName

	return request
}
