<?php

namespace App\Services\Appointments;

use App\Enums\AppointmentStatus;
use App\Models\Appointment;
use App\Models\Business;
use App\Models\Service;
use App\Models\Setting;
use App\Models\StaffBreak;
use App\Models\StaffProfile;
use App\Models\WorkingHour;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Carbon;
use Illuminate\Validation\ValidationException;

class AvailabilityService
{
    public function ensureBookable(Business $business, Service $service, StaffProfile $staff, Carbon $startsAt, ?Appointment $ignore = null): void
    {
        $this->ensureSameBusiness($business, $service, $staff);
        $this->ensureStaffCanPerformService($service, $staff);
        $this->ensureAdvanceWindow($business, $startsAt);

        [$endsAt, $blockedStart, $blockedEnd] = $this->appointmentWindows($service, $startsAt);

        $this->ensureWithinWorkingHours($business, $staff, $startsAt, $endsAt);
        $this->ensureDoesNotOverlapBreak($business, $staff, $blockedStart, $blockedEnd);
        $this->ensureNoAppointmentConflict($business, $staff, $service, $startsAt, $blockedStart, $blockedEnd, $ignore);
    }

    public function isSlotAvailable(Business $business, Service $service, StaffProfile $staff, Carbon $startsAt, ?Appointment $ignore = null): bool
    {
        try {
            $this->ensureBookable($business, $service, $staff, $startsAt, $ignore);

            return true;
        } catch (ValidationException) {
            return false;
        }
    }

    public function availableSlots(Business $business, Service $service, StaffProfile $staff, Carbon $date): array
    {
        $workingHour = $this->workingHourFor($business, $staff, $date);

        if (! $workingHour || $workingHour->is_closed || ! $workingHour->starts_at || ! $workingHour->ends_at) {
            return [];
        }

        $interval = $this->bookingSetting($business, 'slot_interval', 30);
        $cursor = $date->copy()->setTimeFromTimeString((string) $workingHour->starts_at);
        $workEnd = $date->copy()->setTimeFromTimeString((string) $workingHour->ends_at);
        $lastStart = $workEnd->copy()->subMinutes((int) $service->duration_minutes);
        $slots = [];

        while ($cursor->lessThanOrEqualTo($lastStart)) {
            if ($this->isSlotAvailable($business, $service, $staff, $cursor->copy())) {
                $slots[] = $cursor->format('H:i');
            }

            $cursor->addMinutes(max(5, (int) $interval));
        }

        return $slots;
    }

    public function appointmentWindows(Service $service, Carbon $startsAt): array
    {
        $endsAt = $startsAt->copy()->addMinutes((int) $service->duration_minutes);
        $blockedStart = $startsAt->copy()->subMinutes((int) $service->buffer_before_minutes);
        $blockedEnd = $endsAt->copy()->addMinutes((int) $service->buffer_after_minutes);

        return [$endsAt, $blockedStart, $blockedEnd];
    }

    public function bookingSetting(Business $business, string $name, mixed $default): mixed
    {
        $value = Setting::query()
            ->where('business_id', $business->id)
            ->where('group', 'booking')
            ->where('name', $name)
            ->value('value');

        if ($value === null) {
            return $default;
        }

        $decoded = is_string($value) ? json_decode($value, true) : $value;

        return $decoded ?? $default;
    }

    private function ensureSameBusiness(Business $business, Service $service, StaffProfile $staff): void
    {
        if ((int) $service->business_id !== (int) $business->id || (int) $staff->business_id !== (int) $business->id) {
            throw ValidationException::withMessages([
                'staff_profile_id' => 'The selected staff member or service is not available for this business.',
            ]);
        }
    }

    private function ensureStaffCanPerformService(Service $service, StaffProfile $staff): void
    {
        $hasAssignments = $service->staffProfiles()->exists();

        if ($hasAssignments && ! $service->staffProfiles()->whereKey($staff->id)->exists()) {
            throw ValidationException::withMessages([
                'staff_profile_id' => 'The selected staff member is not assigned to this service.',
            ]);
        }
    }

