package flag

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/duke-git/lancet/v2/slice"
	"github.com/tidbcloud/account/central/utils"

	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const (
	flagSeparator = ","
)

var (
	featureFlagSvcInitOnce sync.Once
	featureFlagSvcInstance FeatureFlagSvcIface
)

type FeatureFlagSvcIface interface {
	CreateFeatureFlag(ctx context.Context, param *CreateFeatureFlagParam) error
	UpdateFeatureFlag(ctx context.Context, param *CreateFeatureFlagParam) error
	DeleteFeatureFlag(ctx context.Context, flagName string) error
	ListFeatureFlags(ctx context.Context, param *ListFeatureFlagsParam) (int32, []*FeatureFlagInfo, error)
	ConfigFeatureFlagOrg(ctx context.Context, flagName string, orgIDs []uint64) error
	GetFeatureFlagByName(ctx context.Context, flagName string) (*FeatureFlagInfo, error)
	ListHitFeatureFlagNames(ctx context.Context, email string, orgID uint64) ([]string, error)
}

type FeatureFlagSvc struct {
}

func NewFeatureFlagSvc() FeatureFlagSvcIface {
	return &FeatureFlagSvc{}
}

func GetFeatureFlagSvc() FeatureFlagSvcIface {
	featureFlagSvcInitOnce.Do(func() {
		featureFlagSvcInstance = NewFeatureFlagSvc()
	})
	return featureFlagSvcInstance
}

func (s *FeatureFlagSvc) CreateFeatureFlag(ctx context.Context, param *CreateFeatureFlagParam) error {
	tilog.Info(ctx, "CreateFeatureFlag start", zap.Any("param", param))
	if param == nil || param.FlagName == "" {
		return errs.NewBadRequest("flag_name invalid", nil)
	}
	flagDao := dao.GetDaoHub().FeatureFlag
	dbFlag, err := flagDao.GetFeatureFlagByName(ctx, database.DBConn.DB, param.FlagName)
	if err != nil {
		tilog.Error(ctx, "CreateFeatureFlag GetFeatureFlagByName error", zap.Any("flagName", param.FlagName), zap.Any("err", err))
		return errs.NewInternalUnknown("internal error", err)
	}
	if dbFlag != nil {
		return errs.NewBadRequest("flag name already exist", nil)
	}

	var trimEmailSuffix []string
	for _, es := range param.EmailSuffix {
		trimEmailSuffix = append(trimEmailSuffix, strings.TrimSpace(es))
	}

	toCreateFlag := &entity.FeatureFlag{
		FlagName:    param.FlagName,
		Description: param.Description,
		Owner:       param.Owner,
		EmailSuffix: joinSlice2String(trimEmailSuffix),
		MinOrgID:    strings.TrimSpace(param.MinOrgID),
	}

	err = database.DBConn.DB.Transaction(func(tx *gorm.DB) error {
		// flag
		flag, err := flagDao.CreateFeatureFlag(ctx, tx, toCreateFlag)
		if err != nil {
			tilog.Error(ctx, "CreateFeatureFlag create in DB error", zap.Any("flag", toCreateFlag), zap.Any("err", err))
			return errs.NewInternalUnknown("internal error", err)
		}
		// flag org
		var toCreateFlagOrgs = genFeatureFlagOrgs(flag.ID.Uint64(), param.OrgIDs)
		if len(toCreateFlagOrgs) > 0 {
			err := flagDao.BatchCreateFeatureFlagOrg(ctx, tx, toCreateFlagOrgs)
			if err != nil {
				tilog.Error(ctx, "CreateFeatureFlag BatchCreateFeatureFlagOrg error", zap.Any("flag_orgs", toCreateFlagOrgs), zap.Any("err", err))
				return errs.NewInternalUnknown("internal error", err)
			}
		}
		return nil
	})

	tilog.Info(ctx, "CreateFeatureFlag result", zap.Any("flag", toCreateFlag), zap.Any("err", err))
	return err
}

