package marketing

import (
	"errors"
	"fmt"
	"math"
	"sort"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"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/eventbushandler"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/pingcap/dbaas/billing2/utils/transformer"
	"github.com/pingcap/log"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

const MIN_RECHARGE_EARLY_DURATION = 3 * time.Hour
const RechargeEventType = "billing-recharge-credits"

var executedBefore = errors.New("failed to query recharge plan, status may changed, stopping recharge")

type RechargeCreditsModel struct{}

func tenantRechargePlanFromEntity(e *entity.TenantRechargeCreditsPlan) *forms.TenantRechargeCreditsPlan {
	return &forms.TenantRechargeCreditsPlan{
		PlanID:             e.ID,
		MainPlanID:         e.MainPlanID,
		TenantID:           e.TenantID,
		NextCycleStartTime: e.NextCycleStartTime,
		RechargeTimes:      e.RechargeTimes,
		StartDate:          e.StartDate,
		ExpiredDate:        e.ExpiredDate,
		ClosedAt:           e.ClosedAt,
		UnitFilters:        transformer.UnitFiltersSlimFromEntities(e.UnitFilters),
	}
}

func rechargePlanFromEntity(e *entity.RechargeCreditsPlan) *forms.RechargeCreditsPlan {
	return &forms.RechargeCreditsPlan{
		PlanID:      e.ID,
		CreatedAt:   e.CreatedAt,
		UpdatedAt:   e.UpdatedAt,
		Value:       e.Value,
		Usage:       e.Usage,
		Note:        e.Note,
		Internal:    e.Internal,
		UnitFilters: transformer.UnitFiltersSlimFromEntities(e.UnitFilters),
		Rule:        forms.RechargeCreditsPlanRule(e.Rule),
		Cycle:       e.Cycle,
		Trigger:     e.Trigger,
		Duration:    e.Duration,
		StartDate:   e.StartDate,
		ExpiredDate: e.ExpiredDate,
		ClosedAt:    e.ClosedAt,
		Source:      e.Source,
	}
}

func (m *RechargeCreditsModel) GetRechargePlanWithTx(tx *gorm.DB, planID uint) (ret *forms.RechargeCreditsPlan, err error) {
	var e entity.RechargeCreditsPlan
	if err = entity.NewRechargeCreditsPlanQuerySet(tx.Preload(clause.Associations)).IDEq(planID).One(&e); err != nil {
		return
	}
	return rechargePlanFromEntity(&e), nil
}

func (m *RechargeCreditsModel) GetRechargePlan(planID uint) (ret *forms.RechargeCreditsPlan, err error) {
	return m.GetRechargePlanWithTx(db.GetDB(), planID)
}

func (m *RechargeCreditsModel) CreateRechargePlan(req *forms.RechargeCreditsPlan) (ret *forms.RechargeCreditsPlan, err error) {
	return m.CreateRechargePlanWithTx(db.GetDB(), req)
}

func (m *RechargeCreditsModel) CreateRechargePlanWithTx(tx *gorm.DB, req *forms.RechargeCreditsPlan) (ret *forms.RechargeCreditsPlan, err error) {
	e := entity.RechargeCreditsPlan{
		Value:       req.Value,
		UnitFilters: transformer.UnitFiltersEntitiesFromUnitFilterSlimForm(req.UnitFilters),
		Usage:       req.Usage,
		Note:        req.Note,
		Rule:        entity.RechargeCreditsPlanRule(req.Rule),
		Cycle:       req.Cycle,
		Trigger:     req.Trigger,
		Duration:    req.Duration,
		StartDate:   req.StartDate,
		ExpiredDate: req.ExpiredDate,
		Internal:    req.Internal,
		Source:      req.Source,
	}
	err = e.Create(tx)
	if err != nil {
		return nil, err
	}
	return rechargePlanFromEntity(&e), nil
}

func (m *RechargeCreditsModel) UpdateRechargePlanValue(planID uint, value decimal.Decimal) (*forms.RechargeCreditsPlan, error) {
	_, err := m.GetRechargePlan(planID)
	if err != nil {
		return nil, err
	}
	err = entity.NewRechargeCreditsPlanQuerySet(db.GetDB()).IDEq(planID).GetUpdater().SetValue(value).Update()
	if err != nil {
		return nil, err
	}
	return m.GetRechargePlan(planID)
}

func (m *RechargeCreditsModel) UpdateRechargePlan(req *forms.RechargeCreditsPlan) (*forms.RechargeCreditsPlan, error) {
	return m.UpdateRechargePlanWithTx(db.GetDB(), req)
}

func (m *RechargeCreditsModel) UpdateRechargePlanWithTx(tx *gorm.DB, req *forms.RechargeCreditsPlan) (*forms.RechargeCreditsPlan, error) {
	affectRows, err := entity.NewRechargeCreditsPlanQuerySet(tx).IDEq(req.PlanID).GetUpdater().
		SetValue(req.Value).SetUsage(req.Usage).SetCycle(req.Cycle).SetRule(entity.RechargeCreditsPlanRule(req.Rule)).
		SetDuration(req.Duration).SetStartDate(req.StartDate).SetExpiredDate(req.ExpiredDate).
		UpdateNum()
	if err != nil {
		return nil, err
	} else if affectRows == 0 {
		return nil, billing_errors.ObjectNotExistsError
	}
	return m.GetRechargePlanWithTx(tx, req.PlanID)
}

func (m *RechargeCreditsModel) DisableRechargePlan(planID uint) (err error) {
	return m.DisableRechargePlanWithTx(db.GetDB(), planID)
}

func (m *RechargeCreditsModel) DisableRechargePlanWithTx(tx *gorm.DB, planID uint) (err error) {
	log.Info("start close expired recharge plan", zap.Uint("planID", planID))
	now := timeutils.GetNowAddr()
	err = entity.NewRechargeCreditsPlanQuerySet(tx).IDEq(planID).ClosedAtIsNull().GetUpdater().SetClosedAt(now).Update()
	return
}

func (*RechargeCreditsModel) listRechargePlans(source string) (ret []*forms.RechargeCreditsPlan, err error) {
	query := entity.NewRechargeCreditsPlanQuerySet(db.GetDB().Preload(clause.Associations))
	if source != "" {
		query = query.SourceEq(source)
	}
	var entities []entity.RechargeCreditsPlan
	if err = query.All(&entities); err != nil {
		return
	}
	ret = make([]*forms.RechargeCreditsPlan, len(entities))
	for i, e := range entities {
		ret[i] = rechargePlanFromEntity(&e)
	}
	return
}

func (m *RechargeCreditsModel) ListRechargePlans() (ret []*forms.RechargeCreditsPlan, err error) {
	return m.listRechargePlans("")
}

func (m *RechargeCreditsModel) ListWebRechargePlans() (ret []*forms.RechargeCreditsPlan, err error) {
	return m.listRechargePlans(entity.RechargeCreditsPlanSource_Web)
}

func (m *RechargeCreditsModel) EnableTenantRechargePlanWithTx(tx *gorm.DB, tenantID uint, mainPlanID uint, unitFilters ...*forms.UnitFilterSlim) (ret *forms.TenantRechargeCreditsPlan, err error) {
	if len(unitFilters) == 0 {
		unitFilters = []*forms.UnitFilterSlim{{FilterID: config.Get().GetUint(config.GeneralCreditTypeID)}}
	} else if len(unitFilters) > config.Get().GetInt(config.MaxTenantPlanUnitFilters) {
		return nil, billing_errors.NewInvalidInputError(errors.New("too many unit filters"))
	}
	mainPlan, err := m.GetRechargePlanWithTx(tx, mainPlanID)
	if err != nil {
		return
	} else if mainPlan.ClosedAt != nil {
		return nil, errors.New(fmt.Sprintf("failed to enable tenant(id=%d) plan, main plan(id=%d) has been closed", tenantID, mainPlanID))
	}
	StartDate, err := m.StartTime(mainPlan.Cycle, mainPlan.Rule.FirstCycleStartedAt)
	if err != nil {
		return
	}
	ExpireDate := StartDate.Add(mainPlan.Duration)
	if mainPlan.Duration == time.Duration(0) {
		ExpireDate = StartDate.Add(time.Duration(math.MaxInt64))
	}
	sort.Slice(unitFilters, func(i, j int) bool {
		return unitFilters[i].FilterID < unitFilters[j].FilterID
	})
	filterIDList := make([]string, len(unitFilters))
	for i, uf := range unitFilters {
		filterIDList[i] = fmt.Sprint(uf.FilterID)
	}
	tenantPlanEntity := entity.TenantRechargeCreditsPlan{
		TenantID:           tenantID,
		MainPlanID:         mainPlanID,
		RechargeTimes:      0,
		NextCycleStartTime: StartDate,
		StartDate:          StartDate,
		ExpiredDate:        ExpireDate,
		FilterIDs:          strings.Join(filterIDList, ","),
		UnitFilters:        transformer.UnitFiltersEntitiesFromUnitFilterSlimForm(unitFilters),
	}
	err = tenantPlanEntity.Create(tx)
	if err != nil {
		return nil, err
	}
	ret = tenantRechargePlanFromEntity(&tenantPlanEntity)
	if m.rechargingShouldStart(StartDate) {
		err = m.sendRechargeEvent(tx, ret)
	}
	return
}

func (m *RechargeCreditsModel) EnableTenantRechargePlan(tenantID uint, mainPlanID uint, unitFilters ...*forms.UnitFilterSlim) (tenantPlan *forms.TenantRechargeCreditsPlan, err error) {
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		tenantPlan, err = m.EnableTenantRechargePlanWithTx(tx, tenantID, mainPlanID, unitFilters...)
		return err
	})
	return
}

