<?php

namespace App\Cancellation\Service;

use App\Appointment\Entity\Appointment;
use App\Cancellation\Entity\CancellationPolicy;
use App\Cancellation\Repository\CancellationPolicyRepository;
use App\Cancellation\ValueObject\PenaltyResult;
use App\Payment\Entity\Payment;
use Doctrine\ORM\EntityManagerInterface;

/**
 * محاسبهٔ جریمهٔ لغو — خالص، بدون هیچ نوشتنی.
 *
 * همین کلاس هم پیش‌نمایش را می‌دهد و هم عددی که واقعاً کسر می‌شود؛ دو مسیر جدا یعنی
 * بالاخره روزی دو عدد متفاوت.
 */
final class PenaltyCalculator
{
    public function __construct(
        private readonly CancellationPolicyRepository $policies,
        private readonly EntityManagerInterface       $em,
    ) {}

    public function calculate(Appointment $appointment, string $cancelledBy, ?int $now = null): PenaltyResult
    {
        // ⚠️ این شرط باید **اولین** خط باشد. اگر بعد از محاسبهٔ پنجرهٔ زمانی بیاید، یک
        // refactor می‌تواند ترتیب را عوض کند و کلینیک از بیمار برای لغو خودش جریمه بگیرد.
        if ($cancelledBy === Appointment::STATUS_CANCELLED_BY_DOCTOR) {
            return PenaltyResult::free(true, ['لغو توسط کلینیک هرگز جریمه ندارد']);
        }

        $now    = $now ?? time();
        $policy = $this->policies->resolve(
            $appointment->getEntityType(),
            $appointment->getEntityId(),
            $appointment->getServiceItem(),
        );

        if ($policy === null) {
            return PenaltyResult::free(true, ['برای این محیط سیاست لغو تعریف نشده است']);
        }

        $hoursLeft = ($appointment->getSlotStart() - $now) / 3600;

        if ($hoursLeft >= $policy->getFreeWindowHours()) {
            return PenaltyResult::free(true);
        }

        $paid    = $this->paidRials($appointment);
        $penalty = $this->rawPenalty($policy, $appointment, $paid);

        $notes = [];

        // جریمهٔ بیشتر از پرداختی یعنی بدهی، و بدهی مسئلهٔ صورتحساب است نه لغو.
        if ($penalty > $paid) {
            $notes[] = $paid === 0
                ? 'این نوبت پرداختی نداشته، پس جریمه‌ای کسر نمی‌شود'
                : 'جریمه تا سقف مبلغ پرداختی کاهش یافت';
            $penalty = $paid;
        }

        return new PenaltyResult(
            penaltyRials: $penalty,
            depositRefundable: $policy->isDepositRefundable(),
            creditRefundable: $policy->isCreditRefundable(),
            withinFreeWindow: false,
            notes: $notes,
        );
    }

    private function rawPenalty(CancellationPolicy $policy, Appointment $appointment, int $paid): int
    {
        return match ($policy->getPenaltyMode()) {
            CancellationPolicy::MODE_PERCENT => (int) floor($this->baseFor($appointment, $paid) * $policy->getPenaltyValue() / 100),
            CancellationPolicy::MODE_FIXED   => $policy->getPenaltyValue(),
            default                          => 0,
        };
    }

    /**
     * مبنای درصد: مبلغ ثبت‌شدهٔ نوبت، و اگر نبود آنچه واقعاً پرداخت شده.
     *
     * درصدِ «قیمت امروزِ سرویس» غلط است: بیمار روی قیمت آن روز توافق کرده.
     */
    private function baseFor(Appointment $appointment, int $paid): int
    {
        return (int) ($appointment->getVisitPriceRials() ?? $paid);
    }

    /** جمع پرداخت‌های موفق همین نوبت. */
    public function paidRials(Appointment $appointment): int
    {
        return (int) $this->em->createQueryBuilder()
            ->select('COALESCE(SUM(p.amountRials), 0)')
            ->from(Payment::class, 'p')
            ->where('p.appointment = :appointment')
            ->andWhere('p.status = :status')
            ->setParameter('appointment', $appointment)
            ->setParameter('status', Payment::STATUS_SUCCESS)
            ->getQuery()
            ->getSingleScalarResult();
    }
}
