<?php

namespace Pixeldev\SquareWooSync\Woo;

use Pixeldev\SquareWooSync\Logger\Logger;
use Pixeldev\SquareWooSync\Square\SquareHelper;
use Pixeldev\SquareWooSync\Woo\ValidateWooProduct;  // your custom validator

use Automattic\WooCommerce\Utilities\NumberUtil;
use ActionScheduler;

if (!defined('ABSPATH')) {
  exit; // Exit if accessed directly
}

class WooImport
{
  private $square_categories = [];

  public function __construct() {}

  public function init()
  {
    // 1) Add multiple bulk actions
    // 2) Handle all of them in a single method
    add_filter('handle_bulk_actions-edit-product', [$this, 'handle_bulk_action'], 10, 3);

    // 3) Admin notices
    add_action('admin_notices', [$this, 'bulk_action_notices']);

    // 4) Cron job for actual exporting
    add_action('squarewoosync_export_bulk_products', [$this, 'squarewoosync_export_products'], 10, 2);



    add_action('pmxi_after_xml_import', [$this, 'wp_all_import_completed'], 10, 1);
    add_action('pmxi_saved_post', [$this, 'wp_all_import_add_meta'], 10, 3);
    // 5) (Optional) Cron job for actual syncing from Square
    //    You can create your own hooking logic. For demonstration only:
    // add_action('squarewoosync_sync_bulk_products', [$this, 'squarewoosync_sync_products'], 10, 2);
  }

  public function wp_all_import_add_meta($post_id, $xml_node, $is_update)
  {
    // Only run for products.
    if ('product' !== get_post_type($post_id)) {
      return;
    }

    // If _pmxi_import_id is not set, add it.
    if (empty(get_post_meta($post_id, '_pmxi_import_id', true))) {
      if (function_exists('wp_all_import_get_import_id')) {
        $import_id = wp_all_import_get_import_id();
        update_post_meta($post_id, '_pmxi_import_id', $import_id);
      }
    }
  }

  /**
   * Fires after WP All Import completes an import.
   *
   * @param int $import_id The ID of the completed import.
   */
  public function wp_all_import_completed($import_id)
  {
    $settings = get_option('square-woo-sync_settings', []);


    if (empty($settings['wooAuto']['autoCreateProduct']) || !$settings['wooAuto']['autoCreateProduct']) {
      return;
    }

    // 1) Fetch all product IDs that have _pmxi_import_id = this import.
    $args = [
      'post_type' => 'product',
      'post_status' => 'any',
      'fields' => 'ids',
      'posts_per_page' => -1,
      'meta_query' => [
        [
          'key' => '_pmxi_import_id',
          'value' => $import_id,
        ]
      ],
    ];
    $query = new \WP_Query($args);
    $product_ids = $query->posts;


    // 2) If no products found, nothing to do.
    if (empty($product_ids)) {
      return;
    }

    // 3) Filter out products that already have a square_product_id.
    $filtered_ids = array_filter($product_ids, function ($pid) {
      return empty(get_post_meta($pid, 'square_product_id', true));
    });


    if (empty($filtered_ids)) {
      return;
    }

    // 4) Schedule the export job.
    $this->schedule_export_job($filtered_ids);
  }



  /**
   * -----------------------------------------------------------------
   * (A) REGISTER THE BULK ACTIONS
   * -----------------------------------------------------------------
   */
  public function register_bulk_actions($bulk_actions)
  {
    // Existing: Exports only the selected product IDs
    $bulk_actions['squarewoosync_schedule_import_to_square'] = __('Export to Square', 'squarewoosync');

    // NEW: Exports all products (across all pages) that match current filters
    $bulk_actions['squarewoosync_schedule_import_to_square_all'] = __('Export to Square (All Pages)', 'squarewoosync');

    // // OPTIONAL: Add two bulk actions for syncing from Square
    // $bulk_actions['squarewoosync_schedule_sync_from_square'] = __('Sync from Square', 'squarewoosync');
    // $bulk_actions['squarewoosync_schedule_sync_from_square_all'] = __('Sync from Square (All Pages)', 'squarewoosync');

    return $bulk_actions;
  }

  /**
   * -----------------------------------------------------------------
   * (B) HANDLE BULK ACTION: DECIDE IF WE EXPORT SELECTED OR ALL
   * -----------------------------------------------------------------
   */
  public function handle_bulk_action($redirect_to, $doaction, $post_ids)
  {
    // Identify which of the four possible actions we're handling:
    switch ($doaction) {

      /**
       * Export: Only the selected product IDs
       */
      case 'squarewoosync_schedule_import_to_square':
        // Possibly check if a job is already scheduled
        $next_scheduled = as_next_scheduled_action('squarewoosync_export_bulk_products');
        if ($next_scheduled) {
          $redirect_to = add_query_arg('squarewoosync_cron_already_scheduled', '1', $redirect_to);
          return $redirect_to;
        }
        // Schedule the selected products
        $this->schedule_export_job($post_ids);
        $redirect_to = add_query_arg('squarewoosync_cron_scheduled', '1', $redirect_to);
        return $redirect_to;

      /**
         * Export: All matching products across all pages
         */
      case 'squarewoosync_schedule_import_to_square_all':
        // If a job is running, skip
        $next_scheduled = as_next_scheduled_action('squarewoosync_export_bulk_products');
        if ($next_scheduled) {
          $redirect_to = add_query_arg('squarewoosync_cron_already_scheduled', '1', $redirect_to);
          return $redirect_to;
        }
        // 1) Gather all product IDs for current filter
        $all_ids = $this->get_all_filtered_product_ids();
        // 2) Schedule them
        $this->schedule_export_job($all_ids);
        $redirect_to = add_query_arg('squarewoosync_cron_scheduled', '1', $redirect_to);
        return $redirect_to;

        // /**
        //  * Sync from Square: Only the selected product IDs
        //  * (Placeholder example – adapt as needed)
        //  */
        // case 'squarewoosync_schedule_sync_from_square':
        //   $next_scheduled = as_next_scheduled_action('squarewoosync_sync_bulk_products');
        //   if ($next_scheduled) {
        //     $redirect_to = add_query_arg('squarewoosync_cron_already_scheduled', '1', $redirect_to);
        //     return $redirect_to;
        //   }
        //   $this->schedule_sync_job($post_ids);
        //   $redirect_to = add_query_arg('squarewoosync_cron_scheduled', '1', $redirect_to);
        //   return $redirect_to;

        // /**
        //  * Sync from Square: All matching products (across pages)
        //  */
        // case 'squarewoosync_schedule_sync_from_square_all':
        //   $next_scheduled = as_next_scheduled_action('squarewoosync_sync_bulk_products');
        //   if ($next_scheduled) {
        //     $redirect_to = add_query_arg('squarewoosync_cron_already_scheduled', '1', $redirect_to);
        //     return $redirect_to;
        //   }
        //   $all_ids = $this->get_all_filtered_product_ids();
        //   $this->schedule_sync_job($all_ids);
        //   $redirect_to = add_query_arg('squarewoosync_cron_scheduled', '1', $redirect_to);
        //   return $redirect_to;
    }

    // If it's none of our custom actions, just return
    return $redirect_to;
  }

