<?php

namespace App\ClinicService\Entity;

use App\ClinicService\Repository\ServiceItemRepository;
use App\Insurance\Enum\ServiceCategory;
use App\Staff\Entity\ClinicStaff;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use App\Shared\Tenant\TenantOwnedTrait;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;

#[ORM\Entity(repositoryClass: ServiceItemRepository::class)]
#[ORM\Table(name: 'service_items')]
#[ORM\Index(columns: ['entity_type', 'entity_id'], name: 'idx_service_items_entity')]
class ServiceItem
{
    use TenantOwnedTrait;

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

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

    #[ORM\ManyToOne(targetEntity: ServiceSection::class, inversedBy: 'items')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    private ServiceSection $section;

    // Legacy single-staff column, kept for backward compatibility with existing
    // consumers (reception/session). Mirrors the first entry of $staffMembers.
    #[ORM\ManyToOne(targetEntity: ClinicStaff::class)]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    private ?ClinicStaff $staff = null;

    /**
     * @var Collection<int, ClinicStaff> personnel assigned to this service.
     * EAGER so hydration always populates the typed property (avoids the
     * "accessed before initialization" pitfall on lazy typed collections).
     */
    #[ORM\ManyToMany(targetEntity: ClinicStaff::class, fetch: 'EAGER')]
    #[ORM\JoinTable(name: 'service_item_staff')]
    private Collection $staffMembers;

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

    #[ORM\Column(name: 'price_rials', type: 'integer')]
    private int $priceRials = 0;

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

    #[ORM\Column(name: 'insurance_covered', type: 'boolean')]
    private bool $insuranceCovered = false;

    /** نوع خدمت (سرپایی/بستری) — درصد پوشش بیمه به ازای همین نوع تعیین می‌شود. */
    #[ORM\Column(name: 'service_category', type: 'string', length: 30, enumType: ServiceCategory::class, options: ['default' => 'outpatient'])]
    private ServiceCategory $serviceCategory = ServiceCategory::Outpatient;

    /**
     * @deprecated منبع حقیقتِ پوشش، TenantServiceCoverage است و هیچ محاسبه‌ای این مقدار
     * را نمی‌خواند. ستون برای داده‌ی تاریخی مانده ولی نه نوشته می‌شود و نه منتشر.
     */
    #[ORM\Column(name: 'insurance_price_rials', type: 'integer', nullable: true)]
    private ?int $insurancePriceRials = null;

    /**
     * @deprecated مقدار قدیمی؛ منبع حقیقتِ مدت اکنون `soloDurationMinutes` است.
     *             هنوز نوشته می‌شود تا مصرف‌کننده‌های موجود نشکنند.
     */
    #[ORM\Column(name: 'duration_minutes', type: 'integer', nullable: true)]
    private ?int $durationMinutes = null;

    /** مدت این آیتم وقتی **تنها** انجام شود. */
    #[ORM\Column(name: 'solo_duration_minutes', type: 'smallint', nullable: true)]
    private ?int $soloDurationMinutes = null;

    /**
     * مدت این آیتم وقتی **کنار آیتم دیگری** در همان نوبت انجام شود.
     *
     * بند ۵ مستند: جمع سادهٔ مدت‌ها ظرفیت را هدر می‌دهد. «صورت + بیکینی» ۱۵+۱۲=۲۷
     * نیست، ۱۵+۸=۲۳ است؛ آماده‌سازی و استقرار بیمار دو بار انجام نمی‌شود.
     *
     * `null` یعنی «همان مدت تنها» — سازگاری با دادهٔ موجودی که فقط یک عدد داشت.
     */
    #[ORM\Column(name: 'additional_duration_minutes', type: 'smallint', nullable: true)]
    private ?int $additionalDurationMinutes = null;

    /** تعداد جلسات؛ ۱ یعنی تک‌جلسه‌ای. پروتکل کامل دوره در تسک ۱۲. */
    #[ORM\Column(name: 'session_count', type: 'smallint', options: ['default' => 1])]
    private int $sessionCount = 1;

    #[ORM\ManyToOne(targetEntity: CatalogCategory::class)]
    #[ORM\JoinColumn(name: 'catalog_category_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
    private ?CatalogCategory $catalogCategory = null;

    /** نمایش این سرویس در نوبت‌دهی (پزشک ممکن است همهٔ سرویس‌ها را ارائه ندهد). */
    #[ORM\Column(type: 'boolean', options: ['default' => false])]
    private bool $bookable = false;

