package paymentsub

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

	"github.com/pingcap/dbaas/billing2/model/costexplorer"
	"github.com/pingcap/dbaas/billing2/utils/httpclient"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"

	"github.com/pingcap/dbaas/billing2/utils/timeutils"

	"github.com/pingcap/dbaas/billing2/handler/eventbushandler"

	"github.com/google/uuid"
	"gorm.io/gorm"

	"github.com/pingcap/dbaas/billing2/handler/cloudmarketplacehandler"

	"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"
	"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/model/aliyunsubscriptionmngt"
	"github.com/pingcap/dbaas/billing2/model/billingconfig"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"go.uber.org/zap"
)

var unknownProvider = errors.New("unknown provider")

func AccountProvider2PaymentType(provider string) string {
	var paymentType string
	switch provider {
	case constant.AwsAccountProvider:
		paymentType = entity.PaymentType_AWS
	case constant.GCPAccountProvider:
		paymentType = entity.PaymentType_GCP
	case constant.MSPAccountProvider:
		paymentType = entity.PaymentType_MSP
	case constant.AliCloudAccountProvider:
		paymentType = entity.PaymentType_AliCloud
	case constant.AzureAccountProvider:
		paymentType = entity.PaymentType_AZURE
	case "mock":
		paymentType = "mock"
		log.Error("mock payment found")
	default:
		err := unknownProvider
		log.Error(err.Error(), zap.Error(err), zap.String("provider", provider))
	}
	return paymentType
}

func (m *PaymentModel) CreateOrReplacePaymentMethodAfterMarketplaceBind(tenantID uint) error {
	tenantInfo, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		return err
	}
	if tenantInfo.IsCPPOCustomer || tenantInfo.IsMspCustomer || tenantInfo.IsMCPOCustomer || tenantInfo.IsResaleCustomer {
		log.Info("ignore processing on tenant is CPPOCustomer or MspCustomer", zap.Uint("tenantID", tenantID))
		return nil
	}
	if tenantInfo.ThirdPartyAccountProvider == constant.AliCloudAccountProvider {
		return m.OnOrgSubscribedThroughAliCloud(tenantInfo)
	}
	if tenantInfo.ThirdPartyAccountProvider == "" {
		log.Info("third party account provider is empty", zap.Uint("tenantID", tenantID))
		return nil
	}
	paymentType := AccountProvider2PaymentType(tenantInfo.ThirdPartyAccountProvider)
	if paymentType == "" {
		return nil
	}
	allPaymentTypes := []string{entity.PaymentType_AWS, entity.PaymentType_GCP, entity.PaymentType_AliCloud, entity.PaymentType_AZURE}

	for _, pt := range allPaymentTypes {
		if pt == paymentType {
			continue
		}
		conflictedPayment, err := m.GetPaymentMethodWithPaymentTypeCurrentBillingConfig(tenantID, pt)
		if err == nil {
			err := errors.New(fmt.Sprintf("conflicted payments, tenant have %s payment, can not create %s payment", pt, paymentType))
			log.Error(err.Error(), zap.Error(err), zap.Uint("conflictedPaymentID", conflictedPayment.PaymentMethodID), zap.String("targetPaymentType", paymentType), zap.String("conflictedPaymentType", pt))
			return nil
		} else if !errors.Is(err, billing_errors.ObjectNotExistsError) {
			return err
		}
	}
	paymentInfo, err := m.GetPaymentMethodWithPaymentTypeCurrentBillingConfig(tenantID, paymentType)
	if err != nil {
		if !errors.Is(err, billing_errors.ObjectNotExistsError) {
			return err
		}
		log.Info("tenant do not have that payment method, create it", zap.Uint("tenantID", tenantID), zap.String("paymentType", paymentType))
		billingConfigType := entity.BillingConfigType_DirectCustomer
		if tenantInfo.IsMsp {
			billingConfigType = entity.BillingConfigType_MSP
		}
		billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetBillingConfigIDWithType(tenantID, billingConfigType)
		if err != nil {
			log.Error("tenant get billing config  error", zap.Uint("tenantID", tenantID), zap.String("paymentType", paymentType), zap.String("billingConfigType", billingConfigType), zap.Error(err))

			return err
		}
		_, err = m.CreatePaymentMethodAndActive(tenantID, &forms.PaymentMethodCreateQuery{
			PaymentType:           paymentType,
			BindedBillingConfigID: &billingConfigID,
		})
		if err != nil {
			log.Error("CreatePaymentMethodAndActive error", zap.Uint("tenantID", tenantID), zap.String("paymentType", paymentType), zap.String("billingConfigType", billingConfigType), zap.Error(err))
			return err
		}
		log.Info("success to create payment method after bind marketplace", zap.Uint("tenantID", tenantID), zap.String("paymentType", paymentType), zap.String("billingConfigType", billingConfigType))
	} else {
		log.Info("tenant have that payment method, try to sync",
			zap.Uint("tenantID", tenantID), zap.String("paymentType", paymentType), zap.Uint("paymentID", paymentInfo.PaymentMethodID),
		)
		return m.syncPaymentMethod(tenantID, paymentInfo.PaymentMethodID)
	}

	return nil
}

