<?php

namespace App\Patient\Service;

use App\Appointment\Entity\Appointment;
use App\Appointment\Service\AppointmentInsuranceService;
use App\Auth\Repository\UserRepository;
use App\Billing\Service\BillingCalculator;
use App\Billing\ValueObject\Money;
use App\ClinicService\Repository\ServiceItemRepository;
use App\Clinic\Repository\ClinicRepository;
use App\Insurance\Enum\ServiceCategory;
use App\Insurance\Repository\EntityInsurancePricingRepository;
use App\Insurance\Service\TenantInsuranceService;
use App\Inventory\Repository\InventoryItemRepository;
use App\Inventory\Repository\InventoryPackageRepository;
use App\Doctor\Repository\DoctorAddressRepository;
use App\Auth\Entity\User;
use App\Patient\Entity\PatientRecord;
use App\Patient\Entity\PatientSession;
use App\Patient\Entity\SessionConsumable;
use App\Patient\Entity\SessionPayment;
use App\Patient\Entity\SessionService;
use App\Patient\Repository\PatientRecordRepository;
use App\Patient\Repository\PatientSessionRepository;
use App\Patient\Repository\SessionConsumableRepository;
use App\Patient\Repository\SessionPaymentRepository;
use App\Patient\Repository\SessionServiceRepository;
use App\Settlement\Service\WalletService;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use App\Staff\Repository\ClinicStaffRepository;
use App\Subscription\Service\SubscriptionService;
use Psr\Log\LoggerInterface;

class PatientService
{
    public function __construct(
        private readonly PatientRecordRepository  $recordRepo,
        private readonly PatientSessionRepository $sessionRepo,
        private readonly SessionServiceRepository $sessionServiceRepo,
        private readonly SessionPaymentRepository $sessionPaymentRepo,
        private readonly SessionConsumableRepository $sessionConsumableRepo,
        private readonly ServiceItemRepository    $serviceItemRepo,
        private readonly InventoryItemRepository  $inventoryItemRepo,
        private readonly InventoryPackageRepository $inventoryPackageRepo,
        private readonly ClinicStaffRepository    $staffRepo,
        private readonly UserRepository           $userRepo,
        private readonly SubscriptionService      $subscriptionService,
        private readonly DoctorAddressRepository  $addressRepo,
        private readonly ClinicRepository         $clinicRepo,
        private readonly TenantInsuranceService   $tenantInsuranceService,
        private readonly AppointmentInsuranceService $appointmentInsurance,
        private readonly BillingCalculator        $billingCalculator,
        private readonly \App\Billing\Service\SessionBillingService $sessionBilling,
        private readonly WalletService            $walletService,
        private readonly EntityInsurancePricingRepository $pricingRepo,
        private readonly \App\Discount\Service\DiscountEngine $discountEngine,
        private readonly \App\Patient\Repository\SessionAuditLogRepository $auditRepo,
        private readonly \App\Shared\Tenant\TenantOwnershipChecker $tenantOwnership,
        private readonly LoggerInterface $logger,
    ) {}

    /** ثبت یک رکورد تاریخچه‌ی تغییر مالی/خدماتی روی مراجعه. */
    public function logSessionChange(PatientSession $session, string $field, string $operation, ?string $old, ?string $new, ?User $actor, ?string $note = null): void
    {
        $log = new \App\Patient\Entity\SessionAuditLog($session, $field, $operation);
        $log->setActor($actor?->getId(), $this->walletService->resolveActorName($actor));
        $log->setValues($old, $new);
        $log->setNote($note);
        $this->auditRepo->save($log);
    }

    /** خلاصه‌ی خوانا از سرویس‌های یک مراجعه: «نام ×تعداد، ...». */
    private function servicesSummary(PatientSession $session): string
    {
        $parts = array_map(
            fn(SessionService $s) => ($s->toArray()['service_name'] ?? '?') . ' ×' . $s->getQuantity(),
            $session->getServices()->toArray(),
        );
        return $parts === [] ? '—' : implode('، ', $parts);
    }

    private function consumablesSummary(PatientSession $session): string
    {
        $parts = array_map(
            fn(SessionConsumable $c) => ($c->toArray()['item_name'] ?? '?') . ' ×' . $c->getQuantity(),
            $session->getConsumables()->toArray(),
        );
        return $parts === [] ? '—' : implode('، ', $parts);
    }

