<?php

declare(strict_types=1);

namespace app\api\controller\v1;

use ba\Random;
use app\common\controller\Api;
use app\common\facade\Token;
use app\common\library\Auth as UserAuth;
use app\common\model\MallItem;
use app\common\model\MallPlayxClaimLog;
use app\common\model\MallPlayxDailyPush;
use app\common\model\MallPlayxSession;
use app\common\model\MallPlayxOrder;
use app\common\model\MallPlayxUserAsset;
use support\think\Db;
use Webman\Http\Request;
use support\Response;

/**
 * PlayX 积分商城 API
 */
class Playx extends Api
{
    /**
     * 从请求解析 mall_playx_user_asset.id（muser token、session、user_id 均指向资产表主键或 playx_user_id）
     */
    private function resolvePlayxAssetIdFromRequest(Request $request): ?int
    {
        $sessionId = strval($request->post('session_id', $request->get('session_id', '')));
        if ($sessionId !== '') {
            $session = MallPlayxSession::where('session_id', $sessionId)->find();
            if ($session) {
                $expireTime = intval($session->expire_time ?? 0);
                if ($expireTime > time()) {
                    $asset = MallPlayxUserAsset::where('playx_user_id', strval($session->user_id ?? ''))->find();
                    if ($asset) {
                        return intval($asset->getKey());
                    }
                }
            }
            $assetId = $this->resolveAssetIdByToken($sessionId);
            if ($assetId !== null) {
                return $assetId;
            }
        }

        $token = strval($request->post('token', $request->get('token', '')));
        if ($token === '') {
            $token = get_auth_token(['ba', 'token'], $request);
        }
        if ($token !== '') {
            return $this->resolveAssetIdByToken($token);
        }

        $userId = strval($request->post('user_id', $request->get('user_id', '')));
        if ($userId === '') {
            return null;
        }
        if (ctype_digit($userId)) {
            return intval($userId);
        }

        $asset = MallPlayxUserAsset::where('playx_user_id', $userId)->find();
        if ($asset) {
            return intval($asset->getKey());
        }

        return null;
    }

    private function resolveAssetIdByToken(string $token): ?int
    {
        $tokenData = Token::get($token);
        $tokenType = strval($tokenData['type'] ?? '');
        $isMemberOrMall = $tokenType === UserAuth::TOKEN_TYPE || $tokenType === UserAuth::TOKEN_TYPE_MALL_USER;
        if (!empty($tokenData)
            && $isMemberOrMall
            && intval($tokenData['expire_time'] ?? 0) > time()
            && intval($tokenData['user_id'] ?? 0) > 0
        ) {
            return intval($tokenData['user_id']);
        }

        return null;
    }

    private function buildTempPhone(): ?string
    {
        for ($i = 0; $i < 8; $i++) {
            $candidate = '13' . str_pad(strval(mt_rand(0, 999999999)), 9, '0', STR_PAD_LEFT);
            if (!MallPlayxUserAsset::where('phone', $candidate)->find()) {
                return $candidate;
            }
        }

        return null;
    }

    private function ensureAssetForPlayx(string $playxUserId, string $username): ?MallPlayxUserAsset
    {
        $asset = MallPlayxUserAsset::where('playx_user_id', $playxUserId)->find();
        if ($asset) {
            return $asset;
        }

        $effectiveUsername = trim($username);
        if ($effectiveUsername === '') {
            $effectiveUsername = 'playx_' . $playxUserId;
        }
        $byName = MallPlayxUserAsset::where('username', $effectiveUsername)->find();
        if ($byName) {
            $byName->playx_user_id = $playxUserId;
            $byName->save();

            return $byName;
        }

        $phone = $this->buildTempPhone();
        if ($phone === null) {
            return null;
        }
        $pwd = hash_password(Random::build('alnum', 16));
        $now = time();

        return MallPlayxUserAsset::create([
            'playx_user_id'            => $playxUserId,
            'username'                 => $effectiveUsername,
            'phone'                    => $phone,
            'password'                 => $pwd,
            'admin_id'                 => 0,
            'locked_points'            => 0,
            'available_points'         => 0,
            'today_limit'              => 0,
            'today_claimed'            => 0,
            'today_limit_date'         => null,
            'create_time'              => $now,
            'update_time'              => $now,
        ]);
    }

