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

import (
	"fmt"
	"slices"
	"strings"

	"github.com/cgrates/birpc/context"
	"github.com/cgrates/cgrates/attributes"
	"github.com/cgrates/cgrates/config"
	"github.com/cgrates/cgrates/engine"
	"github.com/cgrates/cgrates/utils"
)

// NewAgentRequest returns a new AgentRequest
func NewAgentRequest(req utils.DataProvider,
	vars, cgrRply *utils.DataNode,
	rply *utils.OrderedNavigableMap,
	opts utils.MapStorage,
	tntTpl utils.RSRParsers,
	dfltTenant, timezone string,
	filterS *engine.FilterS,
	extraDP map[string]utils.DataProvider) (ar *AgentRequest) {
	if cgrRply == nil {
		cgrRply = &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
	}
	if vars == nil {
		vars = &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
	}
	if rply == nil {
		rply = utils.NewOrderedNavigableMap()
	}
	if opts == nil {
		opts = make(utils.MapStorage)
	}
	if extraDP == nil {
		extraDP = make(map[string]utils.DataProvider)
	}
	ar = &AgentRequest{
		Request:    req,
		Tenant:     dfltTenant,
		Vars:       vars,
		CGRRequest: utils.NewOrderedNavigableMap(),
		diamreq:    utils.NewOrderedNavigableMap(), // special case when CGRateS is building the request
		radDAReq:   utils.NewOrderedNavigableMap(),
		CGRReply:   cgrRply,
		Reply:      rply,
		Timezone:   timezone,
		filterS:    filterS,
		Opts:       opts,
		Cfg:        config.CgrConfig().GetDataProvider(),
		ExtraDP:    extraDP, // used for extra dataproviders like *hdr and *trl
	}
	if tnt, err := tntTpl.ParseDataProvider(ar); err == nil && tnt != utils.EmptyString {
		ar.Tenant = tnt
	}
	ar.Vars.Set([]string{utils.NodeID}, config.CgrConfig().GeneralCfg().NodeID)
	return
}

// AgentRequest represents data related to one request towards agent
// implements utils.DataProvider so we can pass it to filters
type AgentRequest struct {
	Request    utils.DataProvider         // request
	Vars       *utils.DataNode            // shared data
	CGRRequest *utils.OrderedNavigableMap // Used in reply to access the request that was send
	CGRReply   *utils.DataNode
	Reply      *utils.OrderedNavigableMap
	Tenant     string
	Timezone   string
	filterS    *engine.FilterS
	diamreq    *utils.OrderedNavigableMap // used in case of building requests (ie. DisconnectSession)
	radDAReq   *utils.OrderedNavigableMap // used for building RADIUS server-initiated Disconnect Requests
	tmp        *utils.DataNode            // used in case you want to store temporary items and access them later
	Opts       utils.MapStorage
	Cfg        utils.DataProvider
	ExtraDP    map[string]utils.DataProvider
}

// String implements utils.DataProvider
func (ar *AgentRequest) String() string {
	return utils.ToIJSON(ar)
}

// FieldAsInterface implements utils.DataProvider
func (ar *AgentRequest) FieldAsInterface(fldPath []string) (val any, err error) {
	switch fldPath[0] {
	default:
		dp, has := ar.ExtraDP[fldPath[0]]
		if !has {
			return nil, fmt.Errorf("unsupported field prefix: <%s>", fldPath[0])
		}
		val, err = dp.FieldAsInterface(fldPath[1:])
	case utils.MetaReq:
		if len(fldPath) != 1 {
			val, err = ar.Request.FieldAsInterface(fldPath[1:])
		} else {
			val = ar.Request
		}
	case utils.MetaVars:
		if len(fldPath) != 1 {
			val, err = ar.Vars.FieldAsInterface(fldPath[1:])
		} else {
			val = ar.Vars
		}
	case utils.MetaCgreq:
		if len(fldPath) != 1 {
			val, err = ar.CGRRequest.FieldAsInterface(fldPath[1:])
		} else {
			val = ar.CGRRequest
		}
	case utils.MetaCgrep:
		if len(fldPath) != 1 {
			val, err = ar.CGRReply.FieldAsInterface(fldPath[1:])
		} else {
			val = ar.CGRReply
		}
	case utils.MetaDiamreq:
		if len(fldPath) != 1 {
			val, err = ar.diamreq.FieldAsInterface(fldPath[1:])
		} else {
			val = ar.diamreq
		}
	case utils.MetaRadDAReq:
		if len(fldPath) != 1 {
			val, err = ar.radDAReq.FieldAsInterface(fldPath[1:])
		}
	case utils.MetaRep:
		if len(fldPath) != 1 {
			val, err = ar.Reply.FieldAsInterface(fldPath[1:])
		} else {
			val = ar.Reply
		}
	case utils.MetaTmp:
		if len(fldPath) != 1 {
			val, err = ar.tmp.FieldAsInterface(fldPath[1:])
		} else {
			val = ar.tmp
		}
	case utils.MetaUCH:
		if cacheVal, ok := engine.Cache.Get(utils.CacheUCH, strings.Join(fldPath[1:], utils.NestingSep)); !ok {
			err = utils.ErrNotFound
		} else {
			val = cacheVal
		}
	case utils.MetaOpts:
		if len(fldPath) != 1 {
			val, err = ar.Opts.FieldAsInterface(fldPath[1:])
		} else {
			val = ar.Opts
		}
	case utils.MetaCfg:
		if len(fldPath) != 1 {
			val, err = ar.Cfg.FieldAsInterface(fldPath[1:])
		} else {
			val = ar.Cfg
		}
	case utils.MetaTenant:
		return ar.Tenant, nil
	}
	if err != nil {
		return
	}
	if nmItems, isNMItems := val.([]*utils.DataNode); isNMItems { // special handling of NMItems, take the last value out of it
		el := nmItems[len(nmItems)-1]
		if el.Type == utils.NMDataType {
			val = el.Value.Data
		}
	}
	return
}

