<?php

namespace App\Appointment\Controller;

use App\Appointment\Entity\Appointment;
use App\Shared\Context\EntityContext;
use App\Shared\Constant\ErrorCodes;
use App\Appointment\Repository\AppointmentRepository;
use App\Appointment\Repository\SlotTakenException;
use App\Appointment\Service\SlotCalculatorService;
use App\Auth\Entity\User;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Entity\Doctor;
use App\Doctor\Repository\DoctorRepository;
use App\Insurance\Service\VisitPriceRequirementResolver;
use App\Patient\Service\PatientResolver;
use App\Secretary\Entity\DoctorSecretary;
use App\Secretary\Repository\DoctorSecretaryRepository;
use App\Shared\Controller\BaseController;
use App\Shared\Service\InputValidator;
use Doctrine\ORM\EntityManagerInterface;
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: 'My Appointments')]
class MyAppointmentsController extends BaseController
{
    public function __construct(
        private readonly EntityManagerInterface        $em,
        private readonly AppointmentRepository         $appointmentRepo,
        private readonly DoctorRepository              $doctorRepo,
        private readonly ClinicRepository              $clinicRepo,
        private readonly DoctorSecretaryRepository     $secretaryRepo,
        private readonly \App\Shared\Context\EntityContextResolver $contextResolver,
        private readonly SlotCalculatorService         $slotCalculator,
        private readonly \App\Appointment\Service\BookingContextResolver $bookingContext,
        private readonly \App\ClinicService\Repository\ServiceSectionRepository $sectionRepo,
        private readonly \App\ClinicService\Repository\ServiceItemRepository $itemRepo,
        private readonly \App\Staff\Repository\ClinicStaffRepository $staffRepo,
        private readonly PatientResolver              $patientResolver,
        private readonly \App\Auth\Repository\UserRepository $userRepo,
        private readonly \App\UserProfile\Repository\UserProfileRepository $profileRepo,
        private readonly VisitPriceRequirementResolver $visitPriceResolver,
        private readonly \App\Shared\Tenant\TenantOwnershipChecker $tenantOwnership,
    ) {}

