package logic

import (
	"context"

	"code.justin.tv/live/autohost/internal/hosting/utils/stringslice"

	"code.justin.tv/live/autohost/lib"

	"code.justin.tv/creator-collab/log/errors"
	"code.justin.tv/live/autohost/internal/hosting/clients/recs"
	"code.justin.tv/live/autohost/internal/hosting/clients/spade"
	"code.justin.tv/live/autohost/internal/hosting/utils"
	"code.justin.tv/live/autohost/internal/logfield"
	"code.justin.tv/live/autohost/rpc/hosting"
	uuid "github.com/satori/go.uuid"
)

func (T *Impl) GetEndorsedChannels(
	ctx context.Context,
	userContext *hosting.UserContext,
	targetChannelID string,
	limit int) ([]*hosting.EndorsedChannel, error) {

	if limit == 0 {
		return nil, nil
	}

	endorsementList, err := T.GetList(ctx, targetChannelID)
	if err != nil {
		return nil, err
	} else if len(endorsementList) == 0 {
		return nil, nil
	}

	numEndorsedChannels := utils.MinInt(len(endorsementList), limit, MaxEndorsedChannels)

	if T.shouldOrderWithRecs(ctx, targetChannelID, userContext) {
		return T.getRecsOrderedEndorsedChannels(
			ctx, userContext, targetChannelID, endorsementList, numEndorsedChannels)
	}
	return T.getCreatorOrderedEndorsedChannels(ctx, endorsementList, numEndorsedChannels)
}

func (T *Impl) shouldOrderWithRecs(ctx context.Context, channelID string, userContext *hosting.UserContext) bool {
	settings, err := T.getSettingsWithoutAuth(ctx, channelID)
	if err != nil {
		wrappedErr := errors.Wrap(
			err,
			"checking whether to order endorsed channels with recommendations failed",
			errors.Fields{logfield.ChannelID: channelID})
		T.logger.Error(wrappedErr)

		return true
	}

	if userContext != nil && channelID == userContext.UserId {
		return false
	}

	return settings.Strategy == lib.AutohostStrategyRandom
}

func (T *Impl) getRecsOrderedEndorsedChannels(
	ctx context.Context,
	userContext *hosting.UserContext,
	channelID string,
	endorsementList []string,
	numEndorsedChannels int) ([]*hosting.EndorsedChannel, error) {

	// If we should order by recommendations, but missing a UserContext, order by view count instead.
	if userContext == nil ||
		userContext.DeviceId == "" ||
		userContext.Country == "" ||
		userContext.Platform == "" ||
		len(userContext.Languages) == 0 {
		return T.getViewCountOrderedEndorsedChannels(ctx, endorsementList, numEndorsedChannels)
	}

	endorsedChannels := make([]*hosting.EndorsedChannel, 0, numEndorsedChannels)

	// Retrieve Endorsements list sorted by user affinity
	recommendedItems, recsRequestID, err := T.recs.GetRecommendedChannels(
		ctx, userContext, endorsementList, int64(numEndorsedChannels))
	if err != nil {
		T.logger.Error(errors.Wrap(err, "Failed to retrieve sorted list from recommendation service", errors.Fields{
			logfield.UserID:   userContext.GetUserId(),
			logfield.TargetID: channelID,
		}))
	}

	// Convert recommended items to endorsed channel list
	for _, item := range recommendedItems {
		endorsedChannels = append(endorsedChannels, &hosting.EndorsedChannel{
			ChannelId:       item.GetItemId(),
			ModelTrackingId: item.GetTrackingId(),
		})
	}

	// GetRecommendedChannels may not include all the channels on the endorsement list in its response.
	// If it returned enough for this request, then we're done.
	// If not, we backfill our response with the remaining channels on the list
	if len(endorsedChannels) >= numEndorsedChannels {
		return endorsedChannels[:numEndorsedChannels], nil
	}

	// Backfill the response with the remaining channels on the endorsements list.
	// Order these channels by their view counts.

	backfillChannelIDs := T.getRemainingChannelIDs(endorsementList, endorsedChannels)
	backfillChannelIDs = T.sortChannelsByCCV(ctx, backfillChannelIDs)

	numNeeded := numEndorsedChannels - len(endorsedChannels)
	backfill := make([]*hosting.EndorsedChannel, numNeeded)
	for i := 0; i < numNeeded; i++ {
		backfill[i] = &hosting.EndorsedChannel{
			ChannelId:       backfillChannelIDs[i],
			ModelTrackingId: uuid.NewV4().String(),
		}
	}

	// Recommendations requires us to send a Spade event for each item that we serve
	// that was not provided by GetRecommendedChannels.
	// https://docs.google.com/document/d/1szmiZByK7aXUF3f3ZPN3o6MNUV3FHc6vR3ohHyvW95I/edit#heading=h.xfipcf8ldpze
	for offset, endorsedChannel := range backfill {
		itemPosition := len(endorsedChannels) + offset
		event := spade.InsertedRecommendationTrackingProperties{
			TrackingID:   endorsedChannel.ModelTrackingId,
			RequestID:    recsRequestID,
			ItemType:     recs.Stream,
			ItemID:       endorsedChannel.ChannelId,
			ItemPosition: itemPosition,
			RowPosition:  0,
			SourceType:   "endorsed",
		}
		T.tracking.QueueInsertedRecommendationEvents(T.recs.GetProductID(), event)
	}

	endorsedChannels = append(endorsedChannels, backfill...)
	return endorsedChannels, nil
}