func (m *RechargeCreditsModel) sendRechargeEvent(tx *gorm.DB, plan *forms.TenantRechargeCreditsPlan) (err error) {
	return eventbushandler.Get().PublishEventWithTransaction(
		RechargeEventType, &eventbushandler.Event{
			EventID:   fmt.Sprintf("%d-%d", plan.PlanID, plan.NextCycleStartTime.Unix()),
			EventType: RechargeEventType,
			EventData: plan,
		}, tx,
	)
}

func (m *RechargeCreditsModel) rechargeHandler(event *eventbushandler.Event) (err error) {
	plan, ok := event.EventData.(*forms.TenantRechargeCreditsPlan)
	if !ok {
		return errors.New("failed to parse event data")
	}
	log.Info("try to recharge credits", zap.Uint("planID", plan.PlanID))
	charged, err := m.TryRecharge(plan.PlanID)
	if err != nil {
		log.Error("failed to recharge credits", zap.Uint("planID", plan.PlanID))
		return err
	}
	log.Info("recharging credits has been finished", zap.Uint("planID", plan.PlanID), zap.Bool("charged", charged))
	return
}

func (m *RechargeCreditsModel) TryRecharge(tenantPlanID uint) (charged bool, err error) {
	tenantPlan, err := m.GetTenantRechargePlan(tenantPlanID)
	if err != nil {
		return
	}
	mainPlan, err := m.GetRechargePlan(tenantPlan.MainPlanID)
	if err != nil {
		return
	}
	if m.rechargingShouldStart(tenantPlan.NextCycleStartTime) {
		if err = m.recharge(tenantPlan, mainPlan); err != nil {
			if errors.Is(err, executedBefore) {
				log.Info("executed before", zap.Error(executedBefore))
				return false, nil
			}
			return
		}
		charged = true
	}
	return
}