func genFeatureFlagOrgs(flagID uint64, orgIds []uint64) []*entity.FeatureFlagOrg {
	var flagOrgs []*entity.FeatureFlagOrg
	for _, orgId := range orgIds {
		flagOrg := &entity.FeatureFlagOrg{
			FlagID: entity.ID(flagID),
			OrgID:  entity.ID(orgId),
		}
		flagOrgs = append(flagOrgs, flagOrg)
	}
	return flagOrgs
}

func splitString2Slice(str string) []string {
	if str == "" {
		return []string{}
	}
	var ret []string
	split := strings.Split(str, flagSeparator)
	for _, s := range split {
		ts := strings.TrimSpace(s)
		ret = append(ret, ts)
	}
	return ret
}

func joinSlice2String(sli []string) string {
	if len(sli) == 0 {
		return ""
	}
	return strings.Join(sli, flagSeparator)
}

func (s *FeatureFlagSvc) UpdateFeatureFlag(ctx context.Context, param *CreateFeatureFlagParam) error {
	tilog.Info(ctx, "UpdateFeatureFlag start", zap.Any("param", param))
	if param == nil || param.FlagName == "" {
		return errs.NewBadRequest("flag_name invalid", nil)
	}

	flagDao := dao.GetDaoHub().FeatureFlag
	dbFlag, err := flagDao.GetFeatureFlagByName(ctx, database.DBConn.DB, param.FlagName)
	if err != nil {
		tilog.Error(ctx, "UpdateFeatureFlag GetFeatureFlagByName error", zap.Any("flagName", param.FlagName), zap.Any("err", err))
		return errs.NewInternalUnknown("internal error", err)
	}
	if dbFlag == nil {
		return errs.NewBadRequest("flag not exist", nil)
	}

	dbFlag.Description = param.Description
	dbFlag.MinOrgID = param.MinOrgID

	var trimEmailSuffix []string
	if len(param.EmailSuffix) > 0 {
		for _, es := range param.EmailSuffix {
			trimEmailSuffix = append(trimEmailSuffix, strings.TrimSpace(es))
		}
	}
	dbFlag.EmailSuffix = joinSlice2String(trimEmailSuffix)

	err = database.DBConn.DB.Transaction(func(tx *gorm.DB) error {
		if err := flagDao.UpdateFeatureFlagByID(ctx, tx, dbFlag); err != nil {
			tilog.Error(ctx, "UpdateFeatureFlag UpdateFeatureFlagByID error", zap.Any("flag", dbFlag), zap.Any("err", err))
			return errs.NewInternalUnknown("internal error", err)
		}

		if err := flagDao.DeleteFeatureFlagOrgByFlagID(ctx, tx, dbFlag.ID.Uint64()); err != nil {
			tilog.Error(ctx, "UpdateFeatureFlag DeleteFeatureFlagOrgByFlagID error", zap.Any("flagID", dbFlag.ID), zap.Any("err", err))
			return errs.NewInternalUnknown("internal error", err)
		}
		toCreateFlagOrgs := genFeatureFlagOrgs(dbFlag.ID.Uint64(), param.OrgIDs)
		if len(toCreateFlagOrgs) > 0 {
			if err := flagDao.BatchCreateFeatureFlagOrg(ctx, tx, toCreateFlagOrgs); err != nil {
				tilog.Error(ctx, "UpdateFeatureFlag BatchCreateFeatureFlagOrg error", zap.Any("flag_orgs", toCreateFlagOrgs), zap.Any("err", err))
				return errs.NewInternalUnknown("internal error", err)
			}
		}

		return nil
	})
	if err != nil {
		return err
	}

	tilog.Info(ctx, "UpdateFeatureFlag success", zap.Any("flagName", param.FlagName))
	return nil
}

