package wallet

import (
	"context"
	"strconv"
	"strings"

	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
	resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
)

func (s *Service) ListResources(ctx context.Context, query resourcedomain.ListResourcesQuery) ([]resourcedomain.Resource, int64, error) {
	if s.repository == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	query.AppCode = appcode.Normalize(query.AppCode)
	ctx = appcode.WithContext(ctx, query.AppCode)
	if strings.TrimSpace(query.ResourceType) != "" && !resourcedomain.ValidResourceType(query.ResourceType) {
		return nil, 0, xerr.New(xerr.InvalidArgument, "resource_type is invalid")
	}
	return s.repository.ListResources(ctx, query)
}

func (s *Service) GetResource(ctx context.Context, appCode string, resourceID int64) (resourcedomain.Resource, error) {
	if resourceID <= 0 {
		return resourcedomain.Resource{}, xerr.New(xerr.InvalidArgument, "resource_id is required")
	}
	if s.repository == nil {
		return resourcedomain.Resource{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	ctx = appcode.WithContext(ctx, appcode.Normalize(appCode))
	return s.repository.GetResource(ctx, resourceID)
}

func (s *Service) CreateResource(ctx context.Context, command resourcedomain.ResourceCommand) (resourcedomain.Resource, error) {
	if s.repository == nil {
		return resourcedomain.Resource{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	// 资源编码是系统业务键，不再要求后台运营手工维护；仅在创建请求未提供编码时生成，
	// 以兼容批量导入、初始化脚本等仍需指定稳定编码的内部调用方。
	if strings.TrimSpace(command.ResourceCode) == "" {
		command.ResourceCode = idgen.New("resource")
	}
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.CreateResource(ctx, command)
}

func (s *Service) UpdateResource(ctx context.Context, command resourcedomain.ResourceCommand) (resourcedomain.Resource, error) {
	if s.repository == nil {
		return resourcedomain.Resource{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.UpdateResource(ctx, command)
}

func (s *Service) SetResourceStatus(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.Resource, error) {
	if s.repository == nil {
		return resourcedomain.Resource{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.SetResourceStatus(ctx, command)
}

func (s *Service) DeleteResource(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.Resource, error) {
	if s.repository == nil {
		return resourcedomain.Resource{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.DeleteResource(ctx, command)
}

func (s *Service) BatchDeleteResources(ctx context.Context, command resourcedomain.BatchDeleteResourcesCommand) ([]resourcedomain.Resource, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.BatchDeleteResources(ctx, command)
}

func (s *Service) ListResourceGroups(ctx context.Context, query resourcedomain.ListResourceGroupsQuery) ([]resourcedomain.ResourceGroup, int64, error) {
	if s.repository == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	query.AppCode = appcode.Normalize(query.AppCode)
	ctx = appcode.WithContext(ctx, query.AppCode)
	return s.repository.ListResourceGroups(ctx, query)
}

func (s *Service) GetResourceGroup(ctx context.Context, appCode string, groupID int64) (resourcedomain.ResourceGroup, error) {
	if groupID <= 0 {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.InvalidArgument, "group_id is required")
	}
	if s.repository == nil {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	ctx = appcode.WithContext(ctx, appcode.Normalize(appCode))
	return s.repository.GetResourceGroup(ctx, groupID)
}

func (s *Service) CreateResourceGroup(ctx context.Context, command resourcedomain.ResourceGroupCommand) (resourcedomain.ResourceGroup, error) {
	if s.repository == nil {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	// 资源组编码由 wallet owner 生成，随机熵与数据库唯一键共同保证并发创建安全；
	// 显式编码仍保留给初始化和跨租户映射流程使用。
	if strings.TrimSpace(command.GroupCode) == "" {
		command.GroupCode = idgen.New("resource_group")
	}
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.CreateResourceGroup(ctx, command)
}

func (s *Service) UpdateResourceGroup(ctx context.Context, command resourcedomain.ResourceGroupCommand) (resourcedomain.ResourceGroup, error) {
	if s.repository == nil {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.UpdateResourceGroup(ctx, command)
}

func (s *Service) SetResourceGroupStatus(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.ResourceGroup, error) {
	if s.repository == nil {
		return resourcedomain.ResourceGroup{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.SetResourceGroupStatus(ctx, command)
}

func (s *Service) PinResourceGroupSnapshot(ctx context.Context, command resourcedomain.PinResourceGroupSnapshotCommand) (resourcedomain.ResourceGroupSnapshot, error) {
	store, ok := s.repository.(interface {
		PinResourceGroupSnapshot(context.Context, resourcedomain.PinResourceGroupSnapshotCommand) (resourcedomain.ResourceGroupSnapshot, error)
	})
	if !ok {
		return resourcedomain.ResourceGroupSnapshot{}, xerr.New(xerr.Unavailable, "resource group snapshot repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return store.PinResourceGroupSnapshot(ctx, command)
}

func (s *Service) ListGiftConfigs(ctx context.Context, query resourcedomain.ListGiftConfigsQuery) ([]resourcedomain.GiftConfig, int64, error) {
	if s.repository == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	if query.FilterRegion && query.RegionID < 0 {
		return nil, 0, xerr.New(xerr.InvalidArgument, "region_id is invalid")
	}
	query.AppCode = appcode.Normalize(query.AppCode)
	ctx = appcode.WithContext(ctx, query.AppCode)
	return s.repository.ListGiftConfigs(ctx, query)
}

func (s *Service) ListGiftTypeConfigs(ctx context.Context, query resourcedomain.ListGiftTypeConfigsQuery) ([]resourcedomain.GiftTypeConfig, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	query.AppCode = appcode.Normalize(query.AppCode)
	ctx = appcode.WithContext(ctx, query.AppCode)
	return s.repository.ListGiftTypeConfigs(ctx, query)
}

func (s *Service) GetGiftCatalogVersion(ctx context.Context, appCode string) (int64, error) {
	if s.repository == nil {
		return 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	appCode = appcode.Normalize(appCode)
	ctx = appcode.WithContext(ctx, appCode)
	return s.repository.GetGiftCatalogVersion(ctx, appCode)
}

func (s *Service) CreateGiftConfig(ctx context.Context, command resourcedomain.GiftConfigCommand) (resourcedomain.GiftConfig, error) {
	if s.repository == nil {
		return resourcedomain.GiftConfig{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.CreateGiftConfig(ctx, command)
}

func (s *Service) BatchCreateGiftConfigs(ctx context.Context, appCode string, commands []resourcedomain.GiftConfigCommand, operatorUserID int64) ([]resourcedomain.GiftConfig, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	appCode = appcode.Normalize(appCode)
	ctx = appcode.WithContext(ctx, appCode)
	if len(commands) == 0 {
		return nil, xerr.New(xerr.InvalidArgument, "gift configs are required")
	}
	batchCommands := make([]resourcedomain.GiftConfigCommand, 0, len(commands))
	for _, command := range commands {
		command.AppCode = appCode
		if command.OperatorUserID == 0 {
			command.OperatorUserID = operatorUserID
		}
		batchCommands = append(batchCommands, command)
	}
	return s.repository.BatchCreateGiftConfigs(ctx, batchCommands)
}

func (s *Service) UpdateGiftConfig(ctx context.Context, command resourcedomain.GiftConfigCommand) (resourcedomain.GiftConfig, error) {
	if s.repository == nil {
		return resourcedomain.GiftConfig{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.UpdateGiftConfig(ctx, command)
}

func (s *Service) SetGiftConfigStatus(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.GiftConfig, error) {
	if s.repository == nil {
		return resourcedomain.GiftConfig{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.SetGiftConfigStatus(ctx, command)
}

func (s *Service) DeleteGiftConfig(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.GiftConfig, error) {
	if s.repository == nil {
		return resourcedomain.GiftConfig{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.DeleteGiftConfig(ctx, command)
}

func (s *Service) UpsertGiftTypeConfig(ctx context.Context, command resourcedomain.GiftTypeConfigCommand) (resourcedomain.GiftTypeConfig, error) {
	if s.repository == nil {
		return resourcedomain.GiftTypeConfig{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.UpsertGiftTypeConfig(ctx, command)
}

func (s *Service) GrantResource(ctx context.Context, command resourcedomain.GrantResourceCommand) (resourcedomain.ResourceGrant, error) {
	if s.repository == nil {
		return resourcedomain.ResourceGrant{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.GrantResource(ctx, command)
}

func (s *Service) GrantResourceGroup(ctx context.Context, command resourcedomain.GrantResourceGroupCommand) (resourcedomain.ResourceGrant, error) {
	if s.repository == nil {
		return resourcedomain.ResourceGrant{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.GrantResourceGroup(ctx, command)
}

func (s *Service) GrantPinnedResourceGroup(ctx context.Context, command resourcedomain.GrantPinnedResourceGroupCommand) (resourcedomain.ResourceGrant, error) {
	store, ok := s.repository.(interface {
		GrantPinnedResourceGroup(context.Context, resourcedomain.GrantPinnedResourceGroupCommand) (resourcedomain.ResourceGrant, error)
	})
	if !ok {
		return resourcedomain.ResourceGrant{}, xerr.New(xerr.Unavailable, "pinned resource group grant repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return store.GrantPinnedResourceGroup(ctx, command)
}

func (s *Service) RevokeResourceGrant(ctx context.Context, command resourcedomain.RevokeResourceGrantCommand) (resourcedomain.ResourceGrant, error) {
	if s.repository == nil {
		return resourcedomain.ResourceGrant{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.RevokeResourceGrant(ctx, command)
}

func (s *Service) ListUserResources(ctx context.Context, query resourcedomain.ListUserResourcesQuery) ([]resourcedomain.UserResourceEntitlement, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	query.AppCode = appcode.Normalize(query.AppCode)
	ctx = appcode.WithContext(ctx, query.AppCode)
	return s.repository.ListUserResources(ctx, query)
}

func (s *Service) RevokeUserResource(ctx context.Context, command resourcedomain.RevokeUserResourceCommand) (resourcedomain.UserResourceEntitlement, error) {
	if s.repository == nil {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.RevokeUserResource(ctx, command)
}

func (s *Service) EquipUserResource(ctx context.Context, command resourcedomain.EquipUserResourceCommand) (resourcedomain.UserResourceEntitlement, error) {
	if s.repository == nil {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	if command.UserID <= 0 || command.ResourceID <= 0 {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.InvalidArgument, "user_id and resource_id are required")
	}
	command.RequestID = strings.TrimSpace(command.RequestID)
	command.CommandID = strings.TrimSpace(command.CommandID)
	if command.CommandID == "" {
		// 兼容旧内部调用方：历史代码只传 request_id。新 HTTP 契约会显式传 command_id；
		// 这里的回退只防止 Lalu/既有任务立即失效，不把 request_id 宣称为新的业务协议。
		command.CommandID = command.RequestID
	}
	if len(command.CommandID) > 128 {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.InvalidArgument, "command_id is too long")
	}
	command.EntitlementID = strings.TrimSpace(command.EntitlementID)
	if command.CommandID != "" {
		// 完成命令必须在当前 entitlement/VIP 状态校验前重放；否则首请求成功后资源到期，
		// 同 command_id 会错误地变成 NotFound/VIP_BENEFIT_REQUIRED，破坏幂等回包契约。
		if replayed, exists, err := s.repository.ReplayEquipUserResource(ctx, command); err != nil || exists {
			return replayed, err
		}
	}
	items, err := s.repository.ListUserResources(ctx, resourcedomain.ListUserResourcesQuery{
		AppCode: command.AppCode, UserID: command.UserID, ActiveOnly: true,
	})
	if err != nil {
		return resourcedomain.UserResourceEntitlement{}, err
	}
	var selected *resourcedomain.UserResourceEntitlement
	for index := range items {
		if items[index].ResourceID == command.ResourceID && (command.EntitlementID == "" || items[index].EntitlementID == command.EntitlementID) {
			selected = &items[index]
			break
		}
	}
	if selected == nil {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.NotFound, "user resource entitlement not found")
	}
	if err := s.requireVipBenefitForResourceEquip(ctx, command.UserID, command.ResourceID, selected.Resource.ResourceType); err != nil {
		return resourcedomain.UserResourceEntitlement{}, err
	}
	if resourcedomain.NormalizeResourceType(selected.Resource.ResourceType) == resourcedomain.TypeVIPTrialCard {
		// 体验卡在 UI 上仍是普通背包物品，但其装备动作会改变最终 VIP 权益。
		// 因此通用入口必须委托专用状态机，不能直接写 equipment 而绕过 App 开关和 VipEffectiveChanged outbox。
		command.EntitlementID = strings.TrimSpace(command.EntitlementID)
		if command.CommandID == "" || command.EntitlementID == "" {
			return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.InvalidArgument, "command_id and entitlement_id are required for vip_trial_card")
		}
		if selected.EntitlementID != command.EntitlementID {
			return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.NotFound, "vip trial card entitlement not found")
		}
		if _, _, err := s.EquipVipTrialCard(ctx, ledger.EquipVipTrialCardCommand{
			AppCode: command.AppCode,
			// 体验卡专用状态机的 RequestID 实际承担稳定 outbox event key；这里必须使用业务 command_id，
			// 不能退回每次重试都会变化的 HTTP request_id。
			RequestID:          command.CommandID,
			EquipmentCommandID: command.CommandID,
			ResourceID:         command.ResourceID,
			UserID:             command.UserID,
			EntitlementID:      command.EntitlementID,
		}); err != nil {
			return resourcedomain.UserResourceEntitlement{}, err
		}
		// 专用事务已经再次校验 entitlement 并提交装备/outbox；这里复用预读完整资源投影满足通用 API 返回契约。
		selected.Equipped = true
		return *selected, nil
	}
	return s.repository.EquipUserResource(ctx, command)
}

// requireVipBenefitForResourceEquip 对 tiered VIP 绑定资源执行服务端二次门禁。
// repository 的绑定查询已联结 program_type，因此 Lalu legacy_timed 资源不会进入此分支；
// 资源型权益允许当前 effective 等级向下继承，但历史 entitlement 仍不能绕过当前 active 目录绑定。
func (s *Service) requireVipBenefitForResourceEquip(ctx context.Context, userID int64, resourceID int64, resourceType string) error {
	requirements, err := s.repository.ListVipBenefitRequirementsForResource(ctx, resourceID)
	if err != nil || len(requirements) == 0 {
		return err
	}
	var denied ledger.CheckVipBenefitResult
	for _, requirement := range requirements {
		result, checkErr := s.repository.CheckVipResourceBenefit(ctx, userID, requirement.BenefitCode, resourceID, resourceType)
		if checkErr != nil {
			// 权益 owner 读取失败必须失败关闭，不能因为用户已有 entitlement 就跳过付费门禁。
			return checkErr
		}
		// resource_id/type 已由 owner 查询限定；这里再次核对返回投影，避免未来仓储实现
		// 只按 benefit_code 放行而把同类其他等级素材错误装备。
		if result.Allowed && result.Benefit.ResourceID == resourceID &&
			resourcedomain.NormalizeResourceType(result.Benefit.ResourceType) == resourcedomain.NormalizeResourceType(resourceType) &&
			resourcedomain.NormalizeResourceType(requirement.ResourceType) == resourcedomain.NormalizeResourceType(resourceType) {
			return nil
		}
		denied = result
	}
	requirement := requirements[0]
	return xerr.NewWithMetadata(xerr.VIPBenefitRequired, "vip benefit is required for resource equip", map[string]string{
		"benefit_code":            requirement.BenefitCode,
		"required_level":          strconv.FormatInt(int64(denied.RequiredLevel), 10),
		"current_effective_level": strconv.FormatInt(int64(denied.CurrentEffectiveLevel), 10),
		"action":                  "equip_resource",
	})
}

func (s *Service) UnequipUserResource(ctx context.Context, command resourcedomain.UnequipUserResourceCommand) (resourcedomain.UnequipUserResourceResult, error) {
	if s.repository == nil {
		return resourcedomain.UnequipUserResourceResult{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	command.ResourceType = resourcedomain.NormalizeResourceType(command.ResourceType)
	if command.ResourceType == resourcedomain.TypeVIPTrialCard {
		// 和装备相同，通用卸下必须发布 VIP 有效态变化，不能只删除通用装备指针。
		unequipped, state, err := s.UnequipVipTrialCard(ctx, ledger.UnequipVipTrialCardCommand{
			AppCode:   command.AppCode,
			RequestID: strings.TrimSpace(command.RequestID),
			UserID:    command.UserID,
		})
		if err != nil {
			return resourcedomain.UnequipUserResourceResult{}, err
		}
		return resourcedomain.UnequipUserResourceResult{
			ResourceType: command.ResourceType,
			Unequipped:   unequipped,
			UpdatedAtMS:  state.EvaluatedAtMS,
		}, nil
	}
	return s.repository.UnequipUserResource(ctx, command)
}

func (s *Service) BatchGetUserEquippedResources(ctx context.Context, query resourcedomain.BatchGetUserEquippedResourcesQuery) ([]resourcedomain.UserEquippedResources, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	query.AppCode = appcode.Normalize(query.AppCode)
	ctx = appcode.WithContext(ctx, query.AppCode)
	return s.repository.BatchGetUserEquippedResources(ctx, query)
}

func (s *Service) ListResourceGrants(ctx context.Context, query resourcedomain.ListResourceGrantsQuery) ([]resourcedomain.ResourceGrant, int64, error) {
	if s.repository == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	query.AppCode = appcode.Normalize(query.AppCode)
	ctx = appcode.WithContext(ctx, query.AppCode)
	return s.repository.ListResourceGrants(ctx, query)
}

// GetResourceGrant 只按当前 app_code 读取一个精确 grant，避免异步消费者分页扫描后误拿同用户的其它活动奖励。
func (s *Service) GetResourceGrant(ctx context.Context, appCode string, grantID string) (resourcedomain.ResourceGrant, error) {
	if s.repository == nil {
		return resourcedomain.ResourceGrant{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	appCode = appcode.Normalize(appCode)
	grantID = strings.TrimSpace(grantID)
	if grantID == "" {
		return resourcedomain.ResourceGrant{}, xerr.New(xerr.InvalidArgument, "grant_id is required")
	}
	return s.repository.GetResourceGrant(appcode.WithContext(ctx, appCode), grantID)
}

func (s *Service) ListResourceShopItems(ctx context.Context, query resourcedomain.ListResourceShopItemsQuery) ([]resourcedomain.ResourceShopItem, int64, error) {
	if s.repository == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	query.AppCode = appcode.Normalize(query.AppCode)
	ctx = appcode.WithContext(ctx, query.AppCode)
	if strings.TrimSpace(query.ResourceType) != "" {
		resourceType := resourcedomain.NormalizeResourceType(query.ResourceType)
		if !resourcedomain.ResourceTypeSellableInShop(resourceType) {
			return nil, 0, xerr.New(xerr.InvalidArgument, "resource_type is invalid")
		}
		query.ResourceType = resourceType
	}
	return s.repository.ListResourceShopItems(ctx, query)
}

func (s *Service) UpsertResourceShopItems(ctx context.Context, command resourcedomain.ResourceShopItemsCommand) ([]resourcedomain.ResourceShopItem, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.UpsertResourceShopItems(ctx, command)
}

func (s *Service) SetResourceShopItemStatus(ctx context.Context, command resourcedomain.ResourceShopItemStatusCommand) (resourcedomain.ResourceShopItem, error) {
	if s.repository == nil {
		return resourcedomain.ResourceShopItem{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.SetResourceShopItemStatus(ctx, command)
}

func (s *Service) ListResourceShopPurchaseOrders(ctx context.Context, query resourcedomain.ListResourceShopPurchaseOrdersQuery) ([]resourcedomain.ResourceShopPurchaseOrder, int64, error) {
	if s.repository == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	query.AppCode = appcode.Normalize(query.AppCode)
	ctx = appcode.WithContext(ctx, query.AppCode)
	if strings.TrimSpace(query.ResourceType) != "" {
		resourceType := resourcedomain.NormalizeResourceType(query.ResourceType)
		if !resourcedomain.ResourceTypeSellableInShop(resourceType) {
			return nil, 0, xerr.New(xerr.InvalidArgument, "resource_type is invalid")
		}
		query.ResourceType = resourceType
	}
	return s.repository.ListResourceShopPurchaseOrders(ctx, query)
}

func (s *Service) PurchaseResourceShopItem(ctx context.Context, command resourcedomain.ResourceShopPurchaseCommand) (resourcedomain.ResourceShopPurchaseReceipt, error) {
	if strings.TrimSpace(command.CommandID) == "" || command.UserID <= 0 || command.ShopItemID <= 0 {
		return resourcedomain.ResourceShopPurchaseReceipt{}, xerr.New(xerr.InvalidArgument, "resource shop purchase command is incomplete")
	}
	if len(command.CommandID) > 128 {
		return resourcedomain.ResourceShopPurchaseReceipt{}, xerr.New(xerr.InvalidArgument, "command_id is too long")
	}
	if s.repository == nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	command.CommandID = strings.TrimSpace(command.CommandID)
	ctx = appcode.WithContext(ctx, command.AppCode)
	return s.repository.PurchaseResourceShopItem(ctx, command)
}
