/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>
*/

package engine

import (
	"encoding/json"
	"errors"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/cgrates/birpc/context"
	"github.com/cgrates/cgrates/config"
	"github.com/cgrates/cgrates/guardian"
	"github.com/cgrates/cgrates/utils"
)

type AttrSetAccount struct {
	Tenant                 string
	Account                string
	ActionPlanIDs          []string
	ActionPlansOverwrite   bool
	ActionTriggerIDs       []string
	ActionTriggerOverwrite bool
	ExtraOptions           map[string]bool
	ReloadScheduler        bool
}

// Account structure containing information about user's credit (minutes, cents, sms...).'
// This can represent a user or a shared group.
type Account struct {
	ID                string
	BalanceMap        map[string]Balances
	UnitCounters      UnitCounters
	ActionTriggers    ActionTriggers
	AllowNegative     bool
	Disabled          bool
	UpdateTime        time.Time
	executingTriggers bool
}

type AccountWithAPIOpts struct {
	*Account
	APIOpts map[string]any
}

// User's available minutes for the specified destination
func (acc *Account) getCreditForPrefix(cd *CallDescriptor) (duration time.Duration, credit float64, balances Balances) {
	creditBalances := acc.getBalancesForPrefix(cd.Destination, cd.Category, utils.MetaMonetary, "", cd.TimeStart)

	unitBalances := acc.getBalancesForPrefix(cd.Destination, cd.Category, cd.ToR, "", cd.TimeStart)
	// gather all balances from shared groups
	var extendedCreditBalances Balances
	for _, cb := range creditBalances {
		if len(cb.SharedGroups) > 0 {
			for sg := range cb.SharedGroups {
				if sharedGroup, _ := dm.GetSharedGroup(sg, false, utils.NonTransactional); sharedGroup != nil {
					sgb := sharedGroup.GetBalances(cd.Destination, cd.Category, utils.MetaMonetary, acc, cd.TimeStart)
					sgb = sharedGroup.SortBalancesByStrategy(cb, sgb)
					extendedCreditBalances = append(extendedCreditBalances, sgb...)
				}
			}
		} else {
			extendedCreditBalances = append(extendedCreditBalances, cb)
		}
	}
	var extendedMinuteBalances Balances
	for _, mb := range unitBalances {
		if len(mb.SharedGroups) > 0 {
			for sg := range mb.SharedGroups {
				if sharedGroup, _ := dm.GetSharedGroup(sg, false, utils.NonTransactional); sharedGroup != nil {
					sgb := sharedGroup.GetBalances(cd.Destination, cd.Category, cd.ToR, acc, cd.TimeStart)
					sgb = sharedGroup.SortBalancesByStrategy(mb, sgb)
					extendedMinuteBalances = append(extendedMinuteBalances, sgb...)
				}
			}
		} else {
			extendedMinuteBalances = append(extendedMinuteBalances, mb)
		}
	}
	credit = extendedCreditBalances.GetTotalValue()
	balances = extendedMinuteBalances
	for _, b := range balances {
		d, c := b.GetMinutesForCredit(cd, credit)
		credit = c
		duration += d
	}
	return
}

// sets all the fields of the balance
func (acc *Account) setBalanceAction(a *Action, fltrS *FilterS) error {
	if a == nil {
		return errors.New("nil action")
	}
	if acc.BalanceMap == nil {
		acc.BalanceMap = make(map[string]Balances)
	}
	var balance *Balance
	var found bool
	var previousSharedGroups utils.StringMap            // kept for comparison
	if a.Balance.Uuid != nil && *a.Balance.Uuid != "" { // balance uuid match
		for balanceType := range acc.BalanceMap {
			for _, b := range acc.BalanceMap[balanceType] {
				if b.Uuid == *a.Balance.Uuid && !b.IsExpiredAt(time.Now()) {
					previousSharedGroups = b.SharedGroups
					balance = b
					found = true
					break // only set one balance
				}
			}
			if found {
				break
			}
		}
		if !found {
			return fmt.Errorf("cannot find balance with uuid: <%s>", *a.Balance.Uuid)
		}
	} else { // balance id match
		for balanceType := range acc.BalanceMap {
			for _, b := range acc.BalanceMap[balanceType] {
				if a.Balance.ID != nil && b.ID == *a.Balance.ID && !b.IsExpiredAt(time.Now()) {
					previousSharedGroups = b.SharedGroups
					balance = b
					found = true
					break // only set one balance
				}
			}
			if found {
				break
			}
		}
		// if it is not found then we create it
		if !found {
			if a.Balance.Type == nil { // cannot create the entry in the balance map without this info
				return errors.New("missing balance type")
			}
			balance = &Balance{}
			balance.Uuid = utils.GenUUID() // alway overwrite the uuid for consistency
			acc.BalanceMap[*a.Balance.Type] = append(acc.BalanceMap[*a.Balance.Type], balance)
		}
	}
	if a.Balance.ID != nil && *a.Balance.ID == utils.MetaDefault { // treat it separately since modifyBalance sets expiry and others parameters, not specific for *default
		if a.Balance.Value != nil {
			balance.ID = *a.Balance.ID
			balance.Value = a.Balance.GetValue()
			balance.SetDirty() // Mark the balance as dirty since we have modified and it should be checked by action triggers
		}
	} else {
		a.Balance.ModifyBalance(balance)
	}
	// modify if necessary the shared groups here
	if !found || !previousSharedGroups.Equal(balance.SharedGroups) {
		err := guardian.Guardian.Guard(func() error {
			i := 0
			for sgID := range balance.SharedGroups {
				// add shared group member
				sg, err := dm.GetSharedGroup(sgID, false, utils.NonTransactional)
				if err != nil || sg == nil {
					//than is problem
					utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgID))
				} else {
					if _, found := sg.MemberIds[acc.ID]; !found {
						// add member and save
						if sg.MemberIds == nil {
							sg.MemberIds = make(utils.StringMap)
						}
						sg.MemberIds[acc.ID] = true
						dm.SetSharedGroup(sg)
					}
				}
				i++
			}
			return nil
		}, config.CgrConfig().GeneralCfg().LockingTimeout, balance.SharedGroups.Slice()...)
		if err != nil {
			return err
		}
	}
	acc.InitCounters()
	acc.ExecuteActionTriggers(nil, fltrS)
	return nil
}

