package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
	resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
	"strings"
	"time"
)

const vipTrialCardDayMS int64 = 24 * 60 * 60 * 1000

type vipTrialCardMetadata struct {
	Level      int32
	DurationMS int64
}

func vipTrialCardResourceMetadata(metadataJSON string) (vipTrialCardMetadata, error) {
	var payload struct {
		VIPLevel     int32 `json:"vip_level"`
		DurationDays int64 `json:"duration_days"`
	}
	if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil {
		return vipTrialCardMetadata{}, err
	}
	if payload.DurationDays < 0 || payload.DurationDays > 36500 {
		return vipTrialCardMetadata{}, fmt.Errorf("vip trial card duration_days is invalid")
	}
	return vipTrialCardMetadata{Level: payload.VIPLevel, DurationMS: payload.DurationDays * vipTrialCardDayMS}, nil
}

// GrantVipTrialCard 原子写入资源发放、背包 entitlement、体验卡元数据和 outbox。
// 发卡时即开始绝对计时，但不会自动佩戴；切卡因此不会暂停、延长或销毁任何卡片。
func (r *Repository) GrantVipTrialCard(ctx context.Context, command ledger.GrantVipTrialCardCommand) (ledger.GrantVipTrialCardReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.GrantVipTrialCardReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	requestHash := stableHash(fmt.Sprintf("vip_trial_card|%s|%d|%d|%d|%d|%s|%d|%s",
		command.AppCode,
		command.TargetUserID,
		command.Level,
		command.DurationMS,
		command.ResourceID,
		command.GrantSource,
		command.OperatorUserID,
		strings.TrimSpace(command.Reason),
	))

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.GrantVipTrialCardReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	nowMS := time.Now().UnixMilli()
	if err := r.lockVipCommand(ctx, tx, command.CommandID, bizTypeVIPTrialCardGrant, requestHash, nowMS, xerr.IdempotencyConflict); err != nil {
		return ledger.GrantVipTrialCardReceipt{}, err
	}
	existing, existingHash, err := r.queryVipTrialCardByCommand(ctx, tx, command.CommandID, nowMS, false)
	if err == nil {
		if existingHash != requestHash {
			return ledger.GrantVipTrialCardReceipt{}, xerr.New(xerr.IdempotencyConflict, "vip trial card command payload changed")
		}
		_ = tx.Rollback()
		return r.vipTrialCardReceiptForCommand(ctx, command, requestHash, existing)
	}
	if !errors.Is(err, sql.ErrNoRows) {
		return ledger.GrantVipTrialCardReceipt{}, err
	}

	resourceID, err := r.resolveVipTrialCardResourceID(ctx, tx, command.Level, command.ResourceID)
	if err != nil {
		return ledger.GrantVipTrialCardReceipt{}, err
	}
	resource, err := r.getResourceForUpdate(ctx, tx, resourceID)
	if err != nil {
		return ledger.GrantVipTrialCardReceipt{}, err
	}
	grantID := resourceGrantID(command.AppCode, command.CommandID)
	if err := r.insertResourceGrant(
		ctx,
		tx,
		grantID,
		command.CommandID,
		command.TargetUserID,
		vipResourceGrantSource(command.GrantSource),
		resourcedomain.GrantSubjectResource,
		fmt.Sprintf("%d", resource.ResourceID),
		requestHash,
		"",
		command.Reason,
		command.OperatorUserID,
		nowMS,
	); err != nil {
		// 普通一致性读不会锁住“不存在的 command_id”。两个并发首发都可能通过前置查询，
		// 但 resource_grants 唯一键会让后提交者等待首笔事务；确认 duplicate 后回滚本事务并
		// 读取已提交卡片，才能稳定返回幂等回执而不是泄漏 MySQL 1062。
		if isMySQLDuplicateError(err) {
			_ = tx.Rollback()
			return r.vipTrialCardReceiptForCommand(ctx, command, requestHash, ledger.VipTrialCard{})
		}
		return ledger.GrantVipTrialCardReceipt{}, err
	}
	result, err := r.grantVipTrialCardItemTx(ctx, tx, vipTrialCardGrantItemCommand{
		GrantID: grantID, CommandID: command.CommandID, RequestHash: requestHash,
		TargetUserID: command.TargetUserID, Resource: resource, ExpectedLevel: command.Level,
		DurationMS: command.DurationMS, GrantSource: command.GrantSource,
		OperatorUserID: command.OperatorUserID, Reason: command.Reason, NowMS: nowMS,
		ValidateGrantSource: true,
	})
	if err != nil {
		return ledger.GrantVipTrialCardReceipt{}, err
	}
	card := result.Card
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		resourceOutboxEvent("ResourceGranted", command.CommandID, command.TargetUserID, resource.ResourceID, map[string]any{
			"grant_id": grantID,
			"resource": resource,
		}, nowMS),
		resourceOutboxEvent("VipTrialCardGranted", command.CommandID, command.TargetUserID, resource.ResourceID, map[string]any{
			"trial_card":     card,
			"program_type":   result.Program.ProgramType,
			"config_version": result.Program.ConfigVersion,
		}, nowMS),
	}); err != nil {
		return ledger.GrantVipTrialCardReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.GrantVipTrialCardReceipt{}, err
	}
	state, err := r.GetVipState(ctx, command.TargetUserID)
	if err != nil {
		return ledger.GrantVipTrialCardReceipt{}, err
	}
	return ledger.GrantVipTrialCardReceipt{TrialCard: card, State: state, ServerTimeMS: nowMS}, nil
}

