<?php

namespace App\Services\Jackpot;

use App\Models\Draw;
use App\Models\Player;
use App\Models\TicketItem;
use App\Lottery\DrawStatus;
use App\Models\JackpotPool;
use App\Models\JackpotPayoutLog;
use App\Models\SettlementBatch;
use App\Models\TicketSettlementDetail;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use App\Lottery\SettlementBatchStatus;
use App\Lottery\DrawResultBatchStatus;
use App\Models\DrawResultBatch;
use App\Services\Draw\DrawHallSnapshotBuilder;
use App\Services\Draw\DrawResultViewService;
use App\Services\Draw\LotteryHallRealtimeBroadcaster;
use App\Services\Ticket\TicketWalletService;

/**
 * 产品文档：超管紧急手动爆池 —— 对已结算期号的头奖中奖者按奖池派彩比例分配，合并入账并广播动画。
 */
final class JackpotManualBurstService
{
    public function __construct(
        private readonly JackpotBurstAllocator $allocator,
        private readonly TicketWalletService $wallet,
        private readonly LotteryHallRealtimeBroadcaster $hallRealtime,
        private readonly DrawHallSnapshotBuilder $hallSnapshot,
        private readonly DrawResultViewService $drawResults,
    ) {}

    /**
     * @return array{
     *   current_amount: int,
     *   burst_amount: int,
     *   log_id: int|null,
     *   winner_count: int,
     *   draw_no: string,
     *   wallet_credited: bool
     * }
     */
    public function execute(JackpotPool $pool, int $drawId): array
    {
        return DB::transaction(function () use ($pool, $drawId): array {
            /** @var JackpotPool $locked */
            $locked = JackpotPool::query()->whereKey($pool->id)->lockForUpdate()->firstOrFail();

            if ((int) $locked->status !== 1) {
                throw new \RuntimeException('jackpot_disabled');
            }

            if ((int) $locked->current_amount <= 0) {
                throw new \RuntimeException('jackpot_pool_empty');
            }

            $draw = Draw::query()->whereKey($drawId)->firstOrFail();
            $this->assertDrawReady($draw);

            if (JackpotPayoutLog::query()
                ->where('jackpot_pool_id', $locked->id)
                ->where('draw_id', $drawId)
                ->exists()) {
                throw new \RuntimeException('jackpot_already_burst_for_draw');
            }

            $batch = $this->resolveSettlementBatch($draw);
            $winnerItems = $this->firstPrizeWinnerItems($batch, (string) $locked->currency_code);
            if ($winnerItems->isEmpty()) {
                throw new \RuntimeException('jackpot_manual_no_first_prize_winners');
            }

            $existingJackpot = (int) $batch->total_jackpot_payout_amount;
            if ($existingJackpot > 0) {
                throw new \RuntimeException('jackpot_already_allocated_for_draw');
            }

            $burst = $this->allocator->burstManual($draw, $locked, $winnerItems);
            $poolPayout = (int) $burst['pool_payout'];
            if ($poolPayout <= 0) {
                return [
                    'current_amount' => (int) $locked->current_amount,
                    'burst_amount' => 0,
                    'log_id' => null,
                    'winner_count' => 0,
                    'draw_no' => (string) $draw->draw_no,
                    'wallet_credited' => false,
                ];
            }

            $allocations = $burst['allocations'];
            $this->applyAllocationsToSettlement($batch, $allocations);

            $walletCredited = $this->creditWalletsIfAlreadyPaid($batch, $allocations, (int) $burst['log_id'], $locked->currency_code);

            $locked->refresh();

            $firstPrizeNumber = $this->drawResults->firstPrizeNumber4dForDraw($draw);
            if ($firstPrizeNumber === '') {
                $firstPrizeNumber = '----';
            }

            $this->hallRealtime->notifyJackpotBurst(
                (int) $draw->id,
                (string) $draw->draw_no,
                $firstPrizeNumber,
                (string) $locked->currency_code,
                $poolPayout,
                count($allocations),
                'manual',
                (int) $locked->current_amount,
            );
            $this->hallRealtime->notifyStatusChange($this->hallSnapshot->build());

            return [
                'current_amount' => (int) $locked->current_amount,
                'burst_amount' => $poolPayout,
                'log_id' => (int) $burst['log_id'],
                'winner_count' => count($allocations),
                'draw_no' => (string) $draw->draw_no,
                'wallet_credited' => $walletCredited,
            ];
        });
    }

