<?php

declare(strict_types=1);

/*
 * This file is part of the VITICORE2 package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CpCreation\VitiCore\Commerce\Catalog\Model;

use CpCreation\VitiCore\Commerce\Catalog\Repository\CatalogChangeRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;

#[ORM\Entity(repositoryClass: CatalogChangeRepository::class)]
#[ORM\Table(name: 'commerce_catalog_change')]
#[ORM\UniqueConstraint(name: 'commerce_catalog_change_event_id_uniq', columns: ['event_id'])]
#[ORM\Index(name: 'commerce_catalog_change_delivery_idx', columns: ['delivery_status', 'last_queued_at'])]
#[ORM\Index(name: 'commerce_catalog_change_occurred_at_idx', columns: ['occurred_at'])]
class CatalogChange
{
    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: 'IDENTITY')]
    #[ORM\Column(type: Types::BIGINT)]
    private ?int $cursor = null;

    #[ORM\Column(type: 'uuid')]
    private UuidInterface $eventId;

    #[ORM\Column(type: 'uuid')]
    private UuidInterface $instanceId;

    #[ORM\Column(length: 64, enumType: CatalogChangeType::class)]
    private CatalogChangeType $type;

    #[ORM\Column(length: 32, enumType: CatalogResourceType::class)]
    private CatalogResourceType $resourceType;

    #[ORM\Column(length: 255, nullable: true)]
    private ?string $resourceId;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
    private \DateTimeImmutable $occurredAt;

    #[ORM\Column(length: 32, enumType: CatalogDeliveryStatus::class)]
    private CatalogDeliveryStatus $deliveryStatus = CatalogDeliveryStatus::PENDING;

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

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
    private ?\DateTimeImmutable $lastQueuedAt = null;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
    private ?\DateTimeImmutable $lastAttemptAt = null;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
    private ?\DateTimeImmutable $deliveredAt = null;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
    private ?\DateTimeImmutable $permanentlyFailedAt = null;

    #[ORM\Column(type: Types::TEXT, nullable: true)]
    private ?string $lastError = null;

    public function __construct(
        UuidInterface|string $instanceId,
        CatalogChangeType $type,
        CatalogResourceType $resourceType,
        ?string $resourceId,
        ?\DateTimeImmutable $occurredAt = null,
        ?UuidInterface $eventId = null,
    ) {
        $this->eventId = $eventId ?? Uuid::uuid7();
        $this->instanceId = $instanceId instanceof UuidInterface ? $instanceId : Uuid::fromString($instanceId);
        $this->type = $type;
        $this->resourceType = $resourceType;
        $this->resourceId = $resourceId;
        $this->occurredAt = $occurredAt ?? new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
    }

    public function getCursor(): int
    {
        return $this->cursor ?? throw new \LogicException('The catalog change has not been persisted yet.');
    }

    public function getEventId(): UuidInterface
    {
        return $this->eventId;
    }

    public function getInstanceId(): UuidInterface
    {
        return $this->instanceId;
    }

    public function getType(): CatalogChangeType
    {
        return $this->type;
    }

    public function getResourceType(): CatalogResourceType
    {
        return $this->resourceType;
    }

    public function getResourceId(): ?string
    {
        return $this->resourceId;
    }

    public function getOccurredAt(): \DateTimeImmutable
    {
        return $this->occurredAt;
    }

    public function getDeliveryStatus(): CatalogDeliveryStatus
    {
        return $this->deliveryStatus;
    }

    public function getDeliveryAttempts(): int
    {
        return $this->deliveryAttempts;
    }

    public function getLastQueuedAt(): ?\DateTimeImmutable
    {
        return $this->lastQueuedAt;
    }

    public function getLastAttemptAt(): ?\DateTimeImmutable
    {
        return $this->lastAttemptAt;
    }

    public function getDeliveredAt(): ?\DateTimeImmutable
    {
        return $this->deliveredAt;
    }

    public function getPermanentlyFailedAt(): ?\DateTimeImmutable
    {
        return $this->permanentlyFailedAt;
    }

    public function getLastError(): ?string
    {
        return $this->lastError;
    }

    public function isDeliveryComplete(): bool
    {
        return \in_array($this->deliveryStatus, [CatalogDeliveryStatus::DELIVERED, CatalogDeliveryStatus::FAILED_PERMANENT], true);
    }

    public function markQueued(\DateTimeImmutable $queuedAt): void
    {
        if ($this->isDeliveryComplete()) {
            return;
        }

        $this->deliveryStatus = CatalogDeliveryStatus::QUEUED;
        $this->lastQueuedAt = $queuedAt;
    }

    public function markDelivered(\DateTimeImmutable $deliveredAt): void
    {
        if ($this->isDeliveryComplete()) {
            return;
        }

        ++$this->deliveryAttempts;
        $this->lastAttemptAt = $deliveredAt;
        $this->deliveredAt = $deliveredAt;
        $this->deliveryStatus = CatalogDeliveryStatus::DELIVERED;
        $this->lastError = null;
    }

    public function markRetryableFailure(\DateTimeImmutable $attemptedAt, string $error): void
    {
        if ($this->isDeliveryComplete()) {
            return;
        }

        ++$this->deliveryAttempts;
        $this->lastAttemptAt = $attemptedAt;
        $this->deliveryStatus = CatalogDeliveryStatus::QUEUED;
        $this->lastError = $error;
    }

    public function markPermanentFailure(\DateTimeImmutable $attemptedAt, string $error): void
    {
        if ($this->isDeliveryComplete()) {
            return;
        }

        ++$this->deliveryAttempts;
        $this->lastAttemptAt = $attemptedAt;
        $this->permanentlyFailedAt = $attemptedAt;
        $this->deliveryStatus = CatalogDeliveryStatus::FAILED_PERMANENT;
        $this->lastError = $error;
    }

    public function markRetriesExhausted(\DateTimeImmutable $failedAt, string $error): void
    {
        if ($this->isDeliveryComplete()) {
            return;
        }

        $this->permanentlyFailedAt = $failedAt;
        $this->deliveryStatus = CatalogDeliveryStatus::FAILED_PERMANENT;
        $this->lastError = $error;
    }
}
