<?php

namespace App\Http\Controllers\Guest;

use App\Http\Controllers\Controller;
use App\Http\Requests\Guest\StoreGuestBookingRequest;
use App\Http\Requests\Guest\UploadReceiptRequest;
use App\Http\Requests\Guest\UploadGuestDocumentsRequest;
use App\Models\Booking;
use App\Models\BookingDocument;
use App\Models\BookingDeadlineRule;
use App\Models\BookingBlock;
use App\Models\BookingStatusLog;
use App\Models\DailyRoomRate;
use App\Models\Guest;
use App\Models\Room;
use App\Services\BookingPricingService;
use App\Services\BookingRoomDayService;
use App\Services\GoogleSyncService;
use App\Services\GuestGoogleBridgeService;
use App\Services\NotificationService;
use App\Support\PhoneNumber;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;

class BookingController extends Controller
{
    public function index(Request $request): View
    {
        $guest = Auth::guard('guest')->user();

        $bookings = Booking::query()
            ->with('room')
            ->where('guest_id', $guest->id)
            ->when($request->filled('status'), fn (Builder $q) => $q->where('status', $request->string('status')))
            ->latest()
            ->paginate(10)
            ->withQueryString();

        $statuses = collect(Booking::statusOptions())->except(['draft'])->all();
        $latestBooking = Booking::query()
            ->where('guest_id', $guest->id)
            ->latest()
            ->first();

        return view('guest.bookings.index', [
            'bookings' => $bookings,
            'statuses' => $statuses,
            'headingStatus' => $latestBooking ? ($statuses[$latestBooking->status] ?? $latestBooking->status) : null,
            'headingStatusValue' => $latestBooking?->status,
            'filters' => [
                'status' => (string) $request->string('status'),
            ],
        ]);
    }

    public function create(): View|RedirectResponse
    {
        if (Auth::guard('guest')->check()) {
            return redirect()->route('guest.bookings.create_discount');
        }

        return redirect()->route('public.booking_offer');
    }

    public function createDiscount(): View
    {
        return $this->renderCreate('discount');
    }

    public function quote(Request $request, BookingPricingService $pricingService, GuestGoogleBridgeService $bridgeService): JsonResponse
    {
        /** @var Guest|null $guest */
        $guest = Auth::guard('guest')->user();

        $validated = $request->validate([
            'room_id' => ['required', 'integer', 'exists:rooms,id'],
            'arrival_date' => ['required', 'date', 'after_or_equal:today'],
            'departure_date' => ['required', 'date', 'after:arrival_date'],
            'breakfast' => ['nullable', 'boolean'],
            'discount_code' => ['nullable', 'string', 'max:50'],
            'variant' => ['nullable', 'string'],
            'source' => ['nullable', 'string', 'max:50'],
        ]);

        $variant = ($validated['variant'] ?? 'regular') === 'discount' ? 'discount' : 'regular';
        $discountAccepted = $this->discountAccepted($guest, $variant, $validated['discount_code'] ?? null);
        $source = $this->normalizeSourceLabel($validated['source'] ?? 'Site');

        try {
            $quote = $this->resolveQuote(
                (int) $validated['room_id'],
                $validated['arrival_date'],
                $validated['departure_date'],
                (bool) ($validated['breakfast'] ?? false),
                $discountAccepted,
                $source,
                $pricingService,
                $bridgeService,
            );
        } catch (\RuntimeException $e) {
            return response()->json([
                'ok' => false,
                'message' => $e->getMessage(),
            ], 422);
        }

        $deadline = $this->resolveGuestPaymentDeadline((int) $validated['room_id'], Carbon::parse($validated['arrival_date'])->startOfDay());
        $quote['payment_deadline'] = $deadline?->format('Y-m-d');
        $quote['payment_deadline_display'] = $deadline?->format('d.m.Y');
        $quote['payment_deadline_days'] = $deadline ? max(0, today()->diffInDays($deadline->copy()->startOfDay(), false)) : null;
        $quote['payment_deadline_enabled'] = ! is_null($deadline);

        return response()->json([
            'ok' => true,
            'quote' => $quote,
            'pricing' => $this->localPricingForDate((int) $validated['room_id'], Carbon::parse($validated['arrival_date'])->startOfDay()),
            'discount_accepted' => $discountAccepted,
        ]);
    }

    private function renderCreate(string $variant): View
    {
        /** @var Guest|null $guest */
        $guest = Auth::guard('guest')->user();
        $createdBookingData = session('created_booking_data');
        if (! is_array($createdBookingData)) {
            $createdBookingData = null;
        }

        $isDiscount = $variant === 'discount';

        return view('guest.bookings.create', [
            'guest' => $guest,
            'variant' => $variant,
            'quoteUrl' => route('guest.bookings.quote'),
            'availabilityUrl' => route('guest.bookings.availability'),
            'receiptUploadUrl' => route('guest.bookings.receipt.public'),
            'guestDocumentsUploadUrl' => route('guest.bookings.documents.public'),
            'rooms' => Room::query()->where('is_active', true)->orderBy('sort_order')->get(),
            'createdBookingData' => $createdBookingData,
            'hasSuccessFlash' => session()->has('success'),
            'defaults' => [
                'arrival_date' => old('arrival_date', now()->format('Y-m-d')),
                'departure_date' => old('departure_date', now()->addDay()->format('Y-m-d')),
                'full_name' => old('full_name', $isDiscount ? ($guest?->full_name ?? '') : ''),
                'phone' => old('phone', $isDiscount ? ($guest?->phone ?? '') : ''),
                'email' => old('email', $isDiscount ? ($guest?->email ?? '') : ''),
                'adults' => old('adults', 2),
                'children_under_2' => old('children_under_2', 0),
                'breakfast' => old('breakfast', false),
                'discount_code' => old('discount_code', $isDiscount ? $this->guestDiscountCodeForDisplay($guest) : ''),
            ],
        ]);
    }

