//revive:disable:var-naming

package operation

//revive:enable:var-naming

import (
	"fmt"
	"strings"
	"time"

	errors "github.com/pingcap/errors"
	zap "go.uber.org/zap"
	context "golang.org/x/net/context"
	"google.golang.org/api/compute/v1"
	"google.golang.org/api/container/v1"
)

// Operation abstracts over the different GCP Operation types
// The underlying Operation structs should be the same unless you have different API Versions
type Operation interface {
	Reload() (Operation, error)
	Name() string
	Status() string
	StatusMessage() string
}

// ComputeOperation is an Operation of the type used by compute APIs
type ComputeOperation struct {
	Operation *compute.Operation
	Service   *compute.Service
	ProjectID string
	Region    string
}

// Name is the operation name
func (op ComputeOperation) Name() string { return op.Operation.Name }

// Status is the operation status
func (op ComputeOperation) Status() string { return op.Operation.Status }

// StatusMessage is a message about the status
func (op ComputeOperation) StatusMessage() string { return op.Operation.StatusMessage }

// Name is the operation name
func (op ContainerOperation) Name() string { return op.Operation.Name }

// Status is the operation status
func (op ContainerOperation) Status() string { return op.Operation.Status }

// StatusMessage is a message about the status
func (op ContainerOperation) StatusMessage() string { return op.Operation.Status }

// Reload refreshes the operation
func (op ComputeOperation) Reload() (Operation, error) {
	ctx := context.Background()
	var freshOp *compute.Operation
	var getErr error
	if strings.Contains(op.Operation.SelfLink, "global") {
		freshOp, getErr = op.Service.GlobalOperations.Get(op.ProjectID, op.Name()).Context(ctx).Do()
	} else {
		freshOp, getErr = op.Service.RegionOperations.Get(op.ProjectID, op.Region, op.Name()).Context(ctx).Do()
	}
	if getErr != nil {
		return nil, getErr
	}
	op.Operation = freshOp
	var err error
	if freshOp.Error != nil {
		err = goError{err: freshOp.Error}
	}
	return op, err
}

type goError struct {
	err *compute.OperationError
}

func (e goError) Error() string {
	var errStr string
	for _, err := range e.err.Errors {
		errStr += fmt.Sprintf("%+v\n", *err)
	}
	return errStr
}

// ContainerOperation is an Operation of the type used by container APIs
type ContainerOperation struct {
	Operation *container.Operation
	Service   *container.Service
}

// Reload refreshes the operation
func (op ContainerOperation) Reload() (Operation, error) {
	ctx := context.Background()
	id := strings.TrimPrefix(op.Operation.SelfLink, "https://container.googleapis.com/v1/")
	freshOp, getErr := op.Service.Projects.Locations.Operations.Get(id).Context(ctx).Do()
	if getErr != nil {
		return nil, getErr
	}
	var err error
	if freshOp.StatusMessage != "" {
		err = fmt.Errorf("%s %s", freshOp.Status, freshOp.StatusMessage)
	}
	return ContainerOperation{
		Operation: freshOp,
		Service:   op.Service,
	}, err
}

// WaitForOperation refreshes the operation until it completes
func WaitForOperation(log *zap.SugaredLogger, operation Operation, sleepDuration time.Duration) error {
	name := operation.Name()
	statusMsg := operation.StatusMessage()

	status := operation.Status()
	if status == "DONE" {
		log.Debugf("DONE operation %s %s", name, statusMsg)
		return nil
	}

	newOperation, err := operation.Reload()
	if err != nil {
		return errors.Annotate(err, "waitForOperation")
	}
	log.Debugf("waiting on operation %s for %v. Status %s %s.", name, sleepDuration, status, statusMsg)
	time.Sleep(sleepDuration)
	return WaitForOperation(log, newOperation, sleepDuration)
}
