package billing

import (
	"errors"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/model/payment/paymentsub"
	"github.com/pingcap/dbaas/billing2/utils/mathutils"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/pingcap/log"
	"github.com/shopspring/decimal"
	"github.com/spf13/viper"

	"go.uber.org/zap"
	"gorm.io/gorm"
)

type MeasureModel struct {
}

func labelMatch(unitLabels map[string]string, matchLabels map[string]string, exprLabels []forms.ExpressionLabel) bool {
	for k, v := range matchLabels {
		uv, exists := unitLabels[k]
		if !exists {
			return false
		}
		if v != uv {
			return false
		}
	}

	for _, exprLabel := range exprLabels {
		label := exprLabel.Label
		value := exprLabel.Value
		operator := exprLabel.Operator
		method, exists := mathutils.OperatorFuncMap[operator]
		if !exists {
			return false
		}

		uv, exists := unitLabels[label]
		if !exists {
			return false
		}
		res, err := method(uv, value)
		if err != nil {
			return false
		}
		if !res {
			return false
		}
	}
	return true
}
func GetEffectivePrice(unit string, unitLabels map[string]string, bindedInfo *forms.BindedPackageInfo) (*decimal.Decimal, bool) {
	effectedPrice, _, exists := GetPrice(unit, unitLabels, bindedInfo)
	return effectedPrice, exists
}

func GetPriceFromPriceInfo(unit string, unitLabels map[string]string, discount decimal.Decimal, priceInfo forms.UnitPrice) (*decimal.Decimal, *decimal.Decimal, bool) {
	price := priceInfo.Price.Mul(discount)
	rawPrice := priceInfo.Price
	if priceInfo.MultipleFactorLabel != "" {
		valueLabel, exist := unitLabels[priceInfo.MultipleFactorLabel]
		if !exist {
			log.Error("failed find multiple factor label", zap.String("label", priceInfo.MultipleFactorLabel))
			return nil, nil, false
		}
		value, err := strconv.Atoi(valueLabel)
		if err != nil {
			log.Error("failed parse multiple factor label to value", zap.String("label", priceInfo.MultipleFactorLabel), zap.String("value", valueLabel))
			return nil, nil, false
		}
		factor := 0
		if value > priceInfo.MultipleFactorThreshold {
			factor = value - priceInfo.MultipleFactorThreshold
		}
		price = price.Mul(decimal.NewFromInt(int64(factor)))
		rawPrice = rawPrice.Mul(decimal.NewFromInt(int64(factor)))
	}
	return &price, &rawPrice, true
}

func GetPrice(unit string, unitLabels map[string]string, bindedInfo *forms.BindedPackageInfo) (*decimal.Decimal, *decimal.Decimal, bool) {
	packageInfo := bindedInfo.PackageInfo
	if !labelMatch(unitLabels, packageInfo.Labels, packageInfo.ExpressionLabels) {
		return nil, nil, false
	}

	effectDiscount := bindedInfo.PackageInfo.Discount
	if bindedInfo.BindedDiscount != nil {
		if bindedInfo.BindedDiscount.Cmp(effectDiscount) < 0 {
			effectDiscount = *bindedInfo.BindedDiscount
		}
	}

	price := decimal.Zero
	rawPrice := decimal.Zero
	found := false

	if _, exist := packageInfo.UnitPrices[unit]; exist {
		packagePrice, packageRawPrice, valid := GetPriceFromPriceInfo(unit, unitLabels, effectDiscount, bindedInfo.PackageInfo.UnitPrices[unit])
		if valid {
			price = *packagePrice
			rawPrice = *packageRawPrice
			found = true
		}
	}

	for _, extraPricing := range bindedInfo.PackageInfo.ExtraPricingInfos {
		if !labelMatch(unitLabels, extraPricing.Labels, extraPricing.ExpressionLabels) {
			continue
		}
		if _, exist := extraPricing.UnitPrices[unit]; exist {
			var extraEffectDiscount decimal.Decimal
			if extraPricing.Discount == nil {
				extraEffectDiscount = effectDiscount
			} else {
				extraEffectDiscount = *extraPricing.Discount
			}
			packagePrice, packageRawPrice, valid := GetPriceFromPriceInfo(unit, unitLabels, extraEffectDiscount, extraPricing.UnitPrices[unit])
			if valid {
				if !found {
					price = *packagePrice
					rawPrice = *packageRawPrice
					found = true
				} else {
					price = decimal.Min(price, *packagePrice)
					rawPrice = decimal.Min(rawPrice, *packageRawPrice)
				}
			}
		}
	}

	return &price, &rawPrice, found
}

