<?php

namespace App\Resource\Entity;

use App\Doctor\Entity\Doctor;
use App\Doctor\Entity\DoctorAddress;
use App\Branch\Entity\Room;
use App\Resource\Repository\ClinicResourceRepository;
use App\Shared\Tenant\TenantOwnedTrait;
use App\Staff\Entity\ClinicStaff;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;

/**
 * هر چیزی که ممکن است اشغال باشد: پزشک، اپراتور، دستیار، دستگاه، اتاق، تخت، یونیت.
 * قانون طلایی مستند: «تقویم مال منبع است، نه مال پزشک» — تقویمش در تسک ۰۳ می‌آید.
 *
 * نام کلاس عمداً `Resource` نیست: در این کدبیس با مفهوم «منبع API» قاطی می‌شود و
 * جستجوی کد را پر نویز می‌کند.
 *
 * ## پل، نه ادغام
 *
 * `Doctor`، `ClinicStaff` و `Room` هرکدام هویت مستقل و مصرف‌کنندهٔ زنده دارند
 * (`appointments.doctor_id`، `service_item_staff`، سایت عمومی). تبدیلشان به زیرکلاس
 * یعنی مهاجرت هم‌زمان همهٔ آن مسیرها. به‌جایش حداکثر **یکی** از سه ستون پل پر است؛
 * منبعِ بدون پل یعنی دستگاه یا تجهیزات.
 */
#[ORM\Entity(repositoryClass: ClinicResourceRepository::class)]
#[ORM\Table(name: 'clinic_resources')]
#[ORM\Index(columns: ['entity_type', 'entity_id', 'active'], name: 'idx_resources_tenant')]
#[ORM\Index(columns: ['address_id', 'resource_type_id', 'active'], name: 'idx_resources_address_type')]
#[ORM\UniqueConstraint(name: 'uniq_resource_doctor_address', columns: ['doctor_id', 'address_id'])]
#[ORM\UniqueConstraint(name: 'uniq_resource_staff_address', columns: ['staff_id', 'address_id'])]
#[ORM\UniqueConstraint(name: 'uniq_resource_room', columns: ['room_id'])]
class ClinicResource
{
    use TenantOwnedTrait;

    public const MAX_ATTRIBUTES = 20;

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private ?int $id = null;

    #[ORM\Column(type: 'string', length: 36, unique: true)]
    private string $uuid;

    /**
     * منابع همیشه مال یک شعبه‌اند، و «شعبه» همان آدرس محل نوبت‌دهی است
     * ({@see docs/new_feture/taskes/_shared/branch-is-doctor-address.md}).
     */
    #[ORM\ManyToOne(targetEntity: DoctorAddress::class)]
    #[ORM\JoinColumn(name: 'address_id', referencedColumnName: 'id', nullable: false, onDelete: 'RESTRICT')]
    private DoctorAddress $address;

    #[ORM\ManyToOne(targetEntity: ResourceType::class)]
    #[ORM\JoinColumn(name: 'resource_type_id', referencedColumnName: 'id', nullable: false, onDelete: 'RESTRICT')]
    private ResourceType $type;

    #[ORM\Column(type: 'string', length: 150)]
    private string $name;

    /** ظرفیت هم‌زمان: اتاق تزریق سه‌تخته یک ردیف با ظرفیت ۳ است، نه سه ردیف. */
    #[ORM\Column(type: 'smallint', options: ['default' => 1])]
    private int $capacity = 1;

    /** آماده‌سازی پیش از بیمار — جزو نوبت بیمار نیست، ولی منبع را اشغال می‌کند. */
    #[ORM\Column(name: 'setup_minutes', type: 'smallint', options: ['default' => 0])]
    private int $setupMinutes = 0;

    #[ORM\Column(name: 'cleanup_minutes', type: 'smallint', options: ['default' => 0])]
    private int $cleanupMinutes = 0;

    /** JSON آزاد ولی فقط اسکالر — {@see \App\Resource\Service\ResourceService::normalizeAttributes()} */
    #[ORM\Column(type: 'json', nullable: true)]
    private ?array $attributes = null;

    #[ORM\ManyToOne(targetEntity: Doctor::class)]
    #[ORM\JoinColumn(name: 'doctor_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
    private ?Doctor $doctor = null;

    #[ORM\ManyToOne(targetEntity: ClinicStaff::class)]
    #[ORM\JoinColumn(name: 'staff_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
    private ?ClinicStaff $staff = null;

    #[ORM\ManyToOne(targetEntity: Room::class)]
    #[ORM\JoinColumn(name: 'room_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
    private ?Room $room = null;

    #[ORM\Column(type: 'boolean', options: ['default' => true])]
    private bool $active = true;

    #[ORM\Column(name: 'created_at', type: 'integer')]
    private int $createdAt;

    #[ORM\Column(name: 'updated_at', type: 'integer')]
    private int $updatedAt;

    /** @var Collection<int, ResourceSkill> */
    #[ORM\OneToMany(targetEntity: ResourceSkill::class, mappedBy: 'resource', cascade: ['persist', 'remove'], orphanRemoval: true)]
    private Collection $skills;

    public function __construct(DoctorAddress $address, ResourceType $type, string $name)
    {
        $this->uuid      = Uuid::v4()->toRfc4122();
        $this->address   = $address;
        $this->type      = $type;
        $this->name      = $name;
        $this->createdAt = time();
        $this->updatedAt = time();
        $this->skills    = new ArrayCollection();

        // جفت از آدرس مشتق می‌شود، نه از بدنهٔ درخواست — پس هیچ نقطهٔ ساختی
        // نمی‌تواند فراموشش کند و کلاینت هم نمی‌تواند منبع را به محیط دیگری بچسباند.
        $this->assignTenantPair($address->tenantEntityType(), $address->tenantEntityId());
    }

