/*
Copyright 2017 The Kubernetes Authors.

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.
*/

package openstacktasks

import (
	"context"
	"fmt"
	"time"

	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports"

	"github.com/gophercloud/gophercloud/v2"
	"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers"
	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/klog/v2"
	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
)

// +kops:fitask
type LB struct {
	ID            *string
	Name          *string
	Subnet        *string
	VipSubnet     *string
	Lifecycle     fi.Lifecycle
	PortID        *string
	SecurityGroup *SecurityGroup
	Provider      *string
	FlavorID      *string
}

const (
	// loadbalancerActive* is configuration of exponential backoff for
	// going into ACTIVE loadbalancer provisioning status. Starting with 1
	// seconds, multiplying by 1.2 with each step and taking 22 steps at maximum
	// it will time out after 326s, which roughly corresponds to about 5 minutes
	loadbalancerActiveInitDelay = 1 * time.Second
	loadbalancerActiveFactor    = 1.2
	loadbalancerActiveSteps     = 22

	activeStatus = "ACTIVE"
	errorStatus  = "ERROR"
)

func waitLoadbalancerActiveProvisioningStatus(client *gophercloud.ServiceClient, loadbalancerID string) (string, error) {
	backoff := wait.Backoff{
		Duration: loadbalancerActiveInitDelay,
		Factor:   loadbalancerActiveFactor,
		Steps:    loadbalancerActiveSteps,
	}

	var provisioningStatus string
	err := wait.ExponentialBackoff(backoff, func() (bool, error) {
		loadbalancer, err := loadbalancers.Get(context.TODO(), client, loadbalancerID).Extract()
		if err != nil {
			return false, err
		}
		provisioningStatus = loadbalancer.ProvisioningStatus
		switch loadbalancer.ProvisioningStatus {
		case activeStatus:
			return true, nil
		case errorStatus:
			return true, fmt.Errorf("loadbalancer has gone into ERROR state")
		default:
			klog.Infof("Waiting for Loadbalancer to be ACTIVE...")
			return false, nil
		}
	})

	if err == wait.ErrWaitTimeout {
		err = fmt.Errorf("loadbalancer failed to go into ACTIVE provisioning status within allotted time")
	}
	return provisioningStatus, err
}

// GetDependencies returns the dependencies of the Instance task
func (e *LB) GetDependencies(tasks map[string]fi.CloudupTask) []fi.CloudupTask {
	var deps []fi.CloudupTask
	for _, task := range tasks {
		if _, ok := task.(*Subnet); ok {
			deps = append(deps, task)
		}
		if _, ok := task.(*SecurityGroup); ok {
			deps = append(deps, task)
		}
	}
	return deps
}

var _ fi.CompareWithID = &LB{}

func (s *LB) CompareWithID() *string {
	return s.ID
}

func NewLBTaskFromCloud(cloud openstack.OpenstackCloud, lifecycle fi.Lifecycle, lb *loadbalancers.LoadBalancer, find *LB) (*LB, error) {
	osCloud := cloud
	sub, err := subnets.Get(context.TODO(), osCloud.NetworkingClient(), lb.VipSubnetID).Extract()
	if err != nil {
		return nil, err
	}

	secGroup := find == nil || find.SecurityGroup != nil
	actual := &LB{
		ID:        fi.PtrTo(lb.ID),
		Name:      fi.PtrTo(lb.Name),
		Lifecycle: lifecycle,
		PortID:    fi.PtrTo(lb.VipPortID),
		Subnet:    fi.PtrTo(sub.Name),
		VipSubnet: fi.PtrTo(lb.VipSubnetID),
		Provider:  fi.PtrTo(lb.Provider),
		FlavorID:  fi.PtrTo(lb.FlavorID),
	}

	if secGroup {
		sg, err := getSecurityGroupByName(&SecurityGroup{Name: fi.PtrTo(lb.Name)}, osCloud)
		if err != nil {
			return nil, err
		}
		actual.SecurityGroup = sg
	}
	if find != nil {
		find.ID = actual.ID
		find.PortID = actual.PortID
		find.VipSubnet = actual.VipSubnet
		find.Provider = actual.Provider
		find.FlavorID = actual.FlavorID
	}
	return actual, nil
}

