package workers

import (
	"time"

	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/db"
	"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/utils/concurrency"
	"github.com/pingcap/dbaas/billing2/utils/dateinterval"
	"gorm.io/gorm"

	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/model/marketing"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"go.uber.org/zap"
)

// TODO move this code to model tier, keep the worker as a trigger

func SyncSupportPlanStatusByTenant(tenantID uint, now time.Time) {
	model := &marketing.SupportPlanModel{}
	bindedSupportPlans, err := model.ListTenantsBindedSupportPlan(tenantID, entity.BindedSupportPlanStatus_ACTIVE, entity.BindedSupportPlanStatus_TOBEACTIVATED, entity.BindedSupportPlanStatus_Expired)
	if err != nil {
		log.Error("failed to get tenant support plan", zap.Uint("TenantID", tenantID), zap.Error(err))
		return
	}
	if len(bindedSupportPlans) == 0 {
		return
	}
	var needExpiredIDs, needActivatedIDs, needCancelIDs []uint
	for _, bindedSupportPlan := range bindedSupportPlans {
		planDateInterval := forms.DateInterval{StartDate: timeutils.GetTimeAddr(bindedSupportPlan.ActiveDate), EndDate: bindedSupportPlan.ExpiredDate}
		if !dateinterval.IsWithin(now, planDateInterval) {
			switch bindedSupportPlan.Status {
			case entity.BindedSupportPlanStatus_Expired:
				if timeutils.GetMonthDate(*bindedSupportPlan.ExpiredDate).AddDate(0, 1, 0).Add(config.Get().GetDuration(config.BindedSupportPlanInactiveDelayHour) * time.Hour).Before(now) {
					// cancel on next invoice date after expired date
					needCancelIDs = append(needCancelIDs, bindedSupportPlan.BindedSupportPlanID)
				}
			case entity.BindedSupportPlanStatus_ACTIVE:
				needExpiredIDs = append(needExpiredIDs, bindedSupportPlan.BindedSupportPlanID)
			case entity.BindedSupportPlanStatus_TOBEACTIVATED:
				if bindedSupportPlan.ExpiredDate != nil && bindedSupportPlan.ExpiredDate.Before(now) {
					needCancelIDs = append(needCancelIDs, bindedSupportPlan.BindedSupportPlanID)
				}
			}
		} else if bindedSupportPlan.Status == entity.BindedSupportPlanStatus_TOBEACTIVATED {
			needActivatedIDs = append(needActivatedIDs, bindedSupportPlan.BindedSupportPlanID)
		}
	}
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		if len(needActivatedIDs) != 0 {
			if len(needActivatedIDs) == 1 {
				err = entity.NewBindedSupportPlanQuerySet(tx).TenantIDEq(tenantID).IDIn(needActivatedIDs...).
					GetUpdater().SetStatus(entity.BindedSupportPlanStatus_ACTIVE).Update()
				if err != nil {
					log.Error("failed to active support plan", zap.Error(err), zap.Uints("needActivatedIDs", needActivatedIDs))
				} else {
					log.Info("success to active support plan", zap.Uints("needActivatedIDs", needActivatedIDs))
				}
			} else {
				log.Error("detected invalidate needActivatedIDs length", zap.Uints("needActivatedIDs", needActivatedIDs))
			}
		}
		if len(needExpiredIDs) != 0 {
			err = entity.NewBindedSupportPlanQuerySet(tx).TenantIDEq(tenantID).IDIn(needExpiredIDs...).
				GetUpdater().SetStatus(entity.BindedSupportPlanStatus_Expired).Update()
			if err != nil {
				log.Error("failed to expire support plan", zap.Error(err), zap.Uints("needExpiredIDs", needExpiredIDs))
			} else {
				log.Info("success to expire support plan", zap.Uints("needExpiredIDs", needExpiredIDs))
			}
		}
		if len(needCancelIDs) != 0 {
			err = entity.NewBindedSupportPlanQuerySet(tx).TenantIDEq(tenantID).IDIn(needCancelIDs...).
				GetUpdater().SetStatus(entity.BindedSupportPlanStatus_CANCELED).SetClosedDate(&now).Update()
			if err != nil {
				log.Error("failed to expire support plan", zap.Error(err), zap.Uints("needExpiredIDs", needCancelIDs))
			} else {
				log.Info("success to expire support plan", zap.Uints("needExpiredIDs", needCancelIDs))
			}
		}
		return nil
	})
	if err != nil {
		log.Error("failed to Sync Support Plan Status", zap.Error(err))
	}
	if len(needExpiredIDs) != 0 || len(needActivatedIDs) != 0 {
		model.NotifyCurrentSupport(tenantID)
	}
}

