<?php

namespace Storeplugin\PDFInvoice\Framework\Abstracts;

defined('ABSPATH') || exit;

use Storeplugin\PDFInvoice\Framework\Contracts\FieldInterface;
use Storeplugin\PDFInvoice\Framework\Fields\Types\CheckboxField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\ColorField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\EmailField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\FileUploadField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\IconPickerField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\NumberField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\RadioField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\RangeField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\Select2Field;
use Storeplugin\PDFInvoice\Framework\Fields\Types\SelectField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\SwitchField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\TelField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\TextField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\TextareaField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\RepeaterField;
use Storeplugin\PDFInvoice\Framework\Fields\Types\WysiwygField;
use Storeplugin\PDFInvoice\Framework\Utils\FieldState;

abstract class AbstractFieldRenderer
{
    /**
     * Per-renderer field type registry keyed by renderer class.
     *
     * @var array<string, array<string, class-string<FieldInterface>>>
     */
    protected static $registry = [];

    /**
     * Cached allowed HTML schema keyed by renderer class.
     *
     * @var array<string, array>
     */
    protected static $allowedHtmlCache = [];

    /**
     * Define the field type map for the renderer.
     *
     * @return array<string, class-string<FieldInterface>>
     */
    abstract protected static function getFieldTypes(): array;

    /**
     * Base field types shared across renderers.
     *
     * @return array<string, class-string<FieldInterface>>
     */
    protected static function getBaseFieldTypes(): array
    {
        return [
            TextField::getType()       => TextField::class,
            TextareaField::getType()   => TextareaField::class,
            NumberField::getType()     => NumberField::class,
            EmailField::getType()      => EmailField::class,
            TelField::getType()        => TelField::class,
            ColorField::getType()      => ColorField::class,
            RangeField::getType()      => RangeField::class,
            SwitchField::getType()     => SwitchField::class,
            RadioField::getType()      => RadioField::class,
            CheckboxField::getType()   => CheckboxField::class,
            SelectField::getType()     => SelectField::class,
            Select2Field::getType()    => Select2Field::class,
            IconPickerField::getType() => IconPickerField::class,
            FileUploadField::getType() => FileUploadField::class,
            WysiwygField::getType()    => WysiwygField::class,
            RepeaterField::getType()   => RepeaterField::class,
        ];
    }

    /**
     * Register a field type handler.
     *
     * @param string $type  Field type identifier.
     * @param string $class Fully-qualified class name implementing FieldInterface.
     * @return void
     */
    public static function register(string $type, string $class): void
    {
        $type = \sanitize_key($type);
        if ($type === '') {
            return;
        }

        if (!class_exists($class)) {
            return;
        }

        if (!is_subclass_of($class, FieldInterface::class)) {
            return;
        }

        $rendererClass = static::class;
        if (!isset(static::$registry[$rendererClass]) || !is_array(static::$registry[$rendererClass])) {
            static::$registry[$rendererClass] = [];
        }

        static::$registry[$rendererClass][$type] = $class;
    }

    /**
     * Render a field's HTML
     *
     * @param array  $field       Field definition
     * @param mixed  $value       Current value
     * @param string $namePrefix Prefix for the input name attribute
     * @param bool   $wrapped     Whether to wrap the field with label and container
     * @param bool   $echo        Whether to echo the output immediately
     * @return string Escaped HTML output (safe to use without additional escaping)
     */
    public static function render($field, $value = null, string $namePrefix = '', $wrapped = true, $echo = true)
    {
        if (!is_array($field) || $namePrefix === '') {
            return '';
        }

        $namePrefix = \sanitize_key($namePrefix);

        $field = static::normalizeField($field);

        if ($value === null && $field['default'] !== '') {
            $value = $field['default'];
        }

        if ($value === null) {
            $value = '';
        }

        $fieldClass = static::resolveFieldClass($field['type']);
        $fieldInstance = new $fieldClass();

        $inputHtml = $fieldInstance->render($field, $value, $namePrefix);

        if (static::shouldRenderPresenceMarker($field)) {
            $inputHtml = sprintf(
                '<input type="hidden" name="%s[_nxtf_rendered_%s]" value="1">',
                \esc_attr($namePrefix),
                \esc_attr($field['id'])
            ) . $inputHtml;
        }

        if (!empty($field['options_when']) && is_array($field['options_when'])) {
            $field['_options_when_value'] = $value;
        }

        $outputHtml = $wrapped ? static::wrap($field, $inputHtml) : $inputHtml;
        $safeHtml = \wp_kses($outputHtml, static::getAllowedHtml($field));

        if ($echo) {
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            printf('%s', $safeHtml);
        }

        return $safeHtml;
    }