// Debits some amount of user's specified balance adding the balance if it does not exists.
// Returns the remaining credit in user's balance.
func (acc *Account) debitBalanceAction(a *Action, reset, resetIfNegative bool, fltrS *FilterS) error {
	if a == nil {
		return errors.New("nil action")
	}
	bClone := a.Balance.CreateBalance()
	//log.Print("Bclone: ", utils.ToJSON(a.Balance))
	if bClone == nil {
		return errors.New("nil balance in action")
	}
	if acc.BalanceMap == nil {
		acc.BalanceMap = make(map[string]Balances)
	}
	found := false
	balanceType := a.Balance.GetType()
	for _, b := range acc.BalanceMap[balanceType] {
		if b.IsExpiredAt(time.Now()) {
			continue // just to be safe (cleaned expired balances above)
		}
		b.account = acc
		if b.MatchFilter(a.Balance, "", false, false) {
			if reset || (resetIfNegative && b.Value < 0) {
				b.SetValue(0)
			}
			b.SubtractValue(bClone.GetValue())
			b.dirty = true
			found = true
			a.balanceValue = b.GetValue()
		}
	}
	// if it is not found then we add it to the list
	if !found {
		// check if the Id is *default (user trying to create the default balance)
		// use only it's value value
		if bClone.ID == utils.MetaDefault {
			bClone = &Balance{
				ID:    utils.MetaDefault,
				Value: -bClone.GetValue(),
			}
		} else {
			if bClone.GetValue() != 0 {
				bClone.SetValue(-bClone.GetValue())
			}
		}
		bClone.dirty = true // Mark the balance as dirty since we have modified and it should be checked by action triggers
		a.balanceValue = bClone.GetValue()
		bClone.Uuid = utils.GenUUID() // alway overwrite the uuid for consistency
		// load ValueFactors if defined in extra parametrs
		if a.ExtraParameters != "" {
			vf := ValueFactors{}
			err := json.Unmarshal([]byte(a.ExtraParameters), &vf)
			if err == nil {
				bClone.Factors = vf
			} else {
				utils.Logger.Warning(fmt.Sprintf("Could load value factors from actions: extra parametrs: %s", a.ExtraParameters))
			}
		}
		acc.BalanceMap[balanceType] = append(acc.BalanceMap[balanceType], bClone)
		err := guardian.Guardian.Guard(func() error {
			sgs := make([]string, len(bClone.SharedGroups))
			i := 0
			for sgID := range bClone.SharedGroups {
				// add shared group member
				sg, err := dm.GetSharedGroup(sgID, false, utils.NonTransactional)
				if err != nil || sg == nil {
					//than is problem
					utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgID))
				} else {
					if _, found := sg.MemberIds[acc.ID]; !found {
						// add member and save
						if sg.MemberIds == nil {
							sg.MemberIds = make(utils.StringMap)
						}
						sg.MemberIds[acc.ID] = true
						dm.SetSharedGroup(sg)
					}
				}
				i++
			}
			dm.CacheDataFromDB(utils.SharedGroupPrefix, sgs, true)
			return nil
		}, config.CgrConfig().GeneralCfg().LockingTimeout, bClone.SharedGroups.Slice()...)
		if err != nil {
			return err
		}
	}
	acc.InitCounters()
	acc.ExecuteActionTriggers(nil, fltrS)
	return nil
}

func (acc *Account) getBalancesForPrefix(prefix, category, tor,
	sharedGroup string, aTime time.Time) Balances {
	var balances Balances
	balances = append(balances, acc.BalanceMap[tor]...)
	if tor != utils.MetaMonetary && tor != utils.MetaGeneric {
		balances = append(balances, acc.BalanceMap[utils.MetaGeneric]...)
	}

	var usefulBalances Balances
	for _, b := range balances {
		if b.Disabled {
			continue
		}
		if b.IsExpiredAt(aTime) || (len(b.SharedGroups) == 0 && b.GetValue() <= 0 && !b.Blocker) {
			continue
		}
		if sharedGroup != "" && !b.SharedGroups[sharedGroup] {
			continue
		}
		if !b.MatchCategory(category) {
			continue
		}
		b.account = acc

		if len(b.DestinationIDs) > 0 && !b.DestinationIDs[utils.MetaAny] {
			for _, p := range utils.SplitPrefix(prefix, MIN_PREFIX_MATCH) {
				if destIDs, err := dm.GetReverseDestination(p, true, true, utils.NonTransactional); err == nil {
					foundResult := false
					allInclude := true // whether it is excluded or included
					for _, dID := range destIDs {
						inclDest, found := b.DestinationIDs[dID]
						if found {
							foundResult = true
							allInclude = allInclude && inclDest
						}
					}
					// check wheter all destination ids in the balance were exclusions
					allExclude := true
					for _, inclDest := range b.DestinationIDs {
						if inclDest {
							allExclude = false
							break
						}
					}
					if foundResult || allExclude {
						if allInclude {
							b.precision = len(p)
							usefulBalances = append(usefulBalances, b)
						} else {
							b.precision = 1 // fake to exit the outer loop
						}
					}
				}
				if b.precision > 0 {
					break
				}
			}
		} else {
			usefulBalances = append(usefulBalances, b)
		}
	}

	// resort by precision
	usefulBalances.Sort()
	// clear precision
	for _, b := range usefulBalances {
		b.precision = 0
	}
	return usefulBalances
}

// like getBalancesForPrefix but expanding shared balances
func (acc *Account) getAlldBalancesForPrefix(destination, category,
	balanceType string, aTime time.Time) (bc Balances) {
	balances := acc.getBalancesForPrefix(destination, category, balanceType, "", aTime)
	for _, b := range balances {
		if len(b.SharedGroups) > 0 {
			for sgID := range b.SharedGroups {
				sharedGroup, err := dm.GetSharedGroup(sgID, false, utils.NonTransactional)
				if err != nil || sharedGroup == nil {
					utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgID))
					continue
				}
				sharedBalances := sharedGroup.GetBalances(destination, category, balanceType, acc, aTime)
				sharedBalances = sharedGroup.SortBalancesByStrategy(b, sharedBalances)
				bc = append(bc, sharedBalances...)
			}
		} else {
			bc = append(bc, b)
		}
	}
	return
}

