package cfnversion

import (
	"context"
	"fmt"
	"regexp"
	"strconv"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/client"
	"github.com/aws/aws-sdk-go/service/iam"
	"github.com/aws/aws-sdk-go/service/iam/iamiface"
	"github.com/pkg/errors"
)

const (
	crossAccountRoleName = "EventBus"
	versionKey           = "Version"
)

var (
	versionRegex = regexp.MustCompile(`^v([0-9]+)\.([0-9]+)\.([0-9]+)$`)

	errVersionNotFound = errors.New("version not found")
)

type version struct {
	major int
	minor int
	patch int
}

func (v *version) isGreaterThanOrEqual(other *version) bool {
	if v.major > other.major {
		return true
	}

	if v.major == other.major {
		if v.minor > other.minor {
			return true
		}

		if v.minor == other.minor {
			if v.patch >= other.patch {
				return true
			}
		}
	}

	return false
}

func parse(v string) (*version, error) {
	groups := versionRegex.FindStringSubmatch(v)
	if groups == nil {
		return nil, errors.New("invalid required version string " + v)
	}

	if len(groups) != 4 {
		return nil, errors.New("unexpected number of capture groups in version parse: " + string(len(groups)))
	}

	major, err := strconv.Atoi(groups[1])
	if err != nil {
		return nil, errors.New("invalid major version " + groups[1])
	}

	minor, err := strconv.Atoi(groups[2])
	if err != nil {
		return nil, errors.New("invalid minor version " + groups[2])
	}

	patch, err := strconv.Atoi(groups[3])
	if err != nil {
		return nil, errors.New("invalid patch version " + groups[3])
	}

	return &version{
		major: major,
		minor: minor,
		patch: patch,
	}, nil
}

func Require(ctx context.Context, sess client.ConfigProvider, ver string) (bool, error) {
	return requireWithClient(ctx, iam.New(sess), ver)
}

func requireWithClient(ctx context.Context, iamClient iamiface.IAMAPI, ver string) (bool, error) {
	requiredVersion, err := parse(ver)
	if err != nil {
		return false, errors.Wrap(err, "could not parse required version")
	}

	foundVersion, err := getWithClient(ctx, iamClient)
	if err != nil {
		if accessDenied(err) || errors.Cause(err) == errVersionNotFound {
			// For AccessDenied, one of two things is true
			// 1. The user does not have the cross-account CloudFormation installed/attached
			// 2. Their version is so old that the check feature isn't permissioned yet
			// In both cases, the answer of "does the caller meet the requirement?" is _no_,
			// so swallow the error
			// If ErrVersionNotFound, the user is running an old version of the CFN prior to version tagging,
			// and their IAM role can still call iam:ListRoleTags.
			return false, nil
		} else {
			return false, errors.Wrap(err, "could not get cross-account cloudformation version")
		}
	}

	return foundVersion.isGreaterThanOrEqual(requiredVersion), nil
}

func Get(ctx context.Context, sess client.ConfigProvider) (string, error) {
	v, err := getWithClient(ctx, iam.New(sess))
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("v%d.%d.%d", v.major, v.minor, v.patch), nil
}

func getWithClient(ctx context.Context, iamClient iamiface.IAMAPI) (*version, error) {

	resp, err := iamClient.ListRoleTagsWithContext(ctx, &iam.ListRoleTagsInput{
		RoleName: aws.String(crossAccountRoleName),
	})
	if err != nil {
		return nil, errors.Wrap(err, "could not list iam role tags")
	}

	var found bool
	var tagValue string
	for _, tag := range resp.Tags {
		if aws.StringValue(tag.Key) == versionKey {
			tagValue = aws.StringValue(tag.Value)
			found = true
			break
		}
	}
	if !found {
		return nil, errVersionNotFound
	}

	return parse(tagValue)
}

func accessDenied(originalErr error) bool {
	err := errors.Cause(originalErr)
	awsErr, ok := err.(awserr.Error)
	if ok {
		return awsErr.Code() == "AccessDenied"
	}
	return false
}