type CombinedBindedInfo struct {
	BindedPackageInfo forms.BindedPackageInfo
	EffectPrice       decimal.Decimal
	RawPrice          decimal.Decimal
}

func getLeastPriceAvaiableBindedPackages(unit string, cost int, availableBindedPackages []CombinedBindedInfo, payable bool, dryrun bool) *CombinedBindedInfo {
	sort.Slice(availableBindedPackages, func(i, j int) bool {
		left := availableBindedPackages[i].EffectPrice
		right := availableBindedPackages[j].EffectPrice
		return left.Cmp(right) < 0
	})

	for _, availableBindedPackage := range availableBindedPackages {
		costPrice := availableBindedPackage.EffectPrice.Mul(decimal.NewFromInt(int64(cost)))
		if costPrice.Equal(decimal.NewFromInt(0)) {
			return &availableBindedPackage
		}
	}

	for _, availableBindedPackage := range availableBindedPackages {
		if availableBindedPackage.BindedPackageInfo.PackageInfo.CreditsEnable && (dryrun || availableBindedPackage.BindedPackageInfo.RemainingCredits.Cmp(decimal.Zero) > 0) {
			return &availableBindedPackage
		}
	}

	if !dryrun && !payable {
		return nil
	}

	for _, availableBindedPackage := range availableBindedPackages {
		if !availableBindedPackage.BindedPackageInfo.PackageInfo.CreditsOnly {
			return &availableBindedPackage
		}
	}
	return nil
}