func (acc *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun bool, goNegative bool, fltrS *FilterS) (cc *CallCost, err error) {
	usefulUnitBalances := acc.getAlldBalancesForPrefix(cd.Destination, cd.Category, cd.ToR, cd.TimeStart)
	usefulMoneyBalances := acc.getAlldBalancesForPrefix(cd.Destination, cd.Category, utils.MetaMonetary, cd.TimeStart)
	// intiValues map[UUID]float64 and pass them to publish updating initial value
	initUnitBal, initMoneyBal := balancesValues(usefulUnitBalances), balancesValues(usefulMoneyBalances)

	var leftCC *CallCost
	cc = cd.CreateCallCost()
	var hadBalanceSubj bool
	generalBalanceChecker := true
	for generalBalanceChecker {
		generalBalanceChecker = false

		// debit minutes
		unitBalanceChecker := true
		for unitBalanceChecker {
			// try every balance multiple times in case one becomes active or ratig changes
			unitBalanceChecker = false
			for _, balance := range usefulUnitBalances {
				partCC, debitErr := balance.debit(cd, balance.account,
					usefulMoneyBalances, count, dryRun, len(cc.Timespans) == 0, true, fltrS)
				if debitErr != nil {
					return nil, debitErr
				}
				if balance.RatingSubject != "" &&
					!strings.HasPrefix(balance.RatingSubject, utils.MetaRatingSubjectPrefix) {
					hadBalanceSubj = true
				}
				if partCC != nil {
					cc.Timespans = append(cc.Timespans, partCC.Timespans...)
					cc.negativeConnectFee = partCC.negativeConnectFee
					cd.TimeStart = cc.GetEndTime()
					// check if the calldescriptor is covered
					if cd.GetDuration() <= 0 {
						goto COMMIT
					}
					unitBalanceChecker = true
					generalBalanceChecker = true
					// check for max cost disconnect
					if dryRun && partCC.maxCostDisconect {
						// only return if we are in dry run (max call duration)
						return
					}
				}
				// check for blocker
				if balance.Blocker {
					if cd.GetDuration() != 0 {
						if !dryRun {
							return nil, utils.ErrInsufficientCreditBalanceBlocker
						}
						return
					}
					goto COMMIT // don't go to next balances
				}
			}
		}
		// debit money
		moneyBalanceChecker := true
		for moneyBalanceChecker {
			// try every balance multiple times in case one becomes active or ratig changes
			moneyBalanceChecker = false
			for _, balance := range usefulMoneyBalances {
				partCC, debitErr := balance.debit(cd, balance.account,
					usefulMoneyBalances, count, dryRun, len(cc.Timespans) == 0, false, fltrS)
				if debitErr != nil {
					return nil, debitErr
				}
				if partCC != nil {
					cc.Timespans = append(cc.Timespans, partCC.Timespans...)
					cc.negativeConnectFee = partCC.negativeConnectFee

					cd.TimeStart = cc.GetEndTime()
					// check if the calldescriptor is covered
					if cd.GetDuration() <= 0 {
						goto COMMIT
					}
					moneyBalanceChecker = true
					generalBalanceChecker = true
					if dryRun && partCC.maxCostDisconect {
						// only return if we are in dry run (max call duration)
						return
					}
				}
				// check for blocker
				if balance.Blocker {
					if cd.GetDuration() != 0 {
						if !dryRun {
							return nil, utils.ErrInsufficientCreditBalanceBlocker
						}
						return
					}
					goto COMMIT // don't go to next balances
				}
			}
		}
	}
	if hadBalanceSubj {
		cd.RatingInfos = nil
	}
	leftCC, err = cd.getCost()
	if err != nil {
		utils.Logger.Err(fmt.Sprintf("Error getting new cost for balance subject: %v", err))
	}
	if leftCC.Cost == 0 && len(leftCC.Timespans) > 0 {
		// put AccountID ubformation in increments
		for _, ts := range leftCC.Timespans {
			for _, inc := range ts.Increments {
				if inc.BalanceInfo == nil {
					inc.BalanceInfo = &DebitInfo{}
				}
				inc.BalanceInfo.AccountID = acc.ID
			}
		}
		cc.Timespans = append(cc.Timespans, leftCC.Timespans...)
	}

	if leftCC.Cost > 0 && goNegative {
		initialLength := len(cc.Timespans)
		cc.Timespans = append(cc.Timespans, leftCC.Timespans...)

		var debitedConnectFeeBalance Balance
		var ok bool

		if initialLength == 0 {
			// this is the first add, debit the connect fee
			ok, debitedConnectFeeBalance = acc.DebitConnectionFee(cc, usefulMoneyBalances, count, true, fltrS)
		}
		// get the default money balance
		// and go negative on it with the amount still unpaid
		if len(leftCC.Timespans) > 0 && leftCC.Cost > 0 && !acc.AllowNegative && !dryRun {
			utils.Logger.Warning(fmt.Sprintf("<Rater> Going negative on account %s with AllowNegative: false", cd.GetAccountKey()))
		}
		leftCC.Timespans.Decompress()
		for tsIndex, ts := range leftCC.Timespans {
			if ts.Increments == nil {
				ts.createIncrementsSlice()
			}

			if tsIndex == 0 && ts.RateInterval.Rating.ConnectFee > 0 && cc.deductConnectFee && ok {

				inc := &Increment{
					Duration: 0,
					Cost:     ts.RateInterval.Rating.ConnectFee,
					BalanceInfo: &DebitInfo{
						Monetary: &MonetaryInfo{
							UUID:  debitedConnectFeeBalance.Uuid,
							ID:    debitedConnectFeeBalance.ID,
							Value: debitedConnectFeeBalance.Value,
						},
						AccountID: acc.ID,
					},
				}

				incs := []*Increment{inc}
				ts.Increments = append(incs, ts.Increments...)
			}

			for incIndex, increment := range ts.Increments {
				// connect fee was processed and skip it
				if tsIndex == 0 && incIndex == 0 && ts.RateInterval.Rating.ConnectFee > 0 && cc.deductConnectFee && ok {
					continue
				}
				cost := increment.Cost
				defaultBalance := acc.GetDefaultMoneyBalance()
				defaultBalance.SubtractValue(cost)

				increment.BalanceInfo.Monetary = &MonetaryInfo{
					UUID:  defaultBalance.Uuid,
					ID:    defaultBalance.ID,
					Value: defaultBalance.Value,
				}
				increment.BalanceInfo.AccountID = acc.ID
				if count {
					acc.countUnits(
						cost,
						utils.MetaMonetary,
						leftCC,
						&Balance{
							Value:          cost,
							DestinationIDs: utils.NewStringMap(leftCC.Destination),
						}, fltrS)
				}
			}
		}
	}

COMMIT:
	if !dryRun {
		// save darty shared balances
		usefulMoneyBalances.SaveDirtyBalances(acc, initMoneyBal)
		usefulUnitBalances.SaveDirtyBalances(acc, initUnitBal)
	}
	//log.Printf("Final CC: %+v", cc)
	return
}

