<?php

namespace App;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;
use Mtc\MercuryDataModels\Booking;
use Mtc\MercuryDataModels\BookingAvailability;
use Mtc\MercuryDataModels\BookingDeliveryOption;
use Mtc\MercuryDataModels\BookingStatus;
use Mtc\MercuryDataModels\Dealership;
use Mtc\MercuryDataModels\ServicePackage;
use Mtc\MercuryDataModels\VehicleMake;
use Mtc\MercuryDataModels\VehicleModel;
use Mtc\VehicleLookup\VehicleLookupService;
use Ramsey\Uuid\Uuid;

class BookingRepository
{
    public function __construct(private readonly VehicleLookupService $lookupService)
    {
    }

    public function createDraft(array $data): Model
    {
        $data['uuid'] = Uuid::uuid4();
        $data['status'] = BookingStatus::DRAFT->value;
        $data['vehicle_data'] = $this->lookupService->findByVRM($data['registration_number'], $data['mileage']);
        return Booking::query()->create($data);
    }

    public function loadBooking(string $id): Model
    {
        return Booking::query()->where('uuid', $id)->firstOrFail();
    }

    public function setLocation(string $id, int $location): Model
    {
        $booking = Booking::query()->where('uuid', $id)->firstOrFail();
        $booking->update([
            'location_id' => $location
        ]);
        return $booking;
    }

    public function setPackage(string $id, array $packages): Model
    {
        /** @var Booking $booking */
        $booking = Booking::query()->where('uuid', $id)->firstOrFail();
        $booking->packages()->sync($packages);
        return $booking;
    }

    public function setTime(string $id, string $date, string $time): Model
    {
        /** @var Booking $booking */
        $booking = Booking::query()->where('uuid', $id)->firstOrFail();
        $this->checkBookingTimeAvailable($booking->location->serviceAvailability, $booking, $date, $time);
        $booking->booking_time = $date . ' ' . $time;
        $booking->save();
        return $booking;
    }

    public function setDelivery(string $id, int $delivery_method): Model
    {
        /** @var Booking $booking */
        $booking = Booking::query()->where('uuid', $id)->firstOrFail();
        $booking->delivery_option_id = $delivery_method;
        $booking->save();
        return $booking;
    }


    public function book(array $input): Model
    {
        $booking = Booking::query()->where('uuid', $input['booking_id'])->firstOrFail();
        $booking->fill($input);
        $booking->setStatus(BookingStatus::PENDING->value);
        $booking->save();
        return $booking;
    }

    public function getLocations(): Collection
    {
        return Dealership::query()
            ->whereHas('serviceAvailability', fn ($query) => $query->where('active', 1))
            ->get();
    }

    public function getPackages(?Booking $booking = null): Collection
    {
        $query = ServicePackage::query()
            ->orderBy('order')
            ->orderBy('name');

        $makeId = $this->resolveBookingMakeId($booking);
        if ($makeId) {
            $query->where(fn ($q) => $q->whereNull('applicable_make_id')
                ->orWhere('applicable_make_id', $makeId));
        }

        // Model filtering is strict: a model-specific package is only shown when the booking's
        // model resolves to it. When no model is known, model-specific packages are hidden.
        $modelId = $this->resolveBookingModelId($booking, $makeId);
        $query->where(function ($q) use ($modelId) {
            $q->whereNull('applicable_model_id');
            if ($modelId) {
                $q->orWhere('applicable_model_id', $modelId);
            }
        });

        return $query->get();
    }

    private function resolveBookingMakeId(?Booking $booking): ?int
    {
        $makeName = $booking?->vehicle_data['make'] ?? null;
        if (empty($makeName)) {
            return null;
        }

        $exactMatchId = VehicleMake::query()
            ->where('name', $makeName)
            ->value('id');
        if ($exactMatchId) {
            return $exactMatchId;
        }

        $provider = $this->lookupService->findActiveDriver();
        if (empty($provider)) {
            return null;
        }

        return TaxonomyMap::query()
            ->where('provider', $provider)
            ->where('taxonomy_type', TaxonomyMap::MAKE)
            ->where('term', $makeName)
            ->whereNotNull('taxonomy_id')
            ->value('taxonomy_id');
    }

    private function resolveBookingModelId(?Booking $booking, ?int $makeId): ?int
    {
        $modelName = $booking?->vehicle_data['model'] ?? null;
        if (empty($modelName)) {
            return null;
        }

        // Model names are not globally unique, so scope to the resolved make when known.
        $exactMatchId = VehicleModel::query()
            ->where('name', $modelName)
            ->when($makeId, fn ($q) => $q->where('make_id', $makeId))
            ->value('id');
        if ($exactMatchId) {
            return $exactMatchId;
        }

        $provider = $this->lookupService->findActiveDriver();
        if (empty($provider)) {
            return null;
        }

        return TaxonomyMap::query()
            ->where('provider', $provider)
            ->where('taxonomy_type', TaxonomyMap::MODEL)
            ->where('term', $modelName)
            ->whereNotNull('taxonomy_id')
            ->value('taxonomy_id');
    }