    public function availability(Request $request, GuestGoogleBridgeService $bridgeService): JsonResponse
    {
        $validated = $request->validate([
            'arrival_date' => ['required', 'date', 'after_or_equal:today'],
            'nights' => ['required', 'integer', 'min:1', 'max:30'],
            'room_id' => ['nullable', 'integer', 'exists:rooms,id'],
            'breakfast' => ['nullable', 'boolean'],
            'is_discount' => ['nullable', 'boolean'],
            'source' => ['nullable', 'string', 'max:50'],
        ]);

        $arrivalDate = Carbon::parse($validated['arrival_date'])->startOfDay();
        $nights = (int) $validated['nights'];
        $departureDate = $arrivalDate->copy()->addDays($nights);
        $selectedRoomId = ! empty($validated['room_id']) ? (int) $validated['room_id'] : null;
        $source = $this->normalizeSourceLabel($validated['source'] ?? 'Site');
        $isDiscount = (bool) ($validated['is_discount'] ?? false);
        $breakfast = (bool) ($validated['breakfast'] ?? false);

        $statusMap = $this->emptyStatusMapByRoomCode();
        $roomIdToCode = $this->roomIdToCodeMap();

        $pricing = $selectedRoomId !== null ? $this->localPricingForDate($selectedRoomId, $arrivalDate) : null;

        // Guest forms must read statuses and prices from MySQL first.
        // The Google bridge is only a short fallback when the local daily_room_rates row is missing.

        $conflicts = $this->occupancyConflicts($arrivalDate, $departureDate)
            ->get(['room_id', 'status']);

        foreach ($conflicts as $conflict) {
            $roomCode = $roomIdToCode[$conflict->room_id] ?? null;
            if ($roomCode !== null) {
                $statusMap[$roomCode] = $conflict->status === 'confirmed' ? 'occupied' : 'held';
            }
        }

        $bookingBlocks = $this->bookingBlockConflicts($arrivalDate, $departureDate)
            ->get(['room_id']);

        foreach ($bookingBlocks as $block) {
            $roomCode = $roomIdToCode[$block->room_id] ?? null;
            if ($roomCode !== null && ($statusMap[$roomCode] ?? 'free') === 'free') {
                $statusMap[$roomCode] = 'held';
            }
        }

        if ($pricing === null && $selectedRoomId !== null) {
            $pricing = $this->localPricingForDate($selectedRoomId, $arrivalDate);
        }

        return response()->json([
            'ok' => true,
            'status_map' => $statusMap,
            'pricing' => $pricing,
        ]);
    }

    public function store(
        StoreGuestBookingRequest $request,
        BookingPricingService $pricingService,
        GuestGoogleBridgeService $bridgeService,
        GoogleSyncService $googleSyncService,
        BookingRoomDayService $bookingRoomDayService,
        NotificationService $notificationService
    ): RedirectResponse {
        /** @var Guest|null $guest */
        $guest = Auth::guard('guest')->user();
        $validated = $request->validated();

        if (! $guest) {
            return redirect()
                ->route('public.booking_offer')
                ->with('error', 'Для бронирования пройдите простую регистрацию или войдите в кабинет.');
        }

        $arrivalDate = Carbon::parse($validated['arrival_date'])->startOfDay();
        $departureDate = Carbon::parse($validated['departure_date'])->startOfDay();
        $nights = (int) $arrivalDate->diffInDays($departureDate);
        $breakfast = (bool) ($validated['breakfast'] ?? false);
        $variant = 'discount';
        $discountAccepted = $this->discountAccepted($guest, $variant, $validated['discount_code'] ?? null);
        $discountCode = $this->resolveGuestDiscountCode($guest, $discountAccepted, $validated['discount_code'] ?? null);

        $guestName = $variant === 'discount' && $guest
            ? $guest->full_name
            : trim((string) ($validated['full_name'] ?? ''));
        $phone = $variant === 'discount' && $guest
            ? PhoneNumber::normalize($guest->phone)
            : PhoneNumber::normalize($validated['phone'] ?? '');
        $email = $variant === 'discount' && $guest
            ? $guest->email
            : trim((string) ($validated['email'] ?? ''));
        $guestLanguage = $this->normalizeGuestLanguage($validated['lang'] ?? null);
        if ($guest && $guest->language !== $guestLanguage) {
            $guest->forceFill(['language' => $guestLanguage])->save();
        }

        try {
            $quote = $this->resolveQuote(
                (int) $validated['room_id'],
                $arrivalDate,
                $departureDate,
                $breakfast,
                $discountAccepted,
                'Site',
                $pricingService,
                $bridgeService,
            );
        } catch (\RuntimeException $e) {
            throw ValidationException::withMessages([
                'arrival_date' => $e->getMessage(),
            ]);
        }

        $booking = DB::transaction(function () use ($guest, $validated, $variant, $guestName, $phone, $email, $guestLanguage, $arrivalDate, $departureDate, $nights, $breakfast, $discountAccepted, $discountCode, $quote, $bookingRoomDayService): Booking {
            $conflict = $this->occupancyConflicts($arrivalDate, $departureDate, (int) $validated['room_id'])
                ->lockForUpdate()
                ->first();

            if ($conflict !== null) {
                throw ValidationException::withMessages([
                    'room_id' => 'Эта комната уже занята на выбранные даты. Выберите другую.',
                ]);
            }

            if ($this->bookingBlockConflicts($arrivalDate, $departureDate, (int) $validated['room_id'])->lockForUpdate()->exists()) {
                throw ValidationException::withMessages([
                    'room_id' => 'Эта комната временно закрыта для продажи на Booking. Выберите другую комнату.',
                ]);
            }

            $bookingGuest = $guest;

            $booking = Booking::create([
                'booking_number' => $this->generateBookingNumber(),
                'guest_id' => $bookingGuest?->id,
                'room_id' => $validated['room_id'],
                'guest_name' => $guestName,
                'phone' => $phone !== '' ? $phone : null,
                'email' => $email !== '' ? $email : null,
                'source' => 'site',
                'booking_variant' => $discountAccepted ? 'discount' : 'regular',
                'rate_name' => $quote['rate_name'],
                'status' => 'awaiting_prepayment',
                'arrival_date' => $arrivalDate->toDateString(),
                'departure_date' => $departureDate->toDateString(),
                'nights' => $nights,
                'min_nights_applied' => (int) $quote['min_nights'],
                'adults' => (int) $validated['adults'],
                'children_under_2' => (int) ($validated['children_under_2'] ?? 0),
                'breakfast' => $breakfast,
                'discount_applied' => $discountAccepted,
                'discount_code' => $discountCode,
                'pricing_mode' => 'calendar',
                'manual_price_override' => false,
                'rate_snapshot_json' => $quote['snapshot'],
                'price_calculated_at' => now(),
                'nightly_amount' => $quote['nightly_amount'],
                'full_amount' => $quote['full_amount'],
                'prepayment_amount' => $quote['prepayment_amount'],
                'remaining_amount' => $quote['remaining_amount'],
                'payment_deadline' => $this->resolveGuestPaymentDeadline((int) $validated['room_id'], $arrivalDate),
                'comment_guest' => $validated['comment_guest'] ?? null,
                'comment_admin' => $discountAccepted ? 'Гостевая бронь со скидкой.' : 'Гостевая бронь без скидки.',
                'check_in_time_from' => '12:00',
                'check_in_time_to' => '23:00',
                'receipt_received' => false,
                'prepayment_received' => false,
                'secure_token' => Str::random(64),
            ]);

            $bookingRoomDayService->reserveBooking($booking);

            BookingStatusLog::create([
                'booking_id' => $booking->id,
                'old_status' => null,
                'new_status' => 'awaiting_prepayment',
                'changed_by_user_id' => null,
                'note' => 'Гостевая бронь создана. Тариф: '.$quote['rate_name'].'.',
            ]);

            return $booking;
        });

        $booking->load(['room', 'guest']);
        if (! Auth::guard('guest')->check() && $booking->guest) {
            Auth::guard('guest')->login($booking->guest);
            $request->session()->regenerate();
        }
        $this->ensureBookingPrivateFolder($booking);
        $googleSyncService->syncBooking($booking, 'booking_created');
        $notificationService->sendGuestBookingCreated($booking);
        $notificationService->sendAdminNewBooking($booking);

        return redirect()
            ->route('guest.bookings.created', ['bookingNumber' => $booking->booking_number, 'token' => $booking->secure_token]);
    }