type vipTrialCardGrantItemCommand struct {
	GrantID             string
	CommandID           string
	RequestHash         string
	TargetUserID        int64
	Resource            resourcedomain.Resource
	ExpectedLevel       int32
	DurationMS          int64
	GrantSource         string
	OperatorUserID      int64
	Reason              string
	NowMS               int64
	EntitlementSnapshot resourceEntitlementSnapshot
	ValidateGrantSource bool
}

type vipTrialCardGrantItemResult struct {
	Item    resourcedomain.ResourceGrantItem
	Card    ledger.VipTrialCard
	Program ledger.VipProgramConfig
}

// grantVipTrialCardItemTx 是专用发卡、通用资源赠送和资源组展开共用的事务内原语。
// 它同时写 entitlement 与 user_vip_trial_cards，确保任何入口都不会产生只有背包资源、
// 没有 VIP 等级投影的半成品体验卡；父级 grant/outbox 仍由各入口按自己的审计语义负责。
func (r *Repository) grantVipTrialCardItemTx(ctx context.Context, tx *sql.Tx, command vipTrialCardGrantItemCommand) (vipTrialCardGrantItemResult, error) {
	if command.DurationMS <= 0 || command.ExpectedLevel <= 0 {
		return vipTrialCardGrantItemResult{}, xerr.New(xerr.InvalidArgument, "vip trial card level and duration are required")
	}
	if ledger.NormalizeVipGrantSource(command.GrantSource) == ledger.VipGrantSourceManagerCenter && command.ExpectedLevel > 6 {
		// This primitive is shared by the dedicated trial-card RPC and generic
		// GrantResource. Enforce the portal ceiling here so a manager_center caller
		// cannot bypass VIP6 by submitting a manager-enabled VIP7+ resource ID.
		return vipTrialCardGrantItemResult{}, xerr.New(xerr.PermissionDenied, "manager center can grant at most VIP6 trial card")
	}
	program, err := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` LOCK IN SHARE MODE`, appcode.FromContext(ctx)))
	if errors.Is(err, sql.ErrNoRows) {
		return vipTrialCardGrantItemResult{}, xerr.New(xerr.NotFound, "vip program config not found")
	}
	if err != nil {
		return vipTrialCardGrantItemResult{}, err
	}
	if program.Status != ledger.VipStatusActive || !program.TrialCardEnabled || program.GrantMode != ledger.VipGrantModeTrialCard {
		return vipTrialCardGrantItemResult{}, xerr.New(xerr.Conflict, "vip trial card is disabled for this app")
	}
	if command.ExpectedLevel > program.LevelCount {
		return vipTrialCardGrantItemResult{}, xerr.New(xerr.InvalidArgument, "vip trial card level exceeds program level_count")
	}
	level, err := r.getVipLevelForUpdate(ctx, tx, command.ExpectedLevel)
	if err != nil {
		return vipTrialCardGrantItemResult{}, err
	}
	if level.Status != ledger.VipStatusActive {
		return vipTrialCardGrantItemResult{}, xerr.New(xerr.VIPLevelDisabled, "vip level is disabled")
	}
	if err := validateVipTrialCardResource(command.Resource, command.ExpectedLevel); err != nil {
		return vipTrialCardGrantItemResult{}, err
	}
	if command.ValidateGrantSource {
		if err := validateGrantableResourceForSource(command.Resource, vipResourceGrantSource(command.GrantSource)); err != nil {
			return vipTrialCardGrantItemResult{}, err
		}
	}
	item, err := r.applyGrantItemWithEntitlementSnapshotOptions(
		ctx, tx, command.GrantID, command.CommandID, command.TargetUserID, command.Resource,
		1, command.DurationMS, command.NowMS, resourcedomain.GrantStrategyNewEntitlement,
		command.EntitlementSnapshot, false,
	)
	if err != nil {
		return vipTrialCardGrantItemResult{}, err
	}
	if item.EntitlementID == "" {
		return vipTrialCardGrantItemResult{}, xerr.New(xerr.LedgerConflict, "vip trial card did not create entitlement")
	}
	expiresAtMS := command.NowMS + command.DurationMS
	trialCardID := "vip_card_" + stableHash(appcode.FromContext(ctx)+"|"+command.CommandID)
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO user_vip_trial_cards (
		   app_code, trial_card_id, command_id, request_hash, user_id, entitlement_id,
		   resource_id, level, name, status, duration_ms, effective_at_ms, expires_at_ms,
		   grant_source, source_grant_id, operator_user_id, reason, program_type,
		   config_version, created_at_ms, updated_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		appcode.FromContext(ctx), trialCardID, command.CommandID, command.RequestHash,
		command.TargetUserID, item.EntitlementID, command.Resource.ResourceID,
		level.Level, level.Name, ledger.VipTrialCardStatusActive, command.DurationMS,
		command.NowMS, expiresAtMS, command.GrantSource, command.GrantID,
		command.OperatorUserID, command.Reason, program.ProgramType, program.ConfigVersion,
		command.NowMS, command.NowMS,
	); err != nil {
		return vipTrialCardGrantItemResult{}, err
	}
	card := ledger.VipTrialCard{
		TrialCardID: trialCardID, EntitlementID: item.EntitlementID, ResourceID: command.Resource.ResourceID,
		UserID: command.TargetUserID, Level: level.Level, Name: level.Name,
		Status: ledger.VipTrialCardStatusActive, DurationMS: command.DurationMS,
		EffectiveAtMS: command.NowMS, ExpiresAtMS: expiresAtMS, RemainingDurationMS: command.DurationMS,
		GrantSource: command.GrantSource, SourceGrantID: command.GrantID,
		CreatedAtMS: command.NowMS, UpdatedAtMS: command.NowMS,
	}
	return vipTrialCardGrantItemResult{Item: item, Card: card, Program: program}, nil
}

