<?php

declare(strict_types=1);

namespace App\Administration\Domain\Model\PasswordResetToken;

use App\Administration\Domain\Event\PasswordResetTokenGenerated;
use App\Administration\Domain\Event\PasswordResetTokenUsed;
use App\Administration\Domain\Exception\PasswordResetTokenAlreadyUsedException;
use App\Administration\Domain\Exception\PasswordResetTokenExpiredException;
use App\Shared\Domain\AggregateRoot;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Ramsey\Uuid\Uuid;

use function sprintf;

final class PasswordResetToken extends AggregateRoot
{
    private const int EXPIRATION_HOURS = 1;

    public private(set) ?DateTimeImmutable $usedAt = null;

    private function __construct(
        public private(set) PasswordResetTokenId $id,
        public private(set) string $tokenValue,
        public private(set) string $userId,
        public private(set) string $email,
        public private(set) TenantId $tenantId,
        public private(set) DateTimeImmutable $createdAt,
        public private(set) DateTimeImmutable $expiresAt,
    ) {
    }

    public static function generate(
        string $userId,
        string $email,
        TenantId $tenantId,
        DateTimeImmutable $createdAt,
    ): self {
        $token = new self(
            id: PasswordResetTokenId::generate(),
            tokenValue: Uuid::uuid7()->toString(),
            userId: $userId,
            email: $email,
            tenantId: $tenantId,
            createdAt: $createdAt,
            expiresAt: $createdAt->modify(sprintf('+%d hour', self::EXPIRATION_HOURS)),
        );

        $token->recordEvent(new PasswordResetTokenGenerated(
            tokenId: $token->id,
            tokenValue: $token->tokenValue,
            userId: $userId,
            email: $email,
            tenantId: $tenantId,
            occurredOn: $createdAt,
        ));

        return $token;
    }

    /**
     * Reconstitute a PasswordResetToken from storage.
     * Does NOT record domain events (this is not a new creation).
     *
     * @internal For use by Infrastructure layer only
     */
    public static function reconstitute(
        PasswordResetTokenId $id,
        string $tokenValue,
        string $userId,
        string $email,
        TenantId $tenantId,
        DateTimeImmutable $createdAt,
        DateTimeImmutable $expiresAt,
        ?DateTimeImmutable $usedAt,
    ): self {
        $token = new self(
            id: $id,
            tokenValue: $tokenValue,
            userId: $userId,
            email: $email,
            tenantId: $tenantId,
            createdAt: $createdAt,
            expiresAt: $expiresAt,
        );

        $token->usedAt = $usedAt;

        return $token;
    }

    public function isExpired(DateTimeImmutable $at): bool
    {
        return $at >= $this->expiresAt;
    }

    public function isUsed(): bool
    {
        return $this->usedAt !== null;
    }

    /**
     * Validate that the token can be used (not expired, not already used).
     * Does NOT mark the token as used - use use() for that after successful password reset.
     *
     * @throws PasswordResetTokenAlreadyUsedException if token was already used
     * @throws PasswordResetTokenExpiredException     if token is expired
     */
    public function validateForUse(DateTimeImmutable $at): void
    {
        if ($this->isUsed()) {
            throw PasswordResetTokenAlreadyUsedException::forToken($this->id);
        }

        if ($this->isExpired($at)) {
            throw PasswordResetTokenExpiredException::forToken($this->id);
        }
    }

    /**
     * Mark the token as used. Should only be called after successful password reset.
     *
     * @throws PasswordResetTokenAlreadyUsedException if token was already used
     * @throws PasswordResetTokenExpiredException     if token is expired
     */
    public function use(DateTimeImmutable $at): void
    {
        $this->validateForUse($at);

        $this->usedAt = $at;

        $this->recordEvent(new PasswordResetTokenUsed(
            tokenId: $this->id,
            userId: $this->userId,
            occurredOn: $at,
        ));
    }
}