    public function created(Request $request, string $bookingNumber): View
    {
        $booking = Booking::query()
            ->with(['room', 'guest'])
            ->where('booking_number', $bookingNumber)
            ->firstOrFail();

        $token = (string) $request->query('token', '');
        if ($booking->secure_token && ! hash_equals((string) $booking->secure_token, $token)) {
            abort(403);
        }

        return view('bookings.created', [
            'booking' => $booking,
            'createdBookingData' => $this->buildCreatedBookingData($booking),
            'isAdmin' => false,
            'receiptUploadUrl' => route('guest.bookings.receipt.public'),
            'guestDocumentsUploadUrl' => route('guest.bookings.documents.public'),
            'secureToken' => $booking->secure_token,
            'backUrl' => route('home'),
            'backLabel' => 'На главную',
        ]);
    }

    public function show(Booking $booking): View
    {
        /** @var Guest $guest */
        $guest = Auth::guard('guest')->user();
        abort_unless((int) $booking->guest_id === (int) $guest->id, 403);

        $booking->load(['room', 'guest', 'payments', 'documents', 'statusLogs', 'syncLogs']);

        return view('guest.bookings.show', [
            'booking' => $booking,
        ]);
    }

    public function uploadReceipt(UploadReceiptRequest $request, Booking $booking, GoogleSyncService $googleSyncService, NotificationService $notificationService): RedirectResponse
    {
        /** @var Guest $guest */
        $guest = Auth::guard('guest')->user();
        abort_unless((int) $booking->guest_id === (int) $guest->id, 403);

        $file = $request->file('receipt');
        $path = $this->storeReceiptInPrivateBookingFolder($booking, $file);

        DB::transaction(function () use ($booking, $file, $path, $request): void {
            BookingDocument::create([
                'booking_id' => $booking->id,
                'type' => 'receipt',
                'file_path' => $path,
                'original_name' => $file->getClientOriginalName(),
                'mime_type' => $file->getClientMimeType(),
                'uploaded_by_user_id' => null,
                'uploaded_at' => now(),
            ]);

            $oldStatus = $booking->status;
            $extra = trim((string) $request->string('note'));
            $booking->update([
                'status' => 'payment_uploaded',
                'comment_guest' => trim(($booking->comment_guest ? $booking->comment_guest.PHP_EOL.PHP_EOL : '').'Квитанция загружена'.($extra ? ': '.$extra : '.')),
                'receipt_received' => true,
            ]);

            BookingStatusLog::create([
                'booking_id' => $booking->id,
                'old_status' => $oldStatus,
                'new_status' => 'payment_uploaded',
                'changed_by_user_id' => null,
                'note' => 'Гость загрузил квитанцию о предоплате.',
            ]);
        });

        $booking->refresh()->load('room');
        $googleSyncService->syncBooking($booking, 'receipt_uploaded');
        $notificationService->sendGuestReceiptUploaded($booking);

        return back()->with('success', 'Квитанция загружена. Ожидается проверка администратором.');
    }

    public function uploadReceiptPublic(UploadReceiptRequest $request, GoogleSyncService $googleSyncService, NotificationService $notificationService): JsonResponse
    {
        $reservationId = trim((string) $request->input('reservation_id', $request->input('booking_number')));
        if ($reservationId === '') {
            return response()->json([
                'ok' => false,
                'message' => 'Не найден номер бронирования для загрузки квитанции.',
            ], 422);
        }

        $booking = Booking::query()->where('booking_number', $reservationId)->first();
        if (! $booking) {
            return response()->json([
                'ok' => false,
                'message' => 'Бронирование не найдено.',
            ], 404);
        }

        if ($booking->secure_token && ! hash_equals((string) $booking->secure_token, (string) $request->input('token', ''))) {
            return response()->json([
                'ok' => false,
                'message' => 'Доступ к бронированию запрещён.',
            ], 403);
        }

        $file = $request->file('receipt');
        $path = $this->storeReceiptInPrivateBookingFolder($booking, $file);

        DB::transaction(function () use ($booking, $file, $path, $request): void {
            BookingDocument::create([
                'booking_id' => $booking->id,
                'type' => 'receipt',
                'file_path' => $path,
                'original_name' => $file->getClientOriginalName(),
                'mime_type' => $file->getClientMimeType(),
                'uploaded_by_user_id' => null,
                'uploaded_at' => now(),
            ]);

            $oldStatus = (string) $booking->status;
            $extra = trim((string) $request->string('note'));
            $newStatus = $oldStatus === 'confirmed' ? 'confirmed' : 'payment_uploaded';

            $booking->update([
                'status' => $newStatus,
                'comment_guest' => trim(($booking->comment_guest ? $booking->comment_guest.PHP_EOL.PHP_EOL : '').'Квитанция загружена'.($extra ? ': '.$extra : '.')),
                'receipt_received' => true,
            ]);

            if ($newStatus !== $oldStatus) {
                BookingStatusLog::create([
                    'booking_id' => $booking->id,
                    'old_status' => $oldStatus,
                    'new_status' => $newStatus,
                    'changed_by_user_id' => null,
                    'note' => 'Гость загрузил квитанцию о предоплате через публичную форму.',
                ]);
            }
        });

        $booking->refresh()->load('room');
        $googleSyncService->syncBooking($booking, 'receipt_uploaded');
        $notificationService->sendGuestReceiptUploaded($booking);

        return response()->json([
            'ok' => true,
            'message' => 'Квитанция загружена. Теперь можно пройти онлайн-регистрацию и загрузить документы проживающих.',
            'documents_available' => true,
        ]);
    }