func costUnitUsage(costs *entity.UserUnitCost, infos []forms.BindedPackageInfo, dryrun bool) (*entity.UserUnitCost, error) {
	costID := costs.CostID
	unit := costs.Unit
	cost := costs.Cost
	labels := costs.Labels
	availableBindedPackages := make([]CombinedBindedInfo, 0)
	log.Info("Cost Unit Usage All Candidate Packages", zap.String("CostID", costID), zap.Int("info num", len(infos)))
	// filter all label not matched
	for _, info := range infos {
		price, rawPrice, found := GetPrice(unit, labels, &info)
		if found {
			availableBindedPackages = append(availableBindedPackages, CombinedBindedInfo{
				BindedPackageInfo: info,
				EffectPrice:       *price,
				RawPrice:          *rawPrice,
			})
		} else {
			log.Info("Package not matched filtered", zap.String("CostID", costID), zap.Uint("BindedPackageID", info.BindedPackageID))
		}
	}
	log.Info("Cost Unit Usage All Matched Packages", zap.String("CostID", costID), zap.Int("avaialbe packages num", len(availableBindedPackages)))

	payable, err := (&paymentsub.PaymentModel{}).Payable(costs.TenantID)
	if err != nil {
		return nil, err
	}

	// for now, should less than one package available here.
	// a cluster should be free / dev tier / poc / standard. and should only be consumed by exactly package with labels.

	choosedBindedPackage := getLeastPriceAvaiableBindedPackages(unit, cost, availableBindedPackages, payable, dryrun)

	if choosedBindedPackage == nil {
		log.Info("no valid package match", zap.String("CostID", costID))
		err := entity.NewUserUnitCostQuerySet(db.GetDB()).CostIDEq(costID).StatusEq(entity.UserUnitCostStatus_INIT).GetUpdater().SetStatus(entity.UserUnitCostStatus_Failed).Update()
		if err != nil {
			return nil, billing_errors.NewSystemError(err)
		}
		return nil, billing_errors.NewNotCostableError(errors.New("no valid package match"))
	}

	costPrice := choosedBindedPackage.EffectPrice.Mul(decimal.NewFromInt(int64(cost)))
	choosedPackage := choosedBindedPackage.BindedPackageInfo

	log.Info("Choosed Package", zap.Uint("BindedPackageID", choosedPackage.BindedPackageID), zap.Uint("PackageID", choosedPackage.PackageID), zap.Bool("Package Credits Enable", choosedPackage.PackageInfo.CreditsEnable), zap.String("Package Credits", choosedPackage.RemainingCredits.String()))

	log.Info("Cost Unit Price", zap.String("Unit Price", choosedBindedPackage.EffectPrice.String()), zap.String("Discount", choosedPackage.PackageInfo.Discount.String()), zap.Int("Count", cost), zap.String("Price", costPrice.String()))
	newCosts := *costs
	newCosts.Status = entity.UserUnitCostStatus_DONE
	newCosts.CostPrice = costPrice
	newCosts.RawPrice = choosedBindedPackage.RawPrice.Mul(decimal.NewFromInt(int64(cost)))
	newCosts.PackageID = choosedPackage.PackageID
	newCosts.PackageVersion = choosedPackage.PackageInfo.Version
	newCosts.BindedPackageID = choosedPackage.BindedPackageID
	newCosts.BindedDiscountChangeRecordID = choosedPackage.BindedDiscountRecordID
	if choosedPackage.PackageInfo.CreditsEnable && choosedPackage.RemainingCredits.Cmp(decimal.Zero) > 0 {
		newCosts.ConsumedSource = entity.ConsumedSource_Credits
		newCosts.InvoiceID = entity.InvoiceNoUseID
	} else {
		newCosts.ConsumedSource = entity.ConsumedSource_Invoice
		newCosts.InvoiceID = entity.InvoiceUnbindID
	}

	if dryrun {
		return &newCosts, nil
	}

	// must add price compare for package could be credits only false and use payment to pay.
	if newCosts.ConsumedSource == entity.ConsumedSource_Credits {
		log.Info("use package remaining credits to cost price")
		// should direct cost packageCredits and Result Value
		err := db.GetDB().Transaction(func(tx *gorm.DB) error {
			num, err := entity.NewUserUnitCostQuerySet(tx).CostIDEq(costID).StatusEq(entity.UserUnitCostStatus_INIT).
				GetUpdater().SetStatus(entity.UserUnitCostStatus_DONE).SetConsumedSource(entity.ConsumedSource_Credits).SetInvoiceID(entity.InvoiceNoUseID).
				SetCostPrice(costPrice).SetRawPrice(newCosts.RawPrice).
				SetBindedPackageID(newCosts.BindedPackageID).SetPackageID(choosedPackage.PackageID).SetPackageVersion(choosedPackage.PackageInfo.Version).SetBindedDiscountChangeRecordID(newCosts.BindedDiscountChangeRecordID).
				UpdateNum()
			if err != nil {
				return err
			}
			if num < 0 {
				// already be executed before, just in a retry.
				log.Info("Already executed before. skip this execute")
				return nil
			}
			res := tx.Model(&entity.BindedPackage{}).Where("id = ?", choosedPackage.BindedPackageID).Where("remaining_credits >= ?", 0).Update("remaining_credits", gorm.Expr("remaining_credits - ?", costPrice))
			if res.Error != nil {
				return res.Error
			}
			if res.RowsAffected < 1 {
				return errors.New("no remaining credits in package when execute")
			}
			log.Info("Remaining Credits", zap.String("Package Creidts", (choosedPackage.RemainingCredits.Sub(costPrice).String())))
			return nil
		})
		if err != nil {
			// atom update db failed.
			return nil, billing_errors.NewSystemError(err)
		}
	} else {
		log.Info("use payment to cost price, just a record")
		num, err := entity.NewUserUnitCostQuerySet(db.GetDB()).CostIDEq(costID).StatusEq(entity.UserUnitCostStatus_INIT).
			GetUpdater().SetStatus(entity.UserUnitCostStatus_DONE).SetConsumedSource(entity.ConsumedSource_Invoice).SetInvoiceID(entity.InvoiceUnbindID).
			SetCostPrice(costPrice).SetRawPrice(newCosts.RawPrice).
			SetBindedPackageID(newCosts.BindedPackageID).SetPackageID(choosedPackage.PackageID).SetPackageVersion(choosedPackage.PackageInfo.Version).SetBindedDiscountChangeRecordID(newCosts.BindedDiscountChangeRecordID).
			UpdateNum()
		if err != nil {
			return nil, billing_errors.NewSystemError(err)
		}
		if num < 0 {
			// already be executed before, just in a retry.
			return nil, nil
		}
		// should be consume by payment system after.
	}
	return &newCosts, nil
}

