<?php

namespace Formatrica\Frontend;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use Formatrica\Fields\Field_Render_Context;
use Formatrica\Fields\Field_Renderer_Registry;
use Formatrica\Forms\Form_Sanitizer;
use Formatrica\Submission\Submission_Service;

final class Form_Renderer
{
    /**
     * Field types rendered by the Free core as native text-style inputs.
     * Anything not in this list and not explicitly branched below is either a
     * Pro type (rendered via the Field_Renderer_Registry when the Pro package
     * is present) or unknown — both render '' in Free, never a text input.
     *
     * @var array<int, string>
     */
    private const FREE_INPUT_TYPES = ['text', 'email', 'tel', 'password', 'url', 'number', 'range', 'color', 'datetime'];

    /**
     * @var array<int, string>
     */
    private static array $custom_css_rendered = [];

    /** Shared per-render context handed to registered field renderers. */
    private ?Field_Render_Context $field_context = null;

    /**
     * Map of stable field key => submission name, for resolving the sameas
     * rule's target when emitting client-side validation config. Rebuilt per
     * render_fields() call.
     *
     * @var array<string, string>
     */
    private array $field_name_by_key = [];

    /** Form-level payment currency, used by the presentational price field. */
    private string $payment_currency = 'USD';

    public function render(array $form): string
    {
        $config = Form_Sanitizer::merge_with_defaults($form['config'] ?? []);
        $fields = $config['fields'];
        $options = $config['options'];

        $this->payment_currency = isset($options['payments']['currency']) && is_string($options['payments']['currency'])
            ? $options['payments']['currency']
            : 'USD';

        $this->field_context = new Field_Render_Context(
            $this->payment_currency,
            fn (array $field): string => $this->render_select($field)
        );

        $has_button = false;
        foreach ($fields as $field) {
            if (($field['type'] ?? '') === 'button') {
                $has_button = true;
                break;
            }
        }

        if (!$has_button) {
            $fields[] = [
                'type' => 'button',
                'label' => __('Send', 'formatrica'),
            ];
        }

        $captcha = $options['captcha'] ?? [];
        $captcha_provider = isset($captcha['provider']) && is_string($captcha['provider']) ? $captcha['provider'] : 'none';
        $captcha_site_key = isset($captcha['site_key']) && is_string($captcha['site_key']) ? $captcha['site_key'] : '';

        // Captcha rides inside the field markup so a multi-step form places it in
        // the LAST step. Rendered form-level it would show on every step, and its
        // token could expire before the visitor reaches the final submit.
        $captcha_markup = $this->render_captcha($captcha_provider, $captcha_site_key);

        $fields_markup = $this->render_fields($fields, $captcha_markup);
        $nonce = wp_create_nonce('formatrica_submit_' . $form['id']);
        $form_id = (int) $form['id'];
        $honeypot_name = Submission_Service::honeypot_field_name($form_id);
        $timestamp_name = Submission_Service::timestamp_field_name($form_id);
        $rendered_at = time();

        // Custom wrapper class (Pro forms.wrapper_class) comes from the form-render
        // extension; Free adds nothing beyond the base class.
        $extra_wrapper_class = Form_Render_Extensions::current()->wrapper_class($options);
        $wrapper_class_attr = $extra_wrapper_class !== '' ? 'formatrica ' . esc_attr($extra_wrapper_class) : 'formatrica';

        $wrapper_attributes = [
            'class="' . $wrapper_class_attr . '"',
            'data-form-id="' . esc_attr((string) $form_id) . '"',
        ];

        if ($captcha_provider !== 'none' && $captcha_site_key !== '') {
            $wrapper_attributes[] = 'data-captcha-provider="' . esc_attr($captcha_provider) . '"';
            $wrapper_attributes[] = 'data-captcha-site-key="' . esc_attr($captcha_site_key) . '"';
        }

        $wrapper_attrs = implode(' ', $wrapper_attributes);

        $messages = sprintf(
            '<div class="formatrica__messages" aria-live="polite"></div>'
        );

        $success_message_attr = esc_attr(wp_strip_all_tags($options['success_message'] ?? __('Thanks! Your message has been sent.', 'formatrica')));
        $error_message_attr = esc_attr(wp_strip_all_tags($options['error_message'] ?? __('Something went wrong. Please try again.', 'formatrica')));

        $hidden = sprintf(
            '<input type="hidden" name="_fmt_nonce" value="%s" />',
            esc_attr($nonce)
        );

        $honeypot = sprintf(
            '<div class="formatrica__fmt_hp" aria-hidden="true"><label><input type="text" name="%s" tabindex="-1" autocomplete="off" /></label></div><input type="hidden" name="%s" value="%d" />',
            esc_attr($honeypot_name),
            esc_attr($timestamp_name),
            $rendered_at
        );

        // Save & Resume drafts (Pro submissions.drafts) come from the form-render
        // extension; Free renders no draft button.
        $draft_markup = Form_Render_Extensions::current()->draft_button($options, $fields);

        $custom_style = $this->render_custom_css($form_id, $options['custom_css'] ?? '');

        $noscript = sprintf(
            '<noscript>%s</noscript>',
            esc_html__('JavaScript is required to submit this form.', 'formatrica')
        );

        $enctype = $this->contains_file_field($fields) ? ' enctype="multipart/form-data"' : '';

        // Appearance theme: preset modifier class + brand/mode attributes.
        $theme = isset($options['theme']) && is_array($options['theme']) ? $options['theme'] : [];
        $theme_resolver = new Theme_Overlay_Resolver();
        $theme_modifier = $theme_resolver->modifier_class($theme);
        $theme_class = $theme_modifier !== '' ? ' ' . esc_attr($theme_modifier) : '';
        $theme_attrs = $theme_resolver->inline_attributes($theme);

        // Column layout mode (capability-blind: a downgraded form keeps its columns).
        $layout_mode = $options['layout']['mode'] ?? 'single';
        $layout_mode = in_array($layout_mode, ['single', 'two_column', 'three_column'], true) ? $layout_mode : 'single';

        // If any field carries an advanced width, enable the column grid even when
        // no form-level mode was chosen — a half/third field should just work.
        if ($layout_mode === 'single' && $this->has_advanced_width($fields)) {
            $layout_mode = 'two_column';
        }

        $layout_attr = ' data-fmt-layout="' . esc_attr($layout_mode) . '"';

        $form_tag = sprintf(
            '<form class="formatrica__form%1$s" method="post" novalidate role="form" data-success-message="%2$s" data-error-message="%3$s"%4$s%5$s%6$s>%7$s%8$s%9$s%12$s%10$s%11$s</form>',
            $theme_class,
            $success_message_attr,
            $error_message_attr,
            $enctype,
            $theme_attrs,
            $layout_attr,
            $messages,
            $fields_markup,
            '', // captcha now rides inside $fields_markup (last step in multi-step)
            $hidden,
            $honeypot,
            $draft_markup
        );

        return sprintf('<div %s>%s%s%s</div>', $wrapper_attrs, $form_tag, $custom_style, $noscript);
    }