// FieldAsString implements utils.DataProvider
func (ar *AgentRequest) FieldAsString(fldPath []string) (val string, err error) {
	var iface any
	if iface, err = ar.FieldAsInterface(fldPath); err != nil {
		return
	}
	return utils.IfaceAsString(iface), nil
}

// SetFields will populate fields of AgentRequest out of templates
func (ar *AgentRequest) SetFields(tplFlds []*config.FCTemplate) (err error) {
	ar.tmp = &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
	for _, tplFld := range tplFlds {
		if pass, err := ar.filterS.Pass(context.TODO(), ar.Tenant,
			tplFld.Filters, ar); err != nil {
			return err
		} else if !pass {
			continue
		}
		switch tplFld.Type {
		case utils.MetaNone:
		case utils.MetaRemove:
			if err = ar.Remove(&utils.FullPath{
				PathSlice: tplFld.GetPathSlice(),
				Path:      tplFld.Path,
			}); err != nil {
				return
			}
		case utils.MetaRemoveAll:
			if err = ar.RemoveAll(tplFld.GetPathSlice()[0]); err != nil {
				return
			}
		default:
			var out any
			out, err = ar.ParseField(tplFld)
			if err != nil {
				if err == utils.ErrNotFound {
					if !tplFld.Mandatory {
						err = nil
						continue
					}
					err = utils.ErrPrefixNotFound(tplFld.Tag)
				}
				return
			}
			var fullPath *utils.FullPath
			if fullPath, err = utils.GetFullFieldPath(tplFld.Path, ar); err != nil {
				return
			} else if fullPath == nil { // no dynamic path
				fullPath = &utils.FullPath{
					PathSlice: slices.Clone(tplFld.GetPathSlice()), // need to clone so me do not modify the template
					Path:      tplFld.Path,
				}
			}
			nMItm := &utils.DataLeaf{Data: out, NewBranch: tplFld.NewBranch, AttributeID: tplFld.AttributeID}
			switch tplFld.Type {
			case utils.MetaComposed:
				err = ar.Compose(fullPath, nMItm)
			case utils.MetaGroup: // in case of *group type simply append to valSet
				err = ar.Append(fullPath, nMItm)
			default:
				err = ar.SetAsSlice(fullPath, nMItm)
			}
			if err != nil {
				return
			}
		}
		if tplFld.Blocker { // useful in case of processing errors first
			break
		}
	}
	return
}

