<?php

namespace Modules\PortalApi\Support;

use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Modules\VetClinic\Entities\VcAppointment;
use Modules\VetClinic\Entities\VcDoctor;
use Modules\VetClinic\Entities\VcPet;

/**
 * Computes available booking slots from real data.
 *
 *  - Reads vc_doctor_schedules to know when each doctor is at work.
 *    Schedules can be scoped to a location_id (migration added it).
 *    NULL location_id = doctor works at ANY location that day.
 *
 *  - Reads vc_appointments to subtract already-taken slots.
 *
 *  - When the pet's preferred doctor isn't available at the requested
 *    location/date, returns a structured "alternatives" payload so the
 *    PWA can offer:
 *      • same doctor, different day
 *      • different doctor, same day/location
 *      • same doctor, different location same day
 */
class BookingSlotResolver
{
    public function __construct(private int $businessId) {}

    /**
     * Main entry point. Returns the resolution shape expected by the PWA.
     *
     * @return array{
     *   slots: array<int, array{start:string,label:string,doctor_name:string,available:bool}>,
     *   preferred: array{
     *     doctor: ?array{id:int,name:string},
     *     available: bool,
     *     reason: ?string,
     *     next_available_with_preferred: ?array
     *   },
     *   alternatives: array,
     *   slots_at_other_locations: array
     * }
     */
    public function resolve(
        ?VcPet $pet,
        ?int $locationId,
        Carbon $date,
        int $durationMinutes = 30,
        ?int $overrideDoctorId = null
    ): array {
        // When the caller passes an explicit doctor (owner accepted an
        // "alternative" earlier), treat THAT as the effective preferred
        // so the response says preferred.available=true once they're
        // free at the requested location/date.
        $preferredDoctorId = $overrideDoctorId
            ?: ($pet ? $pet->effectivePreferredDoctorId() : null);
        $dow = (int) $date->dayOfWeek;     // 0..6 (matches MySQL DAYOFWEEK-1)

        $preferredAvailable = false;
        $preferredReason    = null;
        $slots              = [];

        if ($preferredDoctorId) {
            // Try the preferred doctor at the requested location & date.
            [$slots, $reason] = $this->slotsFor($preferredDoctorId, $locationId, $date, $dow, $durationMinutes);
            if (! empty($slots)) {
                $preferredAvailable = true;
            } else {
                $preferredReason = $reason;
            }
        } else {
            // No preferred doctor — show any-doctor slots for the location.
            $slots = $this->slotsAtLocation($locationId, $date, $dow, $durationMinutes);
        }

        // Build alternatives when preferred is unavailable.
        $alternatives           = [];
        $slotsAtOtherLocations  = [];
        $nextWithPreferred      = null;

        if ($preferredDoctorId && ! $preferredAvailable) {
            $alternatives          = $this->otherDoctorsAtLocation($preferredDoctorId, $locationId, $date, $dow, $durationMinutes);
            $slotsAtOtherLocations = $this->preferredAtOtherLocations($preferredDoctorId, $locationId, $date, $dow, $durationMinutes);
            $nextWithPreferred     = $this->nextSlotWithPreferred($preferredDoctorId, $locationId, $date, $durationMinutes);
        }

        $preferredDoctor = $preferredDoctorId ? VcDoctor::find($preferredDoctorId) : null;

        return [
            'slots' => $slots,
            'preferred' => [
                'doctor'        => $preferredDoctor ? ['id' => $preferredDoctor->id, 'name' => $preferredDoctor->name] : null,
                'available'     => $preferredAvailable || ! $preferredDoctorId,
                'reason'        => $preferredReason,
                'next_available_with_preferred' => $nextWithPreferred,
            ],
            'alternatives'           => $alternatives,
            'slots_at_other_locations' => $slotsAtOtherLocations,
        ];
    }

    /* ─── Core slot generation ──────────────────────────────── */