    /**
     * پکیج کالای مصرفی این خدمت ({@see \App\Inventory\Entity\InventoryPackage}).
     * ارجاع خام int بدون FK — همان الگوی Tariff و TenantServiceCoverage — تا دامنهٔ
     * ClinicService به Inventory وابسته نشود.
     */
    #[ORM\Column(name: 'inventory_package_id', type: 'integer', nullable: true)]
    private ?int $inventoryPackageId = null;

    /**
     * اقلام کالای تکیِ این خدمت — مستقل از پکیج و قابل استفاده هم‌زمان با آن.
     *
     * @var Collection<int, ServiceItemConsumable>
     */
    #[ORM\OneToMany(mappedBy: 'serviceItem', targetEntity: ServiceItemConsumable::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
    private Collection $consumables;

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

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

    public function __construct(ServiceSection $section, string $name, int $priceRials = 0)
    {
        $this->uuid       = Uuid::v4()->toRfc4122();
        $this->section    = $section;
        $this->assignTenantPair($section->getEntityType(), $section->getEntityId());
        $this->name       = $name;
        $this->priceRials = $priceRials;
        $this->createdAt  = time();
        $this->updatedAt  = time();
        $this->staffMembers = new ArrayCollection();
        $this->consumables  = new ArrayCollection();
    }

    /** @return Collection<int, ServiceItemConsumable> */
    public function getConsumables(): Collection
    {
        // Doctrine بدون constructor هیدریت می‌کند؛ از property تایپ‌شده محافظت کن.
        return $this->consumables ??= new ArrayCollection();
    }

    /**
     * جایگزینی کامل اقلام تکی. کلید تطبیق، خودِ InventoryItem است تا ردیف بدون تغییر
     * حذف و دوباره ساخته نشود.
     *
     * @param array<int, array{item: \App\Inventory\Entity\InventoryItem, amount: int}> $lines
     */
    public function replaceConsumables(array $lines): self
    {
        $existing = [];
        foreach ($this->getConsumables() as $consumable) {
            $existing[$consumable->getItem()->getId()] = $consumable;
        }

        $keep = [];
        foreach ($lines as $line) {
            $itemId = $line['item']->getId();
            $keep[] = $itemId;

            if (isset($existing[$itemId])) {
                $existing[$itemId]->setAmount($line['amount']);
                continue;
            }

            $this->getConsumables()->add((new ServiceItemConsumable($line['item'], $line['amount']))->setServiceItem($this));
        }

        foreach ($existing as $itemId => $consumable) {
            if (!in_array($itemId, $keep, true)) {
                $this->getConsumables()->removeElement($consumable);
            }
        }

        $this->updatedAt = time();

        return $this;
    }

    public function getId(): ?int              { return $this->id; }
    public function getUuid(): string          { return $this->uuid; }
    public function getSection(): ServiceSection { return $this->section; }

    public function getStaff(): ?ClinicStaff   { return $this->staff; }
    public function getName(): string          { return $this->name; }
    public function getPriceRials(): int       { return $this->priceRials; }
    public function isActive(): bool           { return $this->active; }
    public function isInsuranceCovered(): bool  { return $this->insuranceCovered; }
    public function getServiceCategory(): ServiceCategory { return $this->serviceCategory; }
    public function getDurationMinutes(): ?int { return $this->durationMinutes; }
    public function isBookable(): bool         { return $this->bookable; }
    public function getInventoryPackageId(): ?int { return $this->inventoryPackageId; }
    public function getCreatedAt(): int        { return $this->createdAt; }
    public function getUpdatedAt(): int        { return $this->updatedAt; }

    public function setStaff(?ClinicStaff $staff): self  { $this->staff = $staff; $this->updatedAt = time(); return $this; }

    /** @return Collection<int, ClinicStaff> */
    public function getStaffMembers(): Collection
    {
        // Doctrine hydrates without the constructor; guard the typed property.
        return $this->staffMembers ??= new ArrayCollection();
    }

    /**
     * Replace the assigned personnel. Also mirrors the first member into the
     * legacy single {@see $staff} column so back-compat consumers keep working.
     *
     * @param ClinicStaff[] $members
     */
    public function setStaffMembers(array $members): self
    {
        $collection = $this->getStaffMembers();
        $collection->clear();
        foreach ($members as $m) {
            if (!$collection->contains($m)) {
                $collection->add($m);
            }
        }
        $this->staff = $members[0] ?? null;
        $this->updatedAt = time();
        return $this;
    }
    public function setName(string $name): self           { $this->name = $name; $this->updatedAt = time(); return $this; }
    public function setPriceRials(int $price): self       { $this->priceRials = $price; $this->updatedAt = time(); return $this; }
    public function setActive(bool $active): self         { $this->active = $active; $this->updatedAt = time(); return $this; }
    public function setInsuranceCovered(bool $v): self    { $this->insuranceCovered = $v; $this->updatedAt = time(); return $this; }
    public function setServiceCategory(ServiceCategory $v): self { $this->serviceCategory = $v; $this->updatedAt = time(); return $this; }
    /** هر دو ستون را هم‌زمان می‌نویسد تا «مدت» یک منبع حقیقت داشته باشد. */
    public function setDurationMinutes(?int $v): self
    {
        $this->durationMinutes     = $v;
        $this->soloDurationMinutes = $v;
        $this->updatedAt           = time();

        return $this;
    }

