<?php
if (!defined('ABSPATH')) exit;

class FORMFORGE_Form_Renderer {
    private static $instance = null;
    public static function instance() { if (!self::$instance) self::$instance = new self(); return self::$instance; }
    private function __construct() {}

    /**
 * Pass a form-owned string through the LangForge integration filter.
 * No-op when LangForge isn't around (returns the original verbatim) so
 * sites without a translation plugin keep rendering exactly the same
 * markup they did before this hook existed.
 *
 * @param string $string The raw label / placeholder / option text.
 * @param string $key Token describing where the string lives, e.g.
 * 'label', 'placeholder', 'option_label',
 * 'submit_text', 'success_message'. Used by the
 * integration to derive a stable string_name.
 * @param string $field_id The field id from `wp_formforge_forms.fields`,
 * empty string for form-level strings.
 * @param int $form_id The form id; required for the per-form domain.
 */
    public static function translate_string($string, $key, $field_id = '', $form_id = 0) {
        if ($string === '' || $string === null) {
            return $string;
        }
        return apply_filters('formforge_translate_string', $string, $key, $field_id, (int) $form_id);
    }

    /**
 * Render a form by ID
 */
    public function render($form_id) {
        $form = FORMFORGE_Form_Builder::instance()->get($form_id);
        if (!$form || $form->status !== 'publish') {
            return '<p class="formforge-error">' . __('Form not found.', 'formforge') . '</p>';
        }

        // Ensure frontend assets are loaded (handles do_shortcode in templates)
        if (!wp_script_is('formforge-frontend', 'enqueued')) {
            // cache-bust: append filemtime so visitors pick up new
            // calendar-field.js / frontend.js immediately after a deploy.
            // Previously we used FORMFORGE_VERSION alone, which only bumps
            // on a release tag — submissions and slot picker logic could
            // run on a 6-week-old browser cache after a hot-fix deploy.
            $css_file = FORMFORGE_DIR . 'assets/css/frontend.css';
            $js_file  = FORMFORGE_DIR . 'assets/js/frontend.js';
            $vcss = FORMFORGE_VERSION . (file_exists($css_file) ? '.' . filemtime($css_file) : '');
            $vjs  = FORMFORGE_VERSION . (file_exists($js_file)  ? '.' . filemtime($js_file)  : '');
            wp_enqueue_style('formforge-frontend', FORMFORGE_URL . 'assets/css/frontend.css', [], $vcss);
            wp_enqueue_script('formforge-frontend', FORMFORGE_URL . 'assets/js/frontend.js', ['jquery'], $vjs, true);
            wp_localize_script('formforge-frontend', 'formforgeFront', [
                'ajaxUrl' => admin_url('admin-ajax.php'),
                'nonce'   => wp_create_nonce('formforge_submit'),
            ]);
        }
        if (class_exists('FORMFORGE_Anti_Spam')) {
            FORMFORGE_Anti_Spam::instance()->enqueue_recaptcha(true);
        }

        // Check form scheduling
        $settings = $form->settings ?? [];
        $schedule_start = $settings['schedule_start'] ?? '';
        $schedule_end = $settings['schedule_end'] ?? '';
        $schedule_msg = $settings['schedule_message'] ?? __('This form is currently closed.', 'formforge');
        $now = current_time('Y-m-d\TH:i');

        if ($schedule_start && $now < $schedule_start) {
            $msg = self::translate_string($schedule_msg, 'schedule_message', '', $form_id);
            return '<div class="formforge-closed">' . esc_html($msg) . '</div>';
        }
        if ($schedule_end && $now > $schedule_end) {
            $msg = self::translate_string($schedule_msg, 'schedule_message', '', $form_id);
            return '<div class="formforge-closed">' . esc_html($msg) . '</div>';
        }

        // Track view. Analytics is Pro — skip silently on Free-only sites.
        if (class_exists('FORMFORGE_Analytics') && $this->can_feature('analytics')) {
            FORMFORGE_Analytics::instance()->track($form_id, 'view');
        }

        $settings = $form->settings;
        $fields = $this->filter_unavailable_fields($form->fields);
        $css_class = $settings['css_class'] ?? '';
        $is_conversational = !empty($settings['conversational']) && $this->can_feature('conversational');

        // Conversational mode: separate rendering path
        if ($is_conversational) {
            $this->enqueue_integration_assets($fields);
            return $this->render_conversational($form_id, $form, $settings, $fields, $css_class);
        }

        // Enqueue integration-specific assets based on field types
        $this->enqueue_integration_assets($fields);

        return $this->render_standard_markup((int) $form_id, $form, $settings, $fields, $css_class);
    }

