<?php

namespace App\Waitlist\Entity;

use App\Appointment\Entity\Appointment;
use App\ClinicService\Entity\ServiceItem;
use App\Doctor\Entity\DoctorAddress;
use App\Patient\Entity\PatientRecord;
use App\Shared\Tenant\TenantOwnedTrait;
use App\Waitlist\Repository\WaitlistEntryRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;

/**
 * «اگر وقتی در این بازه آزاد شد، خبرم کن.»
 *
 * توسعهٔ همان ایدهٔ `Appointment.is_reserve` موجود، ولی با بازهٔ صریح و وضعیت — تا
 * بشود گفت چه کسی، برای چه، در چه بازه‌ای منتظر است.
 */
#[ORM\Entity(repositoryClass: WaitlistEntryRepository::class)]
#[ORM\Table(name: 'waitlist_entries')]
#[ORM\Index(columns: ['service_item_id', 'branch_id', 'status', 'desired_from', 'desired_to'], name: 'idx_waitlist_match')]
#[ORM\Index(columns: ['entity_type', 'entity_id', 'status', 'created_at'], name: 'idx_waitlist_tenant')]
#[ORM\Index(columns: ['patient_record_id', 'status'], name: 'idx_waitlist_patient')]
class WaitlistEntry
{
    use TenantOwnedTrait;

    public const STATUS_WAITING   = 'waiting';
    public const STATUS_NOTIFIED  = 'notified';
    public const STATUS_CONVERTED = 'converted';
    public const STATUS_EXPIRED   = 'expired';

    /** سقف اطلاع‌رسانی — بدون آن، یک بازهٔ پرلغو به منبع اسپم تبدیل می‌شود. */
    public const MAX_NOTIFICATIONS = 3;

    /**
     * بخش‌های روز — فهرست **بسته**، با مرز ساعت محلیِ شعبه.
     *
     * مرزها اینجاست نه در UI: «عصر» باید در تطبیق و در نمایش یک معنا داشته باشد،
     * وگرنه بیمار برای ساعتی خبر می‌شود که خودش رد کرده بود. `[start, end)` است تا
     * ساعت ۱۲ دقیقاً یک‌بار شمرده شود.
     *
     * @var array<string, array{label: string, from: int, to: int}>
     */
    public const DAY_PARTS = [
        'morning'   => ['label' => 'صبح',       'from' => 6,  'to' => 12],
        'afternoon' => ['label' => 'بعدازظهر',  'from' => 12, 'to' => 17],
        'evening'   => ['label' => 'عصر',       'from' => 17, 'to' => 22],
    ];

    #[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: PatientRecord::class)]
    #[ORM\JoinColumn(name: 'patient_record_id', nullable: false, onDelete: 'CASCADE')]
    private PatientRecord $patientRecord;

    #[ORM\ManyToOne(targetEntity: ServiceItem::class)]
    #[ORM\JoinColumn(name: 'service_item_id', nullable: false, onDelete: 'CASCADE')]
    private ServiceItem $serviceItem;

    #[ORM\Column(name: 'branch_id', type: 'integer', nullable: true)]
    private ?int $branchId = null;

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

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

    /** @var list<string>|null `["morning","evening"]` */
    #[ORM\Column(name: 'preferred_day_parts', type: 'json', nullable: true)]
    private ?array $preferredDayParts = null;

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

    #[ORM\Column(type: 'string', length: 12, options: ['default' => self::STATUS_WAITING])]
    private string $status = self::STATUS_WAITING;

    #[ORM\Column(name: 'notified_at', type: 'integer', nullable: true)]
    private ?int $notifiedAt = null;

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

    #[ORM\ManyToOne(targetEntity: Appointment::class)]
    #[ORM\JoinColumn(name: 'converted_appointment_id', nullable: true, onDelete: 'SET NULL')]
    private ?Appointment $convertedAppointment = null;

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

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

    public function __construct(
        PatientRecord $patientRecord,
        ServiceItem $serviceItem,
        int $desiredFrom,
        int $desiredTo,
        ?int $branchId = null,
    ) {
        if ($desiredTo <= $desiredFrom) {
            throw new \InvalidArgumentException('The waitlist window must end after it starts.');
        }

        $this->uuid          = Uuid::v4()->toRfc4122();
        $this->patientRecord = $patientRecord;
        $this->serviceItem   = $serviceItem;
        $this->desiredFrom   = $desiredFrom;
        $this->desiredTo     = $desiredTo;
        $this->branchId      = $branchId;
        $this->createdAt     = time();
        $this->updatedAt     = time();

        $this->assignTenantPair($patientRecord->getEntityType(), $patientRecord->getEntityId());
    }

