<?php

namespace App\Services\Settlement;

use App\Models\Draw;
use App\Models\TicketItem;
use App\Lottery\DrawStatus;
use App\Models\BetProvider;
use App\Models\JackpotPool;
use App\Models\DrawResultItem;
use App\Models\DrawResultBatch;
use App\Models\SettlementBatch;
use Illuminate\Support\Facades\DB;
use App\Lottery\DrawResultBatchStatus;
use App\Lottery\SettlementBatchStatus;
use App\Models\TicketSettlementDetail;
use App\Services\Ticket\RiskPoolService;
use App\Services\Draw\DrawHallSnapshotBuilder;
use App\Services\Jackpot\JackpotBurstAllocator;
use App\Services\Draw\LotteryHallRealtimeBroadcaster;
use App\Services\AgentSettlement\AgentGameSettlementRecorder;

/**
 * 阶段 6：对已发布开奖、处于 `settling` 的期号执行结算（匹配 → 回水派彩调整 → Jackpot 爆池分配 → 明细 → 风险池释放 → 待审核）。
 *
 * 派彩入账由审核通过后的独立 payout 动作执行，避免未确认结果直接入账。
 */
final class SettlementOrchestrator
{
    public function __construct(
        private readonly SettlementMatcherRegistry $matchers,
        private readonly SettlementPayoutAdjuster $payoutAdjuster,
        private readonly JackpotBurstAllocator $jackpotBurst,
        private readonly RiskPoolService $riskPool,
        private readonly LotteryHallRealtimeBroadcaster $hallRealtime,
        private readonly DrawHallSnapshotBuilder $hallSnapshot,
        private readonly AgentGameSettlementRecorder $agentGameSettlement,
    ) {}

