<?php

namespace App\Doctor\Repository;

use App\Auth\Entity\User;
use App\Clinic\Entity\Clinic;
use App\Doctor\Entity\Doctor;
use App\Doctor\Entity\DoctorAddress;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Doctrine\Persistence\ManagerRegistry;

class DoctorRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Doctor::class);
    }

    public function findByUuid(string $uuid): ?Doctor
    {
        return $this->findOneBy(['uuid' => $uuid]);
    }

    public function findByUser(User $user): ?Doctor
    {
        return $this->findOneBy(['user' => $user]);
    }

    public function findOneByMobile(string $mobile): ?Doctor
    {
        return $this->createQueryBuilder('d')
            ->join('d.user', 'u')
            ->where('u.mobileNumber = :mobile')
            ->setParameter('mobile', $mobile)
            ->setMaxResults(1)
            ->getQuery()
            ->getOneOrNullResult();
    }

    public function findWithFilters(array $filters): array
    {
        $page  = max(1, (int) ($filters['page'] ?? 1));
        $limit = min(50, max(1, (int) ($filters['limit'] ?? 10)));
        $sort  = strtoupper($filters['sort'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';

        // Location comes from the doctor's own address (doctor_addresses), plus a
        // fallback to the clinic address for doctors listed under a clinic.
        $qb = $this->createQueryBuilder('d')
            ->leftJoin('d.specialties', 's')
            ->leftJoin(DoctorAddress::class, 'da', Join::WITH, 'da.doctor = d')
            ->distinct();

        if (!empty($filters['state_id'])) {
            $stateId   = (int) $filters['state_id'];
            $clinicIds = $this->doctorIdsViaClinicLocation('province', $stateId);
            $orX = $qb->expr()->orX('IDENTITY(da.province) = :state');
            if ($clinicIds) {
                $orX->add('d.id IN (:stateClinicDoctorIds)');
                $qb->setParameter('stateClinicDoctorIds', $clinicIds);
            }
            $qb->andWhere($orX)->setParameter('state', $stateId);
        }
        if (!empty($filters['city_id'])) {
            $cityId    = (int) $filters['city_id'];
            $clinicIds = $this->doctorIdsViaClinicLocation('city', $cityId);
            $orX = $qb->expr()->orX('IDENTITY(da.city) = :city');
            if ($clinicIds) {
                $orX->add('d.id IN (:cityClinicDoctorIds)');
                $qb->setParameter('cityClinicDoctorIds', $clinicIds);
            }
            $qb->andWhere($orX)->setParameter('city', $cityId);
        }
        if (!empty($filters['specialty_id'])) {
            $qb->andWhere('s.id = :specialty')->setParameter('specialty', (int) $filters['specialty_id']);
        }
        // Scope دامنه‌ی نماینده‌ی سراسری (تزریق‌شده توسط DomainContextResolver در کنترلر).
        if (!empty($filters['representation_id'])) {
            $qb->andWhere('d.representationId = :repId')->setParameter('repId', (int) $filters['representation_id']);
        }
        if (!empty($filters['gender'])) {
            $qb->andWhere('d.gender = :gender')->setParameter('gender', $filters['gender']);
        }
        if (!empty($filters['degree'])) {
            $qb->andWhere('d.degree = :degree')->setParameter('degree', $filters['degree']);
        }
        if (!empty($filters['name'])) {
            $qb->andWhere('d.name LIKE :name')->setParameter('name', '%' . $filters['name'] . '%');
        }
        // Public list shows only doctors with appointment enabled unless the
        // caller explicitly asks otherwise (e.g. admin passing active=0).
        $active = isset($filters['active']) ? (bool) $filters['active'] : true;
        $qb->andWhere('d.activeDoctorAppointment = :active')
           ->setParameter('active', $active);

        $qb->orderBy('d.doctorRate', $sort);

        $total   = (new Paginator($qb))->count();
        $results = $qb->setFirstResult(($page - 1) * $limit)
                      ->setMaxResults($limit)
                      ->getQuery()
                      ->getResult();

        return [
            'items'       => $results,
            'total'       => $total,
            'page'        => $page,
            'limit'       => $limit,
            'totalPages'  => (int) ceil($total / $limit),
        ];
    }

    /**
     * IDs of doctors who belong to a clinic whose own address (DoctorAddress with
     * doctor IS NULL) is in the given city/province. Used so doctors without a
     * direct address still surface under their clinic's location, without a
     * per-row correlated subquery on the main search.
     *
     * @return int[]
     */
    private function doctorIdsViaClinicLocation(string $field, int $locationId): array
    {
        $column = $field === 'city' ? 'ca.city' : 'ca.province';

        $rows = $this->getEntityManager()->createQueryBuilder()
            ->select('cd.id AS doctorId')
            ->from(Clinic::class, 'cl')
            ->join('cl.doctors', 'cd')
            ->join(DoctorAddress::class, 'ca', Join::WITH, 'ca.clinicId = cl.id AND ca.doctor IS NULL')
            ->where(sprintf('IDENTITY(%s) = :loc', $column))
            ->setParameter('loc', $locationId)
            ->getQuery()
            ->getScalarResult();

        return array_values(array_unique(array_map('intval', array_column($rows, 'doctorId'))));
    }

    public function findByClinicWithFilters(int $clinicId, array $filters): array
    {
        $page  = max(1, (int) ($filters['page'] ?? 1));
        $limit = min(50, max(1, (int) ($filters['limit'] ?? 10)));
        $sort  = strtoupper($filters['sort'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';

        // Doctor has no inverse 'clinics' relation; the ManyToMany is owned by
        // Clinic.doctors. Join Clinic and match its doctors collection to d.
        $qb = $this->createQueryBuilder('d')
            ->innerJoin(Clinic::class, 'c', Join::WITH, 'd MEMBER OF c.doctors')
            ->leftJoin('d.specialties', 's')
            ->where('c.id = :clinicId')
            ->setParameter('clinicId', $clinicId)
            ->distinct();

        if (!empty($filters['specialty'])) {
            $qb->andWhere('s.id = :specialty')->setParameter('specialty', (int) $filters['specialty']);
        }
        if (!empty($filters['gender'])) {
            $qb->andWhere('d.gender = :gender')->setParameter('gender', $filters['gender']);
        }
        if (!empty($filters['degree'])) {
            $qb->andWhere('d.degree = :degree')->setParameter('degree', $filters['degree']);
        }
        if (!empty($filters['name'])) {
            $qb->andWhere('d.name LIKE :name')->setParameter('name', '%' . $filters['name'] . '%');
        }
        if (isset($filters['active'])) {
            $qb->andWhere('d.activeDoctorAppointment = :active')
               ->setParameter('active', (bool) $filters['active']);
        }

        // Hydrate specialties in the same query so toListArray() doesn't lazy-load
        // them per doctor (N+1). fetchJoinCollection keeps LIMIT paginating by
        // doctor, not by joined rows.
        $qb->addSelect('s')
           ->orderBy('d.doctorRate', $sort)
           ->setFirstResult(($page - 1) * $limit)
           ->setMaxResults($limit);

        $paginator = new Paginator($qb, fetchJoinCollection: true);
        $total     = count($paginator);
        $results   = iterator_to_array($paginator);

        return [
            'items'      => $results,
            'total'      => $total,
            'page'       => $page,
            'limit'      => $limit,
            'totalPages' => (int) ceil($total / $limit),
        ];
    }

    public function save(Doctor $doctor, bool $flush = true): void
    {
        $this->getEntityManager()->persist($doctor);
        if ($flush) {
            $this->getEntityManager()->flush();
        }
    }

    public function remove(Doctor $doctor, bool $flush = true): void
    {
        $this->getEntityManager()->remove($doctor);
        if ($flush) {
            $this->getEntityManager()->flush();
        }
    }
}