    /**
     * Available slots for ONE doctor at a specific location/date.
     * Returns [slots[], reason] where reason is null on success.
     */
    private function slotsFor(int $doctorId, ?int $locationId, Carbon $date, int $dow, int $duration): array
    {
        $schedules = DB::table('vc_doctor_schedules')
            ->where('doctor_id', $doctorId)
            ->where('day_of_week', $dow)
            ->where(function ($q) { $q->where('status', 'active')->orWhere('status', 1); })
            ->where(function ($q) use ($locationId) {
                if ($locationId === null) return;
                $q->where('location_id', $locationId)->orWhereNull('location_id');
            })
            ->get();

        if ($schedules->isEmpty()) {
            // Why? Doctor exists but no schedule for this dow/location.
            $anySchedule = DB::table('vc_doctor_schedules')
                ->where('doctor_id', $doctorId)
                ->where('day_of_week', $dow)
                ->exists();
            return [[], $anySchedule ? 'not_at_location' : 'off_today'];
        }

        $taken    = $this->takenIntervalsForDoctor($doctorId, $date);
        $doctor   = VcDoctor::find($doctorId);
        $allSlots = [];
        foreach ($schedules as $sched) {
            $slotMin = (int) ($sched->slot_duration_minutes ?: 30);
            $allSlots = array_merge($allSlots, $this->buildSlots($date, $sched->start_time, $sched->end_time, $slotMin, $duration, $taken, $doctor?->name ?? '—'));
        }

        // If schedule existed but every slot is taken, surface that distinctly.
        if (empty($allSlots)) {
            return [[], 'no_slot_today'];
        }

        return [$this->dedupSlots($allSlots), null];
    }

    /** Available slots at a location across ALL doctors who work there that day. */
    private function slotsAtLocation(?int $locationId, Carbon $date, int $dow, int $duration): array
    {
        $schedules = DB::table('vc_doctor_schedules')
            ->where('day_of_week', $dow)
            ->where(function ($q) { $q->where('status', 'active')->orWhere('status', 1); })
            ->when($locationId !== null, fn ($q) =>
                $q->where(function ($qq) use ($locationId) {
                    $qq->where('location_id', $locationId)->orWhereNull('location_id');
                })
            )
            ->get();

        $all = [];
        foreach ($schedules as $sched) {
            $doctor  = VcDoctor::find($sched->doctor_id);
            $taken   = $this->takenIntervalsForDoctor((int) $sched->doctor_id, $date);
            $slotMin = (int) ($sched->slot_duration_minutes ?: 30);
            $all = array_merge($all, $this->buildSlots($date, $sched->start_time, $sched->end_time, $slotMin, $duration, $taken, $doctor?->name ?? '—'));
        }
        return $this->dedupSlots($all);
    }

    /* ─── Alternatives ──────────────────────────────────────── */

    private function otherDoctorsAtLocation(int $excludeDoctorId, ?int $locationId, Carbon $date, int $dow, int $duration): array
    {
        $schedules = DB::table('vc_doctor_schedules')
            ->where('day_of_week', $dow)
            ->where('doctor_id', '!=', $excludeDoctorId)
            ->where(function ($q) { $q->where('status', 'active')->orWhere('status', 1); })
            ->when($locationId !== null, fn ($q) =>
                $q->where(function ($qq) use ($locationId) {
                    $qq->where('location_id', $locationId)->orWhereNull('location_id');
                })
            )
            ->get()
            ->groupBy('doctor_id');

        $out = [];
        foreach ($schedules as $docId => $rows) {
            $doctor = VcDoctor::find($docId);
            if (! $doctor) continue;
            $taken = $this->takenIntervalsForDoctor((int) $docId, $date);
            $slots = [];
            foreach ($rows as $sched) {
                $slotMin = (int) ($sched->slot_duration_minutes ?: 30);
                $slots = array_merge($slots, $this->buildSlots($date, $sched->start_time, $sched->end_time, $slotMin, $duration, $taken, $doctor->name));
            }
            $slots = $this->dedupSlots($slots);
            if (! empty($slots)) {
                $out[] = [
                    'doctor'   => ['id' => $doctor->id, 'name' => $doctor->name, 'specialization' => $doctor->specialization],
                    'location_id' => $locationId,
                    'slots'    => array_slice($slots, 0, 8),
                ];
            }
        }
        return $out;
    }

