<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Enums\BookingType;
use App\Enums\SessionType;
use App\Exceptions\SlotUnavailableException;
use App\Models\Room;
use App\Models\User;
use App\Services\Booking\BookingService;
use Carbon\CarbonImmutable;
use Illuminate\Console\Command;
use Throwable;

/**
 * Attempts one booking and reports whether it won. Exists so the concurrency
 * test can launch genuinely parallel OS processes rather than pretending.
 *
 * A single-process test cannot prove anything about a race: PHP would run the
 * two attempts one after the other and both would pass. Real processes on
 * real connections, released together by --at, actually contend for the index.
 */
class AttemptBooking extends Command
{
    protected $signature = 'booking:attempt
        {user : User id}
        {room : Room slug}
        {start : Session start, ISO 8601 UTC}
        {slots=6 : How many slots to claim}
        {--session-type=with_engineer}
        {--at= : Unix timestamp with microseconds to hold until, so every process fires at once}';

    protected $description = 'Attempt a booking and print OK or FAIL. Used by the concurrency test.';

    public function handle(BookingService $bookings): int
    {
        // Hold every racer at the gate, then release them together. Without
        // this the processes drift apart by however long PHP takes to boot,
        // and the race we are trying to test never happens.
        if ($at = $this->option('at')) {
            $target = (float) $at;

            while (microtime(true) < $target) {
                // Spin rather than sleep: we want microsecond alignment.
            }
        }

        try {
            $booking = $bookings->create(
                user: User::findOrFail((int) $this->argument('user')),
                room: Room::where('slug', $this->argument('room'))->firstOrFail(),
                startsAt: CarbonImmutable::parse($this->argument('start'), 'UTC'),
                slotCount: (int) $this->argument('slots'),
                sessionType: SessionType::from((string) $this->option('session-type')),
                type: BookingType::Session,
            );

            $this->line('OK '.$booking->reference);

            return self::SUCCESS;
        } catch (SlotUnavailableException $e) {
            $this->line('FAIL '.$e->getMessage());

            return self::FAILURE;
        } catch (Throwable $e) {
            // Anything else is a real defect, not a lost race. Surfaced
            // distinctly so the test can tell the difference.
            $this->line('ERROR '.$e::class.': '.$e->getMessage());

            return 2;
        }
    }
}
