<?php

namespace App\Appointment\Service;

use App\Appointment\Repository\AppointmentRepository;
use App\Appointment\Repository\DateOverrideRepository;
use App\Appointment\Repository\HolidayRepository;
use App\Appointment\Repository\WeeklyScheduleRepository;
use App\Appointment\Entity\WeeklySchedule;
use App\Doctor\Entity\Doctor;


class SlotCalculatorService
{
    public function __construct(
        private readonly WeeklyScheduleRepository $scheduleRepo,
        private readonly DateOverrideRepository   $overrideRepo,
        private readonly HolidayRepository        $holidayRepo,
        private readonly AppointmentRepository    $appointmentRepo,
    ) {}

    /**
     * Returns available slots (flat array) for booking conflict checks.
     * Day index convention: 0=Saturday(شنبه), 1=Sunday, ..., 6=Friday(جمعه)
     *
     * @return array[] [{start, end, start_time, end_time, location_id}]
     */
    public function getAvailableSlots(Doctor $doctor, string $date): array
    {
        $sessions = $this->buildAllSessions($doctor, $date);
        if (empty($sessions)) return [];
        $flat = array_merge(...array_map(fn($s) => $s['slots'], $sessions));
        return $this->filterBookedSlots($doctor, $flat);
    }

    /**
     * آدرس (location_id) متناظر با اسلاتِ شروع‌شده در تاریخ مشخص. اگر پیدا نشد null.
     */
    public function resolveSlotLocationId(Doctor $doctor, int $slotStart): ?int
    {
        $date     = date('Y-m-d', $slotStart);
        $sessions = $this->buildAllSessions($doctor, $date);
        foreach ($sessions as $session) {
            foreach (($session['slots'] ?? []) as $slot) {
                if ((int) ($slot['start'] ?? 0) === $slotStart) {
                    $loc = $slot['location_id'] ?? null;
                    return $loc !== null ? (int) $loc : null;
                }
            }
        }
        return null;
    }

    /**
     * Returns sessions grouped by shift, each slot tagged with is_available.
     * Used by the schedule view to show real shift boundaries.
     *
     * @return array[] [{start_time, end_time, slots: [{start, end, start_time, end_time, location_id, is_available}]}]
     */
    public function getAllSlotsWithAvailability(Doctor $doctor, string $date): array
    {
        $sessions = $this->buildAllSessions($doctor, $date);
        $now = time();
        return array_map(fn(array $session) => [
            'start_time' => $session['start_time'],
            'end_time'   => $session['end_time'],
            'slots'      => array_map(fn(array $slot) => array_merge($slot, [
                'is_available' => $slot['start'] >= $now
                    && !$this->appointmentRepo->isSlotTaken($doctor, $slot['start'], $slot['end']),
            ]), $session['slots']),
        ], $sessions);
    }

    /**
     * Whether a doctor has at least one slot on the given date.
     * Lightweight check for the month-availability endpoint.
     */
    public function hasAnyAvailability(Doctor $doctor, string $date): bool
    {
        return !empty($this->buildAllSessions($doctor, $date));
    }

    /**
     * Booking is allowed only when online booking is enabled and the date is
     * today..(today + window). Past dates are always rejected.
     */
    private function isWithinBookingWindow(Doctor $doctor, int $dayStart): bool
    {
        $todayStart = (int) strtotime('today 00:00:00');
        if ($dayStart < $todayStart) {
            return false;
        }

        $meta = $this->getBookingMeta($doctor);
        if (!($meta['online_booking_enabled'] ?? true)) {
            return false;
        }

        $value = max(1, (int)($meta['booking_window_value'] ?? 1));
        $unit  = ($meta['booking_window_unit'] ?? 'month') === 'week' ? 'week' : 'month';
        $maxStart = (int) strtotime("today +{$value} {$unit} 00:00:00");

        return $dayStart <= $maxStart;
    }

    private function getBookingMeta(Doctor $doctor): array
    {
        $schedule = $this->scheduleRepo->findByDoctor($doctor);
        return $schedule ? $schedule->getMeta() : WeeklySchedule::DEFAULT_META;
    }

