package cache

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log/slog"
	"os"
	"sync"
	"time"
)

const storeFileName = "store.json"

// var MemCache Cache
var (
	MemCache *MemoryCache
	log      = slog.New(slog.NewJSONHandler(os.Stdout, nil))
)

func readJSON(fileName string) (map[string][]byte, error) {
	data := make(map[string][]byte)
	file, err := os.Open(fileName)
	if err != nil {
		return data, err
	}
	defer file.Close()
	decoder := json.NewDecoder(file)
	if err := decoder.Decode(&data); err != nil {
		return data, err
	}
	return data, nil
}

func init() {
	data, err := readJSON(storeFileName)
	if err != nil {
		log.Warn("failed to load store from file", "error", err)
	}
	MemCache = &MemoryCache{
		data:    data,
		timeMap: make(map[string]time.Time),
		lock:    &sync.RWMutex{},
	}
	MemCache.StartExpiryRoutine(time.Hour * 24 * 30)
	MemCache.StartBackupRoutine(time.Minute)
}

type MemoryCache struct {
	data    map[string][]byte
	timeMap map[string]time.Time
	lock    *sync.RWMutex
}

// Get a value by key from the cache
func (mc *MemoryCache) Get(key string) (value []byte, err error) {
	var ok bool
	mc.lock.RLock()
	if value, ok = mc.data[key]; !ok {
		err = fmt.Errorf("not found data in mc for the key: %v", key)
	}
	mc.lock.RUnlock()
	return value, err
}

// Update a single value in the cache
func (mc *MemoryCache) Set(key string, value []byte) {
	// no async writing
	mc.lock.Lock()
	mc.data[key] = value
	mc.lock.Unlock()
}

func (mc *MemoryCache) Expire(key string, exp int64) {
	mc.lock.RLock()
	mc.timeMap[key] = time.Now().Add(time.Duration(exp) * time.Second)
	mc.lock.RUnlock()
}

func (mc *MemoryCache) GetAll() (resp map[string][]byte) {
	resp = make(map[string][]byte)
	mc.lock.RLock()
	for k, v := range mc.data {
		resp[k] = v
	}
	mc.lock.RUnlock()
	return
}

func (mc *MemoryCache) GetAllTime() (resp map[string]time.Time) {
	resp = make(map[string]time.Time)
	mc.lock.RLock()
	for k, v := range mc.timeMap {
		resp[k] = v
	}
	mc.lock.RUnlock()
	return
}

func (mc *MemoryCache) RemoveKey(key string) {
	mc.lock.RLock()
	delete(mc.data, key)
	delete(mc.timeMap, key)
	mc.lock.RUnlock()
}

func (mc *MemoryCache) StartExpiryRoutine(n time.Duration) {
	ticker := time.NewTicker(n)
	go func() {
		for {
			<-ticker.C
			// get all
			timeData := mc.GetAllTime()
			// check time
			currentTS := time.Now()
			for k, ts := range timeData {
				if ts.Before(currentTS) {
					// delete exp keys
					mc.RemoveKey(k)
					log.Info("remove by expiry", "key", k)
				}
			}
		}
	}()
}

func (mc *MemoryCache) StartBackupRoutine(n time.Duration) {
	ticker := time.NewTicker(n)
	go func() {
		for {
			<-ticker.C
			// get all
			data := mc.GetAll()
			jsonString, err := json.Marshal(data)
			if err != nil {
				log.Warn("failed to marshal kv store", "error", err)
				continue
			}
			err = ioutil.WriteFile(storeFileName, jsonString, os.ModePerm)
			if err != nil {
				log.Warn("failed to write to json file", "error", err)
				continue
			}
		}
	}()
}