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

import (
	"time"

	"github.com/ericlagergren/decimal"

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

// NewRateS instantiates the RateS
func NewRateS(cfg *config.CGRConfig, filterS *engine.FilterS, dm *engine.DataManager) *RateS {
	return &RateS{
		cfg:   cfg,
		fltrS: filterS,
		dm:    dm,
	}
}

// RateS calculates costs for events
type RateS struct {
	cfg   *config.CGRConfig
	fltrS *engine.FilterS
	dm    *engine.DataManager
}

// ListenAndServe keeps the service alive
func (rS *RateS) ListenAndServe(stopChan, cfgRld chan struct{}) {
	for {
		select {
		case <-stopChan:
			return
		case rld := <-cfgRld: // configuration was reloaded
			cfgRld <- rld
		}
	}
}

// matchingRateProfileForEvent returns the matched RateProfile for the given event
func (rS *RateS) matchingRateProfileForEvent(ctx *context.Context, tnt string, rpIDs []string, args *utils.CGREvent,
	ignoreFilters bool, ignoredRPIDs utils.StringSet) (matchedProfile *utils.RateProfile, err error) {
	evNm := utils.MapStorage{
		utils.MetaReq:  args.Event,
		utils.MetaOpts: args.APIOpts,
	}
	if len(rpIDs) == 0 {
		ignoreFilters = false
		var rpIDMap utils.StringSet
		if rpIDMap, err = engine.MatchingItemIDsForEvent(ctx,
			evNm,
			rS.cfg.RateSCfg().StringIndexedFields,
			rS.cfg.RateSCfg().PrefixIndexedFields,
			rS.cfg.RateSCfg().SuffixIndexedFields,
			rS.cfg.RateSCfg().ExistsIndexedFields,
			rS.cfg.RateSCfg().NotExistsIndexedFields,
			rS.dm,
			utils.CacheRateProfilesFilterIndexes,
			tnt,
			rS.cfg.RateSCfg().IndexedSelects,
			rS.cfg.RateSCfg().NestedFields,
		); err != nil {
			return
		}
		rpIDs = rpIDMap.AsSlice()
	}
	var maxWeight float64
	for _, rpID := range rpIDs {
		if ignoredRPIDs.Has(rpID) { // already processed and gave errors or not appropriate for our request
			continue
		}
		var rp *utils.RateProfile
		if rp, err = rS.dm.GetRateProfile(ctx, tnt, rpID,
			true, true, utils.NonTransactional); err != nil {
			if err == utils.ErrNotFound {
				err = nil
				continue
			}
			return
		}
		if !ignoreFilters {
			var pass bool
			if pass, err = rS.fltrS.Pass(ctx, tnt, rp.FilterIDs, evNm); err != nil {
				return
			} else if !pass {
				continue
			}
		}
		weight, err := engine.WeightFromDynamics(ctx, rp.Weights, rS.fltrS, tnt, evNm)
		if err != nil {
			return nil, err
		}
		if matchedProfile == nil || weight > maxWeight {
			matchedProfile = rp
			maxWeight = weight
		}
	}
	if matchedProfile == nil {
		return nil, utils.ErrNotFound
	}

	return matchedProfile, nil
}

// rateProfileCostForEvent computes the rateProfileCost for an event based on a preselected rate profile
func (rS *RateS) rateProfileCostForEvent(ctx *context.Context, rtPfl *utils.RateProfile, args *utils.CGREvent, verbosity int) (rpCost *utils.RateProfileCost, err error) {
	evNm := utils.MapStorage{
		utils.MetaReq:  args.Event,
		utils.MetaOpts: args.APIOpts,
	}
	rtIDs := utils.StringSet{}
	if rS.cfg.RateSCfg().RateIndexedSelects {
		if rtIDs, err = engine.MatchingItemIDsForEvent(ctx,
			evNm,
			rS.cfg.RateSCfg().RateStringIndexedFields,
			rS.cfg.RateSCfg().RatePrefixIndexedFields,
			rS.cfg.RateSCfg().RateSuffixIndexedFields,
			rS.cfg.RateSCfg().RateExistsIndexedFields,
			rS.cfg.RateSCfg().RateNotExistsIndexedFields,
			rS.dm,
			utils.CacheRateFilterIndexes,
			utils.ConcatenatedKey(args.Tenant, rtPfl.ID),
			rS.cfg.RateSCfg().RateIndexedSelects,
			rS.cfg.RateSCfg().RateNestedFields,
		); err != nil {
			return
		}
	} else {
		for id := range rtPfl.Rates {
			rtIDs.Add(id)
		}
	}
	aRates := make([]*utils.Rate, 0, len(rtIDs))
	for rtID := range rtIDs {
		rt := rtPfl.Rates[rtID] // pick the rate directly from map based on matched ID
		var pass bool
		if pass, err = rS.fltrS.Pass(ctx, args.Tenant, rt.FilterIDs, evNm); err != nil {
			return
		} else if !pass {
			continue
		}
		aRates = append(aRates, rt)
	}
	// populate weights to be used on ordering
	wghts := make([]float64, len(aRates))
	for i, aRt := range aRates {
		if wghts[i], err = engine.WeightFromDynamics(ctx, aRt.Weights,
			rS.fltrS, args.Tenant, evNm); err != nil {
			return
		}
	}
	dP := args.AsDataProvider()
	var sTime time.Time
	if sTime, err = engine.GetTimeOpts(ctx, args.Tenant, dP, nil, rS.fltrS, rS.cfg.RateSCfg().Opts.StartTime,
		rS.cfg.GeneralCfg().DefaultTimezone, utils.OptsRatesStartTime, utils.MetaStartTime); err != nil {
		return
	}
	var usage *decimal.Big
	if usage, err = engine.GetDecimalBigOpts(ctx, args.Tenant, dP, nil, rS.fltrS, rS.cfg.RateSCfg().Opts.Usage,
		utils.OptsRatesUsage, utils.MetaUsage); err != nil {
		return
	}
	var ordRts []*orderedRate
	if ordRts, err = orderRatesOnIntervals(aRates, wghts, sTime, usage, true, verbosity); err != nil {
		return
	}
	rpCost = &utils.RateProfileCost{
		ID:    rtPfl.ID,
		Rates: make(map[string]*utils.IntervalRate),
	}
	if rtPfl.MinCost != nil {
		rpCost.MinCost = rtPfl.MinCost
	}
	if rtPfl.MaxCost != nil {
		rpCost.MaxCost = rtPfl.MaxCost
	}
	var ivalStart *decimal.Big
	if ivalStart, err = engine.GetDecimalBigOpts(ctx, args.Tenant, dP, nil, rS.fltrS, rS.cfg.RateSCfg().Opts.IntervalStart,
		utils.OptsRatesIntervalStart); err != nil {
		return
	}
	var costIntervals []*utils.RateSInterval
	if costIntervals, err = computeRateSIntervals(ordRts, ivalStart, usage, rpCost.Rates); err != nil {
		return nil, err
	}
	rpCost.CostIntervals = make([]*utils.RateSIntervalCost, len(costIntervals))
	finalCost := decimal.WithContext(utils.DecimalContext)
	for idx, costInt := range costIntervals {
		finalCost = utils.SumBig(finalCost, costInt.Cost(rpCost.Rates)) // sums the costs for all intervals
		rpCost.CostIntervals[idx] = costInt.AsRatesIntervalsCost()      //this does not contains IncrementStart and IntervalStart so we convert in RatesIntervalCosts
	}
	rpCost.Cost = &utils.Decimal{Big: finalCost}
	return
}
