<?php

namespace App\Services\Draw;

use Carbon\Carbon;
use App\Models\Draw;
use App\Models\RiskPool;
use App\Lottery\DrawStatus;
use App\Models\DrawResultItem;
use App\Models\DrawResultBatch;
use App\Lottery\DrawResultBatchStatus;
use Illuminate\Support\Facades\Cache;
use App\Services\Jackpot\JackpotSummaryService;
use App\Services\LotterySettings;

/**
 * `GET draw/current` 与大厅 WS 快照共用数据结构。
 *
 * @return array<string, mixed>|null
 */
final class DrawHallSnapshotBuilder
{
    private const JACKPOT_CACHE_TTL_SECONDS = 5;

    private const RISK_ALERTS_CACHE_TTL_SECONDS = 2;

    private const RESULT_ITEMS_CACHE_TTL_SECONDS = 5;

    public function __construct(
        private readonly JackpotSummaryService $jackpotSummary,
    ) {}

    /**
     * Tick 未及时跑时，DB 仍为 `open` 但已到封盘时刻；对外快照与界面应对齐真实可下注态（见 DrawTickService::openToClosingOrClosed）。
     *
     * 后台「当前大厅可见状态」预览可共用本方法。
     */
    public function effectiveHallDisplayStatus(Draw $target, Carbon $nowUtc): string
    {
        $db = (string) $target->status;

        if ($db === DrawStatus::Pending->value) {
            $startUtc = $target->start_time;
            if ($startUtc instanceof Carbon && $startUtc <= $nowUtc) {
                $closeUtc = $target->close_time;
                if ($closeUtc === null || $closeUtc > $nowUtc) {
                    $db = DrawStatus::Open->value;
                }
            } else {
                return $db;
            }
        }

        if ($db !== DrawStatus::Open->value) {
            return $db;
        }

        $closeUtc = $target->close_time;
        if (! $closeUtc instanceof Carbon || $closeUtc > $nowUtc) {
            return $db;
        }

        $drawUtc = $target->draw_time;
        if ($drawUtc instanceof Carbon && $drawUtc <= $nowUtc) {
            return DrawStatus::Closed->value;
        }

        return DrawStatus::Closing->value;
    }

    /** 与大厅 {@see effectiveHallDisplayStatus} 一致：是否仍接受 preview/place。 */
    public function isBettingOpen(Draw $draw, ?Carbon $nowUtc = null): bool
    {
        $nowUtc ??= now()->utc();

        return $this->effectiveHallDisplayStatus($draw, $nowUtc) === DrawStatus::Open->value;
    }

    private function showsPublishedResults(string $drawStatus): bool
    {
        return in_array($drawStatus, [
            DrawStatus::Cooldown->value,
            DrawStatus::Settling->value,
            DrawStatus::Settled->value,
        ], true);
    }

    /** 与 {@see build()} 使用同一套「大厅指向的当期行」 */
    public function resolveHallTarget(?Carbon $nowUtc = null): ?Draw
    {
        $nowUtc = ($nowUtc ?? Carbon::now())->utc();

        $bettingOpen = Draw::query()
            ->where(function ($q) use ($nowUtc): void {
                $q->where('status', DrawStatus::Open->value)
                    ->orWhere(function ($q2) use ($nowUtc): void {
                        $q2->where('status', DrawStatus::Pending->value)
                            ->whereNotNull('start_time')
                            ->where('start_time', '<=', $nowUtc);
                    });
            })
            ->where(function ($q) use ($nowUtc): void {
                $q->whereNull('close_time')
                    ->orWhere('close_time', '>', $nowUtc);
            })
            ->orderBy('draw_time')
            ->first();

        if ($bettingOpen !== null) {
            return $bettingOpen;
        }

        $upcoming = Draw::query()
            ->whereNotIn('status', [
                DrawStatus::Settled->value,
                DrawStatus::Cancelled->value,
            ])
            ->where(function ($q) use ($nowUtc): void {
                $q->where('status', '!=', DrawStatus::Pending->value)
                    ->orWhere(function ($q2) use ($nowUtc): void {
                        $q2->where('status', DrawStatus::Pending->value)
                            ->where(function ($q3) use ($nowUtc): void {
                                $q3->whereNull('close_time')
                                    ->orWhere('close_time', '>', $nowUtc);
                            })
                            ->where(function ($q3) use ($nowUtc): void {
                                $q3->whereNull('draw_time')
                                    ->orWhere('draw_time', '>', $nowUtc);
                            });
                    });
            })
            ->where(function ($q) use ($nowUtc): void {
                $q->where(function ($q2) use ($nowUtc): void {
                    $q2->whereNotNull('close_time')
                        ->where('close_time', '>', $nowUtc);
                })->orWhere(function ($q2) use ($nowUtc): void {
                    $q2->whereNull('close_time')
                        ->whereNotNull('draw_time')
                        ->where('draw_time', '>', $nowUtc);
                });
            })
            ->orderBy('draw_time')
            ->get();

        foreach ($upcoming as $candidate) {
            if ($this->isStalePendingRow($candidate, $nowUtc)) {
                continue;
            }

            return $candidate;
        }

        $chronological = Draw::query()
            ->whereNotIn('status', [
                DrawStatus::Settled->value,
                DrawStatus::Cancelled->value,
            ])
            ->orderByDesc('draw_time')
            ->first();

        if ($chronological !== null && $this->isCooldownExpired($chronological, $nowUtc)) {
            $next = Draw::query()
                ->whereNotIn('status', [
                    DrawStatus::Settled->value,
                    DrawStatus::Cancelled->value,
                ])
                ->where('draw_time', '>', $chronological->draw_time)
                ->orderBy('draw_time')
                ->first();

            if ($next !== null) {
                return $next;
            }
        }

        return $chronological;
    }