    /**
     * @param array<int, array<string, mixed>> $fields
     */
    private function render_fields(array $fields, string $captcha_markup = ''): string
    {
        $output = '';

        // Build key => name lookup so the sameas rule can resolve its target
        // for the client-side validation payload.
        $this->field_name_by_key = [];
        foreach ($fields as $field) {
            if (!empty($field['key']) && !empty($field['name'])) {
                $this->field_name_by_key[(string) $field['key']] = (string) $field['name'];
            }
        }

        $extension = Form_Render_Extensions::current();

        // Bucket field markup into steps, split on step_break dividers.
        $steps = [[]];
        $step_meta = [];

        foreach ($fields as $field) {
            $type = $field['type'] ?? 'text';

            if ($type === 'step_break') {
                $step_meta[] = [
                    'label' => (string) ($field['label'] ?? ''),
                    'next_label' => (string) ($field['next_label'] ?? ''),
                    'prev_label' => (string) ($field['prev_label'] ?? ''),
                    // Title for the FIRST step (the page above this break) —
                    // only the first break's value is used.
                    'first_step_label' => (string) ($field['first_step_label'] ?? ''),
                ];
                $steps[] = [];
                continue;
            }

            if ($type === 'button') {
                $markup = $this->render_button($field);
            } elseif ($type === 'checkbox') {
                $markup = $this->render_checkbox($field);
            } elseif ($type === 'textarea') {
                $markup = $this->render_textarea($field);
            } elseif ($type === 'custom_text') {
                $markup = $this->render_custom_text($field);
            } elseif ($type === 'select') {
                $markup = $this->render_select($field);
            } elseif ($type === 'radio') {
                $markup = $this->render_radio($field);
            } elseif ($type === 'file') {
                $markup = $this->render_file($field);
            } elseif ($type === 'date') {
                $markup = $this->render_date($field);
            } elseif ($type === 'hidden') {
                $markup = $this->render_hidden($field);
            } elseif ($type === 'country') {
                $markup = $this->render_country($field);
            } elseif ($type === 'state') {
                $markup = $this->render_state($field);
            } elseif (in_array($type, self::FREE_INPUT_TYPES, true)) {
                $markup = $this->render_input($field);
            } else {
                // Pro / unknown type: rendered by a registered Pro field
                // renderer when the Pro package is present, otherwise ''.
                // Never falls back to a text input — a missing renderer must
                // not expose data or change submission semantics.
                $renderer = Field_Renderer_Registry::default()->get($type);
                $markup = $renderer !== null ? $renderer->render($field, $this->field_context) : '';
            }

            $markup = $this->apply_field_layout_class($markup, $field);
            // Pro form-level per-field decoration (conditional-logic attr, custom
            // attributes). Inert in Free.
            $markup = $extension->decorate_field($markup, $field, $this->field_name_by_key);
            $steps[count($steps) - 1][] = $markup;
        }

        // Captcha goes in the LAST step so multi-step forms show it only on the
        // final page (single-step forms keep it after the fields, unchanged).
        if ($captcha_markup !== '') {
            $steps[count($steps) - 1][] = $captcha_markup;
        }

        // Let the Pro extension assemble multi-step markup; Free (and any
        // single-step form) renders the buckets flat — step breaks are ignored.
        $stepped = $extension->render_steps($steps, $step_meta);
        if ($stepped !== null) {
            return $stepped;
        }

        foreach ($steps as $bucket) {
            foreach ($bucket as $markup) {
                $output .= $markup;
            }
        }

        return $output;
    }