    public function getSoloDurationMinutes(): ?int       { return $this->soloDurationMinutes ?? $this->durationMinutes; }
    public function getAdditionalDurationMinutes(): ?int { return $this->additionalDurationMinutes; }
    public function getSessionCount(): int               { return $this->sessionCount; }
    public function getCatalogCategory(): ?CatalogCategory { return $this->catalogCategory; }

    /**
     * مدتِ «کنار بقیه». آیتمی که مقدارش را نگذاشته، همان مدت تنها را می‌گیرد — پس
     * دادهٔ موجود دقیقاً مثل قبل حساب می‌شود و این تغییر افزایشی است.
     */
    public function effectiveAdditionalMinutes(): ?int
    {
        return $this->additionalDurationMinutes ?? $this->getSoloDurationMinutes();
    }

    public function setSoloDurationMinutes(?int $v): self
    {
        $this->soloDurationMinutes = $v;
        $this->durationMinutes     = $v;   // ستون قدیمی هم‌گام می‌ماند
        $this->updatedAt           = time();

        return $this;
    }

    public function setAdditionalDurationMinutes(?int $v): self
    {
        $this->additionalDurationMinutes = $v;
        $this->updatedAt = time();

        return $this;
    }

    /** @throws \InvalidArgumentException روی تعداد جلسهٔ کمتر از ۱ */
    public function setSessionCount(int $v): self
    {
        if ($v < 1) {
            throw new \InvalidArgumentException('session_count must be at least 1.');
        }

        $this->sessionCount = $v;
        $this->updatedAt    = time();

        return $this;
    }

    public function setCatalogCategory(?CatalogCategory $v): self
    {
        $this->catalogCategory = $v;
        $this->updatedAt       = time();

        return $this;
    }
    public function setBookable(bool $v): self { $this->bookable = $v; $this->updatedAt = time(); return $this; }
    public function setInventoryPackageId(?int $v): self { $this->inventoryPackageId = $v; $this->updatedAt = time(); return $this; }

    public function toArray(): array
    {
        // Prefer the multi-staff collection; fall back to the legacy single
        // staff so rows created before the migration still expose personnel.
        $members = array_values($this->getStaffMembers()->toArray());
        if (empty($members) && $this->staff !== null) {
            $members = [$this->staff];
        }
        $primary = $members[0] ?? null;

        return [
            'uuid'         => $this->uuid,
            'section_uuid' => $this->section->getUuid(),
            'section_name' => $this->section->getName(),
            'staff_uuid'   => $primary?->getUuid(),
            'staff_name'   => $primary?->getFullName(),
            'staff'        => $primary !== null
                ? ['uuid' => $primary->getUuid(), 'full_name' => $primary->getFullName()]
                : null,
            'staff_members' => array_map(
                fn(ClinicStaff $s) => ['uuid' => $s->getUuid(), 'full_name' => $s->getFullName()],
                $members
            ),
            'name'         => $this->name,
            'price_rials'  => $this->priceRials,
            'active'       => $this->active,
            'insurance_covered'     => $this->insuranceCovered,
            'service_category'      => $this->getServiceCategory()->value,
            'service_category_label' => $this->getServiceCategory()->label(),
            'duration_minutes'      => $this->durationMinutes,
            'solo_duration_minutes' => $this->getSoloDurationMinutes(),
            'additional_duration_minutes' => $this->effectiveAdditionalMinutes(),
            'session_count'         => $this->sessionCount,
            'catalog_category_uuid' => $this->catalogCategory?->getUuid(),
            'bookable'              => $this->bookable,
            'inventory_package_id'  => $this->inventoryPackageId,
            'consumables'           => array_map(
                fn(ServiceItemConsumable $c) => $c->toArray(),
                $this->getConsumables()->toArray()
            ),
            'created_at'   => $this->createdAt,
            'updated_at'   => $this->updatedAt,
        ];
    }
}
