<?php

namespace App\Policy\Entity;

use App\ClinicService\Entity\CatalogCategory;
use App\ClinicService\Entity\ServiceItem;
use App\Doctor\Entity\DoctorAddress;
use App\Policy\Repository\PolicyRepository;
use App\Shared\Tenant\TenantOwnedTrait;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;

/**
 * یک قانون از شش دستهٔ بند ۸ مستند.
 *
 * سه چیز عمداً **نیست**: کد دلخواه، شرط آزاد، و ویرایش درجا.
 *
 * - شرط فقط از فهرست بستهٔ {@see \App\Policy\Service\PolicySchema} می‌آید. قانونی که
 *   بتواند هر عبارتی را ارزیابی کند، دیگر قابل تحلیل ایستا نیست و دستهٔ `spacing`
 *   هرگز به کوئری تبدیل نمی‌شود.
 * - قانون **ویرایش نمی‌شود، نسخه می‌گیرد**. نوبتی که دیروز ثبت شده باید همان نسخه‌ای
 *   را که رویش اعمال شده نگه دارد (قانون پنجم مستند).
 */
#[ORM\Entity(repositoryClass: PolicyRepository::class)]
#[ORM\Table(name: 'policies')]
#[ORM\Index(columns: ['entity_type', 'entity_id', 'category', 'active'], name: 'idx_policy_tenant_category')]
#[ORM\Index(columns: ['valid_from', 'valid_to'], name: 'idx_policy_validity')]
class Policy
{
    use TenantOwnedTrait;

    public const CATEGORY_SELECTION   = 'selection';
    public const CATEGORY_ELIGIBILITY = 'eligibility';
    public const CATEGORY_RESOURCE    = 'resource';
    public const CATEGORY_TIMING      = 'timing';
    public const CATEGORY_SPACING     = 'spacing';
    public const CATEGORY_PRICING     = 'pricing';

    public const CATEGORIES = [
        self::CATEGORY_SELECTION,
        self::CATEGORY_ELIGIBILITY,
        self::CATEGORY_RESOURCE,
        self::CATEGORY_TIMING,
        self::CATEGORY_SPACING,
        self::CATEGORY_PRICING,
    ];

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

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

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

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

    /**
     * نام ستون `condition` نیست چون در MariaDB کلمهٔ کلیدی است و هر INSERT را
     * می‌شکند؛ نام فیلد در API همان `condition` می‌ماند.
     *
     * @var array{match?: string, conditions?: list<array<string, mixed>>}
     */
    #[ORM\Column(name: 'condition_json', type: 'json')]
    private array $condition = [];

    /** @var list<array<string, mixed>> */
    #[ORM\Column(type: 'json')]
    private array $effects = [];

    /** بزرگ‌تر یعنی مهم‌تر. اولین معیار حل تناقض. */
    #[ORM\Column(type: 'smallint', options: ['default' => 0])]
    private int $priority = 0;

    /** دومین معیار حل تناقض — هنگام **ذخیره** حساب می‌شود، نه در هر رزرو. */
    #[ORM\Column(type: 'smallint', options: ['default' => 0])]
    private int $specificity = 0;

    // ── دامنه: هرچه باریک‌تر، در تساویِ اولویت برنده‌تر ──────────────────────
    #[ORM\ManyToOne(targetEntity: DoctorAddress::class)]
    #[ORM\JoinColumn(name: 'address_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
    private ?DoctorAddress $address = null;

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

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

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

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

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

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

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

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

    public function __construct(string $entityType, int $entityId, string $category, string $name)
    {
        if (!in_array($category, self::CATEGORIES, true)) {
            throw new \InvalidArgumentException(sprintf('Unknown policy category "%s".', $category));
        }

        $this->uuid      = Uuid::v4()->toRfc4122();
        $this->category  = $category;
        $this->name      = $name;
        $this->createdAt = time();
        $this->recomputeSpecificity();
        $this->updatedAt = time();

        $this->assignTenantPair($entityType, $entityId);
    }

