<?php

namespace App\Services\Draw;

use Carbon\Carbon;
use App\Models\Draw;
use App\Lottery\DrawStatus;
use Illuminate\Support\Facades\Log;
use App\Services\LotterySettings;
use App\Services\Settlement\SettlementOrchestrator;
use App\Services\Settlement\SettlementTickFinalizer;

/**
 * 每分钟调度：期号状态推进 → RNG（若到期号）→ 冷静期结束时进入结算态 → 补齐未来缓冲。
 *
 * @see 《04-领域字典》draw_status
 */
final class DrawTickService
{
    public function __construct(
        private readonly DrawPlannerService $planner,
        private readonly DrawRngRunner $rng,
        private readonly DrawHallSnapshotBuilder $hallSnapshot,
        private readonly LotteryHallRealtimeBroadcaster $hallRealtime,
        private readonly SettlementOrchestrator $settlementOrchestrator,
        private readonly SettlementTickFinalizer $settlementFinalizer,
    ) {}

    /**
     * @return array{
     *     status_updates: array<string, int>,
     *     settling_settled: int,
     *     settlement_finalized: array{approved: int, paid: int},
     *     rng_rung: int,
     *     rng_errors: array<int, string>,
     *     planned: array<string, int>
     * }
     */
    public function tick(?Carbon $now = null): array
    {
        $nowUtc = ($now ?? Carbon::now())->utc();
        $startedAt = hrtime(true);
        $stageTimings = [];

        $hallFpBefore = $this->measureStage('hall_fp_before', $stageTimings, fn () => $this->hallSnapshot->hallTargetFingerprint($nowUtc));

        $statusUpdates = $this->measureStage('status_updates', $stageTimings, fn (): array => [
            'pending_to_open_or_later' => $this->promoteStalePendingRows($nowUtc),
            'open_to_closing_or_closed' => $this->openToClosingOrClosed($nowUtc),
            'closing_to_closed' => $this->closingToClosed($nowUtc),
            'cooldown_to_settling' => $this->cooldownToSettling($nowUtc),
        ]);

        $settlingSettled = $this->measureStage('settle_settling_draws', $stageTimings, fn (): int => $this->settleSettlingDraws());
        $settlementFinalized = $this->measureStage('finalize_pending_batches', $stageTimings, fn (): array => $this->settlementFinalizer->finalizePendingBatches());

        $rngOutcome = $this->measureStage('rng_run_due', $stageTimings, fn (): array => $this->rng->runDue($nowUtc));
        $planned = $this->measureStage('ensure_buffer', $stageTimings, fn (): array => $this->planner->ensureBuffer($nowUtc));

        $report = [
            'status_updates' => $statusUpdates,
            'settling_settled' => $settlingSettled,
            'settlement_finalized' => $settlementFinalized,
            'rng_rung' => $rngOutcome['rung'],
            'rng_errors' => $rngOutcome['errors'],
            'planned' => $planned,
        ];

        $snapshotAfter = $this->measureStage('hall_snapshot_after', $stageTimings, fn () => $this->hallSnapshot->build($nowUtc));
        $hallFpAfter = $this->measureStage('hall_fp_after', $stageTimings, fn () => $this->hallSnapshot->hallTargetFingerprint($nowUtc));

        $this->measureStage('notify_status_change', $stageTimings, function () use ($hallFpBefore, $hallFpAfter, $snapshotAfter): void {
            $this->hallRealtime->notifyStatusChangeIfHallDbChanged($hallFpBefore, $hallFpAfter, $snapshotAfter);
        });

        $this->logIfSlow($startedAt, $stageTimings, $report);

        return $report;
    }