    /**
     * Add the column-width modifier class to a field's group wrapper. Only
     * advanced widths (half/third) inject a class; full is the default. Renderer
     * is capability-blind, so a downgraded form keeps its authored widths.
     *
     * @param array<string, mixed> $field
     */
    /**
     * @param array<int, array<string, mixed>> $fields
     */
    private function has_advanced_width(array $fields): bool
    {
        foreach ($fields as $field) {
            $width = $field['layout']['width'] ?? 'full';
            if (in_array($width, ['half', 'third'], true)) {
                return true;
            }
        }
        return false;
    }

    private function apply_field_layout_class(string $markup, array $field): string
    {
        $width = $field['layout']['width'] ?? 'full';

        if (!in_array($width, ['half', 'third'], true)) {
            return $markup;
        }

        $replaced = preg_replace(
            '/class="formatrica__group/',
            'class="formatrica__group formatrica__group--' . $width,
            $markup,
            1
        );

        return is_string($replaced) ? $replaced : $markup;
    }


    private function render_captcha(string $provider, string $site_key): string
    {
        $input = '<input type="hidden" name="_fmt_captcha_token" value="" />';

        if ($provider === 'none' || $site_key === '') {
            return $input;
        }

        if ($provider === 'turnstile') {
            return sprintf('<div class="cf-turnstile" data-sitekey="%1$s" data-theme="auto"></div>%2$s', esc_attr($site_key), $input);
        }

        if ($provider === 'friendlycaptcha') {
            return sprintf('<div class="frc-captcha" data-sitekey="%1$s"></div>%2$s', esc_attr($site_key), $input);
        }

        return $input;
    }

