<?php

namespace App\Patient\Controller;

use App\Auth\Entity\User;
use App\Auth\Repository\UserRepository;
use App\Billing\Repository\InvoiceRepository;
use App\Billing\Service\SessionBillingService;
use App\Patient\Entity\PatientRecord;
use App\Patient\Repository\PatientRecordRepository;
use App\Patient\Repository\PatientSessionRepository;
use App\Patient\Security\PatientRecordScope;
use App\Patient\Security\PatientRecordScopeResolver;
use App\Secretary\Security\SecretaryAccessChecker;
use App\Patient\Service\PatientService;
use App\UserProfile\Entity\UserProfile;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use App\Shared\Exception\AppException;
use App\Subscription\Service\SubscriptionService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use OpenApi\Attributes as OA;

#[OA\Tag(name: 'Patients')]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class PatientController extends BaseController
{
    public function __construct(
        private readonly PatientRecordRepository     $recordRepo,
        private readonly PatientSessionRepository    $sessionRepo,
        private readonly PatientService              $patientService,
        private readonly SubscriptionService         $subscriptionService,
        private readonly UserRepository              $userRepo,
        private readonly PatientRecordScopeResolver  $scopeResolver,
        private readonly \App\UserProfile\Repository\UserProfileRepository $profileRepo,
        private readonly \App\Insurance\Repository\InsuranceRepository $insuranceRepo,
        private readonly SessionBillingService       $sessionBilling,
        private readonly InvoiceRepository           $invoiceRepo,
        private readonly \App\Appointment\Repository\AppointmentRepository $appointmentRepo,
        private readonly \App\Tag\Repository\TenantTagRepository $tenantTagRepo,
        private readonly \App\Patient\Repository\PatientAttachmentRepository $attachmentRepo,
        private readonly \App\Patient\Repository\PatientMedicalRecordRepository $medicalRepo,
        private readonly \App\Patient\Repository\PatientMessageRepository $messageRepo,
        private readonly \App\Patient\Repository\PatientNoteRepository $noteRepo,
        private readonly \App\Patient\Repository\PatientCallRepository $callRepo,
        private readonly \App\Shared\Service\FileUploadService $fileUpload,
        private readonly \App\Payment\Repository\PaymentRepository $paymentRepo,
        private readonly \App\Settlement\Repository\WalletTransactionRepository $walletRepo,
        private readonly \App\Settlement\Repository\SettlementRepository $settlementRepo,
        private readonly \App\Settlement\Service\WalletService $walletService,
        private readonly \App\Discount\Repository\DiscountRuleRepository $discountRuleRepo,
        private readonly \App\Patient\Repository\SessionPaymentRepository $sessionPaymentRepo,
        private readonly \App\Patient\Repository\SessionAuditLogRepository $sessionAuditRepo,
        private readonly SecretaryAccessChecker      $secretaryAccess,
        private readonly \App\Clinic\Security\ClinicDoctorAccessChecker $clinicDoctorAccess,
    ) {}

    // ── Financials (مالی: پرداخت / تراکنش / کیف‌پول) ────────────────────────────
    //
    // The patient's money is scoped to the record's owner User. The generic
    // wallet/payment endpoints are bound to #[CurrentUser] (the requester's own
    // finances), so a doctor/secretary viewing a record needs these
    // record-owner-gated reads to see the *patient's* finances instead.

    /** List gateway payments made by the patient (paginated, optional ?status). */
    #[Route('/api/v1/patient/{uuid}/payments', methods: ['GET'])]
    public function listPayments(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $page    = max(1, (int) $request->query->get('page', 1));
        $limit   = min(100, max(1, (int) $request->query->get('limit', 20)));
        $status  = $request->query->get('status') ?: null;
        $patient = $record->getUser();

        $payments = array_map(
            fn(\App\Payment\Entity\Payment $p) => $p->toArray(),
            $this->paymentRepo->findByUser($patient, $status, $page, $limit)
        );

        return $this->paginated($payments, $this->paymentRepo->countByUser($patient, $status), $page, $limit);
    }

    /** Patient wallet balance + the 10 most recent transactions (کیف‌پول tab). */
    #[Route('/api/v1/patient/{uuid}/wallet', methods: ['GET'])]
    public function walletBalance(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $patient = $record->getUser();

        // موجودی سراسری است (پول مالِ بیمار است، نه محیط) ولی سطرهای دفتر به همین
        // محیط محدودند؛ وگرنه اینجا هم پیدا بود بیمار در محیط دیگر چه پرداخت کرده.
        return $this->success([
            'balance_rials'       => $this->settlementRepo->getWalletBalance($patient),
            'recent_transactions' => array_map(
                fn(\App\Settlement\Entity\WalletTransaction $t) => $t->toArray(),
                $this->walletRepo->findByUserForEnvironment($patient, $entityType, (int) $entityId, 10)
            ),
        ]);
    }

    /** Full paginated wallet-transaction ledger for the patient (تراکنش tab). */
    #[Route('/api/v1/patient/{uuid}/wallet/transactions', methods: ['GET'])]
    public function walletTransactions(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $page    = max(1, (int) $request->query->get('page', 1));
        $limit   = min(100, max(1, (int) $request->query->get('limit', 50)));
        $patient = $record->getUser();

        // دفتر به محیط جاری محدود می‌شود: موجودی مالِ شخص است ولی تاریخچهٔ او در
        // کلینیک دیگر به این محیط ربطی ندارد.
        $txns = array_map(
            fn(\App\Settlement\Entity\WalletTransaction $t) => $t->toArray(),
            $this->walletRepo->findByUserForEnvironment($patient, $entityType, (int) $entityId, $limit, ($page - 1) * $limit)
        );

        return $this->paginated(
            $txns,
            $this->walletRepo->countByUserForEnvironment($patient, $entityType, (int) $entityId),
            $page,
            $limit,
        );
    }

    /**
     * Manual wallet top-up (شارژ کیف پول) — e.g. a deposit taken at the desk.
     * Creates a credit WalletTransaction for the record's owner User; balance
     * is derived (credit − debit), so balance_after is computed here.
     */
    #[Route('/api/v1/patient/{uuid}/wallet/charge', methods: ['POST'])]
    public function chargeWallet(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'payments', 'create');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'payments', 'create');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $data   = json_decode($request->getContent(), true) ?? [];
        $amount = (int) ($data['amount_rials'] ?? 0);
        if ($amount <= 0) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'مبلغ شارژ باید بزرگ‌تر از صفر باشد', 422, 'amount_rials');
        }

        $patient = $record->getUser();
        $txn = $this->walletService->charge(
            $patient, $amount, $user,
            trim((string) ($data['description'] ?? '')) ?: null,
            $this->normalizeMethod($data['payment_method'] ?? null),
            trim((string) ($data['reference'] ?? '')) ?: null,
            $entityType, (int) $entityId,
        );

        return $this->success([
            'transaction'   => $txn->toArray(),
            'balance_rials' => $txn->getBalanceAfter(),
        ], 201);
    }

    /**
     * Manual wallet withdrawal (برداشت از کیف پول) — e.g. a refund or cash
     * hand-back at the desk. Creates a debit WalletTransaction for the record's
     * owner User (with acting user + payment method recorded). Rejected (422)
     * when the amount exceeds the current balance, mirroring the offline app.
     */
    #[Route('/api/v1/patient/{uuid}/wallet/withdraw', methods: ['POST'])]
    public function withdrawWallet(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'payments', 'create');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'payments', 'create');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $data   = json_decode($request->getContent(), true) ?? [];
        $amount = (int) ($data['amount_rials'] ?? 0);
        if ($amount <= 0) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'مبلغ برداشت باید بزرگ‌تر از صفر باشد', 422, 'amount_rials');
        }

        // Insufficient balance → WalletService throws AppException (422), handled globally.
        $patient = $record->getUser();
        $txn = $this->walletService->withdraw(
            $patient, $amount, $user,
            trim((string) ($data['description'] ?? '')) ?: null,
            $this->normalizeMethod($data['payment_method'] ?? null),
            trim((string) ($data['reference'] ?? '')) ?: null,
            $entityType, (int) $entityId,
        );

        return $this->success([
            'transaction'   => $txn->toArray(),
            'balance_rials' => $txn->getBalanceAfter(),
        ], 201);
    }

    /** روش پرداختِ مجاز برای کیف پول؛ ورودیِ ناشناخته نادیده گرفته می‌شود. */
    private function normalizeMethod(mixed $method): ?string
    {
        $method = is_string($method) ? trim($method) : '';
        return in_array($method, ['card', 'pos', 'cash', 'gateway', 'wallet'], true) ? $method : null;
    }

    // ── Call center (کال سنتر) ─────────────────────────────────────────────────

    /** List the patient's call log (newest first, optional ?outcome=success|missed). */
    #[Route('/api/v1/patient/{uuid}/calls', methods: ['GET'])]
    public function listCalls(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $outcome = $request->query->get('outcome');
        $outcome = in_array($outcome, ['success', 'missed'], true) ? $outcome : null;

        return $this->success(array_map(
            fn(\App\Patient\Entity\PatientCall $c) => $c->toArray(),
            $this->callRepo->findByRecord($record, $outcome)
        ));
    }

    /** Log a new call. `subject` required; `outcome` defaults to success; `personnel`/`summary`/`called_at` optional. */
    #[Route('/api/v1/patient/{uuid}/call', methods: ['POST'])]
    public function createCall(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'update');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $data    = json_decode($request->getContent(), true) ?? [];
        $subject = trim((string) ($data['subject'] ?? ''));
        if ($subject === '') {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'موضوع تماس الزامی است', 422, 'subject');
        }

        $outcome  = (string) ($data['outcome'] ?? 'success');
        if (!in_array($outcome, ['success', 'missed'], true)) {
            $outcome = 'success';
        }
        $calledAt = isset($data['called_at']) ? (int) $data['called_at'] : null;

        $call = new \App\Patient\Entity\PatientCall($record, $subject, $outcome, $calledAt);
        $summary = trim((string) ($data['summary'] ?? ''));
        if ($summary !== '') {
            $call->setSummary($summary);
        }
        $personnel = trim((string) ($data['personnel'] ?? ''));
        if ($personnel !== '') {
            $call->setPersonnel($personnel);
        }
        $this->callRepo->save($call);

        return $this->success($call->toArray(), 201);
    }

    /** Delete a call log entry. Owner-scoped; otherwise 404. */
    #[Route('/api/v1/patient/call/{uuid}', methods: ['DELETE'])]
    public function deleteCall(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'delete');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'delete');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $call = $this->callRepo->findByUuid($uuid);
        if ($call === null || !$this->ownsRecord($call->getRecord(), $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $this->callRepo->remove($call);

        return $this->success(['deleted' => true]);
    }

    // ── Messages (پیام‌ها) ─────────────────────────────────────────────────────

    #[Route('/api/v1/patient/{uuid}/messages', methods: ['GET'])]
    public function listMessages(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        return $this->success(array_map(
            fn(\App\Patient\Entity\PatientMessage $m) => $m->toArray(),
            $this->messageRepo->findByRecord($record)
        ));
    }

    #[Route('/api/v1/patient/{uuid}/message', methods: ['POST'])]
    public function createMessage(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'update');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $data = json_decode($request->getContent(), true) ?? [];
        $body = trim((string) ($data['body'] ?? ''));
        if ($body === '') {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'متن پیام الزامی است', 422, 'body');
        }

        $channel = (string) ($data['channel'] ?? 'sms');
        if (!in_array($channel, ['sms', 'note', 'call', 'email'], true)) {
            $channel = 'sms';
        }
        $message = new \App\Patient\Entity\PatientMessage($record, $body, $channel);
        $this->messageRepo->save($message);

        return $this->success($message->toArray(), 201);
    }

    #[Route('/api/v1/patient/message/{uuid}', methods: ['DELETE'])]
    public function deleteMessage(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'delete');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'delete');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $message = $this->messageRepo->findByUuid($uuid);
        if ($message === null || !$this->ownsRecord($message->getRecord(), $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پیام یافت نشد', 404);
        }

        $this->messageRepo->remove($message);

        return $this->success(['message' => 'پیام حذف شد']);
    }

    // ── Notes (یادداشت‌ها) ─────────────────────────────────────────────────────
    //
    // Personal staff memos on a record; shared with everyone who owns the record.
    // The author's display name is captured at write time (survives user deletion).

    #[Route('/api/v1/patient/{uuid}/notes', methods: ['GET'])]
    public function listNotes(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        return $this->success(array_map(
            fn(\App\Patient\Entity\PatientNote $n) => $n->toArray(),
            $this->noteRepo->findByRecord($record)
        ));
    }

    #[Route('/api/v1/patient/{uuid}/note', methods: ['POST'])]
    public function createNote(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'update');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $data = json_decode($request->getContent(), true) ?? [];
        $body = trim((string) ($data['body'] ?? ''));
        if ($body === '') {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'متن یادداشت الزامی است', 422, 'body');
        }

        $note = new \App\Patient\Entity\PatientNote($record, $body, (bool) ($data['pinned'] ?? false));
        $note->setAuthor($user, $user->getRealName() ?? $user->getMobileNumber());
        $this->noteRepo->save($note);

        return $this->success($note->toArray(), 201);
    }

    #[Route('/api/v1/patient/note/{uuid}', methods: ['PATCH'])]
    public function updateNote(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'update');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $note = $this->noteRepo->findByUuid($uuid);
        if ($note === null || !$this->ownsRecord($note->getRecord(), $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'یادداشت یافت نشد', 404);
        }

        $data = json_decode($request->getContent(), true) ?? [];
        if (array_key_exists('body', $data)) {
            $body = trim((string) $data['body']);
            if ($body === '') {
                return $this->error(ErrorCodes::ERR_VALIDATION_001, 'متن یادداشت الزامی است', 422, 'body');
            }
            $note->setBody($body);
        }
        if (array_key_exists('pinned', $data)) {
            $note->setPinned((bool) $data['pinned']);
        }
        $this->noteRepo->save($note);

        return $this->success($note->toArray());
    }

    #[Route('/api/v1/patient/note/{uuid}', methods: ['DELETE'])]
    public function deleteNote(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'delete');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'delete');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $note = $this->noteRepo->findByUuid($uuid);
        if ($note === null || !$this->ownsRecord($note->getRecord(), $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'یادداشت یافت نشد', 404);
        }

        $this->noteRepo->remove($note);

        return $this->success(['message' => 'یادداشت حذف شد']);
    }

    // ── Medical records (پرونده پزشکی) ────────────────────────────────────────

    #[Route('/api/v1/patient/{uuid}/medical-records', methods: ['GET'])]
    public function listMedicalRecords(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        return $this->success(array_map(
            fn(\App\Patient\Entity\PatientMedicalRecord $m) => $m->toArray(),
            $this->medicalRepo->findByRecord($record)
        ));
    }

    #[Route('/api/v1/patient/{uuid}/medical-record', methods: ['POST'])]
    public function createMedicalRecord(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'update');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $data  = json_decode($request->getContent(), true) ?? [];
        $title = trim((string) ($data['title'] ?? ''));
        if ($title === '') {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'عنوان معاینه الزامی است', 422, 'title');
        }

        $body       = isset($data['body']) ? trim((string) $data['body']) : null;
        $recordedAt = isset($data['recorded_at']) && $data['recorded_at'] !== '' ? (int) $data['recorded_at'] : null;

        $medical = new \App\Patient\Entity\PatientMedicalRecord($record, $title, $body ?: null, $recordedAt);
        $this->medicalRepo->save($medical);

        return $this->success($medical->toArray(), 201);
    }

    #[Route('/api/v1/patient/medical-record/{uuid}', methods: ['PATCH'])]
    public function updateMedicalRecord(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'update');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $medical = $this->medicalRepo->findByUuid($uuid);
        if ($medical === null || !$this->ownsRecord($medical->getRecord(), $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'رکورد پزشکی یافت نشد', 404);
        }

        $data = json_decode($request->getContent(), true) ?? [];
        if (isset($data['title'])) {
            $t = trim((string) $data['title']);
            if ($t === '') {
                return $this->error(ErrorCodes::ERR_VALIDATION_001, 'عنوان معاینه الزامی است', 422, 'title');
            }
            $medical->setTitle($t);
        }
        if (array_key_exists('body', $data)) {
            $b = trim((string) ($data['body'] ?? ''));
            $medical->setBody($b === '' ? null : $b);
        }
        if (isset($data['recorded_at']) && $data['recorded_at'] !== '') {
            $medical->setRecordedAt((int) $data['recorded_at']);
        }

        $this->medicalRepo->save($medical);

        return $this->success($medical->toArray());
    }

    #[Route('/api/v1/patient/medical-record/{uuid}', methods: ['DELETE'])]
    public function deleteMedicalRecord(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'delete');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'delete');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $medical = $this->medicalRepo->findByUuid($uuid);
        if ($medical === null || !$this->ownsRecord($medical->getRecord(), $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'رکورد پزشکی یافت نشد', 404);
        }

        $this->medicalRepo->remove($medical);

        return $this->success(['message' => 'رکورد پزشکی حذف شد']);
    }

    // ── Attachments (ضمیمه) ───────────────────────────────────────────────────

    #[Route('/api/v1/patient/{uuid}/attachments', methods: ['GET'])]
    public function listAttachments(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        return $this->success(array_map(
            fn(\App\Patient\Entity\PatientAttachment $a) => $a->toArray(),
            $this->attachmentRepo->findByRecord($record)
        ));
    }

    #[Route('/api/v1/patient/{uuid}/attachment', methods: ['POST'])]
    public function uploadAttachment(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'update');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        try {
            $stored = $this->fileUpload->storeFromRequest($request, 'patients/attachments');
        } catch (\RuntimeException $e) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, $e->getMessage(), 422);
        }

        $name = trim((string) $request->query->get('name', '')) ?: $stored['filename'];
        $attachment = new \App\Patient\Entity\PatientAttachment($record, $name, $stored['url'], $stored['filemime'], $stored['size']);
        $this->attachmentRepo->save($attachment);

        return $this->success($attachment->toArray(), 201);
    }

    #[Route('/api/v1/patient/attachment/{uuid}', methods: ['DELETE'])]
    public function deleteAttachment(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'delete');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'delete');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $attachment = $this->attachmentRepo->findByUuid($uuid);
        if ($attachment === null || !$this->ownsRecord($attachment->getRecord(), $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'ضمیمه یافت نشد', 404);
        }

        $this->attachmentRepo->remove($attachment);

        return $this->success(['message' => 'ضمیمه حذف شد']);
    }

    /**
     * Assign record labels from the payload (`tags` = array of TenantTag uuids),
     * scoped to the caller's entity. Returns a 422 response on a foreign tag,
     * otherwise null. Does nothing when `tags` is absent.
     */
    private function applyRecordTags(PatientRecord $record, array $data, string $entityType, int $entityId): ?JsonResponse
    {
        if (!array_key_exists('tags', $data)) {
            return null;
        }
        $uuids = is_array($data['tags']) ? $data['tags'] : [];
        $tags  = [];
        foreach (array_values(array_unique(array_filter($uuids))) as $uuid) {
            $tag = $this->tenantTagRepo->findByUuid((string) $uuid);
            if ($tag === null || $tag->getEntityType() !== $entityType || $tag->getEntityId() !== $entityId) {
                return $this->error(ErrorCodes::ERR_VALIDATION_001, 'برچسب انتخاب‌شده متعلق به شما نیست', 422, 'tags');
            }
            $tags[] = $tag;
        }
        $record->setTags($tags);
        return null;
    }

    private function buildPatientProfile(User $patient): array
    {
        $p = $this->profileRepo->findByUser($patient);
        $insName = function (?int $id): ?string {
            if ($id === null) { return null; }
            return $this->insuranceRepo->find($id)?->getName();
        };

        return [
            'full_name'                  => trim(($patient->getRealName() ?? '') . ' ' . ($p?->getFamily() ?? '')) ?: $patient->getRealName(),
            'name'                       => $patient->getRealName(),
            'family'                     => $p?->getFamily(),
            'fathers_name'               => $p?->getFathersName(),
            'national_code'              => $p?->getNationalCode() ?? $patient->getNationalCode(),
            'gender'                     => $p?->getGender(),
            'date_of_birth'              => $p?->getDateOfBirth(),
            'blood_type'                 => $p?->getBloodType(),
            'marital_status'             => $p?->getMaritalStatus(),
            'education'                  => $p?->getEducation(),
            'field_of_study'            => $p?->getFieldOfStudy(),
            'job'                        => $p?->getJob(),
            'address'                    => $p?->getAddress(),
            'province_id'                => $p?->getProvinceId(),
            'city_id'                    => $p?->getCityId(),
            'postal_code'                => $p?->getPostalCode(),
            'referral_source'            => $p?->getReferralSource(),
            'description'                => $p?->getDescription(),
            'home_phone'                 => $p?->getHomePhone(),
            'work_phone'                 => $p?->getWorkPhone(),
            'mobile'                     => $patient->getMobileNumber(),
            'basic_insurance_id'         => $p?->getBasicInsuranceId(),
            'basic_insurance_name'       => $insName($p?->getBasicInsuranceId()),
            'supplementary_insurance_id' => $p?->getSupplementaryInsuranceId(),
            'supplementary_insurance_name' => $insName($p?->getSupplementaryInsuranceId()),
        ];
    }

    #[Route('/api/v1/patient/search-user', methods: ['GET'])]
    public function searchUser(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $mobile = trim($request->query->get('mobile', ''));
        if ($mobile === '') {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'mobile الزامی است', 422);
        }

        $patient = $this->userRepo->findByMobile($mobile);
        if ($patient === null) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'کاربری با این شماره یافت نشد', 404);
        }

        return $this->success([
            'uuid' => $patient->getUuid(),
            'name' => $patient->getRealName(),
            'mobile' => $patient->getMobileNumber(),
            'national_code' => $patient->getNationalCode(),
        ]);
    }

    #[Route('/api/v1/patients', methods: ['GET'])]
    public function list(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(50, max(10, (int) $request->query->get('limit', 20)));
        $search = $request->query->get('search') ?: null;

        $tags = $request->query->get('tags');
        $filters = [
            'tags'           => $tags ? array_filter(array_map('trim', explode(',', $tags))) : null,
            'gender'         => $request->query->get('gender') ?: null,
            'insurance_id'   => $request->query->get('insurance_id') ?: null,
            'admitted_from'  => $request->query->get('admitted_from') ?: null,
            'admitted_to'    => $request->query->get('admitted_to') ?: null,
            'service_status' => $request->query->get('service_status') ?: null,
            'has_debt'       => $request->query->getBoolean('has_debt'),
        ];

        $restrictTo = $this->scope($user)->restrictToDoctorIds;
        $records = $this->recordRepo->findByEntity($entityType, $entityId, $page, $limit, $search, $filters, $restrictTo);
        $total   = $this->recordRepo->countByEntity($entityType, $entityId, $search, $filters, $restrictTo);

        // کد ملی روی profiles ذخیره می‌شود نه users؛ اگر روی user خالی بود از پروفایل پر کن.
        $userIds = array_map(fn(PatientRecord $r) => $r->getUser()->getId(), $records);
        $profileCodes = $this->profileRepo->nationalCodesByUserIds($userIds);
        $rows = array_map(function (PatientRecord $r) use ($profileCodes) {
            $row = $r->toArray();
            if (empty($row['user_national_code'])) {
                $row['user_national_code'] = $profileCodes[$r->getUser()->getId()] ?? null;
            }
            return $row;
        }, $records);

        return $this->paginated($rows, $total, $page, $limit);
    }

    #[Route('/api/v1/patient', methods: ['POST'])]
    public function create(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'create');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'create');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $data     = json_decode($request->getContent(), true) ?? [];
        $userUuid = trim($data['user_uuid'] ?? '');
        $mobile   = trim($data['mobile'] ?? '');
        $name     = trim($data['name'] ?? '');

        $nationalCode = trim((string) ($data['national_code'] ?? ''));
        if ($nationalCode !== '' && !preg_match('/^\d{10}$/', $nationalCode)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی باید ۱۰ رقم باشد', 422);
        }

        $patient = null;
        if ($userUuid !== '') {
            $patient = $this->userRepo->findByUuid($userUuid);
        } elseif ($mobile !== '') {
            if (!preg_match('/^09\d{9}$/', $mobile)) {
                return $this->error(ErrorCodes::ERR_VALIDATION_001, 'شماره موبایل نامعتبر است', 422);
            }
            $patient = $this->userRepo->findByMobile($mobile);
        }

        // کد ملی باید در سطح بیمار یکتا باشد: اگر به پروفایلِ کاربر دیگری تعلق دارد، رد کن
        if ($nationalCode !== '') {
            $owner = $this->profileRepo->findOneByNationalCode($nationalCode);
            if ($owner !== null && ($patient === null || $owner->getUser()->getId() !== $patient->getId())) {
                $masked = \App\Shared\Service\InputValidator::maskMobile($owner->getUser()->getMobileNumber());
                return $this->error(
                    ErrorCodes::ERR_PROFILE_NATIONAL_CODE_TAKEN,
                    "این کد ملی قبلاً با شماره {$masked} ثبت شده است",
                    409,
                    'national_code'
                );
            }
        }

        // بیمار جدید بدون ثبت‌نام قبلی: موبایل + نام آمده ولی کاربری وجود ندارد
        if ($patient === null) {
            if ($mobile === '' || $name === '') {
                return $this->error(ErrorCodes::ERR_VALIDATION_001, 'برای ساخت بیمار جدید، شماره موبایل و نام الزامی است', 422);
            }
            $patient = new User($mobile);
            $patient->setRealName($name);
            if ($nationalCode !== '') {
                $patient->setNationalCode($nationalCode);
            }
            $this->userRepo->save($patient);
        } elseif ($nationalCode !== '' && $patient->getNationalCode() === null) {
            $patient->setNationalCode($nationalCode);
            $this->userRepo->save($patient);
        }

        $existing = $this->recordRepo->findByEntityAndUser($entityType, $entityId, $patient);
        if ($existing !== null) {
            return $this->success($existing->toArray());
        }

        $record = new PatientRecord($entityType, $entityId, $patient, $user->hasRole('ROLE_DOCTOR') ? 'doctor' : 'clinic', $entityId);

        if (($rn = trim((string) ($data['record_number'] ?? ''))) !== '') {
            $record->setRecordNumber($rn);
        }
        $tagError = $this->applyRecordTags($record, $data, $entityType, $entityId);
        if ($tagError !== null) {
            return $tagError;
        }

        $this->recordRepo->save($record);

        return $this->success($record->toArray(), 201);
    }

    #[Route('/api/v1/patient/{uuid}', methods: ['GET'])]
    public function show(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $data = $record->toArray();
        $data['profile'] = $this->buildPatientProfile($record->getUser());

        return $this->success($data);
    }

    #[Route('/api/v1/patient/{uuid}', methods: ['PATCH'])]
    public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'update');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $patient = $record->getUser();
        $data    = json_decode($request->getContent(), true) ?? [];

        if (array_key_exists('name', $data)) {
            $name = trim((string) $data['name']);
            if ($name !== '') {
                $patient->setRealName($name);
            }
        }

        // کد ملی باید ۱۰ رقم و در سطح بیمار یکتا باشد
        if (array_key_exists('national_code', $data)) {
            $nationalCode = trim((string) ($data['national_code'] ?? ''));
            if ($nationalCode !== '') {
                if (!preg_match('/^\d{10}$/', $nationalCode)) {
                    return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی باید ۱۰ رقم باشد', 422, 'national_code');
                }
                $owner = $this->profileRepo->findOneByNationalCode($nationalCode);
                if ($owner !== null && $owner->getUser()->getId() !== $patient->getId()) {
                    $masked = \App\Shared\Service\InputValidator::maskMobile($owner->getUser()->getMobileNumber());
                    return $this->error(
                        ErrorCodes::ERR_PROFILE_NATIONAL_CODE_TAKEN,
                        "این کد ملی قبلاً با شماره {$masked} ثبت شده است",
                        409,
                        'national_code'
                    );
                }
                $patient->setNationalCode($nationalCode);
            }
        }

        // موبایل = شناسه ورود کاربر؛ تغییر آن باید ۱۱ رقمی معتبر و در سطح کاربران یکتا باشد
        if (array_key_exists('mobile', $data)) {
            $mobile = trim((string) ($data['mobile'] ?? ''));
            if ($mobile !== '' && $mobile !== $patient->getMobileNumber()) {
                if (!preg_match('/^09\d{9}$/', $mobile)) {
                    return $this->error(ErrorCodes::ERR_VALIDATION_001, 'شماره موبایل نامعتبر است', 422, 'mobile');
                }
                $owner = $this->userRepo->findByMobile($mobile);
                if ($owner !== null && $owner->getId() !== $patient->getId()) {
                    return $this->error(
                        ErrorCodes::ERR_PROFILE_MOBILE_TAKEN,
                        ErrorCodes::message(ErrorCodes::ERR_PROFILE_MOBILE_TAKEN),
                        409,
                        'mobile'
                    );
                }
                $patient->setMobileNumber($mobile);
            }
        }
        $this->userRepo->save($patient);

        $profile = $this->profileRepo->findByUser($patient) ?? new UserProfile($patient);

        $stringFields = [
            'family'          => 'setFamily',
            'fathers_name'    => 'setFathersName',
            'gender'          => 'setGender',
            'blood_type'      => 'setBloodType',
            'marital_status'  => 'setMaritalStatus',
            'education'       => 'setEducation',
            'field_of_study'  => 'setFieldOfStudy',
            'job'             => 'setJob',
            'address'         => 'setAddress',
            'postal_code'     => 'setPostalCode',
            'referral_source' => 'setReferralSource',
            'description'     => 'setDescription',
            'home_phone'      => 'setHomePhone',
            'work_phone'      => 'setWorkPhone',
        ];
        foreach ($stringFields as $key => $setter) {
            if (array_key_exists($key, $data)) {
                $v = is_string($data[$key]) ? trim($data[$key]) : $data[$key];
                $profile->$setter($v === '' ? null : $v);
            }
        }

        $intFields = [
            'province_id' => 'setProvinceId',
            'city_id'     => 'setCityId',
        ];
        foreach ($intFields as $key => $setter) {
            if (array_key_exists($key, $data)) {
                $v = $data[$key];
                $profile->$setter(($v === null || $v === '') ? null : (int) $v);
            }
        }

        if (array_key_exists('national_code', $data)) {
            $nc = trim((string) ($data['national_code'] ?? ''));
            $profile->setNationalCode($nc === '' ? null : $nc);
        }
        if (array_key_exists('date_of_birth', $data)) {
            $dob = $data['date_of_birth'];
            $profile->setDateOfBirth(($dob === null || $dob === '') ? null : (int) $dob);
        }
        if (array_key_exists('basic_insurance_id', $data)) {
            $bi = $data['basic_insurance_id'];
            $profile->setBasicInsuranceId(($bi === null || $bi === '') ? null : (int) $bi);
        }
        if (array_key_exists('supplementary_insurance_id', $data)) {
            $si = $data['supplementary_insurance_id'];
            $profile->setSupplementaryInsuranceId(($si === null || $si === '') ? null : (int) $si);
        }
        $this->profileRepo->save($profile);

        if (array_key_exists('record_number', $data)) {
            $rn = trim((string) ($data['record_number'] ?? ''));
            $record->setRecordNumber($rn === '' ? null : $rn);
        }
        $tagError = $this->applyRecordTags($record, $data, $entityType, $entityId);
        if ($tagError !== null) {
            return $tagError;
        }
        $this->recordRepo->save($record);

        $out = $record->toArray();
        $out['profile'] = $this->buildPatientProfile($patient);

        return $this->success($out);
    }

    #[Route('/api/v1/patient/{uuid}/sessions', methods: ['GET'])]
    public function sessions(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $page     = max(1, (int) $request->query->get('page', 1));
        $limit    = min(50, max(10, (int) $request->query->get('limit', 20)));
        // فیلتر آرشیو: active (پیش‌فرض — آرشیوها مخفی) | all | archived
        $filter   = in_array($request->query->get('filter'), ['all', 'archived'], true) ? $request->query->get('filter') : 'active';
        $sessions = $this->sessionRepo->findByRecord($record, $page, $limit, $filter);
        $total    = $this->sessionRepo->countByRecord($record, $filter);

        return $this->paginated(
            array_map(fn($s) => $this->sessionWithBilling($s), $sessions),
            $total,
            $page,
            $limit
        );
    }

    #[Route('/api/v1/patient/{uuid}/appointments', methods: ['GET'])]
    public function appointments(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        // محیط نوبت با appointment.clinic بیان می‌شود. تکیه بر عضویت فعلیِ پزشک
        // یعنی با پایان همکاری، تاریخچهٔ نوبت‌های همان کلینیک از پرونده ناپدید شود.
        $appointments = $entityType === 'doctor'
            ? $this->appointmentRepo->findByUserAndDoctorIds($record->getUser(), [$entityId])
            : $this->appointmentRepo->findByUserAndClinic($record->getUser(), $entityId);

        return $this->success(array_map(fn(\App\Appointment\Entity\Appointment $a) => [
            'uuid'         => $a->getUuid(),
            'starts_at'    => $a->getSlotStart(),
            'ends_at'      => $a->getSlotEnd(),
            'status'       => $a->getStatus(),
            'version'      => $a->getVersion(),
            'doctor_name'  => $a->getDoctor()->getName(),
            'service_name' => null,
            'price_rials'  => null,
            'created_at'   => $a->getSlotStart(),
        ], $appointments));
    }

    /**
     * خروجی session به‌همراه خلاصه‌ی صورتحساب: uuid فاکتور (در صورت وجود) و
     * مانده‌ی بدهیِ سهم بیمار. اگر session تسویه شده باشد (payment_method != pending)
     * بدهی صفر است؛ در غیر این صورت سهم بیمار از فاکتور یا کل مبلغ نهایی.
     */
    /** @var array<int, string|null> نام بیمه‌ها، یک‌بار در هر درخواست (لیست مراجعه‌ها N+1 نشود). */
    private array $insuranceNameCache = [];

    private function insuranceNameById(?int $id): ?string
    {
        if ($id === null) {
            return null;
        }

        return $this->insuranceNameCache[$id] ??= $this->insuranceRepo->find($id)?->getName();
    }

    private function sessionWithBilling(\App\Patient\Entity\PatientSession $session): array
    {
        $data    = $session->toArray();
        $data['insurance_base_name']          = $this->insuranceNameById($session->getInsuranceBaseId());
        $data['insurance_supplementary_name'] = $this->insuranceNameById($session->getInsuranceSupplementaryId());
        $invoice = $session->getId() !== null ? $this->invoiceRepo->findBySession($session->getId()) : null;

        $data['invoice_uuid']   = $invoice?->getUuid();
        $data['invoice_status'] = $invoice?->getStatus();

        // مانده‌ی بدهی همیشه از خودِ مراجعه (سازگار با is_paid): مبلغ نهایی منهای تخفیف
        // و پرداخت‌ها. اگر مراجعه پس از صدور فاکتور ویرایش شود، همین منبعِ واحد ملاک است.
        $data['patient_debt_rials'] = $session->getRemainingRials();

        return $data;
    }

    #[Route('/api/v1/patient/{uuid}/session', methods: ['POST'])]
    public function createSession(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'update');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $data    = json_decode($request->getContent(), true) ?? [];
        $session = $this->patientService->createSession($record, $data, $entityType, $entityId);

        $this->sessionBilling->ensureFinalizedInvoice($session, $entityType, $entityId);

        return $this->success($this->sessionWithBilling($session), 201);
    }


    #[Route('/api/v1/session/{uuid}', methods: ['PATCH'])]
    public function updateSession(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'patients', 'update');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'patients', 'update');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $session = $this->sessionRepo->findByUuid($uuid);
        if ($session === null || !$this->ownsRecord($session->getRecord(), $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_SESSION_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_SESSION_NOT_FOUND), 404);
        }

        $data = json_decode($request->getContent(), true) ?? [];

        if (isset($data['notes'])) { $session->setNotes($data['notes']); }

        // آرشیو نرم: مخفی‌سازی مراجعه‌ی اشتباه بدون حذف سابقه.
        if (array_key_exists('archived', $data)) { $session->setArchived((bool) $data['archived']); }

        // ویرایش سرویس‌ها/کالاها/قیمت ویزیت/بیمه — با بازمحاسبه و ثبت تاریخچه.
        if (array_key_exists('services', $data) || array_key_exists('consumables', $data)
            || array_key_exists('visit_price_rials', $data) || array_key_exists('insurance_base_id', $data)
            || array_key_exists('base_insurance_discount_percent', $data)) {
            $this->patientService->updateSessionServices($session, $data, $entityType, $entityId, $user);

            // بیمه ممکن است همین حالا به مراجعه اضافه شده باشد؛ بدون این، مراجعه‌ای که
            // بیمه‌اش بعداً ثبت می‌شود هرگز صورتحساب و مطالبه نمی‌گیرد.
            $this->sessionBilling->ensureFinalizedInvoice($session, $entityType, $entityId);
        }

        // تخفیف بر اساس قانون (discount_rule_uuid): مقدار از خود قانون، با ثبت منبع.
        // '' یا null → حذف تخفیف. اولویت بر تخفیف دستی.
        if (array_key_exists('discount_rule_uuid', $data)) {
            $ruleUuid = $data['discount_rule_uuid'];
            if ($ruleUuid === null || $ruleUuid === '') {
                $this->patientService->applyDiscount($session, null, 0, null, null, $user);
            } else {
                $rule = $this->discountRuleRepo->findByUuidForOwner((string) $ruleUuid, $entityType, $entityId);
                if ($rule === null) {
                    return $this->error(ErrorCodes::ERR_VALIDATION_002, 'قانون تخفیف یافت نشد', 404, 'discount_rule_uuid');
                }
                $this->patientService->applyDiscountRule($session, $rule, $user);
            }
        } elseif (array_key_exists('discount_type', $data)) {
            // تخفیف دستی: discount_type = percent|fixed|null (null = حذف تخفیف)
            $type = $data['discount_type'] !== null ? (string) $data['discount_type'] : null;
            $this->patientService->applyDiscount($session, $type, (int) ($data['discount_value'] ?? 0), null, null, $user);
        }

        if (isset($data['paid_at'])) { $session->setPaidAt((int) $data['paid_at']); }

        if (isset($data['payment_method'])) {
            $method = (string) $data['payment_method'];
            // پرداخت از کیف پول: مانده را از موجودی کسر کن و همزمان SessionPayment ثبت کن.
            // از addSessionPayment استفاده می‌شود تا مبلغ = مانده (پس از تخفیف و پرداخت‌های
            // قبلی) باشد و is_paid/paid_at درست ست شود؛ موجودی ناکافی → AppException (۴۲۲).
            if ($method === 'wallet' && $session->getRemainingRials() > 0) {
                $this->patientService->addSessionPayment(
                    $session,
                    'wallet',
                    $session->getRemainingRials(),
                    isset($data['paid_at']) ? (int) $data['paid_at'] : null,
                    $user,
                );
            } else {
                $session->setPaymentMethod($method);
            }
        }

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

        return $this->success($this->sessionWithBilling($session));
    }

    /**
     * ثبت پرداخت جزئی روی مراجعه (تسویه چندتکه).
     * body: { method: wallet|pos|cash|card, amount_rials: int, paid_at?: int }
     * روش wallet همان مبلغ را از کیف پول بیمار کسر می‌کند. وقتی مانده صفر شود
     * مراجعه تسویه‌شده (is_paid) می‌شود.
     */
    #[Route('/api/v1/session/{uuid}/payments', methods: ['POST'])]
    public function addSessionPayment(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'payments', 'create');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'payments', 'create');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $session = $this->sessionRepo->findByUuid($uuid);
        if ($session === null || !$this->ownsRecord($session->getRecord(), $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_SESSION_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_SESSION_NOT_FOUND), 404);
        }

        $data = json_decode($request->getContent(), true) ?? [];

        $this->patientService->addSessionPayment(
            $session,
            (string) ($data['method'] ?? ''),
            (int) ($data['amount_rials'] ?? 0),
            isset($data['paid_at']) ? (int) $data['paid_at'] : null,
            $user,
        );

        return $this->success($this->sessionWithBilling($session), 201);
    }

    /** ویرایش یک پرداخت ثبت‌شده. body: { method?, amount_rials?, paid_at? }. */
    #[Route('/api/v1/session/{uuid}/payments/{paymentUuid}', methods: ['PATCH'])]
    public function updateSessionPayment(string $uuid, string $paymentUuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'payments', 'update');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'payments', 'update');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $session = $this->sessionRepo->findByUuid($uuid);
        if ($session === null || !$this->ownsRecord($session->getRecord(), $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_SESSION_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_SESSION_NOT_FOUND), 404);
        }
        $payment = $this->sessionPaymentRepo->findByUuid($paymentUuid);
        if ($payment === null || $payment->getSession()->getUuid() !== $uuid) {
            return $this->error(ErrorCodes::ERR_SESSION_PAYMENT_INVALID, 'پرداخت یافت نشد', 404, 'paymentUuid');
        }

        $this->patientService->updatePayment($payment, json_decode($request->getContent(), true) ?? [], $user);

        return $this->success($this->sessionWithBilling($session));
    }

    /** حذف یک پرداخت ثبت‌شده. */
    #[Route('/api/v1/session/{uuid}/payments/{paymentUuid}', methods: ['DELETE'])]
    public function deleteSessionPayment(string $uuid, string $paymentUuid, #[CurrentUser] User $user): JsonResponse
    {
        $this->secretaryAccess->denyUnlessGranted($user, 'payments', 'delete');
        $this->clinicDoctorAccess->denyUnlessGranted($user, 'payments', 'delete');
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $session = $this->sessionRepo->findByUuid($uuid);
        if ($session === null || !$this->ownsRecord($session->getRecord(), $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_SESSION_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_SESSION_NOT_FOUND), 404);
        }
        $payment = $this->sessionPaymentRepo->findByUuid($paymentUuid);
        if ($payment === null || $payment->getSession()->getUuid() !== $uuid) {
            return $this->error(ErrorCodes::ERR_SESSION_PAYMENT_INVALID, 'پرداخت یافت نشد', 404, 'paymentUuid');
        }

        $this->patientService->deletePayment($payment, $user);

        return $this->success($this->sessionWithBilling($session));
    }

    /** تاریخچه‌ی تغییرات مالی/خدماتی مراجعه (Audit Log). */
    #[Route('/api/v1/session/{uuid}/audit-log', methods: ['GET'])]
    public function sessionAuditLog(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $session = $this->sessionRepo->findByUuid($uuid);
        if ($session === null || !$this->ownsRecord($session->getRecord(), $entityType, $entityId, $user)) {
            return $this->error(ErrorCodes::ERR_SESSION_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_SESSION_NOT_FOUND), 404);
        }

        return $this->success($this->sessionAuditRepo->findBySessionUuid($uuid));
    }

    /** @var array<int, PatientRecordScope> حل‌شده یک‌بار در هر درخواست، نه یک‌بار به‌ازای هر چک. */
    private array $scopeCache = [];

    private function scope(User $user): PatientRecordScope
    {
        return $this->scopeCache[$user->getId()] ??= $this->resolveScope($user);
    }

    /** منشیِ بدون مجوز patients.view هیچ پرونده‌ای نمی‌بیند؛ سایر نقش‌ها از رزولور. */
    private function resolveScope(User $user): PatientRecordScope
    {
        if ($user->hasRole('ROLE_SECRETARY') && !$this->secretaryAccess->can($user, 'patients', 'view')) {
            return PatientRecordScope::unknown();
        }

        return $this->scopeResolver->resolve($user);
    }

    /** @return array{0: string, 1: int|null} */
    private function resolveEntity(User $user): array
    {
        return $this->scope($user)->toLegacyTuple();
    }

    private function assertPatientGate(string $entityType, ?int $entityId): void
    {
        if ($entityId === null) {
            throw new AppException(ErrorCodes::ERR_FORBIDDEN_001, null, 403);
        }

        if (!$this->subscriptionService->hasFeature($entityType, $entityId, 'patient_records')) {
            throw new AppException(ErrorCodes::ERR_SUBSCRIPTION_REQUIRED, null, 403);
        }
    }

    /**
     * محیط پرونده باید همان محیط کاربر باشد، و اگر دسترسی کاربر به بیمارانِ پزشک(های)
     * مشخصی محدود است، پرونده هم باید در همان محدوده بیفتد — همان قاعدهٔ لیست.
     */
    private function ownsRecord($record, string $entityType, ?int $entityId, User $user): bool
    {
        if ($entityId === null
            || $record->getEntityType() !== $entityType
            || $record->getEntityId() !== $entityId) {
            return false;
        }

        return $this->recordRepo->isVisibleToDoctors(
            $record,
            $this->scope($user)->restrictToDoctorIds,
        );
    }
}
