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"
)

// ListResourceShopItems 返回 App 道具商店和后台配置共用的售卖视图；价格始终由资源一日价乘以售卖天数派生。
func (r *Repository) ListResourceShopItems(ctx context.Context, query resourcedomain.ListResourceShopItemsQuery) ([]resourcedomain.ResourceShopItem, 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 = normalizeResourceShopItemsQuery(query)
	where, args := resourceShopItemsWhereSQL(query)
	var total int64
	if err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM resource_shop_items si
		JOIN resources r ON r.app_code = si.app_code AND r.resource_id = si.resource_id
		`+where, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	rows, err := r.db.QueryContext(ctx, resourceShopItemSelectSQL()+where+`
		ORDER BY si.sort_order ASC, si.shop_item_id DESC
		LIMIT ? OFFSET ?`,
		append(args, query.PageSize, resourceOffset(query.Page, query.PageSize))...,
	)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()

	items := make([]resourcedomain.ResourceShopItem, 0, query.PageSize)
	for rows.Next() {
		item, err := scanResourceShopItem(rows)
		if err != nil {
			return nil, 0, err
		}
		items = append(items, item)
	}
	return items, total, rows.Err()
}

// ListResourceShopPurchaseOrders 返回后台售卖记录；订单事实取购买表，素材展示优先使用发放时的资源快照。
func (r *Repository) ListResourceShopPurchaseOrders(ctx context.Context, query resourcedomain.ListResourceShopPurchaseOrdersQuery) ([]resourcedomain.ResourceShopPurchaseOrder, 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 = normalizeResourceShopPurchaseOrdersQuery(query)
	where, args := resourceShopPurchaseOrdersWhereSQL(query)

	var total int64
	if err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM resource_shop_purchase_orders po
		JOIN resources r ON r.app_code = po.app_code AND r.resource_id = po.resource_id
		`+where, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	rows, err := r.db.QueryContext(ctx, resourceShopPurchaseOrderSelectSQL()+where+`
		ORDER BY po.created_at_ms DESC, po.order_id DESC
		LIMIT ? OFFSET ?`,
		append(args, query.PageSize, resourceOffset(query.Page, query.PageSize))...,
	)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()

	items := make([]resourcedomain.ResourceShopPurchaseOrder, 0, query.PageSize)
	for rows.Next() {
		item, err := scanResourceShopPurchaseOrder(rows)
		if err != nil {
			return nil, 0, err
		}
		items = append(items, item)
	}
	return items, total, rows.Err()
}