func SendSupportPlanMeteringByTenant(tenantID uint, now time.Time) {
	model := &marketing.SupportPlanModel{}
	bindedSupportPlans, err := model.ListTenantsBindedSupportPlan(tenantID, entity.BindedSupportPlanStatus_ACTIVE, entity.BindedSupportPlanStatus_Expired)
	if err != nil {
		log.Error("failed to get tenant support plan", zap.Uint("TenantID", tenantID), zap.Error(err))
		return
	}
	if len(bindedSupportPlans) == 0 {
		return
	}
	tenant, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		log.Error("SendSupportPlanMeteringByTenant failed to get tenant from account.", zap.Error(err), zap.Uint("tenantID", tenantID))
		return
	}

	for _, bindedSupportPlan := range bindedSupportPlans {
		planDateInterval := forms.DateInterval{StartDate: timeutils.GetTimeAddr(bindedSupportPlan.ActiveDate), EndDate: bindedSupportPlan.ExpiredDate}
		delayHours := 24
		if bindedSupportPlan.Type != entity.SupportPlanType_FixedPrice {
			//run time 60 -24 =36h
			delayHours = config.Get().GetInt(config.MeasureDelayHours)
		}
		if tenant.IsMsp {
			//run time 76 -24 =52h >  config.Invoicehours 50h
			delayHours = config.Get().GetInt(config.PartnerSupportPlanHours)
		}
		// Send normal metering
		meteringTime := now.Add(-time.Hour * time.Duration(delayHours))
		if dateinterval.IsWithin(meteringTime, planDateInterval) {
			log.Info("matched support plan interval", zap.Time("meteringTime", meteringTime), zap.Uint("TenantID", tenantID), zap.Uint("bindedSupportPlanID", bindedSupportPlan.BindedSupportPlanID))
			// wait days to send normal metering
			_, err = model.SendNormalMetering(bindedSupportPlan.BindedSupportPlanID, meteringTime)
			if err != nil {
				log.Error("failed to send tenant support plan normal metering", zap.Int("delayHours", delayHours), zap.String("type", bindedSupportPlan.Type), zap.Time("time", meteringTime), zap.Uint("TenantID", tenantID), zap.Uint("bindedSupportPlanID", bindedSupportPlan.BindedSupportPlanID), zap.Error(err))
			} else {
				log.Info("success send tenant support plan normal metering", zap.Int("delayHours", delayHours), zap.String("type", bindedSupportPlan.Type), zap.Time("time", meteringTime), zap.Uint("TenantID", tenantID), zap.Uint("bindedSupportPlanID", bindedSupportPlan.BindedSupportPlanID))
			}
		}

		// Send Reseller Normal metering if need
		delayHours = config.Get().GetInt(config.PartnerSupportPlanHours)
		meteringTime = now.Add(-time.Hour * time.Duration(delayHours))
		if dateinterval.IsWithin(meteringTime, planDateInterval) {
			log.Info("matched support plan interval for send reseller support fee", zap.Time("meteringTime", meteringTime), zap.Uint("TenantID", tenantID), zap.Uint("bindedSupportPlanID", bindedSupportPlan.BindedSupportPlanID))
			// wait days to send normal metering
			_, err = model.SendResellerNormalMeteringIfNeed(bindedSupportPlan.BindedSupportPlanID, meteringTime)
			if err != nil {
				log.Error("failed to send reseller support plan normal metering", zap.Int("delayHours", delayHours), zap.String("type", bindedSupportPlan.Type), zap.Time("time", meteringTime), zap.Uint("TenantID", tenantID), zap.Uint("bindedSupportPlanID", bindedSupportPlan.BindedSupportPlanID), zap.Error(err))
			} else {
				log.Info("success send reseller support plan normal metering", zap.Int("delayHours", delayHours), zap.String("type", bindedSupportPlan.Type), zap.Time("time", meteringTime), zap.Uint("TenantID", tenantID), zap.Uint("bindedSupportPlanID", bindedSupportPlan.BindedSupportPlanID))
			}
		}

		// Send Commitment metering
		if bindedSupportPlan.Type != entity.SupportPlanType_FixedPrice {
			// 60 -24 +6 = 42h ,
			delayHours = config.Get().GetInt(config.MeasureDelayHours) - 24 + 6
			//direct customer invoice time is config.Invoicehours 48h,
			//Msp invoice time is config.Invoicehours +config.MspInvoicehours =48 +6
			//worker SupportPlanMeteringInterval Interval 1h
			//Msp CommitmentMetering time need config.Invoicehours < c+Interval <config.MspInvoicehours
			// day3 2h run
			if tenant.IsMsp {
				//52h day3 2h run
				delayHours = config.Get().GetInt(config.Invoicehours) + 4
			}
			//direct customer  now - month.GetMonthDate >42  running
			//Msp  now - month.GetMonthDate >52 running
			if int(now.Sub(timeutils.GetMonthDate(now)).Hours()) > delayHours {
				meteringTime = timeutils.GetLastMonth(now)
			} else {
				meteringTime = timeutils.GetLast2Month(now)
			}
			// wait delayHours to send commitment metering
			log.Info("try to send commitment support plan interval", zap.Time("meteringTime", meteringTime), zap.Uint("TenantID", tenantID), zap.Uint("bindedSupportPlanID", bindedSupportPlan.BindedSupportPlanID))
			_, _, err = model.SendCommitmentMetering(bindedSupportPlan.BindedSupportPlanID, meteringTime)
			if err != nil {
				log.Error("failed to send tenant support plan commitment metering", zap.Time("time", meteringTime), zap.Uint("TenantID", tenantID), zap.Uint("bindedSupportPlanID", bindedSupportPlan.BindedSupportPlanID), zap.Error(err))
			} else {
				log.Info("success send tenant support plan commitment metering", zap.Time("time", meteringTime), zap.String("type", bindedSupportPlan.Type), zap.Uint("TenantID", tenantID), zap.Uint("bindedSupportPlanID", bindedSupportPlan.BindedSupportPlanID))
			}

			// Send Reseller Commitment metering if needed
			delayHours = config.Get().GetInt(config.Invoicehours) + 4
			if int(now.Sub(timeutils.GetMonthDate(now)).Hours()) > delayHours {
				meteringTime = timeutils.GetLastMonth(now)
			} else {
				meteringTime = timeutils.GetLast2Month(now)
			}
			_, _, err = model.SendResellerCommitmentMeteringIfNeed(bindedSupportPlan.BindedSupportPlanID, meteringTime)
			if err != nil {
				log.Error("failed to send reseller support plan commitment metering", zap.Time("time", meteringTime), zap.Uint("TenantID", tenantID), zap.Uint("bindedSupportPlanID", bindedSupportPlan.BindedSupportPlanID), zap.Error(err))
			} else {
				log.Info("success send reseller support plan commitment metering", zap.Time("time", meteringTime), zap.String("type", bindedSupportPlan.Type), zap.Uint("TenantID", tenantID), zap.Uint("bindedSupportPlanID", bindedSupportPlan.BindedSupportPlanID))
			}
		}
	}
	return
}

