package service

import (
	"context"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/secretsmanager"
	"github.com/samber/lo"
)

const AWSCURRENT = "AWSCURRENT"

type SecretService interface {
	// GetSecret returns the "AWSCURRENT" version secret value of the given key.
	GetSecret(ctx context.Context, key string) (string, error)
	// PutSecret puts the "AWSCURRENT" version secret value of the given key.
	CreateOrUpdateSecret(ctx context.Context, key, value string, tags map[string]string) error
	DeleteSecret(ctx context.Context, key string) error
}

type secretService struct {
	*secretsmanager.SecretsManager
}

// NewSecretService returns a new SecretService. It will create a new session instance and a new secrets manager instance.
func NewSecretService(region string) (SecretService, error) {
	awsConfig := aws.NewConfig().WithMaxRetries(3).WithRegion(region)
	opts := session.Options{Config: *awsConfig, SharedConfigState: session.SharedConfigEnable}
	sess, err := session.NewSessionWithOptions(opts)
	if err != nil {
		return nil, err
	}
	svc := secretsmanager.New(sess)
	return &secretService{svc}, nil
}

func (s *secretService) GetSecret(ctx context.Context, key string) (string, error) {
	input := &secretsmanager.GetSecretValueInput{
		SecretId:     aws.String(key),
		VersionStage: aws.String(AWSCURRENT),
	}
	result, err := s.GetSecretValueWithContext(ctx, input)
	if err != nil {
		return "", err
	}
	return *result.SecretString, nil
}

func (s *secretService) CreateOrUpdateSecret(ctx context.Context, key, value string, tags map[string]string) error {
	createOrUpdate := func() error {
		// Try to create at first.
		if err := s.createSecret(ctx, key, value, tags); err != nil {
			// If the secret already exists, update it.
			if aerr, ok := err.(awserr.Error); ok && aerr.Code() == secretsmanager.ErrCodeResourceExistsException {
				return s.putSecretValue(ctx, key, value)
			}
			return err
		}
		return nil
	}

	// Retry logic.
	//
	// Comments from aws sdk:
	// > Secrets Manager performs the actual deletion with an asynchronous background
	// > process, so there might be a short delay before the secret is permanently
	// > deleted. If you delete a secret and then immediately create a secret with
	// > the same name, use appropriate back off and retry logic.
	//
	// See: https://github.com/aws/aws-sdk-go/blob/41d339d7fbaddcf08eb7312c22c9bfc1ec9ef0eb/service/secretsmanager/api.go#L3424-L3427
	var err error
	waits := 1 // s
	for retry := 0; retry < 4; retry++ {
		err = createOrUpdate()
		if err != nil {
			if aerr, ok := err.(awserr.Error); ok && aerr.Code() == secretsmanager.ErrCodeInvalidRequestException {
				// Err: You can't create this secret because a secret with this name is already scheduled for deletion.
				time.Sleep(time.Duration(waits) * time.Second)
				waits *= 2
				continue
			}
		}
		break
	}
	return err
}

func (s *secretService) DeleteSecret(ctx context.Context, key string) error {
	input := &secretsmanager.DeleteSecretInput{
		SecretId:                   aws.String(key),
		ForceDeleteWithoutRecovery: lo.ToPtr(true),
	}
	_, err := s.DeleteSecretWithContext(ctx, input)
	return err
}

func (s *secretService) createSecret(ctx context.Context, key, value string, tags map[string]string) error {
	awsTags := make([]*secretsmanager.Tag, 0, len(tags))
	for k, v := range tags {
		awsTags = append(awsTags, &secretsmanager.Tag{
			Key:   aws.String(k),
			Value: aws.String(v),
		})
	}
	input := &secretsmanager.CreateSecretInput{
		Name:         aws.String(key),
		SecretString: aws.String(value),
		Tags:         awsTags,
	}
	_, err := s.CreateSecretWithContext(ctx, input)
	return err
}

func (s *secretService) putSecretValue(ctx context.Context, key, value string) error {
	input := &secretsmanager.PutSecretValueInput{
		SecretId:      aws.String(key),
		VersionStages: []*string{aws.String(AWSCURRENT)},
		SecretString:  aws.String(value),
	}
	_, err := s.PutSecretValueWithContext(ctx, input)
	return err
}