  /**
   * Add an admin notice if we used one of our actions
   */
  public function bulk_action_notices()
  {
    if (!empty($_REQUEST['squarewoosync_cron_scheduled'])) {
      echo '<div class="notice notice-success is-dismissible"><p>' . __('Export/Sync job initiated.', 'squarewoosync') . '</p></div>';
    }
    if (!empty($_REQUEST['squarewoosync_cron_already_scheduled'])) {
      echo '<div class="notice notice-warning is-dismissible"><p>' . __('A job is already running. Please wait until it finishes.', 'squarewoosync') . '</p></div>';
    }
  }

  /**
   * -----------------------------------------------------------------
   * (C) SCHEDULE EXPORT (CHUNK + ACTIONSCHEDULER)
   * -----------------------------------------------------------------
   */
  private function schedule_export_job(array $post_ids)
  {
    if (empty($post_ids)) {
      return;
    }

    // Master process ID for logging
    $masterProcessId = wp_generate_uuid4();

    // Chunk them up
    $chunks = array_chunk($post_ids, 10);

    // For spacing out each chunk
    $initial_delay = 10;
    $delay_increment = 5;
    $batch_index = 0;

    foreach ($chunks as $batch) {
      $batch_index++;
      $batchProcessId = wp_generate_uuid4();

      // Each chunk schedules a single action
      as_schedule_single_action(
        time() + $initial_delay,
        'squarewoosync_export_bulk_products',
        [
          'process_id' => $batchProcessId,
          'post_ids' => $batch,
        ],
        'squarewoo-sync'
      );

      // Log scheduling
      Logger::log(
        'info',
        sprintf(
          'Scheduled export batch #%d with %d product(s). Master Process: %s',
          $batch_index,
          count($batch),
          $masterProcessId
        ),
        [
          'batch_index' => $batch_index,
          'process_id' => $batchProcessId,
          'master_process_id' => $masterProcessId
        ]
      );

      $initial_delay += $delay_increment;
    }

    // Final log
    Logger::log(
      'success',
      sprintf(
        'All %d product(s) split into %d batch(es) for export. Master Process: %s',
        count($post_ids),
        $batch_index,
        $masterProcessId
      ),
      [
        'master_process_id' => $masterProcessId,
        'total_batches' => $batch_index
      ]
    );
  }

  /**
   * -----------------------------------------------------------------
   * (D) SCHEDULE SYNC (CHUNK + ACTIONSCHEDULER) – OPTIONAL EXAMPLE
   * -----------------------------------------------------------------
   * This is a placeholder. Adapt to your actual “sync from Square” logic.
   */
  private function schedule_sync_job(array $post_ids)
  {
    if (empty($post_ids)) {
      return;
    }

    $masterProcessId = wp_generate_uuid4();
    $chunks = array_chunk($post_ids, 5);

    $initial_delay = 10;
    $delay_increment = 5;
    $batch_index = 0;

    foreach ($chunks as $batch) {
      $batch_index++;
      $batchProcessId = wp_generate_uuid4();

      as_schedule_single_action(
        time() + $initial_delay,
        'squarewoosync_sync_bulk_products', // a separate hook for syncing
        [
          'process_id' => $batchProcessId,
          'post_ids' => $batch,
        ],
        'squarewoo-sync'
      );

      Logger::log(
        'info',
        sprintf(
          'Scheduled sync batch #%d with %d product(s). Master Process: %s',
          $batch_index,
          count($batch),
          $masterProcessId
        ),
        [
          'batch_index' => $batch_index,
          'process_id' => $batchProcessId,
          'master_process_id' => $masterProcessId
        ]
      );

      $initial_delay += $delay_increment;
    }

    Logger::log(
      'success',
      sprintf(
        'All %d product(s) split into %d batch(es) for sync. Master Process: %s',
        count($post_ids),
        $batch_index,
        $masterProcessId
      ),
      [
        'master_process_id' => $masterProcessId,
        'total_batches' => $batch_index
      ]
    );
  }