func SupportPlanWorkerByTenant(tenantID uint) {
	now := timeutils.GetNow()
	SyncSupportPlanStatusByTenant(tenantID, now)
	SendSupportPlanMeteringByTenant(tenantID, now)
}

func SendSupportPlanMetering() error {
	model := &marketing.SupportPlanModel{}
	tenantIDs, err := model.GetBindedSupportPlanTenantIDs()
	if err != nil {
		log.Error("failed to get allTenantsBindedSupportPlan in worker.", zap.Error(err))
		return err
	}
	log.Info("SendSupportPlanMetering ", zap.Int("allTenantsBindedSupportPlan", len(tenantIDs)))
	return concurrency.RunInPoolWithIDs(
		config.Get().GetUint(config.DefaultWorkerConcurrency),
		SupportPlanWorkerByTenant,
		tenantIDs,
	)
}

func SendSupportPlanFreeTrialReminder() error {
	model := &marketing.SupportPlanModel{}
	now := timeutils.GetNow()

	plans, err := model.GetBindedSupportPlansEndingFreeTrialSoon(now)
	if err != nil {
		log.Error("failed to get binded support plans ending free trial soon", zap.Error(err))
		return err
	}

	log.Info("found binded support plans ending free trial soon", zap.Int("count", len(plans)))

	for _, plan := range plans {
		cacheKey := "support_plan_free_trial_reminder"
		var cachedTenantID uint
		found := cachehandler.Get().Get(cacheKey, &cachedTenantID, plan.TenantID)
		if found {
			log.Info("free trial reminder email already sent to tenant, skip",
				zap.Uint("tenantID", plan.TenantID),
				zap.Uint("bindedSupportPlanID", plan.ID))
			continue
		}

		err := model.SendFreeTrialReminderEmail(plan.ID)
		if err != nil {
			log.Error("failed to send free trial reminder email",
				zap.Uint("bindedSupportPlanID", plan.ID),
				zap.Uint("tenantID", plan.TenantID),
				zap.Error(err))
			continue
		}

		cachehandler.Get().SetWithTimeOut(10*24*time.Hour, cacheKey, plan.TenantID, plan.TenantID)
		log.Info("cached tenant for free trial reminder",
			zap.Uint("tenantID", plan.TenantID),
			zap.Uint("bindedSupportPlanID", plan.ID))
	}

	return nil
}
