<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\RejectBookingPaymentRequest;
use App\Http\Requests\Admin\StoreBookingPaymentRequest;
use App\Http\Requests\Admin\StoreBookingRequest;
use App\Models\Booking;
use App\Models\BookingDocument;
use App\Models\BookingBlock;
use App\Models\BookingDeadlineRule;
use App\Models\BookingPayment;
use App\Models\BookingStatusLog;
use App\Models\DailyRoomRate;
use App\Models\Room;
use App\Services\BookingPricingService;
use App\Services\BookingBlockGoogleService;
use App\Services\BookingRoomDayService;
use App\Services\GoogleSyncService;
use App\Services\GuestGoogleBridgeService;
use App\Services\NotificationService;
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\Facades\Storage;
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
    {
        $bookings = Booking::query()
            ->with(['room', 'guest'])
            ->when($request->filled('status'), fn (Builder $query) => $query->where('status', $request->string('status')))
            ->when($request->filled('source'), fn (Builder $query) => $query->where('source', $request->string('source')))
            ->when($request->filled('room_id'), fn (Builder $query) => $query->where('room_id', (int) $request->integer('room_id')))
            ->when($request->filled('arrival_date'), fn (Builder $query) => $query->whereDate('arrival_date', $request->date('arrival_date')))
            ->when($request->filled('search'), function (Builder $query) use ($request): void {
                $search = trim((string) $request->string('search'));

                $query->where(function (Builder $builder) use ($search): void {
                    $builder->where('booking_number', 'like', "%{$search}%")
                        ->orWhere('guest_name', 'like', "%{$search}%")
                        ->orWhere('phone', 'like', "%{$search}%")
                        ->orWhere('email', 'like', "%{$search}%")
                        ->orWhere('rate_name', 'like', "%{$search}%");
                });
            })
            ->latest()
            ->paginate(12)
            ->withQueryString();

        return view('admin.bookings.index', [
            'bookings' => $bookings,
            'statuses' => Booking::statusOptions(),
            'sources' => Booking::sourceOptions(),
            'rooms' => Room::query()->where('is_active', true)->orderBy('sort_order')->get(),
            'filters' => [
                'status' => (string) $request->string('status'),
                'source' => (string) $request->string('source'),
                'room_id' => (string) $request->string('room_id'),
                'arrival_date' => (string) $request->string('arrival_date'),
                'search' => (string) $request->string('search'),
            ],
        ]);
    }

    public function create(Request $request): View
    {
        return view('admin.bookings.create', [
            'rooms' => Room::query()->where('is_active', true)->orderBy('sort_order')->get(),
            'statuses' => Booking::statusOptions(),
            'sources' => Booking::sourceOptions(),
            'quoteUrl' => route('admin.bookings.quote'),
            'availabilityUrl' => route('admin.bookings.availability'),
            'defaults' => [
                'arrival_date' => $request->string('arrival_date')->toString() ?: now()->format('Y-m-d'),
                'departure_date' => $request->string('departure_date')->toString() ?: now()->addDay()->format('Y-m-d'),
                'adults' => 2,
                'children_under_2' => 0,
                'status' => 'pending_payment',
                'source' => 'admin',
                'check_in_time_from' => '12:00',
                'check_in_time_to' => '23:00',
                'prepayment_amount' => '0.00',
                'full_amount' => '0.00',
                'manual_price_override' => false,
                'extended_date_range' => false,
            ],
        ]);
    }

    public function availability(Request $request, GuestGoogleBridgeService $bridgeService): JsonResponse
    {
        $validated = $request->validate([
            'arrival_date' => ['required', 'date', function (string $attribute, mixed $value, \Closure $fail): void {
                $minDate = request()->boolean('extended_date_range') ? Carbon::create(2026, 4, 1)->startOfDay() : today();
                if (Carbon::parse($value)->startOfDay()->lessThan($minDate)) {
                    $fail('Дата заезда не может быть раньше '.$minDate->format('d.m.Y').'.');
                }
            }],
            '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'],
            'extended_date_range' => ['nullable', 'boolean'],
        ]);

        $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;
        $allowBookingBlockOverride = $this->normalizeSourceLabel($validated['source'] ?? 'admin') === 'Booking';

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

        $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';
            }
        }

        if (! $allowBookingBlockOverride) {
            $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 quote(Request $request, BookingPricingService $pricingService, GuestGoogleBridgeService $bridgeService): JsonResponse
    {
        $validated = $request->validate([
            'room_id' => ['required', 'integer', 'exists:rooms,id'],
            'arrival_date' => ['required', 'date', function (string $attribute, mixed $value, \Closure $fail): void {
                $minDate = request()->boolean('extended_date_range') ? Carbon::create(2026, 4, 1)->startOfDay() : today();
                if (Carbon::parse($value)->startOfDay()->lessThan($minDate)) {
                    $fail('Дата заезда не может быть раньше '.$minDate->format('d.m.Y').'.');
                }
            }],
            'departure_date' => ['required', 'date', 'after:arrival_date'],
            'breakfast' => ['nullable', 'boolean'],
            'is_discount' => ['nullable', 'boolean'],
            'source' => ['nullable', 'string', 'max:50'],
            'extended_date_range' => ['nullable', 'boolean'],
        ]);

        $source = $this->normalizeSourceLabel($validated['source'] ?? 'admin');
        $discountForQuote = (bool) ($validated['is_discount'] ?? false) && $source !== 'Booking';

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

        $deadline = $this->resolveAdminPaymentDeadline('pending_payment', (int) $validated['room_id'], Carbon::parse($validated['arrival_date'])->startOfDay(), $validated);
        $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()),
        ]);
    }

    public function store(StoreBookingRequest $request, BookingPricingService $pricingService, GuestGoogleBridgeService $bridgeService, GoogleSyncService $googleSyncService, BookingBlockGoogleService $bookingBlockGoogleService, BookingRoomDayService $bookingRoomDayService, NotificationService $notificationService): RedirectResponse|JsonResponse
    {
        $validated = $request->validated();
        if (($validated['source'] ?? '') === 'draft' || ($validated['status'] ?? '') === 'draft') {
            if ($request->expectsJson()) {
                return response()->json([
                    'ok' => false,
                    'message' => 'Черновик не создаёт бронирование. Выберите другой источник или статус для сохранения.',
                ], 422);
            }

            return back()->withInput()->withErrors([
                'source' => 'Черновик не создаёт бронирование. Выберите другой источник или статус для сохранения.',
            ]);
        }

        $arrivalDate = Carbon::parse($validated['arrival_date'])->startOfDay();
        $departureDate = Carbon::parse($validated['departure_date'])->startOfDay();
        $nights = (int) $arrivalDate->diffInDays($departureDate);
        $breakfast = (bool) ($validated['breakfast'] ?? false);
        $manualOverride = (bool) ($validated['manual_price_override'] ?? false);
        $isBookingSource = ($validated['source'] ?? '') === 'booking';
        $discountApplied = (bool) ($validated['is_discount'] ?? false) && ! $isBookingSource;
        $discountCode = $this->resolveAdminDiscountCode($validated, $discountApplied);

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

        $fullAmount = $manualOverride
            ? round((float) $validated['full_amount'], 2)
            : round((float) $quote['full_amount'], 2);

        $prepaymentAmount = $isBookingSource
            ? $fullAmount
            : round((float) ($validated['prepayment_amount'] ?? 0), 2);
        $remainingAmount = $isBookingSource ? 0 : max(0, round($fullAmount - $prepaymentAmount, 2));
        $nightlyAmount = $manualOverride
            ? ($nights > 0 ? round($fullAmount / $nights, 2) : $fullAmount)
            : round((float) $quote['nightly_amount'], 2);
        $rateName = $manualOverride ? 'Ручная цена администратора' : $quote['rate_name'];
        $pricingMode = $manualOverride ? 'manual' : 'calendar';
        $rateSnapshot = $manualOverride ? [
            'manual_full_amount' => $fullAmount,
            'calendar_quote' => $quote,
        ] : $quote['snapshot'];

        $booking = DB::transaction(function () use ($validated, $arrivalDate, $departureDate, $nights, $fullAmount, $prepaymentAmount, $remainingAmount, $nightlyAmount, $rateName, $pricingMode, $manualOverride, $breakfast, $quote, $rateSnapshot, $isBookingSource, $discountApplied, $discountCode, $bookingRoomDayService): Booking {
            $conflict = $this->occupancyConflicts($arrivalDate, $departureDate, (int) $validated['room_id'])
                ->with('room')
                ->lockForUpdate()
                ->first();

            if ($conflict !== null) {
                $roomTitle = $conflict->room?->code ? '№ '.$conflict->room->code : 'эта комната';

                throw ValidationException::withMessages([
                    'room_id' => sprintf(
                        '%s уже занята на пересекающиеся даты: %s — %s.',
                        $roomTitle,
                        $conflict->arrival_date?->format('d.m.Y'),
                        $conflict->departure_date?->format('d.m.Y')
                    ),
                ]);
            }

            if (! $isBookingSource && $this->bookingBlockConflicts($arrivalDate, $departureDate, (int) $validated['room_id'])->lockForUpdate()->exists()) {
                throw ValidationException::withMessages([
                    'room_id' => 'Эта комната закрыта для Booking на выбранные даты. Разблокируйте её или выберите источник Booking.',
                ]);
            }

            if ($isBookingSource) {
                $this->deleteBookingBlocksForRange((int) $validated['room_id'], $arrivalDate, $departureDate, $bookingRoomDayService);
            }

            $status = $isBookingSource ? 'confirmed' : $validated['status'];
            $paymentDeadline = $this->resolveAdminPaymentDeadline($status, (int) $validated['room_id'], $arrivalDate, $validated);
            $releaseOnExpire = false;

            $booking = Booking::create([
                'booking_number' => $this->generateBookingNumber(),
                'created_by_user_id' => Auth::id(),
                'room_id' => $validated['room_id'],
                'guest_name' => trim($validated['guest_name']),
                'phone' => $validated['phone'] ?: null,
                'email' => $validated['email'] ?: null,
                'source' => $validated['source'],
                'booking_variant' => $discountApplied ? 'discount' : 'admin',
                'rate_name' => $rateName,
                'status' => $status,
                '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' => $discountApplied,
                'discount_code' => $discountCode,
                'pricing_mode' => $pricingMode,
                'manual_price_override' => $manualOverride,
                'rate_snapshot_json' => $rateSnapshot,
                'price_calculated_at' => now(),
                'nightly_amount' => $nightlyAmount,
                'full_amount' => $fullAmount,
                'prepayment_amount' => $prepaymentAmount,
                'remaining_amount' => $remainingAmount,
                'payment_deadline' => $paymentDeadline,
                'release_on_expire' => $releaseOnExpire,
                'comment_admin' => $validated['comment_admin'] ?: null,
                'booking_link' => $validated['booking_link'] ?: null,
                'check_in_time_from' => $validated['check_in_time_from'] ?: null,
                'check_in_time_to' => $validated['check_in_time_to'] ?: null,
                'confirmed_at' => $status === 'confirmed' ? now() : null,
                'receipt_received' => $isBookingSource,
                'prepayment_received' => $isBookingSource,
            ]);

            $bookingRoomDayService->reserveBooking($booking);

            BookingStatusLog::create([
                'booking_id' => $booking->id,
                'old_status' => null,
                'new_status' => $booking->status,
                'changed_by_user_id' => Auth::id(),
                'note' => 'Бронь создана администратором. Тариф: '.$rateName.'.',
            ]);

            return $booking;
        });

        $booking->load('room');
        $this->ensureBookingPrivateFolder($booking);
        if ($isBookingSource) {
            $this->syncBookingBlockRemovalForBooking($booking, $bookingBlockGoogleService);
        }
        $googleSyncService->syncBooking($booking, 'booking_created_admin');
        $notificationService->sendAdminNewBooking($booking);
        if ($booking->email) {
            $notificationService->sendGuestBookingCreated($booking);
        }

        if ($request->expectsJson()) {
            return response()->json([
                'ok' => true,
                'message' => 'Предварительное бронирование создано.',
                'offer_message' => $this->buildOfferMessagePayload($booking),
                'booking' => [
                    'id' => $booking->id,
                    'booking_number' => $booking->booking_number,
                    'status' => $booking->status,
                    'rate_name' => $booking->rate_name,
                    'full_amount' => (float) $booking->full_amount,
                    'prepayment_amount' => (float) $booking->prepayment_amount,
                    'remaining_amount' => (float) $booking->remaining_amount,
                    'arrival_date' => optional($booking->arrival_date)->format('Y-m-d'),
                    'nights' => $booking->nights,
                    'room_code' => $booking->room?->code,
                    'room_label' => $booking->room ? ('№ '.$booking->room->code.' — '.$booking->room->title) : null,
                    'min_nights_applied' => $booking->min_nights_applied,
                ],
                'created_url' => route('admin.bookings.created', $booking),
            ]);
        }

        return redirect()
            ->route('admin.bookings.created', $booking)
            ->with('success', 'Бронь успешно создана.');
    }

    public function created(Booking $booking): View
    {
        $booking->load('room');

        return view('bookings.created', [
            'booking' => $booking,
            'createdBookingData' => $this->buildCreatedBookingData($booking),
            'isAdmin' => true,
            'receiptUploadUrl' => route('guest.bookings.receipt.public'),
            'backUrl' => route('admin.bookings.show', $booking),
            'backLabel' => 'Открыть карточку бронирования',
        ]);
    }

    public function show(Booking $booking): View
    {
        $booking->load([
            'room',
            'guest',
            'createdBy',
            'payments',
            'documents',
            'statusLogs.changedBy',
            'syncLogs',
        ]);

        return view('admin.bookings.show', [
            'booking' => $booking,
            'bookingFolderDisplayPath' => $this->bookingPrivateFolderDisplayPath($booking),
            'bookingFolderFiles' => $this->bookingFolderFiles($booking),
        ]);
    }

    public function cancel(Booking $booking, GoogleSyncService $googleSyncService, BookingRoomDayService $bookingRoomDayService, NotificationService $notificationService): RedirectResponse
    {
        if ($booking->status === 'cancelled') {
            return back()->with('success', 'Бронь уже была отменена ранее.');
        }

        DB::transaction(function () use ($booking, $bookingRoomDayService): void {
            $oldStatus = $booking->status;

            $booking->update([
                'status' => 'cancelled',
                'payment_deadline' => null,
                'confirmed_at' => null,
            ]);

            $bookingRoomDayService->releaseBooking($booking);

            BookingStatusLog::create([
                'booking_id' => $booking->id,
                'old_status' => $oldStatus,
                'new_status' => 'cancelled',
                'changed_by_user_id' => Auth::id(),
                'note' => 'Бронь отменена из админки.',
            ]);
        });

        $booking->refresh()->load('room');
        $googleSyncService->syncBooking($booking, 'booking_cancelled');
        if ($booking->email) {
            $notificationService->sendGuestPaymentRejected($booking, 'Бронь отменена администратором.');
        }

        return redirect()
            ->route('admin.bookings.show', $booking)
            ->with('success', 'Бронь переведена в статус «Отменена».');
    }

    public function storePayment(StoreBookingPaymentRequest $request, Booking $booking, GoogleSyncService $googleSyncService, NotificationService $notificationService): RedirectResponse
    {
        $validated = $request->validated();
        if (($validated['status'] ?? '') === 'draft') {
            if ($request->expectsJson()) {
                return response()->json([
                    'ok' => false,
                    'message' => 'Черновик не создаёт бронирование. Выберите другой источник или статус для сохранения.',
                ], 422);
            }

            return back()->withInput()->withErrors([
                'source' => 'Черновик не создаёт бронирование. Выберите другой источник или статус для сохранения.',
            ]);
        }

        DB::transaction(function () use ($validated, $booking): void {
            $payment = BookingPayment::create([
                'booking_id' => $booking->id,
                'type' => $validated['type'],
                'amount' => round((float) $validated['amount'], 2),
                'status' => 'confirmed',
                'channel' => $validated['channel'] ?: 'manual',
                'note' => $validated['note'] ?: 'Платёж подтверждён администратором.',
                'paid_at' => now(),
            ]);

            $oldStatus = $booking->status;
            $amount = (float) $payment->amount;

            if ($payment->type === 'refund') {
                $booking->update([
                    'prepayment_amount' => max(0, round((float) $booking->prepayment_amount - $amount, 2)),
                    'remaining_amount' => min((float) $booking->full_amount, round((float) $booking->remaining_amount + $amount, 2)),
                    'status' => 'pending_payment',
                    'confirmed_at' => null,
                ]);

                $newStatus = 'awaiting_prepayment';
                $note = 'Администратор зафиксировал возврат.';
            } else {
                $newPrepayment = $payment->type === 'full_payment'
                    ? (float) $booking->full_amount
                    : max((float) $booking->prepayment_amount, $amount);

                $remaining = max(0, round((float) $booking->full_amount - $newPrepayment, 2));

                $booking->update([
                    'prepayment_amount' => round($newPrepayment, 2),
                    'remaining_amount' => $remaining,
                    'status' => 'confirmed',
                    'confirmed_at' => now(),
                    'payment_deadline' => null,
                    'receipt_received' => true,
                    'prepayment_received' => true,
                ]);

                $newStatus = 'confirmed';
                $note = $payment->type === 'full_payment'
                    ? 'Администратор подтвердил полную оплату.'
                    : 'Администратор подтвердил предоплату.';
            }

            BookingStatusLog::create([
                'booking_id' => $booking->id,
                'old_status' => $oldStatus,
                'new_status' => $newStatus,
                'changed_by_user_id' => Auth::id(),
                'note' => trim(($validated['note'] ?: $note).' Сумма: '.number_format($amount, 2, '.', ' ').' €'),
            ]);
        });

        $booking->refresh()->load('room');
        $googleSyncService->syncBooking($booking, 'payment_confirmed');
        if ($booking->email) {
            $notificationService->sendGuestPaymentConfirmed($booking);
        }

        return back()->with('success', 'Платёж сохранён и статус брони обновлён.');
    }

    public function rejectPayment(RejectBookingPaymentRequest $request, Booking $booking, GoogleSyncService $googleSyncService, NotificationService $notificationService): RedirectResponse
    {
        DB::transaction(function () use ($request, $booking): void {
            BookingPayment::create([
                'booking_id' => $booking->id,
                'type' => 'prepayment',
                'amount' => max(0.01, (float) $booking->prepayment_amount ?: 0.01),
                'status' => 'rejected',
                'channel' => 'receipt_review',
                'note' => $request->string('note')->toString(),
                'paid_at' => now(),
            ]);

            $oldStatus = $booking->status;

            $booking->update([
                'status' => 'payment_rejected',
                'confirmed_at' => null,
                'prepayment_received' => false,
            ]);

            BookingStatusLog::create([
                'booking_id' => $booking->id,
                'old_status' => $oldStatus,
                'new_status' => 'payment_rejected',
                'changed_by_user_id' => Auth::id(),
                'note' => 'Платёж отклонён: '.$request->string('note')->toString(),
            ]);
        });

        $booking->refresh()->load('room');
        $googleSyncService->syncBooking($booking, 'payment_rejected');
        if ($booking->email) {
            $notificationService->sendGuestPaymentRejected($booking, $request->string('note')->toString());
        }

        return back()->with('success', 'Платёж отклонён, гость может загрузить квитанцию повторно.');
    }


    public function updateFlags(Request $request, Booking $booking, NotificationService $notificationService): RedirectResponse
    {
        $data = $request->validate([
            'receipt_received' => ['nullable', 'boolean'],
            'prepayment_received' => ['nullable', 'boolean'],
        ]);

        $receiptReceived = (bool) ($data['receipt_received'] ?? false);
        $prepaymentReceived = (bool) ($data['prepayment_received'] ?? false);
        $becameConfirmed = false;

        DB::transaction(function () use ($booking, $receiptReceived, $prepaymentReceived, &$becameConfirmed): void {
            $oldStatus = (string) $booking->status;
            $updates = [
                'receipt_received' => $receiptReceived,
                'prepayment_received' => $prepaymentReceived,
            ];

            if ($receiptReceived && $prepaymentReceived && $oldStatus !== 'confirmed') {
                $updates['status'] = 'confirmed';
                $updates['confirmed_at'] = now();
                $updates['payment_deadline'] = null;
                $becameConfirmed = true;
            }

            $booking->update($updates);

            if ($becameConfirmed) {
                BookingStatusLog::create([
                    'booking_id' => $booking->id,
                    'old_status' => $oldStatus,
                    'new_status' => 'confirmed',
                    'changed_by_user_id' => Auth::id(),
                    'note' => 'Администратор подтвердил квитанцию и поступление предоплаты.',
                ]);
            }
        });

        if ($becameConfirmed) {
            $booking->refresh()->load(['guest', 'room']);
            $notificationService->sendGuestPaymentConfirmed($booking);

            return back()->with('success', 'Отметки сохранены. Бронь подтверждена, подтверждение отправлено на email гостя или записано в лог.');
        }

        return back()->with('success', 'Отметки по квитанции и предоплате сохранены.');
    }

    public function updateGuestRating(Request $request, Booking $booking): RedirectResponse
    {
        $data = $request->validate([
            'guest_rating' => ['nullable', 'integer', 'min:1', 'max:10'],
        ]);

        $rating = $data['guest_rating'] ?? null;

        DB::transaction(function () use ($booking, $rating): void {
            $booking->update(['guest_rating' => $rating]);

            if ($booking->guest) {
                $booking->guest->update(['admin_rating' => $rating]);
            }
        });

        return back()->with('success', 'Оценка гостя сохранена.');
    }

    public function destroy(Booking $booking, BookingRoomDayService $bookingRoomDayService): RedirectResponse
    {
        $booking->loadMissing(['documents', 'payments', 'statusLogs', 'syncLogs']);

        DB::transaction(function () use ($booking, $bookingRoomDayService): void {
            $bookingRoomDayService->releaseBooking($booking);

            foreach ($booking->documents as $document) {
                if ($document->file_path) {
                    $this->deleteStoredDocument($document);
                }
            }

            $booking->payments()->delete();
            $booking->documents()->delete();
            $booking->statusLogs()->delete();
            $booking->syncLogs()->delete();
            $booking->delete();
        });

        return redirect()->route('admin.bookings.index')->with('success', 'Запись бронирования удалена из базы.');
    }

    public function retrySync(Booking $booking, GoogleSyncService $googleSyncService): RedirectResponse
    {
        $booking->load('room');
        $googleSyncService->retryLatest($booking);

        return back()->with('success', 'Повторная синхронизация записана в лог.');
    }

    public function downloadDocument(Booking $booking, BookingDocument $document): BinaryFileResponse|StreamedResponse
    {
        abort_unless((int) $document->booking_id === (int) $booking->id, 404);

        return $this->downloadStoredDocument($document);
    }

    public function viewDocument(Booking $booking, BookingDocument $document): BinaryFileResponse
    {
        abort_unless((int) $document->booking_id === (int) $booking->id, 404);

        return $this->viewStoredDocument($document);
    }

    public function viewFolderFile(Booking $booking, string $filename): BinaryFileResponse
    {
        $path = $this->resolveBookingFolderFilePath($booking, $filename);
        $absolute = Storage::disk('local')->path($path);

        abort_unless(is_file($absolute), 404);

        $mime = Storage::disk('local')->mimeType($path) ?: 'application/octet-stream';
        $safeFilename = str_replace('"', '', basename($absolute));

        return response()->file($absolute, [
            'Content-Type' => $mime,
            'Content-Disposition' => 'inline; filename="'.$safeFilename.'"',
        ]);
    }

    public function downloadFolderFile(Booking $booking, string $filename): BinaryFileResponse|StreamedResponse
    {
        $path = $this->resolveBookingFolderFilePath($booking, $filename);

        return Storage::disk('local')->download($path, basename($path));
    }

    public function uploadDocument(Request $request, Booking $booking): RedirectResponse
    {
        $validated = $request->validate([
            'document_type' => ['required', 'string', 'in:receipt,guest_document,other'],
            'document' => ['required', 'file', 'mimes:jpg,jpeg,png,pdf,webp,txt', 'max:10240'],
        ]);

        $file = $request->file('document');
        $type = (string) $validated['document_type'];
        $path = $this->storeDocumentInPrivateBookingFolder($booking, $file, $type);

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

            if ($type === 'receipt' && ! $booking->receipt_received) {
                $oldStatus = (string) $booking->status;
                $newStatus = $oldStatus === 'confirmed' ? 'confirmed' : 'payment_uploaded';
                $booking->update([
                    'receipt_received' => true,
                    'status' => $newStatus,
                ]);

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

        return back()->with('success', 'Документ загружен в приватную папку брони.');
    }

    private function bookingPrivateFolderPath(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');

        return 'private/bookings/'.$arrival.'/'.$roomCode;
    }

    private function bookingPrivateFolderDisplayPath(Booking $booking): string
    {
        return 'storage/app/'.$this->bookingPrivateFolderPath($booking);
    }

    private function ensureBookingPrivateFolder(Booking $booking): string
    {
        $folder = $this->bookingPrivateFolderPath($booking);

        Storage::disk('local')->makeDirectory($folder);

        return $folder;
    }

    private function bookingFolderFiles(Booking $booking): array
    {
        $folder = $this->ensureBookingPrivateFolder($booking);
        $knownDocuments = $booking->documents->keyBy('file_path');
        $files = Storage::disk('local')->files($folder);

        return collect($files)
            ->filter(fn (string $path): bool => basename($path) !== '.gitignore')
            ->sortBy(fn (string $path): string => basename($path))
            ->map(function (string $path) use ($knownDocuments): array {
                $document = $knownDocuments->get($path);
                $filename = basename($path);

                return [
                    'filename' => $filename,
                    'original_name' => $document?->original_name ?: $filename,
                    'type' => $document?->type ?: $this->guessDocumentTypeFromFilename($filename),
                    'mime_type' => $document?->mime_type ?: $this->mimeTypeForStoredPath($path),
                    'size_label' => $this->fileSizeLabel($path),
                    'modified_label' => $this->fileModifiedLabel($path),
                    'is_registered' => (bool) $document,
                ];
            })
            ->values()
            ->all();
    }

    private function guessDocumentTypeFromFilename(string $filename): string
    {
        $name = mb_strtolower($filename);

        if (str_contains($name, 'receipt') || str_contains($name, 'kvit') || str_contains($name, 'payment')) {
            return 'receipt';
        }

        if (str_contains($name, 'document') || str_contains($name, 'passport') || str_contains($name, 'id') || str_contains($name, 'scan')) {
            return 'guest_document';
        }

        if (str_ends_with($name, '.txt')) {
            return 'recognition_text';
        }

        return 'other';
    }

    private function mimeTypeForStoredPath(string $path): string
    {
        $absolute = storage_path('app/'.$path);

        return file_exists($absolute) ? (mime_content_type($absolute) ?: 'application/octet-stream') : 'application/octet-stream';
    }

    private function fileSizeLabel(string $path): string
    {
        try {
            $bytes = (int) Storage::disk('local')->size($path);
        } catch (\Throwable) {
            return 'размер неизвестен';
        }

        if ($bytes < 1024) {
            return $bytes.' Б';
        }

        if ($bytes < 1024 * 1024) {
            return number_format($bytes / 1024, 1, '.', ' ').' КБ';
        }

        return number_format($bytes / 1024 / 1024, 1, '.', ' ').' МБ';
    }

    private function fileModifiedLabel(string $path): string
    {
        try {
            $timestamp = (int) Storage::disk('local')->lastModified($path);
        } catch (\Throwable) {
            return 'дата неизвестна';
        }

        return Carbon::createFromTimestamp($timestamp)->format('d.m.Y H:i');
    }

    private function resolveBookingFolderFilePath(Booking $booking, string $filename): string
    {
        $filename = basename(rawurldecode($filename));
        abort_if($filename === '' || $filename === '.' || $filename === '..', 404);

        $folder = $this->ensureBookingPrivateFolder($booking);
        $path = $folder.'/'.$filename;

        abort_unless(Storage::disk('local')->exists($path), 404);

        return $path;
    }

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

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

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

    private function downloadStoredDocument(BookingDocument $document): BinaryFileResponse|StreamedResponse
    {
        $path = (string) $document->file_path;
        $publicPath = public_path($path);

        if ($path !== '' && file_exists($publicPath)) {
            return response()->download($publicPath, $document->original_name);
        }

        $legacyPath = storage_path('app/'.$path);
        if ($path !== '' && file_exists($legacyPath)) {
            return response()->download($legacyPath, $document->original_name);
        }

        return \Illuminate\Support\Facades\Storage::disk('local')->download($path, $document->original_name);
    }

    private function viewStoredDocument(BookingDocument $document): BinaryFileResponse
    {
        $path = (string) $document->file_path;
        $absolute = null;

        if ($path !== '' && file_exists(public_path($path))) {
            $absolute = public_path($path);
        } elseif ($path !== '' && file_exists(storage_path('app/private/'.$path))) {
            $absolute = storage_path('app/private/'.$path);
        } elseif ($path !== '' && file_exists(storage_path('app/'.$path))) {
            $absolute = storage_path('app/'.$path);
        }

        abort_if(! $absolute, 404);

        $filename = str_replace('"', '', $document->original_name ?: basename($absolute));
        $mime = $document->mime_type ?: (mime_content_type($absolute) ?: 'application/octet-stream');

        return response()->file($absolute, [
            'Content-Type' => $mime,
            'Content-Disposition' => 'inline; filename="'.$filename.'"',
        ]);
    }

    private function deleteStoredDocument(BookingDocument $document): void
    {
        $path = (string) $document->file_path;
        $publicPath = public_path($path);

        if ($path !== '' && file_exists($publicPath)) {
            @unlink($publicPath);
            return;
        }

        if ($path !== '' && file_exists(storage_path('app/'.$path))) {
            @unlink(storage_path('app/'.$path));
            return;
        }

        \Illuminate\Support\Facades\Storage::disk('local')->delete($path);
    }

    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 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',
            'site' => 'Site',
            'draft', 'черновик' => 'Draft',
            default => 'Admin',
        };
    }


    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 deleteBookingBlocksForRange(int $roomId, Carbon $arrivalDate, Carbon $departureDate, BookingRoomDayService $bookingRoomDayService): int
    {
        $blockIds = $this->bookingBlockConflicts($arrivalDate, $departureDate, $roomId)
            ->where('source', 'booking')
            ->pluck('id')
            ->map(fn ($id) => (int) $id)
            ->all();

        $bookingRoomDayService->releaseBlocksByIds($blockIds);

        return BookingBlock::query()
            ->whereIn('id', $blockIds)
            ->delete();
    }

    private function syncBookingBlockRemovalForBooking(Booking $booking, BookingBlockGoogleService $googleService): void
    {
        $booking->loadMissing('room');
        $roomCode = (string) ($booking->room?->code ?? '');
        $arrival = $booking->arrival_date?->copy()->startOfDay();
        $departure = $booking->departure_date?->copy()->startOfDay();

        if ($roomCode === '' || ! $arrival || ! $departure || $departure->lessThanOrEqualTo($arrival)) {
            return;
        }

        $lastBlockedDate = $departure->copy()->subDay();
        $googleService->unblock([$roomCode], $arrival->toDateString(), $lastBlockedDate->toDateString(), 'booking_source_created');
    }

    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));
    }

    /** @param array<string, mixed> $validated */
    private function resolveAdminPaymentDeadline(string $status, int $roomId, Carbon $arrivalDate, array $validated): ?Carbon
    {
        if (! in_array($status, ['pending_payment', 'awaiting_prepayment'], true)) {
            return null;
        }

        if (! empty($validated['extended_date_range'])) {
            return null;
        }

        $rule = BookingDeadlineRule::activeRuleFor($roomId, $arrivalDate);
        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 resolveAdminDiscountCode(array $validated, bool $discountApplied): ?string
    {
        if (! $discountApplied) {
            return null;
        }

        $code = trim((string) ($validated['discount_code'] ?? ''));
        if ($code !== '') {
            return mb_substr($code, 0, 50);
        }

        return 'PINETA25-'.Str::upper(Str::random(5));
    }

    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 buildCreatedBookingData(Booking $booking): array
    {
        $booking->loadMissing('room');

        $payload = [
            'reservation_id' => $booking->booking_number,
            'booking_number' => $booking->booking_number,
            'guest_name' => (string) $booking->guest_name,
            'room_code' => (string) ($booking->room?->code ?? ''),
            'room_label' => $booking->room ? ('№ '.$booking->room->code.' — '.$booking->room->title) : '',
            '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),
            'nights' => (int) $booking->nights,
            'guests_count' => (int) $booking->adults,
            'children_under_2' => (int) $booking->children_under_2,
            'breakfast' => (bool) $booking->breakfast,
            'discount_code' => $this->discountCodeForOutput($booking),
            'source' => (string) $booking->source,
            'full_amount' => round((float) $booking->full_amount, 2),
            'prepayment_amount' => round((float) $booking->prepayment_amount, 2),
            'remaining_amount' => round((float) $booking->remaining_amount, 2),
            'phone' => (string) ($booking->phone ?? ''),
            'confirmed_at' => $booking->confirmed_at?->format('Y-m-d') ?? '',
        ];

        $payload['offer_message'] = $this->buildOfferMessagePayload($booking);
        $payload['payment_message'] = $this->buildPaymentMessagePayload($payload);

        return $payload;
    }

    private function buildPaymentMessagePayload(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 buildOfferMessagePayload(Booking $booking): string
    {
        $roomLabel = $booking->room ? ('№ '.$booking->room->code.' — '.$booking->room->title) : '—';
        $roomOfferLine = $this->roomOfferLineRu($booking, $roomLabel);
        $breakfast = $booking->breakfast ? 'с завтраком' : 'без завтрака';
        $discountLine = $this->discountCodeForOutput($booking)
            ? 'Код скидки До 25%: '.$this->discountCodeForOutput($booking)
            : null;

        return trim(implode("\n", array_values(array_filter([
            $booking->guest_name ? ($booking->guest_name.', здравствуйте!') : 'Здравствуйте!',
            '',
            $roomOfferLine,
            'Дата заезда: '.optional($booking->arrival_date)->format('d.m.Y'),
            'Количество ночей: '.$booking->nights,
            'Количество гостей: '.$booking->adults,
            'Питание: '.$breakfast,
            'Итого: '.number_format((float) $booking->full_amount, 2, '.', ' ').' €',
            'Предоплата: '.number_format((float) $booking->prepayment_amount, 2, '.', ' ').' €',
            $discountLine,
        ], fn ($line) => $line !== null && $line !== ''))));
    }

    private function roomOfferLineRu(Booking $booking, string $roomLabel): string
    {
        return 'Предлагаем Вам: '.$roomLabel.'.';
    }

    private function roomCapacityKind(?string $roomCode): ?string
    {
        $code = str_replace(['A', 'a'], 'А', trim((string) $roomCode));

        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;
    }

}
