<?php
defined('ABSPATH') || exit;

use Google\ApiCore\ApiException;
use Google\Shopping\Merchant\Products\V1\Client\ProductsServiceClient;
use Google\Shopping\Merchant\Products\V1\GetProductRequest;
use Google\Shopping\Merchant\Products\V1\Product;
use Google\Shopping\Merchant\Products\V1\ProductStatus;
use Google\Shopping\Merchant\Products\V1\ProductStatus\ItemLevelIssue\Severity;

/**
 * Plugin bootstrap: singleton that registers all WordPress hooks.
 *
 * Sync is queued via Action Scheduler (bundled with WooCommerce) when available,
 * so the admin request returns immediately and the sync runs in the background.
 * Falls back to inline execution if Action Scheduler is not present.
 *
 * Scheduled automatic delta sync runs twice daily via WP-Cron.
 */
class JRA_Merchant_Plugin {

    private static ?JRA_Merchant_Plugin $instance = null;

    private ?JRA_Merchant_Product_Mapper $cachedMapper     = null;
    private ?array                       $cachedMarketData = null;

    public static function instance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        $this->registerHooks();
    }

    // -------------------------------------------------------------------------
    // Activation / deactivation
    // -------------------------------------------------------------------------

    public static function activate(): void {
        self::runUpgrade();
    }

    /**
     * Idempotent upgrade routine — safe to call on every load.
     * Runs table creation and cron scheduling whenever the stored DB version
     * is behind the current plugin version (i.e. after any zip update).
     */
    public static function maybeUpgrade(): void {
        $stored = get_option('jra_merchant_db_version', '0');
        if (version_compare($stored, JRA_MERCHANT_VERSION, '<')) {
            self::runUpgrade();
        }
    }

    private static function runUpgrade(): void {
        JRA_Merchant_Logger::createTable();
        self::createTermMappingsTable();
        JRA_Merchant_Offer_Id_Map::createTable();

        if (!wp_next_scheduled('jra_merchant_cron_delta')) {
            wp_schedule_event(time(), 'twicedaily', 'jra_merchant_cron_delta');
        }

        update_option('jra_merchant_db_version', JRA_MERCHANT_VERSION, false);
    }

    public static function deactivate(): void {
        wp_clear_scheduled_hook('jra_merchant_cron_delta');
    }

    // -------------------------------------------------------------------------
    // Hook registration
    // -------------------------------------------------------------------------

    private function registerHooks(): void {
        if (is_admin()) {
            (new JRA_Merchant_Admin())->register();
        }

        // OAuth callback — user IS authenticated (they initiated the flow)
        add_action('admin_post_jra_merchant_oauth_callback', [$this, 'handleOauthCallback']);

        // Sync form submissions
        add_action('admin_post_jra_merchant_full_sync',  [$this, 'handleFullSync']);
        add_action('admin_post_jra_merchant_delta_sync', [$this, 'handleDeltaSync']);

        // Action Scheduler hooks (background execution)
        add_action('jra_merchant_run_full_sync',  [$this, 'runFullSync']);
        add_action('jra_merchant_run_delta_sync', [$this, 'runDeltaSync']);
        add_action('jra_merchant_run_report_scan', [new JRA_Merchant_Report_Service(), 'generateIssueReport']);

        // WP-Cron scheduled delta
        add_action('jra_merchant_cron_delta', [$this, 'runDeltaSync']);

        // Product edit screen: Merchant Feed tab in Product Data
        add_filter('woocommerce_product_data_tabs',   [$this, 'addMerchantFeedTab']);
        add_action('woocommerce_product_data_panels', [$this, 'renderMerchantFeedPanel']);
        add_action('admin_enqueue_scripts',           [$this, 'enqueueProductAssets']);
        add_action('woocommerce_process_product_meta',              [$this, 'saveExcludeMeta']);
        add_action('wp_ajax_jra_merchant_fetch_product_status',  [$this, 'handleFetchProductStatus']);
        add_action('wp_ajax_jra_merchant_sync_product',          [$this, 'handleSyncProduct']);
        add_action('wp_ajax_jra_merchant_preview_variation',     [$this, 'handlePreviewVariation']);
        add_action('wp_ajax_jra_merchant_preview_product',       [$this, 'handlePreviewProduct']);

        // Variation availability fields (_due_date, _lead_time)
        add_action('woocommerce_product_after_variable_attributes', [$this, 'renderVariationAvailabilityFields'], 10, 3);
        add_action('woocommerce_save_product_variation',            [$this, 'saveVariationAvailabilityFields'],   10, 2);
    }

    // -------------------------------------------------------------------------
    // OAuth callback handler
    // -------------------------------------------------------------------------

    public function handleOauthCallback(): void {
        if (!current_user_can('manage_options')) {
            wp_die('Insufficient permissions.', 403);
        }

        $code  = sanitize_text_field(wp_unslash($_GET['code']  ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $state = sanitize_text_field(wp_unslash($_GET['state'] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $error = sanitize_text_field(wp_unslash($_GET['error'] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended

        if ($error) {
            $this->setNotice('error', 'Google authorisation denied: ' . $error);
            wp_safe_redirect(admin_url('admin.php?page=jra-merchant-feed-settings'));
            exit;
        }

        try {
            (new JRA_Merchant_Auth())->handleCallback($code, $state);
            $this->setNotice('success', 'Successfully connected to Google Merchant Center.');
        } catch (Exception $e) {
            $this->setNotice('error', 'OAuth failed: ' . $e->getMessage());
        }

        wp_safe_redirect(admin_url('admin.php?page=jra-merchant-feed-settings'));
        exit;
    }

    // -------------------------------------------------------------------------
    // Sync HTTP handlers (redirect back immediately; run via Action Scheduler)
    // -------------------------------------------------------------------------

    public function handleFullSync(): void {
        if (!current_user_can('manage_woocommerce') || !check_admin_referer('jra_merchant_sync')) {
            wp_die('Insufficient permissions.', 403);
        }

        $this->dispatchSync('full');
        $this->setNotice('success', 'Full sync queued — check the log in a minute for results.');
        wp_safe_redirect(admin_url('admin.php?page=jra-merchant-feed'));
        exit;
    }

    public function handleDeltaSync(): void {
        if (!current_user_can('manage_woocommerce') || !check_admin_referer('jra_merchant_sync')) {
            wp_die('Insufficient permissions.', 403);
        }

        $this->dispatchSync('delta');
        $this->setNotice('success', 'Delta sync queued.');
        wp_safe_redirect(admin_url('admin.php?page=jra-merchant-feed'));
        exit;
    }

    private function dispatchSync(string $mode): void {
        $hook = "jra_merchant_run_{$mode}_sync";

        if (function_exists('as_enqueue_async_action')) {
            // Action Scheduler (WooCommerce): runs in background, no timeout risk
            as_enqueue_async_action($hook, [], 'jra-merchant-feed');
        } else {
            // Fallback: trigger via WP-Cron in 5 seconds
            wp_schedule_single_event(time() + 5, $hook);
            spawn_cron();
        }
    }

    // -------------------------------------------------------------------------
    // Sync runners (called by Action Scheduler or WP-Cron)
    // -------------------------------------------------------------------------

    public function runFullSync(): void {
        try {
            (new JRA_Merchant_Sync_Service())->fullSync();
        } catch (Throwable $e) {
            (new JRA_Merchant_Logger())->error('Full sync exception: ' . $e->getMessage(), 'full');
        }
    }

    public function runDeltaSync(): void {
        try {
            (new JRA_Merchant_Sync_Service())->deltaSync();
        } catch (Throwable $e) {
            (new JRA_Merchant_Logger())->error('Delta sync exception: ' . $e->getMessage(), 'delta');
        }
    }

    // -------------------------------------------------------------------------
    // Product Data tab: Merchant Feed
    // -------------------------------------------------------------------------

    public function addMerchantFeedTab(array $tabs): array {
        $tabs['jra_merchant_feed'] = [
            'label'    => 'Merchant Feed',
            'target'   => 'jra_merchant_feed_data',
            'class'    => [],
            'priority' => 90,
        ];
        return $tabs;
    }

    public function renderMerchantFeedPanel(): void {
        global $post;

        $product           = $post->ID > 0 ? wc_get_product($post->ID) : null;
        $isVariable        = $product && $product->is_type('variable');
        $isGrouped         = $product && $product->is_type('grouped');
        $isBundleComposite = $product && ($product->is_type('bundle') || $product->is_type('composite'));
        $offerIdData = (!$isVariable && !$isGrouped && $post->ID > 0) ? $this->getMapper()->resolveOfferId($post->ID) : null;
        ['markets' => $markets, 'defaultIdx' => $defaultIdx] = $this->getMarketToolbarData();
        ?>
        <div id="jra_merchant_feed_data" class="panel woocommerce_options_panel">
            <?php wp_nonce_field('jra_merchant_product_meta', '_jra_merchant_nonce'); ?>
            <div class="options_group">
                <?php
                woocommerce_wp_checkbox([
                    'id'          => 'jra_merchant_exclude',
                    'label'       => 'Exclude from Google Merchant feed',
                    'description' => 'When checked, this product and all its variations will not be synced to Google Merchant Center.',
                    'value'       => get_post_meta($post->ID, '_jra_merchant_exclude', true),
                    'cbvalue'     => '1',
                ]);
                woocommerce_wp_text_input([
                    'id'          => '_jra_custom_label',
                    'label'       => 'Custom label (label 4)',
                    'desc_tip'    => true,
                    'description' => 'Free-text value sent as custom_label_4 to Google. Use for Shopping campaign segmentation (e.g. "clearance", "new-arrival").',
                    'value'       => get_post_meta($post->ID, '_jra_custom_label', true),
                ]);

                if ($isBundleComposite):
                    if (jpffgmc_fs()->is__premium_only()):
                        $bundleSettings     = (new JRA_Merchant_Auth())->getSettings();
                        $bundlePriceMetaKey = $bundleSettings['bundle_price_meta_key'] ?? '';
                        $mappedPrice        = $bundlePriceMetaKey !== '' ? (float) get_post_meta($post->ID, $bundlePriceMetaKey, true) : 0.0;
                        $isMappedByKey      = $bundlePriceMetaKey !== '' && $mappedPrice > 0;
                        $manualPrice        = get_post_meta($post->ID, 'custom_price', true);
                    ?>
                <p class="form-field">
                    <label for="_jra_custom_price">Feed price</label>
                    <input type="number" id="_jra_custom_price" name="_jra_custom_price" min="0" step="0.01"
                           value="<?php echo esc_attr($isMappedByKey ? $mappedPrice : ($manualPrice ?: '')) ?>"
                           style="width:120px;"
                           <?php echo $isMappedByKey ? 'disabled' : '' ?>>
                    <?php if ($isMappedByKey): ?>
                    <span class="description" style="color:#646970;">Set by the <code><?php echo esc_html($bundlePriceMetaKey) ?></code> meta key (configured in <a href="<?php echo esc_url(admin_url('admin.php?page=jra-merchant-feed-settings')) ?>">plugin settings</a>).</span>
                    <?php elseif ($bundlePriceMetaKey !== ''): ?>
                    <span class="description" style="color:#646970;">The mapped meta key <code><?php echo esc_html($bundlePriceMetaKey) ?></code> has no value for this product — enter a price here to override.</span>
                    <?php else: ?>
                    <span class="description">Override price sent to Google for this bundle/composite product. Only needed when WooCommerce reports a price of zero.</span>
                    <?php endif; ?>
                </p>
                    <?php else: ?>
                <p class="form-field">
                    <label>Feed price</label>
                    <span class="description"><a href="<?php echo esc_url(jpffgmc_fs()->get_upgrade_url()) ?>">Upgrade to Pro</a> to set a custom feed price for bundle and composite products.</span>
                </p>
                    <?php endif; ?>
                <?php endif; ?>

                <?php if ($isGrouped):
                    $groupTitlePref = get_post_meta($post->ID, '_jra_group_title_prefix', true);
                ?>
                <p class="form-field">
                    <label for="_jra_group_title_prefix">Child title prefix</label>
                    <select id="_jra_group_title_prefix" name="_jra_group_title_prefix" style="width:auto;">
                        <option value=""  <?php selected($groupTitlePref, '')  ?>>Use global setting</option>
                        <option value="1" <?php selected($groupTitlePref, '1') ?>>Always combine (Parent — Child)</option>
                        <option value="0" <?php selected($groupTitlePref, '0') ?>>Never combine</option>
                    </select>
                    <span class="description">Override the global setting for children of this grouped product.</span>
                </p>
                <?php
                    $childIds = $product->get_children();
                    if (!empty($childIds)):
                ?>
                <p class="form-field" style="color:#646970;font-size:12px;margin-top:0;">
                    <strong><?php echo count($childIds) ?> child product<?php echo count($childIds) !== 1 ? 's' : '' ?></strong>
                    will be synced to Google as individual listings — the grouped parent itself is not submitted.
                </p>
                <?php
                    endif;
                endif;
                ?>
            </div>
            <div class="options_group jra-google-status-group">
                <div class="jra-status-toolbar">
                    <?php if ($isVariable): ?>
                    <span class="jra-offer-id-display" style="color:#646970;">
                        Variable product — each variation is synced as a separate listing
                    </span>
                    <?php elseif ($isGrouped): ?>
                    <span class="jra-offer-id-display" style="color:#646970;">
                        Grouped product — each child product is synced as a separate listing
                    </span>
                    <?php elseif ($offerIdData): ?>
                    <span class="jra-offer-id-display">
                        <strong>Offer ID:</strong> <code><?php echo esc_html($offerIdData['effective']) ?></code>
                        <?php if ($offerIdData['is_mapped']): ?>
                            <span class="jra-mapped-note">(mapped from <code><?php echo esc_html($offerIdData['local']) ?></code>)</span>
                        <?php endif; ?>
                    </span>
                    <?php endif; ?>
                    <button type="button" id="jra-fetch-google-status" class="button">Fetch status from Google</button>
                    <button type="button" id="jra-sync-product" class="button">Sync to Google</button>
                    <?php if (!$isVariable && !$isGrouped): ?>
                    <button type="button" id="jra-preview-product" class="button">Sync preview</button>
                    <?php endif; ?>
                    <span id="jra-sync-result" style="display:none;font-size:12px;margin-left:6px;"></span>
                    <?php if (!empty($markets)): ?>
                    <select id="jra-market-select">
                        <?php foreach ($markets as $i => $m): ?>
                        <option value="<?php echo esc_attr($m['country']) ?>:<?php echo esc_attr($m['language']) ?>"<?php echo $i === $defaultIdx ? ' selected' : '' ?>>
                            <?php echo esc_html($m['country']) ?> (<?php echo esc_html($m['language']) ?>)
                        </option>
                        <?php endforeach; ?>
                    </select>
                    <?php endif; ?>
                </div>
                <div id="jra-status-loading" style="display:none;padding:0 12px 8px;">
                    <span class="spinner is-active" style="float:none;vertical-align:middle;margin:0 4px 0 0;"></span>
                    Fetching from Google&hellip;
                </div>
                <div id="jra-status-error" style="display:none;padding:0 12px 8px;">
                    <p class="jra-text-error jra-status-error-msg" style="margin:0;"></p>
                </div>
                <div id="jra-market-view" style="padding:0 12px;"></div>
                <?php if (!$isVariable && !$isGrouped): ?>
                <div id="jra-preview-loading" style="display:none;padding:0 12px 8px;">
                    <span class="spinner is-active" style="float:none;vertical-align:middle;margin:0 4px 0 0;"></span>
                    Building preview&hellip;
                </div>
                <div id="jra-preview-error" style="display:none;padding:0 12px 8px;">
                    <p class="jra-text-error jra-preview-error-msg" style="margin:0;"></p>
                </div>
                <div id="jra-preview-view" style="padding:0 12px;"></div>
                <?php endif; ?>
            </div>
        </div>
        <?php
    }

    public function enqueueProductAssets(string $hook): void {
        if (!in_array($hook, ['post.php', 'post-new.php'], true)) {
            return;
        }
        global $typenow;
        if ($typenow !== 'product') {
            return;
        }
        wp_enqueue_style('jra-merchant-feed', JRA_MERCHANT_URL . 'assets/admin.css', [], JRA_MERCHANT_VERSION);
        wp_enqueue_script('jra-merchant-panel', JRA_MERCHANT_URL . 'assets/product-panel.js', ['jquery'], JRA_MERCHANT_VERSION, true);

        $productId  = isset($_GET['post']) ? absint($_GET['post']) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $variations = [];
        if ($productId) {
            $product = wc_get_product($productId);
            if ($product && $product->is_type('variable')) {
                foreach (array_slice($product->get_children(), 0, 20) as $varId) {
                    $var = wc_get_product($varId);
                    if (!$var) continue;
                    $label = wc_get_formatted_variation($var, true);
                    $variations[] = ['id' => $varId, 'label' => $label ?: '#' . $varId];
                }
            }
        }

        wp_localize_script('jra-merchant-panel', 'jraMerchantPanel', [
            'ajaxUrl'    => admin_url('admin-ajax.php'),
            'nonce'      => wp_create_nonce('jra_merchant_product_status'),
            'productId'  => $productId,
            'variations' => $variations,
        ]);
    }

    // -------------------------------------------------------------------------
    // AJAX: fetch product status from Google Merchant Center
    // -------------------------------------------------------------------------

    public function handleFetchProductStatus(): void {
        check_ajax_referer('jra_merchant_product_status', 'nonce');

        if (!current_user_can('manage_woocommerce')) {
            wp_send_json_error('Insufficient permissions.');
            return;
        }

        $productId = absint($_POST['product_id'] ?? 0);
        if (!$productId) {
            wp_send_json_error('Invalid product ID.');
            return;
        }

        try {
            $auth = new JRA_Merchant_Auth();

            if ($auth->getMerchantId() === '') {
                wp_send_json_error('No Merchant ID configured — check plugin Settings.');
                return;
            }
            if (!$auth->isConnected()) {
                wp_send_json_error('Not connected to Google — complete the OAuth flow in plugin Settings.');
                return;
            }

            $mapper  = new JRA_Merchant_Product_Mapper();
            $markets = $auth->getMarkets();

            $offerIdData = $mapper->resolveOfferId($productId);
            $offerId     = $offerIdData['effective'];

            // Resolve the requested market; fall back to first configured market
            $reqCountry     = strtoupper(explode(':', sanitize_text_field(wp_unslash($_POST['market'] ?? '')), 2)[0]); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $selectedMarket = null;
            foreach ($markets as $m) {
                if (strtoupper($m['country']) === $reqCountry) {
                    $selectedMarket = $m;
                    break;
                }
            }
            $selectedMarket = $selectedMarket ?? ($markets[0] ?? null);

            if (!$selectedMarket) {
                wp_send_json_error('No markets configured.');
                return;
            }

            $merchantId       = $auth->getMerchantId();
            $productIdSegment = "{$selectedMarket['language']}~{$selectedMarket['country']}~{$offerId}";
            $productsClient   = new ProductsServiceClient($auth->gapicClientOptions());
            $row = [
                'country'   => $selectedMarket['country'],
                'language'  => $selectedMarket['language'],
                'google_id' => $productIdSegment,
                'status'    => null,
                'issues'    => [],
                'product'   => null,
                'error'     => null,
            ];

            try {
                $product = $productsClient->getProduct(new GetProductRequest([
                    'name' => ProductsServiceClient::productName($merchantId, $productIdSegment),
                ]));
                $status = $product->getProductStatus();
                if ($status) {
                    $row['status'] = $this->formatProductStatus($status);
                    $row['issues'] = $this->formatProductIssues($status);
                }
                $row['product'] = $this->formatProductData($product);
            } catch (ApiException $e) {
                $row['error'] = $e->getStatus() === 'NOT_FOUND'
                    ? 'Product not found in Google Merchant Center for this market. It may not have been synced yet.'
                    : 'Google API error (' . $e->getStatus() . '): ' . $e->getBasicMessage();
            }

            wp_send_json_success([
                'offer_id'  => $offerId,
                'is_mapped' => $offerIdData['is_mapped'],
                'local_id'  => $offerIdData['local'],
                'market'    => $row,
            ]);

        } catch (\Throwable $e) {
            wp_send_json_error( esc_html( $e->getMessage() ) );
        }
    }

    public function handlePreviewVariation(): void {
        check_ajax_referer('jra_merchant_product_status', 'nonce');

        if (!current_user_can('manage_woocommerce')) {
            wp_send_json_error('Insufficient permissions.');
            return;
        }

        $variationId = absint($_POST['variation_id'] ?? 0);
        if (!$variationId) {
            wp_send_json_error('Invalid variation ID.');
            return;
        }

        try {
            $auth           = new JRA_Merchant_Auth();
            $markets        = $auth->getMarkets();
            $reqCountry     = strtoupper(explode(':', sanitize_text_field(wp_unslash($_POST['market'] ?? '')), 2)[0]);
            $selectedMarket = null;
            foreach ($markets as $m) {
                if (strtoupper($m['country']) === $reqCountry) {
                    $selectedMarket = $m;
                    break;
                }
            }
            $selectedMarket = $selectedMarket ?? ($markets[0] ?? null);

            $mapper      = new JRA_Merchant_Product_Mapper();
            $product     = $mapper->map($variationId, $selectedMarket);
            $offerIdData = $mapper->resolveOfferId($variationId);

            wp_send_json_success([
                'offer_id'  => $offerIdData['effective'],
                'is_mapped' => $offerIdData['is_mapped'],
                'local_id'  => $offerIdData['local'],
                'product'   => $this->formatPreviewData($product),
            ]);
        } catch (\Throwable $e) {
            wp_send_json_error($e->getMessage());
        }
    }

    public function handlePreviewProduct(): void {
        check_ajax_referer('jra_merchant_product_status', 'nonce');

        if (!current_user_can('manage_woocommerce')) {
            wp_send_json_error('Insufficient permissions.');
            return;
        }

        $productId = absint($_POST['product_id'] ?? 0);
        if (!$productId) {
            wp_send_json_error('Invalid product ID.');
            return;
        }

        try {
            $auth           = new JRA_Merchant_Auth();
            $markets        = $auth->getMarkets();
            $reqCountry     = strtoupper(explode(':', sanitize_text_field(wp_unslash($_POST['market'] ?? '')), 2)[0]);
            $selectedMarket = null;
            foreach ($markets as $m) {
                if (strtoupper($m['country']) === $reqCountry) {
                    $selectedMarket = $m;
                    break;
                }
            }
            $selectedMarket = $selectedMarket ?? ($markets[0] ?? null);

            $mapper      = new JRA_Merchant_Product_Mapper();
            $product     = $mapper->map($productId, $selectedMarket);
            $offerIdData = $mapper->resolveOfferId($productId);

            wp_send_json_success([
                'offer_id'  => $offerIdData['effective'],
                'is_mapped' => $offerIdData['is_mapped'],
                'local_id'  => $offerIdData['local'],
                'product'   => $this->formatPreviewData($product),
            ]);
        } catch (\Throwable $e) {
            wp_send_json_error($e->getMessage());
        }
    }

    private function formatPreviewData(\Google\Shopping\Merchant\Products\V1\ProductInput $productInput): array {
        $attrs = $productInput->getProductAttributes();
        $base  = $this->formatAttributesData($attrs);
        $desc  = (string) $attrs?->getDescription();

        return array_merge($base, [
            'offer_id'       => $productInput->getOfferId(),
            'item_group_id'  => $attrs?->getItemGroupId(),
            'description'    => $desc !== '' ? (mb_strlen($desc) > 200 ? mb_substr($desc, 0, 200) . '…' : $desc) : null,
            'custom_label_0' => $attrs?->getCustomLabel0(),
            'custom_label_1' => $attrs?->getCustomLabel1(),
            'custom_label_2' => $attrs?->getCustomLabel2(),
            'custom_label_3' => $attrs?->getCustomLabel3(),
            'custom_label_4' => $attrs?->getCustomLabel4(),
        ]);
    }

    public function handleSyncProduct(): void {
        check_ajax_referer('jra_merchant_product_status', 'nonce');

        if (!current_user_can('manage_woocommerce')) {
            wp_send_json_error('Insufficient permissions.');
            return;
        }

        $productId = absint($_POST['product_id'] ?? 0);
        if (!$productId) {
            wp_send_json_error('Invalid product ID.');
            return;
        }

        try {
            $auth = new JRA_Merchant_Auth();

            if ($auth->getMerchantId() === '') {
                wp_send_json_error('No Merchant ID configured — check plugin Settings.');
                return;
            }
            if (!$auth->isConnected()) {
                wp_send_json_error('Not connected to Google — complete the OAuth flow in plugin Settings.');
                return;
            }

            $result = (new JRA_Merchant_Sync_Service())->syncProducts([$productId]);

            if (!empty($result['errors'])) {
                wp_send_json_error(implode(' | ', $result['errors']));
                return;
            }

            wp_send_json_success([
                'upserted' => $result['upserted'],
                'message'  => sprintf('%d listing(s) synced to Google.', $result['upserted']),
            ]);

        } catch (\Throwable $e) {
            wp_send_json_error( esc_html( $e->getMessage() ) );
        }
    }

    private function formatProductStatus(ProductStatus $status): array {
        $destinations = [];
        foreach ($status->getDestinationStatuses() ?? [] as $dest) {
            $approved    = iterator_to_array($dest->getApprovedCountries()    ?? []);
            $disapproved = iterator_to_array($dest->getDisapprovedCountries() ?? []);
            $pending     = iterator_to_array($dest->getPendingCountries()     ?? []);
            // The API no longer returns a single overall status string per destination —
            // derive the same 'approved'/'disapproved'/'pending' badge the admin UI expects.
            $overallStatus = 'not_applicable';
            if (!empty($disapproved)) {
                $overallStatus = 'disapproved';
            } elseif (!empty($pending)) {
                $overallStatus = 'pending';
            } elseif (!empty($approved)) {
                $overallStatus = 'approved';
            }
            $destinations[] = [
                'destination'           => \Google\Shopping\Type\ReportingContext\ReportingContextEnum::name($dest->getReportingContext()),
                'status'                => $overallStatus,
                'approved_countries'    => $approved,
                'disapproved_countries' => $disapproved,
                'pending_countries'     => $pending,
            ];
        }
        return ['destinations' => $destinations];
    }

    private function formatProductIssues(ProductStatus $status): array {
        $issues = [];
        $seen   = [];
        foreach ($status->getItemLevelIssues() ?? [] as $issue) {
            // The API returns one entry per destination per issue; deduplicate by code+attribute.
            $key = $issue->getCode() . '|' . (string) $issue->getAttribute();
            if (isset($seen[$key])) {
                continue;
            }
            $seen[$key] = true;
            $issues[] = [
                'code'          => $issue->getCode(),
                'severity'      => strtolower(Severity::name($issue->getSeverity())),
                'resolution'    => $issue->getResolution(),
                'attribute'     => $issue->getAttribute(),
                'description'   => $issue->getDescription(),
                'detail'        => $issue->getDetail(),
                'documentation' => $issue->getDocumentation(),
                'countries'     => iterator_to_array($issue->getApplicableCountries() ?? []),
            ];
        }
        return $issues;
    }

    private function formatProductData(Product $product): array {
        return $this->formatAttributesData($product->getProductAttributes());
    }

    private function formatAttributesData(?\Google\Shopping\Merchant\Products\V1\ProductAttributes $attrs): array {
        if (!$attrs) {
            return [];
        }
        $price       = $attrs->getPrice();
        $rawCat      = $attrs->getGoogleProductCategory();
        $resolvedCat = $rawCat;
        if ($rawCat !== null && ctype_digit((string) $rawCat)) {
            $taxonomy    = (array) get_option('jra_merchant_google_taxonomy', []);
            $label       = $taxonomy[(int) $rawCat] ?? null;
            $resolvedCat = $label ? $rawCat . ' — ' . $label : $rawCat;
        }
        $gtins = iterator_to_array($attrs->getGtins() ?? []);
        return [
            'title'                   => $attrs->getTitle(),
            'availability'            => strtolower(\Google\Shopping\Merchant\Products\V1\Availability::name($attrs->getAvailability())),
            'availability_date'       => $attrs->getAvailabilityDate() ? $attrs->getAvailabilityDate()->toDateTime()->format(DATE_ATOM) : null,
            'brand'                   => $attrs->getBrand(),
            'gtin'                    => !empty($gtins) ? implode(', ', $gtins) : null,
            'mpn'                     => $attrs->getMpn(),
            'price'                   => $price ? number_format($price->getAmountMicros() / 1000000, 2) . ' ' . $price->getCurrencyCode() : null,
            'google_product_category' => $resolvedCat,
            'color'                   => $attrs->getColor(),
            'size'                    => $attrs->getSize() !== '' ? $attrs->getSize() : null,
            'material'                => $attrs->getMaterial(),
            'pattern'                 => $attrs->getPattern(),
            'age_group'               => strtolower(\Google\Shopping\Merchant\Products\V1\AgeGroup::name($attrs->getAgeGroup())),
            'gender'                  => strtolower(\Google\Shopping\Merchant\Products\V1\Gender::name($attrs->getGender())),
            'link'                    => $attrs->getLink(),
            'image'                   => $attrs->getImageLink(),
            'additional_images'       => iterator_to_array($attrs->getAdditionalImageLinks() ?? []),
        ];
    }

    // -------------------------------------------------------------------------
    // Product meta: save exclude flag
    // -------------------------------------------------------------------------

    public function saveExcludeMeta(int $postId): void {
        if (empty($_POST['_jra_merchant_nonce']) ||
            !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_jra_merchant_nonce'])), 'jra_merchant_product_meta')) {
            return;
        }

        $value = !empty($_POST['jra_merchant_exclude']) ? '1' : '0';
        update_post_meta($postId, '_jra_merchant_exclude', $value);

        $customLabel = sanitize_text_field(wp_unslash($_POST['_jra_custom_label'] ?? ''));
        update_post_meta($postId, '_jra_custom_label', $customLabel);

        // Bundle/composite: manual feed price (Pro only; skipped when overridden by a mapped meta key)
        if (isset($_POST['_jra_custom_price']) && jpffgmc_fs()->is__premium_only()) {
            $settings           = (new JRA_Merchant_Auth())->getSettings();
            $bundlePriceMetaKey = $settings['bundle_price_meta_key'] ?? '';
            $isMappedByKey      = $bundlePriceMetaKey !== '' &&
                                  (float) get_post_meta($postId, $bundlePriceMetaKey, true) > 0;
            if (!$isMappedByKey) {
                $customPrice = sanitize_text_field(wp_unslash($_POST['_jra_custom_price']));
                $customPrice = $customPrice !== '' ? max(0, (float) $customPrice) : '';
                if ($customPrice !== '') {
                    update_post_meta($postId, 'custom_price', $customPrice);
                } else {
                    delete_post_meta($postId, 'custom_price');
                }
            }
        }

        // Grouped product: per-product title prefix override ('1', '0', or '' for global)
        if (isset($_POST['_jra_group_title_prefix'])) {
            $groupTitlePref = sanitize_text_field(wp_unslash($_POST['_jra_group_title_prefix']));
            if (in_array($groupTitlePref, ['1', '0', ''], true)) {
                update_post_meta($postId, '_jra_group_title_prefix', $groupTitlePref);
            }
        }
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private function getMapper(): JRA_Merchant_Product_Mapper {
        return $this->cachedMapper ??= new JRA_Merchant_Product_Mapper();
    }

    private function getMarketToolbarData(): array {
        if ($this->cachedMarketData !== null) {
            return $this->cachedMarketData;
        }
        $markets = (new JRA_Merchant_Auth())->getMarkets();
        usort($markets, fn($a, $b) => strcmp($a['country'], $b['country']));
        $siteCountry = strtoupper(wc_get_base_location()['country'] ?? '');
        $defaultIdx  = 0;
        foreach ($markets as $i => $m) {
            if (strtoupper($m['country']) === $siteCountry) {
                $defaultIdx = $i;
                break;
            }
        }
        return $this->cachedMarketData = compact('markets', 'defaultIdx');
    }

    private function setNotice(string $type, string $message): void {
        set_transient('jra_merchant_notice', compact('type', 'message'), 60);
    }

    // -------------------------------------------------------------------------
    // Variation availability fields
    // -------------------------------------------------------------------------

    /**
     * Render _due_date and _lead_time inputs inside each variation panel.
     *
     * Uses $loop as the HTML input array key — this is standard WooCommerce
     * convention. The matching save hook receives the same $loop, so the
     * correct variation ID is always paired with the correct POST value.
     *
     * @param int     $loop           0-based variation index in this page render
     * @param array   $variationData  Variation data array (unused here)
     * @param WP_Post $variation      The variation post object
     */
    public function renderVariationAvailabilityFields(int $loop, array $variationData, \WP_Post $variation): void
    {
        $variationId     = $variation->ID;
        $pluginSettings  = (array) get_option('jra_merchant_settings', []);
        $dueDateMetaKey  = $pluginSettings['due_date_meta_key']  ?? '';
        $leadTimeMetaKey = $pluginSettings['lead_time_meta_key'] ?? '';
        $dueDate         = get_post_meta($variationId, '_due_date',  true);
        $leadTime        = get_post_meta($variationId, '_lead_time', true);

        // When due date is mapped, compute the interpreted value to show in the disabled input
        $mappedDueDateDisplay = '';
        if ($dueDateMetaKey !== '') {
            $rawMapped    = trim((string) get_post_meta($variationId, $dueDateMetaKey, true));
            $mappedFormat = ($pluginSettings['due_date_meta_format'] ?? '') ?: 'Y/m/d';
            if ($rawMapped !== '') {
                $parsed = DateTime::createFromFormat($mappedFormat, $rawMapped, new DateTimeZone('UTC'));
                $mappedDueDateDisplay = $parsed ? $parsed->format('Y-m-d') : 'Invalid date';
            }
        }

        $excludedVariation = get_post_meta($variationId, '_jra_merchant_exclude', true) === '1';
        $offerIdData       = $this->getMapper()->resolveOfferId($variationId);
        ['markets' => $markets, 'defaultIdx' => $defaultIdx] = $this->getMarketToolbarData();
        ?>
        <div class="jra-var-section">
            <div class="jra-var-section__head">
                <span class="dashicons dashicons-google" style="font-size:14px;width:14px;height:14px;line-height:1;margin-right:5px;vertical-align:middle;"></span>
                Google Merchant Feed
            </div>
            <div class="jra-var-section__body">
                <div class="jra-status-toolbar" style="padding:0 0 10px;border-bottom:1px solid #f0f0f0;margin-bottom:10px;">
                    <span class="jra-offer-id-display">
                        <strong>Offer ID:</strong> <code><?php echo esc_html($offerIdData['effective']) ?></code>
                        <?php if ($offerIdData['is_mapped']): ?>
                            <span class="jra-mapped-note">(mapped from <code><?php echo esc_html($offerIdData['local']) ?></code>)</span>
                        <?php endif; ?>
                    </span>
                    <button type="button"
                            class="button button-small jra-fetch-variation-status"
                            data-variation-id="<?php echo esc_attr($variationId) ?>"
                            data-loop="<?php echo esc_attr($loop) ?>">
                        Fetch status
                    </button>
                    <button type="button"
                            class="button button-small jra-preview-variation"
                            data-variation-id="<?php echo esc_attr($variationId) ?>"
                            data-loop="<?php echo esc_attr($loop) ?>">
                        Sync preview
                    </button>
                    <?php if (!empty($markets)): ?>
                    <select class="jra-variation-market-select" data-loop="<?php echo esc_attr($loop) ?>" style="font-size:12px;">
                        <?php foreach ($markets as $i => $m): ?>
                        <option value="<?php echo esc_attr($m['country']) ?>:<?php echo esc_attr($m['language']) ?>"<?php echo $i === $defaultIdx ? ' selected' : '' ?>>
                            <?php echo esc_html($m['country']) ?> (<?php echo esc_html($m['language']) ?>)
                        </option>
                        <?php endforeach; ?>
                    </select>
                    <?php endif; ?>
                </div>
                <div id="jra-status-loading-<?php echo esc_attr($loop) ?>" style="display:none;margin-bottom:8px;">
                    <span class="spinner is-active" style="float:none;vertical-align:middle;margin:0 4px 0 0;"></span>
                    Fetching from Google&hellip;
                </div>
                <div id="jra-status-error-<?php echo esc_attr($loop) ?>" style="display:none;margin-bottom:8px;">
                    <p class="jra-text-error jra-status-error-msg" style="margin:0;"></p>
                </div>
                <div id="jra-market-view-<?php echo esc_attr($loop) ?>" style="margin-bottom:10px;"></div>
                <div id="jra-preview-loading-<?php echo esc_attr($loop) ?>" style="display:none;margin-bottom:8px;">
                    <span class="spinner is-active" style="float:none;vertical-align:middle;margin:0 4px 0 0;"></span>
                    Building preview&hellip;
                </div>
                <div id="jra-preview-error-<?php echo esc_attr($loop) ?>" style="display:none;margin-bottom:8px;">
                    <p class="jra-text-error jra-preview-error-msg" style="margin:0;"></p>
                </div>
                <div id="jra-preview-view-<?php echo esc_attr($loop) ?>" style="margin-bottom:10px;"></div>
                <p class="form-field" style="margin:0 0 10px;">
                    <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
                        <input type="checkbox"
                               name="_jra_merchant_exclude_variation[<?php echo esc_attr($loop) ?>]"
                               value="1"
                               <?php echo $excludedVariation ? 'checked' : '' ?>>
                        <span style="font-size:13px;">Exclude from Google Merchant feed</span>
                    </label>
                    <?php if ($excludedVariation): ?>
                    <span style="display:block;color:#d63638;font-size:11px;margin-top:4px;">
                        This variation will not be pushed to Google Merchant Center.
                    </span>
                    <?php endif; ?>
                </p>
                <p style="margin:0 0 8px;font-weight:600;font-size:12px;color:#555;">
                    Availability
                    <span style="font-weight:400;color:#999;margin-left:4px;">Used when out of stock</span>
                </p>
                <div style="display:flex;gap:20px;flex-wrap:wrap;">
                    <p class="form-field" style="margin:0;flex:1;min-width:180px;">
                        <label for="_jra_due_date_<?php echo esc_attr($loop) ?>" style="display:block;margin-bottom:4px;font-size:12px;">
                            Due date <span style="color:#999">(YYYY-MM-DD)</span>
                        </label>
                        <input type="<?php echo $dueDateMetaKey !== '' ? 'text' : 'date' ?>"
                               id="_jra_due_date_<?php echo esc_attr($loop) ?>"
                               name="_jra_due_date[<?php echo esc_attr($loop) ?>]"
                               value="<?php echo esc_attr($dueDateMetaKey !== '' ? $mappedDueDateDisplay : $dueDate) ?>"
                               style="width:100%;max-width:180px;<?php echo $dueDateMetaKey !== '' ? 'opacity:0.5;background:#f6f7f7;' : '' ?>"
                               <?php echo $dueDateMetaKey !== '' ? 'disabled' : '' ?>>
                        <?php if ($dueDateMetaKey !== ''): ?>
                        <span style="display:block;font-size:11px;color:#9e6c00;margin-top:3px;">mapped from <code><?php echo esc_html($dueDateMetaKey) ?></code></span>
                        <?php else: ?>
                        <span style="display:block;font-size:11px;color:#999;margin-top:3px;">Expected restock (from PO or manual)</span>
                        <?php endif; ?>
                    </p>
                    <p class="form-field" style="margin:0;flex:1;min-width:120px;">
                        <label for="_jra_lead_time_<?php echo esc_attr($loop) ?>" style="display:block;margin-bottom:4px;font-size:12px;">
                            Lead time <span style="color:#999">(days)</span>
                        </label>
                        <input type="number"
                               id="_jra_lead_time_<?php echo esc_attr($loop) ?>"
                               name="_jra_lead_time[<?php echo esc_attr($loop) ?>]"
                               value="<?php echo esc_attr($leadTime) ?>"
                               min="0"
                               step="1"
                               style="width:80px;<?php echo $leadTimeMetaKey !== '' ? 'opacity:0.5;' : '' ?>"
                               <?php echo $leadTimeMetaKey !== '' ? 'disabled' : '' ?>>
                        <?php if ($leadTimeMetaKey !== ''): ?>
                        <span style="display:block;font-size:11px;color:#9e6c00;margin-top:3px;">mapped from <code><?php echo esc_html($leadTimeMetaKey) ?></code></span>
                        <?php else: ?>
                        <span style="display:block;font-size:11px;color:#999;margin-top:3px;">Days to order/manufacture</span>
                        <?php endif; ?>
                    </p>
                </div>
            </div>
        </div>
        <?php
    }

    /**
     * Save _due_date and _lead_time for a variation.
     * The $loop value matches what was used as the array key in the input names.
     */
    public function saveVariationAvailabilityFields(int $variationId, int $loop): void
    {
        $pluginSettings  = (array) get_option('jra_merchant_settings', []);
        $dueDateMetaKey  = $pluginSettings['due_date_meta_key']  ?? '';
        $leadTimeMetaKey = $pluginSettings['lead_time_meta_key'] ?? '';

        // Due date — skip if mapped to an external meta key (external system owns the value)
        if ($dueDateMetaKey === '') {
            $dueDate = sanitize_text_field(wp_unslash($_POST['_jra_due_date'][$loop] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Missing
            if ($dueDate !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dueDate)) {
                $dueDate = '';
            }
            update_post_meta($variationId, '_due_date', $dueDate);
        }

        // Lead time — skip if mapped to an external meta key (external system owns the value)
        if ($leadTimeMetaKey === '') {
            $rawLeadTime = sanitize_text_field(wp_unslash($_POST['_jra_lead_time'][$loop] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Missing
            $leadTime    = $rawLeadTime !== '' ? max(0, absint($rawLeadTime)) : '';
            update_post_meta($variationId, '_lead_time', $leadTime !== 0 ? $leadTime : '');
        }

        // Exclude from Google Merchant feed
        $excludeVariation = !empty($_POST['_jra_merchant_exclude_variation'][$loop]) ? '1' : '0'; // phpcs:ignore WordPress.Security.NonceVerification.Missing
        update_post_meta($variationId, '_jra_merchant_exclude', $excludeVariation);
    }

    // -------------------------------------------------------------------------
    // Schema
    // -------------------------------------------------------------------------

    private static function createTermMappingsTable(): void {
        global $wpdb;
        $table   = $wpdb->prefix . 'jra_merchant_term_mappings';
        $charset = $wpdb->get_charset_collate();

        $sql = "CREATE TABLE IF NOT EXISTS {$table} (
            id               int(11)      NOT NULL AUTO_INCREMENT,
            google_attribute varchar(50)  NOT NULL,
            local_value      varchar(255) NOT NULL,
            google_value     varchar(255) NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY  uq_attr_local (google_attribute, local_value(191))
        ) {$charset};";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta($sql);
    }
}
