package dbManager

import (
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"reflect"
	"strconv"
	"strings"
	"time"

	"github.com/go-openapi/strfmt"
	"github.com/prometheus/client_golang/prometheus"
	datamodels "gitlab.com/cyclops-utilities/datamodels"
	"github.com/Cyclops-Labs/cyclops-4-hpc.git/services/cdr/models"
	"github.com/Cyclops-Labs/cyclops-4-hpc.git/services/cdr/server/cacheManager"
	cusModels "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/customerdb/models"
	pmModels "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/plan-manager/models"
	udrModels "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/udr/models"
	l "gitlab.com/cyclops-utilities/logging"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

const (
	scaler           = 1e9
	statusDuplicated = iota
	statusFail
	statusMissing
	statusOK
)

var (
	totalTime  float64
	totalCount int64
)

// DbParameter is the struct defined to group and contain all the methods
// that interact with the database.
// On it there is the following parameters:
// - Cache: CacheManager pointer for the cache mechanism.
// - connStr: strings with the connection information to the database.
// - Db: a gorm.DB pointer to the db to invoke all the db methods.
type DbParameter struct {
	Cache   *cacheManager.CacheManager
	connStr string
	Db      *gorm.DB
	Metrics map[string]*prometheus.GaugeVec
	Pipe    chan interface{}
}

// New is the function to create the struct DbParameter.
// Parameters:
// - dbConn: strings with the connection information to the database.
// - tables: array of interfaces that will contains the models to migrate
// to the database on initialization.
// Returns:
// - DbParameter: struct to interact with dbManager functionalities.
func New(dbConn string, tables ...interface{}) *DbParameter {

	l.Trace.Printf("[DB] Gerenating new DBParameter.\n")

	var (
		dp  DbParameter
		err error
	)

	dp.connStr = dbConn

	dp.Db, err = gorm.Open(postgres.Open(dbConn), &gorm.Config{})

	if err != nil {

		l.Error.Printf("[DB] Error opening connection. Error: %v\n", err)

	}

	l.Trace.Printf("[DB] Migrating tables.\n")

	//Database migration, it handles everything
	dp.Db.AutoMigrate(tables...)

	//l.Trace.Printf("[DB] Generating hypertables.\n")

	// Hypertables creation for timescaledb in case of needed
	//dp.Db.Exec("SELECT create_hypertable('" + dp.Db.NewScope(&models.TABLE).TableName() + "', 'TIMESCALE-ROW-INDEX');")

	return &dp

}

// AddRecord job is to save in the system the provided CDR report to be easy to
// retrieve later with the usage endpoints.
// Parameters:
// - cdr: the CDR report model to be added to the system.
// Returns:
// - e: error in case of duplication or failure in the task.
func (d *DbParameter) AddRecord(cdr models.CReport) (e error) {

	l.Trace.Printf("[DB] Attempting to add a new cost report to the system.\n")

	if r := d.Db.Where(&cdr).First(&models.CReport{}).Error; errors.Is(r, gorm.ErrRecordNotFound) {

		e = d.Db.Create(&cdr).Error

		if e != nil {

			l.Trace.Printf("[DB] Something went wrong when adding a new usage report to the system. Error: %v\n", e)

		} else {

			d.Metrics["count"].With(prometheus.Labels{"type": "CDR Reports added"}).Inc()

			l.Trace.Printf("[DB] New cost report added to the system.\n")

		}

	} else {

		l.Warning.Printf("[DB] Cost report already in the system, skipping creation...\n")

	}

	l.Trace.Printf("[DB] Attempting to add CDR Records from reported account [ %v ] to the system.\n", cdr.AccountID)

	for i := range cdr.Usage {

		var cdrrecord, cdrUpdate models.CDRRecord

		cdrrecord.AccountID = cdr.AccountID
		cdrrecord.Metadata = cdr.Usage[i].Metadata
		cdrrecord.ResourceID = cdr.Usage[i].ResourceID
		cdrrecord.ResourceName = cdr.Usage[i].ResourceName
		cdrrecord.ResourceType = cdr.Usage[i].ResourceType
		cdrrecord.TimeFrom = cdr.TimeFrom
		cdrrecord.TimeTo = cdr.TimeTo
		cdrrecord.Unit = cdr.Usage[i].Unit

		if r := d.Db.Where(&cdrrecord).First(&cdrUpdate).Error; errors.Is(r, gorm.ErrRecordNotFound) {

			cdrrecord.Cost = cdr.Usage[i].Cost
			cdrrecord.UsageBreakup = cdr.Usage[i].UsageBreakup

			if e = d.Db.Create(&cdrrecord).Error; e != nil {

				l.Warning.Printf("[DB] Something went wrong when adding a new CDR Record to the system. Error: %v\n", e)

			} else {

				d.Metrics["count"].With(prometheus.Labels{"type": "CDR Records added"}).Inc()

				l.Trace.Printf("[DB] New CDR Record added in the system.\n")

			}

		} else {

			l.Trace.Printf("[DB] CDR record already in the system, updating...\n")

			cdrrecord.Cost = cdr.Usage[i].Cost
			cdrrecord.UsageBreakup = cdr.Usage[i].UsageBreakup

			if e = d.Db.Model(&cdrUpdate).Updates(&cdrrecord).Error; e == nil {

				d.Metrics["count"].With(prometheus.Labels{"type": "CDR Records updated"}).Inc()

				l.Trace.Printf("[DB] CDR Record updated in the system.\n")

			} else {

				l.Trace.Printf("[DB] Something went wrong when updating the existing CDR Record to the system. Error: %v\n", e)

			}

		}

	}

	return

}

// GetReport job is to retrieve the compacted usage records from the system for
// the provided account in the requested time-window with the posibility of
// filter by metric.
// Parameters:
// - id: string containing the id of the account requested.
// - metric: a string with the metric to filter the records.
// - from: a datatime reference for the initial border of the time-window.
// - to: a datatime reference for the final border of the time-window.
// Returns:
// - a slice of references with the usage contained in the system within the
// requested time-window.
func (d *DbParameter) GetReport(id string, metric string, from strfmt.DateTime, to strfmt.DateTime) []*models.CDRReport {

	l.Trace.Printf("[DB] Attempting to retrieve the compacted usage records.\n")

	var r []*models.CDRReport
	var u models.CDRRecord

	window := d.getWindow(from, to)

	if id != "" {

		u.AccountID = id

	}

	if metric != "" {

		u.ResourceType = metric

	}

	if e := d.Db.Where(window).Where(&u).Find(&models.CDRRecord{}).Scan(&r).Error; e != nil {

		l.Warning.Printf("[DB] Something went wrong while retrieving the compacted usage records from the system. Error: %v\n", e)

	} else {

		l.Debug.Printf("[DB] [ %v ] compacted usage records retrieved from the system.\n", len(r))

	}

	return r

}

// GetUsage job is to retrieve the usage report of the system or the specified
// account within the requested time-window with the posibility to filter by
// metric.
// Parameters:
// - id: string containing the id of the account requested.
// - metric: a string with the metric to filter the records.
// - from: a datatime reference for the initial border of the time-window.
// - to: a datatime reference for the final border of the time-window.
// Returns:
// - a slice of references with the usage report contained in the system within
// the requested time-window.
// - error in case of duplication or failure in the task.
func (d *DbParameter) GetUsage(id, metric string, from, to strfmt.DateTime) ([]*models.CReport, error) {

	l.Trace.Printf("[DB] Attempting to retrieve the usage report from the system.\n")

	var r []*models.CReport
	var r0 models.CReport
	var e error

	window := d.getWindow(from, to)

	if id != "" {

		r0.AccountID = id

	}

	e = d.Db.Where(window).Where(&r0).Find(&r).Error

	if e == nil {

		for idx := range r {

			r[idx].Usage = d.GetReport(r[idx].AccountID, metric, r[idx].TimeFrom, r[idx].TimeTo)

		}

		l.Debug.Printf("[DB] [ %v ] Usage reports retrieved from the system.\n", len(r))

	} else {

		l.Warning.Printf("[DB] Something went wrong while retrieving the usage reports from the system. Error: %v\n", e)

	}

	return r, e

}

// GetUsages job is to retrieve the usage report of the system or the specified
// account accounts within the requested time-window with the posibility to
// filter by metric.
// Parameters:
// - ids: string containing the list of ids of the accounts requested.
// - metric: a string with the metric to filter the records.
// - from: a datatime reference for the initial border of the time-window.
// - to: a datatime reference for the final border of the time-window.
// Returns:
// - a slice of references with the usage report contained in the system within
// the requested time-window.
// - error in case of duplication or failure in the task.
func (d *DbParameter) GetUsages(ids, metric string, from, to strfmt.DateTime) ([]*models.CReport, error) {

	l.Trace.Printf("[DB] Attempting to retrieve the usage report from the system.\n")

	var total, r []*models.CReport
	var r0 models.CReport
	var e error

	window := d.getWindow(from, to)

	list := strings.Split(ids, ",")

	for _, acc := range list {

		r0.AccountID = acc

		if e = d.Db.Where(window).Where(&r0).Find(&r).Error; e == nil {

			for idx := range r {

				r[idx].Usage = d.GetReport(r[idx].AccountID, metric, r[idx].TimeFrom, r[idx].TimeTo)

			}

			l.Debug.Printf("[DB] [ %v ] Usage reports retrieved from the system.\n", len(r))

		} else {

			l.Warning.Printf("[DB] Something went wrong while retrieving the usage reports from the system. Error: %v\n", e)

		}

		total = append(total, r...)

	}

	return total, e

}

// GetUsageSummary job is to retrieve the usage report of the system of the specified
// account within the requested time-window and process it into an easy to use
// estructure for the UI.
// Parameters:
// - id: string containing the id of the account requested.
// - from: a datatime reference for the initial border of the time-window.
// - to: a datatime reference for the final border of the time-window.
// Returns:
// - a references with the usage report contained in the system within
// the requested time-window.
// - error in case of duplication or failure in the task.
func (d *DbParameter) GetUsageSummary(id string, from, to strfmt.DateTime) (*models.UISummary, error) {

	l.Trace.Printf("[DB] Attempting to retrieve the usage report from the system.\n")

	var usages []*models.CDRReport
	var report models.UISummary

	interval := float64(28800) //UDR are created every 8h and the usage is provided in GB*s...
	rounder := float64(((time.Time)(to)).Sub((time.Time)(from)).Seconds()) / interval

	report.AccountID = id
	report.TimeFrom = from
	report.TimeTo = to

	// Get the reseller data
	r, err := d.Cache.Get(id, "reseller", "")

	if err != nil {

		l.Warning.Printf("[DB] Something went wrong while retrieving the reseller info for id [ %v ]. Error: %v\n", id, err)

		return nil, err

	}

	reseller := r.(cusModels.Reseller)

	// Get all the usages linked to the reseller
	for _, customer := range reseller.Customers {

		for _, product := range customer.Products {

			u := d.GetReport(product.ProductID, "", from, to)

			usages = append(usages, u...)

		}

	}

	// Scan the usages and make the numbers
	resourcesMap := make(datamodels.JSONdb)

	for i := range usages {

		masterKey := strings.ToTitle(usages[i].ResourceType)

		if _, exists := resourcesMap[masterKey]; !exists {

			resourcesMap[masterKey] = make(datamodels.JSONdb)

		}

		// First the total
		key := "TotalCount"
		if _, exists := resourcesMap[masterKey].(datamodels.JSONdb)[key]; !exists {

			resourcesMap[masterKey].(datamodels.JSONdb)[key] = float64(0)

		}

		resourcesMap[masterKey].(datamodels.JSONdb)[key] = resourcesMap[masterKey].(datamodels.JSONdb)[key].(float64) + float64(1/rounder)

		// Second the regions
		if region, exists := usages[i].Metadata["region"]; exists {

			key := "Regions"

			if _, exists := resourcesMap[masterKey].(datamodels.JSONdb)[key]; !exists {

				resourcesMap[masterKey].(datamodels.JSONdb)[key] = make(datamodels.JSONdb)

			}

			regionKey := region.(string)
			if _, exists := resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[regionKey]; !exists {

				resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[regionKey] = float64(0)

			}

			resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[regionKey] = resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[regionKey].(float64) + float64(1/rounder)

		}

		// Third the flavors in case of
		if flavor, exists := usages[i].Metadata["flavorname"]; exists {

			key := "Flavors"

			if _, exists := resourcesMap[masterKey].(datamodels.JSONdb)[key]; !exists {

				resourcesMap[masterKey].(datamodels.JSONdb)[key] = make(datamodels.JSONdb)

			}

			flavorKey := flavor.(string)
			if _, exists := resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[flavorKey]; !exists {

				resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[flavorKey] = float64(0)

			}

			resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[flavorKey] = resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[flavorKey].(float64) + float64(1/rounder)

		}

		// Fourth the sizes in case of
		if size, exists := usages[i].Metadata["size"]; exists {

			sizeF, err := strconv.ParseFloat(size.(string), 64)

			if err != nil {

				l.Warning.Printf("[DB] Failed to turn the size [ %v ] into a float64 value. Error: %v\n", size, err)

			} else {

				key := "TotalAmount"

				if _, exists := resourcesMap[key]; !exists {

					resourcesMap[masterKey].(datamodels.JSONdb)[key] = float64(0)

				}

				resourcesMap[masterKey].(datamodels.JSONdb)[key] = resourcesMap[masterKey].(datamodels.JSONdb)[key].(float64) + float64(sizeF/rounder)

			}

		}

		// UDR usages
		if size, exists := usages[i].UsageBreakup["used"]; exists {

			var sizeF float64
			key := "TotalAmount"

			// size to float64
			if k := reflect.ValueOf(size); k.Kind() == reflect.Float64 {

				sizeF = size.(float64) / interval

			} else {

				v, err := size.(json.Number).Float64()

				if err != nil {

					l.Warning.Printf("[DB] Failed to turn the size [ %v ] into a float64 value. Error: %v\n", size, err)

				} else {

					sizeF = float64(v) / interval

				}

			}

			if _, exists := resourcesMap[masterKey].(datamodels.JSONdb)[key]; !exists {

				resourcesMap[masterKey].(datamodels.JSONdb)[key] = float64(0)

			}

			resourcesMap[masterKey].(datamodels.JSONdb)[key] = resourcesMap[masterKey].(datamodels.JSONdb)[key].(float64) + float64(sizeF/rounder)

		}

	}

	// Reshape of the JSON to easy showing in the front..
	for key, value := range resourcesMap {

		metricData := value.(datamodels.JSONdb)

		if _, exists := metricData["Regions"]; exists {

			var regions []datamodels.JSONdb

			for k, v := range metricData["Regions"].(datamodels.JSONdb) {

				region := make(datamodels.JSONdb)
				region["name"] = k
				region["amount"] = v

				regions = append(regions, region)

			}

			resourcesMap[key].(datamodels.JSONdb)["Regions"] = regions

		}

		if _, exists := metricData["Flavors"]; exists {

			var regions []datamodels.JSONdb

			for k, v := range metricData["Flavors"].(datamodels.JSONdb) {

				region := make(datamodels.JSONdb)
				region["name"] = k
				region["amount"] = v

				regions = append(regions, region)

			}

			resourcesMap[key].(datamodels.JSONdb)["Flavors"] = regions

		}

	}

	// Create the summarey report
	report.UsageBreakup = resourcesMap

	return &report, nil

}

// getWindow job is to select the timeframe for the usage retrievals according
// to the data providad from<window, window<to, or from<window<to.
// Parameters:
// - from: a datatime reference for the initial border of the time-window.
// - to: a datatime reference for the final border of the time-window.
// Returns:
// - s: a string with the needed window correctly formated to be used with GORM.
func (d *DbParameter) getWindow(from, to strfmt.DateTime) (s string) {

	l.Trace.Printf("[DB] Attempting to get a window for a db query.\n")

	if !((time.Time)(from)).IsZero() {

		if ((time.Time)(to)).IsZero() {

			s = fmt.Sprintf("%v >= '%v'", d.Db.NamingStrategy.ColumnName("", "TimeFrom"), from)

		} else {

			s = fmt.Sprintf("%v >= '%v' AND %v <= '%v'", d.Db.NamingStrategy.ColumnName("", "TimeFrom"), from, d.Db.NamingStrategy.ColumnName("", "TimeTo"), to)

		}

	} else {

		if !((time.Time)(to)).IsZero() {

			s = fmt.Sprintf("%v <= '%v'", d.Db.NamingStrategy.ColumnName("", "TimeTo"), to)

		}

	}

	return s

}

// ProcessUDR job is to process the UDR Reports and turn then into CDR Reports
// and store them in the system.
// Parameters:
// - report: UDR Report model reference to be processed.
// - token: an optional keycloak token in case it's provided.
// Returns:
// - e: error in case of duplication or failure in the task.
func (d *DbParameter) ProcessUDR(report udrModels.UReport, token string) (e error) {

	l.Trace.Printf("[DB] Attempting to process and transform the UDR report into a CDR report and save it to the system.\n")

	// First we clean all the records so we always have a clean UDR generation
	templateRecord := models.CDRRecord{
		AccountID: report.AccountID,
		TimeFrom:  report.TimeFrom,
		TimeTo:    report.TimeTo,
	}

	l.Info.Printf("[DB] Clean up of previous CDRRecords started for period [ %v ] - [ %v ].\n", report.TimeFrom, report.TimeTo)

	if e := d.Db.Where(&templateRecord).Delete(&models.CDRRecord{}).Error; e != nil {

		l.Warning.Printf("[DB] The clean up of previous CDRecords failed. Error: %v\n", e)

	}

	templateReport := models.CReport{
		AccountID: report.AccountID,
		TimeFrom:  report.TimeFrom,
		TimeTo:    report.TimeTo,
	}

	l.Info.Printf("[DB] Clean up of previous CReports started for period [ %v ] - [ %v ].\n", report.TimeFrom, report.TimeTo)

	if e := d.Db.Where(&templateReport).Delete(&models.CReport{}).Error; e != nil {

		l.Warning.Printf("[DB] The clean up of previous CReports failed. Error: %v\n", e)

	}

	// First we get the metric and accounts in the system.
	var planID string
	var cdr models.CReport
	var usages []*models.CDRReport

	now := time.Now().UnixNano()

	x, e := d.Cache.Get(report.AccountID, "product", token)

	if e != nil {

		l.Warning.Printf("[DB] Something went wrong while retrieving the product info for id [ %v ]. Error: %v\n", report.AccountID, e)

		return

	}

	product := x.(cusModels.Product)

	if product.PlanID != "" {

		planID = product.PlanID

	} else if product.Type != "" {

		planID = product.Type

	} else {

		c, e := d.Cache.Get(product.CustomerID, "customer", token)

		if e != nil {

			l.Warning.Printf("[DB] Something went wrong while retrieving the customer info for id [ %v ]. Error: %v\n", product.CustomerID, e)

			return e

		}

		customer := c.(cusModels.Customer)

		if customer.PlanID != "" {

			planID = customer.PlanID

		} else {

			r, e := d.Cache.Get(customer.ResellerID, "reseller", token)

			if e != nil {

				l.Warning.Printf("[DB] Something went wrong while retrieving the reseller info for id [ %v ]. Error: %v\n", customer.ResellerID, e)

				return e

			}

			reseller := r.(cusModels.Reseller)

			planID = reseller.PlanID

		}

	}

PlanDefault:
	p, e := d.Cache.Get(planID, "plan", token)

	if e != nil {

		if planID == "DEFAULT" {

			l.Warning.Printf("[DB] Something went wrong while retrieving the default plan id [ %v ]. Error: %v\n", planID, e)

			return

		}

		l.Warning.Printf("[DB] Something went wrong while retrieving the plan id [ %v ]. Re-trying with default plan. Error: %v\n", planID, e)

		planID = "DEFAULT"

		goto PlanDefault

	}

	plan := p.(pmModels.Plan)
	savedPlan := p.(pmModels.Plan)

	// In case the plan is not valid we return to the deault plan (id 0) which is valid ad vitam
	if (time.Now()).After((time.Time)(*plan.OfferedEndDate)) || (time.Now()).Before((time.Time)(*plan.OfferedStartDate)) {

		l.Warning.Printf("[DB] The plan [ %v ] is only valid between [ %v ] and [ %v ]. Falling back to default plan.\n", plan.ID, *plan.OfferedStartDate, *plan.OfferedEndDate)

		planID = "DEFAULT"

		goto PlanDefault

	}

	s, e := d.Cache.Get("ALL", "sku", token)

	if e != nil {

		l.Warning.Printf("[DB] Something went wrong while retrieving the skus list. Error: %v\n", e)

		return

	}

	skus := make(map[string]string)

	for _, k := range s.([]*pmModels.Sku) {

		skus[*k.Name] = k.ID

	}

	// having the customer and the plan...
	// iter the report
	for _, u := range report.Usage {

		costBreakup := make(datamodels.JSONdb)

		var costSkuTotal []datamodels.JSONdb
		var cycles []*pmModels.Cycle
		var bundle pmModels.SkuBundle
		var skuDiscount, skuPrice, usage, multiplier, amount, quantity float64

		// get cycle of resourceType
		c, e := d.Cache.Get(u.ResourceType, "cycle", token)

		if e != nil {

			l.Warning.Printf("[DB] Something went wrong while retrieving the states for the life cycle linked to the ResourceType [ %v ]. Error: %v\n", u.ResourceType, e)

			return e

		}

		cycles = c.([]*pmModels.Cycle)

		// if server, get the skubundle associated
		if _, exist := cycles[0].SkuList[u.ResourceType]; !exist {

			if id, exists := u.Metadata["flavorid"]; exists {

				b, e := d.Cache.Get(id, "bundle", token)

				if e != nil {

					l.Warning.Printf("[DB] Something went wrong while retrieving the skuBundle id [ %v ]. Error: %v\n", u.Metadata["flavorid"], e)

					return e

				}

				bundle = b.(pmModels.SkuBundle)

				// Usecase: flavor is a generic one but then the image is a windows base one, so the license is not included by default
				if id, exists := u.Metadata["imagename"]; exists && strings.Contains(strings.ToLower(id.(string)), "windows") {

					_, license := bundle.SkuPrices["license"]
					vcpu, exists := bundle.SkuPrices["vcpu"]

					if !license && exists {

						var previous pmModels.SkuBundle

						previous.ID = bundle.ID
						previous.Name = bundle.Name
						previous.SkuPrices = make(datamodels.JSONdb)

						for k, v := range bundle.SkuPrices {

							previous.SkuPrices[k] = v

						}

						previous.SkuPrices["license"] = vcpu

						bundle = previous

					}

				}

			} else {

				l.Warning.Printf("[DB] Something is wrong. Error: The flavorid is supposed to exist in the metadata.\n")

				continue

			}

		}

		// METADA OVERRIDERS
		if override, exists := u.Metadata["PlanOverride"]; exists && override.(bool) {

			overridePlan, e := d.Cache.Get("DEFAULT", "plan", token)

			if e != nil {

				l.Warning.Printf("[DB] Something went wrong while retrieving the Overriding plan. Error: %v\n", e)

			} else {

				plan = overridePlan.(pmModels.Plan)

			}

		} else {

			plan = savedPlan

		}

		// loop cycle
		for i := range cycles {

			// if usageBreakup(cycle.state) !exists, continue
			if stateUse, exists := u.UsageBreakup[*(cycles[i].State)]; !exists {

				continue

				// else, get the usage value
			} else {

				// usage to float64
				if k := reflect.ValueOf(stateUse); k.Kind() == reflect.Float64 {

					usage = stateUse.(float64)

				} else {

					v, _ := stateUse.(json.Number).Int64()
					usage = float64(v)

				}

			}

			// and loop actual.cycle.skus
			for sku, value := range cycles[i].SkuList {

				// get the skuDiscount and skuPrice associated
				if id, exists := skus[sku]; exists {

					for j := range plan.SkuPrices {

						if *plan.SkuPrices[j].SkuID == id {

							skuDiscount = float64(plan.SkuPrices[j].Discount)
							skuPrice = float64(*plan.SkuPrices[j].UnitPrice)

							break

						}

					}

				} else {

					l.Warning.Printf("[DB] Something is wrong. Error: [ %v ] is supposed to exist in the system as a sku.\n", u.ResourceType)

					continue

				}

				// multiplier to float64
				if k := reflect.ValueOf(value); k.Kind() == reflect.Float64 {

					multiplier = value.(float64)

				} else {

					v, _ := value.(json.Number).Int64()
					multiplier = float64(v)

				}

				// if resType not server then use actual.cycle.skus as multiplier for usage
				if *cycles[i].ResourceType == sku {

					// Size of buckets to be computed
					if sz, exists := u.Metadata["size"]; exists {

						size, e := strconv.ParseInt(sz.(string), 10, 0)

						if e != nil {

							l.Warning.Printf("[DB] Something is wrong with the size [ %v ] of the metadata. Error: %v.\n", sz, u.ResourceType)

							amount = usage * multiplier

						} else {

							amount = usage * multiplier * float64(size)

						}

					} else {

						amount = usage * multiplier

					}

					// else, resType is server
				} else {

					// check if in bundle for amounts, get bundle value times the usage and use actual.cycle.skus as an extra multiplier
					if q, exists := bundle.SkuPrices[sku]; !exists {

						continue

					} else {

						// quantity to float64
						if k := reflect.ValueOf(q); k.Kind() == reflect.Float64 {

							quantity = q.(float64)

						} else {

							v, _ := q.(json.Number).Int64()
							quantity = float64(v)

						}

						amount = usage * multiplier * quantity

					}

				}

				// create the cost elemt
				costSku := make(datamodels.JSONdb)
				costSku["sku"] = sku
				costSku["sku-state"] = *cycles[i].State

				// get cost
				costSku["sku-cost"] = amount * skuPrice

				// get discount as % of cost
				costSku["sku-discount"] = costSku["sku-cost"].(float64) * skuDiscount

				// get the net cost as cost-discount
				costSku["sku-net"] = costSku["sku-cost"].(float64) - costSku["sku-discount"].(float64)

				// Add the cost to the collection
				costSkuTotal = append(costSkuTotal, costSku)

			}

		}

		// place the costbreakup in place
		costBreakup["costBreakup"] = costSkuTotal

		if costBreakup["costBreakup"] == nil {

			l.Warning.Printf("[DB] Something went wrong in the cost breakup generation. Skipping this element...\n")

			continue

		}

		// Let's create the total cost
		costBreakup["totalFromSku"] = float64(0)
		for _, c := range costBreakup["costBreakup"].([]datamodels.JSONdb) {

			costBreakup["totalFromSku"] = costBreakup["totalFromSku"].(float64) + c["sku-net"].(float64)

		}

		costBreakup["appliedDiscount"] = costBreakup["totalFromSku"].(float64) * float64(plan.Discount)
		costBreakup["netTotal"] = costBreakup["totalFromSku"].(float64) - costBreakup["appliedDiscount"].(float64)

		var use models.CDRReport
		use.Cost = costBreakup
		use.Metadata = u.Metadata
		use.ResourceID = u.ResourceID
		use.ResourceName = u.ResourceName
		use.ResourceType = u.ResourceType
		use.Unit = u.Unit
		use.UsageBreakup = u.UsageBreakup

		usages = append(usages, &use)

	}

	cdr.AccountID = report.AccountID
	cdr.TimeFrom = report.TimeFrom
	cdr.TimeTo = report.TimeTo
	cdr.Usage = usages

	if e = d.AddRecord(cdr); e != nil {

		l.Warning.Printf("[DB] Something went wrong while saving the CDR record in the system. Error: %v\n", e)

		return

	}

	l.Trace.Printf("[DB] UDR processed and transformed into CDR and saved in the system successfully .\n")

	d.Pipe <- cdr

	l.Trace.Printf("[DB] CDR transmited to the Credit Manager successfully .\n")

	totalTime += float64(time.Now().UnixNano() - now)
	totalCount++

	d.Metrics["count"].With(prometheus.Labels{"type": "Total UDRs transformed to CDRs"}).Inc()

	d.Metrics["time"].With(prometheus.Labels{"type": "CDRs average generation time"}).Set(totalTime / float64(totalCount) / float64(time.Millisecond))

	return

}

// getNiceFloat job is to turn the ugly float64 provided into a "nice looking"
// one according to the scale set, so we move from a floating coma number into
// a fixed coma number.
// Parameters:
// - i: the ugly floating coma number.
// Returns:
// - o: the "nice looking" fixed coma float.
func (d *DbParameter) getNiceFloat(i float64) (o float64) {

	min := float64(float64(1) / float64(scaler))

	if diff := math.Abs(i - min); diff < min {

		o = float64(0)

	} else {

		o = float64(math.Round(i*scaler) / scaler)

	}

	return

}
