package utils

import (
	"errors"
	"strconv"

	"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/billing"
	"github.com/pingcap/dbaas/billing2/model/costexplorer"
	"github.com/pingcap/dbaas/billing2/model/marketing/unit_filter"
	"github.com/pingcap/dbaas/billing2/model/payment"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

func getPrimaryUnitFilters() ([]*forms.UnitFilter, error) {
	ufModel := &unit_filter.Model{}
	var unitFilters []*forms.UnitFilter
	for _, idStr := range config.Get().GetStringSlice(config.AWSCPPODiscountedProductUnitFilterIDs) {
		ufID, err := strconv.ParseUint(idStr, 10, 64)
		if err != nil {
			return nil, err
		}
		uf, err := ufModel.GetUnitFilter(uint(ufID))
		if err != nil {
			return nil, err
		}
		unitFilters = append(unitFilters, uf)
	}
	return unitFilters, nil
}

func splitInvoice(invoiceID uint, paymentInfo *forms.PaymentMethod, primaryUnitFilters []*forms.UnitFilter) ([]forms.Invoice, error) {
	invoiceModel := &payment.InvoiceModel{}
	inv, err := invoiceModel.GetInvoice(invoiceID)
	if err != nil {
		return nil, err
	}
	var invoices []forms.Invoice
	if paymentInfo.PaymentSubCode == nil {
		log.Info("payment subCode is null, skip splitting")
		return []forms.Invoice{*inv}, err
	}
	var primaryInvoice, secondaryInvoice entity.Invoice
	for _, subInv := range []*entity.Invoice{&primaryInvoice, &secondaryInvoice} {
		subInv.Status = entity.InvoiceStatus_CONFIRMED
		subInv.Type = entity.InvoiceType_SUBORDINATED
		subInv.InvoiceDate = inv.StartDate
		subInv.TenantID = inv.TenantID
		subInv.BindedBillingConfigID = *inv.BindedBillingConfigID
		subInv.PaymentMethodID = &paymentInfo.PaymentMethodID
	}
	primaryInvoice.PaymentCode = &paymentInfo.PaymentCode
	secondaryInvoice.PaymentCode = paymentInfo.PaymentSubCode
	err = (&costexplorer.CostExplorerModel{}).ForEachCostSummary([]uint{inv.TenantID}, []uint{*inv.BindedBillingConfigID}, &forms.CostExplorerQueryV2{
		InvoiceID:  invoiceID,
		IgnoreBeta: true,
	}, func(cost forms.UnitCostSummary) error {
		if cost.Internal {
			log.Info("skip splitting for internal cost")
			return nil
		}
		unitInfo, err := (&billing.UnitModel{}).GetUnit(cost.Unit)
		if err != nil {
			return err
		}
		if billing.UnitMatch(unitInfo, primaryUnitFilters, cost.Labels) {
			primaryInvoice.RawPrice = primaryInvoice.RawPrice.Add(cost.RawPrice)
			primaryInvoice.CostPrice = primaryInvoice.CostPrice.Add(cost.CostPrice)
		} else {
			secondaryInvoice.RawPrice = secondaryInvoice.RawPrice.Add(cost.RawPrice)
			secondaryInvoice.CostPrice = secondaryInvoice.CostPrice.Add(cost.CostPrice)
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	var resultIDs []uint
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		var inv entity.Invoice
		err := entity.NewInvoiceQuerySet(tx.Clauses(clause.Locking{Strength: "UPDATE"})).IDEq(invoiceID).One(&inv)
		if err != nil {
			return err
		}
		if inv.Status != entity.InvoiceStatus_CONFIRMED {
			err = billing_errors.NewSystemError(errors.New("cannot split invoice because of status is not confirmed"))
			log.Info(err.Error(), zap.Error(err), zap.Uint("invoiceID", invoiceID))
			return err
		}
		for _, subInv := range []*entity.Invoice{&primaryInvoice, &secondaryInvoice} {
			if subInv.CostPrice.Equal(decimal.Zero) {
				log.Info("subInvoice cost is zero, skip creating", zap.Uint("tenantID", inv.TenantID), zap.Uint("invoiceID", invoiceID))
				continue
			}
			log.Info("create subInvoice", zap.Uint("tenantID", inv.TenantID), zap.Uint("invoiceID", invoiceID), zap.Stringp("paymentCode", subInv.PaymentCode))
			err = subInv.Create(tx)
			if err != nil {
				return err
			}
			resultIDs = append(resultIDs, subInv.ID)
		}
		err = entity.NewInvoiceQuerySet(tx).IDEq(invoiceID).GetUpdater().SetStatus(entity.InvoiceStatus_SPLIT).Update()
		if err != nil {
			return err
		}
		log.Info("success to split invoice", zap.Uint("tenantID", inv.TenantID), zap.Uint("invoiceID", invoiceID))
		return nil
	})
	if err != nil {
		return nil, err
	}
	for _, id := range resultIDs {
		inv, err = invoiceModel.GetInvoice(id)
		if err != nil {
			return nil, err
		}
		invoices = append(invoices, *inv)
	}
	return invoices, nil
}

func SplitCPPOInvoice(invoiceID uint, paymentInfo *forms.PaymentMethod) ([]forms.Invoice, error) {
	ufs, err := getPrimaryUnitFilters()
	if err != nil {
		return nil, err
	}
	return splitInvoice(invoiceID, paymentInfo, ufs)
}

type InvoicesGroup struct {
	PaymentCode *string
	Invoices    []forms.Invoice
}

func GroupInvoicesByPaymentCode(invoices []forms.Invoice) []InvoicesGroup {
	var unsetPaymentCodeInvoices []forms.Invoice
	codeGroupedInvoicesMap := make(map[string][]forms.Invoice)
	for idx := 0; idx != len(invoices); idx++ {
		invoice := invoices[idx]
		if invoice.PaymentCode != nil {
			codeGroupedInvoicesMap[*invoice.PaymentCode] = append(codeGroupedInvoicesMap[*invoice.PaymentCode], invoice)
		} else {
			unsetPaymentCodeInvoices = append(unsetPaymentCodeInvoices, invoice)
		}
	}
	var invoicesGroupList []InvoicesGroup
	if len(unsetPaymentCodeInvoices) != 0 {
		invoicesGroupList = append(invoicesGroupList, InvoicesGroup{Invoices: unsetPaymentCodeInvoices})
	}
	for paymentCode, invoices := range codeGroupedInvoicesMap {
		tmpCode := paymentCode
		invoicesGroupList = append(invoicesGroupList, InvoicesGroup{PaymentCode: &tmpCode, Invoices: invoices})
	}
	return invoicesGroupList
}
