/*
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 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 PURPOev.  See the
GNU General Public License for more details.

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

package config

import (
	"encoding/xml"
	"errors"
	"fmt"
	"net"
	"reflect"
	"strconv"
	"strings"
	"time"

	"github.com/cgrates/cgrates/utils"
)

// CGRReplier is the interface supported by replies convertible to CGRReply
type NavigableMapper interface {
	AsNavigableMap([]*FCTemplate) (*NavigableMap, error)
}

// NewNavigableMap constructs a NavigableMap
func NewNavigableMap(data map[string]interface{}) *NavigableMap {
	if data == nil {
		data = make(map[string]interface{})
	}
	return &NavigableMap{data: data}
}

// NMItem is an item in the NavigableMap
type NMItem struct {
	Path   []string    // path in map
	Data   interface{} // value of the element
	Config *FCTemplate // so we can store additional configuration
}

// NavigableMap is a map who's values can be navigated via path
// data can be retrieved as ordered
// NavigableMap is not thread safe due to performance demands, could come back if needed
type NavigableMap struct {
	data  map[string]interface{} // layered map
	order [][]string             // order of field paths
}

// Set will set items into NavigableMap populating also order
// apnd parameter allows appending the data if both sides are []*NMItem
func (nM *NavigableMap) Set(path []string, data interface{}, apnd, ordered bool) {
	if ordered {
		nM.order = append(nM.order, path)
	}
	mp := nM.data
	for i, spath := range path {
		if i == len(path)-1 { // last path
			oData, has := mp[spath]
			if !has || !apnd { // no need to append
				mp[spath] = data
				return
			}
			dataItms, isNMItems := data.([]*NMItem)
			if !isNMItems { // new data is not items
				mp[spath] = data
				return
			}
			oItms, isNMItems := oData.([]*NMItem)
			if !isNMItems { // previous data is not items, simply overwrite
				mp[spath] = data
				return
			}
			mp[spath] = append(oItms, dataItms...)
			return
		}
		if _, has := mp[spath]; !has {
			mp[spath] = make(map[string]interface{})
		}
		mp = mp[spath].(map[string]interface{}) // so we can check further down
	}
}

// FieldAsInterface returns the field value as interface{} for the path specified
// implements DataProvider
// supports spath with selective elements in case of []*NMItem
func (nM *NavigableMap) FieldAsInterface(fldPath []string) (fldVal interface{}, err error) {
	lenPath := len(fldPath)
	if lenPath == 0 {
		return nil, errors.New("empty field path")
	}
	lastMp := nM.data // last map when layered
	for i, spath := range fldPath {
		if i == lenPath-1 { // lastElement
			return nM.getLastItem(lastMp, spath)
		}
		if lastMp, err = nM.getNextMap(lastMp, spath); err != nil {
			return
		}
	}
	err = errors.New("end of function")
	return
}

// getLastItem returns the item from the map
// checking if it needs to return the item or an element of him if the item is a slice
func (nM *NavigableMap) getLastItem(mp map[string]interface{}, spath string) (val interface{}, err error) {
	var idx *int
	spath, idx = nM.getIndex(spath)
	var has bool
	val, has = mp[spath]
	if !has {
		return nil, utils.ErrNotFound
	}
	if idx == nil {
		return val, nil
	}
	switch vt := val.(type) {
	case []string:
		if *idx > len(vt) {
			return nil, utils.ErrNotFound
			// return nil, fmt.Errorf("selector index %d out of range", *idx)
		}
		return vt[*idx], nil
	case []*NMItem:
		if *idx > len(vt) {
			return nil, utils.ErrNotFound
			// return nil, fmt.Errorf("selector index %d out of range", *idx)
		}
		return vt[*idx].Data, nil
	default:
	}
	// only if all above fails use reflect:
	vr := reflect.ValueOf(val)
	if vr.Kind() == reflect.Ptr {
		vr = vr.Elem()
	}
	if vr.Kind() != reflect.Slice && vr.Kind() != reflect.Array {
		return nil, utils.ErrNotFound
		// return nil, fmt.Errorf("selector index used on non slice type(%T)", val)
	}
	if *idx > vr.Len() {
		return nil, utils.ErrNotFound
		// return nil, fmt.Errorf("selector index %d out of range", *idx)
	}
	return vr.Index(*idx).Interface(), nil
}

// getNextMap returns the next map from the given map
// used only for path parsing
func (nM *NavigableMap) getNextMap(mp map[string]interface{}, spath string) (map[string]interface{}, error) {
	var idx *int
	spath, idx = nM.getIndex(spath)
	mi, has := mp[spath]
	if !has {
		return nil, utils.ErrNotFound
	}
	if idx == nil {
		switch mv := mi.(type) {
		case map[string]interface{}:
			return mv, nil
		case *map[string]interface{}:
			return *mv, nil
		case NavigableMap:
			return mv.data, nil
		case *NavigableMap:
			return mv.data, nil
		default:
		}
	} else {
		switch mv := mi.(type) {
		case []interface{}:
			// in case we create the map using json and we marshall the value into a map[string]interface{}
			// we can have slice of interfaces that is masking a slice of map[string]interface{}
			// this is for CostDetails BalanceSummaries
			if *idx < len(mv) {
				mm := mv[*idx]
				switch mmv := mm.(type) {
				case map[string]interface{}:
					return mmv, nil
				case *map[string]interface{}:
					return *mmv, nil
				case NavigableMap:
					return mmv.data, nil
				case *NavigableMap:
					return mmv.data, nil
				default:
				}
			}
		case []map[string]interface{}:
			if *idx < len(mv) {
				return mv[*idx], nil
			}
		case []NavigableMap:
			if *idx < len(mv) {
				return mv[*idx].data, nil
			}
		case []*NavigableMap:
			if *idx < len(mv) {
				return mv[*idx].data, nil
			}
		default:
		}
		return nil, utils.ErrNotFound // xml compatible
	}
	return nil, fmt.Errorf("cannot cast field: <%+v> type: %T with path: <%s> to map[string]interface{}",
		mi, mi, spath)
}

// getIndex returns the path and index if index present
// path[index]=>path,index
// path=>path,nil
func (nM *NavigableMap) getIndex(spath string) (opath string, idx *int) {
	idxStart := strings.Index(spath, utils.IdxStart)
	if idxStart == -1 || !strings.HasSuffix(spath, utils.IdxEnd) {
		return spath, nil
	}
	slctr := spath[idxStart+1 : len(spath)-1]
	opath = spath[:idxStart]
	if strings.HasPrefix(slctr, utils.DynamicDataPrefix) {
		return
	}
	idxVal, err := strconv.Atoi(slctr)
	if err != nil {
		return spath, nil
	}
	return opath, &idxVal
}

// FieldAsString returns the field value as string for the path specified
// implements DataProvider
func (nM *NavigableMap) FieldAsString(fldPath []string) (fldVal string, err error) {
	var valIface interface{}
	valIface, err = nM.FieldAsInterface(fldPath)
	if err != nil {
		return
	}
	return utils.IfaceAsString(valIface), nil
}

// String is part of engine.DataProvider interface
func (nM *NavigableMap) String() string {
	return utils.ToJSON(nM.data)
}

// RemoteHost is part of engine.DataProvider interface
func (nM *NavigableMap) RemoteHost() net.Addr {
	return utils.LocalAddr()
}

// indexMapElements will recursively go through map and index the element paths into elmns
func indexMapElements(mp map[string]interface{}, path []string, vals *[]interface{}) {
	for k, v := range mp {
		vPath := append(path, k)
		if mpIface, isMap := v.(map[string]interface{}); isMap {
			indexMapElements(mpIface, vPath, vals)
			continue
		}
		valsOut := append(*vals, v)
		*vals = valsOut
	}
}

// Values returns the values in map, ordered by order information
func (nM *NavigableMap) Values() (vals []interface{}) {
	if len(nM.data) == 0 {
		return
	}
	if len(nM.order) == 0 {
		indexMapElements(nM.data, []string{}, &vals)
		return
	}
	vals = make([]interface{}, len(nM.order))
	for i, path := range nM.order {
		val, _ := nM.FieldAsInterface(path)
		vals[i] = val
	}
	return
}

// AsNavigableMap implements both NavigableMapper as well as DataProvider interfaces
func (nM *NavigableMap) AsNavigableMap(
	tpl []*FCTemplate) (oNM *NavigableMap, err error) {
	return nil, utils.ErrNotImplemented
}

// Merge will update nM with values from a second one
func (nM *NavigableMap) Merge(nM2 *NavigableMap) {
	if nM2 == nil {
		return
	}
	if len(nM2.order) == 0 {
		indexMapPaths(nM2.data, nil, &nM.order)
	}
	pathIdx := make(map[string]int) // will hold references for last index exported in case of []*NMItem
	for _, path := range nM2.order {
		val, _ := nM2.FieldAsInterface(path)
		if valItms, isItms := val.([]*NMItem); isItms {
			pathStr := strings.Join(path, utils.NestingSep)
			pathIdx[pathStr] += 1
			if pathIdx[pathStr] > len(valItms) {
				val = valItms[len(valItms)-1:] // slice with only last element in, so we can set it unlimited
			} else {
				val = []*NMItem{valItms[pathIdx[pathStr]-1]} // set only one item per path
			}
		}
		nM.Set(path, val, true,
			(len(nM.order) != 0 || len(nM.data) == 0))
	}
	return
}

// indexMapPaths parses map returning the parsed branchPath, useful when not having order for NavigableMap
func indexMapPaths(mp map[string]interface{}, branchPath []string, parsedPaths *[][]string) {
	for k, v := range mp {
		if mpIface, isMap := v.(map[string]interface{}); isMap {
			indexMapPaths(mpIface, append(branchPath, k), parsedPaths)
			continue
		}
		tmpPaths := append(*parsedPaths, append(branchPath, k))
		*parsedPaths = tmpPaths
	}
}

// AsCGREvent builds a CGREvent considering Time as time.Now()
// and Event as linear map[string]interface{} with joined paths
// treats particular case when the value of map is []*NMItem - used in agents/AgentRequest
func (nM *NavigableMap) AsCGREvent(tnt string, pathSep string) (cgrEv *utils.CGREvent) {
	if nM == nil || len(nM.data) == 0 {
		return
	}
	cgrEv = &utils.CGREvent{
		Tenant: tnt,
		ID:     utils.UUIDSha1Prefix(),
		Time:   utils.TimePointer(time.Now()),
		Event:  make(map[string]interface{})}
	if len(nM.order) == 0 {
		indexMapPaths(nM.data, nil, &nM.order)
	}
	for _, branchPath := range nM.order {
		val, _ := nM.FieldAsInterface(branchPath)
		if nmItms, isNMItems := val.([]*NMItem); isNMItems { // special case when we have added multiple items inside a key, used in agents
			for _, nmItm := range nmItms {
				if nmItm.Config == nil ||
					nmItm.Config.AttributeID == "" {
					val = nmItm.Data // first item which is not an attribute will become the value
					break
				}
			}
		}
		cgrEv.Event[strings.Join(branchPath, pathSep)] = val
	}
	return
}

// XMLElement is specially crafted to be automatically marshalled by encoding/xml
type XMLElement struct {
	XMLName    xml.Name
	Value      string        `xml:",chardata"`
	Attributes []*xml.Attr   `xml:",attr"`
	Elements   []*XMLElement `xml:"omitempty"`
}

// AsXMLElements returns the values as []*XMLElement which can be later marshaled
// considers each value returned by .Values() in the form of []*NMItem, otherwise errors
func (nM *NavigableMap) AsXMLElements() (ents []*XMLElement, err error) {
	pathIdx := make(map[string]*XMLElement) // Keep the index of elements based on path
	for _, val := range nM.Values() {
		nmItms, isNMItems := val.([]*NMItem)
		if !isNMItems {
			return nil, fmt.Errorf("value: %+v is not []*NMItem", val)
		}
		for _, nmItm := range nmItms {
			if nmItm.Config != nil && nmItm.Config.NewBranch {
				pathIdx = make(map[string]*XMLElement) // reset cache so we can start having other elements with same path
			}
			val := utils.IfaceAsString(nmItm.Data)
			var pathCached bool
			for i := len(nmItm.Path); i > 0; i-- {
				var cachedElm *XMLElement
				if cachedElm, pathCached = pathIdx[strings.Join(nmItm.Path[:i], "")]; !pathCached {
					continue
				}
				if i == len(nmItm.Path) { // lastElmnt, overwrite value or add attribute
					if nmItm.Config != nil &&
						nmItm.Config.AttributeID != "" {
						cachedElm.Attributes = append(cachedElm.Attributes,
							&xml.Attr{xml.Name{Local: nmItm.Config.AttributeID}, val})
					} else {
						cachedElm.Value = val
					}
					break
				}
				// create elements in reverse order so we can append already created
				var newElm *XMLElement
				for j := len(nmItm.Path); j > i; j-- {
					elm := &XMLElement{XMLName: xml.Name{Local: nmItm.Path[j-1]}}
					pathIdx[strings.Join(nmItm.Path[:j], "")] = elm
					if newElm == nil {
						if nmItm.Config != nil &&
							nmItm.Config.AttributeID != "" {
							elm.Attributes = append(elm.Attributes,
								&xml.Attr{xml.Name{Local: nmItm.Config.AttributeID}, val})
						} else {
							elm.Value = val
						}
						newElm = elm // last element
					} else {
						elm.Elements = append(elm.Elements, newElm)
						newElm = elm
					}
				}
				cachedElm.Elements = append(cachedElm.Elements, newElm)
			}
			if !pathCached { // not an update but new element to be created
				var newElm *XMLElement
				for i := len(nmItm.Path); i > 0; i-- {
					elm := &XMLElement{XMLName: xml.Name{Local: nmItm.Path[i-1]}}
					pathIdx[strings.Join(nmItm.Path[:i], "")] = elm
					if newElm == nil { // last element, create data inside
						if nmItm.Config != nil &&
							nmItm.Config.AttributeID != "" {
							elm.Attributes = append(elm.Attributes,
								&xml.Attr{xml.Name{Local: nmItm.Config.AttributeID}, val})
						} else {
							elm.Value = val
						}
						newElm = elm // last element
					} else {
						elm.Elements = append(elm.Elements, newElm)
						newElm = elm
					}
				}
				ents = append(ents, newElm)
			}
		}
	}
	return
}
