package cache

import (
	"encoding/json"
	"fmt"

	redis "gopkg.in/redis.v5"

	"time"

	"sync"

	"bitbucket.org/nemt/nemt-portal-api/domain"
	"bitbucket.org/nemt/nemt-portal-api/domain/contract"
	"bitbucket.org/nemt/nemt-portal-api/infra/config"
	"bitbucket.org/nemt/nemt-portal-api/infra/errors"
)

var (
	instance *RedisCache
	once     sync.Once
)

// RedisCache implements the CacheManager interface
type RedisCache struct {
	cfg   *config.Config
	redis *redis.Client
}

// Instance returns a CacheManager instance
func Instance(cfg *config.Config) contract.CacheManager {
	once.Do(func() {
		client := redis.NewFailoverClient(&redis.FailoverOptions{
			MasterName:    cfg.Cache.Master,
			SentinelAddrs: []string{fmt.Sprintf("%s:%v", cfg.Cache.Server, cfg.Cache.Port)},
			Password:      cfg.Cache.Pass,
			DB:            cfg.Cache.DB,
			MaxRetries:    10,
		})

		instance = &RedisCache{cfg, client}
	})

	return instance
}

func (r *RedisCache) buildKey(key string) string {
	return fmt.Sprintf("%s-%s", r.cfg.Cache.Prefix, key)
}

// GetItem returns an Item from cache
func (r *RedisCache) GetItem(key string) (data []byte, err error) {
	val, err := r.redis.Get(r.buildKey(key)).Bytes()
	if err == redis.Nil {
		return val, domain.ErrCacheMiss
	} else if err != nil {
		return val, errors.Wrap(err)
	}

	return val, nil
}

// SetItem sets an item in cache
func (r *RedisCache) SetItem(key string, data []byte) error {
	err := r.redis.Set(r.buildKey(key), data, r.cfg.Cache.DefaultExpiration).Err()
	if err != nil {
		return errors.Wrap(err)
	}

	return nil
}

// GetString returns an string from cache
func (r *RedisCache) GetString(key string) (data string, err error) {
	val, err := r.GetItem(key)
	if err == domain.ErrCacheMiss {
		return data, domain.ErrCacheMiss
	} else if err != nil {
		return data, errors.Wrap(err)
	}

	return string(val), nil
}

// SetString sets an item in cache
func (r *RedisCache) SetString(key string, data string) error {
	err := r.SetItem(key, []byte(data))
	if err != nil {
		return errors.Wrap(err)
	}

	return nil
}

// GetStruct returns an struct from cache
func (r *RedisCache) GetStruct(key string, data interface{}) (err error) {
	val, err := r.GetItem(key)
	if err == domain.ErrCacheMiss {
		return domain.ErrCacheMiss
	} else if err != nil {
		return errors.Wrap(err)
	}

	err = json.Unmarshal(val, &data)
	if err != nil {
		return errors.Wrap(err)
	}

	return nil
}

// SetStruct sets an item in cache
func (r *RedisCache) SetStruct(key string, data interface{}) error {
	dataString, err := json.Marshal(data)
	if err != nil {
		return errors.Wrap(err)
	}

	err = r.SetItem(key, dataString)
	if err != nil {
		return errors.Wrap(err)
	}

	return nil
}

// GetExpiration returns the expiration time for a key
func (r *RedisCache) GetExpiration(key string) (expiration time.Duration, err error) {
	expiration, err = r.redis.TTL(r.buildKey(key)).Result()
	if err != nil {
		return expiration, errors.Wrap(err)
	}

	return expiration, nil
}

// SetExpiration sets the expiration time for a key
func (r *RedisCache) SetExpiration(key string, expiration time.Duration) (err error) {
	err = r.redis.Expire(r.buildKey(key), expiration).Err()
	if err != nil {
		return errors.Wrap(err)
	}

	return nil
}
