<?php

namespace Modules\ClinicCore\Services\Booking;

use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Modules\ClinicCore\Entities\ClcAppointment;
use Modules\ClinicCore\Entities\ClcDoctorSchedule;
use Modules\ClinicCore\Entities\ClcScheduleException;

/**
 * SIFA P3-T7 — the single source of truth for doctor slot availability.
 *
 * BOTH the public booking driver (ClcBookingDriver::availability) and the
 * staff-side slot endpoint (ClcAppointmentController::availableSlots) call this
 * so slot rules live in exactly one place. It:
 *
 *   1. loads the doctor's schedules for the weekday and keeps only patterns
 *      whose effective_from/effective_to window contains the date;
 *   2. expands each pattern into slot pairs (break periods already subtracted by
 *      ClcDoctorSchedule::getSlotPairs);
 *   3. applies same-day exceptions:
 *        - unavailable → drops the whole day, or the given window;
 *        - modified    → replaces the pattern's window with the exception's;
 *        - extra       → adds an additional window (using the pattern's slot size);
 *   4. computes per-slot capacity = count of same-start appointments
 *      (excluding cancelled/no_show) against max_per_slot (+ overbooking_max
 *      when overbooking_allowed), so a slot stays "available" until capacity is
 *      reached rather than being killed by a single booking.
 *
 * Each returned slot is a plain array (see slot() below) that each caller maps
 * to its own response shape. Deterministic + business_id-scoped.
 */
class ScheduleAvailabilityService
{
    /**
     * Normalized open slots for a doctor on a date.
     *
     * @return array<int, array{
     *   start:string, end:string, duration:int, schedule_id:int|null,
     *   max_per_slot:int, taken:int, capacity:int, available:bool
     * }>  ordered by start time, one entry per distinct start time
     */
    public function slotsForDoctor(int $businessId, int $doctorId, \DateTimeInterface $date, ?int $deptId = null): array
    {
        $carbon = Carbon::instance($date instanceof \DateTimeImmutable ? \DateTime::createFromInterface($date) : $date);
        $dateStr = $carbon->toDateString();

        $schedules = $this->effectiveSchedules($businessId, $doctorId, $carbon, $deptId);
        $exceptions = $this->exceptionsFor($businessId, $doctorId, $carbon);

        // A full-day "unavailable" (no window) removes every slot.
        $fullDayOff = $exceptions->contains(
            fn ($e) => $e->type === 'unavailable' && $e->window() === null
        );
        if ($fullDayOff) {
            return [];
        }

        $bookingCounts = $this->bookingCountsByStart($businessId, $doctorId, $dateStr);

        // Collect candidate slots keyed by start time (dedupe overlapping patterns).
        $byStart = [];

        // Modified exceptions replace the regular pattern windows for the day.
        $modified = $exceptions->where('type', 'modified')->filter(fn ($e) => $e->window() !== null);

        foreach ($schedules as $sched) {
            $pairs = $sched->getSlotPairs();

            if ($modified->isNotEmpty()) {
                // Keep only the pattern slots that fall inside a modified window.
                $pairs = array_filter($pairs, function ($pair) use ($modified) {
                    foreach ($modified as $m) {
                        [$ws, $we] = $m->window();
                        if ($pair['start'] >= $ws && $pair['end'] <= $we) {
                            return true;
                        }
                    }
                    return false;
                });
            }

            foreach ($pairs as $pair) {
                $this->addSlot($byStart, $pair, $sched, $bookingCounts);
            }
        }

        // Extra exceptions add windows on top (use a default 30-min slot size,
        // or the doctor's first pattern size when present).
        $extraSize = $schedules->first()->slot_duration_minutes ?? 30;
        foreach ($exceptions->where('type', 'extra') as $ex) {
            $win = $ex->window();
            if (!$win) {
                continue;
            }
            foreach ($this->windowSlotPairs($win[0], $win[1], (int) $extraSize) as $pair) {
                $this->addSlot($byStart, $pair, null, $bookingCounts);
            }
        }

        // Partial "unavailable" windows blank out overlapping slots.
        foreach ($exceptions->where('type', 'unavailable') as $ex) {
            $win = $ex->window();
            if (!$win) {
                continue; // full-day already handled above
            }
            foreach (array_keys($byStart) as $start) {
                $end = $byStart[$start]['end'];
                if ($start < $win[1] && $end > $win[0]) {
                    unset($byStart[$start]);
                }
            }
        }

        ksort($byStart);
        return array_values($byStart);
    }