func (r *Repository) vipTrialCardReceiptForCommand(ctx context.Context, command ledger.GrantVipTrialCardCommand, requestHash string, known ledger.VipTrialCard) (ledger.GrantVipTrialCardReceipt, error) {
	nowMS := time.Now().UnixMilli()
	card := known
	storedHash := requestHash
	if card.TrialCardID == "" {
		tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
		if err != nil {
			return ledger.GrantVipTrialCardReceipt{}, err
		}
		defer func() { _ = tx.Rollback() }()
		card, storedHash, err = r.queryVipTrialCardByCommand(ctx, tx, command.CommandID, nowMS, false)
		if errors.Is(err, sql.ErrNoRows) {
			return ledger.GrantVipTrialCardReceipt{}, xerr.New(xerr.IdempotencyConflict, "command_id is already used by another resource grant")
		}
		if err != nil {
			return ledger.GrantVipTrialCardReceipt{}, err
		}
		_ = tx.Rollback()
	}
	if storedHash != requestHash || card.UserID != command.TargetUserID {
		return ledger.GrantVipTrialCardReceipt{}, xerr.New(xerr.IdempotencyConflict, "vip trial card command payload changed")
	}
	state, err := r.GetVipState(ctx, command.TargetUserID)
	if err != nil {
		return ledger.GrantVipTrialCardReceipt{}, err
	}
	return ledger.GrantVipTrialCardReceipt{TrialCard: card, State: state, ServerTimeMS: nowMS}, nil
}

