<?php

namespace app\admin\controller\order;

use app\common\controller\Backend;
use app\common\library\finance\DDPayGateway;
use app\common\library\finance\MockPay;
use support\think\Db;
use support\Response;
use Throwable;
use Webman\Http\Request as WebmanRequest;

/**
 * 提现订单
 *
 * 当前审核流转：
 *   - 用户端提交提现时，立即冻结余额（user.coin - apply_amount）并生成 withdraw_order（status=0）与 withdraw 流水（direction=2）。
 *   - 管理员在后台审核：通过（approve）→ status=1；拒绝（reject）→ status=2 并回冲用户余额与流水。
 *   - 通过流程不再额外扣钱包，因为申请时已冻结；仅在管理员调整 amount/fee 时写一条差额流水。
 */
class WithdrawOrder extends Backend
{
    protected ?object $model = null;

    protected bool $modelValidate = true;

    protected bool $modelSceneValidate = true;

    protected string|array $quickSearchField = ['id', 'order_no', 'idempotency_key', 'pay_channel', 'receive_type', 'receive_account', 'ddpay_receiver_name', 'receiver_email', 'receiver_mobile', 'remark'];

    protected string|array $defaultSortField = ['id' => 'desc'];

    protected string|array $orderGuarantee = ['id' => 'desc'];

    protected array $withJoinTable = ['user', 'channel', 'reviewAdmin'];

    protected function initController(WebmanRequest $request): ?Response
    {
        $this->model = new \app\common\model\WithdrawOrder();
        return null;
    }

    protected function _index(): Response
    {
        if ($this->request && $this->request->get('select')) {
            return $this->select($this->request);
        }

        list($where, $alias, $limit, $order) = $this->queryBuilder();
        $table = strtolower($this->model->getTable());
        $mainShort = $alias[$table] ?? '';
        if ($mainShort !== '' && $this->auth && !$this->auth->isSuperAdmin()) {
            $where[] = ['user.admin_id', 'in', $this->scopedAdminIds()];
        }

        $res = $this->model
            ->withJoin($this->withJoinTable, $this->withJoinType)
            ->with($this->withJoinTable)
            ->visible([
                'user' => ['username', 'phone'],
                'channel' => ['name'],
                'reviewAdmin' => ['username'],
            ])
            ->alias($alias)
            ->where($where)
            ->order($order)
            ->paginate($limit);

        return $this->success('', [
            'list'   => $res->items(),
            'total'  => $res->total(),
            'remark' => get_route_remark(),
        ]);
    }

    /**
     * GET 时返回关联信息，便于编辑弹窗直接渲染 user.username/channel.name
     */
    protected function _edit(): Response
    {
        $pk = $this->model->getPk();
        $id = $this->request ? ($this->request->post($pk) ?? $this->request->get($pk)) : null;
        if ($id === null || $id === '') {
            return $this->error(__('Parameter error'));
        }

        if ($this->request && $this->request->method() === 'POST') {
            // 历史 CRUD 的 POST 编辑已被 approve/reject 替代，这里阻止直接改金额绕过审核流程
            return $this->error(__('Please use approve/reject buttons to complete the review'));
        }

        $row = $this->loadWithRelations(intval(strval($id)));
        if (!$row) {
            return $this->error(__('Record not found'));
        }
        if (!$this->checkChannelScoped($row)) {
            return $this->error(__('You have no permission'));
        }
        return $this->success('', ['row' => $row]);
    }

