package services

import (
	"context"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"code.justin.tv/eventbus/controlplane/infrastructure/validation"

	"go.uber.org/zap"

	"code.justin.tv/eventbus/controlplane/internal/arn"
	"code.justin.tv/eventbus/controlplane/internal/auditlog"
	"code.justin.tv/eventbus/controlplane/internal/clients/servicecatalog"
	"code.justin.tv/eventbus/controlplane/internal/db"
	"code.justin.tv/eventbus/controlplane/internal/ldap"
	"code.justin.tv/eventbus/controlplane/internal/logger"
	"code.justin.tv/eventbus/controlplane/internal/twirperr"
	"code.justin.tv/eventbus/controlplane/internal/validator"
	"code.justin.tv/eventbus/controlplane/rpc"

	"github.com/pkg/errors"
	"github.com/twitchtv/twirp"
	"golang.org/x/sync/errgroup"
)

// ServicesService is a service for services
// nherson: not thrilled about this name (services.ServicesService) but I guess that's how it goes sometimes
type ServicesService struct {
	DB             db.DB
	ServiceCatalog servicecatalog.Client
	DisableGrants  bool

	EncryptionAtRestManager EncryptionAtRestManager
	CloudformationManager   CloudformationManager
	LDAPManager             LDAPManager
}

func (s *ServicesService) List(ctx context.Context, req *rpc.ListServicesReq) (*rpc.ListServicesResp, error) {
	services, err := s.DB.ServicesWithIAMRoles(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "could not list services with IAM roles")
	}

	// TODO [ASYNC-310] - push joining logic into the db layer
	accounts, err := s.DB.Accounts(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "could not get accounts")
	}

	accountsByServiceID := make(map[int][]*db.Account)
	for _, account := range accounts {
		accountsByServiceID[account.ServiceID] = append(accountsByServiceID[account.ServiceID], account)
	}

	mu := &sync.Mutex{}
	g, ctx := errgroup.WithContext(ctx)
	serviceProtos := make([]*rpc.Service, 0)
	for _, service := range services {
		service := service // avoid concurrency issue with go routines starting at random times

		g.Go(func() error {
			if ldap.BelongsToGroup(ctx, service.LDAPGroup) {
				serviceCatalogResp, err := s.serviceCatalogRequest(ctx, service.ServiceCatalogID)
				if err != nil {
					return err
				}

				mu.Lock()
				serviceProtos = append(serviceProtos, genServiceProto(service, accountsByServiceID[service.ID], service.IAMRoles, serviceCatalogResp))
				mu.Unlock()
			}
			return nil
		})
	}

	if err := g.Wait(); err != nil {
		return nil, twirp.NewError(twirp.Unavailable, "could not get service details from service catalog: "+err.Error())
	}

	return &rpc.ListServicesResp{
		Services: serviceProtos,
	}, nil
}

func (s *ServicesService) Get(ctx context.Context, req *rpc.GetServiceReq) (*rpc.Service, error) {
	if req.Id == "" {
		return nil, twirp.NewError(twirp.InvalidArgument, "no id provided to get service")
	}
	id, err := strconv.Atoi(req.Id)
	if err != nil {
		return nil, errors.Wrap(err, "invalid service id provided"+req.Id)
	}

	serviceModel, err := s.DB.ServiceByID(ctx, id)
	if err != nil {
		return nil, twirperr.Convert(err)
	} else if !ldap.BelongsToGroup(ctx, serviceModel.LDAPGroup) {
		return nil, twirp.NewError(twirp.PermissionDenied, "access denied")
	}

	accounts, err := s.DB.AccountsByServiceID(ctx, id)
	if err != nil {
		return nil, errors.Wrap(err, "could not get accounts for service id "+req.Id)
	}

	iamRoles, err := s.DB.IAMRolesByServiceID(ctx, id)
	if err != nil {
		return nil, errors.Wrap(err, "could not get IAM roles for service id "+req.Id)
	}

	serviceCatalogResp, err := s.serviceCatalogRequest(ctx, serviceModel.ServiceCatalogID)
	if err != nil {
		return nil, twirp.NewError(twirp.Internal, "could not get service details from service catalog")
	}

	return genServiceProto(serviceModel, accounts, iamRoles, serviceCatalogResp), nil
}