    /** 调度未跑时：库内仍是 pending，但封盘/开奖时刻已过，不应再作为大厅「当期」。 */
    private function isStalePendingRow(Draw $draw, Carbon $nowUtc): bool
    {
        if ((string) $draw->status !== DrawStatus::Pending->value) {
            return false;
        }

        $closeUtc = $draw->close_time;
        if ($closeUtc instanceof Carbon && $closeUtc <= $nowUtc) {
            return true;
        }

        $drawUtc = $draw->draw_time;

        return $drawUtc instanceof Carbon && $drawUtc <= $nowUtc;
    }

    private function isCooldownExpired(Draw $draw, Carbon $nowUtc): bool
    {
        return (string) $draw->status === DrawStatus::Cooldown->value
            && $draw->cooling_end_time instanceof Carbon
            && $draw->cooling_end_time <= $nowUtc;
    }

    /**
     * {@see DrawTickService} 发 `draw.status_change` 用：按 **数据库** `draw_no`+`status`，不用展示态规范化。
     *
     * @return array{draw_no: string, status: string}|null
     */
    public function hallTargetFingerprint(?Carbon $nowUtc = null): ?array
    {
        $target = $this->resolveHallTarget($nowUtc);
        if ($target === null) {
            return null;
        }

        return [
            'draw_no' => (string) $target->draw_no,
            'status' => (string) $target->status,
        ];
    }

    /**
     * @return array<string, mixed>|null
     */
    public function build(?Carbon $nowUtc = null, ?string $currencyCode = null): ?array
    {
        $nowUtc = ($nowUtc ?? Carbon::now())->utc();
        $currencyCode = $this->normalizeCurrencyCode($currencyCode);

        $target = $this->resolveHallTarget($nowUtc);

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

        $closeUtc = $target->close_time;
        $secsToClose = ($closeUtc !== null && $closeUtc > $nowUtc)
            ? max(0, (int) $closeUtc->getTimestamp() - (int) $nowUtc->getTimestamp())
            : 0;

        $secsToDraw = ($target->draw_time !== null && $target->draw_time > $nowUtc)
            ? max(0, (int) $target->draw_time->getTimestamp() - (int) $nowUtc->getTimestamp())
            : 0;

        $startUtc = $target->start_time;
        $secsToStart = ($startUtc !== null && $startUtc > $nowUtc)
            ? max(0, (int) $startUtc->getTimestamp() - (int) $nowUtc->getTimestamp())
            : 0;

        $coolingRemain = null;
        if (
            $target->cooling_end_time instanceof Carbon
            && $target->cooling_end_time > $nowUtc
        ) {
            $coolingRemain = max(
                0,
                (int) $target->cooling_end_time->getTimestamp() - (int) $nowUtc->getTimestamp(),
            );
        }

        $effectiveStatus = $this->effectiveHallDisplayStatus($target, $nowUtc);

        $scheduleTz = LotterySettings::drawTimezone();

        $payload = [
            'schedule_timezone' => $scheduleTz,
            'schedule_now' => $nowUtc->copy()->timezone($scheduleTz)->format('Y-m-d H:i:s'),
            'draw_no' => $target->draw_no,
            'business_date' => $target->business_date instanceof Carbon
                ? $target->business_date->format('Y-m-d')
                : (string) $target->business_date,
            'sequence_no' => (int) $target->sequence_no,
            'status' => $effectiveStatus,
            'start_time' => $target->start_time?->toIso8601String(),
            'close_time' => $target->close_time?->toIso8601String(),
            'draw_time' => $target->draw_time?->toIso8601String(),
            'seconds_to_close' => $secsToClose,
            'seconds_to_start' => $secsToStart,
            'seconds_to_draw' => $secsToDraw,
            'cooling_end_time' => $target->cooling_end_time?->toIso8601String(),
            'seconds_remaining_in_cooldown' => $coolingRemain,
            'jackpot_currency_code' => $currencyCode,
            'jackpot' => $this->cachedJackpotSummary($currencyCode),
        ];

        $payload['risk_pool_alerts'] = $this->cachedRiskAlerts((int) $target->id);

        if ($this->showsPublishedResults((string) $target->status)) {
            $resultItems = $this->cachedPublishedResultItems(
                (int) $target->id,
                (int) $target->current_result_version,
            );

            if ($resultItems !== null) {
                $payload['result_items'] = $resultItems;
            }

            $payload['result_version'] = (int) $target->current_result_version;
            $payload['result_source'] = $target->result_source;
        }

        return $payload;
    }

