<?php

namespace App\Rules;

use AltchaOrg\Altcha\Algorithm\Pbkdf2;
use AltchaOrg\Altcha\Altcha;
use AltchaOrg\Altcha\Challenge;
use AltchaOrg\Altcha\ChallengeParameters;
use AltchaOrg\Altcha\Payload;
use AltchaOrg\Altcha\Solution;
use AltchaOrg\Altcha\VerifySolutionOptions;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Translation\PotentiallyTranslatedString;

/**
 * Validation rule to verify captcha solution.
 */
class ValidCaptcha implements ValidationRule
{
    /**
     * Altcha instance.
     */
    protected Altcha $altcha;

    /**
     * Pbkdf2 algorithm instance.
     */
    protected Pbkdf2 $pbkdf2;

    /**
     * Class constructor.
     */
    public function __construct()
    {
        $this->pbkdf2 = new Pbkdf2;
        $this->altcha = new Altcha(
            hmacSignatureSecret: config('captcha.hmac_key'),
        );
    }

    /**
     * Parse payload and return the decoded data as an array.
     */
    private function parsePayload(string $value): ?array
    {
        $decoded = base64_decode($value, true);
        if ($decoded === false) {
            return null;
        }

        $payload = json_decode($decoded, true);
        if (! is_array($payload)) {
            return null;
        }

        return $payload;
    }

    /**
     * Verify if payload has required fields.
     */
    private function verifyFields(array $payload): bool
    {
        if (! isset($payload['challenge'], $payload['solution'])) {
            return false;
        }

        if (! is_array($payload['challenge']) || ! is_array($payload['solution'])) {
            return false;
        }

        return true;
    }

    /**
     * Create a Challenge object from challenge data.
     */
    private function createChallenge(array $challengeData): Challenge
    {
        return new Challenge(
            ChallengeParameters::fromArray($challengeData['parameters'] ?? []),
            $challengeData['signature'] ?? null,
        );
    }

    /**
     * Create a Solution object from solution data.
     */
    private function createSolution(array $solutionData): Solution
    {
        return new Solution(
            counter: (int) ($solutionData['counter'] ?? 0),
            derivedKey: (string) ($solutionData['derivedKey'] ?? ''),
        );
    }

    /**
     * Run the validation rule.
     *
     * @param  Closure(string, ?string=): PotentiallyTranslatedString  $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $payload = $this->parsePayload($value);
        if (! $payload) {
            $fail('Invalid captcha.');

            return;
        }

        if (! $this->verifyFields($payload)) {
            $fail('Invalid captcha.');

            return;
        }

        $challenge = $this->createChallenge($payload['challenge']);
        $solution = $this->createSolution($payload['solution']);

        $result = $this->altcha->verifySolution(new VerifySolutionOptions(
            algorithm: $this->pbkdf2,
            payload: new Payload($challenge, $solution),
        ));

        if (! $result->verified) {
            $fail('Invalid captcha.');
        }
    }
}
