<?php

defined('ABSPATH') || exit;


/**
 * Class MC4WP_Procaptcha
 *
 * @private
 */
class MC4WP_Procaptcha
{
    private const SCRIPT_URL      = 'https://js.prosopo.io/js/procaptcha.bundle.js';
    private const FORM_FIELD_NAME = 'procaptcha-response';
    private const API_URL         = 'https://api.prosopo.io/siteverify';

    /**
     * @var MC4WP_Procaptcha
     */
    private static $instance;

    /**
     * @var bool
     */
    private $is_in_use;
    /**
     * @var bool
     */
    private $is_enabled;
    /**
     * @var bool
     */
    private $is_displayed_for_authorized;
    /**
     * @var string
     */
    private $site_key;
    /**
     * @var string
     */
    private $secret_key;
    /**
     * @var string
     */
    private $theme;
    /**
     * @var string
     */
    private $type;

    private function __construct()
    {
        $this->is_in_use                   = false;
        $this->is_enabled                  = false;
        $this->is_displayed_for_authorized = false;
        $this->site_key                    = '';
        $this->secret_key                  = '';
        $this->theme                       = '';
        $this->type                        = '';

        $this->read_settings();
    }

    /**
     * @return void
     */
    protected function read_settings()
    {
        $integrations = get_option('mc4wp_integrations', []);
        if (
            false === is_array($integrations) ||
            false === key_exists('prosopo-procaptcha', $integrations) ||
            false === is_array($integrations['prosopo-procaptcha'])
        ) {
            return;
        }

        $settings = $integrations['prosopo-procaptcha'];

        $this->is_enabled                  = true === key_exists('enabled', $settings) &&
            '1' === $settings['enabled'];
        $this->is_displayed_for_authorized = true === key_exists('display_for_authorized', $settings) &&
            '1' === $settings['display_for_authorized'];
        $this->site_key                    = true === key_exists('site_key', $settings) &&
        true === is_string($settings['site_key']) ?
            $settings['site_key'] :
            '';
        $this->secret_key                  = true === key_exists('secret_key', $settings) &&
        true === is_string($settings['secret_key']) ?
            $settings['secret_key'] :
            '';
        $this->theme                       = true === key_exists('theme', $settings) &&
        true === is_string($settings['theme']) ?
            $settings['theme'] :
            '';
        $this->type                        = true === key_exists('type', $settings) &&
        true === is_string($settings['type']) ?
            $settings['type'] :
            '';
    }

