<?php

namespace App\Admin\Controller;

use App\Appointment\Entity\Appointment;
use App\Shared\Constant\ErrorCodes;
use App\Appointment\Repository\SlotTakenException;
use App\Auth\Entity\User;
use App\Shared\Service\InputValidator;
use App\Clinic\Entity\Clinic;
use App\Doctor\Entity\Doctor;
use App\Specialty\Entity\Specialty;
use App\Payment\Entity\Payment;
use App\Rating\Entity\Comment;
use App\Rating\Entity\Rate;
use App\Representation\Entity\Representation;
use App\Representation\Repository\RepresentationRepository;
use App\Secretary\Entity\DoctorSecretary;
use App\Settlement\Entity\FinancialBreakdown;
use App\Settlement\Entity\Settlement;
use App\Shared\Logging\AppLog;
use App\Sms\Entity\SmsLog;
use App\Sms\Entity\SmsTemplate;
use App\Shared\Controller\BaseController;
use Doctrine\ORM\EntityManagerInterface;
use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[OA\Tag(name: 'Admin')]
#[IsGranted('ROLE_ADMIN')]
class AdminApiController extends BaseController
{
    public function __construct(
        private readonly EntityManagerInterface $em,
        private readonly \App\Appointment\Service\SlotCalculatorService $slotCalculator,
        private readonly \App\Insurance\Service\TenantInsuranceCleanupService $insuranceCleanup,
        private readonly \App\Payment\Service\PaymentManager $paymentManager,
        private readonly \App\Payment\Repository\PaymentRepository $paymentRepo,
    ) {}