    /**
     * Render a sub-field inside a compound field (e.g., repeater).
     *
     * Unlike render(), this method does NOT sanitize the namePrefix
     * (which may contain brackets for nested names), does not echo,
     * and does not run wp_kses (the parent field's render handles that).
     *
     * @param array  $field     Sub-field definition.
     * @param mixed  $value     Current value.
     * @param string $rawPrefix Unsanitized name prefix (may contain brackets).
     * @return string HTML output.
     */
    public static function renderSubField(array $field, $value, string $rawPrefix): string
    {
        if (!is_array($field) || $rawPrefix === '') {
            return '';
        }

        $field = static::normalizeField($field);
        $ancestorRenderPrefix = isset($field['_nxtf_ancestor_render_prefix']) && is_string($field['_nxtf_ancestor_render_prefix'])
            ? $field['_nxtf_ancestor_render_prefix']
            : '';
        unset($field['_nxtf_ancestor_render_prefix']);
        $field['_use_div_label'] = true;
        $field['_render_prefix'] = static::composeRenderPrefix(
            $ancestorRenderPrefix,
            static::buildRawRenderPrefix($rawPrefix)
        );
        $field['_render_id'] = static::composeRenderId($field['id'], $field['_render_prefix']);

        if ($value === null && $field['default'] !== '') {
            $value = $field['default'];
        }

        if ($value === null) {
            $value = '';
        }

        $fieldClass = static::resolveFieldClass($field['type']);
        $fieldInstance = new $fieldClass();

        $inputHtml = $fieldInstance->render($field, $value, $rawPrefix);

        if (static::shouldRenderPresenceMarker($field)) {
            $inputHtml = sprintf(
                '<input type="hidden" name="%s[_nxtf_rendered_%s]" value="1">',
                \esc_attr($rawPrefix),
                \esc_attr($field['id'])
            ) . $inputHtml;
        }

        if (!empty($field['options_when']) && is_array($field['options_when'])) {
            $field['_options_when_value'] = $value;
        }

        return static::wrap($field, $inputHtml);
    }

    /**
     * Normalize and sanitize field definition
     *
     * @param array $field Raw field definition
     * @return array Normalized field with sanitized values
     */
    protected static function normalizeField(array $field): array
    {
        $defaults = [
            'id'          => '',
            'label'       => '',
            'type'        => 'text',
            'placeholder' => '',
            'default'     => '',
            'description' => '',
            'tip'         => '',
            'options'     => [],
        ];

        $field = array_merge($defaults, $field);

        if ($field['id'] === '') {
            $field['id'] = static::generateFallbackId($field);
        } else {
            $field['id'] = \sanitize_key($field['id']);
        }

        static::registerDefaults();
        $registry = static::$registry[static::class] ?? [];
        $type = \sanitize_key((string) $field['type']);
        $field['type'] = ($type !== '' && isset($registry[$type])) ? $type : 'text';
        unset($field['_ancestor_render_prefix']);
        $field['_render_prefix'] = '';
        $field['_render_id'] = static::composeRenderId($field['id']);

        return $field;
    }

