<?php

declare(strict_types=1);

namespace apexl\Io\modules\user\services\twoFactorAuth;

use apexl\Config\ConfigurationInterface;
use apexl\Io\dto\LoaderCondition;
use apexl\Io\modules\user\entities\TwoFactorRecoveryCodeEntity;
use DateTimeImmutable;
use Random\RandomException;

/**
 * Generates, stores (hashed), verifies and burns one-time recovery codes
 * (Plan 007 §4). Codes are shown to the user once at enrolment; only their
 * password_hash is persisted, so a DB read can never recover a usable code.
 */
final readonly class RecoveryCodeService
{
    private const int GROUP_LENGTH = 5;
    private const int GROUPS = 2;

    public function __construct(private ConfigurationInterface $config) {}

    /**
     * Replace a user's recovery codes with a fresh set and return the plaintext
     * codes for one-time display. Any prior codes are deleted.
     *
     * @return string[] plaintext codes (caller must show once, then discard)
     * @throws RandomException
     */
    public function regenerateForUser(int $userId): array
    {
        $this->deleteForUser($userId);

        $count = max(1, (int) $this->config->get('auth.two_factor.recovery_code_count'));
        $now = new DateTimeImmutable();
        $plainCodes = [];

        for ($i = 0; $i < $count; $i++) {
            $code = $this->generateCode();
            $plainCodes[] = $code;

            $entity = new TwoFactorRecoveryCodeEntity();
            $entity->user_id = $userId;
            // Hash the NORMALISED form so a user re-typing with or without the
            // dash / different case still verifies (consume() normalises too).
            $entity->code_hash = password_hash($this->normalise($code), PASSWORD_DEFAULT);
            $entity->used_at = null;
            $entity->created = $now;
            $entity->modified = $now;
            $entity->store();
        }

        return $plainCodes;
    }

    /**
     * Verify a supplied recovery code for a user and, on a match, burn it
     * (single-use). Returns true only when an as-yet-unused code matched.
     */
    public function consume(int $userId, string $suppliedCode): bool
    {
        $suppliedCode = $this->normalise($suppliedCode);
        if ($suppliedCode === '') {
            return false;
        }

        foreach ($this->unusedForUser($userId) as $codeEntity) {
            if (password_verify($suppliedCode, (string) $codeEntity->code_hash)) {
                $codeEntity->used_at = new DateTimeImmutable();
                $codeEntity->modified = new DateTimeImmutable();
                $codeEntity->store();

                return true;
            }
        }

        return false;
    }

    public function deleteForUser(int $userId): void
    {
        foreach ($this->forUser($userId) as $codeEntity) {
            $codeEntity->delete();
        }
    }

    /**
     * @return TwoFactorRecoveryCodeEntity[]
     */
    private function unusedForUser(int $userId): array
    {
        return array_values(array_filter(
            $this->forUser($userId),
            static fn(TwoFactorRecoveryCodeEntity $code): bool => !$code->isUsed(),
        ));
    }

    /**
     * @return TwoFactorRecoveryCodeEntity[]
     */
    private function forUser(int $userId): array
    {
        return TwoFactorRecoveryCodeEntity::findMultiple(
            [LoaderCondition::equals('user_id', $userId)],
            limit: 200,
        )->toArray();
    }

    /**
     * @throws RandomException
     */
    private function generateCode(): string
    {
        $alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Crockford-ish, no ambiguous chars
        $groups = [];
        for ($g = 0; $g < self::GROUPS; $g++) {
            $chars = '';
            for ($c = 0; $c < self::GROUP_LENGTH; $c++) {
                $chars .= $alphabet[random_int(0, strlen($alphabet) - 1)];
            }
            $groups[] = $chars;
        }

        return implode('-', $groups);
    }

    /** Normalise for comparison: strip spaces/dashes, uppercase. */
    private function normalise(string $code): string
    {
        return strtoupper(preg_replace('/[\s-]+/', '', $code) ?? '');
    }
}
