<?php
/**
 * Plugin Name: Canvas Printing Fulfillment for WooCommerce
 * Description: Connects WooCommerce products to Canvas Prints fulfillment for custom canvas print orders.
 * Version: 1.7.0
 * Author: Canvas Prints
 * Requires Plugins: woocommerce
 */

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

require_once __DIR__ . '/includes/class-cpf-artwork-wizard.php';

final class Canvas_Printing_Fulfillment
{
    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 = 'cpf_api_url';
    private const OPTION_API_KEY = 'cpf_api_key';
    private const CURRENCY_TRANSIENT = 'cpf_canvas_currency_context';
    private const OPTION_API_URL_DEFAULT = 'https://www.canvasprintscalgary.com/api/partner/v1';
    private const META_ENABLED = '_cpf_enabled';
    private const META_IMAGE_MODE = '_cpf_image_mode';
    private const META_ALLOWED_SIZES = '_cpf_allowed_sizes';
    private const META_ARTWORK_URL = '_cpf_artwork_url';
    private const META_DEFAULT_SIZE = '_cpf_default_size';
    private const ORDER_META_SENT = '_cpf_fulfillment_order_id';
    private const ORDER_META_STATUS = '_cpf_fulfillment_status';
    private const ORDER_META_TRACKING_NUMBER = '_cpf_tracking_number';
    private const ORDER_META_TRACKING_CARRIER = '_cpf_tracking_carrier';
    private const ORDER_META_TRACKING_URL = '_cpf_tracking_url';
    private const ORDER_META_TRACKING_NOTIFIED = '_cpf_tracking_notified_hash';

