<?php
/**
 * Import REST endpoints.
 *
 * @package PrimeLink
 */

namespace PrimeLink\Core\Rest;

use PrimeLink\Modules\Imports\Service\ImportPreview;
use PrimeLink\Modules\Imports\Parsers\ParserFactory;
use PrimeLink\Modules\Imports\DataSources\UrlSource;
use PrimeLink\Modules\Imports\DataSources\ExistingFileSource;
use PrimeLink\Modules\Imports\DataSources\DropboxSource;
use PrimeLink\Modules\Imports\DataSources\FtpSource;
use PrimeLink\Modules\Imports\DataSources\GoogleDriveSource;
use PrimeLink\Modules\Imports\DataSources\OneDriveSource;
use PrimeLink\Modules\Imports\Service\ImportRunner;
use PrimeLink\Modules\Imports\Service\StatisticsService;
use PrimeLink\Modules\Imports\Service\ImportTrackingService;
use PrimeLink\Modules\Imports\Service\DryRunService;
use PrimeLink\Modules\Imports\Service\PlatformConnectorService;
use PrimeLink\Helpers\EnvatoHelper;

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

final class ImportRest {

    public static function register() {
        // Use priority 10 to ensure routes are registered early
        add_action( 'rest_api_init', array( __CLASS__, 'routes' ), 10 );
    }