func (m *PaymentModel) MarkMarketplacePaymentMethodUnsubscribed(tenantID uint) (err error) {
	tenantInfo, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		return err
	}
	if tenantInfo.ThirdPartyAccountProvider == "" {
		log.Info("third party account provider is empty", zap.Uint("tenantID", tenantID))
		return nil
	}
	paymentType := AccountProvider2PaymentType(tenantInfo.ThirdPartyAccountProvider)
	if paymentType == "" {
		return nil
	}
	payments, err := m.GetPaymentMethodListWithPaymentCode(paymentType, tenantInfo.ThirdPartyAccountCode)
	if err != nil {
		return err
	}
	if len(payments) == 0 && paymentType == entity.PaymentType_AZURE {
		payments, err = m.GetPaymentMethodWithPaymentTypeEntity(tenantID, entity.PaymentType_AZURE)
		if err != nil {
			return err
		}
	}
	if len(payments) == 0 {
		log.Info("no payment found", zap.String("paymentCode", tenantInfo.ThirdPartyAccountCode))
	}
	for _, p := range payments {
		if slices.Contains(config.Get().GetStringSlice(config.AWSProduct1DollarUnitBlacklist), p.PaymentCode) {
			log.Error("a unsubscribed customer on AWSProduct1DollarUnitBlacklist", zap.String("paymentCode", p.PaymentCode))
		}
		err := m.syncPaymentMethod(p.TenantID, p.ID)
		if err != nil {
			log.Error("sync payment error on unsubscribed", zap.Error(err), zap.Uint("paymentID", p.ID))
		}
	}
	return
}

func (m *PaymentModel) OnOrgSubscribedThroughAliCloud(tenant *forms.Tenant) error {
	if tenant.ThirdPartyAccountProvider != constant.AliCloudAccountProvider {
		log.Info("skip processing because the org is not the alibaba cloud provider", zap.Uint("tenantID", tenant.TenantID), zap.String("thirdPartyAccountProvider", tenant.ThirdPartyAccountProvider))
		return nil
	}
	isThirdParty := m.isAliCloudThirdPartyByAliUid(tenant.ThirdPartyAccountCode)
	log.Info("alicloud subscription check", zap.Uint("tenantID", tenant.TenantID), zap.String("aliUid", tenant.ThirdPartyAccountCode), zap.Bool("isThirdParty", isThirdParty))
	billingConfigModel := &billingconfig.BillingConfigModel{}
	billingConfig, err := billingConfigModel.GetActiveBillingConfig(tenant.TenantID)
	if err != nil {
		if !errors.Is(err, billing_errors.ObjectNotExistsError) {
			log.Error("tenant get billing config  error", zap.Uint("tenantID", tenant.TenantID), zap.Error(err))
			return err
		}
	}
	targetBillingConfigType := entity.BillingConfigType_AlicloudCustomer
	if isThirdParty {
		targetBillingConfigType = entity.BillingConfigType_DirectCustomer
	}
	alicloudBillingConfigID, err := billingConfigModel.GetBillingConfigIDWithType(tenant.TenantID, targetBillingConfigType)
	if err != nil {
		if !errors.Is(err, billing_errors.ObjectNotExistsError) {
			log.Error("tenant get billing config  error", zap.Uint("tenantID", tenant.TenantID), zap.Error(err))
			return err
		}
	}
	var alicloudBillingConfig *forms.BillingConfig
	if alicloudBillingConfigID != 0 {
		alicloudBillingConfig, err = billingConfigModel.GetBillingConfig(alicloudBillingConfigID)
		if err != nil {
			return err
		}
		if alicloudBillingConfig.Status != entity.BillingConfigStatus_Active && alicloudBillingConfig.Status != entity.BillingConfigStatus_Scheduled {
			log.Error("invalid alicloud billing config status", zap.Uint("tenantID", tenant.TenantID), zap.Uint("billingConfigID", alicloudBillingConfigID), zap.String("alicloudBillingConfigStatus", alicloudBillingConfig.Status))
			return nil
		}
	} else if billingConfig == nil {
		log.Info("tenant is new customer, should to create alicloud billing config", zap.Uint("tenantID", tenant.TenantID))
		effectiveDate := timeutils.GetNow()
		alicloudBillingConfig, err = (&billingconfig.BillingConfigModel{}).CreateAlicloudCustomerBillingConfig(tenant.TenantID, effectiveDate)
		if err != nil {
			return err
		}
	} else {
		log.Error("customer is not validated for alicloud but subscribed, check it in time", zap.Uint("tenantID", tenant.TenantID), zap.String("ActiveBillingConifgIDType", billingConfig.Type), zap.Uint("ActiveBillingConifgID", billingConfig.BillingConfigID))
		return nil
	}
	paymentInfo, err := m.GetPaymentMethodWithPaymentType(tenant.TenantID, alicloudBillingConfig.BillingConfigID, entity.PaymentType_AliCloud)
	if err != nil {
		if !errors.Is(err, billing_errors.ObjectNotExistsError) {
			log.Error("failed to get alicloud payment info", zap.Uint("tenantID", tenant.TenantID), zap.Error(err))
			return err
		}
		isTP := isThirdParty
		paymentInfo, err = m.CreatePaymentMethodAndActive(tenant.TenantID, &forms.PaymentMethodCreateQuery{
			PaymentType:           entity.PaymentType_AliCloud,
			BindedBillingConfigID: &alicloudBillingConfig.BillingConfigID,
			PaymentMeta:           forms.PaymentMeta{IsThirdParty: &isTP},
		})
		if err != nil {
			log.Error("failed to create alicloud payment info", zap.Uint("tenantID", tenant.TenantID), zap.Uint("billingConfigID", alicloudBillingConfig.BillingConfigID))
			return err
		}
		log.Info("create alicloud payment info", zap.Uint("tenantID", tenant.TenantID), zap.Uint("billingConfigID", alicloudBillingConfig.BillingConfigID), zap.Uint("paymentID", paymentInfo.PaymentMethodID))
	}
	return m.SyncPayableByBillingConfigID(tenant.TenantID, alicloudBillingConfig.BillingConfigID)
}

