package resource

import (
	"context"
	"fmt"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/kms"
	"github.com/pkg/errors"
	"go.uber.org/multierr"

	"code.justin.tv/eventbus/controlplane/e2e/internal/e2eutil"
	"code.justin.tv/eventbus/controlplane/e2e/internal/expected"
	"code.justin.tv/eventbus/controlplane/e2e/internal/httpserver"
	"code.justin.tv/eventbus/controlplane/e2e/internal/report"
	"code.justin.tv/eventbus/controlplane/e2e/internal/test"

	"code.justin.tv/eventbus/controlplane/rpc"
)

var _ test.Runner = &Service{}

type Service struct {
	test.TestRunner

	ServiceCatalogID string `json:"service_catalog_id"`
	Description      string `json:"description"`
	LdapGroup        string `json:"ldap_group"`

	Accounts []*Account `json:"accounts"`
	IAMRoles []*IAMRole `json:"iam_roles"`

	AuthorizedFieldGrants *AuthorizedFieldGrants `json:"authorized_field_grants"`

	JobIdentifier string `json:"-"`

	kmsGrants []*kms.GrantListEntry
}

type Account struct {
	ID    string `json:"id"`
	Label string `json:"label"`
}

type IAMRole struct {
	ARN   string `json:"arn"`
	Label string `json:"label"`
}

type AuthorizedFieldGrants struct {
	PublisherGrants  []*PublisherGrant  `json:"publisher"`
	SubscriberGrants []*SubscriberGrant `json:"subscriber"`
}

type PublisherGrant struct {
	IAMRoleARN  string `json:"iam_role_arn"`
	EventType   string `json:"event_type"`
	Environment string `json:"environment"`
}

func (p *PublisherGrant) MatchesAPIGrant(ctx context.Context, grant *rpc.AuthorizedFieldPublisherGrant) bool {
	return p.IAMRoleARN == grant.IamRole.Arn &&
		p.Environment == grant.Environment &&
		e2eutil.Suffix(p.EventType, e2eutil.JobID(ctx)) == grant.EventType
}

func (p *PublisherGrant) MatchesKMSGrant(ctx context.Context, grant *kms.GrantListEntry) bool {
	if grant == nil {
		return false
	}
	constraints := aws.StringValueMap(grant.Constraints.EncryptionContextSubset)
	return p.IAMRoleARN == aws.StringValue(grant.GranteePrincipal) &&
		p.Environment == constraints[expected.EncryptionContextEnvironment] &&
		e2eutil.Suffix(p.EventType, e2eutil.JobID(ctx)) == constraints[expected.EncryptionContextEventType]
}

func (p *PublisherGrant) ExistsInAPI(ctx context.Context, grants []*rpc.AuthorizedFieldPublisherGrant) bool {
	for _, grant := range grants {
		if p.MatchesAPIGrant(ctx, grant) {
			return true
		}
	}
	return false
}

func (p *PublisherGrant) ExistsInKMS(ctx context.Context, grants []*kms.GrantListEntry) bool {
	for _, grant := range grants {
		if p.MatchesKMSGrant(ctx, grant) {
			return true
		}
	}
	return false
}

func (p *PublisherGrant) String() string {
	return fmt.Sprintf("(%s,%s,%s)", p.IAMRoleARN, p.EventType, p.Environment)
}

type SubscriberGrant struct {
	IAMRoleARN  string `json:"iam_role_arn"`
	EventType   string `json:"event_type"`
	Environment string `json:"environment"`
	MessageName string `json:"message_name"`
	FieldName   string `json:"field_name"`
}

func (s *SubscriberGrant) MatchesAPIGrant(ctx context.Context, grant *rpc.AuthorizedFieldSubscriberGrant) bool {
	return s.IAMRoleARN == grant.IamRole.Arn &&
		s.Environment == grant.Environment &&
		e2eutil.Suffix(s.EventType, e2eutil.JobID(ctx)) == grant.EventType &&
		s.MessageName == grant.AuthorizedField.MessageName &&
		s.FieldName == grant.AuthorizedField.FieldName
}

