package plan

import (
	"errors"
	"fmt"
	"slices"

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"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/cachehandler"
	"github.com/pingcap/dbaas/billing2/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/handler/eventbushandler"
	"github.com/pingcap/dbaas/billing2/handler/salesforcehandler"
	"github.com/pingcap/dbaas/billing2/model/billingconfig"
	"github.com/pingcap/dbaas/billing2/model/creditriskcontrol"
	"github.com/pingcap/dbaas/billing2/model/marketing"
	"github.com/pingcap/dbaas/billing2/model/marketing/discount"
	"github.com/pingcap/dbaas/billing2/model/payment/paymentsub"
	"github.com/pingcap/dbaas/billing2/utils/errorutils"
	"github.com/pingcap/dbaas/billing2/utils/jsm"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const TenantRegisteredEventType = "tenant_registered"

const TenantPlanEventType = "tenant_plan"

type TenantPlanModel struct{}

func (m *TenantPlanModel) getTenantEffectivePlan(tenantID uint) (*forms.TenantPlanInfo, error) {
	result := &forms.TenantPlanInfo{TenantID: tenantID, TenantIDStr: tenantID}

	planInfo, err := m.GetTenantDevTierPlan(tenantID)
	if err != nil {
		return nil, err
	}

	result.DevTierPlanInfo = planInfo

	planInfo, err = m.GetTenantPOCPlanInfo(tenantID)
	if err != nil {
		return nil, err
	}

	result.POCPlanInfo = planInfo
	billingConfigID, err := m.getTenantBillingConfigID(tenantID)
	if err != nil && !errors.Is(err, billing_errors.ObjectNotExistsError) {
		log.Error("getTenantEffectivePlan tenant get billing config error", zap.Uint("tenantID", tenantID), zap.Error(err))
		return nil, err
	}
	planInfo, err = m.GetTenantOnDemandPlanInfo(tenantID, billingConfigID)
	if err != nil {
		return nil, err
	}

	result.OnDemandPlanInfo = planInfo

	result.EffectivePlan = m.calcEffectivePlan(result)

	effectiveDiscount, err := (&discount.Model{}).GetTenantOnDemandDiscount(tenantID, billingConfigID)
	if err != nil {
		return nil, err
	}
	result.EffectiveDiscount = effectiveDiscount.Value
	summary, err := (&marketing.CreditsPackageModel{}).GetTenantCreditsSummary(tenantID, billingConfigID)
	if err != nil {
		return nil, err
	}
	result.POCCredits = summary.AvailableRemainingPOCCredits

	return result, nil
}

func (m *TenantPlanModel) calcEffectivePlan(tenantPlan *forms.TenantPlanInfo) (effectivePlan string) {
	effectivePlan = entity.TenantPlan_NewUser
	if tenantPlan.DevTierPlanInfo.Status != entity.TenantPlanStatus_NotStarted {
		effectivePlan = entity.TenantPlan_DevTier
	}
	if tenantPlan.POCPlanInfo.Status != entity.TenantPlanStatus_NotStarted {
		effectivePlan = entity.TenantPlan_POC
	}
	if tenantPlan.OnDemandPlanInfo.Status == entity.TenantPlanStatus_Active {
		effectivePlan = entity.TenantPlan_OnDemand
	}
	return
}

func (*TenantPlanModel) getTenantPlanInfo(tenantID uint, billingConfigID uint, plan string) (*forms.PlanInfo, error) {
	tenantPlanEntity := entity.TenantPlan{}
	var err error
	if billingConfigID == 0 {
		err = errorutils.WrapSQLError(entity.NewTenantPlanQuerySet(db.GetDB()).TenantIDEq(tenantID).PlanEq(plan).One(&tenantPlanEntity), fmt.Sprintf("Tenant %d %s Plan", tenantID, plan))
	} else {
		err = errorutils.WrapSQLError(entity.NewTenantPlanQuerySet(db.GetDB()).TenantIDEq(tenantID).BindedBillingConfigIDEq(billingConfigID).PlanEq(plan).One(&tenantPlanEntity), fmt.Sprintf("Tenant %d %s Plan", tenantID, plan))
	}
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			return &forms.PlanInfo{
				Plan:   plan,
				Status: entity.TenantPlanStatus_NotStarted,
			}, nil
		}
		return nil, err
	}
	return &forms.PlanInfo{
		PlanID:                tenantPlanEntity.ID,
		Plan:                  entity.TenantPlan_DevTier,
		Status:                tenantPlanEntity.Status,
		StartedAt:             tenantPlanEntity.StartedAt,
		ClosedAt:              tenantPlanEntity.ClosedAt,
		BindedPackageID:       tenantPlanEntity.BindedPackageID,
		BindedBillingConfigID: &tenantPlanEntity.BindedBillingConfigID,
	}, nil
}

