/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package store

import (
	"encoding/gob"
	"os"
	"sync"
	"time"
)

import (
	"github.com/dubbogo/gost/log/logger"

	"github.com/hashicorp/golang-lru"
)

type CacheManager struct {
	name         string        // The name of the cache manager
	cacheFile    string        // The file path where the cache is stored
	dumpInterval time.Duration // The duration after which the cache dump
	stop         chan struct{} // Channel used to stop the cache expiration routine
	cache        *lru.Cache    // The LRU cache implementation
	lock         sync.Mutex
	enableDump   bool
}

type Item struct {
	Key   string
	Value any
}

// NewCacheManager creates a new CacheManager instance.
// It initializes the cache manager with the provided parameters and starts a routine for cache dumping.
func NewCacheManager(name, cacheFile string, dumpInterval time.Duration, maxCacheSize int, enableDump bool) (*CacheManager, error) {
	cm := &CacheManager{
		name:         name,
		cacheFile:    cacheFile,
		dumpInterval: dumpInterval,
		stop:         make(chan struct{}),
		enableDump:   enableDump,
	}
	cache, err := lru.New(maxCacheSize)
	if err != nil {
		return nil, err
	}
	cm.cache = cache

	// Check if the cache file exists and load the cache if it does
	if _, err := os.Stat(cacheFile); err == nil {
		if err = cm.loadCache(); err != nil {
			logger.Warnf("Failed to load the cache file:[%s].The err is %v", cm.cacheFile, err)
		}
	}

	if enableDump {
		cm.runDumpTask()
	}

	return cm, nil
}

// Get retrieves the value associated with the given key from the cache.
func (cm *CacheManager) Get(key string) (any, bool) {
	return cm.cache.Get(key)
}

// Set sets the value associated with the given key in the cache.
func (cm *CacheManager) Set(key string, value any) {
	cm.cache.Add(key, value)
}

// Delete removes the value associated with the given key from the cache.
func (cm *CacheManager) Delete(key string) {
	cm.cache.Remove(key)
}

// GetAll returns all the key-value pairs in the cache.
func (cm *CacheManager) GetAll() map[string]any {
	keys := cm.cache.Keys()

	result := make(map[string]any)
	for _, k := range keys {
		result[k.(string)], _ = cm.cache.Get(k)
	}

	return result
}

// loadCache loads the cache from the cache file.
func (cm *CacheManager) loadCache() error {
	cf, err := os.Open(cm.cacheFile)
	if err != nil {
		return err
	}

	decoder := gob.NewDecoder(cf)
	for {
		var it Item
		err = decoder.Decode(&it)
		if err != nil {
			if err.Error() == "EOF" {
				break // Reached end of file
			}
			return err
		}
		// Add the loaded keys to the front of the LRU list
		cm.cache.Add(it.Key, it.Value)
	}

	return cf.Close()
}

// dumpCache dumps the cache to the cache file.
func (cm *CacheManager) dumpCache() error {

	cm.lock.Lock()
	defer cm.lock.Unlock()

	items := cm.GetAll()

	file, err := os.Create(cm.cacheFile)
	if err != nil {
		return err
	}
	defer file.Close()

	encoder := gob.NewEncoder(file)
	for k, v := range items {
		gob.Register(v)
		err = encoder.Encode(&Item{
			Key:   k,
			Value: v,
		})
		if err != nil {
			return err
		}
	}
	return nil
}

func (cm *CacheManager) runDumpTask() {
	go func() {
		ticker := time.NewTicker(cm.dumpInterval)
		for {
			select {
			case <-ticker.C:
				// Dump the cache to the file
				if err := cm.dumpCache(); err != nil {
					// Handle error
					logger.Warnf("Failed to dump cache,the err is %v", err)
				} else {
					logger.Infof("Dumping [%s] caches, latest entries %d", cm.name, cm.cache.Len())
				}
			case <-cm.stop:
				ticker.Stop()
				return
			}
		}
	}()
}

func (cm *CacheManager) StopDump() {
	cm.lock.Lock()
	defer cm.lock.Unlock()
	if cm.enableDump {
		cm.stop <- struct{}{} // Stop the cache dump routine
		cm.enableDump = false
	}
}

// destroy stops the cache dump routine, clears the cache and removes the cache file.
func (cm *CacheManager) destroy() {
	cm.StopDump()    // Stop the cache dump routine
	cm.cache.Purge() // Clear the cache

	// Delete the cache file if it exists
	if _, err := os.Stat(cm.cacheFile); err == nil {
		if err := os.Remove(cm.cacheFile); err == nil {
			logger.Infof("The cacheFile [%s] was cleared", cm.cacheFile)
		}
	}
}
