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

use APD\Domain\Product\ProductConfigBinder;
use APD\Domain\Pricing\PricingEngine;
use APD\Repositories\UserDesignRepository;
use APD\Support\DesignSummary;
use APD\Support\PreviewStorage;
use APD\Support\PricingSnapshot;
use APD\Support\Nonces;
use APD\Support\RequestData;
use APD\Support\UserDesignLinks;

defined('ABSPATH') || exit;

class CartIntegration
{
    protected string $lastResumeUrl = '';

    protected bool $lastCartEditSucceeded = false;

    public function boot() : void
    {
        add_filter('woocommerce_add_to_cart_validation', [$this, 'validate'], 10, 3);
        add_filter('woocommerce_add_cart_item_data', [$this, 'capture'], 10, 3);
        add_action('woocommerce_add_to_cart', [$this, 'replaceEditedCartItem'], 20, 6);
        add_filter('wc_add_to_cart_message_html', [$this, 'filterAddToCartMessage'], 20, 3);
        add_filter('woocommerce_add_to_cart_redirect', [$this, 'filterAddToCartRedirect'], 20, 1);
        add_filter('woocommerce_get_item_data', [$this, 'display'], 10, 2);
        add_filter('woocommerce_add_cart_item', [$this, 'hydrateCartItemProductPreview'], 20, 1);
        add_filter('woocommerce_get_cart_item_from_session', [$this, 'hydrateCartItemFromSession'], 20, 3);
        add_filter('woocommerce_cart_item_thumbnail', [$this, 'replaceThumbnail'], PHP_INT_MAX, 3);
        add_filter('woocommerce_cart_item_permalink', [$this, 'filterCartItemPermalink'], 10, 3);
        add_action('woocommerce_remove_cart_item', [$this, 'cleanupRemovedCartItem'], 10, 2);
        add_action('woocommerce_cart_emptied', [$this, 'cleanupCartEmptiedPreviews']);
        add_filter('woocommerce_cart_item_name', [$this, 'prependPreviewToItemName'], 20, 3);
        add_filter('woocommerce_cart_item_class', [$this, 'filterCartItemClass'], 20, 3);
        add_action('woocommerce_after_cart_item_name', [$this, 'renderCartSummary'], 10, 2);
    }

    public function validate(bool $passed, int $productId, int $quantity) : bool
    {
        $post = RequestData::post();
        if (! $this->isDesignerSubmission($post)) {
            return $passed;
        }

        if (! Nonces::verifyCartSubmission((string) ($post['apd_add_to_cart_nonce'] ?? ''))) {
            wc_add_notice(__('Your customization session has expired. Please refresh the page and try again.', 'asuka-product-designer'), 'error');
            return false;
        }

        $configId = absint($post['apd_config_id'] ?? 0);
        $designJson = (string) ($post['apd_design_json'] ?? '');
        $stateJson = (string) ($post['apd_state_json'] ?? '');
        $linkedProductId = absint($post['apd_product_id'] ?? $productId);
        $variationId = absint($post['variation_id'] ?? ($post['apd_variation_id'] ?? 0));

        if ($configId <= 0 || trim($designJson) === '') {
            wc_add_notice(__('The product designer payload is incomplete.', 'asuka-product-designer'), 'error');
            return false;
        }

        if (! $this->isJsonPayload($designJson)) {
            wc_add_notice(__('The customization payload is invalid. Please reload the designer and try again.', 'asuka-product-designer'), 'error');
            return false;
        }

        if (trim($stateJson) !== '' && ! $this->isJsonPayload($stateJson)) {
            wc_add_notice(__('The customization state is invalid. Please reload the designer and try again.', 'asuka-product-designer'), 'error');
            return false;
        }

        $decodedDesign = $this->decodeJson($designJson);
        $decodedState = $this->decodeJson($stateJson);
        $restorableDesign = $this->mergeConfigDrawingZonesIntoCartDocument($this->preferRestorableCartDesignPayload($decodedDesign, $decodedState), $configId);
        $this->logSubmitDebug('validate:payload', [
            'productId' => $productId,
            'linkedProductId' => $linkedProductId,
            'configId' => $configId,
            'variationId' => $variationId,
            'post' => $this->summarizeSubmitPostPayload($post),
            'design' => $this->summarizeSubmitPayload($decodedDesign),
            'stateDocument' => $this->summarizeSubmitPayload($this->extractBestDocumentPayload($decodedState)),
            'chosen' => $this->summarizeSubmitPayload($restorableDesign),
            'submitProof' => $this->summarizeSubmitProof($this->decodeJson((string) ($post['apd_submit_proof'] ?? ''))),
            'designProof' => $this->summarizeSubmitProof($this->extractSubmitProof($decodedDesign)),
            'chosenProof' => $this->summarizeSubmitProof($this->extractSubmitProof($restorableDesign)),
        ]);
        if ($this->payloadRestorationScore($restorableDesign) <= 0) {
            wc_add_notice(__('The product designer was not ready yet. Please wait a moment and try adding the product to the cart again.', 'asuka-product-designer'), 'error');
            return false;
        }

        if (! $this->variationBelongsToProduct($linkedProductId, $variationId)) {
            wc_add_notice(__('The selected variation no longer matches this product.', 'asuka-product-designer'), 'error');
            return false;
        }

        $binder = new ProductConfigBinder();
        $expectedConfigId = $this->resolveExpectedConfigId($binder, $linkedProductId, $variationId);
        if ($expectedConfigId <= 0) {
            wc_add_notice(__('This product is no longer linked to a valid custom product configuration.', 'asuka-product-designer'), 'error');
            return false;
        }

        if ($expectedConfigId !== $configId) {
            wc_add_notice(__('The selected custom product no longer matches the current product or variation.', 'asuka-product-designer'), 'error');
            return false;
        }

        if (! $this->isAvailableConfig($configId)) {
            wc_add_notice(__('The selected custom product configuration is no longer available.', 'asuka-product-designer'), 'error');
            return false;
        }

        $frontBehavior = $this->decodeJson((string) get_post_meta($configId, '_apd_front_behavior', true));
        $permissions = $this->decodeJson((string) get_post_meta($configId, '_apd_permissions', true));
        $validationMessage = $this->validateDesignAgainstBehavior($designJson, $frontBehavior, $permissions, $stateJson, $post);
        if ($validationMessage !== '') {
            wc_add_notice($validationMessage, 'error');
            return false;
        }

        return $passed;
    }

