<?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\Behavior\HasTimestamp;
use CpCreation\VitiCore\Behavior\Impl\Timestamp;
use CpCreation\VitiCore\Commerce\Catalog\Configuration\CatalogSecretCipher;
use CpCreation\VitiCore\Commerce\Catalog\Repository\CatalogConfigurationRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

#[ORM\Entity(repositoryClass: CatalogConfigurationRepository::class)]
#[ORM\Table(name: 'commerce_catalog_configuration')]
#[ORM\HasLifecycleCallbacks]
class CatalogConfiguration implements HasTimestamp
{
    use Timestamp;

    public const string SINGLETON_ID = 'default';
    public const string DEFAULT_CURRENCY = 'EUR';
    public const string DEFAULT_AUTH_AUDIENCE = 'serenity:catalog';

    #[ORM\Id]
    #[ORM\Column(length: 32)]
    private string $id = self::SINGLETON_ID;

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

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

    #[Assert\Length(max: 2048)]
    #[Assert\Url(protocols: ['http', 'https'])]
    #[ORM\Column(type: Types::TEXT, nullable: true)]
    private ?string $hubUrl = null;

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

    #[ORM\Column(length: 3, options: ['default' => self::DEFAULT_CURRENCY])]
    private string $currency = self::DEFAULT_CURRENCY;

    #[ORM\Column(length: 255, options: ['default' => self::DEFAULT_AUTH_AUDIENCE])]
    private string $authAudience = self::DEFAULT_AUTH_AUDIENCE;

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

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

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

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

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

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

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

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

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

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

    #[ORM\Column(length: 32, enumType: CatalogConfigurationStatus::class, options: ['default' => 'disabled'])]
    private CatalogConfigurationStatus $status = CatalogConfigurationStatus::DISABLED;

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

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

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

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

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

    public function __construct(?UuidInterface $instanceId = null)
    {
        $this->instanceId = $instanceId ?? Uuid::uuid7();
    }

    public function getId(): string
    {
        return $this->id;
    }

    public function isEnabled(): bool
    {
        return $this->enabled;
    }

    public function setEnabled(bool $enabled): void
    {
        $this->enabled = $enabled;
    }

    public function isMonitoringEnabled(): bool
    {
        return $this->monitoringEnabled;
    }

    public function setMonitoringEnabled(bool $monitoringEnabled): void
    {
        $this->monitoringEnabled = $monitoringEnabled;
    }

    public function isHubConnectionEnabled(): bool
    {
        return $this->enabled || $this->monitoringEnabled;
    }

    public function getHubUrl(): ?string
    {
        return $this->hubUrl;
    }

    public function setHubUrl(?string $hubUrl): void
    {
        $hubUrl = null === $hubUrl ? null : mb_rtrim(mb_trim($hubUrl), '/');
        $this->hubUrl = '' === $hubUrl ? null : $hubUrl;
    }

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

    public function getCurrency(): string
    {
        return $this->currency;
    }

    public function getAuthAudience(): string
    {
        return $this->authAudience;
    }

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

    public function getEnrollmentTokenJti(): ?string
    {
        return $this->enrollmentTokenJti;
    }

    public function getJwtPublicKeyPem(): ?string
    {
        return $this->jwtPublicKeyPem;
    }

    public function getJwtIssuer(): ?string
    {
        return $this->jwtIssuer;
    }

    public function getJwtKeyId(): ?string
    {
        return $this->jwtKeyId;
    }

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

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

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

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

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

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

    public function hasUsableEnrollmentToken(?\DateTimeImmutable $now = null): bool
    {
        $now ??= new \DateTimeImmutable('now', new \DateTimeZone('UTC'));

        return null !== $this->enrollmentTokenCiphertext
            && null !== $this->enrollmentTokenExpiresAt
            && $this->enrollmentTokenExpiresAt > $now->modify('+1 minute');
    }

