<?php

namespace App\Course\Service;

use App\Appointment\Availability\ValueObject\AvailableSlot;
use App\Appointment\Booking\Service\BookingService;
use App\Appointment\Booking\Service\HoldService;
use App\Appointment\Entity\Appointment;
use App\Appointment\Plan\Service\AppointmentPlanBuilder;
use App\Auth\Entity\User;
use App\Course\Entity\CourseSession;
use App\Course\Entity\TreatmentCourse;
use App\Doctor\Entity\Doctor;
use App\Doctor\Entity\DoctorAddress;
use App\Resource\Entity\ClinicResource;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use Doctrine\ORM\EntityManagerInterface;

/**
 * رزرو یکجای جلسات باقی‌ماندهٔ دوره.
 *
 * سه قاعده که ترتیبشان مهم است:
 *
 * ۱. **همه یا هیچ** — کل حلقه در یک تراکنش. رزرو نیمه‌کاره بدترین حالت است: بیمار فکر
 *    می‌کند دوره‌اش رزرو شده و نصفش نیست.
 * ۲. **لنگر متحرک** — هر جلسه از جلسهٔ قبلی فاصله می‌گیرد، نه از شروع دوره.
 * ۳. **سقف افق جستجو** — جلساتی که بیرون بازهٔ مجاز می‌افتند `planned` می‌مانند و
 *    پیام روشن برمی‌گردد؛ خطا نیستند.
 */
final class CourseBooker
{
    public function __construct(
        private readonly CourseScheduler        $scheduler,
        private readonly AppointmentPlanBuilder $planner,
        private readonly HoldService            $holds,
        private readonly BookingService         $booking,
        private readonly CourseSessionLinker    $linker,
        private readonly EntityManagerInterface $em,
    ) {}

    /**
     * @return array{booked: int, remaining: int, message: string|null}
     */
    public function bookAll(TreatmentCourse $course, DoctorAddress $address, Doctor $doctor, User $operator, ?int $now = null): array
    {
        $now = $now ?? time();

        return $this->em->wrapInTransaction(function () use ($course, $address, $doctor, $operator, $now): array {
            $planned = $course->plannedSessions();

            usort($planned, static fn (CourseSession $a, CourseSession $b): int
                => $a->getSessionNumber() <=> $b->getSessionNumber());

            $anchor  = $course->lastCompletedAt() ?? $now;
            $minDays = $this->scheduler->effectiveMinDays($course, $now);
            $horizon = $now + CourseScheduler::SEARCH_HORIZON_DAYS * 86400;

            $booked  = 0;
            $skipped = 0;

            foreach ($planned as $session) {
                $min   = max($anchor + $minDays * 86400, $now);
                $ideal = $anchor + max($course->getIdealDays(), $minDays) * 86400;
                $max   = $anchor + max($course->getMaxDays(), $minDays) * 86400;

                if ($min > $horizon) {
                    $skipped++;
                    continue;
                }

                $slot = $this->scheduler->slotsFor($course, $address, $min, min($max, $horizon), $ideal, $now)[0] ?? null;

                if ($slot === null) {
                    throw new AppException(
                        ErrorCodes::ERR_VALIDATION_001,
                        sprintf('برای جلسهٔ %d هیچ وقت مناسبی در بازهٔ مجاز پیدا نشد', $session->getSessionNumber()),
                        422,
                        'session_number',
                    );
                }

                $this->bookOne($course, $session, $slot, $address, $doctor, $operator, $now);

                $booked++;
                $anchor = $slot->start;
            }

            return [
                'booked'    => $booked,
                'remaining' => $skipped,
                'message'   => $skipped === 0
                    ? null
                    : sprintf(
                        '%d جلسه بیرون از بازهٔ %d روزهٔ رزرو افتاد و برنامه‌ریزی‌شده ماند؛ نزدیک‌تر که شدیم رزروشان کنید.',
                        $skipped,
                        CourseScheduler::SEARCH_HORIZON_DAYS,
                    ),
            ];
        });
    }

    private function bookOne(
        TreatmentCourse $course,
        CourseSession $session,
        AvailableSlot $slot,
        DoctorAddress $address,
        Doctor $doctor,
        User $operator,
        int $now,
    ): void {
        $plan = $this->planner->build($course->getServiceItem(), [], $address);

        $hold = $this->holds->hold(
            $operator,
            $plan,
            $this->assignmentOf($slot),
            $slot->start,
            $course->getEntityType(),
            $course->getEntityId(),
            $now,
        );

        $appointment = new Appointment($doctor, $course->getPatientRecord()->getUser(), $slot->start, $slot->end);
        $appointment->assignTenantPair($course->getEntityType(), $course->getEntityId());
        $appointment->setServiceItem($course->getServiceItem());
        $appointment->setAddressId($address->getId());

        $this->em->persist($appointment);
        $this->em->flush();

        $this->booking->confirm($hold, $appointment, $now);
        $this->linker->link($session, $appointment);
    }

    /** @return array<string, list<ClinicResource>> */
    private function assignmentOf(AvailableSlot $slot): array
    {
        return $slot->assignment->byRole;
    }
}
