<?php

// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedNamespaceFound -- APD is the established namespace shared with the Pro add-on and retained for backward compatibility.
namespace APD\Admin;

use APD\PostTypes\ProductConfigPostType;
use APD\PostTypes\TemplatePostType;
use APD\Repositories\TemplateRepository;
use APD\Support\AssetCatalog;
use APD\Support\DesignerAssets;
use APD\Support\Json;
use APD\Support\LayoutStateConverter;
use APD\Support\RequestData;
use APD\Theme\DesignerTheme;
use APD\PostTypes\ThemePostType;

defined('ABSPATH') || exit;

class ProductConfigEditor
{
    private array $fields = [
        '_apd_mode' => ['label' => 'Mode', 'type' => 'select', 'options' => ['standalone', 'customer', 'admin']],
        '_apd_theme' => ['label' => 'Theme JSON', 'type' => 'json', 'rows' => 12],
        '_apd_layout' => ['label' => 'Layout JSON', 'type' => 'json', 'rows' => 12],
        '_apd_modules' => ['label' => 'Modules JSON', 'type' => 'json', 'rows' => 16],
        '_apd_permissions' => ['label' => 'Permissions JSON', 'type' => 'json', 'rows' => 12],
        '_apd_views' => ['label' => 'Views JSON', 'type' => 'json', 'rows' => 14],
        '_apd_drawing_zones' => ['label' => 'Drawing zones JSON', 'type' => 'json', 'rows' => 12],
        '_apd_fonts' => ['label' => 'Fonts JSON', 'type' => 'json', 'rows' => 10],
        '_apd_upload' => ['label' => 'Upload JSON', 'type' => 'json', 'rows' => 10],
        '_apd_colors' => ['label' => 'Colors JSON', 'type' => 'json', 'rows' => 10],
        '_apd_pricing_rules' => ['label' => 'Pricing rules JSON', 'type' => 'json', 'rows' => 12],
        '_apd_selection_menu' => ['label' => 'Selection menu JSON', 'type' => 'json', 'rows' => 8],
        '_apd_ui' => ['label' => 'UI JSON', 'type' => 'json', 'rows' => 8],
        '_apd_front_behavior' => ['label' => 'Front behavior JSON', 'type' => 'json', 'rows' => 8],
        '_apd_mobile_behavior' => ['label' => 'Mobile behavior JSON', 'type' => 'json', 'rows' => 8],
        '_apd_initial_document' => ['label' => 'Initial document JSON', 'type' => 'json', 'rows' => 14],
        '_apd_initial_state' => ['label' => 'Initial state JSON', 'type' => 'json', 'rows' => 14],
    ];

    public function registerMetaBoxes() : void
    {
        add_meta_box('apd_config_builder', __('Product builder', 'asuka-product-designer'), [$this, 'renderBuilder'], ProductConfigPostType::TYPE, 'normal', 'high');
        add_meta_box('apd_client_permissions', __('Client permissions', 'asuka-product-designer'), [$this, 'renderPermissionsBox'], ProductConfigPostType::TYPE, 'side', 'high');
        add_meta_box('apd_front_validation', __('Validation guards', 'asuka-product-designer'), [$this, 'renderFrontBehaviorBox'], ProductConfigPostType::TYPE, 'side', 'high');
        add_meta_box('apd_catalog_filters', __('Catalog filters', 'asuka-product-designer'), [$this, 'renderCatalogFilters'], ProductConfigPostType::TYPE, 'side', 'default');
        add_meta_box('apd_pricing_rule_filters', __('Pricing rules', 'asuka-product-designer'), [$this, 'renderPricingRules'], ProductConfigPostType::TYPE, 'side', 'default');
        add_meta_box('apd_config_audit', __('Config audit', 'asuka-product-designer'), [$this, 'renderAuditBox'], ProductConfigPostType::TYPE, 'side', 'low');
        add_meta_box('apd_template_assignment', __('Template', 'asuka-product-designer'), [$this, 'renderTemplateBox'], ProductConfigPostType::TYPE, 'side', 'high');
        add_meta_box('apd_designer_theme_assignment', __('Designer theme', 'asuka-product-designer'), [$this, 'renderThemeAssignmentBox'], ProductConfigPostType::TYPE, 'side', 'high');
        add_meta_box('apd_view_pagination', __('Canvas controls', 'asuka-product-designer'), [$this, 'renderViewPaginationBox'], ProductConfigPostType::TYPE, 'side', 'default');

        add_meta_box('apd_template_modules', __('Template workspace', 'asuka-product-designer'), [$this, 'renderTemplateModulesBox'], TemplatePostType::TYPE, 'normal', 'high');
        add_meta_box('apd_template_builder', __('Live preview', 'asuka-product-designer'), [$this, 'renderBuilder'], TemplatePostType::TYPE, 'normal', 'default');
        add_meta_box('apd_designer_theme_assignment', __('Designer theme', 'asuka-product-designer'), [$this, 'renderThemeAssignmentBox'], TemplatePostType::TYPE, 'side', 'high');
    }

    public function columns(array $columns) : array
    {
        $beforeDate = [];
        foreach ($columns as $key => $label) {
            if ($key === 'date') {
                $beforeDate['apd_mode'] = __('Mode', 'asuka-product-designer');
                $beforeDate['apd_views'] = __('Views', 'asuka-product-designer');
            }
            $beforeDate[$key] = $label;
        }
        return $beforeDate;
    }

    public function renderColumn(string $column, int $postId) : void
    {
        if ($column === 'apd_mode') {
            echo esc_html($this->translateModeOption((string) get_post_meta($postId, '_apd_mode', true) ?: 'standalone'));
        }
        if ($column === 'apd_views') {
            $views = (string) get_post_meta($postId, '_apd_views', true);
            $decoded = json_decode($views, true);
            echo is_array($decoded) ? esc_html((string) count($decoded)) : '—';
        }
    }

    public function save(int $postId) : void
    {
        $post = RequestData::post();
        if (! isset($post['apd_config_nonce']) || ! wp_verify_nonce(sanitize_text_field((string) $post['apd_config_nonce']), 'apd_save_config')) {
            return;
        }

        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return;
        }

        if (! current_user_can('edit_post', $postId)) {
            return;
        }

        $postType = (string) get_post_type($postId);
        $invalidJsonFields = [];
        $this->debugSaveLog('start', $postId, [
            'postType' => $postType,
            'posted' => $this->summarizePostedSaveFields(['_apd_initial_document', '_apd_initial_state', '_apd_views', '_apd_drawing_zones', '_apd_template_id'], $post),
        ]);

        foreach ($this->fields as $key => $field) {
            if ($postType === ProductConfigPostType::TYPE && $key === '_apd_layout') {
                delete_post_meta($postId, '_apd_layout');
                continue;
            }

            $raw = $post[$key] ?? null;
            if ($raw === null) {
                continue;
            }

            $value = is_string($raw) ? $raw : '';
            if ($field['type'] === 'json') {
                $normalized = $this->sanitizeJson($value);
                if ($normalized === null) {
                    $invalidJsonFields[] = $this->translateFieldLabel((string) ($field['label'] ?? $key));
                    continue;
                }
                if ($postType === ProductConfigPostType::TYPE && $key === '_apd_initial_document') {
                    $postedState = isset($post['_apd_initial_state']) && is_string($post['_apd_initial_state']) ? $post['_apd_initial_state'] : '';
                    $normalized = $this->protectInitialDocumentFromInactiveEmptyViews(
                        $normalized,
                        (string) get_post_meta($postId, '_apd_initial_document', true),
                        (string) $postedState
                    );
                }
                if ($key === '_apd_initial_document') {
                    $normalized = $this->compactInitialDocumentForStorage($normalized);
                } elseif ($key === '_apd_initial_state') {
                    $normalized = $this->compactInitialStateForStorage($normalized);
                }
                $this->updateJsonPostMeta($postId, $key, $normalized);
                continue;
            }

            $value = sanitize_text_field($value);
            update_post_meta($postId, $key, $value);
        }

        $this->saveThemeAssignment($postId, $post);