// replaceVipTrialCardWithPaidTx 执行“购买等级 >= 当前体验等级”的原子替换规则。
// 卡片来源 grant 可能同时包含其它资源，因此这里只回收精确 entitlement；由该卡派生的
// vip_trial benefit grants 则是卡片专属，可以整体标记 revoked 并回收全部资源实例。
func (r *Repository) replaceVipTrialCardWithPaidTx(
	ctx context.Context,
	tx *sql.Tx,
	card ledger.VipTrialCard,
	paidLevel int32,
	replacementCommandID string,
	nowMS int64,
) error {
	if card.UserID <= 0 || card.EntitlementID == "" || card.TrialCardID == "" || paidLevel < card.Level {
		return xerr.New(xerr.LedgerConflict, "paid vip cannot replace the equipped trial card")
	}
	reason := fmt.Sprintf("replaced by paid VIP%d", paidLevel)
	// revokeGrantedEntitlement 会在 request_id 后继续附加 item_id；短前缀为 BIGINT
	// 最大位数预留空间，确保派生 command_id 始终落在 VARCHAR(128) 边界内。
	requestID := "vtr:" + stableHash(fmt.Sprintf(
		"%s|%d|%s|%s",
		appcode.FromContext(ctx), card.UserID, card.TrialCardID, strings.TrimSpace(replacementCommandID),
	))
	revokeCommand := resourcedomain.RevokeResourceGrantCommand{
		AppCode: appcode.FromContext(ctx), RequestID: requestID,
		GrantID: card.SourceGrantID, Reason: reason, OperatorUserID: card.UserID,
	}

	// source_grant_id 可能是单卡发放，也可能是包含多项资源的组发放。使用已有
	// (app_code,grant_id) 明细索引只找当前卡对应项，不能把同批其它奖励一起撤销。
	cardItems, err := r.listResourceGrantItemsTx(ctx, tx, card.SourceGrantID)
	if err != nil {
		return err
	}
	var cardItem *resourcedomain.ResourceGrantItem
	for index := range cardItems {
		if cardItems[index].ResultType == resourcedomain.ResultEntitlement &&
			cardItems[index].EntitlementID == card.EntitlementID {
			cardItem = &cardItems[index]
			break
		}
	}
	if cardItem == nil {
		return xerr.New(xerr.LedgerConflict, "vip trial card grant item is missing")
	}
	cardEvent, err := r.revokeGrantedEntitlement(
		ctx,
		tx,
		resourcedomain.ResourceGrant{
			AppCode: appcode.FromContext(ctx), GrantID: card.SourceGrantID, TargetUserID: card.UserID,
		},
		*cardItem,
		revokeCommand,
		nowMS,
	)
	if err != nil {
		return err
	}
	outboxEvents := []walletOutboxEvent{cardEvent}

	benefitGrantIDs, err := r.listVipTrialBenefitGrantIDsForUpdateTx(ctx, tx, card)
	if err != nil {
		return err
	}
	for _, grantID := range benefitGrantIDs {
		items, err := r.listResourceGrantItemsTx(ctx, tx, grantID)
		if err != nil {
			return err
		}
		grantRequestID := "vtrb:" + stableHash(requestID+"|"+grantID)
		grantCommand := revokeCommand
		grantCommand.RequestID = grantRequestID
		grantCommand.GrantID = grantID
		for _, item := range items {
			if item.ResultType != resourcedomain.ResultEntitlement {
				// 体验资源 grant 只能包含展示 entitlement；若历史数据混入钱包资产，
				// 必须整笔回滚，不能让“回收体验卡”意外形成金融扣款。
				return xerr.New(xerr.LedgerConflict, "vip trial benefit grant contains a non-entitlement item")
			}
			event, err := r.revokeGrantedEntitlement(
				ctx,
				tx,
				resourcedomain.ResourceGrant{
					AppCode: appcode.FromContext(ctx), GrantID: grantID, TargetUserID: card.UserID,
				},
				item,
				grantCommand,
				nowMS,
			)
			if err != nil {
				return err
			}
			outboxEvents = append(outboxEvents, event)
		}
		result, err := tx.ExecContext(ctx, `
			UPDATE resource_grants
			SET status = ?, revoked_at_ms = ?, revoked_by_user_id = ?,
			    revoke_reason = ?, updated_at_ms = ?
			WHERE app_code = ? AND grant_id = ? AND status = ?`,
			resourcedomain.GrantStatusRevoked, nowMS, card.UserID,
			reason, nowMS, appcode.FromContext(ctx), grantID, resourcedomain.GrantStatusDone,
		)
		if err != nil {
			return err
		}
		affected, err := result.RowsAffected()
		if err != nil {
			return err
		}
		if affected != 1 {
			return xerr.New(xerr.LedgerConflict, "vip trial benefit grant revoke state conflict")
		}
		outboxEvents = append(outboxEvents, resourceOutboxEvent(
			"ResourceGrantRevoked",
			grantRequestID,
			card.UserID,
			0,
			map[string]any{
				"action":                 "replace_vip_trial_with_paid",
				"grant_id":               grantID,
				"trial_card_id":          card.TrialCardID,
				"paid_level":             paidLevel,
				"revoked_at_ms":          nowMS,
				"revoke_reason":          reason,
				"replacement_command_id": replacementCommandID,
			},
			nowMS,
		))
	}
	outboxEvents = append(outboxEvents, resourceOutboxEvent(
		"VipEffectiveChanged",
		requestID,
		card.UserID,
		card.ResourceID,
		map[string]any{
			"source":                 ledger.VipEffectiveSourcePaid,
			"level":                  paidLevel,
			"replaced_source":        ledger.VipEffectiveSourceTrial,
			"trial_card_id":          card.TrialCardID,
			"entitlement_id":         card.EntitlementID,
			"replacement_command_id": replacementCommandID,
			"updated_at_ms":          nowMS,
		},
		nowMS,
	))
	return r.insertWalletOutbox(ctx, tx, outboxEvents)
}