func (s *SubscriberGrant) MatchesKMSGrant(ctx context.Context, grant *kms.GrantListEntry) bool {
	constraints := aws.StringValueMap(grant.Constraints.EncryptionContextSubset)
	return s.IAMRoleARN == aws.StringValue(grant.GranteePrincipal) &&
		s.Environment == constraints[expected.EncryptionContextEnvironment] &&
		e2eutil.Suffix(s.EventType, e2eutil.JobID(ctx)) == constraints[expected.EncryptionContextEventType] &&
		s.MessageName == constraints[expected.EncryptionContextMessageName] &&
		s.FieldName == constraints[expected.EncryptionContextFieldName]
}

func (s *SubscriberGrant) ExistsInAPI(ctx context.Context, grants []*rpc.AuthorizedFieldSubscriberGrant) bool {
	for _, grant := range grants {
		if s.MatchesAPIGrant(ctx, grant) {
			return true
		}
	}
	return false
}

func (s *SubscriberGrant) ExistsInKMS(ctx context.Context, grants []*kms.GrantListEntry) bool {
	for _, grant := range grants {
		if s.MatchesKMSGrant(ctx, grant) {
			return true
		}
	}
	return false
}

func (s *SubscriberGrant) String() string {
	return fmt.Sprintf("(%s,%s,%s,%s,%s)", s.IAMRoleARN, s.EventType, s.Environment, s.MessageName, s.FieldName)
}

func (s *Service) TestName() string {
	return fmt.Sprintf("Service{%s}", s.ServiceCatalogID)
}

func (s *Service) Setup(ctx context.Context) report.Error {
	ctx = e2eutil.AppendTestPath(ctx, s.TestName())
	serviceClient := rpc.NewServicesProtobufClient(expected.TwirpEndpoint, e2eutil.HTTPClientWithLDAP())
	resp, err := serviceClient.Create(ctx, &rpc.CreateServiceReq{
		Service: &rpc.Service{
			ServiceCatalogUrl: expected.ServiceCatalogURL(s.ServiceCatalogID),
			Description:       s.Description,
			LdapGroup:         s.LdapGroup,
			Accounts:          rpcAccounts(s.Accounts),
		},
	})

	if err != nil {
		return report.ErrorFromContext(ctx, "Could not create service in database", err)
	}

	for _, iamRole := range rpcIAMRoles(s.IAMRoles) {
		_, err := serviceClient.CreateIAMRole(ctx, &rpc.CreateIAMRoleReq{
			IamRole:   iamRole,
			ServiceId: resp.GetService().GetId(),
		})
		if err != nil {
			return report.ErrorFromContext(ctx, "could not create iam role for service", err)
		}
	}

	if s.AuthorizedFieldGrants != nil {
		for _, subGrant := range s.AuthorizedFieldGrants.SubscriberGrants {
			_, err := httpserver.CreateAuthorizedFieldSubscriberGrant(
				e2eutil.JobID(ctx),
				subGrant.EventType,
				subGrant.Environment,
				subGrant.MessageName,
				subGrant.FieldName,
				subGrant.IAMRoleARN,
			)
			if err != nil {
				return report.ErrorFromContext(ctx, "Could not create authorized field subscriber grant in database", err)
			}
		}
	}

	return nil
}

func (s *Service) Test(ctx context.Context) {
	ctx = e2eutil.AppendTestPath(ctx, s.TestName())
	if s.AuthorizedFieldGrants == nil {
		return
	}

	service, err := httpserver.Service(s.ServiceCatalogID)
	if err != nil {
		s.Error(ctx, "could not retrieve service", err)
		return
	}

	serviceClient := rpc.NewServicesProtobufClient(expected.TwirpEndpoint, e2eutil.HTTPClientWithLDAP())
	grants, err := serviceClient.GetAuthorizedFieldGrants(ctx, &rpc.GetAuthorizedFieldGrantsReq{
		ServiceId: service.Id,
	})
	if err != nil {
		s.Error(ctx, "could not retrieve service authorized field grants", err)
		return
	}

	if s.AuthorizedFieldGrants != nil {
		time.Sleep(5 * time.Second) // KMS is eventually consistent, so do a short wait when needed
		s.comparePublisherGrants(ctx, grants.PublisherGrants)
		s.compareSubscriberGrants(ctx, grants.SubscriberGrants)
		s.verifyKMSGrants(ctx)
	}
}