    public function uploadGuestDocumentsPublic(UploadGuestDocumentsRequest $request, NotificationService $notificationService): JsonResponse
    {
        $reservationId = trim((string) $request->input('reservation_id'));
        $booking = Booking::query()->with('room')->where('booking_number', $reservationId)->first();

        if (! $booking) {
            return response()->json([
                'ok' => false,
                'message' => 'Бронирование не найдено.',
            ], 404);
        }

        if ($booking->secure_token && ! hash_equals((string) $booking->secure_token, (string) $request->input('token', ''))) {
            return response()->json([
                'ok' => false,
                'message' => 'Доступ к бронированию запрещён.',
            ], 403);
        }

        if (! $booking->receipt_received) {
            return response()->json([
                'ok' => false,
                'message' => 'Сначала загрузите квитанцию о предоплате.',
            ], 422);
        }

        $files = $request->file('documents', []);
        $stored = 0;

        DB::transaction(function () use ($booking, $files, &$stored): void {
            foreach ($files as $index => $file) {
                $path = $this->storeGuestDocumentInPrivateBookingFolder($booking, $file, (int) $index + 1);

                BookingDocument::create([
                    'booking_id' => $booking->id,
                    'type' => 'guest_document',
                    'file_path' => $path,
                    'original_name' => $file->getClientOriginalName(),
                    'mime_type' => $file->getClientMimeType(),
                    'uploaded_by_user_id' => null,
                    'uploaded_at' => now(),
                ]);

                $stored++;
            }

            $booking->update([
                'guest_documents_received' => true,
                'guest_documents_consent_at' => now(),
                'comment_guest' => trim(($booking->comment_guest ? $booking->comment_guest.PHP_EOL.PHP_EOL : '').'Документы проживающих загружены для онлайн-регистрации.'),
            ]);
        });

        $booking->refresh()->load(['guest', 'room']);
        $lang = $this->normalizeGuestLanguage((string) $request->input('lang', $booking->guest?->language ?? 'ru'));

        return response()->json([
            'ok' => true,
            'message' => $this->onlineRegistrationCompleteMessage($booking, $lang),
            'files_count' => $stored,
        ]);
    }


    private function onlineRegistrationCompleteMessage(Booking $booking, string $lang): string
    {
        $code = (string) $booking->booking_number;
        $messages = [
            'ru' => "Ваше предварительное бронирование создано.\nКод брони: {$code}.\nВся информация доступна в личном кабинете гостя.\nОжидайте сообщение о подтверждении бронирования.",
            'uk' => "Ваше попереднє бронювання створено.\nКод бронювання: {$code}.\nУся інформація доступна в особистому кабінеті гостя.\nОчікуйте повідомлення про підтвердження бронювання.",
            'it' => "La vostra prenotazione preliminare è stata creata.\nCodice prenotazione: {$code}.\nTutte le informazioni sono disponibili nell’area ospite.\nAttendete il messaggio di conferma della prenotazione.",
            'en' => "Your provisional booking has been created.\nBooking code: {$code}.\nAll information is available in the guest cabinet.\nPlease wait for the booking confirmation message.",
            'fr' => "Votre réservation préliminaire a été créée.\nCode de réservation : {$code}.\nToutes les informations sont disponibles dans l’espace client.\nVeuillez attendre le message de confirmation de réservation.",
            'de' => "Ihre vorläufige Buchung wurde erstellt.\nBuchungscode: {$code}.\nAlle Informationen sind im Gästebereich verfügbar.\nBitte warten Sie auf die Buchungsbestätigung.",
        ];

        return $messages[$lang] ?? $messages['ru'];
    }

    public function downloadDocument(Booking $booking, BookingDocument $document): BinaryFileResponse|StreamedResponse
    {
        abort(403, 'Документы доступны только администратору.');
    }

    private function ensureBookingPrivateFolder(Booking $booking): string
    {
        $booking->loadMissing('room');

        $roomCode = (string) ($booking->room?->code ?: 'room');
        $roomCode = str_replace(['№', 'А', 'а'], ['', 'A', 'A'], $roomCode);
        $roomCode = preg_replace('/[^A-Za-z0-9_-]+/', '', $roomCode) ?: 'room';
        $arrival = $booking->arrival_date?->format('d.m.Y') ?: now()->format('d.m.Y');
        $folder = 'private/bookings/'.$arrival.'/'.$roomCode;

        \Illuminate\Support\Facades\Storage::disk('local')->makeDirectory($folder);

        return $folder;
    }

    private function storeReceiptInPrivateBookingFolder(Booking $booking, $file): string
    {
        $folder = $this->ensureBookingPrivateFolder($booking);
        $extension = strtolower($file->getClientOriginalExtension() ?: $file->extension() ?: 'bin');
        $safeBookingNumber = preg_replace('/[^A-Za-z0-9_-]+/', '-', $booking->booking_number) ?: 'booking';
        $filename = 'receipt-'.$safeBookingNumber.'-'.now()->format('YmdHis').'.'.$extension;

        $file->storeAs($folder, $filename, 'local');

        return $folder.'/'.$filename;
    }

    private function storeGuestDocumentInPrivateBookingFolder(Booking $booking, $file, int $number): string
    {
        $folder = $this->ensureBookingPrivateFolder($booking);
        $extension = strtolower($file->getClientOriginalExtension() ?: $file->extension() ?: 'bin');
        $safeBookingNumber = preg_replace('/[^A-Za-z0-9_-]+/', '-', $booking->booking_number) ?: 'booking';
        $filename = 'guest-document-'.$safeBookingNumber.'-'.now()->format('YmdHis').'-'.str_pad((string) $number, 2, '0', STR_PAD_LEFT).'.'.$extension;

        $file->storeAs($folder, $filename, 'local');

        return $folder.'/'.$filename;
    }

    private function findOrCreateRegularGuest(string $fullName, string $phone, string $email, string $language): Guest
    {
        $fullName = trim($fullName);
        $phone = PhoneNumber::normalize($phone);
        $email = trim($email);

        $guest = Guest::query()->where('phone', $phone)->first();

        if (! $guest && $email !== '') {
            $guest = Guest::query()->where('email', $email)->first();
        }

        if ($guest) {
            $updates = [
                'full_name' => $fullName !== '' ? $fullName : $guest->full_name,
                'language' => $language,
                'status' => 'active',
                'privacy_consent' => true,
            ];

            if ($email !== '' && ! Guest::query()->where('email', $email)->whereKeyNot($guest->id)->exists()) {
                $updates['email'] = $email;
            }

            if ($phone !== '' && ! Guest::query()->where('phone', $phone)->whereKeyNot($guest->id)->exists()) {
                $updates['phone'] = $phone;
            }

            $guest->update($updates);

            return $guest->refresh();
        }

        return Guest::create([
            'full_name' => $fullName,
            'phone' => $phone,
            'email' => $email !== '' ? $email : null,
            'language' => $language,
            'password' => Str::random(32),
            'status' => 'active',
            'discount_percent' => 0,
            'discount_code' => null,
            'privacy_consent' => true,
            'notes' => 'Автоматическая регистрация после гостевого бронирования без скидки.',
        ]);
    }