    private function getAssetById(int $assetId): ?MallPlayxUserAsset
    {
        return MallPlayxUserAsset::where('id', $assetId)->find();
    }

    /**
     * Daily Push API - PlayX 调用商城接收 T+1 数据
     * POST /api/v1/playx/daily-push
     */
    public function dailyPush(Request $request): Response
    {
        $response = $this->initializeApi($request);
        if ($response !== null) {
            return $response;
        }

        $body = $request->post();
        if (empty($body)) {
            $raw = $request->rawBody();
            if ($raw) {
                $body = json_decode($raw, true) ?? [];
            }
        }

        $requestId = $body['request_id'] ?? '';
        $date = $body['date'] ?? '';
        $playxUserId = strval($body['user_id'] ?? '');
        $yesterdayWinLossNet = $body['yesterday_win_loss_net'] ?? 0;
        $yesterdayTotalDeposit = $body['yesterday_total_deposit'] ?? 0;

        if ($requestId === '' || $date === '' || $playxUserId === '') {
            return $this->error(__('Missing required fields: request_id, date, user_id'));
        }

        $secret = config('playx.daily_push_secret', '');
        if ($secret !== '') {
            $sig = $request->header('X-Signature', '');
            $ts = $request->header('X-Timestamp', '');
            $rid = $request->header('X-Request-Id', '');
            if ($sig === '' || $ts === '' || $rid === '') {
                return $this->error(__('Invalid signature'), null, 0, ['statusCode' => 401]);
            }
            $canonical = $ts . "\n" . $rid . "\nPOST\n/api/v1/playx/daily-push\n" . hash('sha256', json_encode($body));
            $expected = hash_hmac('sha256', $canonical, $secret);
            if (!hash_equals($expected, $sig)) {
                return $this->error(__('Invalid signature'), null, 0, ['statusCode' => 401]);
            }
        }

        $exists = MallPlayxDailyPush::where('user_id', $playxUserId)->where('date', $date)->find();
        if ($exists) {
            return $this->success('', [
                'request_id' => $requestId,
                'accepted'   => true,
                'deduped'    => true,
                'message'    => __('Duplicate input'),
            ]);
        }

        Db::startTrans();
        try {
            MallPlayxDailyPush::create([
                'user_id'                  => $playxUserId,
                'date'                     => $date,
                'username'                 => $body['username'] ?? '',
                'yesterday_win_loss_net'   => $yesterdayWinLossNet,
                'yesterday_total_deposit' => $yesterdayTotalDeposit,
                'lifetime_total_deposit'  => $body['lifetime_total_deposit'] ?? 0,
                'lifetime_total_withdraw' => $body['lifetime_total_withdraw'] ?? 0,
                'create_time'              => time(),
            ]);

            $returnRatio = config('playx.return_ratio', 0.1);
            $unlockRatio = config('playx.unlock_ratio', 0.1);

            $newLocked = 0;
            if ($yesterdayWinLossNet < 0) {
                $newLocked = intval(round(abs(floatval($yesterdayWinLossNet)) * $returnRatio));
            }
            $todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio));

            $asset = $this->ensureAssetForPlayx($playxUserId, strval($body['username'] ?? ''));
            if (!$asset) {
                throw new \RuntimeException(__('Failed to map playx user to mall user'));
            }
            $todayLimitDate = $date;
            if ($asset->today_limit_date !== $todayLimitDate) {
                $asset->today_claimed = 0;
                $asset->today_limit_date = $todayLimitDate;
            }
            $asset->locked_points = intval($asset->locked_points ?? 0) + $newLocked;
            $asset->today_limit = $todayLimit;
            $asset->playx_user_id = $playxUserId;
            $uname = trim(strval($body['username'] ?? ''));
            if ($uname !== '') {
                $asset->username = $uname;
            }
            $asset->save();

