<?php

namespace App\Services\Ticket;

use App\Exceptions\TicketOperationException;
use App\Lottery\ErrorCode;
use App\Models\Player;
use App\Models\PlayerWallet;
use App\Models\TicketOrder;
use App\Models\WalletTxn;
use App\Services\Wallet\WalletBalanceRealtimeNotifier;

final class TicketWalletService
{
    public function __construct(
        private readonly WalletBalanceRealtimeNotifier $balanceRealtime,
    ) {}

    private const TXN_POSTED = 'posted';

    private const TXN_DIR_OUT = 2;

    private const TXN_DIR_IN = 1;

    private const BIZ_BET_RESERVE = 'bet_reserve';

    private const BIZ_BET_RESERVE_RELEASE = 'bet_reserve_release';

    public function reserveBetDeduct(Player $player, string $currencyCode, int $amountMinor, TicketOrder $order): void
    {
        if ($amountMinor <= 0) {
            return;
        }

        $idempotentKey = self::BIZ_BET_RESERVE.':'.$order->order_no;
        if (WalletTxn::query()->where('biz_type', self::BIZ_BET_RESERVE)->where('idempotent_key', $idempotentKey)->exists()) {
            return;
        }

        $wallet = $this->lockOrCreateWallet($player, $currencyCode);
        $before = (int) $wallet->balance;
        $frozenBefore = (int) $wallet->frozen_balance;
        $available = $before - $frozenBefore;
        if ($available < $amountMinor) {
            throw new TicketOperationException('bet_insufficient_balance', ErrorCode::BetInsufficientBalance->value);
        }

        $wallet->forceFill([
            'frozen_balance' => $frozenBefore + $amountMinor,
            'version' => (int) $wallet->version + 1,
        ])->save();

        WalletTxn::query()->create([
            'txn_no' => $this->newTxnNo(),
            'player_id' => $player->id,
            'wallet_id' => $wallet->id,
            'biz_type' => self::BIZ_BET_RESERVE,
            'biz_no' => $order->order_no,
            'direction' => self::TXN_DIR_OUT,
            'amount' => $amountMinor,
            'balance_before' => $before,
            'balance_after' => $before,
            'status' => self::TXN_POSTED,
            'external_ref_no' => null,
            'idempotent_key' => $idempotentKey,
            'remark' => 'pending_confirm_reserve',
        ]);
    }

    public function finalizeReservedBetDeduct(Player $player, string $currencyCode, int $amountMinor, TicketOrder $order): int
    {
        if ($amountMinor <= 0) {
            $wallet = PlayerWallet::query()
                ->where('player_id', $player->id)
                ->where('wallet_type', 'lottery')
                ->where('currency_code', strtoupper($currencyCode))
                ->first();

            return $wallet !== null ? (int) $wallet->balance : 0;
        }

        $deductIdempotentKey = 'bet_deduct:'.$order->order_no;
        $existingDeduct = WalletTxn::query()
            ->where('biz_type', 'bet_deduct')
            ->where('idempotent_key', $deductIdempotentKey)
            ->first();
        if ($existingDeduct !== null) {
            return (int) $existingDeduct->balance_after;
        }

        $wallet = $this->lockOrCreateWallet($player, $currencyCode);
        $before = (int) $wallet->balance;
        $frozenBefore = (int) $wallet->frozen_balance;
        if ($frozenBefore < $amountMinor) {
            throw new TicketOperationException('bet_reserve_missing', ErrorCode::BetInsufficientBalance->value);
        }

        $after = $before - $amountMinor;
        $wallet->forceFill([
            'balance' => $after,
            'frozen_balance' => $frozenBefore - $amountMinor,
            'version' => (int) $wallet->version + 1,
        ])->save();

        WalletTxn::query()->create([
            'txn_no' => $this->newTxnNo(),
            'player_id' => $player->id,
            'wallet_id' => $wallet->id,
            'biz_type' => 'bet_deduct',
            'biz_no' => $order->order_no,
            'direction' => self::TXN_DIR_OUT,
            'amount' => $amountMinor,
            'balance_before' => $before,
            'balance_after' => $after,
            'status' => self::TXN_POSTED,
            'external_ref_no' => null,
            'idempotent_key' => $deductIdempotentKey,
            'remark' => null,
        ]);

        $wallet->refresh();
        $this->balanceRealtime->notifyAfterMovement($wallet, -$amountMinor, 'bet_deduct');

        return $after;
    }

