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

import (
	"fmt"
	"strings"

	"github.com/cgrates/birpc/context"

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

// NewLoadDistributionSorter .
func NewLoadDistributionSorter(cfg *config.CGRConfig, connMgr *engine.ConnManager) *LoadDistributionSorter {
	return &LoadDistributionSorter{cfg: cfg, connMgr: connMgr}
}

// LoadDistributionSorter orders suppliers based on their Resource Usage
type LoadDistributionSorter struct {
	cfg     *config.CGRConfig
	connMgr *engine.ConnManager
}

// SortRoutes .
func (ws *LoadDistributionSorter) SortRoutes(ctx *context.Context, prflID string,
	routes map[string]*RouteWithWeight, ev *utils.CGREvent, extraOpts *optsGetRoutes) (sortedRoutes *SortedRoutes, err error) {
	if len(ws.cfg.RouteSCfg().StatSConns) == 0 {
		return nil, utils.NewErrMandatoryIeMissing("connIDs")
	}
	sortedRoutes = &SortedRoutes{
		ProfileID: prflID,
		Sorting:   utils.MetaLoad,
		Routes:    make([]*SortedRoute, 0, len(routes)),
	}
	for _, route := range routes {
		// we should have at least 1 statID defined for counting CDR (a.k.a *sum:1)
		if len(route.StatIDs) == 0 {
			utils.Logger.Warning(
				fmt.Sprintf("<%s> supplier: <%s> - empty StatIDs",
					utils.RouteS, route.ID))
			return nil, utils.NewErrMandatoryIeMissing("StatIDs")
		}
		srtRoute := &SortedRoute{
			RouteID: route.ID,
			SortingData: map[string]any{
				utils.Weight: route.Weight,
			},
			sortingDataDecimal: map[string]*utils.Decimal{
				utils.Weight: utils.NewDecimalFromFloat64(route.Weight),
			},
			RouteParameters: route.RouteParameters,
		}
		if route.blocker {
			srtRoute.SortingData[utils.Blocker] = true
		}
		var metricSum *utils.Decimal
		if metricSum, err = populateStatsForLoadRoute(ctx, ws.cfg, ws.connMgr, route.StatIDs, ev.Tenant); err != nil { //create metric map for route
			if extraOpts.ignoreErrors {
				utils.Logger.Warning(
					fmt.Sprintf("<%s> ignoring route with ID: %s, err: %s",
						utils.RouteS, route.ID, err.Error()))
				err = nil
				continue
			}
			return
		}
		srtRoute.SortingData[utils.Load] = metricSum
		srtRoute.sortingDataDecimal[utils.Load] = metricSum
		var pass bool
		if pass, err = routeLazyPass(ctx, route.lazyCheckRules, ev, srtRoute.SortingData,
			ws.cfg.FilterSCfg().ResourceSConns,
			ws.cfg.FilterSCfg().StatSConns,
			ws.cfg.FilterSCfg().AccountSConns,
			ws.cfg.FilterSCfg().TrendSConns, ws.cfg.FilterSCfg().RankingSConns); err != nil {
			return
		} else if pass {
			// Add the ratio in SortingData so we can used it later in SortLoadDistribution
			floatRatio, err := utils.IfaceAsBig(route.Ratio())
			if err != nil {
				utils.Logger.Warning(
					fmt.Sprintf("<%s> cannot convert ratio <%+v> to float64 supplier: <%s>",
						utils.RouteS, route.Ratio(), route.ID))
			}
			srtRoute.SortingData[utils.Ratio] = floatRatio
			srtRoute.sortingDataDecimal[utils.Ratio] = &utils.Decimal{Big: floatRatio}
			sortedRoutes.Routes = append(sortedRoutes.Routes, srtRoute)
		}
	}
	sortedRoutes.SortLoadDistribution()
	return
}

// populateStatsForLoadRoute will query a list of statIDs and return the sum of metrics
// first metric found is always returned
func populateStatsForLoadRoute(ctx *context.Context, cfg *config.CGRConfig,
	connMgr *engine.ConnManager, statIDs []string, tenant string) (result *utils.Decimal, err error) {
	result = utils.NewDecimalFromFloat64(0)
	for _, statID := range statIDs {
		// check if we get an ID in the following form (StatID:MetricID)
		statWithMetric := strings.Split(statID, utils.InInFieldSep)
		var metrics map[string]*utils.Decimal
		if err = connMgr.Call(ctx,
			cfg.RouteSCfg().StatSConns,
			utils.StatSv1GetQueueDecimalMetrics,
			&utils.TenantIDWithAPIOpts{
				TenantID: &utils.TenantID{
					Tenant: tenant, ID: statWithMetric[0]}},
			&metrics); err != nil &&
			err.Error() != utils.ErrNotFound.Error() {
			utils.Logger.Warning(
				fmt.Sprintf("<%s> error: %s getting statMetrics for stat : %s",
					utils.RouteS, err.Error(), statWithMetric[0]))
			continue
		}
		if len(statWithMetric) == 2 { // in case we have MetricID defined with StatID we consider only that metric
			// check if statQueue have metric defined
			metricVal, has := metrics[statWithMetric[1]]
			if !has {
				return nil, fmt.Errorf("<%s> error: %s metric %s for statID: %s",
					utils.RouteS, utils.ErrNotFound, statWithMetric[1], statWithMetric[0])
			}
			result = utils.SumDecimal(result, metricVal)
		} else { // otherwise we consider all metrics
			for _, val := range metrics {
				//add value of metric in a slice in case that we get the same metric from different stat
				result = utils.SumDecimal(result, val)
			}
		}
	}
	return
}
