package supportcalc

import (
	"fmt"
	"math"
	"strconv"
	"time"

	"github.com/pingcap/dbaas/billing2/constant"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/shopspring/decimal"
)

const (
	SKUCategory                 = "Support Plan"
	MonthDay                    = 30
	SupportPlanPercentageBase   = 1000
	SnapshotGeneratedByLabelKey = "snapshot_generated_by"
)

func UnitKeys() []string {
	return []string{
		constant.FixedPrice_SKU,
		constant.PercentagePrice_SKU,
		constant.TieredPrice_SKU,
		constant.CommintmentPrice_SKU,
	}
}

func IsUnit(unit string) bool {
	switch unit {
	case constant.FixedPrice_SKU, constant.PercentagePrice_SKU, constant.TieredPrice_SKU, constant.CommintmentPrice_SKU:
		return true
	default:
		return false
	}
}

func BindedSupportPlanFromEntity(bindedSupportPlanEntity *entity.BindedSupportPlan) *forms.BindedSupportPlan {
	if bindedSupportPlanEntity == nil {
		return nil
	}
	res := &forms.BindedSupportPlan{
		SupportPlan: forms.SupportPlan{
			SupportPlanID:          bindedSupportPlanEntity.SupportPlan.ID,
			FixedChargePrice:       bindedSupportPlanEntity.SupportPlan.FixedChargePrice,
			SupportPricePercentage: bindedSupportPlanEntity.SupportPlan.SupportPricePercentage,
			Type:                   bindedSupportPlanEntity.SupportPlan.Type,
			Public:                 bindedSupportPlanEntity.SupportPlan.Public,
			Level:                  bindedSupportPlanEntity.SupportPlan.Level,
			TieredPriceRules:       bindedSupportPlanEntity.SupportPlan.TieredPriceRules,
			IsSupportFreeTrial:     bindedSupportPlanEntity.SupportPlan.IsSupportFreeTrial,
		},
		BindedSupportPlanID:    bindedSupportPlanEntity.ID,
		Status:                 bindedSupportPlanEntity.Status,
		ActiveDate:             bindedSupportPlanEntity.ActiveDate,
		ClosedDate:             bindedSupportPlanEntity.ClosedDate,
		ExpiredDate:            bindedSupportPlanEntity.ExpiredDate,
		TenantID:               bindedSupportPlanEntity.TenantID,
		BindedPackageID:        bindedSupportPlanEntity.BindedPackageID,
		IsEligibleForFreeTrial: bindedSupportPlanEntity.IsEligibleForFreeTrial,
	}
	if res.ExpiredDate == nil {
		res.ExpiredDate = timeutils.GetTimeAddr(time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC))
	}
	return res
}

func GetMeasureDay(bindedInfoStartTime time.Time, bindedInfoExpiredTime, bindedInfoClosedTime *time.Time, measureDateStartTime time.Time, measureDateEndTime time.Time) (time.Time, time.Time, int) {
	if bindedInfoExpiredTime != nil {
		bindedInfoExpiredTime = timeutils.GetTimeAddr(timeutils.GetDayDate(*bindedInfoExpiredTime).AddDate(0, 0, 1))
	}
	bindedInfoEndTime := bindedInfoExpiredTime
	if (bindedInfoExpiredTime != nil && bindedInfoClosedTime != nil && bindedInfoClosedTime.Before(*bindedInfoExpiredTime)) || (bindedInfoExpiredTime == nil && bindedInfoClosedTime != nil) {
		bindedInfoEndTime = bindedInfoClosedTime
	}
	startTime := measureDateStartTime
	normalizedBindedInfoStartTime := timeutils.GetDayDate(bindedInfoStartTime)
	if normalizedBindedInfoStartTime.Before(bindedInfoStartTime) {
		normalizedBindedInfoStartTime = normalizedBindedInfoStartTime.AddDate(0, 0, 1)
	}
	if startTime.Unix() < normalizedBindedInfoStartTime.Unix() {
		startTime = normalizedBindedInfoStartTime
	}
	endTime := measureDateEndTime
	if bindedInfoEndTime != nil {
		normalizedBindedInfoEndTime := timeutils.GetDayDate(*bindedInfoEndTime)
		if endTime.Unix() > normalizedBindedInfoEndTime.Unix() {
			endTime = normalizedBindedInfoEndTime
		}
	}
	if endTime.Before(startTime) {
		endTime = startTime
	}
	return startTime, endTime, int(endTime.Sub(startTime).Hours() / 24)
}

func IsInFreeTrialForMeasureDate(measureDate time.Time, bindedSupportPlan *forms.BindedSupportPlan, billingConfig *forms.BillingConfig) bool {
	if bindedSupportPlan == nil || billingConfig == nil {
		return false
	}
	if bindedSupportPlan.TenantID == 0 || billingConfig.TenantID != bindedSupportPlan.TenantID {
		return false
	}
	if !bindedSupportPlan.IsEligibleForFreeTrial || billingConfig.Type != entity.BillingConfigType_DirectCustomer {
		return false
	}
	trialStart := timeutils.GetNextMidnight(bindedSupportPlan.ActiveDate)
	trialEnd := timeutils.GetNextMidnight(bindedSupportPlan.ActiveDate.AddDate(0, 0, 30))
	return measureDate.Compare(trialStart) >= 0 && measureDate.Compare(trialEnd) < 0
}

