package worker

import (
	"context"
	"fmt"
	"time"

	"code.justin.tv/live/autohost/internal/worker/clock"

	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/database"
	"code.justin.tv/live/autohost/internal/worker/clients/clue"
)

var processDuration = 15 * time.Second

// UnhosterParams is used to create an Unhoster struct
type UnhosterParams struct {
	DB             database.Database
	Logger         log.Logger
	Multiplex      LiveService
	SampleReporter *telemetry.SampleReporter
	Users          UsersService
	Clue           clue.Client
}

// Unhoster gets live channel data in the background
type Unhoster struct {
	db               database.Database
	clue             clue.Client
	liveChannelCheck *liveChannelCheck
	logger           log.Logger
	multiplex        LiveService
	sampleReporter   *telemetry.SampleReporter
	users            UsersService

	liveLogins map[string]bool
}

// NewUnhoster returns a new Unhoster struct
func NewUnhoster(p UnhosterParams) *Unhoster {
	return &Unhoster{
		db:             p.DB,
		multiplex:      p.Multiplex,
		clue:           p.Clue,
		users:          p.Users,
		logger:         p.Logger,
		sampleReporter: p.SampleReporter,

		liveLogins:       map[string]bool{},
		liveChannelCheck: newLiveChannelCheck(clock.NewRealClock()),
	}
}

// Process spawns a go routine to fetch and process live channels
func (u *Unhoster) Process(ctx context.Context) {
	ctx = telemetry.ContextWithOperationName(ctx, "UpdateLiveChannels")
	u.logger.Debug("Starting Unhoster's Process loop")

	for !isDone(ctx) {
		startTime := time.Now()

		err := u.ProcessOnce(ctx)
		if err != nil {
			u.logger.Error(err)
		}

		dur := time.Since(startTime)

		// Send metrics on the loop to CloudWatch metrics.
		sampleReporter := telemetry.SampleReporterWithContext(*u.sampleReporter, ctx)
		availabilityCode := telemetry.AvailabilityCodeSucccess
		if err != nil {
			availabilityCode = telemetry.AvailabilityCodeServerError
		}
		sampleReporter.ReportAvailabilitySamples(availabilityCode)
		sampleReporter.ReportDurationSample(telemetry.MetricDuration, dur)

		if !isDone(ctx) {
			time.Sleep(processDuration - dur)
		}
	}

	u.logger.Debug("Exiting Unhoster's Process loop")
}

// ProcessOnce performs one iteration of processLoop
func (u *Unhoster) ProcessOnce(ctx context.Context) error {
	currLiveCount := len(u.liveLogins)

	// If the stored newLiveLogins is empty, fetch the latest version from the database
	if currLiveCount == 0 {
		loginMap, err := u.db.GetCachedLiveUsers(ctx)
		if err != nil {
			return errors.Wrap(err, "async live channels collector - failed to fetch latest version of live users from the database")
		}

		u.liveLogins = loginMap
	}

	newLiveLogins, err := u.multiplex.LiveChannels(ctx)
	if err != nil {
		return errors.Wrap(err, "async live channels collector - failed to fetch live users from multiplex")
	}
	if len(newLiveLogins) == 0 {
		return errors.New("async live channels collector - failed to fetch any live users from multiplex")
	}
	err = u.liveChannelCheck.requireWithinThreshold(len(newLiveLogins))
	if err != nil {
		return err
	}

	streamUpLogins := make([]string, 0, len(newLiveLogins))
	if len(u.liveLogins) > 0 {
		for login := range newLiveLogins {
			if !u.liveLogins[login] {
				streamUpLogins = append(streamUpLogins, login)
			}
		}
	}

	u.liveLogins = newLiveLogins

	err = u.unhostLiveChannels(ctx, streamUpLogins)
	if err != nil {
		u.logger.Error(err)
	}

	return u.db.CacheLiveUsers(ctx, newLiveLogins)
}

func (u *Unhoster) unhostLiveChannels(ctx context.Context, streamUpLogins []string) error {
	loginIDMap, err := u.users.GetChannelIDs(ctx, streamUpLogins)
	if err != nil {
		return err
	}

	userIDs := make([]string, 0, len(loginIDMap))
	for _, id := range loginIDMap {
		userIDs = append(userIDs, id)
	}

	// Get the hosting status of each channel so that we can reduce the number of unhost calls we make.
	hostMap, err := u.clue.GetHostTargets(ctx, userIDs)
	if err != nil {
		u.logger.Error(err)
	}

	for _, userID := range userIDs {
		// Skip unhosting if we know the channel is not hosting.
		currentHost, hostStatusKnown := hostMap[userID]
		if hostStatusKnown && currentHost == "" {
			continue
		}

		u.logger.Debug(fmt.Sprintf("Stream Up Unhost: %s", userID))

		err := u.clue.Unhost(ctx, userID)
		if err != nil {
			u.logger.Error(errors.Wrap(err, "failed Stream Up Unhost", errors.Fields{
				"id": userID,
			}))

			continue
		}
	}

	return nil
}
