<?php

namespace App\Services\Jackpot;

use App\Models\Draw;
use App\Models\TicketItem;
use App\Lottery\DrawStatus;
use App\Models\JackpotPool;
use App\Models\JackpotPayoutLog;
use Illuminate\Support\Collection;

/**
 * 产品文档 §5.11.2–5.11.3：中头奖且满足阈值或连续未爆期数 → 按比例/全额释放奖池，按注项 `total_bet_amount` 比例分配。
 */
final class JackpotBurstAllocator
{
    /**
     * @param  Collection<int, array{item: TicketItem, matched_tier: ?string, gross_win: int}>  $results
     * @return array{allocations: array<int, int>, pool_payout: int, trigger: ?string}
     */
    public function allocate(Draw $draw, JackpotPool $pool, Collection $results): array
    {
        $winners = $results->filter(
            fn (array $r) => ($r['matched_tier'] ?? null) === 'first' && (int) $r['gross_win'] > 0,
        );

        if ($winners->isEmpty()) {
            return ['allocations' => [], 'pool_payout' => 0, 'trigger' => null];
        }

        $thresholdOk = (int) $pool->current_amount >= (int) $pool->trigger_threshold;
        $gapOk = $this->gapTriggerMet($pool);
        $comboOk = $this->comboTriggerMet($pool, $winners);
        if (! $thresholdOk && ! $gapOk && ! $comboOk) {
            return ['allocations' => [], 'pool_payout' => 0, 'trigger' => null];
        }

        $trigger = $thresholdOk ? 'threshold' : ($gapOk ? 'forced_gap' : 'play_combo');
        $releaseFullPool = $trigger === 'forced_gap';

        $winnerItems = $winners->map(fn (array $r): TicketItem => $r['item'])->values();

        return $this->burstToWinners(
            $draw,
            $pool,
            $winnerItems,
            $trigger,
            $releaseFullPool,
            [
                'threshold_ok' => $thresholdOk,
                'gap_ok' => $gapOk,
                'combo_ok' => $comboOk,
                'combo_trigger_play_codes' => $this->comboTriggerPlayCodes($pool),
            ],
        );
    }

    /**
     * 超管手动爆池：跳过头奖触发条件校验，仍要求存在头奖中奖注单，并按配置派彩比例释放奖池。
     *
     * @param  Collection<int, TicketItem>  $winnerItems
     * @return array{allocations: array<int, int>, pool_payout: int, trigger: string, log_id: int}
     */
    public function burstManual(Draw $draw, JackpotPool $pool, Collection $winnerItems): array
    {
        if ($winnerItems->isEmpty()) {
            throw new \RuntimeException('jackpot_manual_no_first_prize_winners');
        }

        $out = $this->burstToWinners($draw, $pool, $winnerItems, 'manual', false, ['manual' => true]);

        return [
            'allocations' => $out['allocations'],
            'pool_payout' => $out['pool_payout'],
            'trigger' => 'manual',
            'log_id' => (int) ($out['log_id'] ?? 0),
        ];
    }

    /**
     * @param  Collection<int, TicketItem>  $winnerItems
     * @param  array<string, mixed>  $snapshotExtra
     * @return array{allocations: array<int, int>, pool_payout: int, trigger: string, log_id: int}
     */
    private function burstToWinners(
        Draw $draw,
        JackpotPool $pool,
        Collection $winnerItems,
        string $trigger,
        bool $releaseFullPool,
        array $snapshotExtra,
    ): array {
        $poolBefore = (int) $pool->current_amount;
        $poolPayout = $releaseFullPool
            ? $poolBefore
            : (int) floor($poolBefore * (float) $pool->payout_rate);

        if ($poolPayout <= 0) {
            return ['allocations' => [], 'pool_payout' => 0, 'trigger' => $trigger, 'log_id' => 0];
        }

        $allocations = $this->distributeByBetWeight($winnerItems, $poolPayout);
        if ($allocations === []) {
            return ['allocations' => [], 'pool_payout' => 0, 'trigger' => $trigger, 'log_id' => 0];
        }

        $pool->forceFill([
            'current_amount' => max(0, $poolBefore - $poolPayout),
            'last_trigger_draw_id' => $draw->id,
        ])->save();

        $log = JackpotPayoutLog::query()->create([
            'draw_id' => $draw->id,
            'jackpot_pool_id' => $pool->id,
            'trigger_type' => $trigger,
            'total_payout_amount' => $poolPayout,
            'winner_count' => count($allocations),
            'trigger_snapshot_json' => array_merge($snapshotExtra, [
                'pool_amount_before' => $poolBefore,
                'payout_rate' => (string) $pool->payout_rate,
                'release_full_pool' => $releaseFullPool,
            ]),
        ]);

        return [
            'allocations' => $allocations,
            'pool_payout' => $poolPayout,
            'trigger' => $trigger,
            'log_id' => (int) $log->id,
        ];
    }

    /**
     * @param  Collection<int, TicketItem>  $winnerItems
     * @return array<int, int>
     */
    private function distributeByBetWeight(Collection $winnerItems, int $poolPayout): array
    {
        $list = $winnerItems->values()->all();
        $weightTotal = 0;
        foreach ($list as $item) {
            $weightTotal += (int) $item->total_bet_amount;
        }
        if ($weightTotal <= 0) {
            return [];
        }

        $allocations = [];
        $remaining = $poolPayout;
        $n = count($list);
        foreach ($list as $idx => $item) {
            $w = (int) $item->total_bet_amount;
            if ($idx === $n - 1) {
                $share = max(0, $remaining);
            } else {
                $share = (int) floor($poolPayout * $w / $weightTotal);
                $remaining -= $share;
            }
            if ($share > 0) {
                $allocations[(int) $item->id] = $share;
            }
        }

        return $allocations;
    }

    private function gapTriggerMet(JackpotPool $pool): bool
    {
        $gap = (int) $pool->force_trigger_draw_gap;
        if ($gap <= 0) {
            return false;
        }

        $lastId = (int) ($pool->last_trigger_draw_id ?? 0);
        $count = Draw::query()
            ->where('status', DrawStatus::Settled->value)
            ->when($lastId > 0, fn ($q) => $q->where('id', '>', $lastId))
            ->count();

        return $count >= $gap;
    }

    /**
     * @param  Collection<int, array{item: TicketItem, matched_tier: ?string, gross_win: int}>  $winners
     */
    private function comboTriggerMet(JackpotPool $pool, Collection $winners): bool
    {
        $codes = $this->comboTriggerPlayCodes($pool);
        if ($codes === []) {
            return false;
        }

        return $winners->contains(
            fn (array $r): bool => in_array((string) $r['item']->play_code, $codes, true),
        );
    }

    /**
     * @return list<string>
     */
    private function comboTriggerPlayCodes(JackpotPool $pool): array
    {
        $raw = $pool->combo_trigger_play_codes;
        if (! is_array($raw)) {
            return [];
        }

        return array_values(array_filter(
            array_map(fn ($v): string => strtolower(trim((string) $v)), $raw),
            fn (string $v): bool => $v !== '',
        ));
    }
}
