<?php
/**
 * Plugin Name: Canvas Prints Ltd. Print-on-Demand for WooCommerce
 * Plugin URI: https://www.canvasprintscalgary.com/art-dropshipping
 * Description: Create and fulfill custom canvas print-on-demand products in WooCommerce with automated orders, shipping, and tracking.
 * Version: 1.10.0
 * Requires at least: 6.2
 * Requires PHP: 8.1
 * Requires Plugins: woocommerce
 * Author: Canvas Prints Ltd.
 * Author URI: https://www.canvasprintscalgary.com/
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: canvas-prints-ltd-print-on-demand-for-woocommerce
 * WC requires at least: 8.2
 * WC tested up to: 10.1
 */

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

require_once __DIR__ . '/includes/class-cplpod-print-on-demand-artwork-wizard.php';

final class CPLPOD_Print_On_Demand
{
    private const MAX_UPLOAD_BYTES = 110 * 1024 * 1024;
    private const MIN_PRINT_DPI = 150;
    private const RATIO_TOLERANCE = 0.04;
    private const OPTION_API_URL = 'cplpod_print_on_demand_api_url';
    private const OPTION_API_KEY = 'cplpod_print_on_demand_api_key';
    private const OPTION_MARKET = 'cplpod_print_on_demand_market';
    private const CURRENCY_TRANSIENT = 'cplpod_print_on_demand_currency_context';
    private const LEGACY_OPTION_API_URL = 'cplpod_api_url';
    private const LEGACY_OPTION_API_KEY = 'cplpod_api_key';
    private const ORIGINAL_OPTION_API_URL = 'canvas_printing_fulfillment_api_url';
    private const ORIGINAL_OPTION_API_KEY = 'canvas_printing_fulfillment_api_key';
    private const OPTION_API_URL_DEFAULT = 'https://www.canvasprintscalgary.com/api/partner/v1';
    private const OPTION_API_URL_US = 'https://www.canvasprintsusa.com/api/partner/v1';
    private const META_ENABLED = '_cplpod_enabled';
    private const META_IMAGE_MODE = '_cplpod_image_mode';
    private const META_ALLOWED_SIZES = '_cplpod_allowed_sizes';
    private const META_ARTWORK_URL = '_cplpod_artwork_url';
    private const META_DEFAULT_SIZE = '_cplpod_default_size';
    private const ORDER_META_SENT = '_cplpod_fulfillment_order_id';
    private const ORDER_META_STATUS = '_cplpod_fulfillment_status';
    private const ORDER_META_TRACKING_NUMBER = '_cplpod_tracking_number';
    private const ORDER_META_TRACKING_CARRIER = '_cplpod_tracking_carrier';
    private const ORDER_META_TRACKING_URL = '_cplpod_tracking_url';
    private const ORDER_META_TRACKING_NOTIFIED = '_cplpod_tracking_notified_hash';

    public function __construct()
    {
        add_action('admin_menu', [$this, 'register_settings_page']);
        add_action('admin_init', [$this, 'register_settings']);
        add_action('admin_init', [$this, 'add_privacy_policy_content']);
        add_action('woocommerce_product_options_general_product_data', [$this, 'render_product_fields']);
        add_action('woocommerce_admin_process_product_object', [$this, 'save_product_fields']);
        add_action('woocommerce_before_add_to_cart_button', [$this, 'render_upload_field']);
        add_action('wp_enqueue_scripts', [$this, 'enqueue_preview_assets']);
        add_filter('woocommerce_add_to_cart_validation', [$this, 'validate_upload'], 10, 3);
        add_filter('woocommerce_add_cart_item_data', [$this, 'attach_cart_item_data'], 10, 3);
        add_action('woocommerce_checkout_create_order_line_item', [$this, 'attach_order_line_meta'], 10, 4);
        add_filter('woocommerce_package_rates', [$this, 'canvas_shipping_rates'], 90, 2);
        add_action('woocommerce_order_status_processing', [$this, 'send_order_to_fulfillment']);
        add_action('woocommerce_order_status_completed', [$this, 'send_order_to_fulfillment']);
        add_action('woocommerce_admin_order_data_after_order_details', [$this, 'render_order_fulfillment_status']);
        add_action('woocommerce_email_order_meta', [$this, 'render_tracking_in_email'], 20, 4);
        add_action('admin_notices', [$this, 'woocommerce_notice']);
        add_action('rest_api_init', [$this, 'register_canvas_push_routes']);
    }

    public static function activate(): void
    {
        if (get_option(self::OPTION_API_URL, '') === '') {
            $legacy_url = (string) get_option(
                self::LEGACY_OPTION_API_URL,
                get_option(self::ORIGINAL_OPTION_API_URL, '')
            );
            if ($legacy_url !== '') {
                update_option(self::OPTION_API_URL, esc_url_raw($legacy_url));
            }
        }

        if (get_option(self::OPTION_API_KEY, '') === '') {
            $legacy_key = (string) get_option(
                self::LEGACY_OPTION_API_KEY,
                get_option(self::ORIGINAL_OPTION_API_KEY, '')
            );
            if ($legacy_key !== '') {
                update_option(self::OPTION_API_KEY, sanitize_text_field($legacy_key));
            }
        }

        if (!in_array((string) get_option(self::OPTION_MARKET, ''), ['CA', 'US'], true)) {
            $savedUrl = strtolower((string) get_option(self::OPTION_API_URL, ''));
            $market = str_contains($savedUrl, 'canvasprintsusa.com') ? 'US' : self::detect_store_market();
            update_option(self::OPTION_MARKET, $market);
        }
    }