    public function __construct()
    {
        add_action('admin_menu', [$this, 'register_settings_page']);
        add_action('admin_init', [$this, 'register_settings']);
        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 function woocommerce_notice(): void
    {
        if (class_exists('WooCommerce')) {
            return;
        }

        echo '<div class="notice notice-error"><p>Canvas Printing Fulfillment requires WooCommerce.</p></div>';
    }

    public function register_settings_page(): void
    {
        add_submenu_page(
            'woocommerce',
            'Canvas Fulfillment',
            'Canvas Fulfillment',
            'manage_woocommerce',
            'canvas-printing-fulfillment',
            [$this, 'render_settings_page']
        );
    }

    public function register_settings(): void
    {
        register_setting('cpf_settings', self::OPTION_API_URL, ['sanitize_callback' => 'esc_url_raw']);
        register_setting('cpf_settings', self::OPTION_API_KEY, ['sanitize_callback' => 'sanitize_text_field']);
    }

    public function render_settings_page(): void
    {
        $status = $this->request('GET', '/status');
        ?>
        <div class="wrap">
            <h1>Canvas Fulfillment</h1>
            <form method="post" action="options.php">
                <?php settings_fields('cpf_settings'); ?>
                <table class="form-table" role="presentation">
                    <tr>
                        <th scope="row"><label for="cpf_api_url">API URL</label></th>
                            <td><input id="cpf_api_url" name="<?php echo esc_attr(self::OPTION_API_URL); ?>" type="url" class="regular-text" value="<?php echo esc_attr($this->api_url()); ?>" placeholder="<?php echo esc_attr(self::OPTION_API_URL_DEFAULT); ?>"></td>
                    </tr>
                    <tr>
                        <th scope="row"><label for="cpf_api_key">API Key</label></th>
                        <td><input id="cpf_api_key" name="<?php echo esc_attr(self::OPTION_API_KEY); ?>" type="password" class="regular-text" value="<?php echo esc_attr(get_option(self::OPTION_API_KEY, '')); ?>"></td>
                    </tr>
                </table>
                <?php submit_button('Save connection'); ?>
            </form>
            <h2>Connection</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',
            'description' => 'Send this product to Canvas Prints for fulfillment.',
        ]);
        woocommerce_wp_select([
            'id' => self::META_IMAGE_MODE,
            'label' => 'Image source',
            'options' => [
                'customer_upload' => 'Customer uploads image',
                'merchant_artwork' => 'Merchant artwork',
                'both' => 'Customer upload or merchant artwork',
            ],
        ]);
        woocommerce_wp_text_input([
            'id' => self::META_ALLOWED_SIZES,
            'label' => 'Allowed sizes',
            'description' => 'Comma-separated sizes, for example 12x16,16x20,24x36.',
            'desc_tip' => true,
        ]);
        woocommerce_wp_text_input([
            'id' => self::META_DEFAULT_SIZE,
            'label' => 'Default canvas size',
            'description' => 'Used for merchant artwork when the shopper does not choose a size, for example 16x20.',
            'desc_tip' => true,
        ]);
        woocommerce_wp_text_input([
            'id' => self::META_ARTWORK_URL,
            'label' => 'Production artwork URL',
            'description' => 'Public HTTPS URL for the full-resolution merchant artwork. If empty, the full-size featured product image is used.',
            'desc_tip' => true,
        ]);
        echo '</div>';
    }

    public function save_product_fields(WC_Product $product): void
    {
        $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="cpf_selected_size">Canvas size</label>';
            echo '<select id="cpf_selected_size" name="cpf_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') {
            echo '<p class="cpf-merchant-artwork-note">This product uses the artwork selected by the store owner. Choose your print size above.</p>';
            return;
        }

        echo '<p class="form-row form-row-wide">';
        echo '<label for="cpf_customer_image">Upload image for canvas print</label>';
        echo '<input id="cpf_customer_image" name="cpf_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="cpf-preview" data-min-dpi="' . esc_attr((string) self::MIN_PRINT_DPI) . '">';
        echo '<input type="hidden" id="cpf_crop_x" name="cpf_crop_x" value="0">';
        echo '<input type="hidden" id="cpf_crop_y" name="cpf_crop_y" value="0">';
        echo '<input type="hidden" id="cpf_crop_scale" name="cpf_crop_scale" value="1">';
        echo '<div class="cpf-customizer">';
        echo '<section class="cpf-customizer__step"><h3>Canvas options</h3>';
        echo '<div class="cpf-option-row">';
        echo '<label><input type="radio" name="cpf_edge_style" value="mirror" checked> Mirror edge</label>';
        echo '<label><input type="radio" name="cpf_edge_style" value="blur"> Blur edge</label>';
        echo '<label><input type="radio" name="cpf_edge_style" value="fill"> Color edge</label>';
        echo '</div>';
        echo '<label class="cpf-color-row">Edge color <input type="color" id="cpf_edge_color" name="cpf_edge_color" value="#ffffff"></label>';
        echo '<div class="cpf-option-row">';
        echo '<label><input type="radio" name="cpf_frame_depth" value="1.5" checked> 1.5 inch gallery wrap</label>';
        echo '<label><input type="radio" name="cpf_frame_depth" value="0.75"> 0.75 inch thin wrap</label>';
        echo '</div></section>';
        echo '<section class="cpf-customizer__step"><h3>Position image</h3>';
        echo '<div class="cpf-tool-row">';
        echo '<label for="cpf_zoom">Zoom</label>';
        echo '<input id="cpf_zoom" type="range" min="1" max="3" step="0.01" value="1">';
        echo '<button type="button" id="cpf_reset_crop">Center</button>';
        echo '<button type="button" id="cpf_toggle_3d">3D Preview</button>';
        echo '</div></section>';
        echo '</div>';
        echo '<div class="cpf-preview__stage">';
        echo '<div class="cpf-preview__mockup" id="cpf_preview_mockup">';
        echo '<div class="cpf-preview__edge cpf-preview__edge--left"></div>';
        echo '<div class="cpf-preview__edge cpf-preview__edge--right"></div>';
        echo '<div class="cpf-preview__edge cpf-preview__edge--top"></div>';
        echo '<div class="cpf-preview__edge cpf-preview__edge--bottom"></div>';
        echo '<div class="cpf-preview__canvas" id="cpf_preview_canvas">';
        echo '<img id="cpf_preview_image" alt="" hidden draggable="false">';
        echo '<span id="cpf_preview_placeholder">Upload an image, choose size, then drag and zoom to customize</span>';
        echo '</div>';
        echo '</div>';
        echo '</div>';
        echo '<div class="cpf-preview__meta" id="cpf_preview_meta" aria-live="polite"></div>';
        echo '</div>';
    }

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

        wp_register_style('cpf-preview', false, [], '0.1.0-dev');
        wp_enqueue_style('cpf-preview');
        wp_add_inline_style('cpf-preview', $this->preview_css());

        wp_register_script('cpf-preview', false, [], '0.1.0-dev', true);
        wp_enqueue_script('cpf-preview');
        wp_add_inline_script('cpf-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;
        }

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

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

        $selectedSize = sanitize_text_field(wp_unslash($_POST['cpf_selected_size'] ?? ''));
        $validation = $this->validate_image_file($_FILES['cpf_customer_image'], $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;
        }

        $selectedSize = sanitize_text_field(wp_unslash($_POST['cpf_selected_size'] ?? $_POST['attribute_canvas-size'] ?? ''));
        if ($selectedSize === '' && $variation instanceof WC_Product_Variation) {
            $selectedSize = (string) $variation->get_meta('_cpf_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('_cpf_style')) ?: 'stretched_standard';
        $variationEdgeStyle = (string) (($variation instanceof WC_Product_Variation ? $variation : $product)->get_meta('_cpf_edge_style'));
        $variationEdgeColor = (string) (($variation instanceof WC_Product_Variation ? $variation : $product)->get_meta('_cpf_edge_color'));
        if (empty($_FILES['cpf_customer_image']['name']) && 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['cpf_customer_image_url'] = $artwork['url'];
                $cart_item_data['cpf_selected_size'] = $selectedSize;
                $cart_item_data['cpf_image_width'] = $artwork['width'];
                $cart_item_data['cpf_image_height'] = $artwork['height'];
                $cart_item_data['cpf_edge_style'] = in_array($variationEdgeStyle, ['mirror', 'blur', 'image', 'color'], true) ? $variationEdgeStyle : 'mirror';
                $cart_item_data['cpf_edge_color'] = sanitize_hex_color($variationEdgeColor) ?: '#ffffff';
                $cart_item_data['cpf_frame_depth'] = '1.5';
                $cart_item_data['cpf_crop_x'] = '0';
                $cart_item_data['cpf_crop_y'] = '0';
                $cart_item_data['cpf_crop_scale'] = '1';
                $cart_item_data['cpf_style'] = $selectedStyle ?: 'stretched_standard';
            }
            return $cart_item_data;
        }

        if (empty($_FILES['cpf_customer_image']['name'])) {
            return $cart_item_data;
        }

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

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

            $cart_item_data['cpf_customer_image_url'] = esc_url_raw($upload['url']);
            $cart_item_data['cpf_selected_size'] = $selectedSize;
            $cart_item_data['cpf_image_width'] = $dimensions['width'] ?? null;
            $cart_item_data['cpf_image_height'] = $dimensions['height'] ?? null;
            $postedEdgeStyle = sanitize_text_field(wp_unslash($_POST['cpf_edge_style'] ?? 'mirror'));
            $cart_item_data['cpf_edge_style'] = in_array($variationEdgeStyle, ['mirror', 'blur', 'image', 'color'], true) ? $variationEdgeStyle : $postedEdgeStyle;
            $cart_item_data['cpf_edge_color'] = sanitize_hex_color($variationEdgeColor) ?: (sanitize_hex_color(wp_unslash($_POST['cpf_edge_color'] ?? '#ffffff')) ?: '#ffffff');
            $cart_item_data['cpf_frame_depth'] = sanitize_text_field(wp_unslash($_POST['cpf_frame_depth'] ?? '1.5'));
            $cart_item_data['cpf_crop_x'] = sanitize_text_field(wp_unslash($_POST['cpf_crop_x'] ?? '0'));
            $cart_item_data['cpf_crop_y'] = sanitize_text_field(wp_unslash($_POST['cpf_crop_y'] ?? '0'));
            $cart_item_data['cpf_crop_scale'] = sanitize_text_field(wp_unslash($_POST['cpf_crop_scale'] ?? '1'));
            $cart_item_data['cpf_style'] = $selectedStyle ?: 'stretched_standard';
            $cart_item_data['unique_key'] = md5($upload['url'] . microtime(true));
        }

        return $cart_item_data;
    }

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

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

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

        if (!empty($values['cpf_image_width']) && !empty($values['cpf_image_height'])) {
            $item->add_meta_data('Canvas image width', (int) $values['cpf_image_width'], true);
            $item->add_meta_data('Canvas image height', (int) $values['cpf_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('_cpf_fulfillment_enabled', 'yes', true);
            $item->add_meta_data('_cpf_allowed_sizes', (string) $product->get_meta(self::META_ALLOWED_SIZES), true);
            $item->add_meta_data('_cpf_image_mode', (string) $product->get_meta(self::META_IMAGE_MODE), true);
            $item->add_meta_data('Canvas style', sanitize_text_field((string) ($values['cpf_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 = [
            'cpf_standard' => new WC_Shipping_Rate('cpf_standard', 'Free standard delivery', 0, [], 'canvas_printing'),
        ];
        $remoteCanadianTerritory = $country === 'CA' && in_array($state, ['NT', 'NU', 'YT'], true);
        if (!$remoteCanadianTerritory && $rate > 0) {
            $canvasRates['cpf_expedited'] = new WC_Shipping_Rate('cpf_expedited', 'Expedited delivery', round(11 * $rate, 2), [], 'canvas_printing');
            $canvasRates['cpf_express'] = new WC_Shipping_Rate('cpf_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('_cpf_fulfillment_enabled') !== 'yes') {
                continue;
            }

            [$width, $height] = $this->size_from_meta((string) $line_item->get_meta('Canvas selected size'), (string) $line_item->get_meta('_cpf_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', '/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 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: ' . $carrier . "\n";
            if ($number !== '') echo 'Tracking number: ' . $number . "\n";
            if ($url !== '') echo 'Track shipment: ' . $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">Track your shipment</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, 'cpf_express')) return 'express';
            if (str_contains($methodId, 'cpf_expedited')) return 'expedited';
            if (str_contains($methodId, 'cpf_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 = (string) get_option(self::OPTION_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('cpf_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' => '_cpf_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('_cpf_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('_cpf_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('_cpf_size', sanitize_text_field((string) ($singleVariant['size'] ?? '')));
                $product->update_meta_data('_cpf_style', sanitize_text_field((string) ($singleVariant['style_id'] ?? '')));
                $product->update_meta_data('_cpf_edge_style', sanitize_text_field((string) ($singleVariant['wrap'] ?? 'mirror')));
                $product->update_meta_data('_cpf_edge_color', sanitize_hex_color((string) ($singleVariant['wrap_color'] ?? '')) ?: '#ffffff');
                $product->update_meta_data('_cpf_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('_cpf_size', sanitize_text_field((string) $variantData['size']));
                $variation->update_meta_data('_cpf_style', sanitize_text_field((string) $variantData['style_id']));
                $variation->update_meta_data('_cpf_edge_style', sanitize_text_field((string) ($variantData['wrap'] ?? 'mirror')));
                $variation->update_meta_data('_cpf_edge_color', sanitize_hex_color((string) ($variantData['wrap_color'] ?? '')) ?: '#ffffff');
                $variation->update_meta_data('_cpf_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('_cpf_artwork_attachment_id', $variantArtworkId);
                }
                $variation->save();
            }

            $mockupIds = [];
            foreach ((array) ($payload['mockup_urls'] ?? []) as $index => $mockupUrl) {
                $mockupIds[] = $this->import_canvas_image((string) $mockupUrl, sanitize_text_field((string) $payload['title']) . ' mockup ' . ($index + 1), $productId);
            }
            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: ' . $temporary->get_error_message());
        $path = (string) parse_url($url, PHP_URL_PATH); $query = []; parse_str((string) 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)) { @unlink($temporary); throw new RuntimeException('WooCommerce could not import a Canvas image: ' . $attachmentId->get_error_message()); }
        return (int) $attachmentId;
    }

    private function unique_canvas_sku(string $title): string
    {
        global $wpdb;

        $base = trim('ART-' . strtoupper(substr(preg_replace('/[^A-Za-z0-9]+/', '-', $title), 0, 28)), '-');
        $candidate = $base; $index = 2;
        $lookupTable = $wpdb->prefix . 'wc_product_meta_lookup';
        while (
            wc_get_product_id_by_sku($candidate)
            || (int) $wpdb->get_var($wpdb->prepare("SELECT product_id FROM {$lookupTable} WHERE sku = %s LIMIT 1", $candidate)) > 0
        ) {
            $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' => (string) get_option(self::OPTION_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
    {
        return rtrim((string) get_option(self::OPTION_API_URL, self::OPTION_API_URL_DEFAULT), '/');
    }

    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'
.cpf-preview {
    margin: 18px 0;
}
.cpf-customizer {
    display: grid;
    gap: 12px;
    margin-bottom: 14px;
}
.cpf-customizer__step {
    padding: 12px;
    border: 1px solid #ddd8cf;
    background: #fff;
}
.cpf-customizer__step h3 {
    margin: 0 0 10px;
    font-size: 16px;
}
.cpf-option-row,
.cpf-tool-row {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: center;
}
.cpf-option-row label,
.cpf-color-row {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 36px;
    padding: 6px 10px;
    border: 1px solid #d8d2c8;
    background: #faf9f6;
}
.cpf-color-row {
    margin: 10px 0;
}
.cpf-tool-row input[type="range"] {
    flex: 1 1 180px;
}
.cpf-tool-row button {
    min-height: 36px;
    padding: 6px 12px;
    border: 1px solid #bdb5aa;
    background: #fff;
    cursor: pointer;
}
.cpf-preview__stage {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 320px;
    padding: 24px;
    background: #f4f1ec;
    border: 1px solid #ddd8cf;
}
.cpf-preview__mockup {
    --cpf-edge: 18px;
    --cpf-edge-color: #ffffff;
    position: relative;
    width: min(100%, 380px);
    transform-style: preserve-3d;
    transition: transform .2s ease, filter .2s ease;
}
.cpf-preview__mockup.is-3d {
    transform: perspective(900px) rotateY(-18deg) rotateX(3deg);
    filter: drop-shadow(18px 20px 18px rgba(38, 32, 24, .25));
}
.cpf-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);
}
.cpf-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);
}
.cpf-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;
}
.cpf-preview__canvas img.is-dragging {
    cursor: grabbing;
}
.cpf-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;
}
.cpf-preview__meta {
    margin-top: 10px;
    color: #3f3a34;
    font-size: 14px;
    line-height: 1.45;
}
.cpf-preview__meta strong {
    display: block;
    color: #1f1b17;
}
.cpf-preview__meta .cpf-warning {
    color: #8a5a00;
}
.cpf-preview__meta .cpf-error {
    color: #a32020;
}
.cpf-preview__edge {
    position: absolute;
    z-index: 0;
    opacity: .78;
    pointer-events: none;
    background-color: var(--cpf-edge-color);
    background-size: cover;
    background-position: center;
}
.cpf-preview__edge--left,
.cpf-preview__edge--right {
    top: var(--cpf-edge);
    bottom: var(--cpf-edge);
    width: var(--cpf-edge);
}
.cpf-preview__edge--left {
    right: 100%;
    transform: skewY(38deg);
    transform-origin: right center;
}
.cpf-preview__edge--right {
    left: 100%;
    transform: skewY(-38deg);
    transform-origin: left center;
}
.cpf-preview__edge--top,
.cpf-preview__edge--bottom {
    left: var(--cpf-edge);
    right: var(--cpf-edge);
    height: var(--cpf-edge);
}
.cpf-preview__edge--top {
    bottom: 100%;
    transform: skewX(52deg);
    transform-origin: bottom center;
}
.cpf-preview__edge--bottom {
    top: 100%;
    transform: skewX(-52deg);
    transform-origin: top center;
}
.cpf-preview[data-edge-style="blur"] .cpf-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('cpf_customer_image'),
            sizeInput: document.getElementById('cpf_selected_size'),
            canvas: document.getElementById('cpf_preview_canvas'),
            mockup: document.getElementById('cpf_preview_mockup'),
            image: document.getElementById('cpf_preview_image'),
            placeholder: document.getElementById('cpf_preview_placeholder'),
            meta: document.getElementById('cpf_preview_meta'),
            wrapper: document.querySelector('.cpf-preview'),
            zoom: document.getElementById('cpf_zoom'),
            reset: document.getElementById('cpf_reset_crop'),
            toggle3d: document.getElementById('cpf_toggle_3d'),
            edgeColor: document.getElementById('cpf_edge_color'),
            cropX: document.getElementById('cpf_crop_x'),
            cropY: document.getElementById('cpf_crop_y'),
            cropScale: document.getElementById('cpf_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="cpf-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="cpf-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="cpf_edge_style"]:checked');
        return selected ? selected.value : 'mirror';
    }

    function frameDepth() {
        var selected = document.querySelector('input[name="cpf_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('--cpf-edge', edgeSize + 'px');
        fields.mockup.style.setProperty('--cpf-edge-color', edgeColor);

        Array.prototype.forEach.call(document.querySelectorAll('.cpf-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 === 'cpf_edge_style' || event.target.name === 'cpf_frame_depth' || event.target.id === 'cpf_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="cpf-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 === 'cpf_zoom') {
            state.scale = parseFloat(event.target.value || '1');
            applyImageTransform(fields);
        }

        if (event.target && event.target.id === 'cpf_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('plugins_loaded', static function (): void {
    new Canvas_Printing_Fulfillment();
    new CPF_Artwork_Wizard();
});
