<?php

namespace App\Services;

use App\Models\Booking;
use App\Models\BookingBlock;
use App\Models\BookingRoomDay;
use Carbon\Carbon;
use Illuminate\Database\QueryException;
use Illuminate\Validation\ValidationException;

class BookingRoomDayService
{
    public function reserveBooking(Booking $booking): void
    {
        $arrival = $booking->arrival_date?->copy()->startOfDay();
        $departure = $booking->departure_date?->copy()->startOfDay();

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

        foreach ($this->stayDates($arrival, $departure) as $date) {
            $this->createRoomDayOrFail(
                (int) $booking->room_id,
                $date,
                [
                    'booking_id' => (int) $booking->id,
                    'booking_block_id' => null,
                    'source' => (string) ($booking->source ?: 'booking'),
                ],
                'Эта комната уже занята на выбранные даты. База не разрешила двойное бронирование.'
            );
        }
    }

    public function releaseBooking(Booking $booking): int
    {
        return BookingRoomDay::query()
            ->where('booking_id', $booking->id)
            ->delete();
    }

    public function reserveBlock(BookingBlock $block): void
    {
        $date = $block->block_date?->copy()->startOfDay();
        if (! $date) {
            return;
        }

        $this->createRoomDayOrFail(
            (int) $block->room_id,
            $date,
            [
                'booking_id' => null,
                'booking_block_id' => (int) $block->id,
                'source' => (string) ($block->source ?: 'booking_block'),
            ],
            'Эта комната уже занята на выбранную дату. База не разрешила двойную блокировку.'
        );
    }

    /** @param array<int> $blockIds */
    public function releaseBlocksByIds(array $blockIds): int
    {
        $blockIds = array_values(array_filter(array_map('intval', $blockIds)));
        if ($blockIds === []) {
            return 0;
        }

        return BookingRoomDay::query()
            ->whereIn('booking_block_id', $blockIds)
            ->delete();
    }

    /** @return array<int, Carbon> */
    private function stayDates(Carbon $arrival, Carbon $departure): array
    {
        $dates = [];
        $cursor = $arrival->copy();

        while ($cursor->lessThan($departure)) {
            $dates[] = $cursor->copy();
            $cursor->addDay();
        }

        return $dates;
    }

    /** @param array{booking_id:int|null,booking_block_id:int|null,source:string} $payload */
    private function createRoomDayOrFail(int $roomId, Carbon $date, array $payload, string $message): void
    {
        $existing = BookingRoomDay::query()
            ->where('room_id', $roomId)
            ->whereDate('date', $date->toDateString())
            ->lockForUpdate()
            ->first();

        if ($existing) {
            if (
                (int) ($existing->booking_id ?? 0) === (int) ($payload['booking_id'] ?? 0)
                && (int) ($existing->booking_block_id ?? 0) === (int) ($payload['booking_block_id'] ?? 0)
            ) {
                return;
            }

            throw ValidationException::withMessages([
                'room_id' => $message,
            ]);
        }

        try {
            BookingRoomDay::create([
                'room_id' => $roomId,
                'date' => $date->toDateString(),
                'booking_id' => $payload['booking_id'],
                'booking_block_id' => $payload['booking_block_id'],
                'source' => $payload['source'],
            ]);
        } catch (QueryException $e) {
            if (! $this->isUniqueConstraintError($e)) {
                throw $e;
            }

            throw ValidationException::withMessages([
                'room_id' => $message,
            ]);
        }
    }

    private function isUniqueConstraintError(QueryException $e): bool
    {
        $sqlState = (string) ($e->errorInfo[0] ?? '');
        $driverCode = (string) ($e->errorInfo[1] ?? '');

        return $sqlState === '23000' || $driverCode === '1062' || str_contains($e->getMessage(), 'booking_room_days_room_date_unique');
    }
}