    private function render_input(array $field): string
    {
        $id = esc_attr($field['id']);
        $name = esc_attr($field['name']);
        $label = esc_html($field['label'] ?? '');
        $placeholder = esc_attr($field['placeholder'] ?? '');
        $required = !empty($field['required']) ? 'required' : '';

        $type_map = [
            'email' => 'email',
            'tel' => 'tel',
            'password' => 'password',
            'url' => 'url',
            'number' => 'number',
            'range' => 'range',
            'color' => 'color',
            'datetime' => 'datetime-local',
        ];
        $type = esc_attr($type_map[$field['type']] ?? 'text');

        // Numeric bounds for number/range.
        $extra = '';
        if (in_array($field['type'], ['number', 'range'], true)) {
            foreach (['min', 'max', 'step'] as $prop) {
                if (isset($field[$prop]) && is_numeric($field[$prop])) {
                    $extra .= sprintf(' %s="%s"', $prop, esc_attr((string) $field[$prop]));
                }
            }
        }

        return sprintf(
            '<div class="formatrica__group"><label class="formatrica__label" for="%1$s">%2$s%5$s</label><input class="formatrica__input" type="%4$s" id="%1$s" name="%3$s" placeholder="%6$s" %7$s%8$s%9$s /><p class="formatrica__error" data-error-for="%3$s" role="alert"></p></div>',
            $id,
            $label,
            $name,
            $type,
            $required ? '<span class="formatrica__required" data-required-label="' . esc_attr__('Required', 'formatrica') . '" aria-hidden="true">*</span>' : '',
            $placeholder,
            $required,
            $this->validation_data_attr($field),
            $extra
        );
    }

    /**
     * Build the client-side validation data attribute for a field, or '' when
     * the field has no advanced rule. The value is a JSON blob the frontend
     * mirror (src/frontend/validation) consumes for on-blur feedback; the server
     * remains the canonical validator. Never emits the admin's custom message
     * here — the frontend uses rule defaults and the server sends the final text.
     */
    private function validation_data_attr(array $field): string
    {
        $v = isset($field['validation']) && is_array($field['validation']) ? $field['validation'] : [];

        if (empty($v['advanced_enabled']) || empty($v['rule'])) {
            return '';
        }

        $rule = (string) $v['rule'];

        // Server-only Pro rules (regex/restrictedwords/emaildns/password) have no
        // client twin BY DESIGN — pre-checking would leak the wordlist/pattern or
        // enable an offline passphrase guess. The client surfaces the server
        // verdict on submit; emit nothing for these.
        if (in_array($rule, self::SERVER_ONLY_RULES, true)) {
            return '';
        }

        $params = isset($v['params']) && is_array($v['params']) ? $v['params'] : [];

        if ($rule === 'sameas' && !empty($params['field_key'])) {
            $params['_resolved_target_name'] = $this->field_name_by_key[(string) $params['field_key']] ?? '';
        }
        unset($params['field_key']);

        // multiplerules: keep only client-safe nested rules; drop server-only
        // ones (and never carry their sensitive params) into the client payload.
        if ($rule === 'multiplerules' && isset($params['rules']) && is_array($params['rules'])) {
            $params['rules'] = array_values(array_filter($params['rules'], static function ($r) {
                return is_array($r)
                    && isset($r['rule'])
                    && !in_array($r['rule'], self::SERVER_ONLY_RULES, true);
            }));
            if ($params['rules'] === []) {
                return '';
            }
        }

        // Belt-and-braces: never expose these to the browser under any rule.
        unset($params['hash'], $params['list'], $params['pattern']);

        $json = wp_json_encode([
            'advanced_enabled' => true,
            'rule' => $rule,
            'params' => $params,
        ]);

        return $json === false ? '' : ' data-fmt-validation="' . esc_attr($json) . '"';
    }

    /** Pro rules with no client twin — never emitted to the browser. */
    private const SERVER_ONLY_RULES = ['regex', 'restrictedwords', 'emaildns', 'password'];

    private function render_hidden(array $field): string
    {
        $id = esc_attr($field['id']);
        $name = esc_attr($field['name']);
        $value = isset($field['value']) ? esc_attr((string) $field['value']) : '';

        return sprintf('<input type="hidden" id="%1$s" name="%2$s" value="%3$s" />', $id, $name, $value);
    }