    private function ensureAdvanceWindow(Business $business, Carbon $startsAt): void
    {
        $minimumMinutes = (int) $this->bookingSetting($business, 'minimum_advance_minutes', 120);
        $maximumDays = (int) $this->bookingSetting($business, 'maximum_advance_days', 60);

        if ($startsAt->lt(now()->addMinutes($minimumMinutes))) {
            throw ValidationException::withMessages([
                'appointment_time' => "Appointments must be booked at least {$minimumMinutes} minutes in advance.",
            ]);
        }

        if ($startsAt->gt(now()->addDays($maximumDays)->endOfDay())) {
            throw ValidationException::withMessages([
                'appointment_date' => "Appointments cannot be booked more than {$maximumDays} days in advance.",
            ]);
        }
    }

    private function ensureWithinWorkingHours(Business $business, StaffProfile $staff, Carbon $startsAt, Carbon $endsAt): void
    {
        $workingHour = $this->workingHourFor($business, $staff, $startsAt);

        if (! $workingHour || $workingHour->is_closed || ! $workingHour->starts_at || ! $workingHour->ends_at) {
            throw ValidationException::withMessages([
                'appointment_date' => 'The selected staff member is not working on this day.',
            ]);
        }

        $workStart = $startsAt->copy()->setTimeFromTimeString((string) $workingHour->starts_at);
        $workEnd = $startsAt->copy()->setTimeFromTimeString((string) $workingHour->ends_at);

        if ($startsAt->lt($workStart) || $endsAt->gt($workEnd)) {
            throw ValidationException::withMessages([
                'appointment_time' => 'The selected slot is outside the staff member working hours.',
            ]);
        }
    }

    private function ensureDoesNotOverlapBreak(Business $business, StaffProfile $staff, Carbon $blockedStart, Carbon $blockedEnd): void
    {
        $breaks = StaffBreak::query()
            ->forBusiness($business)
            ->where('staff_profile_id', $staff->id)
            ->where(function ($query) use ($blockedStart): void {
                $query->whereDate('date', $blockedStart->toDateString())
                    ->orWhere(function ($query) use ($blockedStart): void {
                        $query->where('is_recurring', true)
                            ->where('day_of_week', $blockedStart->dayOfWeek);
                    });
            })
            ->get();

        foreach ($breaks as $break) {
            $breakStart = $blockedStart->copy()->setTimeFromTimeString((string) $break->starts_at);
            $breakEnd = $blockedStart->copy()->setTimeFromTimeString((string) $break->ends_at);

            if ($this->overlaps($blockedStart, $blockedEnd, $breakStart, $breakEnd)) {
                throw ValidationException::withMessages([
                    'appointment_time' => 'The selected slot overlaps a staff break.',
                ]);
            }
        }
    }

    private function ensureNoAppointmentConflict(Business $business, StaffProfile $staff, Service $service, Carbon $startsAt, Carbon $blockedStart, Carbon $blockedEnd, ?Appointment $ignore = null): void
    {
        $appointments = Appointment::query()
            ->forBusiness($business)
            ->where('staff_profile_id', $staff->id)
            ->whereIn('status', AppointmentStatus::blockingValues())
            ->when($ignore, fn ($query) => $query->whereKeyNot($ignore->id))
            ->whereBetween('starts_at', [
                $blockedStart->copy()->subDay(),
                $blockedEnd->copy()->addDay(),
            ])
            ->with('service')
            ->lockForUpdate()
            ->get();

        if ($this->hasConflict($appointments, $blockedStart, $blockedEnd)) {
            throw ValidationException::withMessages([
                'appointment_time' => 'The selected slot conflicts with another appointment.',
            ]);
        }
    }

    private function hasConflict(EloquentCollection $appointments, Carbon $blockedStart, Carbon $blockedEnd): bool
    {
        foreach ($appointments as $appointment) {
            $existingService = $appointment->service;
            $existingStart = $appointment->starts_at->copy()->subMinutes((int) ($existingService?->buffer_before_minutes ?? 0));
            $existingEnd = $appointment->ends_at->copy()->addMinutes((int) ($existingService?->buffer_after_minutes ?? 0));

            if ($this->overlaps($blockedStart, $blockedEnd, $existingStart, $existingEnd)) {
                return true;
            }
        }

        return false;
    }

    private function workingHourFor(Business $business, StaffProfile $staff, Carbon $date): ?WorkingHour
    {
        return WorkingHour::query()
            ->forBusiness($business)
            ->where('staff_profile_id', $staff->id)
            ->where('day_of_week', $date->dayOfWeek)
            ->first();
    }

    private function overlaps(Carbon $startA, Carbon $endA, Carbon $startB, Carbon $endB): bool
    {
        return $startA->lt($endB) && $endA->gt($startB);
    }
}