// UpsertResourceShopItems 批量添加或重配售卖资源；同一个资源在商店只保留一个当前售卖配置。
func (r *Repository) UpsertResourceShopItems(ctx context.Context, command resourcedomain.ResourceShopItemsCommand) ([]resourcedomain.ResourceShopItem, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	command = normalizeResourceShopItemsCommand(command)
	if err := validateResourceShopItemsCommand(command); err != nil {
		return nil, err
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer func() { _ = tx.Rollback() }()

	nowMs := time.Now().UnixMilli()
	resourceIDs := make([]int64, 0, len(command.Items))
	seen := make(map[int64]struct{}, len(command.Items))
	for _, item := range command.Items {
		if _, exists := seen[item.ResourceID]; exists {
			return nil, xerr.New(xerr.InvalidArgument, "resource shop contains duplicate resource")
		}
		seen[item.ResourceID] = struct{}{}
		resource, err := r.getResourceForUpdate(ctx, tx, item.ResourceID)
		if err != nil {
			return nil, err
		}
		if err := validateResourceShopResource(resource); err != nil {
			return nil, err
		}
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO resource_shop_items (
			   app_code, resource_id, status, duration_days, effective_from_ms, effective_to_ms, sort_order,
			   created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
			 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
			ON DUPLICATE KEY UPDATE
			   status = VALUES(status),
			   duration_days = VALUES(duration_days),
			   effective_from_ms = VALUES(effective_from_ms),
			   effective_to_ms = VALUES(effective_to_ms),
			   sort_order = VALUES(sort_order),
			   updated_by_user_id = VALUES(updated_by_user_id),
			   updated_at_ms = VALUES(updated_at_ms)`,
			command.AppCode, item.ResourceID, item.Status, item.DurationDays, item.EffectiveFromMS, item.EffectiveToMS, item.SortOrder,
			command.OperatorUserID, command.OperatorUserID, nowMs, nowMs,
		); err != nil {
			return nil, err
		}
		resourceIDs = append(resourceIDs, item.ResourceID)
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return r.listResourceShopItemsByResourceIDs(ctx, resourceIDs)
}

func (r *Repository) SetResourceShopItemStatus(ctx context.Context, command resourcedomain.ResourceShopItemStatusCommand) (resourcedomain.ResourceShopItem, error) {
	if r == nil || r.db == nil {
		return resourcedomain.ResourceShopItem{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	command.Status = resourcedomain.NormalizeStatus(command.Status)
	if command.ShopItemID <= 0 {
		return resourcedomain.ResourceShopItem{}, xerr.New(xerr.InvalidArgument, "shop_item_id is required")
	}
	if !resourcedomain.ValidStatus(command.Status) {
		return resourcedomain.ResourceShopItem{}, xerr.New(xerr.InvalidArgument, "status is invalid")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return resourcedomain.ResourceShopItem{}, err
	}
	defer func() { _ = tx.Rollback() }()

	item, err := r.getResourceShopItemTx(ctx, tx, command.ShopItemID, true)
	if err != nil {
		return resourcedomain.ResourceShopItem{}, err
	}
	if command.Status == resourcedomain.StatusActive {
		if err := validateResourceShopResource(item.Resource); err != nil {
			return resourcedomain.ResourceShopItem{}, err
		}
	}
	nowMs := time.Now().UnixMilli()
	result, err := tx.ExecContext(ctx, `
		UPDATE resource_shop_items
		SET status = ?, updated_by_user_id = ?, updated_at_ms = ?
		WHERE app_code = ? AND shop_item_id = ?`,
		command.Status, command.OperatorUserID, nowMs, command.AppCode, command.ShopItemID,
	)
	if err != nil {
		return resourcedomain.ResourceShopItem{}, err
	}
	if affected, err := result.RowsAffected(); err != nil {
		return resourcedomain.ResourceShopItem{}, err
	} else if affected == 0 {
		return resourcedomain.ResourceShopItem{}, xerr.New(xerr.NotFound, "resource shop item not found")
	}
	if err := tx.Commit(); err != nil {
		return resourcedomain.ResourceShopItem{}, err
	}
	return r.getResourceShopItem(ctx, command.ShopItemID)
}

// PurchaseResourceShopItem 是 App 道具商店的唯一购买入口；扣金币、写订单和发资源权益必须共享同一事务。
func (r *Repository) PurchaseResourceShopItem(ctx context.Context, command resourcedomain.ResourceShopPurchaseCommand) (resourcedomain.ResourceShopPurchaseReceipt, error) {
	if r == nil || r.db == nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	command.CommandID = strings.TrimSpace(command.CommandID)

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

	requestHash := resourceShopPurchaseRequestHash(command)
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeResourceShopPurchase); err != nil || exists {
		if err != nil {
			return resourcedomain.ResourceShopPurchaseReceipt{}, err
		}
		return r.resourceShopPurchaseReceiptForTransaction(ctx, tx, txRow.TransactionID, command.UserID)
	}

	nowMs := time.Now().UnixMilli()
	item, err := r.getResourceShopItemTx(ctx, tx, command.ShopItemID, true)
	if err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	if err := validatePurchasableResourceShopItem(item, nowMs); err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, false, nowMs)
	if err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	if account.AvailableAmount < item.CoinPrice {
		return resourcedomain.ResourceShopPurchaseReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
	}

	transactionID := transactionID(command.AppCode, command.CommandID)
	orderID := "rshop_order_" + stableHash(command.AppCode+"|"+command.CommandID)
	grantID := resourceGrantID(command.AppCode, command.CommandID)
	durationMS := int64(item.DurationDays) * 24 * int64(time.Hour/time.Millisecond)
	coinBalanceAfter := account.AvailableAmount - item.CoinPrice
	metadata := map[string]any{
		"app_code":          command.AppCode,
		"order_id":          orderID,
		"user_id":           command.UserID,
		"shop_item_id":      item.ShopItemID,
		"resource_id":       item.ResourceID,
		"resource_code":     item.Resource.ResourceCode,
		"resource_type":     item.Resource.ResourceType,
		"duration_days":     item.DurationDays,
		"duration_ms":       durationMS,
		"price_type":        item.PriceType,
		"coin_spent":        item.CoinPrice,
		"balance_after":     coinBalanceAfter,
		"resource_grant_id": grantID,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeResourceShopPurchase, requestHash, orderID, metadata, nowMs); err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, -item.CoinPrice, 0, nowMs); err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.UserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: -item.CoinPrice,
		FrozenDelta:    0,
		AvailableAfter: coinBalanceAfter,
		FrozenAfter:    account.FrozenAmount,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	if err := r.insertResourceGrant(ctx, tx, grantID, command.CommandID, command.UserID, resourcedomain.GrantSourceResourceShop, resourcedomain.GrantSubjectResource, fmt.Sprintf("%d", item.ResourceID), requestHash, "", "resource shop purchase", command.UserID, nowMs); err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	grantItem, err := r.applyGrantItem(ctx, tx, grantID, command.CommandID, command.UserID, item.Resource, 1, durationMS, nowMs)
	if err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	if grantItem.EntitlementID == "" {

		return resourcedomain.ResourceShopPurchaseReceipt{}, xerr.New(xerr.InvalidArgument, "resource shop item must grant entitlement")
	}
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO resource_shop_purchase_orders (
		   app_code, order_id, command_id, user_id, shop_item_id, resource_id, duration_days, price_coin,
		   status, wallet_transaction_id, resource_grant_id, entitlement_id, request_hash, created_at_ms, updated_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'succeeded', ?, ?, ?, ?, ?, ?)`,
		command.AppCode, orderID, command.CommandID, command.UserID, item.ShopItemID, item.ResourceID, item.DurationDays, item.CoinPrice,
		transactionID, grantID, grantItem.EntitlementID, requestHash, nowMs, nowMs,
	); err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	resource, err := r.getUserResourceEntitlementTx(ctx, tx, grantItem.EntitlementID)
	if err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.UserID, ledger.AssetCoin, -item.CoinPrice, 0, coinBalanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs, bizTypeResourceShopPurchase),
		resourceOutboxEvent("ResourceGranted", command.CommandID, command.UserID, item.ResourceID, map[string]any{"grant_id": grantID, "resource": item.Resource, "source": resourcedomain.GrantSourceResourceShop}, nowMs),
		{
			EventID:       eventID(transactionID, "ResourceShopItemPurchased", command.UserID, resourceOutboxAsset),
			EventType:     "ResourceShopItemPurchased",
			TransactionID: transactionID,
			CommandID:     command.CommandID,
			UserID:        command.UserID,
			AssetType:     resourceOutboxAsset,
			Payload:       metadata,
			CreatedAtMS:   nowMs,
		},
	}); err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}

	return resourcedomain.ResourceShopPurchaseReceipt{
		OrderID:       orderID,
		TransactionID: transactionID,
		ShopItem:      item,
		Resource:      resource,
		Balance: ledger.AssetBalance{
			AppCode:         command.AppCode,
			UserID:          command.UserID,
			AssetType:       ledger.AssetCoin,
			AvailableAmount: coinBalanceAfter,
			FrozenAmount:    account.FrozenAmount,
			Version:         account.Version + 1,
		},
		CoinSpent: item.CoinPrice,
		GrantID:   grantID,
	}, nil
}

