package control

import (
	"strings"

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

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/model/billingconfig"
	"github.com/pingcap/dbaas/billing2/model/marketing"
	"github.com/pingcap/dbaas/billing2/model/marketing/plan"
	"github.com/pingcap/dbaas/billing2/model/payment/paymentsub"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pkg/errors"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"
)

type Model struct{}

const (
	NotMatchedProviderMsg              = "You cannot perform that action because your payment method restricts the creation of resources on this cloud provider."
	UnpayableMsg                       = "You cannot perform that action because your payment cannot be processed."
	NoPOCCreditsMsg                    = "You cannot perform that action because your PoC credits is exhausted."
	OrgSuspendMsg                      = "You cannot perform that action because your Org is is suspended."
	ServerlessAlicloudNonThirdPartyMsg = "You cannot perform that action because serverless cluster on Alicloud is only available for third-party accounts."
)

func (m *Model) paymentType2Provider(paymentType string) forms.ProviderEnum {
	provider := forms.Provider_All
	switch paymentType {
	case entity.PaymentType_AWS, entity.PaymentType_AWSCPPOTransfer:
		provider = forms.Provider_AWS
	case entity.PaymentType_GCP, entity.PaymentType_GCPMCPOTransfer:
		provider = forms.Provider_GCP
	case entity.PaymentType_AZURE:
		provider = forms.Provider_Azure
	case entity.PaymentType_AliCloud:
		provider = forms.Provider_AliCloud
	}
	return provider
}

func (m *Model) getThirdPartyProviderFromAccount(tenantID uint) (forms.ProviderEnum, error) {
	if !config.Get().GetBool(config.EnableThirdPartyProviderFromAccountCheck) {
		return forms.Provider_All, nil
	}
	tenant, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		log.Info("failed get tenant on action check")
		return "", err
	}
	if tenant.ThirdPartyAccountProvider == "" {
		return forms.Provider_All, nil
	}
	paymentType := paymentsub.AccountProvider2PaymentType(tenant.ThirdPartyAccountProvider)
	if paymentType == "" {
		log.Info("invalid third party account provider", zap.String("provider", tenant.ThirdPartyAccountProvider))
		return forms.Provider_All, nil
	}
	return m.paymentType2Provider(paymentType), nil
}

func (m *Model) providerMatchedCheck(actionInfo *forms.BillingActionInfo, provider forms.ProviderEnum) (*forms.BillingActionCheckResult, error) {
	if !config.Get().GetBool(config.EnableActionCheckProviderMatched) {
		log.Info("skip providerMatchedCheck")
		return &forms.BillingActionCheckResult{
			Pass: true,
		}, nil
	}
	if (actionInfo.Action == forms.BillingAction_Create || actionInfo.Action == forms.BillingAction_View) && actionInfo.Resource.Provider != nil && provider != forms.Provider_All && strings.ToUpper(string(provider)) != strings.ToUpper(string(*actionInfo.Resource.Provider)) {
		reason := NotMatchedProviderMsg
		log.Info("not matched actionInfo.Resource.Provider", zap.String("actionInfo.Resource.Provider", string(*actionInfo.Resource.Provider)))
		return &forms.BillingActionCheckResult{
			Pass:   false,
			Reason: &reason,
		}, nil
	}
	return &forms.BillingActionCheckResult{
		Pass: true,
	}, nil
}

func (m *Model) billingActionProviderMatchedCheck(tenantID uint, billingConfigID uint, actionInfo *forms.BillingActionInfo) (*forms.BillingActionCheckResult, error) {
	if !config.Get().GetBool(config.EnableActionCheckProviderMatched) {
		return &forms.BillingActionCheckResult{
			Pass: true,
		}, nil
	}
	paymentMethod, err := (&paymentsub.PaymentModel{}).GetActivePaymentMethod(tenantID, billingConfigID)
	if err != nil {
		if !errors.Is(err, billing_errors.ObjectNotExistsError) {
			return nil, err
		}
	}
	provider := forms.Provider_All
	if paymentMethod != nil {
		provider = m.paymentType2Provider(paymentMethod.PaymentType)
	} else {
		provider, err = m.getThirdPartyProviderFromAccount(tenantID)
		if err != nil {
			log.Error("failed to get third party provider from account", zap.Error(err), zap.Uint("tenantID", tenantID))
			return nil, err
		}
	}
	if (actionInfo.Resource.Type == forms.BillingResourceType_ServerlessCluster || actionInfo.Resource.Type == forms.BillingResourceType_StarterCluster) &&
		((actionInfo.Resource.Provider != nil && *actionInfo.Resource.Provider == forms.Provider_AliCloud) || provider == forms.Provider_AliCloud) {
		return m.serverlessAlicloudProviderCheck(paymentMethod, tenantID, actionInfo)
	}
	return m.providerMatchedCheck(actionInfo, provider)
}