// listVipTrialBenefitGrantIDsForUpdateTx 从单用户发放索引定位该卡的专属体验资源。
// app_code + target_user_id 是 idx_resource_grants_target_time 的左前缀；后续按来源和
// 卡片 subject 过滤，不扫描全 App 发放历史。grant_id 排序让并发清理锁序稳定。
func (r *Repository) listVipTrialBenefitGrantIDsForUpdateTx(
	ctx context.Context,
	tx *sql.Tx,
	card ledger.VipTrialCard,
) ([]string, error) {
	rows, err := tx.QueryContext(ctx, `
		SELECT grant_id
		FROM resource_grants FORCE INDEX (idx_resource_grants_target_time)
		WHERE app_code = ? AND target_user_id = ? AND grant_source = ?
		  AND grant_subject_type = ? AND grant_subject_id = ? AND status = ?
		ORDER BY created_at_ms ASC, grant_id ASC
		FOR UPDATE`,
		appcode.FromContext(ctx),
		card.UserID,
		resourcedomain.GrantSourceVIPTrial,
		resourcedomain.GrantSubjectGroup,
		vipTrialBenefitGrantSubjectID(appcode.FromContext(ctx), card.TrialCardID),
		resourcedomain.GrantStatusDone,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	grantIDs := make([]string, 0, 2)
	for rows.Next() {
		var grantID string
		if err := rows.Scan(&grantID); err != nil {
			return nil, err
		}
		grantIDs = append(grantIDs, grantID)
	}
	return grantIDs, rows.Err()
}

// EquipVipTrialCard 只更新 vip_trial_card 单选装备槽。目标 entitlement 必须属于当前用户、
// 仍在绝对有效期内且等级不低于用户当前 paid/trial 最终等级。
func (r *Repository) EquipVipTrialCard(ctx context.Context, command ledger.EquipVipTrialCardCommand) (ledger.VipTrialCard, ledger.VipState, error) {
	if r == nil || r.db == nil {
		return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.VipTrialCard{}, ledger.VipState{}, err
	}
	defer func() { _ = tx.Rollback() }()
	nowMS := time.Now().UnixMilli()
	var equipmentCommand resourcedomain.EquipUserResourceCommand
	if command.EquipmentCommandID != "" {
		equipmentCommand = resourcedomain.EquipUserResourceCommand{
			AppCode: command.AppCode, CommandID: command.EquipmentCommandID, UserID: command.UserID,
			ResourceID: command.ResourceID, EntitlementID: command.EntitlementID,
		}
		// 通用入口的命令 guard 必须先于 program、entitlement 和 VIP 有效态读取加锁；
		// 并发重试会等待首事务并读取快照，不会在到期边界重新执行状态机。
		_, replayed, err := r.lockResourceEquipmentCommand(ctx, tx, equipmentCommand, nowMS)
		if err != nil {
			return ledger.VipTrialCard{}, ledger.VipState{}, err
		}
		if replayed {
			return ledger.VipTrialCard{}, ledger.VipState{}, nil
		}
	}
	program, err := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` LOCK IN SHARE MODE`, command.AppCode))
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPProgramInactive, "vip program config not found")
	}
	if err != nil {
		return ledger.VipTrialCard{}, ledger.VipState{}, err
	}
	if program.Status != ledger.VipStatusActive || !program.TrialCardEnabled {
		return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPProgramInactive, "vip trial card is disabled for this app")
	}
	// membership 主键行既提供付费 VIP 快照，也作为同一用户体验卡切换的稳定串行化点。
	// 即使 equipment 尚无记录，并发 VIP6/VIP3 佩戴也会按此锁排队，后到的低等级卡
	// 必须看到先提交的高等级卡，不能再次把最终 VIP 覆盖回低等级。
	currentPaid, err := r.ensureUserVipForUpdate(ctx, tx, command.UserID, nowMS)
	if err != nil {
		return ledger.VipTrialCard{}, ledger.VipState{}, err
	}
	currentEffectiveLevel := int32(0)
	if currentPaid.Active {
		currentEffectiveLevel = currentPaid.Level
	}
	currentTrial, err := r.queryEquippedVipTrialCardWithQuery(ctx, tx, command.UserID, nowMS)
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		return ledger.VipTrialCard{}, ledger.VipState{}, err
	}
	if err == nil && currentTrial.Level > currentEffectiveLevel {
		currentEffectiveLevel = currentTrial.Level
	}
	// 锁序必须与 RevokeResourceGrant 保持 entitlement -> trial_card；反向先锁 card 会和后台撤销形成死锁。
	// entitlement 同时承担并发有效性门禁，避免撤销在校验后抢先提交又被重新写回 equipment。
	entitlement, err := r.getUserResourceEntitlementForUpdateTx(ctx, tx, command.EntitlementID)
	if err != nil {
		if xerr.IsCode(err, xerr.NotFound) {
			return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPTrialCardNotFound, "vip trial card entitlement not found")
		}
		return ledger.VipTrialCard{}, ledger.VipState{}, err
	}
	card, err := r.queryVipTrialCardForUpdate(ctx, tx, command.UserID, command.EntitlementID, nowMS)
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPTrialCardNotFound, "vip trial card not found")
	}
	if err != nil {
		return ledger.VipTrialCard{}, ledger.VipState{}, err
	}
	if card.ExpiresAtMS <= nowMS || (entitlement.ExpiresAtMS > 0 && entitlement.ExpiresAtMS <= nowMS) {
		return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPTrialCardExpired, "vip trial card expired")
	}
	if entitlement.UserID != command.UserID || entitlement.ResourceID != card.ResourceID ||
		entitlement.Status != resourcedomain.StatusActive || entitlement.EffectiveAtMS > nowMS ||
		card.Status != ledger.VipTrialCardStatusActive || card.EffectiveAtMS > nowMS || entitlement.RemainingQuantity <= 0 {
		return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPTrialCardNotFound, "vip trial card entitlement is inactive")
	}
	if resourcedomain.NormalizeResourceType(entitlement.Resource.ResourceType) != resourcedomain.TypeVIPTrialCard {
		return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.Conflict, "vip trial card resource type mismatch")
	}
	if card.Level < currentEffectiveLevel {
		level := fmt.Sprintf("%d", currentEffectiveLevel)
		return ledger.VipTrialCard{}, ledger.VipState{}, xerr.NewWithMetadata(
			xerr.VIPDowngradeNotAllowed,
			"vip trial card level is below current effective vip",
			map[string]string{
				"current_effective_level": level,
				"required_level":          level,
			},
		)
	}

	var currentEntitlementID string
	err = tx.QueryRowContext(ctx, `
		SELECT entitlement_id FROM user_resource_equipment
		WHERE app_code = ? AND user_id = ? AND resource_type = ?
		LIMIT 1 FOR UPDATE`, command.AppCode, command.UserID, resourcedomain.TypeVIPTrialCard).Scan(&currentEntitlementID)
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		return ledger.VipTrialCard{}, ledger.VipState{}, err
	}
	if currentEntitlementID != card.EntitlementID {
		if err := r.equipUserResourceEntitlementTx(ctx, tx, command.UserID, entitlement, nowMS); err != nil {
			return ledger.VipTrialCard{}, ledger.VipState{}, err
		}
		if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
			resourceOutboxEvent("UserResourceChanged", command.RequestID, command.UserID, card.ResourceID, map[string]any{
				"action":         "equip",
				"resource_type":  resourcedomain.TypeVIPTrialCard,
				"entitlement_id": card.EntitlementID,
				"updated_at_ms":  nowMS,
			}, nowMS),
			resourceOutboxEvent("VipEffectiveChanged", command.RequestID, command.UserID, card.ResourceID, map[string]any{
				"source":         ledger.VipEffectiveSourceTrial,
				"level":          card.Level,
				"entitlement_id": card.EntitlementID,
				"updated_at_ms":  nowMS,
			}, nowMS),
		}); err != nil {
			return ledger.VipTrialCard{}, ledger.VipState{}, err
		}
	}
	if program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 {
		// 体验卡只下发当前等级显式绑定的真实 benefit resources，不复用可能含钱包奖励的付费资源组；
		// grant_id 由 trial_card_id 稳定派生，来源和有效期与付费会员 entitlement 完全隔离。
		if err := r.grantVipTrialBenefitResourcesTx(ctx, tx, card, program.ConfigVersion, nowMS); err != nil {
			return ledger.VipTrialCard{}, ledger.VipState{}, err
		}
		// 首次佩戴或等级提升时采用新等级默认素材；同等级卡片切换仍尊重当前合法选择。
		preserveValidSelection := currentEffectiveLevel > 0 && card.Level <= currentEffectiveLevel
		if err := r.reconcileTieredVipResourceEquipmentTx(ctx, tx, command.UserID, card.Level, ledger.VipEffectiveSourceTrial, command.RequestID, nowMS, preserveValidSelection); err != nil {
			return ledger.VipTrialCard{}, ledger.VipState{}, err
		}
	}
	if command.EquipmentCommandID != "" {
		entitlement.Equipped = true
		if err := r.completeResourceEquipmentCommandTx(ctx, tx, equipmentCommand, entitlement, nowMS); err != nil {
			return ledger.VipTrialCard{}, ledger.VipState{}, err
		}
	}
	if err := tx.Commit(); err != nil {
		return ledger.VipTrialCard{}, ledger.VipState{}, err
	}
	card.Equipped = true
	state, err := r.GetVipState(ctx, command.UserID)
	return card, state, err
}