func (s *ServicesService) Create(ctx context.Context, req *rpc.CreateServiceReq) (*rpc.CreateServiceResp, error) {
	if req.Service == nil {
		return nil, twirp.NewError(twirp.InvalidArgument, "no service data provided")
	} else if req.Service.Id != "" {
		return nil, twirp.NewError(twirp.InvalidArgument, "must not provide service id to create")
	} else if req.Service.LdapGroup == "" {
		return nil, twirp.NewError(twirp.InvalidArgument, "must specify ldap group ownership")
	} else if !ldap.BelongsToGroup(ctx, req.Service.LdapGroup) {
		return nil, twirp.NewError(twirp.PermissionDenied, "user must belong to ldap group of created service")
	}

	err := validator.Validate(req.Service.LdapGroup, isAllowedLDAPGroup)
	if err != nil {
		return nil, twirp.NewError(twirp.PermissionDenied, "must specify allowed ldap group")
	}

	err = validator.ValidateWithError(req.Service.GetServiceCatalogUrl(), isValidServiceCatalogURL)
	if err != nil && validator.IsInternalError(err) {
		return nil, twirp.NewError(twirp.Internal, "validation error")
	} else if err != nil {
		return nil, twirp.NewError(twirp.InvalidArgument, "invalid service catalog url "+err.Error())
	}

	service := &db.Service{}
	serviceCatalogResp, err := s.serviceCatalogRequest(ctx, serviceCatalogID(req.Service.ServiceCatalogUrl))
	if err != nil {
		return nil, twirp.NewError(twirp.Internal, "could not get service details from service catalog")
	}

	if serviceCatalogResp.Name == "" {
		return nil, twirp.NewError(twirp.InvalidArgument, "cannot create a service without a valid name, please make sure your service has a valid name in the service catalog")
	}
	updateServiceModel(service, req.Service, serviceCatalogResp)

	id, err := s.DB.ServiceCreate(ctx, service)
	if err != nil {
		return nil, twirperr.Convert(errors.Wrap(err, "error writing created service to postgres"))
	}

	base := &auditlog.BaseLog{
		ResourceName: service.Name,
		ServiceID:    id,
		DB:           s.DB,
	}
	base.LogServiceCreate(ctx, nil, service)

	req.Service.Id = strconv.Itoa(id)
	req.Service.Name = service.Name
	return &rpc.CreateServiceResp{
		Service: req.Service,
	}, nil
}

// Update updates a service's details
func (s *ServicesService) Update(ctx context.Context, req *rpc.UpdateServiceReq) (*rpc.UpdateServiceResp, error) {
	if req.Service == nil {
		return nil, twirp.NewError(twirp.InvalidArgument, "no service data provided")
	} else if req.Service.Id == "" {
		return nil, twirp.NewError(twirp.InvalidArgument, "must provide id of service to update")
	}

	id, err := strconv.Atoi(req.Service.Id)
	if err != nil {
		return nil, twirp.NewError(twirp.InvalidArgument, "could not convert service id to int "+req.Service.Id)
	}

	service, err := s.DB.ServiceByID(ctx, id)
	if err == db.ErrServiceNotFound {
		return nil, twirp.NewError(twirp.InvalidArgument, "service "+req.Service.Id+" does not exist")
	} else if err != nil {
		return nil, twirp.NewError(twirp.Internal, "unable to fetch service for update")
	}

	// validate current ownership
	if !ldap.BelongsToGroup(ctx, service.LDAPGroup) {
		return nil, twirp.NewError(twirp.PermissionDenied, "user must belong to ldap group of created service")
	}

	// validate target ownership, if changing
	if req.Service.LdapGroup != "" {
		if !ldap.BelongsToGroup(ctx, req.Service.LdapGroup) {
			return nil, twirp.NewError(twirp.PermissionDenied, "user must belong to updated ldap group")
		}

		if err = validator.Validate(req.Service.LdapGroup, isAllowedLDAPGroup); err != nil {
			return nil, twirp.NewError(twirp.PermissionDenied, "must specify allowed ldap group")
		}
	}

	err = validator.ValidateWithError(req.Service.GetServiceCatalogUrl(), isValidServiceCatalogURL)
	if err != nil {
		return nil, twirp.NewError(twirp.InvalidArgument, "invalid service catalog url "+err.Error())
	}

	serviceCatalogResp, err := s.serviceCatalogRequest(ctx, serviceCatalogID(req.Service.ServiceCatalogUrl))
	if err != nil {
		return nil, twirp.NewError(twirp.Internal, "could not get service details from service catalog")
	}

	if serviceCatalogResp.Name == "" {
		return nil, twirp.NewError(twirp.InvalidArgument, "cannot create a service without a valid name, please make sure your service has a valid name in the service catalog")
	}

	updateServiceModel(service, req.Service, serviceCatalogResp)

	editableService := &db.ServiceEditable{
		ServiceCatalogID: service.ServiceCatalogID,
		Name:             service.Name,
		LDAPGroup:        service.LDAPGroup,
		Description:      service.Description,
	}

	_, err = s.DB.ServiceUpdate(ctx, id, editableService)
	if err != nil {
		return nil, twirperr.Convert(errors.Wrap(err, "could not update service "+strconv.Itoa(id)))
	}

	accounts, err := s.DB.AccountsByServiceID(ctx, id)
	if err != nil {
		return nil, errors.Wrap(err, "could not get accounts for service "+strconv.Itoa(id))
	}

	iamRoles, err := s.DB.IAMRolesByServiceID(ctx, id)
	if err != nil {
		return nil, errors.Wrap(err, "could not get IAM roles for service "+strconv.Itoa(id))
	}

	return &rpc.UpdateServiceResp{
		Service: genServiceProto(service, accounts, iamRoles, serviceCatalogResp),
	}, nil
}