    private function preferredAtOtherLocations(int $doctorId, ?int $excludeLocationId, Carbon $date, int $dow, int $duration): array
    {
        $schedules = DB::table('vc_doctor_schedules')
            ->where('doctor_id', $doctorId)
            ->where('day_of_week', $dow)
            ->where(function ($q) { $q->where('status', 'active')->orWhere('status', 1); })
            ->when($excludeLocationId !== null, fn ($q) =>
                $q->where(function ($qq) use ($excludeLocationId) {
                    $qq->where('location_id', '!=', $excludeLocationId)->whereNotNull('location_id');
                })
            )
            ->get();

        $out = [];
        $taken = $this->takenIntervalsForDoctor($doctorId, $date);
        $doctor = VcDoctor::find($doctorId);
        foreach ($schedules->groupBy('location_id') as $locId => $rows) {
            if ($locId === null) continue;
            $loc = DB::table('vc_clinic_locations')->where('id', $locId)->first(['id','name']);
            $slots = [];
            foreach ($rows as $sched) {
                $slotMin = (int) ($sched->slot_duration_minutes ?: 30);
                $slots = array_merge($slots, $this->buildSlots($date, $sched->start_time, $sched->end_time, $slotMin, $duration, $taken, $doctor?->name ?? '—'));
            }
            $slots = $this->dedupSlots($slots);
            if (! empty($slots)) {
                $out[] = [
                    'doctor'        => ['id' => $doctorId, 'name' => $doctor?->name ?? '—'],
                    'location_id'   => (int) $locId,
                    'location_name' => $loc?->name ?? 'Location ' . $locId,
                    'slots'         => array_slice($slots, 0, 6),
                ];
            }
        }
        return $out;
    }

    private function nextSlotWithPreferred(int $doctorId, ?int $locationId, Carbon $fromDate, int $duration): ?array
    {
        $cur = $fromDate->copy()->addDay();
        for ($i = 0; $i < 14; $i++, $cur->addDay()) {
            $dow = (int) $cur->dayOfWeek;
            [$slots, $_] = $this->slotsFor($doctorId, $locationId, $cur, $dow, $duration);
            if (! empty($slots)) {
                return [
                    'date'        => $cur->toDateString(),
                    'date_label'  => $cur->format('D M j'),
                    'location_id' => $locationId,
                    'time'        => $slots[0]['label'],
                ];
            }
        }
        return null;
    }

    /* ─── Building blocks ──────────────────────────────────── */

    /**
     * Generate 30-min (or doctor's slot_duration_minutes) slots between
     * start_time and end_time, skipping any that overlap a taken interval
     * or fall in the past.
     */
    private function buildSlots(Carbon $date, string $start, string $end, int $stepMin, int $duration, array $taken, string $doctorName): array
    {
        $cursor = Carbon::parse($date->toDateString() . ' ' . substr($start, 0, 5) . ':00');
        $hardEnd = Carbon::parse($date->toDateString() . ' ' . substr($end, 0, 5) . ':00');
        $now = now();
        $slots = [];

        while ($cursor->copy()->addMinutes($duration) <= $hardEnd) {
            $start = $cursor->copy();
            $finish = $cursor->copy()->addMinutes($duration);

            $isPast = $start->lt($now);
            $clash  = false;
            foreach ($taken as [$ts, $te]) {
                if ($start->lt($te) && $finish->gt($ts)) { $clash = true; break; }
            }

            if (! $isPast) {
                $slots[] = [
                    'start'       => $start->toIso8601String(),
                    'label'       => $start->format('H:i'),
                    'doctor_name' => $doctorName,
                    'available'   => ! $clash,
                ];
            }
            $cursor->addMinutes($stepMin);
        }
        return $slots;
    }

    private function takenIntervalsForDoctor(int $doctorId, Carbon $date): array
    {
        $rows = VcAppointment::where('business_id', $this->businessId)
            ->where('doctor_id', $doctorId)
            ->whereDate('appointment_date', $date->toDateString())
            ->whereNotIn('status', ['cancelled', 'no_show'])
            ->get(['appointment_time', 'duration_minutes']);

        $intervals = [];
        foreach ($rows as $r) {
            if (! $r->appointment_time) continue;
            $start = Carbon::parse($date->toDateString() . ' ' . substr((string) $r->appointment_time, 0, 8));
            $dur   = (int) ($r->duration_minutes ?: 30);
            $intervals[] = [$start, $start->copy()->addMinutes($dur)];
        }
        return $intervals;
    }

    /** Dedupe by label (same time slot from overlapping schedule rows). */
    private function dedupSlots(array $slots): array
    {
        $byLabel = [];
        foreach ($slots as $s) {
            // Available wins over unavailable for the same label.
            if (! isset($byLabel[$s['label']]) || ($s['available'] && ! $byLabel[$s['label']]['available'])) {
                $byLabel[$s['label']] = $s;
            }
        }
        ksort($byLabel);
        return array_values($byLabel);
    }
}
