package hosting

import (
	telemetry "code.justin.tv/amzn/TwitchTelemetry"
	"code.justin.tv/creator-collab/log"
	"code.justin.tv/creator-collab/log/errors"
	"code.justin.tv/live/autohost/internal/hosting/auth"
	"code.justin.tv/live/autohost/internal/hosting/clients/clue"
	"code.justin.tv/live/autohost/internal/hosting/clients/hallpass"
	"code.justin.tv/live/autohost/internal/hosting/clients/liveline"
	"code.justin.tv/live/autohost/internal/hosting/clients/recs"
	"code.justin.tv/live/autohost/internal/hosting/clients/sns"
	"code.justin.tv/live/autohost/internal/hosting/clients/spade"
	"code.justin.tv/live/autohost/internal/hosting/clients/users"
	"code.justin.tv/live/autohost/internal/hosting/config"
	"code.justin.tv/live/autohost/internal/hosting/eventbus"
	"code.justin.tv/live/autohost/internal/hosting/logic"
	"code.justin.tv/live/autohost/internal/hosting/memcached"
	"code.justin.tv/live/autohost/internal/hosting/pdms"
	"code.justin.tv/live/autohost/internal/hosting/redis"
	"code.justin.tv/live/autohost/internal/hosting/server"
	"code.justin.tv/live/autohost/internal/hosting/storage"
	"code.justin.tv/live/autohost/internal/localdynamo"
	"code.justin.tv/live/autohost/internal/metrics"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/endpoints"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
	"github.com/cactus/go-statsd-client/statsd"
)

const awsRegion = "us-west-2"

type Components struct {
	Logic          logic.Logic
	TwirpHandlers  *server.TwirpHandlers
	Logger         log.Logger
	SampleReporter *telemetry.SampleReporter
	EventBusClient eventbus.Client
}

func NewHostingComponents(logicOverride logic.Logic) (*Components, error) {
	conf, err := config.GetConfig()
	if err != nil {
		return nil, err
	}
	env := conf.Environment

	awsSession, err := session.NewSession(&aws.Config{
		Region: aws.String(awsRegion),
		// Use the regional STS endpoint when assuming roles instead of the global endpoint.
		// This reduces latency, and prevents our AWS account from getting flagged with an
		// STSGlobalEndpointDeprecation policy engine violation.
		STSRegionalEndpoint: endpoints.RegionalSTSEndpoint,
	})
	if err != nil {
		return nil, errors.Wrap(err, "creating aws session failed")
	}

	secrets, err := config.LoadSecrets(conf, awsSession)
	if err != nil {
		return nil, err
	}

	var logger log.Logger
	if conf.UseDevelopmentLogger {
		logger = log.NewDevelopmentLogger()
	} else {
		logger = log.NewProductionLogger(secrets.RollbarToken, env)
	}

	sampleReporter := newSampleReporter(conf, logger)

	statsClient, err := statsd.NewNoopClient()
	if err != nil {
		return nil, errors.Wrap(err, "creating noop statsd client failed")
	}

	// Create the logic components that are not shared with the shared objects (logger and statsClient)
	var hostingLogic = logicOverride
	if hostingLogic == nil {
		hostingLogic, err = createHostingLogic(conf, logger, statsClient, sampleReporter)
		if err != nil {
			return nil, err
		}
	}

	// Initialize event bus flows
	var eventBusClient eventbus.Client
	if conf.UseDevelopmentLogger {
		logger.Debug("Using a no-op event bus client. Autohost will not respond to incoming EventBus messages.")
		eventBusClient = eventbus.NewNoopClient()
	} else {
		eventBusClient, err = eventbus.CreateClient(conf.EventBusQueueURL, awsSession, hostingLogic, sampleReporter)
		if err != nil {
			return nil, errors.Wrap(err, "Failed to create EventBus client")
		}
	}

	twirpHandlers := server.NewTwirpHandlers(hostingLogic)

	return &Components{
		Logic:          hostingLogic,
		TwirpHandlers:  twirpHandlers,
		Logger:         logger,
		SampleReporter: sampleReporter,
		EventBusClient: eventBusClient,
	}, nil
}