    /**
     * Core: build all sessions with their slots, grouped by shift.
     *
     * @return array[] [{start_time: string, end_time: string, slots: array[]}]
     */
    private function buildAllSessions(Doctor $doctor, string $date): array
    {
        $dayStart = (int) strtotime($date . ' 00:00:00');
        $dayEnd   = $dayStart + 86400;

        // 0. Online booking disabled or date outside the booking window
        if (!$this->isWithinBookingWindow($doctor, $dayStart)) {
            return [];
        }

        // 1. Blocked by holiday
        if (!empty($this->holidayRepo->findActiveByDoctor($doctor, $dayStart, $dayEnd - 1))) {
            return [];
        }

        // 2. Date override takes precedence over weekly schedule
        foreach ($this->overrideRepo->findByDoctor($doctor) as $override) {
            if (date('Y-m-d', $override->getDate()) === $date) {
                if (!$override->isActive()) return [];
                return $this->buildSessionsFromOverride($override->getSetting() ?? [], $dayStart);
            }
        }

        // 3. Weekly schedule
        $schedule = $this->scheduleRepo->findByDoctor($doctor);
        if ($schedule === null) return [];

        // Convert PHP date('w') (0=Sunday) to Iranian week index (0=Saturday)
        $phpDow = (int) date('w', $dayStart);
        $dayKey = (string)(($phpDow + 1) % 7);

        $dayConf = $schedule->getSetting()[$dayKey] ?? null;
        if ($dayConf === null) return [];

        $sessionConfigs = $dayConf['sessions'] ?? [];
        $activeSessions = array_filter($sessionConfigs, fn($s) => $s['active'] ?? false);
        usort($activeSessions, fn($a, $b) =>
            $this->parseTime($a['start_time'] ?? '00:00') <=> $this->parseTime($b['start_time'] ?? '00:00')
        );

        $result  = [];
        $prevEnd = 0;
        foreach ($activeSessions as $session) {
            $sessionStart = $this->parseTime($session['start_time'] ?? '00:00');
            if ($sessionStart < $prevEnd) continue; // skip overlapping session
            $slots = $this->buildSessionSlots($session, $dayStart);
            if (!empty($slots)) {
                $result[] = [
                    'start_time' => $session['start_time'] ?? '00:00',
                    'end_time'   => $session['end_time']   ?? '00:00',
                    'slots'      => $slots,
                ];
            }
            $prevEnd = $this->parseTime($session['end_time'] ?? '00:00');
        }

        return $result;
    }

    /**
     * Build sessions from date-override config.
     * Supports new SessionConfig format {start_time, end_time, ...} and legacy {start, end, duration}.
     */
    private function buildSessionsFromOverride(array $slotConfigs, int $dayStart): array
    {
        $sessions = [];
        foreach ($slotConfigs as $config) {
            if (isset($config['start_time'])) {
                $slots = $this->buildSessionSlots(array_merge(['active' => true], $config), $dayStart);
                if (!empty($slots)) {
                    $sessions[] = [
                        'start_time' => $config['start_time'],
                        'end_time'   => $config['end_time'] ?? '00:00',
                        'slots'      => $slots,
                    ];
                }
            } else {
                // Legacy flat format: {start, end, duration}
                $startSec = $this->parseTime($config['start'] ?? '00:00');
                $endSec   = $this->parseTime($config['end']   ?? '00:00');
                $duration = (int)($config['duration'] ?? 30) * 60;
                if ($duration <= 0 || $endSec <= $startSec) continue;
                $slots = [];
                for ($t = $startSec; $t + $duration <= $endSec; $t += $duration) {
                    $slots[] = [
                        'start'       => $dayStart + $t,
                        'end'         => $dayStart + $t + $duration,
                        'start_time'  => gmdate('H:i', $t),
                        'end_time'    => gmdate('H:i', $t + $duration),
                        'location_id' => null,
                    ];
                }
                if (!empty($slots)) {
                    $sessions[] = [
                        'start_time' => $config['start'] ?? '00:00',
                        'end_time'   => $config['end']   ?? '00:00',
                        'slots'      => $slots,
                    ];
                }
            }
        }
        return $sessions;
    }

    /**
     * Build individual slot entries from a session config (weekly or override).
     * Handles rest breaks and patient limits.
     */
    private function buildSessionSlots(array $session, int $dayStart): array
    {
        $startSec   = $this->parseTime($session['start_time'] ?? '00:00');
        $endSec     = $this->parseTime($session['end_time']   ?? '00:00');
        $dur        = (int)($session['duration_per_patient'] ?? 20) * 60;
        $hasRest    = (bool)($session['has_rest'] ?? false);
        $restInt    = (int)($session['rest_interval'] ?? 60) * 60;
        $restDur    = (int)($session['time_to_rest']  ?? 10) * 60;
        $limit      = isset($session['patient_limit']) && $session['patient_limit'] !== null
                        ? (int)$session['patient_limit'] : null;
        $locationId = isset($session['location_id']) ? (int)$session['location_id'] : null;

        if ($dur <= 0 || $endSec <= $startSec) return [];

        $slots        = [];
        $currentSec   = $startSec;
        $elapsedWork  = 0;
        $patientCount = 0;

        while ($currentSec + $dur <= $endSec) {
            if ($limit !== null && $patientCount >= $limit) break;

            if ($hasRest && $restInt > 0 && $elapsedWork > 0 && $elapsedWork >= $restInt) {
                $currentSec  += $restDur;
                $elapsedWork  = 0;
                continue;
            }

            $slots[] = [
                'start'       => $dayStart + $currentSec,
                'end'         => $dayStart + $currentSec + $dur,
                'start_time'  => gmdate('H:i', $currentSec),
                'end_time'    => gmdate('H:i', $currentSec + $dur),
                'location_id' => $locationId,
            ];

            $currentSec   += $dur;
            $elapsedWork  += $dur;
            $patientCount += 1;
        }

        return $slots;
    }

    private function filterBookedSlots(Doctor $doctor, array $slots): array
    {
        $now = time();
        return array_values(array_filter($slots, fn(array $slot): bool =>
            $slot['start'] >= $now
            && !$this->appointmentRepo->isSlotTaken($doctor, $slot['start'], $slot['end'])
        ));
    }

    private function parseTime(string $time): int
    {
        [$h, $m] = explode(':', $time, 2) + [0, 0];
        return ((int)$h * 3600) + ((int)$m * 60);
    }
}
