package publisher

import (
	"context"
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/client"
	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
	"github.com/aws/aws-sdk-go/aws/endpoints"
	"github.com/aws/aws-sdk-go/service/sns"
	"github.com/aws/aws-sdk-go/service/sts"
	"github.com/aws/aws-sdk-go/service/sts/stsiface"
	"github.com/pkg/errors"

	eventbus "code.justin.tv/eventbus/client"
	"code.justin.tv/eventbus/client/encryption"
	sess "code.justin.tv/eventbus/client/internal/session"
	"code.justin.tv/eventbus/client/internal/stsregion"
	"code.justin.tv/eventbus/client/lowlevel/cfnversion"
	"code.justin.tv/eventbus/client/lowlevel/snsclient"
)

// New creates a publisher ready to publish to the event bus.
//
// Creating a publisher can fail if any of the following occur:
//
//  - We could not retrieve ARN configuration from the metadata store
//  - Any of the EventTypes specified are not known message names
//  - Network connection cannot be made to the SNS endpoint
//  - Permissioning issues due to not having properly followed instructions in the eventbus docs
//    https://git-aws.internal.justin.tv/pages/eventbus/docs/getting_started/#run-the-cloudformation-template1
func New(config Config) (*Publisher, error) {
	if config.Session == nil {
		return nil, errors.New("no session information found")
	}

	// The default setting will use the deprecated STS global endpoint. If
	// we detect that the publisher has not correctly set this value to a
	// valid endpoint, we default to the STS regional endpoint.
	stsEndpoint := stsregion.ConfiguredEndpoint(config.Session)
	if stsEndpoint == endpoints.UnsetSTSEndpoint || stsEndpoint == endpoints.LegacySTSEndpoint {
		correctedSession, err := sess.Corrected(config.Session)
		if err != nil {
			return nil, errors.New("failed to copy session configuration for setting sts regional endpoint in publisher")
		}
		config.Session = correctedSession
	}

	stsClient := sts.New(config.Session, &aws.Config{STSRegionalEndpoint: stsEndpoint})
	return newWithAWSClients(config, stsClient, cfnversion.NewFetcher())
}

func newWithAWSClients(config Config, stsClient stsiface.STSAPI, versionChecker cfnversion.Fetcher) (*Publisher, error) {
	if config.RouteFetcher == nil {
		config.RouteFetcher = DefaultRouteFetcher(routeBaseURI)
	}

	meetsNoAssumeRequirement, err := versionChecker.Require(context.Background(), config.Session, "v1.3.0")
	if err != nil {
		return nil, errors.Wrap(err, "unable to determine cross account cloudformation version")
	}

	routes, err := routes(config.RouteFetcher, config.Environment, config.EventTypes)
	if err != nil {
		return nil, err
	}

	// Try no-assume-role sns client first
	var snsClient *sns.SNS
	if meetsNoAssumeRequirement || config.Environment == EnvLocal {
		snsClient = sns.New(config.Session)

		err = publishNoopsToRoutes(snsClient, routes, config.Environment)
		if err != nil {
			snsClient = nil
		}
	}

	// if no-assume-role CFN requirement not met, or noop publishing failed, revert to assume-role sns client
	if snsClient == nil {
		snsClient, err = assumeRoleSNSClient(config.Session, stsClient)
		if err != nil {
			return nil, errors.Wrap(err, "could not initialize sns client")
		}

		// publish a noop to -all- declared topics on startup.
		if err := publishNoopsToRoutes(snsClient, routes, config.Environment); err != nil {
			return nil, err
		}
	}

	authFieldClient, err := encryption.NewAuthorizedFieldClient(config.Session, config.AuthorizedFieldClientOptions...)
	if err != nil {
		return nil, errors.Wrap(err, "failure creating authorized field client")
	}

	p := &Publisher{
		client:                snsclient.New(snsClient),
		routes:                routes,
		environment:           config.Environment,
		authorizedFieldClient: authFieldClient,
	}
	return p, nil
}

// Publisher client marshals and sends messages into the event bus.
type Publisher struct {
	client                *snsclient.SNSClient
	routes                map[string]*Route
	environment           Environment
	authorizedFieldClient *encryption.AuthorizedFieldClient
}

// Publish a message to the event bus.
// This method will block until the message is queued into the bus or errors.
func (p *Publisher) Publish(ctx context.Context, message eventbus.Message) error {
	route := p.routes[message.EventBusName()]
	if route == nil {
		return errUnregistered
	}
	return p.client.PublishWithEnvironment(ctx, route.Arn, message, string(p.environment))
}

func (p *Publisher) Environment() string {
	return string(p.environment)
}

func (p *Publisher) Encrypter() encryption.Encrypter {
	return p.authorizedFieldClient
}

// DEPRECATED: Close the publisher to free up resources associated.
// It is invalid to publish to this publisher after closing.
func (p *Publisher) Close() error {
	// Currently there is nothing to close on this publisher object, but we can
	// zero out the client to maybe free up some handles.
	p.client = nil

	return nil
}

func assumeRoleARNPublisher(accountID string) string {
	return fmt.Sprintf("arn:aws:iam::%s:role/EventBus/EventBusPublisherV1", accountID)
}

func assumeRoleSNSClient(sess client.ConfigProvider, stsClient stsiface.STSAPI) (*sns.SNS, error) {
	callerIdentity, err := stsClient.GetCallerIdentity(&sts.GetCallerIdentityInput{})
	if err != nil {
		return nil, errors.Wrap(err, "could not request caller identity (GetCallerIdentity) for accessing aws account id, please ensure you have followed the instructions here: https://git-aws.internal.justin.tv/pages/eventbus/docs/getting_started/#run-the-cloudformation-template")
	}
	accountID := aws.StringValue(callerIdentity.Account)
	if accountID == "" {
		return nil, errors.New("no aws account id was found, please ensure you have followed the instructions here: https://git-aws.internal.justin.tv/pages/eventbus/docs/getting_started/#run-the-cloudformation-template")
	}
	creds := stscreds.NewCredentials(sess, assumeRoleARNPublisher(accountID))
	return sns.New(sess, &aws.Config{Credentials: creds}), nil
}