func createHostingLogic(conf *config.Config, logger log.Logger, statsClient statsd.Statter, sampleReporter *telemetry.SampleReporter) (logic.Logic, error) {
	env := conf.Environment

	instrumentedAWSSession, err := newInstrumentedAWSSession(sampleReporter)
	if err != nil {
		return nil, err
	}

	dynamoClient, err := newDynamoDBClient(conf, instrumentedAWSSession)
	if err != nil {
		return nil, err
	}

	hostingStorage := storage.New(&storage.Params{
		Client:            dynamoClient,
		Logger:            logger,
		HostTableName:     conf.DynamoDB.HostTableName,
		SettingsTableName: conf.DynamoDB.SettingsTableName,
	})

	var setListSNSClient sns.Client
	if runningInDevLikeEnv(env) {
		setListSNSClient = sns.NewNoopClient()
	} else {
		setListSNSClient = sns.NewClient(instrumentedAWSSession, conf.SetListSNSARNV2)
	}

	clueClient, err := clue.NewClient(conf.ClientURL.Clue, statsClient, logger, sampleReporter)
	if err != nil {
		return nil, err
	}

	hallpassClient, err := hallpass.NewClient(conf.ClientURL.Hallpass, statsClient, sampleReporter)
	if err != nil {
		return nil, err
	}

	usersClient, err := users.NewClient(conf.ClientURL.Users, statsClient, sampleReporter)
	if err != nil {
		return nil, err
	}

	hostingAuth := auth.NewAuth(hallpassClient, usersClient)

	var spadeClient spade.Client
	if env == config.EnvProduction {
		spadeClient, err = spade.NewSpadeClient(statsClient, sampleReporter, logger)
		if err != nil {
			return nil, err
		}
	} else {
		spadeClient = spade.NewNoopClient()
	}

	cacheClient, err := memcached.NewCache(conf.Memcached)
	if err != nil {
		return nil, errors.Wrap(err, "Failed to create the memcached client")
	}

	pdmsClient := pdms.CreateClient(conf.PDMSRoleArn, conf.PDMSLambdaArn, instrumentedAWSSession)

	var recsClient recs.Client
	if runningInDevLikeEnv(env) {
		logger.Debug("Using a stub client for Recommendations that returns nothing.")
		recsClient = recs.NewStubClient()
	} else {
		recsClient, err = recs.NewClient(
			conf.S2S2ServiceName,
			conf.TwitchRecs.EndpointURL,
			conf.TwitchRecs.ProductID,
			conf.TwitchRecs.ProxyURL,
			statsClient,
			sampleReporter)

		if err != nil {
			return nil, errors.Wrap(err, "Failed to create recs client")
		}
	}

	livelineClient := liveline.NewClient(conf.ClientURL.Liveline, statsClient, sampleReporter)

	var publisher eventbus.Publisher
	if runningInDevLikeEnv(env) {
		publisher = eventbus.NewNoopPublisher()
		logger.Debug("Using a no-op EventBus publisher. Autohost will not publish EventBus events.")
	} else {
		publisher, err = eventbus.NewPublisher(conf.EventBusPublisherEnvironment, instrumentedAWSSession)
		if err != nil {
			return nil, errors.Wrap(err, "Failed to create eventbus publisher client")
		}
	}

	rateLimiter := redis.NewRateLimiter(&redis.RateLimiterParams{
		RedisAddress:         conf.Redis.Address,
		IsCluster:            conf.Redis.Cluster,
		SampleReporter:       sampleReporter,
		Logger:               logger,
		MaxHostOperations:    conf.RateLimiting.MaxHostOperations,
		HostWindowDuration:   conf.RateLimiting.HostWindowDuration,
		MaxUnhostOperations:  conf.RateLimiting.MaxUnhostOperations,
		UnhostWindowDuration: conf.RateLimiting.UnhostWindowDuration,
	})

	hostingLogic, err := logic.New(&logic.Params{
		EventPublisher:   publisher,
		DB:               hostingStorage,
		SNSClient:        setListSNSClient,
		Auth:             hostingAuth,
		ClueClient:       clueClient,
		SpadeClient:      spadeClient,
		Memcached:        cacheClient,
		PDMSClient:       pdmsClient,
		TwitchRecsClient: recsClient,
		LivelineClient:   livelineClient,
		Logger:           logger,
		SampleReporter:   sampleReporter,
		RateLimiter:      rateLimiter,
	})

	if err != nil {
		return nil, err
	}

	return hostingLogic, nil
}

func runningInDevLikeEnv(env string) bool {
	return env == config.EnvDev || env == config.EnvCI
}

func newDynamoDBClient(conf *config.Config, awsSession *session.Session) (dynamodbiface.DynamoDBAPI, error) {
	if conf.DynamoDB.LocalDynamo {
		return localdynamo.NewLocalDynamoDBClient(conf.DynamoDB.LocalDynamoEndpoint)
	}

	return dynamodb.New(awsSession), nil
}

func newSampleReporter(conf *config.Config, logger log.Logger) *telemetry.SampleReporter {
	return metrics.NewSampleReporter(&metrics.SampleReporterConfig{
		Environment:             conf.Environment,
		Logger:                  logger,
		Region:                  awsRegion,
		RunningInEC2:            conf.RunningInEC2,
		SendMetricsToCloudwatch: conf.EnableCloudWatchMetrics,
		ServiceName:             "Autohost Server",
	})
}

func newInstrumentedAWSSession(sampleReporter *telemetry.SampleReporter) (*session.Session, error) {
	awsSession, err := session.NewSession(&aws.Config{
		Region: aws.String(awsRegion),
		// Use the regional STS endpoint when assuming roles instead of the global endpoint.
		// This reduces latency, and prevents our AWS account from getting flagged with an
		// STSGlobalEndpointDeprecation policy engine violation.
		STSRegionalEndpoint: endpoints.RegionalSTSEndpoint,
	})
	if err != nil {
		return nil, errors.Wrap(err, "creating aws session failed")
	}

	// Add middleware to the AWS session to report metrics to Cloudwatch.
	metricClient := metrics.NewAWSMiddleware(sampleReporter)
	awsSession = metricClient.AddToSession(awsSession)

	return awsSession, nil
}