    public static function routes() {

        // Index endpoint for namespace
        register_rest_route( 'primelink/v1', '/', array(
            'methods'             => 'GET',
            'callback'            => array( __CLASS__, 'index' ),
            'permission_callback' => '__return_true', // Public endpoint for discovery
        ) );

        register_rest_route( 'primelink/v1', '/import/create-draft', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'create_draft' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/import/save-step', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'save_step' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
                'step'      => array( 'required' => true, 'type' => 'integer' ),
                // JS sends associative object here.
                'data'      => array(
                    'required' => false,
                    'type'     => 'object',
                ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/handle-source', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'handle_source' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args'                => array(
                'import_id' => array(
                    'required' => false,
                    'type'     => 'integer',
                ),
                'source'    => array(
                    'required' => false,
                    'type'     => 'string',
                ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/dropbox/list', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'dropbox_list' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'path' => array( 'required' => false, 'type' => 'string' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/gdrive/list', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'gdrive_list' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'folder_id' => array( 'required' => false, 'type' => 'string' ),
                'query'     => array( 'required' => false, 'type' => 'string' ),
                'page_token' => array( 'required' => false, 'type' => 'string' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/preview', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'preview_file' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
                'xpath'     => array( 'required' => false, 'type' => 'string' ),
                'page'      => array( 'required' => false, 'type' => 'integer' ),
                'per_page'  => array( 'required' => false, 'type' => 'integer' ),
                'filters'   => array( 'required' => false, 'type' => 'array' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/mapping-preview', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'mapping_preview' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
                'index'     => array( 'required' => false, 'type' => 'integer' ),
                'filters'   => array( 'required' => false, 'type' => 'array' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/templates', array(
            'methods'             => 'GET',
            'callback'            => array( __CLASS__, 'list_templates' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'type' => array( 'required' => false, 'type' => 'string' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/list', array(
            'methods'             => 'GET',
            'callback'            => array( __CLASS__, 'list_imports' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'type' => array( 'required' => false, 'type' => 'string' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/reload', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'reload_import' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/templates/save', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'save_template' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'name' => array( 'required' => true, 'type' => 'string' ),
                'data' => array( 'required' => true, 'type' => 'object' ),
                'type' => array( 'required' => false, 'type' => 'string' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/templates/load', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'load_template' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                // For loading by template name (classic behavior).
                'name'      => array( 'required' => false, 'type' => 'string' ),
                // For loading mapping from a specific import draft (used by Reload Import & UI auto-load).
                'import_id' => array( 'required' => false, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/templates/delete', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'delete_template' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'name' => array( 'required' => true, 'type' => 'string' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/filter-presets', array(
            'methods'             => 'GET',
            'callback'            => array( __CLASS__, 'list_filter_presets' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/import/filter-presets/save', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'save_filter_preset' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'name'    => array( 'required' => true, 'type' => 'string' ),
                'filters' => array( 'required' => true, 'type' => 'array' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/filter-presets/load', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'load_filter_preset' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'name' => array( 'required' => true, 'type' => 'string' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/filter-presets/delete', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'delete_filter_preset' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'name' => array( 'required' => true, 'type' => 'string' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/run/start', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'run_start' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/run/pause', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'run_pause' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/run/resume', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'run_resume' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/schedule', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'schedule_import' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
                'run_at'    => array( 'required' => false, 'type' => 'string' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/cron/tick', array(
            'methods'             => 'GET',
            'callback'            => array( __CLASS__, 'cron_tick' ),
            'permission_callback' => '__return_true',
            'args' => array(
                'token'     => array( 'required' => true, 'type' => 'string' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/run/status', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'run_status' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/ai/test', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'ai_test' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/import/delete-products', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'delete_products' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/delete-draft', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'delete_draft' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/log', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'get_log' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/anomalies', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'get_anomalies' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/xml-roots', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'xml_roots' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/xml-tags', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'xml_tags' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
                'root'      => array( 'required' => false, 'type' => 'string' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/statistics', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'get_statistics' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'date_from' => array( 'required' => false, 'type' => 'string' ),
                'date_to'   => array( 'required' => false, 'type' => 'string' ),
                'import_id' => array( 'required' => false, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/rollback', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'rollback_import' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id'       => array( 'required' => true, 'type' => 'integer' ),
                'delete_created'  => array( 'required' => false, 'type' => 'boolean' ),
                'restore_updated' => array( 'required' => false, 'type' => 'boolean' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/rollback-summary', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'get_rollback_summary' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/dry-run', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'dry_run_import' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'import_id'         => array( 'required' => true, 'type' => 'integer' ),
                'limit'             => array( 'required' => false, 'type' => 'integer' ),
                'validate_required' => array( 'required' => false, 'type' => 'boolean' ),
                'validate_format'   => array( 'required' => false, 'type' => 'boolean' ),
                'check_duplicates'  => array( 'required' => false, 'type' => 'boolean' ),
                'skip_unchanged'    => array( 'required' => false, 'type' => 'boolean' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/platform/connectors', array(
            'methods'             => 'GET',
            'callback'            => array( __CLASS__, 'list_connectors' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/platform/test-connection', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'test_platform_connection' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'connector'   => array( 'required' => true, 'type' => 'string' ),
                'credentials' => array( 'required' => true, 'type' => 'object' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/platform/fetch', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'fetch_platform_data' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'connector'   => array( 'required' => true, 'type' => 'string' ),
                'credentials' => array( 'required' => true, 'type' => 'object' ),
                'data_type'   => array( 'required' => true, 'type' => 'string' ),
                'limit'       => array( 'required' => false, 'type' => 'integer' ),
                'page'        => array( 'required' => false, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/webhook/(?P<import_id>\d+)', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'handle_webhook' ),
            'permission_callback' => array( __CLASS__, 'webhook_permission' ), // Verify webhook token
            'args' => array(
                'import_id' => array( 'required' => true, 'type' => 'integer' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/import/uploads', array(
            'methods'             => 'GET',
            'callback'            => array( __CLASS__, 'list_uploads' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/settings', array(
            'methods'             => 'GET',
            'callback'            => array( __CLASS__, 'get_settings' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/settings/save', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'save_settings' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/settings/notify-test', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'notify_test' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/settings/test-license', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'test_license' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/settings/deactivate-license', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'deactivate_license' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/settings/check-updates', array(
            'methods'             => 'GET',
            'callback'            => array( __CLASS__, 'check_updates' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/settings/gdrive/auth', array(
            'methods'             => array( 'GET', 'POST' ),
            'callback'            => array( __CLASS__, 'gdrive_auth_url' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/settings/gdrive/callback', array(
            'methods'             => array( 'GET', 'POST' ),
            'callback'            => array( __CLASS__, 'gdrive_oauth_callback' ),
            'permission_callback' => '__return_true', // Public endpoint for OAuth callback
        ) );

        register_rest_route( 'primelink/v1', '/settings/gdrive/revoke', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'gdrive_revoke' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        // OneDrive OAuth endpoints
        register_rest_route( 'primelink/v1', '/onedrive/list', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'onedrive_list' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'folder_id' => array( 'required' => false, 'type' => 'string' ),
                'query'     => array( 'required' => false, 'type' => 'string' ),
                'page_token' => array( 'required' => false, 'type' => 'string' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/settings/onedrive/auth', array(
            'methods'             => array( 'GET', 'POST' ),
            'callback'            => array( __CLASS__, 'onedrive_auth_url' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/settings/onedrive/callback', array(
            'methods'             => array( 'GET', 'POST' ),
            'callback'            => array( __CLASS__, 'onedrive_oauth_callback' ),
            'permission_callback' => '__return_true', // Public endpoint for OAuth callback
        ) );

        register_rest_route( 'primelink/v1', '/settings/onedrive/revoke', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'onedrive_revoke' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/import/get-draft', array(
        'methods'             => 'POST',
        'callback'            => array( __CLASS__, 'get_draft' ),
        'permission_callback' => array( __CLASS__, 'permission' ),
        'args' => array(
            'import_id' => array( 'required' => true, 'type' => 'integer' ),
        ),
         ) );

        // Activation Wizard endpoints
        register_rest_route( 'primelink/v1', '/wizard/verify-license', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'wizard_verify_license' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
            'args' => array(
                'purchase_code' => array( 'required' => true, 'type' => 'string' ),
            ),
        ) );

        register_rest_route( 'primelink/v1', '/wizard/save-setup', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'wizard_save_setup' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/wizard/test-system', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'wizard_test_system' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/wizard/complete', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'wizard_complete' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/wizard/restart', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'wizard_restart' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/wizard/skip', array(
            'methods'             => 'POST',
            'callback'            => array( __CLASS__, 'wizard_skip' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        // Developer API endpoints
        register_rest_route( 'primelink/v1', '/system/status', array(
            'methods'             => 'GET',
            'callback'            => array( __CLASS__, 'system_status' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );

        register_rest_route( 'primelink/v1', '/system/health', array(
            'methods'             => 'GET',
            'callback'            => array( __CLASS__, 'health_check' ),
            'permission_callback' => '__return_true', // Public endpoint
        ) );

        register_rest_route( 'primelink/v1', '/system/info', array(
            'methods'             => 'GET',
            'callback'            => array( __CLASS__, 'system_info' ),
            'permission_callback' => array( __CLASS__, 'permission' ),
        ) );


    }

    /**
     * Check permissions and rate limit for REST API requests.
     *
     * @param \WP_REST_Request $request Request object.
     * @return bool|WP_Error
     */
    /**
     * Permission callback for REST API endpoints.
     * Envato compliant: Verifies user capabilities and nonce.
     *
     * @param WP_REST_Request|null $request Request object.
     * @return bool|WP_Error
     */
    public static function permission( $request = null ) {
        // Check user capabilities first (filterable for WooCommerce/Envato compliance).
        // Default to manage_woocommerce if WooCommerce is active, otherwise manage_options.
        $default_capability = class_exists( 'WooCommerce' ) ? 'manage_woocommerce' : 'manage_options';
        $capability         = apply_filters( 'mt_primelink_capability', $default_capability );
        if ( ! current_user_can( $capability ) ) {
            return new \WP_Error(
                'rest_forbidden',
                __( 'You are not allowed to manage PrimeLink imports.', 'primelink' ),
                array( 'status' =>  rest_authorization_required_code() )
            );
        }

        // For POST/PUT/DELETE requests, verify nonce explicitly (Envato compliance)
        if ( $request instanceof \WP_REST_Request ) {
            $method = $request->get_method();
            if ( in_array( $method, array( 'POST', 'PUT', 'DELETE', 'PATCH' ), true ) ) {
                $nonce = $request->get_header( 'X-WP-Nonce' );
                if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
                    return new \WP_Error(
                        'rest_forbidden',
                        __( 'Invalid nonce. Please refresh the page and try again.', 'primelink' ),
                        array( 'status' => 403 )
                    );
                }
            }

            // Apply rate limiting (exclude polling endpoints for active imports)
            $settings = get_option( 'mt_primelink_settings', array() );
            $rate_limit_enabled = $settings['rate_limit_enabled'] ?? true;
            // Increased default limits to support multiple concurrent imports
            $rate_limit_max = absint( $settings['rate_limit_max'] ?? 500 ); // Increased from 100 to 500
            $rate_limit_window = absint( $settings['rate_limit_window'] ?? 60 );

            // Exclude polling endpoints from rate limiting to allow multiple concurrent imports
            $route = $request->get_route();
            $polling_endpoints = array(
                '/import/run/status',    // Status polling
                '/import/log',            // Log polling
                '/import/anomalies',      // Anomalies polling
                '/settings',              // Settings (read-only, low impact)
            );
            
            $is_polling_endpoint = false;
            foreach ( $polling_endpoints as $endpoint ) {
                if ( strpos( $route, $endpoint ) !== false ) {
                    $is_polling_endpoint = true;
                    break;
                }
            }

            // Only apply rate limiting to non-polling endpoints
            if ( $rate_limit_enabled && ! $is_polling_endpoint ) {
                $rate_limit_result = \PrimeLink\Helpers\RateLimiterHelper::check_request(
                    $request,
                    array(
                        'limit'    => $rate_limit_max,
                        'interval' => $rate_limit_window,
                        'policy'   => 'fixed_window',
                    )
                );

                // If rate limited, return error
                if ( is_wp_error( $rate_limit_result ) ) {
                    return $rate_limit_result;
                }
            }
        }

        return true;
    }

    /**
     * Verify webhook token for public webhook endpoint.
     * 
     * @param WP_REST_Request $request Request object.
     * @return bool|WP_Error
     */
    public static function webhook_permission( $request ) {
        $import_id = absint( $request['import_id'] );
        
        if ( $import_id < 1 ) {
            return false;
        }

        // Get webhook token from header or query parameter
        $token = $request->get_header( 'X-Webhook-Token' );
        if ( empty( $token ) ) {
            $token = $request->get_param( 'token' );
        }
        $token = sanitize_text_field( $token );

        // Get stored webhook token for this import
        $draft = ImportRunner::get_draft( $import_id );
        if ( empty( $draft ) ) {
            return false;
        }

        // Check if webhook token is set in import settings
        $webhook_token = $draft['step_data'][2]['webhook_token'] ?? '';
        
        // If no token is configured, default to deny for security.
        // Allowing this can be explicitly opted-in via filter if needed.
        if ( empty( $webhook_token ) ) {
            $allow_without_token = (bool) apply_filters( 'mt_primelink_allow_webhook_without_token', false, $import_id, $request );
            if ( ! $allow_without_token ) {
                return new \WP_Error(
                    'rest_forbidden',
                    __( 'Webhook token is not configured for this import.', 'primelink' ),
                    array( 'status' => 403 )
                );
            }

            // If explicitly allowed, log a warning for transparency.
            \PrimeLink\Helpers\LoggerHelper::warning(
                'Webhook called without configured token but allowed via filter.',
                array( 'import_id' => $import_id )
            );
            return true;
        }

        // Verify token matches
        if ( empty( $token ) || ! hash_equals( $webhook_token, $token ) ) {
            return new \WP_Error(
                'rest_forbidden',
                __( 'Invalid webhook token.', 'primelink' ),
                array( 'status' => 403 )
            );
        }

        return true;
    }

    public static function run_start( $request ) {
        $import_id = absint( $request['import_id'] );
        if ( $import_id < 1 ) {
            return new \WP_REST_Response( array( 'message' => 'Missing import ID.' ), 400 );
        }
        try {
            // Use ImportRunner for both products and orders
            // ImportRunner::tick() already handles order imports correctly
            $state = ImportRunner::start( $import_id );
        } catch ( \Throwable $e ) {
            return new \WP_REST_Response( array(
                'message' => __( 'Failed to start import:', 'primelink' ) . ' ' . $e->getMessage(),
            ), 500 );
        }
        return $state;
    }

    public static function run_pause( $request ) {
        $import_id = absint( $request['import_id'] );
        return ImportRunner::pause( $import_id );
    }

    public static function run_resume( $request ) {
        $import_id = absint( $request['import_id'] );
        return ImportRunner::resume( $import_id );
    }

    public static function run_status( $request ) {
        $import_id = absint( $request['import_id'] );
        try {
            return ImportRunner::status( $import_id );
        } catch ( \Throwable $e ) {
            return new \WP_REST_Response( array(
                'message' => __( 'Failed to fetch status:', 'primelink' ) . ' ' . $e->getMessage(),
            ), 500 );
        }
    }

    /** ---------------- AI Test ---------------- */
    public static function ai_test( $request ) {
        $api_key = sanitize_text_field( $request['api_key'] ?? '' );
        $model   = sanitize_text_field( $request['model'] ?? '' );

        $settings = self::get_settings();
        if ( empty( $api_key ) ) {
            $api_key = $settings['ai_key'] ?? '';
        }
        if ( empty( $model ) ) {
            $model = $settings['ai_model'] ?? 'gpt-4o-mini';
        }

        if ( empty( $api_key ) ) {
            return new \WP_REST_Response( array( 'message' => 'API key is required to test AI.' ), 400 );
        }

        return array(
            'ok'      => true,
            'model'   => $model,
            'message' => 'AI key stored, ready for semantic suggestions.',
        );
    }

    public static function schedule_import( $request ) {
        $import_id = absint( $request['import_id'] );
        $run_at    = isset( $request['run_at'] ) ? sanitize_text_field( $request['run_at'] ) : '';

        if ( $import_id < 1 ) {
            return new \WP_REST_Response( array( 'message' => 'Import ID is required.' ), 400 );
        }

        if ( $run_at ) {
            $timestamp = strtotime( $run_at );
            if ( ! $timestamp || $timestamp < time() ) {
                return new \WP_REST_Response( array( 'message' => 'Run time must be in the future.' ), 400 );
            }
            wp_schedule_single_event( $timestamp, 'mt_primelink_import_start_scheduled', array( $import_id ) );
            // Persist next run for external cron fallback.
            $drafts = get_option( 'mt_primelink_import_drafts', array() );
            if ( isset( $drafts[ $import_id ]['step_data'][6]['schedule'] ) ) {
                $drafts[ $import_id ]['step_data'][6]['schedule']['next_run'] = $timestamp;
                update_option( 'mt_primelink_import_drafts', $drafts, false );
            }
            return array(
                'scheduled' => true,
                'run_at'    => date_i18n( 'Y-m-d H:i:s', $timestamp ),
                'recurring' => false,
            );
        }

        // If no explicit time provided, use draft schedule config (step 6).
        $drafts = get_option( 'mt_primelink_import_drafts', array() );
        $draft  = $drafts[ $import_id ] ?? array();
        $schedule = $draft['step_data'][6]['schedule'] ?? array();
        $enabled  = ! empty( $schedule['enabled'] );
        if ( ! $enabled ) {
            return new \WP_REST_Response( array( 'message' => 'Scheduling is not enabled in step 6.' ), 400 );
        }
        $interval = isset( $schedule['interval'] ) ? $schedule['interval'] : 'daily';
        $start    = isset( $schedule['start_time'] ) ? $schedule['start_time'] : '';

        $start_ts = $start ? strtotime( 'today ' . $start ) : time();
        if ( $start_ts < time() ) {
            $start_ts = time() + 60; // next minute
        }

        $recurrence = in_array( $interval, array( 'hourly', 'twicedaily', 'daily', 'weekly', 'monthly' ), true ) ? $interval : 'daily';

        // Check cron mode - external cron doesn't use WordPress scheduling
        $settings = self::get_settings();
        $cron_mode = $settings['cron_mode'] ?? 'wp';

        if ( $cron_mode === 'external' ) {
            // External cron mode: only store next_run, don't use WordPress scheduling
            $drafts = get_option( 'mt_primelink_import_drafts', array() );
            if ( isset( $drafts[ $import_id ]['step_data'][6]['schedule'] ) ) {
                $drafts[ $import_id ]['step_data'][6]['schedule']['next_run'] = $start_ts;
                $drafts[ $import_id ]['step_data'][6]['schedule']['interval'] = $recurrence;
                update_option( 'mt_primelink_import_drafts', $drafts, false );
            }
        } else {
            // WordPress cron mode: use wp_schedule_event
            // Clear existing scheduled events for this import
            $crons = _get_cron_array();
            if ( is_array( $crons ) ) {
                foreach ( $crons as $timestamp => $hooks ) {
                    if ( isset( $hooks['mt_primelink_import_start_scheduled'] ) ) {
                        foreach ( $hooks['mt_primelink_import_start_scheduled'] as $sig => $data ) {
                            if ( isset( $data['args'][0] ) && absint( $data['args'][0] ) === $import_id ) {
                                wp_unschedule_event( $timestamp, 'mt_primelink_import_start_scheduled', array( $import_id ) );
                            }
                        }
                    }
                }
            }

            wp_schedule_event( $start_ts, $recurrence, 'mt_primelink_import_start_scheduled', array( $import_id ) );
            // Persist next run for external cron fallback (in case mode changes later).
            $drafts = get_option( 'mt_primelink_import_drafts', array() );
            if ( isset( $drafts[ $import_id ]['step_data'][6]['schedule'] ) ) {
                $drafts[ $import_id ]['step_data'][6]['schedule']['next_run'] = $start_ts;
                $drafts[ $import_id ]['step_data'][6]['schedule']['interval'] = $recurrence;
                update_option( 'mt_primelink_import_drafts', $drafts, false );
            }
        }

        return array(
            'scheduled' => true,
            'run_at'    => date_i18n( 'Y-m-d H:i:s', $start_ts ),
            'recurring' => $recurrence,
        );
    }

    public static function cron_tick( $request ) {
        $token     = sanitize_text_field( $request['token'] ?? '' );

        $settings = self::get_settings();
        $saved_token = $settings['cron_token'] ?? '';
        $mode        = $settings['cron_mode'] ?? 'wp';

        if ( $mode !== 'external' ) {
            return new \WP_REST_Response( array( 'message' => 'External cron disabled.' ), 400 );
        }
        if ( empty( $saved_token ) || $token !== $saved_token ) {
            return new \WP_REST_Response( array( 'message' => 'Invalid cron token.' ), 403 );
        }

        try {
            // External cron mode: don't use WordPress cron, rely only on next_run timestamps
            // tick_all() will check next_run and start scheduled imports/exports automatically
            ImportRunner::tick_all();
            \PrimeLink\Modules\Exports\Service\ExportRunner::tick_all();
            return array( 'message' => 'Cron ticked all running imports and exports.' );
        } catch ( \Throwable $e ) {
            return new \WP_REST_Response( array( 'message' => $e->getMessage() ), 500 );
        }
    }

    public static function get_log( $request ) {
        $import_id = absint( $request['import_id'] );
        if ( $import_id < 1 ) {
            return new \WP_REST_Response( array( 'message' => 'Invalid import ID.' ), 400 );
        }
        $state = get_option( 'mt_primelink_import_state_' . $import_id, array() );
        $log   = isset( $state['log'] ) && is_array( $state['log'] ) ? $state['log'] : array();
        return array( 'log' => $log );
    }

    public static function get_anomalies( $request ) {
        $import_id = absint( $request['import_id'] );
        if ( $import_id < 1 ) {
            return new \WP_REST_Response( array( 'message' => 'Invalid import ID.' ), 400 );
        }
        
        $anomalies_data = get_post_meta( $import_id, '_mt_primelink_anomalies', true );
        if ( ! is_array( $anomalies_data ) ) {
            $anomalies_data = array();
        }
        
        return array( 
            'anomalies' => $anomalies_data,
            'count' => count( $anomalies_data ),
        );
    }

    public static function delete_products( $request ) {
        $import_id = absint( $request['import_id'] );
        if ( $import_id < 1 ) {
            return new \WP_REST_Response( array( 'message' => 'Invalid import ID.' ), 400 );
        }

        $deleted_products    = 0;
        $deleted_attachments = 0;

        $paged = 1;
        do {
            $q = new \WP_Query( array(
                'post_type'      => 'product',
                'post_status'    => 'any',
                'posts_per_page' => 100,
                'paged'          => $paged,
                'fields'         => 'ids',
                'meta_query'     => array(
                    array(
                        'key'   => '_mt_primelink_import_id',
                        'value' => $import_id,
                    ),
                ),
            ) );

            if ( empty( $q->posts ) ) {
                break;
            }

            foreach ( $q->posts as $pid ) {
                $pid = absint( $pid );
                $thumb_id = absint( get_post_thumbnail_id( $pid ) );

                $gallery_ids = array();
                $gallery_meta = get_post_meta( $pid, '_product_image_gallery', true );
                if ( $gallery_meta ) {
                    $gallery_ids = array_filter( array_map( 'absint', explode( ',', $gallery_meta ) ) );
                }

                foreach ( array_filter( array_merge( array( $thumb_id ), $gallery_ids ) ) as $att_id ) {
                    if ( wp_delete_attachment( $att_id, true ) ) {
                        $deleted_attachments++;
                    }
                }

                if ( wp_delete_post( $pid, true ) ) {
                    $deleted_products++;
                }
            }

            $paged++;
        } while ( $q->max_num_pages >= $paged );

        return array(
            'deleted_products'    => $deleted_products,
            'deleted_attachments' => $deleted_attachments,
        );
    }

    public static function delete_draft( $request ) {
        $import_id = absint( $request['import_id'] );

        $drafts = get_option( 'mt_primelink_import_drafts', array() );
        if ( isset( $drafts[ $import_id ] ) ) {
            unset( $drafts[ $import_id ] );
            update_option( 'mt_primelink_import_drafts', $drafts, false );
        }

        // Clean runner state if exists
        delete_option( 'mt_primelink_import_state_' . $import_id );

        return array(
            'deleted'   => true,
            'import_id' => $import_id,
        );
    }

    /** ---------------- XML helpers (roots & tags) ---------------- */
    public static function xml_roots( $request ) {
        $import_id = absint( $request['import_id'] );
        $file      = self::resolve_file_from_draft( $import_id );
        if ( is_wp_error( $file ) ) {
            return $file;
        }

        $roots = \PrimeLink\Modules\Imports\Parsers\XmlHelper::scan_roots( $file['path'], array( 'limit' => 200 ) );
        return array( 'roots' => $roots );
    }

    public static function xml_tags( $request ) {
        $import_id = absint( $request['import_id'] );
        $root      = sanitize_text_field( $request['root'] ?? '' );
        $file      = self::resolve_file_from_draft( $import_id );
        if ( is_wp_error( $file ) ) {
            return $file;
        }

        $tags = \PrimeLink\Modules\Imports\Parsers\XmlHelper::scan_tags_under_root( $file['path'], $root, array( 'limit' => 300 ) );
        return array( 'tags' => $tags );
    }

    /** ---------------- Statistics & Analytics ---------------- */
    public static function get_statistics( $request ) {
        try {
            $args = array(
                'date_from' => sanitize_text_field( $request['date_from'] ?? '' ),
                'date_to'   => sanitize_text_field( $request['date_to'] ?? '' ),
                'import_id' => absint( $request['import_id'] ?? 0 ),
                'type'      => sanitize_text_field( $request['type'] ?? '' ),
            );

            $stats = StatisticsService::get_statistics( $args );
            return new \WP_REST_Response( $stats, 200 );
        } catch ( \Exception $e ) {
            return new \WP_REST_Response( array(
                'message' => 'Failed to load statistics: ' . $e->getMessage(),
                'error'   => $e->getMessage(),
            ), 500 );
        }
    }

    /** ---------------- Rollback/Undo ---------------- */
    public static function rollback_import( $request ) {
        $import_id       = absint( $request['import_id'] );
        $delete_created  = isset( $request['delete_created'] ) ? (bool) $request['delete_created'] : true;
        $restore_updated = isset( $request['restore_updated'] ) ? (bool) $request['restore_updated'] : true;

        if ( $import_id < 1 ) {
            return new \WP_REST_Response( array( 'message' => 'Invalid import ID.' ), 400 );
        }

        if ( ! ImportTrackingService::can_rollback( $import_id ) ) {
            return new \WP_REST_Response( array( 'message' => 'No items to rollback for this import.' ), 404 );
        }

        try {
            $results = ImportTrackingService::rollback( $import_id, $delete_created, $restore_updated );
            return array(
                'success'   => true,
                'message'   => sprintf(
                    'Rollback completed. Deleted: %d, Restored: %d, Errors: %d',
                    $results['deleted'],
                    $results['restored'],
                    $results['errors']
                ),
                'results'   => $results,
            );
        } catch ( \Throwable $e ) {
            return new \WP_REST_Response( array(
                'message' => 'Rollback failed: ' . $e->getMessage(),
                'error'   => $e->getMessage(),
            ), 500 );
        }
    }

    public static function get_rollback_summary( $request ) {
        $import_id = absint( $request['import_id'] );
        if ( $import_id < 1 ) {
            return new \WP_REST_Response( array( 'message' => 'Invalid import ID.' ), 400 );
        }

        $summary = ImportTrackingService::get_rollback_summary( $import_id );
        $can_rollback = ImportTrackingService::can_rollback( $import_id );

        return array(
            'can_rollback' => $can_rollback,
            'summary'      => $summary,
        );
    }

    /** ---------------- Platform Connectors ---------------- */
    public static function list_connectors( $request ) {
        $connectors = PlatformConnectorService::get_connectors();
        $list = array();

        foreach ( $connectors as $name => $connector ) {
            $fields = $connector->get_credentials_fields();
            // Ensure fields is properly formatted as associative array
            // WooCommerce REST API will serialize this correctly
            $list[] = array(
                'name'         => $name,
                'display_name' => $connector->get_display_name(),
                'fields'       => $fields, // This is already an associative array
            );
        }

        return new \WP_REST_Response( array(
            'connectors' => $list,
        ), 200 );
    }

    public static function test_platform_connection( $request ) {
        $connector_name = sanitize_text_field( $request['connector'] );
        $credentials    = (array) $request['credentials'];

        if ( empty( $connector_name ) ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'message' => 'Connector name required.',
            ), 400 );
        }

        $result = PlatformConnectorService::test_connection( $connector_name, $credentials );
        return $result;
    }

    public static function fetch_platform_data( $request ) {
        $connector_name = sanitize_text_field( $request['connector'] );
        $credentials    = (array) $request['credentials'];
        $data_type      = sanitize_text_field( $request['data_type'] );
        $options        = array(
            'limit' => isset( $request['limit'] ) ? absint( $request['limit'] ) : 10,
        );

        // Shopify doesn't support 'page' parameter, use 'since_id' instead
        // For other platforms, use 'page' if provided
        if ( $connector_name !== 'shopify' ) {
            $options['page'] = isset( $request['page'] ) ? absint( $request['page'] ) : 1;
        }

        if ( empty( $connector_name ) || empty( $data_type ) ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => 'Missing required parameters.',
            ), 400 );
        }

        // Validate credentials before fetching
        if ( empty( $credentials ) || ! is_array( $credentials ) ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => 'Invalid credentials: Credentials are empty or not an array.',
            ), 400 );
        }

        // For BigCommerce, ensure access_token is present and not empty
        if ( $connector_name === 'bigcommerce' ) {
            $access_token = isset( $credentials['access_token'] ) ? trim( $credentials['access_token'] ) : '';
            if ( empty( $access_token ) ) {
                return new \WP_REST_Response( array(
                    'success' => false,
                    'error'   => 'Missing or empty access token. Please check your BigCommerce API credentials.',
                    'debug'   => array(
                        'has_credentials' => ! empty( $credentials ),
                        'credentials_keys' => array_keys( $credentials ),
                    ),
                ), 400 );
            }
        }

        $result = PlatformConnectorService::fetch_data( $connector_name, $credentials, $data_type, $options );

        // Convert first item to preview format
        if ( $result['success'] && ! empty( $result['data'] ) ) {
            $first_item = $result['data'][0] ?? array();
            if ( ! empty( $first_item ) ) {
                $converted = PlatformConnectorService::convert_data( $connector_name, $first_item, $data_type );
                $result['preview'] = $converted;
            }
        }

        return $result;
    }

    public static function handle_webhook( $request ) {
        $import_id = absint( $request['import_id'] );
        $payload   = $request->get_json_params();

        if ( $import_id < 1 ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => 'Invalid import ID.',
            ), 400 );
        }

        $result = PlatformConnectorService::process_webhook( $import_id, $payload );
        return $result;
    }

    /** ---------------- Dry Run / Test Mode ---------------- */
    public static function dry_run_import( $request ) {
        $import_id = absint( $request['import_id'] );
        if ( $import_id < 1 ) {
            return new \WP_REST_Response( array( 
                'success' => false,
                'message' => 'Invalid import ID.' 
            ), 400 );
        }

        $options = array(
            'limit'             => isset( $request['limit'] ) ? absint( $request['limit'] ) : 10,
            'validate_required' => isset( $request['validate_required'] ) ? (bool) $request['validate_required'] : true,
            'validate_format'   => isset( $request['validate_format'] ) ? (bool) $request['validate_format'] : true,
            'check_duplicates'  => isset( $request['check_duplicates'] ) ? (bool) $request['check_duplicates'] : true,
            'skip_unchanged'    => isset( $request['skip_unchanged'] ) ? (bool) $request['skip_unchanged'] : false,
        );

        try {
            $results = DryRunService::run( $import_id, $options );
            if ( ! isset( $results['success'] ) ) {
                $results['success'] = true;
            }
            return $results;
        } catch ( \Throwable $e ) {
            \PrimeLink\Helpers\LoggerHelper::exception( $e, array( 'context' => 'Dry Run' ) );
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => 'Dry run failed: ' . $e->getMessage(),
                'trace'   => defined( 'WP_DEBUG' ) && WP_DEBUG ? $e->getTraceAsString() : '',
            ), 500 );
        }
    }

    /**
     * List files inside uploads/primelink (common feed extensions).
     */
    public static function list_uploads( $request ) {
        $upload_dir = wp_upload_dir();
        $base       = trailingslashit( $upload_dir['basedir'] ) . 'primelink';

        if ( ! is_dir( $base ) ) {
            return rest_ensure_response( array( 'files' => array() ) );
        }

        $extensions = array( 'csv', 'xml', 'xlsx', 'xls', 'txt', 'json' );

        $files = array();
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator( $base, \FilesystemIterator::SKIP_DOTS ),
            \RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ( $iterator as $file_info ) {
            if ( $file_info->isDir() ) {
                continue;
            }
            $ext = strtolower( $file_info->getExtension() );
            if ( ! in_array( $ext, $extensions, true ) ) {
                continue;
            }
            $full = $file_info->getPathname();
            $files[] = array(
                'path'  => str_replace( $upload_dir['basedir'], '/wp-content/uploads', $full ),
                'mtime' => $file_info->getMTime(),
                'size'  => $file_info->getSize(),
            );
        }

        usort(
            $files,
            function ( $a, $b ) {
                return ( $b['mtime'] ?? 0 ) <=> ( $a['mtime'] ?? 0 );
            }
        );

        $files = array_slice( $files, 0, 100 );

        return rest_ensure_response( array( 'files' => $files ) );
    }

    private static function resolve_file_from_draft( $import_id ) {
        $drafts = get_option( 'mt_primelink_import_drafts', array() );
        if ( empty( $drafts[ $import_id ] ) ) {
            return new \WP_REST_Response( array( 'message' => 'Draft not found.' ), 404 );
        }

        $draft = $drafts[ $import_id ];
        $file  = $draft['step_data'][2]['file'] ?? null;
        if ( empty( $file['path'] ) ) {
            $file = $draft['file'] ?? null;
        }
        if ( empty( $file['path'] ) && ! empty( $draft['step_data'] ) && is_array( $draft['step_data'] ) ) {
            foreach ( $draft['step_data'] as $step_data ) {
                if ( ! empty( $step_data['file']['path'] ) ) {
                    $file = $step_data['file'];
                    break;
                }
            }
        }
        if ( empty( $file['path'] ) ) {
            return new \WP_REST_Response( array( 'message' => 'No file selected.' ), 404 );
        }
        return $file;
    }

    /** ---------------- Settings ---------------- */
    private static function get_settings_option_key() {
        return 'mt_primelink_settings';
    }

    public static function get_settings() {
        $settings = get_option( self::get_settings_option_key(), array() );
        if ( ! is_array( $settings ) ) {
            $settings = array();
        }

        $defaults = array(
            'license_key'   => '',
            'license_status'=> '',
            'enable_beta'   => 0,
            'enable_logs'   => 0,
            'default_iteration_size' => 20,
            'default_speed_opt'      => 0,
            'fast_import_mode'       => 0,
            'notify_email'           => get_option( 'admin_email' ),
            'notify_on_finish'       => 0,
            'notify_on_error'        => 0,
            'notify_phone'           => '',
            'notify_sms'             => 0,
            'notify_whatsapp'        => 0,
            'notify_tpl_finish'      => 'Import #{import_id} finished. Status: {status}. Created: {created}, Updated: {updated}, Skipped: {skipped}, Errors: {errors}',
            'notify_tpl_error'       => 'Import #{import_id} finished with errors: {errors}. Created: {created}, Updated: {updated}, Skipped: {skipped}',
            'notify_builder'         => '',
            'cleanup_days'           => 0,
            'uninstall_delete_data'  => 0,
            'dropbox_access_token'   => '',
            'dropbox_default_path'   => '',
            'gdrive_api_key'         => '',
            'gdrive_client_id'       => '',
            'gdrive_client_secret'   => '',
            'gdrive_access_token'    => '',
            'gdrive_refresh_token'   => '',
            'gdrive_token_expires'   => 0,
            'gdrive_default_file'    => '',
            'onedrive_client_id'     => '',
            'onedrive_client_secret' => '',
            'onedrive_tenant_id'     => '',
            'onedrive_access_token'  => '',
            'onedrive_refresh_token'  => '',
            'onedrive_token_expires' => 0,
            'onedrive_default_file'  => '',
            'ai_enable'             => 0,
            'ai_key'                => '',
            'ai_model'              => 'gpt-4o-mini',
            'ai_max_tokens'         => 1200,
            'ai_rpm'                => 5,
            'ai_threshold'          => 50,
            'cron_mode'              => 'wp',
            'cron_token'             => '',
            'source_retention_days'  => 0,
            'ftp_protocol'           => 'ftp',
            'ftp_host'               => '',
            'ftp_port'               => '',
            'ftp_user'               => '',
            'ftp_pass'               => '',
            'ftp_passive'            => 1,
            'ftp_root'               => '',
            'ftp_default_file'       => '',
            'platform_connectors'    => array(),
            // Module settings (all enabled by default for backward compatibility)
            'module_import'          => 1,
            'module_export'          => 1,
            'module_templates'       => 1,
            'module_statistics'      => 1,
            'module_system_status'    => 1,
            'module_live_feed'       => 1,
            'module_anomaly_detection' => 1,
            'module_automation'      => 1,
        );

        $merged = array_merge( $defaults, $settings );

        if ( empty( $merged['cron_token'] ) ) {
            $merged['cron_token'] = wp_generate_password( 16, false, false );
            update_option( self::get_settings_option_key(), $merged, false );
        }

        return $merged;
    }

    public static function save_settings( $request ) {
        $key         = sanitize_text_field( $request['license_key'] ?? '' );
        $enable_beta = ! empty( $request['enable_beta'] ) ? 1 : 0;
        $enable_logs = ! empty( $request['enable_logs'] ) ? 1 : 0;
        $default_iteration_size = isset( $request['default_iteration_size'] ) ? max( 1, min( 500, absint( $request['default_iteration_size'] ) ) ) : 20;
        $default_speed_opt      = ! empty( $request['default_speed_opt'] ) ? 1 : 0;
        $fast_import_mode       = ! empty( $request['fast_import_mode'] ) ? 1 : 0;
        $notify_email           = sanitize_email( $request['notify_email'] ?? '' );
        $notify_on_finish       = ! empty( $request['notify_on_finish'] ) ? 1 : 0;
        $notify_on_error        = ! empty( $request['notify_on_error'] ) ? 1 : 0;
        $notify_phone           = sanitize_text_field( $request['notify_phone'] ?? '' );
        $notify_sms             = ! empty( $request['notify_sms'] ) ? 1 : 0;
        $notify_whatsapp        = ! empty( $request['notify_whatsapp'] ) ? 1 : 0;
        $notify_tpl_finish      = sanitize_textarea_field( $request['notify_tpl_finish'] ?? '' );
        $notify_tpl_error       = sanitize_textarea_field( $request['notify_tpl_error'] ?? '' );
        // JSON structure, keep as-is to preserve HTML payload inside blocks (sanitization happens when rendering).
        $notify_builder         = isset( $request['notify_builder'] ) ? (string) $request['notify_builder'] : '';
        $cleanup_days           = isset( $request['cleanup_days'] ) ? max( 0, absint( $request['cleanup_days'] ) ) : 0;
        $uninstall_delete_data  = ! empty( $request['uninstall_delete_data'] ) ? 1 : 0;
        $dropbox_access_token   = sanitize_text_field( $request['dropbox_access_token'] ?? '' );
        $dropbox_default_path   = sanitize_text_field( $request['dropbox_default_path'] ?? '' );
        $gdrive_api_key         = sanitize_text_field( $request['gdrive_api_key'] ?? '' );
        $gdrive_client_id        = sanitize_text_field( $request['gdrive_client_id'] ?? '' );
        $gdrive_client_secret    = sanitize_text_field( $request['gdrive_client_secret'] ?? '' );
        $gdrive_default_file    = sanitize_text_field( $request['gdrive_default_file'] ?? '' );
        $onedrive_client_id     = sanitize_text_field( $request['onedrive_client_id'] ?? '' );
        $onedrive_client_secret = sanitize_text_field( $request['onedrive_client_secret'] ?? '' );
        $onedrive_tenant_id     = sanitize_text_field( $request['onedrive_tenant_id'] ?? '' );
        $onedrive_default_file  = sanitize_text_field( $request['onedrive_default_file'] ?? '' );
        $ai_enable             = ! empty( $request['ai_enable'] ) ? 1 : 0;
        $ai_key                = sanitize_text_field( $request['ai_key'] ?? '' );
        $ai_model              = sanitize_text_field( $request['ai_model'] ?? '' );
        $ai_max_tokens         = isset( $request['ai_max_tokens'] ) ? max( 0, absint( $request['ai_max_tokens'] ) ) : 0;
        $ai_rpm                = isset( $request['ai_rpm'] ) ? max( 0, absint( $request['ai_rpm'] ) ) : 0;
        $ai_threshold          = isset( $request['ai_threshold'] ) ? max( 0, min( 100, absint( $request['ai_threshold'] ) ) ) : 0;
        $cron_mode              = in_array( $request['cron_mode'] ?? 'wp', array( 'wp', 'external' ), true ) ? $request['cron_mode'] : 'wp';
        $cron_token             = sanitize_text_field( $request['cron_token'] ?? '' );
        if ( empty( $cron_token ) ) {
            $cron_token = wp_generate_password( 16, false, false );
        }
        $source_retention_days  = isset( $request['source_retention_days'] ) ? max( 0, absint( $request['source_retention_days'] ) ) : 0;
        $ftp_protocol           = sanitize_text_field( $request['ftp_protocol'] ?? 'ftp' );
        $ftp_host               = sanitize_text_field( $request['ftp_host'] ?? '' );
        $ftp_port               = isset( $request['ftp_port'] ) ? absint( $request['ftp_port'] ) : '';
        $ftp_user               = sanitize_text_field( $request['ftp_user'] ?? '' );
        $ftp_pass               = sanitize_text_field( $request['ftp_pass'] ?? '' );
        $ftp_passive            = ! empty( $request['ftp_passive'] ) ? 1 : 0;
        $ftp_root               = sanitize_text_field( $request['ftp_root'] ?? '' );
        $ftp_default_file       = sanitize_text_field( $request['ftp_default_file'] ?? '' );

        // Platform connectors (can be nested arrays/objects)
        $platform_connectors_raw = $request['platform_connectors'] ?? array();
        if ( $platform_connectors_raw instanceof \stdClass ) {
            $platform_connectors_raw = (array) $platform_connectors_raw;
        }
        $platform_connectors = is_array( $platform_connectors_raw ) ? $platform_connectors_raw : array();

        // Start from existing settings then override keys from request (to avoid wiping unrelated keys)
        $settings = self::get_settings();

        $settings['license_key']    = $key;
        $settings['license_status'] = $key ? 'saved' : ( $settings['license_status'] ?? '' );
        $settings['enable_beta']    = $enable_beta;
        $settings['enable_logs']    = $enable_logs;
        $settings['default_iteration_size'] = $default_iteration_size;
        $settings['default_speed_opt']      = $default_speed_opt;
        $settings['fast_import_mode']       = $fast_import_mode;
        $settings['notify_email']           = $notify_email;
        $settings['notify_on_finish']       = $notify_on_finish;
        $settings['notify_on_error']        = $notify_on_error;
        $settings['notify_phone']           = $notify_phone;
        $settings['notify_sms']             = $notify_sms;
        $settings['notify_whatsapp']        = $notify_whatsapp;
        $settings['notify_tpl_finish']      = $notify_tpl_finish;
        $settings['notify_tpl_error']       = $notify_tpl_error;
        $settings['notify_builder']         = $notify_builder;
        $settings['cleanup_days']           = $cleanup_days;
        $settings['uninstall_delete_data']  = $uninstall_delete_data;
        $settings['dropbox_access_token']   = $dropbox_access_token;
        $settings['dropbox_default_path']   = $dropbox_default_path;
        $settings['gdrive_api_key']         = $gdrive_api_key;
        $settings['gdrive_client_id']       = $gdrive_client_id;
        $settings['gdrive_client_secret']  = $gdrive_client_secret;
        $settings['gdrive_default_file']    = $gdrive_default_file;
        $settings['onedrive_client_id']     = $onedrive_client_id;
        $settings['onedrive_client_secret'] = $onedrive_client_secret;
        $settings['onedrive_tenant_id']     = $onedrive_tenant_id;
        $settings['onedrive_default_file']  = $onedrive_default_file;
        $settings['ai_enable']              = $ai_enable;
        $settings['ai_key']                 = $ai_key;
        $settings['ai_model']               = $ai_model;
        $settings['ai_max_tokens']          = $ai_max_tokens;
        $settings['ai_rpm']                 = $ai_rpm;
        $settings['ai_threshold']           = $ai_threshold;
        $settings['cron_mode']              = $cron_mode;
        $settings['cron_token']             = $cron_token;
        $settings['source_retention_days']  = $source_retention_days;
        $settings['ftp_protocol']           = $ftp_protocol;
        $settings['ftp_host']               = $ftp_host;
        $settings['ftp_port']               = $ftp_port;
        $settings['ftp_user']               = $ftp_user;
        $settings['ftp_pass']               = $ftp_pass;
        $settings['ftp_passive']            = $ftp_passive;
        $settings['ftp_root']               = $ftp_root;
        $settings['ftp_default_file']       = $ftp_default_file;
        $settings['platform_connectors']    = $platform_connectors;

        // Module settings
        $settings['module_import']           = ! empty( $request['module_import'] ) ? 1 : 0;
        $settings['module_export']           = ! empty( $request['module_export'] ) ? 1 : 0;
        $settings['module_templates']        = ! empty( $request['module_templates'] ) ? 1 : 0;
        $settings['module_statistics']       = ! empty( $request['module_statistics'] ) ? 1 : 0;
        $settings['module_system_status']    = ! empty( $request['module_system_status'] ) ? 1 : 0;
        $settings['module_live_feed']        = ! empty( $request['module_live_feed'] ) ? 1 : 0;
        $settings['module_anomaly_detection'] = ! empty( $request['module_anomaly_detection'] ) ? 1 : 0;
        $settings['module_automation']       = ! empty( $request['module_automation'] ) ? 1 : 0;

        // Mirror uninstall preference into dedicated option used by uninstall.php.
        update_option( 'mt_primelink_uninstall_delete_data', $uninstall_delete_data, false );

        // Anomaly Detection Settings
        $settings['anomaly_detection_enabled'] = ! empty( $request['anomaly_detection_enabled'] ) ? 1 : 0;
        $settings['anomaly_detection_config']  = isset( $request['anomaly_detection_config'] ) ? array(
            'price_drop_threshold'      => isset( $request['anomaly_detection_config']['price_drop_threshold'] ) ? max( 0, min( 100, absint( $request['anomaly_detection_config']['price_drop_threshold'] ) ) ) : 50,
            'price_increase_threshold'  => isset( $request['anomaly_detection_config']['price_increase_threshold'] ) ? max( 0, min( 1000, absint( $request['anomaly_detection_config']['price_increase_threshold'] ) ) ) : 200,
            'price_deviation_threshold' => isset( $request['anomaly_detection_config']['price_deviation_threshold'] ) ? max( 0, min( 100, absint( $request['anomaly_detection_config']['price_deviation_threshold'] ) ) ) : 50,
            'max_absolute_price_change' => isset( $request['anomaly_detection_config']['max_absolute_price_change'] ) ? max( 0, floatval( $request['anomaly_detection_config']['max_absolute_price_change'] ) ) : 0,
            'stock_change_threshold'    => isset( $request['anomaly_detection_config']['stock_change_threshold'] ) ? max( 0, min( 100, absint( $request['anomaly_detection_config']['stock_change_threshold'] ) ) ) : 90,
            'max_stock_threshold'       => isset( $request['anomaly_detection_config']['max_stock_threshold'] ) ? max( 0, absint( $request['anomaly_detection_config']['max_stock_threshold'] ) ) : 10000,
            'content_change_threshold'  => isset( $request['anomaly_detection_config']['content_change_threshold'] ) ? max( 0, min( 100, absint( $request['anomaly_detection_config']['content_change_threshold'] ) ) ) : 30,
            'require_sku'               => ! empty( $request['anomaly_detection_config']['require_sku'] ) ? 1 : 0,
            'pause_on_critical'         => ! empty( $request['anomaly_detection_config']['pause_on_critical'] ) ? 1 : 0,
            'detect_script_injection'   => ! empty( $request['anomaly_detection_config']['detect_script_injection'] ) ? 1 : 0,
        ) : array();

        update_option( self::get_settings_option_key(), $settings, false );

        return array(
            'saved'    => true,
            'settings' => $settings,
        );
    }


    /** ---------------- Create Draft ---------------- */
    /**
     * Index endpoint - returns API information.
     *
     * @param \WP_REST_Request $request Request object.
     * @return array
     */
    public static function index( $request ) {
        return array(
            'name'        => 'PrimeLink REST API',
            'version'     => MT_PRIMELINK_VERSION,
            'namespace'   => 'primelink/v1',
            'description' => __( 'REST API endpoints for PrimeLink Import/Export plugin.', 'primelink' ),
            'routes'      => array(
                'import' => array(
                    '/import/create-draft',
                    '/import/handle-source',
                    '/import/preview',
                    '/import/save-step',
                    '/import/templates',
                    '/import/run/start',
                    '/import/run/status',
                    '/import/uploads',
                    '/import/get-draft',
                ),
                'export' => array(
                    '/export/create-draft',
                    '/export/get-draft',
                    '/export/preview',
                    '/export/run/start',
                    '/export/download',
                ),
                'wizard' => array(
                    '/wizard/verify-license',
                    '/wizard/save-setup',
                    '/wizard/test-system',
                    '/wizard/complete',
                    '/wizard/restart',
                ),
                'settings' => array(
                    '/settings',
                    '/settings/save',
                ),
            ),
        );
    }

    public static function create_draft() {
        $drafts = get_option( 'mt_primelink_import_drafts', array() );

        $id = time();
        $drafts[ $id ] = array(
            'created_at'    => current_time( 'mysql' ),
            'step_data'     => array(),
            'current_step'  => 1,
        );

        update_option( 'mt_primelink_import_drafts', $drafts, false );

        return array( 'import_id' => $id );
    }

    /** ---------------- Save Step ---------------- */
    public static function save_step( $request ) {
        $import_id = absint( $request['import_id'] );
        $step      = absint( $request['step'] );
        $data      = (array) ( $request['data'] ?? array() );

        $drafts = get_option( 'mt_primelink_import_drafts', array() );
        if ( empty( $drafts[ $import_id ] ) ) {
            return new \WP_REST_Response( array( 'message' => 'Draft not found.' ), 404 );
        }

        if ( empty( $drafts[ $import_id ]['step_data'] ) ) {
            $drafts[ $import_id ]['step_data'] = array();
        }

        $prev = $drafts[ $import_id ]['step_data'][ $step ] ?? array();
        if ( is_array( $prev ) && is_array( $data ) ) {
            $drafts[ $import_id ]['step_data'][ $step ] = array_replace_recursive( $prev, $data );
        } else {
            $drafts[ $import_id ]['step_data'][ $step ] = $data;
        }

        // If step 1 was saved with a type, set it on the draft root for easy access
        if ( 1 === $step && isset( $data['type'] ) ) {
            $drafts[ $import_id ]['type'] = sanitize_text_field( $data['type'] );
        }

        // If step 6 scheduling was saved, persist next_run for cron safety.
        if ( 6 === $step ) {
            $sched = $drafts[ $import_id ]['step_data'][6]['schedule'] ?? array();
            if ( ! empty( $sched['enabled'] ) ) {
                $start = isset( $sched['start_time'] ) ? $sched['start_time'] : '';
                $start_ts = $start ? strtotime( 'today ' . $start ) : time();
                if ( $start_ts < time() ) {
                    $start_ts = time() + 60;
                }
                $drafts[ $import_id ]['step_data'][6]['schedule']['next_run'] = $start_ts;
            }
        }

        $drafts[ $import_id ]['current_step'] = $step;

        update_option( 'mt_primelink_import_drafts', $drafts, false );

        return array( 'saved' => true );
    }

    /** ---------------- Handle Source (upload/url/existing) ---------------- */
    public static function handle_source( $request ) {
        // For FormData, try $_POST first (WooCommerce might populate it)
        // Then try get_param() (works for JSON and registered args)
        // Then try body_params and query_params
        
        $import_id = 0;
        $source    = '';
        
        // Try $_POST first (for FormData)
        if ( ! empty( $_POST['import_id'] ) ) {
            $import_id = absint( $_POST['import_id'] );
        }
        if ( ! empty( $_POST['source'] ) ) {
            $source = sanitize_text_field( $_POST['source'] );
        }
        
        // If not found in $_POST, try get_param() (works for JSON and registered args)
        if ( empty( $import_id ) ) {
            $import_id_param = $request->get_param( 'import_id' );
            if ( ! is_null( $import_id_param ) ) {
                $import_id = absint( $import_id_param );
            }
        }
        if ( empty( $source ) ) {
            $source_param = $request->get_param( 'source' );
            if ( ! is_null( $source_param ) ) {
                $source = sanitize_text_field( $source_param );
            }
        }
        
        // If still not found, try body_params and query_params
        if ( empty( $import_id ) || empty( $source ) ) {
            $body_params = $request->get_body_params();
            $query_params = $request->get_params();
            $params = array_merge( $query_params, $body_params );
            
            if ( empty( $import_id ) && isset( $params['import_id'] ) ) {
                $import_id = absint( $params['import_id'] );
            }
            if ( empty( $source ) && isset( $params['source'] ) ) {
                $source = sanitize_text_field( $params['source'] );
            }
        }


        $drafts = get_option( 'mt_primelink_import_drafts', array() );
        
        // If draft not found, try to create it if import_id is 0
        if ( empty( $drafts[ $import_id ] ) ) {
            // If import_id is 0 or invalid, create a new draft
            if ( $import_id === 0 ) {
                $id = time();
                $drafts[ $id ] = array(
                    'created_at'    => current_time( 'mysql' ),
                    'step_data'     => array(),
                    'current_step'  => 1,
                );
                update_option( 'mt_primelink_import_drafts', $drafts, false );
                $import_id = $id;
            } else {
                return new \WP_REST_Response( array( 
                    'message' => 'Draft not found.',
                    'import_id' => $import_id,
                ), 404 );
            }
        }

        // If source is empty but we have a file, assume it's desktop upload
        if ( empty( $source ) ) {
            $files = $request->get_file_params();
            if ( ! empty( $files['file'] ) ) {
                $source = 'desktop';
            }
        }

        // Get params from request - try multiple methods
        $params = array();
        $body_params = $request->get_body_params();
        $query_params = $request->get_params();
        $merged_params = array_merge( $query_params, $body_params );
        
        // If JSON was sent (Content-Type: application/json), body_params will contain the parsed JSON
        // If form data was sent, check for payload JSON string or direct params
        $payload_raw = $merged_params['payload'] ?? '';
        if ( empty( $payload_raw ) && ! empty( $_POST['payload'] ) ) {
            $payload_raw = $_POST['payload'];
        }
        
        // If payload is a JSON string, decode it
        if ( ! empty( $payload_raw ) && is_string( $payload_raw ) ) {
            $decoded = json_decode( wp_unslash( $payload_raw ), true );
            if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
                $params = $decoded;
                // If source is in payload but not yet set, use it
                if ( empty( $source ) && ! empty( $params['source'] ) ) {
                    $source = sanitize_text_field( $params['source'] );
                }
            }
        }
        
        // If params is still empty or incomplete, use merged_params directly
        // This handles both JSON body and form data cases
        if ( empty( $params ) || ( empty( $params['protocol'] ) && empty( $params['host'] ) ) ) {
            $params = $merged_params;
            
            // If source is in params but not yet set, use it
            if ( empty( $source ) && ! empty( $params['source'] ) ) {
                $source = sanitize_text_field( $params['source'] );
            }
        }

        switch ( $source ) {

            case 'desktop':
                // For REST API, populate $_FILES from request file params
                // DesktopUploadSource expects $_FILES['file']
                $files = $request->get_file_params();
                if ( ! empty( $files['file'] ) ) {
                    // Temporarily populate $_FILES for DesktopUploadSource
                    $original_files = isset( $_FILES ) ? $_FILES : array();
                    $_FILES['file'] = $files['file'];
                }
                
                // Use DesktopUploadSource handler (same as AJAX)
                $handler = new \PrimeLink\Modules\Imports\DataSources\DesktopUploadSource();
                $res = $handler->handle( array() );
                
                // Restore original $_FILES
                if ( isset( $original_files ) ) {
                    $_FILES = $original_files;
                }
                
                // Normalize response format for REST API
                if ( ! isset( $res['error'] ) ) {
                    // Determine file type from extension
                    $ext  = strtolower( pathinfo( $res['path'], PATHINFO_EXTENSION ) );
                    $type = 'csv';
                    if ( 'xml' === $ext ) {
                        $type = 'xml';
                    } elseif ( in_array( $ext, array( 'xlsx', 'xls' ), true ) ) {
                        $type = 'xlsx';
                    } elseif ( 'json' === $ext ) {
                        $type = 'json';
                    }
                    
                    $res['type'] = $type;
                    $res['source'] = 'desktop';
                    $res['uploaded_at'] = current_time( 'mysql' );
                }
                break;

            case 'url':
                $handler = new UrlSource();
                $res = $handler->handle( array( 'url' => $params['url'] ?? '' ) );
                // Normalize legacy keys if needed
                if ( isset( $res['file_path'] ) && empty( $res['path'] ) ) {
                    $res['path'] = $res['file_path'];
                }
                if ( isset( $res['file_type'] ) && empty( $res['type'] ) ) {
                    $res['type'] = $res['file_type'];
                }
                break;

            case 'existing':
                $handler = new ExistingFileSource();
                $res = $handler->handle( array( 'path' => $params['path'] ?? '' ) );
                if ( isset( $res['file_path'] ) && empty( $res['path'] ) ) {
                    $res['path'] = $res['file_path'];
                }
                if ( isset( $res['file_type'] ) && empty( $res['type'] ) ) {
                    $res['type'] = $res['file_type'];
                }
                break;

            case 'dropbox':
                $handler = new DropboxSource( self::get_settings() );
                $res = $handler->handle( array(
                    'path' => $params['path'] ?? '',
                ) );
                if ( isset( $res['file_path'] ) && empty( $res['path'] ) ) {
                    $res['path'] = $res['file_path'];
                }
                if ( isset( $res['file_type'] ) && empty( $res['type'] ) ) {
                    $res['type'] = $res['file_type'];
                }
                break;

            case 'ftp':
                $handler = new FtpSource( self::get_settings() );
                $res = $handler->handle( array(
                    'protocol' => $params['protocol'] ?? '',
                    'host'     => $params['host'] ?? '',
                    'port'     => $params['port'] ?? '',
                    'user'     => $params['user'] ?? '',
                    'pass'     => $params['pass'] ?? '',
                    'root'     => $params['root'] ?? '',
                    'path'     => $params['path'] ?? '',
                    'passive'  => isset( $params['passive'] ) ? (bool) $params['passive'] : null,
                    'test_only' => isset( $params['test_only'] ) ? (bool) $params['test_only'] : false,
                ) );
                if ( isset( $res['file_path'] ) && empty( $res['path'] ) ) {
                    $res['path'] = $res['file_path'];
                }
                if ( isset( $res['file_type'] ) && empty( $res['type'] ) ) {
                    $res['type'] = $res['file_type'];
                }
                break;

            case 'gdrive':
                $handler = new GoogleDriveSource( self::get_settings() );
                $res = $handler->handle( array(
                    'file' => $params['path'] ?? ( $params['file'] ?? '' ),
                ) );
                if ( isset( $res['file_path'] ) && empty( $res['path'] ) ) {
                    $res['path'] = $res['file_path'];
                }
                if ( isset( $res['file_type'] ) && empty( $res['type'] ) ) {
                    $res['type'] = $res['file_type'];
                }
                break;

            case 'onedrive':
                $handler = new OneDriveSource( self::get_settings() );
                $res = $handler->handle( array(
                    'file' => $params['path'] ?? ( $params['file'] ?? '' ),
                ) );
                if ( isset( $res['file_path'] ) && empty( $res['path'] ) ) {
                    $res['path'] = $res['file_path'];
                }
                if ( isset( $res['file_type'] ) && empty( $res['type'] ) ) {
                    $res['type'] = $res['file_type'];
                }
                break;

            default:
                $res = array( 'error' => 'Source not implemented yet.' );
        }

        // No longer auto-converting XLS/XLSX to CSV - we now use PhpSpreadsheet/XlsxParser directly
        // This preserves the original file and allows better Excel support

        // For test_only mode (connection testing), return success/error directly
        if ( isset( $params['test_only'] ) && $params['test_only'] ) {
            if ( isset( $res['error'] ) ) {
                return new \WP_REST_Response( array( 
                    'success' => false,
                    'message' => $res['error']
                ), 400 );
            }
            if ( isset( $res['success'] ) && $res['success'] ) {
                return new \WP_REST_Response( array( 
                    'success' => true,
                    'data' => array( 'message' => $res['message'] ?? 'Connection successful' )
                ), 200 );
            }
            // If neither success nor error, treat as error
            return new \WP_REST_Response( array( 
                'success' => false,
                'message' => 'Unknown error during connection test'
            ), 400 );
        }

        if ( isset( $res['error'] ) ) {
            return new \WP_REST_Response( array( 'message' => $res['error'] ), 400 );
        }

        // ---- SAVE FILE SAFELY IN DRAFT (redundant locations) ----
        if ( empty( $drafts[ $import_id ]['step_data'] ) ) {
            $drafts[ $import_id ]['step_data'] = array();
        }

        // standard location
        $drafts[ $import_id ]['step_data'][2]['file'] = $res;
        // root fallback
        $drafts[ $import_id ]['file'] = $res;

        update_option( 'mt_primelink_import_drafts', $drafts, false );

        return array(
            'file'      => $res,
            'import_id' => $import_id,
        );
    }

    /**
     * Convert XLSX/XLS to CSV so the rest of the pipeline can parse it.
     */
    private static function maybe_convert_spreadsheet_to_csv( $res ) {
        if ( empty( $res['path'] ) || empty( $res['type'] ) ) {
            return $res;
        }
        $type = strtolower( $res['type'] );
        if ( ! in_array( $type, array( 'xlsx', 'xls' ), true ) ) {
            return $res;
        }

        if ( ! class_exists( '\ZipArchive' ) ) {
            $res['error'] = 'PHP ZipArchive extension is missing. Convert the file to CSV and try again.';
            return $res;
        }

        $parser = ParserFactory::make( 'xlsx' );
        $preview = $parser->preview( $res['path'], array( 'max_rows' => 0 ) );

        if ( empty( $preview['headers'] ) && empty( $preview['rows'] ) ) {
            $msg = isset( $preview['message'] ) ? $preview['message'] : 'Spreadsheet conversion failed. Save as CSV and retry.';
            $res['error'] = $msg;
            return $res;
        }

        $rows = array_merge( array( $preview['headers'] ), $preview['rows'] );
        $dir  = dirname( $res['path'] );
        $csv_name = pathinfo( $res['path'], PATHINFO_FILENAME ) . '-converted.csv';
        $csv_path = $dir . DIRECTORY_SEPARATOR . $csv_name;

        $fh = @fopen( $csv_path, 'w' );
        if ( ! $fh ) {
            $res['error'] = 'Unable to create CSV file from spreadsheet.';
            return $res;
        }
        foreach ( $rows as $row ) {
            fputcsv( $fh, $row );
        }
        fclose( $fh );

        $res['path']          = $csv_path;
        $res['type']          = 'csv';
        $res['original_name'] = ( isset( $res['original_name'] ) ? $res['original_name'] . ' (converted)' : basename( $csv_path ) );

        return $res;
    }

    /** ---------------- Dropbox list ---------------- */
    public static function dropbox_list( $request ) {
        $settings = self::get_settings();
        $token    = $settings['dropbox_access_token'] ?? '';
        if ( ! $token ) {
            return new \WP_REST_Response( array( 'message' => 'Dropbox token missing. Add it in Settings → Integrations.' ), 400 );
        }

        $path = isset( $request['path'] ) ? (string) $request['path'] : '';
        // If a file path is provided, use its directory.
        if ( $path && false === strpos( $path, '://' ) ) {
            $parts = explode( '/', trim( $path, '/' ) );
            if ( count( $parts ) > 1 ) {
                array_pop( $parts );
                $path = '/' . implode( '/', $parts );
            } else {
                $path = '';
            }
        }

        $body = wp_json_encode( array(
            'path'      => $path,
            'recursive' => false,
            'limit'     => 100,
        ) );

        $response = \PrimeLink\Helpers\HttpHelper::post( 'https://api.dropboxapi.com/2/files/list_folder', array(
            'headers' => array(
                'Authorization' => 'Bearer ' . $token,
                'Content-Type'  => 'application/json',
            ),
            'body'        => json_decode( $body, true ), // HttpHelper will handle JSON encoding
            'timeout'     => 30,
            'retry'       => 1,
            'retry_delay' => 1,
        ) );

        if ( ! $response['success'] ) {
            return new \WP_REST_Response( array( 'message' => $response['error'] ?? __( 'Failed to list Dropbox folder.', 'primelink' ) ), 400 );
        }

        $code = $response['code'];
        $data = json_decode( $response['body'], true );

        if ( $code < 200 || $code > 299 ) {
            $msg = isset( $data['error_summary'] ) ? $data['error_summary'] : 'Dropbox listing failed.';
            return new \WP_REST_Response( array( 'message' => $msg ), 400 );
        }

        $files = array();
        if ( ! empty( $data['entries'] ) && is_array( $data['entries'] ) ) {
            foreach ( $data['entries'] as $entry ) {
                if ( isset( $entry['.tag'] ) && 'file' === $entry['.tag'] ) {
                    $size = isset( $entry['size'] ) ? absint( $entry['size'] ) : 0;
                    $files[] = array(
                        'name'       => $entry['name'] ?? '',
                        'path'       => $entry['path_display'] ?? '',
                        'size'       => $size,
                        'size_human' => size_format( $size ),
                    );
                }
            }
        }

        return array( 'files' => $files );
    }

    public static function gdrive_list( $request ) {
        $settings = self::get_settings();
        $api_key = $settings['gdrive_api_key'] ?? '';
        $access_token = $settings['gdrive_access_token'] ?? '';
        $refresh_token = $settings['gdrive_refresh_token'] ?? '';
        $token_expires = $settings['gdrive_token_expires'] ?? 0;
        
        // Check if token is expired and try to refresh (only if we have OAuth token)
        if ( $access_token && $refresh_token ) {
            // Refresh if expired or will expire in next 5 minutes
            if ( ! $token_expires || time() >= ( $token_expires - 300 ) ) {
                $new_access_token = self::refresh_gdrive_token( $refresh_token, $settings );
                if ( ! is_wp_error( $new_access_token ) && is_string( $new_access_token ) && ! empty( $new_access_token ) ) {
                    $access_token = $new_access_token;
                    $settings['gdrive_access_token'] = $access_token;
                    // Token expiration is already updated in refresh_gdrive_token
                    update_option( 'mt_primelink_settings', $settings );
                } elseif ( is_wp_error( $new_access_token ) ) {
                    // Refresh failed - clear tokens and require re-auth
                    unset( $settings['gdrive_access_token'] );
                    unset( $settings['gdrive_refresh_token'] );
                    unset( $settings['gdrive_token_expires'] );
                    update_option( 'mt_primelink_settings', $settings );
                    return new \WP_REST_Response( array( 
                        'message' => __( 'Google Drive authentication expired. Please re-authenticate using the "Authenticate with Google Drive" button in Settings → Integrations.', 'primelink' ),
                        'requires_oauth' => true,
                    ), 401 );
                }
            }
        }
        
        // For listing files, prefer OAuth but allow API Key for public files
        // Note: API Key can only list public files (shared with "Anyone with the link")
        if ( ! $access_token && ! $api_key ) {
            return new \WP_REST_Response( array( 
                'message' => __( 'Google Drive authentication required. Please add API Key or authenticate with OAuth in Settings → Integrations.', 'primelink' ),
                'requires_auth' => true,
            ), 400 );
        }
        
        // For listing files, prefer OAuth but allow API Key for public files
        // Note: API Key can only list public files (shared with "Anyone with the link")
        if ( ! $access_token && ! $api_key ) {
            return new \WP_REST_Response( array( 
                'message' => 'Google Drive authentication required. Please add API Key or authenticate with OAuth in Settings → Integrations.',
                'requires_auth' => true,
            ), 400 );
        }

        $folder_id = isset( $request['folder_id'] ) ? sanitize_text_field( $request['folder_id'] ) : 'root';
        $query = isset( $request['query'] ) ? sanitize_text_field( $request['query'] ) : '';
        $page_token = isset( $request['page_token'] ) ? sanitize_text_field( $request['page_token'] ) : '';

        // Build query parameters for Google Drive API
        // For API Key, we can only access public files
        // Use minimal query - API Key may not support 'root' in parents
        $q_parts = array();
        
        // Always exclude folders and trashed files
        $q_parts[] = "trashed = false";
        $q_parts[] = "mimeType != 'application/vnd.google-apps.folder'";
        
        // Folder filter - only if not root
        if ( $folder_id && $folder_id !== 'root' ) {
            $q_parts[] = "'" . $folder_id . "' in parents";
        }
        // Note: For root folder with API Key, we don't use 'root' in parents
        // as it may not work with API Key - we'll get all public files instead
        
        // Add search query if provided
        if ( $query && trim( $query ) !== '' ) {
            $q_parts[] = "name contains '" . str_replace( "'", "\\'", trim( $query ) ) . "'";
        }
        
        $q = implode( ' and ', $q_parts );
        
        // Supported MIME types for filtering in PHP
        $supported_mimes = array(
            'text/csv',
            'application/vnd.ms-excel',
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            'application/vnd.ms-excel.sheet.macroEnabled.12',
            'text/xml',
            'application/xml',
            'application/json',
        );
        
        // Build URL for Google Drive API
        $base_url = 'https://www.googleapis.com/drive/v3/files';
        $params = array(
            'q' => $q,
            'fields' => 'nextPageToken,files(id,name,mimeType,size,modifiedTime)',
            'pageSize' => '50',
            'orderBy' => 'name',
        );
        
        if ( $page_token ) {
            $params['pageToken'] = $page_token;
        }
        
        $url = $base_url . '?' . http_build_query( $params );

        // Use OAuth access token if available, otherwise fall back to API key
        $headers = array();
        if ( $access_token ) {
            $headers['Authorization'] = 'Bearer ' . $access_token;
        } elseif ( $api_key ) {
            $url .= '&key=' . rawurlencode( $api_key );
        }

        $response = \PrimeLink\Helpers\HttpHelper::get( $url, array(
            'headers'     => $headers,
            'timeout'     => 30,
            'retry'       => 1,
            'retry_delay' => 1,
        ) );

        if ( ! $response['success'] ) {
            return new \WP_REST_Response( array( 'message' => $response['error'] ?? __( 'Failed to list Google Drive files.', 'primelink' ) ), 400 );
        }

        $code = $response['code'];
        $data = json_decode( $response['body'], true );

        if ( $code < 200 || $code > 299 ) {
            $error_details = isset( $data['error'] ) ? $data['error'] : array();
            $error_message = isset( $error_details['message'] ) ? $error_details['message'] : __( 'Google Drive listing failed.', 'primelink' );
            $error_reason = isset( $error_details['errors'][0]['reason'] ) ? $error_details['errors'][0]['reason'] : '';
            
            // Provide helpful messages for common errors
            if ( $code === 404 ) {
                if ( ! $access_token && $api_key ) {
                    $msg = __( 'API Key cannot list files from Google Drive. Please authenticate with Google Drive using the "Authenticate with Google Drive" button in Settings → Integrations.', 'primelink' );
                } else {
                    $msg = __( 'No files found or access denied. Please check your Google Drive authentication and ensure files are accessible.', 'primelink' );
                }
            } elseif ( $code === 403 ) {
                if ( ! $access_token ) {
                    $msg = __( 'OAuth authentication required for browsing files. API Key cannot list files from Google Drive. Please click "Authenticate with Google Drive" in Settings → Integrations to enable file browsing.', 'primelink' );
                } elseif ( $error_reason === 'insufficientPermissions' || $error_reason === 'forbidden' ) {
                    $msg = __( 'Access denied. Please check your Google Drive OAuth permissions and try re-authenticating.', 'primelink' );
                } else {
                    $msg = __( 'Access denied. Please check your Google Drive authentication.', 'primelink' );
                }
            } elseif ( $code === 401 ) {
                $msg = __( 'Authentication expired. Please re-authenticate with Google Drive using the "Authenticate with Google Drive" button.', 'primelink' );
            } elseif ( $code === 400 ) {
                $msg = __( 'Invalid request. Error: ', 'primelink' ) . $error_message;
            } else {
                $msg = $error_message;
            }
            
            return new \WP_REST_Response( array( 'message' => $msg ), 400 );
        }

        $files = array();
        if ( ! empty( $data['files'] ) && is_array( $data['files'] ) ) {
            foreach ( $data['files'] as $file ) {
                $mime_type = $file['mimeType'] ?? '';
                
                // Filter by supported MIME types
                $is_supported = false;
                foreach ( $supported_mimes as $supported_mime ) {
                    if ( $mime_type === $supported_mime ) {
                        $is_supported = true;
                        break;
                    }
                }
                
                // Also check by file extension as fallback
                if ( ! $is_supported ) {
                    $name = $file['name'] ?? '';
                    $ext = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
                    $supported_exts = array( 'csv', 'xls', 'xlsx', 'xml', 'json' );
                    if ( in_array( $ext, $supported_exts, true ) ) {
                        $is_supported = true;
                    }
                }
                
                if ( $is_supported ) {
                    $size = isset( $file['size'] ) ? absint( $file['size'] ) : 0;
                    $files[] = array(
                        'id'         => $file['id'] ?? '',
                        'name'       => $file['name'] ?? '',
                        'mime_type'  => $mime_type,
                        'size'       => $size,
                        'size_human' => size_format( $size ),
                        'modified'   => $file['modifiedTime'] ?? '',
                    );
                }
            }
        }

        return array(
            'files'      => $files,
            'next_page_token' => $data['nextPageToken'] ?? null,
        );
    }

    /** ---------------- Step 4 Preview ---------------- */
    public static function preview_file( $request ) {
        $import_id = absint( $request['import_id'] );
        $xpath     = isset( $request['xpath'] ) ? (string) $request['xpath'] : '';
        $page      = isset( $request['page'] ) ? max( 1, absint( $request['page'] ) ) : 1;
        $per_page  = isset( $request['per_page'] ) ? max( 1, min( 200, absint( $request['per_page'] ) ) ) : 20;

        $drafts = get_option( 'mt_primelink_import_drafts', array() );
        if ( empty( $drafts[ $import_id ] ) ) {
            return new \WP_REST_Response( array( 'message' => 'Draft not found.' ), 404 );
        }

        $draft = $drafts[ $import_id ];

        // Check if this is a platform import
        $step2_data = $draft['step_data'][2] ?? array();
        // Check both direct source and nested data source (save_step uses array_replace_recursive)
        $source = $step2_data['source'] ?? ($step2_data['data']['source'] ?? '');

        if ( $source === 'platform' ) {
            // Handle platform import preview
            // Check both direct and nested data structure
            $connector_name = $step2_data['connector'] ?? ($step2_data['data']['connector'] ?? '');
            $credentials    = $step2_data['credentials'] ?? ($step2_data['data']['credentials'] ?? array());
            $data_type      = $step2_data['data_type'] ?? ($step2_data['data']['data_type'] ?? 'products');

            if ( empty( $connector_name ) || empty( $credentials ) ) {
                // Try to get credentials from saved settings if not in draft
                if ( empty( $credentials ) && ! empty( $connector_name ) ) {
                    $settings = get_option( 'mt_primelink_settings', array() );
                    $saved_connectors = $settings['platform_connectors'] ?? array();
                    if ( ! empty( $saved_connectors[ $connector_name ] ) ) {
                        $credentials = $saved_connectors[ $connector_name ];
                    }
                }
                
                if ( empty( $connector_name ) || empty( $credentials ) ) {
                    return new \WP_REST_Response( array( 
                        'message' => 'Platform connector not configured.',
                        'debug' => array(
                            'step2_keys' => array_keys($step2_data),
                            'connector' => $connector_name,
                            'has_credentials' => !empty($credentials),
                            'step2_source' => $step2_data['source'] ?? 'not found',
                        )
                    ), 404 );
                }
            }

            // Calculate offset for pagination
            $offset = max( 0, ( $page - 1 ) * $per_page );
            
            // Fetch data from platform (use original data_type, e.g. 'products')
            // For preview, we fetch enough items to cover the current page
            $options = array(
                'limit' => min( 250, $per_page * $page ), // Fetch enough for current page (max 250 for Shopify)
            );

            // For non-Shopify platforms, use page parameter
            if ( $connector_name !== 'shopify' ) {
                $options['page'] = $page;
                $options['limit'] = $per_page;
            }

            $result = PlatformConnectorService::fetch_data( $connector_name, $credentials, $data_type, $options );
            
            // Normalize data_type for convert_data: 'products' -> 'product', 'orders' -> 'order', 'customers' -> 'customer', 'coupons' -> 'coupon'
            $convert_data_type = $data_type;
            if ( $convert_data_type === 'products' ) {
                $convert_data_type = 'product';
            } elseif ( $convert_data_type === 'orders' ) {
                $convert_data_type = 'order';
            } elseif ( $convert_data_type === 'customers' ) {
                $convert_data_type = 'customer';
            } elseif ( $convert_data_type === 'coupons' ) {
                $convert_data_type = 'coupon';
            }

            if ( ! $result['success'] ) {
                return new \WP_REST_Response( array(
                    'message' => 'Failed to fetch data from platform: ' . ( $result['error'] ?? 'Unknown error' ),
                ), 500 );
            }

            // Convert platform data to preview format
            $platform_items = $result['data'] ?? array();
            $headers = array();
            $rows = array();
            $all_keys = array(); // Collect all possible keys from all rows

            foreach ( $platform_items as $platform_item ) {
                // Convert to WooCommerce format (use normalized data_type)
                $woo_data = PlatformConnectorService::convert_data( $connector_name, $platform_item, $convert_data_type );
                
                if ( empty( $woo_data ) || ! is_array( $woo_data ) ) {
                    \PrimeLink\Helpers\LoggerHelper::debug(
                        'Empty or invalid woo_data',
                        array(
                            'connector' => $connector_name,
                            'data_type' => $data_type,
                            'item_keys' => array_keys( $platform_item ?? array() ),
                        )
                    );
                }
                
                // Flatten to row format
                $row = ImportRunner::flatten_woo_data_to_row( $woo_data );
                
                // Ensure row is an array
                if ( ! is_array( $row ) ) {
                    $row = array();
                }
                
                if ( empty( $row ) && ! empty( $woo_data ) ) {
                    \PrimeLink\Helpers\LoggerHelper::debug(
                        'Row is empty after flattening',
                        array(
                            'woo_data_keys' => array_keys( $woo_data ),
                            'woo_data'      => $woo_data,
                        )
                    );
                }
                
                $rows[] = $row;

                // Collect all keys from this row
                // Iterate through row to collect string keys (associative array keys)
                if ( ! empty( $row ) && is_array( $row ) ) {
                    foreach ( $row as $key => $value ) {
                        // Only collect string keys (not numeric indices)
                        // PHP array_keys can return numeric strings like "0", "1" for indexed arrays
                        // We want only associative keys that are actual strings
                        if ( is_string( $key ) && ! is_numeric( $key ) && ! in_array( $key, $all_keys, true ) ) {
                            $all_keys[] = $key;
                        }
                    }
                }
            }

            // Build headers from all collected keys (unique, preserve order)
            if ( ! empty( $all_keys ) ) {
                $headers = array_values( array_unique( $all_keys ) );
            }

            // Convert rows from associative arrays to indexed arrays for consistency with file imports
            // Each row should have values in the same order as headers
            $indexed_rows = array();
            foreach ( $rows as $row ) {
                $indexed_row = array();
                foreach ( $headers as $header ) {
                    $indexed_row[] = isset( $row[ $header ] ) ? $row[ $header ] : '';
                }
                $indexed_rows[] = $indexed_row;
            }
            $rows = $indexed_rows;

            // Apply pagination for Shopify (since we fetched more items than needed)
            if ( $connector_name === 'shopify' && count( $rows ) > $per_page ) {
                $rows = array_slice( $rows, $offset, $per_page );
            }

            $total = $result['total'] ?? count( $platform_items );
            $total_filtered = count( $rows );

            // Apply filters if any
            $filters = $draft['step_data'][4]['filters'] ?? array();
            if ( isset( $request['filters'] ) && is_array( $request['filters'] ) ) {
                $filters = $request['filters'];
            }
            $filters = self::normalize_filters( $filters );
            if ( ! empty( $filters ) ) {
                $rows = self::apply_filters_to_rows( $headers, $rows, $filters );
                $total_filtered = count( $rows );
            }

            // Return in same format as file-based preview
            return array(
                'preview' => array(
                    'headers'          => $headers,
                    'rows'             => $rows,
                    'total_unfiltered' => $total,
                    'total_filtered'   => $total_filtered,
                    'total'            => $total_filtered,
                    'page'             => $page,
                    'per_page'         => $per_page,
                ),
                'type'    => 'platform',
            );
        }

        // File-based import (existing logic)
        // 1) Standard location (step 2)
        $file = $draft['step_data'][2]['file'] ?? null;

        // 2) Root fallback
        if ( empty( $file['path'] ) ) {
            $file = $draft['file'] ?? null;
        }

        // 3) Deep fallback: scan all steps
        if ( empty( $file['path'] ) && ! empty( $draft['step_data'] ) && is_array( $draft['step_data'] ) ) {
            foreach ( $draft['step_data'] as $step_data ) {
                if ( ! empty( $step_data['file']['path'] ) ) {
                    $file = $step_data['file'];
                    break;
                }
            }
        }

        if ( empty( $file['path'] ) ) {
            // If no file found, check if this might be a platform import that wasn't detected
            $step2_check = $draft['step_data'][2] ?? array();
            if ( ! empty( $step2_check['connector'] ) || ! empty( $step2_check['source'] ) ) {
                return new \WP_REST_Response( array( 
                    'message' => 'Platform import detected but not configured correctly.',
                    'debug' => array(
                        'step2_data' => $step2_check,
                        'has_connector' => !empty($step2_check['connector']),
                        'has_source' => !empty($step2_check['source']),
                    )
                ), 404 );
            }
            return new \WP_REST_Response( array( 'message' => 'No file for preview.' ), 404 );
        }

        // Normalize type if legacy key used
        if ( empty( $file['type'] ) && ! empty( $file['file_type'] ) ) {
            $file['type'] = $file['file_type'];
        }

        $parser = ParserFactory::make( $file['type'] );

        $filters = $draft['step_data'][4]['filters'] ?? array();
        if ( isset( $request['filters'] ) && is_array( $request['filters'] ) ) {
            $filters = $request['filters'];
        }
        $filters = self::normalize_filters( $filters );
        $filters = apply_filters( 'primelink_import_filters', $filters, $import_id, $file );

        $args = array(
            'xpath'    => $xpath,
            'max_rows' => 0, // unlimited
            'filters'  => $filters,
        );

        $args = apply_filters( 'primelink_import_parser_args', $args, $import_id, $file );

        // Cache preview results for URL sources (not local files that might change)
        $use_cache = ( $file['source'] ?? '' ) === 'url';
        $cache_key = null;
        
        if ( $use_cache ) {
            $cache_key = sprintf(
                'preview_%s_%s_%s',
                md5( $file['path'] ?? '' ),
                md5( serialize( $args ) ),
                filemtime( $file['path'] ) ?? 0
            );
            
            $cached = \PrimeLink\Helpers\CacheHelper::get( $cache_key );
            if ( $cached !== null ) {
                return $cached;
            }
        }

        try {
            $preview = $parser->preview( $file['path'], $args );
        } catch ( \Throwable $e ) {
            return new \WP_REST_Response( array(
                'message' => 'Preview failed: ' . $e->getMessage(),
                'error'   => $e->getMessage(),
                'file'    => $e->getFile(),
                'line'    => $e->getLine(),
            ), 500 );
        }

        /**
         * Allow plugins to adjust the preview payload (e.g. filter rows or add meta).
         *
         * @param array $preview    Parser preview array with headers/rows/total.
         * @param int   $import_id  Current import ID.
         * @param array $file       File descriptor from step 2.
         * @param array $args       Parser args (xpath, filters, max_rows).
         */
        $preview = apply_filters( 'primelink_import_preview_result', $preview, $import_id, $file, $args );

        $headers = isset( $preview['headers'] ) && is_array( $preview['headers'] ) ? $preview['headers'] : array();
        $rows    = isset( $preview['rows'] ) && is_array( $preview['rows'] ) ? $preview['rows'] : array();
        $total_unfiltered = count( $rows );

        if ( ! empty( $filters ) ) {
            $rows = self::apply_filters_to_rows( $headers, $rows, $filters );
        }

        $total_filtered = count( $rows );
        $offset         = max( 0, ( $page - 1 ) * $per_page );
        $rows_page      = array_slice( $rows, $offset, $per_page );

        $preview['headers']          = $headers;
        $preview['rows']             = $rows_page;
        $preview['total_unfiltered'] = $total_unfiltered;
        $preview['total_filtered']   = $total_filtered;
        $preview['total']            = $total_filtered;
        $preview['page']             = $page;
        $preview['per_page']         = $per_page;

        $result = array(
            'type'    => $file['type'],
            'preview' => $preview,
        );

        // Cache successful preview results for URL sources (15 minutes)
        if ( $use_cache && $cache_key ) {
            \PrimeLink\Helpers\CacheHelper::set( $cache_key, $result, 900 ); // 15 minutes
        }

        return $result;
    }

    /** ---------------- Step 5 Mapping Preview ---------------- */
    public static function mapping_preview( $request ) {
        $import_id = absint( $request['import_id'] );
        $index     = isset( $request['index'] ) ? absint( $request['index'] ) : 0;
        $filters   = $request['filters'] ?? null;

        $drafts = get_option( 'mt_primelink_import_drafts', array() );
        if ( empty( $drafts[ $import_id ] ) ) {
            return new \WP_REST_Response( array( 'message' => 'Draft not found.' ), 404 );
        }

        $draft = $drafts[ $import_id ];

        // Check if this is a platform import
        $step2_data = $draft['step_data'][2] ?? array();
        // Check both direct source and nested data source
        $source = $step2_data['source'] ?? ($step2_data['data']['source'] ?? '');

        if ( $source === 'platform' ) {
            // Handle platform import preview
            // Check both direct and nested data structure
            $connector_name = $step2_data['connector'] ?? ($step2_data['data']['connector'] ?? '');
            $credentials    = $step2_data['credentials'] ?? ($step2_data['data']['credentials'] ?? array());
            $data_type      = $step2_data['data_type'] ?? ($step2_data['data']['data_type'] ?? 'products');

            if ( empty( $connector_name ) || empty( $credentials ) ) {
                return new \WP_REST_Response( array( 'message' => 'Platform connector not configured.' ), 404 );
            }

            // Fetch data from platform (use original data_type, e.g. 'products')
            $options = array(
                'limit' => 50, // Get more items for preview
            );
            
            // For Shopify, don't use page parameter
            if ( $connector_name !== 'shopify' ) {
                $options['page'] = isset( $request['page'] ) ? absint( $request['page'] ) : 1;
            }

            $result = PlatformConnectorService::fetch_data( $connector_name, $credentials, $data_type, $options );
            
            // Normalize data_type for convert_data: 'products' -> 'product', 'orders' -> 'order', 'customers' -> 'customer'
            $convert_data_type = $data_type;
            if ( $convert_data_type === 'products' ) {
                $convert_data_type = 'product';
            } elseif ( $convert_data_type === 'orders' ) {
                $convert_data_type = 'order';
            } elseif ( $convert_data_type === 'customers' ) {
                $convert_data_type = 'customer';
            }

            if ( ! $result['success'] ) {
                return new \WP_REST_Response( array(
                    'message' => 'Failed to fetch data from platform: ' . ( $result['error'] ?? 'Unknown error' ),
                ), 500 );
            }

            // Convert platform data to preview format
            $platform_items = $result['data'] ?? array();
            $headers = array();
            $rows = array();
            $all_keys = array(); // Collect all possible keys from all rows

            foreach ( $platform_items as $platform_item ) {
                // Convert to WooCommerce format (use normalized data_type)
                $woo_data = PlatformConnectorService::convert_data( $connector_name, $platform_item, $convert_data_type );
                // Flatten to row format
                $row = ImportRunner::flatten_woo_data_to_row( $woo_data );
                $rows[] = $row;

                // Collect all keys from this row
                if ( ! empty( $row ) && is_array( $row ) ) {
                    $all_keys = array_merge( $all_keys, array_keys( $row ) );
                }
            }

            // Build headers from all collected keys (unique, preserve order)
            if ( ! empty( $all_keys ) ) {
                $headers = array_values( array_unique( $all_keys ) );
            }

            $total = $result['total'] ?? count( $rows );
            $item  = isset( $rows[ $index ] ) ? $rows[ $index ] : ( ! empty( $rows ) ? $rows[0] : array() );

            // Build fields tree (flat structure for platform data)
            $fields_tree = array();
            foreach ( $headers as $header ) {
                $fields_tree[] = array(
                    'name'      => $header,
                    'full'      => $header,
                    'is_field'  => true,
                    'children'  => array(),
                );
            }

            return array(
                'success'     => true,
                'fields'      => $headers,
                'fields_tree' => $fields_tree,
                'item'        => $item,
                'total'       => $total,
                'index'       => $index,
                'type'        => 'platform',
            );
        }

        // File-based import (existing logic)
        // Same robust file lookup as preview_file
        $file = $draft['step_data'][2]['file'] ?? null;
        if ( empty( $file['path'] ) ) {
            $file = $draft['file'] ?? null;
        }
        if ( empty( $file['path'] ) && ! empty( $draft['step_data'] ) && is_array( $draft['step_data'] ) ) {
            foreach ( $draft['step_data'] as $step_data ) {
                if ( ! empty( $step_data['file']['path'] ) ) {
                    $file = $step_data['file'];
                    break;
                }
            }
        }

        if ( empty( $file['path'] ) ) {
            return new \WP_REST_Response( array( 'message' => 'No file selected.' ), 404 );
        }

        $xpath = ! empty( $draft['step_data'][4]['xpath'] )
            ? $draft['step_data'][4]['xpath']
            : '';

        try {
            // Check if we need to force refresh (bypass cache)
            $force_refresh = ! empty( $request['force_refresh'] );
            
            // Clear cache if force refresh is requested
            if ( $force_refresh ) {
                $cache_key = sprintf(
                    'preview_%s_%s_%s',
                    md5( $file['path'] ?? '' ),
                    md5( serialize( array( 'index' => $index, 'xpath' => $xpath, 'filters' => is_array( $filters ) ? $filters : ( $draft['step_data'][4]['filters'] ?? array() ) ) ) ),
                    filemtime( $file['path'] ) ?? 0
                );
                \PrimeLink\Helpers\CacheHelper::delete( $cache_key );
            }
            
            return ImportPreview::get_mapping_preview(
                $file,
                array(
                    'index'   => $index,
                    'xpath'   => $xpath,
                    'filters' => is_array( $filters ) ? $filters : ( $draft['step_data'][4]['filters'] ?? array() ),
                )
            );
        } catch ( \Throwable $e ) {
            return new \WP_REST_Response(
                array(
                    'message' => 'Mapping preview failed: ' . $e->getMessage(),
                ),
                500
            );
        }
    }

    /**
     * Filters helper for preview endpoints.
     */
    private static function normalize_filters( $filters ) {
        $out = array();
        foreach ( (array) $filters as $f ) {
            $field = isset( $f['field'] ) ? sanitize_text_field( $f['field'] ) : '';
            $op    = isset( $f['op'] ) ? sanitize_text_field( $f['op'] ) : '=';
            $val   = $f['value'] ?? '';
            if ( is_array( $val ) ) {
                $val = implode( ',', $val );
            }
            $val = (string) $val;
            if ( $field === '' && $val === '' ) {
                continue;
            }
            $out[] = array(
                'field' => $field,
                'op'    => $op,
                'value' => $val,
            );
        }
        return $out;
    }

    private static function apply_filters_to_rows( $headers, $rows, $filters ) {
        if ( empty( $filters ) ) {
            return $rows;
        }

        $headers = array_values( (array) $headers );
        $filtered = array();

        foreach ( (array) $rows as $row ) {
            $assoc = array();
            foreach ( $headers as $idx => $h ) {
                $assoc[ $h ] = isset( $row[ $idx ] ) ? (string) $row[ $idx ] : '';
            }
            if ( self::row_matches_filters( $assoc, $filters ) ) {
                $filtered[] = $row;
            }
        }

        return $filtered;
    }

    private static function row_matches_filters( $assoc, $filters ) {
        foreach ( (array) $filters as $f ) {
            $field = $f['field'] ?? '';
            $op    = $f['op'] ?? '=';
            $value = isset( $f['value'] ) ? (string) $f['value'] : '';

            $left = isset( $assoc[ $field ] ) ? trim( (string) $assoc[ $field ] ) : '';
            $list = self::split_values( $value );

            $match = true;
            switch ( $op ) {
                case '!=':
                    $match = ( $left !== $value );
                    break;
                case 'contains':
                    $match = ( stripos( $left, $value ) !== false );
                    break;
                case 'not_contains':
                    $match = ( stripos( $left, $value ) === false );
                    break;
                case 'in':
                    $match = empty( $list ) ? false : in_array( strtolower( $left ), array_map( 'strtolower', $list ), true );
                    break;
                case 'not_in':
                    $match = empty( $list ) ? true : ! in_array( strtolower( $left ), array_map( 'strtolower', $list ), true );
                    break;
                case 'regex':
                case 'not_regex':
                    $pattern = '/' . str_replace( '/', '\/', $value ) . '/i';
                    $ok = @preg_match( $pattern, $left );
                    $match = $op === 'regex' ? ( $ok === 1 ) : ( $ok === 0 || $ok === false );
                    break;
                case '>':
                    $match = floatval( $left ) > floatval( $value );
                    break;
                case '<':
                    $match = floatval( $left ) < floatval( $value );
                    break;
                case '>=':
                    $match = floatval( $left ) >= floatval( $value );
                    break;
                case '<=':
                    $match = floatval( $left ) <= floatval( $value );
                    break;
                case '=':
                default:
                    $match = ( $left === $value );
                    break;
            }

            if ( ! $match ) {
                return false;
            }
        }
        return true;
    }

    private static function split_values( $value ) {
        $parts = preg_split( '/\s*[,|;\n\r]+\s*/', (string) $value );
        $parts = array_values( array_filter( array_map( 'trim', $parts ), function( $v ) { return $v !== ''; } ) );
        return $parts;
    }

    /** ---------------- Templates storage ---------------- */

private static function get_templates_option_key() {
    return 'mt_primelink_import_templates';
}

public static function list_templates( $request ) {
    $templates = get_option( self::get_templates_option_key(), array() );
    if ( ! is_array( $templates ) ) {
        $templates = array();
    }

    // Get type parameter from GET request (query string)
    $type = $request->get_param( 'type' );
    if ( empty( $type ) && isset( $request['type'] ) ) {
        $type = $request['type'];
    }
    $type = sanitize_text_field( $type );
    
    $names = array_keys( $templates );
    
    // Filter by type if provided - strict filtering: only show templates with the correct prefix
    if ( $type === 'order' ) {
        // Show only templates that start with "order:"
        $names = array_filter( $names, function( $name ) {
            return strpos( $name, 'order:' ) === 0;
        } );
    } elseif ( $type === 'product' ) {
        // Show only templates that start with "product:" or have no prefix (for backward compatibility with old templates)
        $names = array_filter( $names, function( $name ) {
            return strpos( $name, 'product:' ) === 0 || ( strpos( $name, 'order:' ) !== 0 && strpos( $name, 'customer:' ) !== 0 && strpos( $name, 'coupon:' ) !== 0 && strpos( $name, 'category:' ) !== 0 && strpos( $name, 'tag:' ) !== 0 && strpos( $name, 'attribute:' ) !== 0 && strpos( $name, 'variation:' ) !== 0 && strpos( $name, 'review:' ) !== 0 && strpos( $name, 'download:' ) !== 0 );
        } );
    } elseif ( $type === 'customer' ) {
        // Show only templates that start with "customer:"
        $names = array_filter( $names, function( $name ) {
            return strpos( $name, 'customer:' ) === 0;
        } );
    } elseif ( $type === 'coupon' ) {
        // Show only templates that start with "coupon:"
        $names = array_filter( $names, function( $name ) {
            return strpos( $name, 'coupon:' ) === 0;
        } );
    } elseif ( $type === 'category' || $type === 'categories' ) {
        // Show only templates that start with "category:"
        $names = array_filter( $names, function( $name ) {
            return strpos( $name, 'category:' ) === 0;
        } );
    } elseif ( $type === 'tag' || $type === 'tags' ) {
        // Show only templates that start with "tag:"
        $names = array_filter( $names, function( $name ) {
            return strpos( $name, 'tag:' ) === 0;
        } );
    } elseif ( $type === 'attribute' || $type === 'attributes' ) {
        // Show only templates that start with "attribute:"
        $names = array_filter( $names, function( $name ) {
            return strpos( $name, 'attribute:' ) === 0;
        } );
    } elseif ( $type === 'variation' || $type === 'variations' ) {
        // Show only templates that start with "variation:"
        $names = array_filter( $names, function( $name ) {
            return strpos( $name, 'variation:' ) === 0;
        } );
    } elseif ( $type === 'review' || $type === 'reviews' ) {
        // Show only templates that start with "review:"
        $names = array_filter( $names, function( $name ) {
            return strpos( $name, 'review:' ) === 0;
        } );
    } elseif ( $type === 'download' || $type === 'downloads' ) {
        // Show only templates that start with "download:"
        $names = array_filter( $names, function( $name ) {
            return strpos( $name, 'download:' ) === 0;
        } );
    }
    // If no type specified, return all (for backward compatibility)
    
    $names = array_values( $names );
    sort( $names );

    return array(
        'templates' => $names,
    );
}

public static function save_template( $request ) {
    $name = sanitize_text_field( $request['name'] );
    $data = (array) $request['data'];
    $type = isset( $request['type'] ) ? sanitize_text_field( $request['type'] ) : '';

    if ( empty( $name ) ) {
        return new \WP_REST_Response( array( 'message' => 'Template name is required.' ), 400 );
    }

    // Add prefix based on type
    $prefix = '';
    if ( $type === 'order' ) {
        $prefix = 'order:';
    } elseif ( $type === 'product' ) {
        $prefix = 'product:';
    } elseif ( $type === 'customer' ) {
        $prefix = 'customer:';
    } elseif ( $type === 'coupon' ) {
        $prefix = 'coupon:';
    } elseif ( $type === 'category' || $type === 'categories' ) {
        $prefix = 'category:';
    } elseif ( $type === 'tag' || $type === 'tags' ) {
        $prefix = 'tag:';
    } elseif ( $type === 'attribute' || $type === 'attributes' ) {
        $prefix = 'attribute:';
    } elseif ( $type === 'variation' || $type === 'variations' ) {
        $prefix = 'variation:';
    } elseif ( $type === 'review' || $type === 'reviews' ) {
        $prefix = 'review:';
    } elseif ( $type === 'download' || $type === 'downloads' ) {
        $prefix = 'download:';
    }

    // If type is still empty (couldn't be detected on JS side), default templates to "product"
    if ( $type === '' ) {
        $type   = 'product';
        $prefix = 'product:';
    }

    // Only add prefix if name doesn't already have one
    if ( $prefix && strpos( $name, $prefix ) !== 0 ) {
        // Check if it has a different prefix and remove it
        $name = preg_replace( '/^(order|product|customer|coupon|category|tag|attribute|variation|review|download):/', '', $name );
        $name = $prefix . $name;
    }

    $templates = get_option( self::get_templates_option_key(), array() );
    if ( ! is_array( $templates ) ) {
        $templates = array();
    }

    $templates[ $name ] = array(
        'updated_at' => current_time( 'mysql' ),
        'data'       => $data,
        'type'       => $type, // Store type for future reference
    );

    update_option( self::get_templates_option_key(), $templates, false );

    return array(
        'saved' => true,
        'name'  => $name,
    );
}

private static function get_filter_presets_option_key() {
    return 'mt_primelink_filter_presets';
}

public static function list_filter_presets() {
    $presets = get_option( self::get_filter_presets_option_key(), array() );
    if ( ! is_array( $presets ) ) {
        $presets = array();
    }
    $names = array_keys( $presets );
    sort( $names );
    return array( 'presets' => $names );
}

public static function save_filter_preset( $request ) {
    $name    = sanitize_text_field( $request['name'] ?? '' );
    $filters = isset( $request['filters'] ) ? (array) $request['filters'] : array();

    if ( $name === '' ) {
        return new \WP_REST_Response( array( 'message' => 'Preset name is required.' ), 400 );
    }

    $filters = self::normalize_filters( $filters );

    $presets = get_option( self::get_filter_presets_option_key(), array() );
    if ( ! is_array( $presets ) ) {
        $presets = array();
    }

    $presets[ $name ] = array(
        'updated_at' => current_time( 'mysql' ),
        'filters'    => $filters,
    );

    update_option( self::get_filter_presets_option_key(), $presets, false );

    return array( 'saved' => true, 'name' => $name );
}

public static function load_filter_preset( $request ) {
    $name = sanitize_text_field( $request['name'] ?? '' );
    if ( $name === '' ) {
        return new \WP_REST_Response( array( 'message' => 'Preset name is required.' ), 400 );
    }

    $presets = get_option( self::get_filter_presets_option_key(), array() );
    if ( empty( $presets[ $name ] ) ) {
        return new \WP_REST_Response( array( 'message' => 'Preset not found.' ), 404 );
    }

    return array(
        'name'    => $name,
        'filters' => $presets[ $name ]['filters'] ?? array(),
    );
}

public static function delete_filter_preset( $request ) {
    $name = sanitize_text_field( $request['name'] ?? '' );
    if ( $name === '' ) {
        return new \WP_REST_Response( array( 'message' => 'Preset name is required.' ), 400 );
    }

    $presets = get_option( self::get_filter_presets_option_key(), array() );
    if ( isset( $presets[ $name ] ) ) {
        unset( $presets[ $name ] );
        update_option( self::get_filter_presets_option_key(), $presets, false );
    }

    return array( 'deleted' => true, 'name' => $name );
}

public static function load_template( $request ) {
    $name = sanitize_text_field( $request['name'] ?? '' );
    $import_id = absint( $request['import_id'] ?? 0 );

    // If import_id is provided, load from import draft instead of template
    if ( $import_id > 0 ) {
        $drafts = get_option( 'mt_primelink_import_drafts', array() );
        if ( empty( $drafts[ $import_id ] ) ) {
            return new \WP_REST_Response( array( 'message' => 'Import not found.' ), 404 );
        }

        $draft = $drafts[ $import_id ];
        
        // Extract mapping data from step 5
        $mapping_data = $draft['step_data'][5] ?? array();
        
        return array(
            'import_id' => $import_id,
            'name'      => sprintf( 'Import #%d', $import_id ),
            'data'      => $mapping_data,
            'is_import' => true,
        );
    }

    // Otherwise, load from template
    if ( empty( $name ) ) {
        return new \WP_REST_Response( array( 
            'message' => 'Template name is required.',
            'debug'   => array(
                'request_params' => $request->get_params(),
                'request_body'   => $request->get_body_params(),
                'raw_body'       => $request->get_body(),
            ),
        ), 400 );
    }
    
    $templates = get_option( self::get_templates_option_key(), array() );
    if ( ! is_array( $templates ) ) {
        $templates = array();
    }
    
    if ( ! isset( $templates[ $name ] ) || empty( $templates[ $name ]['data'] ) ) {
        return new \WP_REST_Response( array( 
            'message' => 'Template not found.',
            'debug'   => array(
                'requested_name' => $name,
                'available_templates' => array_keys( $templates ),
            ),
        ), 404 );
    }

    $data = $templates[ $name ]['data'];

    return array(
        'name'      => $name,
        'data'      => $data,
        'is_import' => false,
    );
}

public static function delete_template( $request ) {
    $name = sanitize_text_field( $request['name'] );

    $templates = get_option( self::get_templates_option_key(), array() );
    if ( ! is_array( $templates ) ) {
        $templates = array();
    }

    if ( isset( $templates[ $name ] ) ) {
        unset( $templates[ $name ] );
        update_option( self::get_templates_option_key(), $templates, false );
    }

    return array(
        'deleted' => true,
        'name'    => $name,
    );
}

public static function get_draft( $request ) {
    $import_id = absint( $request['import_id'] );

    $drafts = get_option( 'mt_primelink_import_drafts', array() );
    if ( empty( $drafts[ $import_id ] ) ) {
        return new \WP_REST_Response( array( 'message' => 'Draft not found.' ), 404 );
    }

    return array(
        'import_id' => $import_id,
        'draft'     => $drafts[ $import_id ],
    );
}

/**
 * List all imports for template/import selection dropdown.
 * 
 * @param \WP_REST_Request $request Request object.
 * @return array List of imports with ID, type, and status.
 */
public static function list_imports( $request ) {
    $type_filter = sanitize_text_field( $request['type'] ?? '' );
    
    $drafts = get_option( 'mt_primelink_import_drafts', array() );
    if ( ! is_array( $drafts ) ) {
        $drafts = array();
    }

    $imports = array();
    foreach ( $drafts as $import_id => $draft ) {
        $import_type = $draft['type'] ?? ( $draft['step_data'][1]['type'] ?? 'product' );
        
        // Normalize type
        if ( $import_type === 'products' ) {
            $import_type = 'product';
        } elseif ( $import_type === 'orders' ) {
            $import_type = 'order';
        } elseif ( $import_type === 'customers' ) {
            $import_type = 'customer';
        }
        
        // Filter by type if specified
        if ( ! empty( $type_filter ) && $import_type !== $type_filter ) {
            continue;
        }
        
        // Get status
        $state = get_option( 'mt_primelink_import_state_' . absint( $import_id ), array() );
        $status = $state['status'] ?? 'idle';
        
        // Get step
        $current_step = $draft['current_step'] ?? 1;
        
        // Get created date
        $created = $draft['created_at'] ?? '';
        
        $imports[] = array(
            'id'           => absint( $import_id ),
            'type'         => $import_type,
            'status'       => $status,
            'step'         => $current_step,
            'created'      => $created,
            'display_name' => sprintf( 
                'Import #%d (%s) - %s', 
                absint( $import_id ),
                ucfirst( $import_type ),
                ucfirst( $status )
            ),
        );
    }
    
    // Sort by created date (newest first)
    usort( $imports, function( $a, $b ) {
        $at = strtotime( $a['created'] ?? '' );
        $bt = strtotime( $b['created'] ?? '' );
        return $bt <=> $at;
    } );

    return array(
        'imports' => $imports,
    );
}

/**
 * Reload (duplicate) an import with the same settings.
 * Creates a new import draft with all the same step data.
 * 
 * @param \WP_REST_Request $request Request object.
 * @return array New import ID.
 */
public static function reload_import( $request ) {
    $source_import_id = absint( $request['import_id'] ?? 0 );
    
    if ( $source_import_id < 1 ) {
        return new \WP_REST_Response( array( 'message' => 'Invalid import ID.' ), 400 );
    }
    
    $drafts = get_option( 'mt_primelink_import_drafts', array() );
    if ( empty( $drafts[ $source_import_id ] ) ) {
        return new \WP_REST_Response( array( 'message' => 'Source import not found.' ), 404 );
    }
    
    $source_draft = $drafts[ $source_import_id ];
    
    // Create new draft with same data
    $new_id = time();
    $new_draft = array(
        'created_at'    => current_time( 'mysql' ),
        'step_data'     => $source_draft['step_data'] ?? array(),
        'current_step'  => $source_draft['current_step'] ?? 1,
        'type'          => $source_draft['type'] ?? '',
    );
    
    $drafts[ $new_id ] = $new_draft;
    update_option( 'mt_primelink_import_drafts', $drafts, false );
    
    return array(
        'import_id' => $new_id,
        'message'   => sprintf( 'Import #%d reloaded as Import #%d', $source_import_id, $new_id ),
    );
}

    public static function notify_test( $request ) {
        $email = sanitize_email( $request['email'] ?? '' );
        if ( ! $email || ! is_email( $email ) ) {
            return new \WP_REST_Response( array( 'message' => 'Invalid email.' ), 400 );
        }
        $sent = wp_mail( $email, 'PrimeLink test notification', 'This is a test email from PrimeLink notifications.' );
        if ( ! $sent ) {
            return new \WP_REST_Response( array( 'message' => 'Failed to send email.' ), 500 );
        }
        return array( 'message' => 'Test email sent to ' . $email );
    }

    /**
     * Test and activate license from Settings page.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response|array
     */
    public static function test_license( $request ) {
        // Robust param extraction (works for form-data and JSON)
        $purchase_code = '';
        $client_name   = '';

        $purchase_code_param = $request->get_param( 'purchase_code' );
        $client_name_param   = $request->get_param( 'client_name' );

        if ( ! is_null( $purchase_code_param ) ) {
            $purchase_code = sanitize_text_field( $purchase_code_param );
        }
        if ( ! is_null( $client_name_param ) ) {
            $client_name = sanitize_text_field( $client_name_param );
        }

        // Fallback: body params
        if ( empty( $purchase_code ) || empty( $client_name ) ) {
            $body_params = $request->get_body_params();
            if ( empty( $purchase_code ) && isset( $body_params['purchase_code'] ) ) {
                $purchase_code = sanitize_text_field( $body_params['purchase_code'] );
            }
            if ( empty( $client_name ) && isset( $body_params['client_name'] ) ) {
                $client_name = sanitize_text_field( $body_params['client_name'] );
            }
        }

        // Fallback: array access
        if ( empty( $purchase_code ) && isset( $request['purchase_code'] ) ) {
            $purchase_code = sanitize_text_field( $request['purchase_code'] );
        }
        if ( empty( $client_name ) && isset( $request['client_name'] ) ) {
            $client_name = sanitize_text_field( $request['client_name'] );
        }

        $purchase_code = trim( $purchase_code );
        $client_name   = trim( $client_name );

        // Minimal validation: both fields must be non-empty; rest is validated server-side (Envato)
        if ( $purchase_code === '' || $client_name === '' ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => __( 'Please enter both Purchase Code and Client Name.', 'primelink' ),
            ), 400 );
        }

        // Activate license via Envato API
        $activation = \PrimeLink\Helpers\EnvatoHelper::activate_license( $purchase_code, $client_name, true );

        if ( ! isset( $activation['status'] ) || $activation['status'] !== true ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => isset( $activation['message'] ) ? $activation['message'] : __( 'License activation failed.', 'primelink' ),
            ), 400 );
        }

        // Save license key in settings
        $settings = self::get_settings();
        $settings['license_key'] = $purchase_code;
        $settings['license_client_name'] = $client_name;
        $settings['license_status'] = 'active';
        update_option( self::get_settings_option_key(), $settings, false );

        return array(
            'success' => true,
            'message' => isset( $activation['message'] ) ? $activation['message'] : __( 'License activated successfully!', 'primelink' ),
            'data'    => array(
                'license_code' => $purchase_code,
                'client_name'  => $client_name,
            ),
        );
    }

    /**
     * Deactivate license from Settings page.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response|array
     */
    public static function deactivate_license( $request ) {
        $activation = get_option( 'mt_primelink_license_activation', array() );
        
        if ( empty( $activation ) ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => __( 'No active license found.', 'primelink' ),
            ), 400 );
        }

        $license_code = $activation['license_code'] ?? '';
        $client_name  = $activation['client_name'] ?? '';

        // Deactivate license via Envato API
        $deactivation = \PrimeLink\Helpers\EnvatoHelper::deactivate_license( $license_code, $client_name );

        if ( ! isset( $deactivation['status'] ) || $deactivation['status'] !== true ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => isset( $deactivation['message'] ) ? $deactivation['message'] : __( 'License deactivation failed.', 'primelink' ),
            ), 400 );
        }

        // Clear license from settings
        $settings = self::get_settings();
        $settings['license_key'] = '';
        $settings['license_client_name'] = '';
        $settings['license_status'] = 'inactive';
        update_option( self::get_settings_option_key(), $settings, false );

        return array(
            'success' => true,
            'message' => isset( $deactivation['message'] ) ? $deactivation['message'] : __( 'License deactivated successfully.', 'primelink' ),
        );
    }

    /**
     * Check for updates (for debugging/testing).
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response|array
     */
    public static function check_updates( $request ) {
        // Clear cache to force fresh check
        delete_transient( 'mt_primelink_next_update_check' );
        
        // Clear WordPress update cache to force refresh
        delete_site_transient( 'update_plugins' );
        
        $license_status = \PrimeLink\Helpers\EnvatoHelper::get_license_status();
        
        if ( ! $license_status['activated'] ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => __( 'License is not activated. Please activate your license first.', 'primelink' ),
                'license_status' => $license_status,
            ), 400 );
        }

        $current_version = \PrimeLink\Helpers\EnvatoHelper::get_current_version();
        
        // Force fresh check (clears all caches)
        $update_data = \PrimeLink\Core\Updater::check_for_updates();

        return array(
            'success' => true,
            'current_version' => $current_version,
            'update_data' => $update_data,
            'license_status' => $license_status,
            'message' => __( 'For updates, please check CodeCanyon or download manually.', 'primelink' ),
        );
    }

    /** ---------------- Activation Wizard ---------------- */

    /**
     * Verify license in activation wizard.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response|array
     */
    public static function wizard_verify_license( $request ) {
        // Try multiple methods to get parameters (works for both JSON and form data)
        $purchase_code = '';
        $client_name = '';
        
        // Try get_param() first (works for JSON and registered args)
        $purchase_code_param = $request->get_param( 'purchase_code' );
        $client_name_param = $request->get_param( 'client_name' );
        
        if ( ! is_null( $purchase_code_param ) ) {
            $purchase_code = sanitize_text_field( $purchase_code_param );
        }
        if ( ! is_null( $client_name_param ) ) {
            $client_name = sanitize_text_field( $client_name_param );
        }
        
        // If not found, try body_params
        if ( empty( $purchase_code ) || empty( $client_name ) ) {
            $body_params = $request->get_body_params();
            if ( empty( $purchase_code ) && isset( $body_params['purchase_code'] ) ) {
                $purchase_code = sanitize_text_field( $body_params['purchase_code'] );
            }
            if ( empty( $client_name ) && isset( $body_params['client_name'] ) ) {
                $client_name = sanitize_text_field( $body_params['client_name'] );
            }
        }
        
        // If still not found, try array access (fallback)
        if ( empty( $purchase_code ) && isset( $request['purchase_code'] ) ) {
            $purchase_code = sanitize_text_field( $request['purchase_code'] );
        }
        if ( empty( $client_name ) && isset( $request['client_name'] ) ) {
            $client_name = sanitize_text_field( $request['client_name'] );
        }
        
        // Trim values
        $purchase_code = trim( $purchase_code );
        $client_name = trim( $client_name );

        if ( empty( $purchase_code ) ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => __( 'Purchase code is required.', 'primelink' ),
            ), 400 );
        }

        if ( empty( $client_name ) ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => __( 'Client name (Envato username) is required.', 'primelink' ),
            ), 400 );
        }

        // Activate license via Envato API
        $activation = \PrimeLink\Helpers\EnvatoHelper::activate_license( $purchase_code, $client_name, true );

        if ( ! isset( $activation['status'] ) || $activation['status'] !== true ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => isset( $activation['message'] ) ? $activation['message'] : __( 'License activation failed.', 'primelink' ),
            ), 400 );
        }

        // Save license key in settings
        $settings = self::get_settings();
        $settings['license_key'] = $purchase_code;
        $settings['license_client_name'] = $client_name;
        $settings['license_status'] = 'active';
        update_option( self::get_settings_option_key(), $settings, false );

        return array(
            'success' => true,
            'message' => isset( $activation['message'] ) ? $activation['message'] : __( 'License activated successfully!', 'primelink' ),
            'data'    => array(
                'license_code' => $purchase_code,
                'client_name'  => $client_name,
            ),
        );
    }

    /**
     * Save setup data from activation wizard.
     *
     * @param WP_REST_Request $request Request object.
     * @return array
     */
    public static function wizard_save_setup( $request ) {
        $notify_email = sanitize_email( $request['notify_email'] ?? '' );
        $iteration_size = isset( $request['iteration_size'] ) ? max( 1, min( 500, absint( $request['iteration_size'] ) ) ) : 20;
        $enable_logs = ! empty( $request['enable_logs'] ) ? 1 : 0;

        $settings = self::get_settings();
        
        if ( ! empty( $notify_email ) ) {
            $settings['notify_email'] = $notify_email;
        }
        
        $settings['default_iteration_size'] = $iteration_size;
        $settings['enable_logs'] = $enable_logs;

        update_option( self::get_settings_option_key(), $settings, false );

        return array(
            'success' => true,
            'message' => __( 'Settings saved successfully.', 'primelink' ),
        );
    }

    /**
     * Test system requirements in activation wizard.
     *
     * @param WP_REST_Request $request Request object.
     * @return array
     */
    public static function wizard_test_system( $request ) {
        $tests = array();
        $all_passed = true;

        // Check PHP version
        $php_version = PHP_VERSION;
        $php_required = '7.4';
        $php_ok = version_compare( $php_version, $php_required, '>=' );
        $tests[] = array(
            'name'    => __( 'PHP Version', 'primelink' ),
            'status'  => $php_ok ? 'pass' : 'fail',
            'message' => $php_ok 
                ? sprintf( __( 'PHP %s (Required: %s+)', 'primelink' ), $php_version, $php_required )
                : sprintf( __( 'PHP %s (Required: %s+)', 'primelink' ), $php_version, $php_required ),
        );
        if ( ! $php_ok ) {
            $all_passed = false;
        }

        // Check WordPress version
        global $wp_version;
        $wp_required = '5.8';
        $wp_ok = version_compare( $wp_version, $wp_required, '>=' );
        $tests[] = array(
            'name'    => __( 'WooCommerce Version', 'primelink' ),
            'status'  => $wp_ok ? 'pass' : 'fail',
            'message' => $wp_ok 
                ? sprintf( __( 'WooCommerce %s (Required: %s+)', 'primelink' ), $wp_version, $wp_required )
                : sprintf( __( 'WooCommerce %s (Required: %s+)', 'primelink' ), $wp_version, $wp_required ),
        );
        if ( ! $wp_ok ) {
            $all_passed = false;
        }

        // Check memory limit
        $memory_limit = ini_get( 'memory_limit' );
        $memory_bytes = wp_convert_hr_to_bytes( $memory_limit );
        $memory_required = 128 * 1024 * 1024; // 128MB
        $memory_ok = $memory_bytes >= $memory_required;
        $tests[] = array(
            'name'    => __( 'Memory Limit', 'primelink' ),
            'status'  => $memory_ok ? 'pass' : 'warning',
            'message' => sprintf( __( '%s (Recommended: 128MB+)', 'primelink' ), $memory_limit ),
        );

        // Check upload directory permissions
        $upload_dir = wp_upload_dir();
        $upload_ok = wp_is_writable( $upload_dir['basedir'] );
        $tests[] = array(
            'name'    => __( 'Upload Directory', 'primelink' ),
            'status'  => $upload_ok ? 'pass' : 'fail',
            'message' => $upload_ok 
                ? __( 'Writable', 'primelink' )
                : __( 'Not writable', 'primelink' ),
        );
        if ( ! $upload_ok ) {
            $all_passed = false;
        }

        // Check required PHP extensions
        $extensions = array( 'curl', 'json', 'mbstring', 'zip' );
        foreach ( $extensions as $ext ) {
            $ext_ok = extension_loaded( $ext );
            $tests[] = array(
                'name'    => sprintf( __( 'PHP Extension: %s', 'primelink' ), $ext ),
                'status'  => $ext_ok ? 'pass' : 'fail',
                'message' => $ext_ok ? __( 'Loaded', 'primelink' ) : __( 'Not loaded', 'primelink' ),
            );
            if ( ! $ext_ok ) {
                $all_passed = false;
            }
        }

        return array(
            'success'    => $all_passed,
            'tests'      => $tests,
            'all_passed' => $all_passed,
        );
    }

    /**
     * Complete activation wizard.
     *
     * @param WP_REST_Request $request Request object.
     * @return array
     */
    public static function wizard_complete( $request ) {
        \PrimeLink\Core\Admin\ActivationWizard::complete_wizard();

        return array(
            'success' => true,
            'message' => __( 'Wizard completed successfully!', 'primelink' ),
        );
    }

    /**
     * Restart activation wizard.
     *
     * @param WP_REST_Request $request Request object.
     * @return array
     */
    public static function wizard_restart( $request ) {
        // Reset wizard flags - clear skip flag and show wizard
        update_option( 'mt_primelink_show_activation_wizard', true, false );
        delete_option( 'mt_primelink_wizard_completed' );
        delete_option( 'mt_primelink_wizard_skipped' );

        return array(
            'success' => true,
            'message' => __( 'Wizard restarted. Please refresh the page.', 'primelink' ),
        );
    }

    /**
     * Skip activation wizard (user chose to skip for now).
     *
     * @param WP_REST_Request $request Request object.
     * @return array
     */
    public static function wizard_skip( $request ) {
        // Set skip flag - wizard won't show again until restart
        update_option( 'mt_primelink_wizard_skipped', true, false );
        delete_option( 'mt_primelink_show_activation_wizard' );

        return array(
            'success' => true,
            'message' => __( 'Wizard skipped. You can restart it anytime from the dashboard.', 'primelink' ),
        );
    }

    /**
     * Get system status (detailed).
     * Developer API endpoint.
     *
     * @param \WP_REST_Request $request Request object.
     * @return array
     */
    public static function system_status( $request ) {
        global $wp_version, $wpdb;

        $status = array(
            'plugin' => array(
                'name'    => 'PrimeLink',
                'version' => MT_PRIMELINK_VERSION,
                'active'  => true,
            ),
            'wordpress' => array(
                'version' => $wp_version,
                'multisite' => is_multisite(),
            ),
            'php' => array(
                'version'      => PHP_VERSION,
                'memory_limit' => ini_get( 'memory_limit' ),
                'max_execution_time' => ini_get( 'max_execution_time' ),
                'upload_max_filesize' => ini_get( 'upload_max_filesize' ),
                'post_max_size' => ini_get( 'post_max_size' ),
            ),
            'database' => array(
                'version' => $wpdb->db_version(),
                'charset' => $wpdb->charset,
                'collate' => $wpdb->collate,
            ),
            'server' => array(
                'software' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'Unknown',
            ),
            'extensions' => array(
                'curl'     => extension_loaded( 'curl' ),
                'json'     => extension_loaded( 'json' ),
                'mbstring' => extension_loaded( 'mbstring' ),
                'zip'      => extension_loaded( 'zip' ),
                'xml'      => extension_loaded( 'xml' ),
                'libxml'   => extension_loaded( 'libxml' ),
                'simplexml' => function_exists( 'simplexml_load_file' ),
                'dom'      => class_exists( 'DOMDocument' ),
            ),
            'woocommerce' => array(
                'active'  => class_exists( 'WooCommerce' ),
                'version' => class_exists( 'WooCommerce' ) ? WC()->version : null,
            ),
            'uploads' => array(
                'writable' => wp_is_writable( wp_upload_dir()['basedir'] ),
                'path'     => wp_upload_dir()['basedir'],
            ),
            'settings' => array(
                'cron_mode' => get_option( 'mt_primelink_settings', array() )['cron_mode'] ?? 'wp',
                'rate_limit_max' => get_option( 'mt_primelink_settings', array() )['rate_limit_max'] ?? 500,
            ),
            'statistics' => array(
                'total_imports' => count( get_option( 'mt_primelink_import_drafts', array() ) ),
                'total_exports' => count( get_option( 'mt_primelink_export_drafts', array() ) ),
                'active_imports' => self::count_active_imports(),
            ),
        );

        return $status;
    }

    /**
     * Health check endpoint (public).
     * Returns basic health status without sensitive information.
     *
     * @param \WP_REST_Request $request Request object.
     * @return array
     */
    public static function health_check( $request ) {
        $health = array(
            'status' => 'healthy',
            'timestamp' => current_time( 'mysql' ),
            'plugin' => array(
                'active' => true,
                'version' => MT_PRIMELINK_VERSION,
            ),
            'checks' => array(
                'php_version' => version_compare( PHP_VERSION, '7.4', '>=' ),
                'wordpress' => true,
                'uploads_writable' => wp_is_writable( wp_upload_dir()['basedir'] ),
                'woocommerce' => class_exists( 'WooCommerce' ),
            ),
        );

        // Determine overall health
        $all_checks_pass = true;
        foreach ( $health['checks'] as $check ) {
            if ( ! $check ) {
                $all_checks_pass = false;
                break;
            }
        }

        if ( ! $all_checks_pass ) {
            $health['status'] = 'degraded';
        }

        return $health;
    }

    /**
     * Get system information (developer-friendly).
     *
     * @param \WP_REST_Request $request Request object.
     * @return array
     */
    public static function system_info( $request ) {
        return array(
            'api' => array(
                'version' => '1.0',
                'namespace' => 'primelink/v1',
                'base_url' => rest_url( 'primelink/v1/' ),
            ),
            'plugin' => array(
                'name' => 'PrimeLink',
                'version' => MT_PRIMELINK_VERSION,
                'path' => MT_PRIMELINK_PATH,
                'url' => MT_PRIMELINK_URL,
            ),
            'capabilities' => array(
                'required' => apply_filters( 'mt_primelink_capability', class_exists( 'WooCommerce' ) ? 'manage_woocommerce' : 'manage_options' ),
                'current_user_can' => current_user_can( apply_filters( 'mt_primelink_capability', class_exists( 'WooCommerce' ) ? 'manage_woocommerce' : 'manage_options' ) ),
            ),
            'filters' => array(
                'mt_primelink_capability' => 'Filter to customize required capability',
                'mt_primelink_allow_webhook_without_token' => 'Filter to allow webhooks without token',
            ),
            'endpoints' => array(
                'import' => array(
                    'count' => 30,
                    'base' => '/import',
                ),
                'export' => array(
                    'count' => 12,
                    'base' => '/export',
                ),
                'live_feed' => array(
                    'count' => 8,
                    'base' => '/live-feed',
                ),
            ),
        );
    }

    /**
     * Count active (running) imports.
     *
     * @return int
     */
    private static function count_active_imports() {
        $drafts = get_option( 'mt_primelink_import_drafts', array() );
        $active = 0;

        foreach ( $drafts as $import_id => $draft ) {
            $state = get_option( 'mt_primelink_import_state_' . absint( $import_id ), array() );
            if ( isset( $state['status'] ) && $state['status'] === 'running' ) {
                $active++;
            }
        }

        return $active;
    }

    /** ---------------- Google Drive OAuth ---------------- */

    public static function gdrive_auth_url( $request ) {
        $settings = self::get_settings();
        $client_id = $settings['gdrive_client_id'] ?? '';
        $client_secret = $settings['gdrive_client_secret'] ?? '';
        
        if ( ! $client_id || ! $client_secret ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'message' => __( 'Client ID and Client Secret are required for OAuth authentication.', 'primelink' ),
            ), 400 );
        }

        // Generate OAuth URL
        // Use admin page callback instead of REST endpoint (more reliable, like integrate-google-drive)
        $redirect_uri = admin_url( 'admin.php?mt_primelink_gdrive_auth=callback' );
        // Use 'drive' scope (not 'drive.readonly') to allow both read and write (upload) operations
        $scope = 'https://www.googleapis.com/auth/drive';
        $state = wp_create_nonce( 'gdrive_oauth_' . get_current_user_id() );
        
        // Store state in transient for verification
        set_transient( 'gdrive_oauth_state_' . get_current_user_id(), $state, 600 );
        
        // Also store redirect URI for verification
        set_transient( 'gdrive_oauth_redirect_' . get_current_user_id(), $redirect_uri, 600 );
        
        $auth_url = add_query_arg( array(
            'client_id'     => $client_id,
            'redirect_uri'  => $redirect_uri,
            'response_type' => 'code',
            'scope'         => $scope,
            'access_type'  => 'offline',
            'prompt'        => 'consent',
            'state'         => $state,
        ), 'https://accounts.google.com/o/oauth2/v2/auth' );

        return array(
            'success' => true,
            'auth_url' => $auth_url,
            'redirect_uri' => $redirect_uri, // Return for debugging
            'instructions' => sprintf(
                __( 'Make sure this Redirect URI is added in Google Cloud Console: %s', 'primelink' ),
                $redirect_uri
            ),
        );
    }

    /**
     * Handle OAuth callback via admin_init hook (more reliable than REST endpoint).
     * Called from Admin::handle_gdrive_oauth_callback()
     */
    public static function gdrive_oauth_callback_admin() {
        $code = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : '';
        $state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : '';
        $error = isset( $_GET['error'] ) ? sanitize_text_field( wp_unslash( $_GET['error'] ) ) : '';
        $error_description = isset( $_GET['error_description'] ) ? sanitize_text_field( wp_unslash( $_GET['error_description'] ) ) : '';
        
        self::gdrive_oauth_callback_process( $code, $state, $error, $error_description );
    }
    
    /**
     * Handle OAuth callback via REST API endpoint (fallback).
     */
    public static function gdrive_oauth_callback( $request ) {
        $code = $request->get_param( 'code' );
        $state = $request->get_param( 'state' );
        $error = $request->get_param( 'error' );
        $error_description = $request->get_param( 'error_description' );
        
        self::gdrive_oauth_callback_process( $code, $state, $error, $error_description );
    }
    
    /**
     * Process OAuth callback (shared logic for both admin_init and REST API).
     */
    private static function gdrive_oauth_callback_process( $code, $state, $error, $error_description ) {
        if ( $error ) {
            $error_msg = $error_description ?: $error;
            $redirect_url = admin_url( 'admin.php?page=mt-primelink-settings&tab=integrations&gdrive_auth=error&error=' . urlencode( $error ) . '&error_desc=' . urlencode( $error_msg ) );
            wp_safe_redirect( $redirect_url );
            exit;
        }

        if ( ! $code || ! $state ) {
            $redirect_url = admin_url( 'admin.php?page=mt-primelink-settings&tab=integrations&gdrive_auth=error&error=missing_params' );
            wp_safe_redirect( $redirect_url );
            exit;
        }

        // Verify state - try to get user from state or require login
        $user_id = get_current_user_id();
        if ( ! $user_id ) {
            // Try to find user by checking all transients (fallback)
            global $wpdb;
            $transients = $wpdb->get_results( $wpdb->prepare(
                "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
                $wpdb->esc_like( '_transient_gdrive_oauth_state_' ) . '%'
            ) );
            
            $found_user_id = 0;
            foreach ( $transients as $transient ) {
                $transient_name = str_replace( '_transient_', '', $transient->option_name );
                $transient_value = get_transient( $transient_name );
                if ( $transient_value && hash_equals( $transient_value, $state ) ) {
                    // Extract user ID from transient name
                    $parts = explode( '_', $transient_name );
                    if ( isset( $parts[4] ) && is_numeric( $parts[4] ) ) {
                        $found_user_id = absint( $parts[4] );
                        break;
                    }
                }
            }
            
            if ( ! $found_user_id ) {
                $redirect_url = admin_url( 'admin.php?page=mt-primelink-settings&tab=integrations&gdrive_auth=error&error=not_logged_in' );
                wp_safe_redirect( $redirect_url );
                exit;
            }
            
            $user_id = $found_user_id;
            // Temporarily set current user for this request
            wp_set_current_user( $user_id );
        }

        $stored_state = get_transient( 'gdrive_oauth_state_' . $user_id );
        if ( ! $stored_state || ! hash_equals( $stored_state, $state ) ) {
            $redirect_url = admin_url( 'admin.php?page=mt-primelink-settings&tab=integrations&gdrive_auth=error&error=invalid_state' );
            wp_safe_redirect( $redirect_url );
            exit;
        }

        delete_transient( 'gdrive_oauth_state_' . $user_id );

        $settings = self::get_settings();
        $client_id = $settings['gdrive_client_id'] ?? '';
        $client_secret = $settings['gdrive_client_secret'] ?? '';

        if ( ! $client_id || ! $client_secret ) {
            wp_die( esc_html__( 'Client ID and Client Secret are not configured.', 'primelink' ), __( 'Configuration Error', 'primelink' ), array( 'response' => 400 ) );
        }

        // Exchange code for token
        // Use the same redirect URI that was used in the auth request
        $redirect_uri = get_transient( 'gdrive_oauth_redirect_' . $user_id );
        if ( ! $redirect_uri ) {
            // Fallback to admin page callback
            $redirect_uri = admin_url( 'admin.php?mt_primelink_gdrive_auth=callback' );
        }
        $token_url = 'https://oauth2.googleapis.com/token';
        
        // Log request details (remove sensitive data in production)
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            error_log( 'PrimeLink OAuth: Exchanging code for token' );
            error_log( 'PrimeLink OAuth: Redirect URI: ' . $redirect_uri );
            error_log( 'PrimeLink OAuth: Client ID: ' . substr( $client_id, 0, 20 ) . '...' );
        }
        
        $response = wp_remote_post( $token_url, array(
            'body' => array(
                'code'          => $code,
                'client_id'     => $client_id,
                'client_secret' => $client_secret,
                'redirect_uri'  => $redirect_uri,
                'grant_type'    => 'authorization_code',
            ),
            'timeout' => 30,
        ) );

        if ( is_wp_error( $response ) ) {
            $error_msg = $response->get_error_message();
            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                error_log( 'PrimeLink OAuth: wp_remote_post error: ' . $error_msg );
            }
            wp_die( esc_html( sprintf( __( 'Failed to exchange code for token: %s', 'primelink' ), $error_msg ) ), __( 'OAuth Error', 'primelink' ), array( 'response' => 500 ) );
        }

        $response_code = wp_remote_retrieve_response_code( $response );
        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );

        // Log response details
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            error_log( 'PrimeLink OAuth: Response code: ' . $response_code );
            error_log( 'PrimeLink OAuth: Response body: ' . substr( $body, 0, 500 ) );
        }

        if ( ! isset( $data['access_token'] ) ) {
            $error_msg = isset( $data['error_description'] ) ? $data['error_description'] : ( isset( $data['error'] ) ? $data['error'] : __( 'Unknown error', 'primelink' ) );
            $error_code = isset( $data['error'] ) ? $data['error'] : 'unknown';
            
            // Log detailed error
            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                error_log( 'PrimeLink OAuth: Token exchange failed. Error: ' . $error_code . ', Description: ' . $error_msg );
                error_log( 'PrimeLink OAuth: Full response: ' . print_r( $data, true ) );
                error_log( 'PrimeLink OAuth: Redirect URI used: ' . $redirect_uri );
                error_log( 'PrimeLink OAuth: Redirect URI in Google Console should match exactly!' );
            }
            
            // Provide more helpful error message
            $helpful_msg = $error_msg;
            if ( $error_code === 'redirect_uri_mismatch' ) {
                $helpful_msg = sprintf( 
                    __( 'Redirect URI mismatch. The redirect URI in Google Cloud Console must exactly match: %s', 'primelink' ),
                    $redirect_uri
                );
            } elseif ( $error_code === 'invalid_grant' ) {
                $helpful_msg = __( 'Invalid authorization code. The code may have expired or already been used. Please try authenticating again.', 'primelink' );
            }
            
            wp_die( 
                esc_html( sprintf( __( 'Failed to get access token: %s', 'primelink' ), $helpful_msg ) ) . 
                '<br><br><small>' . esc_html__( 'Check WordPress debug.log for more details.', 'primelink' ) . '</small>',
                __( 'OAuth Error', 'primelink' ),
                array( 'response' => 400 )
            );
        }

        // Save access token and refresh token
        $settings['gdrive_access_token'] = $data['access_token'];
        if ( isset( $data['refresh_token'] ) ) {
            $settings['gdrive_refresh_token'] = $data['refresh_token'];
        }
        if ( isset( $data['expires_in'] ) ) {
            $settings['gdrive_token_expires'] = time() + absint( $data['expires_in'] );
        }
        
        // Ensure we're using the correct option key
        $option_key = self::get_settings_option_key();
        $saved = update_option( $option_key, $settings, false );
        
        // Log for debugging (remove in production)
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            error_log( 'PrimeLink OAuth: Token saved. Option key: ' . $option_key . ', Saved: ' . ( $saved ? 'true' : 'false' ) );
            error_log( 'PrimeLink OAuth: Access token length: ' . strlen( $settings['gdrive_access_token'] ) );
            error_log( 'PrimeLink OAuth: Has refresh token: ' . ( isset( $settings['gdrive_refresh_token'] ) ? 'yes' : 'no' ) );
        }

        // Redirect to settings page with success message
        $settings_url = admin_url( 'admin.php?page=mt-primelink-settings&tab=integrations&gdrive_auth=success' );
        wp_safe_redirect( $settings_url );
        exit;
    }