    /**
     * Shared wrapper and label logic
     *
     * @param array  $field   Field definition
     * @param string $content Input HTML (already escaped)
     * @return string Escaped HTML
     */
    protected static function wrap(array $field, string $content): string
    {
        $type = $field['type'];
        $useDivLabel = in_array($type, ['checkbox', 'radio', 'switch', 'repeater'], true) || !empty($field['_use_div_label']);
        $fileClass = $type === 'file_upload' ? 'nxtf-image-upload' : 'nxtf-form-field-text';
        $labelText = trim($field['label']);
        $description = isset($field['description']) && is_scalar($field['description'])
            ? trim((string) $field['description'])
            : '';

        if (isset($field['description_callback']) && is_callable($field['description_callback'])) {
            $resolvedDescription = call_user_func($field['description_callback'], $field);
            if (is_scalar($resolvedDescription)) {
                $description = trim((string) $resolvedDescription);
            }
        }
        $tipText = isset($field['tip']) && is_string($field['tip']) ? trim($field['tip']) : '';

        $conditionalAttrs = static::buildConditionalAttrs($field);
        $colStyle = '';
        $labelHtml = '';
        $descriptionHtml = '';
        $tipHtml = '';

        if ($tipText !== '') {
            $tipText = \sanitize_text_field($tipText);
            $tipHtml = sprintf(
                '<span class="nxtf-tip" title="%s" aria-label="%s" tabindex="0">?</span>',
                \esc_attr($tipText),
                \esc_attr($tipText)
            );
        }

        if ($labelText !== '') {
            $labelClass = $useDivLabel ? 'nxtf-label-wrapper' : 'nxtf-label';
            $labelContent = \esc_html($labelText) . $tipHtml;
            $renderId = (string) ($field['_render_id'] ?? $field['id']);

            $labelHtml = $useDivLabel
                ? sprintf(
                    '<div class="%s">%s</div>',
                    \esc_attr($labelClass),
                    $labelContent
                )
                : sprintf(
                    '<div class="nxtf-label-wrapper"><label for="%s" class="%s">%s</label></div>',
                    \esc_attr($renderId),
                    \esc_attr($labelClass),
                    $labelContent
                );
        }

        if ($description !== '') {
            $descriptionHtml = sprintf(
                '<p class="nxtf-label-description">%s</p>',
                \esc_html($description)
            );
        }

        if (isset($field['col_span'])) {
            $colSpan = $field['col_span'] === 'full' ? 12 : (int) $field['col_span'];
            $colSpan = max(1, min(12, $colSpan));
            $colStyle = sprintf(' style="grid-column: span %d"', $colSpan);
        }

        return sprintf(
            '<div class="nxtf-form-field"%s data-field-id="%s"%s>%s<div class="%s">%s%s</div></div>',
            $colStyle,
            \esc_attr($field['id']),
            $conditionalAttrs,
            $labelHtml,
            $fileClass,
            $content,
            $descriptionHtml
        );
    }

    /**
     * Build data-show-when / data-hide-when / data-disable-when attribute string from field config.
     *
     * @param array $field Normalized field definition.
     * @return string HTML attribute fragment (leading space included) or empty string.
     */
    private static function buildConditionalAttrs(array $field): string
    {
        $attrs = '';

        if (!empty($field['show_when']) && is_array($field['show_when'])) {
            $json = \wp_json_encode($field['show_when']);
            if ($json) {
                $attrs .= sprintf(' data-show-when="%s"', \esc_attr($json));
            }
        }

        if (!empty($field['hide_when']) && is_array($field['hide_when'])) {
            $json = \wp_json_encode($field['hide_when']);
            if ($json) {
                $attrs .= sprintf(' data-hide-when="%s"', \esc_attr($json));
            }
        }

        if (!empty($field['disable_when']) && is_array($field['disable_when'])) {
            $json = \wp_json_encode($field['disable_when']);
            if ($json) {
                $attrs .= sprintf(' data-disable-when="%s"', \esc_attr($json));
            }
        }

        if (!empty($field['options_when']) && is_array($field['options_when'])) {
            $owData = $field['options_when'];
            if (!empty($field['options']) && is_array($field['options'])) {
                $owData['options'] = $field['options'];
            }
            if (array_key_exists('_options_when_value', $field)) {
                $owData['value'] = $field['_options_when_value'];
            }
            $json = \wp_json_encode($owData);
            if ($json) {
                $attrs .= sprintf(' data-options-when="%s"', \esc_attr($json));
            }
        }

        return $attrs;
    }

    /**
     * Generate a stable fallback ID when a field ID is missing
     *
     * @param array $field Field definition
     * @return string Sanitized fallback ID
     */
    protected static function generateFallbackId(array $field): string
    {
        if (!empty($field['label']) && is_string($field['label'])) {
            return \sanitize_title($field['label']);
        }

        $seed = \wp_json_encode($field);
        if (!is_string($seed) || $seed === '') {
            $seed = serialize($field);
        }

        return 'field_' . substr(md5($seed), 0, 8);
    }

    /**
     * Build the current raw-prefix-to-DOM-prefix transform for repeater fields.
     *
     * This intentionally preserves the existing output format, including the
     * template placeholder markers used by repeater row cloning.
     *
     * @param string $rawPrefix
     * @return string
     */
    protected static function buildRawRenderPrefix(string $rawPrefix): string
    {
        return str_replace(['[', ']'], ['_', ''], $rawPrefix);
    }