// Set implements utils.NMInterface
func (ar *AgentRequest) SetAsSlice(fullPath *utils.FullPath, nm *utils.DataLeaf) (err error) {
	switch fullPath.PathSlice[0] {
	default:
		return fmt.Errorf("unsupported field prefix: <%s> when set field", fullPath.PathSlice[0])
	case utils.MetaVars:
		_, err = ar.Vars.Set(fullPath.PathSlice[1:], []*utils.DataNode{{Type: utils.NMDataType, Value: nm}})
		return
	case utils.MetaCgreq:
		return ar.CGRRequest.SetAsSlice(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[7:],
		}, []*utils.DataNode{{Type: utils.NMDataType, Value: nm}})
	case utils.MetaCgrep:
		_, err = ar.CGRReply.Set(fullPath.PathSlice[1:], []*utils.DataNode{{Type: utils.NMDataType, Value: nm}})
		return
	case utils.MetaRep:
		return ar.Reply.SetAsSlice(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[5:],
		}, []*utils.DataNode{{Type: utils.NMDataType, Value: nm}})
	case utils.MetaDiamreq:
		return ar.diamreq.SetAsSlice(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[9:],
		}, []*utils.DataNode{{Type: utils.NMDataType, Value: nm}})
	case utils.MetaRadDAReq:
		return ar.radDAReq.SetAsSlice(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[14:],
		}, []*utils.DataNode{{Type: utils.NMDataType, Value: nm}})
	case utils.MetaTmp:
		_, err = ar.tmp.Set(fullPath.PathSlice[1:], []*utils.DataNode{{Type: utils.NMDataType, Value: nm}})
		return
	case utils.MetaOpts:
		return ar.Opts.Set(fullPath.PathSlice[1:], nm.Data)
	case utils.MetaUCH:
		return engine.Cache.Set(context.TODO(), utils.CacheUCH, fullPath.Path[5:], nm.Data, nil, true, utils.NonTransactional)
	}
}

// RemoveAll deletes all fields at given prefix
func (ar *AgentRequest) RemoveAll(prefix string) error {
	switch prefix {
	default:
		return fmt.Errorf("unsupported field prefix: <%s> when set fields", prefix)
	case utils.MetaVars:
		ar.Vars = &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
	case utils.MetaCgreq:
		ar.CGRRequest.RemoveAll()
	case utils.MetaCgrep:
		ar.CGRReply = &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
	case utils.MetaRep:
		ar.Reply.RemoveAll()
	case utils.MetaDiamreq:
		ar.diamreq.RemoveAll()
	case utils.MetaRadDAReq:
		ar.radDAReq.RemoveAll()
	case utils.MetaTmp:
		ar.tmp = &utils.DataNode{Type: utils.NMMapType, Map: make(map[string]*utils.DataNode)}
	case utils.MetaUCH:
		engine.Cache.Clear([]string{utils.CacheUCH})
	case utils.MetaOpts:
		ar.Opts = make(utils.MapStorage)
	}
	return nil
}

// Remove deletes the fields found at path with the given prefix
func (ar *AgentRequest) Remove(fullPath *utils.FullPath) error {
	switch fullPath.PathSlice[0] {
	default:
		return fmt.Errorf("unsupported field prefix: <%s> when set fields", fullPath.PathSlice[0])
	case utils.MetaVars:
		return ar.Vars.Remove(slices.Clone(fullPath.PathSlice[1:]))
	case utils.MetaCgreq:
		return ar.CGRRequest.Remove(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[7:],
		})
	case utils.MetaCgrep:
		return ar.CGRReply.Remove(slices.Clone(fullPath.PathSlice[1:]))
	case utils.MetaRep:
		return ar.Reply.Remove(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[5:],
		})
	case utils.MetaDiamreq:
		return ar.diamreq.Remove(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[9:],
		})
	case utils.MetaRadDAReq:
		return ar.radDAReq.Remove(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[14:],
		})
	case utils.MetaTmp:
		return ar.tmp.Remove(slices.Clone(fullPath.PathSlice[1:]))
	case utils.MetaOpts:
		return ar.Opts.Remove(fullPath.PathSlice[1:])
	case utils.MetaUCH:
		return engine.Cache.Remove(context.TODO(), utils.CacheUCH, fullPath.Path[5:], true, utils.NonTransactional)
	}
}

// ParseField outputs the value based on the template item
func (ar *AgentRequest) ParseField(
	cfgFld *config.FCTemplate) (out any, err error) {
	tmpType := cfgFld.Type
	switch tmpType {
	case utils.MetaFiller:
		cfgFld.Padding = utils.MetaRight
		tmpType = utils.MetaConstant
	case utils.MetaGroup:
		tmpType = utils.MetaVariable
	}
	out, err = attributes.ParseAttribute(ar, tmpType, cfgFld.Path, cfgFld.Value, config.CgrConfig().GeneralCfg().RoundingDecimals, utils.FirstNonEmpty(cfgFld.Timezone, config.CgrConfig().GeneralCfg().DefaultTimezone), cfgFld.Layout)

	if err != nil &&
		!strings.HasPrefix(err.Error(), "Could not find") {
		return
	}
	if utils.StringTmplType.Has(tmpType) { // format the string additionally with fmtFieldWidth
		out, err = utils.FmtFieldWidth(cfgFld.Tag, out.(string), cfgFld.Width,
			cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory)
	}
	return
}