func (s *ServicesService) GetAuthorizedFieldGrants(ctx context.Context, req *rpc.GetAuthorizedFieldGrantsReq) (*rpc.GetAuthorizedFieldGrantsResp, error) {
	serviceID, err := strconv.Atoi(req.ServiceId)
	if err != nil {
		return nil, twirp.NewError(twirp.InvalidArgument, "invalid service id")
	}
	service, err := s.DB.ServiceByID(ctx, serviceID)
	if err == db.ErrServiceNotFound {
		return nil, twirp.NewError(twirp.InvalidArgument, "service "+req.ServiceId+" does not exist")
	} else if err != nil {
		return nil, errors.Wrap(err, "could not fetch service with id "+req.ServiceId)
	} else if !ldap.BelongsToGroup(ctx, service.LDAPGroup) {
		return nil, twirp.NewError(twirp.PermissionDenied, "access denied")
	}

	iamRoles, err := s.DB.IAMRolesByServiceID(ctx, serviceID)
	if err != nil {
		return nil, errors.Wrap(err, "unable to get iam roles for service")
	}

	subGrants := []*rpc.AuthorizedFieldSubscriberGrant{}
	pubGrants := []*rpc.AuthorizedFieldPublisherGrant{}
	for _, iamRole := range iamRoles {
		subs, err := s.DB.AuthorizedFieldSubscriberGrantsByIAMRoleID(ctx, iamRole.ID)
		if err != nil {
			return nil, errors.Wrap(err, "unable to get subscriber grants for iam role "+iamRole.ARN)
		}
		sG, err := s.genSubscriberGrantsProto(ctx, subs, iamRole)
		if err != nil {
			return nil, errors.Wrap(err, "error formatting subscribers response")
		}
		subGrants = append(subGrants, sG...)

		pubs, err := s.DB.AuthorizedFieldPublisherGrantsByIAMRoleID(ctx, iamRole.ID)
		if err != nil {
			return nil, errors.Wrap(err, "unable to get publisher grants for iam role "+iamRole.ARN)
		}
		pG, err := s.genPublisherGrantsProto(ctx, pubs, iamRole)
		if err != nil {
			return nil, errors.Wrap(err, "error formatting publishersresponse")
		}
		pubGrants = append(pubGrants, pG...)
	}

	return &rpc.GetAuthorizedFieldGrantsResp{
		SubscriberGrants: subGrants,
		PublisherGrants:  pubGrants,
	}, nil
}

