<?php

namespace App\Appointment\Booking\Service;

use App\Appointment\Availability\Entity\ResourceOccupancy;
use App\Appointment\Booking\Entity\AppointmentHold;
use App\Appointment\Booking\Entity\AppointmentSegment;
use App\Appointment\Entity\Appointment;
use App\Package\Service\CreditLedgerService;
use App\Course\Service\CourseSessionLinker;
use App\Package\Service\PackageConsumptionService;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Event\DomainEventPublisher;
use App\Shared\Event\DomainEvents;
use App\Shared\Exception\AppException;
use Doctrine\ORM\EntityManagerInterface;

/**
 * ثبت نهایی از یک رزرو موقت، و آزادسازی هنگام لغو.
 *
 * تبدیل `hold → booked` هیچ منبعی را دوباره نمی‌گیرد: صندلی‌ها از لحظهٔ رزرو موقت
 * گرفته شده‌اند و اینجا فقط برچسبشان عوض می‌شود. اگر ثبت نهایی دوباره رزرو می‌کرد،
 * همان پنجرهٔ مسابقه‌ای که hold حذفش کرده بود برمی‌گشت.
 */
final class BookingService
{
    public function __construct(
        private readonly HoldService                $holds,
        private readonly PackageConsumptionService  $packages,
        private readonly CreditLedgerService        $credits,
        private readonly CourseSessionLinker        $courseSessions,
        private readonly DomainEventPublisher       $events,
        private readonly EntityManagerInterface     $em,
    ) {}

    /**
     * @throws AppException ۴۰۹ روی رزروِ منقضی یا ثبت‌شده
     */
    public function confirm(AppointmentHold $hold, Appointment $appointment, ?int $now = null): Appointment
    {
        $now = $now ?? time();

        if ($hold->isConfirmed()) {
            throw new AppException(ErrorCodes::ERR_SLOT_TAKEN, 'این رزرو قبلاً ثبت شده است', 409);
        }

        if ($hold->isExpired($now)) {
            throw new AppException(ErrorCodes::ERR_HOLD_EXPIRED, 'مهلت رزرو موقت تمام شده است', 409);
        }

        $occupancies = $this->holds->occupanciesOfHold($hold);

        if ($occupancies === []) {
            throw new AppException(ErrorCodes::ERR_HOLD_EXPIRED, 'رزرو موقت دیگر معتبر نیست', 409);
        }

        foreach ($occupancies as $occupancy) {
            $occupancy->markBooked()->setAppointmentId($appointment->getId());
        }

        $this->writeSegments($hold, $appointment);
        $hold->markConfirmed($now);

        // رویداد در همان flushِ ثبت نوبت می‌رود؛ اگر این تراکنش برگردد، رویدادی هم
        // نمی‌ماند که کسی به آن واکنش نشان دهد.
        $this->events->record(
            $appointment->getEntityType(),
            $appointment->getEntityId(),
            DomainEvents::APPOINTMENT_BOOKED,
            ['appointment_uuid' => $appointment->getUuid(), 'hold_uuid' => $hold->getUuid()],
            $now,
        );

        $this->em->flush();

        // مصرف اعتبار **اینجا**ست نه در پیش‌نمایش قیمت: تنها لحظه‌ای که نوبت واقعاً
        // وجود دارد. کلید یکتای دفتر هم تضمین می‌کند اجرای دوباره جلسهٔ دوم نخورد.
        $this->packages->consumeFor($appointment);

        return $appointment;
    }

    /**
     * بخش‌های نوبت از همان `payload` رزرو ساخته می‌شوند، نه از الگوی امروزِ سرویس:
     * الگو ممکن است بین رزرو و ثبت عوض شده باشد و نوبت باید همان چیزی بماند که کاربر
     * دیده و پذیرفته.
     */
    private function writeSegments(AppointmentHold $hold, Appointment $appointment): void
    {
        $segments = $hold->getPayload()['plan']['segments'] ?? [];

        foreach ($segments as $segment) {
            $start = $hold->getStartsAt() + (int) ($segment['offset_minutes'] ?? 0) * 60;
            $end   = $start + (int) ($segment['duration_minutes'] ?? 0) * 60;

            if ($end <= $start) {
                continue;
            }

            $this->em->persist(new AppointmentSegment(
                $appointment,
                (int) ($segment['sequence'] ?? 1),
                (string) ($segment['name'] ?? '—'),
                $start,
                $end,
                (bool) ($segment['patient_present'] ?? true),
            ));
        }
    }

    /**
     * لغو: ردیف‌های اشغال `released` می‌شوند، **حذف فیزیکی نمی‌شوند**.
     * تاریخچه ورودی گزارش بهره‌وری است.
     */
    public function cancel(Appointment $appointment): int
    {
        $occupancies = $this->em->getRepository(ResourceOccupancy::class)
            ->findBy(['appointmentId' => $appointment->getId()]);

        $this->holds->release($occupancies);

        // ردیف `consume` **حذف نمی‌شود**؛ بازگشت یک ردیف تازه است تا تاریخچه بماند.
        $this->credits->refund($appointment);

        // جلسهٔ دوره به `planned` برمی‌گردد؛ بقیهٔ جلسات دست‌نخورده می‌مانند.
        $this->courseSessions->unlink($appointment);

        $this->events->recordAndFlush(
            $appointment->getEntityType(),
            $appointment->getEntityId(),
            DomainEvents::APPOINTMENT_CANCELLED,
            ['appointment_uuid' => $appointment->getUuid(), 'released_resources' => count($occupancies)],
        );

        return count($occupancies);
    }

    /** رزروِ منقضی: همان آزادسازی، ولی از سمت رزرو موقت. */
    public function releaseHold(AppointmentHold $hold): int
    {
        $occupancies = $this->holds->occupanciesOfHold($hold);
        $this->holds->release($occupancies);

        return count($occupancies);
    }
}