    public function getId(): ?int                     { return $this->id; }
    public function getUuid(): string                 { return $this->uuid; }
    public function getPatientRecord(): PatientRecord { return $this->patientRecord; }
    public function getServiceItem(): ServiceItem     { return $this->serviceItem; }
    public function getBranchId(): ?int               { return $this->branchId; }
    public function getDesiredFrom(): int             { return $this->desiredFrom; }
    public function getDesiredTo(): int               { return $this->desiredTo; }
    public function getPreferredDayParts(): array     { return $this->preferredDayParts ?? []; }
    public function getPriority(): int                { return $this->priority; }
    public function getStatus(): string               { return $this->status; }
    public function getNotifiedAt(): ?int             { return $this->notifiedAt; }
    public function getNotifyCount(): int             { return $this->notifyCount; }

    /**
     * @param list<string> $parts
     * @throws \InvalidArgumentException روی بخشی که در فهرست بسته نیست
     */
    public function setPreferredDayParts(array $parts): self
    {
        $clean = array_values(array_unique(array_filter($parts, 'is_string')));

        foreach ($clean as $part) {
            if (!isset(self::DAY_PARTS[$part])) {
                throw new \InvalidArgumentException(sprintf('Unknown day part "%s".', $part));
            }
        }

        $this->preferredDayParts = $clean === [] ? null : $clean;

        return $this->touch();
    }

    public function setPriority(int $v): self { $this->priority = $v; return $this->touch(); }

    public function markNotified(?int $at = null): self
    {
        $this->status      = self::STATUS_NOTIFIED;
        $this->notifiedAt  = $at ?? time();
        $this->notifyCount++;

        return $this->touch();
    }

    public function markConverted(Appointment $appointment): self
    {
        $this->status               = self::STATUS_CONVERTED;
        $this->convertedAppointment = $appointment;

        return $this->touch();
    }

    public function markExpired(): self
    {
        $this->status = self::STATUS_EXPIRED;

        return $this->touch();
    }

    /** هنوز منتظر است و سقف اطلاع‌رسانی را رد نکرده. */
    public function isNotifiable(?int $now = null): bool
    {
        $now = $now ?? time();

        return in_array($this->status, [self::STATUS_WAITING, self::STATUS_NOTIFIED], true)
            && $this->notifyCount < self::MAX_NOTIFICATIONS
            && $this->desiredTo >= $now;
    }

    public function covers(int $start): bool
    {
        return $start >= $this->desiredFrom && $start <= $this->desiredTo;
    }

    /**
     * آیا این زمان در یکی از بخش‌های روزِ خواسته‌شده می‌افتد؟
     *
     * نداشتنِ ترجیح یعنی «هر ساعتی» — نه «هیچ ساعتی». ساعت به وقت **محلی شعبه**
     * حساب می‌شود، چون بیمار «عصر» را با ساعت خودش می‌فهمد نه با UTC.
     */
    public function coversDayPart(int $start, string $timezone = DoctorAddress::DEFAULT_TIMEZONE): bool
    {
        if ($this->preferredDayParts === null || $this->preferredDayParts === []) {
            return true;
        }

        $hour = (int) (new \DateTimeImmutable('@' . $start))
            ->setTimezone(new \DateTimeZone($timezone))
            ->format('G');

        foreach ($this->preferredDayParts as $part) {
            $range = self::DAY_PARTS[$part] ?? null;

            if ($range !== null && $hour >= $range['from'] && $hour < $range['to']) {
                return true;
            }
        }

        return false;
    }

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

        return $this;
    }

    /** @return array<string, mixed> */
    public function toArray(): array
    {
        return [
            'uuid'                => $this->uuid,
            'patient_uuid'        => $this->patientRecord->getUuid(),
            'service_uuid'        => $this->serviceItem->getUuid(),
            'service_name'        => $this->serviceItem->getName(),
            'branch_id'           => $this->branchId,
            'desired_from'        => $this->desiredFrom,
            'desired_to'          => $this->desiredTo,
            'preferred_day_parts' => $this->preferredDayParts ?? [],
            'priority'            => $this->priority,
            'status'              => $this->status,
            'notified_at'         => $this->notifiedAt,
            'notify_count'        => $this->notifyCount,
            'created_at'          => $this->createdAt,
        ];
    }
}
