<?php

namespace App\Package\Service;

use App\Appointment\Entity\Appointment;
use App\Auth\Entity\User;
use App\ClinicService\Entity\ServiceItem;
use App\Package\Entity\PatientPackage;
use App\Package\Entity\SessionCreditLedger;
use App\Package\Repository\SessionCreditLedgerRepository;
use App\Shared\Event\DomainEventPublisher;
use App\Shared\Event\DomainEvents;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\DBAL\LockMode;

/**
 * تنها نویسندهٔ دفتر اعتبار.
 *
 * هیچ کلاس دیگری نباید در `session_credit_ledger` بنویسد؛ اگر بنویسد، قواعد این کلاس
 * (مصرف یکتا per نوبت، مانده‌ای که منفی نمی‌شود) دور زده می‌شوند و دفتر همان چیزی
 * می‌شود که قرار بود نباشد: عددی که کسی نمی‌داند از کجا آمده.
 */
final class CreditLedgerService
{
    public function __construct(
        private readonly SessionCreditLedgerRepository $ledger,
        private readonly DomainEventPublisher          $events,
        private readonly EntityManagerInterface        $em,
    ) {}

    /** مانده = جمع delta ها. هیچ ستون ذخیره‌شده‌ای نیست. */
    public function balance(PatientPackage $package): int
    {
        return $this->ledger->sumDelta($package);
    }

    public function record(
        PatientPackage $package,
        string $kind,
        int $delta,
        ?Appointment $appointment = null,
        ?ServiceItem $service = null,
        ?string $reason = null,
        ?User $by = null,
        bool $flush = true,
    ): SessionCreditLedger {
        $row = new SessionCreditLedger($package, $kind, $delta, $appointment, $service, $reason, $by);

        $this->em->persist($row);

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

        return $row;
    }

    /**
     * مصرف یک جلسه — `false` یعنی «اعتباری نبود»، نه خطا.
     *
     * بیمار بدون اعتبار باید بتواند نقدی بپردازد؛ استثنا پرتاب کردن اینجا یعنی
     * رزروِ کاملاً معتبر شکست بخورد.
     *
     * قفل بدبینانه روی همان یک ردیف پکیج است. برخلاف اسلات‌های تسک ۰۷ — که نرخ رقابت
     * بالا و ده‌ها ردیف درگیر دارند — اینجا یک بیمار و یک پکیج است، پس هزینهٔ قفل
     * ناچیز و سادگی‌اش برنده است.
     */
    public function consume(PatientPackage $package, Appointment $appointment, ?ServiceItem $service = null): bool
    {
        // قفل بدون تراکنش معنا ندارد؛ خواندن و نوشتن باید در یک واحد اتمی باشند
        // وگرنه دو درخواست هم‌زمان هر دو ماندهٔ ۱ را می‌بینند.
        return $this->em->wrapInTransaction(function () use ($package, $appointment, $service): bool {
            $locked = $this->em->find(PatientPackage::class, $package->getId(), LockMode::PESSIMISTIC_WRITE);

            if ($locked === null || $locked->isExpired()) {
                return false;
            }

            // همین نوبت قبلاً مصرف کرده؟ `confirm` idempotent است و اجرای دومش نباید
            // جلسهٔ دوم بخورد. بررسی **پیش از** درج است نه گرفتنِ استثنا: نقض کلید
            // یکتا در Doctrine خودِ EntityManager را می‌بندد و بقیهٔ همان request را
            // هم می‌سوزاند. کلید یکتا آخرین خط دفاع می‌ماند، نه مسیر عادی.
            if ($this->ledger->findForAppointment($appointment, SessionCreditLedger::KIND_CONSUME) !== null) {
                return true;
            }

            if ($this->balance($locked) <= 0) {
                return false;
            }

            $this->record($locked, SessionCreditLedger::KIND_CONSUME, -1, $appointment, $service);

            $this->events->recordAndFlush(
                $locked->getEntityType(),
                $locked->getEntityId(),
                DomainEvents::CREDIT_CONSUMED,
                ['patient_package_uuid' => $locked->getUuid(), 'appointment_uuid' => $appointment->getUuid()],
            );

            return true;
        });
    }

    /**
     * بازگشت اعتبار هنگام لغو — ردیف `consume` **حذف نمی‌شود**.
     *
     * فعلاً هر لغوی اعتبار را کامل برمی‌گرداند. سیاست واقعی (لغو دیرهنگام، جریمه،
     * عدم‌حضور) کارِ تسک ۱۳ است و همان‌جا این متد یک پارامتر سیاست می‌گیرد؛ پرچم
     * نیم‌کاره اینجا فقط رفتاری می‌ساخت که هیچ‌کس تنظیمش نمی‌کند.
     *
     * @return bool `false` یعنی این نوبت اصلاً از پکیج مصرف نکرده بود
     */
    public function refund(Appointment $appointment, ?User $by = null): bool
    {
        $consumed = $this->ledger->findForAppointment($appointment, SessionCreditLedger::KIND_CONSUME);

        if ($consumed === null) {
            return false;
        }

        if ($this->ledger->findForAppointment($appointment, SessionCreditLedger::KIND_REFUND) !== null) {
            return false;
        }

        $this->record(
            $consumed->getPatientPackage(),
            SessionCreditLedger::KIND_REFUND,
            -$consumed->getDelta(),
            $appointment,
            $consumed->getServiceItem(),
            'بازگشت اعتبار با لغو نوبت',
            $by,
        );

        $this->events->recordAndFlush(
            $consumed->getPatientPackage()->getEntityType(),
            $consumed->getPatientPackage()->getEntityId(),
            DomainEvents::CREDIT_REFUNDED,
            [
                'patient_package_uuid' => $consumed->getPatientPackage()->getUuid(),
                'appointment_uuid'     => $appointment->getUuid(),
            ],
        );

        return true;
    }

    /** @return SessionCreditLedger[] */
    public function history(PatientPackage $package): array
    {
        return $this->ledger->historyFor($package);
    }
}
