package validation

import (
	"context"

	"code.justin.tv/eventbus/schema/pkg/eventbus/authorization"
	"github.com/aws/aws-sdk-go/aws"

	"code.justin.tv/eventbus/controlplane/internal/logger"
	"go.uber.org/zap"

	"code.justin.tv/eventbus/controlplane/internal/db"
	"github.com/aws/aws-sdk-go/service/kms"
	"github.com/pkg/errors"
)

type AuthorizedFieldPublisherGrant struct {
	*db.AuthorizedFieldPublisherGrant
	EventType   string
	Environment string
	IAMRoleARN  string

	existingKMSGrants []*kms.GrantListEntry
}

func (a *AuthorizedFieldPublisherGrant) ID() string {
	return itemID(a)
}

func (a *AuthorizedFieldPublisherGrant) Type() string {
	return "AuthorizedFieldPublisherGrant"
}

func (a *AuthorizedFieldPublisherGrant) Attributes() []*ItemAttribute {
	return []*ItemAttribute{
		{
			Key:   "IAMRole",
			Value: a.IAMRoleARN,
		},
		{
			Key:   "EventType",
			Value: a.EventType,
		},
		{
			Key:   "Environment",
			Value: a.Environment,
		},
	}
}

func (a *AuthorizedFieldPublisherGrant) Validate(ctx context.Context) (*Report, error) {
	for _, kmsGrant := range a.existingKMSGrants {
		if a.grantMatches(kmsGrant) {
			return Ok(a), nil
		}
	}
	return ReportWithEnvironmentSeverity(a, a.Environment, "could not find authorized field publisher grant in kms grant list"), nil
}

func (a *AuthorizedFieldPublisherGrant) grantMatches(kmsGrant *kms.GrantListEntry) bool {
	return kmsGrant.Constraints != nil &&
		aws.StringValueMap(kmsGrant.Constraints.EncryptionContextSubset)[authorization.EventType] == a.EventType &&
		aws.StringValueMap(kmsGrant.Constraints.EncryptionContextSubset)[authorization.Environment] == a.Environment &&
		aws.StringValue(kmsGrant.GranteePrincipal) == a.IAMRoleARN
}

func (a *AuthorizedFieldPublisherGrant) WithExistingGrants(grants []*kms.GrantListEntry) {
	a.existingKMSGrants = grants
}

type AuthorizedFieldSubscriberGrant struct {
	*db.AuthorizedFieldSubscriberGrant
	EventType   string
	Environment string
	MessageName string
	FieldName   string
	IAMRoleARN  string

	existingKMSGrants []*kms.GrantListEntry
}

func (a *AuthorizedFieldSubscriberGrant) ID() string {
	return itemID(a)
}

func (a *AuthorizedFieldSubscriberGrant) Type() string {
	return "AuthorizedFieldSubscriberGrant"
}

func (a *AuthorizedFieldSubscriberGrant) Attributes() []*ItemAttribute {
	return []*ItemAttribute{
		{
			Key:   "IAMRole",
			Value: a.IAMRoleARN,
		},
		{
			Key:   "EventType",
			Value: a.EventType,
		},
		{
			Key:   "Environment",
			Value: a.Environment,
		},
		{
			Key:   "MessageName",
			Value: a.MessageName,
		},
		{
			Key:   "FieldName",
			Value: a.FieldName,
		},
	}
}

func (a *AuthorizedFieldSubscriberGrant) Validate(ctx context.Context) (*Report, error) {
	for _, kmsGrant := range a.existingKMSGrants {
		if a.grantMatches(kmsGrant) {
			return Ok(a), nil
		}
	}
	return ReportWithEnvironmentSeverity(a, a.Environment, "could not find authorized field publisher grant in kms grant list"), nil
}

