<?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\Front;

use APD\Domain\Config\ConfigAssembler;
use APD\Domain\Product\ProductConfigBinder;
use APD\Repositories\ProductConfigRepository;
use APD\Repositories\TemplateRepository;
use APD\Support\Json;
use APD\Support\RequestData;
use APD\Support\UserDesignAccess;

defined('ABSPATH') || exit;

class FrontContextBuilder
{
    public function build(array $atts) : array
    {
        $query = RequestData::query();
        $repository = new ProductConfigRepository();
        $productId = absint($atts['product_id'] ?? 0);
        $variationId = absint($atts['variation_id'] ?? 0);
        $configId = absint($atts['id'] ?? 0);

        if ($configId <= 0 && $productId > 0) {
            $configId = (new ProductConfigBinder())->getConfigIdForProduct($productId, $variationId);
        }

        $sourceConfig = $configId > 0 ? $repository->find($configId) : [];
        $config = $this->mergeAssignedTemplateConfig($sourceConfig);

        $resumeDesignId = absint($atts['resume_design_id'] ?? ($query['apd_resume_design'] ?? 0));
        $resumeToken = UserDesignAccess::sanitizeToken((string) ($atts['resume_token'] ?? ($query['apd_access_token'] ?? '')));
        $resumeDesign = $this->resolveResumeDesign($resumeDesignId, $resumeToken, $productId, $configId);
        if (! empty($resumeDesign)) {
            // Cart resume must follow the exact same path as an admin-saved
            // product/template: replace the product config metadata before the
            // runtime payload is assembled. RuntimeConfigBuilder then exposes the
            // saved cart document as `initialDocument` naturally, so the frontend
            // calls createAsukaDesigner(..., { initialDocument }) with no special
            // post-init canvas restore.
            $config = $this->applyResumeDesignAsProductConfigMeta($config, $resumeDesign);
        }

        $assembler = new ConfigAssembler();
        $runtimeMode = sanitize_key((string) ($atts['mode'] ?? 'standalone'));
        $runtimeHeight = sanitize_text_field((string) ($atts['height'] ?? '720px'));

        $frontendConfig = $assembler->assemble($config, [
            'mode' => $runtimeMode,
            'productId' => $productId,
            'variationId' => $variationId,
            'height' => $runtimeHeight,
            'configId' => $configId,
        ]);


        $assignedTemplateLayout = $this->resolveAssignedTemplateLayout($config);
        $hasAssignedTemplateLayout = $assignedTemplateLayout !== [];
        $frontendConfig = $this->applyAssignedTemplateLayoutToFrontendConfig($config, $frontendConfig, $assignedTemplateLayout);

        if (! empty($resumeDesign)) {
            $frontendConfig['context'] = is_array($frontendConfig['context'] ?? null) ? $frontendConfig['context'] : [];
            $frontendConfig['context']['userDesignId'] = (int) ($resumeDesign['id'] ?? 0);
            $frontendConfig['context']['resumeDesignId'] = (int) ($resumeDesign['id'] ?? 0);
            $frontendConfig['context']['apdBootStrategy'] = 'initial_document_config_override';
            $this->logSubmitResumeDebug('resume:boot-payload', [
                'productId' => $productId,
                'configId' => $configId,
                'resumeDesignId' => (int) ($resumeDesign['id'] ?? 0),
                'design' => $this->summarizeViewPayload(is_array($resumeDesign['designJson'] ?? null) ? $resumeDesign['designJson'] : []),
                'state' => $this->summarizeStatePayload(is_array($resumeDesign['stateJson'] ?? null) ? $resumeDesign['stateJson'] : []),
                'bootInitialDocument' => $this->summarizeViewPayload(is_array($frontendConfig['initialDocument'] ?? null) ? $frontendConfig['initialDocument'] : []),
                'bootInitialState' => $this->summarizeStatePayload(is_array($frontendConfig['initialState'] ?? null) ? $frontendConfig['initialState'] : []),
                'designProof' => $this->summarizeSubmitProof($this->extractSubmitProof(is_array($resumeDesign['designJson'] ?? null) ? $resumeDesign['designJson'] : [])),
            ]);
        }

        if ($runtimeMode === 'admin' && is_admin()) {
            $frontendConfig = $this->forceAdminEditorLayout($frontendConfig, $runtimeHeight);
            $frontendConfig = $this->stripProductLayoutFromBootPayload($frontendConfig);
        } else {
            $frontendConfig = $hasAssignedTemplateLayout
                ? $this->stripProductLayoutFromBootPayload($frontendConfig)
                : $this->forceDefaultLayoutWhenNoTemplateAssigned($frontendConfig);
        }

        $replaceCartItemKey = sanitize_text_field((string) ($query['apd_replace_cart_item_key'] ?? ''));
        $cartEditContext = $this->resolveCartEditContextFromRequest($replaceCartItemKey, $productId, $configId, $resumeDesign, $query);
        if ($cartEditContext !== []) {
            $frontendConfig['context']['replaceCartItemKey'] = $cartEditContext['cartItemKey'];
            $frontendConfig['context']['cartLineToken'] = $cartEditContext['lineToken'];
            $frontendConfig['context']['isCartEdit'] = true;
            $frontendConfig['context']['cartSubmitLabel'] = __('Update cart item', 'asuka-product-designer');
        } else {
            unset($frontendConfig['context']['replaceCartItemKey'], $frontendConfig['context']['cartLineToken'], $frontendConfig['context']['isCartEdit'], $frontendConfig['context']['cartSubmitLabel']);
        }

        $frontendConfig = $this->enableFrontendRuntimeStateBridge($frontendConfig);

        if ($this->isTemplateLayoutDebugEnabled()) {
            $frontendConfig['context']['apdTemplateLayoutDebug'] = $this->buildTemplateLayoutDebugPayload(
                $sourceConfig,
                $config,
                $frontendConfig,
                [
                    'productId' => $productId,
                    'configId' => $configId,
                    'variationId' => $variationId,
                    'resumeDesignId' => (int) ($resumeDesign['id'] ?? 0),
                ]
            );
        }


        return [
            'atts' => $atts,
            'config' => $frontendConfig,
            'productId' => $productId,
            'configId' => $configId,
            'canSaveDesign' => is_user_logged_in(),
            'resumeDesign' => $resumeDesign,
        ];
    }



    protected function resolveCartEditContextFromRequest(string $cartItemKey, int $productId, int $configId, array $resumeDesign, array $query = []) : array
    {
        $cartItemKey = sanitize_text_field($cartItemKey);
        $lineToken = sanitize_text_field((string) ($query['apd_cart_line_token'] ?? ''));
        if ($cartItemKey === '' || $lineToken === '' || ! function_exists('WC') || ! WC()->cart) {
            return [];
        }

        $cartItem = WC()->cart->get_cart_item($cartItemKey);
        if (! is_array($cartItem) || empty($cartItem['apd']) || ! is_array($cartItem['apd'])) {
            return [];
        }

        $apd = $cartItem['apd'];
        $storedToken = sanitize_text_field((string) ($apd['line_token'] ?? ($apd['cart_line_token'] ?? ($cartItem['apd_cart_line_token'] ?? ''))));
        if ($storedToken === '' || ! hash_equals($storedToken, $lineToken)) {
            return [];
        }

        $cartProductId = absint($apd['product_id'] ?? ($cartItem['product_id'] ?? 0));
        $cartConfigId = absint($apd['config_id'] ?? 0);
        if ($productId > 0 && $cartProductId !== $productId) {
            return [];
        }
        if ($configId > 0 && $cartConfigId !== $configId) {
            return [];
        }

        $resumeDesignId = absint($resumeDesign['id'] ?? ($query['apd_resume_design'] ?? 0));
        if ($resumeDesignId > 0 && absint($apd['user_design_id'] ?? 0) !== $resumeDesignId) {
            return [];
        }

        return [
            'cartItemKey' => $cartItemKey,
            'lineToken' => $lineToken,
        ];
    }