    /**
     * تفکیک سهم بیمه‌ها و سهم بیمار برای یک مراجعه.
     *
     * ویزیت خدمتِ سرپایی است و با قاعده‌ی قرارداد بیمه (coverageRule) محاسبه می‌شود؛ هر خدمت
     * با قاعده‌ی پوشش نوعِ خودش (coverageRuleForService بر پایه‌ی service_category همان خدمت)
     * محاسبه می‌شود — هر دو از طریق BillingCalculator، همان مسیری
     * که InvoiceService برای صدور فاکتور استفاده می‌کند. تنها منبع محاسبه همین است تا مبلغ
     * صفحه‌ی پرداخت و فاکتور نتوانند از هم واگرا شوند.
     *
     * @param array<array{item_id: int, price_rials: int}> $serviceItems قیمت کل هر ردیف (با احتساب تعداد)
     * @param ?ServiceCategory $visitCategory نوع خدمتِ ویزیت؛ null → سرپایی
     * @return array{services_total_rials: int, gross_total_rials: int, base_insurance_rials: int, supplementary_insurance_rials: int, patient_share_rials: int, final_price_rials: int}
     */
    public function calculateFinalPrice(
        int $visitPrice,
        array $serviceItems,
        string $entityType = 'doctor',
        int $entityId = 0,
        ?int $baseInsuranceId = null,
        ?int $suppInsuranceId = null,
        ?ServiceCategory $visitCategory = null,
    ): array {
        $visitCategory ??= ServiceCategory::Outpatient;
        $baseShare    = 0;
        $suppShare    = 0;
        $patientShare = 0;

        if ($visitPrice > 0) {
            $visit = $this->billingCalculator->calculateItem(
                new Money($visitPrice),
                $this->tenantInsuranceService->coverageRule($entityType, $entityId, $baseInsuranceId, $visitCategory),
                $this->tenantInsuranceService->coverageRule($entityType, $entityId, $suppInsuranceId, $visitCategory),
            );
            $baseShare    += $visit->baseInsuranceRials;
            $suppShare    += $visit->supplementaryRials;
            $patientShare += $visit->patientRials;
        }

        $servicesTotal = 0;
        foreach ($serviceItems as $svc) {
            $servicesTotal += $svc['price_rials'];

            $line = $this->billingCalculator->calculateItem(
                new Money($svc['price_rials']),
                $this->tenantInsuranceService->coverageRuleForService($entityType, $entityId, $baseInsuranceId, $svc['item_id']),
                $this->tenantInsuranceService->coverageRuleForService($entityType, $entityId, $suppInsuranceId, $svc['item_id']),
            );
            $baseShare    += $line->baseInsuranceRials;
            $suppShare    += $line->supplementaryRials;
            $patientShare += $line->patientRials;
        }

        return [
            'services_total_rials'          => $servicesTotal,
            'gross_total_rials'             => $visitPrice + $servicesTotal,
            'base_insurance_rials'          => $baseShare,
            'supplementary_insurance_rials' => $suppShare,
            'patient_share_rials'           => $patientShare,
            'final_price_rials'             => $patientShare,
        ];
    }

    /**
     * درصد پوشش مؤثر ویزیت (سرپایی) از همان زنجیره‌ی resolve محاسبه —
     * snapshot نمایشی روی مراجعه، نه ورودی محاسبه.
     */
    private function contractPercent(
        string $entityType,
        int $entityId,
        ?int $insuranceId,
        ?ServiceCategory $category = null,
    ): float {
        return $this->tenantInsuranceService
            ->coverageRule($entityType, $entityId, $insuranceId, $category ?? ServiceCategory::Outpatient)
            ->coveragePercent;
    }

    /**
     * پرونده و مراجعهٔ خودکار برای یک نوبت قطعی‌شده.
     *
     * محیط رزرو تعیین‌کننده است: کلینیک، یا مطب شخصی پزشک — هرگز هر دو. دو پرونده
     * برای یک نوبت یعنی درآمد یک ویزیت دو بار شمرده می‌شود.
     */
    public function autoCreateOnAppointmentConfirm(Appointment $appointment): ?PatientSession
    {
        $clinic = $appointment->getClinic();

        [$entityType, $entityId] = $clinic !== null
            ? ['clinic', (int) $clinic->getId()]
            : ['doctor', (int) $appointment->getDoctor()->getId()];

        return $this->autoCreateForEntity($entityType, $entityId, $appointment, $entityId);
    }