func (s *FeatureFlagSvc) DeleteFeatureFlag(ctx context.Context, flagName string) error {
	tilog.Info(ctx, "DeleteFeatureFlag start", zap.Any("flagName", flagName))
	dbFlag, err := dao.GetDaoHub().FeatureFlag.GetFeatureFlagByName(ctx, database.DBConn.DB, flagName)
	if err != nil {
		tilog.Error(ctx, "DeleteFeatureFlag GetFeatureFlagByName error", zap.Any("flagName", flagName), zap.Any("err", err))
		return errs.NewInternalUnknown("internal error", err)
	}
	if dbFlag == nil {
		return errs.NewBadRequest("flag not exist", nil)
	}

	flagDao := dao.GetDaoHub().FeatureFlag
	err = database.DBConn.DB.Transaction(func(tx *gorm.DB) error {
		err := flagDao.DeleteFeatureFlagByID(ctx, tx, dbFlag.ID.Uint64())
		if err != nil {
			tilog.Error(ctx, "DeleteFeatureFlag DeleteFeatureFlagByID error", zap.Any("dbFlag", dbFlag), zap.Any("err", err))
			return errs.NewInternalUnknown("internal error", err)
		}
		err = flagDao.DeleteFeatureFlagOrgByFlagID(ctx, tx, dbFlag.ID.Uint64())
		if err != nil {
			tilog.Error(ctx, "DeleteFeatureFlag DeleteFeatureFlagOrgByFlagID error", zap.Any("dbFlag", dbFlag), zap.Any("err", err))
			return errs.NewInternalUnknown("internal error", err)
		}
		return nil
	})

	tilog.Info(ctx, "DeleteFeatureFlag result", zap.Any("flagName", flagName), zap.Any("err", err))
	return err
}

func (s *FeatureFlagSvc) ConfigFeatureFlagOrg(ctx context.Context, flagName string, orgIds []uint64) error {
	tilog.Info(ctx, "ConfigFeatureFlagOrg start", zap.Any("flagName", flagName), zap.Any("orgIds", orgIds))
	if flagName == "" || len(orgIds) == 0 {
		return errs.NewBadRequest("param invalid", nil)
	}

	flagDao := dao.GetDaoHub().FeatureFlag
	dbFlag, err := flagDao.GetFeatureFlagByName(ctx, database.DBConn.DB, flagName)
	if err != nil {
		tilog.Error(ctx, "ConfigFeatureFlagOrg GetFeatureFlagByName error", zap.Any("flagName", flagName), zap.Any("err", err))
		return errs.NewInternalUnknown("internal error", err)
	}
	if dbFlag == nil {
		return errs.NewNotFound("flag not exist", nil)
	}

	dbFlagOrgs, err := flagDao.ListFeatureFlagOrgByFlagID(ctx, database.DBConn.DB, dbFlag.ID.Uint64())
	if err != nil {
		tilog.Error(ctx, "ConfigFeatureFlagOrg ListFeatureFlagOrgByFlagID error", zap.Any("flagID", dbFlag.ID.Uint64()), zap.Any("err", err))
		return errs.NewInternalUnknown("internal error", err)
	}
	dbOrgIdMap := map[uint64]struct{}{}
	for _, dbFlagOrg := range dbFlagOrgs {
		dbOrgIdMap[dbFlagOrg.OrgID.Uint64()] = struct{}{}
	}
	var toCreateOrgIds []uint64
	for _, orgId := range orgIds {
		if _, ok := dbOrgIdMap[orgId]; ok {
			tilog.Info(ctx, fmt.Sprintf("ConfigFeatureFlagOrg flagId %d orgId %d already configured, ignore", dbFlag.ID, orgId))
			continue
		}
		toCreateOrgIds = append(toCreateOrgIds, orgId)
	}
	toCreateFlagOrgs := genFeatureFlagOrgs(dbFlag.ID.Uint64(), toCreateOrgIds)
	if len(toCreateFlagOrgs) > 0 {
		err = flagDao.BatchCreateFeatureFlagOrg(ctx, database.DBConn.DB, toCreateFlagOrgs)
		if err != nil {
			tilog.Error(ctx, "ConfigFeatureFlagOrg BatchCreateFeatureFlagOrg error", zap.Any("flagName", flagName), zap.Any("err", err))
			return errs.NewInternalUnknown("internal error", err)
		}
	}
	tilog.Info(ctx, "ConfigFeatureFlagOrg success", zap.Any("flagName", flagName), zap.Any("toCreateOrgIds", toCreateOrgIds))
	return nil
}