            Db::commit();
        } catch (\Throwable $e) {
            Db::rollback();
            return $this->error($e->getMessage());
        }

        return $this->success('', [
            'request_id' => $requestId,
            'accepted'   => true,
            'deduped'    => false,
            'message'    => __('Ok'),
        ]);
    }

    /**
     * Token 验证 - POST /api/v1/playx/verify-token
     * 配置 playx.verify_token_local_only=true 时仅本地校验 token（不请求 PlayX）。
     */
    public function verifyToken(Request $request): Response
    {
        $response = $this->initializeApi($request);
        if ($response !== null) {
            return $response;
        }

        $token = strval($request->post('token', $request->post('session', $request->get('token', ''))));
        if ($token === '') {
            return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
        }

        if (config('playx.verify_token_local_only', false)) {
            return $this->verifyTokenLocal($token);
        }

        $baseUrl = config('playx.api.base_url', '');
        $verifyUrl = config('playx.api.token_verify_url', '/api/v1/auth/verify-token');
        if ($baseUrl === '') {
            return $this->error(__('PlayX API not configured'));
        }

        try {
            $client = new \GuzzleHttp\Client([
                'base_uri' => rtrim($baseUrl, '/') . '/',
                'timeout'  => 10,
            ]);
            $res = $client->post($verifyUrl, [
                'json' => [
                    'request_id' => 'mall_' . uniqid(),
                    'token'      => $token,
                ],
            ]);
            $code = $res->getStatusCode();
            $data = json_decode(strval($res->getBody()), true);
            if ($code !== 200 || empty($data['user_id'])) {
                $remoteMsg = $data['message'] ?? '';
                $msg = is_string($remoteMsg) && $remoteMsg !== '' ? $remoteMsg : __('Invalid token');

                return $this->error($msg, null, 0, ['statusCode' => 401]);
            }

            $userId = strval($data['user_id']);
            $username = strval($data['username'] ?? '');

            $expireAt = time() + intval(config('playx.session_expire_seconds', 3600));
            if (!empty($data['token_expire_at'])) {
                $ts = strtotime(strval($data['token_expire_at']));
                if ($ts !== false && $ts > 0) {
                    $expireAt = intval($ts);
                }
            }

            $sessionId = bin2hex(random_bytes(16));
            MallPlayxSession::create([
                'session_id'  => $sessionId,
                'user_id'     => $userId,
                'username'    => $username,
                'expire_time' => $expireAt,
                'create_time' => time(),
                'update_time' => time(),
            ]);

            return $this->success('', [
                'session_id'      => $sessionId,
                'user_id'         => $userId,
                'username'        => $username,
                'token_expire_at' => date('c', $expireAt),
            ]);
        } catch (\Throwable $e) {
            return $this->error($e->getMessage(), null, 0, ['statusCode' => 500]);
        }
    }

    /**
     * 本地校验 temLogin 等写入的商城 token（类型 muser），写入 mall_playx_session
     */
    private function verifyTokenLocal(string $token): Response
    {
        $tokenData = Token::get($token);
        if (empty($tokenData) || (isset($tokenData['expire_time']) && intval($tokenData['expire_time']) <= time())) {
            return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
        }
        $tokenType = strval($tokenData['type'] ?? '');
        if ($tokenType !== UserAuth::TOKEN_TYPE_MALL_USER) {
            return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
        }
        $assetId = intval($tokenData['user_id'] ?? 0);
        if ($assetId <= 0) {
            return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
        }

        $asset = MallPlayxUserAsset::where('id', $assetId)->find();
        if (!$asset) {
            return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
        }

        $playxUserId = strval($asset->playx_user_id ?? '');
        if ($playxUserId === '') {
            $playxUserId = strval($assetId);
        }

        $expireAt = time() + intval(config('playx.session_expire_seconds', 3600));
        $sessionId = bin2hex(random_bytes(16));
        MallPlayxSession::create([
            'session_id'  => $sessionId,
            'user_id'     => $playxUserId,
            'username'    => strval($asset->username ?? ''),
            'expire_time' => $expireAt,
            'create_time' => time(),
            'update_time' => time(),
        ]);

        return $this->success('', [
            'session_id'      => $sessionId,
            'user_id'         => $playxUserId,
            'username'        => strval($asset->username ?? ''),
            'token_expire_at' => date('c', $expireAt),
        ]);
    }

    /**
     * 用户资产
     * GET /api/v1/playx/assets?user_id=xxx
     */
    public function assets(Request $request): Response
    {
        $response = $this->initializeApi($request);
        if ($response !== null) {
            return $response;
        }

        $assetId = $this->resolvePlayxAssetIdFromRequest($request);
        if ($assetId === null) {
            return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
        }

        $asset = $this->getAssetById($assetId);
        if (!$asset) {
            return $this->success('', [
                'locked_points'   => 0,
                'available_points' => 0,
                'today_limit'     => 0,
                'today_claimed'    => 0,
                'withdrawable_cash' => 0,
            ]);
        }

        $ratio = config('playx.points_to_cash_ratio', 0.1);
        $withdrawableCash = round($asset->available_points * $ratio, 2);

        return $this->success('', [
            'locked_points'    => $asset->locked_points,
            'available_points' => $asset->available_points,
            'today_limit'      => $asset->today_limit,
            'today_claimed'    => $asset->today_claimed,
            'withdrawable_cash' => $withdrawableCash,
        ]);
    }

    /**
     * 领取
     * POST /api/v1/playx/claim
     */
    public function claim(Request $request): Response
    {
        $response = $this->initializeApi($request);
        if ($response !== null) {
            return $response;
        }

        $claimRequestId = strval($request->post('claim_request_id', ''));
        $assetId = $this->resolvePlayxAssetIdFromRequest($request);
        if ($claimRequestId === '' || $assetId === null) {
            return $this->error(__('claim_request_id and user_id/session_id required'));
        }

        $asset = $this->getAssetById($assetId);
        if (!$asset || strval($asset->playx_user_id ?? '') === '') {
            return $this->error(__('User asset not found'));
        }
        $playxUserId = strval($asset->playx_user_id);

        $exists = MallPlayxClaimLog::where('claim_request_id', $claimRequestId)->find();
        if ($exists) {
            return $this->success('', $this->formatAsset($asset));
        }

        $todayLimitDate = date('Y-m-d');
        if ($asset->today_limit_date !== $todayLimitDate) {
            $asset->today_claimed = 0;
            $asset->today_limit_date = $todayLimitDate;
        }

        $remain = $asset->today_limit - $asset->today_claimed;
        if ($asset->locked_points <= 0 || $remain <= 0) {
            return $this->error(__('No points to claim or limit reached'));
        }

        $canClaim = min($asset->locked_points, $remain);

        Db::startTrans();
        try {
            MallPlayxClaimLog::create([
                'claim_request_id' => $claimRequestId,
                'user_id'          => $playxUserId,
                'claimed_amount'   => $canClaim,
                'create_time'      => time(),
            ]);

            $asset->locked_points -= $canClaim;
            $asset->available_points += $canClaim;
            $asset->today_claimed += $canClaim;
            $asset->save();

            Db::commit();
        } catch (\Throwable $e) {
            Db::rollback();
            return $this->error($e->getMessage());
        }

        $asset->refresh();
        return $this->success(__('Claim success'), $this->formatAsset($asset));
    }

    /**
     * 商品列表
     * GET /api/v1/playx/items?type=BONUS|PHYSICAL|WITHDRAW
     */
    public function items(Request $request): Response
    {
        $response = $this->initializeApi($request);
        if ($response !== null) {
            return $response;
        }

        $type = $request->get('type', '');
        $typeMap = ['BONUS' => 1, 'PHYSICAL' => 2, 'WITHDRAW' => 3];
        $query = MallItem::where('status', 1);
        if ($type !== '' && isset($typeMap[$type])) {
            $query->where('type', $typeMap[$type]);
        }
        $list = $query->order('sort', 'asc')->select();

        return $this->success('', ['list' => $list->toArray()]);
    }

    /**
     * 红利兑换
     * POST /api/v1/playx/bonus/redeem
     */
    public function bonusRedeem(Request $request): Response
    {
        return $this->redeemBonus($request);
    }

    /**
     * 实物兑换
     * POST /api/v1/playx/physical/redeem
     */
    public function physicalRedeem(Request $request): Response
    {
        return $this->redeemPhysical($request);
    }

    /**
     * 提现申请
     * POST /api/v1/playx/withdraw/apply
     */
    public function withdrawApply(Request $request): Response
    {
        return $this->redeemWithdraw($request);
    }

    /**
     * 订单列表
     * GET /api/v1/playx/orders?user_id=xxx
     */
    public function orders(Request $request): Response
    {
        $response = $this->initializeApi($request);
        if ($response !== null) {
            return $response;
        }

        $assetId = $this->resolvePlayxAssetIdFromRequest($request);
        if ($assetId === null) {
            return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
        }
        $asset = $this->getAssetById($assetId);
        if (!$asset || strval($asset->playx_user_id ?? '') === '') {
            return $this->success('', ['list' => []]);
        }

        $list = MallPlayxOrder::where('user_id', strval($asset->playx_user_id))
            ->with(['mallItem'])
            ->order('id', 'desc')
            ->limit(100)
            ->select();

        return $this->success('', ['list' => $list->toArray()]);
    }

    private function formatAsset(?MallPlayxUserAsset $asset): array
    {
        if (!$asset) {
            return [
                'locked_points'    => 0,
                'available_points' => 0,
                'today_limit'      => 0,
                'today_claimed'    => 0,
                'withdrawable_cash' => 0,
            ];
        }
        $ratio = config('playx.points_to_cash_ratio', 0.1);
        return [
            'locked_points'     => $asset->locked_points,
            'available_points'   => $asset->available_points,
            'today_limit'       => $asset->today_limit,
            'today_claimed'     => $asset->today_claimed,
            'withdrawable_cash' => round($asset->available_points * $ratio, 2),
        ];
    }

    private function redeemBonus(Request $request): Response
    {
        $response = $this->initializeApi($request);
        if ($response !== null) {
            return $response;
        }

        $itemId = intval($request->post('item_id', 0));
        $assetId = $this->resolvePlayxAssetIdFromRequest($request);
        if ($itemId <= 0 || $assetId === null) {
            return $this->error(__('item_id and user_id/session_id required'));
        }

        $item = MallItem::where('id', $itemId)->where('type', MallItem::TYPE_BONUS)->where('status', 1)->find();
        if (!$item) {
            return $this->error(__('Item not found or not available'));
        }

        $asset = $this->getAssetById($assetId);
        if (!$asset || strval($asset->playx_user_id ?? '') === '' || $asset->available_points < $item->score) {
            return $this->error(__('Insufficient points'));
        }
        $playxUserId = strval($asset->playx_user_id);

        $multiplier = intval($item->multiplier ?? 0);
        if ($multiplier <= 0) {
            $multiplier = 1;
        }
        $amount = floatval($item->amount ?? 0);

        Db::startTrans();
        try {
            $asset->available_points -= $item->score;
            $asset->save();

            $orderNo = 'BONUS_ORD' . date('YmdHis') . mt_rand(1000, 9999);
            $order = MallPlayxOrder::create([
                'user_id'                  => $playxUserId,
                'type'                     => MallPlayxOrder::TYPE_BONUS,
                'status'                   => MallPlayxOrder::STATUS_PENDING,
                'mall_item_id'             => $item->id,
                'points_cost'              => $item->score,
                'amount'                   => $amount,
                'multiplier'               => $multiplier,
                'external_transaction_id'  => $orderNo,
                'grant_status'             => MallPlayxOrder::GRANT_NOT_SENT,
                'create_time'              => time(),
                'update_time'              => time(),
            ]);

            Db::commit();
        } catch (\Throwable $e) {
            Db::rollback();
            return $this->error($e->getMessage());
        }

        $baseUrl = config('playx.api.base_url', '');
        if ($baseUrl !== '') {
            $this->callPlayxBonusGrant($order, $item, $playxUserId);
        }

        return $this->success(__('Redeem submitted, please wait about 10 minutes'), [
            'order_id' => $order->id,
            'status'   => 'PENDING',
        ]);
    }

    private function redeemPhysical(Request $request): Response
    {
        $response = $this->initializeApi($request);
        if ($response !== null) {
            return $response;
        }

        $itemId = intval($request->post('item_id', 0));
        $assetId = $this->resolvePlayxAssetIdFromRequest($request);
        $receiverName = $request->post('receiver_name', '');
        $receiverPhone = $request->post('receiver_phone', '');
        $receiverAddress = $request->post('receiver_address', '');
        if ($itemId <= 0 || $assetId === null || $receiverName === '' || $receiverPhone === '' || $receiverAddress === '') {
            return $this->error(__('Missing required fields'));
        }

        $item = MallItem::where('id', $itemId)->where('type', MallItem::TYPE_PHYSICAL)->where('status', 1)->find();
        if (!$item) {
            return $this->error(__('Item not found or not available'));
        }
        if (isset($item->stock) && $item->stock < 1) {
            return $this->error(__('Out of stock'));
        }

        $asset = $this->getAssetById($assetId);
        if (!$asset || strval($asset->playx_user_id ?? '') === '' || $asset->available_points < $item->score) {
            return $this->error(__('Insufficient points'));
        }
        $playxUserId = strval($asset->playx_user_id);

        Db::startTrans();
        try {
            $asset->available_points -= $item->score;
            $asset->save();

            MallPlayxOrder::create([
                'user_id'          => $playxUserId,
                'type'             => MallPlayxOrder::TYPE_PHYSICAL,
                'status'           => MallPlayxOrder::STATUS_PENDING,
                'mall_item_id'     => $item->id,
                'points_cost'      => $item->score,
                'receiver_name'    => $receiverName,
                'receiver_phone'   => $receiverPhone,
                'receiver_address' => $receiverAddress,
                'create_time'      => time(),
                'update_time'      => time(),
            ]);

            if (isset($item->stock)) {
                $item->stock -= 1;
                $item->save();
            }

            Db::commit();
        } catch (\Throwable $e) {
            Db::rollback();
            return $this->error($e->getMessage());
        }

        return $this->success(__('Redeem success'));
    }

    private function redeemWithdraw(Request $request): Response
    {
        $response = $this->initializeApi($request);
        if ($response !== null) {
            return $response;
        }

        $itemId = intval($request->post('item_id', 0));
        $assetId = $this->resolvePlayxAssetIdFromRequest($request);
        if ($itemId <= 0 || $assetId === null) {
            return $this->error(__('item_id and user_id/session_id required'));
        }

        $item = MallItem::where('id', $itemId)->where('type', MallItem::TYPE_WITHDRAW)->where('status', 1)->find();
        if (!$item) {
            return $this->error(__('Item not found or not available'));
        }

        $asset = $this->getAssetById($assetId);
        if (!$asset || strval($asset->playx_user_id ?? '') === '' || $asset->available_points < $item->score) {
            return $this->error(__('Insufficient points'));
        }
        $playxUserId = strval($asset->playx_user_id);

        $multiplier = intval($item->multiplier ?? 0);
        if ($multiplier <= 0) {
            $multiplier = 1;
        }
        $amount = floatval($item->amount ?? 0);

        Db::startTrans();
        try {
            $asset->available_points -= $item->score;
            $asset->save();

            $orderNo = 'WITHDRAW_ORD' . date('YmdHis') . mt_rand(1000, 9999);
            $order = MallPlayxOrder::create([
                'user_id'                 => $playxUserId,
                'type'                    => MallPlayxOrder::TYPE_WITHDRAW,
                'status'                  => MallPlayxOrder::STATUS_PENDING,
                'mall_item_id'            => $item->id,
                'points_cost'             => $item->score,
                'amount'                  => $amount,
                'multiplier'              => $multiplier,
                'external_transaction_id' => $orderNo,
                'grant_status'            => MallPlayxOrder::GRANT_NOT_SENT,
                'create_time'             => time(),
                'update_time'             => time(),
            ]);

            Db::commit();
        } catch (\Throwable $e) {
            Db::rollback();
            return $this->error($e->getMessage());
        }

        $baseUrl = config('playx.api.base_url', '');
        if ($baseUrl !== '') {
            $this->callPlayxBalanceCredit($order, $playxUserId);
        }

        return $this->success(__('Withdraw submitted, please wait about 10 minutes'), [
            'order_id' => $order->id,
            'status'   => 'PENDING',
        ]);
    }

    private function callPlayxBonusGrant(MallPlayxOrder $order, MallItem $item, string $userId): void
    {
        $baseUrl = rtrim(config('playx.api.base_url', ''), '/');
        $url = config('playx.api.bonus_grant_url', '/api/v1/bonus/grant');
        if ($baseUrl === '') {
            return;
        }

        try {
            $client = new \GuzzleHttp\Client(['timeout' => 15]);
            $res = $client->post($baseUrl . $url, [
                'json' => [
                    'request_id'              => 'mall_bonus_' . uniqid(),
                    'externalTransactionId'   => $order->external_transaction_id,
                    'user_id'                 => $userId,
                    'amount'                  => $order->amount,
                    'rewardName'              => $item->title ?? '',
                    'category'                => $item->category ?? 'daily',
                    'categoryTitle'           => $item->category_title ?? '',
                    'multiplier'              => $order->multiplier,
                ],
            ]);
            $data = json_decode(strval($res->getBody()), true);
            if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
                $order->playx_transaction_id = $data['playx_transaction_id'] ?? '';
                $order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
                $order->save();
            } else {
                $order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
                $order->fail_reason = $data['message'] ?? 'unknown';
                $order->save();
            }
        } catch (\Throwable $e) {
            $order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
            $order->fail_reason = $e->getMessage();
            $order->save();
        }
    }

    private function callPlayxBalanceCredit(MallPlayxOrder $order, string $userId): void
    {
        $baseUrl = rtrim(config('playx.api.base_url', ''), '/');
        $url = config('playx.api.balance_credit_url', '/api/v1/balance/credit');
        if ($baseUrl === '') {
            return;
        }

        try {
            $client = new \GuzzleHttp\Client(['timeout' => 15]);
            $res = $client->post($baseUrl . $url, [
                'json' => [
                    'request_id'            => 'mall_withdraw_' . uniqid(),
                    'externalTransactionId' => $order->external_transaction_id,
                    'user_id'               => $userId,
                    'amount'                => $order->amount,
                    'multiplier'            => $order->multiplier,
                ],
            ]);
            $data = json_decode(strval($res->getBody()), true);
            if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
                $order->playx_transaction_id = $data['playx_transaction_id'] ?? '';
                $order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
                $order->save();
            } else {
                $order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
                $order->fail_reason = $data['message'] ?? 'unknown';
                $order->save();
            }
        } catch (\Throwable $e) {
            $order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
            $order->fail_reason = $e->getMessage();
            $order->save();
        }
    }
}