    public function releaseReservedBetDeduct(TicketOrder $order, string $remark = 'pending_confirm_release'): void
    {
        $reserveIdempotentKey = self::BIZ_BET_RESERVE.':'.$order->order_no;
        $releaseIdempotentKey = self::BIZ_BET_RESERVE_RELEASE.':'.$order->order_no;

        if (! WalletTxn::query()->where('biz_type', self::BIZ_BET_RESERVE)->where('idempotent_key', $reserveIdempotentKey)->exists()) {
            return;
        }

        if (WalletTxn::query()->where('biz_type', self::BIZ_BET_RESERVE_RELEASE)->where('idempotent_key', $releaseIdempotentKey)->exists()) {
            return;
        }

        // Even if bet_deduct exists, we still need to release frozen balance
        // because finalizeReservedBetDeduct may have failed to release it completely

        $wallet = PlayerWallet::query()
            ->where('player_id', $order->player_id)
            ->where('wallet_type', 'lottery')
            ->where('currency_code', strtoupper((string) $order->currency_code))
            ->lockForUpdate()
            ->first();

        if ($wallet === null) {
            return;
        }

        $before = (int) $wallet->balance;
        $frozenBefore = (int) $wallet->frozen_balance;
        $releaseAmount = min($frozenBefore, (int) $order->total_actual_deduct);
        if ($releaseAmount <= 0) {
            return;
        }

        $wallet->forceFill([
            'frozen_balance' => $frozenBefore - $releaseAmount,
            'version' => (int) $wallet->version + 1,
        ])->save();

        WalletTxn::query()->create([
            'txn_no' => $this->newTxnNo(),
            'player_id' => (int) $order->player_id,
            'wallet_id' => $wallet->id,
            'biz_type' => self::BIZ_BET_RESERVE_RELEASE,
            'biz_no' => $order->order_no,
            'direction' => self::TXN_DIR_IN,
            'amount' => $releaseAmount,
            'balance_before' => $before,
            'balance_after' => $before,
            'status' => self::TXN_POSTED,
            'external_ref_no' => null,
            'idempotent_key' => $releaseIdempotentKey,
            'remark' => $remark,
        ]);
    }

    public function reverseBetDeduct(TicketOrder $order): void
    {
        $deductTxn = WalletTxn::query()
            ->where('biz_type', 'bet_deduct')
            ->where('biz_no', $order->order_no)
            ->where('status', self::TXN_POSTED)
            ->first();

        if ($deductTxn === null) {
            return;
        }

        $idempotentKey = 'bet-reverse:'.$order->order_no;
        if (WalletTxn::query()->where('biz_type', 'bet_reverse')->where('idempotent_key', $idempotentKey)->exists()) {
            return;
        }

        $wallet = PlayerWallet::query()
            ->whereKey($deductTxn->wallet_id)
            ->lockForUpdate()
            ->firstOrFail();

        $amount = (int) $deductTxn->amount;
        $before = (int) $wallet->balance;
        $after = $before + $amount;
        $wallet->forceFill([
            'balance' => $after,
            'version' => (int) $wallet->version + 1,
        ])->save();

        WalletTxn::query()->create([
            'txn_no' => $this->newTxnNo(),
            'player_id' => (int) $deductTxn->player_id,
            'wallet_id' => (int) $deductTxn->wallet_id,
            'biz_type' => 'bet_reverse',
            'biz_no' => $order->order_no,
            'direction' => self::TXN_DIR_IN,
            'amount' => $amount,
            'balance_before' => $before,
            'balance_after' => $after,
            'status' => self::TXN_POSTED,
            'external_ref_no' => null,
            'idempotent_key' => $idempotentKey,
            'remark' => 'post_deduct_confirmation_failed',
        ]);

        $wallet->refresh();
        $this->balanceRealtime->notifyAfterMovement($wallet, $amount, 'bet_reverse');
    }

    /**
     * 手动爆池补发：结算批次已派彩后，将 Jackpot 份额追加入账（幂等键按批次 + 玩家 + 爆池日志）。
     */
    public function creditJackpotManualPayout(
        Player $player,
        string $currencyCode,
        int $amountMinor,
        int $settlementBatchId,
        int $jackpotPayoutLogId,
    ): void {
        if ($amountMinor <= 0) {
            return;
        }

        $idempotentKey = 'jackpot-manual:'.$settlementBatchId.':'.$player->id.':'.$jackpotPayoutLogId;
        if (WalletTxn::query()->where('idempotent_key', $idempotentKey)->exists()) {
            return;
        }

        $currency = strtoupper($currencyCode);
        $wallet = $this->lockOrCreateWallet($player, $currency);

        $before = (int) $wallet->balance;
        $after = $before + $amountMinor;
        $wallet->forceFill([
            'balance' => $after,
            'version' => (int) $wallet->version + 1,
        ])->save();

        WalletTxn::query()->create([
            'txn_no' => $this->newTxnNo(),
            'player_id' => $player->id,
            'wallet_id' => $wallet->id,
            'biz_type' => 'jackpot_manual_payout',
            'biz_no' => 'JP'.$jackpotPayoutLogId,
            'direction' => self::TXN_DIR_IN,
            'amount' => $amountMinor,
            'balance_before' => $before,
            'balance_after' => $after,
            'status' => self::TXN_POSTED,
            'external_ref_no' => null,
            'idempotent_key' => $idempotentKey,
            'remark' => 'manual_jackpot_burst',
        ]);

        $wallet->refresh();
        $this->balanceRealtime->notifyAfterMovement($wallet, $amountMinor, 'jackpot_manual_payout');
    }