    public function resetConnection(): void
    {
        $this->enrollmentTokenCiphertext = null;
        $this->enrollmentTokenExpiresAt = null;
        $this->enrollmentTokenJti = null;
        $this->jwtPublicKeyPem = null;
        $this->jwtIssuer = null;
        $this->jwtKeyId = null;
        $this->sourceId = null;
        $this->webhookUrl = null;
        // Un autre hub ne doit jamais heriter du secret HMAC connu du precedent.
        $this->webhookSecretCiphertext = null;
        $this->enrolledAt = null;
        $this->lastError = null;
        $this->clearAuthorization();
        $this->status = $this->isHubConnectionEnabled()
            ? CatalogConfigurationStatus::PENDING_AUTHORIZATION
            : CatalogConfigurationStatus::DISABLED;
    }

    public function prepareActivation(CatalogSecretCipher $cipher): void
    {
        $this->enabled = true;
        $this->prepareHubConnection($cipher);
    }

    public function prepareHubConnection(CatalogSecretCipher $cipher): void
    {
        if (null === $this->hubUrl) {
            throw new \LogicException('L’URL du hub Commerce doit être configurée avant son activation.');
        }

        if (!$this->isHubConnectionEnabled()) {
            throw new \LogicException('Au moins une intégration du hub doit être activée.');
        }

        $this->lastError = null;
        if (null === $this->webhookSecretCiphertext) {
            $this->webhookSecretCiphertext = $cipher->encrypt(
                self::base64UrlEncode(random_bytes(32)),
                CatalogSecretCipher::PURPOSE_WEBHOOK_SECRET,
            );
        }

        $this->status = null !== $this->enrollmentTokenCiphertext
            ? CatalogConfigurationStatus::PENDING_ENROLLMENT
            : CatalogConfigurationStatus::PENDING_AUTHORIZATION;
    }

    public function disable(): void
    {
        $this->enabled = false;
        if (!$this->isHubConnectionEnabled()) {
            $this->disableHubConnection();
        }
    }

    public function disableHubConnection(): void
    {
        $this->enabled = false;
        $this->monitoringEnabled = false;
        $this->status = CatalogConfigurationStatus::DISABLED;
        $this->lastError = null;
        $this->clearAuthorization();
    }

    public function revealEnrollmentToken(CatalogSecretCipher $cipher): ?string
    {
        if (null === $this->enrollmentTokenCiphertext) {
            return null;
        }

        return $cipher->decrypt(
            $this->enrollmentTokenCiphertext,
            CatalogSecretCipher::PURPOSE_ENROLLMENT_TOKEN,
        );
    }

    public function revealWebhookSecret(CatalogSecretCipher $cipher): ?string
    {
        if (null === $this->webhookSecretCiphertext) {
            return null;
        }

        return $cipher->decrypt(
            $this->webhookSecretCiphertext,
            CatalogSecretCipher::PURPOSE_WEBHOOK_SECRET,
        );
    }

    public function applyHubAuthorization(
        #[\SensitiveParameter]
        string $enrollmentToken,
        \DateTimeImmutable $expiresAt,
        string $jti,
        string $publicKeyPem,
        string $issuer,
        string $keyId,
        CatalogSecretCipher $cipher,
    ): void {
        $jti = self::requiredString($jti, 'L’identifiant du jeton d’enrôlement', 255);
        $this->enrollmentTokenCiphertext = $cipher->encrypt(
            self::requiredString($enrollmentToken, 'Le jeton d’enrôlement', 16_384),
            CatalogSecretCipher::PURPOSE_ENROLLMENT_TOKEN,
        );
        $this->enrollmentTokenExpiresAt = $expiresAt;
        $this->enrollmentTokenJti = $jti;
        $this->applyHubVerificationMetadata($publicKeyPem, $issuer, $keyId);
        $this->status = CatalogConfigurationStatus::PENDING_ENROLLMENT;
        $this->lastError = null;
        $this->clearAuthorization();
    }

    public function applyHubVerificationMetadata(
        string $publicKeyPem,
        string $issuer,
        string $keyId,
    ): void {
        $this->jwtPublicKeyPem = self::requiredString($publicKeyPem, 'La clé publique JWT du hub', 16_384);
        $this->jwtIssuer = self::requiredString($issuer, 'L’émetteur JWT du hub', 255);
        $this->jwtKeyId = self::requiredString($keyId, 'L’identifiant de clé JWT du hub', 128);
    }