  /**
   * -----------------------------------------------------------------
   * (E) GATHER ALL FILTERED PRODUCT IDS (FOR "...(All Pages)" ACTION)
   * -----------------------------------------------------------------
   */
  private function get_all_filtered_product_ids()
  {
    // Ensure we’re on the correct screen:
    $screen = get_current_screen();
    if (!$screen || $screen->id !== 'edit-product') {
      return [];
    }

    // Pull the main WP_Query from the global
    global $wp_query;

    // Copy the query vars (these include all filters applied in the admin)
    $query_vars = $wp_query->query_vars;
    $query_vars['posts_per_page'] = -1;
    $query_vars['fields'] = 'ids';
    unset($query_vars['paged']);
    $query_vars['post_type'] = 'product';
    $query_vars['no_found_rows'] = true;
    $all_query = new \WP_Query($query_vars);

    return $all_query->posts; // array of product IDs
  }


  /**
   * -----------------------------------------------------------------
   * (F) CRON JOB CALLBACK FOR EXPORT
   * -----------------------------------------------------------------
   */
  public function squarewoosync_export_products($process_id, $post_ids)
  {
    if (!isset($process_id) || !isset($post_ids)) {
      return;
    }

    set_transient('squarewoosync_export_running', true, 15 * MINUTE_IN_SECONDS);

    $products = [];
    foreach ($post_ids as $post_id) {
      $p = wc_get_product($post_id);
      if ($p) {
        $products[] = $p;
      }
    }

    if (!empty($products)) {
      Logger::log(
        'success',
        'Started exporting products to Square',
        [
          'parent_id' => $process_id,
          'product_count' => count($products)
        ]
      );

      $response = $this->import_products($process_id, $products, 1, 50);
      $inventoryResult = $this->update_square_inventory_counts($products);

      if (is_wp_error($response)) {
        $error_message = $response->get_error_message();
        Logger::log('error', 'Error during product export: ' . $error_message, ['parent_id' => $process_id]);

        // Clean the meta for every product in this batch so we can try again later
        foreach ($products as $product) {
          $this->remove_square_meta($product);
        }

        delete_transient('squarewoosync_export_running');
        return;
      } else {
        // partial success/fail
        if (!empty($response['failures'])) {
          foreach ($response['failures'] as $fail) {
            $pid = $fail['product_id'];
            $name = $fail['title'];
            $errs = implode('; ', $fail['errors']);
            Logger::log(
              'error',
              "Failed to export product ID {$pid} ({$name}): {$errs}",
              ['parent_id' => $process_id]
            );

            // Remove stale meta, the export definitely failed for this product
            $this->remove_square_meta(wc_get_product($pid));
          }
        }

        if (!empty($response['square_responses'])) {
          foreach ($response['square_responses'] as $resp) {
            if (!empty($resp['success']) && !empty($resp['data']['objects'])) {
              foreach ($resp['data']['objects'] as $object) {
                if ($object['type'] === 'ITEM' && isset($object['item_data']['name'])) {
                  $product_name = $object['item_data']['name'];
                  Logger::log(
                    'success',
                    'Product "' . $product_name . '" exported to Square successfully.',
                    ['parent_id' => $process_id]
                  );
                }
              }
            } else {
              // Whole batch failed – wipe for safety
              foreach ($products as $product) {
                $this->remove_square_meta($product);
              }
              Logger::log('error', 'Export may have failed or no objects returned.', ['parent_id' => $process_id]);
            }
          }
        } else {
          // Unrecognised response – wipe for safety
          foreach ($products as $product) {
            $this->remove_square_meta($product);
          }
          Logger::log('error', 'Unexpected response format or no Square responses.', ['parent_id' => $process_id]);
        }
      }
    } else {
      Logger::log('info', 'No products found to export', ['parent_id' => $process_id]);
    }

    delete_transient('squarewoosync_export_running');
  }

  /**
   * -----------------------------------------------------------------
   * (G) CRON JOB CALLBACK FOR SYNC (PLACEHOLDER EXAMPLE)
   * -----------------------------------------------------------------
   */
  public function squarewoosync_sync_products($process_id, $post_ids)
  {
    if (!isset($process_id) || !isset($post_ids)) {
      return;
    }

    // This is just an example.  
    Logger::log('info', "Syncing from Square (placeholder) for process: {$process_id}", [
      'product_count' => count($post_ids)
    ]);

    // You could implement your own logic to fetch each product from Square
    // and update WooCommerce accordingly.
  }

  /**
   * @param string $process_id
   * @param \WC_Product[] $products
   * @param int $exportSynced
   * @param int $per_batch
   * @param array $fields_to_export e.g. ['images' => true, 'categories' => true]
   */
  public function import_products($process_id, $products = [], $exportSynced = 1, $per_batch = 50, $fields_to_export = [])
  {
    if (!class_exists('WooCommerce')) {
      return new \WP_Error('missing_dependency', __('WooCommerce must be installed.', 'squarewoosync'));
    }

    $default_fields = [
      'images' => true,
      'categories' => true,
    ];
    $fields_to_export = array_merge($default_fields, $fields_to_export);

    $allResults = [
      'square_responses' => [],
      'failures' => [],
    ];

    $square = new SquareHelper();

    try {
      if (empty($products)) {
        // If no product array is given, fetch published products in pages
        $page = 1;
        $has_more = true;

        while ($has_more) {
          $result = wc_get_products([
            'status' => 'publish',
            'limit' => $per_batch,
            'page' => $page,
            'paginate' => true,
          ]);

          $page_products = $result->products;
          $has_more = ($page < $result->max_num_pages);

          $chunk_result = $this->process_products($page_products, $square, $exportSynced, $process_id, $fields_to_export);
          if (is_wp_error($chunk_result)) {
            return $chunk_result;
          }
          $allResults['square_responses'] = array_merge($allResults['square_responses'], $chunk_result['square_responses']);
          $allResults['failures'] = array_merge($allResults['failures'], $chunk_result['failures']);
          $page++;
        }
      } else {
        // We already have an array of products
        $chunks = array_chunk($products, $per_batch);
        foreach ($chunks as $chunk) {
          $chunk_result = $this->process_products($chunk, $square, $exportSynced, $process_id, $fields_to_export);
          if (is_wp_error($chunk_result)) {
            return $chunk_result;
          }
          $allResults['square_responses'] = array_merge($allResults['square_responses'], $chunk_result['square_responses']);
          $allResults['failures'] = array_merge($allResults['failures'], $chunk_result['failures']);
        }
      }

      return $allResults;
    } catch (\Exception $e) {
      $opts = get_option('square-woo-sync_settings', []);
      $opts['exportStatus'] = 0;
      $opts['exportResults'] = ['error' => $e->getMessage()];
      update_option('square-woo-sync_settings', $opts);

      return new \WP_Error(
        'import_failed',
        __('Failed to import products: ', 'squarewoosync') . $e->getMessage()
      );
    }
  }