// RequestorLDAPGroups returns the list of LDAP groups the API caller belongs to. Useful for populating
// dropdown list options in the dashboard UI.
func (s *ServicesService) RequestorLDAPGroups(ctx context.Context, req *rpc.RequestorLDAPGroupsReq) (*rpc.RequestorLDAPGroupsResp, error) {
	var allowedGroups []string
	for _, group := range ldap.Groups(ctx) {
		if isAllowedLDAPGroup(group) {
			allowedGroups = append(allowedGroups, group)
		}
	}

	return &rpc.RequestorLDAPGroupsResp{
		LdapGroups: allowedGroups,
	}, nil
}

func (s *ServicesService) ResolveLDAPGroupUsers(ctx context.Context, req *rpc.ResolveLDAPGroupUsersReq) (*rpc.ResolveLDAPGroupUsersResp, error) {
	if !strings.HasPrefix(req.GetLdapGroup(), "team-") {
		return nil, twirp.NewError(twirp.InvalidArgument, "LDAP group must be non-empty and start with 'team-'")
	}
	users, err := s.LDAPManager.UsersInGroup(req.GetLdapGroup())
	if err != nil {
		return nil, err
	}

	protoUsers := []*rpc.LDAPEntry{}
	for _, user := range users {
		protoUsers = append(protoUsers, &rpc.LDAPEntry{
			CommonName: user.CN,
			Uid:        user.UID,
		})
	}

	return &rpc.ResolveLDAPGroupUsersResp{
		LdapEntries: protoUsers,
	}, nil
}

func updateServiceModel(model *db.Service, api *rpc.Service, serviceCatalogResp *servicecatalog.Response) {
	updateString := func(dest *string, src string) {
		if src != "" && src != *dest {
			*dest = src
		}
	}

	updateString(&model.Name, serviceCatalogResp.Name)
	updateString(&model.ServiceCatalogID, serviceCatalogID(api.ServiceCatalogUrl))
	updateString(&model.Description, serviceCatalogResp.Description)
	updateString(&model.LDAPGroup, api.LdapGroup)
}

func (s *ServicesService) serviceCatalogRequest(ctx context.Context, serviceCatalogID string) (*servicecatalog.Response, error) {
	log := logger.FromContext(ctx)
	serviceResp, err := s.ServiceCatalog.Get(ctx, serviceCatalogID)
	if err == servicecatalog.ErrDoesNotExist {
		return &servicecatalog.Response{}, nil
	} else if err != nil {
		log.Warn("could not pull information from service catalog", zap.String("service catalog id", serviceCatalogID), zap.Error(err))
		return nil, err
	}
	return serviceResp, nil
}

func genServiceProto(model *db.Service, dbAccounts []*db.Account, iamRoles []*db.IAMRole, serviceCatalogResp *servicecatalog.Response) *rpc.Service {
	rpcAccounts := make([]*rpc.Account, 0)
	for _, account := range dbAccounts {
		rpcAccounts = append(rpcAccounts, &rpc.Account{
			Id:                   account.AWSAccountID,
			Label:                account.Label,
			CloudformationStatus: account.CloudformationStatus,
		})
	}

	rpcIAMRoles := make([]*rpc.IAMRole, 0)
	for _, iamRole := range iamRoles {
		rpcIAMRoles = append(rpcIAMRoles, genIAMRoleProto(iamRole))
	}

	return &rpc.Service{
		Id:                strconv.Itoa(model.ID),
		Name:              serviceCatalogResp.Name,
		ServiceCatalogUrl: servicecatalog.FormatURL(model.ServiceCatalogID),
		Description:       serviceCatalogResp.Description,
		LdapGroup:         model.LDAPGroup,
		Accounts:          rpcAccounts,
		IamRoles:          rpcIAMRoles,
	}
}

func (s *ServicesService) genSubscriberGrantsProto(ctx context.Context, model []*db.AuthorizedFieldSubscriberGrant, iamRole *db.IAMRole) ([]*rpc.AuthorizedFieldSubscriberGrant, error) {
	var protos []*rpc.AuthorizedFieldSubscriberGrant
	for _, grant := range model {
		authField, err := s.DB.AuthorizedFieldByID(ctx, grant.AuthorizedFieldID)
		if err != nil {
			return nil, errors.Wrap(err, "error looking up authorized field")
		}
		eventStream, err := s.DB.EventStreamByID(ctx, authField.EventStreamID)
		if err != nil {
			return nil, errors.Wrap(err, "error looking up event stream")
		}
		eventType, err := s.DB.EventTypeByID(ctx, eventStream.EventTypeID)
		if err != nil {
			return nil, errors.Wrap(err, "error looking up event type")
		}
		protos = append(protos, &rpc.AuthorizedFieldSubscriberGrant{
			IamRole:     genIAMRoleProto(iamRole),
			EventType:   eventType.Name,
			Environment: eventStream.Environment,
			AuthorizedField: &rpc.AuthorizedField{
				MessageName: authField.MessageName,
				FieldName:   authField.FieldName,
			},
		})
	}
	return protos, nil
}

