<?php

namespace App\Resource\Service;

use App\Branch\Entity\Room;
use App\Doctor\Entity\Doctor;
use App\Doctor\Entity\DoctorAddress;
use App\Resource\Entity\ClinicResource;
use App\Resource\Entity\ResourceType;
use App\Resource\Repository\ClinicResourceRepository;
use App\Resource\Repository\ResourceTypeRepository;
use App\Staff\Entity\ClinicStaff;
use Doctrine\ORM\EntityManagerInterface;

/**
 * تنها نقطه‌ای که نگاشت «موجودیت موجود ↔ منبع» را می‌داند.
 *
 * هیچ سرویس دیگری نباید `$resource->getDoctor()` را برای تصمیم‌گیری بخواند؛ وگرنه
 * افزودن نوع پل بعدی (مثلاً تجهیزات اجاره‌ای) یعنی گشتن دنبال همهٔ آن نقطه‌ها.
 */
final class ResourceLinker
{
    /**
     * نوع‌های ساخته‌شده و هنوز flush-نشده در همین واحدِ کار.
     *
     * بدون این، `systemType()` نوعِ persist-شده‌ای را که هنوز به دیتابیس نرفته
     * نمی‌دید و دوباره می‌ساخت — که در اجرای واقعی backfill با
     * «Duplicate entry 'doctor-1-staff' for key uniq_rt_tenant_code» می‌شکست.
     *
     * @var array<string, ResourceType>
     */
    private array $pendingTypes = [];

    public function __construct(
        private readonly ClinicResourceRepository $resources,
        private readonly ResourceTypeRepository   $types,
        private readonly EntityManagerInterface   $em,
    ) {}

    /**
     * نوع سیستمی محیط را برمی‌گرداند و اگر نبود می‌سازد. بدون flush — فراخوان تصمیم
     * می‌گیرد کِی بنویسد (backfill per محیط flush می‌کند).
     */
    public function systemType(string $entityType, int $entityId, string $code): ResourceType
    {
        $key = "$entityType:$entityId:$code";

        if (isset($this->pendingTypes[$key])) {
            return $this->pendingTypes[$key];
        }

        $type = $this->types->findByCode($entityType, $entityId, $code);

        if ($type === null) {
            $type = new ResourceType($entityType, $entityId, $code, ResourceType::SYSTEM_CODES[$code] ?? $code);
            $type->markSystem();
            $this->em->persist($type);
        }

        return $this->pendingTypes[$key] = $type;
    }

    /** بعد از flush صدا زده می‌شود؛ نگه‌داشتن نمونه‌های قدیمی بعد از clear() خطرناک است. */
    public function forgetPendingTypes(): void
    {
        $this->pendingTypes = [];
    }

    /** منبعِ متناظر با یک موجودیت در یک شعبه؛ اگر نبود می‌سازد. */
    public function link(Doctor|ClinicStaff|Room $subject, DoctorAddress $address, string $name): ClinicResource
    {
        $existing = $this->resources->findForSubject($subject, $address);

        if ($existing !== null) {
            return $existing;
        }

        $code = match (true) {
            $subject instanceof Doctor      => ResourceType::CODE_DOCTOR,
            $subject instanceof ClinicStaff => ResourceType::CODE_STAFF,
            $subject instanceof Room        => ResourceType::CODE_ROOM,
        };

        $type     = $this->systemType($address->tenantEntityType(), $address->tenantEntityId(), $code);
        $resource = new ClinicResource($address, $type, $name);
        $resource->linkTo($subject);

        // اتاق ظرفیت خودش را دارد؛ شخص همیشه ظرفیت ۱.
        if ($subject instanceof Room) {
            $resource->setCapacity($subject->getCapacity());
            $resource->setActive($subject->isActive());
        }

        $this->em->persist($resource);

        return $resource;
    }

    /** برعکس: منبع → موجودیت اصلی. `null` یعنی دستگاه/تجهیزات. */
    public function subject(ClinicResource $resource): Doctor|ClinicStaff|Room|null
    {
        return $resource->subject();
    }

    /**
     * غیرفعال شدن پرسنل/اتاق باید منبعش را هم غیرفعال کند، وگرنه در جستجوی وقتِ تسک ۰۶
     * ظاهر می‌شود.
     *
     * عمداً فراخوانی صریح است و نه Doctrine lifecycle callback: آن callback در
     * `getArrayResult()` — که لیست‌های ادمین با آن ساخته می‌شوند — اجرا نمی‌شود و
     * رفتار نامتقارن می‌سازد.
     *
     * عکسش برقرار نیست: غیرفعال کردن منبع، پرسنل را غیرفعال نمی‌کند (پرسنل ممکن است
     * فقط نقش اداری داشته باشد).
     */
    public function syncActive(Doctor|ClinicStaff|Room $subject, bool $active): int
    {
        $touched = 0;

        foreach ($this->resources->findAllForSubject($subject) as $resource) {
            if ($resource->isActive() !== $active) {
                $resource->setActive($active);
                $touched++;
            }
        }

        if ($touched > 0) {
            $this->em->flush();
        }

        return $touched;
    }
}