type NormalMeteringInput struct {
	CostID              string
	MeasureDate         time.Time
	TenantID            uint
	BillingConfigID     uint
	BindedSupportPlan   *forms.BindedSupportPlan
	BaseCostPrice       decimal.Decimal
	PreviousTieredUsage decimal.Decimal
	Labels              map[string]string
}

type NormalMeteringResult struct {
	UnitCost *forms.UnitCosts
	Usage    decimal.Decimal
}

func CalculateNormalMetering(input NormalMeteringInput) (*NormalMeteringResult, error) {
	if input.BindedSupportPlan == nil {
		return nil, fmt.Errorf("binded support plan is required")
	}
	unitSku := constant.PercentagePrice_SKU
	usage := input.BaseCostPrice.Mul(decimal.NewFromInt(SupportPlanPercentageBase))
	cost := int(usage.Mul(input.BindedSupportPlan.SupportPricePercentage).IntPart())
	switch input.BindedSupportPlan.Type {
	case entity.SupportPlanType_FixedPrice:
		unitSku = constant.FixedPrice_SKU
		usage = input.BindedSupportPlan.FixedChargePrice.DivRound(decimal.NewFromInt(MonthDay), 2).Mul(decimal.NewFromInt(SupportPlanPercentageBase))
		cost = int(usage.IntPart())
	case entity.SupportPlanType_PercentagePrice:
	case entity.SupportPlanType_TieredPrice:
		unitSku = constant.TieredPrice_SKU
		totalUsage := input.PreviousTieredUsage.Add(usage)
		cost = TieredPricing(input.BindedSupportPlan.TieredPriceRules, input.PreviousTieredUsage, totalUsage)
		usage = totalUsage
	default:
		return nil, fmt.Errorf("unsupported support plan type %q", input.BindedSupportPlan.Type)
	}
	return &NormalMeteringResult{
		UnitCost: &forms.UnitCosts{
			CostID:                input.CostID,
			CostTimeStamp:         input.MeasureDate.Unix(),
			Unit:                  unitSku,
			Cost:                  cost,
			Labels:                cloneLabels(input.Labels),
			BindedBillingConfigID: input.BillingConfigID,
		},
		Usage: usage,
	}, nil
}

type CommitmentMeteringInput struct {
	CostID                 string
	CostTime               time.Time
	TenantID               uint
	BillingConfigID        uint
	BindedSupportPlan      *forms.BindedSupportPlan
	MeasureDay             int
	SupportPlanCostPrice   decimal.Decimal
	IsMarketplaceReseller  bool
	MarketplaceCustomerID  uint
	AdditionalLabelEntries map[string]string
}

func CalculateCommitmentMetering(input CommitmentMeteringInput) (*forms.UnitCosts, decimal.Decimal, error) {
	if input.BindedSupportPlan == nil {
		return nil, decimal.Zero, fmt.Errorf("binded support plan is required")
	}
	commitment := input.BindedSupportPlan.FixedChargePrice.Mul(decimal.NewFromInt(int64(input.MeasureDay))).DivRound(decimal.NewFromInt(MonthDay), 2)
	costDecimal := commitment.Sub(input.SupportPlanCostPrice)
	labels := map[string]string{
		"level":               input.BindedSupportPlan.Level,
		"supportPlanId":       fmt.Sprint(input.BindedSupportPlan.SupportPlanID),
		"bindedSupportPlanId": fmt.Sprint(input.BindedSupportPlan.BindedSupportPlanID),
	}
	for key, value := range input.AdditionalLabelEntries {
		labels[key] = value
	}
	if input.IsMarketplaceReseller {
		labels[constant.MarketplaceResellerManagedCustomerIDLabel] = strconv.FormatUint(uint64(input.MarketplaceCustomerID), 10)
	}
	if costDecimal.Cmp(decimal.Zero) <= 0 {
		return nil, costDecimal, nil
	}
	return &forms.UnitCosts{
		CostID:                input.CostID,
		CostTimeStamp:         input.CostTime.Unix(),
		Unit:                  constant.CommintmentPrice_SKU,
		Cost:                  int(costDecimal.Mul(decimal.NewFromInt(SupportPlanPercentageBase)).IntPart()),
		Labels:                labels,
		BindedBillingConfigID: input.BillingConfigID,
	}, costDecimal, nil
}

func TieredPricing(rules []forms.TieredPriceRule, previousTotalUsage decimal.Decimal, totalUsage decimal.Decimal) int {
	cost := decimal.Zero
	for _, rule := range rules {
		lowerBound := rule.LowerBound.Mul(decimal.NewFromInt(SupportPlanPercentageBase))
		upperBound := decimal.NewFromInt(math.MaxInt32)
		if rule.UpperBound != nil {
			upperBound = (*rule.UpperBound).Mul(decimal.NewFromInt(SupportPlanPercentageBase))
		}
		if previousTotalUsage.Cmp(upperBound) >= 0 {
			continue
		}
		if totalUsage.Cmp(lowerBound) <= 0 {
			break
		}
		start := decimal.Max(previousTotalUsage, lowerBound)
		end := decimal.Min(totalUsage, upperBound)
		if end.Cmp(start) > 0 {
			cost = cost.Add(end.Sub(start).Mul(rule.SupportPricePercentage))
		}
	}
	return int(cost.IntPart())
}

func cloneLabels(labels map[string]string) map[string]string {
	if labels == nil {
		return nil
	}
	ret := make(map[string]string, len(labels))
	for key, value := range labels {
		ret[key] = value
	}
	return ret
}
