/*
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 (
	"sort"
	"strings"
	"sync"
	"time"

	"maps"

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

type RankingProfileWithAPIOpts struct {
	*RankingProfile
	APIOpts map[string]any
}

// RankingProfile represents one profile querying StatS and sorting them.
type RankingProfile struct {
	Tenant            string   // Tenant this profile belongs to
	ID                string   // Profile identification
	Schedule          string   // Cron schedule this profile should run at
	StatIDs           []string // List of stat instances to query
	MetricIDs         []string // Filter out only specific metrics in reply for sorting
	Sorting           string   // Sorting strategy. Possible values: <*asc|*desc>
	SortingParameters []string // Sorting parameters: depending on sorting type, list of metric ids for now with optional true or false in case of reverse logic is desired
	Stored            bool     // Offline storage activation for this profile
	ThresholdIDs      []string // List of threshold IDs to limit this Ranking to. *none to disable threshold processing for it.
}

func (rkp *RankingProfile) TenantID() string {
	return utils.ConcatenatedKey(rkp.Tenant, rkp.ID)
}

// Clone will clone a RankingProfile
func (rkP *RankingProfile) Clone() (cln *RankingProfile) {
	if rkP == nil {
		return nil
	}
	cln = &RankingProfile{
		Tenant:   rkP.Tenant,
		ID:       rkP.ID,
		Schedule: rkP.Schedule,
		Sorting:  rkP.Sorting,
		Stored:   rkP.Stored,
	}
	if rkP.StatIDs != nil {
		cln.StatIDs = make([]string, len(rkP.StatIDs))
		copy(cln.StatIDs, rkP.StatIDs)
	}
	if rkP.MetricIDs != nil {
		cln.MetricIDs = make([]string, len(rkP.MetricIDs))
		copy(cln.MetricIDs, rkP.MetricIDs)
	}
	if rkP.SortingParameters != nil {
		cln.SortingParameters = make([]string, len(rkP.SortingParameters))
		copy(cln.SortingParameters, rkP.SortingParameters)
	}
	if rkP.ThresholdIDs != nil {
		cln.ThresholdIDs = make([]string, len(rkP.ThresholdIDs))
		copy(cln.ThresholdIDs, rkP.ThresholdIDs)
	}
	return
}

// CacheClone returns a clone of RankingProfile used by ltcache CacheCloner
func (rkP *RankingProfile) CacheClone() any {
	return rkP.Clone()
}

// NewRankingFromProfile is a constructor for an empty ranking out of it's profile
func NewRankingFromProfile(rkP *RankingProfile) (rk *Ranking) {
	rk = &Ranking{
		Tenant:  rkP.Tenant,
		ID:      rkP.ID,
		Sorting: rkP.Sorting,
		Metrics: make(map[string]map[string]float64),

		rkPrfl:    rkP,
		metricIDs: utils.NewStringSet(rkP.MetricIDs),
	}
	if rkP.SortingParameters != nil {
		rk.SortingParameters = make([]string, len(rkP.SortingParameters))
		copy(rk.SortingParameters, rkP.SortingParameters)
	}
	return
}

type RankingWithAPIOpts struct {
	*Ranking
	APIOpts map[string]any
}

// Ranking is one unit out of a profile
type Ranking struct {
	rMux sync.RWMutex

	Tenant            string
	ID                string
	LastUpdate        time.Time
	Metrics           map[string]map[string]float64 // map[statID]map[metricID]metricValue
	Sorting           string
	SortingParameters []string

	SortedStatIDs []string

	rkPrfl    *RankingProfile // store here the ranking profile so we can have it at hands further
	metricIDs utils.StringSet // convert the metricIDs here for faster matching

}

// Clone clones *Ranking
func (r *Ranking) Clone() *Ranking {
	if r == nil {
		return nil
	}
	r.rMux.RLock()
	defer r.rMux.RUnlock()
	cln := &Ranking{
		Tenant:     r.Tenant,
		ID:         r.ID,
		LastUpdate: r.LastUpdate,
		Sorting:    r.Sorting,
	}
	if r.SortingParameters != nil {
		cln.SortingParameters = make([]string, len(r.SortingParameters))
		copy(cln.SortingParameters, r.SortingParameters)
	}
	if r.SortedStatIDs != nil {
		cln.SortedStatIDs = make([]string, len(r.SortedStatIDs))
		copy(cln.SortedStatIDs, r.SortedStatIDs)
	}
	if r.Metrics != nil {
		cln.Metrics = make(map[string]map[string]float64)
		for statID, metricMap := range r.Metrics {
			cln.Metrics[statID] = make(map[string]float64)
			maps.Copy(cln.Metrics[statID], metricMap)
		}
	}
	if r.rkPrfl != nil {
		cln.rkPrfl = r.rkPrfl.Clone()
	}
	if r.metricIDs != nil {
		cln.metricIDs = r.metricIDs.Clone()
	}
	return cln
}

// CacheClone returns a clone of Ranking used by ltcache CacheCloner
func (r *Ranking) CacheClone() any {
	return r.Clone()
}

func (r *Ranking) TenantID() string {
	return utils.ConcatenatedKey(r.Tenant, r.ID)
}

// asRankingSummary converts the Ranking instance into a RankingSummary one
func (rk *Ranking) asRankingSummary() (rkSm *RankingSummary) {
	rkSm = &RankingSummary{
		Tenant:     rk.Tenant,
		ID:         rk.ID,
		LastUpdate: rk.LastUpdate,
	}
	rkSm.SortedStatIDs = make([]string, len(rk.SortedStatIDs))
	copy(rkSm.SortedStatIDs, rk.SortedStatIDs)
	return
}

type rankingSorter interface {
	sortStatIDs() []string // sortStatIDs returns the sorted list of statIDs
}

// rankingSortStats will return the list of sorted statIDs out of the sortingData map
func rankingSortStats(sortingType string, sortingParams []string,
	Metrics map[string]map[string]float64) (sortedStatIDs []string, err error) {
	var rnkSrtr rankingSorter
	if rnkSrtr, err = newRankingSorter(sortingType, sortingParams, Metrics); err != nil {
		return
	}
	return rnkSrtr.sortStatIDs(), nil
}

// newRankingSorter is the constructor for various ranking sorters
//
//	returns error if the sortingType is not implemented
func newRankingSorter(sortingType string, sortingParams []string,
	Metrics map[string]map[string]float64) (rkStr rankingSorter, err error) {
	switch sortingType {
	default:
		err = utils.ErrPrefixNotErrNotImplemented(sortingType)
		return
	case utils.MetaDesc:
		return newRankingDescSorter(sortingParams, Metrics), nil
	case utils.MetaAsc:
		return newRankingAscSorter(sortingParams, Metrics), nil
	}
}

// newRankingDescSorter is a constructor for rankingDescSorter
func newRankingDescSorter(sortingParams []string,
	Metrics map[string]map[string]float64) (rkDsrtr *rankingDescSorter) {
	clnSp := make([]string, len(sortingParams))
	sPReversed := make(utils.StringSet)
	for i, sP := range sortingParams { // clean the sortingParams, out of param:false or param:true definitions
		sPSlc := strings.Split(sP, utils.InInFieldSep)
		clnSp[i] = sPSlc[0]
		if len(sPSlc) > 1 && sPSlc[1] == utils.FalseStr {
			sPReversed.Add(sPSlc[0]) // param defined as param:false which should be added to reversing comparison
		}
	}
	rkDsrtr = &rankingDescSorter{
		clnSp,
		sPReversed,
		Metrics,
		make([]string, 0, len(Metrics))}
	for statID := range rkDsrtr.Metrics {
		rkDsrtr.statIDs = append(rkDsrtr.statIDs, statID)
	}
	return
}

// rankingDescSorter will sort data descendent for metrics in sortingParams or random if all equal
type rankingDescSorter struct {
	sMetricIDs []string
	sMetricRev utils.StringSet // list of exceptios for sortingParams, reverting the sorting logic
	Metrics    map[string]map[string]float64

	statIDs []string // list of keys of the Metrics
}

// sortStatIDs implements rankingSorter interface
func (rkDsrtr *rankingDescSorter) sortStatIDs() []string {
	if len(rkDsrtr.statIDs) == 0 {
		return rkDsrtr.statIDs
	}
	sort.Slice(rkDsrtr.statIDs, func(i, j int) bool {
		for _, metricID := range rkDsrtr.sMetricIDs {
			val1, hasMetric1 := rkDsrtr.Metrics[rkDsrtr.statIDs[i]][metricID]
			val2, hasMetric2 := rkDsrtr.Metrics[rkDsrtr.statIDs[j]][metricID]
			if !hasMetric1 && !hasMetric2 {
				continue
			}
			if !hasMetric1 {
				return false
			}
			if !hasMetric2 {
				return true
			}
			//in case we have the same value for the current metricID we skip to the next one
			if val1 == val2 {
				continue
			}
			ret := val1 > val2
			if rkDsrtr.sMetricRev.Has(metricID) {
				ret = !ret
			}
			return ret
		}
		//in case that we have the same value for all params we return randomly
		return utils.BoolGenerator().RandomBool()
	})
	return rkDsrtr.statIDs
}

// newRankingAscSorter is a constructor for rankingAscSorter
func newRankingAscSorter(sortingParams []string,
	Metrics map[string]map[string]float64) (rkASrtr *rankingAscSorter) {
	clnSp := make([]string, len(sortingParams))
	sPReversed := make(utils.StringSet)
	for i, sP := range sortingParams { // clean the sortingParams, out of param:false or param:true definitions
		sPSlc := strings.Split(sP, utils.InInFieldSep)
		clnSp[i] = sPSlc[0]
		if len(sPSlc) > 1 && sPSlc[1] == utils.FalseStr {
			sPReversed.Add(sPSlc[0]) // param defined as param:false which should be added to reversing comparison
		}
	}
	rkASrtr = &rankingAscSorter{
		clnSp,
		sPReversed,
		Metrics,
		make([]string, 0, len(Metrics))}
	for statID := range rkASrtr.Metrics {
		rkASrtr.statIDs = append(rkASrtr.statIDs, statID)
	}
	return
}

// rankingAscSorter will sort data ascendent for metrics in sortingParams or randomly if all equal
type rankingAscSorter struct {
	sMetricIDs []string
	sMetricRev utils.StringSet // list of exceptios for sortingParams, reverting the sorting logic
	Metrics    map[string]map[string]float64

	statIDs []string // list of keys of the Metrics
}

// sortStatIDs implements rankingSorter interface
func (rkASrtr *rankingAscSorter) sortStatIDs() []string {
	if len(rkASrtr.statIDs) == 0 {
		return rkASrtr.statIDs
	}
	sort.Slice(rkASrtr.statIDs, func(i, j int) bool {
		for _, metricID := range rkASrtr.sMetricIDs {
			val1, hasMetric1 := rkASrtr.Metrics[rkASrtr.statIDs[i]][metricID]
			val2, hasMetric2 := rkASrtr.Metrics[rkASrtr.statIDs[j]][metricID]
			if !hasMetric1 && !hasMetric2 {
				continue
			}
			if !hasMetric1 {
				return false
			}
			if !hasMetric2 {
				return true
			}
			//in case we have the same value for the current metricID we skip to the next one
			if val1 == val2 {
				continue
			}
			ret := val2 > val1
			if rkASrtr.sMetricRev.Has(metricID) {
				ret = !ret // reversed logic in case of metric:false in params
			}
			return ret
		}
		//in case that we have the same value for all params we return randomly
		return utils.BoolGenerator().RandomBool()
	})
	return rkASrtr.statIDs
}

// RankingSummary is the event sent to TrendS and EEs
type RankingSummary struct {
	Tenant        string
	ID            string
	LastUpdate    time.Time
	SortedStatIDs []string
}