    /**
     * Compose the effective DOM id prefix for the current render context.
     *
     * Nested repeaters intentionally retain the current cumulative prefix
     * behavior so existing markup and JS expectations remain unchanged.
     *
     * @param string $ancestorPrefix
     * @param string $rawRenderPrefix
     * @return string
     */
    protected static function composeRenderPrefix(string $ancestorPrefix = '', string $rawRenderPrefix = ''): string
    {
        if ($ancestorPrefix !== '' && $rawRenderPrefix !== '') {
            return $ancestorPrefix . '_' . $rawRenderPrefix;
        }

        return $ancestorPrefix !== '' ? $ancestorPrefix : $rawRenderPrefix;
    }

    /**
     * Compose the final DOM id for a field within the current render context.
     *
     * @param string $logicalId
     * @param string $renderPrefix
     * @return string
     */
    protected static function composeRenderId(string $logicalId, string $renderPrefix = ''): string
    {
        return $renderPrefix !== '' ? $renderPrefix . '_' . $logicalId : $logicalId;
    }

    /**
     * Allowed HTML for field output.
     *
     * @param array $field Normalized field definition.
     * @return array
     */
    protected static function getAllowedHtml(array $field): array
    {
        $rendererClass = static::class;

        if (!isset(static::$allowedHtmlCache[$rendererClass]) || !is_array(static::$allowedHtmlCache[$rendererClass])) {
            static::$allowedHtmlCache[$rendererClass] = [
                'div' => [
                    'class' => true,
                    'id' => true,
                    'style' => true,
                    'data-field-id' => true,
                    'data-show-when' => true,
                    'data-hide-when' => true,
                    'data-disable-when' => true,
                    'data-range-wrapper' => true,
                    'data-color-wrapper' => true,
                    'data-icon-preview' => true,
                    'data-icon-grid' => true,
                    'data-icon-dropdown' => true,
                    'data-file-upload' => true,
                    'data-file-has-preview' => true,
                    'data-file-preview-kind' => true,
                    'data-file-empty-label' => true,
                    'data-file-empty-meta' => true,
                    'data-file-title' => true,
                    'data-file-button-text' => true,
                    'data-file-library' => true,
                    'data-file-extensions' => true,
                    'data-file-mime-types' => true,
                    'data-file-invalid-message' => true,
                    'data-file-max-bytes' => true,
                    'data-file-max-label' => true,
                    'data-file-invalid-size-message' => true,
                    'data-repeater' => true,
                    'data-repeater-prefix' => true,
                    'data-repeater-max' => true,
                    'data-repeater-block-label' => true,
                    'data-repeater-next-index' => true,
                    'data-repeater-depth' => true,
                    'data-repeater-rows' => true,
                    'data-options-when' => true,
                    'data-repeater-row' => true,
                    'data-repeater-index' => true,
                    'data-repeater-fields' => true,
                    'aria-hidden' => true,
                    'aria-label' => true,
                    'aria-describedby' => true,
                    'aria-labelledby' => true,
                ],
                'template' => [
                    'data-repeater-tpl' => true,
                ],
                'label' => [
                    'class' => true,
                    'for' => true,
                    'id' => true,
                    'aria-hidden' => true,
                    'aria-label' => true,
                    'aria-describedby' => true,
                    'aria-labelledby' => true,
                ],
                'input' => [
                    'type' => true,
                    'id' => true,
                    'name' => true,
                    'value' => true,
                    'class' => true,
                    'placeholder' => true,
                    'maxlength' => true,
                    'minlength' => true,
                    'autocomplete' => true,
                    'min' => true,
                    'max' => true,
                    'step' => true,
                    'checked' => true,
                    'disabled' => true,
                    'readonly' => true,
                    'required' => true,
                    'data-range-input' => true,
                    'data-color-input' => true,
                    'data-icon-picker-input' => true,
                    'data-icon-search' => true,
                    'data-file-input' => true,
                    'data-wysiwyg' => true,
                    'data-wysiwyg-settings' => true,
                    'aria-hidden' => true,
                    'aria-label' => true,
                    'aria-describedby' => true,
                    'aria-labelledby' => true,
                ],
                'textarea' => [
                    'id' => true,
                    'name' => true,
                    'rows' => true,
                    'class' => true,
                    'placeholder' => true,
                    'disabled' => true,
                    'readonly' => true,
                    'required' => true,
                    'data-wysiwyg' => true,
                    'data-wysiwyg-settings' => true,
                    'aria-hidden' => true,
                    'aria-label' => true,
                    'aria-describedby' => true,
                    'aria-labelledby' => true,
                ],
                'select' => [
                    'id' => true,
                    'name' => true,
                    'class' => true,
                    'autocomplete' => true,
                    'multiple' => true,
                    'disabled' => true,
                    'required' => true,
                    'data-select2' => true,
                    'data-select2-search' => true,
                    'data-select2-placeholder' => true,
                    'data-nxtf-disabled-options' => true,
                    'data-nxtf-select-placeholder' => true,
                    'data-nxtf-select2' => true,
                    'data-nxtf-select2-search' => true,
                    'data-nxtf-select2-placeholder' => true,
                    'aria-hidden' => true,
                    'aria-label' => true,
                    'aria-describedby' => true,
                    'aria-labelledby' => true,
                ],
                'option' => [
                    'value' => true,
                    'selected' => true,
                    'disabled' => true,
                    'title' => true,
                ],
                'span' => [
                    'class' => true,
                    'style' => true,
                    'data-range-value' => true,
                    'data-color-value' => true,
                    'data-icon-label' => true,
                    'data-icon-arrow' => true,
                    'data-repeater-handle' => true,
                    'data-repeater-label' => true,
                    'title' => true,
                    'tabindex' => true,
                    'aria-hidden' => true,
                    'aria-label' => true,
                    'aria-describedby' => true,
                    'aria-labelledby' => true,
                ],
                'p' => [
                    'class' => true,
                ],
                'button' => [
                    'type' => true,
                    'class' => true,
                    'id' => true,
                    'style' => true,
                    'data-icon' => true,
                    'data-icon-item' => true,
                    'data-icon-clear' => true,
                    'data-icon-toggle' => true,
                    'data-file-button' => true,
                    'data-file-preview' => true,
                    'data-file-remove' => true,
                    'data-repeater-add' => true,
                    'data-repeater-remove' => true,
                    'data-repeater-toggle' => true,
                    'title' => true,
                    'disabled' => true,
                    'aria-hidden' => true,
                    'aria-label' => true,
                ],
                'code' => [
                    'class' => true,
                    'style' => true,
                    'data-icon-name' => true,
                ],
            ];
        }

        $allowedHtml = static::$allowedHtmlCache[$rendererClass];

        if (\has_filter('nxtpdf_field_renderer_allowed_html') === false) {
            return $allowedHtml;
        }

        return \apply_filters('nxtpdf_field_renderer_allowed_html', $allowedHtml, $field);
    }