func (T *Impl) getViewCountOrderedEndorsedChannels(ctx context.Context,
	endorsementList []string,
	numEndorsedChannels int) ([]*hosting.EndorsedChannel, error) {
	endorsedChannels := make([]*hosting.EndorsedChannel, 0, numEndorsedChannels)

	viewCountOrderedChannels := T.sortChannelsByCCV(ctx, endorsementList)
	for i := 0; i < numEndorsedChannels; i++ {
		endorsedChannels = append(endorsedChannels, &hosting.EndorsedChannel{
			ChannelId:       viewCountOrderedChannels[i],
			ModelTrackingId: uuid.NewV4().String(),
		},
		)
	}

	return endorsedChannels, nil
}

// getRemainingChannelIDs returns the channel IDs in the endorsements list that are not already
// part of the list of endorsed channels.
//  - endorsementList is the creator's entire endorsement list
//  - endorsedChannels contains the subset of endorsementList that was returned by Recommendations
func (T *Impl) getRemainingChannelIDs(
	endorsementList []string, endorsedChannels []*hosting.EndorsedChannel) []string {

	added := make(map[string]bool, len(endorsedChannels))
	for _, ec := range endorsedChannels {
		added[ec.ChannelId] = true
	}

	remainingChannelIDs := make([]string, 0, len(endorsementList)-len(added))
	for _, channelID := range endorsementList {
		if !added[channelID] {
			remainingChannelIDs = append(remainingChannelIDs, channelID)
		}
	}

	return remainingChannelIDs
}

// Sorts the channels by ccv. If an error occurs, fall back to the given ordering.
func (T *Impl) sortChannelsByCCV(ctx context.Context, channelIDs []string) []string {
	sortedChannelIDs, err := T.liveline.SortChannelsByCCV(ctx, channelIDs)
	if err != nil {
		T.logger.Error(err)

		return channelIDs
	}

	return sortedChannelIDs
}

func (T *Impl) getCreatorOrderedEndorsedChannels(
	ctx context.Context,
	endorsementList []string,
	numEndorsedChannels int) ([]*hosting.EndorsedChannel, error) {

	// Rearrange endorsementList so that live channels appear before offline channels,
	// but we otherwise retain the ordering of the channels.

	liveChannelSet, err := T.liveline.GetLiveChannelSet(ctx, endorsementList)
	if err != nil {
		T.logger.Error(err)

		// Fallback to the given ordering.
		liveChannelSet = map[string]bool{}
	}

	liveChannels := make([]string, 0, len(endorsementList))
	offlineChannels := make([]string, 0, len(endorsementList))
	for _, channelID := range endorsementList {
		if liveChannelSet[channelID] {
			liveChannels = append(liveChannels, channelID)
		} else {
			offlineChannels = append(offlineChannels, channelID)
		}
	}

	orderedList := stringslice.Concat(liveChannels, offlineChannels)
	orderedList = stringslice.Truncate(orderedList, numEndorsedChannels)

	// Convert the channel IDs to EndorsedChannels
	endorsedChannels := make([]*hosting.EndorsedChannel, len(orderedList))
	for i, channelID := range orderedList {
		endorsedChannels[i] = &hosting.EndorsedChannel{
			ChannelId:       channelID,
			ModelTrackingId: uuid.NewV4().String(),
		}
	}

	return endorsedChannels, nil
}