// GetDefaultMoneyBalance returns the defaultmoney balance
func (acc *Account) GetDefaultMoneyBalance() *Balance {
	for _, balance := range acc.BalanceMap[utils.MetaMonetary] {
		if balance.IsDefault() {
			return balance
		}
	}
	// create default balance
	defaultBalance := &Balance{
		Uuid: utils.GenUUID(),
		ID:   utils.MetaDefault,
	} // minimum weight
	if acc.BalanceMap == nil {
		acc.BalanceMap = make(map[string]Balances)
	}
	acc.BalanceMap[utils.MetaMonetary] = append(acc.BalanceMap[utils.MetaMonetary], defaultBalance)
	return defaultBalance
}

// ExecuteActionTriggers scans the action triggers and execute the actions for which trigger is met
func (acc *Account) ExecuteActionTriggers(a *Action, fltrS *FilterS) {
	if acc.executingTriggers {
		return
	}
	acc.executingTriggers = true
	defer func() {
		acc.executingTriggers = false
	}()

	acc.ActionTriggers.Sort()
	for _, at := range acc.ActionTriggers {
		// check is effective
		if at.IsExpired(time.Now()) || !at.IsActive(time.Now()) {
			continue
		}

		// sanity check
		if !strings.Contains(at.ThresholdType, "counter") && !strings.Contains(at.ThresholdType, "balance") && !strings.Contains(at.ThresholdType, "connect") {
			continue
		}
		if at.Executed {
			// trigger is marked as executed, so skipp it until
			// the next reset (see RESET_TRIGGERS action type)
			continue
		}
		if !at.Match(a) {
			continue
		}
		if strings.Contains(at.ThresholdType, "counter") || strings.Contains(at.ThresholdType, "connect") {
			if (at.Balance.ID == nil || *at.Balance.ID != "") && at.UniqueID != "" {
				at.Balance.ID = utils.StringPointer(at.UniqueID)
			}
			for _, counters := range acc.UnitCounters {
				for _, uc := range counters {
					if strings.Contains(at.ThresholdType, uc.CounterType[1:]) {
						for _, c := range uc.Counters {
							if strings.HasPrefix(at.ThresholdType, "*max") {
								if c.Filter.Equal(at.Balance) && c.Value >= at.ThresholdValue {
									at.Execute(acc, fltrS)
								}
							} else { //MIN
								if c.Filter.Equal(at.Balance) && c.Value <= at.ThresholdValue {
									at.Execute(acc, fltrS)
								}
							}
						}
					}
				}
			}
		} else { // BALANCE
			for _, b := range acc.BalanceMap[at.Balance.GetType()] {
				if !b.dirty && at.ThresholdType != utils.TriggerBalanceExpired { // do not check clean balances
					continue
				}
				switch at.ThresholdType {
				case utils.TriggerMaxBalance:
					if b.MatchActionTrigger(at) && b.GetValue() >= at.ThresholdValue {
						at.Execute(acc, fltrS)
					}
				case utils.TriggerMinBalance:
					if b.MatchActionTrigger(at) && b.GetValue() <= at.ThresholdValue {
						at.Execute(acc, fltrS)
					}
				case utils.TriggerBalanceExpired:
					if b.MatchActionTrigger(at) && b.IsExpiredAt(time.Now()) {
						at.Execute(acc, fltrS)
					}
				}
			}
		}
	}
	acc.CleanExpiredStuff()
}

// ResetActionTriggers marks all action trigers as ready for execution
// If the action is not nil it acts like a filter
func (acc *Account) ResetActionTriggers(a *Action, fltrS *FilterS) {
	for _, at := range acc.ActionTriggers {
		if !at.Match(a) {
			continue
		}
		at.Executed = false
	}
	acc.ExecuteActionTriggers(a, fltrS)
}

// SetRecurrent sets/unsets recurrent flag for action triggers
func (acc *Account) SetRecurrent(a *Action, recurrent bool) {
	for _, at := range acc.ActionTriggers {
		if !at.Match(a) {
			continue
		}
		at.Recurrent = recurrent
	}
}

// Increments the counter for the type
func (acc *Account) countUnits(amount float64, kind string, cc *CallCost, b *Balance, fltrS *FilterS) {
	acc.UnitCounters.addUnits(amount, kind, cc, b)
	acc.ExecuteActionTriggers(nil, fltrS)
}