func (a *AuthorizedFieldSubscriberGrant) grantMatches(kmsGrant *kms.GrantListEntry) bool {
	return kmsGrant.Constraints != nil &&
		aws.StringValueMap(kmsGrant.Constraints.EncryptionContextSubset)[authorization.EventType] == a.EventType &&
		aws.StringValueMap(kmsGrant.Constraints.EncryptionContextSubset)[authorization.Environment] == a.Environment &&
		aws.StringValueMap(kmsGrant.Constraints.EncryptionContextSubset)[authorization.MessageName] == a.MessageName &&
		aws.StringValueMap(kmsGrant.Constraints.EncryptionContextSubset)[authorization.FieldName] == a.FieldName &&
		aws.StringValue(kmsGrant.GranteePrincipal) == a.IAMRoleARN
}

func (a *AuthorizedFieldSubscriberGrant) WithExistingGrants(grants []*kms.GrantListEntry) {
	a.existingKMSGrants = grants
}

// AuthorizedFieldGrants returns both validatable publisher and subscriber authorized field grants
func AuthorizedFieldGrants(ctx context.Context, dbConn db.DB, grantFetcher GrantFetcher) ([]Item, error) {

	kmsGrants, err := grantFetcher.AllGrants(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "could not get existing kms grants")
	}

	eventStreams, err := getEventStreamMap(ctx, dbConn)
	if err != nil {
		return nil, errors.Wrap(err, "could not get event streams")
	}

	iamRoles, err := getIAMRoleMap(ctx, dbConn)
	if err != nil {
		return nil, errors.Wrap(err, "could not get iam roles")
	}

	authorizedFields, err := getAuthorizedFieldMap(ctx, dbConn)
	if err != nil {
		return nil, errors.Wrap(err, "could not get authorized fields")
	}

	pubGrants, err := dbConn.AuthorizedFieldPublisherGrants(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "could not get authorized field publisher grants")
	}

	subGrants, err := dbConn.AuthorizedFieldSubscriberGrants(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "could not get authorized field susbcriber grants")
	}

	items := []Item{}
	for _, grant := range pubGrants {
		eventStream, found := eventStreams.Get(grant.EventStreamID)
		if !found {
			logger.FromContext(ctx).Warn("could not find event stream for authorized field publisher grant", zap.Object("grant", grant))
			continue
		}

		iamRole, found := iamRoles.Get(grant.IAMRoleID)
		if !found {
			logger.FromContext(ctx).Warn("could not find iam role for authorized field publisher grant", zap.Object("grant", grant))
			continue
		}

		items = append(items, &AuthorizedFieldPublisherGrant{
			AuthorizedFieldPublisherGrant: grant,
			EventType:                     eventStream.EventType.Name,
			Environment:                   eventStream.Environment,
			IAMRoleARN:                    iamRole.ARN,
			existingKMSGrants:             kmsGrants,
		})
	}

	for _, grant := range subGrants {
		authField, found := authorizedFields.Get(grant.AuthorizedFieldID)
		if !found {
			logger.FromContext(ctx).Warn("could not find authorized field for susbcriber grant", zap.Object("grant", grant))
			continue
		}

		eventStream, found := eventStreams.Get(authField.EventStreamID)
		if !found {
			logger.FromContext(ctx).Warn("could not find event stream for authorized field subscriber grant", zap.Object("grant", grant))
			continue
		}

		iamRole, found := iamRoles.Get(grant.IAMRoleID)
		if !found {
			logger.FromContext(ctx).Warn("could not find iam role for authorized field subscriber grant", zap.Object("grant", grant))
			continue
		}

		items = append(items, &AuthorizedFieldSubscriberGrant{
			AuthorizedFieldSubscriberGrant: grant,
			EventType:                      eventStream.EventType.Name,
			Environment:                    eventStream.Environment,
			MessageName:                    authField.MessageName,
			FieldName:                      authField.FieldName,
			IAMRoleARN:                     iamRole.ARN,
			existingKMSGrants:              kmsGrants,
		})
	}

	return items, nil
}
