<?php

namespace App\Appointment\Service;

use App\Appointment\Entity\Appointment;
use App\Appointment\Repository\AppointmentRepository;
use App\Appointment\ValueObject\ServiceBookingDuration;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use Doctrine\ORM\OptimisticLockException;

/**
 * جابه‌جایی سرویس‌آگاهِ یک نوبت: کلاینت **فقط زمان شروع** می‌فرستد و مدت را سرور از
 * سرویس‌های نوبت حساب می‌کند.
 *
 * `PATCH /appointment/{uuid}` هم مدت را اعتبارسنجی می‌کند، ولی کلاینت باید `slot_end`
 * درست را از قبل بداند. اینجا آن دانش لازم نیست — و همین تفاوت است که ورودی دستیِ
 * ساعت را از فرم ویرایش حذف می‌کند.
 */
final class ServiceRescheduleService
{
    public function __construct(
        private readonly AppointmentRepository    $appointments,
        private readonly ServiceBookingCalculator $calculator,
        private readonly SlotCalculatorService    $slots,
    ) {}

    /**
     * @param string[]|null              $serviceUuids null = همان سرویس‌های فعلی نوبت
     * @param array<string, int|string>  $durationOverrides
     *
     * @throws AppException|OptimisticLockException
     */
    public function reschedule(
        Appointment $appointment,
        int $start,
        ?array $serviceUuids = null,
        array $durationOverrides = [],
        bool $forManagement = false,
        ?int $expectedVersion = null,
    ): ServiceBookingDuration {
        $doctor = $appointment->getDoctor();
        $clinic = $appointment->getClinic();

        if (!$this->calculator->isServiceMode($doctor, $clinic)) {
            throw new AppException(
                ErrorCodes::ERR_APPOINTMENT_004,
                'این نوبت در حالت نوبت‌دهی سرویسی نیست',
                422,
            );
        }

        // نوبت رزرو زمان ندارد؛ تبدیلش به نوبت زمان‌دار کار `PATCH` با `is_reserve: false` است.
        if ($appointment->isReserve()) {
            throw new AppException(
                ErrorCodes::ERR_APPOINTMENT_004,
                'نوبت رزرو زمان ندارد؛ برای تعیین زمان از ویرایش نوبت استفاده کنید',
                422,
            );
        }

        if ($start < time()) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'زمان انتخابی در گذشته است', 422, 'start');
        }

        $requested = $serviceUuids ?? $appointment->currentServiceUuids();
        if ($requested === []) {
            throw new AppException(
                ErrorCodes::ERR_VALIDATION_002,
                'این نوبت سرویسی ندارد؛ ابتدا سرویس آن را ثبت کنید',
                422,
                'service_item_uuids',
            );
        }

        $duration = $this->calculator->calculate(
            $doctor,
            $clinic,
            $requested,
            $durationOverrides,
            // سرویس‌های موجودِ نوبت ممکن است غیرفعال شده باشند؛ نوبت نباید قفل شود.
            // ولی فهرستِ صریحِ درخواست باید همه فعال باشند.
            allowInactive: $serviceUuids === null,
        );

        $this->assertStartIsOffered($appointment, $start, $duration->totalMinutes, $forManagement);

        $appointment->rescheduleTo($start, $duration->endFor($start));
        $appointment->replaceServiceItems($duration->serviceItems);
        $appointment->setServiceDuration($duration->totalMinutes, $duration->bufferMinutes);

        if ($expectedVersion !== null) {
            $this->appointments->saveWithLock($appointment, $expectedVersion);
        } else {
            $this->appointments->save($appointment);
        }

        return $duration;
    }

    /**
     * زمان باید واقعاً یکی از زمان‌های پیشنهادی باشد، نه فقط «اشغال نیست».
     *
     * `isSlotTaken()` تنها تداخل با نوبت دیگر را می‌گوید؛ `getServiceStartTimes()` علاوه
     * بر آن شیفت، تعطیلی، `date_override`، پنجرهٔ رزرو و بافر را هم اعمال می‌کند. با شرط
     * اول، منشی می‌توانست نوبت را ساعت ۳ بامداد بگذارد.
     */
    private function assertStartIsOffered(
        Appointment $appointment,
        int $start,
        int $totalMinutes,
        bool $forManagement,
    ): void {
        $offered = $this->slots->getServiceStartTimes(
            $appointment->getDoctor(),
            date('Y-m-d', $start),
            $totalMinutes,
            $appointment->getClinic(),
            $forManagement,
            $appointment->getId(),
        );

        if (!in_array($start, array_map('intval', array_column($offered, 'start')), true)) {
            throw new AppException(
                ErrorCodes::ERR_APPOINTMENT_001,
                'این زمان برای مدت انتخابی در دسترس نیست',
                422,
                'start',
            );
        }
    }
}