    private function render_textarea(array $field): string
    {
        $id = esc_attr($field['id']);
        $name = esc_attr($field['name']);
        $label = esc_html($field['label'] ?? '');
        $placeholder = esc_attr($field['placeholder'] ?? '');
        $required = !empty($field['required']) ? 'required' : '';

        return sprintf(
            '<div class="formatrica__group"><label class="formatrica__label" for="%1$s">%2$s%4$s</label><textarea class="formatrica__textarea" id="%1$s" name="%3$s" placeholder="%5$s" %6$s></textarea><p class="formatrica__error" data-error-for="%3$s" role="alert"></p></div>',
            $id,
            $label,
            $name,
            $required ? '<span class="formatrica__required" data-required-label="' . esc_attr__('Required', 'formatrica') . '" aria-hidden="true">*</span>' : '',
            $placeholder,
            $required
        );
    }

    private function render_checkbox(array $field): string
    {
        $id = esc_attr($field['id']);
        $name = esc_attr($field['name']);
        $label = $this->sanitize_checkbox_label($field['label'] ?? '');
        if ($label === '') {
            $label = esc_html($field['label'] ?? '');
        }
        $required = !empty($field['required']) ? 'required' : '';

        return sprintf(
            '<div class="formatrica__group formatrica__group--checkbox"><label for="%1$s"><input type="checkbox" id="%1$s" name="%2$s" %3$s /> %4$s%5$s</label><p class="formatrica__error" data-error-for="%2$s" role="alert"></p></div>',
            $id,
            $name,
            $required,
            $label,
            $required ? '<span class="formatrica__required" data-required-label="' . esc_attr__('Required', 'formatrica') . '" aria-hidden="true">*</span>' : ''
        );
    }

    /**
     * Multi-select checkbox group (Pro fields.checkbox_group): a <fieldset> +
     * <legend> label wrapping N checkboxes that all share the bare field name.
     * The JS payload builder groups same-name checkboxes into a list<string>,
     * so no `[]` suffix is needed (unlike native-POST forms). Native `required`
     * is deliberately NOT emitted per input — on a same-name checkbox group it
     * would demand every box be checked; the server enforces "at least one".
     * Capability-blind — a grandfathered group keeps working after a lapse.
     *
     * @param array<string, mixed> $field
     */
    private function render_select(array $field): string
    {
        $id = esc_attr($field['id']);
        $name = esc_attr($field['name']);
        $label = esc_html($field['label'] ?? '');
        $required = !empty($field['required']) ? 'required' : '';
        $options = isset($field['options']) && is_array($field['options']) ? $field['options'] : [];
        $is_multiple = !empty($field['multiple']);

        $options_markup = '';
        foreach ($options as $option) {
            $options_markup .= sprintf('<option value="%1$s">%2$s</option>', esc_attr($option), esc_html($option));
        }

        return sprintf(
            '<div class="formatrica__group"><label class="formatrica__label" for="%1$s">%2$s%5$s</label><select class="formatrica__select" id="%1$s" name="%3$s" %4$s %7$s data-multiple="%8$s">%6$s</select><p class="formatrica__error" data-error-for="%3$s" role="alert"></p></div>',
            $id,
            $label,
            $name,
            $required,
            $required ? '<span class="formatrica__required" data-required-label="' . esc_attr__('Required', 'formatrica') . '" aria-hidden="true">*</span>' : '',
            $options_markup,
            $is_multiple ? 'multiple' : '',
            $is_multiple ? '1' : '0'
        );
    }

