package cachehandler

import (
	"bytes"
	"context"
	"encoding/gob"
	"errors"
	"fmt"
	"time"

	"github.com/avast/retry-go"
	redisCache "github.com/go-redis/cache/v8"
	"github.com/go-redis/redis/v8"
	"github.com/patrickmn/go-cache"
	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/panicIf"
	"go.uber.org/zap"
)

type Cache interface {
	Get(funcName string, result interface{}, args ...interface{}) bool
	Set(funcName string, result interface{}, args ...interface{})
	Delete(funcName string, args ...interface{})
	SetWithTimeOut(timeOut time.Duration, funcName string, result interface{}, args ...interface{})
}

type CacheImpl struct {
	c *cache.Cache
}

const DefaultTimeout = 5 * time.Minute

var global Cache = nil

func init() {
	InitWithLocal()
}

func Get() Cache {
	return global
}

func Set(cache Cache) {
	global = cache
}

func Init() {
	client := redis.NewClient(&redis.Options{
		Addr: config.Get().GetString(config.RedisHost),
		DB:   config.Get().GetInt(config.RedisDB),
	})
	global = &RedisCacheImpl{
		c: redisCache.New(
			&redisCache.Options{
				Redis: client,
			},
		),
		cli: client,
	}
}

func InitWithLocal() {
	global = &CacheImpl{
		c: cache.New(DefaultTimeout, 2*DefaultTimeout),
	}
}

const CACHE_KEY_DELIM = "#"

func buildCacheKey(funcName string, args ...interface{}) string {
	cache_key := funcName
	for _, arg := range args {
		cache_key = cache_key + CACHE_KEY_DELIM + fmt.Sprintf("%v", arg)
	}
	return cache_key
}

func (c *CacheImpl) Get(funcName string, value interface{}, args ...interface{}) bool {
	if !config.Get().GetBool(config.EnableCache) {
		return false
	}
	key := buildCacheKey(funcName, args...)
	cacheValue, found := c.c.Get(key)
	if found && value != nil {
		mustUnmarshal(cacheValue.([]byte), value)
	}
	log.Info("get cache", zap.String("key", key), zap.Bool("found", found))
	return found
}

func mustMarshal(obj interface{}) []byte {
	b := bytes.Buffer{}

	err := gob.NewEncoder(&b).Encode(obj)
	panicIf.Error(err)
	return b.Bytes()
}

func mustUnmarshal(data []byte, obj interface{}) {
	b := bytes.NewBuffer(data)

	err := gob.NewDecoder(b).Decode(obj)
	panicIf.Error(err)
}

func (c *CacheImpl) Set(funcName string, value interface{}, args ...interface{}) {
	if !config.Get().GetBool(config.EnableCache) {
		return
	}
	key := buildCacheKey(funcName, args...)
	log.Info("set cache", zap.String("key", key))
	c.c.SetDefault(key, mustMarshal(value))
}

func (c *CacheImpl) Delete(funcName string, args ...interface{}) {
	if !config.Get().GetBool(config.EnableCache) {
		return
	}
	key := buildCacheKey(funcName, args...)
	c.c.Delete(key)
}
func (c *CacheImpl) SetWithTimeOut(timeOut time.Duration, funcName string, value interface{}, args ...interface{}) {
	if !config.Get().GetBool(config.EnableCache) {
		return
	}
	key := buildCacheKey(funcName, args...)
	log.Info("set cache", zap.String("key", key))
	c.c.SetDefault(key, mustMarshal(value))
}

type RedisCacheImpl struct {
	c   *redisCache.Cache
	cli *redis.Client
}

func (impl *RedisCacheImpl) Get(funcName string, value interface{}, args ...interface{}) bool {
	if !config.Get().GetBool(config.EnableCache) {
		return false
	}
	key := buildCacheKey(funcName, args...)
	err := impl.c.Get(context.Background(), key, value)
	found := err == nil
	if found {
		log.Info("cache hit", zap.String("key", key))
	} else if errors.Is(err, redisCache.ErrCacheMiss) {
		log.Info("cache missing", zap.String("key", key))
	} else {
		log.Error("cache missing with unexpected error", zap.String("key", key), zap.Error(err))
		body, err := impl.cli.Get(context.Background(), key).Bytes()
		if err != nil {
			log.Error("failed to get cached body on cache missing with unexpected error", zap.String("key", key), zap.Error(err))
		} else {
			log.Info("success to get cached body on cache missing with unexpected error", zap.String("key", key), zap.Binary("body", body))
		}
	}
	return found
}

func (impl *RedisCacheImpl) Set(funcName string, value interface{}, args ...interface{}) {
	impl.setRedisCache(DefaultTimeout, funcName, value, args...)
}
func (impl *RedisCacheImpl) SetWithTimeOut(timeOut time.Duration, funcName string, value interface{}, args ...interface{}) {
	impl.setRedisCache(timeOut, funcName, value, args...)
}
func (impl *RedisCacheImpl) setRedisCache(timeOut time.Duration, funcName string, value interface{}, args ...interface{}) {
	if !config.Get().GetBool(config.EnableCache) {
		return
	}
	key := buildCacheKey(funcName, args...)
	log.Info("start to set cache", zap.String("key", key))
	err := retry.Do(func() error {
		return impl.c.Set(&redisCache.Item{
			Key:   key,
			Value: value,
			TTL:   timeOut,
		})
	}, retry.Attempts(config.Get().GetUint(config.CacheFailedRetryTimes)))
	if err != nil {
		log.Error("failed to set cache", zap.Error(billing_errors.NewSystemError(err)), zap.String("key", key))
	}
	log.Info("success to set cache", zap.String("key", key))
}

func (impl *RedisCacheImpl) Delete(funcName string, args ...interface{}) {
	if !config.Get().GetBool(config.EnableCache) {
		return
	}
	key := buildCacheKey(funcName, args...)
	log.Info("start to delete cache", zap.String("key", key))
	err := retry.Do(func() error {
		return impl.c.Delete(context.Background(), key)
	}, retry.Attempts(config.Get().GetUint(config.CacheFailedRetryTimes)))
	if err != nil {
		log.Error("failed to delete cache", zap.Error(billing_errors.NewSystemError(err)), zap.String("key", key))
	}
	log.Info("success to delete cache", zap.String("key", key))
}