    private function assertDrawReady(Draw $draw): void
    {
        $allowed = [
            DrawStatus::Settling->value,
            DrawStatus::Settled->value,
        ];
        if (! in_array($draw->status, $allowed, true)) {
            throw new \RuntimeException('draw_not_ready_for_jackpot_burst');
        }

        $hasPublished = DrawResultBatch::query()
            ->where('draw_id', $draw->id)
            ->where('status', DrawResultBatchStatus::Published->value)
            ->where('result_version', (int) $draw->current_result_version)
            ->exists();

        if (! $hasPublished) {
            throw new \RuntimeException('draw_result_not_published');
        }
    }

    private function resolveSettlementBatch(Draw $draw): SettlementBatch
    {
        $batch = SettlementBatch::query()
            ->where('draw_id', $draw->id)
            ->whereIn('status', [
                SettlementBatchStatus::PendingReview->value,
                SettlementBatchStatus::Approved->value,
                SettlementBatchStatus::Paid->value,
                SettlementBatchStatus::Completed->value,
            ])
            ->orderByDesc('id')
            ->first();

        if ($batch === null) {
            throw new \RuntimeException('settlement_batch_not_found');
        }

        return $batch;
    }

    /**
     * @return Collection<int, TicketItem>
     */
    private function firstPrizeWinnerItems(SettlementBatch $batch, string $currencyCode): Collection
    {
        $targetCurrency = strtoupper($currencyCode);
        $details = TicketSettlementDetail::query()
            ->where('settlement_batch_id', $batch->id)
            ->where('matched_prize_tier', 'first')
            ->where('win_amount', '>', 0)
            ->whereHas('ticketItem.order', fn ($q) => $q->where('currency_code', $targetCurrency))
            ->with('ticketItem')
            ->get();

        return $details
            ->map(fn (TicketSettlementDetail $d): ?TicketItem => $d->ticketItem)
            ->filter(fn (?TicketItem $item): bool => $item instanceof TicketItem)
            ->values();
    }

    /**
     * @param  array<int, int>  $allocations
     */
    private function applyAllocationsToSettlement(SettlementBatch $batch, array $allocations): void
    {
        $addedJackpot = 0;

        foreach ($allocations as $ticketItemId => $share) {
            $detail = TicketSettlementDetail::query()
                ->where('settlement_batch_id', $batch->id)
                ->where('ticket_item_id', $ticketItemId)
                ->first();

            if ($detail === null) {
                continue;
            }

            $detail->forceFill(['jackpot_allocation_amount' => $share])->save();

            $item = $detail->ticketItem;
            if ($item !== null) {
                $item->forceFill(['jackpot_win_amount' => $share])->save();
            }

            $addedJackpot += $share;
        }

        if ($addedJackpot > 0) {
            $batch->forceFill([
                'total_jackpot_payout_amount' => (int) $batch->total_jackpot_payout_amount + $addedJackpot,
                'total_payout_amount' => (int) $batch->total_payout_amount + $addedJackpot,
            ])->save();
        }
    }

    /**
     * 若结算批次已派彩，则补发 Jackpot 份额到玩家钱包。
     *
     * @param  array<int, int>  $allocations
     */
    private function creditWalletsIfAlreadyPaid(
        SettlementBatch $batch,
        array $allocations,
        int $jackpotLogId,
        string $currencyCode,
    ): bool {
        if (! in_array($batch->status, [SettlementBatchStatus::Paid->value, SettlementBatchStatus::Completed->value], true)) {
            return false;
        }

        $playerTotals = [];
        foreach ($allocations as $ticketItemId => $share) {
            if ($share <= 0) {
                continue;
            }
            $item = TicketItem::query()->whereKey($ticketItemId)->first();
            if ($item === null) {
                continue;
            }
            $pid = (int) $item->player_id;
            $playerTotals[$pid] = ($playerTotals[$pid] ?? 0) + $share;
        }

        foreach ($playerTotals as $playerId => $amount) {
            $player = Player::query()->whereKey($playerId)->firstOrFail();
            $this->wallet->creditJackpotManualPayout(
                $player,
                $currencyCode,
                $amount,
                (int) $batch->id,
                $jackpotLogId,
            );
        }

        return $playerTotals !== [];
    }
}