    /**
     * @return bool true 表示已处理（新结算或补全期号状态）
     */
    public function trySettleDraw(Draw $draw): bool
    {
        $afterCommit = DB::transaction(function () use ($draw): array {
            /** @var Draw $locked */
            $locked = Draw::query()->whereKey($draw->id)->lockForUpdate()->firstOrFail();

            if ($locked->status === DrawStatus::Settled->value) {
                return ['handled' => false, 'jackpot_bursts' => [], 'should_notify_status' => false];
            }

            if ($locked->status !== DrawStatus::Settling->value) {
                return ['handled' => false, 'jackpot_bursts' => [], 'should_notify_status' => false];
            }

            $jackpotBursts = [];
            $handled = false;
            $latestSettleVersion = (int) $locked->settle_version;
            $publishedBatches = DrawResultBatch::query()
                ->where('draw_id', $locked->id)
                ->where('status', DrawResultBatchStatus::Published->value)
                ->orderBy('provider_code')
                ->orderByDesc('result_version')
                ->orderByDesc('id')
                ->get()
                ->unique(fn (DrawResultBatch $batch): string => strtoupper((string) $batch->provider_code))
                ->keyBy(fn (DrawResultBatch $batch): string => strtoupper((string) $batch->provider_code));

            /**
             * @var array<string, array{
             *     published_batch: DrawResultBatch,
             *     settlement_batch: SettlementBatch,
             *     board: PublishedDrawResultBoard,
             *     prepared: list<array{item: TicketItem, gross_win: int, matched_tier: ?string, net_win: int, match_detail: mixed}>
             * }> $providerContexts
             */
            $providerContexts = [];
            $openProviderContext = function (string $providerCode) use (
                &$providerContexts,
                &$handled,
                &$latestSettleVersion,
                $locked,
                $publishedBatches,
            ): ?array {
                if (isset($providerContexts[$providerCode])) {
                    return $providerContexts[$providerCode];
                }

                /** @var DrawResultBatch|null $publishedBatch */
                $publishedBatch = $publishedBatches->get($providerCode);
                if ($publishedBatch === null) {
                    return null;
                }

                $batchRow = SettlementBatch::query()
                    ->where('draw_id', $locked->id)
                    ->where('result_batch_id', $publishedBatch->id)
                    ->whereIn('status', [
                        SettlementBatchStatus::Running->value,
                        SettlementBatchStatus::PendingReview->value,
                        SettlementBatchStatus::Approved->value,
                        SettlementBatchStatus::Paid->value,
                        SettlementBatchStatus::Completed->value,
                    ])
                    ->orderByDesc('id')
                    ->first();

                if ($batchRow !== null && in_array($batchRow->status, [
                    SettlementBatchStatus::Paid->value,
                    SettlementBatchStatus::Completed->value,
                ], true)) {
                    throw new \RuntimeException('settlement_batch_already_finalized_with_pending_tickets');
                }

                if ($batchRow !== null) {
                    $handled = true;
                    $latestSettleVersion = max($latestSettleVersion, (int) $batchRow->settle_version);
                    $batchRow->forceFill([
                        'status' => SettlementBatchStatus::Running->value,
                        'review_status' => 'pending',
                        'reviewed_by' => null,
                        'reviewed_at' => null,
                        'review_remark' => null,
                        'finished_at' => null,
                    ])->save();
                } else {
                    $latestSettleVersion++;
                    $batchRow = SettlementBatch::query()->create([
                        'draw_id' => $locked->id,
                        'result_batch_id' => $publishedBatch->id,
                        'settle_version' => $latestSettleVersion,
                        'status' => SettlementBatchStatus::Running->value,
                        'review_status' => 'pending',
                        'started_at' => now(),
                    ]);
                }

                $providerContexts[$providerCode] = [
                    'published_batch' => $publishedBatch,
                    'settlement_batch' => $batchRow,
                    'board' => new PublishedDrawResultBoard(
                        DrawResultItem::query()
                            ->where('result_batch_id', $publishedBatch->id)
                            ->orderBy('id')
                            ->get(),
                    ),
                    'prepared' => [],
                ];

                return $providerContexts[$providerCode];
            };

            $chunkSize = max(1, (int) config('lottery.settlement.ticket_chunk_size', 10_000));
            $lastTicketItemId = 0;
            $sawPendingTicket = false;

            while (true) {
                $ticketItems = TicketItem::query()
                    ->where('draw_id', $locked->id)
                    ->where('status', 'pending_draw')
                    ->where('id', '>', $lastTicketItemId)
                    ->with(['combinations', 'order'])
                    ->orderBy('id')
                    ->limit($chunkSize)
                    ->get();

                if ($ticketItems->isEmpty()) {
                    break;
                }

                $sawPendingTicket = true;
                $lastTicketItemId = (int) $ticketItems->last()->id;

                foreach ($ticketItems as $item) {
                    $providerCode = strtoupper((string) ($item->provider_code ?: BetProvider::DEFAULT_CODE));
                    $context = $openProviderContext($providerCode);
                    if ($context === null) {
                        continue;
                    }

                    $matcher = $this->matchers->for((string) $item->play_code);
                    $result = $matcher->match($item, $context['board'], $item->combinations);
                    $gross = max(0, (int) $result['win_amount']);
                    $tier = $result['matched_prize_tier'] ?? null;
                    $providerContexts[$providerCode]['prepared'][] = [
                        'item' => $item,
                        'gross_win' => $gross,
                        'matched_tier' => is_string($tier) ? $tier : null,
                        'net_win' => $this->payoutAdjuster->adjustGrossWin($gross, $item),
                        'match_detail' => $result['match_detail'],
                    ];
                }
            }

            if (! $sawPendingTicket
                && $publishedBatches->has(BetProvider::DEFAULT_CODE)
                && ! SettlementBatch::query()->where('draw_id', $locked->id)->exists()
            ) {
                $openProviderContext(BetProvider::DEFAULT_CODE);
            }

            foreach ($providerContexts as $providerCode => $context) {
                $board = $context['board'];
                $batchRow = $context['settlement_batch'];

                /** @var list<array{item: TicketItem, gross_win: int, matched_tier: ?string, net_win: int, match_detail: mixed}> $prepared */
                $prepared = $context['prepared'];

                $allocations = [];
                $totalJackpotPayout = 0;
                $preparedByCurrency = collect($prepared)->groupBy(
                    fn (array $p): string => strtoupper((string) ($p['item']->order?->currency_code ?? 'NPR')),
                );
                foreach ($preparedByCurrency as $currency => $currencyPrepared) {
                    $pool = JackpotPool::query()
                        ->where('currency_code', $currency)
                        ->where('status', 1)
                        ->lockForUpdate()
                        ->first();
                    if ($pool === null) {
                        continue;
                    }

                    $burstInput = collect($currencyPrepared)->map(fn (array $p): array => [
                        'item' => $p['item'],
                        'matched_tier' => $p['matched_tier'],
                        'gross_win' => $p['gross_win'],
                    ]);
                    $burstOut = $this->jackpotBurst->allocate($locked, $pool, $burstInput);
                    $allocations = array_replace($allocations, $burstOut['allocations']);
                    $currencyPayout = (int) $burstOut['pool_payout'];
                    $totalJackpotPayout += $currencyPayout;

                    if ($currencyPayout > 0 && is_string($burstOut['trigger'])) {
                        $jackpotBursts[] = [
                            'first_prize_number' => $board->firstPrizeNumber4d(),
                            'currency' => $currency,
                            'payout' => $currencyPayout,
                            'trigger' => $burstOut['trigger'],
                            'pool_after' => (int) $pool->fresh()->current_amount,
                            'winner_count' => count($burstOut['allocations']),
                        ];
                    }
                }

                $ticketCount = (int) $batchRow->total_ticket_count;
                $winCount = (int) $batchRow->total_win_count;
                $totalPayout = (int) $batchRow->total_payout_amount;
                $totalJackpotPayout += (int) $batchRow->total_jackpot_payout_amount;

                foreach ($prepared as $p) {
                    /** @var TicketItem $item */
                    $item = $p['item'];
                    $ticketCount++;
                    $net = (int) $p['net_win'];
                    $jackpotShare = (int) ($allocations[(int) $item->id] ?? 0);
                    $finalCredit = $net + $jackpotShare;

                    TicketSettlementDetail::query()->create([
                        'settlement_batch_id' => $batchRow->id,
                        'ticket_item_id' => $item->id,
                        'matched_prize_tier' => $p['matched_tier'],
                        'win_amount' => $net,
                        'jackpot_allocation_amount' => $jackpotShare,
                        'match_detail_json' => $p['match_detail'],
                    ]);

                    $terminalStatus = $finalCredit > 0 ? 'pending_payout' : 'settled_lose';
                    $item->forceFill([
                        'win_amount' => $net,
                        'jackpot_win_amount' => $jackpotShare,
                        'settled_at' => null,
                        'status' => $terminalStatus,
                    ])->save();

                    $this->agentGameSettlement->recordForTicketItem(
                        $item,
                        $net,
                        $terminalStatus,
                        (int) $batchRow->settle_version,
                    );

                    if ($finalCredit > 0) {
                        $winCount++;
                    }
                    $totalPayout += $finalCredit;

                    $locks = [];
                    foreach ($item->combinations as $c) {
                        $locks[] = [
                            'number_4d' => (string) $c->number_4d,
                            'amount' => (int) $c->estimated_payout,
                        ];
                    }
                    $this->riskPool->release(
                        (int) $locked->id,
                        (string) ($item->provider_code ?: BetProvider::DEFAULT_CODE),
                        $item,
                        $locks,
                    );
                }

                $batchRow->forceFill([
                    'status' => SettlementBatchStatus::Running->value,
                    'total_ticket_count' => $ticketCount,
                    'total_win_count' => $winCount,
                    'total_payout_amount' => $totalPayout,
                    'total_jackpot_payout_amount' => $totalJackpotPayout,
                    'finished_at' => null,
                ])->save();

                $handled = true;
            }

            $hasPendingDrawTickets = TicketItem::query()
                ->where('draw_id', $locked->id)
                ->where('status', 'pending_draw')
                ->exists();

            if (! $hasPendingDrawTickets) {
                $finishedBatchCount = SettlementBatch::query()
                    ->where('draw_id', $locked->id)
                    ->where('status', SettlementBatchStatus::Running->value)
                    ->update([
                        'status' => SettlementBatchStatus::PendingReview->value,
                        'finished_at' => now(),
                        'updated_at' => now(),
                    ]);
                $handled = $handled || $finishedBatchCount > 0;
            }

            if (! $handled) {
                $activeBatch = SettlementBatch::query()
                    ->where('draw_id', $locked->id)
                    ->whereIn('status', [
                        SettlementBatchStatus::PendingReview->value,
                        SettlementBatchStatus::Approved->value,
                        SettlementBatchStatus::Paid->value,
                        SettlementBatchStatus::Completed->value,
                    ])
                    ->orderByDesc('settle_version')
                    ->first();
                if ($activeBatch !== null) {
                    $handled = true;
                    $latestSettleVersion = max($latestSettleVersion, (int) $activeBatch->settle_version);
                }
            }

            if (! $handled) {
                return ['handled' => false, 'jackpot_bursts' => [], 'should_notify_status' => false];
            }

            $locked->forceFill([
                'status' => DrawStatus::Settling->value,
                'settle_version' => $latestSettleVersion,
            ])->save();

            return [
                'handled' => true,
                'jackpot_bursts' => array_map(fn (array $burst): array => [
                    'draw_id' => (int) $locked->id,
                    'draw_no' => (string) $locked->draw_no,
                    'first_prize_number' => (string) $burst['first_prize_number'],
                    'currency' => (string) $burst['currency'],
                    'payout' => (int) $burst['payout'],
                    'winner_count' => (int) $burst['winner_count'],
                    'trigger' => (string) $burst['trigger'],
                    'pool_after' => (int) $burst['pool_after'],
                ], $jackpotBursts),
                'should_notify_status' => true,
            ];
        });

        foreach ($afterCommit['jackpot_bursts'] as $burst) {
            $this->hallRealtime->notifyJackpotBurst(
                $burst['draw_id'],
                $burst['draw_no'],
                $burst['first_prize_number'],
                $burst['currency'],
                $burst['payout'],
                $burst['winner_count'],
                $burst['trigger'],
                $burst['pool_after'],
            );
        }

        if (($afterCommit['should_notify_status'] ?? false) === true) {
            $this->hallRealtime->notifyStatusChange($this->hallSnapshot->build());
        }

        return (bool) ($afterCommit['handled'] ?? false);
    }
}