    // ── Users ─────────────────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/users',
        summary: 'List all users (paginated)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
            new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Paginated list of users',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'array', items: new OA\Items(
                            properties: [
                                new OA\Property(property: 'uuid', type: 'string'),
                                new OA\Property(property: 'id', type: 'integer'),
                                new OA\Property(property: 'mobile_number', type: 'string'),
                                new OA\Property(property: 'name', type: 'string'),
                                new OA\Property(property: 'email', type: 'string'),
                                new OA\Property(property: 'roles', type: 'array', items: new OA\Items(type: 'string')),
                                new OA\Property(property: 'is_active', type: 'boolean'),
                                new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
                            ]
                        )),
                        new OA\Property(property: 'meta', properties: [
                            new OA\Property(property: 'totalRecords', type: 'integer'),
                            new OA\Property(property: 'totalPages', type: 'integer'),
                            new OA\Property(property: 'currentPage', type: 'integer'),
                        ], type: 'object'),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
        ]
    )]
    #[Route('/api/v1/admin/users/stats', methods: ['GET'])]
    public function userStats(): JsonResponse
    {
        $conn = $this->em->getConnection();

        $total   = (int) $conn->fetchOne('SELECT COUNT(*) FROM users');
        $active  = (int) $conn->fetchOne('SELECT COUNT(*) FROM users WHERE status = 1');
        $admins  = (int) $conn->fetchOne("SELECT COUNT(*) FROM users WHERE roles LIKE '%ROLE_ADMIN%'");
        $doctors = (int) $conn->fetchOne("SELECT COUNT(*) FROM users WHERE roles LIKE '%ROLE_DOCTOR%' AND roles NOT LIKE '%ROLE_ADMIN%'");

        return $this->success([
            'total'    => $total,
            'active'   => $active,
            'inactive' => $total - $active,
            'admins'   => $admins,
            'doctors'  => $doctors,
            'patients' => max(0, $total - $admins - $doctors),
        ]);
    }

    #[Route('/api/v1/admin/users/{uuid}/status', methods: ['POST'])]
    public function toggleUserStatus(string $uuid): JsonResponse
    {
        $user = $this->em->getRepository(User::class)->findOneBy(['uuid' => $uuid]);
        if (!$user) return $this->error(ErrorCodes::USER_NOT_FOUND, 'کاربر یافت نشد', 404);

        $user->setStatus($user->getStatus() === 1 ? 0 : 1);
        $this->em->flush();

        return $this->success(['is_active' => $user->getStatus() === 1, 'status' => $user->getStatus()]);
    }

    #[Route('/api/v1/admin/users/{uuid}/role', methods: ['PUT'])]
    public function updateUserRole(string $uuid, Request $request): JsonResponse
    {
        $user = $this->em->getRepository(User::class)->findOneBy(['uuid' => $uuid]);
        if (!$user) return $this->error(ErrorCodes::USER_NOT_FOUND, 'کاربر یافت نشد', 404);

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

        $roleMap = [
            'admin'     => ['ROLE_USER', 'ROLE_ADMIN'],
            'doctor'    => ['ROLE_USER', 'ROLE_DOCTOR'],
            'secretary' => ['ROLE_USER', 'ROLE_SECRETARY'],
            'clinic'    => ['ROLE_USER', 'ROLE_CLINIC'],
            'patient'   => ['ROLE_USER'],
        ];

        if (!isset($roleMap[$role])) {
            return $this->error(ErrorCodes::INVALID_ROLE, 'نقش نامعتبر است', 422);
        }

        $user->setRoles($roleMap[$role]);
        $this->em->flush();

        return $this->success(['roles' => $user->getRoles()]);
    }

    #[Route('/api/v1/admin/users/{uuid}', methods: ['GET'])]
    public function userDetail(string $uuid): JsonResponse
    {
        $user = $this->em->getRepository(User::class)->findOneBy(['uuid' => $uuid]);
        if (!$user) return $this->error(ErrorCodes::USER_NOT_FOUND, 'کاربر یافت نشد', 404);

        return $this->success([
            'uuid'          => $user->getUuid(),
            'id'            => $user->getId(),
            'mobile_number' => $user->getMobileNumber(),
            'name'          => $user->getRealName(),
            'email'         => $user->getEmail(),
            'roles'         => $user->getRoles(),
            'is_active'     => $user->getStatus() === 1,
            'status'        => $user->getStatus(),
            'created_at'    => date('c', $user->getCreatedAt()),
            'updated_at'    => date('c', $user->getCreatedAt()),
        ]);
    }

    #[Route('/api/v1/admin/users/{uuid}', methods: ['PUT'])]
    public function updateUser(string $uuid, Request $request): JsonResponse
    {
        $user = $this->em->getRepository(User::class)->findOneBy(['uuid' => $uuid]);
        if (!$user) return $this->error(ErrorCodes::USER_NOT_FOUND, 'کاربر یافت نشد', 404);

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

        if (array_key_exists('name', $data)) {
            $user->setRealName($data['name'] !== '' ? (string) $data['name'] : null);
        }
        if (array_key_exists('email', $data)) {
            $user->setEmail($data['email'] !== '' ? (string) $data['email'] : null);
        }
        if (!empty($data['password'])) {
            $user->setPasswordHash(password_hash((string) $data['password'], PASSWORD_BCRYPT));
        }

        $this->em->flush();

        return $this->success([
            'uuid'       => $user->getUuid(),
            'name'       => $user->getRealName(),
            'email'      => $user->getEmail(),
            'is_active'  => $user->getStatus() === 1,
        ]);
    }

    #[Route('/api/v1/admin/users/{uuid}', methods: ['DELETE'])]
    public function deleteUser(string $uuid): JsonResponse
    {
        $user = $this->em->getRepository(User::class)->findOneBy(['uuid' => $uuid]);
        if (!$user) return $this->error(ErrorCodes::USER_NOT_FOUND, 'کاربر یافت نشد', 404);

        $this->em->remove($user);
        $this->em->flush();

        return $this->success(null);
    }

    #[Route('/api/v1/admin/users', methods: ['GET'])]
    public function users(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(100, max(5, (int) $request->query->get('limit', 25)));
        $search = trim((string) $request->query->get('search', ''));
        $role   = trim((string) $request->query->get('role', ''));
        $status = $request->query->get('status', '');
        $sort   = trim((string) $request->query->get('sort', 'newest'));

        $qb = $this->em->createQueryBuilder()
            ->select('u.uuid, u.id, u.mobileNumber as mobile, u.realName as name, u.email, u.roles, u.status, u.createdAt')
            ->from(User::class, 'u');

        if ($search !== '') {
            $qb->andWhere('u.mobileNumber LIKE :s OR u.realName LIKE :s OR u.email LIKE :s')
               ->setParameter('s', '%' . $search . '%');
        }

        if ($role !== '') {
            match ($role) {
                'admin'     => $qb->andWhere("u.roles LIKE :role")->setParameter('role', '%ROLE_ADMIN%'),
                'doctor'    => $qb->andWhere("u.roles LIKE :role AND u.roles NOT LIKE :notRole")
                                  ->setParameter('role', '%ROLE_DOCTOR%')->setParameter('notRole', '%ROLE_ADMIN%'),
                'secretary' => $qb->andWhere("u.roles LIKE :role")->setParameter('role', '%ROLE_SECRETARY%'),
                'representation' => $qb->andWhere("u.roles LIKE :role")->setParameter('role', '%ROLE_REPRESENTATION%'),
                'clinic'    => $qb->andWhere("u.roles LIKE :role AND u.roles NOT LIKE :notRole")
                                  ->setParameter('role', '%ROLE_CLINIC%')->setParameter('notRole', '%ROLE_ADMIN%'),
                'patient'   => $qb->andWhere("u.roles NOT LIKE :d AND u.roles NOT LIKE :a")
                                  ->setParameter('d', '%ROLE_DOCTOR%')->setParameter('a', '%ROLE_ADMIN%'),
                default     => null,
            };
        }

        if ($status !== '') {
            $qb->andWhere('u.status = :status')->setParameter('status', (int) $status);
        }

        $qb->orderBy('u.createdAt', $sort === 'oldest' ? 'ASC' : 'DESC');

        $total = (clone $qb)->select('COUNT(u.id)')->getQuery()->getSingleScalarResult();
        $rows  = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)->getQuery()->getArrayResult();

        return $this->paginated(array_map(fn(array $u) => [
            'uuid'          => $u['uuid'],
            'id'            => $u['id'],
            'mobile_number' => $u['mobile'],
            'name'          => $u['name'],
            'email'         => $u['email'],
            'roles'         => $u['roles'],
            'is_active'     => $u['status'] === 1,
            'status'        => $u['status'],
            'created_at'    => date('c', (int) $u['createdAt']),
        ], $rows), (int) $total, $page, $limit);
    }

    // ── Doctors ───────────────────────────────────────────────────────────────

    #[Route('/api/v1/admin/doctors/stats', methods: ['GET'])]
    public function doctorStats(): JsonResponse
    {
        $conn   = $this->em->getConnection();
        $total  = (int) $conn->fetchOne('SELECT COUNT(*) FROM doctors');
        $active = (int) $conn->fetchOne('SELECT COUNT(*) FROM doctors WHERE active_doctor_appointment = 1');
        $male   = (int) $conn->fetchOne("SELECT COUNT(*) FROM doctors WHERE gender IN ('man','male')");
        $female = (int) $conn->fetchOne("SELECT COUNT(*) FROM doctors WHERE gender IN ('woman','female')");

        $topSpec = $conn->fetchAllAssociative(
            'SELECT s.name, COUNT(*) as cnt FROM doctor_specialties ds
             JOIN specialties s ON s.id = ds.specialty_id
             GROUP BY s.id ORDER BY cnt DESC LIMIT 5'
        );

        return $this->success([
            'total'         => $total,
            'active'        => $active,
            'inactive'      => $total - $active,
            'male'          => $male,
            'female'        => $female,
            'top_specialty' => $topSpec,
        ]);
    }

    #[Route('/api/v1/admin/doctors/{uuid}/status', methods: ['POST'])]
    public function toggleDoctorStatus(string $uuid): JsonResponse
    {
        $doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $uuid]);
        if (!$doctor) return $this->error(ErrorCodes::DOCTOR_NOT_FOUND, 'پزشک یافت نشد', 404);

        $doctor->setActiveDoctorAppointment(!$doctor->isActiveDoctorAppointment());
        $this->em->flush();

        return $this->success(['is_active' => $doctor->isActiveDoctorAppointment()]);
    }

    #[Route('/api/v1/admin/doctors/{uuid}/representation', methods: ['PUT'])]
    public function setDoctorRepresentation(string $uuid, Request $request, RepresentationRepository $repRepo): JsonResponse
    {
        $doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $uuid]);
        if ($doctor === null) {
            return $this->error(ErrorCodes::DOCTOR_NOT_FOUND, 'پزشک یافت نشد', 404);
        }

        $body  = json_decode($request->getContent(), true) ?? [];
        $repId = $body['representation_id'] ?? null;

        if ($repId === null || $repId === '') {
            $doctor->setRepresentationId(null);
            $this->em->flush();
            return $this->success(['message' => 'نماینده حذف شد', 'representation' => null]);
        }

        $rep = $repRepo->find((int) $repId);
        if ($rep === null) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نماینده یافت نشد', 404);
        }

        $doctor->setRepresentationId($rep->getId());
        $this->em->flush();

        return $this->success([
            'message'        => 'نماینده ثبت شد',
            'representation' => ['id' => $rep->getId(), 'uuid' => $rep->getUuid(), 'full_name' => $rep->getFullName()],
        ]);
    }

    #[Route('/api/v1/admin/doctors', methods: ['GET'])]
    public function doctorsList(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(100, max(5, (int) $request->query->get('limit', 25)));
        $search = trim((string) $request->query->get('search', ''));
        $status = $request->query->get('status', '');
        $gender = trim((string) $request->query->get('gender', ''));
        $specId = (int) $request->query->get('specialty_id', 0);
        $cityId  = (int) $request->query->get('city_id', 0);
        $stateId = (int) $request->query->get('state_id', 0);
        $sort   = trim((string) $request->query->get('sort', 'newest'));

        $conn  = $this->em->getConnection();
        $where = ['1=1'];
        $params = [];

        if ($search !== '') {
            $where[]        = '(d.name LIKE :s OR d.mobile_number LIKE :s OR u.mobile_number LIKE :s OR u.email LIKE :s)';
            $params['s']    = '%' . $search . '%';
        }
        if ($status !== '') {
            $where[]           = 'd.active_doctor_appointment = :status';
            $params['status']  = $status === '1' ? 1 : 0;
        }
        if ($gender !== '') {
            $where[]           = 'd.gender = :gender';
            $params['gender']  = $gender;
        }
        $ownerStatus = trim((string) $request->query->get('owner_status', ''));
        if (in_array($ownerStatus, ['claimed', 'unclaimed', 'pending_transfer'], true)) {
            $where[]               = 'd.owner_status = :ownerStatus';
            $params['ownerStatus'] = $ownerStatus;
        }
        if ($specId > 0) {
            $where[]           = 'EXISTS (SELECT 1 FROM doctor_specialties ds2 WHERE ds2.doctor_id = d.id AND ds2.specialty_id = :specId)';
            $params['specId']  = $specId;
        }
        // پزشکانِ بدون نماینده — برای انتخاب و اتصال به یک نماینده.
        if ($request->query->get('unassigned') === '1') {
            $where[] = 'd.representation_id IS NULL';
        }
        // فیلتر شهر/استان بر اساس آدرسِ خودِ پزشک (doctor_addresses).
        if ($cityId > 0) {
            $where[]          = 'EXISTS (SELECT 1 FROM doctor_addresses da WHERE da.doctor_id = d.id AND da.city_id = :cityId)';
            $params['cityId'] = $cityId;
        }
        if ($stateId > 0) {
            $where[]           = 'EXISTS (SELECT 1 FROM doctor_addresses das WHERE das.doctor_id = d.id AND das.province_id = :stateId)';
            $params['stateId'] = $stateId;
        }

        $whereStr = implode(' AND ', $where);
        $orderBy  = match ($sort) {
            'oldest'  => 'd.created_at ASC',
            'rating'  => 'd.doctor_rate DESC',
            default   => 'd.created_at DESC',
        };

        $total = (int) $conn->fetchOne(
            "SELECT COUNT(*) FROM doctors d JOIN users u ON u.id = d.user_id WHERE $whereStr",
            $params
        );

        $offset = ($page - 1) * $limit;
        $rows = $conn->fetchAllAssociative(
            "SELECT d.id, d.uuid, d.name, d.gender, d.degree, d.medical_system_code,
                    d.mobile_number as doctor_mobile, d.active_doctor_appointment,
                    d.doctor_rate, d.doctor_rate_percentage, d.images, d.created_at,
                    d.owner_status, d.source,
                    d.representation_id, rep.uuid AS representation_uuid, rep.full_name AS representation_name,
                    u.mobile_number as user_mobile, u.email
             FROM doctors d JOIN users u ON u.id = d.user_id
             LEFT JOIN representations rep ON rep.id = d.representation_id
             WHERE $whereStr ORDER BY $orderBy LIMIT $limit OFFSET $offset",
            $params
        );

        $doctorIds = array_column($rows, 'id');
        $specMap   = [];
        if (!empty($doctorIds)) {
            $specRows = $conn->fetchAllAssociative(
                'SELECT ds.doctor_id, s.id, s.name FROM doctor_specialties ds
                 JOIN specialties s ON s.id = ds.specialty_id
                 WHERE ds.doctor_id IN (' . implode(',', array_map('intval', $doctorIds)) . ')'
            );
            foreach ($specRows as $sr) {
                $specMap[(int) $sr['doctor_id']][] = ['id' => (int) $sr['id'], 'name' => $sr['name']];
            }
        }

        $items = array_map(function (array $d) use ($specMap): array {
            $images = $d['images'] ? (json_decode($d['images'], true) ?? []) : [];
            return [
                'uuid'          => $d['uuid'],
                'id'            => (int) $d['id'],
                'name'          => $d['name'],
                'gender'        => $d['gender'],
                'degree'        => $d['degree'],
                'medical_code'  => $d['medical_system_code'],
                'mobile'        => $d['doctor_mobile'] ?: $d['user_mobile'],
                'email'         => $d['email'],
                'is_active'     => (bool) $d['active_doctor_appointment'],
                'owner_status'  => $d['owner_status'],
                'source'        => $d['source'],
                'rate'          => (float) $d['doctor_rate'],
                'representation_id'   => $d['representation_id'] !== null ? (int) $d['representation_id'] : null,
                'representation_uuid' => $d['representation_uuid'] ?? null,
                'representation_name' => $d['representation_name'] ?? null,
                'specialties'   => $specMap[(int) $d['id']] ?? [],
                'profile_image' => !empty($images) ? ($images[0]['url'] ?? null) : null,
                'created_at'    => date('c', (int) $d['created_at']),
            ];
        }, $rows);

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

    #[Route('/api/v1/admin/doctors', methods: ['POST'])]
    public function createDoctor(Request $request): JsonResponse
    {
        $data   = json_decode($request->getContent(), true) ?? [];
        $mobile = InputValidator::toEnglishDigits(trim((string) ($data['mobile'] ?? '')));
        $name   = trim((string) ($data['name'] ?? ''));

        if ($mobile === '' || $name === '') {
            return $this->error(ErrorCodes::VALIDATION, 'موبایل و نام الزامی هستند', 422);
        }
        if (!InputValidator::isValidIranMobile($mobile)) {
            return $this->error(ErrorCodes::VALIDATION, 'شماره موبایل نامعتبر است', 422, 'mobile');
        }

        $user = $this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $mobile]);
        if (!$user) {
            $user = new User($mobile);
            $user->setRealName($name);
            $user->setPasswordHash(password_hash(bin2hex(random_bytes(8)), PASSWORD_BCRYPT));
            $this->em->persist($user);
        }

        $existing = $this->em->getRepository(Doctor::class)->findOneBy(['user' => $user]);
        if ($existing) {
            return $this->error(ErrorCodes::DOCTOR_EXISTS, 'این کاربر قبلاً پروفایل پزشک دارد', 409);
        }

        $doctor = new Doctor($user, $name);

        if (!empty($data['gender']))               $doctor->setGender($data['gender']);
        if (!empty($data['degree']))               $doctor->setDegree($data['degree']);
        if (!empty($data['medical_system_code']))  $doctor->setMedicalSystemCode($data['medical_system_code']);
        if (!empty($data['info']))                 $doctor->setInfo($data['info']);
        if (!empty($data['mobile_number']))        $doctor->setMobileNumber($data['mobile_number']);
        if (!empty($data['activity_time']))        $doctor->setActivityTime((int) $data['activity_time']);

        if (!empty($data['specialties']) && is_array($data['specialties'])) {
            foreach ($data['specialties'] as $id) {
                $s = $this->em->getRepository(Specialty::class)->find((int) $id);
                if ($s !== null) $doctor->getSpecialties()->add($s);
            }
        }

        $roles = $user->getRoles();
        if (!in_array('ROLE_DOCTOR', $roles, true)) {
            $roles[] = 'ROLE_DOCTOR';
            $user->setRoles(array_values(array_unique($roles)));
        }

        $this->em->persist($doctor);
        $this->em->flush();

        return $this->success(['uuid' => $doctor->getUuid()], 201);
    }

    // ── Clinics ───────────────────────────────────────────────────────────────

    #[Route('/api/v1/admin/clinics', methods: ['GET'])]
    public function clinicsList(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(100, max(5, (int) $request->query->get('limit', 15)));
        $search = trim((string) $request->query->get('search', ''));
        $status = $request->query->get('status', '');

        $conn   = $this->em->getConnection();
        $where  = ['1=1'];
        $params = [];

        if ($search !== '') {
            $where[]     = '(c.name LIKE :s OR c.telephone LIKE :s)';
            $params['s'] = '%' . $search . '%';
        }
        if ($status !== '') {
            $where[]            = 'c.is_active = :active';
            $params['active']   = $status === '1' ? 1 : 0;
        }

        $whereStr = implode(' AND ', $where);

        $total = (int) $conn->fetchOne(
            "SELECT COUNT(*) FROM clinics c WHERE $whereStr",
            $params
        );

        $offset = ($page - 1) * $limit;
        $rows   = $conn->fetchAllAssociative(
            "SELECT c.uuid, c.name, c.telephone, c.clinic_logo, c.is_active, c.created_at,
                    COUNT(DISTINCT cd.doctor_id) as doctors_count,
                    u.mobile_number as owner_mobile
             FROM clinics c
             LEFT JOIN clinic_doctors cd ON cd.clinic_id = c.id
             LEFT JOIN users u ON u.id = c.user_id
             WHERE $whereStr
             GROUP BY c.id, u.mobile_number
             ORDER BY c.created_at DESC
             LIMIT $limit OFFSET $offset",
            $params
        );

        $items = array_map(fn(array $c) => [
            'uuid'          => $c['uuid'],
            'name'          => $c['name'],
            'phone'         => $c['telephone'],
            'logo'          => $c['clinic_logo'],
            'is_active'     => (bool) $c['is_active'],
            'doctors_count' => (int) $c['doctors_count'],
            'created_at'    => (int) $c['created_at'],
            'owner_mobile'  => $c['owner_mobile'],
        ], $rows);

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

    #[Route('/api/v1/admin/clinic', methods: ['POST'])]
    public function createClinic(Request $request): JsonResponse
    {
        $data   = json_decode($request->getContent(), true) ?? [];
        $mobile = InputValidator::toEnglishDigits(trim((string) ($data['owner_mobile'] ?? '')));
        $name   = trim((string) ($data['name'] ?? ''));

        if ($mobile === '') {
            return $this->error(ErrorCodes::VALIDATION, 'شماره موبایل الزامی است', 422);
        }
        if (!InputValidator::isValidIranMobile($mobile)) {
            return $this->error(ErrorCodes::VALIDATION, 'شماره موبایل نامعتبر است', 422, 'owner_mobile');
        }
        if ($name === '') {
            return $this->error(ErrorCodes::VALIDATION, 'نام کلینیک الزامی است', 422);
        }

        $user = $this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $mobile]);
        if (!$user) {
            $user = new User($mobile);
            $this->em->persist($user);
        }

        $roles = $user->getRoles();
        if (!in_array('ROLE_CLINIC', $roles, true)) {
            $roles[] = 'ROLE_CLINIC';
            $user->setRoles(array_values(array_unique($roles)));
        }

        $clinic = new Clinic($user);
        $clinic->setName($name);
        if (!empty($data['telephone'])) $clinic->setTelephone($data['telephone']);
        if (!empty($data['address']))   $clinic->setAddress($data['address']);
        if (!empty($data['info']))      $clinic->setInfo($data['info']);

        $this->em->persist($clinic);
        $this->em->flush();

        return $this->success(['uuid' => $clinic->getUuid(), 'name' => $clinic->getName(), 'is_active' => $clinic->isActive()]);
    }

    #[Route('/api/v1/admin/clinic/{uuid}/status', methods: ['PATCH'])]
    public function toggleClinicStatus(string $uuid): JsonResponse
    {
        $clinic = $this->em->getRepository(Clinic::class)->findOneBy(['uuid' => $uuid]);
        if (!$clinic) return $this->error(ErrorCodes::CLINIC_NOT_FOUND, 'کلینیک یافت نشد', 404);

        $clinic->setIsActive(!$clinic->isActive());
        $this->em->flush();

        return $this->success(['is_active' => $clinic->isActive()]);
    }

    #[Route('/api/v1/admin/clinic/{uuid}', methods: ['DELETE'])]
    public function deleteClinic(string $uuid): JsonResponse
    {
        $clinic = $this->em->getRepository(Clinic::class)->findOneBy(['uuid' => $uuid]);
        if (!$clinic) return $this->error(ErrorCodes::CLINIC_NOT_FOUND, 'کلینیک یافت نشد', 404);

        $this->insuranceCleanup->purgeForEntity(\App\Insurance\Entity\TenantInsurance::TYPE_CLINIC, $clinic->getId());
        $this->em->remove($clinic);
        $this->em->flush();

        return $this->success(null);
    }

    // ── Appointments ──────────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/appointments/today-stats',
        summary: 'Get today appointment stats',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'date', in: 'query', required: false, schema: new OA\Schema(type: 'string', format: 'date', example: '2025-06-15'), description: 'Date (YYYY-MM-DD), defaults to today'),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Appointment stats for the given date',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', properties: [
                            new OA\Property(property: 'total', type: 'integer'),
                            new OA\Property(property: 'completed', type: 'integer'),
                            new OA\Property(property: 'waiting', type: 'integer'),
                            new OA\Property(property: 'cancelled', type: 'integer'),
                        ], type: 'object'),
                    ]
                )
            ),
        ]
    )]
    #[Route('/api/v1/admin/appointments/today-stats', methods: ['GET'])]
    public function appointmentsTodayStats(Request $request): 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');

        $rows = $this->em->createQueryBuilder()
            ->select('a.status, COUNT(a.id) AS cnt')
            ->from(Appointment::class, 'a')
            ->where('a.slotStart >= :dayStart AND a.slotStart <= :dayEnd')
            ->setParameter('dayStart', $dayStart)
            ->setParameter('dayEnd', $dayEnd)
            ->groupBy('a.status')
            ->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['cancelled_by_admin'] ?? 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,
        ]);
    }

    #[OA\Get(
        path: '/api/v1/admin/appointments',
        summary: 'List appointments (paginated, filterable by date and doctor)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
            new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
            new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
            new OA\Parameter(name: 'date', in: 'query', required: false, schema: new OA\Schema(type: 'string', format: 'date', example: '2025-06-15'), description: 'Filter by slot date (YYYY-MM-DD)'),
            new OA\Parameter(name: 'doctor_uuid', in: 'query', required: false, schema: new OA\Schema(type: 'string'), description: 'Filter by doctor UUID'),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Paginated list of appointments',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'array', items: new OA\Items(
                            properties: [
                                new OA\Property(property: 'uuid', type: 'string'),
                                new OA\Property(property: 'patient_name', type: 'string'),
                                new OA\Property(property: 'patient_mobile', type: 'string'),
                                new OA\Property(property: 'doctor_uuid', type: 'string'),
                                new OA\Property(property: 'doctor_name', type: 'string'),
                                new OA\Property(property: 'slot_start', type: 'integer', description: 'Unix timestamp'),
                                new OA\Property(property: 'slot_end', type: 'integer', description: 'Unix timestamp'),
                                new OA\Property(property: 'appointment_date', type: 'string', format: 'date'),
                                new OA\Property(property: 'appointment_time', type: 'string', example: '14:30'),
                                new OA\Property(property: 'end_time', type: 'string', example: '14:50'),
                                new OA\Property(property: 'status', type: 'string'),
                                new OA\Property(property: 'version', type: 'integer'),
                                new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
                            ]
                        )),
                        new OA\Property(property: 'meta', properties: [
                            new OA\Property(property: 'totalRecords', type: 'integer'),
                            new OA\Property(property: 'totalPages', type: 'integer'),
                            new OA\Property(property: 'currentPage', type: 'integer'),
                        ], type: 'object'),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
        ]
    )]
    #[Route('/api/v1/admin/appointments', methods: ['GET'])]
    public function appointments(Request $request): 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', ''));

        $qb = $this->em->createQueryBuilder()
            ->select(
                'a.uuid, a.slotStart, a.slotEnd, a.status, a.createdAt, a.version',
                'd.uuid as doctor_uuid, d.name as doctor_name',
                'u.mobileNumber as patient_mobile, u.realName as patient_name',
            )
            ->from(Appointment::class, 'a')
            ->join('a.doctor', 'd')
            ->join('a.user', 'u')
            ->orderBy('a.slotStart', 'ASC');

        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['patient_name'] ?? '',
            'patient_mobile'   => $a['patient_mobile'],
            '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']),
        ], $rows);

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

    // ── Payments ──────────────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/payments',
        summary: 'List all payments (paginated)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
            new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string', enum: ['pending', 'success', 'failed'])),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Paginated list of payments',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'array', items: new OA\Items(
                            properties: [
                                new OA\Property(property: 'uuid', type: 'string'),
                                new OA\Property(property: 'amount', type: 'integer'),
                                new OA\Property(property: 'status', type: 'string'),
                                new OA\Property(property: 'gateway', type: 'string'),
                                new OA\Property(property: 'ref_id', type: 'string', nullable: true),
                                new OA\Property(property: 'patient_mobile', type: 'string'),
                                new OA\Property(property: 'paid_at', type: 'string', format: 'date-time', nullable: true),
                                new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
                            ]
                        )),
                        new OA\Property(property: 'meta', properties: [
                            new OA\Property(property: 'totalRecords', type: 'integer'),
                            new OA\Property(property: 'totalPages', type: 'integer'),
                            new OA\Property(property: 'currentPage', type: 'integer'),
                        ], type: 'object'),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
        ]
    )]
    #[Route('/api/v1/admin/appointment', methods: ['POST'])]
    public function createAppointment(Request $request): JsonResponse
    {
        $data        = json_decode($request->getContent(), true) ?? [];
        $doctorUuid  = trim($data['doctor_uuid']    ?? '');
        $slotStart   = (int) ($data['slot_start']   ?? 0);
        $slotEnd     = (int) ($data['slot_end']      ?? 0);
        $mobile      = trim($data['patient_mobile'] ?? '');
        $patientName = trim($data['patient_name']   ?? '');

        if (empty($doctorUuid) || $slotStart <= 0 || $slotEnd <= $slotStart || empty($mobile) || empty($patientName)) {
            return $this->error(ErrorCodes::VALIDATION, 'doctor_uuid، slot_start، slot_end، patient_mobile و patient_name الزامی است', 422);
        }

        $doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $doctorUuid]);
        if (!$doctor) return $this->error(ErrorCodes::DOCTOR_NOT_FOUND, 'پزشک یافت نشد', 404);

        $patient = $this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $mobile]);
        if (!$patient) {
            $patient = new User($mobile);
            $patient->setRealName($patientName);
            $patient->setRoles(['ROLE_USER']);
            $this->em->persist($patient);
        }

        $appointment = new Appointment($doctor, $patient, $slotStart, $slotEnd);
        if (!empty($data['note'])) $appointment->setNote($data['note']);
        $locationId = $this->slotCalculator->resolveSlotLocationId($doctor, $slotStart);
        if ($locationId !== null) $appointment->setAddressId($locationId);

        try {
            $this->em->getRepository(Appointment::class)->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(),
        ], 201);
    }

    #[Route('/api/v1/admin/payments', methods: ['GET'])]
    public function payments(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(100, max(1, (int) $request->query->get('limit', 15)));
        $search = trim((string) $request->query->get('search', ''));
        $status = trim((string) $request->query->get('status', ''));

        $qb = $this->em->createQueryBuilder()
            ->select(
                'p.uuid, p.amountRials, p.status, p.gateway, p.referenceId, p.createdAt, p.updatedAt',
                'u.mobileNumber as patient_mobile',
            )
            ->from(Payment::class, 'p')
            ->join('p.user', 'u')
            ->orderBy('p.createdAt', 'DESC');

        if ($search !== '') {
            $qb->andWhere('u.mobileNumber LIKE :s OR p.referenceId LIKE :s OR p.orderId LIKE :s')
               ->setParameter('s', '%' . $search . '%');
        }
        if ($status !== '') {
            $qb->andWhere('p.status = :status')->setParameter('status', $status);
        }

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

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

        $items = array_map(fn(array $p) => [
            'uuid'           => $p['uuid'],
            'amount'         => (int) $p['amountRials'],
            'status'         => $p['status'],
            'gateway'        => $p['gateway'],
            'ref_id'         => $p['referenceId'],
            'patient_mobile' => $p['patient_mobile'],
            'paid_at'        => $p['status'] === 'success' ? date('c', (int) $p['updatedAt']) : null,
            'created_at'     => date('c', (int) $p['createdAt']),
        ], $rows);

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

    #[OA\Get(
        path: '/api/v1/admin/payments/{uuid}',
        summary: 'Payment detail by UUID',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
        ],
        responses: [
            new OA\Response(response: 200, description: 'Payment detail'),
            new OA\Response(response: 404, description: 'Payment not found'),
        ]
    )]
    #[Route('/api/v1/admin/payments/{uuid}', methods: ['GET'])]
    public function paymentDetail(string $uuid): JsonResponse
    {
        $rows = $this->em->createQueryBuilder()
            ->select(
                'p.uuid, p.orderId, p.amountRials, p.status, p.gateway, p.type, p.referenceId, p.metadata, p.createdAt, p.updatedAt',
                'u.mobileNumber as patient_mobile, u.realName as patient_name',
                'a.uuid as appointment_uuid',
            )
            ->from(Payment::class, 'p')
            ->join('p.user', 'u')
            ->leftJoin('p.appointment', 'a')
            ->where('p.uuid = :uuid')->setParameter('uuid', $uuid)
            ->getQuery()->getArrayResult();

        if (empty($rows)) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پرداخت یافت نشد', 404);
        }

        $p = $rows[0];
        return $this->success([
            'uuid'             => $p['uuid'],
            'order_id'         => $p['orderId'],
            'amount'           => (int) $p['amountRials'],
            'status'           => $p['status'],
            'gateway'          => $p['gateway'],
            'type'             => $p['type'],
            'ref_id'           => $p['referenceId'],
            'card_pan'         => $p['metadata']['card_pan'] ?? null,
            'refunds'          => $p['metadata']['refunds'] ?? [],
            'patient_mobile'   => $p['patient_mobile'],
            'patient_name'     => $p['patient_name'],
            'appointment_uuid' => $p['appointment_uuid'],
            'paid_at'          => $p['status'] === 'success' ? date('c', (int) $p['updatedAt']) : null,
            'created_at'       => date('c', (int) $p['createdAt']),
        ]);
    }

    #[Route('/api/v1/admin/payments/{uuid}/refund', methods: ['POST'])]
    public function refundPayment(string $uuid, Request $request): JsonResponse
    {
        $payment = $this->paymentRepo->findOneBy(['uuid' => $uuid]);
        if ($payment === null) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پرداخت یافت نشد', 404);
        }
        $amount = $request->toArray()['amount'] ?? null; // ریال؛ null = کل مبلغ
        $result = $this->paymentManager->refundPayment(
            $payment,
            $amount !== null ? (int) $amount : null,
            $request->getClientIp() ?? '',
        );

        return $result->success
            ? $this->success(['status' => $payment->getStatus(), 'refund_ref' => $result->refundRefId])
            : $this->error(ErrorCodes::ERR_PAYMENT_002, $result->errorMessage, 422);
    }

    #[Route('/api/v1/admin/payments/{uuid}/reverse', methods: ['POST'])]
    public function reversePayment(string $uuid, Request $request): JsonResponse
    {
        $payment = $this->paymentRepo->findOneBy(['uuid' => $uuid]);
        if ($payment === null) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پرداخت یافت نشد', 404);
        }
        $result = $this->paymentManager->reversePayment($payment, $request->getClientIp() ?? '');

        return $result->success
            ? $this->success(['status' => $payment->getStatus()])
            : $this->error(ErrorCodes::ERR_PAYMENT_002, $result->errorMessage, 422);
    }

    // ── Representations ───────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/representations',
        summary: 'List all representations (paginated)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
            new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
            new OA\Parameter(name: 'city_id', in: 'query', required: false, schema: new OA\Schema(type: 'integer')),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Paginated list of representations',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'array', items: new OA\Items(
                            properties: [
                                new OA\Property(property: 'uuid', type: 'string'),
                                new OA\Property(property: 'domain', type: 'string'),
                                new OA\Property(property: 'full_name', type: 'string'),
                                new OA\Property(property: 'mobile_number', type: 'string'),
                                new OA\Property(property: 'city_id', type: 'integer', nullable: true),
                                new OA\Property(property: 'city', type: 'string', nullable: true),
                                new OA\Property(property: 'commission_percent', type: 'number', format: 'float'),
                                new OA\Property(property: 'wallet_balance', type: 'integer'),
                                new OA\Property(property: 'is_active', type: 'boolean'),
                                new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
                            ]
                        )),
                        new OA\Property(property: 'meta', properties: [
                            new OA\Property(property: 'totalRecords', type: 'integer'),
                            new OA\Property(property: 'totalPages', type: 'integer'),
                            new OA\Property(property: 'currentPage', type: 'integer'),
                        ], type: 'object'),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
        ]
    )]
    #[Route('/api/v1/admin/representations', methods: ['GET'])]
    public function representations(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(100, max(1, (int) $request->query->get('limit', 15)));
        $search = trim((string) $request->query->get('search', ''));
        $cityId = $request->query->get('city_id');

        $qb = $this->em->createQueryBuilder()
            ->select('r.id, r.uuid, r.fullName, r.mobileNumber, u.mobileNumber as user_mobile, r.cityId, r.domain, r.isGlobal, r.commissionPercent, r.active, r.createdAt')
            ->from(Representation::class, 'r')
            ->join('r.user', 'u')
            ->orderBy('r.createdAt', 'DESC');

        if ($search !== '') {
            $qb->andWhere('r.fullName LIKE :s OR r.mobileNumber LIKE :s OR u.mobileNumber LIKE :s')
               ->setParameter('s', '%' . $search . '%');
        }

        if ($cityId !== null && $cityId !== '') {
            $qb->andWhere(':cityId MEMBER OF r.cities')
               ->setParameter('cityId', (int) $cityId);
        }

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

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

        // نام شهرهای هر نماینده (چند-شهری) در یک کوئری برای صفحه‌ی جاری.
        $repIds = array_map(fn(array $r) => (int) $r['id'], $rows);
        $cityNames = [];
        if ($repIds !== []) {
            $cityRows = $this->em->getConnection()->fetchAllAssociative(
                'SELECT rc.representation_id, c.id AS city_id, c.name
                 FROM representation_cities rc JOIN cities c ON c.id = rc.city_id
                 WHERE rc.representation_id IN (?) ORDER BY c.name',
                [$repIds],
                [\Doctrine\DBAL\ArrayParameterType::INTEGER],
            );
            foreach ($cityRows as $cr) {
                $cityNames[(int) $cr['representation_id']][] = ['id' => (int) $cr['city_id'], 'name' => $cr['name']];
            }
        }

        // تعداد پزشکان و نوبت‌های هر نماینده در دو کوئری گروهی (بدون N+1).
        $docCounts  = [];
        $apptCounts = [];
        if ($repIds !== []) {
            foreach ($this->em->getConnection()->fetchAllAssociative(
                'SELECT representation_id, COUNT(*) c FROM doctors
                 WHERE representation_id IN (?) GROUP BY representation_id',
                [$repIds],
                [\Doctrine\DBAL\ArrayParameterType::INTEGER],
            ) as $row) {
                $docCounts[(int) $row['representation_id']] = (int) $row['c'];
            }
            foreach ($this->em->getConnection()->fetchAllAssociative(
                'SELECT d.representation_id, COUNT(*) c FROM appointments a
                 JOIN doctors d ON d.id = a.doctor_id
                 WHERE d.representation_id IN (?) GROUP BY d.representation_id',
                [$repIds],
                [\Doctrine\DBAL\ArrayParameterType::INTEGER],
            ) as $row) {
                $apptCounts[(int) $row['representation_id']] = (int) $row['c'];
            }
        }

        $items = array_map(function (array $r) use ($cityNames, $docCounts, $apptCounts) {
            $cities = $cityNames[(int) $r['id']] ?? [];
            return [
                'id'                 => (int) $r['id'],
                'uuid'               => $r['uuid'],
                'domain'             => $r['domain'],
                'is_global'          => (bool) $r['isGlobal'],
                'full_name'          => $r['fullName'],
                'mobile_number'      => $r['mobileNumber'] ?: ($r['user_mobile'] ?? null),
                'city_id'            => $cities[0]['id'] ?? $r['cityId'],
                'city_ids'           => array_column($cities, 'id'),
                'cities'             => $cities,
                'city'               => $cities !== [] ? implode('، ', array_column($cities, 'name')) : null,
                'commission_percent' => (float) $r['commissionPercent'],
                'wallet_balance'     => 0,
                'doctor_count'       => $docCounts[(int) $r['id']] ?? 0,
                'appointment_count'  => $apptCounts[(int) $r['id']] ?? 0,
                'is_active'          => (bool) $r['active'],
                'created_at'         => date('c', (int) $r['createdAt']),
            ];
        }, $rows);

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

    #[Route('/api/v1/admin/representations/{uuid}/doctors', methods: ['GET'])]
    public function representationDoctors(string $uuid, Request $request, RepresentationRepository $repRepo): JsonResponse
    {
        $rep = $repRepo->findByUuid($uuid);
        if ($rep === null) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نماینده یافت نشد', 404);
        }

        $page  = max(1, (int) $request->query->get('page', 1));
        $limit = min(100, max(1, (int) $request->query->get('limit', 15)));
        $repId = $rep->getId();

        $conn  = $this->em->getConnection();
        $total = (int) $conn->fetchOne('SELECT COUNT(*) FROM doctors WHERE representation_id = ?', [$repId]);

        $now    = time();
        $offset = ($page - 1) * $limit;
        $rows = $conn->fetchAllAssociative(
            "SELECT d.id, d.uuid, d.name, d.gender, d.medical_system_code,
                    d.active_doctor_appointment, d.owner_status, d.created_at,
                    (SELECT COUNT(*) FROM appointments a WHERE a.doctor_id = d.id AND a.slot_start <  $now) AS past_count,
                    (SELECT COUNT(*) FROM appointments a WHERE a.doctor_id = d.id AND a.slot_start >= $now) AS upcoming_count
             FROM doctors d
             WHERE d.representation_id = ? ORDER BY d.created_at DESC LIMIT $limit OFFSET $offset",
            [$repId]
        );

        $items = array_map(fn(array $d): array => [
            'uuid'           => $d['uuid'],
            'id'             => (int) $d['id'],
            'name'           => $d['name'],
            'gender'         => $d['gender'],
            'medical_code'   => $d['medical_system_code'],
            'is_active'      => (bool) $d['active_doctor_appointment'],
            'owner_status'   => $d['owner_status'],
            'past_count'     => (int) $d['past_count'],
            'upcoming_count' => (int) $d['upcoming_count'],
            'created_at'     => date('c', (int) $d['created_at']),
        ], $rows);

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

    #[Route('/api/v1/admin/representations/{uuid}/doctors', methods: ['POST'])]
    public function attachDoctorToRepresentation(string $uuid, Request $request, RepresentationRepository $repRepo): JsonResponse
    {
        $rep = $repRepo->findByUuid($uuid);
        if ($rep === null) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نماینده یافت نشد', 404);
        }

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

        $doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $doctorUuid]);
        if ($doctor === null) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پزشک یافت نشد', 404);
        }
        if ($doctor->getRepresentationId() !== null) {
            return $this->error(ErrorCodes::ERR_CONFLICT_001, 'این پزشک از قبل به یک نماینده متصل است', 409);
        }

        $doctor->setRepresentationId($rep->getId());
        $this->em->flush();

        return $this->success(['message' => 'پزشک به نماینده متصل شد', 'doctor_uuid' => $doctorUuid, 'representation_id' => $rep->getId()]);
    }

    #[Route('/api/v1/admin/representations/{uuid}/appointments', methods: ['GET'])]
    public function representationAppointments(string $uuid, Request $request, RepresentationRepository $repRepo): JsonResponse
    {
        $rep = $repRepo->findByUuid($uuid);
        if ($rep === null) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نماینده یافت نشد', 404);
        }

        $page  = max(1, (int) $request->query->get('page', 1));
        $limit = min(100, max(1, (int) $request->query->get('limit', 15)));
        $scope = $request->query->get('scope') === 'past' ? 'past' : 'upcoming';
        $repId = $rep->getId();
        $now   = time();

        $conn = $this->em->getConnection();
        $cmp  = $scope === 'past' ? '<' : '>=';
        $order = $scope === 'past' ? 'DESC' : 'ASC';

        $total = (int) $conn->fetchOne(
            "SELECT COUNT(*) FROM appointments a JOIN doctors d ON d.id = a.doctor_id
             WHERE d.representation_id = ? AND a.slot_start $cmp ?",
            [$repId, $now]
        );

        $offset = ($page - 1) * $limit;
        $rows = $conn->fetchAllAssociative(
            "SELECT a.uuid, a.slot_start, a.slot_end, a.status, a.patient_name,
                    d.uuid AS doctor_uuid, d.name AS doctor_name
             FROM appointments a JOIN doctors d ON d.id = a.doctor_id
             WHERE d.representation_id = ? AND a.slot_start $cmp ?
             ORDER BY a.slot_start $order LIMIT $limit OFFSET $offset",
            [$repId, $now]
        );

        $items = array_map(fn(array $a): array => [
            'uuid'         => $a['uuid'],
            'slot_start'   => (int) $a['slot_start'],
            'slot_end'     => (int) $a['slot_end'],
            'status'       => $a['status'],
            'patient_name' => $a['patient_name'],
            'doctor_uuid'  => $a['doctor_uuid'],
            'doctor_name'  => $a['doctor_name'],
        ], $rows);

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

    // ── Financial Breakdowns ──────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/financial-breakdowns',
        summary: 'لیست تفکیک مالی تراکنش‌ها (paginated)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
            new OA\Parameter(name: 'representation_id', in: 'query', required: false, schema: new OA\Schema(type: 'integer')),
            new OA\Parameter(name: 'source', in: 'query', required: false, schema: new OA\Schema(type: 'string', enum: ['appointment', 'subscription'])),
            new OA\Parameter(name: 'from', in: 'query', required: false, schema: new OA\Schema(type: 'integer', description: 'Unix timestamp')),
            new OA\Parameter(name: 'to', in: 'query', required: false, schema: new OA\Schema(type: 'integer', description: 'Unix timestamp')),
        ],
        responses: [new OA\Response(response: 200, description: 'لیست تفکیک مالی')]
    )]
    #[Route('/api/v1/admin/financial-breakdowns', methods: ['GET'])]
    public function financialBreakdowns(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(100, max(1, (int) $request->query->get('limit', 15)));
        $repId  = $request->query->get('representation_id');
        $source = trim((string) $request->query->get('source', ''));
        $from   = $request->query->get('from');
        $to     = $request->query->get('to');

        $qb = $this->em->createQueryBuilder()
            ->select(
                'b.uuid, b.source, b.grossRials, b.smsFeeRials, b.taxPercent, b.taxRials,
                 b.netAfterTaxRials, b.commissionPercent, b.representationShareRials, b.systemShareRials,
                 b.representationId, b.doctorId, b.clinicId, b.createdAt,
                 p.orderId as order_id, r.fullName as representation_name'
            )
            ->from(FinancialBreakdown::class, 'b')
            ->join('b.payment', 'p')
            ->leftJoin(Representation::class, 'r', 'WITH', 'r.id = b.representationId')
            ->orderBy('b.createdAt', 'DESC');

        if ($repId !== null && $repId !== '') {
            $qb->andWhere('b.representationId = :repId')->setParameter('repId', (int) $repId);
        }
        if ($source !== '') {
            $qb->andWhere('b.source = :source')->setParameter('source', $source);
        }
        if ($from !== null && $from !== '') {
            $qb->andWhere('b.createdAt >= :from')->setParameter('from', (int) $from);
        }
        if ($to !== null && $to !== '') {
            $qb->andWhere('b.createdAt <= :to')->setParameter('to', (int) $to);
        }

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

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

        $items = array_map(fn(array $b) => [
            'uuid'                       => $b['uuid'],
            'order_id'                   => $b['order_id'],
            'source'                     => $b['source'],
            'gross_rials'                => (int) $b['grossRials'],
            'sms_fee_rials'              => (int) $b['smsFeeRials'],
            'tax_percent'                => (float) $b['taxPercent'],
            'tax_rials'                  => (int) $b['taxRials'],
            'net_after_tax_rials'        => (int) $b['netAfterTaxRials'],
            'commission_percent'         => (float) $b['commissionPercent'],
            'representation_share_rials' => (int) $b['representationShareRials'],
            'system_share_rials'         => (int) $b['systemShareRials'],
            'representation_id'          => $b['representationId'],
            'representation_name'        => $b['representation_name'] ?? null,
            'doctor_id'                  => $b['doctorId'],
            'clinic_id'                  => $b['clinicId'],
            'created_at'                 => date('c', (int) $b['createdAt']),
        ], $rows);

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

    #[OA\Get(
        path: '/api/v1/admin/financial-summary',
        summary: 'جمعِ کلِ سهم نماینده، مالیات، هزینه پیامک و سهم سیستم',
        security: [['bearerAuth' => []]],
        responses: [new OA\Response(response: 200, description: 'جمع‌های مالی')]
    )]
    #[Route('/api/v1/admin/financial-summary', methods: ['GET'])]
    public function financialSummary(): JsonResponse
    {
        $row = $this->em->createQueryBuilder()
            ->select(
                'COALESCE(SUM(b.grossRials), 0) as gross,
                 COALESCE(SUM(b.representationShareRials), 0) as rep_income,
                 COALESCE(SUM(b.taxRials), 0) as tax,
                 COALESCE(SUM(b.smsFeeRials), 0) as sms_fee,
                 COALESCE(SUM(b.systemShareRials), 0) as system_share'
            )
            ->from(FinancialBreakdown::class, 'b')
            ->getQuery()->getSingleResult();

        return $this->success([
            'total_gross'                 => (int) $row['gross'],
            'total_representation_income' => (int) $row['rep_income'],
            'total_tax_collected'         => (int) $row['tax'],
            'total_sms_fee'               => (int) $row['sms_fee'],
            'total_system_share'          => (int) $row['system_share'],
        ]);
    }

    // ── Secretaries ───────────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/secretaries',
        summary: 'List all secretaries (paginated)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
            new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Paginated list of secretaries',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'array', items: new OA\Items(
                            properties: [
                                new OA\Property(property: 'uuid', type: 'string'),
                                new OA\Property(property: 'user_name', type: 'string'),
                                new OA\Property(property: 'mobile_number', type: 'string'),
                                new OA\Property(property: 'doctor_name', type: 'string'),
                                new OA\Property(property: 'doctor_uuid', type: 'string'),
                                new OA\Property(property: 'is_active', type: 'boolean'),
                                new OA\Property(property: 'permissions', type: 'array', items: new OA\Items(type: 'string')),
                                new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
                            ]
                        )),
                        new OA\Property(property: 'meta', properties: [
                            new OA\Property(property: 'totalRecords', type: 'integer'),
                            new OA\Property(property: 'totalPages', type: 'integer'),
                            new OA\Property(property: 'currentPage', type: 'integer'),
                        ], type: 'object'),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
        ]
    )]
    #[Route('/api/v1/admin/secretaries', methods: ['GET'])]
    public function secretaries(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(100, max(1, (int) $request->query->get('limit', 15)));
        $search = trim((string) $request->query->get('search', ''));

        $qb = $this->em->createQueryBuilder()
            ->select(
                'ds.uuid, ds.permissions, ds.active, ds.createdAt',
                'u.mobileNumber as mobile, u.realName as user_name',
                'd.name as doctor_name, d.uuid as doctor_uuid',
            )
            ->from(DoctorSecretary::class, 'ds')
            ->join('ds.doctor', 'd')
            ->join('ds.secretary', 'u')
            ->orderBy('ds.createdAt', 'DESC');

        if ($search !== '') {
            $qb->andWhere('u.mobileNumber LIKE :s OR u.realName LIKE :s OR d.name LIKE :s')
               ->setParameter('s', '%' . $search . '%');
        }

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

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

        $items = array_map(fn(array $ds) => [
            'uuid'          => $ds['uuid'],
            'user_name'     => $ds['user_name'] ?? $ds['mobile'],
            'mobile_number' => $ds['mobile'],
            'doctor_name'   => $ds['doctor_name'],
            'doctor_uuid'   => $ds['doctor_uuid'],
            'is_active'     => (bool) $ds['active'],
            'permissions'   => $ds['permissions'] ?? DoctorSecretary::DEFAULT_PERMISSIONS,
            'created_at'    => date('c', (int) $ds['createdAt']),
        ], $rows);

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

    // ── Ratings ───────────────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/rates',
        summary: 'List all ratings (paginated)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
            new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Paginated list of ratings',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'array', items: new OA\Items(
                            properties: [
                                new OA\Property(property: 'uuid', type: 'string'),
                                new OA\Property(property: 'patient_name', type: 'string'),
                                new OA\Property(property: 'doctor_name', type: 'string'),
                                new OA\Property(property: 'overall', type: 'integer'),
                                new OA\Property(property: 'score', type: 'integer'),
                                new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
                            ]
                        )),
                        new OA\Property(property: 'meta', properties: [
                            new OA\Property(property: 'totalRecords', type: 'integer'),
                            new OA\Property(property: 'totalPages', type: 'integer'),
                            new OA\Property(property: 'currentPage', type: 'integer'),
                        ], type: 'object'),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
        ]
    )]
    #[Route('/api/v1/admin/rates', methods: ['GET'])]
    public function rates(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(100, max(1, (int) $request->query->get('limit', 15)));
        $search = trim((string) $request->query->get('search', ''));

        $qb = $this->em->createQueryBuilder()
            ->select(
                'r.uuid, r.createdAt',
                'r.waitingTimeAtClinic, r.accuracyOfDiagnosis, r.doctorBehavior, r.clinicCleanliness, r.doctorExpertise',
                'u.realName as patient_name, u.mobileNumber as patient_mobile',
                'd.name as doctor_name',
            )
            ->from(Rate::class, 'r')
            ->join('r.doctor', 'd')
            ->join('r.user', 'u')
            ->orderBy('r.createdAt', 'DESC');

        if ($search !== '') {
            $qb->andWhere('d.name LIKE :s OR u.realName LIKE :s')
               ->setParameter('s', '%' . $search . '%');
        }

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

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

        $items = array_map(function (array $r) {
            $overall = (int) round((
                $r['waitingTimeAtClinic'] + $r['accuracyOfDiagnosis'] + $r['doctorBehavior']
                + $r['clinicCleanliness'] + $r['doctorExpertise']
            ) / 5);
            return [
                'uuid'         => $r['uuid'],
                'patient_name' => $r['patient_name'] ?? $r['patient_mobile'],
                'doctor_name'  => $r['doctor_name'],
                'overall'      => $overall,
                'score'        => (int) round($overall / 20),
                'created_at'   => date('c', (int) $r['createdAt']),
            ];
        }, $rows);

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

    // ── Comments ──────────────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/comments',
        summary: 'List all comments (paginated)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
            new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
            new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string', enum: ['pending', 'approved', 'rejected'])),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Paginated list of comments',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'array', items: new OA\Items(
                            properties: [
                                new OA\Property(property: 'uuid', type: 'string'),
                                new OA\Property(property: 'patient_name', type: 'string'),
                                new OA\Property(property: 'doctor_name', type: 'string'),
                                new OA\Property(property: 'title', type: 'string'),
                                new OA\Property(property: 'body', type: 'string'),
                                new OA\Property(property: 'is_approved', type: 'boolean'),
                                new OA\Property(property: 'status', type: 'string'),
                                new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
                            ]
                        )),
                        new OA\Property(property: 'meta', properties: [
                            new OA\Property(property: 'totalRecords', type: 'integer'),
                            new OA\Property(property: 'totalPages', type: 'integer'),
                            new OA\Property(property: 'currentPage', type: 'integer'),
                        ], type: 'object'),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
        ]
    )]
    #[Route('/api/v1/admin/comments', methods: ['GET'])]
    public function comments(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(100, max(1, (int) $request->query->get('limit', 15)));
        $search = trim((string) $request->query->get('search', ''));
        $status = trim((string) $request->query->get('status', ''));

        $qb = $this->em->createQueryBuilder()
            ->select(
                'c.uuid, c.body, c.status, c.createdAt',
                'u.realName as patient_name',
                'd.name as doctor_name',
            )
            ->from(Comment::class, 'c')
            ->join('c.doctor', 'd')
            ->join('c.user', 'u')
            ->orderBy('c.createdAt', 'DESC');

        if ($search !== '') {
            $qb->andWhere('d.name LIKE :s OR c.body LIKE :s OR u.realName LIKE :s')
               ->setParameter('s', '%' . $search . '%');
        }
        if ($status !== '') {
            $qb->andWhere('c.status = :status')->setParameter('status', $status);
        }

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

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

        $items = array_map(fn(array $c) => [
            'uuid'         => $c['uuid'],
            'patient_name' => $c['patient_name'] ?? '',
            'doctor_name'  => $c['doctor_name'],
            'title'        => mb_substr($c['body'], 0, 50),
            'body'         => $c['body'],
            'is_approved'  => $c['status'] === 'approved',
            'status'       => $c['status'],
            'created_at'   => date('c', (int) $c['createdAt']),
        ], $rows);

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

    // ── SMS Logs ──────────────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/sms/logs',
        summary: 'List SMS send logs (paginated)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Paginated list of SMS logs',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'array', items: new OA\Items(
                            properties: [
                                new OA\Property(property: 'uuid', type: 'string'),
                                new OA\Property(property: 'recipient', type: 'string'),
                                new OA\Property(property: 'message', type: 'string'),
                                new OA\Property(property: 'status', type: 'string', enum: ['sent', 'failed']),
                                new OA\Property(property: 'provider', type: 'string'),
                                new OA\Property(property: 'sent_at', type: 'string', format: 'date-time'),
                                new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
                            ]
                        )),
                        new OA\Property(property: 'meta', properties: [
                            new OA\Property(property: 'totalRecords', type: 'integer'),
                            new OA\Property(property: 'totalPages', type: 'integer'),
                            new OA\Property(property: 'currentPage', type: 'integer'),
                        ], type: 'object'),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
        ]
    )]
    #[Route('/api/v1/admin/sms/logs', methods: ['GET'])]
    public function smsLogs(Request $request): JsonResponse
    {
        $page  = max(1, (int) $request->query->get('page', 1));
        $limit = min(100, max(1, (int) $request->query->get('limit', 15)));
        $tag   = trim((string) $request->query->get('tag', ''));

        $qb = $this->em->createQueryBuilder()
            ->select('s.uuid, s.mobile, s.message, s.provider, s.success, s.tag, s.templateCode, s.createdAt')
            ->from(SmsLog::class, 's')
            ->orderBy('s.createdAt', 'DESC');

        if ($tag !== '') {
            $qb->andWhere('s.tag = :tag')->setParameter('tag', $tag);
        }

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

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

        $items = array_map(fn(array $l) => [
            'uuid'       => $l['uuid'],
            'recipient'  => $l['mobile'],
            'message'    => $l['message'],
            'status'     => $l['success'] ? 'sent' : 'failed',
            'provider'   => $l['provider'],
            'tag'        => $l['tag'],
            'template'   => $l['templateCode'],
            'sent_at'    => date('c', (int) $l['createdAt']),
            'created_at' => date('c', (int) $l['createdAt']),
        ], $rows);

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

    // ── Settlements ───────────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/settlements',
        summary: 'List all settlement requests (paginated)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
            new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string', enum: ['pending', 'approved', 'rejected'])),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Paginated list of settlement requests',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'array', items: new OA\Items(
                            properties: [
                                new OA\Property(property: 'uuid', type: 'string'),
                                new OA\Property(property: 'representation_name', type: 'string'),
                                new OA\Property(property: 'amount', type: 'integer'),
                                new OA\Property(property: 'status', type: 'string'),
                                new OA\Property(property: 'bank_card', type: 'string', nullable: true),
                                new OA\Property(property: 'bank_name', type: 'string', nullable: true),
                                new OA\Property(property: 'reject_reason', type: 'string', nullable: true),
                                new OA\Property(property: 'requested_at', type: 'string', format: 'date-time'),
                                new OA\Property(property: 'processed_at', type: 'string', format: 'date-time', nullable: true),
                            ]
                        )),
                        new OA\Property(property: 'meta', properties: [
                            new OA\Property(property: 'totalRecords', type: 'integer'),
                            new OA\Property(property: 'totalPages', type: 'integer'),
                            new OA\Property(property: 'currentPage', type: 'integer'),
                        ], type: 'object'),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
        ]
    )]
    #[Route('/api/v1/admin/settlements', methods: ['GET'])]
    public function settlements(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(100, max(1, (int) $request->query->get('limit', 15)));
        $status = trim((string) $request->query->get('status', ''));

        $qb = $this->em->createQueryBuilder()
            ->select(
                's.uuid, s.amountRials, s.status, s.bankAccount, s.adminNote, s.reviewedAt, s.createdAt',
                'u.realName as user_name, u.mobileNumber as user_mobile',
            )
            ->from(Settlement::class, 's')
            ->join('s.user', 'u')
            ->orderBy('s.createdAt', 'DESC');

        if ($status !== '') {
            $qb->andWhere('s.status = :status')->setParameter('status', $status);
        }

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

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

        $items = array_map(fn(array $s) => [
            'uuid'                => $s['uuid'],
            'representation_name' => $s['user_name'] ?? $s['user_mobile'],
            'amount'              => (int) $s['amountRials'],
            'status'              => $s['status'],
            'bank_card'           => $s['bankAccount']['card'] ?? null,
            'bank_name'           => $s['bankAccount']['bank_name'] ?? null,
            'bank_iban'           => $s['bankAccount']['iban'] ?? null,
            'bank_owner'          => $s['bankAccount']['owner_name'] ?? null,
            'reject_reason'       => $s['adminNote'],
            'requested_at'        => date('c', (int) $s['createdAt']),
            'processed_at'        => $s['reviewedAt'] ? date('c', (int) $s['reviewedAt']) : null,
        ], $rows);

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

    #[OA\Get(
        path: '/api/v1/admin/settlement/{uuid}',
        summary: 'جزئیات یک درخواست تسویه (ادمین)',
        security: [['bearerAuth' => []]],
        parameters: [new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string'))],
        responses: [
            new OA\Response(response: 200, description: 'جزئیات تسویه'),
            new OA\Response(response: 404, description: 'یافت نشد'),
        ]
    )]
    #[Route('/api/v1/admin/settlement/{uuid}', methods: ['GET'])]
    public function settlementDetail(string $uuid): JsonResponse
    {
        $row = $this->em->createQueryBuilder()
            ->select(
                's.uuid, s.amountRials, s.status, s.bankAccount, s.adminNote, s.receipt, s.reviewedAt, s.createdAt, s.updatedAt',
                'u.realName as user_name, u.mobileNumber as user_mobile',
            )
            ->from(Settlement::class, 's')
            ->join('s.user', 'u')
            ->where('s.uuid = :uuid')
            ->setParameter('uuid', $uuid)
            ->getQuery()->getArrayResult();

        if (empty($row)) {
            return $this->error(ErrorCodes::NOT_FOUND, 'درخواست تسویه یافت نشد', 404);
        }
        $s = $row[0];

        return $this->success([
            'uuid'                => $s['uuid'],
            'representation_name' => $s['user_name'] ?? $s['user_mobile'],
            'representation_mobile' => $s['user_mobile'],
            'amount'              => (int) $s['amountRials'],
            'status'              => $s['status'],
            'bank_card'           => $s['bankAccount']['card'] ?? null,
            'bank_name'           => $s['bankAccount']['bank_name'] ?? null,
            'bank_iban'           => $s['bankAccount']['iban'] ?? null,
            'bank_owner'          => $s['bankAccount']['owner_name'] ?? null,
            'reject_reason'       => $s['adminNote'],
            'receipt'             => $s['receipt'] ?? null,
            'requested_at'        => date('c', (int) $s['createdAt']),
            'processed_at'        => $s['reviewedAt'] ? date('c', (int) $s['reviewedAt']) : null,
        ]);
    }

    // ── SMS Templates (paginated list with optional status filter) ────────────

    #[OA\Get(
        path: '/api/v1/admin/sms/sample-templates',
        summary: 'List SMS sample templates (paginated)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
            new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string', enum: ['approved', 'pending'], default: 'approved')),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Paginated list of SMS templates',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'array', items: new OA\Items(
                            properties: [
                                new OA\Property(property: 'uuid', type: 'string'),
                                new OA\Property(property: 'name', type: 'string'),
                                new OA\Property(property: 'body', type: 'string'),
                                new OA\Property(property: 'provider_code', type: 'string', nullable: true),
                                new OA\Property(property: 'status', type: 'string'),
                                new OA\Property(property: 'admin_note', type: 'string', nullable: true),
                                new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
                            ]
                        )),
                        new OA\Property(property: 'meta', properties: [
                            new OA\Property(property: 'totalRecords', type: 'integer'),
                            new OA\Property(property: 'totalPages', type: 'integer'),
                            new OA\Property(property: 'currentPage', type: 'integer'),
                        ], type: 'object'),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
        ]
    )]
    #[Route('/api/v1/admin/sms/sample-templates', methods: ['GET'])]
    public function smsSampleTemplates(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(100, max(1, (int) $request->query->get('limit', 15)));
        $status = trim((string) $request->query->get('status', 'approved'));

        $qb = $this->em->createQueryBuilder()
            ->select('t.uuid, t.name, t.body, t.providerCode, t.status, t.adminNote, t.createdAt')
            ->from(SmsTemplate::class, 't')
            ->orderBy('t.createdAt', 'DESC');

        if ($status !== '') {
            $qb->where('t.status = :status')->setParameter('status', $status);
        }

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

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

        $items = array_map(fn(array $t) => [
            'uuid'          => $t['uuid'],
            'name'          => $t['name'],
            'body'          => $t['body'],
            'provider_code' => $t['providerCode'],
            'status'        => $t['status'],
            'admin_note'    => $t['adminNote'],
            'created_at'    => date('c', (int) $t['createdAt']),
        ], $rows);

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

    // ── Dashboard Recent ──────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/dashboard/recent',
        summary: 'Get recent dashboard data (appointments, payments, users)',
        security: [['bearerAuth' => []]],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Recent dashboard data',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', properties: [
                            new OA\Property(property: 'appointments', type: 'array', items: new OA\Items(
                                properties: [
                                    new OA\Property(property: 'uuid', type: 'string'),
                                    new OA\Property(property: 'slot_start', type: 'string', format: 'date-time'),
                                    new OA\Property(property: 'status', type: 'string'),
                                    new OA\Property(property: 'doctor_name', type: 'string'),
                                    new OA\Property(property: 'user_mobile', type: 'string'),
                                    new OA\Property(property: 'user_name', type: 'string'),
                                    new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
                                ]
                            )),
                            new OA\Property(property: 'payments', type: 'array', items: new OA\Items(
                                properties: [
                                    new OA\Property(property: 'uuid', type: 'string'),
                                    new OA\Property(property: 'amount', type: 'integer'),
                                    new OA\Property(property: 'status', type: 'string'),
                                    new OA\Property(property: 'gateway', type: 'string'),
                                    new OA\Property(property: 'user_mobile', type: 'string'),
                                    new OA\Property(property: 'user_name', type: 'string'),
                                    new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
                                ]
                            )),
                            new OA\Property(property: 'users', type: 'array', items: new OA\Items(
                                properties: [
                                    new OA\Property(property: 'uuid', type: 'string'),
                                    new OA\Property(property: 'mobile', type: 'string'),
                                    new OA\Property(property: 'name', type: 'string'),
                                    new OA\Property(property: 'email', type: 'string', nullable: true),
                                    new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
                                ]
                            )),
                        ], type: 'object'),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
        ]
    )]
    #[Route('/api/v1/admin/dashboard/recent', methods: ['GET'])]
    public function dashboardRecent(): JsonResponse
    {
        $em = $this->em;

        $recentAppointments = $em->createQueryBuilder()
            ->select(
                'a.uuid, a.slotStart, a.status, a.createdAt',
                'd.name as doctor_name',
                'u.mobileNumber as user_mobile, u.realName as user_name'
            )
            ->from(Appointment::class, 'a')
            ->join('a.doctor', 'd')
            ->join('a.user', 'u')
            ->orderBy('a.createdAt', 'DESC')
            ->setMaxResults(6)
            ->getQuery()->getArrayResult();

        $recentPayments = $em->createQueryBuilder()
            ->select(
                'p.uuid, p.amountRials, p.status, p.gateway, p.createdAt',
                'u.mobileNumber as user_mobile, u.realName as user_name'
            )
            ->from(Payment::class, 'p')
            ->join('p.user', 'u')
            ->orderBy('p.createdAt', 'DESC')
            ->setMaxResults(6)
            ->getQuery()->getArrayResult();

        $recentUsers = $em->createQueryBuilder()
            ->select('u.uuid, u.mobileNumber, u.realName, u.email, u.createdAt')
            ->from(User::class, 'u')
            ->orderBy('u.createdAt', 'DESC')
            ->setMaxResults(6)
            ->getQuery()->getArrayResult();

        return $this->success([
            'appointments' => array_map(fn(array $a) => [
                'uuid'        => $a['uuid'],
                'slot_start'  => date('c', (int) $a['slotStart']),
                'status'      => $a['status'],
                'doctor_name' => $a['doctor_name'],
                'user_mobile' => $a['user_mobile'],
                'user_name'   => $a['user_name'],
                'created_at'  => date('c', (int) $a['createdAt']),
            ], $recentAppointments),
            'payments' => array_map(fn(array $p) => [
                'uuid'        => $p['uuid'],
                'amount'      => (int) $p['amountRials'],
                'status'      => $p['status'],
                'gateway'     => $p['gateway'],
                'user_mobile' => $p['user_mobile'],
                'user_name'   => $p['user_name'],
                'created_at'  => date('c', (int) $p['createdAt']),
            ], $recentPayments),
            'users' => array_map(fn(array $u) => [
                'uuid'        => $u['uuid'],
                'mobile'      => $u['mobileNumber'],
                'name'        => $u['realName'],
                'email'       => $u['email'],
                'created_at'  => date('c', (int) $u['createdAt']),
            ], $recentUsers),
        ]);
    }

    // ── Dashboard Stats ───────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/dashboard/stats',
        summary: 'Get aggregated dashboard statistics',
        security: [['bearerAuth' => []]],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Dashboard statistics',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', properties: [
                            new OA\Property(property: 'total_users', type: 'integer'),
                            new OA\Property(property: 'active_doctors', type: 'integer'),
                            new OA\Property(property: 'total_doctors', type: 'integer'),
                            new OA\Property(property: 'total_clinics', type: 'integer'),
                            new OA\Property(property: 'today_appointments', type: 'integer'),
                            new OA\Property(property: 'total_appointments', type: 'integer'),
                            new OA\Property(property: 'today_payments_count', type: 'integer'),
                            new OA\Property(property: 'today_payments_amount', type: 'integer'),
                            new OA\Property(property: 'total_payments_amount', type: 'integer'),
                            new OA\Property(property: 'pending_comments', type: 'integer'),
                            new OA\Property(property: 'pending_settlements', type: 'integer'),
                        ], type: 'object'),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
        ]
    )]
    #[Route('/api/v1/admin/dashboard/stats', methods: ['GET'])]
    public function dashboardStats(): JsonResponse
    {
        $em = $this->em;

        $todayStart = mktime(0, 0, 0);
        $todayEnd   = mktime(23, 59, 59);

        $totalUsers = (int) $em->createQueryBuilder()
            ->select('COUNT(u.id)')->from(User::class, 'u')
            ->getQuery()->getSingleScalarResult();

        $activeDoctors = (int) $em->createQueryBuilder()
            ->select('COUNT(d.id)')->from(Doctor::class, 'd')
            ->where('d.activeDoctorAppointment = true')
            ->getQuery()->getSingleScalarResult();

        $totalDoctors = (int) $em->createQueryBuilder()
            ->select('COUNT(d.id)')->from(Doctor::class, 'd')
            ->getQuery()->getSingleScalarResult();

        $totalClinics = (int) $em->createQueryBuilder()
            ->select('COUNT(c.id)')->from(Clinic::class, 'c')
            ->getQuery()->getSingleScalarResult();

        $todayAppointments = (int) $em->createQueryBuilder()
            ->select('COUNT(a.id)')->from(Appointment::class, 'a')
            ->where('a.slotStart BETWEEN :s AND :e')
            ->setParameter('s', $todayStart)->setParameter('e', $todayEnd)
            ->getQuery()->getSingleScalarResult();

        $totalAppointments = (int) $em->createQueryBuilder()
            ->select('COUNT(a.id)')->from(Appointment::class, 'a')
            ->getQuery()->getSingleScalarResult();

        $todayPaymentsResult = $em->createQueryBuilder()
            ->select('COUNT(p.id) as cnt, COALESCE(SUM(p.amountRials), 0) as total')
            ->from(Payment::class, 'p')
            ->where('p.status = :s AND p.createdAt BETWEEN :ts AND :te')
            ->setParameter('s', Payment::STATUS_SUCCESS)
            ->setParameter('ts', $todayStart)->setParameter('te', $todayEnd)
            ->getQuery()->getSingleResult();

        $totalPaymentsResult = $em->createQueryBuilder()
            ->select('COUNT(p.id) as cnt, COALESCE(SUM(p.amountRials), 0) as total')
            ->from(Payment::class, 'p')
            ->where('p.status = :s')
            ->setParameter('s', Payment::STATUS_SUCCESS)
            ->getQuery()->getSingleResult();

        $pendingComments = (int) $em->createQueryBuilder()
            ->select('COUNT(c.id)')->from(Comment::class, 'c')
            ->where('c.status = :s')->setParameter('s', Comment::STATUS_PENDING)
            ->getQuery()->getSingleScalarResult();

        $pendingSettlements = (int) $em->createQueryBuilder()
            ->select('COUNT(s.id)')->from(Settlement::class, 's')
            ->where('s.status = :s')->setParameter('s', Settlement::STATUS_PENDING)
            ->getQuery()->getSingleScalarResult();

        $monthStart = mktime(0, 0, 0, (int) date('n'), 1);
        $monthEnd   = time();

        $thisMonthRevenue = (int) $em->createQueryBuilder()
            ->select('COALESCE(SUM(p.amountRials), 0)')
            ->from(Payment::class, 'p')
            ->where('p.status = :s AND p.createdAt BETWEEN :ts AND :te')
            ->setParameter('s', Payment::STATUS_SUCCESS)
            ->setParameter('ts', $monthStart)->setParameter('te', $monthEnd)
            ->getQuery()->getSingleScalarResult();

        $thisMonthAppointments = (int) $em->createQueryBuilder()
            ->select('COUNT(a.id)')
            ->from(Appointment::class, 'a')
            ->where('a.createdAt BETWEEN :s AND :e')
            ->setParameter('s', $monthStart)->setParameter('e', $monthEnd)
            ->getQuery()->getSingleScalarResult();

        return $this->success([
            'total_users'              => $totalUsers,
            'active_doctors'           => $activeDoctors,
            'total_doctors'            => $totalDoctors,
            'total_clinics'            => $totalClinics,
            'today_appointments'       => $todayAppointments,
            'total_appointments'       => $totalAppointments,
            'today_payments_count'     => (int) $todayPaymentsResult['cnt'],
            'today_payments_amount'    => (int) $todayPaymentsResult['total'],
            'total_payments_amount'    => (int) $totalPaymentsResult['total'],
            'pending_comments'         => $pendingComments,
            'pending_settlements'      => $pendingSettlements,
            'this_month_revenue'       => $thisMonthRevenue,
            'this_month_appointments'  => $thisMonthAppointments,
        ]);
    }

    // ── Dashboard Charts ──────────────────────────────────────────────────────

    #[Route('/api/v1/admin/dashboard/charts', methods: ['GET'])]
    public function dashboardCharts(Request $request): JsonResponse
    {
        $conn = $this->em->getConnection();
        $now  = time();

        $from = $request->query->get('from') ? (int) $request->query->get('from') : $now - (30 * 86400);
        $to   = $request->query->get('to')   ? (int) $request->query->get('to')   : $now;

        $apptRows = $conn->fetchAllAssociative(
            'SELECT DATE(FROM_UNIXTIME(slot_start)) AS d, COUNT(*) AS cnt
             FROM appointments WHERE slot_start BETWEEN :s AND :e GROUP BY d ORDER BY d',
            ['s' => $from, 'e' => $to]
        );
        $apptMap = array_column($apptRows, 'cnt', 'd');

        $revRows = $conn->fetchAllAssociative(
            'SELECT DATE(FROM_UNIXTIME(created_at)) AS d, COALESCE(SUM(amount_rials), 0) AS total
             FROM payments WHERE status = :st AND created_at BETWEEN :s AND :e GROUP BY d ORDER BY d',
            ['st' => 'received', 's' => $from, 'e' => $to]
        );
        $revMap = array_column($revRows, 'total', 'd');

        $days        = max(1, (int) ceil(($to - $from) / 86400));
        $apptByDay   = [];
        $revByDay    = [];
        for ($i = 0; $i < $days; $i++) {
            $ts        = $from + $i * 86400;
            $date      = date('Y-m-d', $ts);
            $shortDate = date('m/d',   $ts);
            $apptByDay[] = ['date' => $shortDate, 'count'  => (int)($apptMap[$date] ?? 0)];
            $revByDay[]  = ['date' => $shortDate, 'amount' => (int)($revMap[$date]  ?? 0)];
        }

        $statusRows = $conn->fetchAllAssociative(
            'SELECT status, COUNT(*) AS cnt FROM appointments GROUP BY status ORDER BY cnt DESC'
        );

        $specRows = $conn->fetchAllAssociative(
            'SELECT s.name, COUNT(a.id) AS cnt
             FROM appointments a
             JOIN doctor_specialties ds ON ds.doctor_id = a.doctor_id
             JOIN specialties s ON s.id = ds.specialty_id
             GROUP BY s.id, s.name ORDER BY cnt DESC LIMIT 8'
        );

        $subRows = $conn->fetchAllAssociative(
            'SELECT sp.name AS plan_name, COUNT(cs.id) AS cnt, COALESCE(SUM(p.amount_rials), 0) AS revenue
             FROM clinic_subscriptions cs
             JOIN subscription_plans sp ON sp.id = cs.plan_id
             LEFT JOIN payments p ON p.id = cs.payment_id AND p.status = :st
             WHERE cs.created_at BETWEEN :s AND :e
             GROUP BY sp.id, sp.name ORDER BY cnt DESC',
            ['st' => 'received', 's' => $from, 'e' => $to]
        );

        return $this->success([
            'appointments_by_day'       => $apptByDay,
            'revenue_by_day'            => $revByDay,
            'appointment_status'        => array_map(fn($r) => ['status' => $r['status'], 'count' => (int) $r['cnt']], $statusRows),
            'top_specialties'           => array_map(fn($r) => ['name' => $r['name'], 'count' => (int) $r['cnt']], $specRows),
            'subscription_sales_by_plan' => array_map(fn($r) => [
                'plan'    => $r['plan_name'],
                'count'   => (int) $r['cnt'],
                'revenue' => (int) $r['revenue'],
            ], $subRows),
            'period' => ['from' => $from, 'to' => $to],
        ]);
    }

    // ── Application logs ────────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/logs',
        summary: 'List persisted application logs (paginated)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 25)),
            new OA\Parameter(name: 'level', in: 'query', required: false, description: 'PSR level filter (warning/error/critical/...)', schema: new OA\Schema(type: 'string')),
            new OA\Parameter(name: 'search', in: 'query', required: false, description: 'substring match on message', schema: new OA\Schema(type: 'string')),
            new OA\Parameter(name: 'from', in: 'query', required: false, description: 'unix timestamp lower bound', schema: new OA\Schema(type: 'integer')),
            new OA\Parameter(name: 'to', in: 'query', required: false, description: 'unix timestamp upper bound', schema: new OA\Schema(type: 'integer')),
        ],
        responses: [new OA\Response(response: 200, description: 'Paginated list of logs')]
    )]
    #[Route('/api/v1/admin/logs', methods: ['GET'])]
    public function logs(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(100, max(5, (int) $request->query->get('limit', 25)));
        $level  = trim((string) $request->query->get('level', ''));
        $search = trim((string) $request->query->get('search', ''));
        $from   = trim((string) $request->query->get('from', ''));
        $to     = trim((string) $request->query->get('to', ''));

        $qb = $this->em->createQueryBuilder()
            ->select('l.id, l.level, l.message, l.context, l.channel, l.path, l.createdAt')
            ->from(AppLog::class, 'l');

        if ($level !== '') {
            $qb->andWhere('l.level = :level')->setParameter('level', $level);
        }
        if ($search !== '') {
            $qb->andWhere('l.message LIKE :s')->setParameter('s', '%' . $search . '%');
        }
        if ($from !== '') {
            $qb->andWhere('l.createdAt >= :from')->setParameter('from', (int) $from);
        }
        if ($to !== '') {
            $qb->andWhere('l.createdAt <= :to')->setParameter('to', (int) $to);
        }

        $qb->orderBy('l.id', 'DESC');

        $total = (clone $qb)->select('COUNT(l.id)')->getQuery()->getSingleScalarResult();
        $rows  = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)->getQuery()->getArrayResult();

        return $this->paginated(array_map(fn(array $l) => [
            'id'         => (int) $l['id'],
            'level'      => $l['level'],
            'message'    => $l['message'],
            'context'    => $l['context'],
            'channel'    => $l['channel'],
            'path'       => $l['path'],
            'created_at' => (int) $l['createdAt'],
        ], $rows), (int) $total, $page, $limit);
    }

    #[OA\Get(
        path: '/api/v1/admin/logs/export',
        summary: 'Export all persisted application logs as CSV (respects filters, no pagination)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'level', in: 'query', required: false, description: 'PSR level filter (warning/error/critical/...)', schema: new OA\Schema(type: 'string')),
            new OA\Parameter(name: 'search', in: 'query', required: false, description: 'substring match on message', schema: new OA\Schema(type: 'string')),
            new OA\Parameter(name: 'from', in: 'query', required: false, description: 'unix timestamp lower bound', schema: new OA\Schema(type: 'integer')),
            new OA\Parameter(name: 'to', in: 'query', required: false, description: 'unix timestamp upper bound', schema: new OA\Schema(type: 'integer')),
        ],
        responses: [new OA\Response(response: 200, description: 'CSV file (text/csv) of all matching logs')]
    )]
    #[Route('/api/v1/admin/logs/export', methods: ['GET'])]
    public function exportLogs(Request $request): StreamedResponse
    {
        $level  = trim((string) $request->query->get('level', ''));
        $search = trim((string) $request->query->get('search', ''));
        $from   = trim((string) $request->query->get('from', ''));
        $to     = trim((string) $request->query->get('to', ''));

        $qb = $this->em->createQueryBuilder()
            ->select('l.id, l.level, l.message, l.context, l.channel, l.path, l.createdAt')
            ->from(AppLog::class, 'l');

        if ($level !== '') {
            $qb->andWhere('l.level = :level')->setParameter('level', $level);
        }
        if ($search !== '') {
            $qb->andWhere('l.message LIKE :s')->setParameter('s', '%' . $search . '%');
        }
        if ($from !== '') {
            $qb->andWhere('l.createdAt >= :from')->setParameter('from', (int) $from);
        }
        if ($to !== '') {
            $qb->andWhere('l.createdAt <= :to')->setParameter('to', (int) $to);
        }

        $qb->orderBy('l.id', 'DESC');

        $response = new StreamedResponse(function () use ($qb) {
            $out = fopen('php://output', 'w');
            // UTF-8 BOM so Excel renders Persian correctly
            fwrite($out, "\xEF\xBB\xBF");
            fputcsv($out, ['id', 'level', 'message', 'context', 'channel', 'path', 'created_at']);

            foreach ($qb->getQuery()->toIterable([], \Doctrine\ORM\Query::HYDRATE_ARRAY) as $l) {
                fputcsv($out, [
                    (int) $l['id'],
                    $l['level'],
                    $l['message'],
                    $l['context'],
                    $l['channel'],
                    $l['path'],
                    date('Y-m-d H:i:s', (int) $l['createdAt']),
                ]);
            }
            fclose($out);
        });

        $filename = 'logs-' . date('Ymd-His') . '.csv';
        $response->headers->set('Content-Type', 'text/csv; charset=UTF-8');
        $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"');

        return $response;
    }

    #[Route('/api/v1/admin/logs', methods: ['DELETE'])]
    public function clearLogs(): JsonResponse
    {
        $deleted = $this->em->getRepository(AppLog::class)->deleteAll();

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