// setCGRReply will set the aReq.cgrReply based on reply coming from upstream or error
// returns error in case of reply not converting to NavigableMap
func (ar *AgentRequest) setCGRReply(rply utils.NavigableMapper, err error) {
	ar.CGRReply.Map = make(map[string]*utils.DataNode)
	var errMsg string
	if err != nil {
		errMsg = err.Error()
	} else if rply != nil {
		ar.CGRReply.Map = rply.AsNavigableMap()
	}
	ar.CGRReply.Map[utils.Error] = utils.NewLeafNode(errMsg)
}

func needsMaxUsage(ralsFlags utils.FlagParams) bool {
	return len(ralsFlags) != 0 &&
		(ralsFlags.Has(utils.MetaAuthorize) ||
			ralsFlags.Has(utils.MetaInitiate) ||
			ralsFlags.Has(utils.MetaUpdate))
}

// Append sets the value at the given path
// this used with full path and the processed path to not calculate them for every set
func (ar *AgentRequest) Append(fullPath *utils.FullPath, val *utils.DataLeaf) (err error) {
	switch fullPath.PathSlice[0] {
	default:
		return fmt.Errorf("unsupported field prefix: <%s> when set field", fullPath.PathSlice[0])
	case utils.MetaVars:
		_, err = ar.Vars.Append(fullPath.PathSlice[1:], val)
		return
	case utils.MetaCgreq:
		return ar.CGRRequest.Append(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[7:],
		}, val)
	case utils.MetaCgrep:
		_, err = ar.CGRReply.Append(fullPath.PathSlice[1:], val)
		return
	case utils.MetaRep:
		return ar.Reply.Append(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[5:],
		}, val)
	case utils.MetaDiamreq:
		return ar.diamreq.Append(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[9:],
		}, val)
	case utils.MetaRadDAReq:
		return ar.radDAReq.Append(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[14:],
		}, val)
	case utils.MetaTmp:
		_, err = ar.tmp.Append(fullPath.PathSlice[1:], val)
		return
	case utils.MetaOpts:
		return ar.Opts.Set(fullPath.PathSlice[1:], val.Data)
	case utils.MetaUCH:
		return engine.Cache.Set(context.TODO(), utils.CacheUCH, fullPath.Path[5:], val.Data, nil, true, utils.NonTransactional)
	}
}

// Set sets the value at the given path
// this used with full path and the processed path to not calculate them for every set
func (ar *AgentRequest) Compose(fullPath *utils.FullPath, val *utils.DataLeaf) (err error) {
	switch fullPath.PathSlice[0] {
	default:
		return fmt.Errorf("unsupported field prefix: <%s> when set field", fullPath.PathSlice[0])
	case utils.MetaVars:
		return ar.Vars.Compose(fullPath.PathSlice[1:], val)
	case utils.MetaCgreq:
		return ar.CGRRequest.Compose(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[7:],
		}, val)
	case utils.MetaCgrep:
		return ar.CGRReply.Compose(fullPath.PathSlice[1:], val)
	case utils.MetaRep:
		return ar.Reply.Compose(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[5:],
		}, val)
	case utils.MetaDiamreq:
		return ar.diamreq.Compose(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[9:],
		}, val)
	case utils.MetaRadDAReq:
		return ar.radDAReq.Compose(&utils.FullPath{
			PathSlice: fullPath.PathSlice[1:],
			Path:      fullPath.Path[14:],
		}, val)
	case utils.MetaTmp:
		return ar.tmp.Compose(fullPath.PathSlice[1:], val)
	case utils.MetaOpts:
		var prv any
		if prv, err = ar.Opts.FieldAsInterface(fullPath.PathSlice[1:]); err != nil {
			if err != utils.ErrNotFound {
				return
			}
			prv = val.Data
		} else {
			prv = utils.IfaceAsString(prv) + utils.IfaceAsString(val.Data)
		}
		return ar.Opts.Set(fullPath.PathSlice[1:], prv)

	case utils.MetaUCH:
		path := fullPath.Path[5:]
		var prv any
		if prvI, ok := engine.Cache.Get(utils.CacheUCH, path); !ok {
			prv = val.Data
		} else {
			prv = utils.IfaceAsString(prvI) + utils.IfaceAsString(val.Data)
		}
		return engine.Cache.Set(context.TODO(), utils.CacheUCH, path, prv, nil, true, utils.NonTransactional)
	}
}
