package mysql

import (
	"context"
	"database/sql"
	"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"
)

func (r *Repository) ListResourceGroups(ctx context.Context, query resourcedomain.ListResourceGroupsQuery) ([]resourcedomain.ResourceGroup, int64, error) {
	if r == nil || r.db == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, query.AppCode)
	query.AppCode = appcode.FromContext(ctx)
	query = normalizeResourceGroupListQuery(query)

	where, args := resourceGroupWhereSQL(query)
	total, err := r.countResourceRows(ctx, "resource_groups", where, args...)
	if err != nil {
		return nil, 0, err
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, group_id, group_code, name, status, description, sort_order,
		       created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
		FROM resource_groups
		`+where+`
		ORDER BY sort_order DESC, group_id DESC
		LIMIT ? OFFSET ?`,
		append(args, query.PageSize, resourceOffset(query.Page, query.PageSize))...,
	)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()

	groups := make([]resourcedomain.ResourceGroup, 0, query.PageSize)
	for rows.Next() {
		group, err := scanResourceGroup(rows)
		if err != nil {
			return nil, 0, err
		}
		group.Items, err = r.listResourceGroupItems(ctx, group.GroupID)
		if err != nil {
			return nil, 0, err
		}
		groups = append(groups, group)
	}
	if err := rows.Err(); err != nil {
		return nil, 0, err
	}
	return groups, total, nil
}

func (r *Repository) GetResourceGroup(ctx context.Context, groupID int64) (resourcedomain.ResourceGroup, error) {
	if r == nil || r.db == nil {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	group, err := r.getResourceGroup(ctx, r.db, groupID, false)
	if err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	group.Items, err = r.listResourceGroupItems(ctx, groupID)
	if err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	// Gift catalog facts are not exposed as mutable group fields, but they participate in the owner content
	// hash returned by GetResourceGroup and must therefore be resolved exactly like the later pin operation.
	if err := r.enrichResourceGroupGiftSnapshots(ctx, r.db, group.Items, time.Now().UTC().UnixMilli(), false); err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	return group, nil
}

func (r *Repository) CreateResourceGroup(ctx context.Context, command resourcedomain.ResourceGroupCommand) (resourcedomain.ResourceGroup, error) {
	if r == nil || r.db == nil {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	command = normalizeResourceGroupCommand(command)
	if err := validateResourceGroupCommand(command, true); err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	defer func() { _ = tx.Rollback() }()

	nowMs := time.Now().UnixMilli()
	result, err := tx.ExecContext(ctx, `
		INSERT INTO resource_groups (
		   app_code, group_code, name, status, description, sort_order,
		   created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		command.AppCode, command.GroupCode, command.Name, command.Status, command.Description, command.SortOrder,
		command.OperatorUserID, command.OperatorUserID, nowMs, nowMs,
	)
	if err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	groupID, err := result.LastInsertId()
	if err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	if err := r.replaceResourceGroupItemsTx(ctx, tx, groupID, command.Items, command.Status == resourcedomain.StatusActive, nowMs); err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		resourceOutboxEvent("ResourceGroupChanged", fmt.Sprintf("resource_group:%d:create:%d", groupID, nowMs), 0, groupID, command, nowMs),
	}); err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	if err := tx.Commit(); err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	return r.GetResourceGroup(ctx, groupID)
}

func (r *Repository) UpdateResourceGroup(ctx context.Context, command resourcedomain.ResourceGroupCommand) (resourcedomain.ResourceGroup, error) {
	if r == nil || r.db == nil {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if command.GroupID <= 0 {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.InvalidArgument, "group_id is required")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	command = normalizeResourceGroupCommand(command)
	if err := validateResourceGroupCommand(command, true); err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	defer func() { _ = tx.Rollback() }()

	nowMs := time.Now().UnixMilli()
	result, err := tx.ExecContext(ctx, `
		UPDATE resource_groups
		SET group_code = ?, name = ?, status = ?, description = ?, sort_order = ?,
		    updated_by_user_id = ?, updated_at_ms = ?
		WHERE app_code = ? AND group_id = ?`,
		command.GroupCode, command.Name, command.Status, command.Description, command.SortOrder,
		command.OperatorUserID, nowMs, command.AppCode, command.GroupID,
	)
	if err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	if affected, err := result.RowsAffected(); err != nil {
		return resourcedomain.ResourceGroup{}, err
	} else if affected == 0 {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.NotFound, "resource group not found")
	}
	if err := r.replaceResourceGroupItemsTx(ctx, tx, command.GroupID, command.Items, command.Status == resourcedomain.StatusActive, nowMs); err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		resourceOutboxEvent("ResourceGroupChanged", fmt.Sprintf("resource_group:%d:update:%d", command.GroupID, nowMs), 0, command.GroupID, command, nowMs),
	}); err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	if err := tx.Commit(); err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	return r.GetResourceGroup(ctx, command.GroupID)
}