func (s *ServicesService) genPublisherGrantsProto(ctx context.Context, model []*db.AuthorizedFieldPublisherGrant, iamRole *db.IAMRole) ([]*rpc.AuthorizedFieldPublisherGrant, error) {
	var protos []*rpc.AuthorizedFieldPublisherGrant
	for _, grant := range model {
		eventStream, err := s.DB.EventStreamByID(ctx, grant.EventStreamID)
		if err != nil {
			return nil, errors.Wrap(err, "error looking up event stream")
		}
		eventType, err := s.DB.EventTypeByID(ctx, eventStream.EventTypeID)
		if err != nil {
			return nil, errors.Wrap(err, "error looking up event type")
		}
		protos = append(protos, &rpc.AuthorizedFieldPublisherGrant{
			IamRole:     genIAMRoleProto(iamRole),
			EventType:   eventType.Name,
			Environment: eventStream.Environment,
		})
	}
	return protos, nil
}

func (s *ServicesService) DeleteIAMRole(ctx context.Context, req *rpc.DeleteIAMRoleReq) (*rpc.DeleteIAMRoleResp, error) {
	iamRole, err := s.DB.IAMRoleByARN(ctx, req.GetArn())
	if err != nil {
		return nil, twirperr.Convert(err)
	}

	service, err := s.DB.ServiceByID(ctx, iamRole.ServiceID)
	if err != nil {
		return nil, twirperr.Convert(err)
	}

	if !ldap.BelongsToGroup(ctx, service.LDAPGroup) {
		permDeniedErr := twirp.NewError(twirp.PermissionDenied, "access denied")

		iamRole, err := s.DB.IAMRoleByARN(ctx, req.GetArn())
		if err != nil {
			base := &auditlog.BaseLog{
				ResourceName: req.Arn,
				ServiceID:    iamRole.ServiceID,
				DB:           s.DB,
			}
			base.LogIAMRoleDelete(ctx, permDeniedErr)
		}

		return nil, permDeniedErr
	}

	base := &auditlog.BaseLog{
		ResourceName: req.Arn,
		ServiceID:    iamRole.ServiceID,
		DB:           s.DB,
	}

	lease, ctx, err := s.DB.IAMRoleAcquireLease(ctx, iamRole.ID, 10*time.Second)
	if err != nil {
		err = errors.Wrapf(err, "could not acquire lease for deleting iam role %d", iamRole.ID)
		base.LogIAMRoleDelete(ctx, err)
		return nil, err
	}

	publications, err := s.DB.PublicationsByIAMRoleID(ctx, iamRole.ID)
	if err != nil {
		return nil, twirperr.Convert(err)
	} else if len(publications) != 0 {
		return nil, twirp.NewError(twirp.FailedPrecondition, "Cannot remove IAM role with existing publisher permissions")
	}

	authFieldPubGrants, err := s.DB.AuthorizedFieldPublisherGrantsByIAMRoleID(ctx, iamRole.ID)
	if err != nil {
		return nil, twirperr.Convert(err)
	} else if len(authFieldPubGrants) != 0 {
		return nil, twirp.NewError(twirp.FailedPrecondition, "Cannot remove IAM role with existing publisher permissions")
	}

	authFieldSubGrants, err := s.DB.AuthorizedFieldSubscriberGrantsByIAMRoleID(ctx, iamRole.ID)
	if err != nil {
		return nil, twirperr.Convert(err)
	} else if len(authFieldSubGrants) != 0 {
		return nil, twirp.NewError(twirp.FailedPrecondition, "Cannot remove IAM role with existing authorized field subscriber grants")
	}

	log := logger.FromContext(ctx)
	defer func() {
		if err := s.DB.IAMRoleReleaseLease(lease); err != nil {
			log.Error("could not release iam role lease", zap.Error(err))
		}
	}()

	err = s.DB.IAMRoleDelete(ctx, lease, iamRole.ID)
	if err != nil {
		err = errors.Wrap(err, "could not delete iam role")
		base.LogIAMRoleDelete(ctx, err)
		return nil, err
	}

	base.LogIAMRoleDelete(ctx, nil)
	return &rpc.DeleteIAMRoleResp{}, nil
}

