<?php

namespace App\Services\Draw;

use Carbon\Carbon;
use App\Models\Draw;
use App\Lottery\DrawStatus;
use App\Services\LotterySettings;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\QueryException;

/**
 * 按计划生成未来的 `draws` 行（期号、时间表）。
 */
final class DrawPlannerService
{
    public function __construct(
        private readonly DrawTimelineBuilder $timeline,
    ) {}

    /** @return array{created: int, buffer_target: int, upcoming: int} */
    public function ensureBuffer(?Carbon $now = null): array
    {
        $nowUtc = ($now ?? Carbon::now())->utc();
        $tz = LotterySettings::drawTimezone();
        $interval = LotterySettings::drawIntervalMinutes();
        $buffer = LotterySettings::drawBufferDrawsAhead();
        $maxSeq = intdiv(24 * 60, $interval);

        $upcoming = Draw::query()
            ->where('draw_time', '>', $nowUtc)
            ->where('status', '!=', DrawStatus::Cancelled->value)
            ->count();

        $created = 0;
        $guard = 0;
        while ($upcoming < $buffer && $guard < 10_000) {
            $guard++;
            $nowLocal = $nowUtc->copy()->timezone($tz);
            $last = Draw::query()
                ->orderByDesc('business_date')
                ->orderByDesc('sequence_no')
                ->first();

            $row = $last === null
                ? $this->firstSchedule($tz, $interval, $maxSeq, $nowLocal)
                : $this->scheduleAfter($last, $tz, $interval, $nowLocal);

            try {
                DB::transaction(function () use ($row, $nowUtc, &$created): void {
                    Draw::query()->create($this->timelinePayload($row, $nowUtc));
                    $created++;
                });
                $upcoming++;
            } catch (QueryException $e) {
                if (($e->errorInfo[1] ?? null) === 19 || str_contains($e->getMessage(), 'unique')) {
                    /** 并发或重试：下一循环用新的 last 行 */
                    continue;
                }
                throw $e;
            }
        }

        return [
            'created' => $created,
            'buffer_target' => $buffer,
            'upcoming' => $upcoming,
        ];
    }

    /**
     * @return array{business_date: string, sequence_no: int, draw_local: Carbon}
     */
    private function firstSchedule(string $tz, int $intervalMinutes, int $maxSeqPerDay, Carbon $nowLocal): array
    {
        $day = $nowLocal->copy()->startOfDay();
        $seq = 1;
        $drawLocal = $day->copy()->addMinutes($seq * $intervalMinutes);
        while ($drawLocal <= $nowLocal && $seq <= $maxSeqPerDay) {
            $seq++;
            $drawLocal = $day->copy()->addMinutes($seq * $intervalMinutes);
        }
        if ($seq > $maxSeqPerDay) {
            $day = $day->addDay();
            $seq = 1;
            $drawLocal = $day->copy()->addMinutes($seq * $intervalMinutes);
            while ($drawLocal <= $nowLocal && $seq <= $maxSeqPerDay) {
                $seq++;
                $drawLocal = $day->copy()->addMinutes($seq * $intervalMinutes);
            }
        }

        return [
            'business_date' => $day->format('Y-m-d'),
            'sequence_no' => $seq,
            'draw_local' => $drawLocal->copy()->timezone($tz),
        ];
    }

    /**
     * @return array{business_date: string, sequence_no: int, draw_local: Carbon}
     */
    private function scheduleAfter(Draw $last, string $tz, int $intervalMinutes, Carbon $nowLocal): array
    {
        $lastDrawLocal = $last->draw_time !== null
            ? Carbon::parse($last->draw_time)->timezone($tz)
            : Carbon::parse((string) $last->business_date, $tz)
                ->startOfDay()
                ->addMinutes((int) $last->sequence_no * $intervalMinutes);

        $drawLocal = $lastDrawLocal->copy()->addMinutes($intervalMinutes);
        while ($drawLocal <= $nowLocal) {
            $drawLocal->addMinutes($intervalMinutes);
        }

        $businessDate = $drawLocal->format('Y-m-d');
        $lastDay = Carbon::parse((string) $last->business_date, $tz)->format('Y-m-d');
        $seq = $businessDate === $lastDay
            ? (int) $last->sequence_no + 1
            : 1;

        return [
            'business_date' => $businessDate,
            'sequence_no' => $seq,
            'draw_local' => $drawLocal->copy()->timezone($tz),
        ];
    }

    /**
     * @param  array{business_date: string, sequence_no: int, draw_local: Carbon}  $row
     * @return array<string, mixed>
     */
    private function timelinePayload(array $row, Carbon $nowUtc): array
    {
        $windows = $this->timeline->windowsFromDrawLocal($row['draw_local']->copy());
        $built = $this->timeline->buildFromLocals(
            $windows['start_local'],
            $windows['close_local'],
            $windows['draw_local'],
            $nowUtc,
        );

        return [
            'draw_no' => $this->timeline->drawNo($row['business_date'], $row['sequence_no']),
            'business_date' => $row['business_date'],
            'sequence_no' => $row['sequence_no'],
            'status' => $built['status'],
            'start_time' => $built['start_utc'],
            'close_time' => $built['close_utc'],
            'draw_time' => $built['draw_utc'],
            'cooling_end_time' => null,
            'result_source' => null,
            'current_result_version' => 0,
            'settle_version' => 0,
            'is_reopened' => false,
        ];
    }
}