func (m *RechargeCreditsModel) DisableTenantRechargePlanWithTx(tx *gorm.DB, planID uint) (err error) {
	log.Info("start close expired tenant recharge plan", zap.Uint("planID", planID))
	now := timeutils.GetNowAddr()
	err = entity.NewTenantRechargeCreditsPlanQuerySet(tx).IDEq(planID).ClosedAtIsNull().GetUpdater().SetClosedAt(now).SetDeletedUniqueKeyMark(uuid.NewString()).Update()
	return
}

func (m *RechargeCreditsModel) DisableTenantRechargePlan(planID uint) (err error) {
	return m.DisableTenantRechargePlanWithTx(db.GetDB(), planID)
}

func (*RechargeCreditsModel) GetTenantRechargePlanWithTx(tx *gorm.DB, tenantPlanID uint) (ret *forms.TenantRechargeCreditsPlan, err error) {
	var e entity.TenantRechargeCreditsPlan
	if err = entity.NewTenantRechargeCreditsPlanQuerySet(tx.Preload(clause.Associations)).IDEq(tenantPlanID).One(&e); err != nil {
		return
	}
	return tenantRechargePlanFromEntity(&e), nil
}

func (m *RechargeCreditsModel) GetTenantRechargePlan(tenantPlanID uint) (ret *forms.TenantRechargeCreditsPlan, err error) {
	return m.GetTenantRechargePlanWithTx(db.GetDB(), tenantPlanID)
}

