package service

import (
	"context"
	"dataservices/commons/logger"
	"dataservices/commons/redisutil"
	"dataservices/commons/utils"
	"dataservices/internal/entity"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/go-redis/redis/v8"
	"go.uber.org/zap"
)

const (
	keyPrefix = "dataservice"
	delimiter = ":"

	ApikeyModeRW = "rw"
	ApikeyModeRO = "ro"

	CacheExpireTenMine = 10 * time.Minute
)

var (
	notFoundTag = "__NOTFOUND__"

	ErrNil          = redis.Nil
	ErrNotFound     = errors.New("key not found")
	ErrLockOccupied = errors.New("the lock has been occupied")
	ErrLockExpired  = errors.New("the lock has been expired")
)

func getExpire() time.Duration {
	return CacheExpireTenMine
}

func buildKey(args ...any) string {
	var key strings.Builder
	key.WriteString(keyPrefix)
	for _, arg := range args {
		key.WriteString(delimiter)
		key.WriteString(fmt.Sprint(arg))
	}
	return key.String()
}

type CacheManager interface {
	GetApikeyPermission(ctx context.Context, permission, publickKey string, dataappID uint64) (bool, error)
	SetApikeyPermission(ctx context.Context, permission, publickKey string, dataappID uint64, data bool) error
	BatchClearApikeyPermission(ctx context.Context, permission string) error
	ClearApikeyPermission(ctx context.Context, permission, publickKey string, dataappID uint64) error

	GetDataappBaseInfo(ctx context.Context, dataappExtID string) (*entity.DataApp, error)
	GetDataappEndpointRouter(ctx context.Context, dataappID uint64, mode string) (*utils.PathTrie, error)
	SetDataappBaseInfo(ctx context.Context, dataappExtID string, dataapp *entity.DataApp) error
	SetDataappEndpointRouter(ctx context.Context, dataappID uint64, mode string, tree *utils.PathTrie) error
	ClearDataappBaseInfo(ctx context.Context, dataappExtID string) error
	ClearDataappEndpointRouter(ctx context.Context, dataappID uint64, mode string) error

	GetDraftEndpoint(ctx context.Context, dataappID uint64, method, path string) (*entity.Endpoint, error)
	GetDraftEndpointByID(ctx context.Context, dataappID, endpointID uint64) (*entity.Endpoint, error)
	GetDeployedEndpoint(ctx context.Context, dataappID uint64, method, path string) (*entity.EndpointPublish, error)
	SetDraftEndpoint(ctx context.Context, dataappID uint64, method, path string, endpoint *entity.Endpoint) error
	SetDraftEndpointByID(ctx context.Context, dataappID, endpointID uint64, endpoint *entity.Endpoint) error
	SetDeployedEndpoint(ctx context.Context, dataappID uint64, method, path string, endpoint *entity.EndpointPublish) error
	ClearDraftEndpoint(ctx context.Context, dataappID uint64, method, path string) error
	ClearDraftEndpointByID(ctx context.Context, dataappID, endpointID uint64) error
	ClearDeployedEndpoint(ctx context.Context, dataappID uint64, method, path string) error

	GetAppCluster(ctx context.Context, dataappID, clusterID uint64) (*entity.AppCluster, error)
	SetAppCluster(ctx context.Context, dataappID, clusterID uint64, appCluster *entity.AppCluster) error
	ClearAppCluster(ctx context.Context, dataappID, clusterID uint64) error

	Lock(ctx context.Context, key string, duration time.Duration) error
	RefreshLock(ctx context.Context, key string, duration time.Duration) error
	Unlock(ctx context.Context, key string) error

	SetDDBStreamCheckpoint(ctx context.Context, table, shardID, seqNumber string) error
	GetDDBStreamCheckpoint(ctx context.Context, table, shardID string) (string, error)
}

func NewCacheManager() CacheManager {
	return &cacheManager{client: redisutil.MasterRedisClient()}
}

type cacheManager struct {
	client *redis.Client
}

func (b *cacheManager) GetApikeyPermission(ctx context.Context, permission, publickKey string, dataappID uint64) (bool, error) {
	key := buildKey("apikey", "permission", permission, "publickey", publickKey, "dataapp", dataappID)
	return b.client.Get(ctx, key).Bool()
}

func (b *cacheManager) SetApikeyPermission(ctx context.Context, permission, publickKey string, dataappID uint64, data bool) error {
	key := buildKey("apikey", "permission", permission, "publickey", publickKey, "dataapp", dataappID)
	return b.client.Set(ctx, key, data, getExpire()).Err()
}