    private function normalizeCurrencyCode(?string $currencyCode): string
    {
        $code = strtoupper(substr(trim((string) ($currencyCode ?? '')), 0, 16));

        if ($code !== '') {
            return $code;
        }

        return LotterySettings::defaultCurrency();
    }

    /**
     * @return array<string, mixed>
     */
    private function cachedJackpotSummary(string $currencyCode): array
    {
        $cacheKey = sprintf('hall_snapshot:jackpot:%s', strtoupper($currencyCode));

        /** @var array<string, mixed> */
        return Cache::remember($cacheKey, self::JACKPOT_CACHE_TTL_SECONDS, fn (): array => $this->jackpotSummary->summary($currencyCode));
    }

    /**
     * @return list<array{normalized_number: string, status: string}>
     */
    private function cachedRiskAlerts(int $drawId): array
    {
        $cacheKey = sprintf('hall_snapshot:risk_alerts:%d', $drawId);

        /** @var list<array{normalized_number: string, status: string}> */
        return Cache::remember($cacheKey, self::RISK_ALERTS_CACHE_TTL_SECONDS, function () use ($drawId): array {
            return RiskPool::query()
                ->where('draw_id', $drawId)
                ->where(function ($q): void {
                    $q->where('sold_out_status', 1)
                        ->orWhereRaw('(locked_amount * 1.0 / NULLIF(total_cap_amount, 0)) >= 0.8');
                })
                ->orderByRaw('(locked_amount * 1.0 / NULLIF(total_cap_amount, 0)) DESC')
                ->orderByDesc('locked_amount')
                ->orderBy('normalized_number')
                ->limit(500)
                ->get(['normalized_number', 'sold_out_status'])
                ->map(fn ($row) => [
                    'normalized_number' => (string) $row->normalized_number,
                    'status' => (int) $row->sold_out_status === 1 ? 'sold_out' : 'warning',
                ])
                ->values()
                ->all();
        });
    }

    /**
     * @return list<array<string, mixed>>|null
     */
    private function cachedPublishedResultItems(int $drawId, int $resultVersion): ?array
    {
        if ($resultVersion <= 0) {
            return null;
        }

        $cacheKey = sprintf('hall_snapshot:result_items:%d:%d', $drawId, $resultVersion);

        /** @var list<array<string, mixed>>|null */
        return Cache::remember($cacheKey, self::RESULT_ITEMS_CACHE_TTL_SECONDS, function () use ($drawId, $resultVersion): ?array {
            $batchId = DrawResultBatch::query()
                ->where('draw_id', $drawId)
                ->where('result_version', $resultVersion)
                ->where('status', DrawResultBatchStatus::Published->value)
                ->value('id');

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

            return DrawResultItem::query()
                ->where('result_batch_id', $batchId)
                ->orderBy('prize_type')
                ->orderBy('prize_index')
                ->get([
                    'prize_type', 'prize_index',
                    'number_4d', 'suffix_3d', 'suffix_2d', 'head_digit', 'tail_digit',
                ])
                ->map(fn ($row) => [
                    'prize_type' => $row->prize_type,
                    'prize_index' => (int) $row->prize_index,
                    'number_4d' => $row->number_4d,
                    'suffix_3d' => $row->suffix_3d,
                    'suffix_2d' => $row->suffix_2d,
                    'head_digit' => $row->head_digit,
                    'tail_digit' => $row->tail_digit,
                ])
                ->values()
                ->all();
        });
    }
}