    public function capture(array $cartItemData, int $productId, int $variationId) : array
    {
        $post = RequestData::post();
        $designJson = (string) ($post['apd_design_json'] ?? '');
        if ($designJson === '' || ! $this->isDesignerSubmission($post)) {
            return $cartItemData;
        }

        if (! Nonces::verifyCartSubmission((string) ($post['apd_add_to_cart_nonce'] ?? ''))) {
            return $cartItemData;
        }

        $stateJson = (string) ($post['apd_state_json'] ?? '');
        $design = $this->decodeJson($designJson);
        $state = $this->decodeJson($stateJson);
        $postedSubmitProof = $this->decodeJson((string) ($post['apd_submit_proof'] ?? ''));

        $configId = absint($post['apd_config_id'] ?? 0);
        $linkedProductId = absint($post['apd_product_id'] ?? $productId);
        $selectedVariationId = absint($post['variation_id'] ?? ($post['apd_variation_id'] ?? $variationId));
        $variationAttributes = $this->collectVariationAttributes($post);

        $normalizedDesign = $this->preferRestorableCartDesignPayload($design, $state);
        $normalizedDesign = $this->mergeConfigDrawingZonesIntoCartDocument($normalizedDesign, $configId);
        $this->logSubmitDebug('capture:before-normalize', [
            'productId' => $productId,
            'linkedProductId' => $linkedProductId,
            'configId' => $configId,
            'variationId' => $selectedVariationId,
            'post' => $this->summarizeSubmitPostPayload($post),
            'design' => $this->summarizeSubmitPayload($design),
            'stateDocument' => $this->summarizeSubmitPayload($this->extractBestDocumentPayload($state)),
            'chosen' => $this->summarizeSubmitPayload($normalizedDesign),
            'submitProof' => $this->summarizeSubmitProof($postedSubmitProof),
            'designProof' => $this->summarizeSubmitProof($this->extractSubmitProof($design)),
            'chosenProof' => $this->summarizeSubmitProof($this->extractSubmitProof($normalizedDesign)),
            'proofVerdict' => $this->submitProofVerdict($postedSubmitProof, $normalizedDesign, []),
        ]);
        if ($normalizedDesign !== $design) {
            $design = $normalizedDesign;
            $designJson = (string) wp_json_encode($design, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
        $state = $this->normalizeCartStatePayload($state, [
            'productId' => $linkedProductId,
            'configId' => $configId,
            'variationId' => $selectedVariationId,
            'variationAttributes' => $variationAttributes,
        ]);
        $stateJson = (string) wp_json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        if (! $this->variationBelongsToProduct($linkedProductId, $selectedVariationId)) {
            return $cartItemData;
        }

        $binder = new ProductConfigBinder();
        $expectedConfigId = $this->resolveExpectedConfigId($binder, $linkedProductId, $selectedVariationId);
        if ($configId <= 0 || $expectedConfigId <= 0 || $configId !== $expectedConfigId || ! $this->isAvailableConfig($configId)) {
            return $cartItemData;
        }

        $previewStorage = new PreviewStorage();
        $previewData = (string) ($post['apd_preview_data'] ?? '');
        $previewUrl = esc_url_raw((string) ($post['apd_preview_url'] ?? ''));
        $previewAttachmentId = absint($post['apd_preview_attachment_id'] ?? 0);
        if ($previewUrl === '' && $previewData !== '' && strpos($previewData, 'data:image/') === 0) {
            $stored = $previewStorage->store($previewData, 'apd-cart-preview', [
                'temporary' => true,
                'context' => 'cart',
            ]);
            if (! empty($stored['success']) && ! empty($stored['url'])) {
                $previewUrl = esc_url_raw((string) $stored['url']);
                $previewAttachmentId = absint($stored['attachmentId'] ?? 0);
            }
        }
        if ($previewAttachmentId <= 0 && $previewUrl !== '') {
            $previewAttachmentId = $previewStorage->attachmentIdFromUrl($previewUrl);
        }

        $pricingPayloadRaw = (string) ($post['apd_pricing_payload'] ?? $post['apd_price_breakdown'] ?? '');
        $replaceCartItemKey = sanitize_text_field((string) ($post['apd_replace_cart_item_key'] ?? ''));
        $postedCartLineToken = sanitize_text_field((string) ($post['apd_cart_line_token'] ?? ''));

        $postedUserDesignId = absint($post['apd_user_design_id'] ?? 0);
        $cartEditContext = $this->resolvePostedCartEditContext($replaceCartItemKey, $postedCartLineToken, $linkedProductId, $configId, $postedUserDesignId);
        $isEditingCartItem = $cartEditContext !== [];
        if (! $isEditingCartItem) {
            $replaceCartItemKey = '';
            $postedCartLineToken = '';
        }

        $productRefId = $selectedVariationId > 0 ? $selectedVariationId : $linkedProductId;
        $productRef = $productRefId > 0 ? wc_get_product($productRefId) : null;
        $basePrice = $productRef ? (float) $productRef->get_price('edit') : 0.0;

        $pricingPayload = PricingSnapshot::normalize($pricingPayloadRaw, $basePrice);
        if ($configId > 0 && $designJson !== '') {
            $result = PricingSnapshot::normalize((new PricingEngine())->calculate([
                'basePrice' => $basePrice,
                'configId' => $configId,
                'designJson' => $designJson,
                'stateJson' => $stateJson,
            ]), $basePrice);
            if (is_array($result) && ! empty($result['success'])) {
                $pricingPayload = $result;
            }
        }

        $payloadVersion = (int) ($post['apd_payload_version'] ?? ($pricingPayload['payload_version'] ?? PricingSnapshot::VERSION));
        $priceBase = (float) ($pricingPayload['base_price'] ?? $basePrice);
        $priceAdjustment = (float) ($pricingPayload['adjustment_total'] ?? 0);
        $priceTotal = (float) ($pricingPayload['final_price'] ?? ($priceBase + $priceAdjustment));
        $priceBreakdownArray = $pricingPayload;
        $priceBreakdown = wp_json_encode($pricingPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        $summary = DesignSummary::build($design, $state);
        $configTitle = $configId > 0 ? (string) get_the_title($configId) : '';
        $productSnapshot = $this->buildProductSnapshot($linkedProductId, $selectedVariationId, $variationAttributes);

        $repo = new UserDesignRepository();
        $existing = [];
        // A cart item must be a snapshot of the exact submit moment. A saved-design
        // id coming from the opened product page is only reused while editing an
        // existing cart item; otherwise a fresh design post is created for this cart
        // snapshot so the cart link cannot point back to a stale source design.
        if ($isEditingCartItem && $postedUserDesignId > 0) {
            $candidate = $repo->find($postedUserDesignId);
            if ($candidate && absint($candidate['productId'] ?? 0) === $linkedProductId && absint($candidate['configId'] ?? 0) === $configId) {
                $existing = $candidate;
            }
        }
        $userDesignId = $existing ? $postedUserDesignId : 0;
        $accessToken = (string) ($existing['accessToken'] ?? '');
        $cartLineToken = $isEditingCartItem ? (string) ($cartEditContext['lineToken'] ?? '') : '';
        if ($cartLineToken === '') {
            $cartLineToken = $this->createCartLineToken($linkedProductId, $configId, $selectedVariationId);
        }
        if ($accessToken === '') {
            $accessToken = wp_generate_password(20, false, false);
        }

        $userDesignPayload = [
            'title' => sprintf(/* translators: Placeholder values are populated from the current WooCommerce product, cart, or order. */ __('Cart design — %s', 'asuka-product-designer'), (string) get_the_title($linkedProductId)),
            'userId' => get_current_user_id(),
            'configId' => $configId,
            'productId' => $linkedProductId,
            'designJson' => $design,
            'stateJson' => $state,
            'previewUrls' => $previewUrl !== '' ? [$previewUrl] : [],
            'priceSnapshot' => $priceBreakdownArray,
            'productionData' => [
                'source' => 'woocommerce_cart',
                'variationId' => $selectedVariationId,
                'variationAttributes' => $variationAttributes,
                'priceBase' => $priceBase,
                'priceAdjustment' => $priceAdjustment,
                'priceTotal' => $priceTotal,
                'payloadVersion' => $payloadVersion,
                'summary' => $summary,
                'configTitle' => $configTitle,
                'productTitle' => $productSnapshot['productTitle'],
                'variationLabel' => $productSnapshot['variationLabel'],
                'productSku' => $productSnapshot['sku'],
                'previewAttachmentId' => $previewAttachmentId,
                'submitProof' => is_array($postedSubmitProof) ? $postedSubmitProof : [],
            ],
            'accessToken' => $accessToken,
        ];

        if ($existing) {
            $repo->update($userDesignId, $userDesignPayload);
            $saveAction = 'updated_existing_cart_snapshot';
        } else {
            $userDesignId = $repo->create($userDesignPayload);
            $saveAction = 'created_fresh_cart_snapshot';
        }

        $savedDesign = $userDesignId > 0 ? $repo->find($userDesignId) : [];
        $this->logSubmitDebug('capture:saved-design', [
            'action' => $saveAction,
            'postedUserDesignId' => $postedUserDesignId,
            'userDesignId' => $userDesignId,
            'replaceCartItemKey' => $replaceCartItemKey,
            'submitted' => $this->summarizeSubmitPayload($design),
            'saved' => $this->summarizeSubmitPayload(is_array($savedDesign['designJson'] ?? null) ? $savedDesign['designJson'] : []),
            'postProof' => $this->summarizeSubmitProof($postedSubmitProof),
            'submittedProof' => $this->summarizeSubmitProof($this->extractSubmitProof($design)),
            'savedProof' => $this->summarizeSubmitProof($this->extractSubmitProof(is_array($savedDesign['designJson'] ?? null) ? $savedDesign['designJson'] : [])),
            'proofVerdict' => $this->submitProofVerdict($postedSubmitProof, $design, is_array($savedDesign['designJson'] ?? null) ? $savedDesign['designJson'] : []),
        ]);

        $resumeUrl = UserDesignLinks::buildResumeUrl($linkedProductId, $userDesignId, $accessToken, true, [
            'variation_id' => $selectedVariationId,
            'attributes' => $variationAttributes,
        ]);
        if ($resumeUrl !== '' && $cartLineToken !== '') {
            $resumeUrl = add_query_arg('apd_cart_line_token', $cartLineToken, $resumeUrl);
        }

        if ($this->submitDebugEnabled()) {
            $resumeUrl = add_query_arg('apd_submit_debug', '1', $resumeUrl);
        }

        $this->lastResumeUrl = $isEditingCartItem && function_exists('wc_get_cart_url') ? (string) wc_get_cart_url() : $resumeUrl;


        $cartItemData['apd'] = [
            'config_id' => $configId,
            'config_title' => $configTitle,
            'product_id' => $linkedProductId,
            'variation_id' => $selectedVariationId,
            'variation_attributes' => $variationAttributes,
            'variation_label' => $productSnapshot['variationLabel'],
            'product_title' => $productSnapshot['productTitle'],
            'product_sku' => $productSnapshot['sku'],
            'user_design_id' => $userDesignId,
            'resume_url' => $resumeUrl,
            'preview_data' => $previewData,
            'preview_url' => $previewUrl,
            'preview_attachment_id' => $previewAttachmentId,
            'design_json' => $designJson,
            'state_json' => $stateJson,
            'design_summary' => $summary,
            'payload_version' => $payloadVersion,
            'pricing_payload' => $pricingPayload,
            'price_base' => $priceBase,
            'price_adjustment' => $priceAdjustment,
            'price_total' => $priceTotal,
            'price_breakdown' => $priceBreakdown,
            'replace_cart_item_key' => $replaceCartItemKey,
            'line_token' => $cartLineToken,
            'cart_line_token' => $cartLineToken,
            'submit_proof' => is_array($postedSubmitProof) ? $postedSubmitProof : [],
            'unique_key' => md5($designJson . '|' . $cartLineToken),
        ];

        // Keep a top-level cart identity as well as the nested APD payload. Some
        // WooCommerce extensions rebuild/compare cart item data shallowly; a
        // dedicated top-level token makes every customized line independent and
        // prevents accidental merging/replacement between products sharing the
        // same Asuka product configuration.
        $cartItemData['apd_cart_line_token'] = $cartLineToken;
        $cartItemData['apd_unique_key'] = md5($cartLineToken . '|' . $designJson);
        $cartItemData['unique_key'] = $cartLineToken;

        return $cartItemData;
    }

    public function replaceEditedCartItem(string $cartItemKey, int $productId, int $quantity, int $variationId, array $variation, array $cartItemData) : void
    {
        $this->lastCartEditSucceeded = false;

        $post = RequestData::post();
        if (! Nonces::verifyCartSubmission((string) ($post['apd_add_to_cart_nonce'] ?? ''))) {
            return;
        }

        $replaceKey = sanitize_text_field((string) ($post['apd_replace_cart_item_key'] ?? ''));
        $lineToken = sanitize_text_field((string) ($post['apd_cart_line_token'] ?? ''));
        $configId = absint($post['apd_config_id'] ?? 0);
        $linkedProductId = absint($post['apd_product_id'] ?? $productId);
        $postedUserDesignId = absint($post['apd_user_design_id'] ?? 0);
        if ($replaceKey === '' || $lineToken === '' || $replaceKey === $cartItemKey || ! function_exists('WC') || ! WC()->cart) {
            return;
        }

        $editContext = $this->resolvePostedCartEditContext($replaceKey, $lineToken, $linkedProductId, $configId, $postedUserDesignId);
        if ($editContext === []) {
            return;
        }

        $previousQuantity = max(1, (int) ($editContext['cartItem']['quantity'] ?? 1));
        if ($previousQuantity !== $quantity) {
            WC()->cart->set_quantity($cartItemKey, $previousQuantity, false);
        }

        if (! WC()->cart->remove_cart_item($replaceKey)) {
            return;
        }

        WC()->cart->calculate_totals();
        $this->lastCartEditSucceeded = true;
    }

    public function filterAddToCartMessage(string $message, array $products, bool $showQuantity) : string
    {
        if (! $this->lastCartEditSucceeded) {
            return $message;
        }

        return sprintf(
            '%1$s <a href="%2$s" class="button wc-forward">%3$s</a>',
            esc_html__('Your customization has been updated in the cart.', 'asuka-product-designer'),
            esc_url(wc_get_cart_url()),
            esc_html__('View cart', 'asuka-product-designer')
        );
    }


    public function hydrateCartItemProductPreview(array $cartItem) : array
    {
        return $this->applyPreviewToCartItemProduct($cartItem);
    }

    public function hydrateCartItemFromSession(array $cartItem, array $values, string $cartItemKey) : array
    {
        if (! empty($values['apd']) && is_array($values['apd'])) {
            $cartItem['apd'] = $values['apd'];
        }

        $apd = ! empty($cartItem['apd']) && is_array($cartItem['apd']) ? $cartItem['apd'] : [];

        return $this->applyPreviewToCartItemProduct($cartItem);
    }

    public function filterAddToCartRedirect(string $url) : string
    {
        if ($this->lastResumeUrl === '') {
            return $url;
        }

        if (function_exists('wc_notice_count') && wc_notice_count('error') > 0) {
            return $url;
        }

        if ($url !== '' && $this->sameUrlPath($url, wc_get_cart_url())) {
            return $url;
        }

        if ($url !== '') {
            $post = RequestData::post();
            $productId = absint($post['apd_product_id'] ?? ($post['add-to-cart'] ?? 0));
            $productUrl = $productId > 0 ? get_permalink($productId) : '';
            $referer = wp_get_referer() ?: '';
            if (! $this->sameUrlPath($url, (string) $productUrl) && ! $this->sameUrlPath($url, (string) $referer)) {
                return $url;
            }
        }

        return $this->lastResumeUrl;
    }

    public function display(array $itemData, array $cartItem) : array
    {
        if (empty($cartItem['apd']) || ! is_array($cartItem['apd'])) {
            return $itemData;
        }

        $apd = $cartItem['apd'];
        $summary = is_array($apd['design_summary'] ?? null) ? $apd['design_summary'] : [];
        $summaryText = DesignSummary::describe($summary);
        $configTitle = trim((string) ($apd['config_title'] ?? ''));
        $configId = absint($apd['config_id'] ?? 0);

        $itemData[] = [
            'name' => __('Custom Product', 'asuka-product-designer'),
            'value' => $configTitle !== '' ? $configTitle . ($configId > 0 ? ' (#' . $configId . ')' : '') : ('#' . $configId),
        ];

        if ($summaryText !== '') {
            $itemData[] = [
                'name' => __('Design summary', 'asuka-product-designer'),
                'value' => $summaryText,
            ];
        }

        if (! empty($summary['objectTypes']) && is_array($summary['objectTypes'])) {
            $itemData[] = [
                'name' => __('Objects', 'asuka-product-designer'),
                'value' => implode(', ', array_map('wc_clean', $summary['objectTypes'])),
            ];
        }

        if (! empty($summary['colors']) && is_array($summary['colors'])) {
            $itemData[] = [
                'name' => __('Colors', 'asuka-product-designer'),
                'value' => implode(', ', array_map('wc_clean', $summary['colors'])),
            ];
        }

        if (! empty($summary['catalogElements']) && is_array($summary['catalogElements'])) {
            $itemData[] = [
                'name' => __('Elements', 'asuka-product-designer'),
                'value' => implode(', ', array_map('wc_clean', $summary['catalogElements'])),
            ];
        }

        if (! empty($summary['catalogDesigns']) && is_array($summary['catalogDesigns'])) {
            $itemData[] = [
                'name' => __('Designs', 'asuka-product-designer'),
                'value' => implode(', ', array_map('wc_clean', $summary['catalogDesigns'])),
            ];
        }

        if (! empty($summary['texts']) && is_array($summary['texts'])) {
            $itemData[] = [
                'name' => __('Text content', 'asuka-product-designer'),
                'value' => implode(' · ', array_map('wc_clean', $summary['texts'])),
            ];
        }

        $adjustment = isset($apd['price_adjustment']) ? (float) $apd['price_adjustment'] : 0.0;
        if (abs($adjustment) > 0.00001) {
            $itemData[] = [
                'name' => __('Customization surcharge', 'asuka-product-designer'),
                'value' => wp_strip_all_tags(wc_price($adjustment)),
            ];
        }

        $resumeUrl = (string) ($apd['resume_url'] ?? '');
        $cartItemKey = $this->resolveCartItemKeyFromItem($cartItem);
        $editResumeUrl = $this->withReplaceCartItemKey($resumeUrl, $cartItemKey);
        if ($editResumeUrl !== '') {
            $itemData[] = [
                'name' => __('Resume customization', 'asuka-product-designer'),
                'value' => $editResumeUrl,
                'display' => '<a href="' . esc_url($editResumeUrl) . '">' . esc_html__('Open your saved design', 'asuka-product-designer') . '</a>',
                '__experimental_woocommerce_blocks_hidden' => true,
            ];
        }

        return $itemData;
    }

    public function filterCartItemPermalink(string $permalink, array $cartItem, string $cartItemKey) : string
    {
        if (empty($cartItem['apd']) || ! is_array($cartItem['apd'])) {
            return $permalink;
        }

        $resumeUrl = (string) ($cartItem['apd']['resume_url'] ?? '');
        $resumeUrl = $this->withReplaceCartItemKey($resumeUrl, $cartItemKey);
        return $resumeUrl !== '' ? $resumeUrl : $permalink;
    }

    public function replaceThumbnail(string $thumbnail, array $cartItem, string $cartItemKey) : string
    {
        if (empty($cartItem['apd']) || ! is_array($cartItem['apd'])) {
            return $thumbnail;
        }

        $previewAttachmentId = absint($cartItem['apd']['preview_attachment_id'] ?? 0);
        if ($previewAttachmentId > 0) {
            $imageHtml = wp_get_attachment_image($previewAttachmentId, 'woocommerce_thumbnail', false, [
                'class' => 'attachment-woocommerce_thumbnail size-woocommerce_thumbnail apd-cart-thumbnail',
                'alt' => esc_attr__('Customized product preview', 'asuka-product-designer'),
            ]);
            if (is_string($imageHtml) && $imageHtml !== '') {
                return $imageHtml;
            }
        }

        $previewUrl = $this->resolveApdImageUrlForCartItem($cartItem, $cartItemKey);
        if ($previewUrl === '') {
            return $thumbnail;
        }

        return '<img src="' . $this->escapeImageSrc($previewUrl) . '" alt="' . esc_attr__('Customized product preview', 'asuka-product-designer') . '" class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail apd-cart-thumbnail" />';
    }

    public function prependPreviewToItemName(string $name, array $cartItem, string $cartItemKey) : string
    {
        if (empty($cartItem['apd']) || ! is_array($cartItem['apd'])) {
            return $name;
        }

        $previewUrl = $this->resolveApdImageUrlForCartItem($cartItem, $cartItemKey);
        if ($previewUrl === '') {
            return $name;
        }

        if (strpos($name, 'apd-cart-name-preview') !== false) {
            return $name;
        }

        $previewHtml = '<span class="apd-cart-name-preview"><img src="' . $this->escapeImageSrc($previewUrl) . '" alt="' . esc_attr__('Customized product preview', 'asuka-product-designer') . '" class="apd-cart-name-preview__image" /></span>';
        return $previewHtml . $name;
    }

    public function filterCartItemClass(string $class, array $cartItem, string $cartItemKey) : string
    {
        if (empty($cartItem['apd']) || ! is_array($cartItem['apd'])) {
            return $class;
        }

        return trim(trim($class) . ' apd-cart-item apd-cart-item--customized');
    }

    public function renderCartSummary(array $cartItem, string $cartItemKey) : void
    {
        if (empty($cartItem['apd']) || ! is_array($cartItem['apd'])) {
            return;
        }

        $apd = $cartItem['apd'];
        $configId = absint($apd['config_id'] ?? 0);
        $configTitle = trim((string) ($apd['config_title'] ?? ''));
        $previewUrl = (string) $this->resolveApdImageUrlForCartItem($cartItem, $cartItemKey);
        $resumeUrl = $this->withReplaceCartItemKey((string) ($apd['resume_url'] ?? ''), $cartItemKey);
        $priceAdjustment = (float) ($apd['price_adjustment'] ?? 0);
        $priceTotal = (float) ($apd['price_total'] ?? 0);
        $summary = is_array($apd['design_summary'] ?? null) ? $apd['design_summary'] : [];
        $summaryText = DesignSummary::describe($summary);
        $variationHtml = $this->formatVariationAttributes(is_array($apd['variation_attributes'] ?? null) ? $apd['variation_attributes'] : []);
        $chips = $this->buildCartHighlights($summary);
        $productUrl = '';
        $product = null;
        if (isset($cartItem['data']) && is_object($cartItem['data']) && method_exists($cartItem['data'], 'is_visible')) {
            $product = $cartItem['data'];
        } else {
            $product = wc_get_product(absint($apd['variation_id'] ?? 0) ?: absint($apd['product_id'] ?? 0));
        }
        if ($product && method_exists($product, 'is_visible') && $product->is_visible()) {
            $productUrl = (string) $product->get_permalink();
        }

        ob_start();
        ?>
        <div class="apd-cart-design-card apd-cart-design-card--no-media">
            <div class="apd-cart-design-card__body">
                <div class="apd-cart-design-card__eyebrow"><?php echo esc_html($configTitle !== '' ? $configTitle : ($configId > 0 ? '#' . $configId : __('Customized product', 'asuka-product-designer'))); ?></div>
                <?php if ($summaryText !== '') : ?>
                    <p class="apd-cart-design-card__summary"><?php echo esc_html($summaryText); ?></p>
                <?php endif; ?>
                <?php if ($variationHtml !== '') : ?>
                    <div class="apd-cart-design-card__meta"><?php echo wp_kses_post($variationHtml); ?></div>
                <?php endif; ?>
                <?php if ($chips !== []) : ?>
                    <div class="apd-cart-design-card__chips">
                        <?php foreach ($chips as $chip) : ?>
                            <span class="apd-cart-design-card__chip"><?php echo esc_html($chip); ?></span>
                        <?php endforeach; ?>
                    </div>
                <?php endif; ?>
                <div class="apd-cart-design-card__pricing">
                    <?php if (abs($priceAdjustment) > 0.00001) : ?>
                        <div>
                            <span><?php esc_html_e('Customization', 'asuka-product-designer'); ?></span>
                            <strong><?php echo wp_kses_post(wc_price($priceAdjustment)); ?></strong>
                        </div>
                    <?php endif; ?>
                    <?php if ($priceTotal > 0) : ?>
                        <div>
                            <span><?php esc_html_e('Customized unit price', 'asuka-product-designer'); ?></span>
                            <strong><?php echo wp_kses_post(wc_price($priceTotal)); ?></strong>
                        </div>
                    <?php endif; ?>
                </div>
                <?php if ($resumeUrl !== '' || ($productUrl !== '' && $productUrl !== $resumeUrl)) : ?>
                    <div class="apd-cart-design-card__actions">
                        <?php if ($resumeUrl !== '') : ?>
                            <a class="apd-customer-design-card__button apd-customer-design-card__button--primary" href="<?php echo esc_url($resumeUrl); ?>"><?php esc_html_e('Open saved design', 'asuka-product-designer'); ?></a>
                        <?php endif; ?>
                        <?php if ($productUrl !== '' && $productUrl !== $resumeUrl) : ?>
                            <a class="apd-customer-design-card__button apd-customer-design-card__button--secondary" href="<?php echo esc_url($productUrl); ?>"><?php esc_html_e('View product', 'asuka-product-designer'); ?></a>
                        <?php endif; ?>
                    </div>
                <?php endif; ?>
            </div>
        </div>
        <?php
        echo wp_kses_post((string) ob_get_clean());
    }

    protected function resolveCartItemKeyFromItem(array $targetCartItem) : string
    {
        if (! function_exists('WC') || ! WC()->cart) {
            return '';
        }

        $targetApd = ! empty($targetCartItem['apd']) && is_array($targetCartItem['apd']) ? $targetCartItem['apd'] : [];
        $targetLineToken = sanitize_text_field((string) ($targetApd['line_token'] ?? ($targetApd['cart_line_token'] ?? ($targetCartItem['apd_cart_line_token'] ?? ''))));
        $targetUserDesignId = absint($targetApd['user_design_id'] ?? 0);
        $targetProductId = absint($targetApd['product_id'] ?? ($targetCartItem['product_id'] ?? 0));
        $targetConfigId = absint($targetApd['config_id'] ?? 0);

        foreach (WC()->cart->get_cart() as $cartItemKey => $cartItem) {
            if (! is_array($cartItem)) {
                continue;
            }
            $apd = ! empty($cartItem['apd']) && is_array($cartItem['apd']) ? $cartItem['apd'] : [];
            if ($targetLineToken !== '') {
                $lineToken = sanitize_text_field((string) ($apd['line_token'] ?? ($apd['cart_line_token'] ?? ($cartItem['apd_cart_line_token'] ?? ''))));
                if ($lineToken !== '' && hash_equals($targetLineToken, $lineToken)) {
                    return (string) $cartItemKey;
                }
                continue;
            }

            if ($targetUserDesignId > 0 && absint($apd['user_design_id'] ?? 0) === $targetUserDesignId) {
                $productId = absint($apd['product_id'] ?? ($cartItem['product_id'] ?? 0));
                $configId = absint($apd['config_id'] ?? 0);
                if (($targetProductId <= 0 || $productId === $targetProductId) && ($targetConfigId <= 0 || $configId === $targetConfigId)) {
                    return (string) $cartItemKey;
                }
            }
        }

        return '';
    }

    protected function withReplaceCartItemKey(string $resumeUrl, string $cartItemKey) : string
    {
        if ($resumeUrl === '' || $cartItemKey === '') {
            return $resumeUrl;
        }

        $lineToken = $this->getCartItemLineToken($cartItemKey);
        $args = [
            'apd_replace_cart_item_key' => $cartItemKey,
        ];
        if ($lineToken !== '') {
            $args['apd_cart_line_token'] = $lineToken;
        }

        return add_query_arg($args, $resumeUrl);
    }

    protected function getCartItemLineToken(string $cartItemKey) : string
    {
        if ($cartItemKey === '' || ! function_exists('WC') || ! WC()->cart) {
            return '';
        }
        $cartItem = WC()->cart->get_cart_item($cartItemKey);
        if (! is_array($cartItem)) {
            return '';
        }
        $apd = ! empty($cartItem['apd']) && is_array($cartItem['apd']) ? $cartItem['apd'] : [];
        return sanitize_text_field((string) ($apd['line_token'] ?? ($apd['cart_line_token'] ?? ($cartItem['apd_cart_line_token'] ?? ''))));
    }

    protected function createCartLineToken(int $productId, int $configId, int $variationId = 0) : string
    {
        return wp_hash(implode('|', [
            'apd-cart-line',
            (string) $productId,
            (string) $configId,
            (string) $variationId,
            microtime(true),
            wp_rand(),
            wp_generate_password(12, false, false),
        ]));
    }

    protected function resolvePostedCartEditContext(string $cartItemKey, string $lineToken, int $productId, int $configId, int $userDesignId = 0) : array
    {
        $cartItemKey = sanitize_text_field($cartItemKey);
        $lineToken = sanitize_text_field($lineToken);
        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 [];
        }
        if ($userDesignId > 0 && absint($apd['user_design_id'] ?? 0) !== $userDesignId) {
            return [];
        }

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

    protected function sameUrlPath(string $left, string $right) : bool
    {
        if ($left === '' || $right === '') {
            return false;
        }

        $leftParts = wp_parse_url($left);
        $rightParts = wp_parse_url($right);
        $leftHost = strtolower((string) ($leftParts['host'] ?? ''));
        $rightHost = strtolower((string) ($rightParts['host'] ?? ''));
        if ($leftHost !== '' && $rightHost !== '' && $leftHost !== $rightHost) {
            return false;
        }

        $leftPath = untrailingslashit((string) ($leftParts['path'] ?? ''));
        $rightPath = untrailingslashit((string) ($rightParts['path'] ?? ''));

        return $leftPath !== '' && $leftPath === $rightPath;
    }

    protected function isDesignerSubmission(?array $post = null) : bool
    {
        $post ??= RequestData::post();

        return isset($post['apd_config_id']) || isset($post['apd_design_json']);
    }

    protected function isJsonPayload(string $json) : bool
    {
        $json = trim($json);
        if ($json === '') {
            return false;
        }

        json_decode($json, true);
        return json_last_error() === JSON_ERROR_NONE;
    }

    protected function variationBelongsToProduct(int $productId, int $variationId) : bool
    {
        if ($variationId <= 0) {
            return true;
        }

        $variation = wc_get_product($variationId);
        if (! $variation || ! is_a($variation, '\WC_Product_Variation')) {
            return false;
        }

        return (int) $variation->get_parent_id() === $productId;
    }

    protected function resolveExpectedConfigId(ProductConfigBinder $binder, int $productId, int $variationId) : int
    {
        if ($variationId > 0) {
            $variationConfigId = $binder->getExactConfigId($variationId);
            if ($variationConfigId > 0) {
                return $variationConfigId;
            }
        }

        return $binder->getExactConfigId($productId);
    }

    protected function isAvailableConfig(int $configId) : bool
    {
        if ($configId <= 0) {
            return false;
        }

        $post = get_post($configId);
        if (! $post || $post->post_type !== 'apd_product_config') {
            return false;
        }

        if (in_array($post->post_status, ['publish', 'private'], true)) {
            return true;
        }

        return current_user_can('edit_post', $configId);
    }

    protected function buildProductSnapshot(int $productId, int $variationId, array $variationAttributes = []) : array
    {
        $baseProduct = $productId > 0 ? wc_get_product($productId) : null;
        $selectedProduct = ($variationId > 0 ? wc_get_product($variationId) : null) ?: $baseProduct;
        $productTitle = $baseProduct && method_exists($baseProduct, 'get_name') ? (string) $baseProduct->get_name() : ($productId > 0 ? (string) get_the_title($productId) : '');
        $variationLabel = '';
        if ($variationId > 0) {
            if ($selectedProduct && method_exists($selectedProduct, 'get_formatted_name')) {
                $variationLabel = trim(wp_strip_all_tags((string) $selectedProduct->get_formatted_name()));
            }
            if ($variationLabel === '') {
                $variationLabel = wp_strip_all_tags($this->formatVariationAttributes($variationAttributes));
            }
        }

        $sku = '';
        if ($selectedProduct && method_exists($selectedProduct, 'get_sku')) {
            $sku = trim((string) $selectedProduct->get_sku());
        }
        if ($sku === '' && $baseProduct && method_exists($baseProduct, 'get_sku')) {
            $sku = trim((string) $baseProduct->get_sku());
        }

        return [
            'productTitle' => $productTitle,
            'variationLabel' => $variationLabel,
            'sku' => $sku,
        ];
    }

    protected function decodeJson(string $json) : array
    {
        $decoded = json_decode($json, true);
        return is_array($decoded) ? $decoded : [];
    }

    protected function escapeImageSrc(string $src) : string
    {
        $src = trim($src);
        if ($src === '') {
            return '';
        }

        if (preg_match('#^data:image/(png|jpe?g|gif|webp|svg\+xml);base64,[A-Za-z0-9+/=\r\n]+$#i', $src) || preg_match('#^data:image/svg\+xml;utf8,#i', $src)) {
            return esc_attr($src);
        }

        return esc_url($src);
    }

    protected function resolveApdImageUrl(array $apd) : string
    {
        $previewAttachmentId = absint($apd['preview_attachment_id'] ?? 0);
        if ($previewAttachmentId > 0) {
            $image = wp_get_attachment_image_url($previewAttachmentId, 'woocommerce_thumbnail');
            if (is_string($image) && $image !== '') {
                return $image;
            }
        }

        $previewUrl = esc_url_raw((string) ($apd['preview_url'] ?? ''));
        if ($previewUrl !== '') {
            return $previewUrl;
        }

        $userDesignId = absint($apd['user_design_id'] ?? 0);
        if ($userDesignId > 0) {
            $userDesign = (new UserDesignRepository())->find($userDesignId);
            $previewUrls = is_array($userDesign['previewUrls'] ?? null) ? $userDesign['previewUrls'] : [];
            foreach ($previewUrls as $candidate) {
                $candidate = esc_url_raw((string) $candidate);
                if ($candidate !== '') {
                    return $candidate;
                }
            }
        }

        $previewData = (string) ($apd['preview_data'] ?? '');
        if ($previewData !== '' && str_starts_with($previewData, 'data:image/')) {
            return $previewData;
        }

        $configId = absint($apd['config_id'] ?? 0);
        if ($configId > 0) {
            $thumbnailId = get_post_thumbnail_id($configId);
            if ($thumbnailId > 0) {
                $image = wp_get_attachment_image_url($thumbnailId, 'woocommerce_thumbnail');
                if (is_string($image) && $image !== '') {
                    return $image;
                }
            }
        }

        $productId = absint($apd['variation_id'] ?? 0) ?: absint($apd['product_id'] ?? 0);
        if ($productId > 0) {
            $image = get_the_post_thumbnail_url($productId, 'woocommerce_thumbnail');
            if (is_string($image) && $image !== '') {
                return $image;
            }
        }

        return '';
    }


    protected function applyPreviewToCartItemProduct(array $cartItem) : array
    {
        if (empty($cartItem['apd']) || ! is_array($cartItem['apd'])) {
            return $cartItem;
        }

        if (empty($cartItem['data']) || ! is_object($cartItem['data'])) {
            return $cartItem;
        }

        $previewAttachmentId = absint($cartItem['apd']['preview_attachment_id'] ?? 0);
        if ($previewAttachmentId <= 0) {
            $previewUrl = esc_url_raw((string) ($cartItem['apd']['preview_url'] ?? ''));
            if ($previewUrl !== '') {
                $previewAttachmentId = (new PreviewStorage())->attachmentIdFromUrl($previewUrl);
                if ($previewAttachmentId > 0) {
                    $cartItem['apd']['preview_attachment_id'] = $previewAttachmentId;
                }
            }
        }

        if ($previewAttachmentId <= 0 || ! method_exists($cartItem['data'], 'set_image_id')) {
            return $cartItem;
        }

        $product = clone $cartItem['data'];
        $product->set_image_id($previewAttachmentId);
        if (method_exists($product, 'set_gallery_image_ids')) {
            $product->set_gallery_image_ids([$previewAttachmentId]);
        }
        $cartItem['data'] = $product;

        return $cartItem;
    }

    protected function resolveApdImageUrlForCartItem(array $cartItem, string $cartItemKey = '') : string
    {
        $apd = ! empty($cartItem['apd']) && is_array($cartItem['apd']) ? $cartItem['apd'] : [];
        if ($apd === []) {
            return '';
        }

        $previewAttachmentId = absint($apd['preview_attachment_id'] ?? 0);
        if ($previewAttachmentId > 0) {
            $image = wp_get_attachment_image_url($previewAttachmentId, 'woocommerce_thumbnail');
            if (is_string($image) && $image !== '') {
                return $image;
            }
        }

        $previewUrl = esc_url_raw((string) ($apd['preview_url'] ?? ''));
        if ($previewUrl !== '') {
            return $previewUrl;
        }

        $previewData = (string) ($apd['preview_data'] ?? '');
        if ($previewData !== '' && str_starts_with($previewData, 'data:image/')) {
            $stored = (new PreviewStorage())->store($previewData, 'apd-cart-thumb', [
                'temporary' => true,
                'context' => 'cart',
            ]);
            if (! empty($stored['success']) && ! empty($stored['url'])) {
                $previewUrl = esc_url_raw((string) $stored['url']);
                $previewAttachmentId = absint($stored['attachmentId'] ?? 0);
                if ($previewUrl !== '') {
                    $this->persistCartItemPreview($cartItemKey, $apd, $previewUrl, $previewAttachmentId);
                    return $previewUrl;
                }
            }

            return $previewData;
        }

        return $this->resolveApdImageUrl($apd);
    }

    protected function persistCartItemPreview(string $cartItemKey, array $apd, string $previewUrl, int $previewAttachmentId = 0) : void
    {
        $previewUrl = esc_url_raw($previewUrl);
        if ($previewUrl === '') {
            return;
        }

        if ($previewAttachmentId <= 0) {
            $previewAttachmentId = (new PreviewStorage())->attachmentIdFromUrl($previewUrl);
        }

        if ($cartItemKey !== '' && function_exists('WC') && WC()->cart && isset(WC()->cart->cart_contents[$cartItemKey]['apd']) && is_array(WC()->cart->cart_contents[$cartItemKey]['apd'])) {
            WC()->cart->cart_contents[$cartItemKey]['apd']['preview_url'] = $previewUrl;
            WC()->cart->cart_contents[$cartItemKey]['apd']['preview_attachment_id'] = $previewAttachmentId;
            if (method_exists(WC()->cart, 'set_session')) {
                WC()->cart->set_session();
            }
        }

        $userDesignId = absint($apd['user_design_id'] ?? 0);
        if ($userDesignId > 0) {
            $repo = new UserDesignRepository();
            $existing = $repo->find($userDesignId);
            if ($existing) {
                $previewUrls = is_array($existing['previewUrls'] ?? null) ? $existing['previewUrls'] : [];
                if (! in_array($previewUrl, $previewUrls, true)) {
                    array_unshift($previewUrls, $previewUrl);
                }
                $repo->update($userDesignId, [
                    'previewUrls' => array_values(array_filter($previewUrls)),
                ]);
            }
        }
    }


    public function cleanupRemovedCartItem(string $cartItemKey, $cart) : void
    {
        if (! is_object($cart) || ! isset($cart->removed_cart_contents[$cartItemKey]) || ! is_array($cart->removed_cart_contents[$cartItemKey])) {
            return;
        }

        $this->cleanupCartItemPreview($cart->removed_cart_contents[$cartItemKey]);
    }

    public function cleanupCartEmptiedPreviews() : void
    {
        if (! function_exists('WC') || ! WC()->cart || ! isset(WC()->cart->removed_cart_contents) || ! is_array(WC()->cart->removed_cart_contents)) {
            return;
        }

        foreach (WC()->cart->removed_cart_contents as $removedItem) {
            if (is_array($removedItem)) {
                $this->cleanupCartItemPreview($removedItem);
            }
        }
    }

    protected function cleanupCartItemPreview(array $cartItem) : void
    {
        $apd = ! empty($cartItem['apd']) && is_array($cartItem['apd']) ? $cartItem['apd'] : [];
        if ($apd === []) {
            return;
        }

        $previewUrl = esc_url_raw((string) ($apd['preview_url'] ?? ''));
        $previewAttachmentId = absint($apd['preview_attachment_id'] ?? 0);
        $storage = new PreviewStorage();
        if ($previewAttachmentId <= 0 && $previewUrl !== '') {
            $previewAttachmentId = $storage->attachmentIdFromUrl($previewUrl);
        }
        if ($previewAttachmentId <= 0) {
            return;
        }

        if (! $this->canDeletePreviewAttachment($previewAttachmentId, $previewUrl, $apd)) {
            return;
        }

        $storage->deleteAttachment($previewAttachmentId);
        $this->cleanupUserDesignPreviewReference($apd, $previewUrl);
    }

    protected function canDeletePreviewAttachment(int $previewAttachmentId, string $previewUrl, array $apd) : bool
    {
        if ($previewAttachmentId <= 0) {
            return false;
        }

        if (function_exists('WC') && WC()->cart && isset(WC()->cart->cart_contents) && is_array(WC()->cart->cart_contents)) {
            foreach (WC()->cart->cart_contents as $cartKey => $cartItem) {
                $cartApd = ! empty($cartItem['apd']) && is_array($cartItem['apd']) ? $cartItem['apd'] : [];
                if ($cartApd === []) {
                    continue;
                }
                if (absint($cartApd['preview_attachment_id'] ?? 0) === $previewAttachmentId) {
                    return false;
                }
                if ($previewUrl !== '' && esc_url_raw((string) ($cartApd['preview_url'] ?? '')) === $previewUrl) {
                    return false;
                }
            }
        }

        $userDesignId = absint($apd['user_design_id'] ?? 0);
        if ($userDesignId > 0) {
            $existing = (new UserDesignRepository())->find($userDesignId);
            if (! empty($existing['orderId']) || ! empty($existing['orderItemId'])) {
                return false;
            }
        }

        $isTemporary = (string) get_post_meta($previewAttachmentId, '_apd_temporary_preview', true) === '1';
        return $isTemporary || strpos((string) get_post_mime_type($previewAttachmentId), 'image/') === 0;
    }

    protected function cleanupUserDesignPreviewReference(array $apd, string $previewUrl) : void
    {
        $userDesignId = absint($apd['user_design_id'] ?? 0);
        if ($userDesignId <= 0 || $previewUrl === '') {
            return;
        }

        $repo = new UserDesignRepository();
        $existing = $repo->find($userDesignId);
        if (! $existing || ! empty($existing['orderId']) || ! empty($existing['orderItemId'])) {
            return;
        }

        $previewUrls = is_array($existing['previewUrls'] ?? null) ? $existing['previewUrls'] : [];
        $previewUrls = array_values(array_filter($previewUrls, function ($candidate) use ($previewUrl) {
            return esc_url_raw((string) $candidate) !== $previewUrl;
        }));
        $repo->update($userDesignId, [
            'previewUrls' => $previewUrls,
        ]);
    }

    protected function buildCartHighlights(array $summary) : array
    {
        $highlights = [];
        if (! empty($summary['objectTypes']) && is_array($summary['objectTypes'])) {
            $highlights[] = sprintf(/* translators: Placeholder values are populated from the current WooCommerce product, cart, or order. */ _n('%d object type', '%d object types', count($summary['objectTypes']), 'asuka-product-designer'), count($summary['objectTypes']));
        }
        if (! empty($summary['colors']) && is_array($summary['colors'])) {
            $highlights[] = sprintf(/* translators: Placeholder values are populated from the current WooCommerce product, cart, or order. */ _n('%d color', '%d colors', count($summary['colors']), 'asuka-product-designer'), count($summary['colors']));
        }
        if (! empty($summary['catalogDesigns']) && is_array($summary['catalogDesigns'])) {
            $highlights[] = sprintf(/* translators: Placeholder values are populated from the current WooCommerce product, cart, or order. */ _n('%d design', '%d designs', count($summary['catalogDesigns']), 'asuka-product-designer'), count($summary['catalogDesigns']));
        }
        if (! empty($summary['texts']) && is_array($summary['texts'])) {
            $highlights[] = sprintf(/* translators: Placeholder values are populated from the current WooCommerce product, cart, or order. */ _n('%d text', '%d texts', count($summary['texts']), 'asuka-product-designer'), count($summary['texts']));
        }

        return array_slice($highlights, 0, 4);
    }

    protected function formatVariationAttributes(array $attributes) : string
    {
        if ($attributes === []) {
            return '';
        }

        $items = [];
        foreach ($attributes as $key => $value) {
            $value = (string) $value;
            if ($value === '') {
                continue;
            }

            $label = wc_attribute_label(str_replace('attribute_', '', (string) $key));
            if ($label === '') {
                $label = ucfirst(str_replace(['attribute_', 'pa_', '_', '-'], ['', '', ' ', ' '], (string) $key));
            }
            $items[] = '<span><strong>' . esc_html($label) . ':</strong> ' . esc_html(wc_clean($value)) . '</span>';
        }

        return $items !== [] ? implode(' · ', $items) : '';
    }

    protected function collectVariationAttributes(array $source) : array
    {
        $attributes = [];
        foreach ($source as $key => $value) {
            $name = sanitize_key((string) $key);
            if ($name === '' || strpos($name, 'attribute_') !== 0) {
                continue;
            }
            $attributes[$name] = wc_clean((string) $value);
        }

        return $attributes;
    }

    protected function validateDesignAgainstBehavior(string $designJson, array $frontBehavior, array $permissions = [], string $stateJson = '', array $post = []) : string
    {
        if ($frontBehavior === [] && $permissions === []) {
            return '';
        }

        $stats = $this->collectValidationStats($this->decodeJson($designJson), $this->decodeJson($stateJson));
        $requiresDesign = $this->readBehaviorBool($frontBehavior, 'requireDesign', 'require_design') || $this->readBehaviorBool($frontBehavior, 'requireCustomization', 'require_customization');
        $requirePreview = $this->readBehaviorBool($frontBehavior, 'requirePreview', 'require_preview');
        $requireText = $this->readBehaviorBool($frontBehavior, 'requireText', 'require_text');
        $requireEveryView = $this->readBehaviorBool($frontBehavior, 'requireEveryView', 'require_every_view');
        $minObjects = $this->readBehaviorInt($frontBehavior, 'minObjects', 'min_objects');
        $maxObjects = $this->readBehaviorInt($frontBehavior, 'maxObjects', 'max_objects');
        $minTextObjects = $this->readBehaviorInt($frontBehavior, 'minTextObjects', 'min_text_objects');
        $maxTextObjects = $this->readBehaviorInt($frontBehavior, 'maxTextObjects', 'max_text_objects');
        $minImageObjects = $this->readBehaviorInt($frontBehavior, 'minImageObjects', 'min_image_objects');
        $maxImageObjects = $this->readBehaviorInt($frontBehavior, 'maxImageObjects', 'max_image_objects');
        $minTextLength = $this->readBehaviorInt($frontBehavior, 'minTextLength', 'min_text_length');
        $minObjectsPerView = max(1, $this->readBehaviorInt($frontBehavior, 'minObjectsPerView', 'min_objects_per_view'));
        $requiredViewsCount = $this->readBehaviorInt($frontBehavior, 'requiredViewsCount', 'required_views_count');
        $requiredViews = $this->normalizeBehaviorList($this->readBehaviorList($frontBehavior, 'requiredViews', 'required_views'));
        $allowedObjectTypes = $this->normalizeObjectTypeList($this->readBehaviorList($frontBehavior, 'allowedObjectTypes', 'allowed_object_types'));
        $forbiddenObjectTypes = $this->normalizeObjectTypeList($this->readBehaviorList($frontBehavior, 'forbiddenObjectTypes', 'forbidden_object_types'));
        $requiredObjectTypes = $this->normalizeObjectTypeList($this->readBehaviorList($frontBehavior, 'requiredObjectTypes', 'required_object_types'));

        if ($requiresDesign && $stats['totalObjects'] <= 0) {
            return $this->resolveBehaviorMessage($frontBehavior, 'requireDesign', __('Please complete your customization before adding this product to the cart.', 'asuka-product-designer'));
        }
        if ($minObjects > 0 && $stats['totalObjects'] < $minObjects) {
            return $this->resolveBehaviorMessage($frontBehavior, 'minObjects', sprintf(
                /* translators: %d: minimum objects */
                __('Please add at least %d customization element(s) before adding this product to the cart.', 'asuka-product-designer'),
                $minObjects
            ), ['min' => $minObjects]);
        }
        if ($maxObjects > 0 && $stats['totalObjects'] > $maxObjects) {
            return $this->resolveBehaviorMessage($frontBehavior, 'maxObjects', sprintf(
                /* translators: %d: maximum objects */
                __('Please remove customization elements before adding this product to the cart. Maximum allowed: %d.', 'asuka-product-designer'),
                $maxObjects
            ), ['max' => $maxObjects]);
        }
        if ($minTextObjects > 0 && $stats['textObjects'] < $minTextObjects) {
            return $this->resolveBehaviorMessage($frontBehavior, 'minTextObjects', sprintf(
                /* translators: %d: minimum text objects */
                __('Please add at least %d text element(s) before adding this product to the cart.', 'asuka-product-designer'),
                $minTextObjects
            ), ['min' => $minTextObjects]);
        }
        if ($maxTextObjects > 0 && $stats['textObjects'] > $maxTextObjects) {
            return $this->resolveBehaviorMessage($frontBehavior, 'maxTextObjects', sprintf(
                /* translators: %d: maximum text objects */
                __('Please remove text elements before adding this product to the cart. Maximum allowed: %d.', 'asuka-product-designer'),
                $maxTextObjects
            ), ['max' => $maxTextObjects]);
        }
        if ($minImageObjects > 0 && $stats['imageObjects'] < $minImageObjects) {
            return $this->resolveBehaviorMessage($frontBehavior, 'minImageObjects', sprintf(
                /* translators: %d: minimum image objects */
                __('Please add at least %d image element(s) before adding this product to the cart.', 'asuka-product-designer'),
                $minImageObjects
            ), ['min' => $minImageObjects]);
        }
        if ($maxImageObjects > 0 && $stats['imageObjects'] > $maxImageObjects) {
            return $this->resolveBehaviorMessage($frontBehavior, 'maxImageObjects', sprintf(
                /* translators: %d: maximum image objects */
                __('Please remove image elements before adding this product to the cart. Maximum allowed: %d.', 'asuka-product-designer'),
                $maxImageObjects
            ), ['max' => $maxImageObjects]);
        }
        if ($requireText && $stats['totalTextLength'] <= 0) {
            return $this->resolveBehaviorMessage($frontBehavior, 'requireText', __('Please add personalized text before adding this product to the cart.', 'asuka-product-designer'));
        }
        if ($minTextLength > 0 && $stats['totalTextLength'] < $minTextLength) {
            return $this->resolveBehaviorMessage($frontBehavior, 'minTextLength', sprintf(
                /* translators: %d: minimum text length */
                __('Please enter at least %d text character(s) before adding this product to the cart.', 'asuka-product-designer'),
                $minTextLength
            ), ['min' => $minTextLength]);
        }
        if ($requirePreview) {
            $previewData = (string) ($post['apd_preview_data'] ?? $post['apd_preview_url'] ?? '');
            if ($previewData === '') {
                return $this->resolveBehaviorMessage($frontBehavior, 'requirePreview', __('A preview is required before adding this customized product to the cart.', 'asuka-product-designer'));
            }
        }
        if ($requiredViewsCount > 0 && $stats['viewsWithObjects'] < $requiredViewsCount) {
            return $this->resolveBehaviorMessage($frontBehavior, 'requiredViewsCount', sprintf(
                /* translators: %d: required views count */
                __('Please customize at least %d view(s) before adding this product to the cart.', 'asuka-product-designer'),
                $requiredViewsCount
            ), ['count' => $requiredViewsCount]);
        }
        if (($requireEveryView || $this->readBehaviorInt($frontBehavior, 'minObjectsPerView', 'min_objects_per_view') > 0) && $this->viewsNeedObjects($stats, $minObjectsPerView)) {
            return $this->resolveBehaviorMessage($frontBehavior, 'requireEveryView', __('Please complete every required view before adding this product to the cart.', 'asuka-product-designer'));
        }
        if ($requiredViews !== [] && ! $this->requiredViewsSatisfied($stats, $requiredViews, $minObjectsPerView)) {
            return $this->resolveBehaviorMessage($frontBehavior, 'requiredViews', sprintf(
                /* translators: %s: required views */
                __('Please customize the required views before adding this product to the cart: %s.', 'asuka-product-designer'),
                implode(', ', $requiredViews)
            ), ['views' => implode(', ', $requiredViews)]);
        }
        if ($allowedObjectTypes !== [] && $this->hasDisallowedTypes($stats, $allowedObjectTypes)) {
            return $this->resolveBehaviorMessage($frontBehavior, 'allowedObjectTypes', __('This customization contains unsupported element types for this product.', 'asuka-product-designer'));
        }
        if ($forbiddenObjectTypes !== [] && $this->hasForbiddenTypes($stats, $forbiddenObjectTypes)) {
            return $this->resolveBehaviorMessage($frontBehavior, 'forbiddenObjectTypes', __('Please remove the forbidden element types before adding this product to the cart.', 'asuka-product-designer'));
        }
        if ($requiredObjectTypes !== [] && ! $this->hasRequiredTypes($stats, $requiredObjectTypes)) {
            return $this->resolveBehaviorMessage($frontBehavior, 'requiredObjectTypes', sprintf(
                /* translators: %s: required object types */
                __('This product requires the following element types: %s.', 'asuka-product-designer'),
                implode(', ', $requiredObjectTypes)
            ), ['types' => implode(', ', $requiredObjectTypes)]);
        }
        if (($permissions['canAddText'] ?? null) === false && $stats['textObjects'] > 0) {
            return $this->resolveBehaviorMessage($frontBehavior, 'textForbidden', __('Text customization is not allowed for this product.', 'asuka-product-designer'));
        }
        if (($permissions['canAddImage'] ?? null) === false && $stats['imageObjects'] > 0) {
            return $this->resolveBehaviorMessage($frontBehavior, 'imageForbidden', __('Image customization is not allowed for this product.', 'asuka-product-designer'));
        }

        return '';
    }

    protected function collectValidationStats(array $design, array $state = []) : array
    {
        $viewMeta = [];
        $views = is_array($design['views']['views'] ?? null) ? $design['views']['views'] : [];
        foreach ($views as $view) {
            if (! is_array($view)) {
                continue;
            }
            $viewId = (string) ($view['id'] ?? '');
            if ($viewId === '') {
                continue;
            }
            $viewMeta[$viewId] = [
                'id' => $viewId,
                'name' => (string) ($view['name'] ?? $viewId),
            ];
        }

        $byView = is_array($design['objects']['byView'] ?? null) ? $design['objects']['byView'] : [];
        if ($byView === [] && is_array($design['snapshots']['snapshotsByView'] ?? null)) {
            foreach ($design['snapshots']['snapshotsByView'] as $viewId => $snapshot) {
                if (is_array($snapshot) && is_array($snapshot['objects'] ?? null)) {
                    $byView[(string) $viewId] = $snapshot['objects'];
                }
            }
        }

        $stats = [
            'totalObjects' => 0,
            'textObjects' => 0,
            'imageObjects' => 0,
            'totalTextLength' => 0,
            'objectTypes' => [],
            'countsByView' => [],
            'viewsWithObjects' => 0,
            'viewMeta' => $viewMeta,
        ];

        foreach ($byView as $viewId => $objects) {
            if (! is_array($objects)) {
                continue;
            }
            $stats['countsByView'][(string) $viewId] = count($objects);
            if (count($objects) > 0) {
                $stats['viewsWithObjects'] += 1;
            }
            foreach ($objects as $object) {
                if (! is_array($object)) {
                    continue;
                }
                $stats['totalObjects'] += 1;
                $type = $this->normalizeObjectType((string) ($object['itemGroup'] ?? ($object['type'] ?? '')));
                if ($type !== '') {
                    $stats['objectTypes'][$type] = true;
                }
                if ($type === 'text') {
                    $stats['textObjects'] += 1;
                    $stats['totalTextLength'] += strlen(trim((string) ($object['text'] ?? '')));
                }
                if ($type === 'image') {
                    $stats['imageObjects'] += 1;
                }
            }
        }

        return $stats;
    }

    protected function normalizeCartStatePayload(array $state, array $context = []) : array
    {
        unset(
            $state['document'],
            $state['initialDocument'],
            $state['documentState'],
            $state['objectsByView'],
            $state['documentObjectsByView'],
            $state['snapshotsByView'],
            $state['viewSnapshots'],
            $state['canvas'],
            $state['legacy']
        );

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

        if ($context !== []) {
            $state['product'] = [
                'productId' => absint($context['productId'] ?? 0),
                'configId' => absint($context['configId'] ?? 0),
                'variationId' => absint($context['variationId'] ?? 0),
                'variationAttributes' => is_array($context['variationAttributes'] ?? null) ? $context['variationAttributes'] : [],
            ];
            $state['scope']['product'] = true;
        }

        return $state;
    }

    protected function preferRestorableCartDesignPayload(array $design, array $state) : array
    {
        // The submitted design field is the canonical snapshot of the click/submit
        // moment. The state field is allowed to carry UI/layout/product context,
        // but it must never beat apd_design_json by score, because a full state can
        // contain an older embedded document with the same number of objects. That
        // was the source of the “open saved design does not match submit” issue.
        $normalizedDesign = $this->normalizeCartDocumentPayload($design);
        if ($this->payloadRestorationScore($normalizedDesign) > 0) {
            return $normalizedDesign;
        }

        $nestedDesign = $this->extractBestDocumentPayload($design);
        if ($nestedDesign !== []) {
            $normalizedNestedDesign = $this->normalizeCartDocumentPayload($nestedDesign);
            if ($this->payloadRestorationScore($normalizedNestedDesign) > 0) {
                return $normalizedNestedDesign;
            }
        }

        $stateDocument = $this->extractBestDocumentPayload($state);
        return $stateDocument !== [] ? $this->normalizeCartDocumentPayload($stateDocument) : [];
    }

    protected function normalizeCartDocumentPayload(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->filterCartObjectsByView($this->deriveObjectsByViewFromSnapshots($snapshotsByView, $this->payloadObjectsByView($document)));
        $rootObjects = method_exists($this, 'payloadRootObjects') ? $this->filterCartPersistableObjects($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;
        }
        if (isset($document['meta']) && is_array($document['meta'])) {
            $canonical['meta'] = $document['meta'];
        }
        if (isset($document['__apdSubmitProof']) && is_array($document['__apdSubmitProof'])) {
            $canonical['__apdSubmitProof'] = $document['__apdSubmitProof'];
        }

        return $canonical;
    }


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

        foreach (['excludeFromExport', '_excludeFromExport', '_asukaTemporary', '_asukaSnapIndicator', '_asukaGuide'] as $flag) {
            if (! empty($object[$flag])) {
                return false;
            }
        }

        $type = strtolower((string) ($object['type'] ?? ($object['objectType'] ?? '')));
        $itemGroup = strtolower((string) ($object['itemGroup'] ?? ''));
        $role = strtolower((string) ($object['role'] ?? ''));
        $source = strtolower((string) ($object['_asukaSourceModule'] ?? ($object['sourceModule'] ?? '')));

        if (in_array($itemGroup, ['tools', 'guides', 'guide'], true) || in_array($role, ['guide', 'snap', 'helper'], true) || in_array($source, ['tools', 'guides'], true)) {
            return false;
        }

        if ($type === 'line' && ((isset($object['selectable']) && ! $object['selectable']) || (isset($object['evented']) && ! $object['evented']) || $itemGroup === 'tools')) {
            return false;
        }

        return true;
    }

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

        return array_values(array_filter($objects, fn ($object) => $this->isCartPersistableObject($object)));
    }

    protected function filterCartObjectsByView(mixed $objectsByView) : array
    {
        if (! is_array($objectsByView) || $objectsByView === []) {
            return [];
        }

        $filtered = [];
        foreach ($objectsByView as $viewId => $objects) {
            $filtered[(string) $viewId] = $this->filterCartPersistableObjects($objects);
        }

        return $filtered;
    }


    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'] as $key) {
            if (isset($payload[$key]) && $this->payloadHasViews($payload[$key])) {
                return true;
            }
        }

