// Package file
// @Link  https://github.com/bufanyun/hotgo
// @Copyright  Copyright (c) 2023 HotGo CLI
// @Author  Ms <133814250@qq.com>
// @License  https://github.com/bufanyun/hotgo/blob/master/LICENSE
package file

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/gogf/gf/v2/container/gvar"
	"github.com/gogf/gf/v2/errors/gerror"
	"github.com/gogf/gf/v2/os/gcache"
	"github.com/gogf/gf/v2/os/gfile"
	"github.com/gogf/gf/v2/util/gconv"
	"os"
	"path/filepath"
	"time"
)

type (
	// AdapterFile is the gcache adapter implements using file server.
	AdapterFile struct {
		dir string
	}

	fileContent struct {
		Duration int64       `json:"duration"`
		Data     interface{} `json:"data,omitempty"`
	}
)

const perm = 0o666

var (
	CacheExpiredErr = errors.New("cache expired")
)

// NewAdapterFile creates and returns a new memory cache object.
func NewAdapterFile(dir string) gcache.Adapter {
	return &AdapterFile{
		dir: dir,
	}
}

func (c *AdapterFile) Set(ctx context.Context, key interface{}, value interface{}, lifeTime time.Duration) (err error) {
	fileKey := gconv.String(key)
	if value == nil || lifeTime < 0 {
		return c.Delete(fileKey)
	}
	return c.Save(fileKey, gconv.String(value), lifeTime)
}

func (c *AdapterFile) SetMap(ctx context.Context, data map[interface{}]interface{}, duration time.Duration) (err error) {
	return gerror.New("implement me")
}

func (c *AdapterFile) SetIfNotExist(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (ok bool, err error) {
	return false, gerror.New("implement me")
}

func (c *AdapterFile) SetIfNotExistFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
	return false, gerror.New("implement me")
}

func (c *AdapterFile) SetIfNotExistFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
	return false, gerror.New("implement me")
}

func (c *AdapterFile) Get(ctx context.Context, key interface{}) (*gvar.Var, error) {
	fetch, err := c.Fetch(gconv.String(key))
	if err != nil {
		return nil, err
	}
	return gvar.New(fetch), nil
}

func (c *AdapterFile) GetOrSet(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (result *gvar.Var, err error) {
	result, err = c.Get(ctx, key)
	if err != nil && !errors.Is(err, CacheExpiredErr) {
		return nil, err
	}
	if result.IsNil() {
		return gvar.New(value), c.Set(ctx, key, value, duration)
	}
	return
}

func (c *AdapterFile) GetOrSetFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) {
	v, err := c.Get(ctx, key)
	if err != nil && !errors.Is(err, CacheExpiredErr) {
		return nil, err
	}
	if v.IsNil() {
		value, err := f(ctx)
		if err != nil {
			return nil, err
		}
		if value == nil {
			return nil, nil
		}
		return gvar.New(value), c.Set(ctx, key, value, duration)
	} else {
		return v, nil
	}
}

func (c *AdapterFile) GetOrSetFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) {
	return c.GetOrSetFunc(ctx, key, f, duration)
}

func (c *AdapterFile) Contains(ctx context.Context, key interface{}) (bool, error) {
	return c.Has(gconv.String(key)), nil
}

func (c *AdapterFile) Size(ctx context.Context) (size int, err error) {
	return 0, nil
}

func (c *AdapterFile) Data(ctx context.Context) (data map[interface{}]interface{}, err error) {
	return nil, gerror.New("implement me")
}

func (c *AdapterFile) Keys(ctx context.Context) (keys []interface{}, err error) {
	return nil, gerror.New("implement me")
}

func (c *AdapterFile) Values(ctx context.Context) (values []interface{}, err error) {
	return nil, gerror.New("implement me")
}

func (c *AdapterFile) Update(ctx context.Context, key interface{}, value interface{}) (oldValue *gvar.Var, exist bool, err error) {
	return nil, false, gerror.New("implement me")
}