    public function add_privacy_policy_content(): void
    {
        if (!function_exists('wp_add_privacy_policy_content')) {
            return;
        }

        $content = '<p>' . wp_kses_post(
            __('When a connected Canvas fulfillment product is ordered, this plugin sends the store URL, order identifier, selected canvas configuration, production artwork URL, and the buyer\'s delivery contact and address to Canvas Prints Ltd. This information is used to price, produce, ship, and return tracking for the order. Review the Canvas Prints privacy policy and terms before connecting a store.', 'canvas-prints-ltd-print-on-demand-for-woocommerce')
        ) . '</p>';

        wp_add_privacy_policy_content(
            __('Canvas Prints Ltd. Print-on-Demand', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
            wp_kses_post(wpautop($content, false))
        );
    }

    public function woocommerce_notice(): void
    {
        if (class_exists('WooCommerce')) {
            return;
        }

        echo '<div class="notice notice-error"><p>' . esc_html__('Canvas Prints Ltd. Print-on-Demand requires WooCommerce.', 'canvas-prints-ltd-print-on-demand-for-woocommerce') . '</p></div>';
    }

    public function register_settings_page(): void
    {
        add_submenu_page(
            'woocommerce',
            'Canvas Prints Print-on-Demand',
            'Canvas Print-on-Demand',
            'manage_woocommerce',
            'canvas-prints-ltd-print-on-demand-for-woocommerce',
            [$this, 'render_settings_page']
        );
    }

    public function register_settings(): void
    {
        register_setting('cplpod_print_on_demand_settings', self::OPTION_API_URL, [
            'sanitize_callback' => static function ($value): string {
                $host = strtolower((string) wp_parse_url(esc_url_raw((string) $value), PHP_URL_HOST));

                return in_array($host, ['canvasprintsusa.com', 'www.canvasprintsusa.com'], true)
                    ? self::OPTION_API_URL_US
                    : self::OPTION_API_URL_DEFAULT;
            },
        ]);
        register_setting('cplpod_print_on_demand_settings', self::OPTION_API_KEY, ['sanitize_callback' => 'sanitize_text_field']);
        register_setting('cplpod_print_on_demand_settings', self::OPTION_MARKET, [
            'sanitize_callback' => static fn ($value): string => strtoupper((string) $value) === 'US' ? 'US' : 'CA',
        ]);
    }

    public function render_settings_page(): void
    {
        $status = $this->api_key() !== ''
            ? $this->request('GET', '/status')
            : ['status' => __('Not connected. Choose the pricing market and paste the API key supplied by Canvas Prints.', 'canvas-prints-ltd-print-on-demand-for-woocommerce')];
        $market = $this->market();
        $accountUrl = $market === 'US'
            ? 'https://www.canvasprintsusa.com/customer/dropshipping'
            : 'https://www.canvasprintscalgary.com/customer/dropshipping';
        ?>
        <div class="wrap">
            <h1><?php esc_html_e('Canvas Prints Ltd. Print-on-Demand', 'canvas-prints-ltd-print-on-demand-for-woocommerce'); ?></h1>
            <div class="notice notice-info inline">
                <p>
                    <?php
                    echo wp_kses_post(
                        sprintf(
                            /* translators: 1: Privacy-policy URL. 2: Terms URL. */
                            __('Saving a connection explicitly authorizes this store to exchange the product, order, buyer-delivery, billing-status and tracking data required for fulfillment with Canvas Prints Ltd. Review the <a href="%1$s" target="_blank" rel="noopener noreferrer">privacy policy</a> and <a href="%2$s" target="_blank" rel="noopener noreferrer">terms</a> before connecting.', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
                            esc_url('https://www.canvasprintscalgary.com/canvas-prints-privacy-policy'),
                            esc_url('https://www.canvasprintscalgary.com/canvas-prints-terms')
                        )
                    );
                    ?>
                </p>
            </div>
            <form method="post" action="options.php">
                <?php settings_fields('cplpod_print_on_demand_settings'); ?>
                <table class="form-table" role="presentation">
                    <tr>
                        <th scope="row"><label for="cplpod_market"><?php esc_html_e('Where is your business based?', 'canvas-prints-ltd-print-on-demand-for-woocommerce'); ?></label></th>
                        <td>
                            <select id="cplpod_market" name="<?php echo esc_attr(self::OPTION_MARKET); ?>">
                                <option value="CA" <?php selected($market, 'CA'); ?>><?php esc_html_e('Canada — Canadian account and CAD pricing', 'canvas-prints-ltd-print-on-demand-for-woocommerce'); ?></option>
                                <option value="US" <?php selected($market, 'US'); ?>><?php esc_html_e('United States — U.S. account and USD pricing', 'canvas-prints-ltd-print-on-demand-for-woocommerce'); ?></option>
                            </select>
                            <p class="description"><?php esc_html_e('Choose where your business is based, not where your buyers live. This creates one Canvas account and fixes its catalog, seasonal pricing, membership discounts, currency, and fulfillment charges. Your WooCommerce store may still sell to buyers in both Canada and the United States.', 'canvas-prints-ltd-print-on-demand-for-woocommerce'); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label for="cplpod_api_key"><?php esc_html_e('API Key', 'canvas-prints-ltd-print-on-demand-for-woocommerce'); ?></label></th>
                        <td><input id="cplpod_api_key" name="<?php echo esc_attr(self::OPTION_API_KEY); ?>" type="password" class="regular-text" value="<?php echo esc_attr($this->api_key()); ?>"></td>
                    </tr>
                </table>
                <p>
                    <a class="button button-secondary" href="<?php echo esc_url($accountUrl); ?>" target="_blank" rel="noopener noreferrer">
                        <?php echo esc_html($market === 'US' ? __('Create or connect a U.S. account', 'canvas-prints-ltd-print-on-demand-for-woocommerce') : __('Create or connect a Canadian account', 'canvas-prints-ltd-print-on-demand-for-woocommerce')); ?>
                    </a>
                </p>
                <p class="description"><?php esc_html_e('This is your account home market. Buyer delivery country does not switch your Canvas price list. To move the business to another market later, disconnect and create a new connection on the matching Canvas Prints site.', 'canvas-prints-ltd-print-on-demand-for-woocommerce'); ?></p>
                <?php submit_button(__('Save connection', 'canvas-prints-ltd-print-on-demand-for-woocommerce')); ?>
            </form>
            <h2><?php esc_html_e('Connection', 'canvas-prints-ltd-print-on-demand-for-woocommerce'); ?></h2>
            <pre><?php echo esc_html(wp_json_encode($status, JSON_PRETTY_PRINT)); ?></pre>
        </div>
        <?php
    }

    public function render_product_fields(): void
    {
        echo '<div class="options_group">';
        woocommerce_wp_checkbox([
            'id' => self::META_ENABLED,
            'label' => __('Canvas fulfillment', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
            'description' => __('Send this product to Canvas Prints for fulfillment.', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
        ]);
        woocommerce_wp_select([
            'id' => self::META_IMAGE_MODE,
            'label' => __('Image source', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
            'options' => [
                'customer_upload' => __('Customer uploads image', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
                'merchant_artwork' => __('Merchant artwork', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
                'both' => __('Customer upload or merchant artwork', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
            ],
        ]);
        woocommerce_wp_text_input([
            'id' => self::META_ALLOWED_SIZES,
            'label' => __('Allowed sizes', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
            'description' => __('Comma-separated sizes, for example 12x16,16x20,24x36.', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
            'desc_tip' => true,
        ]);
        woocommerce_wp_text_input([
            'id' => self::META_DEFAULT_SIZE,
            'label' => __('Default canvas size', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
            'description' => __('Used for merchant artwork when the shopper does not choose a size, for example 16x20.', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
            'desc_tip' => true,
        ]);
        woocommerce_wp_text_input([
            'id' => self::META_ARTWORK_URL,
            'label' => __('Production artwork URL', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
            'description' => __('Public HTTPS URL for the full-resolution merchant artwork. If empty, the full-size featured product image is used.', 'canvas-prints-ltd-print-on-demand-for-woocommerce'),
            'desc_tip' => true,
        ]);
        echo '</div>';
    }

    public function save_product_fields(WC_Product $product): void
    {
        $nonce = isset($_POST['woocommerce_meta_nonce'])
            ? sanitize_text_field(wp_unslash($_POST['woocommerce_meta_nonce']))
            : '';
        if (!current_user_can('edit_post', $product->get_id()) || !wp_verify_nonce($nonce, 'woocommerce_save_data')) {
            return;
        }

        $product->update_meta_data(self::META_ENABLED, isset($_POST[self::META_ENABLED]) ? 'yes' : 'no');
        $product->update_meta_data(self::META_IMAGE_MODE, sanitize_text_field(wp_unslash($_POST[self::META_IMAGE_MODE] ?? 'customer_upload')));
        $product->update_meta_data(self::META_ALLOWED_SIZES, sanitize_text_field(wp_unslash($_POST[self::META_ALLOWED_SIZES] ?? '')));
        $product->update_meta_data(self::META_DEFAULT_SIZE, sanitize_text_field(wp_unslash($_POST[self::META_DEFAULT_SIZE] ?? '')));
        $product->update_meta_data(self::META_ARTWORK_URL, esc_url_raw(wp_unslash($_POST[self::META_ARTWORK_URL] ?? '')));
    }

    public function render_upload_field(): void
    {
        global $product;

        if (!$product instanceof WC_Product || $product->get_meta(self::META_ENABLED) !== 'yes') {
            return;
        }

        $mode = (string) $product->get_meta(self::META_IMAGE_MODE);
        $sizes = $this->parse_sizes((string) $product->get_meta(self::META_ALLOWED_SIZES));
        if ($sizes !== [] && !$product->is_type('variable')) {
            echo '<p class="form-row form-row-wide">';
            echo '<label for="cplpod_selected_size">Canvas size</label>';
            echo '<select id="cplpod_selected_size" name="cplpod_selected_size">';
            foreach ($sizes as $size) {
                $value = $size['width'] . 'x' . $size['height'];
                $default = (string) $product->get_meta(self::META_DEFAULT_SIZE);
                echo '<option value="' . esc_attr($value) . '"' . selected($default, $value, false) . '>' . esc_html($value) . '</option>';
            }
            echo '</select>';
            echo '</p>';
        }

        if ($mode === 'merchant_artwork') {
            wp_nonce_field('cplpod_print_on_demand_add_to_cart', 'cplpod_add_to_cart_nonce');
            echo '<p class="cplpod-merchant-artwork-note">This product uses the artwork selected by the store owner. Choose your print size above.</p>';
            return;
        }

        wp_nonce_field('cplpod_print_on_demand_add_to_cart', 'cplpod_add_to_cart_nonce');
        echo '<p class="form-row form-row-wide">';
        echo '<label for="cplpod_customer_image">Upload image for canvas print</label>';
        echo '<input id="cplpod_customer_image" name="cplpod_customer_image" type="file" accept="image/jpeg,image/png,image/webp">';
        if ($mode === 'both') {
            echo '<small>Optional. Leave empty to use the artist\'s displayed artwork.</small>';
        }
        echo '</p>';

        echo '<div class="cplpod-preview" data-min-dpi="' . esc_attr((string) self::MIN_PRINT_DPI) . '">';
        echo '<input type="hidden" id="cplpod_crop_x" name="cplpod_crop_x" value="0">';
        echo '<input type="hidden" id="cplpod_crop_y" name="cplpod_crop_y" value="0">';
        echo '<input type="hidden" id="cplpod_crop_scale" name="cplpod_crop_scale" value="1">';
        echo '<div class="cplpod-customizer">';
        echo '<section class="cplpod-customizer__step"><h3>Canvas options</h3>';
        echo '<div class="cplpod-option-row">';
        echo '<label><input type="radio" name="cplpod_edge_style" value="mirror" checked> Mirror edge</label>';
        echo '<label><input type="radio" name="cplpod_edge_style" value="blur"> Blur edge</label>';
        echo '<label><input type="radio" name="cplpod_edge_style" value="fill"> Color edge</label>';
        echo '</div>';
        echo '<label class="cplpod-color-row">Edge color <input type="color" id="cplpod_edge_color" name="cplpod_edge_color" value="#ffffff"></label>';
        echo '<div class="cplpod-option-row">';
        echo '<label><input type="radio" name="cplpod_frame_depth" value="1.5" checked> 1.5 inch gallery wrap</label>';
        echo '<label><input type="radio" name="cplpod_frame_depth" value="0.75"> 0.75 inch thin wrap</label>';
        echo '</div></section>';
        echo '<section class="cplpod-customizer__step"><h3>Position image</h3>';
        echo '<div class="cplpod-tool-row">';
        echo '<label for="cplpod_zoom">Zoom</label>';
        echo '<input id="cplpod_zoom" type="range" min="1" max="3" step="0.01" value="1">';
        echo '<button type="button" id="cplpod_reset_crop">Center</button>';
        echo '<button type="button" id="cplpod_toggle_3d">3D Preview</button>';
        echo '</div></section>';
        echo '</div>';
        echo '<div class="cplpod-preview__stage">';
        echo '<div class="cplpod-preview__mockup" id="cplpod_preview_mockup">';
        echo '<div class="cplpod-preview__edge cplpod-preview__edge--left"></div>';
        echo '<div class="cplpod-preview__edge cplpod-preview__edge--right"></div>';
        echo '<div class="cplpod-preview__edge cplpod-preview__edge--top"></div>';
        echo '<div class="cplpod-preview__edge cplpod-preview__edge--bottom"></div>';
        echo '<div class="cplpod-preview__canvas" id="cplpod_preview_canvas">';
        echo '<img id="cplpod_preview_image" alt="" hidden draggable="false">';
        echo '<span id="cplpod_preview_placeholder">Upload an image, choose size, then drag and zoom to customize</span>';
        echo '</div>';
        echo '</div>';
        echo '</div>';
        echo '<div class="cplpod-preview__meta" id="cplpod_preview_meta" aria-live="polite"></div>';
        echo '</div>';
    }

    public function enqueue_preview_assets(): void
    {
        if (!is_product()) {
            return;
        }

        wp_register_style('cplpod-print-on-demand-preview', false, [], '1.10.0');
        wp_enqueue_style('cplpod-print-on-demand-preview');
        wp_add_inline_style('cplpod-print-on-demand-preview', $this->preview_css());

        wp_register_script('cplpod-print-on-demand-preview', false, [], '1.10.0', true);
        wp_enqueue_script('cplpod-print-on-demand-preview');
        wp_add_inline_script('cplpod-print-on-demand-preview', $this->preview_js());
    }

    public function validate_upload(bool $passed, int $product_id, int $quantity): bool
    {
        $product = wc_get_product($product_id);
        $product = $this->configuration_product($product);
        if (!$product instanceof WC_Product || $product->get_meta(self::META_ENABLED) !== 'yes') {
            return $passed;
        }

        $nonce = isset($_POST['cplpod_add_to_cart_nonce'])
            ? sanitize_text_field(wp_unslash($_POST['cplpod_add_to_cart_nonce']))
            : '';
        if (!wp_verify_nonce($nonce, 'cplpod_print_on_demand_add_to_cart')) {
            wc_add_notice('Your product form expired. Refresh the page and try again.', 'error');
            return false;
        }

        $customerUpload = $this->customer_upload_file();

        $mode = (string) $product->get_meta(self::META_IMAGE_MODE);
        if ($mode === 'merchant_artwork' || ($mode === 'both' && $customerUpload === [])) {
            if ($this->merchant_artwork_url($product) === '') {
                wc_add_notice('This artwork is not configured for printing yet.', 'error');
                return false;
            }
            return $passed;
        }

        if ($customerUpload === []) {
            wc_add_notice('Please upload an image for this canvas print.', 'error');
            return false;
        }

        $selectedSize = sanitize_text_field(wp_unslash($_POST['cplpod_selected_size'] ?? ''));
        $validation = $this->validate_image_file($customerUpload, $product, $selectedSize);

        foreach ($validation['errors'] as $error) {
            wc_add_notice($error, 'error');
        }

        foreach ($validation['warnings'] as $warning) {
            wc_add_notice($warning, 'notice');
        }

        return $passed && $validation['errors'] === [];
    }

    public function attach_cart_item_data(array $cart_item_data, int $product_id, int $variation_id): array
    {
        $product = wc_get_product($variation_id ?: $product_id);
        $variation = $variation_id ? wc_get_product($variation_id) : null;
        $product = $this->configuration_product($product);
        if (!$product instanceof WC_Product || $product->get_meta(self::META_ENABLED) !== 'yes') {
            return $cart_item_data;
        }

        $nonce = isset($_POST['cplpod_add_to_cart_nonce'])
            ? sanitize_text_field(wp_unslash($_POST['cplpod_add_to_cart_nonce']))
            : '';
        if (!wp_verify_nonce($nonce, 'cplpod_print_on_demand_add_to_cart')) {
            return $cart_item_data;
        }

        $customerUpload = $this->customer_upload_file();

        $selectedSize = sanitize_text_field(wp_unslash($_POST['cplpod_selected_size'] ?? $_POST['attribute_canvas-size'] ?? ''));
        if ($selectedSize === '' && $variation instanceof WC_Product_Variation) {
            $selectedSize = (string) $variation->get_meta('_cplpod_size');
        }
        if ($selectedSize === '') {
            $selectedSize = (string) $product->get_meta(self::META_DEFAULT_SIZE);
        }
        if ($selectedSize === '') {
            $sizes = $this->parse_sizes((string) $product->get_meta(self::META_ALLOWED_SIZES));
            $selectedSize = isset($sizes[0]) ? $sizes[0]['width'] . 'x' . $sizes[0]['height'] : '';
        }

        $mode = (string) $product->get_meta(self::META_IMAGE_MODE);
        $selectedStyle = (string) (($variation instanceof WC_Product_Variation ? $variation : $product)->get_meta('_cplpod_style')) ?: 'stretched_standard';
        $variationEdgeStyle = (string) (($variation instanceof WC_Product_Variation ? $variation : $product)->get_meta('_cplpod_edge_style'));
        $variationEdgeColor = (string) (($variation instanceof WC_Product_Variation ? $variation : $product)->get_meta('_cplpod_edge_color'));
        if ($customerUpload === [] && in_array($mode, ['merchant_artwork', 'both'], true)) {
            $artworkProduct = $variation instanceof WC_Product_Variation && (string) $variation->get_meta(self::META_ARTWORK_URL) !== '' ? $variation : $product;
            $artwork = $this->merchant_artwork_data($artworkProduct);
            if ($artwork['url'] !== '') {
                $cart_item_data['cplpod_customer_image_url'] = $artwork['url'];
                $cart_item_data['cplpod_selected_size'] = $selectedSize;
                $cart_item_data['cplpod_image_width'] = $artwork['width'];
                $cart_item_data['cplpod_image_height'] = $artwork['height'];
                $cart_item_data['cplpod_edge_style'] = in_array($variationEdgeStyle, ['mirror', 'blur', 'image', 'color'], true) ? $variationEdgeStyle : 'mirror';
                $cart_item_data['cplpod_edge_color'] = sanitize_hex_color($variationEdgeColor) ?: '#ffffff';
                $cart_item_data['cplpod_frame_depth'] = '1.5';
                $cart_item_data['cplpod_crop_x'] = '0';
                $cart_item_data['cplpod_crop_y'] = '0';
                $cart_item_data['cplpod_crop_scale'] = '1';
                $cart_item_data['cplpod_style'] = $selectedStyle ?: 'stretched_standard';
            }
            return $cart_item_data;
        }

        if ($customerUpload === []) {
            return $cart_item_data;
        }

        require_once ABSPATH . 'wp-admin/includes/file.php';
        $upload = wp_handle_upload($customerUpload, ['test_form' => false]);

        if (!empty($upload['url'])) {
            $dimensions = $this->read_image_dimensions($customerUpload['tmp_name']);

            $cart_item_data['cplpod_customer_image_url'] = esc_url_raw($upload['url']);
            $cart_item_data['cplpod_selected_size'] = $selectedSize;
            $cart_item_data['cplpod_image_width'] = $dimensions['width'] ?? null;
            $cart_item_data['cplpod_image_height'] = $dimensions['height'] ?? null;
            $postedEdgeStyle = sanitize_text_field(wp_unslash($_POST['cplpod_edge_style'] ?? 'mirror'));
            $cart_item_data['cplpod_edge_style'] = in_array($variationEdgeStyle, ['mirror', 'blur', 'image', 'color'], true) ? $variationEdgeStyle : $postedEdgeStyle;
            $cart_item_data['cplpod_edge_color'] = sanitize_hex_color($variationEdgeColor) ?: (sanitize_hex_color(wp_unslash($_POST['cplpod_edge_color'] ?? '#ffffff')) ?: '#ffffff');
            $cart_item_data['cplpod_frame_depth'] = sanitize_text_field(wp_unslash($_POST['cplpod_frame_depth'] ?? '1.5'));
            $cart_item_data['cplpod_crop_x'] = sanitize_text_field(wp_unslash($_POST['cplpod_crop_x'] ?? '0'));
            $cart_item_data['cplpod_crop_y'] = sanitize_text_field(wp_unslash($_POST['cplpod_crop_y'] ?? '0'));
            $cart_item_data['cplpod_crop_scale'] = sanitize_text_field(wp_unslash($_POST['cplpod_crop_scale'] ?? '1'));
            $cart_item_data['cplpod_style'] = $selectedStyle ?: 'stretched_standard';
            $cart_item_data['unique_key'] = md5($upload['url'] . microtime(true));
        }

        return $cart_item_data;
    }

    /**
     * Return only the expected, normalized fields from the customer upload.
     *
     * @return array{name:string,type:string,tmp_name:string,error:int,size:int}|array{}
     */
    private function customer_upload_file(): array
    {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Both callers verify the add-to-cart nonce; each expected file field is normalized below.
        if (!isset($_FILES['cplpod_customer_image']) || !is_array($_FILES['cplpod_customer_image'])) {
            return [];
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Both callers verify the add-to-cart nonce; each expected file field is normalized below.
        $source = $_FILES['cplpod_customer_image'];
        $name = isset($source['name']) ? sanitize_file_name(wp_unslash($source['name'])) : '';
        if ($name === '') {
            return [];
        }

        return [
            'name' => $name,
            'type' => isset($source['type']) ? sanitize_mime_type(wp_unslash($source['type'])) : '',
            'tmp_name' => isset($source['tmp_name']) ? wp_normalize_path(sanitize_text_field(wp_unslash($source['tmp_name']))) : '',
            'error' => isset($source['error']) ? absint($source['error']) : UPLOAD_ERR_NO_FILE,
            'size' => isset($source['size']) ? absint($source['size']) : 0,
        ];
    }

    public function attach_order_line_meta(WC_Order_Item_Product $item, string $cart_item_key, array $values, WC_Order $order): void
    {
        if (!empty($values['cplpod_customer_image_url'])) {
            $item->add_meta_data('Canvas image URL', esc_url_raw($values['cplpod_customer_image_url']), true);
        }

        if (!empty($values['cplpod_selected_size'])) {
            $item->add_meta_data('Canvas selected size', sanitize_text_field($values['cplpod_selected_size']), true);
        }

        foreach (['edge_style', 'edge_color', 'frame_depth', 'crop_x', 'crop_y', 'crop_scale'] as $field) {
            $key = 'cplpod_' . $field;
            if (isset($values[$key])) {
                $item->add_meta_data('Canvas ' . str_replace('_', ' ', $field), sanitize_text_field((string) $values[$key]), true);
            }
        }

        if (!empty($values['cplpod_image_width']) && !empty($values['cplpod_image_height'])) {
            $item->add_meta_data('Canvas image width', (int) $values['cplpod_image_width'], true);
            $item->add_meta_data('Canvas image height', (int) $values['cplpod_image_height'], true);
        }

        $product = $item->get_product();
        $product = $this->configuration_product($product);
        if ($product instanceof WC_Product && $product->get_meta(self::META_ENABLED) === 'yes') {
            $item->add_meta_data('_cplpod_fulfillment_enabled', 'yes', true);
            $item->add_meta_data('_cplpod_allowed_sizes', (string) $product->get_meta(self::META_ALLOWED_SIZES), true);
            $item->add_meta_data('_cplpod_image_mode', (string) $product->get_meta(self::META_IMAGE_MODE), true);
            $item->add_meta_data('Canvas style', sanitize_text_field((string) ($values['cplpod_style'] ?? 'stretched_standard')), true);
        }
    }

    public function canvas_shipping_rates(array $rates, array $package): array
    {
        $hasCanvasProduct = false;
        foreach ((array) ($package['contents'] ?? []) as $cartItem) {
            $product = $this->configuration_product($cartItem['data'] ?? null);
            if ($product instanceof WC_Product && $product->get_meta(self::META_ENABLED) === 'yes') {
                $hasCanvasProduct = true;
                break;
            }
        }
        if (!$hasCanvasProduct) {
            return $rates;
        }

        $destination = (array) ($package['destination'] ?? []);
        $country = strtoupper((string) ($destination['country'] ?? ''));
        $state = strtoupper((string) ($destination['state'] ?? ''));
        if (!in_array($country, ['CA', 'US'], true)) {
            return [];
        }

        $context = $this->currency_context();
        $rate = (float) ($context['rate'] ?? 0);
        if ($rate <= 0 && get_woocommerce_currency() === 'CAD') $rate = 1.0;
        $canvasRates = [
            'cplpod_standard' => new WC_Shipping_Rate('cplpod_standard', 'Free standard delivery', 0, [], 'canvas_printing'),
        ];
        $remoteCanadianTerritory = $country === 'CA' && in_array($state, ['NT', 'NU', 'YT'], true);
        if (!$remoteCanadianTerritory && $rate > 0) {
            $canvasRates['cplpod_expedited'] = new WC_Shipping_Rate('cplpod_expedited', 'Expedited delivery', round(11 * $rate, 2), [], 'canvas_printing');
            $canvasRates['cplpod_express'] = new WC_Shipping_Rate('cplpod_express', 'Express delivery', round(29 * $rate, 2), [], 'canvas_printing');
        }

        return $canvasRates;
    }

    public function send_order_to_fulfillment(int $order_id): void
    {
        $order = wc_get_order($order_id);
        if (!$order instanceof WC_Order || $order->get_meta(self::ORDER_META_SENT)) {
            return;
        }

        $items = [];
        foreach ($order->get_items() as $line_item) {
            if ($line_item->get_meta('_cplpod_fulfillment_enabled') !== 'yes') {
                continue;
            }

            [$width, $height] = $this->size_from_meta((string) $line_item->get_meta('Canvas selected size'), (string) $line_item->get_meta('_cplpod_allowed_sizes'));
            $items[] = [
                'external_line_id' => (string) $line_item->get_id(),
                'product_id' => 'canvas_print',
                'style' => (string) ($line_item->get_meta('Canvas style') ?: 'stretched_standard'),
                'width' => $width,
                'height' => $height,
                'quantity' => $line_item->get_quantity(),
                'image_url' => (string) $line_item->get_meta('Canvas image URL'),
                'image_width' => (int) $line_item->get_meta('Canvas image width'),
                'image_height' => (int) $line_item->get_meta('Canvas image height'),
                'customization' => [
                    'edge_style' => (string) $line_item->get_meta('Canvas edge style'),
                    'edge_color' => (string) $line_item->get_meta('Canvas edge color'),
                    'frame_depth' => (string) $line_item->get_meta('Canvas frame depth'),
                    'crop_x' => (float) $line_item->get_meta('Canvas crop x'),
                    'crop_y' => (float) $line_item->get_meta('Canvas crop y'),
                    'crop_scale' => (float) $line_item->get_meta('Canvas crop scale'),
                ],
            ];
        }

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

        $response = $this->request('POST', '/orders', [
            'external_order_id' => (string) $order->get_id(),
            'store_order_number' => $order->get_order_number(),
            'currency' => $order->get_currency(),
            'shipping_address' => array_replace($order->get_address('shipping'), [
                'email' => $order->get_billing_email(),
                'phone' => $order->get_shipping_phone() ?: $order->get_billing_phone(),
            ]),
            'billing_address' => $order->get_address('billing'),
            'shipping_service' => $this->canvas_shipping_service($order),
            'items' => $items,
        ]);

        if (!empty($response['id'])) {
            $order->update_meta_data(self::ORDER_META_SENT, sanitize_text_field($response['id']));
            $status = sanitize_text_field((string) ($response['status'] ?? 'created'));
            $order->update_meta_data(self::ORDER_META_STATUS, $status);
            $hold = !empty($response['production_hold']) ? '; production on hold' : '; released for production intake';
            $order->add_order_note('Canvas fulfillment order created: ' . sanitize_text_field($response['id']) . ' (' . $status . $hold . ').');
            $order->save();
        } elseif (!empty($response['error'])) {
            $order->add_order_note('Canvas fulfillment error: ' . sanitize_text_field($response['error']));
        }
    }

    public function render_order_fulfillment_status(WC_Order $order): void
    {
        $id = (string) $order->get_meta(self::ORDER_META_SENT);
        if ($id === '') {
            return;
        }
        $status = (string) $order->get_meta(self::ORDER_META_STATUS);
        echo '<p><strong>Canvas fulfillment:</strong> ' . esc_html($id) . ($status !== '' ? ' &mdash; ' . esc_html(str_replace('_', ' ', $status)) : '') . '</p>';
    }

    public function register_canvas_push_routes(): void
    {
        register_rest_route('canvas-fulfillment/v1', '/products', [
            'methods' => 'POST',
            'callback' => [$this, 'receive_canvas_product'],
            'permission_callback' => [$this, 'authorize_canvas_push'],
        ]);
        register_rest_route('canvas-fulfillment/v1', '/products', [
            'methods' => 'GET',
            'callback' => [$this, 'list_existing_products'],
            'permission_callback' => [$this, 'authorize_canvas_push'],
        ]);
        register_rest_route('canvas-fulfillment/v1', '/products/(?P<id>\d+)/link', [
            'methods' => 'POST',
            'callback' => [$this, 'link_existing_product'],
            'permission_callback' => [$this, 'authorize_canvas_push'],
            'args' => ['id' => ['validate_callback' => static fn ($value): bool => absint($value) > 0]],
        ]);
        register_rest_route('canvas-fulfillment/v1', '/products/(?P<id>\d+)/unlink', [
            'methods' => 'POST',
            'callback' => [$this, 'unlink_existing_product'],
            'permission_callback' => [$this, 'authorize_canvas_push'],
            'args' => ['id' => ['validate_callback' => static fn ($value): bool => absint($value) > 0]],
        ]);
        register_rest_route('canvas-fulfillment/v1', '/orders/(?P<id>\d+)/tracking', [
            'methods' => 'POST',
            'callback' => [$this, 'receive_canvas_tracking'],
            'permission_callback' => [$this, 'authorize_canvas_push'],
            'args' => ['id' => ['validate_callback' => static fn ($value): bool => absint($value) > 0]],
        ]);
    }

    public function receive_canvas_tracking(WP_REST_Request $request): WP_REST_Response
    {
        $order = wc_get_order(absint($request['id']));
        if (!$order instanceof WC_Order || !$order->get_meta(self::ORDER_META_SENT)) {
            return new WP_REST_Response(['ok' => false, 'message' => 'WooCommerce fulfillment order not found.'], 404);
        }

        $payload = (array) $request->get_json_params();
        $number = sanitize_text_field((string) ($payload['tracking_number'] ?? ''));
        $carrier = sanitize_text_field((string) ($payload['carrier'] ?? ''));
        $url = esc_url_raw((string) ($payload['tracking_url'] ?? ''));
        if ($number === '' && $url === '') {
            return new WP_REST_Response(['ok' => false, 'message' => 'A tracking number or tracking URL is required.'], 400);
        }

        $hash = hash('sha256', $number . '|' . $carrier . '|' . $url);
        $alreadyNotified = hash_equals((string) $order->get_meta(self::ORDER_META_TRACKING_NOTIFIED), $hash);
        $order->update_meta_data(self::ORDER_META_STATUS, 'shipped');
        $order->update_meta_data(self::ORDER_META_TRACKING_NUMBER, $number);
        $order->update_meta_data(self::ORDER_META_TRACKING_CARRIER, $carrier);
        $order->update_meta_data(self::ORDER_META_TRACKING_URL, $url);

        if (!$alreadyNotified) {
            $order->update_meta_data(self::ORDER_META_TRACKING_NOTIFIED, $hash);
            $message = 'Your order has shipped.';
            if ($carrier !== '') $message .= ' Carrier: ' . $carrier . '.';
            if ($number !== '') $message .= ' Tracking number: ' . $number . '.';
            if ($url !== '') $message .= ' Track your shipment: ' . $url;
            $completedEmailEnabled = $this->woocommerce_email_enabled('WC_Email_Customer_Completed_Order');
            if (!$order->has_status('completed')) {
                $order->update_status('completed', $message, true);
                if (!$completedEmailEnabled) {
                    $order->add_order_note($message, true, true);
                }
            } else {
                $order->add_order_note($message, true, true);
            }
        }
        $order->save();

        return new WP_REST_Response(['ok' => true, 'notified' => !$alreadyNotified, 'order_id' => $order->get_id()]);
    }

    public function list_existing_products(): WP_REST_Response
    {
        $products = [];
        foreach (wc_get_products(['limit' => 100, 'status' => ['publish', 'draft', 'private'], 'orderby' => 'modified', 'order' => 'DESC']) as $product) {
            if (!$product instanceof WC_Product) continue;
            $variants = [];
            if ($product instanceof WC_Product_Variable) {
                foreach ($product->get_children() as $childId) {
                    $variation = wc_get_product($childId);
                    if (!$variation instanceof WC_Product_Variation) continue;
                    $labels = [];
                    foreach ($variation->get_attributes() as $name => $value) $labels[] = wc_attribute_label($name) . ': ' . $value;
                    $variants[] = ['id' => (string) $variation->get_id(), 'title' => $labels ? implode(' / ', $labels) : 'Variation #' . $variation->get_id(), 'sku' => $variation->get_sku(), 'price' => (float) $variation->get_price(), 'options' => []];
                }
            } else {
                $variants[] = ['id' => (string) $product->get_id(), 'title' => 'Default', 'sku' => $product->get_sku(), 'price' => (float) $product->get_price(), 'options' => []];
            }
            $products[] = [
                'id' => (string) $product->get_id(), 'title' => $product->get_name(), 'status' => $product->get_status(),
                'image' => $product->get_image_id() ? (string) wp_get_attachment_image_url($product->get_image_id(), 'thumbnail') : '',
                'edit_url' => admin_url('post.php?post=' . $product->get_id() . '&action=edit'), 'variants' => $variants,
            ];
        }
        return new WP_REST_Response(['ok' => true, 'products' => $products], 200);
    }

    public function link_existing_product(WP_REST_Request $request): WP_REST_Response
    {
        try {
            $product = wc_get_product(absint($request['id']));
            if (!$product instanceof WC_Product) throw new RuntimeException('WooCommerce product not found.');
            $rows = array_values((array) (($request->get_json_params()['variants'] ?? [])));
            if ($rows === []) throw new RuntimeException('No product variants were mapped.');
            $allowedSizes = []; $defaultSize = '';
            foreach ($rows as $row) {
                $variantId = absint($row['external_variant_id'] ?? 0);
                $target = $variantId === $product->get_id() ? $product : wc_get_product($variantId);
                if (!$target instanceof WC_Product || ($target instanceof WC_Product_Variation && $target->get_parent_id() !== $product->get_id())) throw new RuntimeException('A selected WooCommerce variation does not belong to this product.');
                $size = sanitize_text_field((string) ($row['size'] ?? ''));
                if (!preg_match('/^\d+x\d+$/', $size)) throw new RuntimeException('A mapped Canvas size is invalid.');
                $artworkUrl = esc_url_raw((string) ($row['artwork_url'] ?? ''));
                if ($artworkUrl === '') throw new RuntimeException('Production artwork is missing for ' . $size . '.');
                $artworkId = $this->import_canvas_image($artworkUrl, $product->get_name() . ' ' . $size . ' production artwork');
                $target->update_meta_data('_cplpod_size', $size);
                $target->update_meta_data('_cplpod_style', 'stretched_standard');
                $target->update_meta_data('_cplpod_edge_style', sanitize_text_field((string) ($row['wrap'] ?? 'mirror')));
                $target->update_meta_data('_cplpod_edge_color', sanitize_hex_color((string) ($row['wrap_color'] ?? '')) ?: '#ffffff');
                $target->update_meta_data(self::META_ARTWORK_URL, (string) wp_get_attachment_url($artworkId));
                $target->update_meta_data('_cplpod_artwork_attachment_id', $artworkId);
                $target->save();
                $allowedSizes[] = $size; if ($defaultSize === '') $defaultSize = $size;
            }
            $product->update_meta_data(self::META_ENABLED, 'yes');
            $product->update_meta_data(self::META_IMAGE_MODE, 'merchant_artwork');
            $product->update_meta_data(self::META_ALLOWED_SIZES, implode(',', array_values(array_unique($allowedSizes))));
            $product->update_meta_data(self::META_DEFAULT_SIZE, $defaultSize);
            $product->save();
            return new WP_REST_Response(['ok' => true, 'product_id' => $product->get_id(), 'linked_variants' => count($rows)], 200);
        } catch (Throwable $e) {
            return new WP_REST_Response(['ok' => false, 'message' => $e->getMessage()], 422);
        }
    }

    public function unlink_existing_product(WP_REST_Request $request): WP_REST_Response
    {
        $product = wc_get_product(absint($request['id']));
        if (!$product instanceof WC_Product) {
            return new WP_REST_Response(['ok' => false, 'message' => 'WooCommerce product not found.'], 404);
        }
        $product->update_meta_data(self::META_ENABLED, 'no');
        $product->save();
        return new WP_REST_Response(['ok' => true, 'product_id' => $product->get_id()], 200);
    }

    public function render_tracking_in_email(WC_Order $order, bool $sentToAdmin, bool $plainText, $email): void
    {
        if ($sentToAdmin) return;
        $number = (string) $order->get_meta(self::ORDER_META_TRACKING_NUMBER);
        $carrier = (string) $order->get_meta(self::ORDER_META_TRACKING_CARRIER);
        $url = (string) $order->get_meta(self::ORDER_META_TRACKING_URL);
        if ($number === '' && $url === '') return;

        if ($plainText) {
            echo "\nTRACKING\n";
            if ($carrier !== '') echo 'Carrier: ' . esc_html($carrier) . "\n";
            if ($number !== '') echo 'Tracking number: ' . esc_html($number) . "\n";
            if ($url !== '') echo 'Track shipment: ' . esc_url($url) . "\n";
            return;
        }
        echo '<h2>Tracking</h2><p>';
        if ($carrier !== '') echo '<strong>Carrier:</strong> ' . esc_html($carrier) . '<br>';
        if ($number !== '') echo '<strong>Tracking number:</strong> ' . esc_html($number) . '<br>';
        if ($url !== '') echo '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Track your shipment', 'canvas-prints-ltd-print-on-demand-for-woocommerce') . '</a>';
        echo '</p>';
    }

    private function canvas_shipping_service(WC_Order $order): string
    {
        foreach ($order->get_items('shipping') as $shippingItem) {
            $methodId = strtolower((string) $shippingItem->get_method_id());
            if (str_contains($methodId, 'cplpod_express')) return 'express';
            if (str_contains($methodId, 'cplpod_expedited')) return 'expedited';
            if (str_contains($methodId, 'cplpod_standard')) return 'free';
        }
        $label = strtolower($order->get_shipping_method());
        if (str_contains($label, 'express')) return 'express';
        if (str_contains($label, 'expedited')) return 'expedited';
        return 'free';
    }

    private function woocommerce_email_enabled(string $className): bool
    {
        $emails = WC()->mailer()->get_emails();
        return isset($emails[$className]) && $emails[$className]->is_enabled();
    }

    public function authorize_canvas_push(WP_REST_Request $request)
    {
        $stored = $this->api_key();
        $provided = (string) $request->get_header('X-Canvas-Api-Key');
        $store = rtrim(strtolower((string) $request->get_header('X-Canvas-Store-Url')), '/');
        $home = rtrim(strtolower(home_url('/')), '/');
        if ($stored === '' || $provided === '' || !hash_equals($stored, $provided) || $store !== $home) {
            return new WP_Error('cplpod_print_on_demand_unauthorized', 'Canvas product publishing authorization failed.', ['status' => 401]);
        }
        return true;
    }

    public function receive_canvas_product(WP_REST_Request $request): WP_REST_Response
    {
        try {
            $payload = $request->get_json_params();
            if (!is_array($payload) || empty($payload['template_id']) || empty($payload['title']) || empty($payload['artwork_url']) || empty($payload['variants'])) {
                throw new RuntimeException('Canvas sent an incomplete product.');
            }
            $payloadCurrency = strtoupper(sanitize_text_field((string) ($payload['currency'] ?? '')));
            $storeCurrency = strtoupper((string) get_woocommerce_currency());
            if ($payloadCurrency === '' || $payloadCurrency !== $storeCurrency) {
                throw new RuntimeException('Product pricing currency does not match this WooCommerce store. Reconnect the store and publish again.');
            }
            $templateId = absint($payload['template_id']);
            $existing = get_posts([
                'post_type' => 'product',
                // `any` excludes Trash. Include it explicitly so publishing is
                // idempotent even when the merchant previously trashed a draft.
                'post_status' => ['publish', 'pending', 'draft', 'auto-draft', 'future', 'private', 'inherit', 'trash'],
                'numberposts' => 1, 'fields' => 'ids',
                'meta_key' => '_cplpod_canvas_template_id', 'meta_value' => (string) $templateId,
            ]);
            $variantRows = array_values((array) $payload['variants']);
            $sizeOptions = array_values(array_unique(array_map(static fn ($variant): string => sanitize_text_field((string) ($variant['size'] ?? '')), $variantRows)));
            $styleOptions = array_values(array_unique(array_map(static fn ($variant): string => sanitize_text_field((string) ($variant['style_name'] ?? '')), $variantRows)));
            $varySize = count($sizeOptions) > 1;
            $varyStyle = count($styleOptions) > 1;
            $isVariable = $varySize || $varyStyle;
            $existingId = $existing ? (int) $existing[0] : 0;
            $oldProduct = $existingId ? wc_get_product($existingId) : null;
            if ($oldProduct instanceof WC_Product_Variable) {
                foreach ($oldProduct->get_children() as $childId) {
                    $child = wc_get_product($childId);
                    if ($child) $child->delete(true);
                }
            }
            if ($existingId) {
                wp_set_object_terms($existingId, $isVariable ? 'variable' : 'simple', 'product_type');
                $product = $isVariable ? new WC_Product_Variable($existingId) : new WC_Product_Simple($existingId);
            } else {
                $product = $isVariable ? new WC_Product_Variable() : new WC_Product_Simple();
            }

            $artworkId = $this->import_canvas_image((string) $payload['artwork_url'], sanitize_text_field((string) $payload['title']) . ' production artwork');
            $publicationStatus = ($payload['status'] ?? 'draft') === 'publish' ? 'publish' : 'draft';
            $product->set_name(sanitize_text_field((string) $payload['title']));
            $product->set_status($publicationStatus);
            $product->set_catalog_visibility('visible');
            $description = wp_kses_post((string) ($payload['description'] ?? ''));
            $fixedDetails = [];
            if (!$varySize && isset($sizeOptions[0])) $fixedDetails[] = '<li><strong>Canvas size:</strong> ' . esc_html($sizeOptions[0]) . ' in</li>';
            if (!$varyStyle && isset($styleOptions[0])) $fixedDetails[] = '<li><strong>Canvas style:</strong> ' . esc_html($styleOptions[0]) . '</li>';
            $fixedDetails[] = '<li><strong>Frame depth:</strong> 1.5 in</li>';
            $description .= '<h3>Canvas details</h3><ul>' . implode('', $fixedDetails) . '</ul>';
            $product->set_description($description);
            $artist = sanitize_text_field((string) ($payload['artist'] ?? ''));
            if ($artist !== '') $product->set_short_description('Artwork by ' . esc_html($artist) . '. Printed to order on premium canvas.');
            if (!$product->get_sku()) $product->set_sku($this->unique_canvas_sku((string) $payload['title']));
            $product->update_meta_data('_cplpod_canvas_template_id', $templateId);
            $product->update_meta_data(self::META_ENABLED, 'yes');
            $product->update_meta_data(self::META_IMAGE_MODE, 'merchant_artwork');
            $product->update_meta_data(self::META_ARTWORK_URL, (string) wp_get_attachment_url($artworkId));
            $product->update_meta_data('_cplpod_artwork_attachment_id', $artworkId);
            $product->update_meta_data(self::META_ALLOWED_SIZES, implode(',', array_map('sanitize_text_field', (array) ($payload['sizes'] ?? []))));
            $product->update_meta_data(self::META_DEFAULT_SIZE, sanitize_text_field((string) (($payload['sizes'][0] ?? ''))));

            $attributes = [];
            if ($varySize) {
                $sizeAttribute = new WC_Product_Attribute();
                $sizeAttribute->set_id(0); $sizeAttribute->set_name('Canvas size'); $sizeAttribute->set_options($sizeOptions); $sizeAttribute->set_position(0); $sizeAttribute->set_visible(true); $sizeAttribute->set_variation(true);
                $attributes[] = $sizeAttribute;
            }
            if ($varyStyle) {
                $styleAttribute = new WC_Product_Attribute();
                $styleAttribute->set_id(0); $styleAttribute->set_name('Canvas style'); $styleAttribute->set_options($styleOptions); $styleAttribute->set_position(1); $styleAttribute->set_visible(true); $styleAttribute->set_variation(true);
                $attributes[] = $styleAttribute;
            }
            $product->set_attributes($attributes);
            if (!$isVariable) {
                $singleVariant = $variantRows[0];
                $retail = wc_format_decimal((string) ($singleVariant['retail_price'] ?? '0'));
                $product->set_regular_price($retail); $product->set_price($retail); $product->set_stock_status('instock');
                $product->update_meta_data('_cplpod_size', sanitize_text_field((string) ($singleVariant['size'] ?? '')));
                $product->update_meta_data('_cplpod_style', sanitize_text_field((string) ($singleVariant['style_id'] ?? '')));
                $product->update_meta_data('_cplpod_edge_style', sanitize_text_field((string) ($singleVariant['wrap'] ?? 'mirror')));
                $product->update_meta_data('_cplpod_edge_color', sanitize_hex_color((string) ($singleVariant['wrap_color'] ?? '')) ?: '#ffffff');
                $product->update_meta_data('_cplpod_merchant_cost_cad', wc_format_decimal((string) ($singleVariant['canvas_cost_cad'] ?? $singleVariant['canvas_cost'] ?? '0')));
            }
            $productId = $product->save();

            $variantArtworkIds = [(string) $payload['artwork_url'] => $artworkId];
            foreach ($isVariable ? $variantRows : [] as $variantData) {
                $variation = new WC_Product_Variation();
                $variation->set_parent_id($productId); $variation->set_status('publish');
                $retail = wc_format_decimal((string) ($variantData['retail_price'] ?? '0'));
                $variation->set_regular_price($retail); $variation->set_price($retail); $variation->set_stock_status('instock');
                $variationAttributes = [];
                if ($varySize) $variationAttributes['canvas-size'] = sanitize_text_field((string) $variantData['size']);
                if ($varyStyle) $variationAttributes['canvas-style'] = sanitize_text_field((string) $variantData['style_name']);
                $variation->set_attributes($variationAttributes);
                $variation->update_meta_data('_cplpod_size', sanitize_text_field((string) $variantData['size']));
                $variation->update_meta_data('_cplpod_style', sanitize_text_field((string) $variantData['style_id']));
                $variation->update_meta_data('_cplpod_edge_style', sanitize_text_field((string) ($variantData['wrap'] ?? 'mirror')));
                $variation->update_meta_data('_cplpod_edge_color', sanitize_hex_color((string) ($variantData['wrap_color'] ?? '')) ?: '#ffffff');
                $variation->update_meta_data('_cplpod_merchant_cost_cad', wc_format_decimal((string) ($variantData['canvas_cost_cad'] ?? $variantData['canvas_cost'] ?? '0')));
                $variantArtworkUrl = esc_url_raw((string) ($variantData['artwork_url'] ?? ''));
                if ($variantArtworkUrl !== '' && $variantArtworkUrl !== (string) $payload['artwork_url']) {
                    if (!isset($variantArtworkIds[$variantArtworkUrl])) $variantArtworkIds[$variantArtworkUrl] = $this->import_canvas_image($variantArtworkUrl, sanitize_text_field((string) $payload['title']) . ' ' . sanitize_text_field((string) $variantData['size']) . ' production artwork');
                    $variantArtworkId = (int) $variantArtworkIds[$variantArtworkUrl];
                    $variation->update_meta_data(self::META_ARTWORK_URL, (string) wp_get_attachment_url($variantArtworkId));
                    $variation->update_meta_data('_cplpod_artwork_attachment_id', $variantArtworkId);
                }
                $variantImageUrl = esc_url_raw((string) ($variantData['variant_image_url'] ?? ''));
                if ($variantImageUrl !== '') {
                    if (!isset($variantArtworkIds[$variantImageUrl])) $variantArtworkIds[$variantImageUrl] = $this->import_canvas_image($variantImageUrl, sanitize_text_field((string) $payload['title']) . ' ' . sanitize_text_field((string) ($variantData['ratio'] ?? '')) . ' variant image', $productId);
                    $variation->set_image_id((int) $variantArtworkIds[$variantImageUrl]);
                }
                $variation->save();
            }

            $mockupIds = [];
            foreach ((array) ($payload['mockup_urls'] ?? []) as $index => $mockupUrl) {
                $cleanMockupUrl = esc_url_raw((string) $mockupUrl);
                if ($cleanMockupUrl === '') continue;
                if (!isset($variantArtworkIds[$cleanMockupUrl])) {
                    $variantArtworkIds[$cleanMockupUrl] = $this->import_canvas_image($cleanMockupUrl, sanitize_text_field((string) $payload['title']) . ' mockup ' . ($index + 1), $productId);
                }
                $mockupIds[] = (int) $variantArtworkIds[$cleanMockupUrl];
            }
            if ($mockupIds !== []) {
                $product = wc_get_product($productId); $product->set_image_id($mockupIds[0]); $product->set_gallery_image_ids(array_slice($mockupIds, 1)); $product->save();
            }
            wc_delete_product_transients($productId);
            return new WP_REST_Response(['ok' => true, 'product_id' => $productId, 'status' => $publicationStatus, 'variations' => $isVariable ? count($variantRows) : 0, 'edit_url' => admin_url('post.php?post=' . $productId . '&action=edit')], 201);
        } catch (Throwable $e) {
            return new WP_REST_Response(['ok' => false, 'message' => $e->getMessage()], 422);
        }
    }

    private function import_canvas_image(string $url, string $title, int $parentId = 0): int
    {
        if (!str_starts_with($url, 'https://')) throw new RuntimeException('Canvas image URL must use HTTPS.');
        require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/image.php';
        $temporary = download_url($url, 45);
        if (is_wp_error($temporary)) throw new RuntimeException('WooCommerce could not download a Canvas image: ' . esc_html($temporary->get_error_message()));
        $path = (string) wp_parse_url($url, PHP_URL_PATH); $query = []; parse_str((string) wp_parse_url($url, PHP_URL_QUERY), $query);
        $extension = strtolower(pathinfo((string) ($query['filename'] ?? $path), PATHINFO_EXTENSION));
        if (!in_array($extension, ['jpg', 'jpeg', 'png', 'webp'], true)) $extension = 'jpg';
        $file = ['name' => sanitize_file_name($title) . '.' . $extension, 'tmp_name' => $temporary];
        $attachmentId = media_handle_sideload($file, $parentId, $title);
        if (is_wp_error($attachmentId)) { wp_delete_file($temporary); throw new RuntimeException('WooCommerce could not import a Canvas image: ' . esc_html($attachmentId->get_error_message())); }
        return (int) $attachmentId;
    }

    private function unique_canvas_sku(string $title): string
    {
        $base = trim('ART-' . strtoupper(substr(preg_replace('/[^A-Za-z0-9]+/', '-', $title), 0, 28)), '-');
        $candidate = $base; $index = 2;
        while (wc_get_product_id_by_sku($candidate)) {
            $candidate = $base . '-' . $index++;
        }
        return $candidate;
    }

    private function request(string $method, string $path, array $body = []): array
    {
        $url = rtrim($this->api_url(), '/') . $path;
        $args = [
            'method' => $method,
            'timeout' => 55,
            'headers' => [
                'Content-Type' => 'application/json',
                'X-Canvas-Api-Key' => $this->api_key(),
                'X-Canvas-Store-Url' => home_url('/'),
                'X-Canvas-Store-Currency' => get_woocommerce_currency(),
            ],
        ];

        if ($body !== []) {
            $args['body'] = wp_json_encode($body);
        }

        $response = wp_remote_request($url, $args);
        if (is_wp_error($response)) {
            return ['error' => $response->get_error_message()];
        }

        $decoded = json_decode((string) wp_remote_retrieve_body($response), true);

        return is_array($decoded) ? $decoded : ['error' => 'Invalid API response.'];
    }

    private function currency_context(): array
    {
        $currency = get_woocommerce_currency();
        if ($currency === 'CAD') return ['currency' => 'CAD', 'rate' => 1.0];
        $cached = get_transient(self::CURRENCY_TRANSIENT);
        if (is_array($cached) && ($cached['currency'] ?? '') === $currency && (float) ($cached['rate'] ?? 0) > 0) return $cached;
        $status = $this->request('GET', '/status');
        $exchange = is_array($status['exchange_rate'] ?? null) ? $status['exchange_rate'] : [];
        if (($exchange['currency'] ?? '') === $currency && (float) ($exchange['rate'] ?? 0) > 0) {
            set_transient(self::CURRENCY_TRANSIENT, $exchange, 12 * HOUR_IN_SECONDS);
            return $exchange;
        }
        return [];
    }

    private function api_url(): string
    {
        $url = (string) get_option(self::OPTION_API_URL, '');
        if ($url === '') {
            $url = (string) get_option(
                self::LEGACY_OPTION_API_URL,
                get_option(
                    self::ORIGINAL_OPTION_API_URL,
                    self::OPTION_API_URL_DEFAULT
                )
            );
        }
        $host = strtolower((string) wp_parse_url($url, PHP_URL_HOST));
        if (!in_array($host, ['canvasprintscalgary.com', 'www.canvasprintscalgary.com', 'canvasprintsusa.com', 'www.canvasprintsusa.com'], true)) {
            return $this->market() === 'US' ? self::OPTION_API_URL_US : self::OPTION_API_URL_DEFAULT;
        }

        return in_array($host, ['canvasprintsusa.com', 'www.canvasprintsusa.com'], true)
            ? self::OPTION_API_URL_US
            : self::OPTION_API_URL_DEFAULT;
    }

    private function market(): string
    {
        $market = strtoupper((string) get_option(self::OPTION_MARKET, ''));
        return in_array($market, ['CA', 'US'], true) ? $market : self::detect_store_market();
    }

    private static function detect_store_market(): string
    {
        $country = '';
        if (function_exists('wc_get_base_location')) {
            $location = wc_get_base_location();
            $country = strtoupper((string) ($location['country'] ?? ''));
        }
        if ($country === '') {
            $defaultCountry = strtoupper((string) get_option('woocommerce_default_country', ''));
            $country = explode(':', $defaultCountry, 2)[0];
        }

        return $country === 'US' ? 'US' : 'CA';
    }

    private function api_key(): string
    {
        $key = (string) get_option(self::OPTION_API_KEY, '');
        if ($key !== '') {
            return $key;
        }

        return (string) get_option(
            self::LEGACY_OPTION_API_KEY,
            get_option(self::ORIGINAL_OPTION_API_KEY, '')
        );
    }

    private function merchant_artwork_url(WC_Product $product): string
    {
        return $this->merchant_artwork_data($product)['url'];
    }

    private function merchant_artwork_data(WC_Product $product): array
    {
        $url = esc_url_raw((string) $product->get_meta(self::META_ARTWORK_URL));
        $attachmentId = $url !== '' ? attachment_url_to_postid($url) : (int) $product->get_image_id();
        if ($url === '' && $attachmentId > 0) {
            $url = (string) wp_get_attachment_url($attachmentId);
        }
        $meta = $attachmentId > 0 ? wp_get_attachment_metadata($attachmentId) : [];

        return [
            'url' => $url,
            'width' => (int) ($meta['width'] ?? 0),
            'height' => (int) ($meta['height'] ?? 0),
        ];
    }

    private function configuration_product($product): ?WC_Product
    {
        if (!$product instanceof WC_Product) {
            return null;
        }
        if ($product instanceof WC_Product_Variation && $product->get_parent_id()) {
            $parent = wc_get_product($product->get_parent_id());
            if ($parent instanceof WC_Product) {
                return $parent;
            }
        }

        return $product;
    }

    private function size_from_meta(string $selectedSize, string $fallbackSizes): array
    {
        if (preg_match('/(\d+)\s*x\s*(\d+)/i', $selectedSize, $matches)) {
            return [(int) $matches[1], (int) $matches[2]];
        }

        if (preg_match('/(\d+)\s*x\s*(\d+)/i', $fallbackSizes, $matches)) {
            return [(int) $matches[1], (int) $matches[2]];
        }

        return [12, 16];
    }

    private function validate_image_file(array $file, WC_Product $product, string $selectedSize): array
    {
        $errors = [];
        $warnings = [];

        if (!empty($file['error'])) {
            return [
                'errors' => ['The uploaded image could not be read. Please try again.'],
                'warnings' => [],
            ];
        }

        if ((int) ($file['size'] ?? 0) > self::MAX_UPLOAD_BYTES) {
            $errors[] = 'The image is too large. Maximum file size is 110 MB.';
        }

        $mime = '';
        if (function_exists('wp_check_filetype_and_ext')) {
            $checked = wp_check_filetype_and_ext((string) ($file['tmp_name'] ?? ''), (string) ($file['name'] ?? ''));
            $mime = (string) ($checked['type'] ?? '');
        }

        if (!in_array($mime, ['image/jpeg', 'image/png', 'image/webp'], true)) {
            $errors[] = 'Please upload a JPG, PNG, or WebP image.';
        }

        $dimensions = $this->read_image_dimensions((string) ($file['tmp_name'] ?? ''));
        if ($dimensions === []) {
            $errors[] = 'We could not read the image dimensions. Please upload a standard JPG, PNG, or WebP file.';

            return ['errors' => $errors, 'warnings' => $warnings];
        }

        $availableSizes = $this->parse_sizes((string) $product->get_meta(self::META_ALLOWED_SIZES));
        $targetSize = $this->find_size($availableSizes, $selectedSize);

        if ($targetSize === null) {
            $errors[] = 'Please select a valid canvas size for this upload.';

            return ['errors' => $errors, 'warnings' => $warnings];
        }

        $minimumWidth = $targetSize['width'] * self::MIN_PRINT_DPI;
        $minimumHeight = $targetSize['height'] * self::MIN_PRINT_DPI;
        $fitsPortrait = $dimensions['width'] >= $minimumWidth && $dimensions['height'] >= $minimumHeight;
        $fitsLandscape = $dimensions['width'] >= $minimumHeight && $dimensions['height'] >= $minimumWidth;

        if (!$fitsPortrait && !$fitsLandscape) {
            $errors[] = sprintf(
                'This image is %dx%d px. For %dx%d canvas we need at least %dx%d px.',
                $dimensions['width'],
                $dimensions['height'],
                $targetSize['width'],
                $targetSize['height'],
                $minimumWidth,
                $minimumHeight
            );
        }

        $imageRatio = max($dimensions['width'], $dimensions['height']) / max(1, min($dimensions['width'], $dimensions['height']));
        $targetRatio = max($targetSize['width'], $targetSize['height']) / max(1, min($targetSize['width'], $targetSize['height']));

        if (abs($imageRatio - $targetRatio) > self::RATIO_TOLERANCE) {
            $warnings[] = sprintf(
                'The image shape does not exactly match %dx%d. It may need cropping or borders before printing.',
                $targetSize['width'],
                $targetSize['height']
            );
        }

        return ['errors' => $errors, 'warnings' => $warnings];
    }

    private function read_image_dimensions(string $path): array
    {
        if ($path === '' || !is_readable($path)) {
            return [];
        }

        $size = @getimagesize($path);
        if (!is_array($size) || empty($size[0]) || empty($size[1])) {
            return [];
        }

        return ['width' => (int) $size[0], 'height' => (int) $size[1]];
    }

    private function parse_sizes(string $sizes): array
    {
        preg_match_all('/(\d+)\s*x\s*(\d+)/i', $sizes, $matches, PREG_SET_ORDER);
        $output = [];

        foreach ($matches as $match) {
            $output[] = [
                'width' => (int) $match[1],
                'height' => (int) $match[2],
            ];
        }

        return $output;
    }

    private function find_size(array $sizes, string $selectedSize): ?array
    {
        if (!preg_match('/(\d+)\s*x\s*(\d+)/i', $selectedSize, $matches)) {
            return null;
        }

        $width = (int) $matches[1];
        $height = (int) $matches[2];

        foreach ($sizes as $size) {
            if ((int) $size['width'] === $width && (int) $size['height'] === $height) {
                return $size;
            }
        }

        return null;
    }

    private function preview_css(): string
    {
        return <<<'CSS'
.cplpod-preview {
    margin: 18px 0;
}
.cplpod-customizer {
    display: grid;
    gap: 12px;
    margin-bottom: 14px;
}
.cplpod-customizer__step {
    padding: 12px;
    border: 1px solid #ddd8cf;
    background: #fff;
}
.cplpod-customizer__step h3 {
    margin: 0 0 10px;
    font-size: 16px;
}
.cplpod-option-row,
.cplpod-tool-row {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: center;
}
.cplpod-option-row label,
.cplpod-color-row {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 36px;
    padding: 6px 10px;
    border: 1px solid #d8d2c8;
    background: #faf9f6;
}
.cplpod-color-row {
    margin: 10px 0;
}
.cplpod-tool-row input[type="range"] {
    flex: 1 1 180px;
}
.cplpod-tool-row button {
    min-height: 36px;
    padding: 6px 12px;
    border: 1px solid #bdb5aa;
    background: #fff;
    cursor: pointer;
}
.cplpod-preview__stage {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 320px;
    padding: 24px;
    background: #f4f1ec;
    border: 1px solid #ddd8cf;
}
.cplpod-preview__mockup {
    --cplpod-edge: 18px;
    --cplpod-edge-color: #ffffff;
    position: relative;
    width: min(100%, 380px);
    transform-style: preserve-3d;
    transition: transform .2s ease, filter .2s ease;
}
.cplpod-preview__mockup.is-3d {
    transform: perspective(900px) rotateY(-18deg) rotateX(3deg);
    filter: drop-shadow(18px 20px 18px rgba(38, 32, 24, .25));
}
.cplpod-preview__canvas {
    position: relative;
    width: 100%;
    aspect-ratio: 4 / 3;
    overflow: hidden;
    background: #fff;
    border: 1px solid #cfc7bc;
    box-shadow: 10px 12px 0 #b9afa3, 18px 20px 26px rgba(38, 32, 24, .18);
}
.cplpod-preview__canvas::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    box-shadow: inset 0 0 0 10px rgba(255,255,255,.18), inset -18px 0 24px rgba(0,0,0,.16);
}
.cplpod-preview__canvas img {
    position: absolute;
    top: 50%;
    left: 50%;
    width: auto;
    height: auto;
    max-width: none;
    max-height: none;
    transform: translate(-50%, -50%) scale(1);
    transform-origin: center;
    display: block;
    cursor: grab;
    user-select: none;
    touch-action: none;
}
.cplpod-preview__canvas img.is-dragging {
    cursor: grabbing;
}
.cplpod-preview__canvas span {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 18px;
    color: #6d6257;
    text-align: center;
    font-size: 14px;
}
.cplpod-preview__meta {
    margin-top: 10px;
    color: #3f3a34;
    font-size: 14px;
    line-height: 1.45;
}
.cplpod-preview__meta strong {
    display: block;
    color: #1f1b17;
}
.cplpod-preview__meta .cplpod-warning {
    color: #8a5a00;
}
.cplpod-preview__meta .cplpod-error {
    color: #a32020;
}
.cplpod-preview__edge {
    position: absolute;
    z-index: 0;
    opacity: .78;
    pointer-events: none;
    background-color: var(--cplpod-edge-color);
    background-size: cover;
    background-position: center;
}
.cplpod-preview__edge--left,
.cplpod-preview__edge--right {
    top: var(--cplpod-edge);
    bottom: var(--cplpod-edge);
    width: var(--cplpod-edge);
}
.cplpod-preview__edge--left {
    right: 100%;
    transform: skewY(38deg);
    transform-origin: right center;
}
.cplpod-preview__edge--right {
    left: 100%;
    transform: skewY(-38deg);
    transform-origin: left center;
}
.cplpod-preview__edge--top,
.cplpod-preview__edge--bottom {
    left: var(--cplpod-edge);
    right: var(--cplpod-edge);
    height: var(--cplpod-edge);
}
.cplpod-preview__edge--top {
    bottom: 100%;
    transform: skewX(52deg);
    transform-origin: bottom center;
}
.cplpod-preview__edge--bottom {
    top: 100%;
    transform: skewX(-52deg);
    transform-origin: top center;
}
.cplpod-preview[data-edge-style="blur"] .cplpod-preview__edge {
    filter: blur(4px);
}
CSS;
    }

    private function preview_js(): string
    {
        return <<<'JS'
(function () {
    var state = {
        offsetX: 0,
        offsetY: 0,
        scale: 1,
        dragging: false,
        startX: 0,
        startY: 0,
        startOffsetX: 0,
        startOffsetY: 0,
        imageUrl: ''
    };

    function parseSize(value) {
        var match = String(value || '').match(/(\d+)\s*x\s*(\d+)/i);
        if (!match) {
            return null;
        }

        return { width: parseInt(match[1], 10), height: parseInt(match[2], 10) };
    }

    function setRatio(canvas, size) {
        if (!canvas || !size) {
            return;
        }

        canvas.style.aspectRatio = size.width + ' / ' + size.height;
    }

    function els() {
        return {
            fileInput: document.getElementById('cplpod_customer_image'),
            sizeInput: document.getElementById('cplpod_selected_size'),
            canvas: document.getElementById('cplpod_preview_canvas'),
            mockup: document.getElementById('cplpod_preview_mockup'),
            image: document.getElementById('cplpod_preview_image'),
            placeholder: document.getElementById('cplpod_preview_placeholder'),
            meta: document.getElementById('cplpod_preview_meta'),
            wrapper: document.querySelector('.cplpod-preview'),
            zoom: document.getElementById('cplpod_zoom'),
            reset: document.getElementById('cplpod_reset_crop'),
            toggle3d: document.getElementById('cplpod_toggle_3d'),
            edgeColor: document.getElementById('cplpod_edge_color'),
            cropX: document.getElementById('cplpod_crop_x'),
            cropY: document.getElementById('cplpod_crop_y'),
            cropScale: document.getElementById('cplpod_crop_scale')
        };
    }

    function updateMeta(meta, image, size, minDpi) {
        if (!meta || !image || !size) {
            return;
        }

        var minWidth = size.width * minDpi;
        var minHeight = size.height * minDpi;
        var fitsPortrait = image.naturalWidth >= minWidth && image.naturalHeight >= minHeight;
        var fitsLandscape = image.naturalWidth >= minHeight && image.naturalHeight >= minWidth;
        var imageRatio = Math.max(image.naturalWidth, image.naturalHeight) / Math.max(1, Math.min(image.naturalWidth, image.naturalHeight));
        var targetRatio = Math.max(size.width, size.height) / Math.max(1, Math.min(size.width, size.height));
        var html = '<strong>' + image.naturalWidth + 'x' + image.naturalHeight + ' px image for ' + size.width + 'x' + size.height + ' canvas</strong>';

        if (!fitsPortrait && !fitsLandscape) {
            html += '<span class="cplpod-error">Too small for print. Minimum recommended size is ' + minWidth + 'x' + minHeight + ' px.</span>';
        } else {
            html += '<span>Resolution looks printable at ' + minDpi + ' PPI.</span>';
        }

        if (Math.abs(imageRatio - targetRatio) > 0.04) {
            html += '<br><span class="cplpod-warning">Image shape does not exactly match this canvas size. The preview shows the likely crop.</span>';
        }

        meta.innerHTML = html;
    }

    function baseImageSize(image, canvas) {
        var canvasRect = canvas.getBoundingClientRect();
        var imageRatio = image.naturalWidth / Math.max(1, image.naturalHeight);
        var canvasRatio = canvasRect.width / Math.max(1, canvasRect.height);

        if (imageRatio > canvasRatio) {
            return { width: canvasRect.height * imageRatio, height: canvasRect.height };
        }

        return { width: canvasRect.width, height: canvasRect.width / imageRatio };
    }

    function clampOffsets(image, canvas) {
        if (!image || image.hidden) {
            state.offsetX = 0;
            state.offsetY = 0;
            return;
        }

        var canvasRect = canvas.getBoundingClientRect();
        var base = baseImageSize(image, canvas);
        var renderedWidth = base.width * state.scale;
        var renderedHeight = base.height * state.scale;
        var maxX = Math.max(0, (renderedWidth - canvasRect.width) / 2);
        var maxY = Math.max(0, (renderedHeight - canvasRect.height) / 2);

        state.offsetX = Math.min(maxX, Math.max(-maxX, state.offsetX));
        state.offsetY = Math.min(maxY, Math.max(-maxY, state.offsetY));
    }

    function updateHidden(fields) {
        if (!fields.cropX || !fields.cropY || !fields.cropScale) {
            return;
        }

        fields.cropX.value = String(Math.round(state.offsetX));
        fields.cropY.value = String(Math.round(state.offsetY));
        fields.cropScale.value = String(Number(state.scale).toFixed(2));
    }

    function applyImageTransform(fields) {
        if (!fields.image || !fields.canvas) {
            return;
        }

        var base = baseImageSize(fields.image, fields.canvas);
        clampOffsets(fields.image, fields.canvas);
        fields.image.style.width = base.width + 'px';
        fields.image.style.height = base.height + 'px';
        fields.image.style.transform = 'translate(calc(-50% + ' + state.offsetX + 'px), calc(-50% + ' + state.offsetY + 'px)) scale(' + state.scale + ')';
        updateHidden(fields);
        paintEdges(fields);
    }

    function selectedEdgeStyle() {
        var selected = document.querySelector('input[name="cplpod_edge_style"]:checked');
        return selected ? selected.value : 'mirror';
    }

    function frameDepth() {
        var selected = document.querySelector('input[name="cplpod_frame_depth"]:checked');
        return selected ? selected.value : '1.5';
    }

    function paintEdges(fields) {
        if (!fields.wrapper || !fields.mockup) {
            return;
        }

        var edgeStyle = selectedEdgeStyle();
        var edgeColor = fields.edgeColor ? fields.edgeColor.value : '#ffffff';
        var edgeSize = frameDepth() === '0.75' ? 10 : 18;

        fields.wrapper.setAttribute('data-edge-style', edgeStyle);
        fields.mockup.style.setProperty('--cplpod-edge', edgeSize + 'px');
        fields.mockup.style.setProperty('--cplpod-edge-color', edgeColor);

        Array.prototype.forEach.call(document.querySelectorAll('.cplpod-preview__edge'), function (edge) {
            if (edgeStyle === 'fill' || !state.imageUrl) {
                edge.style.backgroundImage = 'none';
                edge.style.backgroundColor = edgeColor;
                return;
            }

            edge.style.backgroundColor = 'transparent';
            edge.style.backgroundImage = 'url("' + state.imageUrl + '")';
        });
    }

    function resetCrop(fields) {
        state.offsetX = 0;
        state.offsetY = 0;
        state.scale = 1;
        if (fields.zoom) {
            fields.zoom.value = '1';
        }
        applyImageTransform(fields);
    }

    document.addEventListener('change', function (event) {
        var fields = els();
        var fileInput = fields.fileInput;
        var sizeInput = fields.sizeInput;
        var canvas = fields.canvas;
        var image = fields.image;
        var placeholder = fields.placeholder;
        var meta = fields.meta;
        var wrapper = fields.wrapper;

        if (!fileInput || !canvas || !image || !wrapper) {
            return;
        }

        var size = parseSize(sizeInput ? sizeInput.value : '');
        setRatio(canvas, size);

        if (event.target === sizeInput && image.src && size) {
            updateMeta(meta, image, size, parseInt(wrapper.getAttribute('data-min-dpi') || '150', 10));
            applyImageTransform(fields);
            return;
        }

        if (event.target && (event.target.name === 'cplpod_edge_style' || event.target.name === 'cplpod_frame_depth' || event.target.id === 'cplpod_edge_color')) {
            paintEdges(fields);
            return;
        }

        if (event.target !== fileInput || !fileInput.files || !fileInput.files[0]) {
            return;
        }

        var file = fileInput.files[0];
        if (!file.type.match(/^image\/(jpeg|png|webp)$/)) {
            meta.innerHTML = '<span class="cplpod-error">Please upload a JPG, PNG, or WebP image.</span>';
            return;
        }

        var reader = new FileReader();
        reader.onload = function (loadEvent) {
            image.onload = function () {
                image.hidden = false;
                state.imageUrl = loadEvent.target.result;
                if (placeholder) {
                    placeholder.hidden = true;
                }
                resetCrop(fields);
                updateMeta(meta, image, size, parseInt(wrapper.getAttribute('data-min-dpi') || '150', 10));
            };
            image.src = loadEvent.target.result;
        };
        reader.readAsDataURL(file);
    });

    document.addEventListener('input', function (event) {
        var fields = els();

        if (event.target && event.target.id === 'cplpod_zoom') {
            state.scale = parseFloat(event.target.value || '1');
            applyImageTransform(fields);
        }

        if (event.target && event.target.id === 'cplpod_edge_color') {
            paintEdges(fields);
        }
    });

    document.addEventListener('DOMContentLoaded', function () {
        var fields = els();
        setRatio(fields.canvas, parseSize(fields.sizeInput ? fields.sizeInput.value : ''));
        paintEdges(fields);

        if (fields.reset) {
            fields.reset.addEventListener('click', function () {
                resetCrop(els());
            });
        }

        if (fields.toggle3d) {
            fields.toggle3d.addEventListener('click', function () {
                var current = els();
                if (!current.mockup) {
                    return;
                }
                current.mockup.classList.toggle('is-3d');
                current.toggle3d.textContent = current.mockup.classList.contains('is-3d') ? '2D Preview' : '3D Preview';
            });
        }

        if (fields.image && fields.canvas) {
            fields.image.addEventListener('pointerdown', function (event) {
                var current = els();
                if (!current.image || current.image.hidden) {
                    return;
                }

                state.dragging = true;
                state.startX = event.clientX;
                state.startY = event.clientY;
                state.startOffsetX = state.offsetX;
                state.startOffsetY = state.offsetY;
                current.image.classList.add('is-dragging');
                current.image.setPointerCapture(event.pointerId);
            });

            fields.image.addEventListener('pointermove', function (event) {
                if (!state.dragging) {
                    return;
                }

                state.offsetX = state.startOffsetX + event.clientX - state.startX;
                state.offsetY = state.startOffsetY + event.clientY - state.startY;
                applyImageTransform(els());
            });

            fields.image.addEventListener('pointerup', function (event) {
                var current = els();
                state.dragging = false;
                if (current.image) {
                    current.image.classList.remove('is-dragging');
                    try {
                        current.image.releasePointerCapture(event.pointerId);
                    } catch (error) {}
                }
                applyImageTransform(current);
            });
        }

        window.addEventListener('resize', function () {
            applyImageTransform(els());
        });
    });
})();
JS;
    }
}

add_action('before_woocommerce_init', static function (): void {
    if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true);
    }
});

register_activation_hook(__FILE__, [CPLPOD_Print_On_Demand::class, 'activate']);

add_action('plugins_loaded', static function (): void {
    new CPLPOD_Print_On_Demand();
    new CPLPOD_Print_On_Demand_Artwork_Wizard();
});