func (m *Model) serverlessAlicloudProviderCheck(paymentMethod *forms.PaymentMethod, tenantID uint, actionInfo *forms.BillingActionInfo) (*forms.BillingActionCheckResult, error) {
	isThirdParty := false
	if paymentMethod != nil && paymentMethod.PaymentMeta.IsThirdParty != nil {
		isThirdParty = *paymentMethod.PaymentMeta.IsThirdParty
	}
	if isThirdParty {
		log.Info("serverless alicloud third party check passed", zap.Uint("tenantID", tenantID))
		return &forms.BillingActionCheckResult{Pass: true}, nil
	}
	log.Info("serverless alicloud third party check failed", zap.Uint("tenantID", tenantID))
	reason := ServerlessAlicloudNonThirdPartyMsg
	return &forms.BillingActionCheckResult{Pass: false, Reason: &reason}, nil
}

func (m *Model) BillingActionCheckOnBillable(tenantID uint, billingConfigID uint, actionInfo *forms.BillingActionInfo) (*forms.BillingActionCheckResult, error) {
	if !config.Get().GetBool(config.EnableActionCheckOnBillable) {
		return &forms.BillingActionCheckResult{
			Pass: true,
		}, nil
	}
	tenantPlan, err := (&plan.TenantPlanModel{}).GetTenantEffectivePlan(tenantID)
	if err != nil {
		return nil, err
	}
	payable := false
	poc := false

	switch tenantPlan.EffectivePlan {
	case entity.TenantPlan_NewUser, entity.TenantPlan_DevTier, entity.TenantPlan_Closing:
		payable = false
	case entity.TenantPlan_POC:
		poc = true
		if tenantPlan.POCCredits.GreaterThan(decimal.Zero) {
			tenant, _ := centralhandler.GetCentralHandler().GetTenant(tenantID)
			if tenant.Kind == forms.TenantKindInternal || tenantPlan.POCPlanInfo.StartedAt.Before(config.Get().GetTime(config.PocAfterNoPayable)) {
				payable = true

			} else {
				poc = false
			}
		}
	case entity.TenantPlan_OnDemand:
		payable = true
	}
	unpayableMsg := UnpayableMsg
	if poc {
		unpayableMsg = NoPOCCreditsMsg
	}
	if !actionInfo.Resource.IsFree && !payable && actionInfo.Action != forms.BillingAction_Delete {
		return &forms.BillingActionCheckResult{
			Pass:   false,
			Reason: &unpayableMsg,
		}, nil
	}
	return &forms.BillingActionCheckResult{
		Pass: true,
	}, nil
}

func (m *Model) BillingActionCheckOnClosing(tenantID uint, billingConfigID uint, actionInfo *forms.BillingActionInfo) (*forms.BillingActionCheckResult, error) {
	if !config.Get().GetBool(config.EnableActionCheckOnClosing) {
		return &forms.BillingActionCheckResult{
			Pass: true,
		}, nil
	}
	tenantPlan, err := (&plan.TenantPlanModel{}).GetTenantEffectivePlan(tenantID)
	if err != nil {
		return nil, err
	}
	if tenantPlan.EffectivePlan != entity.TenantPlan_Closing {
		return &forms.BillingActionCheckResult{
			Pass: true,
		}, nil
	}
	suspendMsg := OrgSuspendMsg
	switch actionInfo.Action {
	case forms.BillingAction_Delete:
		if !actionInfo.Resource.IsFree && (actionInfo.Resource.Type == forms.BillingResourceType_DedicatedBackup || actionInfo.Resource.Type == forms.BillingResourceType_ServerlessCluster || actionInfo.Resource.Type == forms.BillingResourceType_StarterCluster) {
			return &forms.BillingActionCheckResult{
				Pass:   false,
				Reason: &suspendMsg,
			}, nil
		}
	case forms.BillingAction_Create, forms.BillingAction_Scaleup, forms.BillingAction_Scaledown:
		return &forms.BillingActionCheckResult{
			Pass:   false,
			Reason: &suspendMsg,
		}, nil
	}
	return &forms.BillingActionCheckResult{
		Pass: true,
	}, nil
}