    public function getDeliveryOptions(): Collection
    {
        return BookingDeliveryOption::query()
            ->orderBy('name')
            ->get();
    }

    public function getAvailability(?string $month, ?int $year, int $location_id): Collection
    {
        if ($year !== null) {
            $monthNumber = $month ? Carbon::parse($month)->month : Carbon::now()->month;
            $from = Carbon::createFromDate($year, $monthNumber, 1);
        } else {
            $from = $month ? Carbon::parse($month) : Carbon::now();

            if ($from->month < Carbon::now()->month) {
                $from->addYear();
            }
        }

        $until = $from->copy()->endOfMonth()->endOfWeek();
        $from->startOfMonth()->startOfWeek();

        return  $this->getGrid(
            $from,
            $until,
            $this->getBookingsForPeriod($from, $until, $location_id),
            $this->getLocationAvailability($location_id)
        );
    }

    private function getGrid(Carbon $from, Carbon $until, $bookings, ?BookingAvailability $location_data): Collection
    {
        $grid = collect();

        $cursor = $from->copy()->startOfWeek();

        while ($cursor->lte($until)) {
            $week = collect();

            for ($i = 0; $i < 7; $i++) {
                $date = $cursor->copy()->addDays($i);

                if ($date->between($from, $until)) {
                    $week->push([
                        'name' => $date->format('jS M'),
                        'weekday' => $date->format('D'),
                        'week_of_year' => $date->weekOfYear,
                        'full-date' => $date->format('Y-m-d'),
                        'slots' => $this->getSlotsForDate($date, $bookings, $location_data)
                    ]);
                }
            }

            if ($week->isNotEmpty()) {
                $grid->push($week);
            }

            $cursor->addWeek();
        }

        return $grid;
    }

    private function getBookingsForPeriod(Carbon $from, Carbon $until, ?int $location): Collection
    {
        return Booking::query()
            ->with([
                'location',
            ])
            ->where('booking_time', '>=', $from)
            ->where('booking_time', '<=', $until)
            ->where('location_id', $location)
            ->where('status', '!=', BookingStatus::CANCELLED->value)
            ->orderBy('booking_time')
            ->get();
    }

    private function getLocationAvailability(int $location): ?Model
    {
        return BookingAvailability::query()->where('dealership_id', $location)->first();
    }

    private function getSlotsForDate(Carbon $date, $bookings, ?BookingAvailability $location_data): array
    {
        if (!$location_data) {
            return [];
        }

        $before_allowed_time = $date->lt(Carbon::now()->addDays($location_data->min_days_in_future_for_booking));
        if ($location_data->max_days_in_future_for_booking) {
            $after_allowed_time = $date->gt(Carbon::now()->addDays($location_data->max_days_in_future_for_booking));
        } else {
            $after_allowed_time = false;
        }
        if ($before_allowed_time || $after_allowed_time) {
            return [];
        }

        $weekdayName = strtolower($date->format('l')) . 's';
        if (!$location_data->{$weekdayName}) {
            return [];
        }

        if ($location_data->upcomingHolidays->pluck('holiday_date')->contains($date->format('Y-m-d'))) {
            return [];
        }

        // Check number of bookings per time slot is lower than number of bays available
        return collect($location_data->time_windows)
            ->filter(fn ($time) => $bookings
                ->where('booking_time', $date->format('Y-m-d') . ' ' . $time . ':00')
                ->count() < $location_data->number_of_bays)
            ->values()
            ->toArray();
    }

    private function checkBookingTimeAvailable(
        BookingAvailability $serviceAvailability,
        Booking $booking,
        string $date,
        string $time
    ): void {
        if (!in_array($time, $serviceAvailability->time_windows ?? [])) {
            throw ValidationException::withMessages(['time' => 'Selected time window is not available']);
        }
        $already_booked = Booking::query()
            ->where('id', '!=', $booking->id)
            ->where('location_id', $booking->location_id)
            ->where('status', '!=', BookingStatus::CANCELLED->value)
            ->where('booking_time', 'like', $date . ' ' . $time . '%')
            ->count() >= $serviceAvailability->number_of_bays;

        if ($already_booked) {
            throw ValidationException::withMessages([
                'time' => 'Selected time is already taken, please choose different time'
            ]);
        }
    }
}
