package flag

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"github.com/tidbcloud/account/central/utils"

	rawredis "github.com/go-redis/redis/v8"
	"github.com/tidbcloud/account/commons/redis"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	// set
	flagOrgIdKeyFormat = "account_feature_flag_org_id_%d"
	// set
	flagEmailSuffixKeyFormat = "account_feature_flag_email_suffix_%s"
	// hash
	flagMinOrgIdKey = "account_feature_flag_min_org_id"
	// string
	flagCacheTokenKey = "account_feature_flag_cache_token"

	flagCacheTokenCaching = "0"
	flagCacheTokenCached  = "1"

	expireTime = time.Minute * 5
)

type FeatureFlagCacheSvcIface interface {
	GetFlagNamesCacheStatus(ctx context.Context) (bool, bool, error)
	ListHitFlagNames(ctx context.Context, orgId uint64, emailSuffix string) ([]string, error)
	SetFlagNamesToCache(ctx context.Context, flagInfos []*FeatureFlagInfo) error
}

func GetFeatureFlagCacheSvc() FeatureFlagCacheSvcIface {
	return &FeatureFlagRedisSvc{}
}

type FeatureFlagRedisSvc struct {
}

type ListCacheParam struct {
	OrgID       uint64
	EmailSuffix string
}

type ListCacheRet struct {
	ExistCache   bool
	NeedSetCache bool
	HitFlagNames []string
}

func (s *FeatureFlagRedisSvc) ListHitFlagNames(ctx context.Context, orgId uint64, emailSuffix string) ([]string, error) {
	bm := utils.Begin(ctx, "list feature flags from redis", zap.Any("orgId", orgId), zap.Any("emailSuffix", emailSuffix))
	defer bm.OnExit(ctx)

	client := redis.GetClient()
	var hitFlagNameMap = map[string]struct{}{}

	if orgId != 0 {
		// check org_id
		key := fmt.Sprintf(flagOrgIdKeyFormat, orgId)
		members, err := client.SMembers(ctx, key).Result()
		if err != nil {
			return nil, err
		}
		for _, m := range members {
			hitFlagNameMap[m] = struct{}{}
		}

		// check min_org_id
		fieldValMap, err := client.HGetAll(ctx, flagMinOrgIdKey).Result()
		if err != nil {
			return nil, err
		}
		for field, val := range fieldValMap {
			if val == "" {
				continue
			}
			minOrgId, err := strconv.ParseUint(val, 10, 64)
			if err != nil {
				tilog.Error(ctx, fmt.Sprintf("parse redis value flag_name %s relate min_org_id %s error", field, val), zap.Any("err", err))
				continue
			}
			if orgId >= minOrgId {
				hitFlagNameMap[field] = struct{}{}
			}
		}
	}

	if emailSuffix != "" {
		// check email_suffix
		key := fmt.Sprintf(flagEmailSuffixKeyFormat, emailSuffix)
		members, err := client.SMembers(ctx, key).Result()
		if err != nil {
			return nil, err
		}
		for _, m := range members {
			hitFlagNameMap[m] = struct{}{}
		}
	}

	var hitFlagNames []string
	for k := range hitFlagNameMap {
		hitFlagNames = append(hitFlagNames, k)
	}

	return hitFlagNames, nil
}

func (*FeatureFlagRedisSvc) GetFlagNamesCacheStatus(ctx context.Context) (bool, bool, error) {
	client := redis.GetClient()
	val, err := client.Get(ctx, flagCacheTokenKey).Result()
	if err != nil {
		if err == rawredis.Nil {
			return false, true, nil
		}
		return false, false, err
	}
	if val == flagCacheTokenCached {
		return true, false, nil
	}
	if val == flagCacheTokenCaching {
		return false, false, nil
	}
	return false, true, nil
}

func (s *FeatureFlagRedisSvc) SetFlagNamesToCache(ctx context.Context, flagInfos []*FeatureFlagInfo) error {
	client := redis.GetClient()
	pipeline := client.Pipeline()
	defer func() {
		_ = pipeline.Close()
	}()

	pipeline.Set(ctx, flagCacheTokenKey, flagCacheTokenCaching, time.Second*3)
	_, err := pipeline.Exec(ctx)
	if err != nil {
		tilog.Error(ctx, "pipeline exec cache token to caching error", zap.Any("err", err))
		return err
	}

	for _, flag := range flagInfos {
		name := flag.FlagName
		if flag.MinOrgID != "" {
			pipeline.HSet(ctx, flagMinOrgIdKey, name, flag.MinOrgID)
		}
		pipeline.Expire(ctx, flagMinOrgIdKey, expireTime)
		for _, orgId := range flag.OrgIDs {
			key := fmt.Sprintf(flagOrgIdKeyFormat, orgId)
			pipeline.SAdd(ctx, key, name)
			pipeline.Expire(ctx, key, expireTime)
		}
		for _, es := range flag.EmailSuffix {
			key := fmt.Sprintf(flagEmailSuffixKeyFormat, es)
			pipeline.SAdd(ctx, key, name)
			pipeline.Expire(ctx, key, expireTime)
		}
		_, err = pipeline.Exec(ctx)
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("pipeline exec flag_name %s error", name), zap.Any("err", err))
		}
	}

	pipeline.Set(ctx, flagCacheTokenKey, flagCacheTokenCached, expireTime)
	_, err = pipeline.Exec(ctx)
	if err != nil {
		tilog.Error(ctx, "pipeline exec cache token to cached error", zap.Any("err", err))
		return err
	}

	return nil
}