func (s *FeatureFlagSvc) GetFeatureFlagByName(ctx context.Context, flagName string) (*FeatureFlagInfo, error) {
	tilog.Info(ctx, "GetFeatureFlagByName start", zap.Any("flagName", flagName))
	if flagName == "" {
		return nil, errs.NewBadRequest("flag_name is blank", nil)
	}
	flagDao := dao.GetDaoHub().FeatureFlag
	dbFlag, err := flagDao.GetFeatureFlagByName(ctx, database.DBConn.DB, flagName)
	if err != nil {
		tilog.Error(ctx, "GetFeatureFlagByName get feature flag from DB error", zap.Any("flagName", flagName), zap.Any("err", err))
		return nil, errs.NewInternalUnknown("internal error", err)
	}
	if dbFlag == nil {
		return nil, errs.NewNotFound("flag not exist", nil)
	}
	flagOrgs, err := flagDao.ListFeatureFlagOrgByFlagID(ctx, database.DBConn.DB, dbFlag.ID.Uint64())
	if err != nil {
		tilog.Error(ctx, "GetFeatureFlagByName ListFeatureFlagOrgByFlagID error", zap.Any("flagID", dbFlag.ID), zap.Any("err", err))
		return nil, errs.NewInternalUnknown("internal error", err)
	}
	var orgIds []uint64
	for _, flagOrg := range flagOrgs {
		orgIds = append(orgIds, flagOrg.OrgID.Uint64())
	}

	flagInfo := &FeatureFlagInfo{
		FlagName:    flagName,
		EmailSuffix: splitString2Slice(dbFlag.EmailSuffix),
		MinOrgID:    dbFlag.MinOrgID,
		OrgIDs:      orgIds,
	}

	tilog.Info(ctx, "GetFeatureFlagByName success", zap.Any("flagInfo", flagInfo))
	return flagInfo, nil
}

func (s *FeatureFlagSvc) ListFeatureFlags(ctx context.Context, param *ListFeatureFlagsParam) (int32, []*FeatureFlagInfo, error) {
	tilog.Info(ctx, "ListFeatureFlags start", zap.Any("param", param))
	pageSize := param.PageSize
	if pageSize <= 0 {
		pageSize = 10
	}
	page := param.Page
	if page <= 0 {
		page = 1
	}
	offset := (page - 1) * pageSize

	flagDao := dao.GetDaoHub().FeatureFlag
	flags, total, err := flagDao.PageFeatureFlags(ctx, database.DBConn.DB, param.FlagName, offset, pageSize)
	if err != nil {
		tilog.Error(ctx, "ListFeatureFlags PageFeatureFlags error", zap.Any("err", err))
		return 0, nil, errs.NewInternalUnknown("internal error", err)
	}

	if len(flags) == 0 {
		return int32(total), []*FeatureFlagInfo{}, nil
	}

	var flagIDs []uint64
	for _, f := range flags {
		flagIDs = append(flagIDs, f.ID.Uint64())
	}
	flagOrgs, err := flagDao.ListFeatureFlagOrgsByFlagIDs(ctx, database.DBConn.DB, flagIDs)
	if err != nil {
		tilog.Error(ctx, "ListFeatureFlags ListFeatureFlagOrgsByFlagIDs error", zap.Any("err", err))
		return 0, nil, errs.NewInternalUnknown("internal error", err)
	}

	flagID2OrgIDs := map[uint64][]uint64{}
	for _, fo := range flagOrgs {
		flagID2OrgIDs[fo.FlagID.Uint64()] = append(flagID2OrgIDs[fo.FlagID.Uint64()], fo.OrgID.Uint64())
	}

	var flagInfos []*FeatureFlagInfo
	for _, f := range flags {
		orgIDs := flagID2OrgIDs[f.ID.Uint64()]
		if orgIDs == nil {
			orgIDs = []uint64{}
		}
		flagInfos = append(flagInfos, &FeatureFlagInfo{
			FlagName:    f.FlagName,
			Description: f.Description,
			Owner:       f.Owner,
			EmailSuffix: splitString2Slice(f.EmailSuffix),
			MinOrgID:    strings.TrimSpace(f.MinOrgID),
			OrgIDs:      orgIDs,
			CreatedAt:   &f.CreatedAt,
			UpdatedAt:   f.UpdatedAt,
		})
	}

	return int32(total), flagInfos, nil
}