func (s *Service) comparePublisherGrants(ctx context.Context, actualGrants []*rpc.AuthorizedFieldPublisherGrant) {
	if len(s.AuthorizedFieldGrants.PublisherGrants) != len(actualGrants) {
		s.Error(ctx, "incorrect number of authorized field publisher grants", nil)
	}

	s.Log(ctx, "Comparing publisher grants")
	for _, expectedGrant := range s.AuthorizedFieldGrants.PublisherGrants {
		if !expectedGrant.ExistsInAPI(ctx, actualGrants) {
			s.Error(ctx, "could not find authorized field publisher grant in database: "+expectedGrant.String(), nil)
		}
	}
}

func (s *Service) compareSubscriberGrants(ctx context.Context, actualGrants []*rpc.AuthorizedFieldSubscriberGrant) {
	if len(s.AuthorizedFieldGrants.SubscriberGrants) != len(actualGrants) {
		s.Error(ctx, "incorrect number of authorized field subscriber grants", nil)
	}

	s.Log(ctx, "Comparing subscriber grants")
	for _, expectedGrant := range s.AuthorizedFieldGrants.SubscriberGrants {
		if !expectedGrant.ExistsInAPI(ctx, actualGrants) {
			s.Error(ctx, "could not find authorized field subscriber grant in database: "+expectedGrant.String(), nil)
		}
	}
}

func (s *Service) verifyKMSGrants(ctx context.Context) {
	grants, err := e2eutil.KMSGrants(expected.AuthorizedFieldKeyARN)
	if err != nil {
		s.Error(ctx, "could not fetch kms grants", err)
	}
	s.kmsGrants = grants

	for _, subGrant := range s.AuthorizedFieldGrants.SubscriberGrants {
		if !subGrant.ExistsInKMS(ctx, s.kmsGrants) {
			s.Error(ctx, "authorized field subscriber grant does not exist", nil)
		}
	}

	for _, pubGrant := range s.AuthorizedFieldGrants.PublisherGrants {
		if !pubGrant.ExistsInKMS(ctx, s.kmsGrants) {
			s.Error(ctx, "authorized field subscriber grant does not exist", nil)
		}
	}
}

func (s *Service) Clean(ctx context.Context) []report.Error {
	// Find KMS grants relevant to this service
	grantsToRevoke := make([]*kms.GrantListEntry, 0)
	grantFinderErrors := make([]report.Error, 0)
	found := false
	if s.AuthorizedFieldGrants != nil {
		for _, seedGrant := range s.AuthorizedFieldGrants.PublisherGrants {
			for _, actualGrant := range s.kmsGrants {
				if seedGrant.MatchesKMSGrant(ctx, actualGrant) {
					grantsToRevoke = append(grantsToRevoke, actualGrant)
					found = true
					break
				}
			}
			if !found {
				grantFinderErrors = append(grantFinderErrors, report.ErrorFromContext(ctx, "could not find kms grant to revoke", errors.New("kms grant not found")))
			}
			found = false
		}

		for _, seedGrant := range s.AuthorizedFieldGrants.SubscriberGrants {
			for _, actualGrant := range s.kmsGrants {
				if seedGrant.MatchesKMSGrant(ctx, actualGrant) {
					grantsToRevoke = append(grantsToRevoke, actualGrant)
					found = true
					break
				}
			}
			if !found {
				grantFinderErrors = append(grantFinderErrors, report.ErrorFromContext(ctx, "could not find kms grant to revoke", errors.New("kms grant not found")))
			}
			found = false
		}
	}

	if len(grantFinderErrors) != 0 {
		return grantFinderErrors
	}

	errs := e2eutil.KMSRevokeGrants(expected.AuthorizedFieldKeyARN, grantsToRevoke)
	if errs != nil {
		var reportErrors []report.Error
		for _, err := range multierr.Errors(errs) {
			reportErrors = append(reportErrors, report.ErrorFromContext(ctx, "could not revoke kms grant", err))
		}
		return reportErrors
	}
	return nil
}

func rpcAccounts(a []*Account) []*rpc.Account {
	res := make([]*rpc.Account, len(a))
	for i := range a {
		res[i] = &rpc.Account{
			Id:    a[i].ID,
			Label: a[i].Label,
		}
	}
	return res
}

func rpcIAMRoles(roles []*IAMRole) []*rpc.IAMRole {
	res := make([]*rpc.IAMRole, len(roles))
	for i, r := range roles {
		res[i] = &rpc.IAMRole{
			Arn:   r.ARN,
			Label: r.Label,
		}
	}
	return res
}