func (m *TenantPlanModel) GetTenantEffectivePlan(tenantID uint) (*forms.TenantPlanInfo, error) {
	var planInfo forms.TenantPlanInfo
	found := cachehandler.Get().Get(constant.GetTenantEffectivePlanCacheName, &planInfo, tenantID)
	if found {
		return &planInfo, nil
	}
	res, err := m.getTenantEffectivePlan(tenantID)
	if err != nil {
		return nil, err
	}
	closeInfo, err := (&creditriskcontrol.TenantCloseModel{}).GetTenantCloseInfo(tenantID)
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			cachehandler.Get().Set(constant.GetTenantEffectivePlanCacheName, res, tenantID)
			return res, nil
		}
		return nil, err
	}
	if closeInfo.Status == entity.TenantCloseStatus_RUNNING || closeInfo.Status == entity.TenantCloseStatus_DONE {
		res.EffectivePlan = entity.TenantPlan_Closing
	}
	cachehandler.Get().Set(constant.GetTenantEffectivePlanCacheName, res, tenantID)
	return res, nil
}

func (m *TenantPlanModel) setTenantPlanStatus(tenantID uint, billingConfigID uint, plan string, status string) (err error) {
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		return m.setTenantPlanStatusWithTx(tx, tenantID, billingConfigID, plan, status)
	})
	if err != nil {
		return err
	}
	cachehandler.Get().Delete(constant.GetTenantEffectivePlanCacheName, tenantID)
	return nil
}

func (m *TenantPlanModel) setTenantPlanStatusWithTx(tx *gorm.DB, tenantID uint, billingConfigID uint, plan string, status string) (err error) {
	now := timeutils.GetNowAddr()
	var tenantPlanEntity entity.TenantPlan
	err = entity.NewTenantPlanQuerySet(tx).TenantIDEq(tenantID).BindedBillingConfigIDEq(billingConfigID).PlanEq(plan).One(&tenantPlanEntity)
	if err != nil {
		return err
	}
	tenantPlanEntity.Status = status
	switch status {
	case entity.TenantPlanStatus_Closed, entity.TenantPlanStatus_Closing:
		tenantPlanEntity.ClosedAt = now
	case entity.TenantPlanStatus_Active:
		tenantPlanEntity.ClosedAt = nil
		if tenantPlanEntity.StartedAt == nil {
			tenantPlanEntity.StartedAt = now
		}
	}
	err = tenantPlanEntity.Update(tx, entity.TenantPlanDBSchema.ClosedAt, entity.TenantPlanDBSchema.StartedAt, entity.TenantPlanDBSchema.Status)
	if err != nil {
		return err
	}
	planInfo := &forms.PlanInfo{
		PlanID:          tenantPlanEntity.ID,
		Plan:            entity.TenantPlan_DevTier,
		Status:          tenantPlanEntity.Status,
		StartedAt:       tenantPlanEntity.StartedAt,
		ClosedAt:        tenantPlanEntity.ClosedAt,
		BindedPackageID: tenantPlanEntity.BindedPackageID,
	}
	activeBillingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigIDWithTx(tx, tenantID)
	if activeBillingConfigID != billingConfigID {
		log.Info("not active billing config plan changed, do not send event", zap.Uint("activeBillingConfigID", activeBillingConfigID), zap.Uint("billingConfigID", billingConfigID))
		return
	}
	tenantPlan, err := m.getTenantEffectivePlan(tenantID)
	if err != nil {
		return err
	}
	switch plan {
	case entity.TenantPlan_OnDemand:
		tenantPlan.OnDemandPlanInfo = planInfo
	case entity.TenantPlan_POC:
		tenantPlan.POCPlanInfo = planInfo
	case entity.TenantPlan_DevTier:
		tenantPlan.DevTierPlanInfo = planInfo
	}
	effectivePlan := m.calcEffectivePlan(tenantPlan)
	err = eventbushandler.Get().PublishEventWithTransaction(TenantPlanEventType, &eventbushandler.Event{
		EventID: fmt.Sprintf("%d-%s-%s-%d", tenantID, plan, status, now.Unix()),
		EventData: &forms.TenantPlanEvent{
			TenantID:      tenantID,
			ChangedPlan:   plan,
			ChangedStatus: status,
			EffectivePlan: effectivePlan,
		},
	}, tx)
	return err
}

func (m *TenantPlanModel) SendEffectivePlanEvent(tenantID uint) error {
	plan, err := m.getTenantEffectivePlan(tenantID)
	if err != nil {
		return nil
	}
	now := timeutils.GetNow()
	return db.GetDB().Transaction(func(tx *gorm.DB) error {
		err = eventbushandler.Get().PublishEventWithTransaction(TenantPlanEventType, &eventbushandler.Event{
			EventID: fmt.Sprintf("%d-%s-%d", tenantID, plan.EffectivePlan, now.Unix()),
			EventData: &forms.TenantPlanEvent{
				TenantID:      tenantID,
				ChangedPlan:   plan.EffectivePlan,
				ChangedStatus: entity.TenantPlanStatus_Active,
				EffectivePlan: plan.EffectivePlan,
			},
		}, tx)
		return nil
	})
}

