<?php

declare(strict_types=1);

final class MyronPasswordResetService
{
    public function __construct(
        private readonly PDO $pdo,
        private readonly MyronAuthRepository $authRepository,
        private readonly MyronMailSettingsRepository $mailSettingsRepository,
        private readonly MyronMailer $mailer,
    ) {
    }

    public function requestReset(string $email): void
    {
        $user = $this->authRepository->findUserByEmail($email);
        if (
            !is_array($user)
            || (int) ($user['is_active'] ?? 0) !== 1
            || !$this->mailSettingsRepository->isConfigured()
        ) {
            return;
        }

        $this->expireOldTokens((string) $user['id']);

        $selector = myronBase64UrlEncode(random_bytes(9));
        $verifier = myronBase64UrlEncode(random_bytes(24));
        $token = $selector . '.' . $verifier;
        $tokenId = myronGenerateId('reset');
        $requestedAt = myronNow();
        $expiresAt = gmdate('Y-m-d H:i:s', time() + 3600);

        $statement = $this->pdo->prepare(
            'INSERT INTO password_reset_tokens (
                id, user_id, selector, verifier_hash, requested_at, expires_at, consumed_at
             ) VALUES (
                :id, :user_id, :selector, :verifier_hash, :requested_at, :expires_at, NULL
             )'
        );
        $statement->execute([
            ':id' => $tokenId,
            ':user_id' => (string) $user['id'],
            ':selector' => $selector,
            ':verifier_hash' => hash('sha256', $verifier),
            ':requested_at' => $requestedAt,
            ':expires_at' => $expiresAt,
        ]);

        $settings = $this->mailSettingsRepository->getSettings();
        if (!is_array($settings)) {
            return;
        }

        $resetUrl = myronBuildAbsoluteAdminUrl('auth.passwordReset', ['token' => $token]);
        $subject = 'MyronCMS password reset';
        $htmlBody = '<p>A password reset was requested for your MyronCMS account.</p>'
            . '<p><a href="' . myronEscape($resetUrl) . '">Reset your password</a></p>'
            . '<p>This link expires in 1 hour.</p>';
        $textBody = "A password reset was requested for your MyronCMS account.\n\nReset your password: {$resetUrl}\n\nThis link expires in 1 hour.";

        if (!function_exists('myronMailTransportRegistry')) {
            $this->mailer->send($settings, (string) $user['email'], $subject, $htmlBody, $textBody);
            return;
        }

        myronMailTransportRegistry()->activeTransport()->send(new MailEnvelope(
            to: (string) $user['email'],
            subject: $subject,
            htmlBody: $htmlBody,
            textBody: $textBody,
            fromEmail: (string) ($settings['from_email'] ?? ''),
            fromName: (string) ($settings['from_name'] ?? ''),
        ));
    }

    public function resetPassword(string $token, string $newPassword): void
    {
        [$record, $user] = $this->requireActiveToken($token);

        $this->authRepository->updatePassword((string) $user['id'], $newPassword);
        $statement = $this->pdo->prepare(
            'UPDATE password_reset_tokens
             SET consumed_at = :consumed_at
             WHERE id = :id'
        );
        $statement->execute([
            ':consumed_at' => myronNow(),
            ':id' => (string) $record['id'],
        ]);
    }

    public function validateToken(string $token): ?array
    {
        try {
            [$record, $user] = $this->requireActiveToken($token);
            return [
                'token' => $record,
                'user' => $user,
            ];
        } catch (Throwable) {
            return null;
        }
    }

    private function expireOldTokens(string $userId): void
    {
        $statement = $this->pdo->prepare(
            'UPDATE password_reset_tokens
             SET consumed_at = :consumed_at
             WHERE user_id = :user_id
               AND consumed_at IS NULL'
        );
        $statement->execute([
            ':consumed_at' => myronNow(),
            ':user_id' => $userId,
        ]);
    }

    /**
     * @return array{0: array<string, mixed>, 1: array<string, mixed>}
     */
    private function requireActiveToken(string $token): array
    {
        $parts = explode('.', trim($token), 2);
        if (count($parts) !== 2 || trim($parts[0]) === '' || trim($parts[1]) === '') {
            throw new InvalidArgumentException('That password reset link is invalid.');
        }

        [$selector, $verifier] = $parts;
        $statement = $this->pdo->prepare(
            'SELECT * FROM password_reset_tokens WHERE selector = :selector LIMIT 1'
        );
        $statement->execute([':selector' => $selector]);
        $record = $statement->fetch();
        if (!is_array($record)) {
            throw new InvalidArgumentException('That password reset link is invalid.');
        }

        if ((string) ($record['consumed_at'] ?? '') !== '') {
            throw new InvalidArgumentException('That password reset link has already been used.');
        }

        if (strtotime((string) ($record['expires_at'] ?? '')) < time()) {
            throw new InvalidArgumentException('That password reset link has expired.');
        }

        if (!hash_equals((string) ($record['verifier_hash'] ?? ''), hash('sha256', $verifier))) {
            throw new InvalidArgumentException('That password reset link is invalid.');
        }

        $user = $this->authRepository->findUserById((string) $record['user_id']);
        if (!is_array($user)) {
            throw new InvalidArgumentException('That password reset link is invalid.');
        }

        return [$record, $user];
    }
}
