package impl

import (
	"errors"
	"fmt"
	"time"

	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
	"github.com/alibabacloud-go/marketplaceintl-20221230/client"
	util "github.com/alibabacloud-go/tea-utils/v2/service"
	"github.com/alibabacloud-go/tea/tea"
	"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/centralhandler"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"
	"github.com/pingcap/dbaas/billing2/model/aliyunsubscriptionmngt"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"
	"k8s.io/utils/strings/slices"
)

type AliCloudPaymentHandlerImpl struct{}

func (*AliCloudPaymentHandlerImpl) GetThirdPartyName() string {
	return entity.PaymentType_AliCloud
}

func (*AliCloudPaymentHandlerImpl) Setup(tenant *forms.Tenant) (userCode string, err error) {
	if !config.Get().GetBool(config.AliyunPaymentEnable) {
		return fmt.Sprintf("mock_alicloud_%v", tenant.TenantID), nil
	}
	if tenant.ThirdPartyAccountProvider != constant.AliCloudAccountProvider {
		return "", billing_errors.NewSystemError(errors.New("invalid third_party_account_handler"))
	}
	return tenant.ThirdPartyAccountCode, nil
}

func (*AliCloudPaymentHandlerImpl) Pay(paymentInfo *forms.PaymentMethod, billingCompany *forms.BillingCompany, invoice *forms.Invoice) (*forms.Invoice, error) {
	return nil, billing_errors.NewNotImplementedError(errors.New("can not send aliyun marketplace once per invoice"))
}

func (*AliCloudPaymentHandlerImpl) PayMultiInvoice(paymentInfo *forms.PaymentMethod, billingCompany *forms.BillingCompany, invoices []forms.Invoice) ([]forms.Invoice, error) {
	if !config.Get().GetBool(config.AliyunPaymentEnable) {
		log.Info("unable to push aliyun metering")
		return invoices, nil
	}
	// query bizId by paymentCode (aliUid)
	subscription, err := (&aliyunsubscriptionmngt.AliyunSubscriptionMngt{}).GetLastSubscriptionRecodByAliUid(paymentInfo.PaymentCode)
	if err != nil {
		log.Error("query bizId by aliUid error when push aliyun metering", zap.Error(err))
		return nil, err
	}
	// TODO: can not push metering when instance released for aliyun, it will be implemented in the future
	if !subscription.IsSubscribed {
		log.Info("aliyun instance has been released, it is a supplementary push of metering data", zap.Any("subscription", subscription))
	}
	productCode := subscription.ProductCode
	if productCode == "" {
		productCode = "sgcmgj00035199"
	}
	var accessKeyId, accessKeySecret string
	if productCode == "sgcmgj00036777" {
		accessKeyId = config.Get().GetString(config.AliyunMarketPlaceSgcmgj00036777AccessKeyId)
		accessKeySecret = config.Get().GetString(config.AliyunMarketPlaceSgcmgj00036777AccessKeySecret)
	} else {
		accessKeyId = config.Get().GetString(config.AliyunMarketPlaceAccessKeyId)
		accessKeySecret = config.Get().GetString(config.AliyunMarketPlaceAccessKeySecret)
	}
	if accessKeyId == "" || accessKeySecret == "" {
		log.Error("aliyun marketplace access key is empty", zap.String("productCode", productCode))
		return nil, billing_errors.NewSystemError(errors.New("aliyun marketplace access key is empty"))
	}
	reqClient, err := client.NewClient(&openapi.Config{
		AccessKeyId:     tea.String(accessKeyId),
		AccessKeySecret: tea.String(accessKeySecret),
		RegionId:        tea.String(config.Get().GetString(config.AliyunMarketPlaceRegionId)),
		Endpoint:        tea.String(config.Get().GetString(config.AliyunMarketPlaceEndpoint)),
	})
	if err != nil {
		log.Error("init aliyun request client error", zap.String("productCode", productCode), zap.Error(err))
		return nil, err
	}
	location, err := time.LoadLocation("Asia/Shanghai")
	if err != nil {
		log.Error("failed to load Asia/Shanghai timezone", zap.Error(err))
		return nil, err
	}
	productPriceFactor := config.Get().GetInt64(config.AliyunProductUnitPriceFactor)
	quantity := decimal.Zero
	for _, invoice := range invoices {
		quantity = quantity.Add(invoice.CostPrice.Mul(decimal.NewFromInt(productPriceFactor)))
	}
	meteringEntity := fmt.Sprintf(`{"%s":%d}`, config.Get().GetString(config.AliyunMarketPlaceBillableItem), quantity.IntPart())
	billableItem := config.Get().GetString(config.AliyunMarketPlaceBillableItem)
	meteringAssist := fmt.Sprintf("%s-%s-1", productCode, billableItem)
	now := timeutils.GetNow().In(location)
	endTime := timeutils.GetHourDate(now)
	startTime := endTime.Add(-time.Hour)
	// Aliyun requires (receiveTime - EndTime) <= 1h. When now is near the next
	// hour boundary, network/processing delay may push Aliyun's receive time
	// into the next hour, causing (receiveTime - endTime) > 1h rejection.
	// Skip in that case and retry on the next worker cycle (returning nil,nil
	// leaves invoices unmarked as SENT, so SendAllUnsentInvoice retries them;
	// same pattern as the gcp.go rate-limit skip).
	// safeMargin must be > 0 and < 1h, otherwise it would skip forever;
	// fall back to 5m on misconfiguration.
	safeMargin := resolveAliyunSafeMargin(config.Get().GetDuration(config.AliyunHourBoundarySafeMargin))
	if skip, timeLeft := shouldSkipAliyunHourBoundary(now, safeMargin); skip {
		log.Info("skip aliyun metering push: too close to next hour boundary",
			zap.Time("now", now), zap.Time("endTime", endTime.In(location)),
			zap.Duration("timeLeft", timeLeft), zap.Duration("safeMargin", safeMargin),
			zap.String("paymentCode", paymentInfo.PaymentCode))
		return nil, nil
	}
	pushMeters := &client.PushMeteringDataRequest{
		GmtCreate: tea.String(now.Format("2006-01-02 15:04:05")),
		MeteringData: []*client.PushMeteringDataRequestMeteringData{
			{
				EndTime:        tea.Int64(endTime.UnixMilli()),
				InstanceId:     tea.String(subscription.BizId),
				MeteringAssist: tea.String(meteringAssist),
				MeteringEntity: tea.String(meteringEntity),
				StartTime:      tea.Int64(startTime.UnixMilli()),
			},
		},
	}
	log.Info("params for pushing aliyun metering", zap.Any("params", pushMeters))
	response, err := reqClient.PushMeteringDataWithOptions(pushMeters, &util.RuntimeOptions{Autoretry: tea.Bool(true), MaxAttempts: tea.Int(3)})
	if err != nil {
		log.Error("error to push aliyun metering", zap.Error(err))
		return nil, err
	}
	log.Info("success to push aliyun metering", zap.String("requestId", tea.StringValue(response.Body.RequestId)))
	return invoices, nil
}