    /**
     * پزشکانِ در دسترسِ کاربرِ پنل — برای تب‌ها/تایم‌لاینِ نوبت‌ها. برخلاف
     * /clinic/doctor-list (که عمومی است و همهٔ پزشکانِ کلینیک را می‌دهد)، این
     * اندپوینت احرازشده است: منشی فقط پزشکانِ تخصیص‌یافتهٔ خودش را می‌گیرد، پس تب‌ها
     * دقیقاً با مجوزِ نوبت‌دهی‌اش هم‌راستا می‌شوند.
     */
    #[Route('/api/v1/my/clinic-doctors', methods: ['GET'])]
    #[IsGranted('IS_AUTHENTICATED_FULLY')]
    public function myClinicDoctors(#[CurrentUser] User $user): JsonResponse
    {
        $roles   = $user->getRoles();
        $doctors = [];

        if (in_array('ROLE_SECRETARY', $roles, true)) {
            $filter = $this->resolveSecretaryFilter($user);
            if ($filter !== null) {
                [$type, $value] = $filter;
                $doctors = $type === 'clinic'
                    ? ($value === [] ? [] : $this->doctorRepo->findBy(['id' => $value]))
                    : [$value];
            }
        } elseif (in_array('ROLE_CLINIC', $roles, true)) {
            $clinic  = $this->clinicRepo->findByUser($user);
            $doctors = $clinic !== null ? $clinic->getDoctors()->toArray() : [];
        } elseif (in_array('ROLE_DOCTOR', $roles, true)) {
            $doctor  = $this->doctorRepo->findByUser($user);
            $doctors = $doctor !== null ? [$doctor] : [];
        }

        $data = array_map(
            static fn(Doctor $d) => ['uuid' => $d->getUuid(), 'name' => $d->getName()],
            $doctors,
        );

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

    #[Route('/api/v1/my/appointment', methods: ['POST'])]
    #[IsGranted('IS_AUTHENTICATED_FULLY')]
    public function createAppointment(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $roles = $user->getRoles();
        $allowed = ['ROLE_DOCTOR', 'ROLE_CLINIC', 'ROLE_SECRETARY', 'ROLE_ADMIN'];
        if (!array_intersect($allowed, $roles)) {
            return $this->error(ErrorCodes::FORBIDDEN, 'دسترسی ندارید', 403);
        }

        $data        = json_decode($request->getContent(), true) ?? [];
        $doctorUuid  = trim($data['doctor_uuid']    ?? '');
        $slotStart   = (int) ($data['slot_start']   ?? 0);
        $slotEnd     = (int) ($data['slot_end']      ?? 0);
        $mobile      = InputValidator::toEnglishDigits(trim($data['patient_mobile'] ?? ''));
        $patientName = trim($data['patient_name']   ?? '');
        $nationalCode = InputValidator::toEnglishDigits(trim((string) ($data['patient_national_code'] ?? '')));
        $isReserve   = (bool) ($data['is_reserve'] ?? false);

        // Reserve entries are day-level: only a date is picked in the UI, so
        // slot_end may equal slot_start and the past-slot rule does not apply.
        if ($isReserve && $slotEnd < $slotStart) {
            $slotEnd = $slotStart;
        }

        // حالت نوبت‌دهی سرویسی: مدت نوبت از مجموعِ مدت سرویس‌های انتخاب‌شده تعیین
        // و slot_end سمت سرور محاسبه می‌شود (به مقدار کلاینت اعتماد نمی‌شود).
        // `duration_from_services` فقط در نوبت‌دهی سرویسی true است: آنجا مدت نوبت از
        // مجموع سرویس‌ها محاسبه و slot_end بازنویسی می‌شود. در حالت اسلاتی، سرویس‌ها
        // فقط به نوبت پیوست می‌شوند و ساعت پایانِ دستی حفظ می‌شود.
        $serviceUuids    = array_values(array_filter(array_map('trim', (array) ($data['service_item_uuids'] ?? []))));
        $computeDuration = (bool) ($data['duration_from_services'] ?? false);
        // مدتِ override منشی برای همین نوبت (پیش‌فرض سرویس تغییر نمی‌کند). { uuid: minutes }
        $durationOverrides = (array) ($data['service_durations'] ?? []);
        $serviceItems    = [];
        if (!empty($serviceUuids) && !$isReserve) {
            $totalMinutes = 0;
            foreach ($serviceUuids as $u) {
                $item = $this->itemRepo->findByUuid($u);
                if ($item === null) {
                    return $this->error(ErrorCodes::VALIDATION, 'سرویس یافت نشد', 422, 'service_item_uuids');
                }
                if ($computeDuration) {
                    if (!$item->isBookable()) {
                        return $this->error(ErrorCodes::VALIDATION, 'این سرویس برای نوبت‌دهی فعال نیست', 422, 'service_item_uuids');
                    }
                    $duration = isset($durationOverrides[$u]) && (int) $durationOverrides[$u] > 0
                        ? (int) $durationOverrides[$u]
                        : (int) ($item->getDurationMinutes() ?? 0);
                    if ($duration <= 0) {
                        return $this->error(ErrorCodes::VALIDATION, 'مدت سرویس تعریف نشده است', 422, 'service_item_uuids');
                    }
                    $totalMinutes += $duration;
                }
                $serviceItems[] = $item;
            }
            if ($computeDuration) {
                $slotEnd = $slotStart + $totalMinutes * 60;
            }
        }

        if (empty($doctorUuid) || $slotStart <= 0 || (!$isReserve && $slotEnd <= $slotStart) || empty($mobile) || empty($patientName)) {
            return $this->error(ErrorCodes::VALIDATION, 'همه فیلدها الزامی است', 422);
        }

        if ($nationalCode === '') {
            return $this->error(ErrorCodes::VALIDATION, 'کد ملی بیمار الزامی است', 422, 'patient_national_code');
        }
        if (!InputValidator::isValidIranNationalCode($nationalCode)) {
            return $this->error(ErrorCodes::VALIDATION, 'کد ملی نامعتبر است', 422, 'patient_national_code');
        }

        if (!$isReserve && $slotStart < time()) {
            return $this->error(ErrorCodes::SLOT_PAST, 'زمان این اسلات گذشته است', 422);
        }

        $doctor = $this->doctorRepo->findByUuid($doctorUuid);
        if (!$doctor) return $this->error(ErrorCodes::DOCTOR_NOT_FOUND, 'پزشک یافت نشد', 404);

        if (!$this->canBookForDoctor($user, $doctor)) {
            return $this->error(ErrorCodes::FORBIDDEN, 'برای این پزشک مجاز به ثبت نوبت نیستید', 403);
        }

        $visitPriceRials = isset($data['visit_price_rials']) ? (int) $data['visit_price_rials'] : null;
        if ($this->visitPriceResolver->isRequiredForDoctor($doctor) && ($visitPriceRials ?? 0) <= 0) {
            return $this->error(ErrorCodes::VALIDATION, 'هزینه ویزیت الزامی است', 422, 'visit_price_rials');
        }

        // Identity is keyed on the national code (unique) so the case-file stays
        // single per person even when booked under a different mobile.
        $patient = $this->patientResolver->resolveForBooking($nationalCode, $mobile, $patientName);

        $appointment = new Appointment($doctor, $patient, $slotStart, $slotEnd);
        $appointment->setPatientNationalCode($nationalCode);
        if (!empty($data['note'])) $appointment->setNote($data['note']);
        // محل نوبت باید از همان محیطی بیاید که نوبت در آن ثبت می‌شود؛ بدون clinic_uuid
        // یعنی مطب شخصی، نه «هر برنامه‌ای که پیدا شد».
        $bookingClinic = $this->bookingContext->resolve($doctor, $data['clinic_uuid'] ?? null);
        $appointment->setClinic($bookingClinic);
        $appointment->assignTenant(EntityContext::forBooking($doctor, $bookingClinic));
        $locationId    = $this->slotCalculator->resolveSlotLocationId($doctor, $slotStart, $bookingClinic, true);
        if ($locationId !== null) $appointment->setAddressId($locationId);

        // بخش/سرویس/پرسنل با uuid از خودِ درخواست می‌آیند و TenantFilter پوششان
        // نمی‌دهد (سرویس فرزند aggregate است، و کاربرِ بدون محیطِ انتخاب‌شده اصلاً
        // فیلتر نمی‌خورد). بدون این بررسی، نوبت با بخش/سرویس/پرسنلِ محیط دیگری
        // ذخیره می‌شد — نه فقط خوانده، بلکه نوشته.
        $bookingContext = EntityContext::forBooking($doctor, $bookingClinic);

        foreach ([
            'service_section_uuid' => [$this->sectionRepo, 'setServiceSection', 'بخش'],
            'service_item_uuid'    => [$this->itemRepo, 'setServiceItem', 'سرویس'],
            'staff_uuid'           => [$this->staffRepo, 'setStaff', 'پرسنل'],
        ] as $key => [$repo, $setter, $label]) {
            $value = trim((string) ($data[$key] ?? ''));
            if ($value === '') {
                continue;
            }
            $entity = $repo->findByUuid($value);
            if (!$this->tenantOwnership->belongsTo($bookingContext, $entity)) {
                return $this->error(ErrorCodes::VALIDATION, $label . ' یافت نشد', 422);
            }
            $appointment->$setter($entity);
        }

        if (!$this->tenantOwnership->allBelongTo($bookingContext, $serviceItems)) {
            return $this->error(ErrorCodes::VALIDATION, 'سرویس انتخاب‌شده به این محل نوبت‌دهی تعلق ندارد', 422, 'service_item_uuids');
        }

        // پیوستِ همهٔ سرویس‌های انتخاب‌شده؛ سرویسِ اصلی = اولین سرویس (addServiceItem).
        foreach ($serviceItems as $si) {
            $appointment->addServiceItem($si);
        }
        if (!empty($data['deposit_required'])) {
            $appointment->setDepositRequired(true);
        }
        if (isset($data['deposit_amount_rials'])) {
            $appointment->setDepositAmountRials((int) $data['deposit_amount_rials']);
        }
        if ($visitPriceRials !== null) {
            $appointment->setVisitPriceRials($visitPriceRials);
        }
        // The resolver returns the patient keyed on national code, so its profile
        // name is the real identity. Snapshot that (not the free-typed modal name)
        // so the appointment never diverges from an existing profile; fall back to
        // the entered name only for a brand-new patient without a stored name.
        $appointment->setPatientName($patient->getRealName() ?: $patientName);
        $appointment->setPatientMobile($mobile);

        // نوبت پنلی «ثبت‌شده» (pending) متولد می‌شود، نه قطعی: قطعی‌شدن یک عملِ جداست
        // که هزینه‌ها را نشان می‌دهد و پرداخت می‌گیرد (POST /appointment/{uuid}/confirm).
        // pending هم اسلات را اشغال می‌کند (SLOT_OCCUPYING_STATUSES)، پس جای نوبت
        // محفوظ می‌ماند. expiresAt ست نمی‌شود، پس هرگز خودبه‌خود منقضی نمی‌شود.

        if ($isReserve) {
            // Day-level reserve: no slot occupation, plain save (no atomic slot check).
            $appointment->rescheduleTo($slotStart, $slotEnd, true);
            $this->appointmentRepo->save($appointment);
        } else {
            try {
                $this->appointmentRepo->bookAtomically($appointment);
            } catch (SlotTakenException) {
                return $this->error(ErrorCodes::SLOT_TAKEN, 'این نوبت قبلاً رزرو شده است', 409);
            }
        }

        return $this->success([
            'uuid'       => $appointment->getUuid(),
            'slot_start' => $slotStart,
            'slot_end'   => $slotEnd,
            'status'     => $appointment->getStatus(),
            'is_reserve' => $appointment->isReserve(),
        ], 201);
    }

    /**
     * Booking-scoped patient lookup by mobile OR national code. Lets the booking
     * form search an existing patient before asking for national code / name. Unlike
     * /patient/search-user this is not gated by the patient_records feature and
     * allows ROLE_ADMIN, because booking must work regardless of subscription.
     */
    #[Route('/api/v1/my/appointment/patient-lookup', methods: ['GET'])]
    #[IsGranted('IS_AUTHENTICATED_FULLY')]
    public function patientLookup(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $allowed = ['ROLE_DOCTOR', 'ROLE_CLINIC', 'ROLE_SECRETARY', 'ROLE_ADMIN'];
        if (!array_intersect($allowed, $user->getRoles())) {
            return $this->error(ErrorCodes::FORBIDDEN, 'دسترسی ندارید', 403);
        }

        // Lookup by mobile OR national code — the booking form lets the user
        // search either way. National code takes precedence when both are sent.
        $mobile       = InputValidator::toEnglishDigits(trim((string) $request->query->get('mobile', '')));
        $nationalQuery = InputValidator::toEnglishDigits(trim((string) $request->query->get('national_code', '')));

        if ($nationalQuery !== '') {
            if (!InputValidator::isValidIranNationalCode($nationalQuery)) {
                return $this->error(ErrorCodes::VALIDATION, 'کد ملی نامعتبر است', 422, 'national_code');
            }
            // National code lives on the profile (profiles.national_code), not on User.
            $profile = $this->profileRepo->findOneByNationalCode($nationalQuery);
            $patient = $profile?->getUser();
        } elseif (InputValidator::isValidIranMobile($mobile)) {
            $patient = $this->userRepo->findByMobile($mobile);
        } else {
            return $this->error(ErrorCodes::VALIDATION, 'شماره موبایل یا کد ملی نامعتبر است', 422, 'mobile');
        }

        if ($patient === null) {
            return $this->success(['found' => false]);
        }

        $nationalCode = $this->profileRepo->findByUser($patient)?->getNationalCode();

        return $this->success([
            'found'         => true,
            'name'          => $patient->getRealName(),
            'mobile'        => $patient->getMobileNumber(),
            'national_code' => $nationalCode,
        ]);
    }

    #[Route('/api/v1/my/appointments', methods: ['GET'])]
    #[IsGranted('IS_AUTHENTICATED_FULLY')]
    public function myAppointments(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $page       = max(1, (int) $request->query->get('page', 1));
        $limit      = min(500, max(1, (int) $request->query->get('limit', 15)));
        $search     = trim((string) $request->query->get('search', ''));
        $status     = trim((string) $request->query->get('status', ''));
        $date       = trim((string) $request->query->get('date', ''));
        $doctorUuid = trim((string) $request->query->get('doctor_uuid', ''));

        // reserve=1 → only reserve-list entries; otherwise the regular slot list.
        $reserveOnly = $request->query->get('reserve') === '1';

        $qb = $this->em->createQueryBuilder()
            ->select(
                'DISTINCT a.uuid, a.slotStart, a.slotEnd, a.status, a.createdAt, a.version',
                'a.isReserve, a.depositRequired, a.depositAmountRials, a.note, a.patientName as override_name',
                'a.patientNationalCode as national_code, a.patientGender as gender',
                'd.uuid as doctor_uuid, d.name as doctor_name',
                'u.uuid as patient_uuid, u.mobileNumber as patient_mobile, u.realName as patient_name',
                'ss.uuid as section_uuid, ss.name as section_name',
                'si.uuid as service_uuid, si.name as service_name',
                'st.uuid as staff_uuid, st.fullName as staff_name'
            )
            ->from(Appointment::class, 'a')
            ->join('a.doctor', 'd')
            ->join('a.user', 'u')
            ->leftJoin('a.serviceSection', 'ss')
            ->leftJoin('a.serviceItem', 'si')
            ->leftJoin('a.staff', 'st')
            ->andWhere('a.isReserve = :reserveOnly')
            ->setParameter('reserveOnly', $reserveOnly)
            ->orderBy('a.slotStart', 'ASC');

        $roles = $user->getRoles();

        if (in_array('ROLE_ADMIN', $roles, true)) {
            // Admin sees all
        } elseif (in_array('ROLE_CLINIC', $roles, true)) {
            $clinic = $this->clinicRepo->findByUser($user);
            if ($clinic === null) {
                return $this->paginated([], 0, $page, $limit);
            }
            $qb->join('App\Clinic\Entity\Clinic', 'c', 'WITH', 'd MEMBER OF c.doctors')
               ->andWhere('c = :clinic')
               ->setParameter('clinic', $clinic);
        } elseif (in_array('ROLE_DOCTOR', $roles, true)) {
            $doctor = $this->doctorRepo->findByUser($user);
            if ($doctor === null) {
                return $this->paginated([], 0, $page, $limit);
            }
            $qb->andWhere('a.doctor = :doctor')
               ->setParameter('doctor', $doctor);
        } elseif (in_array('ROLE_SECRETARY', $roles, true)) {
            $filter = $this->resolveSecretaryFilter($user);
            if ($filter === null) {
                return $this->paginated([], 0, $page, $limit);
            }
            [$filterType, $filterValue, $canView] = $filter;
            if (!$canView) {
                return $this->paginated([], 0, $page, $limit);
            }
            if ($filterType === 'clinic') {
                // $filterValue = آرایه‌ی idهای پزشکانِ تخصیص‌یافته به این منشی در کلینیک
                if (empty($filterValue)) {
                    return $this->paginated([], 0, $page, $limit);
                }
                $qb->andWhere('a.doctor IN (:doctorIds)')
                   ->setParameter('doctorIds', $filterValue);
            } else {
                $qb->andWhere('a.doctor = :doctor')
                   ->setParameter('doctor', $filterValue);
            }
        } else {
            // Plain patient: only their own appointments.
            $qb->andWhere('a.user = :patient')
               ->setParameter('patient', $user);
        }

        if ($search !== '') {
            $qb->andWhere('u.mobileNumber LIKE :s OR d.name LIKE :s OR u.realName LIKE :s')
               ->setParameter('s', '%' . $search . '%');
        }
        if ($status !== '') {
            $qb->andWhere('a.status = :status')->setParameter('status', $status);
        }
        if ($date !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            $dayStart = (int) strtotime($date . ' 00:00:00');
            $dayEnd   = (int) strtotime($date . ' 23:59:59');
            $qb->andWhere('a.slotStart >= :dayStart AND a.slotStart <= :dayEnd')
               ->setParameter('dayStart', $dayStart)
               ->setParameter('dayEnd', $dayEnd);
        }
        if ($doctorUuid !== '') {
            $qb->andWhere('d.uuid = :doctorUuid')->setParameter('doctorUuid', $doctorUuid);
        }

        $total = (clone $qb)->select('COUNT(a.id)')->getQuery()->getSingleScalarResult();

        $rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
                   ->getQuery()->getArrayResult();

        $items = array_map(fn(array $a) => [
            'uuid'             => $a['uuid'],
            'patient_name'     => $a['override_name'] ?: ($a['patient_name'] ?? ''),
            'patient_mobile'   => $a['patient_mobile'],
            'patient_uuid'     => $a['patient_uuid'],
            'doctor_uuid'      => $a['doctor_uuid'],
            'doctor_name'      => $a['doctor_name'],
            'slot_start'       => (int) $a['slotStart'],
            'slot_end'         => (int) $a['slotEnd'],
            'appointment_date' => date('Y-m-d', (int) $a['slotStart']),
            'appointment_time' => date('H:i', (int) $a['slotStart']),
            'end_time'         => date('H:i', (int) $a['slotEnd']),
            'status'           => $a['status'],
            'version'          => (int) $a['version'],
            'created_at'       => date('c', (int) $a['createdAt']),
            'is_reserve'       => (bool) $a['isReserve'],
            'patient_national_code' => $a['national_code'],
            'patient_gender'   => $a['gender'],
            'deposit_required'     => (bool) $a['depositRequired'],
            'deposit_amount_rials' => $a['depositAmountRials'] !== null ? (int) $a['depositAmountRials'] : null,
            'note'             => $a['note'],
            'service_section'  => $a['section_uuid'] ? ['uuid' => $a['section_uuid'], 'name' => $a['section_name']] : null,
            'service_item'     => $a['service_uuid'] ? ['uuid' => $a['service_uuid'], 'name' => $a['service_name']] : null,
            'staff'            => $a['staff_uuid'] ? ['uuid' => $a['staff_uuid'], 'full_name' => $a['staff_name']] : null,
        ], $rows);

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

    #[Route('/api/v1/my/appointments/today-stats', methods: ['GET'])]
    #[IsGranted('IS_AUTHENTICATED_FULLY')]
    public function todayStats(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $date = trim((string) $request->query->get('date', date('Y-m-d')));
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            $date = date('Y-m-d');
        }
        $dayStart = (int) strtotime($date . ' 00:00:00');
        $dayEnd   = (int) strtotime($date . ' 23:59:59');

        $qb = $this->em->createQueryBuilder()
            ->select('a.status, COUNT(a.id) AS cnt')
            ->from(Appointment::class, 'a')
            ->join('a.doctor', 'd')
            ->where('a.slotStart >= :dayStart AND a.slotStart <= :dayEnd')
            ->setParameter('dayStart', $dayStart)
            ->setParameter('dayEnd', $dayEnd)
            ->groupBy('a.status');

        $roles = $user->getRoles();
        if (in_array('ROLE_ADMIN', $roles, true)) {
            // Admin sees all
        } elseif (in_array('ROLE_CLINIC', $roles, true)) {
            $clinic = $this->clinicRepo->findByUser($user);
            if ($clinic === null) {
                return $this->emptyTodayStats();
            }
            $qb->join('App\Clinic\Entity\Clinic', 'c', 'WITH', 'd MEMBER OF c.doctors')
               ->andWhere('c = :clinic')
               ->setParameter('clinic', $clinic);
        } elseif (in_array('ROLE_DOCTOR', $roles, true)) {
            $doctor = $this->doctorRepo->findByUser($user);
            if ($doctor === null) {
                return $this->emptyTodayStats();
            }
            $qb->andWhere('a.doctor = :doctor')->setParameter('doctor', $doctor);
        } elseif (in_array('ROLE_SECRETARY', $roles, true)) {
            $filter = $this->resolveSecretaryFilter($user);
            if ($filter === null) {
                return $this->emptyTodayStats();
            }
            // در scope کلینیک، filterValue آرایه‌ی idهای پزشکانِ تخصیص‌یافته است —
            // نه خود کلینیک؛ هم‌شکل با myAppointments.
            [$filterType, $filterValue, $canView] = $filter;
            if (!$canView) {
                return $this->emptyTodayStats();
            }
            if ($filterType === 'clinic') {
                if (empty($filterValue)) {
                    return $this->emptyTodayStats();
                }
                $qb->andWhere('a.doctor IN (:doctorIds)')->setParameter('doctorIds', $filterValue);
            } else {
                $qb->andWhere('a.doctor = :doctor')->setParameter('doctor', $filterValue);
            }
        } else {
            // بیمار عادی: فقط نوبت‌های خودش — نه شمارشِ بی‌محدودهٔ کل سیستم.
            $qb->andWhere('a.user = :patient')->setParameter('patient', $user);
        }

        $rows = $qb->getQuery()->getArrayResult();
        $byStatus = [];
        foreach ($rows as $row) {
            $byStatus[$row['status']] = (int) $row['cnt'];
        }

        $total     = array_sum($byStatus);
        $completed = ($byStatus['completed'] ?? 0);
        $cancelled = ($byStatus['cancelled_by_doctor'] ?? 0)
                   + ($byStatus['cancelled_by_user'] ?? 0)
                   + ($byStatus['no_show'] ?? 0)
                   + ($byStatus['expired'] ?? 0);
        $waiting   = $total - $completed - $cancelled;

        return $this->success([
            'total'     => $total,
            'completed' => $completed,
            'waiting'   => max(0, $waiting),
            'cancelled' => $cancelled,
        ]);
    }

    private function emptyTodayStats(): JsonResponse
    {
        return $this->success(['total' => 0, 'completed' => 0, 'waiting' => 0, 'cancelled' => 0]);
    }

    /**
     * Whether the acting user is allowed to book onto this doctor's calendar.
     * The role gate alone is not enough: a doctor/clinic/secretary must be
     * scoped to the target doctor, otherwise any staff user could book onto
     * any doctor's calendar by passing an arbitrary doctor_uuid.
     */
    private function canBookForDoctor(User $user, Doctor $doctor): bool
    {
        $roles = $user->getRoles();

        if (in_array('ROLE_ADMIN', $roles, true)) {
            return true;
        }

        if (in_array('ROLE_CLINIC', $roles, true)) {
            $clinic = $this->clinicRepo->findByUser($user);
            if ($clinic !== null && $clinic->getDoctors()->contains($doctor)) {
                return true;
            }
        }

        if (in_array('ROLE_DOCTOR', $roles, true)) {
            $own = $this->doctorRepo->findByUser($user);
            if ($own !== null && $own->getId() === $doctor->getId()) {
                return true;
            }
        }

        if (in_array('ROLE_SECRETARY', $roles, true) && $this->secretaryCanBookForDoctor($user, $doctor)) {
            return true;
        }

        return false;
    }

    private function secretaryCanBookForDoctor(User $user, Doctor $doctor): bool
    {
        $context = $this->contextResolver->resolve($user);
        if (!$context->isResolved()) {
            return false;
        }

        if ($context->isClinic()) {
            // منشی فقط برای پزشکانِ تخصیص‌یافته‌ی خودش می‌تواند رزرو کند، نه کل کلینیک
            $rel = $this->secretaryRepo->findActiveClinicRow($user, $context->clinic, $doctor);
            return $rel !== null && (bool) ($rel->getPermissions()['resources']['appointments']['create'] ?? false);
        }

        if ($context->id === $doctor->getId()) {
            $rel = $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor);
            return $rel !== null && (bool) ($rel->getPermissions()['resources']['appointments']['create'] ?? false);
        }

        return false;
    }

    /**
     * تعیین فیلتر نوبت‌ها برای منشی بر اساس scope فعال:
     * Returns [type, entity, canView] یا null اگر رابطه‌ای پیدا نشد.
     * type: 'clinic' | 'doctor'
     * entity: Clinic | Doctor
     */
    private function resolveSecretaryFilter(User $user): ?array
    {
        $context = $this->contextResolver->resolve($user);
        if (!$context->isResolved()) {
            return null;
        }

        // بررسی scope کلینیک — فقط پزشکانِ تخصیص‌یافته به این منشی، نه کل کلینیک
        if ($context->isClinic()) {
            $rel = $this->secretaryRepo->findActiveBySecretaryForClinic($user, $context->clinic);
            if ($rel === null) return null;
            $canView   = (bool) ($rel->getPermissions()['resources']['appointments']['view'] ?? false);
            $doctorIds = array_map(
                fn(Doctor $d) => $d->getId(),
                $this->secretaryRepo->findDoctorsBySecretaryInClinic($user, $context->clinic)
            );
            return ['clinic', $doctorIds, $canView];
        }

        // بررسی scope مطب شخصی
        $rel = $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $context->doctor);
        if ($rel === null) return null;
        $canView = (bool) ($rel->getPermissions()['resources']['appointments']['view'] ?? false);

        return ['doctor', $context->doctor, $canView];
    }
}