func (s *LB) Find(context *fi.CloudupContext) (*LB, error) {
	if s.Name == nil {
		return nil, nil
	}

	cloud := context.T.Cloud.(openstack.OpenstackCloud)
	lbPage, err := loadbalancers.List(cloud.LoadBalancerClient(), loadbalancers.ListOpts{
		Name: fi.ValueOf(s.Name),
	}).AllPages(context.Context())
	if err != nil {
		return nil, fmt.Errorf("Failed to retrieve loadbalancers for name %s: %v", fi.ValueOf(s.Name), err)
	}
	lbs, err := loadbalancers.ExtractLoadBalancers(lbPage)
	if err != nil {
		return nil, fmt.Errorf("Failed to extract loadbalancers : %v", err)
	}
	if len(lbs) == 0 {
		return nil, nil
	}
	if len(lbs) > 1 {
		return nil, fmt.Errorf("Multiple load balancers for name %s", fi.ValueOf(s.Name))
	}

	return NewLBTaskFromCloud(cloud, s.Lifecycle, &lbs[0], s)
}

func (s *LB) Run(context *fi.CloudupContext) error {
	return fi.CloudupDefaultDeltaRunMethod(s, context)
}

func (_ *LB) CheckChanges(a, e, changes *LB) error {
	if a == nil {
		if e.Name == nil {
			return fi.RequiredField("Name")
		}
	} else {
		if changes.ID != nil {
			return fi.CannotChangeField("ID")
		}
		if changes.Name != nil {
			return fi.CannotChangeField("Name")
		}
	}
	return nil
}

func (_ *LB) RenderOpenstack(t *openstack.OpenstackAPITarget, a, e, changes *LB) error {
	if a == nil {
		klog.V(2).Infof("Creating LB with Name: %q", fi.ValueOf(e.Name))

		subnets, err := t.Cloud.ListSubnets(subnets.ListOpts{
			Name: fi.ValueOf(e.Subnet),
		})
		if err != nil {
			return fmt.Errorf("Failed to retrieve subnet `%s` in loadbalancer creation: %v", fi.ValueOf(e.Subnet), err)
		}
		if len(subnets) != 1 {
			return fmt.Errorf("Unexpected desired subnets for `%s`.  Expected 1, got %d", fi.ValueOf(e.Subnet), len(subnets))
		}

		lbopts := loadbalancers.CreateOpts{
			Name:        fi.ValueOf(e.Name),
			VipSubnetID: subnets[0].ID,
		}
		if e.FlavorID != nil {
			lbopts.FlavorID = fi.ValueOf(e.FlavorID)
		}
		lb, err := t.Cloud.CreateLB(lbopts)
		if err != nil {
			return fmt.Errorf("error creating LB: %v", err)
		}
		e.ID = fi.PtrTo(lb.ID)
		e.PortID = fi.PtrTo(lb.VipPortID)
		e.VipSubnet = fi.PtrTo(lb.VipSubnetID)
		e.Provider = fi.PtrTo(lb.Provider)
		e.FlavorID = fi.PtrTo(lb.FlavorID)

		if e.SecurityGroup != nil {
			opts := ports.UpdateOpts{
				SecurityGroups: &[]string{fi.ValueOf(e.SecurityGroup.ID)},
			}
			_, err = ports.Update(context.TODO(), t.Cloud.NetworkingClient(), lb.VipPortID, opts).Extract()
			if err != nil {
				return fmt.Errorf("Failed to update security group for port %s: %v", lb.VipPortID, err)
			}
		}
		return nil
	}
	// We may have failed to update the security groups on the load balancer
	port, err := t.Cloud.GetPort(fi.ValueOf(a.PortID))
	if err != nil {
		return fmt.Errorf("Failed to get port with id %s: %v", fi.ValueOf(a.PortID), err)
	}
	// Ensure the loadbalancer port has one security group and it is the one specified,
	if e.SecurityGroup != nil &&
		(len(port.SecurityGroups) < 1 || port.SecurityGroups[0] != fi.ValueOf(e.SecurityGroup.ID)) {

		opts := ports.UpdateOpts{
			SecurityGroups: &[]string{fi.ValueOf(e.SecurityGroup.ID)},
		}
		_, err = ports.Update(context.TODO(), t.Cloud.NetworkingClient(), fi.ValueOf(a.PortID), opts).Extract()
		if err != nil {
			return fmt.Errorf("Failed to update security group for port %s: %v", fi.ValueOf(a.PortID), err)
		}
		return nil
	}

	klog.V(2).Infof("Openstack task LB::RenderOpenstack did nothing")
	return nil
}