    /**
     * 审核通过：允许调整 amount/fee；actual_amount 自动为 amount - fee。
     * 对金额差额自动在用户钱包与流水中做增减，保持账务平衡。
     */
    public function approve(WebmanRequest $request): Response
    {
        $response = $this->initializeBackend($request);
        if ($response !== null) {
            return $response;
        }
        if ($request->method() !== 'POST') {
            return $this->error(__('Parameter error'));
        }

        $id = $this->intParam($request->post('id'));
        if ($id <= 0) {
            return $this->error(__('Parameter error'));
        }

        $newAmount = $this->decimalParam($request->post('amount'), '0');
        $newFee = $this->decimalParam($request->post('fee'), '0');
        if (bccomp($newAmount, '0', 2) <= 0) {
            return $this->error(__('Apply amount must be greater than 0'));
        }
        if (bccomp($newFee, '0', 2) < 0) {
            return $this->error(__('Fee cannot be negative'));
        }
        if (bccomp($newFee, $newAmount, 2) > 0) {
            return $this->error(__('Fee cannot be greater than apply amount'));
        }
        $newActual = bcsub($newAmount, $newFee, 2);

        $remarkRaw = $request->post('remark');
        $remark = is_string($remarkRaw) ? trim($remarkRaw) : '';

        $order = Db::name('withdraw_order')->where('id', $id)->find();
        if (!$order) {
            return $this->error(__('Record not found'));
        }
        if (!$this->checkChannelScoped($order)) {
            return $this->error(__('You have no permission'));
        }
        $currentStatus = $this->intParam($order['status'] ?? 0);
        if ($currentStatus !== 0) {
            return $this->error(__('This order has already been reviewed'));
        }

        $userId = $this->intParam($order['user_id'] ?? 0);
        if ($userId <= 0) {
            return $this->error(__('Order is missing user info'));
        }
        $oldAmount = bcadd(strval($order['amount'] ?? '0'), '0', 2);
        $diff = bcsub($newAmount, $oldAmount, 2);

        $now = time();
        $adminId = $this->intParam($this->auth->id ?? 0);
        $adminName = $this->adminDisplayName();
        $channelIdRaw = $order['channel_id'] ?? null;
        $channelId = ($channelIdRaw === null || $channelIdRaw === '')
            ? null
            : $this->intParam($channelIdRaw);
        if ($remark === '') {
            $remark = '管理员（' . $adminName . '）审核通过：金额 '
                . $this->shortAmount($newAmount) . '，手续费 ' . $this->shortAmount($newFee)
                . '，实际到账 ' . $this->shortAmount($newActual);
        }

        Db::startTrans();
        try {
            // 金额调整差额处理
            $cmp = bccomp($diff, '0', 2);
            if ($cmp > 0) {
                // 新金额更大：再冻结用户 diff
                $userRow = Db::name('user')->where('id', $userId)->find();
                if (!$userRow) {
                    Db::rollback();
                    return $this->error(__('Related user does not exist'));
                }
                $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2);
                if (bccomp($beforeCoin, $diff, 2) < 0) {
                    Db::rollback();
                    return $this->error(__('User balance is insufficient to cover the adjustment difference'));
                }
                $afterCoin = bcsub($beforeCoin, $diff, 2);
                Db::name('user')->where('id', $userId)->update([
                    'coin'                => $afterCoin,
                    'total_withdraw_coin' => Db::raw('total_withdraw_coin + ' . $diff),
                    'update_time'         => $now,
                ]);
                Db::name('user_wallet_record')->insert([
                    'user_id'           => $userId,
                    'channel_id'        => $channelId,
                    'biz_type'          => 'withdraw',
                    'direction'         => 2,
                    'amount'            => $diff,
                    'balance_before'    => $beforeCoin,
                    'balance_after'     => $afterCoin,
                    'ref_type'          => 'withdraw_order',
                    'ref_id'            => $id,
                    'idempotency_key'   => 'wd_adjust_add_' . strval($order['order_no'] ?? $id) . '_' . $now,
                    'operator_admin_id' => $adminId > 0 ? $adminId : null,
                    'remark'            => '管理员（' . $adminName . '）审核调增申请金额差额 '
                        . $this->shortAmount($diff),
                    'create_time'       => $now,
                ]);
            } elseif ($cmp < 0) {
                // 新金额更小：退回差额
                $abs = bcsub('0', $diff, 2);
                $userRow = Db::name('user')->where('id', $userId)->find();
                if (!$userRow) {
                    Db::rollback();
                    return $this->error(__('Related user does not exist'));
                }
                $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2);
                $afterCoin = bcadd($beforeCoin, $abs, 2);
                Db::name('user')->where('id', $userId)->update([
                    'coin'                => $afterCoin,
                    'total_withdraw_coin' => Db::raw('total_withdraw_coin - ' . $abs),
                    'update_time'         => $now,
                ]);
                Db::name('user_wallet_record')->insert([
                    'user_id'           => $userId,
                    'channel_id'        => $channelId,
                    'biz_type'          => 'withdraw_refund',
                    'direction'         => 1,
                    'amount'            => $abs,
                    'balance_before'    => $beforeCoin,
                    'balance_after'     => $afterCoin,
                    'ref_type'          => 'withdraw_order',
                    'ref_id'            => $id,
                    'idempotency_key'   => 'wd_adjust_sub_' . strval($order['order_no'] ?? $id) . '_' . $now,
                    'operator_admin_id' => $adminId > 0 ? $adminId : null,
                    'remark'            => '管理员（' . $adminName . '）审核调减申请金额差额 '
                        . $this->shortAmount($abs),
                    'create_time'       => $now,
                ]);
            }