func (r *Repository) SetResourceGroupStatus(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.ResourceGroup, error) {
	if r == nil || r.db == nil {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if command.ID <= 0 {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.InvalidArgument, "group_id is required")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.Status = resourcedomain.NormalizeStatus(command.Status)
	// deleted 仅供受独立删除权限保护的管理端入口使用；创建、编辑仍只接受 active/disabled。
	if !resourcedomain.ValidStatus(command.Status) && command.Status != resourcedomain.StatusDeleted {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.InvalidArgument, "status is invalid")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	defer func() { _ = tx.Rollback() }()

	if _, err := r.getResourceGroup(ctx, tx, command.ID, true); err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	if command.Status == resourcedomain.StatusActive {
		items, err := r.listResourceGroupItemsTx(ctx, tx, command.ID, true)
		if err != nil {
			return resourcedomain.ResourceGroup{}, err
		}
		if err := validateActiveResourceGroupItems(items); err != nil {
			return resourcedomain.ResourceGroup{}, err
		}
	}
	nowMs := time.Now().UnixMilli()
	result, err := tx.ExecContext(ctx,
		`UPDATE resource_groups SET status = ?, updated_by_user_id = ?, updated_at_ms = ? WHERE app_code = ? AND group_id = ?`,
		command.Status, command.OperatorUserID, nowMs, appcode.FromContext(ctx), command.ID,
	)
	if err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	if affected, err := result.RowsAffected(); err != nil {
		return resourcedomain.ResourceGroup{}, err
	} else if affected == 0 {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.NotFound, "resource group not found")
	}
	group, err := r.getResourceGroup(ctx, tx, command.ID, false)
	if err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	group.Items, err = r.listResourceGroupItemsTx(ctx, tx, command.ID, false)
	if err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		resourceOutboxEvent("ResourceGroupChanged", fmt.Sprintf("resource_group:%d:status:%s:%d", command.ID, command.Status, nowMs), 0, command.ID, group, nowMs),
	}); err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	if err := tx.Commit(); err != nil {
		return resourcedomain.ResourceGroup{}, err
	}
	return group, nil
}

func (r *Repository) getResourceGroup(ctx context.Context, querier sqlRowQuerier, groupID int64, lock bool) (resourcedomain.ResourceGroup, error) {
	query := `
		SELECT app_code, group_id, group_code, name, status, description, sort_order,
		       created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
		FROM resource_groups
		WHERE app_code = ? AND group_id = ?`
	if lock {
		query += ` FOR UPDATE`
	}
	row := querier.QueryRowContext(ctx, query, appcode.FromContext(ctx), groupID)
	group, err := scanResourceGroup(row)
	if errors.Is(err, sql.ErrNoRows) {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.NotFound, "resource group not found")
	}
	return group, err
}

func (r *Repository) listResourceGroupItems(ctx context.Context, groupID int64) ([]resourcedomain.ResourceGroupItem, error) {
	return r.listResourceGroupItemsWithQuery(ctx, r.db, groupID)
}

func (r *Repository) listResourceGroupItemsTx(ctx context.Context, tx *sql.Tx, groupID int64, lock bool) ([]resourcedomain.ResourceGroupItem, error) {
	if lock {
		if err := r.lockResourceGroupItemRows(ctx, tx, groupID); err != nil {
			return nil, err
		}
	}
	items, err := r.listResourceGroupItemsWithQuery(ctx, tx, groupID)
	if err != nil || !lock {
		return items, err
	}
	for index := range items {
		if resourcedomain.NormalizeGroupItemType(items[index].ItemType) != resourcedomain.GroupItemTypeResource {
			continue
		}
		resource, err := r.getResourceForUpdate(ctx, tx, items[index].ResourceID)
		if err != nil {
			return nil, err
		}
		items[index].Resource = resource
	}
	return items, nil
}

