<?php

/*
 * This file is part of the VITICORE2 package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CpCreation\VitiCore\Bridge\Symfony\Bundle\Form\Site;

use CpCreation\VitiCore\Bridge\Symfony\Bundle\Notification\Command\NotificationCommand;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Notification\Enum\PushLevel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;

final class AltChaType extends AbstractType
{
    public function __construct(
        private readonly RequestStack $requestStack,
        private readonly string $altchaSecret,
        private readonly ?string $altchaVerify,
        private readonly ?string $altchaChallenge,
        private readonly string $altchaTest,
        private readonly TranslatorInterface $translator,
        private readonly MessageBusInterface $commandSimpleBus,
    ) {
    }

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event): void {
            $request = $this->requestStack->getCurrentRequest() ?? $this->requestStack->getMainRequest();
            $form = $event->getForm();

            if ($this->shouldSkipValidationForLiveAction($request?->attributes->get('_live_action'))) {
                return;
            }

            if (!$request) {
                $this->addAltchaError($form);

                return;
            }

            $submittedPayload = $event->getData();
            if (!\is_string($submittedPayload) || '' === $submittedPayload) {
                $submittedPayload = $request->request->get('altcha');
            }

            $form->setData($submittedPayload);

            if (!\is_string($submittedPayload) || '' === $submittedPayload) {
                $this->addAltchaError($form);

                return;
            }

            $decodedPayload = base64_decode($submittedPayload, true);
            $payload = false !== $decodedPayload ? json_decode($decodedPayload, true) : null;

            if (!\is_array($payload) || empty($payload['algorithm']) || empty($payload['signature'])) {
                $this->addAltchaError($form);

                return;
            }

            if (isset($payload['verified']) && !$payload['verified']) {
                $this->addAltchaError($form);

                return;
            }

            $payload['algorithm'] = str_replace('-', '', strtolower((string) $payload['algorithm']));
            if (!\in_array($payload['algorithm'], hash_algos(), true)) {
                $this->addAltchaError($form);

                return;
            }

            if (($payload['test'] ?? false) === true) {
                return;
            }

            if ($this->altchaVerify) {
                if ($this->verifyWithGateCha($submittedPayload)) {
                    return;
                }

                $this->addAltchaError($form);

                return;
            }

            if (
                isset($payload['challenge'], $payload['salt'], $payload['number'])
                && $this->equalChallenge($payload['algorithm'], (string) $payload['challenge'], (string) $payload['salt'].$payload['number'])
                && $this->equalSignature($payload['algorithm'], (string) $payload['signature'], (string) $payload['challenge'])
            ) {
                return;
            }

            if (empty($payload['verificationData'])) {
                $this->addAltchaError($form);

                return;
            }

            parse_str((string) $payload['verificationData'], $verificationData);

            if (!isset($verificationData['score'], $verificationData['fields'], $verificationData['fieldsHash'], $verificationData['expire'])) {
                $this->addAltchaError($form);

                return;
            }

            if ((int) $verificationData['score'] > 5) {
                $this->addAltchaError($form, 'form.error-altcha-score');

                return;
            }

            if (!$this->equalFields($payload['algorithm'], $verificationData, $form)) {
                $this->addAltchaError($form, 'form.error-altcha-field-change');

                return;
            }

            if ((new \DateTimeImmutable())->setTimestamp((int) $verificationData['expire']) < new \DateTimeImmutable()) {
                $this->addAltchaError($form, 'form.error-altcha-field-invalidate');

                return;
            }

            if (isset($payload['verificationData']) && $this->equalSignature($payload['algorithm'], (string) $payload['signature'], hash($payload['algorithm'], (string) $payload['verificationData'], true))) {
                return;
            }

            $this->addAltchaError($form);
        });
    }

    public function buildView(FormView $view, FormInterface $form, array $options): void
    {
        $attr = $view->vars['attr'] ?? [];
        $attr['data-controller'] = $options['captcha_spam_filter-controller-ux'];

        if (\is_string($this->altchaChallenge) && '' !== $this->altchaChallenge) {
            $attr['data-captcha-spam-filter-challenge-url-value'] = $this->altchaChallenge;
        }

        $attr['data-captcha-spam-filter-test-mode-value'] = '1' === $this->altchaTest ? 'true' : 'false';

        $view->vars['attr'] = $attr;
        $view->vars['captcha_spam_filter'] = $options['captcha_spam_filter'];
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'mapped' => false,
            'required' => false,
            'captcha_spam_filter' => true,
            'captcha_spam_filter-controller-ux' => 'captcha-spam-filter',
        ]);
    }

    public function getParent(): string
    {
        return HiddenType::class;
    }

    private function addAltchaError(FormInterface $form, string $messageKey = 'form.error-altcha-verified'): void
    {
        $message = $this->translator->trans($messageKey, [], 'validators');
        $this->commandSimpleBus->dispatch(new NotificationCommand(
            message: $message,
            level: PushLevel::ERROR,
        ));

        $form->addError(new FormError($message));
    }

    private function shouldSkipValidationForLiveAction(mixed $liveAction): bool
    {
        if (!\is_string($liveAction) || '' === $liveAction) {
            return false;
        }

        return !preg_match('/^(submit|save)/i', $liveAction);
    }

    private function equalSignature(string $algorithm, string $signaturePayload, string $verificationData): bool
    {
        return hash_equals($signaturePayload, hash_hmac($algorithm, $verificationData, $this->altchaSecret));
    }

    private function equalChallenge(string $algorithm, string $challengePayload, string $verificationData): bool
    {
        return hash_equals($challengePayload, hash($algorithm, $verificationData));
    }

    private function equalFields(string $algorithm, array $verificationData, FormInterface $form): bool
    {
        $fields = explode(',', (string) $verificationData['fields']);
        $values = $this->getFieldValueHash($fields, $form->getParent()?->all() ?? []);

        return hash_equals((string) $verificationData['fieldsHash'], hash($algorithm, implode("\n", array_values($values))));
    }

    private function verifyWithGateCha(string $payload): bool
    {
        try {
            $ch = curl_init();
            curl_setopt($ch, \CURLOPT_URL, $this->altchaVerify);
            curl_setopt($ch, \CURLOPT_POST, 1);
            curl_setopt($ch, \CURLOPT_POSTFIELDS, json_encode(['payload' => $payload], \JSON_THROW_ON_ERROR));
            curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
            curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, \CURLOPT_TIMEOUT, 5);

            $response = curl_exec($ch);
            $statusCode = (int) curl_getinfo($ch, \CURLINFO_HTTP_CODE);
            curl_close($ch);

            if (!\is_string($response) || $statusCode < 200 || $statusCode >= 300) {
                return false;
            }

            $data = json_decode($response, true);
            if (!\is_array($data)) {
                return false;
            }

            if (($data['verified'] ?? false) === true) {
                return true;
            }

            return ($data['ok'] ?? false) === true;
        } catch (\Throwable) {
            return false;
        }
    }

    /**
     * @param list<string> $fields
     * @param array<string, FormInterface> $formFields
     *
     * @return array<string, mixed>
     */
    private function getFieldValueHash(array $fields, array $formFields): array
    {
        $result = [];

        foreach ($fields as $field) {
            preg_match_all('/\[([^]]+)]/', $field, $matches);
            $dynamicKeys = $matches[1];
            $current = $formFields;

            foreach ($dynamicKeys as $dynamicKey) {
                if (isset($current[$dynamicKey])) {
                    $current = $current[$dynamicKey];
                } else {
                    $current = null;

                    break;
                }
            }

            if ($current instanceof Form) {
                $result[$field] = $current->getData();
            }
        }

        return $result;
    }
}