func (s *ServicesService) GetServicesForEventStream(ctx context.Context, req *rpc.GetServicesForEventStreamReq) (*rpc.GetServicesForEventStreamResp, error) {
	services, err := s.DB.ServicesSubscribedToEventType(ctx, req.EventType)
	if err != nil {
		return nil, twirp.NewError(twirp.Internal, "could not get services for provided event type")
	}

	mu := &sync.Mutex{}
	g, ctx := errgroup.WithContext(ctx)
	serviceProtos := make([]*rpc.Service, 0)
	for _, service := range services {
		service := service // avoid concurrency issue with go routines starting at random times

		g.Go(func() error {
			serviceCatalogResp, err := s.serviceCatalogRequest(ctx, service.ServiceCatalogID)
			if err != nil {
				return err
			}

			mu.Lock()
			serviceProtos = append(serviceProtos, genServiceProto(service, nil, service.IAMRoles, serviceCatalogResp))
			mu.Unlock()
			return nil
		})
	}

	if err := g.Wait(); err != nil {
		return nil, twirp.NewError(twirp.Unavailable, "could not get service details from service catalog: "+err.Error())
	}

	return &rpc.GetServicesForEventStreamResp{
		Services: serviceProtos,
	}, nil
}

func (s *ServicesService) GetCloudFormationVersion(ctx context.Context, req *rpc.GetCloudFormationVersionReq) (*rpc.GetCloudFormationVersionResp, error) {
	acctID, err := arn.AccountID(req.GetIamRole().GetArn())
	if err != nil {
		return nil, errors.Wrap(err, "invalid iam role arn")
	}

	version, err := s.CloudformationManager.GetVersion(ctx, acctID)
	if err != nil {
		version = "unknown"
	}

	return &rpc.GetCloudFormationVersionResp{
		Version: version,
	}, nil
}

func (s *ServicesService) CreateIAMRole(ctx context.Context, req *rpc.CreateIAMRoleReq) (*rpc.CreateIAMRoleResp, error) {
	serviceID, err := strconv.Atoi(req.GetServiceId())
	if err != nil {
		return nil, errors.Wrap(err, "invalid service id")
	}
	service, err := s.DB.ServiceByID(ctx, serviceID)
	if err != nil {
		return nil, errors.Wrap(err, "could not get service")
	}

	if !ldap.BelongsToGroup(ctx, service.LDAPGroup) && !ldap.BelongsToGroup(ctx, "team-eventbus") {
		return nil, twirp.NewError(twirp.PermissionDenied, "access denied")
	}

	err = validator.Validate(req.GetIamRole().GetArn(), validator.IsValidIAMRoleARN)
	if err != nil {
		return nil, twirp.NewError(twirp.InvalidArgument, "invalid iam role "+err.Error())
	}

	err = validator.Validate(req.GetIamRole().GetLabel(), validator.IsValidIAMRoleLabel)
	if err != nil {
		return nil, twirp.NewError(twirp.InvalidArgument, "invalid iam role label "+err.Error())
	}

	var grantID string
	if !s.DisableGrants {
		grantID, err = s.EncryptionAtRestManager.GrantEncryptionAtRest(ctx, req.IamRole.GetArn())
		if err != nil {
			return nil, errors.Wrap(err, "unable to create encryption at rest grant")
		}
	}

	id, err := s.DB.IAMRoleCreate(ctx, &db.IAMRole{
		ARN:        req.GetIamRole().GetArn(),
		Label:      req.GetIamRole().GetLabel(),
		ServiceID:  serviceID,
		KMSGrantID: grantID,
	})
	if err != nil {
		return nil, twirperr.Convert(err)
	}

	base := &auditlog.BaseLog{
		ResourceName: req.GetIamRole().GetArn(),
		ServiceID:    serviceID,
		DB:           s.DB,
	}
	base.LogIAMRoleCreate(ctx, nil, &db.IAMRole{
		ID:        id,
		ARN:       req.GetIamRole().GetArn(),
		Label:     req.GetIamRole().GetLabel(),
		ServiceID: serviceID,
	})

	req.GetIamRole().Id = strconv.Itoa(id)
	return &rpc.CreateIAMRoleResp{
		IamRole: req.IamRole,
	}, nil
}