  private function process_products($products, $square, $exportSynced, $process_id, $fields_to_export = [])
  {
    $results = [
      'square_responses' => [],
      'failures' => [],
    ];

    try {
      $square_item_options = $this->fetch_square_item_options($square);

      $square_data = [
        'idempotency_key' => wp_generate_uuid4(),
        'batches' => [],
      ];
      $batch = [
        'objects' => []
      ];

      // Use your custom validator
      $validator = new ValidateWooProduct();

      foreach ($products as $product) {
        $product_id = $product->get_id();
        $product_name = $product->get_name();

        // If not re-exporting synced products, skip them
        if (!$exportSynced && get_post_meta($product_id, 'square_product_id', true)) {
          continue;
        }

        // Validate
        $fields_core      = array_diff_key($fields_to_export, ['images' => true]);
        $image_only_field = ['images' => true];

        $check_core  = $validator->validateProduct($product_id, $fields_core);
        $image_check = $validator->validateProduct($product_id, $image_only_field);

        if (!$check_core['success']) {               // critical data failed → skip
          $results['failures'][] = [
            'product_id' => $product_id,
            'title'      => $product_name,
            'errors'     => $check_core['reasons'],
          ];
          continue;
        }

        // Image issues are warnings; keep going.
        foreach ($image_check['reasons'] as $warn) {
          Logger::log('warning', "{$product_name}: {$warn}", ['parent_id' => $process_id]);
        }

        // Format for Square
        try {
          $formatted = $this->format_product_for_square($product, $square_item_options, $square, $process_id, $fields_to_export);
          if ($formatted === null) {
            $results['failures'][] = [
              'product_id' => $product_id,
              'title' => $product_name,
              'errors' => ['Formatting product returned null or was skipped.'],
            ];
            continue;
          }
          $batch['objects'][] = $formatted;
        } catch (\Exception $ex) {
          $results['failures'][] = [
            'product_id' => $product_id,
            'title' => $product_name,
            'errors' => [$ex->getMessage()],
          ];
          continue;
        }
      }

      if (!empty($batch['objects'])) {
        $square_data['batches'][] = $batch;
        $response = $square->square_api_request('/catalog/batch-upsert', 'POST', $square_data);
        if ($response['success'] === false) {
          return new \WP_Error(
            'process_failed',
            __('Square batch-upsert failed: ', 'squarewoosync') . $response['error']
          );
        }

        // Update local meta
        $this->update_wc_product_with_square_ids($response);

        $results['square_responses'][] = $response;
      }

      return $results;
    } catch (\Exception $e) {
      return new \WP_Error(
        'process_failed',
        __('Failed to process products: ', 'squarewoosync') . $e->getMessage()
      );
    }
  }

  /**
   * Fetch every ITEM_OPTION from Square – follows the pagination cursor.
   *
   * @return array
   */
  public function fetch_square_item_options(SquareHelper $square): array
  {
    $cursor = null;
    $options = [];

    do {
      $endpoint = '/catalog/list?types=ITEM_OPTION';
      if ($cursor) {
        $endpoint .= '&cursor=' . rawurlencode($cursor);
      }

      $resp = $square->square_api_request($endpoint, 'GET');
      $options = array_merge($options, $resp['data']['objects'] ?? []);
      $cursor = $resp['data']['cursor'] ?? null;
    } while ($cursor);

    return $options;
  }

  /* ------------------------------------------------------------------------------------------------
   * HELPER : Remove stale Square meta when a product fails to export
   * ------------------------------------------------------------------------------------------------*/
  private function remove_square_meta($product)
  {
    if (!$product) {
      return;
    }

    // Remove from parent (simple product OR variable parent)
    delete_post_meta($product->get_id(), 'square_product_id');
    delete_post_meta($product->get_id(), 'square_variation_id');

    // If variable, also wipe the variations
    if ($product->is_type('variable')) {
      foreach ($product->get_children() as $var_id) {
        delete_post_meta($var_id, 'square_product_id');
        delete_post_meta($var_id, 'square_variation_id');
      }
    }
  }