// InitCounters creates counters for all triggered actions
func (acc *Account) InitCounters() {
	oldUcs := acc.UnitCounters
	acc.UnitCounters = make(UnitCounters)
	ucTempMap := make(map[string]*UnitCounter)
	for _, at := range acc.ActionTriggers {
		//log.Print("AT: ", utils.ToJSON(at))
		if !strings.Contains(at.ThresholdType, "counter") && !strings.Contains(at.ThresholdType, "connect") {
			continue
		}
		ct := utils.MetaCounterEvent //default
		if strings.Contains(at.ThresholdType, "balance") {
			ct = utils.MetaBalance
		}
		uc, exists := ucTempMap[at.Balance.GetType()+ct]
		if !exists {
			uc = &UnitCounter{
				CounterType: ct,
			}
			ucTempMap[at.Balance.GetType()+ct] = uc
			uc.Counters = make(CounterFilters, 0)
			acc.UnitCounters[at.Balance.GetType()] = append(acc.UnitCounters[at.Balance.GetType()], uc)
		}

		c := &CounterFilter{Filter: at.Balance.Clone()}
		if (c.Filter.ID == nil || *c.Filter.ID == "") && at.UniqueID != "" {
			c.Filter.ID = utils.StringPointer(at.UniqueID)
		}
		if !uc.Counters.HasCounter(c) {
			uc.Counters = append(uc.Counters, c)
		}
	}
	// copy old counter values
	for key, counters := range acc.UnitCounters {
		oldCounters, found := oldUcs[key]
		if !found {
			continue
		}
		for _, uc := range counters {
			for _, oldUc := range oldCounters {
				if uc.CopyCounterValues(oldUc) {
					break
				}
			}
		}
	}
	if len(acc.UnitCounters) == 0 {
		acc.UnitCounters = nil // leave it nil if empty
	}
}

// CleanExpiredStuff removed expired balances and actiontriggers
func (acc *Account) CleanExpiredStuff() {
	if config.CgrConfig().RalsCfg().RemoveExpired {
		for key, bm := range acc.BalanceMap {
			for i := 0; i < len(bm); i++ {
				if bm[i].IsExpiredAt(time.Now()) {
					// delete it
					bm = append(bm[:i], bm[i+1:]...)
				}
			}
			acc.BalanceMap[key] = bm
		}
	}

	for i := 0; i < len(acc.ActionTriggers); i++ {
		if acc.ActionTriggers[i].IsExpired(time.Now()) {
			acc.ActionTriggers = append(acc.ActionTriggers[:i], acc.ActionTriggers[i+1:]...)
		}
	}
}

func (acc *Account) allBalancesExpired() bool {
	for _, bm := range acc.BalanceMap {
		for i := 0; i < len(bm); i++ {
			if !bm[i].IsExpiredAt(time.Now()) {
				return false
			}
		}
	}
	return true
}

// GetSharedGroups returns the shared groups that this user balance belnongs to
func (acc *Account) GetSharedGroups() (groups []string) {
	for _, balanceChain := range acc.BalanceMap {
		for _, b := range balanceChain {
			for sg := range b.SharedGroups {
				groups = append(groups, sg)
			}
		}
	}
	return
}

// GetUniqueSharedGroupMembers returns the acounts from the group
func (acc *Account) GetUniqueSharedGroupMembers(cd *CallDescriptor) (utils.StringMap, error) { // ToDo: make sure we return accountIDs
	var balances []*Balance
	balances = append(balances, acc.getBalancesForPrefix(cd.Destination, cd.Category, utils.MetaMonetary, "", cd.TimeStart)...)
	balances = append(balances, acc.getBalancesForPrefix(cd.Destination, cd.Category, cd.ToR, "", cd.TimeStart)...)
	// gather all shared group ids
	var sharedGroupIds []string
	for _, b := range balances {
		for sg := range b.SharedGroups {
			sharedGroupIds = append(sharedGroupIds, sg)
		}
	}
	memberIds := make(utils.StringMap)
	for _, sgID := range sharedGroupIds {
		sharedGroup, err := dm.GetSharedGroup(sgID, false, utils.NonTransactional)
		if err != nil {
			utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgID))
			return nil, err
		}
		for memberID := range sharedGroup.MemberIds {
			memberIds[memberID] = true
		}
	}
	return memberIds, nil
}

// Clone creates a copy of the account
func (acc *Account) Clone() *Account {
	newAcc := &Account{
		ID:            acc.ID,
		UnitCounters:  acc.UnitCounters.Clone(),
		AllowNegative: acc.AllowNegative,
		Disabled:      acc.Disabled,
	}
	if acc.BalanceMap != nil {
		newAcc.BalanceMap = make(map[string]Balances, len(acc.BalanceMap))
		for key, balanceChain := range acc.BalanceMap {
			newAcc.BalanceMap[key] = balanceChain.Clone()
		}
	}
	if acc.ActionTriggers != nil {
		newAcc.ActionTriggers = make(ActionTriggers, len(acc.ActionTriggers))
		for key, actionTrigger := range acc.ActionTriggers {
			newAcc.ActionTriggers[key] = actionTrigger.Clone()
		}
	}
	return newAcc
}

// DebitConnectionFee debits the connection fee
func (acc *Account) DebitConnectionFee(cc *CallCost, ufMoneyBalances Balances, count bool, block bool, fltrS *FilterS) (bool, Balance) {
	var debitedBalance Balance
	if !cc.deductConnectFee {
		return true, debitedBalance
	}
	connectFee := cc.GetConnectFee()
	var connectFeePaid bool
	for _, b := range ufMoneyBalances {
		if !b.IsActiveAt(cc.GetStartTime()) {
			continue
		}
		if b.GetValue() >= connectFee {
			b.SubtractValue(connectFee)
			// the conect fee is not refundable!
			if count {
				acc.countUnits(1, utils.MetaEventConnect, cc, b, fltrS)
				acc.countUnits(connectFee, utils.MetaMonetary, cc, b, fltrS)
			}
			connectFeePaid = true
			debitedBalance = *b
			break
		}
		if b.Blocker && block { // stop here
			return false, debitedBalance
		}
	}
	// debit connect fee
	if connectFee > 0 && !connectFeePaid {
		cc.negativeConnectFee = true
		// there are no money for the connect fee; go negative
		b := acc.GetDefaultMoneyBalance()
		b.SubtractValue(connectFee)
		debitedBalance = *b
		// the conect fee is not refundable!
		if count {
			acc.countUnits(1, utils.MetaEventConnect, cc, b, fltrS)
			acc.countUnits(connectFee, utils.MetaMonetary, cc, b, fltrS)
		}
	}
	return true, debitedBalance
}

// GetID returns the account ID
func (acc *Account) GetID() string {
	split := strings.Split(acc.ID, utils.ConcatenatedKeySep)
	if len(split) != 2 {
		return ""
	}
	return split[1]
}

