package creditriskcontrol

import (
	"errors"
	"fmt"
	"strings"
	"time"

	"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/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"
	"github.com/pingcap/dbaas/billing2/model/payment/paymentsub"
	"github.com/pingcap/dbaas/billing2/utils/errorutils"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/shopspring/decimal"
	"github.com/spf13/cast"
	"github.com/stripe/stripe-go/v76"
	"go.uber.org/zap"
)

const REFUND_SUCCESS_STATUS = "succeeded"

type CreditCardVerifyModel struct {
}

func (s *CreditCardVerifyModel) creditCardAddressValidate(tenantID uint, methodInfo *forms.StripePaymentMethod) error {
	doNotValidate, err := (&paymentsub.BillingInformationModel{}).BillingInfoIsNotEmpty(tenantID)
	if err != nil {
		return err
	}
	if doNotValidate {
		return nil
	}
	_, err = (&paymentsub.TaxModel{}).ValidateBillingInfoForTax(&forms.BillingInformation{
		BillingAddress: forms.BillingAddress{
			CountryCode: methodInfo.BillingDetails.Address.Country,
			State:       methodInfo.BillingDetails.Address.State,
			City:        methodInfo.BillingDetails.Address.City,
			Line1:       methodInfo.BillingDetails.Address.Line1,
			Line2:       methodInfo.BillingDetails.Address.Line2,
			PostalCode:  methodInfo.BillingDetails.Address.PostalCode,
		},
	}, "")
	if err != nil {
		return err
	}
	return nil
}

func (s *CreditCardVerifyModel) isSupportedCardBrand(brand string) bool {
	for _, unsupportedCardBrand := range config.Get().GetStringSlice(config.UnsupportedCardBrands) {
		if brand == unsupportedCardBrand {
			return false
		}
	}
	return true
}

func (*CreditCardVerifyModel) IsCreditCardValidationRequired(tenantID uint) bool {
	// main switch
	isAllowed := config.Get().GetBool(config.IsCreditCardValidateAllowed)
	if !isAllowed {
		log.Info("credit card validate forbidden")
		return false
	}
	// white list
	allowedTenants := config.Get().GetString(config.CreditCardValidateAllowedTenants)
	log.Info("credit card validate allowed tenants: ", zap.String("allowedTenants", allowedTenants))
	if allowedTenants == "all" || allowedTenants == "" {
		return true
	}
	tenantList := strings.Split(allowedTenants, "#")
	for _, tenantItem := range tenantList {
		if strings.TrimSpace(tenantItem) == cast.ToString(tenantID) {
			return true
		}
	}
	return false
}

func (s *CreditCardVerifyModel) ComplianceCheck(tenantID uint) error {
	tenant, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		log.Error("failed to get tenant", zap.Error(err))
		return err
	}
	if tenant.Kind == forms.TenantKindInternal && config.Get().GetBool(config.RestrictInternalUserToBindCreditCard) {
		err := errors.New("compliance check failed: internal user are not allowed to bind credit card. If you have any questions, please contact BSS team")
		return billing_errors.NewCreditCardForbiddenError(err)
	}
	restrictedEmailDomains := config.Get().GetStringSlice(config.RestrictedEmailDomainsForBindingCreditCard)
	for _, domain := range restrictedEmailDomains {
		d := domain
		if !strings.HasPrefix(domain, "@") {
			d = fmt.Sprintf("@%s", domain)
		}
		if strings.HasSuffix(strings.ToLower(tenant.Email), strings.ToLower(d)) {
			err := errors.New("compliance check failed: your org are not allowed to bind credit card")
			return billing_errors.NewCreditCardForbiddenError(err)
		}
	}
	return nil
}