func (b *cacheManager) BatchClearApikeyPermission(ctx context.Context, permission string) error {
	var cursor uint64
	var batch int64 = 50
	size := 0
	scanKey := buildKey("apikey", "permission", permission, "*")
	for {
		keys, newCursor, err := b.client.Scan(ctx, cursor, scanKey, batch).Result()
		if err != nil {
			return err
		}
		if err := b.client.Del(ctx, keys...).Err(); err != nil {
			logger.Error(ctx, "failed to delete apikey permission cache", zap.String("scan_key", scanKey), zap.Uint64("cursor", cursor), zap.Int("count", len(keys)), zap.Error(err))
		} else {
			size += len(keys)
		}
		if newCursor == 0 {
			break
		}
		cursor = newCursor
	}
	logger.Info(ctx, "delete apikey permision cache completed", zap.String("scan_key", scanKey), zap.Int("size", size))
	return nil
}

func (b *cacheManager) ClearApikeyPermission(ctx context.Context, permission, publickKey string, dataappID uint64) error {
	key := buildKey("apikey", "permission", permission, "publickey", publickKey, "dataapp", dataappID)
	return b.clear(ctx, key)
}

func (b *cacheManager) GetDataappBaseInfo(ctx context.Context, dataappExtID string) (*entity.DataApp, error) {
	key := buildKey("dataapp", "baseinfo", dataappExtID)
	var data entity.DataApp
	if err := b.get(ctx, key, &data); err != nil {
		return nil, err
	}
	return &data, nil
}
func (b *cacheManager) GetDataappEndpointRouter(ctx context.Context, dataappID uint64, mode string) (*utils.PathTrie, error) {
	key := buildKey("dataapp", "router", mode, dataappID)
	var data utils.PathTrie
	if err := b.get(ctx, key, &data); err != nil {
		return nil, err
	}
	return &data, nil
}

func (b *cacheManager) SetDataappBaseInfo(ctx context.Context, dataappExtID string, dataapp *entity.DataApp) error {
	key := buildKey("dataapp", "baseinfo", dataappExtID)
	return b.set(ctx, key, dataapp)
}
func (b *cacheManager) SetDataappEndpointRouter(ctx context.Context, dataappID uint64, mode string, tree *utils.PathTrie) error {
	key := buildKey("dataapp", "router", mode, dataappID)
	return b.set(ctx, key, tree)
}

func (b *cacheManager) ClearDataappBaseInfo(ctx context.Context, dataappExtID string) error {
	key := buildKey("dataapp", "baseinfo", dataappExtID)
	return b.clear(ctx, key)
}
func (b *cacheManager) ClearDataappEndpointRouter(ctx context.Context, dataappID uint64, mode string) error {
	key := buildKey("dataapp", "router", mode, dataappID)
	return b.clear(ctx, key)
}

func (b *cacheManager) GetDraftEndpoint(ctx context.Context, dataappID uint64, method, path string) (*entity.Endpoint, error) {
	method = strings.ToUpper(method)
	key := buildKey("endpoint", "draft", "baseinfo", dataappID, method, path)
	var data entity.Endpoint
	if err := b.get(ctx, key, &data); err != nil {
		return nil, err
	}
	return &data, nil
}
func (b *cacheManager) GetDraftEndpointByID(ctx context.Context, dataappID, endpointID uint64) (*entity.Endpoint, error) {
	key := buildKey("endpoint", "draft", "baseinfo", dataappID, endpointID)
	var data entity.Endpoint
	if err := b.get(ctx, key, &data); err != nil {
		return nil, err
	}
	return &data, nil
}
func (b *cacheManager) GetDeployedEndpoint(ctx context.Context, dataappID uint64, method, path string) (*entity.EndpointPublish, error) {
	method = strings.ToUpper(method)
	key := buildKey("endpoint", "deployed", "baseinfo", dataappID, method, path)
	var data entity.EndpointPublish
	if err := b.get(ctx, key, &data); err != nil {
		return nil, err
	}
	return &data, nil
}

