<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Persistence\InMemory;

use App\Administration\Domain\Exception\PasswordResetTokenNotFoundException;
use App\Administration\Domain\Model\PasswordResetToken\PasswordResetToken;
use App\Administration\Domain\Model\PasswordResetToken\PasswordResetTokenId;
use App\Administration\Domain\Repository\PasswordResetTokenRepository;
use App\Shared\Domain\Clock;
use DateTimeImmutable;
use Override;

final class InMemoryPasswordResetTokenRepository implements PasswordResetTokenRepository
{
    /** @var array<string, PasswordResetToken> Indexed by token value */
    private array $byTokenValue = [];

    /** @var array<string, string> Maps ID to token value */
    private array $idToTokenValue = [];

    /** @var array<string, string> Maps user ID to token value */
    private array $userIdToTokenValue = [];

    public function __construct(
        private ?Clock $clock = null,
    ) {
    }

    #[Override]
    public function save(PasswordResetToken $token): void
    {
        $this->byTokenValue[$token->tokenValue] = $token;
        $this->idToTokenValue[(string) $token->id] = $token->tokenValue;
        $this->userIdToTokenValue[$token->userId] = $token->tokenValue;
    }

    #[Override]
    public function findByTokenValue(string $tokenValue): ?PasswordResetToken
    {
        return $this->byTokenValue[$tokenValue] ?? null;
    }

    #[Override]
    public function getByTokenValue(string $tokenValue): PasswordResetToken
    {
        $token = $this->findByTokenValue($tokenValue);

        if ($token === null) {
            throw PasswordResetTokenNotFoundException::withTokenValue($tokenValue);
        }

        return $token;
    }

    #[Override]
    public function get(PasswordResetTokenId $id): PasswordResetToken
    {
        $tokenValue = $this->idToTokenValue[(string) $id] ?? null;

        if ($tokenValue === null) {
            throw PasswordResetTokenNotFoundException::withId($id);
        }

        $token = $this->byTokenValue[$tokenValue] ?? null;

        if ($token === null) {
            throw PasswordResetTokenNotFoundException::withId($id);
        }

        return $token;
    }

    #[Override]
    public function delete(PasswordResetTokenId $id): void
    {
        $tokenValue = $this->idToTokenValue[(string) $id] ?? null;

        if ($tokenValue !== null) {
            $token = $this->byTokenValue[$tokenValue] ?? null;

            if ($token !== null) {
                unset($this->userIdToTokenValue[$token->userId]);
            }

            unset($this->byTokenValue[$tokenValue]);
        }

        unset($this->idToTokenValue[(string) $id]);
    }

    #[Override]
    public function deleteByTokenValue(string $tokenValue): void
    {
        $token = $this->byTokenValue[$tokenValue] ?? null;

        if ($token !== null) {
            unset($this->idToTokenValue[(string) $token->id]);
            unset($this->userIdToTokenValue[$token->userId]);
        }

        unset($this->byTokenValue[$tokenValue]);
    }

    #[Override]
    public function findValidTokenForUser(string $userId): ?PasswordResetToken
    {
        $tokenValue = $this->userIdToTokenValue[$userId] ?? null;

        if ($tokenValue === null) {
            return null;
        }

        $token = $this->byTokenValue[$tokenValue] ?? null;

        if ($token === null) {
            return null;
        }

        // Check if token is still valid (not used and not expired)
        $now = $this->clock?->now() ?? new DateTimeImmutable();
        if ($token->isUsed() || $token->isExpired($now)) {
            return null;
        }

        return $token;
    }

    #[Override]
    public function consumeIfValid(string $tokenValue, DateTimeImmutable $at): PasswordResetToken
    {
        $token = $this->getByTokenValue($tokenValue);
        $token->validateForUse($at);
        $token->use($at);
        $this->save($token);

        return $token;
    }
}
