<?php

namespace App\Cancellation\Service;

use App\Appointment\Booking\Service\BookingService;
use App\Appointment\Entity\Appointment;
use App\Appointment\Entity\AppointmentEvent;
use App\Auth\Entity\User;
use App\Cancellation\ValueObject\PenaltyResult;
use App\Package\Service\CreditLedgerService;
use App\Settlement\Service\WalletService;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use App\Waitlist\Service\WaitlistNotifier;
use Doctrine\ORM\EntityManagerInterface;

/**
 * لغو نوبت با اعمال سیاست.
 *
 * ترتیب کارها عمدی است: اول اعتبارسنجی، بعد آزادسازی ظرفیت، بعد پول، و آخر اطلاع به
 * لیست انتظار. اگر اطلاع‌رسانی اول بود، ممکن بود ده نفر برای ظرفیتی خبر شوند که هنوز
 * آزاد نشده.
 */
final class CancellationService
{
    public function __construct(
        private readonly PenaltyCalculator      $calculator,
        private readonly BookingService         $booking,
        private readonly WalletService          $wallet,
        private readonly CreditLedgerService    $credits,
        private readonly WaitlistNotifier       $waitlist,
        private readonly EntityManagerInterface $em,
    ) {}

    /**
     * @return array<string, mixed>
     * @throws AppException ۴۲۲ روی نوبت گذشته، ۴۰۹ روی نوبتِ از قبل لغوشده
     */
    public function cancel(Appointment $appointment, string $status, ?User $actor = null, ?int $now = null, ?string $reason = null): array
    {
        $now = $now ?? time();

        if (in_array($appointment->getStatus(), [
            Appointment::STATUS_CANCELLED_BY_USER,
            Appointment::STATUS_CANCELLED_BY_DOCTOR,
        ], true)) {
            // idempotent: همان وضعیت برمی‌گردد، نه یک لغو دوباره.
            throw new AppException(ErrorCodes::ERR_SLOT_TAKEN, 'این نوبت قبلاً لغو شده است', 409);
        }

        if ($appointment->getSlotStart() < $now) {
            // برای گذشته `no_show` یا `completed` معنا دارد، نه لغو.
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'نوبت گذشته لغو نمی‌شود؛ وضعیت عدم حضور یا انجام‌شده را ثبت کنید', 422);
        }

        $penalty = $this->calculator->calculate($appointment, $status, $now);

        $appointment->transitionTo($status);
        $this->em->flush();

        // آزادسازی منابع و بازگشت اعتبار پکیج و جلسهٔ دوره — همه در `cancel` بوکینگ.
        $released = $this->booking->cancel($appointment);

        if (!$penalty->creditRefundable) {
            $this->revokeRefundedCredit($appointment);
        }

        $charged = $this->chargePenalty($appointment, $penalty, $actor);

        $notified = $this->waitlist->notifyForFreedSlot($appointment);

        $this->recordTimelineEntry($appointment, $actor, $reason);

        return [
            'appointment_uuid'    => $appointment->getUuid(),
            'status'              => $appointment->getStatus(),
            'released_resources'  => $released,
            'waitlist_notified'   => $notified,
            'penalty_charged'     => $charged,
        ] + $penalty->toArray();
    }

    /**
     * ردیف تایم‌لاین — همان چیزی که اپراتور در صفحهٔ نوبت می‌بیند.
     *
     * جدا از رویداد دامنه است و جایگزینش نمی‌شود: آن برای مصرف‌کنندهٔ بیرونی است و این
     * برای آدمی که می‌خواهد بداند چه کسی و چرا لغو کرد. بدون این، لغو از مسیر سیاست
     * هیچ ردی در تاریخچهٔ نوبت نمی‌گذاشت.
     */
    private function recordTimelineEntry(Appointment $appointment, ?User $actor, ?string $reason): void
    {
        $event = new AppointmentEvent($appointment, AppointmentEvent::TYPE_CANCELLED, 'نوبت لغو شد');
        $event->setReason($reason);

        if ($actor !== null) {
            $event->setActor($actor->getId(), $actor->getRealName() ?: $actor->getMobileNumber());
        }

        $this->em->persist($event);
        $this->em->flush();
    }

    /**
     * جریمه از کیف پول کسر می‌شود، و اگر موجودی نبود **کسر نمی‌شود**.
     *
     * موجودی ناکافی نباید لغو را شکست بدهد: نوبت باید آزاد شود حتی اگر پول بعداً
     * وصول شود. بدهی مسئلهٔ صورتحساب است، نه یک عدد منفی پنهان در کیف پول.
     */
    private function chargePenalty(Appointment $appointment, PenaltyResult $penalty, ?User $actor): bool
    {
        if ($penalty->penaltyRials <= 0) {
            return false;
        }

        try {
            $this->wallet->withdraw(
                $appointment->getUser(),
                $penalty->penaltyRials,
                $actor,
                'جریمهٔ لغو نوبت',
                null,
                $appointment->getUuid(),
                // بدون این، کلینیک الف جریمهٔ ثبت‌شده در کلینیک ب را می‌بیند.
                $appointment->getEntityType(),
                $appointment->getEntityId(),
            );
        } catch (AppException) {
            return false;
        }

        return true;
    }

    /**
     * سیاستی که اعتبار را برنمی‌گرداند: ردیف `refund` که `BookingService::cancel()`
     * نوشته با یک `adjustment` منفی خنثی می‌شود.
     *
     * حذف ردیف قبلی ممنوع است — دفتر append-only می‌ماند و تاریخچه نشان می‌دهد
     * اعتبار برگشت و بعد طبق سیاست پس گرفته شد.
     */
    private function revokeRefundedCredit(Appointment $appointment): void
    {
        $refund = $this->em->getRepository(\App\Package\Entity\SessionCreditLedger::class)
            ->findOneBy([
                'appointment' => $appointment,
                'kind'        => \App\Package\Entity\SessionCreditLedger::KIND_REFUND,
            ]);

        if ($refund === null) {
            return;
        }

        $this->credits->record(
            $refund->getPatientPackage(),
            \App\Package\Entity\SessionCreditLedger::KIND_ADJUSTMENT,
            -$refund->getDelta(),
            null,
            $refund->getServiceItem(),
            'سیاست لغو: اعتبار این جلسه برنمی‌گردد',
        );
    }
}
