package memcached

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"code.justin.tv/creator-collab/log/errors"

	"code.justin.tv/live/autohost/internal/hosting/config"

	"code.justin.tv/foundation/gomemcache/memcache"
)

const (
	// DefaultPollInterval is the default poll interval used for auto discovery.
	DefaultPollInterval = 1 * time.Minute

	// DefaultItemExpirationInSeconds is the default expiration time for
	//each item set in the cache
	DefaultItemExpirationInSeconds = 5 * 60 // 5 minutes
)

type memcachedClient struct {
	client            *memcache.Client
	cancelPollingFunc func()
}

func newClient(conf config.Memcached) (*memcachedClient, error) {
	pollInterval := conf.PollInterval
	if pollInterval <= 0 {
		pollInterval = DefaultPollInterval
	}

	var client *memcache.Client
	var cancelPollingFunc func()

	if conf.UseAutoDiscovery == true {
		client, cancelPollingFunc = memcache.Elasticache(conf.ConfigurationEndpoint, pollInterval)
	} else {
		client = memcache.New(conf.ConfigurationEndpoint)
	}
	if client == nil {
		return nil, errors.New("Unable to create memcached client")
	}

	client.Timeout = conf.Timeout
	client.MaxIdleConns(conf.MaxIdleConnections)
	client.Prewarm(conf.PrewarmConnections)

	return &memcachedClient{
		client:            client,
		cancelPollingFunc: cancelPollingFunc,
	}, nil
}

// The result value should be a pointer to a defined variable. Example:
//
// var result String;
// client.Get(ctx.Background(), "key", &result)
//
// If a key was successfully retrieved, this method will populate the result parameter
// and return true with a nil error. If a value was unable to be retrieved for any other
// reason, false will be returned. Cache misses are ignored and can be detected when the
// method returns false, with no error
func (T *memcachedClient) Get(ctx context.Context, key string, result interface{}) (bool, error) {
	item, err := T.client.Get(ctx, key)
	if err != nil {
		// Ignore cache miss errors
		if err == memcache.ErrCacheMiss {
			return false, nil
		}

		return false, errors.Wrap(err, "Failed to retrieve item from cache", errors.Fields{
			"key": key,
		})
	}

	err = json.Unmarshal(item.Value, result)
	if err != nil {
		return false, errors.Wrap(err, fmt.Sprintf("Unable to unmarshal data for key %s", key))
	}

	return true, nil
}

// Deletes an item from the cache, and ignores any cache misses
func (T *memcachedClient) Delete(ctx context.Context, key string) error {
	err := T.client.Delete(ctx, key)
	if err != nil && err != memcache.ErrCacheMiss {
		return errors.Wrap(err, fmt.Sprintf("Failed to delete '%s' from cache", key))
	}

	return nil
}

func (T *memcachedClient) Add(ctx context.Context, key string, value interface{}) error {
	return T._setInternal(ctx, false, DefaultItemExpirationInSeconds, key, value)
}

func (T *memcachedClient) AddWithExpiration(ctx context.Context, key string, value interface{}, expirationInSeconds int32) error {
	return T._setInternal(ctx, false, expirationInSeconds, key, value)
}

func (T *memcachedClient) Set(ctx context.Context, key string, value interface{}) error {
	return T._setInternal(ctx, true, DefaultItemExpirationInSeconds, key, value)
}

func (T *memcachedClient) SetWithExpiration(ctx context.Context, key string, value interface{}, expirationInSeconds int32) error {
	return T._setInternal(ctx, true, expirationInSeconds, key, value)
}

func (T *memcachedClient) _setInternal(ctx context.Context, overwrite bool, expirationInSeconds int32, key string, value interface{}) error {
	bytes, err := json.Marshal(value)
	if err != nil {
		return errors.Wrap(err, fmt.Sprintf("Failed to marshal data when attempting to write to key '%s'", key))
	}

	item := &memcache.Item{
		Expiration: expirationInSeconds,
		Key:        key,
		Value:      bytes,
		Flags:      0,
	}

	if overwrite {
		err = T.client.Set(ctx, item)
	} else {
		err = T.client.Add(ctx, item)
	}

	if err != nil {
		return errors.Wrap(err, fmt.Sprintf("Unable to write an item to key '%s'", key))
	}

	return nil
}

// Close clears connections to memcached instances.
func (T *memcachedClient) Close() error {
	// Stop the AutoDiscovery polling
	if T.cancelPollingFunc != nil {
		T.cancelPollingFunc()
	}

	T.client.Close()
	return nil
}

// FlushAll clears all data in the memcached instance.
func (T *memcachedClient) FlushAll() error {
	err := T.client.FlushAll()
	if err != nil {
		return errors.Wrap(err, "Failed to flush all data")
	}

	return nil
}
