/*
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 sessions

import (
	"errors"
	"slices"
	"strings"
	"time"

	"github.com/cgrates/birpc/context"
	"github.com/cgrates/cgrates/attributes"
	"github.com/cgrates/cgrates/config"
	"github.com/cgrates/cgrates/engine"
	"github.com/cgrates/cgrates/routes"
	"github.com/cgrates/cgrates/utils"
	jwt "github.com/dgrijalva/jwt-go"
)

var unratedReqs = engine.MapEvent{
	utils.MetaPostpaid:      struct{}{},
	utils.MetaPseudoPrepaid: struct{}{},
	utils.MetaRated:         struct{}{},
}

var authReqs = engine.MapEvent{
	utils.MetaPrepaid:       struct{}{},
	utils.MetaPseudoPrepaid: struct{}{},
}

var methodFlags = map[string][]string{
	utils.MetaAuthorize: {
		utils.MetaAccounts,
		utils.MetaAttributes,
		utils.MetaChargers,
		utils.MetaIPs,
		utils.MetaResources,
		utils.MetaRoutes,
		utils.MetaStats,
		utils.MetaThresholds,
	},
	utils.MetaInitiate: {
		utils.MetaInitiate,
		utils.MetaAttributes,
		utils.MetaChargers,
		utils.MetaIPs,
		utils.MetaResources,
		utils.MetaStats,
		utils.MetaThresholds,
	},
	utils.MetaUpdate: {
		utils.MetaUpdate,
		utils.MetaAttributes,
	},
	utils.MetaTerminate: {
		utils.MetaTerminate,
		utils.MetaIPs,
		utils.MetaResources,
		utils.MetaStats,
		utils.MetaThresholds,
	},
}

// ApplyFlags sets relevant flags in opts based on request type and processor flags.
func ApplyFlags(reqType string, flags utils.FlagsWithParams, opts map[string]any) {
	if flagList, exists := methodFlags[reqType]; exists {
		for _, flag := range flagList {
			if flags.Has(flag) {
				opts[flag] = flags.GetBool(flag)
			}
		}
	}
}

// BiRPCClient is the interface implemented by Agents which are able to
// communicate bidirectionally with SessionS and remote Communication Switch
type BiRPCClient interface {
	V1DisconnectSession(*context.Context, utils.CGREvent, *string) error
	V1GetActiveSessionIDs(*context.Context, string, *[]*SessionID) error
	V1AlterSession(*context.Context, utils.CGREvent, *string) error
	V1DisconnectPeer(*context.Context, *utils.DPRArgs, *string) error
	V1WarnDisconnect(*context.Context, map[string]any, *string) error
}

// GetSetOptsOriginID will populate the originID key if not present and return it
func GetSetOptsOriginID(ev engine.MapEvent, opt map[string]any) (originID string) {
	fldIface, has := opt[utils.MetaOriginID]
	if has {
		originID = utils.IfaceAsString(fldIface)
	} else {
		originID = utils.Sha1(ev.GetStringIgnoreErrors(utils.OriginID),
			ev.GetStringIgnoreErrors(utils.OriginHost))
		opt[utils.MetaOriginID] = originID
	}
	return
}

func getFlagIDs(flag string) []string {
	flagWithIDs := strings.Split(flag, utils.InInFieldSep)
	if len(flagWithIDs) <= 1 {
		return nil
	}
	return strings.Split(flagWithIDs[1], utils.ANDSep)
}

// ProcessedStirIdentity the structure that keeps all the header information
type ProcessedStirIdentity struct {
	Tokens     []string
	SigningStr string
	Signature  string
	Header     *utils.PASSporTHeader
	Payload    *utils.PASSporTPayload
}

// NewProcessedIdentity creates a proccessed header
func NewProcessedIdentity(identity string) (pi *ProcessedStirIdentity, err error) {
	pi = new(ProcessedStirIdentity)
	hdrtoken := strings.Split(utils.RemoveWhiteSpaces(identity), utils.InfieldSep)

	if len(hdrtoken) == 1 {
		err = errors.New("missing parts of the message header")
		return
	}
	pi.Tokens = hdrtoken[1:]
	btoken := utils.SplitPath(hdrtoken[0], utils.NestingSep[0], -1)
	if len(btoken) != 3 {
		err = errors.New("wrong header format")
		return
	}
	pi.SigningStr = btoken[0] + utils.NestingSep + btoken[1]
	pi.Signature = btoken[2]

	pi.Header = new(utils.PASSporTHeader)
	if err = utils.DecodeBase64JSON(btoken[0], pi.Header); err != nil {
		return
	}
	pi.Payload = new(utils.PASSporTPayload)
	err = utils.DecodeBase64JSON(btoken[1], pi.Payload)
	return
}

// VerifyHeader returns if the header is corectly populated
func (pi *ProcessedStirIdentity) VerifyHeader() (isValid bool) {
	var x5u string
	for _, pair := range pi.Tokens {
		ptoken := strings.Split(pair, utils.AttrValueSep)
		if len(ptoken) != 2 {
			continue
		}
		switch ptoken[0] {
		case utils.STIRAlgField:
			if ptoken[1] != utils.STIRAlg {
				return false
			}
		case utils.STIRPptField:
			if ptoken[1] != utils.STIRPpt &&
				ptoken[1] != "\""+utils.STIRPpt+"\"" {
				return false
			}
		case utils.STIRInfoField:
			lenParamInfo := len(ptoken[1])
			if lenParamInfo <= 2 {
				return false
			}
			x5u = ptoken[1]
			if x5u[0] == '<' && x5u[lenParamInfo-1] == '>' {
				x5u = x5u[1 : lenParamInfo-1]
			}
		}
	}

	return pi.Header.Alg == utils.STIRAlg &&
		pi.Header.Ppt == utils.STIRPpt &&
		pi.Header.Typ == utils.STIRTyp &&
		pi.Header.X5u == x5u
}

// VerifySignature returns if the signature is valid
func (pi *ProcessedStirIdentity) VerifySignature(ctx *context.Context, timeoutVal time.Duration) (err error) {
	var pubkey any
	var ok bool
	if pubkey, ok = engine.Cache.Get(utils.CacheSTIR, pi.Header.X5u); !ok {
		if pubkey, err = utils.NewECDSAPubKey(pi.Header.X5u, timeoutVal); err != nil {
			if errCh := engine.Cache.Set(ctx, utils.CacheSTIR, pi.Header.X5u, nil,
				nil, false, utils.NonTransactional); errCh != nil {
				return errCh
			}
			return
		}
		if errCh := engine.Cache.Set(ctx, utils.CacheSTIR, pi.Header.X5u, pubkey,
			nil, false, utils.NonTransactional); errCh != nil {
			return errCh
		}
	}

	sigMethod := jwt.GetSigningMethod(pi.Header.Alg)
	return sigMethod.Verify(pi.SigningStr, pi.Signature, pubkey)

}

// VerifyPayload returns if the payload is corectly populated
func (pi *ProcessedStirIdentity) VerifyPayload(originatorTn, originatorURI, destinationTn, destinationURI string,
	hdrMaxDur time.Duration, attest utils.StringSet) (err error) {
	if !attest.Has(utils.MetaAny) && !attest.Has(pi.Payload.ATTest) {
		return errors.New("wrong attest level")
	}
	if hdrMaxDur >= 0 && time.Now().After(time.Unix(pi.Payload.IAT, 0).Add(hdrMaxDur)) {
		return errors.New("expired payload")
	}
	if originatorURI != utils.EmptyString {
		if originatorURI != pi.Payload.Orig.URI {
			return errors.New("wrong originatorURI")
		}
	} else if originatorTn != pi.Payload.Orig.Tn {
		return errors.New("wrong originatorTn")
	}
	if destinationURI != utils.EmptyString {
		if !slices.Contains(pi.Payload.Dest.URI, destinationURI) {
			return errors.New("wrong destinationURI")
		}
	} else if !slices.Contains(pi.Payload.Dest.Tn, destinationTn) {
		return errors.New("wrong destinationTn")
	}
	return
}

// NewSTIRIdentity returns the identiy for stir header
func NewSTIRIdentity(ctx *context.Context, header *utils.PASSporTHeader, payload *utils.PASSporTPayload, prvkeyPath string, timeout time.Duration) (identity string, err error) {
	var prvKey any
	var ok bool
	if prvKey, ok = engine.Cache.Get(utils.CacheSTIR, prvkeyPath); !ok {
		if prvKey, err = utils.NewECDSAPrvKey(prvkeyPath, timeout); err != nil {
			if errCh := engine.Cache.Set(ctx, utils.CacheSTIR, prvkeyPath, nil,
				nil, false, utils.NonTransactional); errCh != nil {
				return utils.EmptyString, errCh
			}
			return
		}
		if errCh := engine.Cache.Set(ctx, utils.CacheSTIR, prvkeyPath, prvKey,
			nil, false, utils.NonTransactional); errCh != nil {
			return utils.EmptyString, errCh
		}
	}
	var headerStr, payloadStr string
	if headerStr, err = utils.EncodeBase64JSON(header); err != nil {
		return
	}
	if payloadStr, err = utils.EncodeBase64JSON(payload); err != nil {
		return
	}
	identity = headerStr + utils.NestingSep + payloadStr

	sigMethod := jwt.GetSigningMethod(header.Alg)
	var signature string
	if signature, err = sigMethod.Sign(identity, prvKey); err != nil {
		return
	}
	identity += utils.NestingSep + signature
	identity += utils.STIRExtraInfoPrefix + header.X5u + utils.STIRExtraInfoSuffix
	return
}

// AuthStirShaken autentificates the given identity using STIR/SHAKEN
func AuthStirShaken(ctx *context.Context, identity, originatorTn, originatorURI, destinationTn, destinationURI string,
	attest utils.StringSet, hdrMaxDur time.Duration) (err error) {
	var pi *ProcessedStirIdentity
	if pi, err = NewProcessedIdentity(identity); err != nil {
		return
	}
	if !pi.VerifyHeader() {
		return errors.New("wrong header")
	}
	if err = pi.VerifySignature(ctx, config.CgrConfig().GeneralCfg().ReplyTimeout); err != nil {
		return
	}
	return pi.VerifyPayload(originatorTn, originatorURI, destinationTn, destinationURI, hdrMaxDur, attest)
}

// V1STIRAuthenticateArgs are the arguments for STIRAuthenticate API
type V1STIRAuthenticateArgs struct {
	Attest             []string // what attest levels are allowed
	DestinationTn      string   // the expected destination telephone number
	DestinationURI     string   // the expected destination URI; if this is populated the DestinationTn is ignored
	Identity           string   // the identity header
	OriginatorTn       string   // the expected originator telephone number
	OriginatorURI      string   // the expected originator URI; if this is populated the OriginatorTn is ignored
	PayloadMaxDuration string   // the duration the payload is valid after it's creation
	APIOpts            map[string]any
}

// V1STIRIdentityArgs are the arguments for STIRIdentity API
type V1STIRIdentityArgs struct {
	Payload        *utils.PASSporTPayload // the STIR payload
	PublicKeyPath  string                 // the path to the public key used in the header
	PrivateKeyPath string                 // the private key path
	OverwriteIAT   bool                   // if true the IAT from payload is overwrited with the present unix timestamp
	APIOpts        map[string]any
}

// getDerivedEvents returns only the *raw event if derivedReply flag is not specified
func getDerivedEvents(events map[string]*utils.CGREvent, derivedReply bool) map[string]*utils.CGREvent {
	if derivedReply {
		return events
	}
	return map[string]*utils.CGREvent{
		utils.MetaRaw: events[utils.MetaRaw],
	}
}

// V1ProcessEventReply is the reply for the ProcessEvent API
type V1ProcessEventReply struct {
	AccountSUsage      map[string]time.Duration                      `json:",omitempty"`
	RateSCost          map[string]float64                            `json:",omitempty"`
	ResourceAllocation map[string]string                             `json:",omitempty"`
	IPsAllocation      map[string]*utils.AllocatedIP                 `json:",omitempty"`
	Attributes         map[string]*attributes.AttrSProcessEventReply `json:",omitempty"`
	RouteProfiles      map[string]routes.SortedRoutesList            `json:",omitempty"`
	ThresholdIDs       map[string][]string                           `json:",omitempty"`
	StatQueueIDs       map[string][]string                           `json:",omitempty"`
	STIRIdentity       map[string]string                             `json:",omitempty"`
}

// AsNavigableMap is part of engine.NavigableMapper interface
func (v1Rply *V1ProcessEventReply) AsNavigableMap() map[string]*utils.DataNode {
	cgrReply := make(map[string]*utils.DataNode)
	if v1Rply.AccountSUsage != nil {
		usage := &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
		for k, v := range v1Rply.AccountSUsage {
			usage.Map[k] = utils.NewLeafNode(v)
		}
		cgrReply[utils.CapMaxUsage] = usage
	}
	if v1Rply.ResourceAllocation != nil {
		res := &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
		for k, v := range v1Rply.ResourceAllocation {
			res.Map[k] = utils.NewLeafNode(v)
		}
		cgrReply[utils.CapResourceAllocation] = res
	}
	if v1Rply.Attributes != nil {
		atts := &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
		for k, att := range v1Rply.Attributes {
			attrs := &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
			for _, altered := range att.AlteredFields {
				for _, fldName := range altered.Fields {
					fldName = strings.TrimPrefix(fldName, utils.MetaReq+utils.NestingSep)
					if att.CGREvent.HasField(fldName) {
						attrs.Map[fldName] = utils.NewLeafNode(att.CGREvent.Event[fldName])
					}
				}
			}
			atts.Map[k] = attrs
		}
		cgrReply[utils.CapAttributes] = atts
	}
	if v1Rply.RouteProfiles != nil {
		routes := &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
		for k, route := range v1Rply.RouteProfiles {
			routes.Map[k] = route.AsNavigableMap()
		}
		cgrReply[utils.CapRouteProfiles] = routes
	}
	if v1Rply.ThresholdIDs != nil {
		th := &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
		for k, thr := range v1Rply.ThresholdIDs {
			thIDs := &utils.DataNode{Type: utils.NMSliceType, Slice: make([]*utils.DataNode, len(thr))}
			for i, v := range thr {
				thIDs.Slice[i] = utils.NewLeafNode(v)
			}
			th.Map[k] = thIDs
		}
		cgrReply[utils.CapThresholds] = th
	}
	if v1Rply.StatQueueIDs != nil {
		st := &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
		for k, sts := range v1Rply.StatQueueIDs {
			stIDs := &utils.DataNode{Type: utils.NMSliceType, Slice: make([]*utils.DataNode, len(sts))}
			for i, v := range sts {
				stIDs.Slice[i] = utils.NewLeafNode(v)
			}
			st.Map[k] = stIDs
		}
		cgrReply[utils.CapStatQueues] = st
	}
	if v1Rply.RateSCost != nil {
		costs := &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
		for k, cost := range v1Rply.RateSCost {
			costs.Map[k] = utils.NewLeafNode(cost)
		}
	}
	if v1Rply.STIRIdentity != nil {
		stir := &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
		for k, v := range v1Rply.STIRIdentity {
			stir.Map[k] = utils.NewLeafNode(v)
		}
		cgrReply[utils.OptsStirIdentity] = stir
	}
	return cgrReply
}

// V1ProcessMessageReply is the reply for the ProcessMessage API
type V1ProcessMessageReply struct {
	MaxUsage           *time.Duration                     `json:",omitempty"`
	ResourceAllocation *string                            `json:",omitempty"`
	Attributes         *attributes.AttrSProcessEventReply `json:",omitempty"`
	RouteProfiles      routes.SortedRoutesList            `json:",omitempty"`
	ThresholdIDs       *[]string                          `json:",omitempty"`
	StatQueueIDs       *[]string                          `json:",omitempty"`

	needsMaxUsage bool // for gob encoding only
}

// SetMaxUsageNeeded used by agent that use the reply as NavigableMapper
// only used for gob encoding
func (v1Rply *V1ProcessMessageReply) SetMaxUsageNeeded(getMaxUsage bool) {
	if v1Rply == nil {
		return
	}
	v1Rply.needsMaxUsage = getMaxUsage
}

// AsNavigableMap is part of engine.NavigableMapper interface
func (v1Rply *V1ProcessMessageReply) AsNavigableMap() map[string]*utils.DataNode {
	cgrReply := make(map[string]*utils.DataNode)
	if v1Rply.MaxUsage != nil {
		cgrReply[utils.CapMaxUsage] = utils.NewLeafNode(*v1Rply.MaxUsage)
	} else if v1Rply.needsMaxUsage {
		cgrReply[utils.CapMaxUsage] = utils.NewLeafNode(0)
	}
	if v1Rply.ResourceAllocation != nil {
		cgrReply[utils.CapResourceAllocation] = utils.NewLeafNode(*v1Rply.ResourceAllocation)
	}
	if v1Rply.Attributes != nil {
		attrs := &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}

		for _, altered := range v1Rply.Attributes.AlteredFields {
			for _, fldName := range altered.Fields {
				fldName = strings.TrimPrefix(fldName, utils.MetaReq+utils.NestingSep)
				if v1Rply.Attributes.CGREvent.HasField(fldName) {
					attrs.Map[fldName] = utils.NewLeafNode(v1Rply.Attributes.CGREvent.Event[fldName])
				}
			}
		}
		cgrReply[utils.CapAttributes] = attrs
	}
	if v1Rply.RouteProfiles != nil {
		cgrReply[utils.CapRouteProfiles] = v1Rply.RouteProfiles.AsNavigableMap()
	}
	if v1Rply.ThresholdIDs != nil {
		thIDs := &utils.DataNode{Type: utils.NMSliceType, Slice: make([]*utils.DataNode, len(*v1Rply.ThresholdIDs))}
		for i, v := range *v1Rply.ThresholdIDs {
			thIDs.Slice[i] = utils.NewLeafNode(v)
		}
		cgrReply[utils.CapThresholds] = thIDs
	}
	if v1Rply.StatQueueIDs != nil {
		stIDs := &utils.DataNode{Type: utils.NMSliceType, Slice: make([]*utils.DataNode, len(*v1Rply.StatQueueIDs))}
		for i, v := range *v1Rply.StatQueueIDs {
			stIDs.Slice[i] = utils.NewLeafNode(v)
		}
		cgrReply[utils.CapStatQueues] = stIDs
	}
	return cgrReply
}

// V1AuthorizeReply are options available in auth reply
type V1AuthorizeReply struct {
	Attributes         *attributes.AttrSProcessEventReply `json:",omitempty"`
	ResourceAllocation *string                            `json:",omitempty"`
	AllocatedIP        *utils.AllocatedIP                 `json:",omitempty"`
	MaxUsage           *utils.Decimal                     `json:",omitempty"`
	RouteProfiles      routes.SortedRoutesList            `json:",omitempty"`
	ThresholdIDs       *[]string                          `json:",omitempty"`
	StatQueueIDs       *[]string                          `json:",omitempty"`

	needsMaxUsage bool // for gob encoding only
}

// SetMaxUsageNeeded used by agent that use the reply as NavigableMapper
// only used for gob encoding
func (r *V1AuthorizeReply) SetMaxUsageNeeded(getMaxUsage bool) {
	if r == nil {
		return
	}
	r.needsMaxUsage = getMaxUsage
}

// AsNavigableMap is part of engine.NavigableMapper interface
func (r *V1AuthorizeReply) AsNavigableMap() map[string]*utils.DataNode {
	cgrReply := make(map[string]*utils.DataNode)
	if r.Attributes != nil {
		attrs := &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
		for _, altered := range r.Attributes.AlteredFields {
			for _, fldName := range altered.Fields {
				fldName = strings.TrimPrefix(fldName, utils.MetaReq+utils.NestingSep)
				if r.Attributes.CGREvent.HasField(fldName) {
					attrs.Map[fldName] = utils.NewLeafNode(r.Attributes.CGREvent.Event[fldName])
				}
			}
		}
		cgrReply[utils.CapAttributes] = attrs
	}
	if r.AllocatedIP != nil {
		cgrReply[utils.AllocatedIPField] = &utils.DataNode{
			Type: utils.NMMapType,
			Map:  r.AllocatedIP.AsNavigableMap(),
		}
	}
	if r.ResourceAllocation != nil {
		cgrReply[utils.CapResourceAllocation] = utils.NewLeafNode(*r.ResourceAllocation)
	}
	if r.MaxUsage != nil {
		cgrReply[utils.CapMaxUsage] = utils.NewLeafNode(r.MaxUsage)
	} else if r.needsMaxUsage {
		cgrReply[utils.CapMaxUsage] = utils.NewLeafNode(0)
	}

	if r.RouteProfiles != nil {
		nm := r.RouteProfiles.AsNavigableMap()
		cgrReply[utils.CapRouteProfiles] = nm
	}
	if r.ThresholdIDs != nil {
		thIDs := &utils.DataNode{Type: utils.NMSliceType, Slice: make([]*utils.DataNode, len(*r.ThresholdIDs))}
		for i, v := range *r.ThresholdIDs {
			thIDs.Slice[i] = utils.NewLeafNode(v)
		}
		cgrReply[utils.CapThresholds] = thIDs
	}
	if r.StatQueueIDs != nil {
		stIDs := &utils.DataNode{Type: utils.NMSliceType, Slice: make([]*utils.DataNode, len(*r.StatQueueIDs))}
		for i, v := range *r.StatQueueIDs {
			stIDs.Slice[i] = utils.NewLeafNode(v)
		}
		cgrReply[utils.CapStatQueues] = stIDs
	}
	return cgrReply
}

// V1AuthorizeReplyWithDigest contains return options for auth with digest
type V1AuthorizeReplyWithDigest struct {
	AttributesDigest   *string
	ResourceAllocation *string
	MaxUsage           int64 // special treat returning time.Duration.Nanoseconds()
	RoutesDigest       *string
	Thresholds         *string
	StatQueues         *string
}

// V1InitSessionReply are options for initialization reply
type V1InitSessionReply struct {
	Attributes         *attributes.AttrSProcessEventReply `json:",omitempty"`
	ResourceAllocation *string                            `json:",omitempty"`
	AllocatedIP        *utils.AllocatedIP                 `json:",omitempty"`
	MaxUsage           *time.Duration                     `json:",omitempty"`
	ThresholdIDs       *[]string                          `json:",omitempty"`
	StatQueueIDs       *[]string                          `json:",omitempty"`

	needsMaxUsage bool // for gob encoding only
}

// SetMaxUsageNeeded used by agent that use the reply as NavigableMapper
// only used for gob encoding
func (r *V1InitSessionReply) SetMaxUsageNeeded(getMaxUsage bool) {
	if r == nil {
		return
	}
	r.needsMaxUsage = getMaxUsage
}

// AsNavigableMap is part of engine.NavigableMapper interface
func (r *V1InitSessionReply) AsNavigableMap() map[string]*utils.DataNode {
	cgrReply := make(map[string]*utils.DataNode)
	if r.Attributes != nil {
		attrs := &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
		for _, altered := range r.Attributes.AlteredFields {
			for _, fldName := range altered.Fields {
				fldName = strings.TrimPrefix(fldName, utils.MetaReq+utils.NestingSep)
				if r.Attributes.CGREvent.HasField(fldName) {
					attrs.Map[fldName] = utils.NewLeafNode(r.Attributes.CGREvent.Event[fldName])
				}
			}
		}
		cgrReply[utils.CapAttributes] = attrs
	}
	if r.AllocatedIP != nil {
		cgrReply[utils.AllocatedIPField] = &utils.DataNode{
			Type: utils.NMMapType,
			Map:  r.AllocatedIP.AsNavigableMap(),
		}
	}
	if r.ResourceAllocation != nil {
		cgrReply[utils.CapResourceAllocation] = utils.NewLeafNode(*r.ResourceAllocation)
	}
	if r.MaxUsage != nil {
		cgrReply[utils.CapMaxUsage] = utils.NewLeafNode(*r.MaxUsage)
	} else if r.needsMaxUsage {
		cgrReply[utils.CapMaxUsage] = utils.NewLeafNode(0)
	}

	if r.ThresholdIDs != nil {
		thIDs := &utils.DataNode{Type: utils.NMSliceType, Slice: make([]*utils.DataNode, len(*r.ThresholdIDs))}
		for i, v := range *r.ThresholdIDs {
			thIDs.Slice[i] = utils.NewLeafNode(v)
		}
		cgrReply[utils.CapThresholds] = thIDs
	}
	if r.StatQueueIDs != nil {
		stIDs := &utils.DataNode{Type: utils.NMSliceType, Slice: make([]*utils.DataNode, len(*r.StatQueueIDs))}
		for i, v := range *r.StatQueueIDs {
			stIDs.Slice[i] = utils.NewLeafNode(v)
		}
		cgrReply[utils.CapStatQueues] = stIDs
	}
	return cgrReply
}

// V1InitReplyWithDigest is the formated reply
type V1InitReplyWithDigest struct {
	AttributesDigest   *string
	ResourceAllocation *string
	MaxUsage           float64
	Thresholds         *string
	StatQueues         *string
}

// V1UpdateSessionReply contains options for session update reply
type V1UpdateSessionReply struct {
	Attributes *attributes.AttrSProcessEventReply `json:",omitempty"`
	MaxUsage   *time.Duration                     `json:",omitempty"`

	needsMaxUsage bool // for gob encoding only
}

// SetMaxUsageNeeded used by agent that use the reply as NavigableMapper
// only used for gob encoding
func (v1Rply *V1UpdateSessionReply) SetMaxUsageNeeded(getMaxUsage bool) {
	if v1Rply == nil {
		return
	}
	v1Rply.needsMaxUsage = getMaxUsage
}

// AsNavigableMap is part of engine.NavigableMapper interface
func (v1Rply *V1UpdateSessionReply) AsNavigableMap() map[string]*utils.DataNode {
	cgrReply := make(map[string]*utils.DataNode)
	if v1Rply.Attributes != nil {
		attrs := &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
		for _, altered := range v1Rply.Attributes.AlteredFields {
			for _, fldName := range altered.Fields {
				fldName = strings.TrimPrefix(fldName, utils.MetaReq+utils.NestingSep)
				if v1Rply.Attributes.CGREvent.HasField(fldName) {
					attrs.Map[fldName] = utils.NewLeafNode(v1Rply.Attributes.CGREvent.Event[fldName])
				}
			}
		}
		cgrReply[utils.CapAttributes] = attrs
	}
	if v1Rply.MaxUsage != nil {
		cgrReply[utils.CapMaxUsage] = utils.NewLeafNode(*v1Rply.MaxUsage)
	} else if v1Rply.needsMaxUsage {
		cgrReply[utils.CapMaxUsage] = utils.NewLeafNode(0)
	}
	return cgrReply
}

// ArgsReplicateSessions used to specify wich Session to replicate over the given connections
type ArgsReplicateSessions struct {
	Passive bool
	ConnIDs []string
	APIOpts map[string]any
	Tenant  string
}

// getMaxUsageFromRuns will return maxUsage with the lowest one from maxAbstractsRuns
func getMaxUsageFromRuns(maxAbstractsRuns map[string]*utils.Decimal) (maxUsage *utils.Decimal) {
	for _, maxAbstr := range maxAbstractsRuns {
		if maxUsage == nil ||
			maxUsage.Compare(maxAbstr) == 1 {
			maxUsage = maxAbstr
		}
	}
	return
}