    public function markEnrolled(
        string $sourceId,
        string $webhookUrl,
        ?\DateTimeImmutable $enrolledAt = null,
    ): void {
        $sourceId = self::requiredString($sourceId, 'L’identifiant source du hub', 255);
        $webhookUrl = self::requiredString($webhookUrl, 'L’URL de webhook du hub', 2048);
        $this->sourceId = $sourceId;
        $this->webhookUrl = $webhookUrl;
        $this->enrolledAt = $enrolledAt ?? new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
        $this->status = CatalogConfigurationStatus::ENROLLED;
        $this->lastError = null;
    }

    public function markError(string $error): void
    {
        $error = mb_trim($error);
        $this->lastError = '' === $error ? null : mb_substr($error, 0, 4000);
        $this->status = CatalogConfigurationStatus::ERROR;
    }

    public function beginAuthorization(
        #[\SensitiveParameter]
        string $state,
        #[\SensitiveParameter]
        string $pkceVerifier,
        \DateTimeImmutable $expiresAt,
        CatalogSecretCipher $cipher,
    ): void {
        if ('' === $state || mb_strlen($state, '8bit') > 2048) {
            throw new \InvalidArgumentException('L’état d’autorisation Commerce est invalide.');
        }
        if (1 !== preg_match('/^[A-Za-z0-9._~-]{43,128}$/D', $pkceVerifier)) {
            throw new \InvalidArgumentException('Le vérificateur PKCE Commerce est invalide.');
        }

        $this->authorizationStateHash = hash('sha256', $state);
        $this->authorizationPkceVerifierCiphertext = $cipher->encrypt(
            $pkceVerifier,
            CatalogSecretCipher::PURPOSE_AUTHORIZATION_PKCE_VERIFIER,
        );
        $this->authorizationExpiresAt = $expiresAt;
        $this->status = CatalogConfigurationStatus::PENDING_AUTHORIZATION;
        $this->lastError = null;
    }

    public function matchesAuthorizationState(
        #[\SensitiveParameter]
        string $state,
        ?\DateTimeImmutable $now = null,
    ): bool {
        $now ??= new \DateTimeImmutable('now', new \DateTimeZone('UTC'));

        return null !== $this->authorizationStateHash
            && null !== $this->authorizationExpiresAt
            && $this->authorizationExpiresAt > $now
            && hash_equals($this->authorizationStateHash, hash('sha256', $state));
    }

    public function revealAuthorizationPkceVerifier(
        CatalogSecretCipher $cipher,
        ?\DateTimeImmutable $now = null,
    ): ?string {
        $now ??= new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
        if (null === $this->authorizationPkceVerifierCiphertext
            || null === $this->authorizationExpiresAt
            || $this->authorizationExpiresAt <= $now
        ) {
            return null;
        }

        return $cipher->decrypt(
            $this->authorizationPkceVerifierCiphertext,
            CatalogSecretCipher::PURPOSE_AUTHORIZATION_PKCE_VERIFIER,
        );
    }

    public function clearAuthorization(): void
    {
        $this->authorizationStateHash = null;
        $this->authorizationPkceVerifierCiphertext = null;
        $this->authorizationExpiresAt = null;
    }

    #[Assert\Callback]
    public function validateConfiguration(ExecutionContextInterface $context): void
    {
        if ($this->isHubConnectionEnabled() && null === $this->hubUrl) {
            $context->buildViolation('commerce_catalog.admin.hub_url_required')
                ->atPath('hubUrl')
                ->addViolation();
        }
    }

    private static function requiredString(string $value, string $label, int $maximumLength): string
    {
        $value = mb_trim($value);
        if ('' === $value || mb_strlen($value, '8bit') > $maximumLength) {
            throw new \InvalidArgumentException($label . ' est invalide.');
        }

        return $value;
    }

    private static function base64UrlEncode(string $value): string
    {
        return mb_rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
    }
}