func (s *FeatureFlagSvc) ListHitFeatureFlagNames(ctx context.Context, email string, orgID uint64) ([]string, error) {
	tilog.Info(ctx, "ListHitFeatureFlagNames start", zap.Any("email", email), zap.Any("orgID", orgID))
	if email == "" && orgID == 0 {
		return nil, errs.NewBadRequest("param invalid", nil)
	}
	//hitFlagNames, err := s.ListHitFlagNamesWithCache(ctx, email, orgID, time.Now(), false)
	//if err != nil {
	//	tilog.Error(ctx, "ListHitFeatureFlagNames listHitFlagNamesWithCache error", zap.Any("email", email), zap.Any("orgID", orgID), zap.Any("err", err))
	//	return nil, err
	//}
	// list from db
	flagInfos, err := s.listAllFeatureFlagInfoFromDB(ctx)
	if err != nil {
		tilog.Error(ctx, "listHitFlagNamesWithCache listAllFeatureFlagInfoFromDB error", zap.Any("err", err))
		return nil, errs.NewInternalUnknown("internal error", err)
	}
	// generate result
	var hitFlagNames []string
	for _, flagInfo := range flagInfos {
		if isHitEmailSuffix(flagInfo, email) || isHitOrgID(ctx, flagInfo, orgID) {
			hitFlagNames = append(hitFlagNames, flagInfo.FlagName)
		}
	}

	tilog.Info(ctx, "ListHitFeatureFlagNames success", zap.Any("hitFlagNames", hitFlagNames))
	return hitFlagNames, nil
}

func (s *FeatureFlagSvc) ListHitFlagNamesWithCache(ctx context.Context, email string, orgID uint64, start time.Time, forceCache bool) ([]string, error) {
	emailSuffix := parseEmailSuffix(email)

	// list from cache
	cachesvc := GetFeatureFlagCacheSvc()
	exist, needCache, err := cachesvc.GetFlagNamesCacheStatus(ctx)
	tilog.Info(ctx, "listHitFlagNamesWithCache GetFlagNamesCacheStatus result", zap.Any("exist", exist), zap.Any("needCache", needCache), zap.Any("err", err))
	if err != nil {
		tilog.Error(ctx, "listHitFlagNamesWithCache GetFlagNamesCacheStatus error", zap.Any("err", err))
		return nil, errs.NewInternalUnknown("internal error", err)
	}
	if exist {
		hitFlagNames, err := cachesvc.ListHitFlagNames(ctx, orgID, emailSuffix)
		tilog.Info(ctx, "listHitFlagNamesWithCache ListHitFlagNames result", zap.Any("orgID", orgID), zap.Any("emailSuffix", emailSuffix), zap.Any("hitFlagNames", hitFlagNames), zap.Any("err", err))
		if err != nil {
			tilog.Error(ctx, "listHitFlagNamesWithCache ListHitFlagNames error", zap.Any("err", err))
			return nil, errs.NewInternalUnknown("internal error", err)
		}
		return hitFlagNames, nil
	}
	if needCache || forceCache {
		tilog.Info(ctx, "listHitFlagNamesWithCache need to cache", zap.Any("needCache", needCache), zap.Any("forceCache", forceCache))
		// list from db
		flagInfos, err := s.listAllFeatureFlagInfoFromDB(ctx)
		if err != nil {
			tilog.Error(ctx, "listHitFlagNamesWithCache listAllFeatureFlagInfoFromDB error", zap.Any("err", err))
			return nil, errs.NewInternalUnknown("internal error", err)
		}
		// set cache
		go func() {
			bgctx := context.Background()
			err = cachesvc.SetFlagNamesToCache(bgctx, flagInfos)
			tilog.Info(bgctx, "listHitFlagNamesWithCache SetFlagNamesToCache result", zap.Any("orgID", orgID), zap.Any("email", email), zap.Any("err", err))
		}()
		// generate result
		var results []string
		for _, flagInfo := range flagInfos {
			if isHitEmailSuffix(flagInfo, email) || isHitOrgID(ctx, flagInfo, orgID) {
				results = append(results, flagInfo.FlagName)
			}
		}
		return results, nil
	}
	// cache not exist and some other goroutine is setting cache
	if time.Since(start).Milliseconds() >= 500 {
		tilog.Info(ctx, "listHitFlagNamesWithCache wait long time and force cache")
		return s.ListHitFlagNamesWithCache(ctx, email, orgID, start, true)
	}
	time.Sleep(time.Millisecond * 10)
	tilog.Info(ctx, "listHitFlagNamesWithCache wait 10ms and continue list cache")
	return s.ListHitFlagNamesWithCache(ctx, email, orgID, start, false)
}