    /**
     * Register default field types.
     *
     * @return void
     */
    protected static function registerDefaults(): void
    {
        $defaults = static::getFieldTypes();
        $rendererClass = static::class;

        if (!isset(static::$registry[$rendererClass]) || !is_array(static::$registry[$rendererClass])) {
            static::$registry[$rendererClass] = [];
        }

        foreach ($defaults as $type => $class) {
            if (!isset(static::$registry[$rendererClass][$type])) {
                static::register($type, $class);
            }
        }
    }

    /**
     * Resolve a field handler class name.
     *
     * @param string $type Field type.
     * @return class-string<FieldInterface>
     */
    protected static function resolveFieldClass(string $type): string
    {
        static::registerDefaults();
        $registry = static::$registry[static::class] ?? [];

        $type = \sanitize_key($type);
        if ($type !== '' && isset($registry[$type])) {
            return $registry[$type];
        }

        return $registry['text'] ?? TextField::class;
    }

    /**
     * Whether the field should emit the hidden rendered marker used by save handlers.
     *
     * Checkbox, switch, and repeater fields always emit rendered markers so save
     * handlers can distinguish "rendered but omitted by the browser" from "never
     * loaded / not part of this request". Locked save paths preserve existing
     * values first, so the marker only affects true first-write empty states.
     *
     * @param array $field
     * @return bool
     */
    protected static function shouldRenderPresenceMarker(array $field): bool
    {
        $type = (string) ($field['type'] ?? '');
        if (!in_array($type, ['checkbox', 'switch', 'repeater'], true)) {
            return false;
        }

        return true;
    }
}