        return false;
    }


    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 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 extractConfigDrawingZones(int $configId) : array
    {
        if ($configId <= 0) {
            return [];
        }
        $raw = trim((string) get_post_meta($configId, '_apd_drawing_zones', true));
        if ($raw === '') {
            return [];
        }
        $decoded = $this->decodeJson($raw);
        if ($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 cartDocumentHasDrawingAreas(array $document) : bool
    {
        if (isset($document['drawingAreas']['byView']) && is_array($document['drawingAreas']['byView']) && $document['drawingAreas']['byView'] !== []) {
            return true;
        }
        if (isset($document['drawingAreas']['zones']) && is_array($document['drawingAreas']['zones']) && $document['drawingAreas']['zones'] !== []) {
            return true;
        }
        if (isset($document['drawingAreasByView']) && is_array($document['drawingAreasByView']) && $document['drawingAreasByView'] !== []) {
            return true;
        }
        if (isset($document['drawingZones']) && is_array($document['drawingZones']) && $document['drawingZones'] !== []) {
            return true;
        }
        return false;
    }

    protected function mergeDrawingZonesIntoCartDocument(array $document, array $zones) : array
    {
        if ($document === [] || $zones === []) {
            return $document;
        }
        $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 mergeConfigDrawingZonesIntoCartDocument(array $document, int $configId) : array
    {
        if ($document === [] || $this->cartDocumentHasDrawingAreas($document)) {
            return $document;
        }
        return $this->mergeDrawingZonesIntoCartDocument($document, $this->extractConfigDrawingZones($configId));
    }


    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 payloadRestorationScore(array $payload) : int
    {
        if ($payload === []) {
            return 0;
        }

        return ($this->payloadViewCount($payload) * 10) + ($this->payloadObjectCount($payload) * 100) + ($this->payloadSnapshotCount($payload) * 2);
    }

    protected function extractBestDocumentPayload(array $payload) : array
    {
        if ($payload === []) {
            return [];
        }

        $candidates = [];
        $this->collectDocumentPayloadCandidates($payload, $candidates, 0);
        if ($candidates === []) {
            return [];
        }

        usort($candidates, function (array $left, array $right) : int {
            return $this->payloadRestorationScore($right) <=> $this->payloadRestorationScore($left);
        });

        return $candidates[0];
    }

    protected function collectDocumentPayloadCandidates(mixed $payload, array &$candidates, int $depth) : void
    {
        if (! is_array($payload) || $payload === [] || $depth > 6) {
            return;
        }

        if ($this->payloadViewCount($payload) > 0) {
            $candidates[] = $payload;
        }

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

    protected function payloadViewCount(array $payload) : int
    {
        $views = $payload['views'] ?? null;
        if (is_array($views)) {
            if (array_is_list($views)) {
                return count($views);
            }
            if (isset($views['views']) && is_array($views['views'])) {
                return count($views['views']);
            }
        }

        return 0;
    }

    protected function payloadObjectCount(array $payload) : int
    {
        $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']);
            }
        }

        return $count;
    }

    protected function payloadSnapshotCount(array $payload) : int
    {
        $count = 0;
        foreach ($this->snapshotPayloadBuckets($payload) as $bucket) {
            $count += count($bucket);
        }

        return $count;
    }

    protected function snapshotPayloadBuckets(array $payload) : array
    {
        $snapshotsByView = $this->payloadSnapshotsByView($payload);

        return $snapshotsByView !== [] ? [$snapshotsByView] : [];
    }


    protected function normalizeObjectType(string $type) : string
    {
        $type = strtolower(trim($type));
        if ($type === '') {
            return '';
        }
        return match ($type) {
            'textbox', 'itext', 'text' => 'text',
            'image' => 'image',
            'rect', 'circle', 'triangle', 'polygon', 'ellipse', 'path', 'line' => 'shape',
            default => $type,
        };
    }

    protected function readBehaviorBool(array $behavior, string $camelKey, string $snakeKey = '') : bool
    {
        $value = $behavior[$camelKey] ?? null;
        if ($value === null && $snakeKey !== '') {
            $value = $behavior[$snakeKey] ?? null;
        }
        return ! empty($value);
    }

    protected function readBehaviorInt(array $behavior, string $camelKey, string $snakeKey = '') : int
    {
        $value = $behavior[$camelKey] ?? null;
        if ($value === null && $snakeKey !== '') {
            $value = $behavior[$snakeKey] ?? null;
        }
        return absint($value);
    }

    protected function readBehaviorList(array $behavior, string $camelKey, string $snakeKey = '') : array
    {
        $value = $behavior[$camelKey] ?? null;
        if ($value === null && $snakeKey !== '') {
            $value = $behavior[$snakeKey] ?? null;
        }
        return is_array($value) ? $value : [];
    }

    protected function normalizeBehaviorList(array $values) : array
    {
        $result = [];
        foreach ($values as $value) {
            $string = trim((string) $value);
            if ($string !== '') {
                $result[$string] = true;
            }
        }
        return array_keys($result);
    }

    protected function normalizeObjectTypeList(array $values) : array
    {
        $result = [];
        foreach ($values as $value) {
            $type = $this->normalizeObjectType((string) $value);
            if ($type !== '') {
                $result[$type] = true;
            }
        }
        return array_keys($result);
    }

    protected function resolveBehaviorMessage(array $behavior, string $key, string $fallback, array $replacements = []) : string
    {
        $messages = [];
        if (is_array($behavior['messages'] ?? null)) {
            $messages = $behavior['messages'];
        } elseif (is_array($behavior['validationMessages'] ?? null)) {
            $messages = $behavior['validationMessages'];
        }

        $message = '';
        if (is_string($messages[$key] ?? null) && trim((string) $messages[$key]) !== '') {
            $message = trim((string) $messages[$key]);
        } elseif (is_string($behavior[$key . 'Message'] ?? null) && trim((string) $behavior[$key . 'Message']) !== '') {
            $message = trim((string) $behavior[$key . 'Message']);
        } elseif ($key === 'requireDesign' && is_string($behavior['message'] ?? null) && trim((string) $behavior['message']) !== '') {
            $message = trim((string) $behavior['message']);
        } else {
            $message = $fallback;
        }

        foreach ($replacements as $token => $value) {
            $message = str_replace('{' . $token . '}', (string) $value, $message);
        }

        return $message;
    }

    protected function viewsNeedObjects(array $stats, int $minimumPerView) : bool
    {
        $viewIds = array_keys($stats['viewMeta'] ?? []);
        if ($viewIds === []) {
            $viewIds = array_keys($stats['countsByView'] ?? []);
        }
        foreach ($viewIds as $viewId) {
            if ((int) ($stats['countsByView'][$viewId] ?? 0) < $minimumPerView) {
                return true;
            }
        }
        return false;
    }

    protected function requiredViewsSatisfied(array $stats, array $requiredViews, int $minimumPerView) : bool
    {
        $lookup = [];
        foreach ($requiredViews as $value) {
            $lookup[strtolower(trim((string) $value))] = true;
        }
        $matched = 0;
        foreach ($stats['viewMeta'] as $viewId => $meta) {
            $idKey = strtolower(trim((string) ($meta['id'] ?? $viewId)));
            $nameKey = strtolower(trim((string) ($meta['name'] ?? $viewId)));
            if (empty($lookup[$idKey]) && empty($lookup[$nameKey])) {
                continue;
            }
            if ((int) ($stats['countsByView'][$viewId] ?? 0) >= $minimumPerView) {
                $matched += 1;
            }
        }
        return $matched >= count($requiredViews);
    }

    protected function hasDisallowedTypes(array $stats, array $allowedTypes) : bool
    {
        $allowed = array_fill_keys($allowedTypes, true);
        foreach (array_keys($stats['objectTypes'] ?? []) as $type) {
            if (empty($allowed[$type])) {
                return true;
            }
        }
        return false;
    }

    protected function hasForbiddenTypes(array $stats, array $forbiddenTypes) : bool
    {
        $forbidden = array_fill_keys($forbiddenTypes, true);
        foreach (array_keys($stats['objectTypes'] ?? []) as $type) {
            if (! empty($forbidden[$type])) {
                return true;
            }
        }
        return false;
    }

    protected function hasRequiredTypes(array $stats, array $requiredTypes) : bool
    {
        foreach ($requiredTypes as $type) {
            if (empty($stats['objectTypes'][$type])) {
                return false;
            }
        }
        return true;
    }

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

    protected function logSubmitDebug(string $tag, array $data) : void
    {
        if (! $this->submitDebugEnabled()) {
            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 shortSubmitHash(string $value) : string
    {
        return substr(hash('sha256', $value), 0, 12);
    }

    protected function summarizeSubmitJsonField(string $raw) : array
    {
        $decoded = $this->decodeJson($raw);
        return [
            'length' => strlen($raw),
            'hash' => $this->shortSubmitHash($raw),
            'parseOk' => is_array($decoded),
            'payload' => is_array($decoded) ? $this->summarizeSubmitPayload($decoded) : [],
        ];
    }

    protected function summarizeSubmitPostPayload(?array $post = null) : array
    {
        $post ??= RequestData::post();

        return [
            'keys' => array_values(array_filter(array_keys($post), static fn ($key) => substr((string) $key, 0, 4) === 'apd_' || $key === 'add-to-cart' || $key === 'variation_id')),
            'addToCart' => (string) ($post['add-to-cart'] ?? ''),
            'productId' => (int) ($post['apd_product_id'] ?? 0),
            'configId' => (int) ($post['apd_config_id'] ?? 0),
            'variationId' => (int) ($post['apd_variation_id'] ?? ($post['variation_id'] ?? 0)),
            'designField' => $this->summarizeSubmitJsonField((string) ($post['apd_design_json'] ?? '')),
            'stateField' => $this->summarizeSubmitJsonField((string) ($post['apd_state_json'] ?? '')),
            'userDesignId' => (int) ($post['apd_user_design_id'] ?? 0),
            'replaceCartItemKey' => (string) ($post['apd_replace_cart_item_key'] ?? ''),
            'cartLineToken' => (string) ($post['apd_cart_line_token'] ?? ''),
        ];
    }

    protected function summarizeSubmitPayload(array $payload) : array
    {
        if ($payload === []) {
            return [
                'empty' => true,
                'score' => 0,
                'views' => 0,
                'objects' => 0,
                'snapshots' => 0,
                'activeViewId' => '',
                'objectsByView' => [],
                'topKeys' => [],
                'proof' => [],
            ];
        }

        $objectsByView = $this->payloadObjectsByView($payload);
        $counts = [];
        foreach ($objectsByView as $viewId => $objects) {
            $counts[(string) $viewId] = is_array($objects) ? count($objects) : 0;
        }

        return [
            'empty' => false,
            'score' => $this->payloadRestorationScore($payload),
            'views' => $this->payloadViewCount($payload),
            'objects' => $this->payloadObjectCount($payload),
            'snapshots' => $this->payloadSnapshotCount($payload),
            'activeViewId' => $this->payloadActiveViewId($payload),
            'objectsByView' => $counts,
            'topKeys' => array_slice(array_keys($payload), 0, 24),
            'proof' => $this->summarizeSubmitProof($this->extractSubmitProof($payload)),
            'hash' => $this->shortSubmitHash((string) wp_json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)),
        ];
    }

    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 submitProofVerdict(array $postProof, array $submittedDesign, array $savedDesign) : array
    {
        $submittedProof = $this->extractSubmitProof($submittedDesign);
        $savedProof = $this->extractSubmitProof($savedDesign);
        $postId = (string) ($postProof['id'] ?? '');
        $submittedId = (string) ($submittedProof['id'] ?? '');
        $savedId = (string) ($savedProof['id'] ?? '');
        return [
            'postMatchesSubmitted' => $postId !== '' && $postId === $submittedId,
            'submittedMatchesSaved' => $savedId === '' ? null : ($submittedId !== '' && $submittedId === $savedId),
            'postId' => $postId,
            'submittedId' => $submittedId,
            'savedId' => $savedId,
        ];
    }

}