    private function resolveGuestPaymentDeadline(int $roomId, Carbon $arrivalDate): ?Carbon
    {
        $rule = BookingDeadlineRule::activeRuleFor($roomId, $arrivalDate);

        // Единый источник правил: панель управления.
        // Авто = стандартная логика: заезд через 1–2 дня → сегодня, иначе +3 дня.
        // Фиксированное количество дней = берём число из поля days_to_payment.
        if ($rule !== null && $rule->mode === 'custom_days') {
            return today()->copy()->addDays($this->effectiveFixedPaymentDays($arrivalDate, (int) $rule->days_to_payment))->endOfDay();
        }

        $today = today();
        if ($arrivalDate->lessThanOrEqualTo($today->copy()->addDays(2))) {
            return $today->endOfDay();
        }

        return $today->copy()->addDays(3)->endOfDay();
    }

    private function effectiveFixedPaymentDays(Carbon $arrivalDate, int $configuredDays): int
    {
        $configuredDays = max(1, $configuredDays);
        $daysUntilArrival = max(0, today()->diffInDays($arrivalDate->copy()->startOfDay(), false));

        if ($daysUntilArrival >= $configuredDays) {
            return $configuredDays;
        }

        if ($daysUntilArrival >= 3) {
            return $daysUntilArrival;
        }

        return 1;
    }

    private function normalizeGuestLanguage(?string $language): string
    {
        $language = strtolower(trim((string) $language));
        $language = explode('-', $language)[0] ?? 'ru';

        if ($language === 'ua') {
            $language = 'uk';
        }

        return in_array($language, ['it', 'en', 'ru', 'uk', 'fr', 'de'], true) ? $language : 'ru';
    }

    private function generateBookingNumber(): string
    {
        do {
            $candidate = 'HP-'.now()->format('ymd').'-'.Str::upper(Str::random(5));
        } while (Booking::query()->where('booking_number', $candidate)->exists());

        return $candidate;
    }

    private function guestDiscountCodeForDisplay(?Guest $guest): string
    {
        if (! $guest) {
            return '';
        }

        $code = trim((string) ($guest->discount_code ?? ''));
        if ($code !== '') {
            return $code;
        }

        $percent = (int) ($guest->discount_percent ?? 0);
        $percent = $percent > 0 ? $percent : 25;

        return 'PINETA'.$percent.'-VIP';
    }

    private function resolveGuestDiscountCode(?Guest $guest, bool $discountAccepted, ?string $submittedCode): ?string
    {
        if (! $discountAccepted) {
            return null;
        }

        $code = trim((string) ($submittedCode ?? ''));
        if ($code !== '') {
            return mb_substr($code, 0, 50);
        }

        $fallback = $this->guestDiscountCodeForDisplay($guest);
        return $fallback !== '' ? mb_substr($fallback, 0, 50) : null;
    }

    private function discountCodeForOutput(Booking $booking): ?string
    {
        if (! $booking->discount_applied || strtolower((string) $booking->source) === 'booking') {
            return null;
        }

        $code = trim((string) ($booking->discount_code ?? ''));
        return $code !== '' ? $code : null;
    }

    private function discountAccepted(?Guest $guest, string $variant, ?string $discountCode): bool
    {
        if ($variant !== 'discount' || $guest === null) {
            return false;
        }

        // The protected guest discount form already means that the logged-in guest is booking with discount.
        // If the guest has no saved code yet, the form fallback code is used and stored in the booking.
        return true;
    }

    private function resolveQuote(
        int $roomId,
        string|Carbon $arrivalDate,
        string|Carbon $departureDate,
        bool $breakfast,
        bool $discount,
        string $source,
        BookingPricingService $pricingService,
        GuestGoogleBridgeService $bridgeService,
    ): array {
        $arrival = $arrivalDate instanceof Carbon ? $arrivalDate->copy()->startOfDay() : Carbon::parse($arrivalDate)->startOfDay();
        $departure = $departureDate instanceof Carbon ? $departureDate->copy()->startOfDay() : Carbon::parse($departureDate)->startOfDay();
        $nights = (int) $arrival->diffInDays($departure);

        $this->assertRoomAvailable($roomId, $arrival, $departure, $source === 'Booking');

        return $pricingService->quoteForStay($roomId, $arrival, $departure, $breakfast, $discount);
    }

    private function normalizeBridgeQuote(array $quote, bool $breakfast, bool $discount, string $source): array
    {
        $nights = max(1, (int) ($quote['nights'] ?? 1));

        return [
            'nightly_amount' => round((float) ($quote['nightly_amount'] ?? 0), 2),
            'full_amount' => round((float) ($quote['full_amount'] ?? $quote['total_amount'] ?? 0), 2),
            'prepayment_amount' => round((float) ($quote['prepayment_amount'] ?? 0), 2),
            'remaining_amount' => round((float) ($quote['remaining_amount'] ?? 0), 2),
            'rate_name' => $this->rateNameFromFlags($breakfast, $discount, $nights, $source),
            'min_nights' => (int) ($quote['min_nights'] ?? $quote['min_nights_required'] ?? 1),
            'discount_applied' => $source === 'Booking' ? false : $discount,
            'three_plus' => $nights > 3,
            'field' => $this->rateFieldFromFlags($breakfast, $discount, $nights, $source),
            'snapshot' => [
                'source' => 'guest_google_bridge',
                'remote_quote' => $quote,
            ],
        ];
    }