// AsOldStructure used in some api for transition
func (acc *Account) AsOldStructure() any {
	type Balance struct {
		Uuid           string //system wide unique
		Id             string // account wide unique
		Value          float64
		ExpirationDate time.Time
		Weight         float64
		DestinationIds string
		RatingSubject  string
		Category       string
		SharedGroup    string
		Timings        []*RITiming
		TimingIDs      string
		Disabled       bool
	}
	type Balances []*Balance
	type UnitsCounter struct {
		BalanceType string
		//	Units     float64
		Balances Balances // first balance is the general one (no destination)
	}
	type ActionTrigger struct {
		Id                    string
		ThresholdType         string
		ThresholdValue        float64
		Recurrent             bool
		MinSleep              time.Duration
		BalanceId             string
		BalanceType           string
		BalanceDestinationIds string
		BalanceWeight         float64
		BalanceExpirationDate time.Time
		BalanceTimingTags     string
		BalanceRatingSubject  string
		BalanceCategory       string
		BalanceSharedGroup    string
		BalanceDisabled       bool
		Weight                float64
		ActionsId             string
		MinQueuedItems        int
		Executed              bool
	}
	type ActionTriggers []*ActionTrigger
	type Account struct {
		Id             string
		BalanceMap     map[string]Balances
		UnitCounters   []*UnitsCounter
		ActionTriggers ActionTriggers
		AllowNegative  bool
		Disabled       bool
	}

	result := &Account{
		Id:             utils.MetaOut + utils.ConcatenatedKeySep + acc.ID,
		BalanceMap:     make(map[string]Balances, len(acc.BalanceMap)),
		UnitCounters:   make([]*UnitsCounter, len(acc.UnitCounters)),
		ActionTriggers: make(ActionTriggers, len(acc.ActionTriggers)),
		AllowNegative:  acc.AllowNegative,
		Disabled:       acc.Disabled,
	}
	for balanceType, counters := range acc.UnitCounters {
		for i, uc := range counters {
			if uc == nil {
				continue
			}
			result.UnitCounters[i] = &UnitsCounter{
				BalanceType: balanceType,
				Balances:    make(Balances, len(uc.Counters)),
			}
			if len(uc.Counters) > 0 {
				for j, c := range uc.Counters {
					result.UnitCounters[i].Balances[j] = &Balance{
						Uuid:           c.Filter.GetUuid(),
						Id:             c.Filter.GetID(),
						Value:          c.Filter.GetValue(),
						ExpirationDate: c.Filter.GetExpirationDate(),
						Weight:         c.Filter.GetWeight(),
						DestinationIds: c.Filter.GetDestinationIDs().String(),
						RatingSubject:  c.Filter.GetRatingSubject(),
						Category:       c.Filter.GetCategories().String(),
						SharedGroup:    c.Filter.GetSharedGroups().String(),
						Timings:        c.Filter.Timings,
						TimingIDs:      c.Filter.GetTimingIDs().String(),
						Disabled:       c.Filter.GetDisabled(),
					}
				}
			}
		}
	}
	for i, at := range acc.ActionTriggers {
		b := at.Balance.CreateBalance()
		result.ActionTriggers[i] = &ActionTrigger{
			Id:                    at.ID,
			ThresholdType:         at.ThresholdType,
			ThresholdValue:        at.ThresholdValue,
			Recurrent:             at.Recurrent,
			MinSleep:              at.MinSleep,
			BalanceType:           at.Balance.GetType(),
			BalanceId:             b.ID,
			BalanceDestinationIds: b.DestinationIDs.String(),
			BalanceWeight:         b.Weight,
			BalanceExpirationDate: b.ExpirationDate,
			BalanceTimingTags:     b.TimingIDs.String(),
			BalanceRatingSubject:  b.RatingSubject,
			BalanceCategory:       b.Categories.String(),
			BalanceSharedGroup:    b.SharedGroups.String(),
			BalanceDisabled:       b.Disabled,
			Weight:                at.Weight,
			ActionsId:             at.ActionsID,
			MinQueuedItems:        at.MinQueuedItems,
			Executed:              at.Executed,
		}
	}
	for key, values := range acc.BalanceMap {
		if len(values) > 0 {
			key += utils.MetaOut
			result.BalanceMap[key] = make(Balances, len(values))
			for i, b := range values {
				result.BalanceMap[key][i] = &Balance{
					Uuid:           b.Uuid,
					Id:             b.ID,
					Value:          b.Value,
					ExpirationDate: b.ExpirationDate,
					Weight:         b.Weight,
					DestinationIds: b.DestinationIDs.String(),
					RatingSubject:  b.RatingSubject,
					Category:       b.Categories.String(),
					SharedGroup:    b.SharedGroups.String(),
					Timings:        b.Timings,
					TimingIDs:      b.TimingIDs.String(),
					Disabled:       b.Disabled,
				}
			}
		}
	}
	return result
}

// AsAccountSummary converts the account into AccountSummary
func (acc *Account) AsAccountSummary() *AccountSummary {
	idSplt := strings.Split(acc.ID, utils.ConcatenatedKeySep)
	ad := &AccountSummary{AllowNegative: acc.AllowNegative, Disabled: acc.Disabled}
	if len(idSplt) == 1 {
		ad.ID = idSplt[0]
	} else if len(idSplt) == 2 {
		ad.Tenant = idSplt[0]
		ad.ID = idSplt[1]
	}

	for _, balanceType := range []string{utils.MetaData, utils.MetaSMS, utils.MetaMMS, utils.MetaVoice, utils.MetaGeneric, utils.MetaMonetary} {
		balances, has := acc.BalanceMap[balanceType]
		if !has {
			continue
		}
		for _, balance := range balances {
			ad.BalanceSummaries = append(ad.BalanceSummaries, balance.AsBalanceSummary(balanceType))
		}
	}
	return ad
}