  public function update_square_inventory_counts($products)
  {
    $settings = get_option('square-woo-sync_settings', []);
    $location = $settings['location'] ?? null;
    $changes = [];

    $square = new SquareHelper();

    foreach ($products as $product) {
      if (!$location) {
        continue;
      }

      if ($product->is_type('simple')) {
        $qty = $product->get_stock_quantity();
        $square_product_id = get_post_meta($product->get_id(), 'square_product_id', true);

        if ($square_product_id !== '') {
          $res = $square->get_square_item_details($square_product_id);
          if (!empty($res['object']['item_data']['variations'][0]['id'])) {
            $variation_id = $res['object']['item_data']['variations'][0]['id'];
            if ($qty !== null) {
              $changes[] = [
                'type' => 'PHYSICAL_COUNT',
                'physical_count' => [
                  'catalog_object_id' => $variation_id,
                  'location_id' => $location,
                  'occurred_at' => gmdate('Y-m-d\TH:i:s\Z'),
                  'state' => 'IN_STOCK',
                  'quantity' => (string) $qty,
                ],
              ];
            }
          }
        }
      } elseif ($product->is_type('variable')) {
        $variation_ids = $product->get_children();
        foreach ($variation_ids as $var_id) {
          $var_obj = wc_get_product($var_id);
          $qty = $var_obj->get_stock_quantity();
          $sq_prod_id = $var_obj->get_meta('square_product_id', true);

          if ($sq_prod_id && $qty !== null) {
            $changes[] = [
              'type' => 'PHYSICAL_COUNT',
              'physical_count' => [
                'catalog_object_id' => $sq_prod_id,
                'location_id' => $location,
                'occurred_at' => gmdate('Y-m-d\TH:i:s\Z'),
                'state' => 'IN_STOCK',
                'quantity' => (string) $qty,
              ],
            ];
          }
        }
      }
    }

    if (!empty($changes)) {
      $this->send_inventory_updates_to_square($square, $changes);
    }
  }

  private function send_inventory_updates_to_square(SquareHelper $square, $changes)
  {
    $endpoint = '/inventory/changes/batch-create';
    $batch_size = 100;
    $chunks = array_chunk($changes, $batch_size);

    foreach ($chunks as $chunk) {
      $payload = [
        'idempotency_key' => wp_generate_uuid4(),
        'changes' => $chunk
      ];

      $resp = $square->square_api_request($endpoint, 'POST', $payload);
      if ($resp['success']) {
        error_log('Successfully updated inventory in Square.');
      } else {
        error_log('Failed to update inventory in Square: ' . $resp['error']);
      }
    }
  }

  private function format_product_for_square(\WC_Product $product, &$square_options, $square, $process_id, $fields_to_export = [])
  {
    try {
      $settings = get_option('square-woo-sync_settings', []);
      $location = $settings['location'] ?? null;
      $unique_id = '#woo_product_' . $product->get_id() . '_' . wp_generate_uuid4();
      $description = $product->get_description();

      if (strlen($description) > 4096) {
        $description = substr(esc_html($description), 0, 4095);
      }

      update_post_meta($product->get_id(), '_temporary_square_id', $unique_id);

      $product_data = [
        'type' => 'ITEM',
        'id' => $unique_id,
        'present_at_all_locations' => true,
        'item_data' => [
          'name' => $product->get_name(),
          'description' => $description,
          'variations' => [],
          'item_options' => [],
        ],
      ];

      if (!empty($fields_to_export['images'])) {
        // Upload images
        try {
          $parent_image_ids = $this->upload_product_images_to_square($product, $square);
          if (!empty($parent_image_ids)) {
            $product_data['item_data']['image_ids'] = $parent_image_ids;
          }
        } catch (\Exception $ex) {
          error_log('Image upload failed: ' . $ex->getMessage());
        }
      }

      // If simple product
      if ($product->is_type('simple')) {
        $price = $product->get_price();
        $price_cents = (int) round((float) ($price ?? 0) * 100);

        $variation_id = '#woo_var_' . $product->get_id() . '_' . wp_generate_uuid4();

        $product_data['item_data']['variations'][] = [
          'type' => 'ITEM_VARIATION',
          'id' => $variation_id,
          'item_variation_data' => [
            'sku' => $product->get_sku(),
            'name' => $product->get_name(),
            'pricing_type' => 'FIXED_PRICING',
            'price_money' => [
              'amount' => $price_cents,
              'currency' => get_woocommerce_currency(),
            ],
          ],
          'present_at_all_locations' => true,
        ];
      }
      // If variable product
      elseif ($product->is_type('variable')) {
        $skip_product = false;
        $item_options_map = [];
        $item_options_array = [];
        $variation_combinations = [];

        foreach ($product->get_children() as $child_id) {
          $variation_obj = wc_get_product($child_id);
          if (!$variation_obj) {
            continue;
          }

          /**
           * Get raw “skip sync” meta.
           *
           * @var string|null $raw_skip_meta 'yes' if user checked “Do not sync/export to Square”, otherwise 'no' or null.
           */
          $raw_skip_meta = get_post_meta($child_id, 'square_skip_sync', true);
          $skip_by_meta = ($raw_skip_meta === 'yes');

          /**
           * Allow external code to override skip logic.
           *
           * @param bool       $skip_by_meta Whether skip is true based on meta.
           * @param int        $variation_id Variation ID.
           * @param WC_Product $variation    Variation product object (may be null; use ID to fetch if needed).
           */
          $should_skip = apply_filters('squarewoosync_skip_variation_sync', $skip_by_meta, $child_id, $variation_obj);

          if ($should_skip) {
            continue;
          }

          $price_cents = (int) round($variation_obj->get_price() * 100);
          $variation_temp_id = '#woo_var_' . $child_id . '_' . wp_generate_uuid4();
          update_post_meta($child_id, '_temporary_square_id', $variation_temp_id);

          $variation_options = [];
          $attr_strings = [];

          if (!empty($fields_to_export['images'])) {
            // Variation image
            $var_image_ids = [];
            $main_img_id = $variation_obj->get_image_id();
            if ($main_img_id && $this->is_valid_image($main_img_id)) { // ⇠ NEW check
              $sq_img_id = $this->upload_image_to_square($main_img_id, $square, false);
              if ($sq_img_id) {
                $var_image_ids[] = $sq_img_id;
              }
            }
          }

          // Variation attributes => Square item options
          foreach ($variation_obj->get_variation_attributes() as $attr_key => $attr_value) {
            $taxonomy = str_replace('attribute_', '', $attr_key);
            $term = get_term_by('slug', $attr_value, $taxonomy);
            $attr_label = wc_attribute_label($taxonomy);
            $attr_slug = $term ? $term->slug : $attr_value;

            $term_name = $term ? $term->name : $attr_value;

            $sq_option_id = $this->get_square_option_id($attr_label, $square_options, $square, $term_name);
            if (!$sq_option_id) {
              $skip_product = true;
              break;
            }
            if (!isset($item_options_map[$attr_label])) {
              $item_options_map[$attr_label] = $sq_option_id;
              $item_options_array[] = [
                'item_option_id' => $sq_option_id,
              ];
            }

            $sq_option_val_id = $this->get_square_option_value_id($sq_option_id, $term_name, $square_options, $square);
            if (!$sq_option_val_id) {
              $skip_product = true;
              break;
            }

            $variation_options[] = [
              'item_option_id' => $sq_option_id,
              'item_option_value_id' => $sq_option_val_id
            ];
            $attr_strings[] = "{$attr_label}: {$term_name}";
          }
          if ($skip_product) {
            break;
          }

          $combination_key = implode('|', array_map(
            fn($o) => $o['item_option_id'] . ':' . $o['item_option_value_id'],
            $variation_options
          ));
          if (isset($variation_combinations[$combination_key])) {
            $skip_product = true;
            error_log("Duplicate variation combination for product ID: {$product->get_id()}");
            break;
          }
          $variation_combinations[$combination_key] = true;

          $var_name = implode(', ', $attr_strings);

          $product_data['item_data']['variations'][] = [
            'type' => 'ITEM_VARIATION',
            'id' => $variation_temp_id,
            'item_variation_data' => [
              'sku' => $variation_obj->get_sku(),
              'name' => $var_name,
              'pricing_type' => 'FIXED_PRICING',
              'price_money' => [
                'amount' => $price_cents,
                'currency' => get_woocommerce_currency(),
              ],
              'item_option_values' => $variation_options,
              'track_inventory' => $variation_obj->managing_stock(),
              'sellable' => true,
              'stockable' => true,
            ],
            'present_at_all_locations' => true
          ];
          // 2. Figure out which array index we just appended.
          $last = array_key_last($product_data['item_data']['variations']);

          if (! empty($var_image_ids)) {
            $product_data['item_data']['variations'][$last]['item_variation_data']['image_ids'] = $var_image_ids;
          }
        }

        if ($skip_product) {
          Logger::log('error', "Skipping product {$product->get_name()} due to invalid/deduplicate variations.", ['parent_id' => $process_id]);
          return null;
        }

        // Square supports up to 6 item_options
        $product_data['item_data']['item_options'] = array_slice($item_options_array, 0, 6);
      }

      if (!empty($fields_to_export['categories'])) {
        // Attach categories
        $terms = get_the_terms($product->get_id(), 'product_cat');
        $categories_arr = [];
        if (!empty($terms) && !is_wp_error($terms)) {
          foreach ($terms as $t) {
            $cat_id = $this->get_or_create_square_category($t, $square);
            if ($cat_id) {
              $categories_arr[] = ['id' => $cat_id];
            }
          }
        }
        if (!empty($categories_arr)) {
          $product_data['item_data']['categories'] = $categories_arr;
        }
      }

      return $product_data;
    } catch (\Exception $e) {
      error_log('Formatting product failed: ' . $e->getMessage());
      return null;
    }
  }