func (b *cacheManager) SetDraftEndpoint(ctx context.Context, dataappID uint64, method, path string, endpoint *entity.Endpoint) error {
	method = strings.ToUpper(method)
	key := buildKey("endpoint", "draft", "baseinfo", dataappID, method, path)
	return b.set(ctx, key, endpoint)
}
func (b *cacheManager) SetDraftEndpointByID(ctx context.Context, dataappID, endpointID uint64, endpoint *entity.Endpoint) error {
	key := buildKey("endpoint", "draft", "baseinfo", dataappID, endpointID)
	return b.set(ctx, key, endpoint)
}
func (b *cacheManager) SetDeployedEndpoint(ctx context.Context, dataappID uint64, method, path string, endpoint *entity.EndpointPublish) error {
	method = strings.ToUpper(method)
	key := buildKey("endpoint", "deployed", "baseinfo", dataappID, method, path)
	return b.set(ctx, key, endpoint)
}

func (b *cacheManager) ClearDraftEndpoint(ctx context.Context, dataappID uint64, method, path string) error {
	method = strings.ToUpper(method)
	key := buildKey("endpoint", "draft", "baseinfo", dataappID, method, path)
	return b.clear(ctx, key)
}
func (b *cacheManager) ClearDraftEndpointByID(ctx context.Context, dataappID, endpointID uint64) error {
	key := buildKey("endpoint", "draft", "baseinfo", dataappID, endpointID)
	return b.clear(ctx, key)
}
func (b *cacheManager) ClearDeployedEndpoint(ctx context.Context, dataappID uint64, method, path string) error {
	method = strings.ToUpper(method)
	key := buildKey("endpoint", "deployed", "baseinfo", dataappID, method, path)
	return b.clear(ctx, key)
}

func (b *cacheManager) GetAppCluster(ctx context.Context, dataappID, clusterID uint64) (*entity.AppCluster, error) {
	key := buildKey("bindcluster", "baseinfo", "dataapp", dataappID, "cluster", clusterID)
	var data entity.AppCluster
	if err := b.get(ctx, key, &data); err != nil {
		return nil, err
	}
	return &data, nil
}
func (b *cacheManager) SetAppCluster(ctx context.Context, dataappID, clusterID uint64, appCluster *entity.AppCluster) error {
	key := buildKey("bindcluster", "baseinfo", "dataapp", dataappID, "cluster", clusterID)
	return b.set(ctx, key, appCluster)
}
func (b *cacheManager) ClearAppCluster(ctx context.Context, dataappID, clusterID uint64) error {
	key := buildKey("bindcluster", "baseinfo", "dataapp", dataappID, "cluster", clusterID)
	return b.clear(ctx, key)
}

func (b *cacheManager) Lock(ctx context.Context, key string, duration time.Duration) error {
	key = buildKey("lock", key)
	ok, err := b.client.SetNX(ctx, key, time.Now().Unix(), duration).Result()
	if err != nil {
		return err
	} else if !ok {
		return ErrLockOccupied
	}
	return nil
}
func (b *cacheManager) RefreshLock(ctx context.Context, key string, duration time.Duration) error {
	key = buildKey("lock", key)
	ok, err := b.client.Expire(ctx, key, duration).Result()
	if err != nil {
		return err
	} else if !ok {
		return ErrLockExpired
	}
	return nil
}
func (b *cacheManager) Unlock(ctx context.Context, key string) error {
	key = buildKey("lock", key)
	return b.clear(ctx, key)
}

func (b *cacheManager) SetDDBStreamCheckpoint(ctx context.Context, table, shardID, seqNumber string) error {
	key := buildKey("ddbstream", "checkpoint", table, shardID)
	return b.client.Set(ctx, key, seqNumber, CacheExpireTenMine).Err()
}
func (b *cacheManager) GetDDBStreamCheckpoint(ctx context.Context, table, shardID string) (string, error) {
	key := buildKey("ddbstream", "checkpoint", table, shardID)
	return b.client.Get(ctx, key).Result()
}

func (b *cacheManager) get(ctx context.Context, key string, target any) error {
	reply, err := b.client.Get(ctx, key).Result()
	if err != nil {
		return err
	}
	if reply == notFoundTag {
		return ErrNotFound
	}
	return json.Unmarshal([]byte(reply), target)
}
func (b *cacheManager) set(ctx context.Context, key string, target any) error {
	var data string

	if utils.IsNil(target) {
		data = notFoundTag
	} else {
		targetBytes, err := json.Marshal(target)
		if err != nil {
			return err
		}
		data = string(targetBytes)
	}
	return b.client.Set(ctx, key, data, getExpire()).Err()
}
func (b *cacheManager) clear(ctx context.Context, key string) error {
	return b.client.Del(ctx, key).Err()
}