func (m *RechargeCreditsModel) ListTenantPlansNeedRecharge(tenantID uint) (ret []*forms.TenantRechargeCreditsPlan, err error) {
	now := timeutils.GetNow()
	threshold := m.thresholdOfShouldRechargeNow()
	var entities []entity.TenantRechargeCreditsPlan
	err = entity.NewTenantRechargeCreditsPlanQuerySet(db.GetDB()).NextCycleStartTimeLte(threshold).TenantIDEq(tenantID).ClosedAtIsNull().ExpiredDateGt(now).All(&entities)
	ret = make([]*forms.TenantRechargeCreditsPlan, len(entities))
	for i, e := range entities {
		ret[i] = tenantRechargePlanFromEntity(&e)
	}
	return
}

func (*RechargeCreditsModel) StartTime(cycle string, RuleOfFirstCycleStartedAt string) (ret time.Time, err error) {
	now := timeutils.GetNow()
	switch cycle {
	case entity.RechargePlanCycle_Month:
		switch RuleOfFirstCycleStartedAt {
		case forms.FirstCycleStartedAt_RightNow:
			return now, nil
		case forms.FirstCycleStartedAt_NextStartOfNaturalCycle:
			yr, mon, day := now.AddDate(0, 1, -now.Day()+1).UTC().Date()
			ret = time.Date(yr, mon, day, 0, 0, 0, 0, time.UTC)
			return
		}
	}
	return ret, errors.New("failed to calc start time, not valid argument")
}

func (*RechargeCreditsModel) NextCycleStartTime(lastRechargeTime time.Time, cycle string, ruleOfFirstCycleFinishedAt string) (ret time.Time, err error) {
	switch cycle {
	case entity.RechargePlanCycle_Month:
		switch ruleOfFirstCycleFinishedAt {
		case forms.FirstCycleFinishedAt_EndOfNaturalCycle:
			yr, mon, day := lastRechargeTime.AddDate(0, 1, -lastRechargeTime.Day()+1).UTC().Date()
			ret = time.Date(yr, mon, day, 0, 0, 0, 0, time.UTC)
			return
		}
	}
	return ret, errors.New("failed to calc next cycle start time, not valid argument")
}

func (*RechargeCreditsModel) RechargeValue(value decimal.Decimal, start time.Time, end time.Time, cycle string, ruleOfFirstCreditsConvertor string) (ret decimal.Decimal, err error) {
	if start.After(end) {
		return decimal.Zero, errors.New("start after end")
	}
	switch ruleOfFirstCreditsConvertor {
	case forms.FirstCreditsConvertor_Full:
		return value, nil
	case forms.FirstCreditsConvertor_Proportional:
		switch cycle {
		case entity.RechargePlanCycle_Month:
			_, _, daysOfStartMon := start.AddDate(0, 1, -start.Day()).Date()
			afterFullMonth := start.AddDate(0, 0, daysOfStartMon)
			if !afterFullMonth.After(end) {
				return value, nil
			}
			diff := int64(afterFullMonth.Sub(end).Hours() / 24)
			precision := config.Get().GetInt32(config.BillingCostsPrecisionDigits)
			return decimal.NewFromInt(1).Sub(decimal.NewFromInt(diff).Div(decimal.NewFromInt(int64(daysOfStartMon)))).Mul(value).Truncate(precision), nil
		}
	}
	return decimal.Zero, errors.New("failed to calc recharge value, not valid argument")
}