    private function normalizeBridgePricing(?array $raw): ?array
    {
        if (! is_array($raw)) {
            return null;
        }

        return [
            'with_breakfast' => isset($raw['with_breakfast']) ? (float) $raw['with_breakfast'] : (float) ($raw['breakfast'] ?? 0),
            'without_breakfast' => isset($raw['without_breakfast']) ? (float) $raw['without_breakfast'] : (float) ($raw['no_breakfast'] ?? 0),
            'with_breakfast_3plus' => isset($raw['with_breakfast_3plus']) ? (float) $raw['with_breakfast_3plus'] : (float) ($raw['breakfast_long'] ?? 0),
            'without_breakfast_3plus' => isset($raw['without_breakfast_3plus']) ? (float) $raw['without_breakfast_3plus'] : (float) ($raw['no_breakfast_long'] ?? 0),
            'with_breakfast_discount' => isset($raw['with_breakfast_discount']) ? (float) $raw['with_breakfast_discount'] : (float) ($raw['breakfast_discount'] ?? 0),
            'without_breakfast_discount' => isset($raw['without_breakfast_discount']) ? (float) $raw['without_breakfast_discount'] : (float) ($raw['no_breakfast_discount'] ?? 0),
            'with_breakfast_discount_3plus' => isset($raw['with_breakfast_discount_3plus']) ? (float) $raw['with_breakfast_discount_3plus'] : (float) ($raw['breakfast_discount_long'] ?? 0),
            'without_breakfast_discount_3plus' => isset($raw['without_breakfast_discount_3plus']) ? (float) $raw['without_breakfast_discount_3plus'] : (float) ($raw['no_breakfast_discount_long'] ?? 0),
            'min_nights' => (int) ($raw['min_nights'] ?? $raw['min_nights_required'] ?? 1),
        ];
    }

    private function localPricingForDate(int $roomId, Carbon $arrivalDate): ?array
    {
        $rate = DailyRoomRate::query()
            ->where('room_id', $roomId)
            ->whereDate('rate_date', $arrivalDate->toDateString())
            ->first();

        if (! $rate) {
            return null;
        }

        return [
            'with_breakfast' => (float) $rate->price_breakfast,
            'without_breakfast' => (float) $rate->price_no_breakfast,
            'with_breakfast_3plus' => (float) $rate->price_breakfast_3plus,
            'without_breakfast_3plus' => (float) $rate->price_no_breakfast_3plus,
            'with_breakfast_discount' => (float) $rate->price_breakfast_discount,
            'without_breakfast_discount' => (float) $rate->price_no_breakfast_discount,
            'with_breakfast_discount_3plus' => (float) $rate->price_breakfast_discount_3plus,
            'without_breakfast_discount_3plus' => (float) $rate->price_no_breakfast_discount_3plus,
            'min_nights' => (int) $rate->min_nights,
        ];
    }

    private function rateNameFromFlags(bool $breakfast, bool $discount, int $nights, string $source): string
    {
        $longStay = $nights > 3;
        $effectiveDiscount = $source === 'Booking' ? false : $discount;

        if ($effectiveDiscount && $longStay) {
            return $breakfast ? 'Со скидкой, с завтраком, более 3 дней' : 'Со скидкой, без завтрака, более 3 дней';
        }

        if ($effectiveDiscount) {
            return $breakfast ? 'Со скидкой, с завтраком' : 'Со скидкой, без завтрака';
        }

        if ($longStay) {
            return $breakfast ? 'С завтраком, более 3 дней' : 'Без завтрака, более 3 дней';
        }

        return $breakfast ? 'С завтраком' : 'Без завтрака';
    }

    private function rateFieldFromFlags(bool $breakfast, bool $discount, int $nights, string $source): string
    {
        $longStay = $nights > 3;
        $effectiveDiscount = $source === 'Booking' ? false : $discount;

        if ($effectiveDiscount && $longStay) {
            return $breakfast ? 'price_breakfast_discount_3plus' : 'price_no_breakfast_discount_3plus';
        }

        if ($effectiveDiscount) {
            return $breakfast ? 'price_breakfast_discount' : 'price_no_breakfast_discount';
        }

        if ($longStay) {
            return $breakfast ? 'price_breakfast_3plus' : 'price_no_breakfast_3plus';
        }

        return $breakfast ? 'price_breakfast' : 'price_no_breakfast';
    }

    private function normalizeSourceLabel(string $source): string
    {
        $value = Str::of($source)->trim()->lower()->toString();

        return match ($value) {
            'booking' => 'Booking',
            'whatsapp' => 'WhatsApp',
            'telegram', 'телеграм' => 'Telegram',
            default => 'Site',
        };
    }

    private function buildCreatedBookingData(Booking $booking): array
    {
        $booking->loadMissing('room');

        $payload = [
            'reservation_id' => $booking->booking_number,
            'booking_number' => $booking->booking_number,
            'secure_token' => (string) $booking->secure_token,
            'guest_name' => (string) $booking->guest_name,
            'room_code' => (string) ($booking->room?->code ?? ''),
            'room_label' => $booking->room ? ('№ '.$booking->room->code.' — '.$booking->room->title) : '',
            'guest_language' => $this->normalizeGuestLanguage($booking->guest?->language ?? 'ru'),
            'arrival_date' => $booking->arrival_date?->format('Y-m-d') ?? '',
            'payment_deadline' => $booking->payment_deadline?->format('Y-m-d') ?? '',
            'payment_deadline_display' => $booking->payment_deadline?->format('d.m.Y') ?? '',
            'payment_deadline_enabled' => ! is_null($booking->payment_deadline),
            'payment_deadline_days' => $booking->payment_deadline ? max(0, today()->diffInDays($booking->payment_deadline->copy()->startOfDay(), false)) : null,
            'nights' => (int) $booking->nights,
            'guests_count' => (int) $booking->adults,
            'breakfast' => (bool) $booking->breakfast,
            'discount_code' => $this->discountCodeForOutput($booking),
            'full_amount' => round((float) $booking->full_amount, 2),
            'prepayment_amount' => round((float) $booking->prepayment_amount, 2),
            'remaining_amount' => round((float) $booking->remaining_amount, 2),
        ];

        $payload['offer_message'] = $this->buildGuestOfferMessage($payload);
        $payload['payment_message'] = $this->buildPaymentMessage($payload);

        return $payload;
    }