// UnequipVipTrialCard 清理当前装备指针，不更新卡片或 entitlement，保证之后仍可在剩余绝对有效期内重新佩戴。
func (r *Repository) UnequipVipTrialCard(ctx context.Context, command ledger.UnequipVipTrialCardCommand) (bool, ledger.VipState, error) {
	if r == nil || r.db == nil {
		return false, ledger.VipState{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return false, ledger.VipState{}, err
	}
	defer func() { _ = tx.Rollback() }()
	nowMS := time.Now().UnixMilli()
	result, err := tx.ExecContext(ctx, `
		DELETE FROM user_resource_equipment
		WHERE app_code = ? AND user_id = ? AND resource_type = ?`,
		command.AppCode, command.UserID, resourcedomain.TypeVIPTrialCard,
	)
	if err != nil {
		return false, ledger.VipState{}, err
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return false, ledger.VipState{}, err
	}
	if affected > 0 {
		program, programErr := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` LOCK IN SHARE MODE`, command.AppCode))
		if programErr != nil && !errors.Is(programErr, sql.ErrNoRows) {
			return false, ledger.VipState{}, programErr
		}
		if programErr == nil && program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 {
			effectiveLevel := int32(0)
			paid, paidErr := r.queryUserVip(ctx, tx, command.UserID, nowMS, false)
			if paidErr != nil && !errors.Is(paidErr, sql.ErrNoRows) {
				return false, ledger.VipState{}, paidErr
			}
			if paidErr == nil && program.Status == ledger.VipStatusActive && paid.Active {
				effectiveLevel = paid.Level
				paid.ProgramType = program.ProgramType
				paid.ConfigVersion = program.ConfigVersion
				if _, _, err := r.grantPaidVipBenefitResourcesTx(ctx, tx, paid, true, nowMS); err != nil {
					return false, ledger.VipState{}, err
				}
			}
			// 卸下体验卡后回落到付费会员或 none；同事务清掉体验卡等级资源并恢复付费等级 auto_equip。
			effectiveSource := ledger.VipEffectiveSourceNone
			if effectiveLevel > 0 {
				effectiveSource = ledger.VipEffectiveSourcePaid
			}
			if err := r.reconcileTieredVipResourceEquipmentTx(ctx, tx, command.UserID, effectiveLevel, effectiveSource, command.RequestID, nowMS, true); err != nil {
				return false, ledger.VipState{}, err
			}
		}
		if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
			resourceOutboxEvent("UserResourceChanged", command.RequestID, command.UserID, 0, map[string]any{
				"action":        "unequip",
				"resource_type": resourcedomain.TypeVIPTrialCard,
				"updated_at_ms": nowMS,
			}, nowMS),
			resourceOutboxEvent("VipEffectiveChanged", command.RequestID, command.UserID, 0, map[string]any{
				"source":        "paid_or_none",
				"updated_at_ms": nowMS,
			}, nowMS),
		}); err != nil {
			return false, ledger.VipState{}, err
		}
	}
	if err := tx.Commit(); err != nil {
		return false, ledger.VipState{}, err
	}
	state, err := r.GetVipState(ctx, command.UserID)
	return affected > 0, state, err
}