    public function getId(): ?int                { return $this->id; }
    public function getUuid(): string            { return $this->uuid; }
    public function getAddress(): DoctorAddress  { return $this->address; }
    public function getType(): ResourceType      { return $this->type; }
    public function getName(): string            { return $this->name; }
    public function getCapacity(): int           { return $this->capacity; }
    public function getSetupMinutes(): int       { return $this->setupMinutes; }
    public function getCleanupMinutes(): int     { return $this->cleanupMinutes; }
    public function getAttributes(): array       { return $this->attributes ?? []; }
    public function getDoctor(): ?Doctor         { return $this->doctor; }
    public function getStaff(): ?ClinicStaff     { return $this->staff; }
    public function getRoom(): ?Room             { return $this->room; }
    public function isActive(): bool             { return $this->active; }
    public function getCreatedAt(): int          { return $this->createdAt; }
    public function getUpdatedAt(): int          { return $this->updatedAt; }

    /** @return Collection<int, ResourceSkill> */
    public function getSkills(): Collection      { return $this->skills; }

    public function setName(string $v): self          { $this->name = $v; $this->touch(); return $this; }
    public function setSetupMinutes(int $v): self     { $this->setupMinutes = $this->assertMinutes($v, 'setup_minutes'); $this->touch(); return $this; }
    public function setCleanupMinutes(int $v): self   { $this->cleanupMinutes = $this->assertMinutes($v, 'cleanup_minutes'); $this->touch(); return $this; }
    public function setAttributes(array $v): self     { $this->attributes = $v === [] ? null : $v; $this->touch(); return $this; }
    public function setActive(bool $v): self          { $this->active = $v; $this->touch(); return $this; }
    public function setType(ResourceType $v): self    { $this->type = $v; $this->touch(); return $this; }

    /**
     * @throws \InvalidArgumentException ظرفیت کمتر از ۱، یا بزرگ‌تر از ۱ روی منبعی که
     *         یک شخص است — پزشک و اپراتور هم‌زمان دو بیمار ندارند.
     */
    public function setCapacity(int $v): self
    {
        if ($v < 1) {
            throw new \InvalidArgumentException('Resource capacity must be at least 1.');
        }

        if ($v > 1 && $this->isPerson()) {
            throw new \InvalidArgumentException('A person resource cannot serve more than one patient at a time.');
        }

        $this->capacity = $v;
        $this->touch();

        return $this;
    }

    /** منبعی که یک انسان است: ظرفیتش همیشه ۱ می‌ماند. */
    public function isPerson(): bool
    {
        return $this->doctor !== null
            || $this->staff !== null
            || in_array($this->type->getCode(), [ResourceType::CODE_DOCTOR, ResourceType::CODE_STAFF], true);
    }

    /**
     * پل به موجودیت اصلی. حداکثر یکی مجاز است — MariaDB قید چندستونی `CHECK` را
     * قابل اتکا اجرا نمی‌کند، پس اجبارش اینجاست.
     *
     * @throws \InvalidArgumentException روی پل دوم
     */
    public function linkTo(Doctor|ClinicStaff|Room $subject): self
    {
        if ($this->subject() !== null) {
            throw new \InvalidArgumentException('A resource can bridge to at most one subject.');
        }

        match (true) {
            $subject instanceof Doctor      => $this->doctor = $subject,
            $subject instanceof ClinicStaff => $this->staff  = $subject,
            $subject instanceof Room        => $this->room   = $subject,
        };

        $this->touch();

        return $this;
    }

    /** موجودیت اصلی پشت این منبع؛ `null` یعنی دستگاه/تجهیزات. */
    public function subject(): Doctor|ClinicStaff|Room|null
    {
        return $this->doctor ?? $this->staff ?? $this->room;
    }

    private function assertMinutes(int $v, string $field): int
    {
        if ($v < 0 || $v > 480) {
            throw new \InvalidArgumentException(sprintf('%s must be between 0 and 480.', $field));
        }

        return $v;
    }

    private function touch(): void { $this->updatedAt = time(); }

    public function toArray(): array
    {
        $subject = $this->subject();

        return [
            'uuid'            => $this->uuid,
            'name'            => $this->name,
            'address_uuid'    => $this->address->getUuid(),
            'address_name'    => $this->address->getName(),
            'type_uuid'       => $this->type->getUuid(),
            'type_code'       => $this->type->getCode(),
            'type_name'       => $this->type->getName(),
            'capacity'        => $this->capacity,
            'setup_minutes'   => $this->setupMinutes,
            'cleanup_minutes' => $this->cleanupMinutes,
            'attributes'      => (object) $this->getAttributes(),
            'subject_kind'    => match (true) {
                $this->doctor !== null => 'doctor',
                $this->staff  !== null => 'staff',
                $this->room   !== null => 'room',
                default                => null,
            },
            'subject_uuid'    => $subject?->getUuid(),
            'skills'          => array_map(
                static fn (ResourceSkill $rs): array => $rs->toArray(),
                $this->skills->toArray(),
            ),
            'active'          => $this->active,
            'created_at'      => $this->createdAt,
            'updated_at'      => $this->updatedAt,
        ];
    }
}