func (m *PaymentModel) isAliCloudThirdPartyByAliUid(aliUid string) bool {
	subscription, err := (&aliyunsubscriptionmngt.AliyunSubscriptionMngt{}).GetLastSubscriptionRecodByAliUid(aliUid)
	if err != nil {
		log.Error("failed to get subscription record for alicloud", zap.String("aliUid", aliUid), zap.Error(err))
		return true
	}
	return subscription.IsThirdParty
}

func (m *PaymentModel) PaymentWorker(paymentType string) error {
	payments, err := m.GetAllActivePaymentsWithType(entity.PaymentType_AZURE)
	if err != nil {
		return err
	}
	if len(payments) == 0 {
		return nil
	}
	handler, err := cloudmarketplacehandler.GetMarketplaceHandler().GetMarketplace(entity.PaymentType_AZURE)
	if err != nil {
		log.Error("Failed to get Azure marketplace handler", zap.Error(err))
		return err
	}

	subscriptionList, err := handler.GetSubscriptionList()
	if err != nil {
		log.Error("AzureReplaceMarketplaceOffer GetSubscriptionByMarketplaceID :", zap.Error(err))
		return err
	}
	subscribedMap := make(map[string]map[string]forms.SubscriptionList)

	for _, subscription := range subscriptionList.Subscriptions {
		status := subscription.SaasSubscriptionStatus

		if subscribedMap[status] == nil {
			subscribedMap[status] = make(map[string]forms.SubscriptionList)
		}
		subscribedMap[status][subscription.Id] = forms.SubscriptionList{
			Subscriptions: append(subscribedMap[status][subscription.Id].Subscriptions, subscription),
		}
	}

	for _, payment := range payments {
		if !payment.Payable {
			continue
		}
		// no Subscribed status.
		_, isGetUnsub := subscribedMap[constant.AzureSubscriptionStatusUnsubscribed][payment.PaymentCode]
		if isGetUnsub {
			log.Info("Subscription no subscribed ,  processUnsubscribed", zap.Error(err), zap.Uint("paymentID", payment.PaymentMethodID))
			err = m.processSubscribed(payment.TenantID, "unsubscribe", payment.PaymentCode)
			if err != nil {
				log.Error("PaymentWorker processSubscribed err :", zap.Error(err), zap.Uint("tenantID", payment.TenantID))
				return err
			}

		}

	}
	return nil
}

type SubscribeEvent struct {
	TenantID uint   `json:"tenant_id"`
	Action   string `json:"action"`
}