func (c *AdapterFile) UpdateExpire(ctx context.Context, key interface{}, duration time.Duration) (oldDuration time.Duration, err error) {
	var (
		v       *gvar.Var
		oldTTL  int64
		fileKey = gconv.String(key)
	)
	// TTL.
	expire, err := c.GetExpire(ctx, fileKey)
	if err != nil {
		return
	}
	oldTTL = int64(expire)
	if oldTTL == -2 {
		// It does not exist.
		return
	}
	oldDuration = time.Duration(oldTTL) * time.Second
	// DEL.
	if duration < 0 {
		err = c.Delete(fileKey)
		return
	}
	v, err = c.Get(ctx, fileKey)
	if err != nil {
		return
	}
	err = c.Set(ctx, fileKey, v.Val(), duration)
	return
}

func (c *AdapterFile) GetExpire(ctx context.Context, key interface{}) (time.Duration, error) {
	content, err := c.read(gconv.String(key))
	if err != nil {
		return -1, nil
	}

	if content.Duration <= time.Now().Unix() {
		return -1, nil
	}
	return time.Duration(time.Now().Unix()-content.Duration) * time.Second, nil
}

func (c *AdapterFile) Remove(ctx context.Context, keys ...interface{}) (lastValue *gvar.Var, err error) {
	if len(keys) == 0 {
		return nil, nil
	}
	// Retrieves the last key value.
	if lastValue, err = c.Get(ctx, gconv.String(keys[len(keys)-1])); err != nil {
		return nil, err
	}
	// Deletes all given keys.
	err = c.DeleteMulti(gconv.Strings(keys)...)
	return
}

func (c *AdapterFile) Clear(ctx context.Context) error {
	return c.Flush()
}

func (c *AdapterFile) Close(ctx context.Context) error {
	return nil
}

func (c *AdapterFile) createName(key string) string {
	h := sha256.New()
	_, _ = h.Write([]byte(key))
	hash := hex.EncodeToString(h.Sum(nil))
	return filepath.Join(c.dir, fmt.Sprintf("%s.cache", hash))
}

func (c *AdapterFile) read(key string) (*fileContent, error) {
	rp := gfile.RealPath(c.createName(key))
	if rp == "" {
		return nil, nil
	}

	value, err := os.ReadFile(rp)
	if err != nil {
		return nil, err
	}

	content := &fileContent{}
	if err := json.Unmarshal(value, content); err != nil {
		return nil, err
	}

	if content.Duration == 0 {
		return content, nil
	}

	if content.Duration <= time.Now().Unix() {
		_ = c.Delete(key)
		return nil, CacheExpiredErr
	}
	return content, nil
}

// Has checks if the cached key exists into the File storage
func (c *AdapterFile) Has(key string) bool {
	fc, err := c.read(key)
	return err == nil && fc != nil
}

// Delete the cached key from File storage
func (c *AdapterFile) Delete(key string) error {
	_, err := os.Stat(c.createName(key))
	if err != nil && os.IsNotExist(err) {
		return nil
	}
	return os.Remove(c.createName(key))
}

// DeleteMulti the cached key from File storage
func (c *AdapterFile) DeleteMulti(keys ...string) (err error) {
	for _, key := range keys {
		if err = c.Delete(key); err != nil {
			return
		}
	}
	return
}

// Fetch retrieves the cached value from key of the File storage
func (c *AdapterFile) Fetch(key string) (interface{}, error) {
	content, err := c.read(key)
	if err != nil {
		return nil, err
	}

	if content == nil {
		return nil, nil
	}
	return content.Data, nil
}

// FetchMulti retrieve multiple cached values from keys of the File storage
func (c *AdapterFile) FetchMulti(keys []string) map[string]interface{} {
	result := make(map[string]interface{})
	for _, key := range keys {
		if value, err := c.Fetch(key); err == nil {
			result[key] = value
		}
	}
	return result
}

// Flush removes all cached keys of the File storage
func (c *AdapterFile) Flush() error {
	dir, err := os.Open(c.dir)
	if err != nil {
		return err
	}

	defer func() {
		_ = dir.Close()
	}()

	names, _ := dir.Readdirnames(-1)

	for _, name := range names {
		_ = os.Remove(filepath.Join(c.dir, name))
	}
	return nil
}

// Save a value in File storage by key
func (c *AdapterFile) Save(key string, value string, lifeTime time.Duration) error {
	duration := int64(0)

	if lifeTime > 0 {
		duration = time.Now().Unix() + int64(lifeTime.Seconds())
	}

	content := &fileContent{duration, value}

	data, err := json.Marshal(content)
	if err != nil {
		return err
	}

	err = os.WriteFile(c.createName(key), data, perm)
	return err
}