    public function getId(): ?int         { return $this->id; }
    public function getUuid(): string     { return $this->uuid; }
    public function getCategory(): string { return $this->category; }
    public function getName(): string     { return $this->name; }
    public function getCondition(): array { return $this->condition; }
    public function getEffects(): array   { return $this->effects; }
    public function getPriority(): int    { return $this->priority; }
    public function getVersion(): int     { return $this->version; }
    public function isActive(): bool      { return $this->active; }
    public function getValidFrom(): ?int  { return $this->validFrom; }
    public function getValidTo(): ?int    { return $this->validTo; }
    public function getCreatedAt(): int   { return $this->createdAt; }

    public function getAddress(): ?DoctorAddress          { return $this->address; }
    public function getServiceItem(): ?ServiceItem        { return $this->serviceItem; }
    public function getCatalogCategory(): ?CatalogCategory { return $this->catalogCategory; }

    public function setName(string $v): self       { $this->name = $v; $this->touch(); return $this; }
    public function setPriority(int $v): self      { $this->priority = $v; $this->touch(); return $this; }
    public function setActive(bool $v): self       { $this->active = $v; $this->touch(); return $this; }
    public function setAddress(?DoctorAddress $v): self { $this->address = $v; $this->touch(); return $this; }
    public function setServiceItem(?ServiceItem $v): self { $this->serviceItem = $v; $this->touch(); return $this; }
    public function setCatalogCategory(?CatalogCategory $v): self { $this->catalogCategory = $v; $this->touch(); return $this; }

    /** @param array<string, mixed> $condition */
    public function setCondition(array $condition): self { $this->condition = $condition; $this->touch(); return $this; }

    /** @param list<array<string, mixed>> $effects */
    public function setEffects(array $effects): self { $this->effects = $effects; $this->touch(); return $this; }

    public function setValidity(?int $from, ?int $to): self
    {
        if ($from !== null && $to !== null && $to <= $from) {
            throw new \InvalidArgumentException('Policy validity end must be after its start.');
        }

        $this->validFrom = $from;
        $this->validTo   = $to;
        $this->touch();

        return $this;
    }

    public function bumpVersion(): self { $this->version++; $this->touch(); return $this; }

    public function appliesAt(int $at): bool
    {
        return $this->active
            && ($this->validFrom === null || $at >= $this->validFrom)
            && ($this->validTo === null || $at < $this->validTo);
    }

    /**
     * هرچه باریک‌تر، بزرگ‌تر. در تساویِ اولویت، اختصاصی‌تر برنده است — «این سرویس» باید
     * بتواند «همهٔ سرویس‌ها» را کنار بزند، وگرنه استثنا غیرقابل بیان می‌شود.
     *
     * وزن‌ها از مستند: شعبه ۸ · سرویس ۴ · دسته ۲ · هر شرط اضافه ۱. شرط‌ها هم می‌شمارند
     * چون قانونی با سه شرط از قانونِ بی‌قید باریک‌تر است، حتی اگر دامنه‌شان یکی باشد.
     */
    public function specificity(): int
    {
        return $this->specificity;
    }

    /**
     * محاسبه **هنگام ذخیره**، نه هنگام اجرا.
     *
     * حل تناقض در هر رزرو روی همین عدد `usort` می‌زند؛ محاسبهٔ دوباره‌اش per قانون per
     * درخواست یعنی کاری که یک بار در عمر قانون کافی بود، هزار بار در روز انجام شود. ضمناً
     * ذخیره‌شدنش یعنی می‌شود روزی مرتب‌سازی را به SQL برد.
     */
    public function recomputeSpecificity(): self
    {
        $this->specificity = ($this->address !== null ? 8 : 0)
            + ($this->serviceItem !== null ? 4 : 0)
            + ($this->catalogCategory !== null ? 2 : 0)
            + count($this->condition['conditions'] ?? []);

        return $this;
    }

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

    public function toArray(): array
    {
        return [
            'uuid'                  => $this->uuid,
            'category'              => $this->category,
            'name'                  => $this->name,
            'condition'             => (object) $this->condition,
            'effects'               => $this->effects,
            'priority'              => $this->priority,
            'version'               => $this->version,
            'active'                => $this->active,
            'valid_from'            => $this->validFrom,
            'valid_to'              => $this->validTo,
            'address_uuid'          => $this->address?->getUuid(),
            'service_uuid'          => $this->serviceItem?->getUuid(),
            'catalog_category_uuid' => $this->catalogCategory?->getUuid(),
            'specificity'           => $this->specificity(),
        ];
    }
}
