<?php

namespace App\Waitlist\Service;

use App\Appointment\Entity\Appointment;
use App\Doctor\Entity\DoctorAddress;
use App\Doctor\Repository\DoctorAddressRepository;
use App\Representation\Service\JalaliDateService;
use App\Sms\Service\SmsService;
use App\Waitlist\Entity\WaitlistEntry;
use App\Waitlist\Repository\WaitlistEntryRepository;
use Doctrine\ORM\EntityManagerInterface;

/**
 * اطلاع‌رسانی به لیست انتظار وقتی ظرفیتی آزاد می‌شود.
 *
 * ## چرا broadcast و نه صف انحصاری
 *
 * ظرفیت آزادشده به حداکثر ده نفر خبر داده می‌شود و **اولین رزروکننده می‌برد**. صف
 * انحصاری («فقط نفر اول ۳۰ دقیقه فرصت دارد») روی کاغذ عادلانه‌تر است، ولی در عمل
 * یعنی وقتی که کسی جوابش را نمی‌دهد نیم ساعت قفل بماند و بعد به نفر دوم برسد — و
 * ظرفیت آزادشدهٔ دو ساعت مانده به نوبت، نیم ساعت وقت تلف‌کردنی ندارد.
 *
 * در عوض، متن پیامک **اجباراً** این را می‌گوید تا کسی احساس نکند وعده‌ای شکسته شده.
 */
final class WaitlistNotifier
{
    public const MAX_RECIPIENTS = 10;

    public function __construct(
        private readonly WaitlistEntryRepository $entries,
        private readonly DoctorAddressRepository $addresses,
        private readonly SmsService              $sms,
        private readonly JalaliDateService       $jalali,
        private readonly EntityManagerInterface  $em,
    ) {}

    /**
     * @return int تعداد کسانی که خبر شدند
     */
    public function notifyForFreedSlot(Appointment $appointment, ?int $now = null): int
    {
        $service = $appointment->getServiceItem();

        if ($service === null) {
            return 0;
        }

        $matches = $this->entries->findMatching(
            $service,
            $appointment->getSlotStart(),
            $appointment->getAddressId(),
            $now,
        );

        $timezone = $this->timezoneOf($appointment->getAddressId());

        // فیلتر بخش روز **قبل از** بریدن به ده نفر اعمال می‌شود، وگرنه ده جای اول را
        // کسانی پر می‌کنند که این ساعت را نمی‌خواستند و نفر یازدهمِ واقعی خبر نمی‌شود.
        $matches = array_values(array_filter(
            $matches,
            static fn (WaitlistEntry $e): bool => $e->coversDayPart($appointment->getSlotStart(), $timezone),
        ));

        $notified = 0;

        foreach (array_slice($matches, 0, self::MAX_RECIPIENTS) as $entry) {
            if (!$entry->isNotifiable($now)) {
                continue;
            }

            $this->notify($entry, $appointment->getSlotStart());
            $notified++;
        }

        if ($notified > 0) {
            $this->em->flush();
        }

        return $notified;
    }

    /** شعبهٔ ناشناخته به منطقهٔ زمانی پیش‌فرض برمی‌گردد؛ نبودِ شعبه نباید تطبیق را بخواباند. */
    private function timezoneOf(?int $addressId): string
    {
        if ($addressId === null) {
            return DoctorAddress::DEFAULT_TIMEZONE;
        }

        return $this->addresses->find($addressId)?->getTimezone() ?? DoctorAddress::DEFAULT_TIMEZONE;
    }

    private function notify(WaitlistEntry $entry, int $slotStart): void
    {
        $mobile = $entry->getPatientRecord()->getUser()->getMobileNumber();

        if ($mobile !== '') {
            $this->sms->dispatchAsync($mobile, $this->messageFor($entry, $slotStart));
        }

        $entry->markNotified();
    }

    /** جملهٔ «اولین نفر می‌برد» اجباری است — وگرنه انتظارِ اشتباه می‌سازد. */
    private function messageFor(WaitlistEntry $entry, int $slotStart): string
    {
        return sprintf(
            'یک وقت برای «%s» در تاریخ %s آزاد شد. اولین نفری که رزرو کند آن را می‌گیرد.',
            $entry->getServiceItem()->getName(),
            $this->jalali->formatDateTime($slotStart),
        );
    }
}