func isHitEmailSuffix(flagInfo *FeatureFlagInfo, email string) bool {
	var emailSuffix = parseEmailSuffix(email)
	if emailSuffix == "" {
		return false
	}
	if len(flagInfo.EmailSuffix) == 0 {
		return false
	}
	return slice.Contain(flagInfo.EmailSuffix, emailSuffix)
}

func isHitOrgID(ctx context.Context, flagInfo *FeatureFlagInfo, orgId uint64) bool {
	if orgId == 0 {
		return false
	}
	if flagInfo.MinOrgID != "" {
		intMinOrgId, err := strconv.ParseUint(flagInfo.MinOrgID, 10, 64)
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("parse flag_name %s min_org_id %s error", flagInfo.FlagName, flagInfo.MinOrgID), zap.Any("err", err))
		} else if orgId >= intMinOrgId {
			return true
		}
	}
	return slice.Contain(flagInfo.OrgIDs, orgId)
}

func parseEmailSuffix(email string) string {
	if email == "" {
		return ""
	}
	res := strings.Split(email, "@")
	if len(res) != 2 {
		return ""
	}
	return res[1]
}

func (s *FeatureFlagSvc) listAllFeatureFlagInfoFromDB(ctx context.Context) ([]*FeatureFlagInfo, error) {
	bm := utils.Begin(ctx, "list feature flags from db")
	defer bm.OnExit(ctx)

	flags, err := dao.GetDaoHub().FeatureFlag.ListAllFeatureFlagsInBatches(ctx, database.DBConn.DB, 200)
	if err != nil {
		return nil, err
	}
	flagOrgs, err := dao.GetDaoHub().FeatureFlag.ListAllFeatureFlagOrgsInBatches(ctx, database.DBConn.DB, 500)
	if err != nil {
		return nil, err
	}

	flagId2OrgIdsMap := map[uint64][]uint64{}
	for _, flagOrg := range flagOrgs {
		flagId := flagOrg.FlagID.Uint64()
		orgIds, ok := flagId2OrgIdsMap[flagId]
		if !ok {
			orgIds = []uint64{}
		}
		orgIds = append(orgIds, flagOrg.OrgID.Uint64())
		flagId2OrgIdsMap[flagId] = orgIds
	}

	var flagInfos []*FeatureFlagInfo
	for _, flag := range flags {
		orgIds, ok := flagId2OrgIdsMap[flag.ID.Uint64()]
		if !ok {
			orgIds = []uint64{}
		}
		flagInfo := &FeatureFlagInfo{
			FlagName:    flag.FlagName,
			EmailSuffix: splitString2Slice(flag.EmailSuffix),
			MinOrgID:    strings.TrimSpace(flag.MinOrgID),
			OrgIDs:      orgIds,
		}
		flagInfos = append(flagInfos, flagInfo)
	}

	return flagInfos, nil
}