  public function upload_product_images_to_square(\WC_Product $product, SquareHelper $square)
  {
    $all_image_ids = [];

    // Featured + gallery.
    $featured_id   = $product->get_image_id();
    $gallery_ids   = $product->get_gallery_image_ids();
    $all_image_ids = array_unique(array_merge(
      $featured_id ? [$featured_id] : [],
      $gallery_ids
    ));

    $square_image_ids = [];

    foreach ($all_image_ids as $wp_attach_id) {
      // ⇢ NEW: validate first
      if (!$this->is_valid_image($wp_attach_id)) {
        continue; // simply ignore bad images
      }

      $is_primary = ($wp_attach_id === $featured_id);
      $sq_id      = $this->upload_image_to_square($wp_attach_id, $square, $is_primary);
      if ($sq_id) {
        $square_image_ids[] = $sq_id;
      }
    }

    return $square_image_ids;
  }

  public function upload_image_to_square($image_id, SquareHelper $square, $is_primary = false)
  {
    if (!$this->is_valid_image($image_id, /*log*/ false)) {
      return null;
    }

    global $wp_filesystem;

    if (empty($wp_filesystem)) {
      require_once ABSPATH . 'wp-admin/includes/file.php';
      WP_Filesystem();
    }

    $file_path = get_attached_file($image_id);
    $image_name = basename($file_path);

    if (!$file_path || !$wp_filesystem->exists($file_path)) {
      error_log("File does not exist for image ID {$image_id}: {$file_path}");
      return null;
    }

    $idempotency_key = wp_generate_uuid4();
    $token = $square->get_active_token();
    $settings = get_option('square-woo-sync_settings', []);
    $url = (isset($settings['environment']) && $settings['environment'] === 'sandbox')
      ? 'https://connect.squareupsandbox.com/v2/catalog/images'
      : 'https://connect.squareup.com/v2/catalog/images';

    $request_data = json_encode([
      'idempotency_key' => $idempotency_key,
      'image' => [
        'id' => '#img_' . wp_generate_uuid4(),
        'type' => 'IMAGE',
        'image_data' => [
          'caption' => 'Uploaded from Woo',
          'is_primary' => $is_primary
        ]
      ]
    ]);

    $post_fields = [
      'file' => new \CURLFile($file_path, mime_content_type($file_path), $image_name),
      'request' => $request_data
    ];

    $ch = curl_init();
    curl_setopt_array($ch, [
      CURLOPT_URL => $url,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST => true,
      CURLOPT_POSTFIELDS => $post_fields,
      CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . esc_attr($token),
        'Accept: application/json',
        'Square-Version: 2024-09-19',
      ],
    ]);

