package impl

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

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/constant"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler/utils"
	"github.com/pingcap/dbaas/billing2/model/billing"
	"github.com/pingcap/dbaas/billing2/model/billingconfig"
	"github.com/pingcap/dbaas/billing2/model/costexplorer"
	"github.com/pingcap/dbaas/billing2/model/payment"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"
)

type AWSCPPOCustomerInvoiceTransferHandlerImpl struct {
}

func (*AWSCPPOCustomerInvoiceTransferHandlerImpl) Setup(tenant *forms.Tenant) (userCode string, err error) {
	billingConfigModel := &billingconfig.BillingConfigModel{}
	billingConfig, err := billingConfigModel.GetBillingConfigByManagedTenantID(tenant.TenantID)
	if err != nil {
		return "", err
	}
	billingConfigID := billingConfig.BillingConfigID
	payerTenantID := billingConfig.TenantID
	return fmt.Sprintf("%d-%d", payerTenantID, billingConfigID), nil
}

func (*AWSCPPOCustomerInvoiceTransferHandlerImpl) Pay(paymentInfo *forms.PaymentMethod, billingCompany *forms.BillingCompany, invoice *forms.Invoice) (*forms.Invoice, error) {
	payerConfigsStr := strings.Split(paymentInfo.PaymentCode, "-")
	if len(payerConfigsStr) != 2 {
		err := errors.New("unknown payer configs")
		log.Error(err.Error(), zap.Error(err), zap.String("userCode", paymentInfo.PaymentCode))
		return nil, err
	}
	var payerConfigs [2]uint
	for i, s := range payerConfigsStr {
		id, err := strconv.ParseUint(s, 10, 64)
		if err != nil {
			return nil, err
		}
		payerConfigs[i] = uint(id)
	}
	payerTenantID := payerConfigs[0]
	payerBillingConfigID := payerConfigs[1]
	measureModel := &billing.MeasureModel{}
	err := (&costexplorer.CostExplorerModel{}).ForEachTransferCostByInvoiceId(invoice.TenantID, invoice.InvoiceID, func(cost entity.DateUnitCostSummary) error {
		if cost.Labels == nil {
			cost.Labels = make(map[string]string)
		}
		if cost.ConsumedSource == entity.ConsumedSource_Credits {
			return nil
		}
		switch cost.Unit {
		case constant.FixedPrice_SKU, constant.PercentagePrice_SKU, constant.CommintmentPrice_SKU, constant.TieredPrice_SKU:
			log.Info("ignore support fee transfer")
		default:
			// Same merged-labels problem as MspHandlerImpl.Pay: the merged
			// row's labels only reflect its first contributing chunk, so a
			// single SecondaryMeasure on the merged row prices every chunk
			// with stale labels. Replay each contribution with its own
			// labels via the shared helper when the history is complete.
			customerIDStr := strconv.FormatUint(uint64(invoice.TenantID), 10)
			return transferMergedCost(measureModel, payerTenantID, invoice.InvoiceID,
				constant.MarketplaceResellerManagedCustomerIDLabel, customerIDStr, cost,
				func(int64) (uint, error) { return payerBillingConfigID, nil })
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return invoice, nil
}

func (*AWSCPPOCustomerInvoiceTransferHandlerImpl) PayMultiInvoice(paymentInfo *forms.PaymentMethod, billingCompany *forms.BillingCompany, invoices []forms.Invoice) ([]forms.Invoice, error) {
	return nil, billing_errors.NewNotImplementedError(errors.New("can not send msp multi invoice"))
}

func (*AWSCPPOCustomerInvoiceTransferHandlerImpl) Payable(tenantID uint, userCode string) (bool, error) {
	// The CPPO Customer must be able to pay because of the contract of the partner.
	return true, nil
}

func (*AWSCPPOCustomerInvoiceTransferHandlerImpl) NeedMultiInvoiceSendOnce() bool {
	return false
}

func (h *AWSCPPOCustomerInvoiceTransferHandlerImpl) GetThirdPartyName() string {
	return entity.PaymentType_AWSCPPOTransfer
}

func (h *AWSCPPOCustomerInvoiceTransferHandlerImpl) EnableReconciliation() bool {
	return false
}

func (h *AWSCPPOCustomerInvoiceTransferHandlerImpl) GetDailyRevenue(date time.Time) ([]*paymenthandler.TenantRevenue, error) {
	return nil, billing_errors.NewNotImplementedError(errors.New("don't need"))
}

func (h *AWSCPPOCustomerInvoiceTransferHandlerImpl) GetMonthlyRevenue(date time.Time) ([]*paymenthandler.TenantRevenue, error) {
	return nil, billing_errors.NewNotImplementedError(errors.New("don't need"))
}

func (h *AWSCPPOCustomerInvoiceTransferHandlerImpl) EnableTaxCalculation() bool {
	return false
}

func (h *AWSCPPOCustomerInvoiceTransferHandlerImpl) EnableHourlySending(tenantID, billingConfigID uint) bool {
	return true
}

func (h *AWSCPPOCustomerInvoiceTransferHandlerImpl) EnableDailySending(tenantID, billingConfigID uint) (bool, int) {
	return false, 0
}

func (h *AWSCPPOCustomerInvoiceTransferHandlerImpl) NeedInMonthInvoice() bool {
	return false
}

func (h *AWSCPPOCustomerInvoiceTransferHandlerImpl) NeedSetInvoiceAutoConfirm() bool {
	return true
}

type AWSCPPOResellerHandlerImpl struct {
}

func (*AWSCPPOResellerHandlerImpl) Setup(tenant *forms.Tenant) (userCode string, err error) {
	return "", billing_errors.NewNotImplementedError(errors.New("cppo payment payment code must be assigned by the deal desk"))
}

func (h *AWSCPPOResellerHandlerImpl) Pay(paymentInfo *forms.PaymentMethod, billingCompany *forms.BillingCompany, invoice *forms.Invoice) (*forms.Invoice, error) {
	return nil, billing_errors.NewNotImplementedError(errors.New("can not send msp multi invoice"))
}

func (h *AWSCPPOResellerHandlerImpl) getUserCodeFromPaymentCode(paymentCode string) (string, error) {
	splitCodes := strings.Split(paymentCode, "-")
	if len(splitCodes) != 2 {
		return "", errors.New("unexpected code length")
	}
	return splitCodes[1], nil
}

func (h *AWSCPPOResellerHandlerImpl) splitInvoices(paymentInfo *forms.PaymentMethod, invoices []forms.Invoice) []forms.Invoice {
	var sentInvoices []forms.Invoice
	for _, inv := range invoices {
		_, err := utils.SplitCPPOInvoice(inv.InvoiceID, paymentInfo)
		if err != nil {
			log.Error("failed to split invoice", zap.Uint("invoiceID", inv.InvoiceID))
			continue
		}
		log.Info("success to split invoice", zap.Uint("invoiceID", inv.InvoiceID))
		inv.Status = entity.InvoiceStatus_SPLIT
		sentInvoices = append(sentInvoices, inv)
	}
	return sentInvoices
}
func (h *AWSCPPOResellerHandlerImpl) sendInvoicesToPrimaryProduct(paymentInfo *forms.PaymentMethod, invoices []forms.Invoice) ([]forms.Invoice, error) {
	userCode, err := h.getUserCodeFromPaymentCode(paymentInfo.PaymentCode)
	if err != nil {
		log.Error("failed to get userCode", zap.Error(err), zap.Uint("tenantId", paymentInfo.TenantID), zap.String("paymentCode", paymentInfo.PaymentCode))
		return nil, err
	}
	lastInvoiceSentDate, err := (&payment.InvoiceModel{}).GetLastSendInvoiceDateByPaymentCode(entity.PaymentType_AWSCPPOReseller, paymentInfo.PaymentCode)
	if err != nil {
		log.Error("failed to get lastInvoiceSentDate", zap.Uint("tenantId", paymentInfo.TenantID), zap.String("paymentCode", paymentInfo.PaymentCode))
		return nil, err
	}
	if lastInvoiceSentDate.Add(time.Hour).After(timeutils.GetNow()) {
		log.Info("can not send invoice to aws twice in less than one hour ", zap.Uint("tenantId", paymentInfo.TenantID), zap.String("paymentCode", paymentInfo.PaymentCode))
		return nil, nil
	}
	productCode := config.Get().GetString(config.AWSCPPODiscountedProductCode)
	productUnit := config.Get().GetString(config.AWSCPPODiscountedProductUnit)
	productPriceFactor := config.Get().GetInt(config.AWSCPPODiscountedProductUnitPriceFactor)
	if paymentInfo.PaymentMeta.EnlargerDiscount.LessThanOrEqual(decimal.Zero) {
		err := errors.New("invalid aws cppo discount detected")
		log.Error(err.Error(), zap.Error(err), zap.String("PaymentCode", paymentInfo.PaymentCode))
		return nil, err
	}

	enlargeDiscountFactor := paymentInfo.PaymentMeta.EnlargerDiscount
	return SendInvoice(&AWSInvoice{
		TenantID:              paymentInfo.TenantID,
		PaymentCode:           userCode,
		ProductCode:           productCode,
		ProductUnit:           productUnit,
		UnitPriceFactor:       int64(productPriceFactor),
		EnlargeDiscountFactor: enlargeDiscountFactor,
		Invoices:              invoices,
	})
}

func (h *AWSCPPOResellerHandlerImpl) sendInvoicesToSecondaryProduct(paymentInfo *forms.PaymentMethod, invoices []forms.Invoice) ([]forms.Invoice, error) {
	if paymentInfo.PaymentSubCode == nil {
		err := errors.New("paymentInfo.PaymentSubCode is nil")
		return nil, billing_errors.NewSystemError(err)
	}
	userCode, err := h.getUserCodeFromPaymentCode(*paymentInfo.PaymentSubCode)
	if err != nil {
		log.Error("failed to get userCode", zap.Error(err), zap.Uint("tenantId", paymentInfo.TenantID), zap.String("paymentCode", *paymentInfo.PaymentSubCode))
		return nil, err
	}
	lastInvoiceSentDate, err := (&payment.InvoiceModel{}).GetLastSendInvoiceDateByPaymentCode(entity.PaymentType_AWSCPPOReseller, *paymentInfo.PaymentSubCode)
	if err != nil {
		log.Error("failed to get lastInvoiceSentDate", zap.Uint("tenantId", paymentInfo.TenantID), zap.String("paymentCode", *paymentInfo.PaymentSubCode))
		return nil, err
	}
	if lastInvoiceSentDate.Add(time.Hour).After(timeutils.GetNow()) {
		log.Info("can not send invoice to aws twice in less than one hour ", zap.Uint("tenantId", paymentInfo.TenantID), zap.String("paymentCode", *paymentInfo.PaymentSubCode))
		return nil, nil
	}
	productCode := config.Get().GetString(config.AWSCPPONonDiscountedProductCode)
	productUnit := config.Get().GetString(config.AWSCPPONonDiscountedProductUnit)
	productPriceFactor := config.Get().GetInt(config.AWSCPPONonDiscountedProductUnitPriceFactor)
	return SendInvoice(&AWSInvoice{
		TenantID:              paymentInfo.TenantID,
		PaymentCode:           userCode,
		ProductCode:           productCode,
		ProductUnit:           productUnit,
		UnitPriceFactor:       int64(productPriceFactor),
		EnlargeDiscountFactor: decimal.NewFromInt(1),
		Invoices:              invoices,
	})
}

func (h *AWSCPPOResellerHandlerImpl) PayMultiInvoice(paymentInfo *forms.PaymentMethod, billingCompany *forms.BillingCompany, invoices []forms.Invoice) ([]forms.Invoice, error) {
	primaryProductCode := config.Get().GetString(config.AWSCPPODiscountedProductCode)
	secondaryProductCode := config.Get().GetString(config.AWSCPPONonDiscountedProductCode)
	if !strings.HasPrefix(paymentInfo.PaymentCode, primaryProductCode) {
		err := errors.New("primary cppo payment code not valid")
		log.Error(err.Error(), zap.String("code", paymentInfo.PaymentCode), zap.Uint("tenantID", paymentInfo.TenantID))
		return nil, billing_errors.NewInvalidInputError(err)
	}
	if paymentInfo.PaymentSubCode != nil && !strings.HasPrefix(*paymentInfo.PaymentSubCode, secondaryProductCode) {
		err := errors.New("secondary cppo payment code not valid")
		log.Error(err.Error(), zap.Stringp("code", paymentInfo.PaymentSubCode), zap.Uint("tenantID", paymentInfo.TenantID))
		return nil, billing_errors.NewInvalidInputError(err)
	}
	invoiceGroupList := utils.GroupInvoicesByPaymentCode(invoices)
	var sentInvoices []forms.Invoice
	for _, invoiceGroup := range invoiceGroupList {
		needSplitAllInvoices := false
		needSendToPrimaryProduct := false
		needSendToSecondaryProduct := false
		if invoiceGroup.PaymentCode == nil {
			if paymentInfo.PaymentSubCode != nil {
				needSplitAllInvoices = true
			} else {
				invoiceGroup.PaymentCode = &paymentInfo.PaymentCode
				needSendToPrimaryProduct = true
			}
		}
		if invoiceGroup.PaymentCode != nil && *invoiceGroup.PaymentCode == paymentInfo.PaymentCode {
			needSendToPrimaryProduct = true
		}
		if paymentInfo.PaymentSubCode != nil && invoiceGroup.PaymentCode != nil && *invoiceGroup.PaymentCode == *paymentInfo.PaymentSubCode {
			needSendToSecondaryProduct = true
		}

		if needSplitAllInvoices {
			splitInvoices := h.splitInvoices(paymentInfo, invoiceGroup.Invoices)
			sentInvoices = append(splitInvoices)
			continue
		}
		if invoiceGroup.PaymentCode == nil {
			log.Error("payment code is unexpectedly nil", zap.Uint("tenantId", paymentInfo.TenantID))
			continue
		}

		if needSendToPrimaryProduct {
			sentPrimaryInvoices, err := h.sendInvoicesToPrimaryProduct(paymentInfo, invoiceGroup.Invoices)
			if err != nil {
				log.Error("failed to send invoice to primary product", zap.Error(err), zap.Uint("paymentID", paymentInfo.PaymentMethodID), zap.Any("needSendInvoices", invoiceGroup.Invoices), zap.Any("sentInvoices", sentPrimaryInvoices), zap.Stringp("paymentCode", invoiceGroup.PaymentCode))
				continue
			}
			sentInvoices = append(sentInvoices, sentPrimaryInvoices...)
			log.Info("success to sent metering to primary product", zap.Uint("paymentID", paymentInfo.PaymentMethodID), zap.Any("needSendInvoices", invoiceGroup.Invoices), zap.Stringp("paymentCode", invoiceGroup.PaymentCode))
		} else if needSendToSecondaryProduct {
			sentSecondaryInvoices, err := h.sendInvoicesToSecondaryProduct(paymentInfo, invoiceGroup.Invoices)
			if err != nil {
				log.Error("failed to send invoice to secondary product", zap.Error(err), zap.Uint("paymentID", paymentInfo.PaymentMethodID), zap.Any("needSendInvoices", invoiceGroup.Invoices), zap.Stringp("paymentCode", invoiceGroup.PaymentCode))
				continue
			}
			log.Info("success to sent metering to secondary product", zap.Uint("paymentID", paymentInfo.PaymentMethodID), zap.Any("needSendInvoices", invoiceGroup.Invoices), zap.Any("sentInvoices", sentSecondaryInvoices), zap.Stringp("paymentCode", invoiceGroup.PaymentCode))
			sentInvoices = append(sentInvoices, sentSecondaryInvoices...)
		} else {
			log.Error("unknown payment code invoices detected", zap.Any("invoices", invoiceGroup.Invoices))
		}
	}
	if len(invoices) != len(sentInvoices) {
		log.Warn("not all invoice has been sent", zap.Uint("paymentID", paymentInfo.PaymentMethodID))
	}
	return sentInvoices, nil
}

func (h *AWSCPPOResellerHandlerImpl) NeedMultiInvoiceSendOnce() bool {
	return true
}

func (h *AWSCPPOResellerHandlerImpl) EnableReconciliation() bool {
	return false
}

func (*AWSCPPOResellerHandlerImpl) Payable(tenantID uint, userCode string) (bool, error) {
	// The CPPO Reseller must be able to pay because of the contract of the partner.
	return true, nil
}

func (h *AWSCPPOResellerHandlerImpl) GetThirdPartyName() string {
	return entity.PaymentType_AWSCPPOReseller
}

func (h *AWSCPPOResellerHandlerImpl) GetDailyRevenue(date time.Time) ([]*paymenthandler.TenantRevenue, error) {
	return nil, billing_errors.NewNotImplementedError(errors.New("NotImplemented"))
}

func (h *AWSCPPOResellerHandlerImpl) GetMonthlyRevenue(date time.Time) ([]*paymenthandler.TenantRevenue, error) {
	return nil, billing_errors.NewNotImplementedError(errors.New("NotImplemented"))
}

func (h *AWSCPPOResellerHandlerImpl) EnableTaxCalculation() bool {
	return false
}

func (h *AWSCPPOResellerHandlerImpl) EnableHourlySending(tenantID, billingConfigID uint) bool {
	return slices.Contains(config.Get().GetStringSlice(config.HourlyInvoiceTenantBillingConfigIDList), fmt.Sprintf("%d-%d", tenantID, billingConfigID))
}

func (h *AWSCPPOResellerHandlerImpl) EnableDailySending(tenantID, billingConfigID uint) (bool, int) {
	return slices.Contains(config.Get().GetStringSlice(config.DailyInvoiceTenantBillingConfigIDList),
		fmt.Sprintf("%d-%d", tenantID, billingConfigID)), config.Get().GetInt(config.DailyInvoiceIssueDelayDays)
}

func (h *AWSCPPOResellerHandlerImpl) NeedInMonthInvoice() bool {
	return true
}

func (h *AWSCPPOResellerHandlerImpl) NeedSetInvoiceAutoConfirm() bool {
	return true
}