    private function render_radio(array $field): string
    {
        $id = esc_attr($field['id']);
        $name = esc_attr($field['name']);
        $label = esc_html($field['label'] ?? '');
        $required = !empty($field['required']) ? 'required' : '';
        $required_mark = $required ? '<span class="formatrica__required" data-required-label="' . esc_attr__('Required', 'formatrica') . '" aria-hidden="true">*</span>' : '';
        $options = isset($field['options']) && is_array($field['options']) ? $field['options'] : [];

        $options_markup = '';
        foreach ($options as $index => $option) {
            $option_id = $id . '-' . ($index + 1);
            $option_attr_id = esc_attr($option_id);
            $option_value = esc_attr($option);
            $option_label = esc_html($option);
            $option_required = $index === 0 ? $required : '';

            $options_markup .= sprintf(
                '<label class="formatrica__choice" for="%1$s"><input type="radio" id="%1$s" name="%2$s" value="%3$s" %4$s /> %5$s</label>',
                $option_attr_id,
                $name,
                $option_value,
                $option_required,
                $option_label
            );
        }

        return sprintf(
            '<div class="formatrica__group formatrica__group--radio"><span class="formatrica__label">%1$s%2$s</span><div class="formatrica__choices">%3$s</div><p class="formatrica__error" data-error-for="%4$s" role="alert"></p></div>',
            $label,
            $required_mark,
            $options_markup,
            $name
        );
    }

    private function render_country(array $field): string
    {
        $id = esc_attr($field['id']);
        $name = esc_attr($field['name']);
        $label = esc_html($field['label'] ?? __('Country', 'formatrica'));
        $required = !empty($field['required']) ? 'required' : '';
        $placeholder = esc_html($field['placeholder'] ?? __('Select a country', 'formatrica'));
        $woo_mode = isset($field['woocommerce_mode']) && is_string($field['woocommerce_mode']) ? esc_attr($field['woocommerce_mode']) : 'all';

        $placeholder_option = sprintf('<option value="">%s</option>', $placeholder);

        return sprintf(
            '<div class="formatrica__group"><label class="formatrica__label" for="%1$s">%2$s%3$s</label><select class="formatrica__select formatrica__country" id="%1$s" name="%4$s" %5$s data-woo-mode="%7$s">%6$s</select><p class="formatrica__error" data-error-for="%4$s" role="alert"></p></div>',
            $id,
            $label,
            $required ? '<span class="formatrica__required" data-required-label="' . esc_attr__('Required', 'formatrica') . '" aria-hidden="true">*</span>' : '',
            $name,
            $required,
            $placeholder_option,
            $woo_mode
        );
    }

    private function render_state(array $field): string
    {
        $id = esc_attr($field['id']);
        $name = esc_attr($field['name']);
        $label = esc_html($field['label'] ?? __('State/Province', 'formatrica'));
        $required = !empty($field['required']) ? 'required' : '';
        $placeholder = esc_html($field['placeholder'] ?? __('Select a state', 'formatrica'));
        $country_field = isset($field['country_field']) && is_string($field['country_field']) ? esc_attr($field['country_field']) : '';
        $allow_text = !empty($field['allow_text_input']);

        $placeholder_option = sprintf('<option value="">%s</option>', $placeholder);

        return sprintf(
            '<div class="formatrica__group"><label class="formatrica__label" for="%1$s">%2$s%3$s</label><select class="formatrica__select formatrica__state" id="%1$s" name="%4$s" %5$s data-country-field="%7$s" data-allow-text="%8$s">%6$s</select><p class="formatrica__error" data-error-for="%4$s" role="alert"></p></div>',
            $id,
            $label,
            $required ? '<span class="formatrica__required" data-required-label="' . esc_attr__('Required', 'formatrica') . '" aria-hidden="true">*</span>' : '',
            $name,
            $required,
            $placeholder_option,
            $country_field,
            $allow_text ? '1' : '0'
        );
    }

    private function sanitize_checkbox_label(string $label): string
    {
        $label = trim($label);

        if ($label === '') {
            return '';
        }

        return wp_kses($label, self::allowed_label_tags());
    }

    /**
     * @return array<string, array<string, bool>>
     */
    private static function allowed_label_tags(): array
    {
        return [
            'a' => [
                'href' => true,
                'title' => true,
                'target' => true,
                'rel' => true,
            ],
        ];
    }

    private function render_button(array $field): string
    {
        $label = esc_html($field['label'] ?? __('Submit', 'formatrica'));
        $spinner = '<span class="formatrica__spinner" aria-hidden="true"></span>';

        return sprintf('<div class="formatrica__actions"><button type="submit" class="formatrica__submit">%s<span class="formatrica__submit-text">%s</span></button></div>', $spinner, $label);
    }