func (m *RechargeCreditsModel) recharge(tenantPlan *forms.TenantRechargeCreditsPlan, mainPlan *forms.RechargeCreditsPlan) (err error) {
	if tenantPlan.NextCycleStartTime.After(tenantPlan.ExpiredDate) {
		return errors.New("tenant recharge plan is expired")
	}
	nextCycleStartTime, err := m.NextCycleStartTime(tenantPlan.NextCycleStartTime, mainPlan.Cycle, mainPlan.Rule.FirstCycleFinishedAt)
	if err != nil {
		log.Error("failed to calc nextCycleStartTime", zap.Error(err), zap.String("Cycle", mainPlan.Cycle), zap.String("FirstCycleFinishedAt", mainPlan.Rule.FirstCycleFinishedAt))
		return err
	}
	value, err := m.RechargeValue(mainPlan.Value, tenantPlan.NextCycleStartTime, nextCycleStartTime, mainPlan.Cycle, mainPlan.Rule.FirstCreditsConvertor)
	if err != nil {
		log.Error("failed to calc value", zap.Error(err), zap.String("Cycle", mainPlan.Cycle), zap.String("FirstCreditsConvertor", mainPlan.Rule.FirstCreditsConvertor))
		return err
	}
	var billingConfigID uint = 0
	createCreditsReq := &forms.CreditsPackage{
		TenantID:              tenantPlan.TenantID,
		TotalCredits:          value,
		UnitFilters:           tenantPlan.UnitFilters,
		Note:                  mainPlan.Note,
		Usage:                 mainPlan.Usage,
		StartDate:             tenantPlan.NextCycleStartTime,
		ExpiredDate:           nextCycleStartTime,
		Internal:              mainPlan.Internal,
		BindedBillingConfigID: &billingConfigID,
	}
	rechargePlanRecord := entity.RechargeCreditsPlanRecord{
		TenantID:     tenantPlan.TenantID,
		MainPlanID:   mainPlan.PlanID,
		TenantPlanID: tenantPlan.PlanID,
		CreatedAt:    timeutils.GetNow(),
	}
	creditsModel := &CreditsPackageModel{}
	//todo msp-billingconfig
	_, err = creditsModel.CreateCreditsPackageAtomically(
		tenantPlan.TenantID, createCreditsReq,
		func(tx *gorm.DB, creditsPackageEntity *entity.CreditsPackage) (err error) {
			var ent entity.TenantRechargeCreditsPlan
			err = entity.NewTenantRechargeCreditsPlanQuerySet(tx.Clauses(clause.Locking{Strength: "UPDATE"})).IDEq(tenantPlan.PlanID).One(&ent)
			if err != nil {
				return err
			}
			if ent.RechargeTimes != tenantPlan.RechargeTimes {
				return executedBefore
			}
			row, err := entity.NewTenantRechargeCreditsPlanQuerySet(tx).IDEq(tenantPlan.PlanID).
				TenantIDEq(tenantPlan.TenantID).RechargeTimesEq(tenantPlan.RechargeTimes).
				GetUpdater().SetRechargeTimes(tenantPlan.RechargeTimes + 1).SetNextCycleStartTime(nextCycleStartTime).
				UpdateNum()
			if err != nil {
				return err
			} else if row == 0 {
				return executedBefore
			}
			rechargePlanRecord.CreditsPackageID = creditsPackageEntity.ID
			err = rechargePlanRecord.Create(tx)
			if err != nil {
				return err
			}
			if nextCycleStartTime.After(tenantPlan.ExpiredDate) {
				err = m.DisableTenantRechargePlanWithTx(tx, tenantPlan.PlanID)
			} else if m.rechargingShouldStart(nextCycleStartTime) {
				tenantPlan, err = m.GetTenantRechargePlanWithTx(tx, tenantPlan.PlanID)
				if err != nil {
					return err
				}
				err = m.sendRechargeEvent(tx, tenantPlan)
				if err != nil {
					return err
				}
			}
			return nil
		},
	)
	return
}

func (*RechargeCreditsModel) thresholdOfShouldRechargeNow() time.Time {
	now := timeutils.GetNow()
	duration := time.Duration(config.Get().GetInt64(config.RechargeCreditsBeforeSeconds) * int64(time.Second))
	if duration < MIN_RECHARGE_EARLY_DURATION {
		duration = MIN_RECHARGE_EARLY_DURATION
	}
	return now.Add(duration)
}

func (m *RechargeCreditsModel) rechargingShouldStart(nextCycleStart time.Time) bool {
	threshold := m.thresholdOfShouldRechargeNow()
	return !nextCycleStart.After(threshold)
}

func (m *RechargeCreditsModel) RegisterEventHandler() (err error) {
	err = eventbushandler.Get().RegisterEventHandleFunc(
		RechargeEventType,
		"recharge",
		forms.TenantRechargeCreditsPlan{},
		m.rechargeHandler,
	)
	return
}
