package common

import (
	"context"
	"encoding/json"
	"time"

	"github.com/perfect-panel/server/internal/model/promo"
	"github.com/perfect-panel/server/internal/model/user"
	"github.com/perfect-panel/server/internal/svc"
	"github.com/perfect-panel/server/pkg/xerr"
	"github.com/pkg/errors"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type PromoResult struct {
	Eligible   bool
	RuleID     int64
	RuleName   string
	RuleType   string
	PromoPrice int64
	ExpiresAt  time.Time
}

type promoRuleParams struct {
	WindowHours    int `json:"window_hours"`
	InactiveMonths int `json:"inactive_months"`
}

// EvaluatePromo decides whether the given (user, subscribe, quantity) tuple
// qualifies for any active promo rule. Each rule type has its own gating:
//   - new_user: requires isFirstPurchase=true (no prior paid subscription)
//   - inactive_user: requires a previously expired subscription (rule self-check)
//   - campaign: applies unconditionally within the configured time window
//
// Pass isFirstPurchase=true on order paths where the request is being routed
// as a brand-new purchase; pass false when the user already has a paid
// subscription (including expired ones) so NewUser cannot be reused.
func EvaluatePromo(ctx context.Context, svcCtx *svc.ServiceContext, userID int64, subscribeID int64, quantity int64, isFirstPurchase bool) (*PromoResult, error) {
	result := &PromoResult{}
	if svcCtx == nil || svcCtx.PromoModel == nil || svcCtx.DB == nil || subscribeID <= 0 || quantity <= 0 {
		return result, nil
	}

	rules, err := svcCtx.PromoModel.QueryEligibleRules(ctx, subscribeID, quantity)
	if err != nil {
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo rules failed: %v", err.Error())
	}
	if len(rules) == 0 {
		return result, nil
	}

	var currentUser user.User
	now := time.Now()
	for _, rule := range rules {
		if rule == nil || !isPromoRuleInTimeWindow(rule, now) {
			continue
		}
		if rule.PromoPrice <= 0 {
			continue
		}

		params := promoRuleParams{}
		if rule.Params != "" {
			if err = json.Unmarshal([]byte(rule.Params), &params); err != nil {
				continue
			}
		}

		eligible, expiresAt, err := evaluatePromoRule(ctx, svcCtx.DB, rule, params, userID, isFirstPurchase, &currentUser, now)
		if err != nil {
			return nil, err
		}
		if !eligible {
			continue
		}

		return &PromoResult{
			Eligible:   true,
			RuleID:     rule.Id,
			RuleName:   rule.Name,
			RuleType:   rule.Type,
			PromoPrice: rule.PromoPrice,
			ExpiresAt:  expiresAt,
		}, nil
	}

	return result, nil
}

func isPromoRuleInTimeWindow(rule *promo.RuleWithPrice, now time.Time) bool {
	if rule.StartTime != nil && !rule.StartTime.IsZero() && now.Before(*rule.StartTime) {
		return false
	}
	if rule.EndTime != nil && !rule.EndTime.IsZero() && now.After(*rule.EndTime) {
		return false
	}
	return true
}

func evaluatePromoRule(
	ctx context.Context,
	db *gorm.DB,
	rule *promo.RuleWithPrice,
	params promoRuleParams,
	userID int64,
	isFirstPurchase bool,
	currentUser *user.User,
	now time.Time,
) (bool, time.Time, error) {
	switch rule.Type {
	case promo.RuleTypeNewUser:
		if userID <= 0 || !isFirstPurchase {
			return false, time.Time{}, nil
		}
		return evaluateNewUserPromo(ctx, db, params, userID, currentUser, now)
	case promo.RuleTypeInactiveUser:
		if userID <= 0 {
			return false, time.Time{}, nil
		}
		return evaluateInactiveUserPromo(ctx, db, params, userID, promoRuleExpiresAt(rule), now)
	case promo.RuleTypeCampaign:
		return true, promoRuleExpiresAt(rule), nil
	default:
		return false, time.Time{}, nil
	}
}

func evaluateNewUserPromo(
	ctx context.Context,
	db *gorm.DB,
	params promoRuleParams,
	userID int64,
	currentUser *user.User,
	now time.Time,
) (bool, time.Time, error) {
	if params.WindowHours <= 0 {
		return false, time.Time{}, nil
	}

	if currentUser.Id == 0 {
		if err := db.WithContext(ctx).Model(&user.User{}).Where("id = ?", userID).First(currentUser).Error; err != nil {
			return false, time.Time{}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo user failed")
		}
	}

	expiresAt := currentUser.CreatedAt.Add(time.Duration(params.WindowHours) * time.Hour)
	return now.Before(expiresAt), expiresAt, nil
}

func evaluateInactiveUserPromo(
	ctx context.Context,
	db *gorm.DB,
	params promoRuleParams,
	userID int64,
	ruleExpiresAt time.Time,
	now time.Time,
) (bool, time.Time, error) {
	if params.InactiveMonths <= 0 {
		return false, time.Time{}, nil
	}

	var lastSub user.Subscribe
	err := db.WithContext(ctx).
		Model(&user.Subscribe{}).
		Where("user_id = ?", userID).
		Order(clause.OrderBy{
			Expression: clause.Expr{
				SQL:  "CASE WHEN expire_time = ? THEN 0 ELSE 1 END, expire_time DESC",
				Vars: []interface{}{time.UnixMilli(0)},
			},
		}).
		Limit(1).
		Take(&lastSub).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return false, time.Time{}, nil
		}
		return false, time.Time{}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo inactive user subscription failed")
	}

	return evaluateInactiveUserExpire(lastSub.ExpireTime, params, now), ruleExpiresAt, nil
}

func evaluateInactiveUserExpire(lastExpire time.Time, params promoRuleParams, now time.Time) bool {
	if lastExpire.Equal(time.UnixMilli(0)) {
		return false
	}

	threshold := now.AddDate(0, -params.InactiveMonths, 0)
	return lastExpire.Before(threshold) || lastExpire.Equal(threshold)
}

func promoRuleExpiresAt(rule *promo.RuleWithPrice) time.Time {
	if rule != nil && rule.EndTime != nil {
		return *rule.EndTime
	}
	return time.Time{}
}