    /** مراجعهٔ ساخته‌شده یا موجود؛ null یعنی این tenant قابلیت پرونده را ندارد. */
    private function autoCreateForEntity(string $entityType, int $entityId, Appointment $appointment, int $createdById): ?PatientSession
    {
        if (!$this->subscriptionService->hasFeature($entityType, $entityId, 'patient_records')) {
            // به‌زور پرونده نمی‌سازیم، ولی بی‌نشانه هم رد نمی‌شویم: بدون این لاگ،
            // «چرا این نوبت پرونده ندارد» غیرقابل‌تشخیص است.
            $this->logger->info('Skipped auto-creating the patient record: the tenant has no patient_records feature', [
                'entity_type'      => $entityType,
                'entity_id'        => $entityId,
                'appointment_uuid' => $appointment->getUuid(),
            ]);

            return null;
        }

        $existing = $this->sessionRepo->findByAppointmentAndEntity($appointment, $entityType, $entityId);
        if ($existing !== null) {
            return $existing;
        }

        $patient = $appointment->getUser();

        $record = $this->recordRepo->findByEntityAndUser($entityType, $entityId, $patient);
        if ($record === null) {
            $record = new PatientRecord($entityType, $entityId, $patient, 'system', $createdById);
            $this->recordRepo->save($record);
        }

        $session = new PatientSession($record, $appointment);

        // زمان مراجعه = زمان واقعی نوبت.
        $session->setSessionAt($appointment->getSlotStart());

        // هزینه ویزیت: از نوبت، در نبود آن از «قیمت ویزیت آزاد» تنظیمات همین tenant.
        $visitPrice = (int) ($appointment->getVisitPriceRials()
            ?? $this->pricingRepo->findOneForInsurance($entityType, $entityId, null)?->getPatientShareRials()
            ?? 0);
        $session->setVisitPriceRials($visitPrice);

        // خطوط هزینه‌ی سرویس (قیمت snapshot از خود سرویس، بدون ورود دستی).
        $servicesTotal = 0;
        $lines = [];
        foreach ($appointment->getServiceItems() as $item) {
            $line = new SessionService($session, $item, null, 1);
            $lines[] = $line;
            $servicesTotal += $line->getLineTotalRials();
        }

        $session->setServicesTotalRials($servicesTotal);

        // بیمهٔ انتخاب‌شده روی نوبت مبنای محاسبه است؛ نوبتِ بدون بیمه مثل قبل کاملاً
        // سهم بیمار می‌ماند (coverageRule برای insuranceId=null، notCovered می‌دهد).
        $category = $this->appointmentInsurance->effectiveCategory($appointment);
        $session->setInsuranceServiceCategory($category);
        $session->setInsuranceBaseId($appointment->getInsuranceBaseId());
        $session->setInsuranceSupplementaryId($appointment->getInsuranceSupplementaryId());
        $session->setBaseInsuranceDiscountPercent(
            $this->contractPercent($entityType, $entityId, $appointment->getInsuranceBaseId(), $category)
        );
        $session->setSupplementaryDiscountPercent(
            $this->contractPercent($entityType, $entityId, $appointment->getInsuranceSupplementaryId(), $category)
        );

        $shares = $this->calculateFinalPrice(
            $visitPrice,
            array_map(
                fn(SessionService $line) => ['item_id' => $line->getServiceItem()->getId(), 'price_rials' => $line->getLineTotalRials()],
                $lines,
            ),
            $entityType,
            $entityId,
            $appointment->getInsuranceBaseId(),
            $appointment->getInsuranceSupplementaryId(),
            $category,
        );

        $session->applyShares(
            $shares['gross_total_rials'],
            $shares['base_insurance_rials'],
            $shares['supplementary_insurance_rials'],
            $shares['patient_share_rials'],
        );

        $this->sessionRepo->save($session);

        foreach ($lines as $line) {
            $this->sessionServiceRepo->save($line);
            $session->addService($line);
        }

        // مراجعهٔ بیمه‌دارِ آمده از نوبت هم باید صورتحساب و مطالبه بگیرد؛ بدون این،
        // نوبت‌های قطعی‌شده هرگز به داشبورد مطالبات نمی‌رسیدند.
        $this->sessionBilling->ensureFinalizedInvoice($session, $entityType, $entityId);

        return $session;
    }

