//go:build callback
// +build callback

/*
Package callback provides functions for creating resource providers
that may need to be called multiple times while waiting
for resources to settle.
*/
package callback

import (
	"fmt"
	"log"

	"github.com/avast/retry-go"
	"github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/cfnerr"
	"github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/logging"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/cloudformation"
	"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
)

const (
	// ServiceInternalError ...
	ServiceInternalError string = "ServiceInternal"
	// MaxRetries is the number of retries allowed to report status.
	MaxRetries uint = 3
)

// CloudFormationCallbackAdapter used to report progress events back to CloudFormation.
type CloudFormationCallbackAdapter struct {
	client      cloudformationiface.CloudFormationAPI
	bearerToken string
	logger      *log.Logger
}

// New creates a CloudFormationCallbackAdapter and returns a pointer to the struct.
func New(client cloudformationiface.CloudFormationAPI, bearerToken string) *CloudFormationCallbackAdapter {
	return &CloudFormationCallbackAdapter{
		client:      client,
		bearerToken: bearerToken,
		logger:      logging.New("callback"),
	}
}

// ReportStatus reports the status back to the Cloudformation service of a handler
// that has moved from Pending to In_Progress
func (c *CloudFormationCallbackAdapter) ReportStatus(operationStatus Status, model []byte, message string, errCode string) error {
	if err := c.reportProgress(errCode, operationStatus, InProgress, model, message); err != nil {
		return err
	}
	return nil
}

// ReportInitialStatus reports the initial status back to the Cloudformation service.
func (c *CloudFormationCallbackAdapter) ReportInitialStatus() error {
	if err := c.reportProgress("", InProgress, Pending, []byte(""), ""); err != nil {
		return err
	}
	return nil
}

// ReportFailureStatus reports the failure status back to the Cloudformation service.
func (c *CloudFormationCallbackAdapter) ReportFailureStatus(model []byte, errCode string, handlerError error) error {
	if err := c.reportProgress(errCode, Failed, InProgress, model, handlerError.Error()); err != nil {
		return err
	}
	return nil
}

// ReportProgress reports the current status back to the Cloudformation service.
func (c *CloudFormationCallbackAdapter) reportProgress(errCode string, operationStatus Status, currentOperationStatus Status, resourceModel []byte, statusMessage string) error {

	in := cloudformation.RecordHandlerProgressInput{
		BearerToken:     aws.String(c.bearerToken),
		OperationStatus: aws.String(TranslateOperationStatus(operationStatus)),
	}

	if len(statusMessage) != 0 {
		in.SetStatusMessage(statusMessage)
	}

	if len(resourceModel) != 0 {
		in.SetResourceModel(string(resourceModel))
	}

	if len(errCode) != 0 {
		in.SetErrorCode(TranslateErrorCode(errCode))
	}

	if len(currentOperationStatus) != 0 {
		in.SetCurrentOperationStatus(TranslateOperationStatus(currentOperationStatus))
	}

	// Do retries and emit logs.
	rerr := retry.Do(
		func() error {
			_, err := c.client.RecordHandlerProgress(&in)
			if err != nil {
				return err
			}
			return nil
		}, retry.OnRetry(func(n uint, err error) {
			s := fmt.Sprintf("Failed to record progress: try:#%d: %s\n ", n+1, err)
			c.logger.Println(s)

		}), retry.Attempts(MaxRetries),
	)

	if rerr != nil {
		return cfnerr.New(ServiceInternalError, "Callback ReportProgress Error", rerr)
	}

	return nil
}

// TranslateErrorCode : Translate the error code into a standard Cloudformation error
func TranslateErrorCode(errorCode string) string {

	// Ensure the error code conforms to one of the available
	switch errorCode {
	case cloudformation.HandlerErrorCodeNotUpdatable,
		cloudformation.HandlerErrorCodeInvalidRequest,
		cloudformation.HandlerErrorCodeAccessDenied,
		cloudformation.HandlerErrorCodeInvalidCredentials,
		cloudformation.HandlerErrorCodeAlreadyExists,
		cloudformation.HandlerErrorCodeNotFound,
		cloudformation.HandlerErrorCodeResourceConflict,
		cloudformation.HandlerErrorCodeThrottling,
		cloudformation.HandlerErrorCodeServiceLimitExceeded,
		cloudformation.HandlerErrorCodeNotStabilized,
		cloudformation.HandlerErrorCodeGeneralServiceException,
		cloudformation.HandlerErrorCodeServiceInternalError,
		cloudformation.HandlerErrorCodeNetworkFailure,
		cloudformation.HandlerErrorCodeInternalFailure:
		return errorCode
	default:
		// InternalFailure is CloudFormation's fallback error code when no more specificity is there
		return cloudformation.HandlerErrorCodeInternalFailure
	}
}

// TranslateOperationStatus Translate the operation Status into a standard Cloudformation error
func TranslateOperationStatus(operationStatus Status) string {

	switch operationStatus {
	case Success:
		return cloudformation.OperationStatusSuccess
	case Failed:
		return cloudformation.OperationStatusFailed
	case InProgress:
		return cloudformation.OperationStatusInProgress
	case Pending:
		return cloudformation.OperationStatusPending
	default:
		// default will be to fail on unknown status
		return cloudformation.OperationStatusFailed
	}

}