func measureFromEntity(entity *entity.UserUnitCost) *forms.UnitCosts {
	return &forms.UnitCosts{
		CostID:        entity.CostID,
		Unit:          entity.Unit,
		Cost:          entity.Cost,
		Labels:        entity.Labels,
		CostTimeStamp: entity.CostTimeStamp,

		CostPrice:       entity.CostPrice,
		RawPrice:        entity.RawPrice,
		PackageID:       entity.PackageID,
		PackageVersion:  entity.PackageVersion,
		BindedPackageID: entity.BindedPackageID,
		ConsumedSource:  entity.ConsumedSource,
		InvoiceID:       entity.InvoiceID,
	}
}

func (model *MeasureModel) CostUnits(tenantID uint, costs *forms.UnitCosts) (*forms.UnitCosts, error) {
	return model.costUnitsInternal(tenantID, costs, false)
}

func (model *MeasureModel) CostUnitsDryrun(tenantID uint, costs *forms.UnitCosts) (*forms.UnitCosts, error) {
	return model.costUnitsInternal(tenantID, costs, true)
}

func (model *MeasureModel) costUnitsInternal(tenantID uint, costs *forms.UnitCosts, dryrun bool) (*forms.UnitCosts, error) {
	if costs.Cost == 0 {
		return &forms.UnitCosts{
			CostID:        costs.CostID,
			Labels:        costs.Labels,
			CostTimeStamp: costs.CostTimeStamp,
			Unit:          costs.Unit,
			Cost:          costs.Cost,
			RawPrice:      decimal.Zero,
			CostPrice:     decimal.Zero,
		}, nil
	}
	cost := &entity.UserUnitCost{}
	if costs.CostID == "" {
		// prefent modify input
		newCosts := *costs
		costs = &newCosts
		costs.CostID = uuid.NewString()
	} else {
		if !dryrun {
			err := entity.NewUserUnitCostQuerySet(db.GetDB()).CostIDEq(costs.CostID).One(cost)
			if !errors.Is(err, gorm.ErrRecordNotFound) {
				if cost.Status == entity.UserUnitCostStatus_Failed {
					return nil, billing_errors.NewNotCostableError(errors.New("no valid package match"))
				}
				// already called before. this request is a network retry
				return measureFromEntity(cost), nil
			}
		}
	}
	cost = &entity.UserUnitCost{
		CostID:        costs.CostID,
		TenantID:      tenantID,
		Labels:        costs.Labels,
		CostTimeStamp: costs.CostTimeStamp,
		Unit:          costs.Unit,
		Cost:          costs.Cost,
		Status:        entity.UserUnitCostStatus_INIT,
	}

	if !dryrun {
		err := cost.Create(db.GetDB())
		if err != nil {
			return nil, billing_errors.NewSystemError(err)
		}
	}
	bindedPackageModel := &BindedPackageModel{}
	bindedInfos, err := bindedPackageModel.GetAllUserBindedPackage(tenantID)
	if err != nil {
		return nil, err
	}

	if dryrun {
		globalPricePackageID := viper.GetUint(config.GlobalPricePackage)
		if globalPricePackageID != 0 {
			globalPriceInfo, err := (&PackageModel{}).GetPackage(globalPricePackageID)
			if err != nil {
				return nil, err
			}
			bindedInfos = append(bindedInfos, forms.BindedPackageInfo{
				PackageInfo: globalPriceInfo,
			})
		}
	}

	cost, err = costUnitUsage(cost, bindedInfos, dryrun)
	if err != nil {
		return nil, err
	}

	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return measureFromEntity(cost), nil
}