    private function render_custom_text(array $field): string
    {
        $content = isset($field['content']) ? wp_kses_post($field['content']) : '';

        if ($content === '') {
            return '';
        }

        return sprintf('<div class="formatrica__group formatrica__group--static">%s</div>', $content);
    }

    private function render_file(array $field): string
    {
        $id = esc_attr($field['id']);
        $name = esc_attr($field['name']);
        $label = esc_html($field['label'] ?? '');
        $required = !empty($field['required']) ? 'required' : '';
        $accept = isset($field['accept']) && $field['accept'] !== ''
            ? ' accept="' . esc_attr($field['accept']) . '"'
            : '';

        return sprintf(
            '<div class="formatrica__group"><label class="formatrica__label" for="%1$s">%2$s%4$s</label><input class="formatrica__input" type="file" id="%1$s" name="%3$s" %5$s%6$s /><p class="formatrica__error" data-error-for="%3$s" role="alert"></p></div>',
            $id,
            $label,
            $name,
            $required ? '<span class="formatrica__required" data-required-label="' . esc_attr__('Required', 'formatrica') . '" aria-hidden="true">*</span>' : '',
            $required,
            $accept
        );
    }

    private function render_date(array $field): string
    {
        $id = esc_attr($field['id']);
        $name = esc_attr($field['name']);
        $label = esc_html($field['label'] ?? '');
        $required = !empty($field['required']) ? 'required' : '';

        return sprintf(
            '<div class="formatrica__group"><label class="formatrica__label" for="%1$s">%2$s%4$s</label><input class="formatrica__input" type="date" id="%1$s" name="%3$s" %5$s /><p class="formatrica__error" data-error-for="%3$s" role="alert"></p></div>',
            $id,
            $label,
            $name,
            $required ? '<span class="formatrica__required" data-required-label="' . esc_attr__('Required', 'formatrica') . '" aria-hidden="true">*</span>' : '',
            $required
        );
    }

    /**
     * @param array<int, array<string, mixed>> $fields
     */
    private function contains_file_field(array $fields): bool
    {
        foreach ($fields as $field) {
            if (in_array($field['type'] ?? '', ['file', 'multi_file'], true)) {
                return true;
            }
        }

        return false;
    }

    private function render_custom_css(int $form_id, string $css): string
    {
        $css = trim($css);

        if ($css === '') {
            return '';
        }

        $scoped = $this->scope_custom_css($css, $form_id);

        if ($scoped === '') {
            return '';
        }

        if (isset(self::$custom_css_rendered[$form_id]) && self::$custom_css_rendered[$form_id] === $scoped) {
            return '';
        }

        self::$custom_css_rendered[$form_id] = $scoped;

        $scoped = wp_kses_no_null($scoped, ['slash_zero' => 'keep']);
        $scoped = preg_replace('#</style#i', '<\\/style', $scoped);
        $handle = wp_style_is('formatrica-frontend-style', 'enqueued') ? 'formatrica-frontend-style' : 'formatrica-frontend-inline';
        wp_add_inline_style($handle, $scoped);

        return '';
    }

    private function scope_custom_css(string $css, int $form_id): string
    {
        $css = trim($css);

        if ($css === '') {
            return '';
        }

        $prefix = '.formatrica[data-form-id="' . absint($form_id) . '"]';

        $pattern = '/(^|\})\s*([^{}@][^{}]*)\{/m';

        $scoped = preg_replace_callback($pattern, static function (array $matches) use ($prefix) {
            $selectors = array_map('trim', explode(',', $matches[2]));
            $selectors = array_filter($selectors, static fn($selector) => $selector !== '');

            if (!$selectors) {
                return $matches[0];
            }

            $scopedSelectors = array_map(static function ($selector) use ($prefix) {
                if (str_starts_with($selector, $prefix)) {
                    return $selector;
                }

                return $prefix . ' ' . ltrim($selector);
            }, $selectors);

            return $matches[1] . ' ' . implode(', ', $scopedSelectors) . ' {';
        }, $css);

        if (!is_string($scoped)) {
            return $css;
        }

        return $scoped;
    }
}