// CreditCardValidate :credit card validate
func (s *CreditCardVerifyModel) CreditCardValidate(tenantID uint, billingConfigID uint, paymentMethodID string) (*forms.CardValidateResult, error) {
	// is user's credit card need to be validated
	if !s.IsCreditCardValidationRequired(tenantID) {
		return nil, nil
	}
	err := s.ComplianceCheck(tenantID)
	if err != nil {
		log.Error("compliance check failed", zap.Uint("tenantID", tenantID), zap.Error(err))
		return nil, err
	}
	stripeHandler, customerID, getErr := getStripeProvider(tenantID, billingConfigID)
	if getErr != nil {
		return nil, getErr
	}
	methodInfo, err := stripeHandler.GetPaymentMethod(paymentMethodID)
	if err != nil {
		return nil, err
	}
	if methodInfo.Card == nil {
		return nil, billing_errors.NewInvalidInputError(errors.New("we only support cards as a payment method"))
	}
	if !s.isSupportedCardBrand(methodInfo.Card.Brand) {
		log.Error("unsupported card brand detected", zap.String("brand", methodInfo.Card.Brand))
		return nil, billing_errors.NewInvalidInputError(errors.New(fmt.Sprintf("the card brand: %s has not been supported now", methodInfo.Card.Brand)))
	}
	if config.Get().GetBool(config.EnableTaxCalculation) {
		if err := s.creditCardAddressValidate(tenantID, methodInfo); err != nil {
			return nil, err
		}
	}
	// create and confirm PaymentMethod (charge 100 cent), if success, attach PaymentMethod to Customer
	paymentIntent, queryErr := stripeHandler.Init3DSPaymentIntent(decimal.NewFromInt(100), paymentMethodID, customerID)
	if paymentIntent != nil {
		// create refund record, need cancel PaymentIntent
		sqlErr := s.CreateRefundRecord(entity.CreditCardValidateCancelStatus_Init, tenantID, paymentIntent.ID, decimal.NewFromInt(100))
		if sqlErr != nil {
			return nil, sqlErr
		}
	}
	if queryErr != nil || paymentIntent == nil {
		log.Info("confirm PaymentIntent fail: ", zap.Uint("tenant", tenantID), zap.Error(queryErr))
		return nil, queryErr
	}
	if paymentIntent.Status == stripe.PaymentIntentStatusRequiresAction {
		return &forms.CardValidateResult{
			Status:          string(paymentIntent.Status),
			PaymentIntentID: paymentIntent.ID,
			ClientSecret:    paymentIntent.ClientSecret,
			PublicKey:       config.Get().GetString(config.StripePublicKey),
		}, nil
	}
	return &forms.CardValidateResult{
		Status: string(paymentIntent.Status),
	}, nil
}

// RefundForCreditCardVerify :background worker task
func (s *CreditCardVerifyModel) RefundForCreditCardVerify() error {
	// limit the maximum number of every single processing
	refundRecords, err := s.QueryUnfinishedRefundRecordsWithLimit(5)
	if err != nil {
		return err
	}
	log.Info(fmt.Sprintf("the length of refunds list: %d", len(refundRecords)))
	if len(refundRecords) == 0 {
		return nil
	}
	handler, _ := paymenthandler.GetPaymentHandler().GetProvider(entity.PaymentType_STRIPE)
	stripeHandler, _ := handler.(paymenthandler.StripePaymentHandler)
	for _, record := range refundRecords {
		// dont use Goroutine
		paymentIntent, err := stripeHandler.GetPaymentIntent(record.PaymentIntentID)
		if err != nil {
			log.Error("failed to retrieve payment intent", zap.String("payment intent_id", record.PaymentIntentID))
			continue
		}
		log.Info("retrieve payment intent", zap.Any("paymentIntent", paymentIntent))
		switch paymentIntent.Status {
		case stripe.PaymentIntentStatusRequiresAction:
			log.Info("refund requires action, waiting", zap.String("payment intent_id", record.PaymentIntentID), zap.Time("createdAt", record.CreatedAt))
			if record.CreatedAt.Add(config.Get().GetDuration(config.WaitingCustomerActionDurationSeconds) * time.Second).Before(timeutils.GetNow()) {
				log.Info("requires action timeout", zap.String("payment_intent_id", record.PaymentIntentID))
				s.cancelPaymentIntent(record, stripeHandler)
			} else {
				log.Info("requires action, let us wait customer action", zap.String("payment_intent_id", record.PaymentIntentID))
			}
		case stripe.PaymentIntentStatusRequiresConfirmation:
			provider, err := paymenthandler.GetStripeProvider()
			if err != nil {
				return err
			}
			err = provider.ConfirmRequiredConfirmedPaymentIntent(record.PaymentIntentID)
			if err != nil {
				return err
			}
			continue
		case stripe.PaymentIntentStatusProcessing:
			log.Info("payment processing", zap.String("payment_intent_id", record.PaymentIntentID))
		case stripe.PaymentIntentStatusSucceeded:
			s.dealWithRefund(record, stripeHandler)
		default:
			log.Info("refund record do not need to do any action",
				zap.String("payment_intent_id", record.PaymentIntentID),
				zap.String("status", string(paymentIntent.Status)),
			)
			err = entity.NewCreditCardValidateRefundRecordQuerySet(db.GetDB()).IDEq(record.ID).GetUpdater().SetStatus(entity.CreditCardValidateCancelStatus_NoAction).Update()
			if err != nil {
				log.Error("failed to update refund record", zap.Error(err))
			}
		}
	}
	return nil
}