// Publish sends the account to stats and threshold
func (acc *Account) Publish(initBal map[string]float64) {
	acntSummary := acc.AsAccountSummary()
	for _, currentBal := range acntSummary.BalanceSummaries {
		currentBal.Initial = initBal[currentBal.UUID]
	}
	cgrEv := &utils.CGREvent{
		Tenant: acntSummary.Tenant,
		ID:     utils.GenUUID(),
		Time:   utils.TimePointer(time.Now()),
		Event:  acntSummary.AsMapInterface(),
		APIOpts: map[string]any{
			utils.MetaEventType: utils.AccountUpdate,
		},
	}
	if len(config.CgrConfig().RalsCfg().ThresholdSConns) != 0 {
		var tIDs []string
		if err := connMgr.Call(context.TODO(), config.CgrConfig().RalsCfg().ThresholdSConns,
			utils.ThresholdSv1ProcessEvent, cgrEv, &tIDs); err != nil &&
			err.Error() != utils.ErrNotFound.Error() {
			utils.Logger.Warning(
				fmt.Sprintf("<AccountS> error: %s processing account event %+v with ThresholdS.", err.Error(), cgrEv))
		}
	}
	if len(config.CgrConfig().RalsCfg().StatSConns) != 0 {
		var stsIDs []string
		if err := connMgr.Call(context.TODO(), config.CgrConfig().RalsCfg().StatSConns,
			utils.StatSv1ProcessEvent, cgrEv, &stsIDs); err != nil &&
			err.Error() != utils.ErrNotFound.Error() {
			utils.Logger.Warning(
				fmt.Sprintf("<AccountS> error: %s processing account event %+v with StatS.", err.Error(), cgrEv))
		}
	}
}

func balancesValues(bals Balances) (init map[string]float64) {
	init = make(map[string]float64)
	for _, bal := range bals {
		init[bal.Uuid] = bal.Value
	}
	return
}

// NewAccountSummaryFromJSON creates a new AcccountSummary from a json string
func NewAccountSummaryFromJSON(jsn string) (acntSummary *AccountSummary, err error) {
	if !slices.Contains([]string{"", "null"}, jsn) { // Unmarshal only when content
		err = json.Unmarshal([]byte(jsn), &acntSummary)
	}
	return
}

// AccountSummary contains compressed information about an Account
type AccountSummary struct {
	Tenant           string
	ID               string
	BalanceSummaries BalanceSummaries
	AllowNegative    bool
	Disabled         bool
}

// Clone creates a copy of the structure
func (as *AccountSummary) Clone() (cln *AccountSummary) {
	cln = new(AccountSummary)
	cln.Tenant = as.Tenant
	cln.ID = as.ID
	cln.AllowNegative = as.AllowNegative
	cln.Disabled = as.Disabled
	if as.BalanceSummaries != nil {
		cln.BalanceSummaries = make(BalanceSummaries, len(as.BalanceSummaries))
		for i, bs := range as.BalanceSummaries {
			cln.BalanceSummaries[i] = new(BalanceSummary)
			*cln.BalanceSummaries[i] = *bs
		}
	}
	return
}

// UpdateInitialValue keeps the old initial balance value
func (as *AccountSummary) UpdateInitialValue(old *AccountSummary) {
	if old == nil {
		return
	}
	for _, initialBal := range old.BalanceSummaries {
		removed := true
		for _, currentBal := range as.BalanceSummaries {
			if currentBal.UUID == initialBal.UUID {
				currentBal.Initial = initialBal.Initial
				removed = false
				break
			}
		}
		if removed { // add back the expired balances
			initialBal.Value = 0   // it expired so lose all the values
			initialBal.Initial = 0 // only keep track of it in this
			as.BalanceSummaries = append(as.BalanceSummaries, initialBal)
		}
	}
}

// SetInitialValue set initial balance value
func (as *AccountSummary) SetInitialValue(old *AccountSummary) {
	if old == nil {
		return
	}
	for _, initialBal := range old.BalanceSummaries {
		removed := true
		for _, currentBal := range as.BalanceSummaries {
			if currentBal.UUID == initialBal.UUID {
				currentBal.Initial = initialBal.Value
				removed = false
				break
			}
		}
		if removed { // add back the expired balances
			initialBal.Value = 0   // it expired so lose all the values
			initialBal.Initial = 0 // only keep track of it in this
			as.BalanceSummaries = append(as.BalanceSummaries, initialBal)
		}
	}
}

// FieldAsInterface func to help EventCost FieldAsInterface
func (as *AccountSummary) FieldAsInterface(fldPath []string) (val any, err error) {
	if as == nil || len(fldPath) == 0 {
		return nil, utils.ErrNotFound
	}
	switch fldPath[0] {
	default:
		opath, indx := utils.GetPathIndex(fldPath[0])
		if opath == utils.BalanceSummaries && indx != nil {
			if len(as.BalanceSummaries) <= *indx {
				return nil, utils.ErrNotFound
			}
			bl := as.BalanceSummaries[*indx]
			if len(fldPath) == 1 {
				return bl, nil
			}
			return bl.FieldAsInterface(fldPath[1:])
		}
		return nil, fmt.Errorf("unsupported field prefix: <%s>", fldPath[0])
	case utils.Tenant:
		if len(fldPath) != 1 {
			return nil, utils.ErrNotFound
		}
		return as.Tenant, nil
	case utils.ID:
		if len(fldPath) != 1 {
			return nil, utils.ErrNotFound
		}
		return as.ID, nil
	case utils.BalanceSummaries:
		if len(fldPath) == 1 {
			return as.BalanceSummaries, nil
		}
		for _, bs := range as.BalanceSummaries {
			if bs.ID == fldPath[1] {
				if len(fldPath) == 2 {
					return bs, nil
				}
				return bs.FieldAsInterface(fldPath[2:])
			}
		}
		return nil, utils.ErrNotFound
	case utils.AllowNegative:
		if len(fldPath) != 1 {
			return nil, utils.ErrNotFound
		}
		return as.AllowNegative, nil
	case utils.Disabled:
		if len(fldPath) != 1 {
			return nil, utils.ErrNotFound
		}
		return as.Disabled, nil
	}
}

func (as *AccountSummary) FieldAsString(fldPath []string) (val string, err error) {
	var iface any
	iface, err = as.FieldAsInterface(fldPath)
	if err != nil {
		return
	}
	return utils.IfaceAsString(iface), nil
}

