<?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\Enrollment\Model;

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

#[ORM\Entity(repositoryClass: CatalogEnrollmentRequestRepository::class)]
#[ORM\Table(name: 'commerce_catalog_enrollment_request')]
#[ORM\UniqueConstraint(name: 'commerce_catalog_enrollment_request_id_uniq', columns: ['request_id'])]
#[ORM\Index(name: 'commerce_catalog_enrollment_delivery_idx', columns: ['status', 'last_queued_at'])]
#[ORM\Index(name: 'commerce_catalog_enrollment_fingerprint_idx', columns: ['configuration_fingerprint'])]
class CatalogEnrollmentRequest
{
    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: 'IDENTITY')]
    #[ORM\Column(type: Types::BIGINT)]
    private ?int $id = null;

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

    #[ORM\Column(type: 'uuid', nullable: true)]
    private ?UuidInterface $instanceId = null;

    #[ORM\Column(length: 64, nullable: true)]
    private ?string $configurationFingerprint = null;

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

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

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

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

    #[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 $registeredAt = null;

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

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

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

    #[ORM\Column(length: 64, nullable: true)]
    private ?string $hubStatus = null;

    /**
     * @var array<string, mixed>|null
     */
    #[ORM\Column(type: Types::JSON, nullable: true)]
    private ?array $lastResponse = null;

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

    public function __construct(
        ?\DateTimeImmutable $createdAt = null,
        UuidInterface|string|null $requestId = null,
    ) {
        $this->requestId = match (true) {
            $requestId instanceof UuidInterface => $requestId,
            \is_string($requestId) => Uuid::fromString($requestId),
            default => Uuid::uuid7(),
        };
        $this->createdAt = $createdAt ?? new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
    }

    public static function prepared(
        UuidInterface|string $instanceId,
        string $configurationFingerprint,
        ?\DateTimeImmutable $createdAt = null,
        UuidInterface|string|null $requestId = null,
    ): self {
        $request = new self($createdAt, $requestId);
        $request->prepare($instanceId, $configurationFingerprint);

        return $request;
    }

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

    public function getRequestId(): UuidInterface
    {
        return $this->requestId;
    }

    public function getInstanceId(): UuidInterface
    {
        return $this->instanceId ?? throw new \LogicException('The catalog enrollment request has not been prepared yet.');
    }

    public function getConfigurationFingerprint(): string
    {
        return $this->configurationFingerprint ?? throw new \LogicException('The catalog enrollment request has not been prepared yet.');
    }

    public function isPrepared(): bool
    {
        return $this->instanceId instanceof UuidInterface && null !== $this->configurationFingerprint;
    }

    public function prepare(UuidInterface|string $instanceId, string $configurationFingerprint): void
    {
        if ($this->isPrepared()) {
            return;
        }
        if (1 !== preg_match('/^[a-f0-9]{64}$/', $configurationFingerprint)) {
            throw new \InvalidArgumentException('The catalog enrollment fingerprint must be a lowercase SHA-256 hash.');
        }

        $this->instanceId = $instanceId instanceof UuidInterface ? $instanceId : Uuid::fromString($instanceId);
        $this->configurationFingerprint = $configurationFingerprint;
    }

    public function getStatus(): CatalogEnrollmentStatus
    {
        return $this->status;
    }

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

    public function getCreatedAt(): \DateTimeImmutable
    {
        return $this->createdAt;
    }

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

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

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

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

    public function getSourceId(): ?string
    {
        return $this->sourceId;
    }

    public function getWebhookUrl(): ?string
    {
        return $this->webhookUrl;
    }

    public function getHubStatus(): ?string
    {
        return $this->hubStatus;
    }

    /**
     * @return array<string, mixed>|null
     */
    public function getLastResponse(): ?array
    {
        return $this->lastResponse;
    }

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

    public function isDeliveryComplete(): bool
    {
        return \in_array($this->status, [CatalogEnrollmentStatus::REGISTERED, CatalogEnrollmentStatus::FAILED_PERMANENT], true);
    }

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

        $this->status = CatalogEnrollmentStatus::QUEUED;
        $this->lastQueuedAt = $queuedAt;
    }

    /**
     * @param array<string, mixed> $response
     */
    public function markRegistered(
        \DateTimeImmutable $registeredAt,
        string $sourceId,
        string $webhookUrl,
        string $hubStatus,
        array $response,
    ): void {
        if ($this->isDeliveryComplete()) {
            return;
        }

        ++$this->deliveryAttempts;
        $this->lastAttemptAt = $registeredAt;
        $this->registeredAt = $registeredAt;
        $this->status = CatalogEnrollmentStatus::REGISTERED;
        $this->sourceId = $sourceId;
        $this->webhookUrl = $webhookUrl;
        $this->hubStatus = $hubStatus;
        $this->lastResponse = $response;
        $this->lastError = null;
    }

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

        ++$this->deliveryAttempts;
        $this->lastAttemptAt = $attemptedAt;
        $this->status = CatalogEnrollmentStatus::QUEUED;
        $this->lastError = $error;
    }

    /**
     * @param array<string, mixed>|null $response
     */
    public function markPermanentFailure(
        \DateTimeImmutable $attemptedAt,
        string $error,
        ?array $response = null,
    ): void {
        if ($this->isDeliveryComplete()) {
            return;
        }

        ++$this->deliveryAttempts;
        $this->lastAttemptAt = $attemptedAt;
        $this->permanentlyFailedAt = $attemptedAt;
        $this->status = CatalogEnrollmentStatus::FAILED_PERMANENT;
        $this->lastError = $error;
        $this->lastResponse = $response;
    }

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

        $this->lastAttemptAt = $failedAt;
        $this->status = CatalogEnrollmentStatus::PENDING;
        $this->lastError = $error;
    }
}