// RefundCheckForCreditCardVerify :check refund status after refund success
func (s *CreditCardVerifyModel) RefundCheckForCreditCardVerify() {
	refundRecords, err := s.QuerySuccessfulRefundRecords()
	if err != nil {
		log.Error("query successful refund records error: ", zap.Error(err))
		return
	}
	log.Info(fmt.Sprintf("the length of succesful refunds list: %d", len(refundRecords)))
	if len(refundRecords) == 0 {
		return
	}
	for _, record := range refundRecords {
		// check refund status
		s.checkRefundStatusAndAlert(record)
	}
}

func (*CreditCardVerifyModel) CreateRefundRecord(status string, tenantID uint, paymentIntentId string, refundPrice decimal.Decimal) error {
	refundRecord := &entity.CreditCardValidateRefundRecord{
		TenantID:        tenantID,
		RefundPrice:     refundPrice,
		PaymentIntentID: paymentIntentId,
		Status:          status,
		RetryCount:      0,
	}
	sqlErr := errorutils.WrapSQLError(refundRecord.Create(db.GetDB()), "create refund record error")
	if sqlErr != nil {
		return sqlErr
	}
	return nil
}

func (*CreditCardVerifyModel) QueryUnfinishedRefundRecordsWithLimit(limit int) ([]entity.CreditCardValidateRefundRecord, error) {
	var refundRecords []entity.CreditCardValidateRefundRecord
	err := errorutils.WrapSQLError(entity.NewCreditCardValidateRefundRecordQuerySet(db.GetDB()).
		StatusNe(entity.CreditCardValidateRefundStatus_Success).
		StatusNe(entity.CreditCardValidateCancelStatus_Success).
		StatusNe(entity.CreditCardValidateCancelStatus_NoAction).
		RetryCountLt(config.Get().GetUint(config.RefundMaxRetryCount)).
		OrderDescByCreatedAt().
		Limit(limit).
		All(&refundRecords),
		"query all unfinished refund records error")
	if err != nil {
		return nil, err
	}
	return refundRecords, nil
}

func (*CreditCardVerifyModel) QuerySuccessfulRefundRecords() ([]entity.CreditCardValidateRefundRecord, error) {
	var refundRecords []entity.CreditCardValidateRefundRecord
	// only query records in 7days
	currentTime := time.Now()
	err := errorutils.WrapSQLError(entity.NewCreditCardValidateRefundRecordQuerySet(db.GetDB()).
		StatusEq(entity.CreditCardValidateRefundStatus_Success).
		CreatedAtGt(currentTime.AddDate(0, 0, -7)).
		All(&refundRecords),
		"query all successful refund records error")
	if err != nil {
		return nil, err
	}
	return refundRecords, nil
}

func (*CreditCardVerifyModel) UpdateStatusAndAddRefundRetryCount(status string, refundId string, refundItem entity.CreditCardValidateRefundRecord) error {
	err := errorutils.WrapSQLError(entity.NewCreditCardValidateRefundRecordQuerySet(db.GetDB()).
		PaymentIntentIDEq(refundItem.PaymentIntentID).
		GetUpdater().
		SetRetryCount(refundItem.RetryCount+uint(1)).
		SetStatus(status).
		SetRefundID(refundId).
		Update(),
		"update refund record error")
	if err != nil {
		return err
	}
	return nil
}