    /** 补偿迟到的调度：pending 可依当前时刻落到 open / closing / closed。 */
    private function promoteStalePendingRows(Carbon $nowUtc): int
    {
        $toClosed = Draw::query()
            ->where('status', DrawStatus::Pending->value)
            ->whereNotNull('draw_time')
            ->where('draw_time', '<=', $nowUtc)
            ->update(['status' => DrawStatus::Closed->value]);

        $toClosing = Draw::query()
            ->where('status', DrawStatus::Pending->value)
            ->whereNotNull('close_time')
            ->whereNotNull('draw_time')
            ->where('close_time', '<=', $nowUtc)
            ->where('draw_time', '>', $nowUtc)
            ->update(['status' => DrawStatus::Closing->value]);

        $toOpen = Draw::query()
            ->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);
            })
            ->update(['status' => DrawStatus::Open->value]);

        return (int) $toClosed + (int) $toClosing + (int) $toOpen;
    }

    /** 先处理「已封盘且已越过开奖时刻」直达 closed，再走正常封盘中。 */
    private function openToClosingOrClosed(Carbon $nowUtc): int
    {
        $toClosed = Draw::query()
            ->where('status', DrawStatus::Open->value)
            ->whereNotNull('close_time')
            ->where('close_time', '<=', $nowUtc)
            ->whereNotNull('draw_time')
            ->where('draw_time', '<=', $nowUtc)
            ->update(['status' => DrawStatus::Closed->value]);

        $toClosing = Draw::query()
            ->where('status', DrawStatus::Open->value)
            ->whereNotNull('close_time')
            ->where('close_time', '<=', $nowUtc)
            ->where(function ($q) use ($nowUtc): void {
                $q->whereNull('draw_time')
                    ->orWhere('draw_time', '>', $nowUtc);
            })
            ->update(['status' => DrawStatus::Closing->value]);

        return (int) $toClosed + (int) $toClosing;
    }

    private function closingToClosed(Carbon $nowUtc): int
    {
        return Draw::query()
            ->where('status', DrawStatus::Closing->value)
            ->whereNotNull('draw_time')
            ->where('draw_time', '<=', $nowUtc)
            ->update(['status' => DrawStatus::Closed->value]);
    }

    /** 冷静期结束 → settling（结算/派彩由后续阶段补齐）。 */
    private function cooldownToSettling(Carbon $nowUtc): int
    {
        return Draw::query()
            ->where('status', DrawStatus::Cooldown->value)
            ->whereNotNull('cooling_end_time')
            ->where('cooling_end_time', '<=', $nowUtc)
            ->update(['status' => DrawStatus::Settling->value]);
    }

    /**
     * 冷静期结束后已进入 `settling` 的期号：执行阶段 6 结算（可经 lottery_settings 关闭自动跑批）。
     *
     * @return int 成功跑完结算的期号数量
     */
    private function settleSettlingDraws(): int
    {
        if (! (bool) LotterySettings::get('settlement.auto_run_on_tick', true)) {
            return 0;
        }

        $n = 0;
        $ids = Draw::query()
            ->where('status', DrawStatus::Settling->value)
            ->orderBy('id')
            ->limit((int) config('lottery.draw_tick_settle_limit', 3))
            ->pluck('id');
        foreach ($ids as $drawId) {
            $draw = Draw::query()->find($drawId);
            if ($draw === null) {
                continue;
            }
            try {
                if ($this->settlementOrchestrator->trySettleDraw($draw)) {
                    $n++;
                }
            } catch (\Throwable $e) {
                report($e);
                \Illuminate\Support\Facades\Log::warning('draw_tick_settlement_failed', [
                    'draw_id' => $draw->id,
                    'draw_no' => $draw->draw_no,
                    'error' => $e->getMessage(),
                ]);
            }
        }

        return $n;
    }

    /**
     * @template T
     * @param  array<string, int>  $stageTimings
     * @param  \Closure(): T  $callback
     * @return T
     */
    private function measureStage(string $stage, array &$stageTimings, \Closure $callback): mixed
    {
        $startedAt = hrtime(true);
        $result = $callback();
        $stageTimings[$stage] = (int) round((hrtime(true) - $startedAt) / 1_000_000);

        return $result;
    }

    /**
     * @param  array<string, int>  $stageTimings
     * @param  array<string, mixed>  $report
     */
    private function logIfSlow(int $startedAt, array $stageTimings, array $report): void
    {
        $totalMs = (int) round((hrtime(true) - $startedAt) / 1_000_000);
        $thresholdMs = (int) config('lottery.draw_tick_warn_threshold_ms', 1500);
        $stageThresholdMs = (int) config('lottery.draw_tick_stage_warn_threshold_ms', 500);
        $slowStages = array_filter($stageTimings, fn (int $elapsedMs): bool => $elapsedMs >= $stageThresholdMs);

        if ($totalMs < $thresholdMs && $slowStages === []) {
            return;
        }

        Log::warning('lottery:draw-tick exceeded warn threshold', [
            'elapsed_ms' => $totalMs,
            'threshold_ms' => $thresholdMs,
            'stage_threshold_ms' => $stageThresholdMs,
            'slow_stages_ms' => $slowStages,
            'all_stages_ms' => $stageTimings,
            'status_update_rows' => array_sum($report['status_updates'] ?? []),
            'settling_settled' => $report['settling_settled'] ?? 0,
            'approved_batches' => $report['settlement_finalized']['approved'] ?? 0,
            'paid_batches' => $report['settlement_finalized']['paid'] ?? 0,
            'rng_rung' => $report['rng_rung'] ?? 0,
            'planned_created' => $report['planned']['created'] ?? 0,
        ]);
    }
}
