package subscribe

import (
	"context"
	stderrors "errors"
	"strings"
	"time"

	"github.com/go-sql-driver/mysql"
	commonLogic "github.com/perfect-panel/server/internal/logic/common"
	"github.com/perfect-panel/server/internal/model/user"
	"github.com/perfect-panel/server/internal/svc"
	"github.com/perfect-panel/server/internal/types"
	"github.com/perfect-panel/server/pkg/constant"
	"github.com/perfect-panel/server/pkg/xerr"
	"github.com/pkg/errors"
	"gorm.io/gorm"
)

const (
	promoRuleTypeNewUser      = "new_user"
	promoRuleTypeInactiveUser = "inactive_user"
	promoRuleTypeCampaign     = "campaign"
)

type subscribePromoCandidate struct {
	SubscribeId int64      `gorm:"column:subscribe_id"`
	Quantity    int64      `gorm:"column:quantity"`
	RuleName    string     `gorm:"column:rule_name"`
	RuleType    string     `gorm:"column:rule_type"`
	PromoPrice  int64      `gorm:"column:promo_price"`
	Params      string     `gorm:"column:params"`
	StartTime   *time.Time `gorm:"column:start_time"`
	EndTime     *time.Time `gorm:"column:end_time"`
}

func loadSubscribePromoMap(ctx context.Context, svcCtx *svc.ServiceContext, subscribeIDs []int64) (map[int64]map[int64]*types.SubscribePromo, error) {
	result := make(map[int64]map[int64]*types.SubscribePromo)
	if len(subscribeIDs) == 0 || svcCtx == nil || svcCtx.DB == nil {
		return result, nil
	}

	userInfo, _ := ctx.Value(constant.CtxKeyUser).(*user.User)
	userID := int64(0)
	isFirstPurchase := true
	if userInfo != nil {
		entitlement, err := commonLogic.ResolveEntitlementUser(ctx, svcCtx.DB, userInfo.Id)
		if err != nil {
			return nil, err
		}
		userID = entitlement.EffectiveUserID

		hasPaid, err := commonLogic.HasPaidSubscription(ctx, svcCtx.DB, userID)
		if err != nil {
			return nil, err
		}
		isFirstPurchase = !hasPaid
	}

	candidates, err := querySubscribePromoCandidates(ctx, svcCtx, subscribeIDs, userInfo != nil)
	if err != nil {
		if isMissingPromoTableError(err) {
			return result, nil
		}
		return nil, err
	}

	for _, candidate := range candidates {
		if candidate.Quantity <= 0 {
			continue
		}
		if result[candidate.SubscribeId] == nil {
			result[candidate.SubscribeId] = make(map[int64]*types.SubscribePromo)
		}
		if _, exists := result[candidate.SubscribeId][candidate.Quantity]; exists {
			continue
		}
		promoResult, err := commonLogic.EvaluatePromo(ctx, svcCtx, userID, candidate.SubscribeId, candidate.Quantity, isFirstPurchase)
		if err != nil {
			return nil, err
		}
		if promoResult == nil || !promoResult.Eligible {
			continue
		}
		result[candidate.SubscribeId][candidate.Quantity] = &types.SubscribePromo{
			RuleName:   promoResult.RuleName,
			RuleType:   promoResult.RuleType,
			PromoPrice: promoResult.PromoPrice,
			ExpiresAt:  unixSeconds(promoResult.ExpiresAt),
		}
	}

	return result, nil
}

func querySubscribePromoCandidates(ctx context.Context, svcCtx *svc.ServiceContext, subscribeIDs []int64, loggedIn bool) ([]subscribePromoCandidate, error) {
	var candidates []subscribePromoCandidate
	err := subscribePromoCandidatesQuery(ctx, svcCtx.DB, subscribeIDs, loggedIn).
		Scan(&candidates).Error
	if err != nil {
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query subscribe promo candidates failed: %v", err)
	}
	return candidates, nil
}

func subscribePromoCandidatesQuery(ctx context.Context, db *gorm.DB, subscribeIDs []int64, loggedIn bool) *gorm.DB {
	query := db.WithContext(ctx).
		Table("subscribe_promo AS sp").
		Select("sp.subscribe_id, sp.quantity, sp.promo_price, pr.name AS rule_name, pr.type AS rule_type, pr.params, pr.start_time, pr.end_time").
		Joins("JOIN promo_rule AS pr ON pr.id = sp.promo_rule_id AND pr.deleted_at IS NULL").
		Where("sp.subscribe_id IN ? AND sp.promo_price > 0 AND pr.enabled = ?", subscribeIDs, true)
	if !loggedIn {
		query = query.Where("pr.type = ?", promoRuleTypeCampaign)
	}
	return query.
		Order("sp.subscribe_id ASC").
		Order("sp.quantity ASC").
		Order("pr.priority DESC").
		Order("pr.id ASC")
}

func unixSeconds(t time.Time) int64 {
	if t.IsZero() {
		return 0
	}
	return t.Unix()
}

func isMissingPromoTableError(err error) bool {
	var mysqlErr *mysql.MySQLError
	if stderrors.As(err, &mysqlErr) {
		return mysqlErr.Number == 1146
	}
	return strings.Contains(err.Error(), "Error 1146")
}