func (r *Repository) resolveVipTrialCardResourceID(ctx context.Context, tx *sql.Tx, level int32, requested int64) (int64, error) {
	if requested > 0 {
		return requested, nil
	}
	var resourceID int64
	err := tx.QueryRowContext(ctx, `
		SELECT resource_id FROM resources
		WHERE app_code = ? AND resource_code = ?
		LIMIT 1 FOR UPDATE`,
		appcode.FromContext(ctx), fmt.Sprintf("vip_trial_card_%d", level),
	).Scan(&resourceID)
	if errors.Is(err, sql.ErrNoRows) {
		return 0, xerr.New(xerr.NotFound, "vip trial card resource not configured")
	}
	return resourceID, err
}

func validateVipTrialCardResource(resource resourcedomain.Resource, expectedLevel int32) error {
	if resourcedomain.NormalizeResourceType(resource.ResourceType) != resourcedomain.TypeVIPTrialCard {
		return xerr.New(xerr.InvalidArgument, "resource is not a vip trial card")
	}
	metadata, err := vipTrialCardResourceMetadata(resource.MetadataJSON)
	if err != nil || metadata.Level != expectedLevel {
		return xerr.New(xerr.Conflict, "vip trial card resource level mismatch")
	}
	if resourcedomain.NormalizeGrantStrategy(resource.GrantStrategy) != resourcedomain.GrantStrategyNewEntitlement {
		return xerr.New(xerr.Conflict, "vip trial card must use new_entitlement strategy")
	}
	return nil
}

