package server

import (
	"context"
	"time"

	"code.justin.tv/live/plucky/api/db"

	"code.justin.tv/live/plucky/api/rpc"
	"github.com/twitchtv/twirp"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func (s *Server) GetItems(ctx context.Context, request *rpc.GetItemsRequest) (*rpc.GetItemsResponse, error) {
	if request == nil {
		return nil, twirp.RequiredArgumentError("GetItemsRequest")
	}
	serviceID := request.ServiceId
	if serviceID == "" {
		return nil, twirp.RequiredArgumentError("ServiceId")
	}
	stageID := request.StageId
	if stageID == "" {
		return nil, twirp.RequiredArgumentError("StageId")
	}
	maxTrendPoints := int(request.MaxTrendPoints)
	if maxTrendPoints == 0 {
		return nil, twirp.RequiredArgumentError("MaxTrendPoints")
	}

	service, err := s.getService(serviceID, stageID)
	if err != nil {
		return nil, err
	}

	queryRange, err := getQueryRange(request.GetStart(), request.GetEnd())
	if err != nil {
		return nil, err
	}

	fetched := s.clock.NowUTC()
	fetchedTimestamp := timestamppb.New(fetched)

	err = s.updateDatabase(ctx, service, queryRange, request.SkipPartialDataRefetch)
	if err != nil {
		s.logger.Error(err)
		return nil, err
	}

	occurrences, err := s.database.GetOccurrences(ctx, serviceID, queryRange)
	if err != nil {
		s.logger.Error(err)
		return nil, err
	}

	bucketSize := getBucketSize(queryRange, maxTrendPoints)
	trendMap := getTrend(queryRange, maxTrendPoints, occurrences)

	latestOccurrences := make([]*rpc.Occurrence, 0)
	seenFingerprints := make(map[string]bool, 0)
	for _, occurrence := range occurrences {
		fingerprint := occurrence.Fingerprint
		if seenFingerprints[fingerprint] {
			continue
		}
		latestOccurrences = append(latestOccurrences, occurrence)
		seenFingerprints[fingerprint] = true
	}

	items := make([]*rpc.Item, 0, len(latestOccurrences))
	for _, lastItem := range latestOccurrences {
		fingerprint := lastItem.Fingerprint
		trendPoints := trendMap[fingerprint]

		item := &rpc.Item{
			Fingerprint: fingerprint,
			Trend: &rpc.Trend{
				Points:     trendPoints,
				BucketSize: int32(bucketSize.Seconds()),
			},
			LastOccurrence: lastItem,
		}
		items = append(items, item)
	}

	startTimestamp := timestamppb.New(queryRange.Start)
	endTimestamp := timestamppb.New(queryRange.End)

	endTime := s.clock.NowUTC()
	queryTimeMs := int32(endTime.Sub(fetched).Milliseconds())

	return &rpc.GetItemsResponse{
		Fetched:     fetchedTimestamp,
		Items:       items,
		Start:       startTimestamp,
		End:         endTimestamp,
		QueryTimeMs: queryTimeMs,
	}, nil
}

func getTrend(queryRange db.TimeRange, maxTrendPoints int, occurrences []*rpc.Occurrence) map[string][]*rpc.TrendPoint {
	bucketSize := getBucketSize(queryRange, maxTrendPoints)

	fingerprintToBucketToCount := make(map[string]map[time.Time]int)

	for _, o := range occurrences {
		bucketToCount, contains := fingerprintToBucketToCount[o.Fingerprint]
		if !contains {
			bucketToCount = make(map[time.Time]int)
			fingerprintToBucketToCount[o.Fingerprint] = bucketToCount
		}

		bucket := getBucketKey(bucketSize, o.Timestamp.AsTime())
		bucketToCount[bucket] = bucketToCount[bucket] + 1
	}

	startBucketKey := getBucketKey(bucketSize, queryRange.Start)
	endBucketKey := getBucketKey(bucketSize, queryRange.End)

	result := make(map[string][]*rpc.TrendPoint)
	for fingerprint, bucketToCount := range fingerprintToBucketToCount {
		points := make([]*rpc.TrendPoint, 0, maxTrendPoints)
		for key := startBucketKey; key.Before(endBucketKey) || key.Equal(endBucketKey); key = key.Add(bucketSize) {
			count := bucketToCount[key]
			points = append(points, &rpc.TrendPoint{
				Start: timestamppb.New(key),
				Count: int32(count),
			})
		}
		result[fingerprint] = points
	}

	return result
}

func getQueryRange(requestStart, requestEnd *timestamppb.Timestamp) (db.TimeRange, error) {
	endTime := time.Now().UTC().Truncate(time.Second)
	if requestEnd != nil {
		parsedTime := requestEnd.AsTime()
		parsedTime = parsedTime.UTC().Truncate(time.Second)
		endTime = parsedTime
	}

	startTime := endTime.Add(-24 * time.Hour)
	if requestStart != nil {
		parsedTime := requestStart.AsTime()
		startTime = parsedTime.UTC().Truncate(time.Second)
	}

	if endTime.Before(startTime) {
		return db.TimeRange{}, twirp.NewError(twirp.InvalidArgument, "StartTime was not after EndTime")
	}

	return db.TimeRange{
		Start: startTime,
		End:   endTime,
	}, nil
}

var bucketSizes = []time.Duration{
	time.Second,
	2 * time.Second,
	5 * time.Second,
	10 * time.Second,
	15 * time.Second,
	20 * time.Second,
	30 * time.Second,
	time.Minute,
	2 * time.Minute,
	5 * time.Minute,
	10 * time.Minute,
	15 * time.Minute,
	20 * time.Minute,
	30 * time.Minute,
	time.Hour,
	6 * time.Hour,
	12 * time.Hour,
	24 * time.Hour,
}

func getBucketKey(bucketSize time.Duration, t time.Time) time.Time {
	return t.Truncate(bucketSize)
}

func getBucketSize(queryRange db.TimeRange, maxTrendPoints int) time.Duration {
	queryRangeDuration := queryRange.End.Sub(queryRange.Start)

	for _, bucketSize := range bucketSizes {
		numBuckets := int(queryRangeDuration / bucketSize)
		if numBuckets <= maxTrendPoints {
			return bucketSize
		}
	}

	return bucketSizes[len(bucketSizes)-1]
}