func (s *CreditCardVerifyModel) cancelPaymentIntent(record entity.CreditCardValidateRefundRecord, stripeHandler paymenthandler.StripePaymentHandler) {
	err := stripeHandler.CancelPaymentIntent(record.PaymentIntentID)
	if err != nil {
		log.Info("cancel PaymentIntent error: ", zap.Uint("tenantID", record.TenantID), zap.String("PaymentIntentId", record.PaymentIntentID), zap.Error(err))
		if record.RetryCount+1 >= config.Get().GetUint(config.RefundMaxRetryCount) {
			// log error to trigger alter
			log.Error(fmt.Sprintf("cancel PaymentIntent failed：%+v", record))
		}
		sqlErr := s.UpdateStatusAndAddRefundRetryCount(entity.CreditCardValidateCancelStatus_Fail, "", record)
		if sqlErr != nil {
			log.Error("cancel PaymentIntent fail and update record error:", zap.Uint("tenantID", record.TenantID), zap.Error(err))
		}
		return
	}
	sqlErr := s.UpdateStatusAndAddRefundRetryCount(entity.CreditCardValidateCancelStatus_Success, "", record)
	if sqlErr != nil {
		log.Error("cancel PaymentIntent success and update record error:", zap.Uint("tenantID", record.TenantID), zap.Error(err))
	}
}

func (s *CreditCardVerifyModel) dealWithRefund(record entity.CreditCardValidateRefundRecord, stripeHandler paymenthandler.StripePaymentHandler) {
	refundID, err := stripeHandler.RefundBackground(record.PaymentIntentID, record.RefundPrice)
	if err != nil {
		log.Info("background refund error: ", zap.Uint("tenantID", record.TenantID), zap.String("PaymentIntentId", record.PaymentIntentID), zap.Error(err))
		if record.RetryCount+1 >= config.Get().GetUint(config.RefundMaxRetryCount) {
			// log error to trigger alter
			log.Error(fmt.Sprintf("refund failed：%+v", record))
		}
		sqlErr := s.UpdateStatusAndAddRefundRetryCount(entity.CreditCardValidateRefundStatus_Fail, "", record)
		if sqlErr != nil {
			log.Error("refund fail and update record error:", zap.Uint("tenantID", record.TenantID), zap.Error(err))
		}
		return
	}
	sqlErr := s.UpdateStatusAndAddRefundRetryCount(entity.CreditCardValidateRefundStatus_Success, refundID, record)
	if sqlErr != nil {
		log.Error("refund success and update record error:", zap.Uint("tenantID", record.TenantID), zap.Error(err))
	}
}

func (s *CreditCardVerifyModel) checkRefundStatusAndAlert(record entity.CreditCardValidateRefundRecord) {
	handler, _ := paymenthandler.GetPaymentHandler().GetProvider(entity.PaymentType_STRIPE)
	stripeHandler, _ := handler.(paymenthandler.StripePaymentHandler)
	// check refund status
	refundStatus, err := stripeHandler.QueryRefundStatus(record.RefundID)
	if err != nil {
		log.Error("query refund status error:", zap.Error(err))
		return
	}
	// print log
	log.Info(fmt.Sprintf("query refund status success, refundID = %s, refundStatus = %s", record.RefundID, refundStatus))
	// refund status changed,trigger alert
	if refundStatus != REFUND_SUCCESS_STATUS {
		log.Error("refund status for card verify is still pending or changed: ",
			zap.Uint("tenantID", record.TenantID), zap.String("refundId", record.RefundID))
	}
}

func getStripeProvider(tenantID uint, billingConfigID uint) (paymenthandler.StripePaymentHandler, string, error) {
	paymentInfo, err := (&paymentsub.PaymentModel{}).GetActivePaymentMethod(tenantID, billingConfigID)
	if err != nil {
		return nil, "?", err
	}
	if paymentInfo.PaymentType != entity.PaymentType_STRIPE {
		return nil, "", billing_errors.NewInvalidInputError(fmt.Errorf("invalid payment type %s", paymentInfo.PaymentType))
	}
	handler, err := paymenthandler.GetPaymentHandler().GetProvider(entity.PaymentType_STRIPE)
	if err != nil {
		return nil, "", err
	}
	return handler.(paymenthandler.StripePaymentHandler), paymentInfo.PaymentCode, nil
}