    protected function applyResumeDesignAsProductConfigMeta(array $config, array $resumeDesign) : array
    {
        $document = is_array($resumeDesign['designJson'] ?? null) ? $resumeDesign['designJson'] : [];
        $state = is_array($resumeDesign['stateJson'] ?? null) ? $resumeDesign['stateJson'] : [];

        if ($document !== []) {
            $views = $this->extractViewListFromDocumentPayload($document);
            $configDrawingZones = $this->extractDrawingZonesFromConfigMeta($config);
            $documentDrawingZones = $this->extractDrawingZonesFromDocumentPayload($document);
            $drawingZones = $documentDrawingZones !== [] ? $documentDrawingZones : $configDrawingZones;

            // Cart resume must boot exactly like an admin-saved product config.
            // AsukaDesigner consumes drawing areas from the document payload
            // (`document.drawingAreas.byView`), not from WordPress-only top-level
            // config buckets. If the customer-saved design only stores user
            // objects, preserve the product/template drawing zones by merging the
            // config meta into the initial document before ConfigAssembler runs.
            if ($drawingZones !== []) {
                $document = $this->mergeDrawingZonesIntoDocumentPayload($document, $drawingZones);
            }

            $encodedDocument = wp_json_encode($document, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            if (is_string($encodedDocument) && $encodedDocument !== '') {
                $config['_apd_initial_document'] = $encodedDocument;
            }

            if ($views !== []) {
                $encodedViews = wp_json_encode($views, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
                if (is_string($encodedViews) && $encodedViews !== '') {
                    $config['_apd_views'] = $encodedViews;
                }
            }

            if ($drawingZones !== []) {
                $encodedZones = wp_json_encode($this->normalizeDrawingZoneList($drawingZones, $this->payloadActiveViewId($document)), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
                if (is_string($encodedZones)) {
                    $config['_apd_drawing_zones'] = $encodedZones;
                }
            }
        }

        if ($state !== []) {
            $encodedState = wp_json_encode($state, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            if (is_string($encodedState) && $encodedState !== '') {
                $config['_apd_initial_state'] = $encodedState;
            }
        } else {
            $config['_apd_initial_state'] = '';
        }

        return $config;
    }

    protected function extractViewListFromDocumentPayload(array $document) : array
    {
        $views = $document['views'] ?? null;
        if (is_array($views) && isset($views['views']) && is_array($views['views'])) {
            return array_values(array_filter($views['views'], 'is_array'));
        }
        if (is_array($views) && array_is_list($views)) {
            return array_values(array_filter($views, 'is_array'));
        }
        return [];
    }

    protected function extractDrawingZonesFromDocumentPayload(array $document) : array
    {
        $areas = $document['drawingAreas'] ?? null;
        if (is_array($areas) && isset($areas['byView']) && is_array($areas['byView'])) {
            $zones = $this->flattenDrawingAreasByView($areas['byView']);
            if ($zones !== []) {
                return $zones;
            }
        }

        if (isset($document['drawingAreasByView']) && is_array($document['drawingAreasByView'])) {
            $zones = $this->flattenDrawingAreasByView($document['drawingAreasByView']);
            if ($zones !== []) {
                return $zones;
            }
        }

        if (is_array($areas) && isset($areas['zones']) && is_array($areas['zones'])) {
            return array_values(array_filter($areas['zones'], 'is_array'));
        }

        if (isset($document['drawingZones']) && is_array($document['drawingZones'])) {
            return array_values(array_filter($document['drawingZones'], 'is_array'));
        }

        return [];
    }

    protected function extractDrawingZonesFromConfigMeta(array $config) : array
    {
        $raw = trim((string) ($config['_apd_drawing_zones'] ?? ''));
        if ($raw === '') {
            return [];
        }

        $decoded = Json::decode($raw, []);
        if (! is_array($decoded) || $decoded === []) {
            return [];
        }

        if (array_is_list($decoded)) {
            return $this->normalizeDrawingZoneList($decoded);
        }

        if (isset($decoded['byView']) && is_array($decoded['byView'])) {
            return $this->normalizeDrawingZoneList($this->flattenDrawingAreasByView($decoded['byView']));
        }

        if (isset($decoded['drawingAreasByView']) && is_array($decoded['drawingAreasByView'])) {
            return $this->normalizeDrawingZoneList($this->flattenDrawingAreasByView($decoded['drawingAreasByView']));
        }

        if (isset($decoded['drawingAreas']['byView']) && is_array($decoded['drawingAreas']['byView'])) {
            return $this->normalizeDrawingZoneList($this->flattenDrawingAreasByView($decoded['drawingAreas']['byView']));
        }

        if (isset($decoded['drawingAreas']['zones']) && is_array($decoded['drawingAreas']['zones'])) {
            return $this->normalizeDrawingZoneList($decoded['drawingAreas']['zones']);
        }

        if (isset($decoded['zones']) && is_array($decoded['zones'])) {
            return $this->normalizeDrawingZoneList($decoded['zones']);
        }

        if (isset($decoded['drawingZones']) && is_array($decoded['drawingZones'])) {
            return $this->normalizeDrawingZoneList($decoded['drawingZones']);
        }

        return [];
    }

    protected function normalizeDrawingZoneList(array $zones, string $fallbackViewId = '') : array
    {
        $normalized = [];
        foreach ($zones as $zone) {
            if (! is_array($zone)) {
                continue;
            }
            $item = $zone;
            $viewId = $item['viewId'] ?? $item['view'] ?? $item['view_id'] ?? $fallbackViewId;
            $viewId = is_scalar($viewId) ? trim((string) $viewId) : '';
            if ($viewId !== '') {
                $item['viewId'] = $viewId;
            }

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

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

    protected function groupDrawingZonesByView(array $zones, string $fallbackViewId = '') : array
    {
        $byView = [];
        foreach ($this->normalizeDrawingZoneList($zones, $fallbackViewId) as $zone) {
            $viewId = $zone['viewId'] ?? $fallbackViewId;
            $viewId = is_scalar($viewId) && trim((string) $viewId) !== '' ? trim((string) $viewId) : '__default__';
            $zone['viewId'] = $viewId;
            if (! isset($byView[$viewId])) {
                $byView[$viewId] = [];
            }
            $byView[$viewId][] = $zone;
        }
        return $byView;
    }

    protected function mergeDrawingZonesIntoDocumentPayload(array $document, array $zones) : array
    {
        $activeViewId = $this->payloadActiveViewId($document);
        $byView = $this->groupDrawingZonesByView($zones, $activeViewId);
        if ($byView === []) {
            return $document;
        }

        $document['drawingAreasByView'] = $byView;
        $document['drawingZones'] = $this->flattenDrawingAreasByView($byView);
        $document['drawingAreas'] = isset($document['drawingAreas']) && is_array($document['drawingAreas']) ? $document['drawingAreas'] : [];
        $document['drawingAreas']['byView'] = $byView;

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

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

        return $document;
    }


    protected function flattenDrawingAreasByView(array $byView) : array
    {
        $zones = [];
        foreach ($byView as $viewId => $items) {
            if (! is_array($items)) {
                continue;
            }
            foreach ($items as $item) {
                if (! is_array($item)) {
                    continue;
                }
                if (! isset($item['viewId']) && ! isset($item['view'])) {
                    $item['viewId'] = (string) $viewId;
                }
                $zones[] = $item;
            }
        }

        return $zones;
    }


    protected function applyAssignedTemplateLayoutToFrontendConfig(array $config, array $frontendConfig, ?array $templateLayout = null) : array
    {
        $templateLayout = $templateLayout ?? $this->resolveAssignedTemplateLayout($config);
        if ($templateLayout === []) {
            return $frontendConfig;
        }

        $height = is_array($frontendConfig['layout'] ?? null) ? ($frontendConfig['layout']['height'] ?? null) : null;
        $frontendConfig['layout'] = $templateLayout;
        if ($height !== null && ! isset($frontendConfig['layout']['height'])) {
            $frontendConfig['layout']['height'] = $height;
        }

        return $frontendConfig;
    }

    protected function resolveAssignedTemplateLayout(array $config) : array
    {
        $templateId = absint($config['_apd_template_id'] ?? 0);
        if ($templateId <= 0) {
            return [];
        }

        $templatePayload = (new TemplateRepository())->getMetaPayload($templateId);
        $rawLayout = trim((string) ($templatePayload['_apd_layout'] ?? ''));
        if ($rawLayout === '') {
            return [];
        }

        $layout = Json::decode($rawLayout, []);
        return is_array($layout) ? $layout : [];
    }


    protected function stripProductLayoutFromBootPayloadWhenTemplateAssigned(array $config, array $frontendConfig) : array
    {
        if ($this->resolveAssignedTemplateLayout($config) === []) {
            return $frontendConfig;
        }

        return $this->stripProductLayoutFromBootPayload($frontendConfig);
    }

    protected function stripProductLayoutFromBootPayload(array $frontendConfig) : array
    {
        if (isset($frontendConfig['initialDocument']) && is_array($frontendConfig['initialDocument'])) {
            $frontendConfig['initialDocument'] = $this->stripLayoutFromStatePayload($frontendConfig['initialDocument']);
        }

        if (isset($frontendConfig['initialState']) && is_array($frontendConfig['initialState'])) {
            $frontendConfig['initialState'] = $this->stripLayoutFromStatePayload($frontendConfig['initialState']);
        }

        return $frontendConfig;
    }

    protected function forceDefaultLayoutWhenNoTemplateAssigned(array $frontendConfig) : array
    {
        // No assigned template means the JS runtime must boot with its own default
        // layout. Keep this explicit as null so variation hot-switches do not reuse
        // a stale layout saved in the product/template full state.
        $frontendConfig['layout'] = null;
        $frontendConfig = $this->stripProductLayoutFromBootPayload($frontendConfig);
        $frontendConfig['context'] = is_array($frontendConfig['context'] ?? null) ? $frontendConfig['context'] : [];
        $frontendConfig['context']['apdLayoutSource'] = 'default_null';

        return $frontendConfig;
    }

    protected function forceAdminEditorLayout(array $frontendConfig, string $height) : array
    {
        $layout = is_array($frontendConfig['layout'] ?? null) ? $frontendConfig['layout'] : [];
        if ($layout === []) {
            $layout = $this->defaultAdminEditorLayoutPayload($height);
        } else {
            if (! isset($layout['height']) || trim((string) $layout['height']) === '') {
                $layout['height'] = $height !== '' ? $height : '820px';
            }
            $layout = $this->ensureAdminLayoutItem($layout, 'left', [
                'id' => 'drawingAreas',
                'kind' => 'drawingAreas',
                'label' => 'Drawing Areas',
                'icon' => 'drawing-areas',
                'zone' => 'left',
                'type' => 'floating',
            ], ['elements', 'marker']);
        }

        $frontendConfig['layout'] = $layout;
        $frontendConfig['permissions'] = $this->adminEditorPermissionsPayload(is_array($frontendConfig['permissions'] ?? null) ? $frontendConfig['permissions'] : []);
        $frontendConfig['context'] = is_array($frontendConfig['context'] ?? null) ? $frontendConfig['context'] : [];
        $frontendConfig['context']['apdLayoutSource'] = 'admin_editor';
        $frontendConfig['context']['apdAdminEditorPermissions'] = true;
        $frontendConfig['context']['isAdmin'] = true;

        if (isset($frontendConfig['initialDocument']) && is_array($frontendConfig['initialDocument'])) {
            $frontendConfig['initialDocument'] = $this->stripPermissionsFromStatePayload($frontendConfig['initialDocument']);
        }
        if (isset($frontendConfig['initialState']) && is_array($frontendConfig['initialState'])) {
            $frontendConfig['initialState'] = $this->stripPermissionsFromStatePayload($frontendConfig['initialState']);
        }

        return $frontendConfig;
    }

    protected function adminEditorPermissionsPayload(array $existing = []) : array
    {
        $permissions = $existing;
        $keys = [
            'canOpenModule', 'canMovePanels', 'canCollapsePanels', 'canUseStateTools',
            'canUseMagnifier', 'canUseZoom', 'canZoomIn', 'canZoomOut', 'canFitViewport',
            'canResetViewport', 'canPanCanvas', 'canUndo', 'canRedo', 'canReset',
            'canAddRect', 'canAddText', 'canAddImage', 'canAddShape', 'canDrawShape',
            'canClearCanvas', 'canDuplicateObject', 'canDeleteObject', 'canMoveObject',
            'canResizeObject', 'canRotateObject', 'canEditObjectColors', 'canEditObjectStudio',
            'canEditObjectText', 'canSwitchView', 'canAddView', 'canViewViews', 'canCreateView',
            'canEditView', 'canDeleteView', 'canViewLayers', 'canReorderLayers', 'canLockLayers',
            'canHideLayers', 'canSelectLayers', 'canViewDesigns', 'canAddDesigns',
            'canOpenImages', 'canUploadImage', 'canUseRemoteImages', 'canUseExternalImageLibraries',
            'canUseFacebookImages', 'canUsePixabayImages', 'canUsePexelsImages', 'canEditText',
            'canEditFontFamily', 'canEditTextFormatting', 'canEditTextCurve', 'canUseColors',
            'canEditColors', 'canUseStudio', 'canApplyStudioEffects', 'canDownload',
            'canDownloadCurrentView', 'canDownloadAllViews', 'canDownloadObjects',
            'canDownloadDrawingZones', 'canExportJson', 'canImportJson', 'canViewDrawingAreas',
            'canCreateDrawingAreas', 'canEditDrawingAreas', 'canDeleteDrawingAreas',
            'canViewFixedSlots', 'canCreateFixedSlots', 'canEditFixedSlots', 'canDeleteFixedSlots',
            'canViewPricing', 'canEditPricingBasePrice', 'canEditPricingRules', 'canAddPricingRule',
            'canRemovePricingRule', 'canViewElements', 'canAddElements', 'canEditElements',
            'canEditElementVariations', 'canDeleteElements',
        ];
        foreach ($keys as $key) {
            $permissions[$key] = true;
        }
        foreach ($permissions as $key => $value) {
            if (is_bool($value)) {
                $permissions[$key] = true;
            }
        }

        $moduleOpen = is_array($permissions['moduleOpen'] ?? null) ? $permissions['moduleOpen'] : [];
        foreach (array_keys($moduleOpen) as $key) {
            $moduleOpen[$key] = true;
        }
        foreach ([
            'views', 'designs', 'text', 'image', 'images', 'shapes', 'layers',
            'elements', 'drawingAreas', 'marker', 'fixedSlots', 'pricing', 'pricingRules',
            'colors', 'download', 'state', 'history', 'zoom', 'ruler', 'magnifier',
            'studio', 'aiTextToImage', 'facebookImage', 'pixabayImage', 'pexelsImage',
        ] as $key) {
            $moduleOpen[$key] = true;
        }
        $permissions['moduleOpen'] = $moduleOpen;

        return $permissions;
    }

    protected function isAdminEditorRuntimeConfig(array $frontendConfig) : bool
    {
        $context = is_array($frontendConfig['context'] ?? null) ? $frontendConfig['context'] : [];

        return is_admin() && (
            ($frontendConfig['mode'] ?? '') === 'admin'
            || ($context['apdLayoutSource'] ?? '') === 'admin_editor'
            || ! empty($context['apdAdminEditorPermissions'])
            || ! empty($context['isAdmin'])
        );
    }

    protected function stripPermissionsFromStatePayload(array $state) : array
    {
        unset($state['permissions'], $state['permissionsByView'], $state['role']);
        if (isset($state['ui']) && is_array($state['ui'])) {
            unset($state['ui']['permissions'], $state['ui']['permissionsByView']);
        }
        if (isset($state['scope']) && is_array($state['scope'])) {
            unset($state['scope']['permissions'], $state['scope']['permissionsByView']);
        }
        if (isset($state['legacy']) && is_array($state['legacy'])) {
            if (isset($state['legacy']['state']) && is_array($state['legacy']['state'])) {
                $state['legacy']['state'] = $this->stripPermissionsFromStatePayload($state['legacy']['state']);
            }
            if (isset($state['legacy']['scope']) && is_array($state['legacy']['scope'])) {
                unset($state['legacy']['scope']['permissions'], $state['legacy']['scope']['permissionsByView']);
            }
        }
        foreach (['state', 'initialState', 'document', 'documentState', 'initialDocument'] as $nestedKey) {
            if (isset($state[$nestedKey]) && is_array($state[$nestedKey])) {
                $state[$nestedKey] = $this->stripPermissionsFromStatePayload($state[$nestedKey]);
            }
        }

        return $state;
    }

    protected function ensureAdminLayoutItem(array $layout, string $zone, array $item, array $beforeKinds = []) : array
    {
        $layout[$zone] = is_array($layout[$zone] ?? null) ? $layout[$zone] : [];
        $layout[$zone]['type'] = (string) ($layout[$zone]['type'] ?? 'default');
        $layout[$zone]['collapsed'] = (bool) ($layout[$zone]['collapsed'] ?? false);
        $layout[$zone]['collapsible'] = array_key_exists('collapsible', $layout[$zone]) ? (bool) $layout[$zone]['collapsible'] : true;
        $items = is_array($layout[$zone]['items'] ?? null) ? array_values($layout[$zone]['items']) : [];
        $targetKind = (string) ($item['kind'] ?? ($item['id'] ?? ''));

        foreach ($items as $existing) {
            if (! is_array($existing)) {
                continue;
            }
            $existingKind = (string) ($existing['kind'] ?? ($existing['id'] ?? ''));
            if ($existingKind === $targetKind) {
                return $layout;
            }
        }

        $insertAt = count($items);
        foreach ($items as $index => $existing) {
            if (! is_array($existing)) {
                continue;
            }
            $existingKind = (string) ($existing['kind'] ?? ($existing['id'] ?? ''));
            if (in_array($existingKind, $beforeKinds, true)) {
                $insertAt = (int) $index;
                break;
            }
        }

        array_splice($items, $insertAt, 0, [$item]);
        $layout[$zone]['items'] = $items;

        return $layout;
    }

    protected function defaultAdminEditorLayoutPayload(string $height) : array
    {
        return [
            'height' => $height !== '' ? $height : '820px',
            'left' => [
                'type' => 'default',
                'collapsed' => false,
                'collapsible' => true,
                'items' => [
                    ['id' => 'views', 'kind' => 'views', 'label' => 'Views', 'icon' => 'views', 'zone' => 'left', 'type' => 'floating'],
                    ['id' => 'designs', 'kind' => 'designs', 'label' => 'Designs', 'icon' => 'brush', 'zone' => 'left', 'type' => 'floating'],
                    ['id' => 'text', 'kind' => 'text', 'label' => 'Text', 'icon' => 'text', 'zone' => 'left', 'type' => 'floating'],
                    ['id' => 'image', 'kind' => 'image', 'label' => 'Images', 'icon' => 'images', 'zone' => 'left', 'type' => 'floating'],
                    ['id' => 'shapes', 'kind' => 'shapes', 'label' => 'Shapes', 'icon' => 'customize', 'zone' => 'left', 'type' => 'floating'],
                    ['id' => 'layers', 'kind' => 'layers', 'label' => 'Layers', 'icon' => 'layers', 'zone' => 'left', 'type' => 'floating'],
                    ['id' => 'drawingAreas', 'kind' => 'drawingAreas', 'label' => 'Drawing Areas', 'icon' => 'drawing-areas', 'zone' => 'left', 'type' => 'floating'],
                    ['id' => 'elements', 'kind' => 'elements', 'label' => 'Elements', 'icon' => 'puzzle', 'zone' => 'left', 'type' => 'floating'],
                    ['id' => 'marker', 'kind' => 'marker', 'label' => 'Marker', 'icon' => 'pin', 'zone' => 'left', 'type' => 'floating'],
                ],
            ],
            'top' => [
                'type' => 'default',
                'collapsed' => false,
                'collapsible' => true,
                'items' => [
                    ['id' => 'zoom', 'kind' => 'zoom', 'label' => 'Zoom', 'icon' => 'zoom', 'zone' => 'top', 'type' => 'floating'],
                    ['id' => 'ruler', 'kind' => 'ruler', 'label' => 'Ruler', 'icon' => 'ruler', 'zone' => 'top', 'type' => 'floating'],
                    ['id' => 'magnifier', 'kind' => 'magnifier', 'label' => 'Magnifier', 'icon' => 'magnifying-glass', 'zone' => 'top', 'type' => 'floating'],
                    ['id' => 'download', 'kind' => 'download', 'label' => 'Download', 'icon' => 'download', 'zone' => 'top', 'type' => 'floating'],
                    ['id' => 'state', 'kind' => 'state', 'label' => 'State', 'icon' => 'database', 'zone' => 'top', 'type' => 'floating'],
                    ['id' => 'history', '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' => [],
            ],
        ];
    }

    protected function stripLayoutFromStatePayload(array $state) : array
    {
        unset($state['layout']);

        if (isset($state['ui']) && is_array($state['ui'])) {
            unset($state['ui']['layout']);
        }

        if (isset($state['scope']) && is_array($state['scope'])) {
            $state['scope']['layout'] = false;
        }

        if (isset($state['legacy']) && is_array($state['legacy'])) {
            if (isset($state['legacy']['state']) && is_array($state['legacy']['state'])) {
                $state['legacy']['state'] = $this->stripLayoutFromStatePayload($state['legacy']['state']);
            }
            if (isset($state['legacy']['scope']) && is_array($state['legacy']['scope'])) {
                $state['legacy']['scope']['layout'] = false;
            }
        }

        foreach (['state', 'initialState', 'document', 'documentState', 'initialDocument'] as $nestedKey) {
            if (isset($state[$nestedKey]) && is_array($state[$nestedKey])) {
                $state[$nestedKey] = $this->stripLayoutFromStatePayload($state[$nestedKey]);
            }
        }

        return $state;
    }

    protected function enableFrontendRuntimeStateBridge(array $frontendConfig) : array
    {
        $permissions = is_array($frontendConfig['permissions'] ?? null) ? $frontendConfig['permissions'] : [];
        $moduleOpen = is_array($permissions['moduleOpen'] ?? null) ? $permissions['moduleOpen'] : [];

        // The customer UI stays locked down, but the WordPress bridge still needs
        // the runtime state API internally to restore saved designs, sync cart fields,
        // refresh variation drafts, previews, and pricing. Keep the public State module
        // closed while allowing the bridge-level import/export calls to execute.
        $permissions['canUseStateTools'] = true;
        $permissions['canImportJson'] = true;
        $permissions['canExportJson'] = true;
        $moduleOpen['state'] = false;
        $permissions['moduleOpen'] = $moduleOpen;

        $frontendConfig['permissions'] = $this->isAdminEditorRuntimeConfig($frontendConfig)
            ? $this->adminEditorPermissionsPayload($permissions)
            : $permissions;
        $frontendConfig['context'] = is_array($frontendConfig['context'] ?? null) ? $frontendConfig['context'] : [];
        $frontendConfig['context']['apdStateBridgeEnabled'] = true;

        return $frontendConfig;
    }

    protected function resolveBootPayload(mixed $document, mixed $state) : array
    {
        $document = is_array($document) ? $this->normalizeDocumentPayloadForRuntime($document) : [];
        $state = is_array($state) ? $this->normalizeStatePayloadForRuntime($state) : [];

        if ($document !== [] && $this->payloadHasViews($document)) {
            return [
                'initialDocument' => $document,
                'initialState' => null,
            ];
        }

        if ($state !== [] && $this->payloadHasViews($state)) {
            return [
                'initialDocument' => null,
                'initialState' => $state,
            ];
        }

        return [
            'initialDocument' => null,
            'initialState' => null,
        ];
    }


    protected function shouldPreferStateBoot(mixed $state, int $stateObjectCount, int $documentObjectCount) : bool
    {
        if (! is_array($state) || $state === []) {
            return false;
        }

        // Element instances need the Elements catalog/variations stored in the
        // full/modular state. A document-only boot can make those instances look
        // like they were not saved, even though the state payload contains them.
        if ($this->payloadHasElementCatalog($state)) {
            return true;
        }

        if (($state['format'] ?? '') === 'asuka-modular-state' && $stateObjectCount >= $documentObjectCount) {
            return true;
        }

        return $documentObjectCount <= 0 && $stateObjectCount > 0;
    }

    protected function normalizeStatePayloadForRuntime(array $payload) : array
    {
        unset(
            $payload['document'],
            $payload['initialDocument'],
            $payload['documentState'],
            $payload['objectsByView'],
            $payload['documentObjectsByView'],
            $payload['snapshotsByView'],
            $payload['viewSnapshots'],
            $payload['canvas'],
            $payload['legacy']
        );

        $payload['format'] = 'asuka-product-designer-state';
        $payload['schemaVersion'] = 1;
        $payload['scope'] = is_array($payload['scope'] ?? null) ? $payload['scope'] : [];
        $payload['scope']['document'] = false;

        return $payload;
    }

    protected function normalizeDocumentPayloadForRuntime(array $document) : array
    {
        if ($document === []) {
            return [];
        }

        if (! $this->payloadHasViews($document) && isset($document['document']) && is_array($document['document'])) {
            $document = $document['document'];
        } elseif (! $this->payloadHasViews($document) && isset($document['initialDocument']) && is_array($document['initialDocument'])) {
            $document = $document['initialDocument'];
        }

        $views = $document['views'] ?? [];
        if (is_array($views) && isset($views['views']) && is_array($views['views'])) {
            $viewList = array_values($views['views']);
            $activeViewId = (string) ($views['activeViewId'] ?? ($document['activeViewId'] ?? ''));
        } elseif (is_array($views) && array_is_list($views)) {
            $viewList = array_values($views);
            $activeViewId = (string) ($document['activeViewId'] ?? '');
        } else {
            $viewList = [];
            $activeViewId = (string) ($document['activeViewId'] ?? '');
        }

        if ($activeViewId === '' && isset($viewList[0]) && is_array($viewList[0])) {
            $activeViewId = (string) ($viewList[0]['id'] ?? '');
        }

        $snapshotsByView = $this->payloadSnapshotsByView($document);
        $objectsByView = $this->deriveObjectsByViewFromSnapshots($snapshotsByView, $this->payloadObjectsByView($document));
        $rootObjects = method_exists($this, 'payloadRootObjects') ? $this->payloadRootObjects($document) : [];
        if ($rootObjects !== []) {
            $viewId = $activeViewId !== '' ? $activeViewId : $this->payloadActiveViewId($document);
            if (! isset($objectsByView[$viewId]) || ! is_array($objectsByView[$viewId]) || $objectsByView[$viewId] === []) {
                $objectsByView[$viewId] = $rootObjects;
            }
        }

        $canonical = [
            'schemaVersion' => 1,
            'views' => [
                'views' => $viewList,
                'activeViewId' => $activeViewId,
            ],
            'activeViewId' => $activeViewId,
        ];

        if ($objectsByView !== []) {
            $canonical['objects'] = ['byView' => $objectsByView];
        }
        if (isset($document['viewportByView']) && is_array($document['viewportByView'])) {
            $canonical['viewportByView'] = $document['viewportByView'];
        } elseif (isset($document['documentViewportByView']) && is_array($document['documentViewportByView'])) {
            $canonical['viewportByView'] = $document['documentViewportByView'];
        }
        if (isset($document['selection']) && is_array($document['selection'])) {
            $canonical['selection'] = $document['selection'];
        }
        if (isset($document['drawingAreas']) && is_array($document['drawingAreas'])) {
            $canonical['drawingAreas'] = $document['drawingAreas'];
            if (isset($document['drawingAreas']['byView']) && is_array($document['drawingAreas']['byView'])) {
                $canonical['drawingAreasByView'] = $document['drawingAreas']['byView'];
                $canonical['drawingZones'] = $this->flattenDrawingAreasByView($document['drawingAreas']['byView']);
            } elseif (isset($document['drawingAreas']['zones']) && is_array($document['drawingAreas']['zones'])) {
                $canonical['drawingZones'] = $this->normalizeDrawingZoneList(array_values(array_filter($document['drawingAreas']['zones'], 'is_array')), $activeViewId);
                $canonical['drawingAreasByView'] = $this->groupDrawingZonesByView($canonical['drawingZones'], $activeViewId);
                $canonical['drawingAreas']['byView'] = $canonical['drawingAreasByView'];
            }
        } elseif (isset($document['drawingAreasByView']) && is_array($document['drawingAreasByView'])) {
            $canonical['drawingAreasByView'] = $document['drawingAreasByView'];
            $canonical['drawingZones'] = $this->flattenDrawingAreasByView($document['drawingAreasByView']);
            $canonical['drawingAreas'] = [
                'byView' => $document['drawingAreasByView'],
                'zones' => isset($document['drawingAreasByView'][$activeViewId]) && is_array($document['drawingAreasByView'][$activeViewId]) ? $document['drawingAreasByView'][$activeViewId] : $canonical['drawingZones'],
                'activeDrawingAreaId' => $document['activeDrawingAreaId'] ?? null,
            ];
        } elseif (isset($document['drawingZones']) && is_array($document['drawingZones'])) {
            $canonical['drawingZones'] = $this->normalizeDrawingZoneList(array_values(array_filter($document['drawingZones'], 'is_array')), $activeViewId);
            $canonical['drawingAreasByView'] = $this->groupDrawingZonesByView($canonical['drawingZones'], $activeViewId);
            $canonical['drawingAreas'] = [
                'byView' => $canonical['drawingAreasByView'],
                'zones' => isset($canonical['drawingAreasByView'][$activeViewId]) ? $canonical['drawingAreasByView'][$activeViewId] : $canonical['drawingZones'],
                'activeDrawingAreaId' => $document['activeDrawingAreaId'] ?? null,
            ];
        }
        if (isset($document['elements']) && is_array($document['elements']) && $document['elements'] !== []) {
            $canonical['elements'] = array_values($document['elements']);
        }
        if (isset($document['fixedSlots']) && is_array($document['fixedSlots']) && $document['fixedSlots'] !== []) {
            $canonical['fixedSlots'] = array_values($document['fixedSlots']);
            $canonical['fixedSlotActiveId'] = $document['fixedSlotActiveId'] ?? null;
        }
        $scenes3d = $document['scenes3d'] ?? ($document['document3dScenesByView'] ?? null);
        if (is_array($scenes3d) && $scenes3d !== []) {
            $canonical['scenes3d'] = $scenes3d;
        }
        if (isset($document['asukaDocument']) && is_array($document['asukaDocument']) && $document['asukaDocument'] !== []) {
            $canonical['asukaDocument'] = $document['asukaDocument'];
        }

        return $canonical;
    }

    protected function payloadActiveViewId(array $payload) : string
    {
        $activeViewId = (string) ($payload['activeViewId'] ?? '');
        $views = $payload['views'] ?? null;

        if ($activeViewId === '' && is_array($views) && isset($views['activeViewId'])) {
            $activeViewId = (string) $views['activeViewId'];
        }

        if ($activeViewId === '') {
            $viewList = [];
            if (is_array($views) && array_is_list($views)) {
                $viewList = $views;
            } elseif (is_array($views) && isset($views['views']) && is_array($views['views'])) {
                $viewList = $views['views'];
            }

            if (isset($viewList[0]) && is_array($viewList[0])) {
                $activeViewId = (string) ($viewList[0]['id'] ?? '');
            }
        }

        return $activeViewId !== '' ? $activeViewId : 'view-1';
    }

    protected function payloadRootObjects(array $payload) : array
    {
        $canvas = isset($payload['canvas']) && is_array($payload['canvas']) ? $payload['canvas'] : [];

        if (isset($payload['objects']) && is_array($payload['objects']) && array_is_list($payload['objects'])) {
            return $payload['objects'];
        }

        if (isset($payload['objects']['objects']) && is_array($payload['objects']['objects']) && array_is_list($payload['objects']['objects'])) {
            return $payload['objects']['objects'];
        }

        if (isset($canvas['objects']) && is_array($canvas['objects']) && array_is_list($canvas['objects'])) {
            return $canvas['objects'];
        }

        if (isset($canvas['objects']['objects']) && is_array($canvas['objects']['objects']) && array_is_list($canvas['objects']['objects'])) {
            return $canvas['objects']['objects'];
        }

        return [];
    }

    protected function payloadObjectsByView(array $payload) : array
    {
        $canvas = isset($payload['canvas']) && is_array($payload['canvas']) ? $payload['canvas'] : [];
        $candidates = [
            (isset($payload['objects']) && is_array($payload['objects']) && ! array_is_list($payload['objects'])) ? ($payload['objects']['byView'] ?? null) : null,
            $payload['documentObjectsByView'] ?? null,
            $payload['objectsByView'] ?? null,
            $payload['byView'] ?? null,
            (isset($canvas['objects']) && is_array($canvas['objects']) && ! array_is_list($canvas['objects'])) ? ($canvas['objects']['byView'] ?? null) : null,
            $canvas['documentObjectsByView'] ?? null,
            $canvas['objectsByView'] ?? null,
            $canvas['byView'] ?? null,
        ];

        $objectsByView = [];
        foreach ($candidates as $candidate) {
            if (is_array($candidate) && $candidate !== []) {
                $objectsByView = $candidate;
                break;
            }
        }

        $rootObjects = $this->payloadRootObjects($payload);
        if ($rootObjects !== []) {
            $viewId = $this->payloadActiveViewId($payload);
            if (! isset($objectsByView[$viewId]) || ! is_array($objectsByView[$viewId]) || $objectsByView[$viewId] === []) {
                $objectsByView[$viewId] = $rootObjects;
            }
        }

        return $objectsByView;
    }

    protected function payloadSnapshotsByView(array $payload) : array
    {
        $canvas = isset($payload['canvas']) && is_array($payload['canvas']) ? $payload['canvas'] : [];
        $candidates = [
            $payload['snapshotsByView'] ?? null,
            $payload['snapshots']['snapshotsByView'] ?? null,
            $payload['viewSnapshots']['snapshotsByView'] ?? null,
            $payload['viewSnapshots'] ?? null,
            $canvas['snapshotsByView'] ?? null,
            $canvas['snapshots']['snapshotsByView'] ?? null,
            $canvas['viewSnapshots']['snapshotsByView'] ?? null,
            $canvas['viewSnapshots'] ?? null,
        ];

        foreach ($candidates as $candidate) {
            if (is_array($candidate) && $candidate !== []) {
                return $candidate;
            }
        }

        return [];
    }

    protected function deriveObjectsByViewFromSnapshots(array $snapshotsByView, array $objectsByView = []) : array
    {
        foreach ($snapshotsByView as $viewId => $snapshot) {
            $key = (string) $viewId;
            if (isset($objectsByView[$key]) && is_array($objectsByView[$key]) && $objectsByView[$key] !== []) {
                continue;
            }
            if (is_array($snapshot) && isset($snapshot['objects']) && is_array($snapshot['objects'])) {
                $objectsByView[$key] = $snapshot['objects'];
            }
        }

        return $objectsByView;
    }

    protected function payloadHasElementCatalog(mixed $payload) : bool
    {
        if (! is_array($payload) || $payload === []) {
            return false;
        }

        if (isset($payload['catalog']) && is_array($payload['catalog'])) {
            $elements = $payload['catalog']['elements'] ?? null;
            if (is_array($elements) && $elements !== []) {
                return true;
            }
        }

        if (isset($payload['elements']) && is_array($payload['elements']) && $payload['elements'] !== []) {
            return true;
        }

        foreach (['state', 'initialState', 'document', 'documentState', 'initialDocument', 'legacy'] as $key) {
            if (isset($payload[$key]) && $this->payloadHasElementCatalog($payload[$key])) {
                return true;
            }
        }

        return false;
    }

    protected function payloadHasViews(mixed $payload) : bool
    {
        if (! is_array($payload) || $payload === []) {
            return false;
        }

        $views = $payload['views'] ?? null;
        if (is_array($views)) {
            if (array_is_list($views) && $views !== []) {
                return true;
            }
            if (isset($views['views']) && is_array($views['views']) && $views['views'] !== []) {
                return true;
            }
        }

        foreach (['document', 'initialDocument', 'state', 'legacy'] as $key) {
            if (isset($payload[$key]) && $this->payloadHasViews($payload[$key])) {
                return true;
            }
        }

        return false;
    }


    protected function payloadObjectCount(mixed $payload) : int
    {
        if (! is_array($payload) || $payload === []) {
            return 0;
        }

        $count = 0;

        $objectsByView = $this->payloadObjectsByView($payload);
        foreach ($objectsByView as $objects) {
            if (is_array($objects)) {
                $count += count($objects);
            }
        }

        $snapshotsByView = $this->payloadSnapshotsByView($payload);
        foreach ($snapshotsByView as $viewId => $snapshot) {
            $key = (string) $viewId;
            if (isset($objectsByView[$key]) && is_array($objectsByView[$key]) && $objectsByView[$key] !== []) {
                continue;
            }
            if (is_array($snapshot) && isset($snapshot['objects']) && is_array($snapshot['objects'])) {
                $count += count($snapshot['objects']);
            }
        }

        foreach (['document', 'initialDocument', 'documentState', 'state', 'legacy'] as $key) {
            if (isset($payload[$key]) && is_array($payload[$key])) {
                $count += $this->payloadObjectCount($payload[$key]);
            }
        }

        return $count;
    }

    protected function mergeAssignedTemplateConfig(array $config) : array
    {
        if ($config === []) {
            return [];
        }

        $templateId = absint($config['_apd_template_id'] ?? 0);
        if ($templateId <= 0) {
            $config['_apd_layout'] = '';
            return $config;
        }

        $templatePayload = (new TemplateRepository())->getMetaPayload($templateId);
        if ($templatePayload === []) {
            $config['_apd_layout'] = '';
            return $config;
        }

        $merged = $config;
        $productHasBootViews = $this->metaPayloadHasBootViews($config);

        foreach ($this->templateRuntimeInheritanceKeys() as $key) {
            if ($key === '_apd_ui') {
                $productUi = trim((string) ($config[$key] ?? ''));
                $templateUi = trim((string) ($templatePayload[$key] ?? ''));
                if ($productUi !== '' && $templateUi !== '') {
                    $merged[$key] = $this->mergeStorefrontUiMeta($productUi, $templateUi);
                    continue;
                }
            }

            if (! $this->shouldInheritTemplateRuntimeMetaKey($key, $config, $productHasBootViews)) {
                continue;
            }

            $value = (string) ($templatePayload[$key] ?? '');
            if (trim($value) === '') {
                continue;
            }

            // Product canvas data is authoritative. The assigned template is a
            // fallback/starter for empty configs; it must not erase a product
            // document/state that already contains real views.
            if (in_array($key, ['_apd_initial_document', '_apd_initial_state'], true)) {
                $decoded = Json::decode($value, []);
                if (! $this->payloadHasViews(is_array($decoded) ? $decoded : [])) {
                    continue;
                }
            }

            $merged[$key] = $value;
        }

        $merged['_apd_template_id'] = (string) $templateId;
        $merged['_apd_template_source_title'] = (string) ($templatePayload['title'] ?? '');

        return $merged;
    }

    protected function mergeStorefrontUiMeta(string $productRaw, string $templateRaw) : string
    {
        $product = Json::decode($productRaw, []);
        $template = Json::decode($templateRaw, []);
        if (! is_array($product) || ! is_array($template)) {
            return $productRaw;
        }

        $merged = array_replace_recursive($template, $product);
        if ($this->storefrontPaginationIsHidden($product) || $this->storefrontPaginationIsHidden($template)) {
            $merged['viewsPagination'] = false;
            $merged['viewsPaginationEnabled'] = false;
            unset($merged['viewsPaginationMode']);
        }

        $productHistory = $this->storefrontHistoryControls($product);
        $templateHistory = $this->storefrontHistoryControls($template);
        if ($productHistory !== null && $templateHistory !== null) {
            $merged['historyControls'] = [
                'undo' => $productHistory['undo'] && $templateHistory['undo'],
                'redo' => $productHistory['redo'] && $templateHistory['redo'],
                'reset' => $productHistory['reset'] && $templateHistory['reset'],
            ];
        }

        return Json::encode($merged);
    }

    protected function storefrontPaginationIsHidden(array $ui) : bool
    {
        if (array_key_exists('viewsPagination', $ui)) {
            if ($ui['viewsPagination'] === false) {
                return true;
            }
            if (is_array($ui['viewsPagination']) && array_key_exists('enabled', $ui['viewsPagination'])) {
                return ! (bool) $ui['viewsPagination']['enabled'];
            }
        }

        return array_key_exists('viewsPaginationEnabled', $ui)
            && ! (bool) $ui['viewsPaginationEnabled'];
    }

    protected function storefrontHistoryControls(array $ui) : ?array
    {
        if (! array_key_exists('historyControls', $ui)) {
            return null;
        }
        if ($ui['historyControls'] === false) {
            return ['undo' => false, 'redo' => false, 'reset' => false];
        }
        if (! is_array($ui['historyControls'])) {
            return null;
        }

        return [
            'undo' => ($ui['historyControls']['undo'] ?? true) !== false,
            'redo' => ($ui['historyControls']['redo'] ?? true) !== false,
            'reset' => ($ui['historyControls']['reset'] ?? true) !== false,
        ];
    }

    protected function shouldInheritTemplateRuntimeMetaKey(string $key, array $config, bool $productHasBootViews) : bool
    {
        if ($productHasBootViews && in_array($key, ['_apd_initial_document', '_apd_initial_state', '_apd_views', '_apd_drawing_zones'], true)) {
            return false;
        }

        $productAuthoritativeKeys = [
            '_apd_permissions',
            '_apd_modules',
            '_apd_fonts',
            '_apd_upload',
            '_apd_colors',
            '_apd_pricing_rules',
            '_apd_ui',
            '_apd_front_behavior',
            '_apd_mobile_behavior',
        ];

        if (in_array($key, $productAuthoritativeKeys, true) && trim((string) ($config[$key] ?? '')) !== '') {
            return false;
        }

        return true;
    }

    protected function metaPayloadHasBootViews(array $payload) : bool
    {
        foreach (['_apd_initial_state', '_apd_initial_document'] as $key) {
            $raw = trim((string) ($payload[$key] ?? ''));
            if ($raw === '') {
                continue;
            }
            $decoded = Json::decode($raw, []);
            if ($this->payloadHasViews(is_array($decoded) ? $decoded : [])) {
                return true;
            }
        }

        return false;
    }

    protected function templateRuntimeInheritanceKeys() : array
    {
        return [
            '_apd_theme',
            '_apd_layout',
            '_apd_modules',
            '_apd_permissions',
            '_apd_views',
            '_apd_drawing_zones',
            '_apd_fonts',
            '_apd_upload',
            '_apd_colors',
            '_apd_pricing_rules',
            '_apd_selection_menu',
            '_apd_ui',
            '_apd_front_behavior',
            '_apd_mobile_behavior',
            '_apd_initial_document',
            '_apd_initial_state',
        ];
    }



    protected function buildTemplateLayoutDebugPayload(array $sourceConfig, array $mergedConfig, array $frontendConfig, array $runtimeContext) : array
    {
        $templateId = absint($mergedConfig['_apd_template_id'] ?? 0);
        $templatePayload = $templateId > 0 ? (new TemplateRepository())->getMetaPayload($templateId) : [];
        $templateLayout = Json::decode((string) ($templatePayload['_apd_layout'] ?? ''), []);
        $sourceLayout = Json::decode((string) ($sourceConfig['_apd_layout'] ?? ''), []);
        $mergedLayout = Json::decode((string) ($mergedConfig['_apd_layout'] ?? ''), []);

        return [
            'productId' => absint($runtimeContext['productId'] ?? 0),
            'variationId' => absint($runtimeContext['variationId'] ?? 0),
            'configId' => absint($runtimeContext['configId'] ?? 0),
            'configTitle' => (string) ($sourceConfig['title'] ?? ''),
            'templateId' => $templateId,
            'templateTitle' => (string) ($templatePayload['title'] ?? ($mergedConfig['_apd_template_source_title'] ?? '')),
            'resumeDesignId' => absint($runtimeContext['resumeDesignId'] ?? 0),
            'sourceProductHasBootViews' => $this->metaPayloadHasBootViews($sourceConfig),
            'templateExists' => $templatePayload !== [],
            'templateHasLayout' => is_array($templateLayout) && $templateLayout !== [],
            'layoutSource' => is_array($frontendConfig['layout'] ?? null) && ($frontendConfig['layout'] ?? []) !== [] ? 'assigned_template' : 'default_null',
            'frontendLayoutIsNull' => array_key_exists('layout', $frontendConfig) && $frontendConfig['layout'] === null,
            'productLayout' => $this->summarizeLayoutPayload(is_array($sourceLayout) ? $sourceLayout : null),
            'templateLayout' => $this->summarizeLayoutPayload(is_array($templateLayout) ? $templateLayout : null),
            'mergedConfigLayout' => $this->summarizeLayoutPayload(is_array($mergedLayout) ? $mergedLayout : null),
            'frontendLayout' => $this->summarizeLayoutPayload(is_array($frontendConfig['layout'] ?? null) ? $frontendConfig['layout'] : null),
            'productInitialDocument' => $this->summarizeViewPayload(Json::decode((string) ($sourceConfig['_apd_initial_document'] ?? ''), [])),
            'productInitialState' => $this->summarizeStatePayload(Json::decode((string) ($sourceConfig['_apd_initial_state'] ?? ''), [])),
            'templateInitialDocument' => $this->summarizeViewPayload(Json::decode((string) ($templatePayload['_apd_initial_document'] ?? ''), [])),
            'templateInitialState' => $this->summarizeStatePayload(Json::decode((string) ($templatePayload['_apd_initial_state'] ?? ''), [])),
            'frontendInitialDocument' => $this->summarizeViewPayload(is_array($frontendConfig['initialDocument'] ?? null) ? $frontendConfig['initialDocument'] : []),
            'frontendInitialState' => $this->summarizeStatePayload(is_array($frontendConfig['initialState'] ?? null) ? $frontendConfig['initialState'] : []),
        ];
    }

    protected function isTemplateLayoutDebugEnabled() : bool
    {
        $query = RequestData::query();
        $flag = $query['apd_debug_template'] ?? ($query['apd_debug'] ?? null);
        if ($flag === null) {
            return false;
        }

        $value = is_scalar($flag) ? strtolower(trim((string) $flag)) : '';
        return in_array($value, ['1', 'true', 'yes', 'on'], true);
    }

    protected function summarizeLayoutPayload(mixed $layout) : array
    {
        if (! is_array($layout)) {
            return [
                'hasLayout' => false,
                'zoneItemCounts' => [],
                'itemIds' => [],
                'topKeys' => [],
            ];
        }

        $zoneItemCounts = [];
        $itemIds = [];
        foreach (['left', 'right', 'top', 'bottom'] as $zone) {
            $zoneConfig = is_array($layout[$zone] ?? null) ? $layout[$zone] : [];
            $items = is_array($zoneConfig['items'] ?? null) ? $zoneConfig['items'] : [];
            if ($items === [] && is_array(($layout['blocksByZone'] ?? null)) && is_array(($layout['blocksByZone'][$zone] ?? null))) {
                $items = $layout['blocksByZone'][$zone];
            }
            $zoneItemCounts[$zone] = count($items);
            foreach ($items as $item) {
                if (! is_array($item)) {
                    continue;
                }
                $id = (string) ($item['id'] ?? $item['kind'] ?? '');
                if ($id !== '') {
                    $itemIds[] = $id;
                }
            }
        }

        return [
            'hasLayout' => $layout !== [],
            'zoneItemCounts' => $zoneItemCounts,
            'itemIds' => array_values(array_unique($itemIds)),
            'topKeys' => array_keys($layout),
        ];
    }

    protected function summarizeStateLayoutPayload(mixed $state) : array
    {
        if (! is_array($state)) {
            return [];
        }

        return [
            'layout' => $this->summarizeLayoutPayload(is_array($state['layout'] ?? null) ? $state['layout'] : null),
            'uiLayout' => $this->summarizeLayoutPayload(is_array(($state['ui'] ?? null)) && is_array(($state['ui']['layout'] ?? null)) ? $state['ui']['layout'] : null),
            'legacyStateLayout' => $this->summarizeLayoutPayload(is_array(($state['legacy'] ?? null)) && is_array(($state['legacy']['state'] ?? null)) && is_array(($state['legacy']['state']['layout'] ?? null)) ? $state['legacy']['state']['layout'] : null),
            'scopeLayout' => is_array($state['scope'] ?? null) && array_key_exists('layout', $state['scope']) ? $state['scope']['layout'] : null,
            'legacyScopeLayout' => is_array(($state['legacy'] ?? null)) && is_array(($state['legacy']['scope'] ?? null)) && array_key_exists('layout', $state['legacy']['scope']) ? $state['legacy']['scope']['layout'] : null,
        ];
    }


    protected function summarizeViewPayload(mixed $payload) : array
    {
        if (! is_array($payload) || $payload === []) {
            return [
                'hasValue' => false,
                'hasViews' => false,
                'objectCount' => 0,
                'topKeys' => [],
            ];
        }

        return [
            'hasValue' => true,
            'hasViews' => $this->payloadHasViews($payload),
            'objectCount' => $this->payloadObjectCount($payload),
            'objectPaths' => $this->summarizePayloadObjectPathsForDebug($payload),
            'topKeys' => array_keys($payload),
        ];
    }

    protected function summarizeStatePayload(mixed $state) : array
    {
        if (! is_array($state) || $state === []) {
            return [
                'hasValue' => false,
                'hasViews' => false,
                'objectCount' => 0,
                'layout' => $this->summarizeLayoutPayload(null),
                'topKeys' => [],
            ];
        }

        $document = [];
        foreach (['initialDocument', 'document', 'documentState'] as $key) {
            if (isset($state[$key]) && is_array($state[$key])) {
                $document = $state[$key];
                break;
            }
        }
        if ($document === []) {
            $document = $state;
        }

        return [
            'hasValue' => true,
            'hasViews' => $this->payloadHasViews($state),
            'objectCount' => $this->payloadObjectCount($state),
            'document' => $this->summarizeViewPayload($document),
            'layout' => $this->summarizeLayoutPayload(is_array($state['layout'] ?? null) ? $state['layout'] : null),
            'uiLayout' => $this->summarizeLayoutPayload(is_array(($state['ui'] ?? null)) && is_array(($state['ui']['layout'] ?? null)) ? $state['ui']['layout'] : null),
            'legacyStateLayout' => $this->summarizeLayoutPayload(is_array(($state['legacy'] ?? null)) && is_array(($state['legacy']['state'] ?? null)) && is_array(($state['legacy']['state']['layout'] ?? null)) ? $state['legacy']['state']['layout'] : null),
            'scopeLayout' => is_array($state['scope'] ?? null) && array_key_exists('layout', $state['scope']) ? $state['scope']['layout'] : null,
            'legacyScopeLayout' => is_array(($state['legacy'] ?? null)) && is_array(($state['legacy']['scope'] ?? null)) && array_key_exists('layout', $state['legacy']['scope']) ? $state['legacy']['scope']['layout'] : null,
            'topKeys' => array_keys($state),
        ];
    }

    protected function summarizeObjectListForDebug(mixed $objects, int $limit = 3) : array
    {
        $objects = is_array($objects) && array_is_list($objects) ? $objects : [];
        $samples = [];
        foreach (array_slice($objects, 0, $limit) as $object) {
            if (! is_array($object)) {
                continue;
            }
            $samples[] = [
                'type' => is_scalar($object['type'] ?? null) ? (string) $object['type'] : (is_scalar($object['objectType'] ?? null) ? (string) $object['objectType'] : ''),
                'id' => is_scalar($object['id'] ?? null) ? (string) $object['id'] : (is_scalar($object['_asukaObjectId'] ?? null) ? (string) $object['_asukaObjectId'] : (is_scalar($object['uuid'] ?? null) ? (string) $object['uuid'] : '')),
                'name' => is_scalar($object['name'] ?? null) ? (string) $object['name'] : (is_scalar($object['_asukaElementName'] ?? null) ? (string) $object['_asukaElementName'] : ''),
                'itemGroup' => is_scalar($object['itemGroup'] ?? null) ? (string) $object['itemGroup'] : '',
                'asukaElementId' => is_scalar($object['_asukaElementId'] ?? null) ? (string) $object['_asukaElementId'] : (is_scalar($object['asukaElementId'] ?? null) ? (string) $object['asukaElementId'] : ''),
                'left' => is_numeric($object['left'] ?? null) ? (float) $object['left'] : null,
                'top' => is_numeric($object['top'] ?? null) ? (float) $object['top'] : null,
                'width' => is_numeric($object['width'] ?? null) ? (float) $object['width'] : null,
                'height' => is_numeric($object['height'] ?? null) ? (float) $object['height'] : null,
                'srcLength' => is_scalar($object['src'] ?? null) ? strlen((string) $object['src']) : 0,
                'keys' => array_slice(array_keys($object), 0, 14),
            ];
        }

        return [
            'present' => $objects !== [],
            'count' => count($objects),
            'samples' => $samples,
        ];
    }

    protected function summarizeObjectBucketForDebug(mixed $bucket, bool $snapshotMode = false) : array
    {
        $summary = [
            'present' => false,
            'viewCount' => 0,
            'totalObjects' => 0,
            'viewIds' => [],
            'counts' => [],
            'samples' => [],
        ];

        if (! is_array($bucket) || $bucket === [] || array_is_list($bucket)) {
            return $summary;
        }

        $summary['present'] = true;
        $summary['viewCount'] = count($bucket);
        $summary['viewIds'] = array_slice(array_map('strval', array_keys($bucket)), 0, 12);
        foreach ($bucket as $viewId => $value) {
            $objects = [];
            if ($snapshotMode && is_array($value) && isset($value['objects']) && is_array($value['objects'])) {
                $objects = $value['objects'];
            } elseif (! $snapshotMode && is_array($value) && array_is_list($value)) {
                $objects = $value;
            } elseif (! $snapshotMode && is_array($value) && isset($value['objects']) && is_array($value['objects'])) {
                $objects = $value['objects'];
            }
            $count = count($objects);
            $summary['counts'][(string) $viewId] = $count;
            $summary['totalObjects'] += $count;
            if ($count > 0) {
                $summary['samples'][(string) $viewId] = $this->summarizeObjectListForDebug($objects, 2)['samples'];
            }
        }

        return $summary;
    }

    protected function maybeAddObjectBucketForDebug(array &$target, string $name, mixed $bucket, bool $snapshotMode = false) : void
    {
        $summary = $this->summarizeObjectBucketForDebug($bucket, $snapshotMode);
        if ($summary['present']) {
            $target[$name] = $summary;
        }
    }

    protected function summarizePayloadObjectPathsForDebug(mixed $payload, int $depth = 0) : array
    {
        if (! is_array($payload) || $payload === []) {
            return [
                'present' => false,
                'type' => gettype($payload),
                'rootObjects' => $this->summarizeObjectListForDebug([], 3),
                'objectBuckets' => [],
                'snapshotBuckets' => [],
                'nested' => [],
            ];
        }

        $canvas = is_array($payload['canvas'] ?? null) ? $payload['canvas'] : [];
        $objectBuckets = [];
        $snapshotBuckets = [];

        $this->maybeAddObjectBucketForDebug($objectBuckets, 'objects.byView', is_array($payload['objects'] ?? null) && ! array_is_list($payload['objects']) ? ($payload['objects']['byView'] ?? null) : null);
        $this->maybeAddObjectBucketForDebug($objectBuckets, 'documentObjectsByView', $payload['documentObjectsByView'] ?? null);
        $this->maybeAddObjectBucketForDebug($objectBuckets, 'objectsByView', $payload['objectsByView'] ?? null);
        $this->maybeAddObjectBucketForDebug($objectBuckets, 'byView', $payload['byView'] ?? null);
        $this->maybeAddObjectBucketForDebug($objectBuckets, 'canvas.objects.byView', is_array($canvas['objects'] ?? null) && ! array_is_list($canvas['objects']) ? ($canvas['objects']['byView'] ?? null) : null);
        $this->maybeAddObjectBucketForDebug($objectBuckets, 'canvas.documentObjectsByView', $canvas['documentObjectsByView'] ?? null);
        $this->maybeAddObjectBucketForDebug($objectBuckets, 'canvas.objectsByView', $canvas['objectsByView'] ?? null);
        $this->maybeAddObjectBucketForDebug($objectBuckets, 'canvas.byView', $canvas['byView'] ?? null);

        $this->maybeAddObjectBucketForDebug($snapshotBuckets, 'snapshotsByView', $payload['snapshotsByView'] ?? null, true);
        $this->maybeAddObjectBucketForDebug($snapshotBuckets, 'snapshots.snapshotsByView', is_array($payload['snapshots'] ?? null) ? ($payload['snapshots']['snapshotsByView'] ?? null) : null, true);
        $this->maybeAddObjectBucketForDebug($snapshotBuckets, 'viewSnapshots.snapshotsByView', is_array($payload['viewSnapshots'] ?? null) ? ($payload['viewSnapshots']['snapshotsByView'] ?? null) : null, true);
        $this->maybeAddObjectBucketForDebug($snapshotBuckets, 'viewSnapshots', $payload['viewSnapshots'] ?? null, true);
        $this->maybeAddObjectBucketForDebug($snapshotBuckets, 'canvas.snapshotsByView', $canvas['snapshotsByView'] ?? null, true);
        $this->maybeAddObjectBucketForDebug($snapshotBuckets, 'canvas.snapshots.snapshotsByView', is_array($canvas['snapshots'] ?? null) ? ($canvas['snapshots']['snapshotsByView'] ?? null) : null, true);
        $this->maybeAddObjectBucketForDebug($snapshotBuckets, 'canvas.viewSnapshots.snapshotsByView', is_array($canvas['viewSnapshots'] ?? null) ? ($canvas['viewSnapshots']['snapshotsByView'] ?? null) : null, true);
        $this->maybeAddObjectBucketForDebug($snapshotBuckets, 'canvas.viewSnapshots', $canvas['viewSnapshots'] ?? null, true);

        $nested = [];
        if ($depth < 1) {
            foreach (['initialDocument', 'document', 'documentState', 'initialState', 'state', 'legacy'] as $key) {
                if (isset($payload[$key]) && is_array($payload[$key])) {
                    $nested[$key] = $this->summarizePayloadObjectPathsForDebug($payload[$key], $depth + 1);
                }
            }
        }

        return [
            'present' => true,
            'topKeys' => array_slice(array_keys($payload), 0, 40),
            'rootObjects' => $this->summarizeObjectListForDebug(is_array($payload['objects'] ?? null) && array_is_list($payload['objects']) ? $payload['objects'] : [], 4),
            'objectBuckets' => $objectBuckets,
            'snapshotBuckets' => $snapshotBuckets,
            'selectedObjectsByView' => $this->summarizeObjectBucketForDebug($this->payloadObjectsByView($payload)),
            'selectedSnapshotsByView' => $this->summarizeObjectBucketForDebug($this->payloadSnapshotsByView($payload), true),
            'nested' => $nested,
        ];
    }

    protected function submitResumeDebugEnabled() : bool
    {
        return defined('WP_DEBUG') && WP_DEBUG
            && defined('APD_SUBMIT_DEBUG') && APD_SUBMIT_DEBUG;
    }

    protected function logSubmitResumeDebug(string $tag, array $data) : void
    {
        if (! $this->submitResumeDebugEnabled()) {
            return;
        }
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- The apd_ hook is a documented integration API retained for backward compatibility.
        do_action('apd_submit_debug_log', $tag, $data);
    }

    protected function extractSubmitProof(mixed $payload) : array
    {
        if (! is_array($payload) || $payload === []) {
            return [];
        }
        if (isset($payload['__apdSubmitProof']) && is_array($payload['__apdSubmitProof'])) {
            return $payload['__apdSubmitProof'];
        }
        if (isset($payload['meta']) && is_array($payload['meta']) && isset($payload['meta']['apdSubmitProof']) && is_array($payload['meta']['apdSubmitProof'])) {
            return $payload['meta']['apdSubmitProof'];
        }
        foreach (['document', 'initialDocument', 'documentState'] as $key) {
            if (isset($payload[$key]) && is_array($payload[$key])) {
                $proof = $this->extractSubmitProof($payload[$key]);
                if ($proof !== []) {
                    return $proof;
                }
            }
        }
        return [];
    }

    protected function summarizeSubmitProof(mixed $proof) : array
    {
        if (! is_array($proof) || $proof === []) {
            return ['present' => false];
        }
        return [
            'present' => true,
            'id' => (string) ($proof['id'] ?? ''),
            'createdAt' => (string) ($proof['createdAt'] ?? ''),
            'source' => (string) ($proof['source'] ?? ''),
            'reason' => (string) ($proof['reason'] ?? ''),
            'activeViewId' => (string) ($proof['activeViewId'] ?? ''),
            'liveObjectCount' => (int) ($proof['liveCanvas']['objectCount'] ?? 0),
            'documentHash' => (string) ($proof['document']['hash'] ?? ''),
            'documentObjects' => (int) ($proof['document']['objects']['totalObjects'] ?? 0),
            'documentCounts' => is_array($proof['document']['objects']['counts'] ?? null) ? $proof['document']['objects']['counts'] : [],
        ];
    }

    protected function resolveResumeDesign(int $resumeDesignId, string $resumeToken, int $productId, int $configId) : array
    {
        if ($resumeDesignId <= 0) {
            return [];
        }

        $item = UserDesignAccess::findAccessibleItem($resumeDesignId, $resumeToken);
        if (empty($item)) {
            return [];
        }

        if ($productId > 0 && (int) ($item['productId'] ?? 0) > 0 && (int) $item['productId'] !== $productId) {
            return [];
        }
        if ($configId > 0 && (int) ($item['configId'] ?? 0) > 0 && (int) $item['configId'] !== $configId) {
            return [];
        }

        return $item;
    }

}