            Db::name('withdraw_order')->where('id', $id)->update([
                'amount'          => $newAmount,
                'fee'             => $newFee,
                'actual_amount'   => $newActual,
                'status'          => 1,
                'review_admin_id' => $adminId > 0 ? $adminId : null,
                'review_time'     => $now,
                'remark'          => substr($remark, 0, 255),
                'update_time'     => $now,
            ]);
            Db::commit();
        } catch (Throwable $e) {
            Db::rollback();
            return $this->error($e->getMessage());
        }

        // 审核通过后自动发起 DDPAY 出金（并在失败时回冲余额）
        try {
            $fresh = Db::name('withdraw_order')->where('id', $id)->find();
            if (!is_array($fresh)) {
                // 理论上不会发生：只写失败备注
                Db::name('withdraw_order')->where('id', $id)->update([
                    'remark' => '[ddpay] payout order missing',
                    'update_time' => time(),
                ]);
            } else {
                $orderNo = is_string($fresh['order_no'] ?? null) ? trim($fresh['order_no'] ?? '') : strval($fresh['order_no'] ?? '');
                $receiveType = is_string($fresh['receive_type'] ?? null) ? strtolower(trim($fresh['receive_type'] ?? '')) : '';
                $payChannel = is_string($fresh['pay_channel'] ?? null) ? strtolower(trim($fresh['pay_channel'] ?? '')) : '';

                // 模拟出金：审核通过即标记已打款，不回冲、不调用 DDPay
                if ($orderNo !== '' && MockPay::shouldSimulateWithdrawPayout($payChannel)) {
                    $prevRemark = is_string($fresh['remark'] ?? null) ? trim($fresh['remark']) : '';
                    $mockNote = '[mock] 管理员（' . $adminName . '）审核通过，模拟打款成功';
                    $finalRemark = $prevRemark === '' ? $mockNote : mb_substr($prevRemark . ' | ' . $mockNote, 0, 255);
                    Db::name('withdraw_order')
                        ->where('id', $id)
                        ->where('status', 1)
                        ->update([
                            'status'      => 3,
                            'remark'      => $finalRemark,
                            'update_time' => time(),
                        ]);
                } elseif ($orderNo !== '' && $receiveType === 'bank' && $payChannel === 'ddpay') {
                    $base = \app\common\library\finance\DDPayGateway::publicBaseUrlForCallbacks($request);
                    if ($base === '') {
                        $base = 'https://' . strval($request->host());
                    }
                    $callbackUrl = rtrim($base, '/') . '/api/finance/ddpayPayoutNotify';
                    $payoutAmount = is_string($fresh['actual_amount'] ?? null) && $fresh['actual_amount'] !== '' ? trim($fresh['actual_amount']) : $newActual;

                    $receiverName = is_string($fresh['ddpay_receiver_name'] ?? null) ? trim($fresh['ddpay_receiver_name'] ?? '') : '';
                    $receiverAccount = is_string($fresh['receive_account'] ?? null) ? trim($fresh['receive_account'] ?? '') : '';
                    $bankName = is_string($fresh['ddpay_bank_name'] ?? null) ? trim($fresh['ddpay_bank_name'] ?? '') : '';
                    $bankBranch = is_string($fresh['ddpay_bank_branch'] ?? null) ? trim($fresh['ddpay_bank_branch'] ?? '') : 'N/A';

                    if ($receiverName === '' || $receiverAccount === '' || $bankName === '') {
                        // 缺少 DDPAY 出金字段：回冲并置为失败
                        Db::startTrans();
                        try {
                            $amountRefund = bcadd(strval($fresh['amount'] ?? '0'), '0', 2);
                            $updated = Db::name('withdraw_order')
                                ->where('id', $id)
                                ->where('status', 1)
                                ->update([
                                    'status' => 2,
                                    'remark' => '[ddpay] missing payout fields',
                                    'update_time' => time(),
                                ]);
                            if ($updated > 0 && bccomp($amountRefund, '0', 2) > 0) {
                                $userIdRefund = intval(strval($fresh['user_id'] ?? 0));
                                $userRow = Db::name('user')->where('id', $userIdRefund)->find();
                                $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2);
                                $afterCoin = bcadd($beforeCoin, $amountRefund, 2);
                                Db::name('user')->where('id', $userIdRefund)->update([
                                    'coin' => $afterCoin,
                                    'total_withdraw_coin' => Db::raw('total_withdraw_coin - ' . $amountRefund),
                                    'update_time' => time(),
                                ]);
                                $idempotencyKey = 'wd_ddpay_failed_' . strval($orderNo);
                                $exists = Db::name('user_wallet_record')->where('idempotency_key', $idempotencyKey)->find();
                                if (!$exists) {
                                    $channelId = null;
                                    if (isset($fresh['channel_id']) && is_numeric(strval($fresh['channel_id']))) {
                                        $channelId = intval(strval($fresh['channel_id']));
                                    }
                                    Db::name('user_wallet_record')->insert([
                                        'user_id' => $userIdRefund,
                                        'channel_id' => $channelId,
                                        'biz_type' => 'withdraw_refund',
                                        'direction' => 1,
                                        'amount' => $amountRefund,
                                        'balance_before' => $beforeCoin,
                                        'balance_after' => $afterCoin,
                                        'ref_type' => 'withdraw_order',
                                        'ref_id' => intval(strval($id)),
                                        'idempotency_key' => $idempotencyKey,
                                        'operator_admin_id' => null,
                                        'remark' => '[ddpay] missing payout fields refund',
                                        'create_time' => time(),
                                    ]);
                                }
                            }
                            Db::commit();
                        } catch (Throwable $e2) {
                            Db::rollback();
                        }
                    } else {
                        $clientId = config('app.ddpay_client_id', '');
                        $identifier = config('app.ddpay_identifier', '');

                        $ddReq = [
                            'client_id' => $clientId,
                            'identifier' => $identifier,
                            'bill_number' => $orderNo,
                            'amount' => $payoutAmount,
                            'receiver_name' => $receiverName,
                            'receiver_account' => $receiverAccount,
                            'bank[name]' => $bankName,
                            'bank_branch' => $bankBranch,
                            'callback_url' => $callbackUrl,
                        ];

                        $ddResp = [];
                        $ts = '';
                        try {
                            $ddResp = DDPayGateway::payoutInitiation($ddReq);
                            $ts = is_string($ddResp['transaction_status'] ?? null) ? strtolower(trim($ddResp['transaction_status'] ?? '')) : '';
                        } catch (Throwable $e) {
                            // initiation 异常：同“failed”处理，回冲并置失败
                            $ts = 'failed';
                            $ddResp = ['error' => (string) $e->getMessage()];
                        }

                        if (is_array($ddResp)) {
                            Db::name('withdraw_order')
                                ->where('id', $id)
                                ->update([
                                    'ddpay_payout_snapshot' => json_encode([
                                        'init_request' => $ddReq,
                                        'init_response' => $ddResp,
                                    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                                ]);
                        }

                        if ($ts === 'completed') {
                            Db::name('withdraw_order')
                                ->where('id', $id)
                                ->where('status', 1)
                                ->update([
                                    'status' => 3,
                                    'remark' => '[ddpay] payout completed',
                                    'update_time' => time(),
                                ]);
                        } elseif ($ts === 'failed') {
                            $amountRefund = bcadd(strval($fresh['amount'] ?? '0'), '0', 2);
                            Db::startTrans();
                            try {
                                $updated = Db::name('withdraw_order')
                                    ->where('id', $id)
                                    ->where('status', 1)
                                    ->update([
                                        'status' => 2,
                                        'remark' => '[ddpay] payout failed',
                                        'update_time' => time(),
                                    ]);
                                if ($updated > 0 && bccomp($amountRefund, '0', 2) > 0) {
                                    $userIdRefund = intval(strval($fresh['user_id'] ?? 0));
                                    $userRow = Db::name('user')->where('id', $userIdRefund)->find();
                                    $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2);
                                    $afterCoin = bcadd($beforeCoin, $amountRefund, 2);
                                    Db::name('user')->where('id', $userIdRefund)->update([
                                        'coin' => $afterCoin,
                                        'total_withdraw_coin' => Db::raw('total_withdraw_coin - ' . $amountRefund),
                                        'update_time' => time(),
                                    ]);
                                    $idempotencyKey = 'wd_ddpay_failed_' . strval($orderNo);
                                    $exists = Db::name('user_wallet_record')->where('idempotency_key', $idempotencyKey)->find();
                                    if (!$exists) {
                                        $channelId = null;
                                        if (isset($fresh['channel_id']) && is_numeric(strval($fresh['channel_id']))) {
                                            $channelId = intval(strval($fresh['channel_id']));
                                        }
                                        Db::name('user_wallet_record')->insert([
                                            'user_id' => $userIdRefund,
                                            'channel_id' => $channelId,
                                            'biz_type' => 'withdraw_refund',
                                            'direction' => 1,
                                            'amount' => $amountRefund,
                                            'balance_before' => $beforeCoin,
                                            'balance_after' => $afterCoin,
                                            'ref_type' => 'withdraw_order',
                                            'ref_id' => intval(strval($id)),
                                            'idempotency_key' => $idempotencyKey,
                                            'operator_admin_id' => null,
                                            'remark' => '[ddpay] payout failed refund',
                                            'create_time' => time(),
                                        ]);
                                    }
                                }
                                Db::commit();
                            } catch (Throwable $e3) {
                                Db::rollback();
                            }
                        } else {
                            // pending：按文档做一次 status inquiry 兜底（Webhook 仍会最终落账）
                            try {
                                $inq = DDPayGateway::payoutStatusInquiry([
                                    'client_id' => $clientId,
                                    'bill_number' => $orderNo,
                                ]);
                                $ts2 = is_string($inq['transaction_status'] ?? null) ? strtolower(trim($inq['transaction_status'] ?? '')) : '';
                                if ($ts2 === 'completed') {
                                    Db::name('withdraw_order')
                                        ->where('id', $id)
                                        ->where('status', 1)
                                        ->update([
                                            'status' => 3,
                                            'remark' => '[ddpay] payout completed (after status inquiry)',
                                            'update_time' => time(),
                                        ]);
                                } elseif ($ts2 === 'failed') {
                                    $amountRefund = bcadd(strval($fresh['amount'] ?? '0'), '0', 2);
                                    Db::startTrans();
                                    try {
                                        $updated = Db::name('withdraw_order')
                                            ->where('id', $id)
                                            ->where('status', 1)
                                            ->update([
                                                'status' => 2,
                                                'remark' => '[ddpay] payout failed (after status inquiry)',
                                                'update_time' => time(),
                                            ]);
                                        if ($updated > 0 && bccomp($amountRefund, '0', 2) > 0) {
                                            $userIdRefund = intval(strval($fresh['user_id'] ?? 0));
                                            $userRow = Db::name('user')->where('id', $userIdRefund)->find();
                                            $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2);
                                            $afterCoin = bcadd($beforeCoin, $amountRefund, 2);
                                            Db::name('user')->where('id', $userIdRefund)->update([
                                                'coin' => $afterCoin,
                                                'total_withdraw_coin' => Db::raw('total_withdraw_coin - ' . $amountRefund),
                                                'update_time' => time(),
                                            ]);
                                            $idempotencyKey = 'wd_ddpay_failed_' . strval($orderNo);
                                            $exists = Db::name('user_wallet_record')->where('idempotency_key', $idempotencyKey)->find();
                                            if (!$exists) {
                                                $channelId = null;
                                                if (isset($fresh['channel_id']) && is_numeric(strval($fresh['channel_id']))) {
                                                    $channelId = intval(strval($fresh['channel_id']));
                                                }
                                                Db::name('user_wallet_record')->insert([
                                                    'user_id' => $userIdRefund,
                                                    'channel_id' => $channelId,
                                                    'biz_type' => 'withdraw_refund',
                                                    'direction' => 1,
                                                    'amount' => $amountRefund,
                                                    'balance_before' => $beforeCoin,
                                                    'balance_after' => $afterCoin,
                                                    'ref_type' => 'withdraw_order',
                                                    'ref_id' => intval(strval($id)),
                                                    'idempotency_key' => $idempotencyKey,
                                                    'operator_admin_id' => null,
                                                    'remark' => '[ddpay] payout failed refund (after status inquiry)',
                                                    'create_time' => time(),
                                                ]);
                                            }
                                        }
                                        Db::commit();
                                    } catch (Throwable $e4) {
                                        Db::rollback();
                                    }
                                }
                            } catch (Throwable $e5) {
                                // ignore：Webhook 会兜底
                            }
                        }
                    }
                }
            }
        } catch (Throwable $e) {
            // 外部出金调用失败不阻断审核流：只记录 remark（避免阻塞用户提现）
            Db::name('withdraw_order')->where('id', $id)->update([
                'remark' => '[ddpay] payout flow exception: ' . substr((string) $e->getMessage(), 0, 200),
                'update_time' => time(),
            ]);
        }

        $finalStatus = Db::name('withdraw_order')->where('id', $id)->value('status');
        $finalStatusInt = is_numeric($finalStatus) ? intval($finalStatus) : 1;

        return $this->success(__('Approved'), [
            'id'            => $id,
            'amount'        => $newAmount,
            'fee'           => $newFee,
            'actual_amount' => $newActual,
            'status'        => $finalStatusInt,
        ]);
    }

    /**
     * 审核拒绝：必须填写驳回原因（remark）。
     * 回冲申请时的冻结：user.coin += amount；total_withdraw_coin -= amount；写一条 withdraw_refund 流水。
     */
    public function reject(WebmanRequest $request): Response
    {
        $response = $this->initializeBackend($request);
        if ($response !== null) {
            return $response;
        }
        if ($request->method() !== 'POST') {
            return $this->error(__('Parameter error'));
        }

        $id = $this->intParam($request->post('id'));
        if ($id <= 0) {
            return $this->error(__('Parameter error'));
        }
        $remarkRaw = $request->post('remark');
        $remark = is_string($remarkRaw) ? trim($remarkRaw) : '';
        if ($remark === '') {
            return $this->error(__('Please provide reject reason'));
        }

        $order = Db::name('withdraw_order')->where('id', $id)->find();
        if (!$order) {
            return $this->error(__('Record not found'));
        }
        if (!$this->checkChannelScoped($order)) {
            return $this->error(__('You have no permission'));
        }
        $currentStatus = $this->intParam($order['status'] ?? 0);
        if ($currentStatus !== 0) {
            return $this->error(__('This order has already been reviewed'));
        }

        $userId = $this->intParam($order['user_id'] ?? 0);
        if ($userId <= 0) {
            return $this->error(__('Order is missing user info'));
        }
        $amount = bcadd(strval($order['amount'] ?? '0'), '0', 2);
        $channelIdRaw = $order['channel_id'] ?? null;
        $channelId = ($channelIdRaw === null || $channelIdRaw === '')
            ? null
            : $this->intParam($channelIdRaw);

        $now = time();
        $adminId = $this->intParam($this->auth->id ?? 0);
        $adminName = $this->adminDisplayName();

        Db::startTrans();
        try {
            $userRow = Db::name('user')->where('id', $userId)->find();
            if (!$userRow) {
                Db::rollback();
                return $this->error(__('Related user does not exist'));
            }
            $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2);
            $afterCoin = bcadd($beforeCoin, $amount, 2);
            Db::name('user')->where('id', $userId)->update([
                'coin'                => $afterCoin,
                'total_withdraw_coin' => Db::raw('total_withdraw_coin - ' . $amount),
                'update_time'         => $now,
            ]);

            Db::name('user_wallet_record')->insert([
                'user_id'           => $userId,
                'channel_id'        => $channelId,
                'biz_type'          => 'withdraw_refund',
                'direction'         => 1,
                'amount'            => $amount,
                'balance_before'    => $beforeCoin,
                'balance_after'     => $afterCoin,
                'ref_type'          => 'withdraw_order',
                'ref_id'            => $id,
                'idempotency_key'   => 'wd_reject_' . strval($order['order_no'] ?? $id) . '_' . $now,
                'operator_admin_id' => $adminId > 0 ? $adminId : null,
                'remark'            => '管理员（' . $adminName . '）驳回提现，退回冻结金额 '
                    . $this->shortAmount($amount) . '：' . $remark,
                'create_time'       => $now,
            ]);

            Db::name('withdraw_order')->where('id', $id)->update([
                'status'          => 2,
                'review_admin_id' => $adminId > 0 ? $adminId : null,
                'review_time'     => $now,
                'remark'          => substr($remark, 0, 255),
                'update_time'     => $now,
            ]);
            Db::commit();
        } catch (Throwable $e) {
            Db::rollback();
            return $this->error($e->getMessage());
        }

        return $this->success(__('Rejected'), [
            'id'     => $id,
            'status' => 2,
            'remark' => $remark,
        ]);
    }

    private function loadWithRelations(int $id): ?array
    {
        $row = $this->model
            ->withJoin($this->withJoinTable, $this->withJoinType)
            ->with($this->withJoinTable)
            ->visible([
                'user' => ['username', 'phone'],
                'channel' => ['name'],
                'reviewAdmin' => ['username'],
            ])
            ->where($this->model->getTable() . '.id', $id)
            ->find();
        if (!$row) {
            return null;
        }
        return $row->toArray();
    }

    private function checkChannelScoped(array|object $row): bool
    {
        if (!$this->auth || $this->auth->isSuperAdmin()) {
            return true;
        }
        $uidRaw = is_array($row) ? ($row['user_id'] ?? null) : ($row->user_id ?? null);
        $uid = $this->intParam($uidRaw);
        if ($uid <= 0) {
            return false;
        }
        $user = Db::name('user')->field(['id', 'admin_id'])->where('id', $uid)->find();
        if (!is_array($user)) {
            return false;
        }
        $ownerAdminId = $this->intParam($user['admin_id'] ?? 0);
        return $ownerAdminId > 0 && in_array($ownerAdminId, $this->scopedAdminIds(), true);
    }

    private function intParam($raw): int
    {
        if ($raw === null || $raw === '') {
            return 0;
        }
        if (!is_numeric(strval($raw))) {
            return 0;
        }
        return intval(strval($raw));
    }

    private function decimalParam($raw, string $default): string
    {
        if ($raw === null || $raw === '' || !is_numeric(strval($raw))) {
            return bcadd($default, '0', 2);
        }
        return bcadd(strval($raw), '0', 2);
    }

    private function adminDisplayName(): string
    {
        if (!$this->auth) {
            return 'admin';
        }
        $name = $this->auth->username ?? null;
        if (is_string($name) && $name !== '') {
            return $name;
        }
        $id = $this->intParam($this->auth->id ?? 0);
        return '#' . strval($id);
    }

    /**
     * 当前管理员可见的管理员ID集合（本人 + 下级角色组内管理员）
     *
     * @return int[]
     */
    private function scopedAdminIds(): array
    {
        if (!$this->auth) {
            return [0];
        }
        if ($this->auth->isSuperAdmin()) {
            return [];
        }
        $groupIds = $this->auth->getAdminChildGroups();
        $adminIds = $groupIds ? $this->auth->getGroupAdmins($groupIds) : [];
        $adminIds[] = $this->auth->id;
        $adminIds = array_map(fn($id) => $this->intParam($id), $adminIds);
        $adminIds = array_values(array_unique(array_filter($adminIds, fn($id) => $id > 0)));
        return $adminIds === [] ? [0] : $adminIds;
    }

    /**
     * 把 2 位小数金额压缩成最多 2 位小数用于展示（不影响落库精度）
     */
    private function shortAmount(string $amount): string
    {
        if (!is_numeric($amount)) {
            return $amount;
        }
        $normalized = bcadd($amount, '0', 2);
        $negative = false;
        if (str_starts_with($normalized, '-')) {
            $negative = true;
            $normalized = substr($normalized, 1);
        }
        $parts = explode('.', $normalized, 2);
        $intPart = $parts[0] ?? '0';
        $fracPart = $parts[1] ?? '0000';
        $displayFrac = substr($fracPart, 0, 2);
        $v = $intPart . '.' . str_pad($displayFrac, 2, '0');
        return $negative ? ('-' . $v) : $v;
    }

}
