<?php

namespace App\Services;

use App\Models\Appointment;
use App\Models\Barber;
use App\Models\BarberBlock;
use App\Models\Service;
use Carbon\Carbon;

class SlotService
{
    public function getAvailableSlots(int $barberId, int $serviceId, string $date): array
    {
        $barber = Barber::with('user')->findOrFail($barberId);
        $service = Service::findOrFail($serviceId);

        if (! ($barber->booking_enabled ?? true)) {
            return [];
        }

        $requestedDate = Carbon::parse($date);
        $dayOfWeek = $requestedDate->dayOfWeek;
        $duration = $service->duration_minutes;

        $dayRow = $barber->dayScheduleForWeekday($dayOfWeek);
        $isWorkingDay = is_array($dayRow) && filter_var($dayRow['enabled'] ?? false, FILTER_VALIDATE_BOOLEAN);

        $slotStarts = [];

        if ($isWorkingDay && is_array($dayRow) && ! empty($dayRow['start']) && ! empty($dayRow['end'])) {
            $workStart = Carbon::parse($dayRow['start'])->setDateFrom($requestedDate);
            $workEnd = Carbon::parse($dayRow['end'])->setDateFrom($requestedDate);
            if ($workEnd->gt($workStart)) {
                $slotStarts = array_merge($slotStarts, $this->buildSlotsInRange($workStart, $workEnd, $duration));
                $slotStarts = $this->removeWeeklyTimeOff($slotStarts, $requestedDate, $duration, $barber->weekly_time_off ?? []);
            }
        }

        foreach ($this->openingWindowsForDate($barberId, $date) as $range) {
            [$openStart, $openEnd] = $range;
            $slotStarts = array_merge($slotStarts, $this->buildSlotsInRange($openStart, $openEnd, $duration));
        }

        $slotStarts = array_values(array_unique($slotStarts));
        sort($slotStarts);

        $appointments = Appointment::where('barber_id', $barberId)
            ->where('appointment_date', $date)
            ->whereNotIn('status', ['cancelled'])
            ->get();

        $blockIntervals = BarberBlock::where('barber_id', $barberId)
            ->where('block_date', $date)
            ->where('kind', 'block')
            ->get();

        return array_values(array_filter($slotStarts, function ($slot) use ($appointments, $blockIntervals, $duration, $date) {
            $slotStart = Carbon::parse("$date $slot");
            $slotEnd = $slotStart->copy()->addMinutes($duration);

            foreach ($appointments as $apt) {
                $aptStart = Carbon::parse("$date {$apt->start_time}");
                $aptEnd = Carbon::parse("$date {$apt->end_time}");
                if ($slotStart->lt($aptEnd) && $slotEnd->gt($aptStart)) {
                    return false;
                }
            }

            foreach ($blockIntervals as $block) {
                $bStart = Carbon::parse("$date {$block->start_time}");
                $bEnd = Carbon::parse("$date {$block->end_time}");
                if ($slotStart->lt($bEnd) && $slotEnd->gt($bStart)) {
                    return false;
                }
            }

            return true;
        }));
    }

    /**
     * @return list<string> H:i start times
     */
    private function buildSlotsInRange(Carbon $rangeStart, Carbon $rangeEnd, int $durationMinutes): array
    {
        $slots = [];
        $current = $rangeStart->copy();
        while ($current->copy()->addMinutes($durationMinutes)->lte($rangeEnd)) {
            $slots[] = $current->format('H:i');
            $current->addMinutes($durationMinutes);
        }

        return $slots;
    }

    /**
     * @param  list<string>  $slotStarts
     * @param  list<array{weekday?:int,start?:string,end?:string}>  $weeklyTimeOff
     * @return list<string>
     */
    private function removeWeeklyTimeOff(array $slotStarts, Carbon $onDate, int $duration, array $weeklyTimeOff): array
    {
        $dateStr = $onDate->format('Y-m-d');
        $dow = $onDate->dayOfWeek;

        return array_values(array_filter($slotStarts, function ($slot) use ($dateStr, $dow, $duration, $weeklyTimeOff) {
            $slotStart = Carbon::parse("$dateStr $slot");
            $slotEnd = $slotStart->copy()->addMinutes($duration);

            foreach ($weeklyTimeOff as $off) {
                $w = (int) ($off['weekday'] ?? -1);
                if ($w !== $dow) {
                    continue;
                }
                $offStart = Carbon::parse($dateStr.' '.($off['start'] ?? '00:00'));
                $offEnd = Carbon::parse($dateStr.' '.($off['end'] ?? '00:00'));
                if ($slotStart->lt($offEnd) && $slotEnd->gt($offStart)) {
                    return false;
                }
            }

            return true;
        }));
    }

    /**
     * @return list<array{0: Carbon, 1: Carbon}>
     */
    private function openingWindowsForDate(int $barberId, string $date): array
    {
        $windows = [];
        $rows = BarberBlock::where('barber_id', $barberId)
            ->where('block_date', $date)
            ->where('kind', 'open')
            ->get();

        foreach ($rows as $row) {
            $windows[] = [
                Carbon::parse("{$date} {$row->start_time}"),
                Carbon::parse("{$date} {$row->end_time}"),
            ];
        }

        return $windows;
    }

    /**
     * List dates (Y-m-d) between inclusive bounds where at least one slot exists.
     */
    public function datesWithAvailability(int $barberId, int $serviceId, string $fromDate, string $toDate): array
    {
        $from = Carbon::parse($fromDate)->startOfDay();
        $to = Carbon::parse($toDate)->startOfDay();
        if ($from->gt($to)) {
            return [];
        }

        $out = [];
        for ($d = $from->copy(); $d->lte($to); $d->addDay()) {
            $ds = $d->toDateString();
            if (count($this->getAvailableSlots($barberId, $serviceId, $ds)) > 0) {
                $out[] = $ds;
            }
        }

        return $out;
    }
}
