<?php

declare(strict_types=1);

namespace app\common\library\finance;

use app\common\service\GameWebSocketEventBus;
use RuntimeException;
use support\think\Db;
use Throwable;

/**
 * 充值订单结算公共库
 *
 * 所有"把 deposit_order 变为成功并给玩家钱包加币"的逻辑必须收敛到这里，
 * 以便 mock 支付瞬时成功、未来第三方网关回调、历史数据的人工补单共用同一事务边界。
 *
 * 关键约束：
 *   - 只结算 status=0 的订单，幂等；重复调用同一订单返回现有结算结果；
 *   - 钱包流水 user_wallet_record 以 "deposit_settle_{order_no}" 为 idempotency_key，保证不重复入账；
 *   - 同时更新 user.coin 与 update_time，user_wallet_record 记录 balance_before/after 快照。
 */
final class DepositSettlement
{
    public const SOURCE_MOCK_GATEWAY = 'mock_gateway';

    public const SOURCE_ADMIN_APPROVE = 'admin_approve';

    public const SOURCE_THIRD_PARTY = 'third_party';

    /**
     * 结算指定订单。
     *
     * @param int         $orderId         deposit_order.id
     * @param string      $source          来源（SOURCE_* 常量），写入 remark
     * @param string      $sourceLabel     人类可读描述，写入 remark，如 "mock gateway auto settled"
     * @param int|null    $operatorAdminId 操作管理员 ID（仅管理员审核时有值）
     * @param string|null $extraRemark     追加到订单 remark（可选）
     *
     * @return array{
     *     order_id: int,
     *     order_no: string,
     *     amount: string,
     *     balance_before: string,
     *     balance_after: string,
     *     pay_time: int,
     *     already_settled: bool,
     * }
     *
     * @throws RuntimeException 订单不存在、金额非法、并发冲突等
     */
    public static function settle(
        int $orderId,
        string $source,
        string $sourceLabel,
        ?int $operatorAdminId = null,
        ?string $extraRemark = null
    ): array {
        if ($orderId <= 0) {
            throw new RuntimeException('Order id is invalid');
        }

        $order = Db::name('deposit_order')->where('id', $orderId)->find();
        if (!$order) {
            throw new RuntimeException('Order does not exist');
        }

        $orderNo = is_string($order['order_no']) ? $order['order_no'] : strval($order['order_no']);
        if ($orderNo === '') {
            throw new RuntimeException('Order number is empty');
        }

        $statusRaw = $order['status'] ?? 0;
        $status = is_numeric($statusRaw) ? intval($statusRaw) : 0;

        // 如果已结算，直接返回已有结果（幂等）
        if ($status === 1) {
            $userId = is_numeric($order['user_id'] ?? null) ? intval($order['user_id']) : 0;
            $coinAfter = '0.00';
            if ($userId > 0) {
                $coin = Db::name('user')->where('id', $userId)->value('coin');
                $coinAfter = is_string($coin) ? $coin : strval($coin);
            }
            $amt = self::amountString($order['amount'] ?? '0');
            $bns = self::amountString($order['bonus_amount'] ?? '0');
            return [
                'order_id'        => $orderId,
                'order_no'        => $orderNo,
                'amount'          => $amt,
                'bonus_amount'    => $bns,
                'credit'          => bcadd($amt, $bns, 2),
                'balance_before'  => $coinAfter,
                'balance_after'   => $coinAfter,
                'pay_time'        => is_numeric($order['pay_time'] ?? null) ? intval($order['pay_time']) : 0,
                'already_settled' => true,
            ];
        }

        if ($status !== 0 && $status !== 3) {
            throw new RuntimeException('Order status does not allow settlement');
        }

        $amount = self::amountString($order['amount'] ?? '0');
        if (bccomp($amount, '0', 2) <= 0) {
            throw new RuntimeException('Order amount is invalid');
        }
        $bonus = self::amountString($order['bonus_amount'] ?? '0');
        if (bccomp($bonus, '0', 2) < 0) {
            $bonus = '0.00';
        }
        $credit = bcadd($amount, $bonus, 2);

        $userId = is_numeric($order['user_id'] ?? null) ? intval($order['user_id']) : 0;
        if ($userId <= 0) {
            throw new RuntimeException('Order user is invalid');
        }

        $user = Db::name('user')->where('id', $userId)->find();
        if (!$user) {
            throw new RuntimeException('User does not exist');
        }

        $channelId = is_numeric($order['channel_id'] ?? null) ? intval($order['channel_id']) : null;
        $balanceBefore = self::amountString($user['coin'] ?? '0');
        $balanceAfter = bcadd($balanceBefore, $credit, 2);

        $now = time();
        $baseRemark = is_string($order['remark'] ?? null) ? $order['remark'] : '';
        // 备注包含充值与赠送的明细，方便后续稽核
        $detail = sprintf('amount=%s,bonus=%s,credit=%s', $amount, $bonus, $credit);
        $note = sprintf('[%s] %s (%s)', $source, $sourceLabel, $detail);
        $combined = $baseRemark === '' ? $note : ($baseRemark . ' | ' . $note);
        if ($extraRemark !== null && $extraRemark !== '') {
            $combined .= ' | ' . $extraRemark;
        }
        $finalRemark = mb_substr($combined, 0, 255);

        $walletIdem = 'deposit_settle_' . $orderNo;

        Db::startTrans();
        try {
            $statusBefore = $status === 3 ? 3 : 0;
            $affected = Db::name('deposit_order')
                ->where('id', $orderId)
                ->where('status', $statusBefore)
                ->update([
                    'status'      => 1,
                    'pay_time'    => $now,
                    'remark'      => $finalRemark,
                    'update_time' => $now,
                ]);
            if ($affected <= 0) {
                throw new RuntimeException('Order state changed, please refresh and retry');
            }

            Db::name('user')->where('id', $userId)->update([
                'coin'        => $balanceAfter,
                'update_time' => $now,
            ]);

            $walletExists = Db::name('user_wallet_record')
                ->where('idempotency_key', $walletIdem)
                ->value('id');
            if (!$walletExists) {
                Db::name('user_wallet_record')->insert([
                    'user_id'           => $userId,
                    'channel_id'        => $channelId,
                    'biz_type'          => 'deposit',
                    'direction'         => 1,
                    'amount'            => $credit,
                    'balance_before'    => $balanceBefore,
                    'balance_after'     => $balanceAfter,
                    'ref_type'          => 'deposit_order',
                    'ref_id'            => $orderId,
                    'idempotency_key'   => $walletIdem,
                    'operator_admin_id' => $operatorAdminId,
                    'remark'            => mb_substr($note, 0, 500),
                    'create_time'       => $now,
                ]);
            }

            Db::commit();
        } catch (Throwable $e) {
            Db::rollback();
            throw new RuntimeException($e->getMessage());
        }

        GameWebSocketEventBus::publish('wallet.changed', \app\common\service\GameWebSocketPayloadHelper::mergeUserStreakInto([
            'user_id' => $userId,
            'balance_after' => $balanceAfter,
            'biz_type' => 'deposit',
            'order_no' => $orderNo,
            'changed_at' => $now,
        ], $userId));

        return [
            'order_id'        => $orderId,
            'order_no'        => $orderNo,
            'amount'          => $amount,
            'bonus_amount'    => $bonus,
            'credit'          => $credit,
            'balance_before'  => $balanceBefore,
            'balance_after'   => $balanceAfter,
            'pay_time'        => $now,
            'already_settled' => false,
        ];
    }

    /**
     * 将任意数值输入格式化为 2 位小数字符串（不做强制类型转换）
     */
    private static function amountString($raw): string
    {
        if (is_string($raw)) {
            $s = trim($raw);
        } elseif (is_int($raw) || is_float($raw)) {
            $s = strval($raw);
        } else {
            return '0.00';
        }
        if (!is_numeric($s)) {
            return '0.00';
        }
        return bcadd($s, '0', 2);
    }
}