    public function render_preview($payload) {
        $form = (object) [
            'id' => 0,
            'title' => isset($payload['title']) ? (string) $payload['title'] : __('Preview', 'formforge'),
            'status' => 'publish',
            'fields' => isset($payload['fields']) && is_array($payload['fields']) ? $payload['fields'] : [],
            'settings' => isset($payload['settings']) && is_array($payload['settings']) ? $payload['settings'] : [],
            'conditional_logic' => isset($payload['conditional_logic']) && is_array($payload['conditional_logic']) ? $payload['conditional_logic'] : [],
        ];
        $settings = $form->settings;
        $fields = $this->filter_unavailable_fields($form->fields);
        $css_class = $settings['css_class'] ?? '';

        if (!empty($settings['conversational']) && $this->can_feature('conversational')) {
            return $this->render_conversational(0, $form, $settings, $fields, $css_class, true);
        }

        return $this->render_standard_markup(0, $form, $settings, $fields, $css_class, true);
    }

    private function render_standard_markup($form_id, $form, $settings, $fields, $css_class, $is_preview = false) {
        $style_config = $settings['styles'] ?? [];
        $wrap_style = FORMFORGE_Field_Types::style_attr($style_config, 'form');
        $submit_style = FORMFORGE_Field_Types::style_attr($style_config, 'submit');
        $message_style = 'display:none;' . FORMFORGE_Field_Types::style_declarations($style_config, 'message');

        ob_start();
        ?>
        <div class="formforge-wrap <?php echo esc_attr($css_class); ?>" id="formforge-<?php echo (int) $form_id; ?>" data-form-id="<?php echo (int) $form_id; ?>"<?php echo $wrap_style; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built style attribute from style_attr */ ?><?php echo $is_preview ? ' data-formforge-preview="1"' : ''; ?>>
            <form class="formforge-form" method="post" enctype="multipart/form-data" novalidate>
                <input type="hidden" name="formforge_form_id" value="<?php echo (int) $form_id; ?>">
                <input type="hidden" name="formforge_nonce" value="<?php echo esc_attr(wp_create_nonce('formforge_submit')); ?>">
                <input type="hidden" name="formforge_token" value="<?php echo esc_attr(FORMFORGE_Anti_Spam::generate_token((int) $form_id)); ?>">
                <input type="hidden" name="formforge_timestamp" value="<?php echo esc_attr(time()); ?>">
                <?php
                // Cross-plugin integration: capture the visitor's current
                // language at render time and ship it back in the submit
                // payload. The form-handler echoes this back through the
                // formforge_translate_string filter so success_message
                // arrives in the same language the visitor was reading
                // the form in. AJAX context doesn't have the URL-based
                // language signal, so without this hint LangForge would
                // resolve to the site's default language. Filterable so a
                // multilingual stack other than LangForge can substitute
                // its own current-language source.
                $rendered_lang = $is_preview ? '' : apply_filters('formforge_current_language', '', $form_id);
                if ($rendered_lang !== '') {
                    echo '<input type="hidden" name="formforge_lang" value="' . esc_attr($rendered_lang) . '">';
                }
                ?>

                <?php // Honeypot ?>
                <div style="position:absolute;left:-9999px;opacity:0;height:0;overflow:hidden;" aria-hidden="true">
                    <input type="text" name="formforge_website_url" value="" tabindex="-1" autocomplete="off">
                </div>

                <?php
                $logic = $this->can_feature('conditional_logic') ? $form->conditional_logic : [];
                if (!empty($logic)) {
                    echo '<script type="application/json" class="formforge-logic-data">' . wp_json_encode($logic, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . '</script>';
                }

                // Check if multi-step
                $has_steps = false;
                foreach ($fields as $f) { if (($f['type'] ?? '') === 'page_break') { $has_steps = true; break; } }

                if ($has_steps): ?>
                <!-- Progress bar -->
                <div class="formforge-progress" style="display:none;">
                    <div class="formforge-progress-bar"><div class="formforge-progress-fill"></div></div>
                    <span class="formforge-progress-text"></span>
                </div>
                <?php endif; ?>

                <?php
                // Group fields into steps split by page_break. This produces
                // properly nested <div class="formforge-step"> blocks so the
                // multi-step JS in frontend.js (which iterates step divs) works.
                $step_groups = [[]];
                foreach ($fields as $field) {
                    if (($field['type'] ?? '') === 'page_break') {
                        // Start a new step. The page_break field itself does
                        // not render any UI.
                        $step_groups[] = [];
                        continue;
                    }
                    $step_groups[count($step_groups) - 1][] = $field;
                }
                $step_index = 0;
                foreach ($step_groups as $group):
                    if (empty($group)) continue;
                    $step_style = ($has_steps && $step_index > 0) ? 'style="display:none;"' : ($has_steps ? '' : 'style="display:block;"');
                    ?>
                    <div class="formforge-step" data-step <?php echo $step_style; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built style attribute literal */ ?>>
                    <?php foreach ($group as $field) {
                        echo FORMFORGE_Field_Types::render($field, '', $form); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from Field_Types::render
                    } ?>
                    </div>
                    <?php
                    $step_index++;
                endforeach;
                ?>

                <?php if ($has_steps): ?>
                <div class="formforge-step-nav" style="display:none;">
                    <button type="button" class="formforge-btn-prev"><?php esc_html_e('Back', 'formforge'); ?></button>
                    <button type="button" class="formforge-btn-next"><?php esc_html_e('Next', 'formforge'); ?></button>
                </div>
                <?php endif; ?>

                <div class="formforge-submit-wrap">
                    <button type="submit" class="formforge-submit-btn" data-ff-style-target="submit"<?php echo $submit_style; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built style attribute from style_attr */ ?>>
                        <?php echo esc_html(self::translate_string($settings['submit_text'] ?? __('Submit', 'formforge'), 'submit_text', '', $form_id)); ?>
                    </button>
                </div>

                <div class="formforge-message" data-ff-style-target="message" style="<?php echo esc_attr($message_style); ?>"></div>
            </form>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
 * Render conversational (Typeform-style) form
 */
    private function render_conversational($form_id, $form, $settings, $fields, $css_class, $is_preview = false) {
        $style_config = $settings['styles'] ?? [];
        $wrap_style = FORMFORGE_Field_Types::style_attr($style_config, 'form');
        $submit_style = FORMFORGE_Field_Types::style_attr($style_config, 'submit');
        $message_style = 'display:none;' . FORMFORGE_Field_Types::style_declarations($style_config, 'message');

        // Filter out layout-only fields that should not be standalone steps
        $non_step_types = ['page_break', 'divider'];
        $step_fields = [];
        foreach ($fields as $field) {
            $type = $field['type'] ?? 'text';
            // Skip page breaks entirely (conversational doesn't use them)
            if ($type === 'page_break') continue;
            // Group hidden fields with the previous step (or first step)
            if ($type === 'hidden') {
                // Hidden fields will be rendered but not as separate steps
                if (empty($step_fields)) {
                    $step_fields[] = ['fields' => [$field], 'hidden_only' => true];
                } else {
                    $last = count($step_fields) - 1;
                    $step_fields[$last]['fields'][] = $field;
                }
                continue;
            }
            // Dividers, headings and html_blocks: combine with the next visible field if possible
            if (in_array($type, ['divider'])) {
                continue; // Skip dividers in conversational mode
            }
            $step_fields[] = ['fields' => [$field], 'hidden_only' => false];
        }

        // Merge hidden-only groups into the next visible step
        $merged = [];
        $pending_hidden = [];
        foreach ($step_fields as $step) {
            if (!empty($step['hidden_only'])) {
                $pending_hidden = array_merge($pending_hidden, $step['fields']);
            } else {
                if (!empty($pending_hidden)) {
                    $step['fields'] = array_merge($pending_hidden, $step['fields']);
                    $pending_hidden = [];
                }
                $merged[] = $step;
            }
        }
        // If there are trailing hidden fields, attach to last step
        if (!empty($pending_hidden) && !empty($merged)) {
            $last = count($merged) - 1;
            $merged[$last]['fields'] = array_merge($merged[$last]['fields'], $pending_hidden);
        }

        $step_fields = $merged;
        $total_steps = count($step_fields) + 1; // +1 for submit step

        // Enqueue conversational assets
        $this->enqueue_conversational_assets($settings, $form_id);

        ob_start();
        ?>
        <div class="formforge-wrap ff-conversational <?php echo esc_attr($css_class); ?>" id="formforge-<?php echo (int) $form_id; ?>" data-form-id="<?php echo (int) $form_id; ?>" data-conversational="1"<?php echo $is_preview ? ' data-formforge-preview="1"' : ''; ?><?php echo $wrap_style; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built style attribute from style_attr */ ?>>
            <form class="formforge-form" method="post" enctype="multipart/form-data" novalidate>
                <input type="hidden" name="formforge_form_id" value="<?php echo (int) $form_id; ?>">
                <input type="hidden" name="formforge_nonce" value="<?php echo esc_attr(wp_create_nonce('formforge_submit')); ?>">
                <input type="hidden" name="formforge_token" value="<?php echo esc_attr(FORMFORGE_Anti_Spam::generate_token($form_id)); ?>">
                <input type="hidden" name="formforge_timestamp" value="<?php echo esc_attr(time()); ?>">
                <?php
                // Conversational mode: same render-time language hint as
                // the standard render path. See comment block in the other
                // emitter for why we round-trip the language through the
                // form payload.
                $rendered_lang = apply_filters('formforge_current_language', '', $form_id);
                if ($rendered_lang !== '') {
                    echo '<input type="hidden" name="formforge_lang" value="' . esc_attr($rendered_lang) . '">';
                }
                ?>

                <?php // Honeypot ?>
                <div style="position:absolute;left:-9999px;opacity:0;height:0;overflow:hidden;" aria-hidden="true">
                    <input type="text" name="formforge_website_url" value="" tabindex="-1" autocomplete="off">
                </div>

                <?php
                $logic = $this->can_feature('conditional_logic') ? $form->conditional_logic : [];
                if (!empty($logic)) {
                    echo '<script type="application/json" class="formforge-logic-data">' . wp_json_encode($logic, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . '</script>';
                }

                // Render each field as a step
                $step_num = 0;
                foreach ($step_fields as $step_data):
                    $step_num++;
                    $is_first = ($step_num === 1);
                ?>
                <div class="ff-conv-step<?php echo $is_first ? ' ff-conv-step--active' : ''; ?>" data-step="<?php echo (int) $step_num; ?>">
                    <div class="ff-conv-step-inner">
                        <div class="ff-conv-step-number">
                            <span><?php echo (int) $step_num; ?></span>
                            <svg viewBox="0 0 10 10"><polygon points="5,0 10,10 0,10"/></svg>
                        </div>
                        <?php
                        foreach ($step_data['fields'] as $field) {
                            echo FORMFORGE_Field_Types::render($field, '', $form); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from Field_Types::render
                        }
                        ?>
                    </div>
                </div>
                <?php endforeach; ?>

                <?php // Submit step ?>
                <div class="ff-conv-step ff-conv-step--submit" data-step="<?php echo (int) ($step_num + 1); ?>">
                    <div class="ff-conv-step-inner">
                        <div class="ff-conv-submit-title"><?php esc_html_e('All done!', 'formforge'); ?></div>
                        <div class="ff-conv-submit-desc"><?php esc_html_e('Check your answers and submit the form.', 'formforge'); ?></div>
                        <button type="button" class="ff-conv-submit-btn" data-ff-style-target="submit"<?php echo $submit_style; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built style attribute from style_attr */ ?>>
                            <?php echo esc_html(self::translate_string($settings['submit_text'] ?? __('Submit', 'formforge'), 'submit_text', '', $form_id)); ?>
                        </button>
                        <div style="margin-top:16px;">
                            <button type="button" class="ff-conv-btn-back" data-action="back">&larr; <?php esc_html_e('Back', 'formforge'); ?></button>
                        </div>
                    </div>
                </div>

                <?php // Hidden actual submit button (triggered by JS) ?>
                <div class="formforge-submit-wrap" style="display:none !important;">
                    <button type="submit" class="formforge-submit-btn" data-ff-style-target="submit"<?php echo $submit_style; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built style attribute from style_attr */ ?>>
                        <?php echo esc_html(self::translate_string($settings['submit_text'] ?? __('Submit', 'formforge'), 'submit_text', '', $form_id)); ?>
                    </button>
                </div>

                <div class="formforge-message" data-ff-style-target="message" style="<?php echo esc_attr($message_style); ?>"></div>
            </form>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
 * Enqueue integration-specific assets based on field types in the form.
 */
    private function enqueue_integration_assets($fields) {
        $has_map = false;
        $has_payment = false;
        $has_calendar = false;

        foreach ($fields as $field) {
            $type = $field['type'] ?? '';
            // Gate detection on Pro-availability: a downgraded form may still
            // store a `payment`/`map_address`/`calendar_picker` field type, but
            // on Free the field is neutralized server-side and never rendered,
            // so its integration script (stripe-js, maps, calendar) must not be
            // enqueued. Use the same field_type_available gate the renderer
            // applies elsewhere so detection can't drift from rendering.
            if (!FORMFORGE_Field_Types::field_type_available($type)) {
                continue;
            }
            if ($type === 'map_address') $has_map = true;
            if ($type === 'payment') $has_payment = true;
            if ($type === 'calendar_picker') $has_calendar = true;
        }

        // Maps: Google or Yandex
	        if ($has_map) {
	            $map_settings = get_option('formforge_settings', []);
	            $provider = $map_settings['map_provider'] ?? 'google';
	            $legacy_api_key = trim((string) ($map_settings['map_api_key'] ?? ''));
	            $google_api_key = trim((string) ($map_settings['map_google_api_key'] ?? ''));
	            $yandex_js_api_key = trim((string) ($map_settings['map_yandex_js_api_key'] ?? ''));
	            if ($google_api_key === '' && $provider === 'google') {
	                $google_api_key = $legacy_api_key;
	            }
	            if ($yandex_js_api_key === '' && $provider === 'yandex') {
	                $yandex_js_api_key = $legacy_api_key;
	            }
	            $api_key = $provider === 'yandex' ? $yandex_js_api_key : $google_api_key;
	            $yandex_geocoder_key = $map_settings['map_yandex_geocoder_key'] ?? '';
	            $yandex_suggest_key = $map_settings['map_yandex_suggest_key'] ?? '';

	            if ($api_key) {
	                if ($provider === 'yandex') {
	                    $yandex_src = 'https://api-maps.yandex.ru/2.1/?apikey=' . urlencode($api_key) . '&lang=ru_RU';
	                    if ($yandex_suggest_key !== '') {
	                        $yandex_src .= '&suggest_apikey=' . urlencode($yandex_suggest_key);
	                    }
	                    wp_enqueue_script('yandex-maps', $yandex_src, [], FORMFORGE_VERSION, true);
	                } else {
	                    wp_enqueue_script('google-maps', 'https://maps.googleapis.com/maps/api/js?key=' . urlencode($api_key) . '&libraries=places', [], FORMFORGE_VERSION, true);
	                }

                $map_file = FORMFORGE_DIR . 'assets/js/map-field.js';
                $map_version = FORMFORGE_VERSION . (file_exists($map_file) ? '.' . filemtime($map_file) : '');
                wp_enqueue_script('formforge-map-field', FORMFORGE_URL . 'assets/js/map-field.js', ['formforge-frontend'], $map_version, true);
                wp_add_inline_script(
	                    'formforge-map-field',
	                    'window.formforgeMapProvider = ' . wp_json_encode($provider) . ';' .
	                    'window.formforgeMapYandexGeocoder = ' . wp_json_encode([
	                        'ajaxUrl' => admin_url('admin-ajax.php'),
	                        'nonce' => wp_create_nonce('formforge_yandex_geocode'),
	                        'enabled' => $provider === 'yandex' && $yandex_geocoder_key !== '',
	                    ]) . ';' .
	                    'window.formforgeMapHasYandexSuggest = ' . wp_json_encode($provider === 'yandex' && $yandex_suggest_key !== '') . ';' .
	                    'window.formforgeMapMessages = ' . wp_json_encode([
	                        'manualInput' => __('Select an address suggestion or choose a point on the map.', 'formforge'),
	                        'manualYandexInput' => __('Press Enter to resolve the address, or choose a point on the map.', 'formforge'),
	                        'chooseFullAddress' => __('Choose a full address suggestion or pick a point on the map.', 'formforge'),
	                        'yandexUnavailable' => __('Yandex Maps is unavailable. Check the API key and try again.', 'formforge'),
	                        'yandexSuggestUnavailable' => __('Yandex address suggestions are unavailable. Check the Yandex Suggest API key or press Enter to resolve a typed address.', 'formforge'),
	                        'unresolved' => __('This address could not be resolved. Choose a suggestion, press Enter after editing, or click the map.', 'formforge'),
	                    ]) . ';',
	                    'before'
	                );
            }
        }

        // Stripe Payment
        if ($has_payment) {
            $stripe_settings = get_option('formforge_stripe_settings', []);
            $mode = $stripe_settings['mode'] ?? 'test';
            $pub_key = ($mode === 'live')
                ? ($stripe_settings['live_publishable_key'] ?? '')
                : ($stripe_settings['test_publishable_key'] ?? '');

            if ($pub_key) {
                $stripe_js_file = FORMFORGE_DIR . 'assets/js/stripe-field.js';
                $stripe_js_version = FORMFORGE_VERSION . (file_exists($stripe_js_file) ? '.' . filemtime($stripe_js_file) : '');
                wp_enqueue_script('stripe-js', 'https://js.stripe.com/v3/', [], FORMFORGE_VERSION, true);
                wp_enqueue_script('formforge-stripe-field', FORMFORGE_URL . 'assets/js/stripe-field.js', ['stripe-js', 'formforge-frontend'], $stripe_js_version, true);
                wp_add_inline_script('formforge-stripe-field', 'window.formforgeStripeKey = ' . wp_json_encode($pub_key) . '; window.formforgeStripeAjaxUrl = ' . wp_json_encode(admin_url('admin-ajax.php')) . ';', 'before');
            }
        }

        // Calendar Picker
        if ($has_calendar) {
            $cf_file = FORMFORGE_DIR . 'assets/js/calendar-field.js';
            $vcf = FORMFORGE_VERSION . (file_exists($cf_file) ? '.' . filemtime($cf_file) : '');
            wp_enqueue_script('formforge-calendar-field', FORMFORGE_URL . 'assets/js/calendar-field.js', ['formforge-frontend'], $vcf, true);
        }
    }

    /**
 * Enqueue conversational CSS/JS assets
 */
    private function enqueue_conversational_assets($settings, $form_id = 0) {
        $v = FORMFORGE_VERSION;
        $css_file = FORMFORGE_DIR . 'assets/css/conversational.css';
        $js_file  = FORMFORGE_DIR . 'assets/js/conversational.js';

        if (file_exists($css_file)) {
            $v_css = $v . '.' . filemtime($css_file);
            wp_enqueue_style('formforge-conversational', FORMFORGE_URL . 'assets/css/conversational.css', ['formforge-frontend'], $v_css);
        }

        if (file_exists($js_file)) {
            $v_js = $v . '.' . filemtime($js_file);
            wp_enqueue_script('formforge-conversational', FORMFORGE_URL . 'assets/js/conversational.js', ['jquery', 'formforge-frontend'], $v_js, true);
            wp_localize_script('formforge-conversational', 'formforgeConv', [
                'submit_text' => self::translate_string($settings['submit_text'] ?? __('Submit', 'formforge'), 'submit_text', '', $form_id),
                'next_label'  => __('Next →', 'formforge'),
                'back_label'  => __('← Back', 'formforge'),
                /* translators: shown as a small hint above the Next button. */
                'hint_label'  => __('Press', 'formforge'),
                /* translators: %s is the field label. */
                'err_required'    => __('%s — required field', 'formforge'),
                /* translators: %s is the field label. */
                'err_select_one'  => __('%s — please select an option', 'formforge'),
                /* translators: %s is the field label. */
                'err_email'       => __('%s — enter a valid email address', 'formforge'),
                /* translators: %s is the field label. */
                'err_url'         => __('%s — enter a valid URL', 'formforge'),
                /* translators: %s is the field label. */
                'err_file'        => __('%s — please attach a file', 'formforge'),
                'rating_label'    => __('Rating', 'formforge'),
            ]);
        }
    }

    private function can_feature($feature) {
        return class_exists('FORMFORGE_License') && FORMFORGE_License::instance()->can($feature);
    }

    private function filter_unavailable_fields($fields) {
        if (!is_array($fields)) {
            return [];
        }

        $allowed = [];
        foreach ($fields as $field) {
            if (!is_array($field)) {
                continue;
            }
            if (!FORMFORGE_Field_Types::field_type_available($field['type'] ?? '')) {
                continue;
            }
            $allowed[] = $field;
        }
        return $allowed;
    }
}