    private function buildGuestOfferMessage(array $details): string
    {
        $roomLabel = $details['room_label'] ?: ('№ '.($details['room_code'] ?: '—'));
        $roomOfferLine = $this->roomOfferLine($roomLabel, (string) ($details['room_code'] ?? ''), (string) ($details['guest_language'] ?? 'ru'));
        $breakfast = ! empty($details['breakfast']) ? 'с завтраком' : 'без завтрака';
        $discountLine = ! empty($details['discount_code']) ? 'Код скидки До 25%: '.$details['discount_code'] : null;
        $arrivalDate = '';
        if (! empty($details['arrival_date'])) {
            try {
                $arrivalDate = Carbon::parse((string) $details['arrival_date'])->format('d.m.Y');
            } catch (\Throwable) {
                $arrivalDate = (string) $details['arrival_date'];
            }
        }

        return trim(implode("\n", array_values(array_filter([
            ! empty($details['guest_name']) ? ($details['guest_name'].', здравствуйте!') : 'Здравствуйте!',
            '',
            $roomOfferLine,
            'Дата заезда: '.($arrivalDate ?: '—'),
            'Количество ночей: '.((int) ($details['nights'] ?? 0)),
            'Количество гостей: '.((int) ($details['guests_count'] ?? 0)),
            'Питание: '.$breakfast,
            'Итого: '.number_format((float) ($details['full_amount'] ?? 0), 2, '.', ' ').' €',
            'Предоплата: '.number_format((float) ($details['prepayment_amount'] ?? 0), 2, '.', ' ').' €',
            $discountLine,
            '',
            $this->buildPrepaymentTimingLine((string) ($details['arrival_date'] ?? ''), (float) ($details['prepayment_amount'] ?? 0), $details['payment_deadline_days'] ?? null, $details['payment_deadline_display'] ?? null, $details['payment_deadline_enabled'] ?? true),
            'В случае отмены бронирования предоплата не возвращается. После оплаты предоплаты Вы получите сообщение о подтверждении бронирования.',
        ], fn ($line) => $line !== null))));
    }

    private function roomOfferLine(string $roomLabel, string $roomCode, string $language): string
    {
        $language = $this->normalizeGuestLanguage($language);
        $fullRoomLabel = $this->localizedRoomLabel($roomCode ?: $roomLabel, $language) ?: $roomLabel;

        return match ($language) {
            'it' => 'Vi proponiamo: '.$fullRoomLabel.'.',
            'en' => 'We offer you: '.$fullRoomLabel.'.',
            'uk' => 'Пропонуємо Вам: '.$fullRoomLabel.'.',
            'fr' => 'Nous vous proposons : '.$fullRoomLabel.'.',
            'de' => 'Wir bieten Ihnen an: '.$fullRoomLabel.'.',
            default => 'Предлагаем Вам: '.$fullRoomLabel.'.',
        };
    }

    private function localizedRoomLabel(string $roomCodeOrLabel, string $language): ?string
    {
        $code = $this->normalizeRoomCodeFromLabel($roomCodeOrLabel);
        if ($code === '') {
            return null;
        }

        $labels = [
            'it' => [
                '1А' => '№ 1A — camera doppia con vista giardino',
                '2А' => '№ 2A — camera doppia con vista mare',
                '3А' => '№ 3A — camera tripla con vista mare',
                '1' => '№ 1 — camera tripla con vista mare',
                '2' => '№ 2 — camera doppia con vista mare',
                '3' => '№ 3 — camera tripla con vista mare',
                '4' => '№ 4 — piccola camera doppia con vista giardino',
                '5' => '№ 5 — camera doppia con vista giardino',
                '6' => '№ 6 — camera doppia con vista mare',
                '7' => '№ 7 — camera doppia con vista mare',
                '8' => '№ 8 — camera doppia con vista mare',
                '9' => '№ 9 — camera doppia con vista mare',
                '10' => '№ 10 — camera tripla con vista mare',
                '11' => '№ 11 — piccola camera doppia con vista giardino',
            ],
            'en' => [
                '1А' => '№ 1A — double room with garden view',
                '2А' => '№ 2A — double room with sea view',
                '3А' => '№ 3A — triple room with sea view',
                '1' => '№ 1 — triple room with sea view',
                '2' => '№ 2 — double room with sea view',
                '3' => '№ 3 — triple room with sea view',
                '4' => '№ 4 — small double room with garden view',
                '5' => '№ 5 — double room with garden view',
                '6' => '№ 6 — double room with sea view',
                '7' => '№ 7 — double room with sea view',
                '8' => '№ 8 — double room with sea view',
                '9' => '№ 9 — double room with sea view',
                '10' => '№ 10 — triple room with sea view',
                '11' => '№ 11 — small double room with garden view',
            ],
            'ru' => [
                '1А' => '№ 1А — двухместный номер с видом на сад',
                '2А' => '№ 2А — двухместный номер с видом на море',
                '3А' => '№ 3А — трехместный номер с видом на море',
                '1' => '№ 1 — трехместный номер с видом на море',
                '2' => '№ 2 — двухместный номер с видом на море',
                '3' => '№ 3 — трехместный номер с видом на море',
                '4' => '№ 4 — небольшой двухместный номер с видом на сад',
                '5' => '№ 5 — двухместный номер с видом на сад',
                '6' => '№ 6 — двухместный номер с видом на море',
                '7' => '№ 7 — двухместный номер с видом на море',
                '8' => '№ 8 — двухместный номер с видом на море',
                '9' => '№ 9 — двухместный номер с видом на море',
                '10' => '№ 10 — трехместный номер с видом на море',
                '11' => '№ 11 — небольшой двухместный номер с видом на сад',
            ],
            'uk' => [
                '1А' => '№ 1А — двомісний номер з видом на сад',
                '2А' => '№ 2А — двомісний номер з видом на море',
                '3А' => '№ 3А — тримісний номер з видом на море',
                '1' => '№ 1 — тримісний номер з видом на море',
                '2' => '№ 2 — двомісний номер з видом на море',
                '3' => '№ 3 — тримісний номер з видом на море',
                '4' => '№ 4 — невеликий двомісний номер з видом на сад',
                '5' => '№ 5 — двомісний номер з видом на сад',
                '6' => '№ 6 — двомісний номер з видом на море',
                '7' => '№ 7 — двомісний номер з видом на море',
                '8' => '№ 8 — двомісний номер з видом на море',
                '9' => '№ 9 — двомісний номер з видом на море',
                '10' => '№ 10 — тримісний номер з видом на море',
                '11' => '№ 11 — невеликий двомісний номер з видом на сад',
            ],
            'fr' => [
                '1А' => '№ 1A — chambre double avec vue sur le jardin',
                '2А' => '№ 2A — chambre double avec vue sur la mer',
                '3А' => '№ 3A — chambre triple avec vue sur la mer',
                '1' => '№ 1 — chambre triple avec vue sur la mer',
                '2' => '№ 2 — chambre double avec vue sur la mer',
                '3' => '№ 3 — chambre triple avec vue sur la mer',
                '4' => '№ 4 — petite chambre double avec vue sur le jardin',
                '5' => '№ 5 — chambre double avec vue sur le jardin',
                '6' => '№ 6 — chambre double avec vue sur la mer',
                '7' => '№ 7 — chambre double avec vue sur la mer',
                '8' => '№ 8 — chambre double avec vue sur la mer',
                '9' => '№ 9 — chambre double avec vue sur la mer',
                '10' => '№ 10 — chambre triple avec vue sur la mer',
                '11' => '№ 11 — petite chambre double avec vue sur le jardin',
            ],
            'de' => [
                '1А' => '№ 1A — Doppelzimmer mit Gartenblick',
                '2А' => '№ 2A — Doppelzimmer mit Meerblick',
                '3А' => '№ 3A — Dreibettzimmer mit Meerblick',
                '1' => '№ 1 — Dreibettzimmer mit Meerblick',
                '2' => '№ 2 — Doppelzimmer mit Meerblick',
                '3' => '№ 3 — Dreibettzimmer mit Meerblick',
                '4' => '№ 4 — kleines Doppelzimmer mit Gartenblick',
                '5' => '№ 5 — Doppelzimmer mit Gartenblick',
                '6' => '№ 6 — Doppelzimmer mit Meerblick',
                '7' => '№ 7 — Doppelzimmer mit Meerblick',
                '8' => '№ 8 — Doppelzimmer mit Meerblick',
                '9' => '№ 9 — Doppelzimmer mit Meerblick',
                '10' => '№ 10 — Dreibettzimmer mit Meerblick',
                '11' => '№ 11 — kleines Doppelzimmer mit Gartenblick',
            ],
        ];

        return ($labels[$language] ?? $labels['ru'])[$code] ?? null;
    }