func (r *Repository) getResourceShopItem(ctx context.Context, shopItemID int64) (resourcedomain.ResourceShopItem, error) {
	return r.getResourceShopItemWithQuerier(ctx, r.db, shopItemID, false)
}

func (r *Repository) getResourceShopItemTx(ctx context.Context, tx *sql.Tx, shopItemID int64, lock bool) (resourcedomain.ResourceShopItem, error) {
	return r.getResourceShopItemWithQuerier(ctx, tx, shopItemID, lock)
}

func (r *Repository) getResourceShopItemWithQuerier(ctx context.Context, querier sqlRowQuerier, shopItemID int64, lock bool) (resourcedomain.ResourceShopItem, error) {
	query := resourceShopItemSelectSQL() + ` WHERE si.app_code = ? AND si.shop_item_id = ?`
	if lock {
		query += ` FOR UPDATE`
	}
	row := querier.QueryRowContext(ctx, query, appcode.FromContext(ctx), shopItemID)
	item, err := scanResourceShopItem(row)
	if errors.Is(err, sql.ErrNoRows) {
		return resourcedomain.ResourceShopItem{}, xerr.New(xerr.NotFound, "resource shop item not found")
	}
	return item, err
}

func (r *Repository) resourceShopPurchaseReceiptForTransaction(ctx context.Context, tx *sql.Tx, transactionID string, userID int64) (resourcedomain.ResourceShopPurchaseReceipt, error) {
	var orderID string
	var shopItemID int64
	var priceCoin int64
	var grantID string
	var entitlementID string
	err := tx.QueryRowContext(ctx, `
		SELECT order_id, shop_item_id, price_coin, resource_grant_id, entitlement_id
		FROM resource_shop_purchase_orders
		WHERE app_code = ? AND wallet_transaction_id = ? AND user_id = ?`,
		appcode.FromContext(ctx), transactionID, userID,
	).Scan(&orderID, &shopItemID, &priceCoin, &grantID, &entitlementID)
	if err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	item, err := r.getResourceShopItemTx(ctx, tx, shopItemID, false)
	if err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	resource, err := r.getUserResourceEntitlementTx(ctx, tx, entitlementID)
	if err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	balance, err := r.balanceAfterTransaction(ctx, tx, transactionID, userID, ledger.AssetCoin)
	if err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	}
	if account, exists, err := r.queryAccountForUpdate(ctx, tx, userID, ledger.AssetCoin); err != nil {
		return resourcedomain.ResourceShopPurchaseReceipt{}, err
	} else if exists {
		balance.Version = account.Version
	}
	return resourcedomain.ResourceShopPurchaseReceipt{
		OrderID:       orderID,
		TransactionID: transactionID,
		ShopItem:      item,
		Resource:      resource,
		Balance:       balance,
		CoinSpent:     priceCoin,
		GrantID:       grantID,
	}, nil
}

