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

import (
	"fmt"
	"net/http"

	compute "google.golang.org/api/compute/v1"

	"github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud"
	"k8s.io/klog"
)

type addressManager struct {
	logPrefix   string
	svc         CloudAddressService
	name        string
	serviceName string
	targetIP    string
	addressType cloud.LbScheme
	region      string
	subnetURL   string
	tryRelease  bool
}

func newAddressManager(svc CloudAddressService, serviceName, region, subnetURL, name, targetIP string, addressType cloud.LbScheme) *addressManager {
	return &addressManager{
		svc:         svc,
		logPrefix:   fmt.Sprintf("AddressManager(%q)", name),
		region:      region,
		serviceName: serviceName,
		name:        name,
		targetIP:    targetIP,
		addressType: addressType,
		tryRelease:  true,
		subnetURL:   subnetURL,
	}
}

// HoldAddress will ensure that the IP is reserved with an address - either owned by the controller
// or by a user. If the address is not the addressManager.name, then it's assumed to be a user's address.
// The string returned is the reserved IP address.
func (am *addressManager) HoldAddress() (string, error) {
	// HoldAddress starts with retrieving the address that we use for this load balancer (by name).
	// Retrieving an address by IP will indicate if the IP is reserved and if reserved by the user
	// or the controller, but won't tell us the current state of the controller's IP. The address
	// could be reserving another address; therefore, it would need to be deleted. In the normal
	// case of using a controller address, retrieving the address by name results in the fewest API
	// calls since it indicates whether a Delete is necessary before Reserve.
	klog.V(4).Infof("%v: attempting hold of IP %q Type %q", am.logPrefix, am.targetIP, am.addressType)
	// Get the address in case it was orphaned earlier
	addr, err := am.svc.GetRegionAddress(am.name, am.region)
	if err != nil && !isNotFound(err) {
		return "", err
	}

	if addr != nil {
		// If address exists, check if the address had the expected attributes.
		validationError := am.validateAddress(addr)
		if validationError == nil {
			klog.V(4).Infof("%v: address %q already reserves IP %q Type %q. No further action required.", am.logPrefix, addr.Name, addr.Address, addr.AddressType)
			return addr.Address, nil
		}

		klog.V(2).Infof("%v: deleting existing address because %v", am.logPrefix, validationError)
		err := am.svc.DeleteRegionAddress(addr.Name, am.region)
		if err != nil {
			if isNotFound(err) {
				klog.V(4).Infof("%v: address %q was not found. Ignoring.", am.logPrefix, addr.Name)
			} else {
				return "", err
			}
		} else {
			klog.V(4).Infof("%v: successfully deleted previous address %q", am.logPrefix, addr.Name)
		}
	}

	return am.ensureAddressReservation()
}

// ReleaseAddress will release the address if it's owned by the controller.
func (am *addressManager) ReleaseAddress() error {
	if !am.tryRelease {
		klog.V(4).Infof("%v: not attempting release of address %q.", am.logPrefix, am.targetIP)
		return nil
	}

	klog.V(4).Infof("%v: releasing address %q named %q", am.logPrefix, am.targetIP, am.name)
	// Controller only ever tries to unreserve the address named with the load balancer's name.
	err := am.svc.DeleteRegionAddress(am.name, am.region)
	if err != nil {
		if isNotFound(err) {
			klog.Warningf("%v: address %q was not found. Ignoring.", am.logPrefix, am.name)
			return nil
		}

		return err
	}

	klog.V(4).Infof("%v: successfully released IP %q named %q", am.logPrefix, am.targetIP, am.name)
	return nil
}

func (am *addressManager) ensureAddressReservation() (string, error) {
	// Try reserving the IP with controller-owned address name
	// If am.targetIP is an empty string, a new IP will be created.
	newAddr := &compute.Address{
		Name:        am.name,
		Description: fmt.Sprintf(`{"kubernetes.io/service-name":"%s"}`, am.serviceName),
		Address:     am.targetIP,
		AddressType: string(am.addressType),
		Subnetwork:  am.subnetURL,
	}

	reserveErr := am.svc.ReserveRegionAddress(newAddr, am.region)
	if reserveErr == nil {
		if newAddr.Address != "" {
			klog.V(4).Infof("%v: successfully reserved IP %q with name %q", am.logPrefix, newAddr.Address, newAddr.Name)
			return newAddr.Address, nil
		}

		addr, err := am.svc.GetRegionAddress(newAddr.Name, am.region)
		if err != nil {
			return "", err
		}

		klog.V(4).Infof("%v: successfully created address %q which reserved IP %q", am.logPrefix, addr.Name, addr.Address)
		return addr.Address, nil
	} else if !isHTTPErrorCode(reserveErr, http.StatusConflict) && !isHTTPErrorCode(reserveErr, http.StatusBadRequest) {
		// If the IP is already reserved:
		//    by an internal address: a StatusConflict is returned
		//    by an external address: a BadRequest is returned
		return "", reserveErr
	}

	// If the target IP was empty, we cannot try to find which IP caused a conflict.
	// If the name was already used, then the next sync will attempt deletion of that address.
	if am.targetIP == "" {
		return "", fmt.Errorf("failed to reserve address %q with no specific IP, err: %v", am.name, reserveErr)
	}

	// Reserving the address failed due to a conflict or bad request. The address manager just checked that no address
	// exists with the name, so it may belong to the user.
	addr, err := am.svc.GetRegionAddressByIP(am.region, am.targetIP)
	if err != nil {
		return "", fmt.Errorf("failed to get address by IP %q after reservation attempt, err: %q, reservation err: %q", am.targetIP, err, reserveErr)
	}

	// Check that the address attributes are as required.
	if err := am.validateAddress(addr); err != nil {
		return "", err
	}

	if am.isManagedAddress(addr) {
		// The address with this name is checked at the beginning of 'HoldAddress()', but for some reason
		// it was re-created by this point. May be possible that two controllers are running.
		klog.Warningf("%v: address %q unexpectedly existed with IP %q.", am.logPrefix, addr.Name, am.targetIP)
	} else {
		// If the retrieved address is not named with the loadbalancer name, then the controller does not own it, but will allow use of it.
		klog.V(4).Infof("%v: address %q was already reserved with name: %q, description: %q", am.logPrefix, am.targetIP, addr.Name, addr.Description)
		am.tryRelease = false
	}

	return addr.Address, nil
}

func (am *addressManager) validateAddress(addr *compute.Address) error {
	if am.targetIP != "" && am.targetIP != addr.Address {
		return fmt.Errorf("address %q does not have the expected IP %q, actual: %q", addr.Name, am.targetIP, addr.Address)
	}
	if addr.AddressType != string(am.addressType) {
		return fmt.Errorf("address %q does not have the expected address type %q, actual: %q", addr.Name, am.addressType, addr.AddressType)
	}

	return nil
}

func (am *addressManager) isManagedAddress(addr *compute.Address) bool {
	return addr.Name == am.name
}

func ensureAddressDeleted(svc CloudAddressService, name, region string) error {
	return ignoreNotFound(svc.DeleteRegionAddress(name, region))
}
