package event

import (
	"context"
	"encoding/base64"
	"fmt"
	"math/rand"
	"net"
	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/bradfitz/gomemcache/memcache"
	"github.com/golang/protobuf/proto"
	"github.com/pkg/errors"

	"code.justin.tv/dta/rockpaperscissors/internal/api/contextlogger"
	"code.justin.tv/dta/rockpaperscissors/internal/elasticache"
	pb "code.justin.tv/dta/rockpaperscissors/proto"
)

const (
	maxCacheItemSize = 1048576 // 1MB in bytes
	maxKeyLen        = 250     // bytes
	// max expiration that memcache considers to be relative instead of absolute.
	maxRelativeExpiration = 60 * 60 * 24 * 30
)

// CachingDatastore implements a Datastore with caching.
type CachingDatastore struct {
	datastore Datastore
	cache     elasticache.Memcache
}

// NewCachingDatastore wraps an event Datastore with a memcaching layer.
func NewCachingDatastore(datastore Datastore, cache elasticache.Memcache) *CachingDatastore {
	return &CachingDatastore{
		datastore: datastore,
		cache:     cache,
	}
}

// Put the event into the datastore. Doesn't invalidate cached entries.
func (d *CachingDatastore) Put(ctx context.Context, event *pb.Event) error {
	return d.datastore.Put(ctx, event)
}

// Get an event from the datastore. Doesn't use the cache.
func (d *CachingDatastore) Get(ctx context.Context, uuid []byte) (*pb.Event, error) {
	return d.datastore.Get(ctx, uuid)
}

// Query for events from the datastore with caching.
//
// If a query is for old data (>day) then it gets the max cache time.
func (d *CachingDatastore) Query(ctx context.Context, q *pb.EventDatastoreQuery) (*pb.EventDatastoreQueryResults, error) {
	key, err := d.queryKey(q)
	if err != nil {
		if _, ok := err.(ErrCacheKeyTooLong); !ok {
			return nil, err
		}
		// For "key too long" errors, we just log it and fall back to non-caching.
		// This is so we can get some idea if and how often this might happen to
		// tweak the key we use.
		log.Error(err)
	}

	if key != "" {
		var item *memcache.Item
		item, err = d.cache.Get(key)
		if err == nil {
			var results *pb.EventDatastoreQueryResults
			results, err = d.itemToResults(item)
			if err == nil {
				log.Debugf("Cache hit:  %q", key)
				// TODO: log this error?
				_ = contextlogger.IncContextLogField(ctx, "event_query_cache_hits")
				return results, nil
			}
			log.Error(err)
		} else if err == memcache.ErrCacheMiss {
			log.Debugf("Cache miss: %q", key)
		} else if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
			log.Debug("Timeout getting key from memcache")
		} else if connectErr, ok := err.(*memcache.ConnectTimeoutError); ok {
			log.Debug("Timed out connecting to memcache host ", connectErr.Addr)
		} else {
			log.Error(errors.Wrapf(err, "Failed to get item %q from cache", key))
		}
	}

	results, err := d.datastore.Query(ctx, q)
	if err != nil {
		return nil, err
	}

	if key != "" {
		item, err := d.resultsToItem(key, results, d.queryExpiration(q))
		if err == nil {
			err = d.cache.Add(item)
			if err == memcache.ErrNotStored {
				log.Debugf("Key %q already in cache", key)
			} else if operr, ok := err.(*net.OpError); ok && operr.Timeout() {
				log.Debug("Timeout adding key from memcache")
			} else if connectErr, ok := err.(*memcache.ConnectTimeoutError); ok {
				log.Debug("Timed out connecting to memcache host ", connectErr.Addr)
			} else if err != nil {
				log.Error(errors.Wrapf(err, "Failed to add item %q to cache", key))
			}
		} else {
			log.Error(err)
		}
	}

	return results, nil
}

func (d *CachingDatastore) itemToResults(item *memcache.Item) (*pb.EventDatastoreQueryResults, error) {
	results := &pb.EventDatastoreQueryResults{}
	if err := proto.Unmarshal(item.Value, results); err != nil {
		return nil, errors.Wrapf(err, "Failed to unmarshal item %q from cache", item.Key)
	}
	return results, nil
}

func (d *CachingDatastore) resultsToItem(key string, results *pb.EventDatastoreQueryResults, expiration int32) (*memcache.Item, error) {
	data, err := proto.Marshal(results)
	if err != nil {
		return nil, errors.Wrapf(err, "Failed to marshal item %q for cache", key)
	}
	if len(data) > maxCacheItemSize {
		return nil, errors.Errorf("Value for item %q too large for cache", key)
	}
	return &memcache.Item{
		Key:        key,
		Value:      data,
		Expiration: expiration,
	}, nil
}

// queryExpiration returns how long query results should be cached, in seconds.
// If the query was for stuff more than a day ago, we cache for between 15-30 days.
// Otherwise, we cache for 5-15 minutes.
// The reason why it's random is so everything doesn't expire at the same time.
func (d *CachingDatastore) queryExpiration(q *pb.EventDatastoreQuery) int32 {
	endTime := time.Unix(q.EndSeconds, 0)
	if time.Now().Sub(endTime) > time.Hour*24 {
		halfMonthSeconds := int32(15 * 24 * 60 * 60)
		return halfMonthSeconds + rand.Int31n(halfMonthSeconds)
	}
	return int32(5*60) + rand.Int31n(10*60)
}

// queryKey generates a cache key from query parameters. At most 250 bytes.
func (d *CachingDatastore) queryKey(q *pb.EventDatastoreQuery) (string, error) {
	data, err := proto.Marshal(q)
	if err != nil {
		return "", errors.Wrap(err, "Failed to marshal query")
	}
	key := base64.StdEncoding.EncodeToString(data)

	if len(key) > maxKeyLen {
		return "", ErrCacheKeyTooLong{key}
	}
	return key, nil
}

// ErrCacheKeyTooLong is returned by QueryKey if the key it makes exceeds memcache limits.
type ErrCacheKeyTooLong struct {
	Key string
}

func (e ErrCacheKeyTooLong) Error() string {
	return fmt.Sprintf("Cache key too long: %q", e.Key)
}