func (r *Repository) listResourceGroupItemsWithQuery(ctx context.Context, querier interface {
	QueryContext(context.Context, string, ...any) (*sql.Rows, error)
}, groupID int64) ([]resourcedomain.ResourceGroupItem, error) {
	query := `
		SELECT i.group_item_id, i.group_id, i.item_type, i.resource_id,
		       i.wallet_asset_type, i.wallet_asset_amount,
		       ` + nullableResourceColumnsWithAlias("r") + `,
		       i.quantity, i.duration_ms, i.sort_order, i.created_at_ms, i.updated_at_ms
		FROM resource_group_items i
		LEFT JOIN resources r ON r.app_code = i.app_code AND r.resource_id = i.resource_id
		WHERE i.app_code = ? AND i.group_id = ?
		ORDER BY i.sort_order DESC, i.group_item_id ASC`
	rows, err := querier.QueryContext(ctx, query, appcode.FromContext(ctx), groupID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := make([]resourcedomain.ResourceGroupItem, 0)
	for rows.Next() {
		item, err := scanResourceGroupItem(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func (r *Repository) lockResourceGroupItemRows(ctx context.Context, tx *sql.Tx, groupID int64) error {
	rows, err := tx.QueryContext(ctx, `
		SELECT group_item_id
		FROM resource_group_items
		WHERE app_code = ? AND group_id = ?
		ORDER BY sort_order DESC, group_item_id ASC
		FOR UPDATE`,
		appcode.FromContext(ctx), groupID,
	)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var id int64
		if err := rows.Scan(&id); err != nil {
			return err
		}
	}
	return rows.Err()
}

func (r *Repository) replaceResourceGroupItemsTx(ctx context.Context, tx *sql.Tx, groupID int64, items []resourcedomain.ResourceGroupItemInput, requireGrantable bool, nowMs int64) error {
	seenResources := make(map[int64]struct{}, len(items))
	seenWalletAssets := make(map[string]struct{}, len(items))
	if _, err := tx.ExecContext(ctx, `DELETE FROM resource_group_items WHERE app_code = ? AND group_id = ?`, appcode.FromContext(ctx), groupID); err != nil {
		return err
	}
	for _, item := range items {
		item = normalizeResourceGroupItemInput(item)
		switch item.ItemType {
		case resourcedomain.GroupItemTypeResource:
			if item.ResourceID <= 0 || item.Quantity <= 0 {
				return xerr.New(xerr.InvalidArgument, "resource group item is invalid")
			}
			if _, ok := seenResources[item.ResourceID]; ok {
				return xerr.New(xerr.InvalidArgument, "resource group contains duplicate resource")
			}
			seenResources[item.ResourceID] = struct{}{}
			resource, err := r.getResourceForUpdate(ctx, tx, item.ResourceID)
			if err != nil {
				return err
			}
			if requireGrantable {
				if err := validateActiveResourceGroupResource(resource); err != nil {
					return err
				}
			}
			if resourcedomain.NormalizeResourceType(resource.ResourceType) == resourcedomain.TypeVIPTrialCard {
				// 体验卡天数属于资源本身；资源组只引用该承诺，不能用条目里的任意天数
				// 悄悄改写。仅对没有 duration_days 的历史标准资源保留旧条目时长。
				_, durationMS, err := resolveVipTrialCardGrantFacts(resource, item.DurationMS)
				if err != nil {
					return err
				}
				item.Quantity = 1
				item.DurationMS = durationMS
			}
			if _, err := tx.ExecContext(ctx, `
				INSERT INTO resource_group_items (
				   app_code, group_id, item_type, resource_id, wallet_asset_type, wallet_asset_amount,
				   quantity, duration_ms, sort_order, created_at_ms, updated_at_ms
				 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
				appcode.FromContext(ctx), groupID, item.ItemType, item.ResourceID, "", int64(0),
				item.Quantity, item.DurationMS, item.SortOrder, nowMs, nowMs,
			); err != nil {
				return err
			}
		case resourcedomain.GroupItemTypeWalletAsset:
			if !validGroupWalletAssetType(item.WalletAssetType) || item.WalletAssetAmount <= 0 {
				return xerr.New(xerr.InvalidArgument, "resource group wallet asset item is invalid")
			}
			if _, ok := seenWalletAssets[item.WalletAssetType]; ok {
				return xerr.New(xerr.InvalidArgument, "resource group contains duplicate wallet asset")
			}
			seenWalletAssets[item.WalletAssetType] = struct{}{}
			if _, err := tx.ExecContext(ctx, `
				INSERT INTO resource_group_items (
				   app_code, group_id, item_type, resource_id, wallet_asset_type, wallet_asset_amount,
				   quantity, duration_ms, sort_order, created_at_ms, updated_at_ms
				 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
				appcode.FromContext(ctx), groupID, item.ItemType, int64(0), item.WalletAssetType, item.WalletAssetAmount,
				int64(1), int64(0), item.SortOrder, nowMs, nowMs,
			); err != nil {
				return err
			}
		default:
			return xerr.New(xerr.InvalidArgument, "resource group item type is invalid")
		}
	}
	return nil
}

func scanResourceGroup(scanner scanTarget) (resourcedomain.ResourceGroup, error) {
	var group resourcedomain.ResourceGroup
	err := scanner.Scan(
		&group.AppCode,
		&group.GroupID,
		&group.GroupCode,
		&group.Name,
		&group.Status,
		&group.Description,
		&group.SortOrder,
		&group.CreatedByUserID,
		&group.UpdatedByUserID,
		&group.CreatedAtMS,
		&group.UpdatedAtMS,
	)
	return group, err
}

func scanResourceGroupItem(scanner scanTarget) (resourcedomain.ResourceGroupItem, error) {
	var item resourcedomain.ResourceGroupItem
	var resource resourcedomain.Resource
	var scopesJSON string
	if err := scanner.Scan(
		&item.GroupItemID,
		&item.GroupID,
		&item.ItemType,
		&item.ResourceID,
		&item.WalletAssetType,
		&item.WalletAssetAmount,
		&resource.AppCode,
		&resource.ResourceID,
		&resource.ResourceCode,
		&resource.ResourceType,
		&resource.Name,
		&resource.Status,
		&resource.Grantable,
		&resource.ManagerGrantEnabled,
		&resource.GrantStrategy,
		&resource.WalletAssetType,
		&resource.WalletAssetAmount,
		&resource.PriceType,
		&resource.CoinPrice,
		&resource.GiftPointAmount,
		&scopesJSON,
		&resource.AssetURL,
		&resource.PreviewURL,
		&resource.AnimationURL,
		&resource.MetadataJSON,
		&resource.SortOrder,
		&resource.CreatedByUserID,
		&resource.UpdatedByUserID,
		&resource.CreatedAtMS,
		&resource.UpdatedAtMS,
		&item.Quantity,
		&item.DurationMS,
		&item.SortOrder,
		&item.CreatedAtMS,
		&item.UpdatedAtMS,
	); err != nil {
		return resourcedomain.ResourceGroupItem{}, err
	}
	resource.UsageScopes = parseStringArray(scopesJSON)
	item.Resource = resource
	return item, nil
}

func resourceGroupWhereSQL(query resourcedomain.ListResourceGroupsQuery) (string, []any) {
	// 软删除行保留给历史发放、快照和审计链路，任何资源组列表都不能重新暴露这些行。
	where := `WHERE app_code = ? AND status <> 'deleted'`
	args := []any{query.AppCode}
	if query.ActiveOnly {
		where += ` AND status = 'active'`
	} else if query.Status != "" {
		where += ` AND status = ?`
		args = append(args, query.Status)
	}
	if query.Keyword != "" {
		like := "%" + query.Keyword + "%"
		where += ` AND (group_code LIKE ? OR name LIKE ?)`
		args = append(args, like, like)
	}
	return where, args
}

func normalizeResourceGroupListQuery(query resourcedomain.ListResourceGroupsQuery) resourcedomain.ListResourceGroupsQuery {
	query.Status = strings.ToLower(strings.TrimSpace(query.Status))
	query.Keyword = strings.TrimSpace(query.Keyword)
	query.Page, query.PageSize = normalizePage(query.Page, query.PageSize)
	return query
}

func normalizeResourceGroupCommand(command resourcedomain.ResourceGroupCommand) resourcedomain.ResourceGroupCommand {
	command.GroupCode = strings.TrimSpace(command.GroupCode)
	command.Name = strings.TrimSpace(command.Name)
	command.Status = resourcedomain.NormalizeStatus(command.Status)
	command.Description = strings.TrimSpace(command.Description)
	for index := range command.Items {
		command.Items[index] = normalizeResourceGroupItemInput(command.Items[index])
	}
	return command
}

func validateResourceGroupCommand(command resourcedomain.ResourceGroupCommand, requireItems bool) error {
	if command.GroupCode == "" || command.Name == "" {
		return xerr.New(xerr.InvalidArgument, "resource group command is incomplete")
	}
	if !resourcedomain.ValidStatus(command.Status) {
		return xerr.New(xerr.InvalidArgument, "status is invalid")
	}
	if requireItems && len(command.Items) == 0 {
		return xerr.New(xerr.InvalidArgument, "resource group has no items")
	}
	seenResources := make(map[int64]struct{}, len(command.Items))
	seenWalletAssets := make(map[string]struct{}, len(command.Items))
	for _, item := range command.Items {
		if !resourcedomain.ValidGroupItemType(item.ItemType) {
			return xerr.New(xerr.InvalidArgument, "resource group item type is invalid")
		}
		switch item.ItemType {
		case resourcedomain.GroupItemTypeResource:
			if item.ResourceID <= 0 || item.Quantity <= 0 || item.DurationMS < 0 || item.WalletAssetType != "" || item.WalletAssetAmount != 0 {
				return xerr.New(xerr.InvalidArgument, "resource group item is invalid")
			}
			if _, ok := seenResources[item.ResourceID]; ok {
				return xerr.New(xerr.InvalidArgument, "resource group contains duplicate resource")
			}
			seenResources[item.ResourceID] = struct{}{}
		case resourcedomain.GroupItemTypeWalletAsset:
			if item.ResourceID != 0 || item.Quantity != 1 || item.DurationMS != 0 || !validGroupWalletAssetType(item.WalletAssetType) || item.WalletAssetAmount <= 0 {
				return xerr.New(xerr.InvalidArgument, "resource group wallet asset item is invalid")
			}
			if _, ok := seenWalletAssets[item.WalletAssetType]; ok {
				return xerr.New(xerr.InvalidArgument, "resource group contains duplicate wallet asset")
			}
			seenWalletAssets[item.WalletAssetType] = struct{}{}
		}
	}
	return nil
}

func normalizeResourceGroupItemInput(item resourcedomain.ResourceGroupItemInput) resourcedomain.ResourceGroupItemInput {
	item.ItemType = resourcedomain.NormalizeGroupItemType(item.ItemType)
	item.WalletAssetType = resourcedomain.NormalizeWalletAssetType(item.WalletAssetType)
	if item.ItemType == resourcedomain.GroupItemTypeWalletAsset {
		item.ResourceID = 0
		item.Quantity = 1
		item.DurationMS = 0
		return item
	}
	item.ItemType = resourcedomain.GroupItemTypeResource
	item.WalletAssetType = ""
	item.WalletAssetAmount = 0
	return item
}

func validGroupWalletAssetType(assetType string) bool {
	switch resourcedomain.NormalizeWalletAssetType(assetType) {
	case ledger.AssetCoin:
		return true
	default:
		return false
	}
}

func validateActiveResourceGroupItems(items []resourcedomain.ResourceGroupItem) error {
	if len(items) == 0 {
		return xerr.New(xerr.InvalidArgument, "resource group has no items")
	}
	for _, item := range items {
		if resourcedomain.NormalizeGroupItemType(item.ItemType) != resourcedomain.GroupItemTypeResource {
			continue
		}
		if err := validateActiveResourceGroupResource(item.Resource); err != nil {
			return err
		}
	}
	return nil
}

func validateActiveResourceGroupResource(resource resourcedomain.Resource) error {
	if err := validateGrantableResource(resource); err != nil {
		return err
	}
	if resourcedomain.IsCoinResource(resource) {
		return xerr.New(xerr.InvalidArgument, "coin resource must use wallet asset item")
	}
	return nil
}