    $response = curl_exec($ch);
    $error = curl_error($ch);
    curl_close($ch);

    if ($response === false) {
      error_log("Square image upload cURL error: $error");
      return null;
    }

    $decoded = json_decode($response, true);
    if (isset($decoded['image']['id'])) {
      return $decoded['image']['id'];
    } else {
      error_log("Square image upload failed: " . $response);
    }
    return null;
  }

  private function load_square_categories(SquareHelper $square)
  {
    if (!empty($this->square_categories)) {
      return $this->square_categories;
    }
    $resp = $square->square_api_request('/catalog/list?types=CATEGORY', 'GET');
    if (!empty($resp['success']) && !empty($resp['data']['objects'])) {
      $this->square_categories = $resp['data']['objects'];
    } else {
      $this->square_categories = [];
    }
    return $this->square_categories;
  }

  private function build_ancestor_chain(\WP_Term $term, $taxonomy = 'product_cat')
  {
    $chain = [$term];
    $current = $term;
    while ($current->parent) {
      $parent = get_term($current->parent, $taxonomy);
      if ($parent && !is_wp_error($parent)) {
        array_unshift($chain, $parent);
        $current = $parent;
      } else {
        break;
      }
    }
    return $chain;
  }

  public function find_square_category_by_term(\WP_Term $term)
  {
    $lower = strtolower($term->name);
    foreach ($this->square_categories as $obj) {
      if ($obj['type'] === 'CATEGORY') {
        $cat_name = strtolower($obj['category_data']['name'] ?? '');
        if ($cat_name === $lower) {
          return $obj['id'];
        }
      }
    }
    return null;
  }

  public function get_or_create_square_category(\WP_Term $term, SquareHelper $square)
  {
    $this->load_square_categories($square);
    $chain = $this->build_ancestor_chain($term);
    $parentSquareId = null;
    $finalSquareId = null;
    $count = count($chain);

    foreach ($chain as $i => $currentTerm) {
      $found = $this->find_square_category_by_term($currentTerm);
      if ($found) {
        $parentSquareId = $found;
        if ($i === $count - 1) {
          $finalSquareId = $found;
        }
        continue;
      }

      $tempId = '#cat_' . wp_generate_uuid4();
      $isTopLevel = ($i === 0);
      $newCat = [
        'id' => $tempId,
        'type' => 'CATEGORY',
        'is_deleted' => false,
        'present_at_all_locations' => true,
        'category_data' => [
          'name' => $currentTerm->name,
          'is_top_level' => $isTopLevel,
          'category_type' => 'REGULAR_CATEGORY',
          'online_visibility' => true,
        ],
      ];
      if (!$isTopLevel && $parentSquareId) {
        $newCat['category_data']['parent_category'] = ['id' => $parentSquareId];
      }

      $resp = $square->square_api_request('/catalog/object', 'POST', [
        'idempotency_key' => wp_generate_uuid4(),
        'object' => $newCat,
      ]);
      if (!empty($resp['success']) && !empty($resp['data']['catalog_object'])) {
        $createdCat = $resp['data']['catalog_object'];
        $createdCatId = $createdCat['id'];
        $this->square_categories[] = $createdCat;
        $parentSquareId = $createdCatId;
        if ($i === $count - 1) {
          $finalSquareId = $createdCatId;
        }
      } else {
        error_log("Failed to create Square category for term: {$currentTerm->name}");
        return null;
      }
    }
    return $finalSquareId;
  }

  public function get_square_option_id($attr_name, &$square_options, $square, $attr_value_name)
  {
    foreach ($square_options as $opt) {
      if (
        isset($opt['type']) &&
        $opt['type'] === 'ITEM_OPTION' &&
        !empty($opt['item_option_data']['name'])
      ) {
        $existing_name = strtolower($opt['item_option_data']['name']);
        if ($existing_name === strtolower($attr_name)) {
          return $opt['id'];
        }
      }
    }

    $temp_option_id = '#opt_' . wp_generate_uuid4();
    $temp_value_id = '#optval_' . wp_generate_uuid4();

    $new_option = [
      'type' => 'ITEM_OPTION',
      'id' => $temp_option_id,
      'item_option_data' => [
        'name' => $attr_name,
        'show_colors' => false,
        'values' => [
          [
            'type' => 'ITEM_OPTION_VAL',
            'id' => $temp_value_id,
            'item_option_value_data' => [
              'item_option_id' => $temp_option_id,
              'name' => $attr_value_name,
            ]
          ]
        ]
      ]
    ];

    $resp = $square->square_api_request('/catalog/object', 'POST', [
      'idempotency_key' => wp_generate_uuid4(),
      'object' => $new_option
    ]);
    if (!empty($resp['data']['catalog_object'])) {
      $square_options[] = $resp['data']['catalog_object'];
      return $resp['data']['catalog_object']['id'];
    }
    error_log("Creating item option failed: " . json_encode($resp));
    return null;
  }

  /**
   * Return the Square ID for a value inside an Item Option, creating it only
   * when the value definitely doesn't exist.
   *
   * @param string        $square_option_id
   * @param string        $term_name
   * @param array<string> &$square_options  Cached options array (updated in-place)
   * @param SquareHelper  $square
   * @return string|null  Value ID or null on failure
   */
  public function get_square_option_value_id(
    string $square_option_id,
    string $term_name,
    array &$square_options,
    SquareHelper $square
  ): ?string {
    $needle = mb_strtolower(trim($term_name));

    // ── 1. Look for the value in our current cache ───────────────────────────
    foreach ($square_options as &$opt) {
      if ($opt['id'] !== $square_option_id) {
        continue;
      }

      foreach ($opt['item_option_data']['values'] ?? [] as $v) {
        $name = mb_strtolower(trim($v['item_option_value_data']['name'] ?? ''));
        if ($name === $needle) {
          return $v['id'];           // ← we already have it
        }
      }

      // ── 2. Refresh the option from Square (in case pagination hid values) ─
      $fresh = $square->square_api_request("/catalog/object/{$square_option_id}", 'GET');
      if (!empty($fresh['success']) && !empty($fresh['data']['object'])) {
        $opt = $fresh['data']['object']; // update the local cache

        foreach ($opt['item_option_data']['values'] ?? [] as $v) {
          $name = mb_strtolower(trim($v['item_option_value_data']['name'] ?? ''));
          if ($name === $needle) {
            return $v['id'];       // ← found after refresh
          }
        }
      }

      // ── 3. Create the value because it really does not exist ──────────────
      $temp_value_id = '#optval_' . wp_generate_uuid4();
      $new_value = [
        'type' => 'ITEM_OPTION_VAL',
        'id' => $temp_value_id,
        'item_option_value_data' => [
          'item_option_id' => $square_option_id,
          'name' => $term_name,
        ],
      ];

      $resp = $square->square_api_request(
        '/catalog/object',
        'POST',
        [
          'idempotency_key' => wp_generate_uuid4(),
          'object' => $new_value,
        ]
      );

      if (!empty($resp['success']) && !empty($resp['data']['catalog_object'])) {
        // push into the cache so subsequent calls see it
        $opt['item_option_data']['values'][] = $resp['data']['catalog_object'];
        return $resp['data']['catalog_object']['id'];
      }

      error_log('Creating item option value failed: ' . json_encode($resp));
      return null;
    }

    error_log("Option {$square_option_id} not found in local cache.");
    return null;
  }

  public function getSquareObjectById($data, $id)
  {
    if (!$data['success'] || empty($data['data']['objects']) || empty($data['data']['id_mappings'])) {
      return null;
    }
    $objectId = null;
    foreach ($data['data']['id_mappings'] as $mapping) {
      if ($mapping['client_object_id'] === $id || $mapping['object_id'] === $id) {
        $objectId = $mapping['object_id'];
        break;
      }
    }
    if (!$objectId) {
      return null;
    }
    foreach ($data['data']['objects'] as $obj) {
      if ($obj['id'] === $objectId) {
        return $obj;
      }
      if (!empty($obj['item_data']['variations'])) {
        foreach ($obj['item_data']['variations'] as $var) {
          if ($var['id'] === $objectId) {
            return $var;
          }
        }
      }
    }
    return null;
  }

  public function update_wc_product_with_square_ids($square_response)
  {
    if ($square_response['success'] && !empty($square_response['data']['id_mappings'])) {
      foreach ($square_response['data']['id_mappings'] as $map) {
        $wc_id = $this->wc_get_product_id_by_temp_id($map['client_object_id']);
        if (!$wc_id) {
          continue;
        }
        $wc_product = wc_get_product($wc_id);
        if (!$wc_product) {
          continue;
        }

        if ($wc_product->is_type('variable')) {
          // main variable product
          if ($map['client_object_id'] === get_post_meta($wc_id, '_temporary_square_id', true)) {
            update_post_meta($wc_id, 'square_product_id', $map['object_id']);
          }
          // Check variations
          foreach ($wc_product->get_children() as $var_id) {
            $temp_id_var = get_post_meta($var_id, '_temporary_square_id', true);
            if ($temp_id_var === $map['client_object_id']) {
              update_post_meta($var_id, 'square_product_id', $map['object_id']);
            }
          }
        } else {
          // Simple
          if ($map['client_object_id'] === get_post_meta($wc_id, '_temporary_square_id', true)) {
            update_post_meta($wc_id, 'square_product_id', $map['object_id']);

            // For a simple item, check if we can find the single variation ID
            $variation_obj = $this->getSquareObjectById($square_response, $map['object_id']);
            if (!empty($variation_obj['item_data']['variations'][0]['id'])) {
              $var_id = $variation_obj['item_data']['variations'][0]['id'];
              update_post_meta($wc_id, 'square_variation_id', $var_id);
            }
          }
        }
      }
    } else {
      error_log("No valid id_mappings in the Square response to update local products.");
    }
  }

  private function wc_get_product_id_by_temp_id($temp_id)
  {
    global $wpdb;
    $sql = "
      SELECT pm.post_id
      FROM {$wpdb->postmeta} pm
      INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
      WHERE pm.meta_key = '_temporary_square_id'
        AND pm.meta_value = %s
        AND (p.post_type = 'product' OR p.post_type = 'product_variation')
      LIMIT 1
    ";
    return $wpdb->get_var($wpdb->prepare($sql, $temp_id));
  }

  /**
   * Check whether a WordPress attachment can be sent to Square.
   *
   * @param  int  $image_id     Attachment ID.
   * @param  bool $log_failure  Write a warning via Logger when invalid.
   * @return bool               True = safe to upload.
   *
   * @phpstan-return bool
   */
  private function is_valid_image(int $image_id, bool $log_failure = true): bool
  {
    $file = get_attached_file($image_id);
    if (!$file || !file_exists($file)) {
      $log_failure && Logger::log('warning', "Image {$image_id} skipped – file missing.");
      return false;
    }

    // 250 MB in bytes.
    if (filesize($file) > 250 * 1024 * 1024) {
      $log_failure && Logger::log('warning', "Image {$image_id} skipped – exceeds 250 MB.");
      return false;
    }

    $mime = wp_check_filetype($file)['type'] ?? '';
    $allowed = ['image/jpeg', 'image/pjpeg', 'image/png', 'image/gif'];
    if (!in_array($mime, $allowed, true)) {
      $log_failure && Logger::log('warning', "Image {$image_id} skipped – unsupported type {$mime}.");
      return false;
    }

    return true;
  }
}