// String implements utils.DataProvider
func (as *AccountSummary) String() string {
	return utils.ToIJSON(as)
}

func (as *AccountSummary) AsMapInterface() map[string]any {
	return map[string]any{
		utils.Tenant:           as.Tenant,
		utils.ID:               as.ID,
		utils.AllowNegative:    as.AllowNegative,
		utils.Disabled:         as.Disabled,
		utils.BalanceSummaries: as.BalanceSummaries,
	}
}

// processAccountSummaryField ensures accSummary is an AccountSummary and calls FieldAsInterface on it.
func processAccountSummaryField(fldPath []string, accSummary any, event map[string]any) (any, error) {
	var err error
	var accSummaryBytes []byte
	switch accSum := accSummary.(type) {
	case *AccountSummary:
		// Directly proceed if already *AccountSummary.
		return accSum.FieldAsInterface(fldPath)
	case string:
		// Convert string to bytes for unmarshalling
		// if it's a serialized *AccountSummary.
		accSummaryBytes = []byte(accSum)
	default:
		// Marshal non-string types to JSON bytes
		// for unmarshalling into *AccountSummary.
		accSummaryBytes, err = json.Marshal(accSum)
		if err != nil {
			return nil, err
		}
	}
	var as AccountSummary
	if err = json.Unmarshal(accSummaryBytes, &as); err != nil {
		return nil, err
	}

	// Update AccountSummary with the unmarshalled *AccountSummary
	// to avoid repetitive serialization.
	event[utils.AccountSummary] = &as

	return as.FieldAsInterface(fldPath)
}

// GetBalanceWithID returns a Balance given balance type and balance ID
func (acc *Account) GetBalanceWithID(blcType, blcID string) (blc *Balance) {
	for _, blc = range acc.BalanceMap[blcType] {
		if blc.ID == blcID {
			return
		}
	}
	return nil
}

// FindBalanceByID searches through all balance types for a balance with the
// specified ID and returns it alongside its type.
func (acc *Account) FindBalanceByID(balanceID string) (blnc *Balance, blncType string) {
	for balanceType, balances := range acc.BalanceMap {
		for _, balance := range balances {
			if balance.ID == balanceID {
				return balance, balanceType
			}
		}
	}
	return nil, ""
}

func (acc *Account) String() string {
	return utils.ToJSON(acc)
}

func (acc *Account) FieldAsInterface(fldPath []string) (val any, err error) {
	if len(fldPath) == 0 {
		return acc, nil
	}
	if acc == nil {
		return nil, utils.ErrNotFound
	}
	switch fldPath[0] {
	default:
		opath, indx := utils.GetPathIndexString(fldPath[0])
		if indx != nil {
			switch opath {
			case utils.BalanceMap:
				bl, has := acc.BalanceMap[*indx]
				if !has {
					return nil, utils.ErrNotFound
				}
				if len(fldPath) == 1 {
					return bl, nil
				}
				return bl.FieldAsInterface(fldPath[1:])
			case utils.UnitCounters:
				uc, has := acc.UnitCounters[*indx]
				if !has || len(fldPath) != 1 {
					return nil, utils.ErrNotFound
				}
				return uc, nil
			case utils.ActionTriggers:
				var idx int
				if idx, err = strconv.Atoi(*indx); err != nil {
					return
				}
				if len(acc.ActionTriggers) <= idx {
					return nil, utils.ErrNotFound
				}
				at := acc.ActionTriggers[idx]
				if len(fldPath) == 1 {
					return at, nil
				}
				return at.FieldAsInterface(fldPath[1:])
			}
		}
		return nil, fmt.Errorf("unsupported field prefix: <%s>", fldPath[0])
	case utils.ID:
		if len(fldPath) != 1 {
			return nil, utils.ErrNotFound
		}
		return acc.ID, nil
	case utils.BalanceMap:
		if len(fldPath) == 1 {
			return acc.BalanceMap, nil
		}
		opath, indx := utils.GetPathIndex(fldPath[1])
		if bc, has := acc.BalanceMap[opath]; has {
			if indx != nil {
				if len(bc) <= *indx {
					return nil, utils.ErrNotFound
				}
				c := bc[*indx]
				if len(fldPath) == 2 {
					return c, nil
				}
				return c.FieldAsInterface(fldPath[2:])
			}
			if len(fldPath) == 2 {
				return bc, nil
			}
			return bc.FieldAsInterface(fldPath[2:])
		}
		return nil, utils.ErrNotFound
	case utils.UnitCounters:
		if len(fldPath) == 1 {
			return acc.UnitCounters, nil
		}
		if uc, has := acc.UnitCounters[fldPath[1]]; has {
			if len(fldPath) == 2 {
				return uc, nil
			}
			var indx int
			if indx, err = strconv.Atoi(fldPath[2]); err != nil {
				return
			}
			if len(uc) <= indx {
				return nil, utils.ErrNotFound
			}
			c := uc[indx]
			if len(fldPath) == 1 {
				return c, nil
			}
			return c.FieldAsInterface(fldPath[3:])
		}
		return nil, utils.ErrNotFound
	case utils.ActionTriggers:
		if len(fldPath) == 1 {
			return acc.ActionTriggers, nil
		}
		for _, at := range acc.ActionTriggers {
			if at.ID == fldPath[1] {
				if len(fldPath) == 2 {
					return at, nil
				}
				return at.FieldAsInterface(fldPath[2:])
			}
		}
		return nil, utils.ErrNotFound
	case utils.AllowNegative:
		if len(fldPath) != 1 {
			return nil, utils.ErrNotFound
		}
		return acc.AllowNegative, nil
	case utils.Disabled:
		if len(fldPath) != 1 {
			return nil, utils.ErrNotFound
		}
		return acc.Disabled, nil
	case utils.UpdateTime:
		if len(fldPath) != 1 {
			return nil, utils.ErrNotFound
		}
		return acc.UpdateTime, nil
	}
}

func (acc *Account) FieldAsString(fldPath []string) (val string, err error) {
	var iface any
	iface, err = acc.FieldAsInterface(fldPath)
	if err != nil {
		return
	}
	return utils.IfaceAsString(iface), nil
}