    /**
     * @return MC4WP_Procaptcha
     */
    public static function get_instance()
    {
        if (null === self::$instance) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    /**
     * @return void
     */
    protected function print_captcha_js()
    {
        $attributes = [
            'siteKey' => $this->site_key,
            'theme' => $this->theme,
            'captchaType' => $this->type,
        ];
        ?>
        <script data-name="prosopo-procaptcha-element" type="module">
            let attributes = <?php echo json_encode($attributes); ?>;

            class MC4WPProcaptcha extends HTMLElement {
                constructor() {
                    super();

                    this.isValid = false;
                    this.validationErrorElement = null;
                }

                connectedCallback() {
                    // wait window.load to make sure 'window.procaptcha' is available.
                    "complete" !== document.readyState ?
                        window.addEventListener("load", this.setup.bind(this)) :
                        this.setup();
                }


                validatedCallback(output) {
                    this.isValid = true;

                    // the element is optional.
                    if (null !== this.validationErrorElement) {
                        this.validationErrorElement.style.visibility = 'hidden';
                    }
                }

                maybePreventSubmission(event) {
                    if (true === this.isValid ||
                        // the element is optional.
                        null === this.validationErrorElement) {
                        return;
                    }

                    event.preventDefault();
                    event.stopPropagation();

                    this.validationErrorElement.style.visibility = 'visible';
                }

                setup() {
                    this.validationErrorElement = this.querySelector('.mc4wp-procaptcha__validation-error')
                    attributes.callback = this.validatedCallback.bind(this);

                    window.procaptcha.render(this.querySelector('.mc4wp-procaptcha__captcha'), attributes);
                    this.closest('form').addEventListener('submit', this.maybePreventSubmission.bind(this));
                }
            }

            customElements.define("mc4wp-procaptcha", MC4WPProcaptcha);
        </script>
        <?php
    }

    /**
     * @return bool
     */
    protected function is_human_made_request()
    {
        // phpcs:ignore WordPress.Security.NonceVerification -- explicitly not using a nonce here
        $token = wp_unslash($_POST[self::FORM_FIELD_NAME] ?? '');
        $token = true === is_string($token) ? $token : '';

        // bail early if the token is empty.
        if ('' === $token) {
            return false;
        }

        $response = wp_remote_post(
            self::API_URL,
            [
                'method' => 'POST',
                // limit waiting time to 20 seconds.
                'timeout' => 20,
                'headers' => [
                    'Content-Type' => 'application/json',
                ],
                'body' => (string) wp_json_encode(
                    [
                        'secret' => $this->secret_key,
                        'token' => $token,
                    ]
                ),
            ]
        );

        // Check if request failed, either locally or remotely
        if (true === is_wp_error($response) || wp_remote_retrieve_response_code($response) >= 400) {
            /** @var MC4WP_Debug_Log */
            $logger = mc4wp_get_service('log');
            $logger->error(sprintf('ProCaptcha request error: %d %s - %s', wp_remote_retrieve_response_code($response), wp_remote_retrieve_response_message($response), wp_remote_retrieve_body($response)));

            // the check failed, but we don't want to break the form in case of Prosopo having server issues
            // so we write to log and act as if this user is human...
            return true;
        }

        $body = wp_remote_retrieve_body($response);
        $data = json_decode($body, true);

        // check if Prosopo API returned a correct JSON response
        if ($data === null || !is_array($data)) {
            $logger = mc4wp_get_service('log');
            $logger->error(sprintf('ProCaptcha returned a non-JSON response: %s', $body));
            return true;
        }

        $is_verified = isset($data['verified']) && $data['verified'];
        return true === $is_verified;
    }

    public function maybe_add_type_module_attribute(string $tag, string $handle, string $src): string
    {
        if (
            'prosopo-procaptcha' !== $handle ||
            // make sure we don't make it twice if other Procaptcha integrations are present.
            false !== strpos('type="module"', $tag)
        ) {
            return $tag;
        }

        // for old WP versions.
        $tag = str_replace(' type="text/javascript"', '', $tag);

        return str_replace(' src=', ' type="module" src=', $tag);
    }

    /**
     * @return bool
     */
    public function is_enabled()
    {
        return $this->is_enabled;
    }

    /**
     * @param bool $is_without_validation_element
     * @param bool $is_forced_render E.g. if it's a preview.
     *
     * @return string
     */
    public function print_captcha_element($is_without_validation_element = false, $is_forced_render = false)
    {
        if (
            false === $this->is_displayed_for_authorized &&
            true === is_user_logged_in() &&
            false === $is_forced_render
        ) {
            return '';
        }

        $this->is_in_use = true;

        $html  = '<mc4wp-procaptcha class="mc4wp-procaptcha" style="display: block;">';
        $html .= '<div class="mc4wp-procaptcha__captcha"></div>';

        // The element is optional, e.g. should be missing on the settings page.
        if (false === $is_without_validation_element) {
            $html .= '<p class="mc4wp-procaptcha__validation-error" style="visibility: hidden;color:red;line-height:1;font-size: 12px;padding: 7px 0 10px 10px;margin:0;">';
            $html .= esc_html__('Please verify that you are human.', 'mailchimp-for-wp');
            $html .= '</p>';
        }

        $html .= '</mc4wp-procaptcha>';

        return $html;
    }

    /**
     * @return void
     */
    public function maybe_enqueue_captcha_js()
    {
        if (false === $this->is_in_use) {
            return;
        }

        // do not use wp_enqueue_module() because it doesn't work on the login screens.
        wp_enqueue_script(
            'prosopo-procaptcha',
            self::SCRIPT_URL,
            [],
            null,
            [
                'in_footer' => true,
                'strategy' => 'defer',
            ]
        );

        $this->print_captcha_js();
    }

    /**
     * @param array<string,string> $messages
     * @return array<string,string>
     */
    public function register_error_message(array $messages)
    {
        $messages['procaptcha_required'] = 'Please verify that you are human.';

        return $messages;
    }

    /**
     * @param string[] $error_keys
     * @param MC4WP_Form $form
     *
     * @return string[]
     */
    public function validate_form($error_keys, $form)
    {
        if (
            false === strpos($form->content, $this->get_field_stub()) ||
            (false === $this->is_displayed_for_authorized && true === is_user_logged_in()) ||
            true === $this->is_human_made_request()
        ) {
            return $error_keys;
        }

        $error_keys[] = 'procaptcha_required';

        return $error_keys;
    }

    /**
     * @param string $html
     * @return string
     */
    public function inject_captcha_element($html)
    {
        $stub = $this->get_field_stub();

        if (false === strpos($html, $stub)) {
            return $html;
        }

        $captcha_element = $this->print_captcha_element();

        return str_replace($stub, $captcha_element, $html);
    }

    /**
     * @return string
     */
    protected function get_field_stub()
    {
        return '<input type="hidden" name="procaptcha">';
    }

    public function set_hooks(): void
    {
        if (false === $this->is_enabled) {
            return;
        }

        add_filter('mc4wp_form_messages', [$this, 'register_error_message']);
        add_action('mc4wp_form_content', [$this, 'inject_captcha_element']);
        add_filter('mc4wp_form_errors', [$this, 'validate_form'], 10, 2);

        add_filter('script_loader_tag', [$this, 'maybe_add_type_module_attribute'], 10, 3);

        $hook = true === is_admin() ?
            'admin_print_footer_scripts' :
            'wp_print_footer_scripts';

        // priority must be less than 10, to make sure the wp_enqueue_script still has effect.
        add_action($hook, [$this, 'maybe_enqueue_captcha_js'], 9);
    }
}