// for CostExplorer Usage
func CostsSummaryFromEntities(costs []entity.UnitCostSummary) []forms.UnitCostSummary {
	infos := make([]forms.UnitCostSummary, len(costs))
	for i, cost := range costs {
		infos[i] = forms.UnitCostSummary{
			Unit:      cost.Unit,
			Labels:    cost.Labels,
			CostPrice: cost.CostPrice,
			RawPrice:  cost.RawPrice,
		}
	}
	return infos
}

// const

func getSummaryLabelFromConfig() map[string]bool {
	res := map[string]bool{}
	for _, label := range strings.Split(viper.GetString(config.CostSummaryGroupLabelList), ",") {
		res[label] = true
	}
	return res
}

func getKeeyShowingCategory() []string {
	listValue := viper.GetString(config.KeeyShowingCategory)
	if listValue == "" {
		return []string{}
	}
	return strings.Split(viper.GetString(config.KeeyShowingCategory), ",")
}

func newCategoryCosts() map[string]forms.Price {
	keeyShowingCategory := getKeeyShowingCategory()
	res := map[string]forms.Price{}
	for _, category := range keeyShowingCategory {
		res[category] = forms.Price{}
	}
	return res
}

func CostSummaryFromUnitCosts(costs []forms.UnitCostSummary, units []forms.UnitInfo) *forms.Costs {
	summaryLabels := getSummaryLabelFromConfig()

	res := &forms.Costs{}

	defaultCategory := viper.GetString(config.DefaultUnitCategory)
	defaultLabelValue := viper.GetString(config.DefaultLabelValueCategory)

	unitsCategory := map[string]string{}
	for _, unit := range units {
		unitsCategory[unit.UnitKey] = unit.Category
	}
	res.CategoryUnitsCosts = newCategoryCosts()

	res.LabelsUnitsCosts = map[string]forms.LabelCosts{}
	for label := range summaryLabels {
		res.LabelsUnitsCosts[label] = forms.LabelCosts{}
	}
	for _, cost := range costs {
		category := unitsCategory[cost.Unit]
		if category == "" {
			category = defaultCategory
		}
		res.CostPrice = res.CostPrice.Add(cost.CostPrice)
		res.RawPrice = res.RawPrice.Add(cost.RawPrice)
		costCategory := res.CategoryUnitsCosts[category]
		costCategory.CostPrice = costCategory.CostPrice.Add(cost.CostPrice)
		costCategory.RawPrice = costCategory.RawPrice.Add(cost.RawPrice)
		res.CategoryUnitsCosts[category] = costCategory
		for label := range summaryLabels {
			labelValue, ok := cost.Labels[label]
			if !ok {
				labelValue = defaultLabelValue
			}
			if summaryLabels[label] {
				costLabel := res.LabelsUnitsCosts[label][labelValue]
				if costLabel.CategoryUnitsCosts == nil {
					costLabel.CategoryUnitsCosts = newCategoryCosts()
				}
				costCategory := costLabel.CategoryUnitsCosts[category]
				costCategory.CostPrice = costCategory.CostPrice.Add(cost.CostPrice)
				costCategory.RawPrice = costCategory.RawPrice.Add(cost.RawPrice)
				costLabel.CategoryUnitsCosts[category] = costCategory
				costLabel.CostPrice = costLabel.CostPrice.Add(cost.CostPrice)
				costLabel.RawPrice = costLabel.RawPrice.Add(cost.RawPrice)
				res.LabelsUnitsCosts[label][labelValue] = costLabel
			}

		}
	}

	return res
}