func (r *Repository) listResourceShopItemsByResourceIDs(ctx context.Context, resourceIDs []int64) ([]resourcedomain.ResourceShopItem, error) {
	resourceIDs = compactPositiveInt64s(resourceIDs)
	if len(resourceIDs) == 0 {
		return []resourcedomain.ResourceShopItem{}, nil
	}
	args := append([]any{appcode.FromContext(ctx)}, int64AnyArgs(resourceIDs)...)
	rows, err := r.db.QueryContext(ctx, resourceShopItemSelectSQL()+`
		WHERE si.app_code = ? AND si.resource_id IN (`+placeholders(len(resourceIDs))+`)
		ORDER BY si.sort_order ASC, si.shop_item_id DESC`,
		args...,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := make([]resourcedomain.ResourceShopItem, 0, len(resourceIDs))
	for rows.Next() {
		item, err := scanResourceShopItem(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func scanResourceShopItem(scanner scanTarget) (resourcedomain.ResourceShopItem, error) {
	var item resourcedomain.ResourceShopItem
	var resource resourcedomain.Resource
	var scopesJSON string
	if err := scanner.Scan(
		&item.AppCode,
		&item.ShopItemID,
		&item.ResourceID,
		&item.Status,
		&item.DurationDays,
		&item.PriceType,
		&item.CoinPrice,
		&item.EffectiveFromMS,
		&item.EffectiveToMS,
		&item.SortOrder,
		&item.CreatedByUserID,
		&item.UpdatedByUserID,
		&item.CreatedAtMS,
		&item.UpdatedAtMS,
		&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,
	); err != nil {
		return resourcedomain.ResourceShopItem{}, err
	}
	resource.UsageScopes = parseStringArray(scopesJSON)
	item.Resource = resource
	return item, nil
}

func scanResourceShopPurchaseOrder(scanner scanTarget) (resourcedomain.ResourceShopPurchaseOrder, error) {
	var item resourcedomain.ResourceShopPurchaseOrder
	var resource resourcedomain.Resource
	var scopesJSON string
	if err := scanner.Scan(
		&item.AppCode,
		&item.OrderID,
		&item.CommandID,
		&item.UserID,
		&item.ShopItemID,
		&item.ResourceID,
		&item.DurationDays,
		&item.PriceCoin,
		&item.Status,
		&item.WalletTransactionID,
		&item.ResourceGrantID,
		&item.EntitlementID,
		&item.ResourceSnapshotJSON,
		&item.CreatedAtMS,
		&item.UpdatedAtMS,
		&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,
	); err != nil {
		return resourcedomain.ResourceShopPurchaseOrder{}, err
	}
	resource.UsageScopes = parseStringArray(scopesJSON)
	item.Resource = resourceFromSnapshotJSON(item.ResourceSnapshotJSON, resource)
	return item, nil
}

func resourceShopItemSelectSQL() string {
	return `
		SELECT si.app_code, si.shop_item_id, si.resource_id, si.status, si.duration_days,
		       r.price_type,
		       CASE WHEN r.price_type = 'coin' THEN r.coin_price * si.duration_days ELSE 0 END AS coin_price,
		       si.effective_from_ms, si.effective_to_ms, si.sort_order,
		       si.created_by_user_id, si.updated_by_user_id, si.created_at_ms, si.updated_at_ms,
		       ` + resourceColumnsWithAlias("r") + `
		FROM resource_shop_items si
		JOIN resources r ON r.app_code = si.app_code AND r.resource_id = si.resource_id
		`
}

func resourceShopPurchaseOrderSelectSQL() string {
	return `
		SELECT po.app_code, po.order_id, po.command_id, po.user_id, po.shop_item_id, po.resource_id,
		       po.duration_days, po.price_coin, po.status, po.wallet_transaction_id, po.resource_grant_id,
		       po.entitlement_id, COALESCE(CAST(gi.resource_snapshot_json AS CHAR), ''), po.created_at_ms, po.updated_at_ms,
		       ` + resourceColumnsWithAlias("r") + `
		FROM resource_shop_purchase_orders po
		JOIN resources r ON r.app_code = po.app_code AND r.resource_id = po.resource_id
		LEFT JOIN resource_grant_items gi ON gi.app_code = po.app_code
		 AND gi.grant_id = po.resource_grant_id
		 AND gi.resource_id = po.resource_id
		`
}

func resourceShopItemsWhereSQL(query resourcedomain.ListResourceShopItemsQuery) (string, []any) {
	where := `WHERE si.app_code = ? AND r.resource_type IN ('avatar_frame', 'profile_card', 'vehicle', 'chat_bubble', 'badge', 'mic_seat_icon', 'mic_seat_animation')`
	args := []any{query.AppCode}
	if query.ActiveOnly {
		nowMs := time.Now().UnixMilli()
		where += ` AND si.status = 'active' AND r.status = 'active'
			AND (si.effective_from_ms = 0 OR si.effective_from_ms <= ?)
			AND (si.effective_to_ms = 0 OR si.effective_to_ms > ?)`
		args = append(args, nowMs, nowMs)
	} else if query.Status != "" {
		where += ` AND si.status = ?`
		args = append(args, query.Status)
	}
	if query.ResourceType != "" {
		where += ` AND r.resource_type = ?`
		args = append(args, query.ResourceType)
	}
	if query.Keyword != "" {
		like := "%" + query.Keyword + "%"
		where += ` AND (r.resource_code LIKE ? OR r.name LIKE ?)`
		args = append(args, like, like)
	}
	return where, args
}

func resourceShopPurchaseOrdersWhereSQL(query resourcedomain.ListResourceShopPurchaseOrdersQuery) (string, []any) {
	where := `WHERE po.app_code = ?`
	args := []any{query.AppCode}
	if query.UserID > 0 {
		where += ` AND po.user_id = ?`
		args = append(args, query.UserID)
	}
	if query.Status != "" {
		where += ` AND po.status = ?`
		args = append(args, query.Status)
	}
	if query.ResourceType != "" {
		where += ` AND r.resource_type = ?`
		args = append(args, query.ResourceType)
	}
	if query.Keyword != "" {
		like := "%" + query.Keyword + "%"
		where += ` AND (po.order_id LIKE ? OR po.command_id LIKE ? OR r.resource_code LIKE ? OR r.name LIKE ?)`
		args = append(args, like, like, like, like)
	}
	return where, args
}

func normalizeResourceShopItemsQuery(query resourcedomain.ListResourceShopItemsQuery) resourcedomain.ListResourceShopItemsQuery {
	query.ResourceType = resourcedomain.NormalizeResourceType(query.ResourceType)
	if query.ResourceType != "" && !resourcedomain.ResourceTypeSellableInShop(query.ResourceType) {
		query.ResourceType = "__invalid__"
	}
	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 normalizeResourceShopPurchaseOrdersQuery(query resourcedomain.ListResourceShopPurchaseOrdersQuery) resourcedomain.ListResourceShopPurchaseOrdersQuery {
	if query.UserID < 0 {
		query.UserID = 0
	}
	query.ResourceType = resourcedomain.NormalizeResourceType(query.ResourceType)
	if query.ResourceType != "" && !resourcedomain.ResourceTypeSellableInShop(query.ResourceType) {
		query.ResourceType = "__invalid__"
	}
	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 normalizeResourceShopItemsCommand(command resourcedomain.ResourceShopItemsCommand) resourcedomain.ResourceShopItemsCommand {
	for index := range command.Items {
		command.Items[index].Status = resourcedomain.NormalizeStatus(command.Items[index].Status)
	}
	return command
}

func validateResourceShopItemsCommand(command resourcedomain.ResourceShopItemsCommand) error {
	if len(command.Items) == 0 || command.OperatorUserID < 0 {
		return xerr.New(xerr.InvalidArgument, "resource shop command is incomplete")
	}
	for _, item := range command.Items {
		if item.ResourceID <= 0 {
			return xerr.New(xerr.InvalidArgument, "resource_id is required")
		}
		if !resourcedomain.ValidStatus(item.Status) {
			return xerr.New(xerr.InvalidArgument, "status is invalid")
		}
		if !resourcedomain.ValidShopDurationDays(item.DurationDays) {
			return xerr.New(xerr.InvalidArgument, "resource shop duration is invalid")
		}
		if item.EffectiveFromMS < 0 || item.EffectiveToMS < 0 || (item.EffectiveFromMS > 0 && item.EffectiveToMS > 0 && item.EffectiveToMS <= item.EffectiveFromMS) {
			return xerr.New(xerr.InvalidArgument, "resource shop effective time is invalid")
		}
	}
	return nil
}

func validateResourceShopResource(resource resourcedomain.Resource) error {
	if !resourcedomain.ResourceTypeSellableInShop(resource.ResourceType) {
		return xerr.New(xerr.InvalidArgument, "resource_type cannot be sold in shop")
	}
	if err := validateGrantableResource(resource); err != nil {
		return err
	}
	if resourcedomain.NormalizePriceType(resource.PriceType) != resourcedomain.PriceTypeCoin || resource.CoinPrice <= 0 {
		return xerr.New(xerr.InvalidArgument, "resource coin price is required")
	}
	return nil
}

func validatePurchasableResourceShopItem(item resourcedomain.ResourceShopItem, nowMs int64) error {
	if item.Status != resourcedomain.StatusActive {
		return xerr.New(xerr.Conflict, "resource shop item is disabled")
	}
	if item.EffectiveFromMS > 0 && item.EffectiveFromMS > nowMs {
		return xerr.New(xerr.Conflict, "resource shop item is not effective")
	}
	if item.EffectiveToMS > 0 && item.EffectiveToMS <= nowMs {
		return xerr.New(xerr.Conflict, "resource shop item is expired")
	}
	if !resourcedomain.ValidShopDurationDays(item.DurationDays) || item.CoinPrice <= 0 {
		return xerr.New(xerr.InvalidArgument, "resource shop item price is invalid")
	}
	return validateResourceShopResource(item.Resource)
}

func resourceShopPurchaseRequestHash(command resourcedomain.ResourceShopPurchaseCommand) string {
	return stableHash(fmt.Sprintf("resource_shop_purchase|%s|%d|%d",
		appcode.Normalize(command.AppCode), command.UserID, command.ShopItemID,
	))
}