    public static function gdrive_revoke( $request ) {
        $settings = self::get_settings();
        $access_token = $settings['gdrive_access_token'] ?? '';

        if ( $access_token ) {
            // Revoke token with Google
            wp_remote_post( 'https://oauth2.googleapis.com/revoke', array(
                'body' => array( 'token' => $access_token ),
                'timeout' => 10,
            ) );
        }

        // Remove tokens from settings
        unset( $settings['gdrive_access_token'] );
        unset( $settings['gdrive_refresh_token'] );
        unset( $settings['gdrive_token_expires'] );
        update_option( 'mt_primelink_settings', $settings );

        return array(
            'success' => true,
            'message' => __( 'Google Drive access revoked successfully.', 'primelink' ),
        );
    }

    /**
     * Refresh Google Drive access token using refresh token.
     *
     * @param string $refresh_token Refresh token.
     * @param array  $settings      Current settings.
     * @return string|WP_Error New access token or error.
     */
    private static function refresh_gdrive_token( $refresh_token, $settings ) {
        $client_id = $settings['gdrive_client_id'] ?? '';
        $client_secret = $settings['gdrive_client_secret'] ?? '';

        if ( ! $client_id || ! $client_secret ) {
            return new \WP_Error( 'missing_credentials', __( 'Client ID and Client Secret are required for token refresh.', 'primelink' ) );
        }

        $response = wp_remote_post( 'https://oauth2.googleapis.com/token', array(
            'body' => array(
                'client_id'     => $client_id,
                'client_secret' => $client_secret,
                'refresh_token' => $refresh_token,
                'grant_type'    => 'refresh_token',
            ),
            'timeout' => 30,
        ) );

        if ( is_wp_error( $response ) ) {
            return $response;
        }

        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );

        if ( ! isset( $data['access_token'] ) ) {
            return new \WP_Error( 'token_refresh_failed', __( 'Failed to refresh access token.', 'primelink' ) );
        }

        // Update token expiration
        if ( isset( $data['expires_in'] ) ) {
            $settings['gdrive_token_expires'] = time() + absint( $data['expires_in'] );
            update_option( 'mt_primelink_settings', $settings );
        }

        return $data['access_token'];
    }

    /** ---------------- OneDrive OAuth ---------------- */

    public static function onedrive_auth_url( $request ) {
        $settings = self::get_settings();
        $use_pkce = $request->get_param( 'use_pkce' ) === 'true' || $request->get_param( 'use_pkce' ) === true;
        
        if ( $use_pkce ) {
            // PKCE flow (no Client Secret required)
            return self::onedrive_auth_url_pkce( $settings );
        } else {
            // Standard OAuth flow (requires Client Secret)
            return self::onedrive_auth_url_standard( $settings );
        }
    }
    
    /**
     * Generate OAuth URL using PKCE (no Client Secret required).
     *
     * @param array $settings Settings.
     * @return WP_REST_Response|array
     */
    private static function onedrive_auth_url_pkce( $settings ) {
        $client_id = $settings['onedrive_client_id'] ?? '';
        $tenant_id = $settings['onedrive_tenant_id'] ?? 'common'; // Default to 'common' for multi-tenant
        
        if ( ! $client_id ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'message' => __( 'Client ID is required for OAuth authentication.', 'primelink' ),
            ), 400 );
        }

        // Generate PKCE parameters
        $code_verifier = self::generate_pkce_code_verifier();
        $code_challenge = self::generate_pkce_code_challenge( $code_verifier );
        $state = wp_create_nonce( 'onedrive_oauth_pkce_' . get_current_user_id() );
        
        // Store code verifier in transient (we'll use transient instead of session for simplicity)
        $user_id = get_current_user_id();
        set_transient( 'onedrive_pkce_code_verifier_' . $user_id, $code_verifier, 600 );
        set_transient( 'onedrive_oauth_state_' . $user_id, $state, 600 );
        
        // Use admin page callback - NO query string in Redirect URI (Azure AD requirement)
        $redirect_uri = admin_url( 'admin.php' );
        set_transient( 'onedrive_oauth_redirect_' . $user_id, $redirect_uri, 600 );
        
        // Microsoft Graph API scopes
        $scope = 'https://graph.microsoft.com/Files.ReadWrite offline_access';
        
        // Use 'common' if tenant_id is empty, otherwise use provided tenant_id
        $tenant = $tenant_id ?: 'common';
        
        // Microsoft OAuth endpoint with PKCE
        $auth_url = add_query_arg( array(
            'client_id'             => $client_id,
            'redirect_uri'          => $redirect_uri,
            'response_type'         => 'code',
            'scope'                 => $scope,
            'state'                 => $state,
            'code_challenge'        => $code_challenge,
            'code_challenge_method' => 'S256',
            'response_mode'         => 'query',
        ), 'https://login.microsoftonline.com/' . $tenant . '/oauth2/v2.0/authorize' );

        return array(
            'success' => true,
            'auth_url' => $auth_url,
            'redirect_uri' => $redirect_uri,
            'use_pkce' => true,
            'instructions' => sprintf(
                __( 'Make sure this Redirect URI is added in Azure AD App Registration → Authentication → Redirect URIs: %s', 'primelink' ),
                $redirect_uri
            ),
        );
    }
    
    /**
     * Generate OAuth URL using standard flow (requires Client Secret).
     *
     * @param array $settings Settings.
     * @return WP_REST_Response|array
     */
    private static function onedrive_auth_url_standard( $settings ) {
        $client_id = $settings['onedrive_client_id'] ?? '';
        $client_secret = $settings['onedrive_client_secret'] ?? '';
        $tenant_id = $settings['onedrive_tenant_id'] ?? '';
        
        if ( ! $client_id || ! $client_secret ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'message' => __( 'Client ID and Client Secret are required for Manual Connection. If you don\'t have Client Secret, use Automatic Connection (PKCE) instead.', 'primelink' ),
            ), 400 );
        }

        // Use 'common' if tenant_id is empty (allows both personal and business accounts)
        if ( empty( $tenant_id ) ) {
            $tenant_id = 'common';
        }

        // Generate OAuth URL
        // Use admin page callback - NO query string in Redirect URI (Azure AD requirement)
        $redirect_uri = admin_url( 'admin.php' );
        // Microsoft Graph API scopes: Files.ReadWrite for read and write access
        $scope = 'https://graph.microsoft.com/Files.ReadWrite offline_access';
        $state = wp_create_nonce( 'onedrive_oauth_' . get_current_user_id() );
        
        // Store state in transient for verification
        $user_id = get_current_user_id();
        set_transient( 'onedrive_oauth_state_' . $user_id, $state, 600 );
        
        // Also store redirect URI for verification
        set_transient( 'onedrive_oauth_redirect_' . $user_id, $redirect_uri, 600 );
        
        // Microsoft OAuth endpoint
        $auth_url = add_query_arg( array(
            'client_id'     => $client_id,
            'redirect_uri'  => $redirect_uri,
            'response_type' => 'code',
            'scope'         => $scope,
            'state'         => $state,
            'response_mode' => 'query',
        ), 'https://login.microsoftonline.com/' . $tenant_id . '/oauth2/v2.0/authorize' );

        return array(
            'success' => true,
            'auth_url' => $auth_url,
            'redirect_uri' => $redirect_uri,
            'use_pkce' => false,
            'instructions' => sprintf(
                __( 'Make sure this Redirect URI is added in Azure AD App Registration → Authentication → Redirect URIs: %s', 'primelink' ),
                $redirect_uri
            ),
        );
    }
    
    /**
     * Generate PKCE code verifier.
     *
     * @param int $length Length of code verifier (43-128 characters).
     * @return string
     */
    private static function generate_pkce_code_verifier( $length = 128 ) {
        $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
        $code_verifier = '';
        $characters_length = strlen( $characters );

        for ( $i = 0; $i < $length; $i++ ) {
            $code_verifier .= $characters[ wp_rand( 0, $characters_length - 1 ) ];
        }

        return $code_verifier;
    }
    
    /**
     * Generate PKCE code challenge from code verifier.
     *
     * @param string $code_verifier The code verifier string.
     * @return string
     */
    private static function generate_pkce_code_challenge( $code_verifier ) {
        return rtrim( strtr( base64_encode( hash( 'sha256', $code_verifier, true ) ), '+/', '-_' ), '=' );
    }

    /**
     * Handle OAuth callback via admin_init hook (more reliable than REST endpoint).
     * Called from Admin::handle_onedrive_oauth_callback()
     * 
     * @param bool $is_popup Whether this is a popup callback.
     * @return bool Success status.
     */
    public static function onedrive_oauth_callback_admin( $is_popup = false ) {
        $code = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : '';
        $state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : '';
        $error = isset( $_GET['error'] ) ? sanitize_text_field( wp_unslash( $_GET['error'] ) ) : '';
        $error_description = isset( $_GET['error_description'] ) ? sanitize_text_field( wp_unslash( $_GET['error_description'] ) ) : '';
        
        return self::onedrive_oauth_callback_process( $code, $state, $error, $error_description, $is_popup );
    }
    
    /**
     * Handle OAuth callback via REST API endpoint (fallback).
     */
    public static function onedrive_oauth_callback( $request ) {
        $code = $request->get_param( 'code' );
        $state = $request->get_param( 'state' );
        $error = $request->get_param( 'error' );
        $error_description = $request->get_param( 'error_description' );
        
        self::onedrive_oauth_callback_process( $code, $state, $error, $error_description );
    }
    
    /**
     * Process OneDrive OAuth callback (shared logic for both admin_init and REST API).
     * 
     * @param string $code Authorization code.
     * @param string $state State parameter.
     * @param string $error Error code if any.
     * @param string $error_description Error description if any.
     * @param bool   $is_popup Whether this is a popup callback.
     * @return bool Success status.
     */
    private static function onedrive_oauth_callback_process( $code, $state, $error, $error_description, $is_popup = false ) {
        if ( $error ) {
            $error_msg = $error_description ?: $error;
            if ( $is_popup ) {
                // For popup, show error in popup and close
                wp_die( 
                    esc_html( sprintf( __( 'Authentication failed: %s', 'primelink' ), $error_msg ) ) . 
                    '<script>setTimeout(function(){ window.close(); }, 3000);</script>',
                    __( 'OAuth Error', 'primelink' ),
                    array( 'response' => 400 )
                );
            }
            $redirect_url = admin_url( 'admin.php?page=mt-primelink-settings&tab=integrations&onedrive_auth=error&error=' . urlencode( $error ) . '&error_desc=' . urlencode( $error_msg ) );
            wp_safe_redirect( $redirect_url );
            exit;
        }

        if ( ! $code || ! $state ) {
            if ( $is_popup ) {
                wp_die( 
                    esc_html__( 'Missing authorization parameters. Please try again.', 'primelink' ) . 
                    '<script>setTimeout(function(){ window.close(); }, 3000);</script>',
                    __( 'OAuth Error', 'primelink' ),
                    array( 'response' => 400 )
                );
            }
            $redirect_url = admin_url( 'admin.php?page=mt-primelink-settings&tab=integrations&onedrive_auth=error&error=missing_params' );
            wp_safe_redirect( $redirect_url );
            exit;
        }

        // Verify state - try to get user from state or require login
        $user_id = get_current_user_id();
        if ( ! $user_id ) {
            // Try to find user by checking all transients (fallback)
            global $wpdb;
            $transients = $wpdb->get_results( $wpdb->prepare(
                "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
                $wpdb->esc_like( '_transient_onedrive_oauth_state_' ) . '%'
            ) );
            
            $found_user_id = 0;
            foreach ( $transients as $transient ) {
                $transient_name = str_replace( '_transient_', '', $transient->option_name );
                $transient_value = get_transient( $transient_name );
                if ( $transient_value && hash_equals( $transient_value, $state ) ) {
                    // Extract user ID from transient name
                    $parts = explode( '_', $transient_name );
                    if ( isset( $parts[4] ) && is_numeric( $parts[4] ) ) {
                        $found_user_id = absint( $parts[4] );
                        break;
                    }
                }
            }
            
            if ( ! $found_user_id ) {
                $redirect_url = admin_url( 'admin.php?page=mt-primelink-settings&tab=integrations&onedrive_auth=error&error=not_logged_in' );
                wp_safe_redirect( $redirect_url );
                exit;
            }
            
            $user_id = $found_user_id;
            wp_set_current_user( $user_id );
        }

        $stored_state = get_transient( 'onedrive_oauth_state_' . $user_id );
        if ( ! $stored_state || ! hash_equals( $stored_state, $state ) ) {
            $redirect_url = admin_url( 'admin.php?page=mt-primelink-settings&tab=integrations&onedrive_auth=error&error=invalid_state' );
            wp_safe_redirect( $redirect_url );
            exit;
        }

        delete_transient( 'onedrive_oauth_state_' . $user_id );

        $settings = self::get_settings();
        
        // Check if we're using PKCE flow
        $code_verifier = get_transient( 'onedrive_pkce_code_verifier_' . $user_id );
        $use_pkce = ! empty( $code_verifier );
        
        // Get credentials from settings
        $client_id = $settings['onedrive_client_id'] ?? '';
        $client_secret = $settings['onedrive_client_secret'] ?? '';
        $tenant_id = $settings['onedrive_tenant_id'] ?? 'common'; // Default to 'common' for multi-tenant

        if ( ! $client_id ) {
            wp_die( esc_html__( 'Client ID is not configured.', 'primelink' ), __( 'Configuration Error', 'primelink' ), array( 'response' => 400 ) );
        }

        // For PKCE flow, Client Secret is not required
        if ( ! $use_pkce && ! $client_secret ) {
            wp_die( esc_html__( 'Client Secret is required for standard OAuth flow. If you don\'t have Client Secret, use Automatic Connection (PKCE) instead.', 'primelink' ), __( 'Configuration Error', 'primelink' ), array( 'response' => 400 ) );
        }
        
        // Use 'common' if tenant_id is empty (allows both personal and business accounts)
        if ( empty( $tenant_id ) ) {
            $tenant_id = 'common';
        }

        // Exchange code for token
        $redirect_uri = get_transient( 'onedrive_oauth_redirect_' . $user_id );
        if ( ! $redirect_uri ) {
            // Fallback - use admin.php without query string (Azure AD requirement)
            $redirect_uri = admin_url( 'admin.php' );
        }
        
        // Use 'common' if tenant_id is empty for PKCE, otherwise use provided tenant_id
        $tenant = ( $use_pkce && empty( $tenant_id ) ) ? 'common' : ( $tenant_id ?: 'common' );
        $token_url = 'https://login.microsoftonline.com/' . $tenant . '/oauth2/v2.0/token';
        
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            error_log( 'PrimeLink OneDrive OAuth: Exchanging code for token' );
            error_log( 'PrimeLink OneDrive OAuth: Using PKCE: ' . ( $use_pkce ? 'yes' : 'no' ) );
            error_log( 'PrimeLink OneDrive OAuth: Redirect URI: ' . $redirect_uri );
            error_log( 'PrimeLink OneDrive OAuth: Client ID: ' . substr( $client_id, 0, 20 ) . '...' );
        }
        
        // Build request body based on flow type
        $body = array(
            'code'          => $code,
            'client_id'     => $client_id,
            'redirect_uri'  => $redirect_uri,
            'grant_type'    => 'authorization_code',
        );
        
        if ( $use_pkce ) {
            // PKCE flow: use code_verifier instead of client_secret
            $body['code_verifier'] = $code_verifier;
            // Delete code verifier after use
            delete_transient( 'onedrive_pkce_code_verifier_' . $user_id );
        } else {
            // Standard flow: use client_secret
            $body['client_secret'] = $client_secret;
        }
        
        $response = wp_remote_post( $token_url, array(
            'body' => $body,
            'timeout' => 30,
        ) );

        if ( is_wp_error( $response ) ) {
            $error_msg = $response->get_error_message();
            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                error_log( 'PrimeLink OneDrive OAuth: wp_remote_post error: ' . $error_msg );
            }
            wp_die( esc_html( sprintf( __( 'Failed to exchange code for token: %s', 'primelink' ), $error_msg ) ), __( 'OAuth Error', 'primelink' ), array( 'response' => 500 ) );
        }

        $response_code = wp_remote_retrieve_response_code( $response );
        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );

        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            error_log( 'PrimeLink OneDrive OAuth: Response code: ' . $response_code );
            error_log( 'PrimeLink OneDrive OAuth: Response body: ' . substr( $body, 0, 500 ) );
        }

        if ( ! isset( $data['access_token'] ) ) {
            $error_msg = isset( $data['error_description'] ) ? $data['error_description'] : ( isset( $data['error'] ) ? $data['error'] : __( 'Unknown error', 'primelink' ) );
            $error_code = isset( $data['error'] ) ? $data['error'] : 'unknown';
            
            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                error_log( 'PrimeLink OneDrive OAuth: Token exchange failed. Error: ' . $error_code . ', Description: ' . $error_msg );
                error_log( 'PrimeLink OneDrive OAuth: Full response: ' . print_r( $data, true ) );
                error_log( 'PrimeLink OneDrive OAuth: Redirect URI used: ' . $redirect_uri );
            }
            
            $helpful_msg = $error_msg;
            $error_details = '';
            
            if ( $error_code === 'invalid_client' ) {
                $helpful_msg = __( 'Invalid client credentials. Please check:', 'primelink' );
                $error_details = __( '• Client ID is correct and matches Azure AD<br>• Client Secret is correct and not expired<br>• Redirect URI in Azure AD matches exactly: ', 'primelink' ) . esc_html( $redirect_uri );
            } elseif ( $error_code === 'invalid_grant' ) {
                $helpful_msg = __( 'Invalid authorization code. The code may have expired or already been used. Please try authenticating again.', 'primelink' );
            } elseif ( $error_code === 'redirect_uri_mismatch' ) {
                $helpful_msg = __( 'Redirect URI mismatch. The redirect URI in Azure AD must exactly match:', 'primelink' );
                $error_details = esc_html( $redirect_uri ) . '<br><br>' . __( 'Go to Azure Portal → App registrations → Your app → Authentication → Redirect URIs and add this exact URL.', 'primelink' );
            } elseif ( $error_code === 'invalid_request' ) {
                $helpful_msg = __( 'Invalid request. Common causes:', 'primelink' );
                $error_details = __( '• Redirect URI mismatch<br>• Missing or invalid Client ID<br>• Missing or invalid Client Secret (for Manual Connection)<br>• Check WordPress debug.log for details', 'primelink' );
            }
            
            $error_html = esc_html( sprintf( __( 'Failed to get access token: %s', 'primelink' ), $helpful_msg ) );
            if ( $error_details ) {
                $error_html .= '<br><br><div style="background:#f8f9fa;padding:12px;border-left:4px solid #0073aa;border-radius:4px;margin-top:12px;">' . $error_details . '</div>';
            }
            $error_html .= '<br><br><small>' . esc_html__( 'Check WordPress debug.log for more details.', 'primelink' ) . '</small>';
            
            wp_die( 
                $error_html,
                __( 'OAuth Error', 'primelink' ),
                array( 'response' => 400 )
            );
        }

        // Save access token and refresh token
        $settings['onedrive_access_token'] = $data['access_token'];
        if ( isset( $data['refresh_token'] ) ) {
            $settings['onedrive_refresh_token'] = $data['refresh_token'];
        }
        if ( isset( $data['expires_in'] ) ) {
            $settings['onedrive_token_expires'] = time() + absint( $data['expires_in'] );
        }
        
        $option_key = self::get_settings_option_key();
        $saved = update_option( $option_key, $settings, false );
        
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            error_log( 'PrimeLink OneDrive OAuth: Token saved. Option key: ' . $option_key . ', Saved: ' . ( $saved ? 'true' : 'false' ) );
            error_log( 'PrimeLink OneDrive OAuth: Access token length: ' . strlen( $settings['onedrive_access_token'] ) );
            error_log( 'PrimeLink OneDrive OAuth: Has refresh token: ' . ( isset( $settings['onedrive_refresh_token'] ) ? 'yes' : 'no' ) );
        }

        // For popup, return success (popup will close itself)
        if ( $is_popup ) {
            return true;
        }
        
        // Redirect to settings page with success message
        $settings_url = admin_url( 'admin.php?page=mt-primelink-settings&tab=integrations&onedrive_auth=success' );
        wp_safe_redirect( $settings_url );
        exit;
    }

    public static function onedrive_revoke( $request ) {
        $settings = self::get_settings();
        $access_token = $settings['onedrive_access_token'] ?? '';

        if ( $access_token ) {
            // Revoke token with Microsoft (optional - Microsoft doesn't require explicit revocation)
            // But we can still clear it from our settings
        }

        // Remove tokens from settings
        unset( $settings['onedrive_access_token'] );
        unset( $settings['onedrive_refresh_token'] );
        unset( $settings['onedrive_token_expires'] );
        update_option( 'mt_primelink_settings', $settings );

        return array(
            'success' => true,
            'message' => __( 'OneDrive access revoked successfully.', 'primelink' ),
        );
    }

    /**
     * List OneDrive files.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public static function onedrive_list( $request ) {
        $settings = self::get_settings();
        $access_token = $settings['onedrive_access_token'] ?? '';
        $refresh_token = $settings['onedrive_refresh_token'] ?? '';
        $client_id = $settings['onedrive_client_id'] ?? '';
        $client_secret = $settings['onedrive_client_secret'] ?? '';
        $tenant_id = $settings['onedrive_tenant_id'] ?? '';
        $token_expires = $settings['onedrive_token_expires'] ?? 0;
        
        // Check if token is expired and try to refresh
        if ( $access_token && $refresh_token && $client_id && $client_secret && $tenant_id ) {
            if ( ! $token_expires || time() >= ( $token_expires - 300 ) ) {
                $new_access_token = self::refresh_onedrive_token( $refresh_token, $client_id, $client_secret, $tenant_id );
                if ( ! is_wp_error( $new_access_token ) && is_string( $new_access_token ) && ! empty( $new_access_token ) ) {
                    $access_token = $new_access_token;
                    $settings['onedrive_access_token'] = $access_token;
                    update_option( 'mt_primelink_settings', $settings );
                } elseif ( is_wp_error( $new_access_token ) ) {
                    return new \WP_REST_Response( array(
                        'success' => false,
                        'error'   => 'OneDrive authentication expired. Please re-authenticate using the "Authenticate with OneDrive" button in Settings → Integrations.',
                    ), 401 );
                }
            }
        }
        
        if ( ! $access_token ) {
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => 'OneDrive OAuth access token is required. Please authenticate with OneDrive in Settings → Integrations.',
            ), 401 );
        }

        $folder_id = $request->get_param( 'folder_id' ) ?: 'root';
        $query = $request->get_param( 'query' );
        $page_token = $request->get_param( 'page_token' );

        $base_url = 'https://graph.microsoft.com/v1.0';
        
        // For OneDrive Personal, use /me/drive directly (as per Microsoft documentation)
        // For business accounts, we'll fallback to /me/drives if needed
        $use_drive_id = false;
        $drive_id = null;
        
        // First, try to get drive info using /me/drive (works for personal accounts)
        $drive_info_url = $base_url . '/me/drive';
        $drive_info_response = \PrimeLink\Helpers\HttpHelper::get( $drive_info_url, array(
            'headers' => array(
                'Authorization' => 'Bearer ' . $access_token,
            ),
            'timeout' => 30,
        ) );
        
        if ( $drive_info_response['success'] ) {
            $drive_info = json_decode( $drive_info_response['body'], true );
            $drive_id = $drive_info['id'] ?? null;
            if ( $drive_id ) {
                $use_drive_id = true;
            }
        }
        
        // If /me/drive fails (e.g., business account without SPO), try /me/drives
        if ( ! $use_drive_id ) {
            $drives_url = $base_url . '/me/drives';
            $drives_response = \PrimeLink\Helpers\HttpHelper::get( $drives_url, array(
                'headers' => array(
                    'Authorization' => 'Bearer ' . $access_token,
                ),
                'timeout' => 30,
            ) );
            
            if ( $drives_response['success'] ) {
                $drives_data = json_decode( $drives_response['body'], true );
                if ( isset( $drives_data['value'] ) && ! empty( $drives_data['value'] ) ) {
                    // Use the first available drive
                    $first_drive = $drives_data['value'][0];
                    $drive_id = $first_drive['id'] ?? null;
                    if ( $drive_id ) {
                        $use_drive_id = true;
                    }
                }
            }
        }
        
        // Build API URL
        if ( $use_drive_id && $drive_id ) {
            // Use specific drive ID (works for both personal and business)
            if ( $query ) {
                $url = $base_url . '/drives/' . rawurlencode( $drive_id ) . '/root/search(q=\'' . rawurlencode( $query ) . '\')';
            } elseif ( $folder_id === 'root' ) {
                $url = $base_url . '/drives/' . rawurlencode( $drive_id ) . '/root/children';
            } else {
                $url = $base_url . '/drives/' . rawurlencode( $drive_id ) . '/items/' . rawurlencode( $folder_id ) . '/children';
            }
        } else {
            // Fallback to /me/drive (for personal accounts)
            if ( $query ) {
                $url = $base_url . '/me/drive/root/search(q=\'' . rawurlencode( $query ) . '\')';
            } elseif ( $folder_id === 'root' ) {
                $url = $base_url . '/me/drive/root/children';
            } else {
                $url = $base_url . '/me/drive/items/' . rawurlencode( $folder_id ) . '/children';
            }
        }

        // Add pagination if provided
        if ( $page_token ) {
            $url .= ( strpos( $url, '?' ) === false ? '?' : '&' ) . '$skiptoken=' . rawurlencode( $page_token );
        } else {
            $url .= ( strpos( $url, '?' ) === false ? '?' : '&' ) . '$top=50';
        }

        // Add fields filter
        $url .= ( strpos( $url, '?' ) === false ? '?' : '&' ) . '$select=id,name,size,lastModifiedDateTime,folder,file,mimeType,webUrl';

        $response = \PrimeLink\Helpers\HttpHelper::get( $url, array(
            'headers' => array(
                'Authorization' => 'Bearer ' . $access_token,
            ),
            'timeout' => 30,
        ) );

        if ( ! $response['success'] ) {
            $code = $response['code'] ?? 500;
            $error_body = json_decode( $response['body'] ?? '{}', true );
            $error_message = $error_body['error']['message'] ?? '';
            
            if ( $code === 401 ) {
                $error_msg = 'OneDrive authentication expired. Please re-authenticate using the "Authenticate with OneDrive" button in Settings → Integrations.';
            } elseif ( $code === 403 ) {
                $error_msg = 'OneDrive access denied. Please check your permissions and re-authenticate.';
            } elseif ( $code === 404 ) {
                $error_msg = 'OneDrive folder not found. Please check the folder ID.';
            } elseif ( $code === 400 ) {
                $error_msg = 'OneDrive API error: ' . ( $error_message ?: 'Bad Request. Your account may not have the required permissions or license.' );
            } else {
                $error_msg = 'Failed to list OneDrive files: ' . ( $error_message ?: 'Unknown error' );
            }
            
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => $error_msg,
            ), $code );
        }

        $data = json_decode( $response['body'], true );
        
        // Debug logging removed for production - use WP_DEBUG if needed
        
        if ( ! isset( $data['value'] ) ) {
            // Debug logging removed for production - use WP_DEBUG if needed
            return new \WP_REST_Response( array(
                'success' => false,
                'error'   => 'Invalid response from OneDrive API.',
            ), 500 );
        }

        // Filter for supported file types
        $supported_mime_types = array(
            'text/csv',
            'application/vnd.ms-excel',
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            'application/vnd.ms-excel.sheet.macroEnabled.12',
            'text/xml',
            'application/xml',
            'application/json',
        );

        $files = array();
        foreach ( $data['value'] as $item ) {
            // Skip trashed items
            if ( isset( $item['deleted'] ) && $item['deleted'] ) {
                continue;
            }
            
            $mime_type = $item['mimeType'] ?? '';
            $is_folder = isset( $item['folder'] );
            $file_name = $item['name'] ?? '';
            
            // For browsing, show both folders and supported file types
            // Folders are always shown for navigation
            if ( ! $is_folder ) {
                // For files, check if file type is supported
                // Check both mimeType and file extension
                $file_ext = strtolower( pathinfo( $file_name, PATHINFO_EXTENSION ) );
                $supported_extensions = array( 'csv', 'xls', 'xlsx', 'xml', 'json', 'txt' );
                
                $is_supported = false;
                if ( $mime_type && in_array( $mime_type, $supported_mime_types, true ) ) {
                    $is_supported = true;
                } elseif ( $file_ext && in_array( $file_ext, $supported_extensions, true ) ) {
                    $is_supported = true;
                }
                
                if ( ! $is_supported ) {
                    continue;
                }
            }

            $files[] = array(
                'id'   => $item['id'],
                'name' => $item['name'],
                'size' => $item['size'] ?? 0,
                'mimeType' => $mime_type,
                'modifiedTime' => $item['lastModifiedDateTime'] ?? '',
                'webUrl' => $item['webUrl'] ?? '',
                'isFolder' => $is_folder,
            );
        }
        
        // Debug logging removed for production - use WP_DEBUG if needed

        return array(
            'success' => true,
            'files'   => $files,
            'nextPageToken' => $data['@odata.nextLink'] ?? null,
        );
    }

    /**
     * Refresh OneDrive access token using refresh token.
     *
     * @param string $refresh_token Refresh token.
     * @param string $client_id     Client ID.
     * @param string $client_secret Client Secret.
     * @param string $tenant_id    Tenant ID.
     * @return string|WP_Error New access token or error.
     */
    private static function refresh_onedrive_token( $refresh_token, $client_id, $client_secret, $tenant_id ) {
        if ( ! $client_id || ! $client_secret || ! $tenant_id ) {
            return new \WP_Error( 'missing_credentials', __( 'Client ID, Client Secret, and Tenant ID are required for token refresh.', 'primelink' ) );
        }

        $token_url = 'https://login.microsoftonline.com/' . $tenant_id . '/oauth2/v2.0/token';

        $response = wp_remote_post( $token_url, array(
            'body' => array(
                'client_id'     => $client_id,
                'client_secret' => $client_secret,
                'refresh_token' => $refresh_token,
                'grant_type'    => 'refresh_token',
            ),
            'timeout' => 30,
        ) );

        if ( is_wp_error( $response ) ) {
            return $response;
        }

        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );

        if ( ! isset( $data['access_token'] ) ) {
            return new \WP_Error( 'token_refresh_failed', __( 'Failed to refresh OneDrive access token.', 'primelink' ) );
        }

        // Update token expiration in settings
        $settings = self::get_settings();
        if ( isset( $data['expires_in'] ) ) {
            $settings['onedrive_token_expires'] = time() + absint( $data['expires_in'] );
        }
        if ( isset( $data['refresh_token'] ) ) {
            $settings['onedrive_refresh_token'] = $data['refresh_token'];
        }
        $settings['onedrive_access_token'] = $data['access_token'];
        update_option( 'mt_primelink_settings', $settings );

        return $data['access_token'];
    }

}