    private function normalizeRoomCodeFromLabel(string $roomCodeOrLabel): string
    {
        $text = str_replace(['A', 'a'], 'А', trim($roomCodeOrLabel));
        if (preg_match('/(?:№\s*)?([0-9]{1,2}\s*[Аа]?)/u', $text, $matches)) {
            return str_replace(' ', '', mb_strtoupper($matches[1]));
        }

        return str_replace(' ', '', mb_strtoupper($text));
    }

    private function roomCapacityKind(string $roomCodeOrLabel): ?string
    {
        $text = str_replace(['A', 'a'], 'А', trim($roomCodeOrLabel));
        if (preg_match('/(?:№\s*)?([0-9]{1,2}\s*[Аа]?)/u', $text, $matches)) {
            $code = str_replace(' ', '', mb_strtoupper($matches[1]));
        } else {
            $code = str_replace(' ', '', mb_strtoupper($text));
        }

        if (in_array($code, ['1А', '2А', '2', '4', '5', '6', '7', '8', '9', '11'], true)) {
            return 'double';
        }

        if (in_array($code, ['3А', '1', '3', '10'], true)) {
            return 'triple';
        }

        return null;
    }

    private function buildPaymentMessage(array $details): string
    {
        return trim(implode("\n", [
            'Реквизиты для оплаты предварительного бронирования',
            '',
            'Сумма предоплаты: '.number_format((float) ($details['prepayment_amount'] ?? 0), 2, '.', ' ').' €',
            'Получатель: PINETA SRL',
            'Банк: BANCO BPM',
            'Codice Iban: IT25T0503449251000000007146',
            'Codice SWIFT: BAPPIT21B7C',
        ]));
    }

    private function buildPrepaymentTimingLine(string $arrivalDate, float $prepaymentAmount, mixed $paymentDeadlineDays = null, ?string $paymentDeadlineDisplay = null, bool $paymentDeadlineEnabled = true): string
    {
        if (! $paymentDeadlineEnabled) {
            return '';
        }

        $prepayment = number_format($prepaymentAmount, 2, '.', ' ').' €';

        if ($paymentDeadlineDays !== null) {
            $days = max(1, (int) $paymentDeadlineDays);
            return 'Для бронирования Вы должны сделать предоплату '.$prepayment.' в течение 🔴 '.$days.' дн.';
        }

        try {
            $today = now()->startOfDay();
            $arrival = Carbon::parse($arrivalDate)->startOfDay();
            $diff = $today->diffInDays($arrival, false);
            if ($diff <= 1) {
                return 'Для бронирования Вы должны сделать предоплату '.$prepayment.' сегодня.';
            }
            if ($diff === 2) {
                return 'Для бронирования Вы должны сделать предоплату '.$prepayment.' сегодня или завтра.';
            }
        } catch (\Throwable) {
            // ignore parse errors, use fallback below
        }

        return 'Для бронирования Вы должны сделать предоплату '.$prepayment.' в течение 3 дней.';
    }


    private function occupancyConflicts(Carbon $arrivalDate, Carbon $departureDate, ?int $roomId = null): Builder
    {
        $query = Booking::query()
            ->whereIn('status', Booking::activeStatuses())
            ->whereRaw("TRIM(COALESCE(guest_name, '')) <> ''")
            ->whereDate('arrival_date', '<', $departureDate->toDateString())
            ->whereDate('departure_date', '>', $arrivalDate->toDateString());

        if ($roomId !== null) {
            $query->where('room_id', $roomId);
        }

        return $query;
    }

    private function bookingBlockConflicts(Carbon $arrivalDate, Carbon $departureDate, ?int $roomId = null): Builder
    {
        $query = BookingBlock::query()
            ->whereDate('block_date', '>=', $arrivalDate->toDateString())
            ->whereDate('block_date', '<', $departureDate->toDateString());

        if ($roomId !== null) {
            $query->where('room_id', $roomId);
        }

        return $query;
    }

    private function assertRoomAvailable(int $roomId, Carbon $arrivalDate, Carbon $departureDate, bool $ignoreBookingBlocks = false): void
    {
        $conflictExists = $this->occupancyConflicts($arrivalDate, $departureDate, $roomId)->exists();

        if ($conflictExists) {
            throw new \RuntimeException('Эта комната уже занята или временно удержана на выбранные даты.');
        }

        if (! $ignoreBookingBlocks && $this->bookingBlockConflicts($arrivalDate, $departureDate, $roomId)->exists()) {
            throw new \RuntimeException('Эта комната временно закрыта для продажи на Booking. Выберите другую комнату.');
        }
    }

    private function emptyStatusMapByRoomCode(): array
    {
        return Room::query()
            ->where('is_active', true)
            ->pluck('code')
            ->mapWithKeys(fn ($code) => [$this->normalizeRoomCode((string) $code) => 'free'])
            ->all();
    }

    private function roomIdToCodeMap(): array
    {
        return Room::query()
            ->where('is_active', true)
            ->get(['id', 'code'])
            ->mapWithKeys(fn (Room $room) => [$room->id => $this->normalizeRoomCode((string) $room->code)])
            ->all();
    }

    private function normalizeRoomCode(string $value): string
    {
        return str_replace(['A', 'a', ' '], ['А', 'а', ''], trim($value));
    }

}