func vipResourceGrantSource(source string) string {
	switch ledger.NormalizeVipGrantSource(source) {
	case ledger.VipGrantSourceManagerCenter:
		return resourcedomain.GrantSourceManagerCenter
	case ledger.VipGrantSourceAdmin:
		return resourcedomain.GrantSourceAdmin
	default:
		return strings.ToLower(strings.TrimSpace(source))
	}
}

func (r *Repository) queryVipTrialCardByCommand(ctx context.Context, tx *sql.Tx, commandID string, nowMS int64, lock bool) (ledger.VipTrialCard, string, error) {
	query := `
		SELECT trial_card_id, entitlement_id, resource_id, user_id, level, name, status,
		       duration_ms, effective_at_ms, expires_at_ms, grant_source, source_grant_id,
		       created_at_ms, updated_at_ms, request_hash
		FROM user_vip_trial_cards
		WHERE app_code = ? AND command_id = ?`
	if lock {
		query += ` FOR UPDATE`
	}
	var card ledger.VipTrialCard
	var requestHash string
	err := tx.QueryRowContext(ctx, query, appcode.FromContext(ctx), commandID).Scan(
		&card.TrialCardID,
		&card.EntitlementID,
		&card.ResourceID,
		&card.UserID,
		&card.Level,
		&card.Name,
		&card.Status,
		&card.DurationMS,
		&card.EffectiveAtMS,
		&card.ExpiresAtMS,
		&card.GrantSource,
		&card.SourceGrantID,
		&card.CreatedAtMS,
		&card.UpdatedAtMS,
		&requestHash,
	)
	if err == nil && card.ExpiresAtMS > nowMS {
		card.RemainingDurationMS = card.ExpiresAtMS - nowMS
	}
	return card, requestHash, err
}

func (r *Repository) queryVipTrialCardForUpdate(ctx context.Context, tx *sql.Tx, userID int64, entitlementID string, nowMS int64) (ledger.VipTrialCard, error) {
	row := tx.QueryRowContext(ctx, `
		SELECT trial_card_id, entitlement_id, resource_id, user_id, level, name, status,
		       duration_ms, effective_at_ms, expires_at_ms, grant_source, source_grant_id,
		       created_at_ms, updated_at_ms
		FROM user_vip_trial_cards
		WHERE app_code = ? AND user_id = ? AND entitlement_id = ?
		FOR UPDATE`,
		appcode.FromContext(ctx), userID, entitlementID,
	)
	return scanVipTrialCard(row, nowMS)
}