    public function createSession(
        PatientRecord $record,
        array $data,
        string $entityType,
        int $entityId
    ): PatientSession {
        $freeVisitRow = $this->pricingRepo->findOneForInsurance($entityType, $entityId, null);
        if (($freeVisitRow?->isRequireVisitPrice() ?? false) && (int) ($data['visit_price_rials'] ?? 0) <= 0) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'هزینه ویزیت الزامی است', 422, 'visit_price_rials');
        }

        $session = new PatientSession($record);

        if (!empty($data['appointment_uuid'])) {
            // appointment را از بیرون resolve می‌کنند و session را ست می‌کنند
        }

        $session->setInsuranceBaseId(isset($data['insurance_base_id']) ? (int) $data['insurance_base_id'] : null);
        $session->setInsuranceSupplementaryId(isset($data['insurance_supplementary_id']) ? (int) $data['insurance_supplementary_id'] : null);
        $session->setInsuranceServiceCategory(ServiceCategory::tryFromValue($data['insurance_service_category'] ?? null));
        $session->setVisitPriceRials((int) ($data['visit_price_rials'] ?? 0));
        $visitCategory = $session->getInsuranceServiceCategory();
        $session->setBaseInsuranceDiscountPercent($this->contractPercent($entityType, $entityId, $session->getInsuranceBaseId(), $visitCategory));
        $session->setSupplementaryDiscountPercent($this->contractPercent($entityType, $entityId, $session->getInsuranceSupplementaryId(), $visitCategory));
        $session->setPaymentMethod($data['payment_method'] ?? 'pending');
        $session->setNotes($data['notes'] ?? null);

        // زمان پذیرش (اختیاری — پیش‌فرض زمان ثبت)
        if (!empty($data['session_at'])) {
            $session->setSessionAt((int) $data['session_at']);
        }

        // پکیج مصرفی (اختیاری، فقط مرجع؛ باید متعلق به همین tenant باشد)
        if (!empty($data['inventory_package_uuid'])) {
            $package = $this->inventoryPackageRepo->findByUuid((string) $data['inventory_package_uuid']);
            if ($package !== null && $package->getEntityType() === $entityType && $package->getEntityId() === $entityId) {
                $session->setInventoryPackage($package);
            }
        }

        // جمع‌آوری service items (با احتساب تعداد). سرویسِ محیط دیگر نادیده گرفته
        // می‌شود، وگرنه قیمتش وارد محاسبهٔ فاکتور این محیط می‌شد.
        $serviceItemsData = [];
        foreach (($data['services'] ?? []) as $svc) {
            $item = $this->serviceItemRepo->findByUuid($svc['service_item_uuid'] ?? '');
            if ($this->tenantOwnership->belongsToPair($entityType, $entityId, $item)) {
                $qty = max(1, (int) ($svc['quantity'] ?? 1));
                $serviceItemsData[] = ['item_id' => $item->getId(), 'price_rials' => $item->getPriceRials() * $qty];
            }
        }

        $priceCalc = $this->calculateFinalPrice(
            $session->getVisitPriceRials(),
            $serviceItemsData,
            $entityType,
            $entityId,
            $session->getInsuranceBaseId(),
            $session->getInsuranceSupplementaryId(),
            $visitCategory,
        );

        $session->setServicesTotalRials($priceCalc['services_total_rials']);

        // کالاهای مصرفی: بدون پوشش بیمه — تمام مبلغ سهم بیمار است.
        // فقط آیتم‌های متعلق به همین tenant پذیرفته می‌شوند؛ بقیه بی‌صدا رد می‌شوند (هم‌رفتار با services).
        $consumableRows = [];
        $consumablesTotal = 0;
        foreach (($data['consumables'] ?? []) as $row) {
            $item = $this->inventoryItemRepo->findByUuid((string) ($row['inventory_item_uuid'] ?? ''));
            if ($item === null || $item->getEntityType() !== $entityType || $item->getEntityId() !== $entityId) {
                continue;
            }
            $qty = max(1, (int) ($row['quantity'] ?? 1));
            $consumableRows[]  = ['item' => $item, 'quantity' => $qty];
            $consumablesTotal += $item->getPrice() * $qty;
        }

        // کالاهای مصرفی پوشش بیمه ندارند: مستقیم به سهم بیمار و به جمع کل اضافه می‌شوند.
        $session->applyShares(
            $priceCalc['gross_total_rials'] + $consumablesTotal,
            $priceCalc['base_insurance_rials'],
            $priceCalc['supplementary_insurance_rials'],
            $priceCalc['patient_share_rials'] + $consumablesTotal,
        );

        $this->sessionRepo->save($session);

        // ثبت session consumables
        foreach ($consumableRows as $row) {
            $sc = new SessionConsumable($session, $row['item'], $row['quantity']);
            $this->sessionConsumableRepo->save($sc);
            $session->addConsumable($sc);
        }

        // ثبت session services — سرویس و پرسنلِ محیط دیگر نادیده گرفته می‌شوند،
        // همان قاعده‌ای که کالای مصرفی بالاتر رعایت می‌کند.
        foreach (($data['services'] ?? []) as $svc) {
            $item = $this->serviceItemRepo->findByUuid($svc['service_item_uuid'] ?? '');
            if (!$this->tenantOwnership->belongsToPair($entityType, $entityId, $item)) {
                continue;
            }

            $staff = !empty($svc['staff_uuid']) ? $this->staffRepo->findByUuid($svc['staff_uuid']) : null;
            if ($staff !== null && !$this->tenantOwnership->belongsToPair($entityType, $entityId, $staff)) {
                $staff = null;
            }
            $qty   = max(1, (int) ($svc['quantity'] ?? 1));
            $ss    = new SessionService($session, $item, $staff, $qty);
            $this->sessionServiceRepo->save($ss);
            $session->addService($ss);
        }

        return $session;
    }

    /**
     * ویرایش سرویس‌ها/کالاها/قیمت ویزیت/بیمه‌ی یک مراجعه پس از ثبت، با بازمحاسبه‌ی
     * مجموع‌ها و ثبت تاریخچه (audit) برای هر فیلد تغییرکرده.
     */
    public function updateSessionServices(PatientSession $session, array $data, string $entityType, int $entityId, User $actor): PatientSession
    {
        $before = [
            'visit_price_rials'    => (string) $session->getVisitPriceRials(),
            'services'             => $this->servicesSummary($session),
            'consumables'          => $this->consumablesSummary($session),
            'services_total_rials' => (string) $session->getServicesTotalRials(),
            'final_price_rials'    => (string) $session->getFinalPriceRials(),
        ];

        // فیلدهای ساده
        if (array_key_exists('visit_price_rials', $data)) { $session->setVisitPriceRials((int) $data['visit_price_rials']); }
        if (array_key_exists('insurance_base_id', $data)) { $session->setInsuranceBaseId($data['insurance_base_id'] !== null ? (int) $data['insurance_base_id'] : null); }
        if (array_key_exists('insurance_supplementary_id', $data)) { $session->setInsuranceSupplementaryId($data['insurance_supplementary_id'] !== null ? (int) $data['insurance_supplementary_id'] : null); }
        if (array_key_exists('insurance_service_category', $data)) { $session->setInsuranceServiceCategory(ServiceCategory::tryFromValue($data['insurance_service_category'])); }
        if (array_key_exists('base_insurance_discount_percent', $data)) { $session->setBaseInsuranceDiscountPercent((float) $data['base_insurance_discount_percent']); }
        if (array_key_exists('supplementary_discount_percent', $data)) { $session->setSupplementaryDiscountPercent((float) $data['supplementary_discount_percent']); }
        if (array_key_exists('notes', $data)) { $session->setNotes($data['notes'] !== null ? (string) $data['notes'] : null); }
        if (!empty($data['session_at'])) { $session->setSessionAt((int) $data['session_at']); }

        // جایگزینی سرویس‌ها (اگر ارسال شده)
        if (array_key_exists('services', $data)) {
            foreach ($session->getServices()->toArray() as $old) {
                $this->sessionServiceRepo->remove($old, false);
                $session->getServices()->removeElement($old);
            }
            foreach (($data['services'] ?? []) as $svc) {
                // سرویس و پرسنل با uuid از بدنهٔ درخواست می‌آیند و روی SessionService
                // ذخیره می‌شوند؛ بدون این بررسی، دادهٔ محیط دیگری در مراجعه می‌نشست.
                $item = $this->serviceItemRepo->findByUuid($svc['service_item_uuid'] ?? '');
                if (!$this->tenantOwnership->belongsToPair($entityType, $entityId, $item)) { continue; }

                $staff = !empty($svc['staff_uuid']) ? $this->staffRepo->findByUuid($svc['staff_uuid']) : null;
                if ($staff !== null && !$this->tenantOwnership->belongsToPair($entityType, $entityId, $staff)) {
                    $staff = null;
                }
                $qty   = max(1, (int) ($svc['quantity'] ?? 1));
                $ss    = new SessionService($session, $item, $staff, $qty);
                $this->sessionServiceRepo->save($ss);
                $session->addService($ss);
            }
        }

        // جایگزینی کالاهای مصرفی (اگر ارسال شده)
        if (array_key_exists('consumables', $data)) {
            foreach ($session->getConsumables()->toArray() as $old) {
                $this->sessionConsumableRepo->remove($old, false);
                $session->getConsumables()->removeElement($old);
            }
            foreach (($data['consumables'] ?? []) as $row) {
                $item = $this->inventoryItemRepo->findByUuid((string) ($row['inventory_item_uuid'] ?? ''));
                if ($item === null || $item->getEntityType() !== $entityType || $item->getEntityId() !== $entityId) { continue; }
                $qty = max(1, (int) ($row['quantity'] ?? 1));
                $sc  = new SessionConsumable($session, $item, $qty);
                $this->sessionConsumableRepo->save($sc);
                $session->addConsumable($sc);
            }
        }

        // بازمحاسبه‌ی مجموع‌ها (مثل createSession)
        $serviceItemsData = array_map(
            fn(SessionService $s) => ['item_id' => $s->getServiceItem()->getId(), 'price_rials' => $s->getLineTotalRials()],
            $session->getServices()->toArray(),
        );
        $visitCategory = $session->getInsuranceServiceCategory();
        $session->setBaseInsuranceDiscountPercent($this->contractPercent($entityType, $entityId, $session->getInsuranceBaseId(), $visitCategory));
        $session->setSupplementaryDiscountPercent($this->contractPercent($entityType, $entityId, $session->getInsuranceSupplementaryId(), $visitCategory));

        $priceCalc = $this->calculateFinalPrice(
            $session->getVisitPriceRials(),
            $serviceItemsData,
            $entityType,
            $entityId,
            $session->getInsuranceBaseId(),
            $session->getInsuranceSupplementaryId(),
            $visitCategory,
        );
        $consumablesTotal = $session->getConsumablesTotalRials();
        $session->setServicesTotalRials($priceCalc['services_total_rials']);
        $session->applyShares(
            $priceCalc['gross_total_rials'] + $consumablesTotal,
            $priceCalc['base_insurance_rials'],
            $priceCalc['supplementary_insurance_rials'],
            $priceCalc['patient_share_rials'] + $consumablesTotal,
        );
        $this->sessionRepo->save($session);

        // پس از تغییر مبلغ، وضعیت تسویه بازمحاسبه شود (افزودن سرویس/پکیج → بدهکار).
        $this->recomputeSettlement($session);

        // ثبت audit برای فیلدهای تغییرکرده
        $after = [
            'visit_price_rials'    => (string) $session->getVisitPriceRials(),
            'services'             => $this->servicesSummary($session),
            'consumables'          => $this->consumablesSummary($session),
            'services_total_rials' => (string) $session->getServicesTotalRials(),
            'final_price_rials'    => (string) $session->getFinalPriceRials(),
        ];
        foreach ($after as $field => $newVal) {
            if ($before[$field] !== $newVal) {
                $this->logSessionChange($session, $field, \App\Patient\Entity\SessionAuditLog::OP_UPDATE, $before[$field], $newVal, $actor);
            }
        }

        return $session;
    }

    /**
     * ویرایش یک پرداخت ثبت‌شده (روش/مبلغ/تاریخ) با ثبت audit و بازمحاسبه‌ی
     * فیلدهای کش‌شده‌ی تسویه. پرداخت wallet مسدود است (جبران کیف پول خارج از scope).
     */
    public function updatePayment(SessionPayment $payment, array $data, User $actor): SessionPayment
    {
        $session = $payment->getSession();
        if ($payment->getMethod() === 'wallet') {
            throw new AppException(ErrorCodes::ERR_SESSION_PAYMENT_INVALID, 'ویرایش پرداخت کیف پول ممکن نیست', 422, 'method');
        }

        $oldAmount = $payment->getAmountRials();
        $newAmount = array_key_exists('amount_rials', $data) ? max(0, (int) $data['amount_rials']) : $oldAmount;
        if ($newAmount <= 0) {
            throw new AppException(ErrorCodes::ERR_SESSION_PAYMENT_INVALID, null, 422, 'amount_rials');
        }
        // مجموع پرداخت‌ها (با مقدار جدید) نباید از مبلغِ پس از تخفیف بیشتر شود.
        $othersTotal = $session->getPaidTotalRials() - $oldAmount;
        if ($othersTotal + $newAmount > $session->getPayableRials()) {
            throw new AppException(ErrorCodes::ERR_SESSION_PAYMENT_EXCEEDS, null, 422, 'amount_rials');
        }

        if (array_key_exists('method', $data)) {
            $method = (string) $data['method'];
            if (!in_array($method, SessionPayment::METHODS, true) || $method === 'wallet') {
                throw new AppException(ErrorCodes::ERR_SESSION_PAYMENT_INVALID, null, 422, 'method');
            }
            $payment->setMethod($method);
        }
        $payment->setAmountRials($newAmount);
        if (!empty($data['paid_at'])) { $payment->setPaidAt((int) $data['paid_at']); }
        $this->sessionPaymentRepo->save($payment);

        $this->recomputeSettlement($session);
        $this->logSessionChange($session, 'payment', \App\Patient\Entity\SessionAuditLog::OP_UPDATE, (string) $oldAmount, (string) $newAmount, $actor, 'ویرایش پرداخت');

        return $payment;
    }

    /**
     * حذف یک پرداخت ثبت‌شده با ثبت audit و بازمحاسبه‌ی تسویه.
     * پرداخت wallet مسدود است (جبران کیف پول خارج از scope).
     */
    public function deletePayment(SessionPayment $payment, User $actor): void
    {
        $session = $payment->getSession();
        if ($payment->getMethod() === 'wallet') {
            throw new AppException(ErrorCodes::ERR_SESSION_PAYMENT_INVALID, 'حذف پرداخت کیف پول ممکن نیست', 422, 'method');
        }
        $amount = $payment->getAmountRials();
        $session->getPayments()->removeElement($payment);
        $this->sessionPaymentRepo->remove($payment);

        $this->recomputeSettlement($session);
        $this->logSessionChange($session, 'payment', \App\Patient\Entity\SessionAuditLog::OP_DELETE, (string) $amount, null, $actor, 'حذف پرداخت');
    }

    /** بازمحاسبه‌ی فیلدهای کش‌شده‌ی تسویه پس از تغییر پرداخت‌ها. */
    private function recomputeSettlement(PatientSession $session): void
    {
        if ($session->getRemainingRials() === 0 && $session->getPaidTotalRials() > 0) {
            if ($session->getPaymentMethod() === 'pending') {
                $session->setPaymentMethod('cash');
            }
            if ($session->getPaidAt() === null) {
                $session->setPaidAt(time());
            }
        } else {
            $session->setPaymentMethod('pending');
            $session->setPaidAt(null);
        }
        $this->sessionRepo->save($session);
    }

    /**
     * اعمال/حذف تخفیف تسویه روی مراجعه.
     * type=null → حذف تخفیف. percent باید 0..100 و fixed حداکثر برابر مبلغ نهایی باشد.
     * تخفیف نمی‌تواند از مانده‌ی قابل‌تخفیف (مبلغ نهایی منهای پرداخت‌های ثبت‌شده) بیشتر شود.
     */
    public function applyDiscount(PatientSession $session, ?string $type, int $value, ?int $ruleId = null, ?string $ruleLabel = null, ?User $actor = null): PatientSession
    {
        if ($type === null) {
            $oldRials = $session->getDiscountRials();
            $session->setDiscount(null, 0, 0);
            $this->sessionRepo->save($session);
            if ($oldRials !== 0) {
                $this->logSessionChange($session, 'discount', \App\Patient\Entity\SessionAuditLog::OP_DELETE, (string) $oldRials, null, $actor);
            }
            return $session;
        }

        if (!in_array($type, ['percent', 'fixed'], true) || $value < 0) {
            throw new AppException(ErrorCodes::ERR_SESSION_DISCOUNT_INVALID, null, 422, 'discount_type');
        }

        $final = $session->getFinalPriceRials();
        if ($type === 'percent') {
            if ($value > 100) {
                throw new AppException(ErrorCodes::ERR_SESSION_DISCOUNT_INVALID, null, 422, 'discount_value');
            }
            $rials = (int) round($final * $value / 100);
        } else {
            if ($value > $final) {
                throw new AppException(ErrorCodes::ERR_SESSION_DISCOUNT_INVALID, null, 422, 'discount_value');
            }
            $rials = $value;
        }

        // تخفیف نباید از آنچه هنوز پرداخت نشده بیشتر باشد (پرداخت‌ها برگشت‌ناپذیرند)
        if ($rials > $final - $session->getPaidTotalRials()) {
            throw new AppException(ErrorCodes::ERR_SESSION_DISCOUNT_INVALID, null, 422, 'discount_value');
        }

        $this->persistDiscount($session, $type, $value, $rials, $ruleId, $ruleLabel, $actor);

        return $session;
    }

    /**
     * اعمال یک قانون تخفیف روی مراجعه؛ مبلغ ریالی از موتور (با در نظر گرفتن نوع/مبنا)
     * محاسبه و منبع قانون برای audit ثبت می‌شود.
     */
    public function applyDiscountRule(PatientSession $session, \App\Discount\Entity\DiscountRule $rule, ?User $actor = null): PatientSession
    {
        $rials = $this->discountEngine->computeForRule($session, $rule);
        if ($rials <= 0) {
            throw new AppException(ErrorCodes::ERR_SESSION_DISCOUNT_INVALID, null, 422, 'discount_rule_uuid');
        }
        $this->persistDiscount($session, $rule->getDiscountType(), $rule->getValue(), $rials, $rule->getId(), $rule->getName(), $actor);

        return $session;
    }

    private function persistDiscount(PatientSession $session, string $type, int $value, int $rials, ?int $ruleId, ?string $ruleLabel, ?User $actor = null): void
    {
        $oldRials = $session->getDiscountRials();
        $session->setDiscount($type, $value, $rials, $ruleId, $ruleLabel);
        if ($session->getRemainingRials() === 0 && $session->getPaymentMethod() === 'pending') {
            // تخفیف صددرصدی بدهی را صفر کرد — مراجعه تسویه‌شده تلقی می‌شود
            $session->setPaymentMethod('cash');
            $session->setPaidAt(time());
        }
        $this->sessionRepo->save($session);
        if ($oldRials !== $rials) {
            $this->logSessionChange($session, 'discount', \App\Patient\Entity\SessionAuditLog::OP_UPDATE, (string) $oldRials, (string) $rials, $actor, $ruleLabel);
        }
    }

    /**
     * ثبت یک پرداخت جزئی روی مراجعه. روش wallet همان مبلغ را از کیف پول بیمار
     * کسر می‌کند (موجودی ناکافی → ۴۲۲). وقتی مانده صفر شود، payment_method و
     * paid_at مراجعه ست می‌شوند تا is_paid برای مصرف‌کننده‌های فعلی درست بماند.
     */
    public function addSessionPayment(
        PatientSession $session,
        string $method,
        int $amountRials,
        ?int $paidAt = null,
        ?User $actor = null,
        ?string $paymentMethodUuid = null,
        ?string $reference = null,
    ): SessionPayment {
        if (!in_array($method, SessionPayment::METHODS, true)) {
            throw new AppException(ErrorCodes::ERR_SESSION_PAYMENT_INVALID, null, 422, 'method');
        }
        if ($amountRials <= 0) {
            throw new AppException(ErrorCodes::ERR_SESSION_PAYMENT_INVALID, null, 422, 'amount_rials');
        }

        $remaining = $session->getRemainingRials();
        if ($amountRials > $remaining) {
            throw new AppException(ErrorCodes::ERR_SESSION_PAYMENT_EXCEEDS, null, 422, 'amount_rials');
        }

        if ($method === 'wallet') {
            $names = array_values(array_filter(array_map(
                fn(SessionService $s) => $s->toArray()['service_name'] ?? null,
                $session->getServices()->toArray(),
            )));
            $label = $names !== [] ? implode('، ', $names) : 'ویزیت';
            $record = $session->getRecord();
            $this->walletService->withdraw(
                $record->getUser(),
                $amountRials,
                $actor,
                'پرداخت سرویس: ' . $label,
                'wallet',
                'session:' . $session->getUuid(),
                $record->getEntityType(),
                $record->getEntityId(),
            );
        }

        $payment = new SessionPayment($session, $method, $amountRials, $paidAt);
        $payment->setCreatedBy($actor)
            ->setCreatedByName($this->walletService->resolveActorName($actor))
            ->setPaymentMethodUuid($paymentMethodUuid !== null && $paymentMethodUuid !== '' ? $paymentMethodUuid : null)
            ->setReference($reference !== null && $reference !== '' ? $reference : null);
        $this->sessionPaymentRepo->save($payment);
        $session->addPayment($payment);

        if ($session->getRemainingRials() === 0) {
            $session->setPaymentMethod($method);
            $session->setPaidAt($paidAt ?? time());
        }
        $this->sessionRepo->save($session);

        $this->logSessionChange($session, 'payment', \App\Patient\Entity\SessionAuditLog::OP_CREATE, null, (string) $amountRials, $actor, 'ثبت پرداخت');

        return $payment;
    }
}