func (m *PaymentModel) processSubscribed(tenantID uint, action string, paymentCode string) error {
	err := db.GetDB().Transaction(func(tx *gorm.DB) error {

		err := eventbushandler.Get().PublishEventWithTransaction(TenantThirdPartyNormalUnsubscribed, &eventbushandler.Event{
			EventID:   uuid.NewString(),
			EventType: TenantThirdPartyNormalUnsubscribed,
			EventData: SubscribeEvent{
				TenantID: tenantID,
				Action:   action,
			},
		}, tx)
		if err != nil {
			return err
		}
		tenantInfo, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
		if err != nil {
			return err
		}
		input := SubscribeChange{
			AccountProvider: constant.AzureAccountProvider,
			AccountCode:     paymentCode,
		}
		// send an email
		err = notifyOpsAndRD(tenantInfo, input, action)
		if err != nil {
			return err
		}
		provider := strings.ToUpper(strings.Split(input.AccountProvider, "_")[0])
		err = sendUnsubEmail(tenantInfo, provider)
		if err != nil {
			return err
		}

		return nil
	})
	return err
}
func GetClient() httpclient.HTTPClient {
	return httpclient.NewClient(nil, "http://devops-server.devops:8088/api/v1/devops-internal/tenants/", "")
}

type SubscribeChange struct {
	AccountProvider string
	AccountCode     string
	SubscribeStatus string
	FreeTrial       bool
}

const TemplateID_TNCustomerUpdateSubscribeStatusChanged = "d-3b68c82c9bd44475ae2d84594303a70e"
const TemplateID_TNMarketplaceUnsubscriptionDetected = "d-9cd557149c3646cf94bf9d71677f2119"

func notifyOpsAndRD(tenantInfo *forms.Tenant, input SubscribeChange, reason string) error {
	var toEmails []string
	if config.Get().Get(config.InvoiceNotifyEmail) != "" {
		toEmails = []string{config.Get().GetString(config.InvoiceNotifyEmail)}
	}

	outPut := map[string]interface{}{}
	tenantID := tenantInfo.TenantID
	err := GetClient().Get(strconv.FormatUint(uint64(tenantID), 10), nil, &outPut)
	if err == nil {
		if managerI, ok := outPut["sales_owner_email"]; ok {
			if managerEmail, ok2 := managerI.(string); ok2 {
				toEmails = append(toEmails, managerEmail)
			}
		}
	}

	lastBillsKey, currentBillsKey := "lastMonthBills", "currentMonthBills"
	vars := map[string]string{
		"accountProvider": input.AccountProvider,
		"accountCode":     input.AccountCode,
		"subscribeStatus": input.SubscribeStatus,
		"freeTrial":       fmt.Sprint(input.FreeTrial),
		"reason":          reason,
		"orgID":           fmt.Sprint(tenantID),
		"orgName":         tenantInfo.Name,
		lastBillsKey:      "Unknown",
		currentBillsKey:   "Unknown",
	}
	now := time.Now().UTC()
	for offset, key := range []string{currentBillsKey, lastBillsKey} {
		dt := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC).AddDate(0, -offset, 0)
		model := &costexplorer.CostExplorerModel{}
		billingConfigModel := &billingconfig.BillingConfigModel{}
		billingConfig, err := billingConfigModel.GetActiveBillingConfig(tenantID)
		monthBills, err := model.GetMonthlyCostWithCache(tenantID, billingConfig.BillingConfigID, dt.Year(), int(dt.Month()))
		if err != nil {
			log.Error("failed to get month bills on customer unsubscribed", zap.Error(err), zap.Uint("tenantID", tenantID), zap.Time("dt", dt))
			continue
		}
		vars[key] = monthBills.InvoiceCostsCosts.CostPrice.String()
	}

	ccEmails := []string{
		"yichao.zhang@pingcap.com",
		"liqing.zhu@pingcap.com",
		"suhaihuang@pingcap.com",
	}
	env := config.Get().GetString(config.ENV)

	err = centralhandler.GetCentralHandler().SendEmail(&account.SendEmailReq{
		OrgId:        uint64(tenantID),
		Scene:        "customer_status_update",
		Identifier:   uuid.New().String(),
		Subject:      "Customer status update" + env,
		Emails:       toEmails,
		CcEmails:     ccEmails,
		TemplateId:   TemplateID_TNCustomerUpdateSubscribeStatusChanged,
		TemplateVars: vars,
	})

	if err != nil {
		return fmt.Errorf("send mail to ops failed: %w", err)
	}

	return nil
}

func sendUnsubEmail(tenant *forms.Tenant, provider string) error {

	suspendDate := time.Now().AddDate(0, 0, 3).Format("2006-01-02")
	vars := map[string]string{
		"provider":    provider,
		"orgID":       fmt.Sprint(tenant.TenantID),
		"suspendDate": suspendDate,
	}

	err := centralhandler.GetCentralHandler().SendTenantEmailWithLabelOnce(tenant.TenantID, uuid.New().String(),
		TemplateID_TNMarketplaceUnsubscriptionDetected,
		vars,
		account.MailLabels_MAIL_LABELS_ORG_OWNER,
		[]account.MailLabels{account.MailLabels_MAIL_LABELS_ORG_BILLING_ADMINS},
	)

	if err != nil {
		return fmt.Errorf("send mail to customer failed: %w", err)
	}

	return nil
}