func (model *MeasureModel) UnpaidCostsBindInvoice(tenantID uint, start time.Time, end time.Time, invoiceID uint) error {
	err := entity.NewUserUnitCostQuerySet(db.GetDB()).TenantIDEq(tenantID).StatusEq(entity.UserUnitCostStatus_DONE).ConsumedSourceEq(entity.ConsumedSource_Invoice).InvoiceIDEq(entity.InvoiceUnbindID).CostTimeStampGte(start.Unix()).CostTimeStampLt(end.Unix()).GetUpdater().SetInvoiceID(int(invoiceID)).Update()
	if err != nil {
		return billing_errors.NewSystemError(err)
	}
	return nil
}

func (model *MeasureModel) CostsUnBindInvoice(tenantID uint, invoiceID uint) error {
	err := entity.NewUserUnitCostQuerySet(db.GetDB()).TenantIDEq(tenantID).InvoiceIDEq(int(invoiceID)).GetUpdater().SetInvoiceID(entity.InvoiceUnbindID).Update()
	if err != nil {
		return billing_errors.NewSystemError(err)
	}
	return nil
}

func (model *MeasureModel) RemoveUnusedCosts(start, end time.Time) error {
	err := entity.NewUserUnitCostQuerySet(db.GetDB()).StatusNe(entity.UserUnitCostStatus_DONE).CostTimeStampGt(start.Unix() - 1).CostTimeStampLt(end.Unix()).Delete()
	if err != nil {
		return billing_errors.NewSystemError(err)
	}
	log.Info("removed all unused units", zap.Time("start", start), zap.Time("end", end))
	return nil
}

var ALL_GROUPING_COLUMNS = []string{
	"unit",
	"tenant_id",
	"labels",
	"binded_package_id",
	"package_id",
	"package_version",
	"binded_discount_change_record_id",
	"consumed_source",
	"invoice_id",
}

func (model *MeasureModel) MergeSmallUnits(start, end time.Time) error {
	start = timeutils.GetHourDate(start)
	end = timeutils.GetHourDate(end)
	now := time.Now()
	err := db.GetDB().Transaction(func(tx *gorm.DB) error {
		// the argument index is started with 1
		sql := fmt.Sprintf(`insert into user_unit_costs (cost_id, cost, raw_price, cost_price, cost_time_stamp, status, type, created_at, updated_at,  %[1]s)
			    select UUID(), sum(cost), sum(raw_price), sum(cost_price), ?, ?, ?, ?, ?, %[1]s from user_unit_costs where status=? and type!=? and 
				cost_time_stamp > ? and cost_time_stamp < ? group by %[1]s`, strings.Join(ALL_GROUPING_COLUMNS, ","))
		err := tx.Exec(sql, start.Unix(), entity.UserUnitCostStatus_DONE, entity.UserUnitCostType_Merged, now, now, entity.UserUnitCostStatus_DONE, entity.UserUnitCostStatus_Merged, start.Unix()-1, end.Unix()+1).Error
		if err != nil {
			return billing_errors.NewSystemError(err)
		}
		err = tx.Exec("update user_unit_costs set status= ? where status=? and type!= ? and cost_time_stamp > ? and cost_time_stamp < ?", entity.UserUnitCostStatus_Merged, entity.UserUnitCostStatus_DONE, entity.UserUnitCostStatus_Merged, start.Unix()-1, end.Unix()+1).Error
		if err != nil {
			return billing_errors.NewSystemError(err)
		}
		return nil
	})
	if err != nil {
		return err
	}
	log.Info("merged all raw units", zap.Time("start", start), zap.Time("end", end))
	return nil
}