func (*AliCloudPaymentHandlerImpl) Payable(tenantID uint, userCode string) (bool, error) {
	tenant, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	return !tenant.ThirdPartyFreeTrial, err
}

func (*AliCloudPaymentHandlerImpl) NeedMultiInvoiceSendOnce() bool {
	return true
}

func (*AliCloudPaymentHandlerImpl) EnableReconciliation() bool {
	// TODO: Auto reconciliation is not supported for aliyun, it will be implemented in the future
	return false
}

func (*AliCloudPaymentHandlerImpl) GetDailyRevenue(date time.Time) ([]*paymenthandler.TenantRevenue, error) {
	// TODO: Auto reconciliation is not supported for aliyun, it will be implemented in the future
	return nil, nil
}

func (*AliCloudPaymentHandlerImpl) GetMonthlyRevenue(date time.Time) ([]*paymenthandler.TenantRevenue, error) {
	// TODO: Auto reconciliation is not supported for aliyun, it will be implemented in the future
	return nil, nil
}

func (*AliCloudPaymentHandlerImpl) EnableTaxCalculation() bool {
	return false
}

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

func (*AliCloudPaymentHandlerImpl) EnableDailySending(tenantID, billingConfigID uint) (bool, int) {
	return true, config.Get().GetInt(config.AliCloudDailyInvoiceIssueDelayDays)
}

func (*AliCloudPaymentHandlerImpl) NeedInMonthInvoice() bool {
	return true
}

func (*AliCloudPaymentHandlerImpl) NeedSetInvoiceAutoConfirm() bool {
	return true
}

// resolveAliyunSafeMargin validates the configured safe margin. It must be
// strictly between 0 and 1h; otherwise it falls back to 5m to avoid skipping
// every push (margin >= 1h) or skipping nothing useful (margin <= 0).
func resolveAliyunSafeMargin(raw time.Duration) time.Duration {
	if raw <= 0 || raw >= time.Hour {
		return 5 * time.Minute
	}
	return raw
}

// shouldSkipAliyunHourBoundary returns true when now is within safeMargin of
// the next hour boundary — the danger zone where network delay can push
// Aliyun's receive time past the 1h-from-endTime limit.
func shouldSkipAliyunHourBoundary(now time.Time, safeMargin time.Duration) (skip bool, timeLeft time.Duration) {
	endTime := timeutils.GetHourDate(now)
	nextHour := endTime.Add(time.Hour)
	timeLeft = nextHour.Sub(now)
	return timeLeft < safeMargin, timeLeft
}
