<?php

declare(strict_types=1);

namespace App\Administration\Domain\Model\ActivationToken;

use App\Administration\Domain\Event\ActivationTokenGenerated;
use App\Administration\Domain\Event\ActivationTokenUsed;
use App\Administration\Domain\Exception\ActivationTokenAlreadyUsedException;
use App\Administration\Domain\Exception\ActivationTokenExpiredException;
use App\Shared\Domain\AggregateRoot;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Ramsey\Uuid\Uuid;

use function sprintf;

final class ActivationToken extends AggregateRoot
{
    private const int EXPIRATION_DAYS = 7;

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

    private function __construct(
        public private(set) ActivationTokenId $id,
        public private(set) string $tokenValue,
        public private(set) string $userId,
        public private(set) string $email,
        public private(set) TenantId $tenantId,
        public private(set) string $role,
        public private(set) string $schoolName,
        public private(set) DateTimeImmutable $createdAt,
        public private(set) DateTimeImmutable $expiresAt,
        public private(set) ?string $studentId = null,
        public private(set) ?string $relationshipType = null,
    ) {
    }

    public static function generate(
        string $userId,
        string $email,
        TenantId $tenantId,
        string $role,
        string $schoolName,
        DateTimeImmutable $createdAt,
        ?string $studentId = null,
        ?string $relationshipType = null,
    ): self {
        $token = new self(
            id: ActivationTokenId::generate(),
            tokenValue: Uuid::uuid7()->toString(),
            userId: $userId,
            email: $email,
            tenantId: $tenantId,
            role: $role,
            schoolName: $schoolName,
            createdAt: $createdAt,
            expiresAt: $createdAt->modify(sprintf('+%d days', self::EXPIRATION_DAYS)),
            studentId: $studentId,
            relationshipType: $relationshipType,
        );

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

        return $token;
    }

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

        $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 activation.
     *
     * @throws ActivationTokenAlreadyUsedException if token was already used
     * @throws ActivationTokenExpiredException     if token is expired
     */
    public function validateForUse(DateTimeImmutable $at): void
    {
        if ($this->isUsed()) {
            throw ActivationTokenAlreadyUsedException::forToken($this->id);
        }

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

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

        $this->usedAt = $at;

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