func (m *TenantPlanModel) activeTenantPlan(tenantID uint, billingConfigID uint, plan string) (err error) {
	return m.setTenantPlanStatus(tenantID, billingConfigID, plan, entity.TenantPlanStatus_Active)
}

func (m *TenantPlanModel) closeTenantPlan(tenantID uint, billingConfigID uint, plan string) (err error) {
	return m.setTenantPlanStatus(tenantID, billingConfigID, plan, entity.TenantPlanStatus_Closed)
}

func (m *TenantPlanModel) closingTenantPlan(tenantID uint, billingConfigID uint, plan string) (err error) {
	return m.setTenantPlanStatus(tenantID, billingConfigID, plan, entity.TenantPlanStatus_Closing)
}

func (m *TenantPlanModel) RegisterEventHandler() error {
	err := m.RegisterDevTierEventHandler()
	if err != nil {
		return err
	}

	err = m.RegisterPOCEventHandler()
	if err != nil {
		return err
	}

	err = m.RegisterOnDemandEventHandler()
	if err != nil {
		return err
	}

	err = m.RegisterPlanEventHandler()
	if err != nil {
		return err
	}

	return nil
}

func (m *TenantPlanModel) UpdateSalesforceAfterPlanChanged(tenantID uint) error {
	t, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		return err
	}

	plan, err := m.getTenantEffectivePlan(tenantID)
	if err != nil {
		return err
	}
	info, err := (&paymentsub.BillingInformationModel{}).GetBillingInfo(tenantID)
	if err != nil {
		return err
	}

	source := t.ThirdPartyAccountProvider
	billingConfigID, err := m.getTenantBillingConfigID(tenantID)
	payment, err := (&paymentsub.PaymentModel{}).GetActivePaymentMethod(tenantID, billingConfigID)
	allPaymentTypes := []string{entity.PaymentType_STRIPE, entity.PaymentType_WireTransfer, entity.PaymentType_AliCloud}

	if payment == nil || slices.Contains(allPaymentTypes, payment.PaymentType) {
		source = ""
	}

	account := salesforcehandler.Account{
		Plan:             plan.EffectivePlan,
		PortalID:         fmt.Sprint(t.TenantID),
		BillingEmail:     info.BillingEmail,
		BindCreditCardAt: timeutils.GetNow().UTC().Format(salesforcehandler.TimeFormat),
		Source:           source,
	}

	err = salesforcehandler.Get().UpdateAccount(account)
	if err != nil {
		log.Error("failed to call salesforce UpdateAccount API", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}
	log.Info("successfully call salesforce UpdateAccount API", zap.Uint("tenantID", tenantID), zap.Reflect("payload", account))
	return nil
}

func (m *TenantPlanModel) SyncOnDemandOrgsToJSM(eventData *forms.TenantPlanEvent) {
	if eventData.EffectivePlan == entity.TenantPlan_OnDemand {
		log.Info("sync ondemand enable org to jsm", zap.Any("tenantPlan", eventData))
		jsm.SyncOnDemandInfoToJSM(eventData.TenantID, true)
	} else {
		log.Info("sync ondemand disable org to jsm", zap.Any("tenantPlan", eventData))
		jsm.SyncOnDemandInfoToJSM(eventData.TenantID, false)
	}
}

func (m *TenantPlanModel) RegisterPlanEventHandler() error {
	group := "tenant_plan"
	dealWithTenantPlanChangeHandler := func(event *eventbushandler.Event) error {
		m.SyncOnDemandOrgsToJSM(event.EventData.(*forms.TenantPlanEvent))
		return m.UpdateSalesforceAfterPlanChanged(event.EventData.(*forms.TenantPlanEvent).TenantID)
	}
	err := eventbushandler.Get().RegisterEventHandleFunc(TenantPlanEventType, group, forms.TenantPlanEvent{}, dealWithTenantPlanChangeHandler)
	if err != nil {
		return err
	}
	syncEffectivePlan := func(event *eventbushandler.Event) error {
		tenantID := event.EventData.(*forms.BillingConfig).TenantID
		billingConfigs, err := (&billingconfig.BillingConfigModel{}).ListAllBillingConfigs(tenantID)
		if err != nil {
			log.Error("failed to list billing configs", zap.Uint("tenantID", tenantID))
			return err
		}
		if len(billingConfigs) == 1 {
			log.Info("only 1 billing config, skip sync effective plan", zap.Uint("tenantID", tenantID))
			return nil
		}
		return m.SendEffectivePlanEvent(event.EventData.(*forms.BillingConfig).TenantID)
	}
	err = eventbushandler.Get().RegisterEventHandleFunc(constant.BillingConfigSwitchedEvent, group, forms.BillingConfig{}, syncEffectivePlan)
	if err != nil {
		return err
	}
	return nil
}

func (m *TenantPlanModel) getTenantBillingConfigID(tenantID uint) (uint, error) {
	return (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(tenantID)
}