    /** Push a computed slot into the keyed map (first pattern to define a start wins). */
    private function addSlot(array &$byStart, array $pair, ?ClcDoctorSchedule $sched, array $bookingCounts): void
    {
        $start = $pair['start'];
        if (isset($byStart[$start])) {
            return;
        }
        $byStart[$start] = $this->slot($pair, $sched, $bookingCounts[$start] ?? 0);
    }

    /** Build one normalized slot descriptor + capacity flag. */
    private function slot(array $pair, ?ClcDoctorSchedule $sched, int $taken): array
    {
        $maxPerSlot = $sched ? max(1, (int) ($sched->max_per_slot ?: 1)) : 1;
        $capacity   = $maxPerSlot;
        if ($sched && $sched->overbooking_allowed) {
            $capacity += max(0, (int) $sched->overbooking_max);
        }

        return [
            'start'        => $pair['start'],
            'end'          => $pair['end'],
            'duration'     => (int) $pair['duration'],
            'schedule_id'  => $sched?->id,
            'max_per_slot' => $maxPerSlot,
            'taken'        => $taken,
            'capacity'     => $capacity,
            'available'    => $taken < $capacity,
        ];
    }

    /** Schedules for the weekday, filtered to those effective on the date + dept scope. */
    private function effectiveSchedules(int $businessId, int $doctorId, Carbon $date, ?int $deptId): Collection
    {
        $dow  = (int) $date->dayOfWeek;
        $base = ClcDoctorSchedule::where('doctor_id', $doctorId)->active()->where('day_of_week', $dow);

        // Same dept-priority logic the legacy controller used: prefer dept-specific,
        // fall back to "any department" rows.
        if ($deptId) {
            $rows = (clone $base)->where('department_id', $deptId)->get();
            if ($rows->isEmpty()) {
                $rows = (clone $base)->whereNull('department_id')->get();
            }
        } else {
            $rows = $base->get();
        }

        return $rows->filter(fn (ClcDoctorSchedule $s) => $s->isEffectiveOn($date))->values();
    }

    /** Same-day exceptions for the doctor. */
    private function exceptionsFor(int $businessId, int $doctorId, Carbon $date): Collection
    {
        return ClcScheduleException::where('business_id', $businessId)
            ->where('doctor_id', $doctorId)
            ->whereDate('date', $date->toDateString())
            ->get();
    }

    /**
     * Count of active appointments per start time (HH:MM) for the doctor/date.
     * Cancelled + no_show don't consume capacity.
     */
    private function bookingCountsByStart(int $businessId, int $doctorId, string $dateStr): array
    {
        $rows = ClcAppointment::where('business_id', $businessId)
            ->where('doctor_id', $doctorId)
            ->whereDate('appointment_date', $dateStr)
            ->whereNotIn('status', ['cancelled', 'no_show'])
            ->get(['appointment_time']);

        $counts = [];
        foreach ($rows as $r) {
            if (!$r->appointment_time) {
                continue;
            }
            $key = substr((string) $r->appointment_time, 0, 5);
            $counts[$key] = ($counts[$key] ?? 0) + 1;
        }
        return $counts;
    }

    /** Expand a HH:MM..HH:MM window into slot pairs of $stepMin minutes. */
    private function windowSlotPairs(string $start, string $end, int $stepMin): array
    {
        $pairs = [];
        $s = strtotime($start);
        $e = strtotime($end);
        $step = max(1, $stepMin) * 60;
        for ($t = $s; $t + $step <= $e; $t += $step) {
            $pairs[] = [
                'start'    => date('H:i', $t),
                'end'      => date('H:i', $t + $step),
                'duration' => $stepMin,
            ];
        }
        return $pairs;
    }
}
