package forecast

import (
	"fmt"
	"strings"

	"github.com/aws-cloudformation/rain/internal/aws/cfn"
	"github.com/aws-cloudformation/rain/internal/aws/s3"
	"github.com/aws-cloudformation/rain/internal/config"
	"github.com/aws-cloudformation/rain/internal/console/spinner"
	"github.com/aws-cloudformation/rain/internal/s11n"
	"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"

	"github.com/google/uuid"
)

// An empty bucket cannot be deleted, which will cause a stack DELETE to fail.
// Returns true if the stack operation will succeed.
func checkBucketNotEmpty(input PredictionInput, bucket *types.StackResourceDetail) (bool, string) {
	if !input.stackExists {
		return true, "Stack does not exist"
	}

	spin(input.typeName, input.logicalId, "bucket not empty?")

	config.Debugf("Checking if the bucket %v is not empty", *bucket.PhysicalResourceId)

	exists, err := s3.BucketExists(*bucket.PhysicalResourceId)
	if err != nil {
		return false, fmt.Sprintf("Unable to check if bucket exists: %v", err)
	}

	if !exists {
		// The bucket might not exist if this is an UPDATE with new resources
		// But we should have already handled this when we got resource details
		return false, "Bucket does not exist"
	}

	hasContents, _ := s3.BucketHasContents(*bucket.PhysicalResourceId)
	if hasContents {
		// Check the deletion policy
		_, deletionPolicy := s11n.GetMapValue(input.resource, "DeletionPolicy")
		if deletionPolicy != nil && deletionPolicy.Value == "Retain" {
			// The bucket is not empty but it is set to retain,
			// so a stack DELETE will not fail
			return true, "Bucket is not empty but is set to RETAIN"
		}
		return false, "Bucket is not empty, so a stack DELETE will fail"

		// TODO - Should we check to see if they are using something like
		// AwsCommunity::S3::DeleteBucketContents?
		// (or a similar custom resource? .. not sure how to do this reliably)
	}

	spinner.Pop()

	return true, ""
}

// Check everything that could go wrong with an AWS::S3::Bucket resource
func checkS3Bucket(input PredictionInput) Forecast {

	// A uuid will be used for policy silumation if the bucket does not already exist
	bucketName := fmt.Sprintf("rain-%v", uuid.New())
	bucketArn := ""

	forecast := makeForecast(input.typeName, input.logicalId)

	if input.stackExists {
		res, err := cfn.GetStackResource(input.stackName, input.logicalId)

		if err != nil {
			// If this is an update, the bucket might not exist yet
			config.Debugf("Unable to get details for %v: %v", input.logicalId, err)
		} else {
			// The bucket exists
			bucketName := *res.PhysicalResourceId
			config.Debugf("Physical bucket name is: %v", bucketName)

			empty, reason := checkBucketNotEmpty(input, res)
			if !empty {
				forecast.Add(false, reason)
			} else {
				forecast.Add(true, "Bucket is empty")
			}
		}
	} else {
		config.Debugf("Stack does not exist, not checking if bucket is empty")
	}

	bucketArn = fmt.Sprintf("arn:aws:s3:::%v", bucketName)

	// TODO - Can we make the permissions check generic so we can
	// run it on all types? What if we can't predict what the arn will be?
	// We could have a map of resource names to functions that provide the arn..
	if !SkipIAM {
		var ok bool
		var reason []string
		if input.stackExists {
			ok, reason = checkPermissions(input, bucketArn, "update")
			if !ok {
				forecast.Add(false,
					fmt.Sprintf("Insufficient permissions to update %v\n\t%v", bucketArn, strings.Join(reason, "\n\t")))
			} else {
				forecast.Add(true, "Role has update permissions")
			}

			ok, reason = checkPermissions(input, bucketArn, "delete")
			if !ok {
				forecast.Add(false,
					fmt.Sprintf("Insufficient permissions to delete %v\n\t%v", bucketArn, strings.Join(reason, "\n\t")))
			} else {
				forecast.Add(true, "Role has delete permissions")
			}
		} else {
			ok, reason = checkPermissions(input, bucketArn, "create")
			if !ok {
				forecast.Add(false,
					fmt.Sprintf("Insufficient permissions to create %v\n\t%v", bucketArn, strings.Join(reason, "\n\t")))
			} else {
				forecast.Add(true, "Role has create permissions")
			}
		}
	}

	return forecast
}