func (s *ServicesService) UpdateIAMRoleLabel(ctx context.Context, req *rpc.UpdateIAMRoleLabelReq) (*rpc.UpdateIAMRoleLabelResp, error) {
	iamRole, err := s.DB.IAMRoleByARN(ctx, req.GetArn())
	if err != nil {
		return nil, twirp.InternalError(err.Error())
	}

	service, err := s.DB.ServiceByID(ctx, iamRole.ServiceID)
	if err != nil {
		return nil, errors.Wrap(err, "could not get service")
	}

	if !ldap.BelongsToGroup(ctx, service.LDAPGroup) && !ldap.BelongsToGroup(ctx, "team-eventbus") {
		return nil, twirp.NewError(twirp.PermissionDenied, "access denied")
	}

	editable := &db.IAMRoleEditable{
		Label: req.GetLabel(),
	}

	_, err = s.DB.IAMRoleUpdate(ctx, iamRole.ID, editable)
	if err != nil {
		return nil, twirperr.Convert(err)
	}

	return &rpc.UpdateIAMRoleLabelResp{
		Arn:   req.GetArn(),
		Label: editable.Label,
	}, nil
}

func (s *ServicesService) ValidateIAMRole(ctx context.Context, req *rpc.ValidateIAMRoleReq) (*rpc.ValidateIAMRoleResp, error) {
	iamRole, err := s.DB.IAMRoleByARN(ctx, req.GetArn())
	if err != nil {
		return nil, twirperr.Convert(err)
	}

	service, err := s.DB.ServiceByID(ctx, iamRole.ServiceID)
	if err != nil {
		return nil, twirperr.Convert(err)
	}

	if !ldap.BelongsToGroup(ctx, service.LDAPGroup) && !ldap.BelongsToGroup(ctx, ldap.CheatCodesEnabled) {
		return nil, twirp.NewError(twirp.PermissionDenied, "access denied")
	}

	// Validation of IAM roles does not make sense in environments where grants are not tracked
	if s.DisableGrants {
		return &rpc.ValidateIAMRoleResp{
			IsValid: true,
		}, nil
	}

	v := &validation.IAMRole{
		IAMRole:                      iamRole,
		EncryptionAtRestGrantFetcher: s.EncryptionAtRestManager,
	}

	report, err := v.Validate(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "could not validate iam role")
	}

	resp := &rpc.ValidateIAMRoleResp{}
	if report.Status != validation.StatusOk {
		resp.IsValid = false
		resp.Message = report.Message
	} else {
		resp.IsValid = true
	}

	return resp, nil
}

func genIAMRoleProto(iamRole *db.IAMRole) *rpc.IAMRole {
	return &rpc.IAMRole{
		Id:                   strconv.Itoa(iamRole.ID),
		Arn:                  iamRole.ARN,
		Label:                iamRole.Label,
		CloudformationStatus: iamRole.CloudformationStatus,
	}
}

func isAllowedLDAPGroup(ldapGroup string) bool {
	return strings.HasPrefix(ldapGroup, "team-")
}

func isValidServiceCatalogURL(url string) (bool, error) {
	matched, err := regexp.MatchString(ServiceCatalogURLRegex, url)
	if err != nil {
		return false, err
	}

	return matched, nil
}

func isValidServiceName(name string) bool {
	return len(name) > ServiceNameMinLength &&
		len(name) < ServiceNameMaxLength &&
		validator.IsValidVisibleASCII(name)
}

func isValidServiceDescription(desc string) bool {
	return len(desc) > ServiceDescriptionMinLength &&
		len(desc) < ServiceDescriptionMaxLength &&
		validator.IsMultilineVisible(desc)
}

// Assumes that the input url is a valid service catalog url
func serviceCatalogID(validServiceCatalogURL string) string {
	tokens := strings.Split(validServiceCatalogURL, "/")
	return tokens[4]
}