    public function creditSettlementPayout(Player $player, string $currencyCode, int $amountMinor, int $settlementBatchId): void
    {
        if ($amountMinor <= 0) {
            return;
        }

        $currency = strtoupper($currencyCode);
        $idempotentKey = 'settle-payout:'.$settlementBatchId.':'.$player->id.':'.$currency;
        if (WalletTxn::query()->where('idempotent_key', $idempotentKey)->where('biz_type', 'settle_payout')->exists()) {
            return;
        }

        $wallet = $this->lockOrCreateWallet($player, $currency);
        $before = (int) $wallet->balance;
        $after = $before + $amountMinor;
        $wallet->forceFill([
            'balance' => $after,
            'version' => (int) $wallet->version + 1,
        ])->save();

        WalletTxn::query()->create([
            'txn_no' => $this->newTxnNo(),
            'player_id' => $player->id,
            'wallet_id' => $wallet->id,
            'biz_type' => 'settle_payout',
            'biz_no' => 'SB'.$settlementBatchId,
            'direction' => self::TXN_DIR_IN,
            'amount' => $amountMinor,
            'balance_before' => $before,
            'balance_after' => $after,
            'status' => self::TXN_POSTED,
            'external_ref_no' => null,
            'idempotent_key' => $idempotentKey,
            'remark' => null,
        ]);

        $wallet->refresh();
        $this->balanceRealtime->notifyAfterMovement($wallet, $amountMinor, 'settle_payout');
    }

    public function applySettlementCorrection(
        Player $player,
        string $currencyCode,
        int $amountMinor,
        string $correctionNo,
        string $remark,
    ): string {
        if ($amountMinor === 0) {
            throw new TicketOperationException('adjustment_delta_zero', ErrorCode::WalletInvalidAmount->value);
        }

        $currency = strtoupper($currencyCode);
        $wallet = $this->lockOrCreateWallet($player, $currency);
        $before = (int) $wallet->balance;
        $available = $before - (int) $wallet->frozen_balance;
        $direction = $amountMinor > 0 ? self::TXN_DIR_IN : self::TXN_DIR_OUT;
        $absAmount = abs($amountMinor);

        if ($amountMinor < 0 && $available < $absAmount) {
            throw new TicketOperationException('bet_insufficient_balance', ErrorCode::BetInsufficientBalance->value);
        }

        $after = $before + $amountMinor;
        $wallet->forceFill([
            'balance' => $after,
            'version' => (int) $wallet->version + 1,
        ])->save();

        $txn = WalletTxn::query()->create([
            'txn_no' => $this->newTxnNo(),
            'player_id' => $player->id,
            'wallet_id' => $wallet->id,
            'biz_type' => 'settlement_adjustment',
            'biz_no' => $correctionNo,
            'direction' => $direction,
            'amount' => $absAmount,
            'balance_before' => $before,
            'balance_after' => $after,
            'status' => self::TXN_POSTED,
            'external_ref_no' => null,
            'idempotent_key' => 'settlement_adjustment:'.$correctionNo,
            'remark' => $remark,
        ]);

        $wallet->refresh();
        $this->balanceRealtime->notifyAfterMovement($wallet, $amountMinor, $amountMinor > 0 ? 'settle_payout' : 'bet_reverse');

        return (string) $txn->txn_no;
    }

    private function lockOrCreateWallet(Player $player, string $currencyCode): PlayerWallet
    {
        $wallet = PlayerWallet::query()
            ->where('player_id', $player->id)
            ->where('wallet_type', 'lottery')
            ->where('currency_code', strtoupper($currencyCode))
            ->lockForUpdate()
            ->first();

        if ($wallet === null) {
            $wallet = PlayerWallet::query()->create([
                'player_id' => $player->id,
                'wallet_type' => 'lottery',
                'currency_code' => strtoupper($currencyCode),
                'balance' => 0,
                'frozen_balance' => 0,
                'status' => 0,
                'version' => 0,
            ]);
            $wallet = PlayerWallet::query()->whereKey($wallet->id)->lockForUpdate()->firstOrFail();
        }

        if ((int) $wallet->status !== 0) {
            throw new TicketOperationException('wallet_frozen', ErrorCode::WalletLotteryFrozen->value);
        }

        return $wallet;
    }

    private function newTxnNo(): string
    {
        return 'WL'.now()->format('YmdHis').str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
    }
}