        if ($postType === ProductConfigPostType::TYPE) {
            delete_post_meta($postId, '_apd_layout');
            $templateId = isset($post['_apd_template_id']) ? absint($post['_apd_template_id']) : 0;
            if ($templateId > 0 && ! $this->templateExists($templateId)) {
                $templateId = 0;
            }
            if ($templateId > 0) {
                update_post_meta($postId, '_apd_template_id', $templateId);
            } else {
                delete_post_meta($postId, '_apd_template_id');
            }

            if (isset($post['apd_permissions_structured']) && is_array($post['apd_permissions_structured'])) {
                $permissions = $this->buildPermissionsPayload($post['apd_permissions_structured'], (string) get_post_meta($postId, '_apd_permissions', true));
                $this->updateJsonPostMeta($postId, '_apd_permissions', (string) wp_json_encode($permissions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
            }

            if (isset($post['apd_front_behavior_structured']) && is_array($post['apd_front_behavior_structured'])) {
                $frontBehavior = $this->buildFrontBehaviorPayload($post['apd_front_behavior_structured'], (string) get_post_meta($postId, '_apd_front_behavior', true));
                $this->updateJsonPostMeta($postId, '_apd_front_behavior', (string) wp_json_encode($frontBehavior, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
            }

            if (isset($post['apd_ui_structured']) && is_array($post['apd_ui_structured'])) {
                $ui = $this->buildUiPayload($post['apd_ui_structured'], (string) get_post_meta($postId, '_apd_ui', true));
                if ($ui !== []) {
                    $this->updateJsonPostMeta($postId, '_apd_ui', (string) wp_json_encode($ui, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
                } else {
                    delete_post_meta($postId, '_apd_ui');
                }
            }

            if (isset($post['apd_asset_filters']) && is_array($post['apd_asset_filters'])) {
                $filters = (new AssetCatalog())->normalizeFilters($post['apd_asset_filters']);
                $this->updateJsonPostMeta($postId, '_apd_asset_filters', (string) wp_json_encode($filters, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
            }

            $pricingRuleIds = [];
            if (isset($post['apd_pricing_rule_ids'])) {
                $pricingRuleIds = (new AssetCatalog())->normalizeSelectedPricingRuleIds($post['apd_pricing_rule_ids']);
            }
            if ($pricingRuleIds !== []) {
                $this->updateJsonPostMeta($postId, '_apd_pricing_rule_ids', (string) wp_json_encode($pricingRuleIds, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
            } else {
                delete_post_meta($postId, '_apd_pricing_rule_ids');
            }

            $this->syncDrawingZonesIntoBootPayloads($postId);
            $this->syncInitialStateDocumentFromInitialDocument($postId);
            $this->syncViewsFromInitialDocument($postId);
        }

        if ($postType === TemplatePostType::TYPE && isset($post['apd_template_modules_structured']) && is_array($post['apd_template_modules_structured'])) {
            $sharedPortalSelector = $this->normalizeTemplatePortalSelector(
                isset($post['apd_template_shared_portal_selector'])
                    ? (string) $post['apd_template_shared_portal_selector']
                    : ''
            );
            $editorState = $this->decodeJsonString((string) get_post_meta($postId, '_apd_template_editor_state', true));
            $editorState['sharedPortalSelector'] = $sharedPortalSelector;

            $existingTemplateSettings = $this->templateModuleSettings($postId, $sharedPortalSelector);
            $settings = $this->normalizeSubmittedTemplateModules(
                $post['apd_template_modules_structured'],
                $sharedPortalSelector,
                $existingTemplateSettings
            );
            $editorState['modulePlacements'] = [];
            foreach ($settings as $moduleId => $moduleSettings) {
                $editorState['modulePlacements'][$moduleId] = [
                    'layout' => ! empty($moduleSettings['layoutMenu']),
                    'selection' => ! empty($moduleSettings['selectionMenu']),
                    'page' => ! empty($moduleSettings['pageContent']),
                ];
            }
            $this->updateJsonPostMeta(
                $postId,
                '_apd_template_editor_state',
                (string) wp_json_encode($editorState, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
            );
            $structuredLayout = $this->buildTemplateLayoutPayload($settings, (string) get_post_meta($postId, '_apd_layout', true), $post['apd_template_zone_settings'] ?? null);

            // The template preview can be rearranged directly with AsukaDesigner's
            // drag/grab layout editor. In that flow the hidden _apd_layout field is
            // the canonical save payload because the right sidebar controls are not
            // updated while the user drags menu blocks inside the preview. Do not let
            // an older _apd_initial_state payload re-apply a stale layout over it.
            $postedLayout = $this->decodePostedJsonField('_apd_layout', $post);
            $layout = $postedLayout !== []
                ? $this->mergePostedTemplateLayoutWithStructuredSettings($postedLayout, $structuredLayout)
                : LayoutStateConverter::extractLayoutConfigFromState($this->decodePostedJsonField('_apd_initial_state', $post), $structuredLayout);

            $postedState = $this->decodePostedJsonField('_apd_initial_state', $post);
            if ($postedState !== []) {
                $stateWithoutRuntimeLayout = LayoutStateConverter::stripLayoutFromState($postedState);
                $this->updateJsonPostMeta($postId, '_apd_initial_state', (string) wp_json_encode($stateWithoutRuntimeLayout, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
            }

            $this->updateJsonPostMeta($postId, '_apd_layout', (string) wp_json_encode($layout, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));

            $permissions = $this->buildTemplatePermissionsPayload($settings, (string) get_post_meta($postId, '_apd_permissions', true));
            $this->updateJsonPostMeta($postId, '_apd_permissions', (string) wp_json_encode($permissions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));

            $selectionMenu = $this->buildTemplateSelectionMenuPayload($settings, (string) get_post_meta($postId, '_apd_selection_menu', true));
            $this->updateJsonPostMeta($postId, '_apd_selection_menu', (string) wp_json_encode($selectionMenu, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        }

        $this->debugSaveLog('after-update', $postId, [
            'postType' => $postType,
            'invalidJsonFields' => $invalidJsonFields,
            'stored' => $this->summarizeStoredSaveFields($postId, ['_apd_initial_document', '_apd_initial_state', '_apd_views', '_apd_drawing_zones', '_apd_template_id']),
        ]);

        if ($invalidJsonFields !== []) {
            AdminNoticeStore::push(
                'post_' . $postId,
                sprintf(
                    /* translators: %s: comma-separated list of JSON field labels. */
                    __('Some JSON fields were not saved because they contain invalid JSON: %s.', 'asuka-product-designer'),
                    implode(', ', array_unique($invalidJsonFields))
                ),
                'error'
            );
        }
    }


    private function debugSaveLog(string $phase, int $postId, array $payload = []) : void
    {
        if (! defined('WP_DEBUG') || ! WP_DEBUG) {
            return;
        }

        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- The apd_ hook is a documented integration API retained for backward compatibility.
        do_action('apd_admin_save_debug_log', [
            'phase' => $phase,
            'postId' => $postId,
            'payload' => $payload,
        ]);
    }

    /**
     * @param array<int, string> $keys
     * @return array<string, array<string, mixed>>
     */
    private function summarizePostedSaveFields(array $keys, array $post) : array
    {
        $summary = [];
        foreach ($keys as $key) {
            $raw = $post[$key] ?? null;
            if ($raw === null) {
                $summary[$key] = ['posted' => false, 'length' => 0, 'validJson' => null];
                continue;
            }
            $value = is_string($raw) ? $raw : wp_json_encode($raw);
            $summary[$key] = $this->summarizeJsonishValue($value);
            $summary[$key]['posted'] = true;
        }

        return $summary;
    }

    /**
     * @param array<int, string> $keys
     * @return array<string, array<string, mixed>>
     */
    private function summarizeStoredSaveFields(int $postId, array $keys) : array
    {
        $summary = [];
        foreach ($keys as $key) {
            $value = (string) get_post_meta($postId, $key, true);
            $summary[$key] = $this->summarizeJsonishValue($value);
        }

        return $summary;
    }

    /**
     * @return array<string, mixed>
     */
    private function summarizeJsonishValue(string $value) : array
    {
        $trimmed = trim($value);
        $decoded = $trimmed !== '' ? json_decode($trimmed, true) : null;
        $validJson = $trimmed === '' ? null : json_last_error() === JSON_ERROR_NONE;
        $viewsCount = 0;
        $topKeys = [];
        if (is_array($decoded)) {
            $topKeys = array_slice(array_keys($decoded), 0, 12);
            if (isset($decoded['views']) && is_array($decoded['views'])) {
                $viewsCount = isset($decoded['views']['views']) && is_array($decoded['views']['views']) ? count($decoded['views']['views']) : count($decoded['views']);
            }
        }

        return [
            'length' => strlen($value),
            'trimmedLength' => strlen($trimmed),
            'validJson' => $validJson,
            'topKeys' => $topKeys,
            'viewsCount' => $viewsCount,
        ];
    }

    public function renderAdminNotices() : void
    {
        $screen = function_exists('get_current_screen') ? get_current_screen() : null;
        if (! $screen || ! in_array($screen->post_type, [ProductConfigPostType::TYPE, TemplatePostType::TYPE], true)) {
            return;
        }

        $query = RequestData::query();
        $postId = absint($query['post'] ?? 0);
        if ($postId <= 0) {
            return;
        }

        foreach (AdminNoticeStore::consume('post_' . $postId) as $notice) {
            $type = sanitize_html_class((string) ($notice['type'] ?? 'error'));
            $message = (string) ($notice['message'] ?? '');
            if ($message === '') {
                continue;
            }

            echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p>' . esc_html($message) . '</p></div>';
        }
    }

    public function renderBuilder(\WP_Post $post) : void
    {
        if ($post->post_type === ProductConfigPostType::TYPE) {
            $this->repairStoredCanonicalPayloads($post->ID);
        }
        $liveAssetCatalog = $this->buildLiveAssetCatalogPayload($post->ID);
        wp_nonce_field('apd_save_config', 'apd_config_nonce');
        ?>
        <div class="apd-builder-page<?php echo esc_attr($post->post_type === TemplatePostType::TYPE ? ' apd-template-builder-page' : ''); ?>" data-apd-product-config-editor="<?php echo esc_attr($post->post_type === ProductConfigPostType::TYPE ? '1' : '0'); ?>" data-apd-template-editor="<?php echo esc_attr($post->post_type === TemplatePostType::TYPE ? '1' : '0'); ?>">
            <div class="apd-builder-stage-shell js-apd-live-preview" data-post-id="<?php echo esc_attr((string) $post->ID); ?>">
                <div class="apd-preview-stage apd-builder-stage" id="apd-admin-preview-<?php echo esc_attr((string) $post->ID); ?>"></div>

                <div class="apd-visually-hidden">
                    <select class="js-apd-starter-template" aria-hidden="true" tabindex="-1">
                        <option value="blank"><?php esc_html_e('Blank starter', 'asuka-product-designer'); ?></option>
                    </select>
                    <input type="text" class="js-apd-preview-height" value="820px" aria-hidden="true" tabindex="-1">
                    <button type="button" class="js-apd-apply-starter" aria-hidden="true" tabindex="-1"><?php esc_html_e('Apply starter', 'asuka-product-designer'); ?></button>
                    <button type="button" class="js-apd-refresh-preview" aria-hidden="true" tabindex="-1"><?php esc_html_e('Refresh canvas', 'asuka-product-designer'); ?></button>
                    <span class="js-apd-preview-status" aria-hidden="true"><?php esc_html_e('Canvas ready', 'asuka-product-designer'); ?></span>
                </div>
            </div>

            <div class="apd-visually-hidden apd-advanced-fields">
                <?php foreach (array_keys($this->fields) as $key) : ?>
                    <?php $this->renderHiddenField($post->ID, $key); ?>
                <?php endforeach; ?>
            </div>
            <script type="application/json" data-apd-live-asset-catalog><?php echo wp_json_encode($liveAssetCatalog, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES); ?></script>
        </div>
        <?php
    }


    /**
     * The global asset library is intentionally resolved at render time.
     * Product meta stores filters and document data, not stale catalog copies.
     */
    private function buildLiveAssetCatalogPayload(int $postId) : array
    {
        $catalog = new AssetCatalog();
        $filters = $catalog->normalizeFilters((string) get_post_meta($postId, '_apd_asset_filters', true));
        $payload = $catalog->buildPayload($filters);

        return [
            'schemaVersion' => (string) ($payload['schemaVersion'] ?? ''),
            'catalog' => [
                'designs' => array_values((array) ($payload['designs'] ?? [])),
                'elements' => array_values((array) ($payload['shapes'] ?? [])),
                'colors' => array_values((array) ($payload['colors'] ?? [])),
                'printingMethods' => array_values((array) ($payload['printingMethods'] ?? [])),
            ],
            'fonts' => array_values((array) ($payload['fonts'] ?? [])),
        ];
    }


    public function renderTemplateBox(\WP_Post $post) : void
    {
        $templates = (new TemplateRepository())->allForSelect();
        $currentTemplateId = (int) get_post_meta($post->ID, '_apd_template_id', true);

        echo '<div class="apd-filter-box apd-template-picker" data-apd-template-picker="1">';
        echo '<p>' . esc_html__('Choose a reusable template, then apply it to graft its designer JSON onto this custom product.', 'asuka-product-designer') . '</p>';
        echo '<label class="apd-field">';
        echo '<span>' . esc_html__('Assigned template', 'asuka-product-designer') . '</span>';
        echo '<select name="_apd_template_id" class="js-apd-template-select">';
        echo '<option value="0">' . esc_html__('No template', 'asuka-product-designer') . '</option>';
        foreach ($templates as $template) {
            $id = (int) ($template['id'] ?? 0);
            if ($id <= 0) {
                continue;
            }
            echo '<option value="' . esc_attr((string) $id) . '" ' . selected($currentTemplateId, $id, false) . '>' . esc_html((string) ($template['title'] ?? ('#' . $id))) . '</option>';
        }
        echo '</select>';
        echo '</label>';
        echo '<div class="apd-inline-actions apd-inline-actions-top">';
        echo '<button type="button" class="button-link js-apd-clear-template">' . esc_html__('Clear template selection', 'asuka-product-designer') . '</button>';
        echo '</div>';
        echo '<p class="description apd-description-top">' . esc_html__('The selected template is used on the frontend. This admin preview keeps the default designer layout.', 'asuka-product-designer') . '</p>';
        foreach ($templates as $template) {
            $id = (int) ($template['id'] ?? 0);
            if ($id <= 0) {
                continue;
            }
            echo '<script type="application/json" data-apd-template-payload="' . esc_attr((string) $id) . '">' . wp_json_encode(
                $this->templateApplyPayload($id),
                JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
            ) . '</script>';
        }
        echo '</div>';
    }

    public function renderThemeAssignmentBox(\WP_Post $post) : void
    {
        $service = new DesignerTheme();
        $themes = $service->choices();
        $defaultThemeId = $service->defaultThemeId();
        $payload = $this->readJsonMetaArray($post->ID, '_apd_theme');
        $assignedThemeId = sanitize_key((string) ($payload['themeId'] ?? ''));
        $hasEmbeddedTheme = $assignedThemeId === '' && (
            isset($payload['preset'])
            || (is_array($payload['colors'] ?? null) && $payload['colors'] !== [])
        );
        $selection = $service->hasTheme($assignedThemeId)
            ? $assignedThemeId
            : ($hasEmbeddedTheme ? '__embedded' : '__inherit');
        $previewThemeId = $selection === '__inherit' ? $defaultThemeId : $assignedThemeId;
        $previewTheme = $service->published($previewThemeId);
        $themeUrl = $service->hasTheme($previewThemeId)
            ? get_edit_post_link((int) $previewThemeId, '')
            : admin_url('edit.php?post_type=' . ThemePostType::TYPE);
        ?>
        <div class="apd-product-theme-picker" data-apd-product-theme-picker data-apd-theme-following-label="<?php echo esc_attr__('Following store default', 'asuka-product-designer'); ?>" data-apd-theme-fixed-label="<?php echo esc_attr__('Fixed assignment', 'asuka-product-designer'); ?>" data-apd-theme-embedded-label="<?php echo esc_attr__('Embedded custom theme', 'asuka-product-designer'); ?>">
            <p><?php esc_html_e('Use the store default or pin this item to a reusable theme from the library.', 'asuka-product-designer'); ?></p>
            <script type="application/json" data-apd-product-theme-options><?php echo wp_json_encode($themes, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES); ?></script>
            <label class="apd-field">
                <span><?php esc_html_e('Theme assignment', 'asuka-product-designer'); ?></span>
                <select name="apd_designer_theme_assignment" data-apd-product-theme-select>
                    <option value="__inherit" <?php selected($selection, '__inherit'); ?>><?php echo esc_html(sprintf(/* translators: Placeholder values are populated from the current administration record. */ __('Store default — %s', 'asuka-product-designer'), (string) $service->published($defaultThemeId)['name'])); ?></option>
                    <?php foreach ($themes as $theme) : ?>
                        <option value="<?php echo esc_attr((string) $theme['id']); ?>" <?php selected($selection, (string) $theme['id']); ?>><?php echo esc_html((string) $theme['name']); ?><?php echo ! empty($theme['isDefault']) ? ' · ' . esc_html__('default', 'asuka-product-designer') : ''; ?></option>
                    <?php endforeach; ?>
                    <?php if ($hasEmbeddedTheme) : ?><option value="__embedded" <?php selected($selection, '__embedded'); ?>><?php esc_html_e('Embedded custom theme (legacy)', 'asuka-product-designer'); ?></option><?php endif; ?>
                </select>
            </label>
            <div class="apd-product-theme-preview" data-apd-product-theme-preview style="--theme-bg: <?php echo esc_attr((string) ($previewTheme['colors']['layout.appBg'] ?? '#f5f7fb')); ?>; --theme-panel: <?php echo esc_attr((string) ($previewTheme['colors']['layout.panelBg'] ?? '#ffffff')); ?>; --theme-text: <?php echo esc_attr((string) ($previewTheme['colors']['text.primary'] ?? '#0f172a')); ?>; --theme-accent: <?php echo esc_attr((string) ($previewTheme['colors']['accent.primary'] ?? '#2563eb')); ?>;">
                <span><i></i><i></i><i></i></span>
                <div><strong data-apd-product-theme-name><?php echo esc_html((string) $previewTheme['name']); ?></strong><small data-apd-product-theme-mode><?php echo esc_html($selection === '__inherit' ? __('Following store default', 'asuka-product-designer') : __('Fixed assignment', 'asuka-product-designer')); ?></small></div>
            </div>
            <div class="apd-inline-actions apd-inline-actions-top">
                <a class="button-link" href="<?php echo esc_url($themeUrl); ?>" data-apd-product-theme-edit><?php esc_html_e('Edit selected theme', 'asuka-product-designer'); ?></a>
                <a class="button-link" href="<?php echo esc_url(admin_url('edit.php?post_type=' . ThemePostType::TYPE)); ?>"><?php esc_html_e('Manage themes', 'asuka-product-designer'); ?></a>
            </div>
            <p class="description"><?php esc_html_e('Assignments reference the published theme, so future theme updates propagate without duplicating configuration data.', 'asuka-product-designer'); ?></p>
        </div>
        <?php
    }

    public function renderTemplateModulesBox(\WP_Post $post) : void
    {
        $definitions = $this->templateModuleDefinitions();
        $modulePermissionRequirements = $this->templateModulePermissionRequirements();
        $editorState = $this->decodeJsonString((string) get_post_meta($post->ID, '_apd_template_editor_state', true));
        $sharedPortalSelector = $this->normalizeTemplatePortalSelector((string) ($editorState['sharedPortalSelector'] ?? '#asuka-module-portal'));
        $settings = $this->templateModuleSettings($post->ID, $sharedPortalSelector);
        $zoneOptions = [
            'left' => __('Left', 'asuka-product-designer'),
            'right' => __('Right', 'asuka-product-designer'),
            'top' => __('Top', 'asuka-product-designer'),
            'bottom' => __('Bottom', 'asuka-product-designer'),
        ];
        $zoneTypeOptions = [
            'default' => __('Default', 'asuka-product-designer'),
            'panels' => __('Panels', 'asuka-product-designer'),
            'inline' => __('Inline', 'asuka-product-designer'),
        ];
        $moduleTypeOptions = [
            'floating' => __('Floating', 'asuka-product-designer'),
            'popover' => __('Popover', 'asuka-product-designer'),
            'panel' => __('Panel (uses selected zone)', 'asuka-product-designer'),
            'modal' => __('Modal', 'asuka-product-designer'),
            'inline' => __('Inline', 'asuka-product-designer'),
            'drawer' => __('Drawer', 'asuka-product-designer'),
            'overlay' => __('Overlay', 'asuka-product-designer'),
        ];
        $portalModeOptions = [
            'internal' => __('Inside the designer', 'asuka-product-designer'),
            'shared' => __('Shared page portal', 'asuka-product-designer'),
            'body' => __('Document body', 'asuka-product-designer'),
            'custom' => __('Custom CSS selector', 'asuka-product-designer'),
        ];
        $layout = $this->decodeJsonString((string) get_post_meta($post->ID, '_apd_layout', true));
        $zoneSettings = [];
        foreach (['left', 'right', 'top', 'bottom'] as $zoneKey) {
            $zoneConfig = is_array($layout[$zoneKey] ?? null) ? (array) $layout[$zoneKey] : [];
            $zoneType = sanitize_key((string) ($zoneConfig['type'] ?? 'default'));
            if ($zoneType === 'panel') {
                $zoneType = 'panels';
            }
            if (! in_array($zoneType, array_keys($zoneTypeOptions), true)) {
                $zoneType = 'default';
            }
            $zoneSettings[$zoneKey] = [
                'type' => $zoneType,
                'collapsed' => array_key_exists('collapsed', $zoneConfig) ? (bool) $zoneConfig['collapsed'] : ($zoneKey === 'top'),
                'collapsible' => array_key_exists('collapsible', $zoneConfig) ? (bool) $zoneConfig['collapsible'] : true,
            ];
        }
        $activeCount = 0;
        $totalCount = 0;
        foreach ($definitions as $items) {
            foreach ($items as $moduleId => $moduleMeta) {
                $totalCount++;
                $moduleItem = $settings[$moduleId] ?? [];
                $availability = $this->templateModuleAvailability($moduleMeta);
                if (! empty($availability['available']) && (bool) ($moduleItem['enabled'] ?? ($moduleMeta['default'] ?? false))) {
                    $activeCount++;
                }
            }
        }
        $portalPresetListId = 'apd-template-portal-presets-' . $post->ID;
        ?>
        <div class="apd-template-workspace" data-apd-template-modules="1">
            <header class="apd-template-workspace__header">
                <div>
                    <span class="apd-template-workspace__eyebrow"><?php esc_html_e('Frontend experience', 'asuka-product-designer'); ?></span>
                    <h2><?php esc_html_e('Build a clear, reusable designer layout', 'asuka-product-designer'); ?></h2>
                    <p><?php esc_html_e('Choose the tools customers can access, where their buttons live, and where each module opens on the page.', 'asuka-product-designer'); ?></p>
                </div>
                <div class="apd-template-workspace__summary" aria-live="polite">
                    <strong data-apd-template-active-count><?php echo esc_html((string) $activeCount); ?></strong>
                    <span><?php echo esc_html(sprintf(/* translators: Placeholder values are populated from the current administration record. */ __('of %d modules active', 'asuka-product-designer'), $totalCount)); ?></span>
                </div>
            </header>

            <nav class="apd-template-tabs" aria-label="<?php esc_attr_e('Template configuration', 'asuka-product-designer'); ?>">
                <button type="button" class="apd-template-tab is-active" data-apd-template-tab="modules" aria-selected="true">
                    <span class="dashicons dashicons-screenoptions" aria-hidden="true"></span>
                    <?php esc_html_e('Modules', 'asuka-product-designer'); ?>
                </button>
                <button type="button" class="apd-template-tab" data-apd-template-tab="layout" aria-selected="false">
                    <span class="dashicons dashicons-layout" aria-hidden="true"></span>
                    <?php esc_html_e('Layout zones', 'asuka-product-designer'); ?>
                </button>
                <button type="button" class="apd-template-tab" data-apd-template-tab="portals" aria-selected="false">
                    <span class="dashicons dashicons-external" aria-hidden="true"></span>
                    <?php esc_html_e('Portals', 'asuka-product-designer'); ?>
                </button>
            </nav>

            <section class="apd-template-panel is-active" data-apd-template-panel="modules">
                <div class="apd-template-module-toolbar">
                    <label class="apd-template-search">
                        <span class="dashicons dashicons-search" aria-hidden="true"></span>
                        <span class="screen-reader-text"><?php esc_html_e('Search modules', 'asuka-product-designer'); ?></span>
                        <input type="search" data-apd-template-search placeholder="<?php esc_attr_e('Search modules...', 'asuka-product-designer'); ?>">
                    </label>
                    <label class="apd-template-filter">
                        <span class="screen-reader-text"><?php esc_html_e('Filter module category', 'asuka-product-designer'); ?></span>
                        <select data-apd-template-category-filter>
                            <option value="all"><?php esc_html_e('All categories', 'asuka-product-designer'); ?></option>
                            <?php foreach (array_keys($definitions) as $group) : ?>
                                <option value="<?php echo esc_attr(sanitize_title((string) $group)); ?>"><?php echo esc_html((string) $group); ?></option>
                            <?php endforeach; ?>
                        </select>
                    </label>
                    <label class="apd-template-active-filter">
                        <input type="checkbox" data-apd-template-active-filter>
                        <span><?php esc_html_e('Active only', 'asuka-product-designer'); ?></span>
                    </label>
                </div>

                <div class="apd-template-module-groups">
                    <?php foreach ($definitions as $group => $items) : ?>
                        <?php
                        $groupSlug = sanitize_title((string) $group);
                        $enabledInGroup = 0;
                        foreach ($items as $moduleId => $moduleMeta) {
                            $moduleItem = $settings[$moduleId] ?? [];
                            $availability = $this->templateModuleAvailability($moduleMeta);
                            if (! empty($availability['available']) && (bool) ($moduleItem['enabled'] ?? ($moduleMeta['default'] ?? false))) {
                                $enabledInGroup++;
                            }
                        }
                        ?>
                        <section class="apd-template-module-group" data-apd-template-category="<?php echo esc_attr($groupSlug); ?>">
                            <header class="apd-template-module-group__header">
                                <div>
                                    <h3><?php echo esc_html((string) $group); ?></h3>
                                    <p><?php echo esc_html($this->templateModuleGroupDescription($groupSlug)); ?></p>
                                </div>
                                <span class="apd-template-group-count" data-apd-template-group-count><?php echo esc_html(sprintf(/* translators: Placeholder values are populated from the current administration record. */ _n('%d active', '%d active', $enabledInGroup, 'asuka-product-designer'), $enabledInGroup)); ?></span>
                            </header>

                            <div class="apd-template-module-grid">
                                <?php foreach ($items as $id => $meta) : ?>
                                    <?php
                                    $item = $settings[$id] ?? [];
                                    $enabled = (bool) ($item['enabled'] ?? ($meta['default'] ?? false));
                                    $zone = (string) ($item['zone'] ?? ($meta['zone'] ?? 'left'));
                                    $label = (string) ($item['label'] ?? ($meta['label'] ?? $id));
                                    $type = (string) ($item['type'] ?? ($meta['type'] ?? 'floating'));
                                    $iconMode = (string) ($item['icon_mode'] ?? 'text');
                                    $iconText = (string) ($item['icon_text'] ?? ($meta['icon'] ?? 'puzzle'));
                                    $iconImage = (string) ($item['icon_image'] ?? '');
                                    $layoutMenu = ! empty($item['layoutMenu']);
                                    $selectionMenu = ! empty($item['selectionMenu']);
                                    $pageContent = ! empty($item['pageContent']);
                                    $portalMode = (string) ($item['portal_mode'] ?? 'internal');
                                    $mountTarget = (string) ($item['mount_target'] ?? '');
                                    $customMountTarget = $portalMode === 'custom' ? $mountTarget : '';
                                    $availability = $this->templateModuleAvailability($meta);
                                    $moduleAvailable = ! empty($availability['available']);
                                    $moduleStatus = sanitize_html_class((string) ($availability['status'] ?? 'core'));
                                    $effectiveEnabled = $enabled && $moduleAvailable;
                                    $contextualByDefault = ! empty($meta['contextualByDefault']);
                                    $searchText = strtolower(implode(' ', [$id, $label, (string) $meta['label'], (string) $meta['description'], (string) $group]));
                                    ?>
                                    <details
                                        class="apd-template-module-card<?php echo $effectiveEnabled ? ' is-enabled' : ' is-disabled'; ?><?php echo $layoutMenu ? ' is-in-layout' : ''; ?><?php echo $selectionMenu ? ' is-in-selection' : ''; ?><?php echo $pageContent ? ' is-in-page' : ''; ?><?php echo $moduleAvailable ? '' : ' is-locked'; ?>"
                                        data-apd-template-module-item="<?php echo esc_attr($id); ?>"
                                        data-apd-template-module-available="<?php echo esc_attr($moduleAvailable ? '1' : '0'); ?>"
                                        data-apd-template-module-status="<?php echo esc_attr($moduleStatus); ?>"
                                        data-apd-template-search-text="<?php echo esc_attr($searchText); ?>"
                                        data-apd-template-permission-requirements="<?php echo esc_attr(implode('|', (array) ($modulePermissionRequirements[$id] ?? []))); ?>"
                                    >
                                        <summary class="apd-template-module-card__head">
                                            <label class="apd-template-module-switch" onclick="event.stopPropagation();">
                                                <input type="hidden" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][enabled]" value="<?php echo esc_attr(! $moduleAvailable && $enabled ? '1' : '0'); ?>">
                                                <input type="checkbox" class="js-apd-template-module-enabled" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][enabled]" value="1" <?php checked($enabled, true); ?> <?php disabled($moduleAvailable, false); ?>>
                                                <span aria-hidden="true"></span>
                                                <span class="screen-reader-text"><?php echo esc_html(sprintf(/* translators: Placeholder values are populated from the current administration record. */ __('Enable %s', 'asuka-product-designer'), (string) $meta['label'])); ?></span>
                                            </label>
                                            <span class="apd-template-module-symbol" aria-hidden="true"><?php echo esc_html(strtoupper(substr((string) $meta['label'], 0, 1))); ?></span>
                                            <span class="apd-template-module-card__identity">
                                                <strong><?php echo esc_html((string) $meta['label']); ?></strong>
                                                <small><?php echo esc_html((string) $meta['description']); ?></small>
                                            </span>
                                            <span class="apd-template-module-card__badges">
                                                <?php if (! empty($meta['licenseFeature'])) : ?>
                                                    <span class="is-premium"><?php esc_html_e('Premium', 'asuka-product-designer'); ?></span>
                                                <?php endif; ?>
                                                <?php if (! $moduleAvailable) : ?>
                                                    <span class="is-locked"><?php echo esc_html((string) ($availability['status'] === 'integration' ? __('Setup required', 'asuka-product-designer') : __('Locked', 'asuka-product-designer'))); ?></span>
                                                <?php else : ?>
                                                    <span data-apd-template-zone-badge<?php echo $layoutMenu ? '' : ' hidden'; ?>><?php echo esc_html($zoneOptions[$zone] ?? ucfirst($zone)); ?></span>
                                                    <span data-apd-template-selection-badge<?php echo $selectionMenu ? '' : ' hidden'; ?>><?php esc_html_e('Selection', 'asuka-product-designer'); ?></span>
                                                    <span data-apd-template-page-badge<?php echo $pageContent ? '' : ' hidden'; ?>><?php esc_html_e('Page content', 'asuka-product-designer'); ?></span>
                                                    <span data-apd-template-portal-badge<?php echo $layoutMenu || $selectionMenu || $pageContent ? '' : ' hidden'; ?>><?php echo esc_html($portalModeOptions[$portalMode] ?? $portalModeOptions['internal']); ?></span>
                                                    <span data-apd-template-unplaced-badge<?php echo $layoutMenu || $selectionMenu || $pageContent || $contextualByDefault ? ' hidden' : ''; ?>><?php esc_html_e('Not placed', 'asuka-product-designer'); ?></span>
                                                    <?php if ($contextualByDefault && ! $layoutMenu && ! $selectionMenu && ! $pageContent) : ?>
                                                        <span><?php esc_html_e('Contextual by default', 'asuka-product-designer'); ?></span>
                                                    <?php endif; ?>
                                                <?php endif; ?>
                                            </span>
                                        </summary>

                                        <div class="apd-template-module-card__body">
                                            <?php if (! $moduleAvailable) : ?>
                                                <div class="apd-template-lock-note">
                                                    <span class="dashicons dashicons-lock" aria-hidden="true"></span>
                                                    <p><?php echo esc_html((string) ($availability['reason'] ?? __('This module is currently unavailable.', 'asuka-product-designer'))); ?></p>
                                                </div>
                                            <?php endif; ?>
                                            <?php if ($contextualByDefault) : ?>
                                                <div class="apd-template-lock-note">
                                                    <span class="dashicons dashicons-marker" aria-hidden="true"></span>
                                                    <p><?php esc_html_e('This module opens automatically when a configurable product part is selected and stays unplaced by default. You can still add it to any destination below.', 'asuka-product-designer'); ?></p>
                                                </div>
                                            <?php endif; ?>
                                            <div class="apd-template-setting-section apd-template-placement-section">
                                                <div class="apd-template-setting-section__title">
                                                    <strong><?php esc_html_e('Where it appears', 'asuka-product-designer'); ?></strong>
                                                    <span><?php esc_html_e('Choose a navigation entry, a contextual action, or persistent page content.', 'asuka-product-designer'); ?></span>
                                                </div>
                                                <div class="apd-template-placement-options">
                                                    <label class="apd-template-placement-option">
                                                        <input type="hidden" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][layout_menu]" value="0">
                                                        <input type="checkbox" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][layout_menu]" value="1" <?php checked($layoutMenu, true); ?> data-apd-template-module-layout-menu>
                                                        <span class="dashicons dashicons-menu-alt" aria-hidden="true"></span>
                                                        <span>
                                                            <strong><?php esc_html_e('Layout navigation', 'asuka-product-designer'); ?></strong>
                                                            <small><?php esc_html_e('Show a persistent module button in one layout zone.', 'asuka-product-designer'); ?></small>
                                                        </span>
                                                    </label>
                                                    <label class="apd-template-placement-option">
                                                        <input type="hidden" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][selection_menu]" value="0">
                                                        <input type="checkbox" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][selection_menu]" value="1" <?php checked($selectionMenu, true); ?> data-apd-template-module-selection-menu>
                                                        <span class="dashicons dashicons-marker" aria-hidden="true"></span>
                                                        <span>
                                                            <strong><?php esc_html_e('Selection menu', 'asuka-product-designer'); ?></strong>
                                                            <small><?php esc_html_e('Show the module beside a compatible selected object.', 'asuka-product-designer'); ?></small>
                                                        </span>
                                                    </label>
                                                    <label class="apd-template-placement-option">
                                                        <input type="hidden" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][page_content]" value="0">
                                                        <input type="checkbox" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][page_content]" value="1" <?php checked($pageContent, true); ?> data-apd-template-module-page-content>
                                                        <span class="dashicons dashicons-admin-page" aria-hidden="true"></span>
                                                        <span>
                                                            <strong><?php esc_html_e('Embedded page content', 'asuka-product-designer'); ?></strong>
                                                            <small><?php esc_html_e('Render the module continuously in the selected portal, without a layout button.', 'asuka-product-designer'); ?></small>
                                                        </span>
                                                    </label>
                                                </div>
                                                <p class="apd-template-placement-empty" data-apd-template-placement-empty<?php echo $layoutMenu || $selectionMenu || $pageContent ? ' hidden' : ''; ?>>
                                                    <?php esc_html_e('The module remains available to the API but has no customer-facing entry point.', 'asuka-product-designer'); ?>
                                                </p>
                                            </div>

                                            <div class="apd-template-layout-placement-settings" data-apd-template-layout-settings<?php echo $layoutMenu || $selectionMenu || $pageContent ? '' : ' hidden'; ?>>
                                                <div class="apd-template-setting-section" data-apd-template-menu-placement-settings<?php echo $layoutMenu ? '' : ' hidden'; ?>>
                                                    <div class="apd-template-setting-section__title">
                                                        <strong><?php esc_html_e('Layout placement', 'asuka-product-designer'); ?></strong>
                                                        <span><?php esc_html_e('Button position and opening behavior', 'asuka-product-designer'); ?></span>
                                                    </div>
                                                    <div class="apd-field-grid apd-field-grid--two">
                                                        <label class="apd-field">
                                                            <span><?php esc_html_e('Menu zone', 'asuka-product-designer'); ?></span>
                                                            <select name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][zone]" data-apd-template-module-zone>
                                                                <?php foreach ($zoneOptions as $zoneKey => $zoneLabel) : ?>
                                                                    <option value="<?php echo esc_attr($zoneKey); ?>" <?php selected($zone, $zoneKey); ?>><?php echo esc_html($zoneLabel); ?></option>
                                                                <?php endforeach; ?>
                                                            </select>
                                                        </label>
                                                        <label class="apd-field">
                                                            <span><?php esc_html_e('Open as', 'asuka-product-designer'); ?></span>
                                                            <select name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][type]" data-apd-template-module-type>
                                                                <?php foreach ($moduleTypeOptions as $typeKey => $typeLabel) : ?>
                                                                    <option value="<?php echo esc_attr($typeKey); ?>" <?php selected($type, $typeKey); ?>><?php echo esc_html($typeLabel); ?></option>
                                                                <?php endforeach; ?>
                                                            </select>
                                                        </label>
                                                    </div>
                                                </div>

                                                <div class="apd-template-setting-section apd-template-portal-setting">
                                                    <div class="apd-template-setting-section__title">
                                                        <strong data-apd-template-destination-title><?php echo esc_html($pageContent ? __('Page content destination', 'asuka-product-designer') : __('Opened module destination', 'asuka-product-designer')); ?></strong>
                                                        <span data-apd-template-destination-description><?php echo esc_html($pageContent ? __('The module stays mounted in this page host.', 'asuka-product-designer') : __('Choose where the module panel opens.', 'asuka-product-designer')); ?></span>
                                                    </div>
                                                    <label class="apd-field">
                                                        <span><?php esc_html_e('Portal', 'asuka-product-designer'); ?></span>
                                                        <select name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][portal_mode]" data-apd-template-module-portal-mode>
                                                            <?php foreach ($portalModeOptions as $modeKey => $modeLabel) : ?>
                                                                <option value="<?php echo esc_attr($modeKey); ?>" <?php selected($portalMode, $modeKey); ?>><?php echo esc_html($modeLabel); ?></option>
                                                            <?php endforeach; ?>
                                                        </select>
                                                    </label>
                                                    <div class="apd-template-portal-shared-note" data-apd-template-portal-shared-note>
                                                        <?php
                                                        echo wp_kses_post(sprintf(
                                                            /* translators: %s: current shared CSS selector. */
                                                            __('Uses the shared portal <code>%s</code>. Change it once in the Portals tab.', 'asuka-product-designer'),
                                                            esc_html($sharedPortalSelector !== '' ? $sharedPortalSelector : __('not configured', 'asuka-product-designer'))
                                                        ));
                                                        ?>
                                                    </div>
                                                    <label class="apd-field apd-template-portal-custom-field" data-apd-template-portal-custom-field>
                                                        <span><?php esc_html_e('CSS selector', 'asuka-product-designer'); ?></span>
                                                        <input
                                                            type="text"
                                                            name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][mount_target]"
                                                            value="<?php echo esc_attr($customMountTarget); ?>"
                                                            list="<?php echo esc_attr($portalPresetListId); ?>"
                                                            placeholder="#product-tools or [data-product-tools]"
                                                            data-apd-template-module-mount-target
                                                        >
                                                        <small><?php esc_html_e('The selector must match an element already present on the customer page.', 'asuka-product-designer'); ?></small>
                                                    </label>
                                                </div>

                                                <div class="apd-template-setting-section apd-template-module-flags" data-apd-template-panel-behavior<?php echo $layoutMenu ? '' : ' hidden'; ?>>
                                                    <div class="apd-template-setting-section__title">
                                                        <strong><?php esc_html_e('Panel behavior', 'asuka-product-designer'); ?></strong>
                                                    </div>
                                                    <div class="apd-template-check-row">
                                                        <label class="apd-check apd-check--compact">
                                                            <input type="hidden" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][collapsed]" value="0">
                                                            <input type="checkbox" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][collapsed]" value="1" <?php checked(! empty($item['collapsed']), true); ?> data-apd-template-module-collapsed>
                                                            <span><?php esc_html_e('Start collapsed', 'asuka-product-designer'); ?></span>
                                                        </label>
                                                        <label class="apd-check apd-check--compact">
                                                            <input type="hidden" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][collapsible]" value="0">
                                                            <input type="checkbox" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][collapsible]" value="1" <?php checked(! array_key_exists('collapsible', $item) || ! empty($item['collapsible']), true); ?> data-apd-template-module-collapsible>
                                                            <span><?php esc_html_e('Customer can collapse it', 'asuka-product-designer'); ?></span>
                                                        </label>
                                                    </div>
                                                </div>
                                            </div>

                                            <details class="apd-template-advanced">
                                                <summary><?php esc_html_e('Label and icon', 'asuka-product-designer'); ?></summary>
                                                <div class="apd-template-advanced__body">
                                                    <div class="apd-field-grid apd-field-grid--two">
                                                        <label class="apd-field">
                                                            <span><?php esc_html_e('Customer-facing label', 'asuka-product-designer'); ?></span>
                                                            <input type="text" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][label]" value="<?php echo esc_attr($label); ?>" data-apd-template-module-label-input>
                                                        </label>
                                                        <label class="apd-field">
                                                            <span><?php esc_html_e('Icon source', 'asuka-product-designer'); ?></span>
                                                            <select name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][icon_mode]" data-apd-template-module-icon-mode>
                                                                <option value="text" <?php selected($iconMode, 'text'); ?>><?php esc_html_e('Asuka icon key', 'asuka-product-designer'); ?></option>
                                                                <option value="image" <?php selected($iconMode, 'image'); ?>><?php esc_html_e('Media image', 'asuka-product-designer'); ?></option>
                                                            </select>
                                                        </label>
                                                        <label class="apd-field apd-template-icon-text-field">
                                                            <span><?php esc_html_e('Icon key', 'asuka-product-designer'); ?></span>
                                                            <input type="text" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][icon_text]" value="<?php echo esc_attr($iconText); ?>" data-apd-template-module-icon-text>
                                                        </label>
                                                        <div class="apd-field apd-template-icon-image-field">
                                                            <span><?php esc_html_e('Icon image', 'asuka-product-designer'); ?></span>
                                                            <div class="apd-media-field">
                                                                <input type="text" name="apd_template_modules_structured[<?php echo esc_attr($id); ?>][icon_image]" value="<?php echo esc_attr($iconImage); ?>" data-apd-template-module-icon-image data-apd-media-input="1">
                                                                <div class="apd-inline-actions apd-inline-actions-tight">
                                                                    <button type="button" class="button button-secondary js-apd-media-pick" data-apd-media-open="1" data-apd-media-type="image"><?php esc_html_e('Choose image', 'asuka-product-designer'); ?></button>
                                                                    <button type="button" class="button-link js-apd-media-clear" data-apd-media-clear="1"><?php esc_html_e('Clear', 'asuka-product-designer'); ?></button>
                                                                </div>
                                                            </div>
                                                        </div>
                                                    </div>
                                                    <code class="apd-template-module-id"><?php echo esc_html($id); ?></code>
                                                </div>
                                            </details>
                                        </div>
                                    </details>
                                <?php endforeach; ?>
                            </div>
                        </section>
                    <?php endforeach; ?>
                </div>
                <p class="apd-template-empty-filter" data-apd-template-empty-filter hidden><?php esc_html_e('No module matches these filters.', 'asuka-product-designer'); ?></p>
            </section>

            <section class="apd-template-panel" data-apd-template-panel="layout" hidden>
                <header class="apd-template-panel__intro">
                    <h3><?php esc_html_e('Organize the four navigation zones', 'asuka-product-designer'); ?></h3>
                    <p><?php esc_html_e('Zones place the module buttons around the canvas. You can still drag enabled buttons directly in the live preview.', 'asuka-product-designer'); ?></p>
                </header>
                <div class="apd-template-zone-map" aria-hidden="true">
                    <span class="is-top"><?php esc_html_e('Top', 'asuka-product-designer'); ?></span>
                    <span class="is-left"><?php esc_html_e('Left', 'asuka-product-designer'); ?></span>
                    <span class="is-canvas"><?php esc_html_e('Canvas', 'asuka-product-designer'); ?></span>
                    <span class="is-right"><?php esc_html_e('Right', 'asuka-product-designer'); ?></span>
                    <span class="is-bottom"><?php esc_html_e('Bottom', 'asuka-product-designer'); ?></span>
                </div>
                <div class="apd-template-zone-grid">
                    <?php foreach ($zoneOptions as $zoneKey => $zoneLabel) : ?>
                        <?php
                        $zoneType = (string) ($zoneSettings[$zoneKey]['type'] ?? 'default');
                        $zoneCollapsed = ! empty($zoneSettings[$zoneKey]['collapsed']);
                        $zoneCollapsible = ! empty($zoneSettings[$zoneKey]['collapsible']);
                        ?>
                        <section class="apd-template-zone-card" data-apd-template-zone-card="<?php echo esc_attr($zoneKey); ?>">
                            <header>
                                <span class="dashicons <?php echo esc_attr(in_array($zoneKey, ['top', 'bottom'], true) ? 'dashicons-align-center' : 'dashicons-align-' . $zoneKey); ?>" aria-hidden="true"></span>
                                <div>
                                    <h4><?php echo esc_html($zoneLabel); ?></h4>
                                    <p><?php echo esc_html(sprintf(/* translators: Placeholder values are populated from the current administration record. */ __('Buttons assigned to the %s edge', 'asuka-product-designer'), strtolower((string) $zoneLabel))); ?></p>
                                </div>
                            </header>
                            <label class="apd-field">
                                <span><?php esc_html_e('Zone behavior', 'asuka-product-designer'); ?></span>
                                <select name="apd_template_zone_settings[<?php echo esc_attr($zoneKey); ?>][type]" data-apd-template-zone-type>
                                    <?php foreach ($zoneTypeOptions as $typeKey => $typeLabel) : ?>
                                        <option value="<?php echo esc_attr($typeKey); ?>" <?php selected($zoneType, $typeKey); ?>><?php echo esc_html($typeLabel); ?></option>
                                    <?php endforeach; ?>
                                </select>
                            </label>
                            <div class="apd-template-check-row">
                                <label class="apd-check apd-check--compact">
                                    <input type="hidden" name="apd_template_zone_settings[<?php echo esc_attr($zoneKey); ?>][collapsed]" value="0">
                                    <input type="checkbox" name="apd_template_zone_settings[<?php echo esc_attr($zoneKey); ?>][collapsed]" value="1" <?php checked($zoneCollapsed, true); ?> data-apd-template-zone-collapsed>
                                    <span><?php esc_html_e('Start collapsed', 'asuka-product-designer'); ?></span>
                                </label>
                                <label class="apd-check apd-check--compact">
                                    <input type="hidden" name="apd_template_zone_settings[<?php echo esc_attr($zoneKey); ?>][collapsible]" value="0">
                                    <input type="checkbox" name="apd_template_zone_settings[<?php echo esc_attr($zoneKey); ?>][collapsible]" value="1" <?php checked($zoneCollapsible, true); ?> data-apd-template-zone-collapsible>
                                    <span><?php esc_html_e('Can be collapsed', 'asuka-product-designer'); ?></span>
                                </label>
                            </div>
                        </section>
                    <?php endforeach; ?>
                </div>
            </section>

            <section class="apd-template-panel" data-apd-template-panel="portals" hidden>
                <div class="apd-template-portal-layout">
                    <div class="apd-template-portal-copy">
                        <span class="apd-template-workspace__eyebrow"><?php esc_html_e('External module host', 'asuka-product-designer'); ?></span>
                        <h3><?php esc_html_e('Open modules in any HTML element', 'asuka-product-designer'); ?></h3>
                        <p><?php esc_html_e('A portal is a CSS selector pointing to an element on the customer page. Modules using the shared portal inherit this selector, while individual modules can override it.', 'asuka-product-designer'); ?></p>
                        <ol class="apd-template-portal-steps">
                            <li><span>1</span><?php esc_html_e('Add an empty host element to the page or theme.', 'asuka-product-designer'); ?></li>
                            <li><span>2</span><?php esc_html_e('Enter its ID, class, or data attribute below.', 'asuka-product-designer'); ?></li>
                            <li><span>3</span><?php esc_html_e('Choose "Shared page portal" on the modules that should open there.', 'asuka-product-designer'); ?></li>
                        </ol>
                    </div>
                    <div class="apd-template-portal-config">
                        <label class="apd-field">
                            <span><?php esc_html_e('Shared portal CSS selector', 'asuka-product-designer'); ?></span>
                            <input
                                type="text"
                                name="apd_template_shared_portal_selector"
                                value="<?php echo esc_attr($sharedPortalSelector); ?>"
                                list="<?php echo esc_attr($portalPresetListId); ?>"
                                placeholder="#asuka-module-portal"
                                data-apd-template-shared-portal
                            >
                            <small><?php esc_html_e('Recommended: use a unique ID or data attribute.', 'asuka-product-designer'); ?></small>
                        </label>
                        <div class="apd-template-portal-status" data-apd-template-portal-status aria-live="polite"></div>
                        <div class="apd-template-portal-code">
                            <span><?php esc_html_e('Example host element', 'asuka-product-designer'); ?></span>
                            <code data-apd-template-portal-code>&lt;div id="asuka-module-portal"&gt;&lt;/div&gt;</code>
                            <button type="button" class="button button-secondary" data-apd-template-copy-portal>
                                <span class="dashicons dashicons-admin-page" aria-hidden="true"></span>
                                <?php esc_html_e('Copy', 'asuka-product-designer'); ?>
                            </button>
                        </div>
                    </div>
                </div>
                <datalist id="<?php echo esc_attr($portalPresetListId); ?>">
                    <option value="#asuka-module-portal"></option>
                    <option value="[data-asuka-module-portal]"></option>
                    <option value=".asuka-module-portal"></option>
                    <option value="body"></option>
                </datalist>
            </section>
        </div>
        <?php
    }

    public function renderPermissionsBox(\WP_Post $post) : void
    {
        $permissions = $this->readJsonMetaArray($post->ID, '_apd_permissions');
        $permissionKeys = $this->permissionDefinitions();
        $modulePermissionKeys = $this->modulePermissionDefinitions();
        $modulePermissions = is_array($permissions['moduleOpen'] ?? null) ? (array) $permissions['moduleOpen'] : [];

        echo '<div class="apd-filter-box apd-structured-panel">';
        echo '<p>' . esc_html__('Control what customers can do on the product page without hand-editing the permissions JSON. The switches below mirror the documented AsukaDesigner permissions, plus a few WooCommerce-specific image guards used by this plugin.', 'asuka-product-designer') . '</p>';
        echo '<div class="apd-inline-actions">';
        echo '<button type="button" class="button button-small" data-apd-permission-preset="default">' . esc_html__('Recommended default', 'asuka-product-designer') . '</button> ';
        echo '<button type="button" class="button button-small" data-apd-permission-preset="full">' . esc_html__('Full customer editing', 'asuka-product-designer') . '</button> ';
        echo '<button type="button" class="button button-small" data-apd-permission-preset="text-only">' . esc_html__('Text only', 'asuka-product-designer') . '</button> ';
        echo '<button type="button" class="button button-small" data-apd-permission-preset="locked">' . esc_html__('Locked preview', 'asuka-product-designer') . '</button>';
        echo '</div>';
        echo '<div class="apd-filter-checklist apd-filter-checklist--compact">';
        foreach ($permissionKeys as $key => $meta) {
            $checked = array_key_exists($key, $permissions) ? (bool) $permissions[$key] : (bool) $meta['default'];
            $moduleAliases = $this->permissionModuleAliasMap()[$key] ?? [];
            $moduleAliasesAttr = $moduleAliases !== [] ? ' data-apd-permission-module-aliases="' . esc_attr(implode('|', $moduleAliases)) . '"' : '';
            echo '<label class="apd-filter-check apd-filter-check--stacked">';
            echo '<input type="hidden" name="apd_permissions_structured[' . esc_attr($key) . ']" value="0">';
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $moduleAliasesAttr contains only an esc_attr()-escaped data attribute assembled directly above.
            echo '<input type="checkbox" name="apd_permissions_structured[' . esc_attr($key) . ']" value="1" ' . checked($checked, true, false) . ' data-apd-permission-key="' . esc_attr($key) . '"' . $moduleAliasesAttr . '>';
            echo '<span><strong>' . esc_html($meta['label']) . '</strong><small class="apd-help-text">' . esc_html($meta['description']) . '</small></span>';
            echo '</label>';
        }
        echo '</div>';

        echo '<hr class="apd-divider">';
        echo '<p class="apd-section-intro"><strong>' . esc_html__('Per-module access', 'asuka-product-designer') . '</strong><br><small class="apd-help-text-inline">' . esc_html__('These toggles map to permissions.moduleOpen for the most common built-in modules documented by AsukaDesigner.', 'asuka-product-designer') . '</small></p>';
        echo '<div class="apd-filter-checklist apd-filter-checklist--compact">';
        foreach ($modulePermissionKeys as $key => $meta) {
            $checked = array_key_exists($key, $modulePermissions) ? (bool) $modulePermissions[$key] : (bool) $meta['default'];
            echo '<label class="apd-filter-check apd-filter-check--stacked">';
            echo '<input type="hidden" name="apd_permissions_structured[moduleOpen][' . esc_attr($key) . ']" value="0">';
            echo '<input type="checkbox" name="apd_permissions_structured[moduleOpen][' . esc_attr($key) . ']" value="1" ' . checked($checked, true, false) . ' data-apd-module-permission-key="' . esc_attr($key) . '">';
            echo '<span><strong>' . esc_html($meta['label']) . '</strong><small class="apd-help-text">' . esc_html($meta['description']) . '</small></span>';
            echo '</label>';
        }
        echo '</div>';
        echo '</div>';
    }

    public function renderFrontBehaviorBox(\WP_Post $post) : void
    {
        $behavior = $this->readJsonMetaArray($post->ID, '_apd_front_behavior');
        echo '<div class="apd-filter-box apd-structured-panel">';
        echo '<p>' . esc_html__('Set common add-to-cart validation rules without writing raw JSON.', 'asuka-product-designer') . '</p>';

        echo '<div class="apd-field-grid">';
        $this->renderToggleField('apd_front_behavior_structured[requireDesign]', __('Require a customized design', 'asuka-product-designer'), (bool) ($behavior['requireDesign'] ?? false));
        $this->renderToggleField('apd_front_behavior_structured[requirePreview]', __('Require a generated preview', 'asuka-product-designer'), (bool) ($behavior['requirePreview'] ?? false));
        $this->renderToggleField('apd_front_behavior_structured[requireText]', __('Require at least one text element', 'asuka-product-designer'), (bool) ($behavior['requireText'] ?? false));
        $this->renderToggleField('apd_front_behavior_structured[requireEveryView]', __('Require every view to be customized', 'asuka-product-designer'), (bool) ($behavior['requireEveryView'] ?? false));
        echo '</div>';

        echo '<div class="apd-field-grid apd-field-grid--two">';
        $this->renderNumberField('apd_front_behavior_structured[minObjects]', __('Minimum objects', 'asuka-product-designer'), $behavior['minObjects'] ?? '');
        $this->renderNumberField('apd_front_behavior_structured[maxObjects]', __('Maximum objects', 'asuka-product-designer'), $behavior['maxObjects'] ?? '');
        $this->renderNumberField('apd_front_behavior_structured[minTextObjects]', __('Minimum text elements', 'asuka-product-designer'), $behavior['minTextObjects'] ?? '');
        $this->renderNumberField('apd_front_behavior_structured[maxTextObjects]', __('Maximum text elements', 'asuka-product-designer'), $behavior['maxTextObjects'] ?? '');
        $this->renderNumberField('apd_front_behavior_structured[minImageObjects]', __('Minimum image elements', 'asuka-product-designer'), $behavior['minImageObjects'] ?? '');
        $this->renderNumberField('apd_front_behavior_structured[maxImageObjects]', __('Maximum image elements', 'asuka-product-designer'), $behavior['maxImageObjects'] ?? '');
        $this->renderNumberField('apd_front_behavior_structured[minTextLength]', __('Minimum text length', 'asuka-product-designer'), $behavior['minTextLength'] ?? '');
        $this->renderNumberField('apd_front_behavior_structured[requiredViewsCount]', __('Minimum customized views', 'asuka-product-designer'), $behavior['requiredViewsCount'] ?? '');
        $this->renderNumberField('apd_front_behavior_structured[minObjectsPerView]', __('Minimum objects per required view', 'asuka-product-designer'), $behavior['minObjectsPerView'] ?? '');
        echo '</div>';

        echo '<div class="apd-field-grid">';
        $this->renderCsvField('apd_front_behavior_structured[requiredViews]', __('Required view IDs / names', 'asuka-product-designer'), $this->implodeList($behavior['requiredViews'] ?? []), __('Comma-separated. Example: front, back', 'asuka-product-designer'));
        $this->renderCsvField('apd_front_behavior_structured[allowedObjectTypes]', __('Allowed object types', 'asuka-product-designer'), $this->implodeList($behavior['allowedObjectTypes'] ?? []), __('Comma-separated. Example: text, image, rect', 'asuka-product-designer'));
        $this->renderCsvField('apd_front_behavior_structured[forbiddenObjectTypes]', __('Forbidden object types', 'asuka-product-designer'), $this->implodeList($behavior['forbiddenObjectTypes'] ?? []), __('Comma-separated. Example: image, path', 'asuka-product-designer'));
        $this->renderCsvField('apd_front_behavior_structured[requiredObjectTypes]', __('Required object types', 'asuka-product-designer'), $this->implodeList($behavior['requiredObjectTypes'] ?? []), __('Comma-separated. Example: text', 'asuka-product-designer'));
        echo '</div>';

        echo '<p class="description">' . esc_html__('Only validation rules that are implemented in both the frontend and server-side cart validation are exposed here. Runtime permissions are configured separately in Client permissions.', 'asuka-product-designer') . '</p>';
        echo '</div>';
    }

    public function renderViewPaginationBox(\WP_Post $post) : void
    {
        $ui = $this->readJsonMetaArray($post->ID, '_apd_ui');
        $selectedMode = $this->getConfiguredViewsPaginationMode($ui);
        $historyMode = $this->getConfiguredHistoryControlsMode($ui);
        $modes = [
            'default' => __('Use JavaScript default', 'asuka-product-designer'),
            'disabled' => __('Disabled', 'asuka-product-designer'),
            'auto' => __('Auto', 'asuka-product-designer'),
            'thumbnails' => __('Thumbnails', 'asuka-product-designer'),
            'dots' => __('Dots', 'asuka-product-designer'),
            'numbers' => __('Numbers', 'asuka-product-designer'),
        ];

        echo '<div class="apd-filter-box apd-structured-panel">';
        echo '<p>' . esc_html__('Choose how customers navigate between product views on the frontend.', 'asuka-product-designer') . '</p>';
        echo '<label class="apd-field">';
        echo '<span>' . esc_html__('Pagination mode', 'asuka-product-designer') . '</span>';
        echo '<select name="apd_ui_structured[viewsPaginationMode]">';
        foreach ($modes as $mode => $label) {
            echo '<option value="' . esc_attr($mode) . '" ' . selected($selectedMode, $mode, false) . '>' . esc_html($label) . '</option>';
        }
        echo '</select>';
        echo '</label>';
        echo '<label class="apd-field">';
        echo '<span>' . esc_html__('Undo / redo controls', 'asuka-product-designer') . '</span>';
        echo '<select name="apd_ui_structured[historyControlsMode]">';
        foreach ([
            'default' => __('Use JavaScript default', 'asuka-product-designer'),
            'visible' => __('Visible', 'asuka-product-designer'),
            'hidden' => __('Hidden', 'asuka-product-designer'),
        ] as $mode => $label) {
            echo '<option value="' . esc_attr($mode) . '" ' . selected($historyMode, $mode, false) . '>' . esc_html($label) . '</option>';
        }
        echo '</select>';
        echo '</label>';
        echo '<p class="description">' . esc_html__('This writes ui.viewsPagination / ui.viewsPaginationMode into the product config, so the same rule is sent to createAsukaDesigner() on the frontend and when a cart design is reopened.', 'asuka-product-designer') . '</p>';
        echo '</div>';
    }

    public function renderPricingRules(\WP_Post $post) : void
    {
        $catalog = new AssetCatalog();
        $rules = (array) $catalog->getCollection('pricing_rules');
        $selectedIds = array_flip($catalog->normalizeSelectedPricingRuleIds((string) get_post_meta($post->ID, '_apd_pricing_rule_ids', true)));

        echo '<div class="apd-filter-box">';
        echo '<p>' . esc_html__('Attach reusable pricing rules to this custom product. They will be merged with the local pricing JSON saved by the builder.', 'asuka-product-designer') . '</p>';

        if ($rules === []) {
            echo '<p class="description">' . esc_html__('No global pricing rules exist yet. Create them first from Asuka Designer → Pricing rules.', 'asuka-product-designer') . '</p>';
            echo '</div>';
            return;
        }

        echo '<div class="apd-filter-checklist">';
        foreach ($rules as $rule) {
            if (! is_array($rule)) {
                continue;
            }
            $id = sanitize_key((string) ($rule['id'] ?? ''));
            if ($id === '') {
                continue;
            }
            $label = (string) ($rule['label'] ?? $id);
            $handler = (string) ($rule['handler'] ?? '');
            $category = (string) ($rule['category'] ?? '');
            echo '<label class="apd-filter-check apd-filter-check--stacked">';
            echo '<input type="checkbox" name="apd_pricing_rule_ids[]" value="' . esc_attr($id) . '" ' . checked(isset($selectedIds[$id]), true, false) . '>';
            echo '<span><strong>' . esc_html($label) . '</strong>';
            if ($category !== '' || $handler !== '') {
                echo '<small class="apd-help-text">' . esc_html(trim($category . ($category !== '' && $handler !== '' ? ' · ' : '') . $handler)) . '</small>';
            }
            echo '</span>';
            echo '</label>';
        }
        echo '</div>';
        echo '</div>';
    }

    public function renderCatalogFilters(\WP_Post $post) : void
    {
        $catalog = (new AssetCatalog())->all();
        $filters = (new AssetCatalog())->normalizeFilters((string) get_post_meta($post->ID, '_apd_asset_filters', true));
        $sections = [
            'designs' => ['label' => __('Designs', 'asuka-product-designer'), 'items' => (array) ($catalog['designs'] ?? [])],
            'fonts' => ['label' => __('Fonts', 'asuka-product-designer'), 'items' => (array) ($catalog['fonts'] ?? [])],
            'colors' => ['label' => __('Colors', 'asuka-product-designer'), 'items' => (array) ($catalog['colors'] ?? [])],
            'printingMethods' => ['label' => __('Printing methods', 'asuka-product-designer'), 'items' => (array) ($catalog['printing_methods'] ?? [])],
            'shapes' => ['label' => __('Shapes', 'asuka-product-designer'), 'items' => (array) ($catalog['shapes'] ?? [])],
        ];

        echo '<div class="apd-filter-box">';
        echo '<p>' . esc_html__('Choose whether this custom product exposes all global resources, only a curated subset, or none for each asset family.', 'asuka-product-designer') . '</p>';
        foreach ($sections as $type => $section) {
            $mode = $filters[$type]['mode'] ?? 'all';
            $selectedIds = array_flip((array) ($filters[$type]['ids'] ?? []));
            $count = is_array($section['items']) ? count($section['items']) : 0;
            echo '<div class="apd-filter-group">';
            echo '<label class="apd-field">';
            echo '<span>' . esc_html($section['label']) . ' <small class="apd-inline-count">(' . esc_html((string) $count) . ')</small></span>';
            echo '<select name="apd_asset_filters[' . esc_attr($type) . '][mode]">';
            echo '<option value="all" ' . selected($mode, 'all', false) . '>' . esc_html__('Use all global items', 'asuka-product-designer') . '</option>';
            echo '<option value="selected" ' . selected($mode, 'selected', false) . '>' . esc_html__('Only selected items', 'asuka-product-designer') . '</option>';
            echo '<option value="none" ' . selected($mode, 'none', false) . '>' . esc_html__('Hide this asset family', 'asuka-product-designer') . '</option>';
            echo '</select>';
            echo '</label>';

            if (! empty($section['items'])) {
                echo '<div class="apd-inline-actions">';
                echo '<button type="button" class="button-link" data-apd-filter-action="all" data-apd-filter-group="' . esc_attr($type) . '">' . esc_html__('Select all', 'asuka-product-designer') . '</button>';
                echo '<button type="button" class="button-link" data-apd-filter-action="none" data-apd-filter-group="' . esc_attr($type) . '">' . esc_html__('Clear', 'asuka-product-designer') . '</button>';
                echo '</div>';
                echo '<div class="apd-filter-checklist" data-apd-filter-list="' . esc_attr($type) . '">';
                foreach ($section['items'] as $item) {
                    $id = sanitize_key((string) ($item['id'] ?? ''));
                    if ($id === '') {
                        continue;
                    }
                    echo '<label class="apd-filter-check">';
                    echo '<input type="checkbox" name="apd_asset_filters[' . esc_attr($type) . '][ids][]" value="' . esc_attr($id) . '" ' . checked(isset($selectedIds[$id]), true, false) . '>';
                    echo '<span>' . esc_html((string) ($item['label'] ?? $id)) . '</span>';
                    echo '</label>';
                }
                echo '</div>';
            } else {
                echo '<p class="description">' . esc_html__('No global items available yet for this family.', 'asuka-product-designer') . '</p>';
            }
            echo '</div>';
        }
        echo '</div>';
    }

    public function renderAuditBox(\WP_Post $post) : void
    {
        $catalog = new AssetCatalog();
        $filters = $catalog->normalizeFilters((string) get_post_meta($post->ID, '_apd_asset_filters', true));
        $permissions = $this->readJsonMetaArray($post->ID, '_apd_permissions');
        $behavior = $this->readJsonMetaArray($post->ID, '_apd_front_behavior');
        $views = $this->readJsonMetaArray($post->ID, '_apd_views');
        $initialDocument = $this->readJsonMetaArray($post->ID, '_apd_initial_document');
        $initialState = $this->readJsonMetaArray($post->ID, '_apd_initial_state');
        $pricingRules = $catalog->normalizeSelectedPricingRuleIds((string) get_post_meta($post->ID, '_apd_pricing_rule_ids', true));

        $audit = [
            __('Mode', 'asuka-product-designer') => $this->translateModeOption((string) get_post_meta($post->ID, '_apd_mode', true) ?: 'standalone'),
            __('Views configured', 'asuka-product-designer') => is_array($views) ? count($views) : 0,
            __('Has initial document', 'asuka-product-designer') => $initialDocument !== [] ? __('Yes', 'asuka-product-designer') : __('No', 'asuka-product-designer'),
            __('Has initial state', 'asuka-product-designer') => $initialState !== [] ? __('Yes', 'asuka-product-designer') : __('No', 'asuka-product-designer'),
            __('Enabled permissions', 'asuka-product-designer') => count(array_filter($permissions, static fn ($value) => $value === true)),
            __('Validation guards', 'asuka-product-designer') => count(array_filter($behavior, static fn ($value) => $value !== '' && $value !== [] && $value !== false && $value !== null)),
            __('Attached pricing rules', 'asuka-product-designer') => count($pricingRules),
        ];

        echo '<div class="apd-filter-box apd-audit-list">';
        foreach ($audit as $label => $value) {
            echo '<div class="apd-audit-row"><strong>' . esc_html($label) . '</strong><span>' . esc_html((string) $value) . '</span></div>';
        }
        foreach ($filters as $type => $group) {
            $summary = $this->translateFilterMode((string) ($group['mode'] ?? 'all'));
            if (($group['mode'] ?? 'all') === 'selected') {
                $summary .= ' · ' . count((array) ($group['ids'] ?? []));
            }
            echo '<div class="apd-audit-row"><strong>' . esc_html($type) . '</strong><span>' . esc_html($summary) . '</span></div>';
        }
        echo '</div>';
    }

    private function renderHiddenField(int $postId, string $key) : void
    {
        $field = $this->fields[$key];
        $value = $this->getBuilderFieldValue($postId, $key);
        if ($field['type'] === 'select') {
            ?>
            <select name="<?php echo esc_attr($key); ?>" hidden>
                <?php foreach ($field['options'] as $option) : ?>
                    <option value="<?php echo esc_attr($option); ?>" <?php selected($value ?: 'standalone', $option); ?>><?php echo esc_html($this->translateModeOption((string) $option)); ?></option>
                <?php endforeach; ?>
            </select>
            <?php
            return;
        }
        ?>
        <textarea class="apd-codearea" name="<?php echo esc_attr($key); ?>" rows="<?php echo esc_attr((string) ($field['rows'] ?? 8)); ?>" hidden><?php echo esc_textarea($value); ?></textarea>
        <?php
    }



    private function translateFieldLabel(string $label) : string
    {
        return match ($label) {
            'Mode' => __('Mode', 'asuka-product-designer'),
            'Theme JSON' => __('Theme JSON', 'asuka-product-designer'),
            'Layout JSON' => __('Layout JSON', 'asuka-product-designer'),
            'Modules JSON' => __('Modules JSON', 'asuka-product-designer'),
            'Permissions JSON' => __('Permissions JSON', 'asuka-product-designer'),
            'Views JSON' => __('Views JSON', 'asuka-product-designer'),
            'Drawing zones JSON' => __('Drawing zones JSON', 'asuka-product-designer'),
            'Fonts JSON' => __('Fonts JSON', 'asuka-product-designer'),
            'Upload JSON' => __('Upload JSON', 'asuka-product-designer'),
            'Colors JSON' => __('Colors JSON', 'asuka-product-designer'),
            'Pricing rules JSON' => __('Pricing rules JSON', 'asuka-product-designer'),
            'Selection menu JSON' => __('Selection menu JSON', 'asuka-product-designer'),
            'UI JSON' => __('UI JSON', 'asuka-product-designer'),
            'Front behavior JSON' => __('Front behavior JSON', 'asuka-product-designer'),
            'Mobile behavior JSON' => __('Mobile behavior JSON', 'asuka-product-designer'),
            'Initial document JSON' => __('Initial document JSON', 'asuka-product-designer'),
            'Initial state JSON' => __('Initial state JSON', 'asuka-product-designer'),
            default => $label,
        };
    }

    private function translateModeOption(string $mode) : string
    {
        return match ($mode) {
            'standalone' => __('Standalone', 'asuka-product-designer'),
            'customer' => __('Customer', 'asuka-product-designer'),
            'admin' => __('Admin', 'asuka-product-designer'),
            default => $mode,
        };
    }

    private function translateFilterMode(string $mode) : string
    {
        return match ($mode) {
            'all' => __('All items', 'asuka-product-designer'),
            'selected' => __('Selected items', 'asuka-product-designer'),
            default => $mode,
        };
    }
    private function decodePostedJsonField(string $key, array $post) : array
    {
        if (! isset($post[$key])) {
            return [];
        }

        $raw = is_string($post[$key]) ? $post[$key] : '';
        return $this->decodeJsonString((string) $raw);
    }

    private function sanitizeJson(string $value) : ?string
    {
        $trimmed = trim($value);
        if ($trimmed === '') {
            return '';
        }

        $decoded = json_decode($trimmed, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            return null;
        }

        $encoded = wp_json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        return is_string($encoded) ? $encoded : null;
    }

    private function updateJsonPostMeta(int $postId, string $key, string $json) : void
    {
        update_post_meta($postId, $key, wp_slash($json));
    }


    /**
     * Avoid losing saved canvas objects when the admin save runs while the
     * preview runtime is rebuilding or briefly exports an empty bucket. If a
     * inactive view had saved objects and the incoming payload suddenly posts
     * an empty list for that same view, keep the previous objects instead of
     * deleting them. The active view is controlled by the visible canvas, so an
     * empty active bucket is an intentional deletion and must be preserved.
     */
    private function protectInitialDocumentFromInactiveEmptyViews(string $newJson, string $existingJson, string $postedStateJson = '') : string
    {
        $document = $this->decodeJsonString($newJson);
        $previous = $this->decodeJsonString($existingJson);
        if ($document === [] || $previous === []) {
            return $newJson;
        }

        $incomingHadViews = $this->extractDocumentViews($document) !== [];
        $documentBeforeViewRepair = $document;
        $document = $this->ensureDocumentViewsForObjectBuckets(
            $document,
            $this->extractDocumentViews($previous)
        );
        $viewsWereRepaired = $document !== $documentBeforeViewRepair;
        $previousByView = $this->extractObjectsByViewFromPayload($previous);
        if ($previousByView === []) {
            if (! $viewsWereRepaired) {
                return $newJson;
            }
            $encoded = wp_json_encode($document, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            return is_string($encoded) ? $encoded : $newJson;
        }

        $documentByView = $this->extractObjectsByViewFromPayload($document);
        $state = $this->decodeJsonString($postedStateJson);
        $activeViewId = $incomingHadViews
            ? $this->getPayloadActiveViewId($document, $this->getPayloadActiveViewId($state, ''))
            : '';
        $changed = false;

        foreach ($previousByView as $viewId => $objects) {
            if (! is_string($viewId) || $viewId === '') {
                continue;
            }
            if ($activeViewId !== '' && $viewId === $activeViewId) {
                continue;
            }
            if (! is_array($objects) || $objects === []) {
                continue;
            }
            $incoming = $documentByView[$viewId] ?? [];
            if (! is_array($incoming) || $incoming === []) {
                $documentByView[$viewId] = $objects;
                $changed = true;
            }
        }

        if (! $changed && ! $viewsWereRepaired) {
            return $newJson;
        }

        $document = $this->setObjectsByViewOnDocumentPayload($document, $documentByView);
        $encoded = wp_json_encode($document, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        return is_string($encoded) ? $encoded : $newJson;
    }

    /**
     * @param array<int|string, mixed> $payload
     * @return array<int, array<int|string, mixed>>
     */
    private function extractDocumentViews(array $payload) : array
    {
        $document = $this->extractDocumentBranch($payload);
        $viewsPayload = isset($document['views']) && is_array($document['views'])
            ? $document['views']
            : [];
        if (isset($viewsPayload['views']) && is_array($viewsPayload['views'])) {
            return array_values(array_filter($viewsPayload['views'], 'is_array'));
        }
        if (array_is_list($viewsPayload)) {
            return array_values(array_filter($viewsPayload, 'is_array'));
        }
        return [];
    }

    /**
     * @param array<int|string, mixed> $payload
     * @param array<int, array<int|string, mixed>> $fallbackViews
     * @return array<int|string, mixed>
     */
    private function ensureDocumentViewsForObjectBuckets(array $payload, array $fallbackViews = []) : array
    {
        $document = $this->extractDocumentBranch($payload);
        $views = $this->extractDocumentViews($payload);
        if ($views !== []) {
            return $payload;
        }

        $views = array_values(array_filter($fallbackViews, 'is_array'));
        if ($views === []) {
            $objectsByView = $this->extractObjectsByViewFromPayload($document);
            $index = 0;
            foreach (array_keys($objectsByView) as $viewId) {
                $viewId = trim((string) $viewId);
                if ($viewId === '') {
                    continue;
                }
                $index++;
                $views[] = [
                    'id' => $viewId,
                    'name' => $index === 1 ? 'Front' : sprintf('View %d', $index),
                    'width' => 800,
                    'height' => 600,
                    'dpi' => 96,
                    'unit' => 'px',
                    'engine' => 'fabric',
                ];
            }
        }

        if ($views === []) {
            return $payload;
        }

        $activeViewId = $this->getPayloadActiveViewId($document);
        $knownIds = array_values(array_filter(array_map(
            static fn (array $view) : string => isset($view['id']) && is_scalar($view['id']) ? trim((string) $view['id']) : '',
            $views
        )));
        if ($activeViewId === '' || ! in_array($activeViewId, $knownIds, true)) {
            $activeViewId = $knownIds[0] ?? '';
        }
        $document['views'] = [
            'views' => $views,
            'activeViewId' => $activeViewId,
        ];
        $document['viewportByView'] = isset($document['viewportByView']) && is_array($document['viewportByView'])
            ? $document['viewportByView']
            : [];
        foreach ($knownIds as $viewId) {
            if (! isset($document['viewportByView'][$viewId]) || ! is_array($document['viewportByView'][$viewId])) {
                $document['viewportByView'][$viewId] = ['zoom' => 1, 'panX' => 0, 'panY' => 0];
            }
        }

        if ($this->isCanonicalStateEnvelope($payload)) {
            $payload['document'] = $document;
            return $this->removeCanonicalRootDocumentAliases($payload);
        }

        return $document;
    }

    /**
     * @param array<int|string, mixed> $payload
     * @return array<string, array<int, mixed>>
     */
    private function extractObjectsByViewFromPayload(array $payload) : array
    {
        $candidates = [];
        if (isset($payload['objects']) && is_array($payload['objects']) && isset($payload['objects']['byView']) && is_array($payload['objects']['byView'])) {
            $candidates[] = $payload['objects']['byView'];
        }
        foreach (['objectsByView', 'documentObjectsByView', 'byView'] as $key) {
            if (isset($payload[$key]) && is_array($payload[$key])) {
                $candidates[] = $payload[$key];
            }
        }

        foreach ($candidates as $candidate) {
            $normalized = [];
            foreach ($candidate as $viewId => $objects) {
                if (! is_scalar($viewId) || ! is_array($objects)) {
                    continue;
                }
                $normalized[(string) $viewId] = $objects;
            }
            if ($normalized !== []) {
                return $normalized;
            }
        }

        foreach (['document', 'initialDocument', 'documentState'] as $key) {
            if (isset($payload[$key]) && is_array($payload[$key])) {
                $nested = $this->extractObjectsByViewFromPayload($payload[$key]);
                if ($nested !== []) {
                    return $nested;
                }
            }
        }

        return [];
    }

    /**
     * @param array<int|string, mixed> $payload
     * @param array<string, array<int, mixed>> $objectsByView
     * @return array<int|string, mixed>
     */
    private function setObjectsByViewOnDocumentPayload(array $payload, array $objectsByView) : array
    {
        if ($this->isCanonicalStateEnvelope($payload)) {
            $document = $this->extractDocumentBranch($payload);
            $payload['document'] = $this->setObjectsByViewOnDocumentPayload($document, $objectsByView);
            return $this->removeCanonicalRootDocumentAliases($payload);
        }

        if (! isset($payload['objects']) || ! is_array($payload['objects'])) {
            $payload['objects'] = [];
        }
        $payload['objects']['byView'] = $objectsByView;

        if (isset($payload['objectsByView']) && is_array($payload['objectsByView'])) {
            $payload['objectsByView'] = $objectsByView;
        }
        if (isset($payload['documentObjectsByView']) && is_array($payload['documentObjectsByView'])) {
            $payload['documentObjectsByView'] = $objectsByView;
        }

        return $payload;
    }

    private function isCanonicalStateEnvelope(array $payload) : bool
    {
        return ($payload['format'] ?? '') === 'asuka-state'
            && (int) ($payload['schemaVersion'] ?? 0) === 2;
    }

    /**
     * @return array<string, bool>
     */
    private function canonicalDocumentScope() : array
    {
        return [
            'views' => true,
            'drawingAreas' => true,
            'snapshots' => true,
            'objects' => true,
            'selection' => true,
            'viewportByView' => true,
            'scenes3d' => true,
            'asukaDocument' => true,
        ];
    }

    /**
     * @param array<int|string, mixed> $payload
     * @return array<int|string, mixed>
     */
    private function extractDocumentBranch(array $payload) : array
    {
        if ($this->isCanonicalStateEnvelope($payload)) {
            $document = isset($payload['document']) && is_array($payload['document'])
                ? $payload['document']
                : [];
            if ($this->isCanonicalStateEnvelope($document)) {
                return $this->extractDocumentBranch($document);
            }
            return $document;
        }

        foreach (['document', 'initialDocument', 'documentState'] as $key) {
            if (! isset($payload[$key]) || ! is_array($payload[$key])) {
                continue;
            }
            $document = $payload[$key];
            return $this->isCanonicalStateEnvelope($document)
                ? $this->extractDocumentBranch($document)
                : $document;
        }

        return $payload;
    }

    /**
     * @param array<int|string, mixed> $payload
     * @return array<int|string, mixed>
     */
    private function removeCanonicalRootDocumentAliases(array $payload) : array
    {
        if (! $this->isCanonicalStateEnvelope($payload)) {
            return $payload;
        }

        unset(
            $payload['views'],
            $payload['activeViewId'],
            $payload['objects'],
            $payload['objectsByView'],
            $payload['documentObjectsByView'],
            $payload['byView'],
            $payload['drawingAreas'],
            $payload['drawingAreasByView'],
            $payload['drawingZones'],
            $payload['snapshots'],
            $payload['snapshotsByView'],
            $payload['viewSnapshots'],
            $payload['nativeSnapshots'],
            $payload['selection'],
            $payload['viewportByView'],
            $payload['documentViewportByView'],
            $payload['scenes3d'],
            $payload['asukaDocument'],
            $payload['documentState'],
            $payload['initialDocument']
        );

        return $payload;
    }

    /**
     * @param array<int|string, mixed> $payload
     * @return array<int|string, mixed>
     */
    private function normalizeCanonicalDocumentEnvelope(array $payload) : array
    {
        if ($payload === []) {
            return [];
        }

        $sourceCatalog = isset($payload['catalog']) && is_array($payload['catalog'])
            ? $payload['catalog']
            : [];
        $document = $this->extractDocumentBranch($payload);

        if ($this->isCanonicalStateEnvelope($payload)) {
            $normalized = $payload;
            $nestedEnvelope = isset($payload['document']) && is_array($payload['document']) && $this->isCanonicalStateEnvelope($payload['document'])
                ? $payload['document']
                : [];
            if ($nestedEnvelope !== [] && isset($nestedEnvelope['catalog']) && is_array($nestedEnvelope['catalog'])) {
                $sourceCatalog = array_replace_recursive($sourceCatalog, $nestedEnvelope['catalog']);
            }
        } else {
            $normalized = [
                'format' => 'asuka-state',
                'schemaVersion' => 2,
                'exportedAt' => gmdate('c'),
                'scope' => [],
            ];
        }

        $normalized['format'] = 'asuka-state';
        $normalized['schemaVersion'] = 2;
        $normalized['exportedAt'] = isset($normalized['exportedAt']) && is_scalar($normalized['exportedAt'])
            ? (string) $normalized['exportedAt']
            : gmdate('c');
        $normalized['scope'] = isset($normalized['scope']) && is_array($normalized['scope'])
            ? $normalized['scope']
            : [];
        $normalized['scope']['document'] = array_replace(
            isset($normalized['scope']['document']) && is_array($normalized['scope']['document'])
                ? $normalized['scope']['document']
                : [],
            $this->canonicalDocumentScope()
        );
        $normalized['scope']['catalog'] = isset($normalized['scope']['catalog']) && is_array($normalized['scope']['catalog'])
            ? $normalized['scope']['catalog']
            : [];
        $normalized['scope']['catalog']['elements'] = true;
        $normalized['document'] = $document;

        if ($sourceCatalog !== []) {
            $normalized['catalog'] = array_replace_recursive(
                isset($normalized['catalog']) && is_array($normalized['catalog']) ? $normalized['catalog'] : [],
                $sourceCatalog
            );
        } elseif (isset($document['elements']) && is_array($document['elements']) && $document['elements'] !== []) {
            $normalized['catalog'] = isset($normalized['catalog']) && is_array($normalized['catalog'])
                ? $normalized['catalog']
                : [];
            $normalized['catalog']['elements'] = array_values($document['elements']);
        }

        return $this->removeCanonicalRootDocumentAliases($normalized);
    }

    /**
     * @param array<int|string, mixed> $payload
     * @param array<int|string, mixed> $documentEnvelope
     * @return array<int|string, mixed>
     */
    private function normalizeCanonicalStateEnvelope(array $payload, array $documentEnvelope = []) : array
    {
        if ($payload === [] && $documentEnvelope === []) {
            return [];
        }

        if ($payload === []) {
            return $this->normalizeCanonicalDocumentEnvelope($documentEnvelope);
        }

        $state = $payload;
        if (! $this->isCanonicalStateEnvelope($state)) {
            $state['format'] = 'asuka-state';
            $state['schemaVersion'] = 2;
            $state['exportedAt'] = isset($state['exportedAt']) && is_scalar($state['exportedAt'])
                ? (string) $state['exportedAt']
                : gmdate('c');
        }

        $document = $this->extractDocumentBranch($state);
        $normalizedDocumentEnvelope = $documentEnvelope !== []
            ? $this->normalizeCanonicalDocumentEnvelope($documentEnvelope)
            : [];
        if ($normalizedDocumentEnvelope !== []) {
            $document = $this->extractDocumentBranch($normalizedDocumentEnvelope);
        }

        $state['format'] = 'asuka-state';
        $state['schemaVersion'] = 2;
        $state['scope'] = isset($state['scope']) && is_array($state['scope']) ? $state['scope'] : [];
        $state['scope']['document'] = array_replace(
            isset($state['scope']['document']) && is_array($state['scope']['document'])
                ? $state['scope']['document']
                : [],
            $this->canonicalDocumentScope()
        );
        $state['document'] = $document;

        if ($normalizedDocumentEnvelope !== [] && isset($normalizedDocumentEnvelope['catalog']) && is_array($normalizedDocumentEnvelope['catalog'])) {
            $state['catalog'] = array_replace_recursive(
                isset($state['catalog']) && is_array($state['catalog']) ? $state['catalog'] : [],
                $normalizedDocumentEnvelope['catalog']
            );
            $state['scope']['catalog'] = isset($state['scope']['catalog']) && is_array($state['scope']['catalog'])
                ? $state['scope']['catalog']
                : [];
            $state['scope']['catalog']['elements'] = true;
        }

        unset($state['legacy'], $state['state'], $state['initialDocument'], $state['documentState']);
        return $this->removeCanonicalRootDocumentAliases($state);
    }

    /**
     * @param array<int|string, mixed> $document
     * @return array<int|string, mixed>
     */
    private function compactDocumentBranch(array $document) : array
    {
        $objectsByView = $this->extractObjectsByViewFromPayload($document);
        $snapshotBuckets = [
            $document['snapshots']['snapshotsByView'] ?? null,
            $document['snapshotsByView'] ?? null,
            $document['viewSnapshots']['snapshotsByView'] ?? null,
            $document['viewSnapshots'] ?? null,
            $document['asukaDocument']['nativeSnapshots']['fabric'] ?? null,
        ];

        foreach ($snapshotBuckets as $bucket) {
            if (! is_array($bucket)) {
                continue;
            }
            foreach ($bucket as $viewId => $snapshot) {
                $viewKey = is_scalar($viewId) ? (string) $viewId : '';
                if ($viewKey === '' || array_key_exists($viewKey, $objectsByView) || ! is_array($snapshot)) {
                    continue;
                }
                $snapshotObjects = $snapshot['objects'] ?? null;
                if (is_array($snapshotObjects)) {
                    $objectsByView[$viewKey] = $snapshotObjects;
                }
            }
        }

        if ($objectsByView !== []) {
            $document = $this->setObjectsByViewOnDocumentPayload($document, $objectsByView);
        }

        unset(
            $document['snapshots'],
            $document['snapshotsByView'],
            $document['viewSnapshots'],
            $document['nativeSnapshots'],
            $document['asukaDocument']
        );
        if (isset($document['objects']) && is_array($document['objects'])) {
            unset($document['objects']['source']);
        }

        return $document;
    }

    private function compactInitialDocumentForStorage(string $json) : string
    {
        $payload = $this->decodeJsonString($json);
        if ($payload === []) {
            return $json;
        }

        $payload = $this->normalizeCanonicalDocumentEnvelope($payload);
        $payload = $this->ensureDocumentViewsForObjectBuckets($payload);
        $payload['document'] = $this->compactDocumentBranch($this->extractDocumentBranch($payload));
        $payload = $this->removeCanonicalRootDocumentAliases($payload);

        $encoded = wp_json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        return is_string($encoded) ? $encoded : $json;
    }

    private function compactInitialStateForStorage(string $json) : string
    {
        $state = $this->decodeJsonString($json);
        if ($state === []) {
            return $json;
        }

        $state = $this->normalizeCanonicalStateEnvelope($state);
        $state['document'] = $this->compactDocumentBranch($this->extractDocumentBranch($state));
        unset($state['history'], $state['legacy'], $state['state'], $state['initialDocument'], $state['documentState']);
        $state = $this->removeCanonicalRootDocumentAliases($state);
        $encoded = wp_json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        return is_string($encoded) ? $encoded : $json;
    }

    private function syncInitialStateDocumentFromInitialDocument(int $postId) : void
    {
        $document = $this->readJsonMetaArray($postId, '_apd_initial_document');
        $state = $this->readJsonMetaArray($postId, '_apd_initial_state');

        if ($document === []) {
            return;
        }

        $document = $this->normalizeCanonicalDocumentEnvelope($document);
        $state = $this->normalizeCanonicalStateEnvelope($state, $document);
        $this->updateJsonPostMeta(
            $postId,
            '_apd_initial_state',
            (string) wp_json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
        );
    }

    private function syncViewsFromInitialDocument(int $postId) : void
    {
        $payload = $this->readJsonMetaArray($postId, '_apd_initial_document');
        $document = $this->extractDocumentBranch($payload);
        if ($document === [] || ! isset($document['views']) || ! is_array($document['views'])) {
            return;
        }

        $viewsPayload = $document['views'];
        if (isset($viewsPayload['views']) && is_array($viewsPayload['views'])) {
            $views = array_values(array_filter($viewsPayload['views'], 'is_array'));
        } elseif (array_is_list($viewsPayload)) {
            $views = array_values(array_filter($viewsPayload, 'is_array'));
        } else {
            return;
        }

        $this->updateJsonPostMeta(
            $postId,
            '_apd_views',
            (string) wp_json_encode($views, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
        );
    }

    private function repairStoredCanonicalPayloads(int $postId) : void
    {
        $document = $this->readJsonMetaArray($postId, '_apd_initial_document');
        if ($document === []) {
            return;
        }

        $document = $this->normalizeCanonicalDocumentEnvelope($document);
        $document = $this->ensureDocumentViewsForObjectBuckets(
            $document,
            $this->readJsonMetaArray($postId, '_apd_views')
        );
        $document['document'] = $this->compactDocumentBranch($this->extractDocumentBranch($document));
        $document = $this->removeCanonicalRootDocumentAliases($document);
        $this->updateJsonPostMeta(
            $postId,
            '_apd_initial_document',
            (string) wp_json_encode($document, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
        );

        $state = $this->normalizeCanonicalStateEnvelope(
            $this->readJsonMetaArray($postId, '_apd_initial_state'),
            $document
        );
        $state['document'] = $this->compactDocumentBranch($this->extractDocumentBranch($state));
        unset($state['history'], $state['legacy'], $state['state'], $state['initialDocument'], $state['documentState']);
        $state = $this->removeCanonicalRootDocumentAliases($state);
        $this->updateJsonPostMeta(
            $postId,
            '_apd_initial_state',
            (string) wp_json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
        );

        $this->syncDrawingZonesIntoBootPayloads($postId);
        $this->syncInitialStateDocumentFromInitialDocument($postId);
        $this->syncViewsFromInitialDocument($postId);
    }

    private function saveThemeAssignment(int $postId, array $post) : void
    {
        if (! isset($post['apd_designer_theme_assignment'])) {
            return;
        }

        $selection = sanitize_key((string) $post['apd_designer_theme_assignment']);
        if ($selection === '__embedded') {
            return;
        }
        if ($selection === '__inherit') {
            delete_post_meta($postId, '_apd_theme');
            return;
        }

        $service = new DesignerTheme();
        if (! $service->hasTheme($selection)) {
            delete_post_meta($postId, '_apd_theme');
            return;
        }

        $this->updateJsonPostMeta(
            $postId,
            '_apd_theme',
            (string) wp_json_encode(['themeId' => $selection], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
        );
    }

    private function readJsonMetaArray(int $postId, string $metaKey) : array
    {
        $raw = (string) get_post_meta($postId, $metaKey, true);
        return $this->decodeJsonString($raw);
    }

    private function syncDrawingZonesIntoBootPayloads(int $postId) : void
    {
        $zones = $this->readJsonMetaArray($postId, '_apd_drawing_zones');
        $document = $this->readJsonMetaArray($postId, '_apd_initial_document');
        $state = $this->readJsonMetaArray($postId, '_apd_initial_state');

        if ($zones === []) {
            $zones = array_merge(
                $this->extractDrawingZonesFromPayload($document),
                $this->extractDrawingZonesFromPayload($state)
            );
        }

        if ($document !== []) {
            $document = $this->mergeDrawingZonesIntoDocumentPayload($document, $zones);
            $this->updateJsonPostMeta($postId, '_apd_initial_document', (string) wp_json_encode($document, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
        }

        if ($state !== []) {
            $state = $this->mergeDrawingZonesIntoStatePayload($state, $zones);
            $this->updateJsonPostMeta($postId, '_apd_initial_state', (string) wp_json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
        }
    }

    /**
     * @param array<int|string, mixed> $payload
     */
    private function getPayloadActiveViewId(array $payload, string $fallback = '') : string
    {
        if (isset($payload['activeViewId']) && is_scalar($payload['activeViewId']) && trim((string) $payload['activeViewId']) !== '') {
            return trim((string) $payload['activeViewId']);
        }
        if (isset($payload['views']) && is_array($payload['views'])) {
            if (isset($payload['views']['activeViewId']) && is_scalar($payload['views']['activeViewId']) && trim((string) $payload['views']['activeViewId']) !== '') {
                return trim((string) $payload['views']['activeViewId']);
            }
            $views = isset($payload['views']['views']) && is_array($payload['views']['views']) ? $payload['views']['views'] : $payload['views'];
            $first = reset($views);
            if (is_array($first)) {
                $id = $first['id'] ?? $first['name'] ?? '';
                if (is_scalar($id) && trim((string) $id) !== '') {
                    return trim((string) $id);
                }
            }
        }

        foreach (['document', 'initialDocument', 'documentState'] as $key) {
            if (isset($payload[$key]) && is_array($payload[$key])) {
                $nested = $this->getPayloadActiveViewId($payload[$key], $fallback);
                if ($nested !== '') {
                    return $nested;
                }
            }
        }

        return $fallback;
    }

    /**
     * @param array<int|string, mixed> $zone
     * @return array<int|string, mixed>|null
     */
    private function normalizeDrawingZone(array $zone, string $fallbackViewId = '') : ?array
    {
        $viewId = $zone['viewId'] ?? $zone['view'] ?? $zone['view_id'] ?? $fallbackViewId;
        $viewId = is_scalar($viewId) ? trim((string) $viewId) : '';
        if ($viewId !== '') {
            $zone['viewId'] = $viewId;
        }

        $id = $zone['id'] ?? $zone['uid'] ?? '';
        if (! is_scalar($id) || trim((string) $id) === '') {
            $zone['id'] = 'drawing-area-' . ($viewId !== '' ? $viewId . '-' : '') . substr(md5(wp_json_encode($zone) ?: uniqid('', true)), 0, 12);
        }

        if (! isset($zone['left']) && isset($zone['x'])) {
            $zone['left'] = $zone['x'];
        }
        if (! isset($zone['top']) && isset($zone['y'])) {
            $zone['top'] = $zone['y'];
        }
        if (! isset($zone['x']) && isset($zone['left'])) {
            $zone['x'] = $zone['left'];
        }
        if (! isset($zone['y']) && isset($zone['top'])) {
            $zone['y'] = $zone['top'];
        }

        foreach (['left', 'top', 'x', 'y', 'width', 'height'] as $key) {
            if (isset($zone[$key]) && is_numeric($zone[$key])) {
                $zone[$key] = $zone[$key] + 0;
            }
        }

        return $zone;
    }

    /**
     * @param array<int|string, mixed> $payload
     * @return array<int, array<int|string, mixed>>
     */
    private function extractDrawingZonesFromPayload(array $payload, string $fallbackViewId = '', int $depth = 0) : array
    {
        if ($payload === [] || $depth > 5) {
            return [];
        }

        $activeViewId = $this->getPayloadActiveViewId($payload, $fallbackViewId);
        $zones = [];

        if (isset($payload['drawingAreasByView']) && is_array($payload['drawingAreasByView'])) {
            foreach ($payload['drawingAreasByView'] as $viewId => $items) {
                if (! is_array($items)) {
                    continue;
                }
                foreach ($items as $zone) {
                    if (! is_array($zone)) {
                        continue;
                    }
                    $zones[] = $this->normalizeDrawingZone($zone, is_string($viewId) ? $viewId : $activeViewId);
                }
            }
        }

        if (isset($payload['drawingAreas']) && is_array($payload['drawingAreas'])) {
            if (isset($payload['drawingAreas']['byView']) && is_array($payload['drawingAreas']['byView'])) {
                foreach ($payload['drawingAreas']['byView'] as $viewId => $items) {
                    if (! is_array($items)) {
                        continue;
                    }
                    foreach ($items as $zone) {
                        if (is_array($zone)) {
                            $zones[] = $this->normalizeDrawingZone($zone, is_string($viewId) ? $viewId : $activeViewId);
                        }
                    }
                }
            }
            if (isset($payload['drawingAreas']['zones']) && is_array($payload['drawingAreas']['zones'])) {
                foreach ($payload['drawingAreas']['zones'] as $zone) {
                    if (is_array($zone)) {
                        $zones[] = $this->normalizeDrawingZone($zone, $activeViewId);
                    }
                }
            }
        }

        if (isset($payload['drawingZones']) && is_array($payload['drawingZones'])) {
            foreach ($payload['drawingZones'] as $zone) {
                if (is_array($zone)) {
                    $zones[] = $this->normalizeDrawingZone($zone, $activeViewId);
                }
            }
        }

        foreach (['document', 'initialDocument', 'documentState', 'state', 'legacy'] as $key) {
            if (isset($payload[$key]) && is_array($payload[$key])) {
                $zones = array_merge($zones, $this->extractDrawingZonesFromPayload($payload[$key], $activeViewId, $depth + 1));
            }
        }

        return array_values(array_filter($zones, 'is_array'));
    }

    /**
     * @param array<int|string, mixed> $zones
     * @return array<string, array<int, array<int|string, mixed>>>
     */
    private function groupDrawingZonesByView(array $zones, string $fallbackViewId = '') : array
    {
        $byView = [];
        foreach ($zones as $zone) {
            if (! is_array($zone)) {
                continue;
            }
            $normalized = $this->normalizeDrawingZone($zone, $fallbackViewId);
            if (! is_array($normalized)) {
                continue;
            }
            $viewId = $normalized['viewId'] ?? $fallbackViewId;
            $viewId = is_scalar($viewId) && trim((string) $viewId) !== '' ? trim((string) $viewId) : '__default__';
            $normalized['viewId'] = $viewId;
            if (! isset($byView[$viewId])) {
                $byView[$viewId] = [];
            }
            $byView[$viewId][] = $normalized;
        }
        return $byView;
    }

    /**
     * @param array<string, array<int, array<int|string, mixed>>> $byView
     * @return array<int, array<int|string, mixed>>
     */
    private function flattenDrawingZonesByView(array $byView) : array
    {
        $zones = [];
        foreach ($byView as $viewId => $items) {
            foreach ($items as $zone) {
                if (! is_array($zone)) {
                    continue;
                }
                if (! isset($zone['viewId']) || trim((string) $zone['viewId']) === '') {
                    $zone['viewId'] = (string) $viewId;
                }
                $zones[] = $zone;
            }
        }
        return $zones;
    }

    /**
     * @param array<int|string, mixed> $payload
     * @param array<int|string, mixed> $zones
     * @return array<int|string, mixed>
     */
    private function mergeDrawingZonesIntoDocumentPayload(array $payload, array $zones) : array
    {
        if ($this->isCanonicalStateEnvelope($payload)) {
            $document = $this->extractDocumentBranch($payload);
            $document = $this->mergeDrawingZonesIntoDocumentPayload($document, $zones);
            unset($document['drawingAreasByView'], $document['drawingZones']);
            $payload['document'] = $document;
            $payload['scope'] = isset($payload['scope']) && is_array($payload['scope']) ? $payload['scope'] : [];
            $payload['scope']['document'] = isset($payload['scope']['document']) && is_array($payload['scope']['document'])
                ? $payload['scope']['document']
                : [];
            $payload['scope']['document']['drawingAreas'] = true;
            return $this->removeCanonicalRootDocumentAliases($payload);
        }

        $activeViewId = $this->getPayloadActiveViewId($payload);
        $byView = $this->groupDrawingZonesByView($zones, $activeViewId);
        $flat = $this->flattenDrawingZonesByView($byView);

        $payload['drawingAreasByView'] = $byView;
        $payload['drawingZones'] = $flat;
        $payload['drawingAreas'] = isset($payload['drawingAreas']) && is_array($payload['drawingAreas']) ? $payload['drawingAreas'] : [];

        if ($activeViewId !== '' && isset($byView[$activeViewId])) {
            $payload['drawingAreas']['zones'] = $byView[$activeViewId];
        } elseif (count($byView) === 1) {
            $payload['drawingAreas']['zones'] = reset($byView) ?: [];
        } else {
            $payload['drawingAreas']['zones'] = [];
        }

        if (! array_key_exists('activeDrawingAreaId', $payload['drawingAreas'])) {
            $payload['drawingAreas']['activeDrawingAreaId'] = null;
        }

        return $payload;
    }

    /**
     * @param array<int|string, mixed> $payload
     * @param array<int|string, mixed> $zones
     * @return array<int|string, mixed>
     */
    private function mergeDrawingZonesIntoStatePayload(array $payload, array $zones) : array
    {
        if ($this->isCanonicalStateEnvelope($payload)) {
            $document = $this->mergeDrawingZonesIntoDocumentPayload(
                $this->extractDocumentBranch($payload),
                $zones
            );
            unset($document['drawingAreasByView'], $document['drawingZones']);
            $payload['document'] = $document;
            $payload['scope'] = isset($payload['scope']) && is_array($payload['scope']) ? $payload['scope'] : [];
            $payload['scope']['document'] = isset($payload['scope']['document']) && is_array($payload['scope']['document']) ? $payload['scope']['document'] : [];
            $payload['scope']['document']['drawingAreas'] = true;
            return $this->removeCanonicalRootDocumentAliases($payload);
        }

        $payload = $this->mergeDrawingZonesIntoDocumentPayload($payload, $zones);
        foreach (['document', 'initialDocument', 'documentState'] as $key) {
            if (isset($payload[$key]) && is_array($payload[$key])) {
                $payload[$key] = $this->mergeDrawingZonesIntoDocumentPayload($payload[$key], $zones);
            }
        }

        $payload['scope'] = isset($payload['scope']) && is_array($payload['scope']) ? $payload['scope'] : [];
        $payload['scope']['document'] = isset($payload['scope']['document']) && is_array($payload['scope']['document']) ? $payload['scope']['document'] : [];
        $payload['scope']['document']['drawingAreas'] = true;
        return $payload;
    }

    private function decodeJsonString(string $value) : array
    {
        $trimmed = trim($value);
        if ($trimmed === '') {
            return [];
        }
        $decoded = json_decode($trimmed, true);
        return is_array($decoded) ? $decoded : [];
    }


    private function getBuilderFieldValue(int $postId, string $key) : string
    {
        $post = get_post($postId);
        $isProductConfig = $post && $post->post_type === ProductConfigPostType::TYPE;

        if ($isProductConfig && $key === '_apd_layout') {
            return wp_json_encode($this->defaultProductConfigAdminLayoutPayload(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
        }

        $value = (string) get_post_meta($postId, $key, true);
        if ($value !== '') {
            return $value;
        }

        return match ($key) {
            '_apd_mode' => 'standalone',
            '_apd_theme' => wp_json_encode(['appearance' => 'light'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
            '_apd_layout' => wp_json_encode($isProductConfig ? $this->defaultProductConfigAdminLayoutPayload() : $this->defaultBuilderLayoutPayload(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
            '_apd_modules' => wp_json_encode($this->defaultBuilderModuleConfig(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
            '_apd_permissions' => wp_json_encode($this->defaultPermissionsPayload(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
            '_apd_upload' => wp_json_encode(['strategy' => 'remote', 'endpoint' => add_query_arg('apd_source', is_admin() ? 'admin' : 'frontend', rest_url('asuka-designer/v1/upload')), 'method' => 'POST', 'fieldName' => 'file', 'source' => is_admin() ? 'admin' : 'frontend', 'credentials' => 'same-origin'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
            '_apd_fonts' => '[]',
            '_apd_colors' => wp_json_encode(['colors' => [], 'printingMethods' => []], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
            '_apd_pricing_rules' => wp_json_encode(['enabled' => true, 'rules' => []], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
            '_apd_selection_menu' => wp_json_encode(['type' => 'popover', 'items' => ['font-family', 'text-formatting', 'text-curve', 'colors'], 'open' => false], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
            default => '',
        };
    }

    private function defaultBuilderLayoutPayload() : array
    {
        return $this->buildTemplateLayoutPayload($this->templateModuleSettings(0), '{}');
    }

    private function defaultProductConfigAdminLayoutPayload() : array
    {
        return [
            'left' => [
                'type' => 'default',
                'collapsed' => false,
                'collapsible' => true,
                'items' => [
                    ['kind' => 'views', 'label' => 'Views', 'icon' => 'views', 'zone' => 'left', 'type' => 'floating'],
                    ['kind' => 'designs', 'label' => 'Designs', 'icon' => 'brush', 'zone' => 'left', 'type' => 'floating'],
                    ['kind' => 'text', 'label' => 'Text', 'icon' => 'text', 'zone' => 'left', 'type' => 'floating'],
                    ['id' => 'image', 'kind' => 'image', 'label' => 'Images', 'icon' => 'images', 'zone' => 'left', 'type' => 'floating'],
                    ['kind' => 'shapes', 'label' => 'Shapes', 'icon' => 'customize', 'zone' => 'left', 'type' => 'floating'],
                    ['kind' => 'layers', 'label' => 'Layers', 'icon' => 'layers', 'zone' => 'left', 'type' => 'floating'],
                    ['id' => 'elements', 'kind' => 'elements', 'label' => 'Elements', 'icon' => 'puzzle', 'zone' => 'left', 'type' => 'floating'],
                    ['id' => 'drawingAreas', 'kind' => 'drawingAreas', 'label' => 'Drawing Areas', 'icon' => 'drawing-areas', 'zone' => 'left', 'type' => 'floating'],
                    ['id' => 'marker', 'kind' => 'marker', 'label' => 'Marker', 'icon' => 'pin', 'zone' => 'left', 'type' => 'floating'],
                ],
            ],
            'top' => [
                'type' => 'default',
                'collapsed' => false,
                'collapsible' => true,
                'items' => [
                    ['kind' => 'zoom', 'label' => 'Zoom', 'icon' => 'zoom', 'zone' => 'top', 'type' => 'floating'],
                    ['kind' => 'ruler', 'label' => 'Ruler', 'icon' => 'ruler', 'zone' => 'top', 'type' => 'floating'],
                    ['kind' => 'magnifier', 'label' => 'Magnifier', 'icon' => 'magnifying-glass', 'zone' => 'top', 'type' => 'floating'],
                    ['kind' => 'download', 'label' => 'Download', 'icon' => 'download', 'zone' => 'top', 'type' => 'floating'],
                    ['kind' => 'state', 'label' => 'State', 'icon' => 'database', 'zone' => 'top', 'type' => 'floating'],
                    ['kind' => 'history', 'label' => 'History', 'icon' => 'history', 'zone' => 'top', 'type' => 'floating'],
                ],
            ],
            'right' => [
                'type' => 'default',
                'collapsed' => false,
                'collapsible' => true,
                'items' => [],
            ],
            'bottom' => [
                'type' => 'default',
                'collapsed' => true,
                'collapsible' => true,
                'items' => [],
            ],
        ];
    }

    private function defaultBuilderModuleConfig() : array
    {
        return [
            'designs' => [
                'items' => [],
            ],
            'shapes' => [
                'svgAssets' => [],
            ],
        ];
    }

    private function templateApplyPayload(int $templateId) : array
    {
        $payload = (new TemplateRepository())->getMetaPayload($templateId);
        if ($payload === []) {
            return [];
        }

        return [
            'id' => (int) ($payload['ID'] ?? 0),
            'title' => (string) ($payload['title'] ?? ''),
            'meta' => array_intersect_key($payload, array_flip(array_keys($this->fields))),
        ];
    }

    private function templateExists(int $templateId) : bool
    {
        $post = get_post($templateId);
        return $post && $post->post_type === TemplatePostType::TYPE;
    }

    private function templateModuleDefinitions() : array
    {
        return [
            __('Creation and editing', 'asuka-product-designer') => [
                'views' => ['label' => __('Views', 'asuka-product-designer'), 'description' => __('View switcher module.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'views', 'type' => 'floating', 'default' => true],
                'designs' => ['label' => __('Designs', 'asuka-product-designer'), 'description' => __('Global design library.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'brush', 'type' => 'floating', 'default' => true],
                'text' => ['label' => __('Text', 'asuka-product-designer'), 'description' => __('Text creation module.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'text', 'type' => 'floating', 'default' => true],
                'image' => ['label' => __('Images', 'asuka-product-designer'), 'description' => __('Image insertion module.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'images', 'type' => 'floating', 'default' => true],
                'shapes' => ['label' => __('Shapes', 'asuka-product-designer'), 'description' => __('Shape library.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'customize', 'type' => 'floating', 'default' => true],
                'layers' => ['label' => __('Layers', 'asuka-product-designer'), 'description' => __('Layer inspector.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'layers', 'type' => 'floating', 'default' => true],
                'elements' => ['label' => __('Elements', 'asuka-product-designer'), 'description' => __('Reusable elements module.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'puzzle', 'type' => 'floating', 'default' => true],
                'element-variation-picker' => ['label' => __('Variations', 'asuka-product-designer'), 'description' => __('Opens automatically when a configurable 2D or 3D element part is selected.', 'asuka-product-designer'), 'zone' => 'right', 'icon' => 'variations', 'type' => 'floating', 'default' => true, 'layoutMenu' => false, 'selectionMenu' => false, 'contextualByDefault' => true],
                'element-editor' => ['label' => __('Element editor', 'asuka-product-designer'), 'description' => __('Advanced editor for element structure and variations.', 'asuka-product-designer'), 'zone' => 'right', 'icon' => 'element-editor', 'type' => 'modal', 'default' => false],
                'drawingAreas' => ['label' => __('Drawing Areas', 'asuka-product-designer'), 'description' => __('Drawing area tools.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'drawing-areas', 'type' => 'floating', 'default' => true, 'licenseFeature' => 'drawingAreas', 'addonFile' => 'drawing-areas-addon.js'],
                'marker' => ['label' => __('Marker', 'asuka-product-designer'), 'description' => __('Fixed marker tools.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'pin', 'type' => 'floating', 'default' => true, 'licenseFeature' => 'fixedSlots', 'addonFile' => 'marker-addon.js'],
                'colors' => ['label' => __('Colors', 'asuka-product-designer'), 'description' => __('Color palette module.', 'asuka-product-designer'), 'zone' => 'right', 'icon' => 'palette', 'type' => 'floating', 'default' => true, 'selectionMenu' => true],
                'studio' => ['label' => __('Studio', 'asuka-product-designer'), 'description' => __('Image and SVG editing studio.', 'asuka-product-designer'), 'zone' => 'right', 'icon' => 'studio', 'type' => 'modal', 'default' => true, 'layoutMenu' => false, 'selectionMenu' => true, 'licenseFeature' => 'studioBasic', 'addonFile' => 'studio-addon.js'],
            ],
            __('3D product tools', 'asuka-product-designer') => [
                'product-parameters' => ['label' => __('Product parameters', 'asuka-product-designer'), 'description' => __('Live controls for configurable 3D product dimensions and parameters.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'product-parameters', 'type' => 'floating', 'default' => false, 'licenseFeature' => 'productParameters', 'addonFile' => 'product-parameters-addon.js'],
                'assembly' => ['label' => __('Spatial assembly', 'asuka-product-designer'), 'description' => __('Configure plans, anchors, snapping, collision rules, BOMs, quotes, and production exports for 3D components.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'magnet', 'type' => 'floating', 'default' => false, 'licenseFeature' => 'assembly', 'addonFile' => 'assembly-addon.js'],
                'camera' => ['label' => __('Camera', 'asuka-product-designer'), 'description' => __('Control and save the active 3D camera position.', 'asuka-product-designer'), 'zone' => 'top', 'icon' => 'camera', 'type' => 'floating', 'default' => false],
            ],
            __('Utilities and output', 'asuka-product-designer') => [
                'zoom' => ['label' => __('Zoom', 'asuka-product-designer'), 'description' => __('Zoom controls.', 'asuka-product-designer'), 'zone' => 'top', 'icon' => 'zoom', 'type' => 'floating', 'default' => true],
                'download' => ['label' => __('Download', 'asuka-product-designer'), 'description' => __('Export/download module.', 'asuka-product-designer'), 'zone' => 'top', 'icon' => 'download', 'type' => 'floating', 'default' => true],
                'magnifier' => ['label' => __('Magnifier', 'asuka-product-designer'), 'description' => __('Magnifier tool.', 'asuka-product-designer'), 'zone' => 'top', 'icon' => 'magnifying-glass', 'type' => 'floating', 'default' => true],
                'ruler' => ['label' => __('Ruler', 'asuka-product-designer'), 'description' => __('Ruler module.', 'asuka-product-designer'), 'zone' => 'top', 'icon' => 'ruler', 'type' => 'floating', 'default' => true],
                'history' => ['label' => __('History', 'asuka-product-designer'), 'description' => __('Undo/redo history module.', 'asuka-product-designer'), 'zone' => 'top', 'icon' => 'history', 'type' => 'floating', 'default' => true],
                'state' => ['label' => __('State', 'asuka-product-designer'), 'description' => __('State tools module.', 'asuka-product-designer'), 'zone' => 'top', 'icon' => 'database', 'type' => 'floating', 'default' => false],
                'pricing' => ['label' => __('Pricing', 'asuka-product-designer'), 'description' => __('Pricing overview module.', 'asuka-product-designer'), 'zone' => 'right', 'icon' => 'price', 'type' => 'floating', 'default' => false, 'licenseFeature' => 'pricing', 'addonFile' => 'pricing-addon.js'],
                'pricingRules' => ['label' => __('Pricing Rules', 'asuka-product-designer'), 'description' => __('Conditional pricing rules editor.', 'asuka-product-designer'), 'zone' => 'right', 'icon' => 'pricing-rules', 'type' => 'floating', 'default' => false, 'licenseFeature' => 'pricingRules', 'addonFile' => 'pricing-rules-addon.js'],
                'modelOptimizer' => ['label' => __('GLB optimizer', 'asuka-product-designer'), 'description' => __('Prepare and optimize GLB assets for 3D products.', 'asuka-product-designer'), 'zone' => 'top', 'icon' => 'model-optimizer', 'type' => 'modal', 'default' => false, 'licenseFeature' => 'modelOptimization', 'addonFile' => 'model-optimizer-addon.js'],
                'license' => ['label' => __('License', 'asuka-product-designer'), 'description' => __('Inspect runtime licensing, quotas, and activation state.', 'asuka-product-designer'), 'zone' => 'top', 'icon' => 'license', 'type' => 'floating', 'default' => false],
            ],
            __('Contextual tools', 'asuka-product-designer') => [
                'font-family' => ['label' => __('Font family', 'asuka-product-designer'), 'description' => __('Font family module.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'text', 'type' => 'floating', 'default' => true, 'layoutMenu' => false, 'selectionMenu' => true],
                'text-formatting' => ['label' => __('Text formatting', 'asuka-product-designer'), 'description' => __('Text formatting module.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'text-format', 'type' => 'floating', 'default' => true, 'layoutMenu' => false, 'selectionMenu' => true],
                'text-curve' => ['label' => __('Text curve', 'asuka-product-designer'), 'description' => __('Text curve module.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'text-curve', 'type' => 'floating', 'default' => true, 'layoutMenu' => false, 'selectionMenu' => true],
                'transform' => ['label' => __('Transform', 'asuka-product-designer'), 'description' => __('Transform module.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'move', 'type' => 'floating', 'default' => true, 'layoutMenu' => false, 'selectionMenu' => true],
            ],
            __('Image shortcuts', 'asuka-product-designer') => [
                'image-upload' => ['label' => __('Upload image', 'asuka-product-designer'), 'description' => __('Open the upload workflow directly.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'upload', 'type' => 'floating', 'default' => false],
                'image-qrcode' => ['label' => __('QR code', 'asuka-product-designer'), 'description' => __('Open the local QR code generator directly.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'qr-code', 'type' => 'floating', 'default' => false],
                'image-ai-text-to-image' => ['label' => __('AI image', 'asuka-product-designer'), 'description' => __('Generate an image from a text prompt.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'ai-image', 'type' => 'floating', 'default' => false, 'licenseFeature' => 'aiCredits'],
                'image-pixabay' => ['label' => __('Pixabay', 'asuka-product-designer'), 'description' => __('Open Pixabay image search directly.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'pixabay', 'type' => 'floating', 'default' => false, 'licenseFeature' => 'pixabay', 'integrationSetting' => 'pixabay_api_key'],
                'image-pexels' => ['label' => __('Pexels', 'asuka-product-designer'), 'description' => __('Open Pexels image search directly.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'pexels', 'type' => 'floating', 'default' => false, 'licenseFeature' => 'pexels', 'integrationSetting' => 'pexels_api_key'],
                'image-facebook' => ['label' => __('Facebook', 'asuka-product-designer'), 'description' => __('Open the configured Facebook image library directly.', 'asuka-product-designer'), 'zone' => 'left', 'icon' => 'facebook', 'type' => 'floating', 'default' => false, 'licenseFeature' => 'facebookImages', 'integrationSetting' => 'facebook_app_id'],
            ],
        ];
    }

    private function templateModuleDefinitionsFlat() : array
    {
        $flat = [];
        foreach ($this->templateModuleDefinitions() as $group) {
            foreach ($group as $id => $meta) {
                $flat[$id] = $meta;
            }
        }
        return $flat;
    }

    private function templateModuleGroupDescription(string $groupSlug) : string
    {
        return match ($groupSlug) {
            'creation-and-editing' => __('The tools customers use to build and manage their design.', 'asuka-product-designer'),
            '3d-product-tools' => __('Controls shown when the active view exposes compatible 3D capabilities.', 'asuka-product-designer'),
            'utilities-and-output' => __('Navigation, history, pricing, inspection, and export tools.', 'asuka-product-designer'),
            'contextual-tools' => __('Object-aware controls that can live in the layout, the selection menu, or both.', 'asuka-product-designer'),
            'image-shortcuts' => __('Optional shortcuts to specific image workflows and licensed providers.', 'asuka-product-designer'),
            default => __('Configure the modules in this group.', 'asuka-product-designer'),
        };
    }

    private function templateModuleAvailability(array $meta) : array
    {
        $feature = trim((string) ($meta['licenseFeature'] ?? ''));
        if ($feature !== '' && ! AdminAccess::canUse($feature)) {
            return [
                'available' => false,
                'reason' => __('Not included in the active license.', 'asuka-product-designer'),
                'status' => 'license',
            ];
        }

        $addonFile = trim((string) ($meta['addonFile'] ?? ''));
        if ($addonFile !== '' && ! DesignerAssets::exists($addonFile)) {
            return [
                'available' => false,
                'reason' => __('The licensed add-on file is not installed.', 'asuka-product-designer'),
                'status' => 'chunk',
            ];
        }

        $integrationSetting = sanitize_key((string) ($meta['integrationSetting'] ?? ''));
        if ($integrationSetting !== '') {
            $settings = (new AdminSettings())->all();
            if (trim((string) ($settings[$integrationSetting] ?? '')) === '') {
                return [
                    'available' => false,
                    'reason' => __('Configure this provider in Asuka Designer settings first.', 'asuka-product-designer'),
                    'status' => 'integration',
                ];
            }
        }

        return [
            'available' => true,
            'reason' => '',
            'status' => $feature !== '' ? 'licensed' : 'core',
        ];
    }

    private function normalizeTemplateModuleKey(string $key) : string
    {
        $key = sanitize_key($key);
        if ($key === 'images') {
            return 'image';
        }
        if ($key === 'fixedslots' || $key === 'fixed-slots') {
            return 'marker';
        }
        if ($key === 'variations' || $key === 'elementvariationpicker') {
            return 'element-variation-picker';
        }
        if ($key === 'drawingareas' || $key === 'drawing-areas') {
            return 'drawingAreas';
        }
        if ($key === 'pricingrules' || $key === 'pricing-rules') {
            return 'pricingRules';
        }
        if ($key === 'modeloptimizer' || $key === 'model-optimizer') {
            return 'modelOptimizer';
        }
        if ($key === 'elementeditor' || $key === 'element-editor') {
            return 'element-editor';
        }
        if ($key === 'productparameters' || $key === 'product-parameters') {
            return 'product-parameters';
        }
        if (in_array($key, ['uploadimage', 'upload-image', 'image-upload'], true)) {
            return 'image-upload';
        }
        if (in_array($key, ['qrcodeimage', 'qrcode-image', 'image-qrcode'], true)) {
            return 'image-qrcode';
        }
        if (in_array($key, ['aitexttoimage', 'ai-text-to-image', 'image-ai-text-to-image'], true)) {
            return 'image-ai-text-to-image';
        }
        if (in_array($key, ['pixabayimage', 'pixabay-image', 'image-pixabay'], true)) {
            return 'image-pixabay';
        }
        if (in_array($key, ['pexelsimage', 'pexels-image', 'image-pexels'], true)) {
            return 'image-pexels';
        }
        if (in_array($key, ['facebookimage', 'facebook-image', 'image-facebook'], true)) {
            return 'image-facebook';
        }
        return $key;
    }

    private function defaultTemplateModuleIds() : array
    {
        return ['views', 'designs', 'text', 'image', 'shapes', 'layers', 'elements', 'drawingAreas', 'marker', 'zoom', 'ruler', 'magnifier', 'download', 'state', 'history', 'colors', 'font-family', 'text-formatting', 'text-curve', 'transform', 'studio'];
    }

    private function isTemplateIconImage(string $icon) : bool
    {
        if ($icon === '') {
            return false;
        }
        return str_starts_with($icon, 'http://') || str_starts_with($icon, 'https://') || str_starts_with($icon, '/') || str_starts_with($icon, 'data:image/');
    }

    private function normalizeTemplatePortalSelector(string $selector) : string
    {
        $selector = trim(wp_strip_all_tags($selector));
        $selector = (string) preg_replace('/[\x00-\x1F\x7F]/u', '', $selector);
        if ($selector === 'document.body') {
            return 'body';
        }
        return function_exists('mb_substr') ? mb_substr($selector, 0, 240) : substr($selector, 0, 240);
    }

    private function templateModuleSettings(int $postId, string $sharedPortalSelector = '') : array
    {
        $definitions = $this->templateModuleDefinitionsFlat();
        $defaultEnabledIds = $this->defaultTemplateModuleIds();
        $rawLayout = (string) get_post_meta($postId, '_apd_layout', true);
        $layout = $this->decodeJsonString($rawLayout);
        $rawSelectionMenu = (string) get_post_meta($postId, '_apd_selection_menu', true);
        $selectionMenu = $this->decodeJsonString($rawSelectionMenu);
        $editorState = $this->decodeJsonString((string) get_post_meta($postId, '_apd_template_editor_state', true));
        $modulePlacements = is_array($editorState['modulePlacements'] ?? null)
            ? (array) $editorState['modulePlacements']
            : [];
        $hasStoredPlacements = $modulePlacements !== [];
        $hasStoredLayout = trim($rawLayout) !== '';
        $hasStoredSelectionMenu = trim($rawSelectionMenu) !== '';
        $settings = [];
        foreach ($definitions as $id => $meta) {
            $savedPlacement = is_array($modulePlacements[$id] ?? null)
                ? (array) $modulePlacements[$id]
                : [];
            $defaultLayoutPlacement = array_key_exists('layoutMenu', $meta)
                ? (bool) $meta['layoutMenu']
                : true;
            $defaultSelectionPlacement = ! empty($meta['selectionMenu']);
            $settings[$id] = [
                'enabled' => in_array($id, $defaultEnabledIds, true),
                'zone' => (string) ($meta['zone'] ?? 'left'),
                'label' => (string) ($meta['label'] ?? $id),
                'type' => (string) ($meta['type'] ?? 'floating'),
                'icon_mode' => 'text',
                'icon_text' => (string) ($meta['icon'] ?? 'puzzle'),
                'icon_image' => '',
                'collapsed' => false,
                'collapsible' => true,
                'layoutMenu' => $hasStoredPlacements
                    ? ! empty($savedPlacement['layout'])
                    : (! $hasStoredLayout && $defaultLayoutPlacement),
                'selectionMenu' => $hasStoredPlacements
                    ? ! empty($savedPlacement['selection'])
                    : (! $hasStoredSelectionMenu && $defaultSelectionPlacement),
                'pageContent' => $hasStoredPlacements
                    ? ! empty($savedPlacement['page'])
                    : false,
                'portal_mode' => 'internal',
                'mount_target' => '',
            ];
        }

        $permissions = $this->decodeJsonString((string) get_post_meta($postId, '_apd_permissions', true));
        $moduleOpen = is_array($permissions['moduleOpen'] ?? null) ? (array) $permissions['moduleOpen'] : [];
        $explicitEnabled = [];
        foreach ($moduleOpen as $key => $value) {
            $normalizedKey = $this->normalizeTemplateModuleKey((string) $key);
            if ($normalizedKey === '' || ! isset($settings[$normalizedKey])) {
                continue;
            }
            $explicitEnabled[$normalizedKey] = (bool) $value;
            $settings[$normalizedKey]['enabled'] = (bool) $value;
        }

        foreach (['left', 'right', 'top', 'bottom'] as $layoutZone) {
            foreach ((array) (($layout[$layoutZone]['items'] ?? [])) as $item) {
                if (! is_array($item)) {
                    continue;
                }
                $kind = $this->normalizeTemplateModuleKey((string) ($item['kind'] ?? $item['id'] ?? ''));
                if ($kind === '' || ! isset($settings[$kind])) {
                    continue;
                }
                $icon = (string) ($item['icon'] ?? $settings[$kind]['icon_text'] ?? 'puzzle');
                $rawType = (string) ($item['type'] ?? $settings[$kind]['type']);
                $resolvedZone = $layoutZone;
                if (str_starts_with($rawType, 'panel-')) {
                    $panelZone = substr($rawType, 6);
                    if (in_array($panelZone, ['left', 'right', 'top', 'bottom'], true)) {
                        $resolvedZone = $panelZone;
                    }
                    $rawType = 'panel';
                }
                if (! array_key_exists($kind, $explicitEnabled)) {
                    $settings[$kind]['enabled'] = true;
                }
                $isPageContent = ($item['renderMode'] ?? null) === 'portal';
                $settings[$kind]['layoutMenu'] = ! $isPageContent;
                $settings[$kind]['pageContent'] = $isPageContent;
                $settings[$kind]['zone'] = in_array($resolvedZone, ['left', 'right', 'top', 'bottom'], true) ? $resolvedZone : (string) $settings[$kind]['zone'];
                $settings[$kind]['label'] = (string) ($item['label'] ?? $settings[$kind]['label']);
                $settings[$kind]['type'] = $rawType;
                $settings[$kind]['icon_mode'] = $this->isTemplateIconImage($icon) ? 'image' : 'text';
                $settings[$kind]['icon_text'] = $settings[$kind]['icon_mode'] === 'text' ? $icon : (string) ($settings[$kind]['icon_text'] ?? 'puzzle');
                $settings[$kind]['icon_image'] = $settings[$kind]['icon_mode'] === 'image' ? $icon : '';
                $settings[$kind]['collapsed'] = array_key_exists('collapsed', $item) ? (bool) $item['collapsed'] : false;
                $settings[$kind]['collapsible'] = array_key_exists('collapsible', $item) ? (bool) $item['collapsible'] : true;
                $mountTarget = $this->normalizeTemplatePortalSelector((string) ($item['mountTarget'] ?? ''));
                $settings[$kind]['mount_target'] = $mountTarget;
                $settings[$kind]['portal_mode'] = $mountTarget === ''
                    ? 'internal'
                    : ($mountTarget === 'body'
                        ? 'body'
                        : ($sharedPortalSelector !== '' && $mountTarget === $sharedPortalSelector ? 'shared' : 'custom'));
            }
        }

        if (! $hasStoredPlacements && $hasStoredSelectionMenu) {
            foreach ((array) ($selectionMenu['items'] ?? []) as $moduleId) {
                $moduleId = $this->normalizeTemplateModuleKey((string) $moduleId);
                if ($moduleId !== '' && isset($settings[$moduleId])) {
                    $settings[$moduleId]['selectionMenu'] = true;
                }
            }
        }

        return $settings;
    }

    private function selectedTemplateModuleIds(int $postId) : array
    {
        $selected = [];
        foreach ($this->templateModuleSettings($postId) as $id => $item) {
            if (! empty($item['enabled'])) {
                $selected[] = $id;
            }
        }
        return $selected === [] ? $this->defaultTemplateModuleIds() : array_values(array_unique($selected));
    }

    private function normalizeSubmittedTemplateModules(mixed $submitted, string $sharedPortalSelector = '', array $existingSettings = []) : array
    {
        $submitted = is_array($submitted) ? $submitted : [];
        $definitions = $this->templateModuleDefinitionsFlat();
        $allowedZones = ['left', 'right', 'top', 'bottom'];
        $allowedTypes = ['floating', 'popover', 'panel', 'modal', 'inline', 'drawer', 'overlay'];
        $allowedPortalModes = ['internal', 'shared', 'body', 'custom'];
        $settings = [];

        foreach ($definitions as $id => $meta) {
            $row = is_array($submitted[$id] ?? null) ? (array) $submitted[$id] : [];
            $zone = sanitize_key((string) ($row['zone'] ?? ($meta['zone'] ?? 'left')));
            if (! in_array($zone, $allowedZones, true)) {
                $zone = (string) ($meta['zone'] ?? 'left');
            }
            $type = sanitize_key((string) ($row['type'] ?? ($meta['type'] ?? 'floating')));
            if (! in_array($type, $allowedTypes, true)) {
                $type = (string) ($meta['type'] ?? 'floating');
            }
            $label = sanitize_text_field((string) ($row['label'] ?? ($meta['label'] ?? $id)));
            $iconMode = (string) ($row['icon_mode'] ?? 'text') === 'image' ? 'image' : 'text';
            $iconText = sanitize_text_field((string) ($row['icon_text'] ?? ($meta['icon'] ?? 'puzzle')));
            $iconImage = esc_url_raw((string) ($row['icon_image'] ?? ''));
            $portalMode = sanitize_key((string) ($row['portal_mode'] ?? 'internal'));
            if (! in_array($portalMode, $allowedPortalModes, true)) {
                $portalMode = 'internal';
            }
            $mountTarget = match ($portalMode) {
                'shared' => $sharedPortalSelector,
                'body' => 'body',
                'custom' => $this->normalizeTemplatePortalSelector((string) ($row['mount_target'] ?? '')),
                default => '',
            };
            $pageContent = isset($row['page_content']) && (string) $row['page_content'] === '1';
            if ($pageContent && $mountTarget === '') {
                $portalMode = 'body';
                $mountTarget = 'body';
            }
            $settings[$id] = [
                'enabled' => isset($row['enabled']) && (string) $row['enabled'] === '1',
                'zone' => $zone,
                'label' => $label !== '' ? $label : (string) ($meta['label'] ?? $id),
                'type' => $type,
                'icon_mode' => $iconMode,
                'icon_text' => $iconText !== '' ? $iconText : (string) ($meta['icon'] ?? 'puzzle'),
                'icon_image' => $iconImage,
                'collapsed' => isset($row['collapsed']) && (string) $row['collapsed'] === '1',
                'collapsible' => ! isset($row['collapsible']) || (string) $row['collapsible'] === '1',
                'layoutMenu' => array_key_exists('layout_menu', $row)
                    ? (string) $row['layout_menu'] === '1' && ! $pageContent
                    : (array_key_exists('layoutMenu', $meta) ? (bool) $meta['layoutMenu'] : true),
                'selectionMenu' => array_key_exists('selection_menu', $row)
                    ? (string) $row['selection_menu'] === '1'
                    : ! empty($meta['selectionMenu']),
                'pageContent' => $pageContent,
                'portal_mode' => $portalMode,
                'mount_target' => $mountTarget,
            ];

            $availability = $this->templateModuleAvailability($meta);
            if (empty($availability['available'])) {
                $existing = is_array($existingSettings[$id] ?? null) ? (array) $existingSettings[$id] : [];
                if ($existing !== []) {
                    $settings[$id] = array_replace($settings[$id], $existing);
                } else {
                    $settings[$id]['enabled'] = false;
                }
            }
        }

        return $settings;
    }

    private function buildTemplateLayoutPayload(mixed $submitted, string $existingJson, mixed $submittedZoneSettings = null) : array
    {
        $existing = $this->decodeJsonString($existingJson);
        $definitions = $this->templateModuleDefinitionsFlat();
        $height = (string) ($existing['height'] ?? '820px');
        $zoneTypes = [];
        $submittedZoneSettings = is_array($submittedZoneSettings) ? $submittedZoneSettings : [];
        $allowedZoneTypes = ['default', 'panels', 'inline'];
        foreach (['left', 'right', 'top', 'bottom'] as $zone) {
            $existingZone = is_array($existing[$zone] ?? null) ? (array) $existing[$zone] : [];
            $row = is_array($submittedZoneSettings[$zone] ?? null) ? (array) $submittedZoneSettings[$zone] : [];
            $type = sanitize_key((string) ($row['type'] ?? ($existingZone['type'] ?? 'default')));
            if ($type === 'panel') {
                $type = 'panels';
            }
            if (! in_array($type, $allowedZoneTypes, true)) {
                $type = 'default';
            }
            $zoneTypes[$zone] = [
                'type' => $type,
                'collapsed' => isset($row['collapsed']) ? ((string) $row['collapsed'] === '1') : (array_key_exists('collapsed', $existingZone) ? (bool) $existingZone['collapsed'] : ($zone === 'top')),
                'collapsible' => isset($row['collapsible']) ? ((string) $row['collapsible'] === '1') : (array_key_exists('collapsible', $existingZone) ? (bool) $existingZone['collapsible'] : true),
            ];
        }

        $settings = is_array($submitted) ? $submitted : [];
        $itemsByZone = ['left' => [], 'right' => [], 'top' => [], 'bottom' => []];
        foreach ($settings as $id => $item) {
            if (
                ! is_array($item)
                || empty($item['enabled'])
                || (empty($item['layoutMenu']) && empty($item['pageContent']))
            ) {
                continue;
            }
            $zone = in_array((string) ($item['zone'] ?? 'left'), ['left', 'right', 'top', 'bottom'], true) ? (string) $item['zone'] : 'left';
            $icon = (string) ($item['icon_mode'] ?? 'text') === 'image' && ! empty($item['icon_image'])
                ? (string) $item['icon_image']
                : (string) ($item['icon_text'] ?? 'puzzle');
            $layoutItem = [
                'id' => $id,
                'kind' => $id,
                'label' => (string) ($item['label'] ?? $id),
                'icon' => $icon,
                'zone' => $zone,
                'type' => $this->resolveTemplateModuleDisplayType((string) ($item['type'] ?? 'floating'), $zone),
                'collapsed' => ! empty($item['collapsed']),
                'collapsible' => ! array_key_exists('collapsible', $item) || ! empty($item['collapsible']),
            ];
            if (! empty($item['pageContent'])) {
                $layoutItem['renderMode'] = 'portal';
            }
            $mountTarget = $this->normalizeTemplatePortalSelector((string) ($item['mount_target'] ?? ''));
            if ($mountTarget !== '' && (! empty($item['pageContent']) || (string) ($item['type'] ?? 'floating') !== 'inline')) {
                $layoutItem['mountTarget'] = $mountTarget;
            }
            $itemsByZone[$zone][] = $layoutItem;
        }

        $payload = ['height' => $height];
        foreach ($itemsByZone as $zone => $items) {
            $items = $this->orderTemplateLayoutItemsByExistingLayout($items, $existing, $zone);
            $payload[$zone] = [
                'items' => array_values($items),
            ];
            $payload[$zone]['collapsed'] = (bool) ($zoneTypes[$zone]['collapsed'] ?? ($zone === 'top'));
            $payload[$zone]['collapsible'] = (bool) ($zoneTypes[$zone]['collapsible'] ?? true);
            if (! empty($zoneTypes[$zone]['type'])) {
                $payload[$zone]['type'] = (string) $zoneTypes[$zone]['type'];
            }
        }

        return $payload;
    }

    private function mergePostedTemplateLayoutWithStructuredSettings(array $postedLayout, array $structuredLayout) : array
    {
        $posted = LayoutStateConverter::normalizeConfigLayout($postedLayout, $structuredLayout);
        $structured = LayoutStateConverter::normalizeConfigLayout($structuredLayout);
        $structuredItems = [];

        foreach (['left', 'right', 'top', 'bottom'] as $zone) {
            foreach ((array) ($structured[$zone]['items'] ?? []) as $item) {
                if (! is_array($item)) {
                    continue;
                }
                $key = $this->templateLayoutItemKey($item);
                if ($key !== '') {
                    $structuredItems[$key] = $item;
                }
            }
        }

        $merged = ['height' => (string) ($posted['height'] ?? $structured['height'] ?? '820px')];
        $used = [];
        foreach (['left', 'right', 'top', 'bottom'] as $zone) {
            $merged[$zone] = [
                'items' => [],
                'collapsed' => (bool) ($structured[$zone]['collapsed'] ?? false),
                'collapsible' => (bool) ($structured[$zone]['collapsible'] ?? true),
                'type' => (string) ($structured[$zone]['type'] ?? 'default'),
            ];

            foreach ((array) ($posted[$zone]['items'] ?? []) as $postedItem) {
                if (! is_array($postedItem)) {
                    continue;
                }
                $key = $this->templateLayoutItemKey($postedItem);
                if ($key === '' || ! isset($structuredItems[$key])) {
                    continue;
                }

                $structuredItem = $structuredItems[$key];
                foreach (['label', 'icon', 'type', 'collapsed', 'collapsible', 'renderMode'] as $field) {
                    if (array_key_exists($field, $structuredItem)) {
                        $postedItem[$field] = $structuredItem[$field];
                    }
                }
                if (array_key_exists('mountTarget', $structuredItem)) {
                    $postedItem['mountTarget'] = $structuredItem['mountTarget'];
                } else {
                    unset($postedItem['mountTarget']);
                }
                $postedItem['id'] = (string) ($postedItem['id'] ?? $key);
                $postedItem['kind'] = (string) ($postedItem['kind'] ?? $key);
                $postedItem['zone'] = $zone;
                $postedItem['order'] = count($merged[$zone]['items']);
                $merged[$zone]['items'][] = $postedItem;
                $used[$key] = true;
            }
        }

        foreach (['left', 'right', 'top', 'bottom'] as $zone) {
            foreach ((array) ($structured[$zone]['items'] ?? []) as $item) {
                if (! is_array($item)) {
                    continue;
                }
                $key = $this->templateLayoutItemKey($item);
                if ($key === '' || isset($used[$key])) {
                    continue;
                }
                $item['zone'] = $zone;
                $item['order'] = count($merged[$zone]['items']);
                $merged[$zone]['items'][] = $item;
                $used[$key] = true;
            }
        }

        return LayoutStateConverter::normalizeConfigLayout($merged, $structured);
    }

    private function orderTemplateLayoutItemsByExistingLayout(array $items, array $existingLayout, string $zone) : array
    {
        $zoneConfig = is_array($existingLayout[$zone] ?? null) ? (array) $existingLayout[$zone] : [];
        $existingItems = is_array($zoneConfig['items'] ?? null) ? (array) $zoneConfig['items'] : [];
        $orderMap = [];

        foreach ($existingItems as $index => $item) {
            if (! is_array($item)) {
                continue;
            }
            $key = $this->templateLayoutItemKey($item);
            if ($key === '' || array_key_exists($key, $orderMap)) {
                continue;
            }
            $orderMap[$key] = $this->templateLayoutItemOrder($item, (int) $index);
        }

        $sortable = [];
        foreach ($items as $index => $item) {
            if (! is_array($item)) {
                continue;
            }
            $key = $this->templateLayoutItemKey($item);
            $sortable[] = [
                'item' => $item,
                'sourceIndex' => (int) $index,
                'order' => $key !== '' && array_key_exists($key, $orderMap) ? (int) $orderMap[$key] : 100000 + (int) $index,
            ];
        }

        usort($sortable, static function (array $a, array $b) : int {
            $orderCompare = ((int) $a['order']) <=> ((int) $b['order']);
            if ($orderCompare !== 0) {
                return $orderCompare;
            }
            return ((int) $a['sourceIndex']) <=> ((int) $b['sourceIndex']);
        });

        $ordered = [];
        foreach ($sortable as $index => $entry) {
            $item = (array) $entry['item'];
            $item['order'] = $index;
            $ordered[] = $item;
        }

        return $ordered;
    }

    private function templateLayoutItemKey(array $item) : string
    {
        return sanitize_key((string) ($item['kind'] ?? ($item['id'] ?? ($item['module'] ?? ''))));
    }

    private function templateLayoutItemOrder(array $item, int $fallbackIndex) : int
    {
        foreach (['order', 'index', 'position'] as $key) {
            if (array_key_exists($key, $item) && is_numeric($item[$key])) {
                return max(0, (int) $item[$key]);
            }
        }

        return $fallbackIndex;
    }

    private function resolveTemplateModuleDisplayType(string $type, string $zone) : string
    {
        $type = sanitize_key($type);
        $zone = in_array($zone, ['left', 'right', 'top', 'bottom'], true) ? $zone : 'left';

        if ($type === 'panel') {
            return 'panel-' . $zone;
        }

        $allowed = ['floating', 'popover', 'modal', 'inline', 'drawer', 'overlay', 'panel-left', 'panel-right', 'panel-top', 'panel-bottom'];
        if (! in_array($type, $allowed, true)) {
            return 'floating';
        }

        return $type;
    }

    private function templateModulePermissionRequirements() : array
    {
        return [
            'views' => ['canViewViews', 'canSwitchView'],
            'designs' => ['canViewDesigns', 'canAddDesigns'],
            'text' => ['canAddText', 'canEditText', 'canEditObjectText'],
            'image' => ['canOpenImages', 'canAddImage', 'canUploadImage'],
            'image-upload' => ['canOpenImages', 'canAddImage', 'canUploadImage'],
            'image-qrcode' => ['canOpenImages', 'canAddImage'],
            'image-ai-text-to-image' => ['canOpenImages', 'canAddImage'],
            'image-pixabay' => ['canOpenImages', 'canAddImage', 'canUseRemoteImages', 'canUseExternalImageLibraries', 'canUsePixabayImages'],
            'image-pexels' => ['canOpenImages', 'canAddImage', 'canUseRemoteImages', 'canUseExternalImageLibraries', 'canUsePexelsImages'],
            'image-facebook' => ['canOpenImages', 'canAddImage', 'canUseRemoteImages', 'canUseExternalImageLibraries', 'canUseFacebookImages'],
            'shapes' => ['canAddRect', 'canAddShape', 'canDrawShape'],
            'layers' => ['canViewLayers', 'canReorderLayers', 'canLockLayers', 'canHideLayers', 'canSelectLayers'],
            'colors' => ['canUseColors', 'canEditColors', 'canEditObjectColors'],
            'zoom' => ['canUseZoom', 'canZoomIn', 'canZoomOut', 'canFitViewport', 'canResetViewport'],
            'magnifier' => 'canUseMagnifier',
            'download' => ['canDownload', 'canDownloadCurrentView', 'canDownloadAllViews', 'canDownloadObjects', 'canDownloadDrawingZones'],
            'history' => ['canUndo', 'canRedo', 'canReset'],
            'state' => ['canUseStateTools', 'canExportJson', 'canImportJson'],
            'pricing' => 'canViewPricing',
            'pricingRules' => ['canViewPricing', 'canEditPricingRules', 'canAddPricingRule', 'canRemovePricingRule'],
            'studio' => ['canUseStudio', 'canApplyStudioEffects', 'canEditObjectStudio'],
            'drawingAreas' => 'canViewDrawingAreas',
            'marker' => 'canViewFixedSlots',
            'elements' => 'canViewElements',
            'element-variation-picker' => 'canViewElements',
            'element-editor' => ['canViewElements', 'canEditElements'],
            'font-family' => ['canEditText', 'canEditFontFamily'],
            'text-formatting' => ['canEditText', 'canEditTextFormatting'],
            'text-curve' => ['canEditText', 'canEditTextCurve'],
            'transform' => ['canMoveObject', 'canResizeObject', 'canRotateObject'],
        ];
    }

    private function buildTemplatePermissionsPayload(mixed $submitted, string $existingJson) : array
    {
        $existing = $this->decodeJsonString($existingJson);
        $permissions = is_array($existing) ? $existing : [];
        $settings = is_array($submitted) ? $submitted : [];
        $moduleOpen = is_array($permissions['moduleOpen'] ?? null) ? (array) $permissions['moduleOpen'] : [];
        foreach ($settings as $id => $item) {
            $enabled = ! empty($item['enabled']);
            $aliasesUpdated = false;
            foreach ($this->moduleAliasGroups() as $aliases) {
                if (! in_array((string) $id, $aliases, true)) {
                    continue;
                }
                $this->setModuleAliases($moduleOpen, $aliases, $enabled);
                $aliasesUpdated = true;
                break;
            }
            if (! $aliasesUpdated) {
                $moduleOpen[$id] = $enabled;
            }
        }

        foreach ($this->templateModulePermissionRequirements() as $moduleId => $permissionKeys) {
            if (! empty($settings[$moduleId]['enabled'])) {
                foreach ((array) $permissionKeys as $permissionKey) {
                    $permissions[$permissionKey] = true;
                }
            }
        }

        $permissions['canOpenModule'] = true;
        $permissions['moduleOpen'] = $moduleOpen;
        return $this->normalizeClientPermissionsPayload($permissions);
    }

    private function buildTemplateSelectionMenuPayload(mixed $submitted, string $existingJson) : array
    {
        $existing = $this->decodeJsonString($existingJson);
        $payload = is_array($existing) ? $existing : [];
        $items = [];
        $settings = is_array($submitted) ? $submitted : [];
        foreach ($settings as $id => $item) {
            if (empty($item['selectionMenu']) || empty($item['enabled'])) {
                continue;
            }
            $items[] = $id;
        }
        $payload['type'] = (string) ($payload['type'] ?? 'popover');
        $payload['items'] = array_values($items);
        $payload['open'] = false;
        return $payload;
    }

    private function defaultPermissionsPayload() : array
    {
        $payload = [];
        foreach ($this->permissionDefinitions() as $key => $meta) {
            $payload[$key] = (bool) ($meta['default'] ?? false);
        }

        $moduleOpen = [];
        foreach ($this->modulePermissionDefinitions() as $key => $meta) {
            $moduleOpen[$key] = (bool) ($meta['default'] ?? false);
        }
        $payload['moduleOpen'] = $moduleOpen;

        return $this->normalizeClientPermissionsPayload($payload);
    }

    private function permissionDefinitions() : array
    {
        return [
            'canOpenModule' => ['label' => __('Open modules', 'asuka-product-designer'), 'description' => __('Allow customers to open tool panels and module surfaces.', 'asuka-product-designer'), 'default' => true],
            'canMovePanels' => ['label' => __('Move panels', 'asuka-product-designer'), 'description' => __('Allow customers to drag menus between layout zones and reorder them when the frontend layout supports it.', 'asuka-product-designer'), 'default' => true],
            'canCollapsePanels' => ['label' => __('Collapse panels', 'asuka-product-designer'), 'description' => __('Allow customers to collapse and expand menus without granting layout editing by itself.', 'asuka-product-designer'), 'default' => true],
            'canUseStateTools' => ['label' => __('Use state tools', 'asuka-product-designer'), 'description' => __('Allow opening state/debug tools when the State module is also available.', 'asuka-product-designer'), 'default' => false],
            'canUseMagnifier' => ['label' => __('Use magnifier', 'asuka-product-designer'), 'description' => __('Allow the magnifier tool/module.', 'asuka-product-designer'), 'default' => true],
            'canUseZoom' => ['label' => __('Use zoom tools', 'asuka-product-designer'), 'description' => __('Allow zoom controls to be used.', 'asuka-product-designer'), 'default' => true],
            'canZoomIn' => ['label' => __('Zoom in', 'asuka-product-designer'), 'description' => __('Allow customers to zoom in.', 'asuka-product-designer'), 'default' => true],
            'canZoomOut' => ['label' => __('Zoom out', 'asuka-product-designer'), 'description' => __('Allow customers to zoom out.', 'asuka-product-designer'), 'default' => true],
            'canFitViewport' => ['label' => __('Fit viewport', 'asuka-product-designer'), 'description' => __('Allow fitting the canvas into the viewport.', 'asuka-product-designer'), 'default' => true],
            'canResetViewport' => ['label' => __('Reset viewport', 'asuka-product-designer'), 'description' => __('Allow resetting zoom and viewport position.', 'asuka-product-designer'), 'default' => true],
            'canPanCanvas' => ['label' => __('Pan canvas', 'asuka-product-designer'), 'description' => __('Allow panning the canvas viewport.', 'asuka-product-designer'), 'default' => true],

            'canUndo' => ['label' => __('Undo', 'asuka-product-designer'), 'description' => __('Allow undo actions.', 'asuka-product-designer'), 'default' => true],
            'canRedo' => ['label' => __('Redo', 'asuka-product-designer'), 'description' => __('Allow redo actions.', 'asuka-product-designer'), 'default' => true],
            'canReset' => ['label' => __('Reset design', 'asuka-product-designer'), 'description' => __('Allow resetting the design to its initial state.', 'asuka-product-designer'), 'default' => true],

            'canAddRect' => ['label' => __('Add rectangles', 'asuka-product-designer'), 'description' => __('Allow adding rectangle objects.', 'asuka-product-designer'), 'default' => true],
            'canAddText' => ['label' => __('Add text', 'asuka-product-designer'), 'description' => __('Allow adding text objects.', 'asuka-product-designer'), 'default' => true],
            'canAddImage' => ['label' => __('Add images', 'asuka-product-designer'), 'description' => __('Allow image-based personalization in this integration.', 'asuka-product-designer'), 'default' => true],
            'canAddShape' => ['label' => __('Add shapes', 'asuka-product-designer'), 'description' => __('Allow adding reusable shape assets.', 'asuka-product-designer'), 'default' => true],
            'canDrawShape' => ['label' => __('Draw shapes', 'asuka-product-designer'), 'description' => __('Allow drawing shape objects where supported.', 'asuka-product-designer'), 'default' => true],
            'canClearCanvas' => ['label' => __('Clear canvas', 'asuka-product-designer'), 'description' => __('Allow clearing the current canvas.', 'asuka-product-designer'), 'default' => true],
            'canDuplicateObject' => ['label' => __('Duplicate objects', 'asuka-product-designer'), 'description' => __('Allow duplicating selected objects.', 'asuka-product-designer'), 'default' => true],
            'canDeleteObject' => ['label' => __('Delete objects', 'asuka-product-designer'), 'description' => __('Allow deleting selected objects.', 'asuka-product-designer'), 'default' => true],
            'canMoveObject' => ['label' => __('Move objects', 'asuka-product-designer'), 'description' => __('Allow moving canvas objects.', 'asuka-product-designer'), 'default' => true],
            'canResizeObject' => ['label' => __('Resize objects', 'asuka-product-designer'), 'description' => __('Allow resizing canvas objects.', 'asuka-product-designer'), 'default' => true],
            'canRotateObject' => ['label' => __('Rotate objects', 'asuka-product-designer'), 'description' => __('Allow rotating canvas objects.', 'asuka-product-designer'), 'default' => true],
            'canEditObjectColors' => ['label' => __('Edit object colors', 'asuka-product-designer'), 'description' => __('Allow editing colors on selected objects.', 'asuka-product-designer'), 'default' => true],
            'canEditObjectStudio' => ['label' => __('Edit objects in Studio', 'asuka-product-designer'), 'description' => __('Allow Studio edits on selected objects.', 'asuka-product-designer'), 'default' => true],
            'canEditObjectText' => ['label' => __('Edit object text', 'asuka-product-designer'), 'description' => __('Allow editing text content on selected objects.', 'asuka-product-designer'), 'default' => true],

            'canSwitchView' => ['label' => __('Switch views', 'asuka-product-designer'), 'description' => __('Allow customers to navigate between views.', 'asuka-product-designer'), 'default' => true],
            'canAddView' => ['label' => __('Add views', 'asuka-product-designer'), 'description' => __('Allow adding views through runtime APIs.', 'asuka-product-designer'), 'default' => false],
            'canViewViews' => ['label' => __('View Views module', 'asuka-product-designer'), 'description' => __('Allow opening and using the Views module.', 'asuka-product-designer'), 'default' => true],
            'canCreateView' => ['label' => __('Create views', 'asuka-product-designer'), 'description' => __('Allow creating new views from the Views module.', 'asuka-product-designer'), 'default' => false],
            'canEditView' => ['label' => __('Edit views', 'asuka-product-designer'), 'description' => __('Allow editing view settings.', 'asuka-product-designer'), 'default' => false],
            'canDeleteView' => ['label' => __('Delete views', 'asuka-product-designer'), 'description' => __('Allow deleting views.', 'asuka-product-designer'), 'default' => false],

            'canViewLayers' => ['label' => __('View layers', 'asuka-product-designer'), 'description' => __('Allow opening the Layers module.', 'asuka-product-designer'), 'default' => true],
            'canReorderLayers' => ['label' => __('Reorder layers', 'asuka-product-designer'), 'description' => __('Allow changing layer order.', 'asuka-product-designer'), 'default' => true],
            'canLockLayers' => ['label' => __('Lock layers', 'asuka-product-designer'), 'description' => __('Allow locking and unlocking layers.', 'asuka-product-designer'), 'default' => true],
            'canHideLayers' => ['label' => __('Hide layers', 'asuka-product-designer'), 'description' => __('Allow hiding and showing layers.', 'asuka-product-designer'), 'default' => true],
            'canSelectLayers' => ['label' => __('Select layers', 'asuka-product-designer'), 'description' => __('Allow selecting objects from the Layers module.', 'asuka-product-designer'), 'default' => true],

            'canViewDesigns' => ['label' => __('View designs', 'asuka-product-designer'), 'description' => __('Allow opening the Designs module.', 'asuka-product-designer'), 'default' => true],
            'canAddDesigns' => ['label' => __('Add designs', 'asuka-product-designer'), 'description' => __('Allow adding design assets to the canvas.', 'asuka-product-designer'), 'default' => true],

            'canOpenImages' => ['label' => __('Open Images module', 'asuka-product-designer'), 'description' => __('Allow opening image tools and libraries.', 'asuka-product-designer'), 'default' => true],
            'canUploadImage' => ['label' => __('Upload images', 'asuka-product-designer'), 'description' => __('Allow uploads from the customer device.', 'asuka-product-designer'), 'default' => true],
            'canUseRemoteImages' => ['label' => __('Use remote images', 'asuka-product-designer'), 'description' => __('Allow loading images from remote URLs.', 'asuka-product-designer'), 'default' => true],
            'canUseExternalImageLibraries' => ['label' => __('Use external image libraries', 'asuka-product-designer'), 'description' => __('Allow third-party image provider modules when configured.', 'asuka-product-designer'), 'default' => true],
            'canUseFacebookImages' => ['label' => __('Use Facebook images', 'asuka-product-designer'), 'description' => __('Allow the Facebook image provider when available.', 'asuka-product-designer'), 'default' => true],
            'canUsePixabayImages' => ['label' => __('Use Pixabay images', 'asuka-product-designer'), 'description' => __('Allow the Pixabay image provider when available.', 'asuka-product-designer'), 'default' => true],
            'canUsePexelsImages' => ['label' => __('Use Pexels images', 'asuka-product-designer'), 'description' => __('Allow the Pexels image provider when available.', 'asuka-product-designer'), 'default' => true],

            'canEditText' => ['label' => __('Edit text', 'asuka-product-designer'), 'description' => __('Allow editing existing text objects.', 'asuka-product-designer'), 'default' => true],
            'canEditFontFamily' => ['label' => __('Edit font family', 'asuka-product-designer'), 'description' => __('Allow changing text fonts.', 'asuka-product-designer'), 'default' => true],
            'canEditTextFormatting' => ['label' => __('Edit text formatting', 'asuka-product-designer'), 'description' => __('Allow changing text style, alignment, and spacing.', 'asuka-product-designer'), 'default' => true],
            'canEditTextCurve' => ['label' => __('Edit text curve', 'asuka-product-designer'), 'description' => __('Allow curved text controls.', 'asuka-product-designer'), 'default' => true],

            'canUseColors' => ['label' => __('Use colors', 'asuka-product-designer'), 'description' => __('Allow opening and applying configured colors.', 'asuka-product-designer'), 'default' => true],
            'canEditColors' => ['label' => __('Edit colors', 'asuka-product-designer'), 'description' => __('Allow editing custom colors where supported.', 'asuka-product-designer'), 'default' => true],
            'canUseStudio' => ['label' => __('Use Studio', 'asuka-product-designer'), 'description' => __('Allow opening the Studio module.', 'asuka-product-designer'), 'default' => true],
            'canApplyStudioEffects' => ['label' => __('Apply Studio effects', 'asuka-product-designer'), 'description' => __('Allow applying Studio effects to the canvas.', 'asuka-product-designer'), 'default' => true],

            'canDownload' => ['label' => __('Download exports', 'asuka-product-designer'), 'description' => __('Allow using download/export features.', 'asuka-product-designer'), 'default' => true],
            'canDownloadCurrentView' => ['label' => __('Download current view', 'asuka-product-designer'), 'description' => __('Allow exporting the active view.', 'asuka-product-designer'), 'default' => true],
            'canDownloadAllViews' => ['label' => __('Download all views', 'asuka-product-designer'), 'description' => __('Allow exporting all views.', 'asuka-product-designer'), 'default' => true],
            'canDownloadObjects' => ['label' => __('Download objects', 'asuka-product-designer'), 'description' => __('Allow exporting selected objects.', 'asuka-product-designer'), 'default' => true],
            'canDownloadDrawingZones' => ['label' => __('Download drawing zones', 'asuka-product-designer'), 'description' => __('Allow exporting drawing areas/zones.', 'asuka-product-designer'), 'default' => true],
            'canExportJson' => ['label' => __('Export JSON', 'asuka-product-designer'), 'description' => __('Allow JSON export actions when a JSON/state UI is exposed.', 'asuka-product-designer'), 'default' => false],
            'canImportJson' => ['label' => __('Import JSON', 'asuka-product-designer'), 'description' => __('Allow JSON import actions when a JSON/state UI is exposed.', 'asuka-product-designer'), 'default' => false],

            'canViewDrawingAreas' => ['label' => __('View drawing areas', 'asuka-product-designer'), 'description' => __('Allow opening the Drawing areas module.', 'asuka-product-designer'), 'default' => true],
            'canCreateDrawingAreas' => ['label' => __('Create drawing areas', 'asuka-product-designer'), 'description' => __('Allow creating drawing areas from the runtime UI.', 'asuka-product-designer'), 'default' => false],
            'canEditDrawingAreas' => ['label' => __('Edit drawing areas', 'asuka-product-designer'), 'description' => __('Allow editing drawing area settings.', 'asuka-product-designer'), 'default' => false],
            'canDeleteDrawingAreas' => ['label' => __('Delete drawing areas', 'asuka-product-designer'), 'description' => __('Allow deleting drawing areas.', 'asuka-product-designer'), 'default' => false],

            'canViewFixedSlots' => ['label' => __('View markers', 'asuka-product-designer'), 'description' => __('Allow opening the Marker / fixed slots module.', 'asuka-product-designer'), 'default' => true],
            'canCreateFixedSlots' => ['label' => __('Create markers', 'asuka-product-designer'), 'description' => __('Allow creating markers/fixed slots.', 'asuka-product-designer'), 'default' => false],
            'canEditFixedSlots' => ['label' => __('Edit markers', 'asuka-product-designer'), 'description' => __('Allow editing markers/fixed slots.', 'asuka-product-designer'), 'default' => false],
            'canDeleteFixedSlots' => ['label' => __('Delete markers', 'asuka-product-designer'), 'description' => __('Allow deleting markers/fixed slots.', 'asuka-product-designer'), 'default' => false],

            'canViewPricing' => ['label' => __('View pricing', 'asuka-product-designer'), 'description' => __('Allow opening pricing-related modules.', 'asuka-product-designer'), 'default' => false],
            'canEditPricingBasePrice' => ['label' => __('Edit pricing base price', 'asuka-product-designer'), 'description' => __('Allow editing the designer pricing base from runtime tools.', 'asuka-product-designer'), 'default' => false],
            'canEditPricingRules' => ['label' => __('Edit pricing rules', 'asuka-product-designer'), 'description' => __('Allow editing pricing rules from runtime modules.', 'asuka-product-designer'), 'default' => false],
            'canAddPricingRule' => ['label' => __('Add pricing rules', 'asuka-product-designer'), 'description' => __('Allow adding or duplicating pricing rules.', 'asuka-product-designer'), 'default' => false],
            'canRemovePricingRule' => ['label' => __('Remove pricing rules', 'asuka-product-designer'), 'description' => __('Allow deleting pricing rules from runtime modules.', 'asuka-product-designer'), 'default' => false],

            'canViewElements' => ['label' => __('View element catalog', 'asuka-product-designer'), 'description' => __('Allow opening the documented Elements module.', 'asuka-product-designer'), 'default' => true],
            'canAddElements' => ['label' => __('Add elements', 'asuka-product-designer'), 'description' => __('Allow adding reusable elements to the canvas.', 'asuka-product-designer'), 'default' => false],
            'canEditElements' => ['label' => __('Edit elements', 'asuka-product-designer'), 'description' => __('Allow editing reusable catalog elements.', 'asuka-product-designer'), 'default' => false],
            'canEditElementVariations' => ['label' => __('Edit element variations', 'asuka-product-designer'), 'description' => __('Allow editing element variations from runtime modules.', 'asuka-product-designer'), 'default' => false],
            'canDeleteElements' => ['label' => __('Delete elements', 'asuka-product-designer'), 'description' => __('Allow deleting catalog elements or element instances where supported.', 'asuka-product-designer'), 'default' => false],
        ];
    }

    private function modulePermissionDefinitions() : array
    {
        return [
            'views' => ['label' => __('Views', 'asuka-product-designer'), 'description' => __('Allow opening the Views module.', 'asuka-product-designer'), 'default' => false],
            'designs' => ['label' => __('Designs', 'asuka-product-designer'), 'description' => __('Allow opening the Designs module.', 'asuka-product-designer'), 'default' => true],
            'text' => ['label' => __('Text', 'asuka-product-designer'), 'description' => __('Allow opening the Text module.', 'asuka-product-designer'), 'default' => true],
            'image' => ['label' => __('Images', 'asuka-product-designer'), 'description' => __('Allow opening the Images module.', 'asuka-product-designer'), 'default' => true],
            'image-upload' => ['label' => __('Upload image', 'asuka-product-designer'), 'description' => __('Allow opening the image upload shortcut.', 'asuka-product-designer'), 'default' => false],
            'image-qrcode' => ['label' => __('QR code', 'asuka-product-designer'), 'description' => __('Allow opening the QR code shortcut.', 'asuka-product-designer'), 'default' => false],
            'image-ai-text-to-image' => ['label' => __('AI image', 'asuka-product-designer'), 'description' => __('Allow opening the licensed AI image shortcut.', 'asuka-product-designer'), 'default' => false],
            'image-pixabay' => ['label' => __('Pixabay', 'asuka-product-designer'), 'description' => __('Allow opening the configured Pixabay shortcut.', 'asuka-product-designer'), 'default' => false],
            'image-pexels' => ['label' => __('Pexels', 'asuka-product-designer'), 'description' => __('Allow opening the configured Pexels shortcut.', 'asuka-product-designer'), 'default' => false],
            'image-facebook' => ['label' => __('Facebook', 'asuka-product-designer'), 'description' => __('Allow opening the configured Facebook shortcut.', 'asuka-product-designer'), 'default' => false],
            'shapes' => ['label' => __('Shapes', 'asuka-product-designer'), 'description' => __('Allow opening the Shapes module.', 'asuka-product-designer'), 'default' => true],
            'layers' => ['label' => __('Layers', 'asuka-product-designer'), 'description' => __('Allow opening the Layers module.', 'asuka-product-designer'), 'default' => true],
            'colors' => ['label' => __('Colors', 'asuka-product-designer'), 'description' => __('Allow opening the Colors module.', 'asuka-product-designer'), 'default' => true],
            'zoom' => ['label' => __('Zoom', 'asuka-product-designer'), 'description' => __('Allow opening the Zoom module.', 'asuka-product-designer'), 'default' => true],
            'magnifier' => ['label' => __('Magnifier', 'asuka-product-designer'), 'description' => __('Allow opening the Magnifier module.', 'asuka-product-designer'), 'default' => true],
            'download' => ['label' => __('Download', 'asuka-product-designer'), 'description' => __('Allow opening the Download module.', 'asuka-product-designer'), 'default' => true],
            'history' => ['label' => __('History', 'asuka-product-designer'), 'description' => __('Allow opening the History module.', 'asuka-product-designer'), 'default' => true],
            'state' => ['label' => __('State', 'asuka-product-designer'), 'description' => __('Allow opening the State module.', 'asuka-product-designer'), 'default' => false],
            'pricing' => ['label' => __('Pricing', 'asuka-product-designer'), 'description' => __('Allow opening the Pricing module.', 'asuka-product-designer'), 'default' => false],
            'pricingRules' => ['label' => __('Pricing rules', 'asuka-product-designer'), 'description' => __('Allow opening the Pricing rules module.', 'asuka-product-designer'), 'default' => false],
            'elements' => ['label' => __('Elements', 'asuka-product-designer'), 'description' => __('Allow opening the Elements module.', 'asuka-product-designer'), 'default' => false],
            'element-variation-picker' => ['label' => __('Variations', 'asuka-product-designer'), 'description' => __('Allow customers to choose product element variations.', 'asuka-product-designer'), 'default' => true],
            'drawingAreas' => ['label' => __('Drawing areas', 'asuka-product-designer'), 'description' => __('Allow opening the Drawing areas module.', 'asuka-product-designer'), 'default' => false],
            'marker' => ['label' => __('Marker', 'asuka-product-designer'), 'description' => __('Allow opening the Marker module.', 'asuka-product-designer'), 'default' => false],
            'element-editor' => ['label' => __('Element editor', 'asuka-product-designer'), 'description' => __('Allow opening the Element editor module.', 'asuka-product-designer'), 'default' => false],
            'product-parameters' => ['label' => __('Product parameters', 'asuka-product-designer'), 'description' => __('Allow opening licensed 3D product parameter controls.', 'asuka-product-designer'), 'default' => false],
            'assembly' => ['label' => __('Spatial assembly', 'asuka-product-designer'), 'description' => __('Allow opening licensed plan, snapping, validation, BOM, and production assembly tools.', 'asuka-product-designer'), 'default' => false],
            'camera' => ['label' => __('Camera', 'asuka-product-designer'), 'description' => __('Allow opening the 3D camera module.', 'asuka-product-designer'), 'default' => false],
            'modelOptimizer' => ['label' => __('GLB optimizer', 'asuka-product-designer'), 'description' => __('Allow opening the licensed GLB optimizer.', 'asuka-product-designer'), 'default' => false],
            'license' => ['label' => __('License', 'asuka-product-designer'), 'description' => __('Allow opening runtime license diagnostics.', 'asuka-product-designer'), 'default' => false],
        ];
    }

    private function permissionModuleAliasMap() : array
    {
        return [
            'canViewLayers' => ['layers'],
            'canViewDesigns' => ['designs'],
            'canOpenImages' => ['image', 'images'],
            'canUseColors' => ['colors'],
            'canUseZoom' => ['zoom'],
            'canUseMagnifier' => ['magnifier'],
            'canDownload' => ['download'],
            'canUseStateTools' => ['state'],
            'canViewPricing' => ['pricing', 'pricingRules', 'pricing-rules'],
            'canUseStudio' => ['studio'],
            'canUseFacebookImages' => ['image-facebook', 'facebookImage', 'facebook-image'],
            'canUsePixabayImages' => ['image-pixabay', 'pixabayImage', 'pixabay-image'],
            'canUsePexelsImages' => ['image-pexels', 'pexelsImage', 'pexels-image'],
        ];
    }

    private function moduleAliasGroups() : array
    {
        return [
            ['views'],
            ['designs'],
            ['text'],
            ['image', 'images'],
            ['image-upload', 'uploadImage', 'upload-image'],
            ['image-qrcode', 'qrcodeImage', 'qrcode-image'],
            ['image-ai-text-to-image', 'aiTextToImage', 'ai-text-to-image'],
            ['image-facebook', 'facebookImage', 'facebook-image'],
            ['image-pixabay', 'pixabayImage', 'pixabay-image'],
            ['image-pexels', 'pexelsImage', 'pexels-image'],
            ['shapes'],
            ['layers'],
            ['colors'],
            ['zoom'],
            ['magnifier'],
            ['download'],
            ['history'],
            ['state'],
            ['pricing'],
            ['pricingRules', 'pricing-rules'],
            ['elements'],
            ['element-variation-picker', 'elementVariationPicker', 'variations'],
            ['drawingAreas', 'drawing-areas'],
            ['marker', 'fixedSlots', 'fixed-slots'],
            ['element-editor', 'elementEditor'],
            ['product-parameters', 'productParameters'],
            ['assembly'],
            ['camera'],
            ['modelOptimizer', 'model-optimizer'],
            ['license'],
            ['studio'],
            ['ruler'],
            ['font-family'],
            ['text-formatting'],
            ['text-curve'],
            ['transform'],
        ];
    }

    private function normalizeModuleOpenAliases(array $moduleOpen) : array
    {
        foreach ($this->moduleAliasGroups() as $aliases) {
            $hasExplicitFalse = false;
            $hasExplicitTrue = false;
            foreach ($aliases as $alias) {
                if (! array_key_exists($alias, $moduleOpen)) {
                    continue;
                }
                if ($moduleOpen[$alias] === false) {
                    $hasExplicitFalse = true;
                    break;
                }
                if ($moduleOpen[$alias] === true) {
                    $hasExplicitTrue = true;
                }
            }
            if (! $hasExplicitFalse && ! $hasExplicitTrue) {
                continue;
            }
            $resolved = ! $hasExplicitFalse;
            foreach ($aliases as $alias) {
                $moduleOpen[$alias] = $resolved;
            }
        }

        return array_filter($moduleOpen, static fn ($value) => $value === true || $value === false);
    }

    private function hasDisabledModuleAlias(array $moduleOpen, array $aliases) : bool
    {
        foreach ($aliases as $alias) {
            if (array_key_exists($alias, $moduleOpen) && $moduleOpen[$alias] === false) {
                return true;
            }
        }
        return false;
    }

    private function setModuleAliases(array &$moduleOpen, array $aliases, bool $enabled) : void
    {
        foreach ($aliases as $alias) {
            $moduleOpen[$alias] = $enabled;
        }
    }

    private function constrainModuleAliases(array &$moduleOpen, array $aliases, bool $allowed) : void
    {
        if (! $allowed) {
            $this->setModuleAliases($moduleOpen, $aliases, false);
            return;
        }

        foreach ($aliases as $alias) {
            if (array_key_exists($alias, $moduleOpen)) {
                return;
            }
        }

        $this->setModuleAliases($moduleOpen, $aliases, true);
    }

    private function setPermissionsFalse(array &$permissions, array $keys) : void
    {
        foreach ($keys as $key) {
            $permissions[$key] = false;
        }
    }

    private function syncStructuredPermissionModuleAliases(array $permissions, array &$moduleOpen) : void
    {
        foreach ($this->permissionModuleAliasMap() as $permissionKey => $aliases) {
            if (! array_key_exists($permissionKey, $permissions) || ! is_bool($permissions[$permissionKey])) {
                continue;
            }
            $this->setModuleAliases($moduleOpen, $aliases, (bool) $permissions[$permissionKey]);
        }

        $this->syncModuleAliasesFromAnyPermission($permissions, $moduleOpen, ['canAddText', 'canEditText'], ['text']);
        $this->syncModuleAliasesFromAnyPermission($permissions, $moduleOpen, ['canAddRect', 'canAddShape', 'canDrawShape'], ['shapes']);
        $this->syncModuleAliasesFromAnyPermission($permissions, $moduleOpen, ['canUndo', 'canRedo', 'canReset'], ['history']);
    }

    private function syncModuleAliasesFromAnyPermission(array $permissions, array &$moduleOpen, array $permissionKeys, array $aliases) : void
    {
        $hasKnownPermission = false;
        $hasEnabledPermission = false;
        foreach ($permissionKeys as $permissionKey) {
            if (! array_key_exists($permissionKey, $permissions) || ! is_bool($permissions[$permissionKey])) {
                continue;
            }
            $hasKnownPermission = true;
            if ((bool) $permissions[$permissionKey]) {
                $hasEnabledPermission = true;
            }
        }

        if (! $hasKnownPermission) {
            return;
        }

        $this->setModuleAliases($moduleOpen, $aliases, $hasEnabledPermission);
    }

    private function normalizeClientPermissionsPayload(array $permissions) : array
    {
        $moduleOpen = is_array($permissions['moduleOpen'] ?? null) ? (array) $permissions['moduleOpen'] : [];
        $moduleOpen = $this->normalizeModuleOpenAliases($moduleOpen);

        if (($permissions['canOpenModule'] ?? null) === false) {
            foreach ($this->moduleAliasGroups() as $aliases) {
                $this->setModuleAliases($moduleOpen, $aliases, false);
            }
        }

        if ($this->hasDisabledModuleAlias($moduleOpen, ['designs'])) {
            $permissions['canViewDesigns'] = false;
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['text'])) {
            $this->setPermissionsFalse($permissions, ['canAddText', 'canEditText']);
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['image', 'images'])) {
            $permissions['canOpenImages'] = false;
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['shapes'])) {
            $this->setPermissionsFalse($permissions, ['canAddRect', 'canAddShape', 'canDrawShape']);
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['layers'])) {
            $permissions['canViewLayers'] = false;
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['colors'])) {
            $permissions['canUseColors'] = false;
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['zoom'])) {
            $permissions['canUseZoom'] = false;
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['magnifier'])) {
            $permissions['canUseMagnifier'] = false;
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['download'])) {
            $permissions['canDownload'] = false;
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['history'])) {
            $this->setPermissionsFalse($permissions, ['canUndo', 'canRedo', 'canReset']);
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['state'])) {
            $permissions['canUseStateTools'] = false;
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['pricing', 'pricingRules', 'pricing-rules'])) {
            $permissions['canViewPricing'] = false;
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['studio'])) {
            $permissions['canUseStudio'] = false;
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['image-facebook', 'facebookImage', 'facebook-image'])) {
            $permissions['canUseFacebookImages'] = false;
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['image-pixabay', 'pixabayImage', 'pixabay-image'])) {
            $permissions['canUsePixabayImages'] = false;
        }
        if ($this->hasDisabledModuleAlias($moduleOpen, ['image-pexels', 'pexelsImage', 'pexels-image'])) {
            $permissions['canUsePexelsImages'] = false;
        }

        if (($permissions['canCreateView'] ?? null) === false || ($permissions['canAddView'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canCreateView', 'canAddView']);
        }
        if (($permissions['canViewViews'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canCreateView', 'canAddView', 'canEditView', 'canDeleteView']);
            $this->setModuleAliases($moduleOpen, ['views'], false);
        }
        if (($permissions['canViewDesigns'] ?? null) === false) {
            $permissions['canAddDesigns'] = false;
            $this->setModuleAliases($moduleOpen, ['designs'], false);
        }
        if (($permissions['canOpenImages'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canAddImage', 'canUploadImage', 'canUseRemoteImages', 'canUseExternalImageLibraries', 'canUseFacebookImages', 'canUsePixabayImages', 'canUsePexelsImages']);
            $this->setModuleAliases($moduleOpen, [
                'image',
                'images',
                'image-upload',
                'uploadImage',
                'upload-image',
                'image-qrcode',
                'qrcodeImage',
                'qrcode-image',
                'image-ai-text-to-image',
                'aiTextToImage',
                'ai-text-to-image',
                'image-facebook',
                'facebookImage',
                'facebook-image',
                'image-pixabay',
                'pixabayImage',
                'pixabay-image',
                'image-pexels',
                'pexelsImage',
                'pexels-image',
            ], false);
        }
        if (($permissions['canAddImage'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canUploadImage', 'canUseRemoteImages']);
            $this->setModuleAliases($moduleOpen, [
                'image-upload',
                'uploadImage',
                'upload-image',
                'image-qrcode',
                'qrcodeImage',
                'qrcode-image',
                'image-ai-text-to-image',
                'aiTextToImage',
                'ai-text-to-image',
                'image-facebook',
                'facebookImage',
                'facebook-image',
                'image-pixabay',
                'pixabayImage',
                'pixabay-image',
                'image-pexels',
                'pexelsImage',
                'pexels-image',
            ], false);
        }
        if (($permissions['canUploadImage'] ?? null) === false) {
            $this->setModuleAliases($moduleOpen, ['image-upload', 'uploadImage', 'upload-image'], false);
        }
        if (($permissions['canUseExternalImageLibraries'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canUseFacebookImages', 'canUsePixabayImages', 'canUsePexelsImages']);
            $this->setModuleAliases($moduleOpen, ['image-facebook', 'facebookImage', 'facebook-image', 'image-pixabay', 'pixabayImage', 'pixabay-image', 'image-pexels', 'pexelsImage', 'pexels-image'], false);
        }
        if (($permissions['canUseFacebookImages'] ?? null) === false) {
            $this->setModuleAliases($moduleOpen, ['image-facebook', 'facebookImage', 'facebook-image'], false);
        }
        if (($permissions['canUsePixabayImages'] ?? null) === false) {
            $this->setModuleAliases($moduleOpen, ['image-pixabay', 'pixabayImage', 'pixabay-image'], false);
        }
        if (($permissions['canUsePexelsImages'] ?? null) === false) {
            $this->setModuleAliases($moduleOpen, ['image-pexels', 'pexelsImage', 'pexels-image'], false);
        }
        if (($permissions['canEditText'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canEditFontFamily', 'canEditTextFormatting', 'canEditTextCurve', 'canEditObjectText']);
        }
        $contextualModulesAllowed = ($permissions['canOpenModule'] ?? true) !== false;
        $this->constrainModuleAliases(
            $moduleOpen,
            ['font-family'],
            $contextualModulesAllowed && ($permissions['canEditFontFamily'] ?? true) !== false
        );
        $this->constrainModuleAliases(
            $moduleOpen,
            ['text-formatting'],
            $contextualModulesAllowed && ($permissions['canEditTextFormatting'] ?? true) !== false
        );
        $this->constrainModuleAliases(
            $moduleOpen,
            ['text-curve'],
            $contextualModulesAllowed && ($permissions['canEditTextCurve'] ?? true) !== false
        );
        $this->constrainModuleAliases(
            $moduleOpen,
            ['transform'],
            $contextualModulesAllowed && (
                ($permissions['canMoveObject'] ?? true) !== false
                || ($permissions['canResizeObject'] ?? true) !== false
                || ($permissions['canRotateObject'] ?? true) !== false
            )
        );
        if (($permissions['canAddShape'] ?? null) === false) {
            $permissions['canDrawShape'] = false;
        }
        if (($permissions['canViewLayers'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canReorderLayers', 'canLockLayers', 'canHideLayers', 'canSelectLayers']);
            $this->setModuleAliases($moduleOpen, ['layers'], false);
        }
        if (($permissions['canUseColors'] ?? null) === false) {
            $permissions['canEditColors'] = false;
            $this->setModuleAliases($moduleOpen, ['colors'], false);
        }
        if (($permissions['canUseZoom'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canZoomIn', 'canZoomOut', 'canFitViewport', 'canResetViewport']);
            $this->setModuleAliases($moduleOpen, ['zoom'], false);
        }
        if (($permissions['canUseMagnifier'] ?? null) === false) {
            $this->setModuleAliases($moduleOpen, ['magnifier'], false);
        }
        if (($permissions['canDownload'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canDownloadCurrentView', 'canDownloadAllViews', 'canDownloadObjects', 'canDownloadDrawingZones']);
            $this->setModuleAliases($moduleOpen, ['download'], false);
        }
        if (($permissions['canUseStateTools'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canExportJson', 'canImportJson']);
            $this->setModuleAliases($moduleOpen, ['state'], false);
        }
        if (($permissions['canUseStudio'] ?? null) === false) {
            $permissions['canApplyStudioEffects'] = false;
            $this->setModuleAliases($moduleOpen, ['studio'], false);
        }
        if (($permissions['canViewDrawingAreas'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canCreateDrawingAreas', 'canEditDrawingAreas', 'canDeleteDrawingAreas']);
            $this->setModuleAliases($moduleOpen, ['drawingAreas', 'drawing-areas'], false);
        }
        if (($permissions['canViewFixedSlots'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canCreateFixedSlots', 'canEditFixedSlots', 'canDeleteFixedSlots']);
            $this->setModuleAliases($moduleOpen, ['marker', 'fixedSlots', 'fixed-slots'], false);
        }
        if (($permissions['canViewPricing'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canEditPricingBasePrice', 'canEditPricingRules', 'canAddPricingRule', 'canRemovePricingRule']);
            $this->setModuleAliases($moduleOpen, ['pricing', 'pricingRules', 'pricing-rules'], false);
        }
        if (($permissions['canEditPricingRules'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canAddPricingRule', 'canRemovePricingRule']);
        }
        if (($permissions['canViewElements'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canAddElements', 'canEditElements', 'canEditElementVariations', 'canDeleteElements']);
            $this->setModuleAliases($moduleOpen, ['elements', 'element-variation-picker', 'elementVariationPicker', 'variations', 'element-editor', 'elementEditor'], false);
        }
        if (($permissions['canEditElements'] ?? null) === false) {
            $this->setPermissionsFalse($permissions, ['canEditElementVariations', 'canDeleteElements']);
            if (($permissions['canAddElements'] ?? null) === false) {
                $this->setModuleAliases($moduleOpen, ['element-editor', 'elementEditor'], false);
            }
        }

        $moduleOpen = $this->normalizeModuleOpenAliases($moduleOpen);
        if ($moduleOpen !== []) {
            $permissions['moduleOpen'] = $moduleOpen;
        } else {
            unset($permissions['moduleOpen']);
        }

        return $permissions;
    }

    private function buildPermissionsPayload(mixed $submitted, string $existingJson) : array
    {
        $existing = $this->decodeJsonString($existingJson);
        $result = is_array($existing) ? $existing : [];
        foreach ($this->permissionDefinitions() as $key => $meta) {
            $result[$key] = isset($submitted[$key]) && (string) $submitted[$key] === '1';
        }

        $modulePermissions = is_array($result['moduleOpen'] ?? null) ? (array) $result['moduleOpen'] : [];
        $submittedModules = is_array($submitted['moduleOpen'] ?? null) ? (array) $submitted['moduleOpen'] : [];
        foreach ($this->modulePermissionDefinitions() as $key => $meta) {
            $modulePermissions[$key] = isset($submittedModules[$key]) && (string) $submittedModules[$key] === '1';
        }
        $this->syncStructuredPermissionModuleAliases($result, $modulePermissions);
        $modulePermissions = array_filter($modulePermissions, static fn ($value) => $value === true || $value === false);
        if ($modulePermissions === []) {
            unset($result['moduleOpen']);
        } else {
            $result['moduleOpen'] = $modulePermissions;
        }

        return $this->normalizeClientPermissionsPayload($result);
    }

    private function buildUiPayload(mixed $submitted, string $existingJson) : array
    {
        $existing = $this->decodeJsonString($existingJson);
        $result = is_array($existing) ? $existing : [];
        $mode = isset($submitted['viewsPaginationMode']) ? sanitize_key((string) $submitted['viewsPaginationMode']) : 'default';
        $validModes = ['default', 'disabled', 'auto', 'thumbnails', 'dots', 'numbers'];
        if (! in_array($mode, $validModes, true)) {
            $mode = 'default';
        }

        unset($result['viewsPagination'], $result['viewsPaginationMode'], $result['viewsPaginationEnabled']);

        if ($mode === 'disabled') {
            $result['viewsPagination'] = false;
            $result['viewsPaginationEnabled'] = false;
        } elseif ($mode !== 'default') {
            $result['viewsPagination'] = [
                'enabled' => true,
                'mode' => $mode,
            ];
            $result['viewsPaginationMode'] = $mode;
            $result['viewsPaginationEnabled'] = true;
        }

        $historyMode = isset($submitted['historyControlsMode']) ? sanitize_key((string) $submitted['historyControlsMode']) : 'default';
        if (! in_array($historyMode, ['default', 'visible', 'hidden'], true)) {
            $historyMode = 'default';
        }
        unset($result['historyControls']);
        if ($historyMode === 'hidden') {
            $result['historyControls'] = false;
        } elseif ($historyMode === 'visible') {
            $result['historyControls'] = ['undo' => true, 'redo' => true, 'reset' => true];
        }

        return $result;
    }

    private function getConfiguredHistoryControlsMode(array $ui) : string
    {
        if (array_key_exists('historyControls', $ui) && $ui['historyControls'] === false) {
            return 'hidden';
        }
        if (isset($ui['historyControls']) && is_array($ui['historyControls'])) {
            $controls = $ui['historyControls'];
            if (($controls['undo'] ?? true) === false && ($controls['redo'] ?? true) === false && ($controls['reset'] ?? true) === false) {
                return 'hidden';
            }
            return 'visible';
        }
        return 'default';
    }

    private function getConfiguredViewsPaginationMode(array $ui) : string
    {
        if (array_key_exists('viewsPagination', $ui) && $ui['viewsPagination'] === false) {
            return 'disabled';
        }
        if (array_key_exists('viewsPaginationEnabled', $ui) && ! (bool) $ui['viewsPaginationEnabled']) {
            return 'disabled';
        }

        $mode = '';
        if (isset($ui['viewsPagination']) && is_array($ui['viewsPagination']) && isset($ui['viewsPagination']['mode'])) {
            $mode = sanitize_key((string) $ui['viewsPagination']['mode']);
        } elseif (isset($ui['viewsPaginationMode'])) {
            $mode = sanitize_key((string) $ui['viewsPaginationMode']);
        }

        return in_array($mode, ['auto', 'thumbnails', 'dots', 'numbers'], true) ? $mode : 'default';
    }

    private function buildFrontBehaviorPayload(mixed $submitted, string $existingJson) : array
    {
        $existing = $this->decodeJsonString($existingJson);
        $result = is_array($existing) ? $existing : [];

        $booleanKeys = ['requireDesign', 'requirePreview', 'requireText', 'requireEveryView'];
        foreach ($booleanKeys as $key) {
            $result[$key] = isset($submitted[$key]) && (string) $submitted[$key] === '1';
        }

        $numberKeys = ['minObjects', 'maxObjects', 'minTextObjects', 'maxTextObjects', 'minImageObjects', 'maxImageObjects', 'minTextLength', 'requiredViewsCount', 'minObjectsPerView'];
        foreach ($numberKeys as $key) {
            $value = isset($submitted[$key]) ? trim((string) $submitted[$key]) : '';
            if ($value === '') {
                unset($result[$key]);
                continue;
            }
            $result[$key] = (int) $value;
        }

        $listKeys = ['requiredViews', 'allowedObjectTypes', 'forbiddenObjectTypes', 'requiredObjectTypes'];
        foreach ($listKeys as $key) {
            $value = isset($submitted[$key]) ? trim((string) $submitted[$key]) : '';
            if ($value === '') {
                unset($result[$key]);
                continue;
            }
            $parts = preg_split('/[\s,]+/', $value);
            $parts = array_values(array_filter(array_map(static fn ($item) => sanitize_key((string) $item), (array) $parts)));
            if ($parts === []) {
                unset($result[$key]);
                continue;
            }
            $result[$key] = $parts;
        }

        return $result;
    }

    private function renderToggleField(string $name, string $label, bool $checked) : void
    {
        echo '<label class="apd-filter-check apd-filter-check--stacked">';
        echo '<input type="hidden" name="' . esc_attr($name) . '" value="0">';
        echo '<input type="checkbox" name="' . esc_attr($name) . '" value="1" ' . checked($checked, true, false) . '>';
        echo '<span><strong>' . esc_html($label) . '</strong></span>';
        echo '</label>';
    }

    private function renderNumberField(string $name, string $label, mixed $value) : void
    {
        echo '<label class="apd-field">';
        echo '<span>' . esc_html($label) . '</span>';
        echo '<input type="number" min="0" step="1" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '">';
        echo '</label>';
    }

    private function renderCsvField(string $name, string $label, string $value, string $help = '') : void
    {
        echo '<label class="apd-field">';
        echo '<span>' . esc_html($label) . '</span>';
        echo '<input type="text" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '">';
        if ($help !== '') {
            echo '<small class="apd-help-text">' . esc_html($help) . '</small>';
        }
        echo '</label>';
    }

    private function implodeList(mixed $value) : string
    {
        if (! is_array($value)) {
            return '';
        }
        return implode(', ', array_filter(array_map(static fn ($item) => (string) $item, $value)));
    }
}
