<?php

namespace App\Shared\Event\Entity;

use App\Shared\Event\Repository\DomainEventLogRepository;
use App\Shared\Tenant\TenantOwnedTrait;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;

/**
 * صندوق خروجی رویدادهای دامنه (outbox).
 *
 * ردیف رویداد **در همان تراکنشی** نوشته می‌شود که خودِ تغییر را انجام می‌دهد، و یک
 * worker بعداً منتشرش می‌کند. بدون این الگو دو حالت شکست ممکن است:
 *
 * | حالت | نتیجه |
 * |---|---|
 * | انتشار پیش از commit، بعد rollback | پیامک رفته، نوبتی وجود ندارد |
 * | commit موفق، انتشار شکست خورد | نوبت هست، هیچ‌کس مطلع نشد |
 *
 * با outbox حداکثر **تأخیر** داریم، هرگز گم‌شدن.
 *
 * این جدول با `AppointmentEvent` موجود اشتباه نشود: آن تاریخچهٔ وضعیت یک نوبت است،
 * این اعلان تغییر به بیرونِ دامنه.
 */
#[ORM\Entity(repositoryClass: DomainEventLogRepository::class)]
#[ORM\Table(name: 'domain_events')]
#[ORM\Index(columns: ['published_at', 'occurred_at'], name: 'idx_de_pending')]
#[ORM\Index(columns: ['entity_type', 'entity_id', 'occurred_at'], name: 'idx_de_tenant')]
#[ORM\Index(columns: ['name', 'occurred_at'], name: 'idx_de_name')]
class DomainEventLog
{
    use TenantOwnedTrait;

    /** سقف تلاش — ردیف مرده با خطایش می‌ماند تا دیده شود، حذف نمی‌شود. */
    public const MAX_ATTEMPTS = 5;

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

    /** شناسهٔ idempotency برای مصرف‌کننده. */
    #[ORM\Column(type: 'string', length: 36, unique: true)]
    private string $uuid;

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

    /** @var array<string, scalar|null> */
    #[ORM\Column(type: 'json')]
    private array $payload;

    /** زمان **وقوع**، نه انتشار. */
    #[ORM\Column(name: 'occurred_at', type: 'integer')]
    private int $occurredAt;

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

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

    #[ORM\Column(name: 'last_error', type: 'string', length: 255, nullable: true)]
    private ?string $lastError = null;

    /** @param array<string, mixed> $payload */
    public function __construct(string $entityType, int $entityId, string $name, array $payload, ?int $occurredAt = null)
    {
        $this->uuid       = Uuid::v4()->toRfc4122();
        $this->name       = $name;
        $this->payload    = self::scalarsOnly($payload);
        $this->occurredAt = $occurredAt ?? time();

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

    /**
     * هیچ entity ای در رویداد نیست — فقط uuid و اسکالر.
     *
     * entity در پیام async یعنی سریال‌سازی، detach شدن، و دادهٔ کهنه؛ مصرف‌کننده باید
     * خودش با uuid واکشی کند تا همیشه تازه‌ترین حالت را ببیند.
     *
     * @param array<string, mixed> $payload
     * @return array<string, scalar|null>
     */
    private static function scalarsOnly(array $payload): array
    {
        return array_filter($payload, static fn (mixed $v): bool => is_scalar($v) || $v === null);
    }

    public function getId(): ?string        { return $this->id; }
    public function getUuid(): string       { return $this->uuid; }
    public function getName(): string       { return $this->name; }
    public function getPayload(): array     { return $this->payload; }
    public function getOccurredAt(): int    { return $this->occurredAt; }
    public function getPublishedAt(): ?int  { return $this->publishedAt; }
    public function getAttempts(): int      { return $this->attempts; }
    public function getLastError(): ?string { return $this->lastError; }
    public function isPublished(): bool     { return $this->publishedAt !== null; }

    public function markPublished(?int $at = null): self
    {
        $this->publishedAt = $at ?? time();
        $this->lastError   = null;

        return $this;
    }

    public function markFailed(string $error): self
    {
        $this->attempts++;
        $this->lastError = mb_substr($error, 0, 255);

        return $this;
    }

    /** @return array<string, mixed> */
    public function toArray(): array
    {
        return [
            'uuid'         => $this->uuid,
            'name'         => $this->name,
            'payload'      => (object) $this->payload,
            'occurred_at'  => $this->occurredAt,
            'published_at' => $this->publishedAt,
            'attempts'     => $this->attempts,
            'last_error'   => $this->lastError,
        ];
    }
}