func (m *Model) actionCheckOnBillingConfigIsNotExisted(tenantID uint, actionInfo *forms.BillingActionInfo) (*forms.BillingActionCheckResult, error) {
	if !config.Get().GetBool(config.EnableActionCheckOnBillingConfigIsNotExisted) {
		return &forms.BillingActionCheckResult{
			Pass: true,
		}, nil
	}
	provider, err := m.getThirdPartyProviderFromAccount(tenantID)
	if err != nil {
		log.Error("failed to get third party provider from account", zap.Error(err), zap.Uint("tenantID", tenantID))
		return nil, err
	}
	return m.providerMatchedCheck(actionInfo, provider)
}

func (m *Model) BillingActionCheck(tenantID uint, actionInfo *forms.BillingActionInfo) (ret *forms.BillingActionCheckResult, err error) {
	defer func() {
		if ret != nil {
			ret.Original = actionInfo.Resource.Origin
		}
	}()
	if !config.Get().GetBool(config.EnableActionCheck) {
		return &forms.BillingActionCheckResult{
			Pass: true,
		}, nil
	}
	billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(tenantID)
	if err != nil {
		log.Error("failed get billing config on billing action check")
		return m.actionCheckOnBillingConfigIsNotExisted(tenantID, actionInfo)
	}
	var fs []func(uint, uint, *forms.BillingActionInfo) (*forms.BillingActionCheckResult, error)
	if actionInfo.Action == forms.BillingAction_View || (actionInfo.Resource.IsFree && actionInfo.Action == forms.BillingAction_Create) {
		fs = append(fs, m.billingActionProviderMatchedCheck)
	} else {
		fs = append(fs, m.billingActionProviderMatchedCheck, m.BillingActionCheckOnClosing, m.BillingActionCheckOnBillable)
	}
	for _, f := range fs {
		res, err := f(tenantID, billingConfigID, actionInfo)
		if err != nil {
			log.Error("failed to check billing action", zap.Error(err), zap.Any("actionInfo", actionInfo), zap.Uint("tenantID", tenantID), zap.Uint("billingConfigID", billingConfigID))
			return nil, err
		}
		if !res.Pass {
			if (actionInfo.Action == forms.BillingAction_Create && (actionInfo.Resource.Type == forms.BillingResourceType_ServerlessCluster || actionInfo.Resource.Type == forms.BillingResourceType_StarterCluster) && !actionInfo.Resource.IsFree) {
				hasCredits, creditErr := m.hasActiveReferralCredits(tenantID, billingConfigID)
				if creditErr != nil {
					log.Error("failed to check active credits on billing action check", zap.Error(creditErr), zap.Uint("tenantID", tenantID))
				} else if hasCredits {
					log.Info("billing action check bypassed: user has active credits",
						zap.Uint("tenantID", tenantID), zap.String("resourceType", string(actionInfo.Resource.Type)))
					continue
				}
			}
			return res, nil
		}
	}
	return &forms.BillingActionCheckResult{
		Pass: true,
	}, nil
}

func (m *Model) hasActiveCredits(tenantID uint, billingConfigID uint) (bool, error) {
	summary, err := (&marketing.CreditsPackageModel{}).GetTenantCreditsSummary(tenantID, billingConfigID)
	if err != nil {
		return false, err
	}
	return summary.AvailableRemainingCredits.GreaterThan(decimal.Zero), nil
}

func (m *Model) hasActiveReferralCredits(tenantID uint, billingConfigID uint) (bool, error) {
	creditsPackages, err := (&marketing.CreditsPackageModel{}).ListTenantCreditsPackages(tenantID, billingConfigID)
	if err != nil {
		return false, err
	}
	for _, cp := range creditsPackages {
		if cp.Status == entity.CreditsPackageStatus_Active && cp.Usage == entity.CreditsPackageUsage_Referral && cp.RemainingCredits.GreaterThan(decimal.Zero) {
			return true, nil
		}
	}
	return false, nil
}
