package resource

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/sns"
	"github.com/pkg/errors"

	"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/internal/arn"
	"code.justin.tv/eventbus/controlplane/internal/e2eaccounts"
	"code.justin.tv/eventbus/controlplane/internal/policy"

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

const publicationRetries = 4

var _ test.Runner = &Publication{}

type Publication struct {
	test.TestRunner `json:"-"`

	EventType   string `json:"event_type"`
	Environment string `json:"environment"`

	// Use ServiceCatalogID for legacy account-based publications
	ServiceCatalogID string `json:"service_catalog_id"`

	// Use IAMRole for IAM role-based publication auth
	IAMRole string `json:"iam_role"`

	// stashed account IDs from setup for testing if publish permissions are granted in SNS policy
	// applicable to both IAM role and account based publications
	accountIDs []string
}

func (p *Publication) TestName() string {
	id := p.ServiceCatalogID
	if p.IAMRole != "" {
		id = p.IAMRole
	}
	return fmt.Sprintf("Publication{%s,%s,%s}", p.EventType, p.Environment, id)
}

func (p *Publication) Setup(ctx context.Context) report.Error {
	ctx = e2eutil.AppendTestPath(ctx, p.TestName())
	// Get the ID by finding the service with the right name
	p.Log(ctx, "Setting up publication")

	if p.ServiceCatalogID != "" {
		return p.accountSetup(ctx)
	}

	if p.IAMRole != "" {
		return p.iamRoleSetup(ctx)
	}

	return report.ErrorFromContext(ctx, "missing service catalog ID or IAM role in publication", errors.New("invalid publication"))

}

func (p *Publication) accountSetup(ctx context.Context) report.Error {
	service, err := httpserver.Service(p.ServiceCatalogID)
	if err != nil {
		return report.ErrorFromContext(ctx, "Could not find service in database for publication", errors.New("service not found"))
	}

	// remember the account IDs for the service so we can check them in the test phase
	p.accountIDs = make([]string, len(service.Accounts))
	for i := range service.Accounts {
		p.accountIDs[i] = service.Accounts[i].Id
	}

	// Now we make the allow publish access call
	infraClient := infraRPC.NewInfrastructureProtobufClient(expected.TwirpEndpoint, e2eutil.HTTPClientWithLDAP())
	var pubResp *infraRPC.AllowAccountsPublishResp

	err = e2eutil.RetryBackoff("allow accounts publish", publicationRetries, func() (err error) {
		pubResp, err = infraClient.AllowAccountsPublish(ctx, &infraRPC.AllowAccountsPublishReq{
			EventType:     e2eutil.Suffix(p.EventType, e2eutil.JobID(ctx)),
			Environment:   p.Environment,
			ServiceId:     service.Id,
			AwsAccountIds: p.accountIDs,
		})
		return err
	})
	if err != nil {
		return report.ErrorFromContext(ctx, "Could not create publication", err)
	} else if !pubResp.GetAdded() {
		p.Log(ctx, "WARNING No publication allowance added in Publication seed")
	}

	return nil
}

func (p *Publication) iamRoleSetup(ctx context.Context) report.Error {
	infraClient := infraRPC.NewInfrastructureProtobufClient(expected.TwirpEndpoint, e2eutil.HTTPClientWithLDAP())

	err := e2eutil.RetryBackoff("allow IAM role publish", publicationRetries, func() (err error) {
		_, err = infraClient.AllowIAMRolePublish(ctx, &infraRPC.AllowIAMRolePublishReq{
			EventType:   e2eutil.Suffix(p.EventType, e2eutil.JobID(ctx)),
			Environment: p.Environment,
			IamRole:     p.IAMRole,
		})
		return err
	})
	if err != nil {
		return report.ErrorFromContext(ctx, "Could not create publication", err)
	}

	// get the account ID from the IAM role, since this is what permission will be based on
	acctID, err := arn.AccountID(p.IAMRole)
	if err != nil {
		return report.ErrorFromContext(ctx, "invalid publication IAM role", err)
	}
	p.accountIDs = append(p.accountIDs, acctID)
	return nil
}

func (p *Publication) Test(ctx context.Context) {
	ctx = e2eutil.AppendTestPath(ctx, p.TestName())
	p.testSNSPublisherPolicy(ctx)
}

func (p *Publication) testSNSPublisherPolicy(ctx context.Context) {
	ctx = e2eutil.AppendTestPath(ctx, "SNSPublisherPolicy")
	sess := session.Must(session.NewSession())
	snsClient := sns.New(sess, e2eaccounts.AccountCredentials().MainConfig())
	topicARN := expected.SNSTopicARN(e2eutil.JobID(ctx), p.EventType, p.Environment)
	attrs, err := snsClient.GetTopicAttributes(&sns.GetTopicAttributesInput{
		TopicArn: aws.String(topicARN),
	})
	if err != nil {
		p.Error(ctx, "Could not get SNS topic policy", err)
		return
	}
	poli := &policy.Policy{}
	err = json.Unmarshal([]byte(aws.StringValue(attrs.Attributes["Policy"])), poli)
	if err != nil {
		p.Error(ctx, "Could not unmarshal SNS topic policy JSON", err)
		return
	}
	statement, pos := poli.FindStatement("give-publishers-publish")
	if pos < 0 {
		p.Error(ctx, "Could not get SNS topic policy statement", fmt.Errorf("no publisher statement found on SNS policy"))
		return
	}
	if statement.Principal == nil || statement.Principal.AWS == nil {
		p.Error(ctx, "Invalid SNS topic pulbisher policy", fmt.Errorf("malformed publisher policy on SNS topic"))
		return
	}
	for _, acctID := range p.accountIDs {
		p.Log(ctx, "Checking publish permission granted to service account")
		if !statement.Principal.AWS.Contains(expected.RootARN(acctID)) {
			p.Error(ctx, "Publisher account not in policy", fmt.Errorf("missing root arn for account '%s' in SNS publisher policy", acctID))
			return
		}
	}
}

func (p *Publication) AccountIDs() []string {
	return p.accountIDs
}

func (p *Publication) Clean(ctx context.Context) []report.Error { return nil }
