<?php
namespace WebshoptigerCatalogSync;

defined( 'ABSPATH' ) || exit;

class Queue {

    const PROCESS_HOOK = 'webshoptigercatalogsync_process_queue';

    private static function queue_table(): string {
        global $wpdb;
        return $wpdb->prefix . 'webshoptigercatalogsync_queue';
    }

    private static function jobs_table(): string {
        global $wpdb;
        return $wpdb->prefix . 'webshoptigercatalogsync_jobs';
    }

    // -------------------------------------------------------------------------
    // Init
    // -------------------------------------------------------------------------

    public static function init(): void {
        add_action( self::PROCESS_HOOK, [ __CLASS__, 'process_batch' ] );
    }

    // -------------------------------------------------------------------------
    // Create a new job
    // -------------------------------------------------------------------------

    public static function enqueue_all(): string {
        // Large catalogs (10,000+ products) need more memory and time
        // to collect product IDs and bulk-insert the queue rows.
        // phpcs:ignore WordPress.PHP.DiscouragedFunctions.Discouraged
        @ini_set( 'memory_limit', '512M' );
        // phpcs:ignore WordPress.PHP.DiscouragedFunctions.Discouraged
        @set_time_limit( 120 );

        $sites  = SiteManager::get_enabled();
        $job_id = self::generate_uuid();
        $total  = 0;
        foreach ( $sites as $site ) {
            $total += self::enqueue_site( (int) $site['id'], $job_id );
        }
        self::create_job( $job_id, 0, $total );
        self::schedule_next_batch();
        return $job_id;
    }

    public static function enqueue_one_site( int $site_id ): string {
        // phpcs:ignore WordPress.PHP.DiscouragedFunctions.Discouraged
        @ini_set( 'memory_limit', '512M' );
        // phpcs:ignore WordPress.PHP.DiscouragedFunctions.Discouraged
        @set_time_limit( 120 );

        $job_id = self::generate_uuid();
        $total  = self::enqueue_site( $site_id, $job_id );
        self::create_job( $job_id, $site_id, $total );
        self::schedule_next_batch();
        return $job_id;
    }

    /**
     * Queue one specific product for synchronization.
     * Used by the "Select a product to sync" button in the admin.
     *
     * @param int      $product_id  Local product ID on the source site
     * @param int|null $site_id     Specific destination site ID, or null for all active sites
     * @return string|\WP_Error  Job ID for tracking progress, or a
     *                           WP_Error if the Lite product limit blocks this.
     */
    public static function enqueue_single_product( int $product_id, ?int $site_id = null ) {
        if ( ! Settings::can_sync_additional_product( $product_id ) ) {
            return new \WP_Error(
                'webshoptigercatalogsync_limit_reached',
                'Product limit reached (' . Settings::product_limit() . ' products). Upgrade to Pro for unlimited synchronization.'
            );
        }

        $job_id = self::generate_uuid();
        $sites  = $site_id !== null
            ? array_filter( [ SiteManager::get( $site_id ) ] )
            : SiteManager::get_enabled();

        global $wpdb;
        $table = self::queue_table();
        $now   = current_time( 'mysql' );
        $total = 0;

        foreach ( $sites as $site ) {
            if ( ! $site ) continue;
            $wpdb->insert( $table, [
                'job_id'     => $job_id,
                'site_id'    => (int) $site['id'],
                'product_id' => $product_id,
                'status'     => 'pending',
                'queued_at'  => $now,
            ] );
            $total++;
        }

        self::create_job( $job_id, $site_id ?? 0, $total );
        self::schedule_next_batch();
        return $job_id;
    }

    /**
     * Search products by name, SKU, or ID — for the product picker in the admin.
     * Returns at most 20 results with basic information.
     *
     * WC_Product_Query's 's' argument only searches the title, not the SKU
     * (a known WooCommerce limitation). That's why we combine three separate
     * search strategies: exact ID, SKU match, and title search.
     */
    public static function search_products( string $term ): array {
        $term = trim( $term );
        if ( $term === '' ) return [];

        $found = []; // [ product_id => true ] to prevent duplicates
        $results = [];

        // Strategy 1: exact product ID
        if ( ctype_digit( $term ) ) {
            $by_id = wc_get_product( (int) $term );
            if ( $by_id && in_array( $by_id->get_status(), [ 'publish', 'private', 'draft' ], true ) ) {
                $found[ $by_id->get_id() ] = true;
                $results[] = self::format_product_result( $by_id );
            }
        }

        // Strategy 2: SKU match (exact and partial via direct query)
        global $wpdb;
        $sku_ids = $wpdb->get_col( $wpdb->prepare(
            "SELECT DISTINCT post_id FROM {$wpdb->postmeta}
             WHERE meta_key = '_sku' AND meta_value LIKE %s
             LIMIT 20",
            '%' . $wpdb->esc_like( $term ) . '%'
        ) );

        foreach ( $sku_ids as $id ) {
            $id = (int) $id;
            if ( isset( $found[ $id ] ) ) continue;
            $product = wc_get_product( $id );
            if ( ! $product ) continue;
            $found[ $id ] = true;
            $results[] = self::format_product_result( $product );
        }

        // Strategy 3: title search
        if ( count( $results ) < 20 ) {
            $title_products = wc_get_products( [
                'limit'  => 20,
                'type'   => [ 'simple', 'variable' ],
                'status' => 'publish',
                'return' => 'objects',
                's'      => $term,
            ] );

            foreach ( $title_products as $product ) {
                if ( isset( $found[ $product->get_id() ] ) ) continue;
                $found[ $product->get_id() ] = true;
                $results[] = self::format_product_result( $product );
                if ( count( $results ) >= 20 ) break;
            }
        }

        return array_slice( $results, 0, 20 );
    }

    private static function format_product_result( \WC_Product $product ): array {
        return [
            'id'    => $product->get_id(),
            'name'  => $product->get_name(),
            'sku'   => $product->get_sku(),
            'type'  => $product->get_type(),
            'image' => wp_get_attachment_image_url( $product->get_image_id(), 'thumbnail' ) ?: '',
        ];
    }

    private static function enqueue_site( int $site_id, string $job_id ): int {
        $site = SiteManager::get( $site_id );
        if ( ! $site ) return 0;

        $categories = (array) $site['categories'];
        if ( empty( $categories ) ) return 0;

        // Validate caches for this site
        CategorySync::validate_cache( $site );
        BrandSync::validate_cache( $site );

        global $wpdb;
        $table = self::queue_table();
        $now   = $wpdb->get_var( 'SELECT NOW()' );

        // Collect all descendant term IDs recursively
        $all_term_ids = self::get_all_child_term_ids( array_map( 'intval', $categories ) );

        // Build safe IN() placeholders for term IDs (all are intval-sanitized above)
        $term_placeholders = implode( ',', array_fill( 0, count( $all_term_ids ), '%d' ) );

        // Build a temporary table of matching product IDs directly in MySQL —
        // avoids loading tens of thousands of IDs into PHP memory.
        $limit = Settings::product_limit();
        $qt    = $wpdb->prefix . 'webshoptigercatalogsync_queue';

        if ( $limit > 0 ) {
            $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $wpdb->prepare(
                    "INSERT INTO `{$qt}` (job_id, site_id, product_id, status, queued_at)
                     SELECT %s, %d, p.ID, 'pending', %s
                     FROM {$wpdb->posts} p
                     INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
                     INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
                         AND tt.taxonomy = 'product_cat'
                         AND tt.term_id IN ({$term_placeholders})
                     WHERE p.post_type = 'product'
                       AND p.post_status = 'publish'
                     GROUP BY p.ID
                     ORDER BY p.ID ASC
                     LIMIT %d",
                    array_merge( [ $job_id, $site_id, $now ], $all_term_ids, [ $limit ] )
                )
            );
        } else {
            $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $wpdb->prepare(
                    "INSERT INTO `{$qt}` (job_id, site_id, product_id, status, queued_at)
                     SELECT %s, %d, p.ID, 'pending', %s
                     FROM {$wpdb->posts} p
                     INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
                     INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
                         AND tt.taxonomy = 'product_cat'
                         AND tt.term_id IN ({$term_placeholders})
                     WHERE p.post_type = 'product'
                       AND p.post_status = 'publish'
                     GROUP BY p.ID
                     ORDER BY p.ID ASC",
                    array_merge( [ $job_id, $site_id, $now ], $all_term_ids )
                )
            );
        }

        if ( $wpdb->last_error ) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            if ( defined('WP_DEBUG') && WP_DEBUG ) { error_log( 'Webshoptiger Catalog Sync enqueue_site error: ' . $wpdb->last_error ); }
            return 0;
        }

        return (int) $wpdb->rows_affected;
    }

    /**
     * Filters a list of product IDs based on the Lite product limit.
     * Products that have already been synced before (updates) always
     * stay in the list. New products are added until the remaining room
     * within the limit is filled. Has no effect with a Pro license
     * (unlimited).
     */
    private static function apply_product_limit( array $product_ids ): array {
        $limit = Settings::product_limit();
        if ( $limit === 0 ) return $product_ids; // Pro: unlimited

        $already_synced_count = Settings::synced_product_count();
        $remaining             = max( 0, $limit - $already_synced_count );

        $kept    = [];
        $new_used = 0;

        foreach ( $product_ids as $product_id ) {
            $is_existing = ! empty( get_post_meta( $product_id, '_webshoptigercatalogsync_remote_ids', true ) );

            if ( $is_existing ) {
                $kept[] = $product_id; // Update of an existing product — doesn't count
                continue;
            }

            if ( $new_used < $remaining ) {
                $kept[]    = $product_id;
                $new_used++;
            }
            // Otherwise: new product falls outside the Lite limit, skipped
        }

        return $kept;
    }

    /**
     * Collect all descendant term IDs for the given taxonomy term IDs,
     * recursively. Uses get_term_children() which is correct even for
     * root-level categories (unlike get_terms with parent__in which
     * returns siblings when parent=0).
     */
    private static function get_all_child_term_ids( array $term_ids, string $taxonomy = 'product_cat' ): array {
        $all = array_map( 'intval', $term_ids );

        foreach ( $term_ids as $term_id ) {
            $children = get_term_children( (int) $term_id, $taxonomy );
            if ( ! is_wp_error( $children ) && ! empty( $children ) ) {
                $all = array_merge( $all, array_map( 'intval', $children ) );
            }
        }

        return array_unique( $all );
    }

    private static function get_product_ids_by_categories( array $category_ids ): array {
        if ( empty( $category_ids ) ) return [];

        global $wpdb;

        // Collect all descendant term IDs (children, grandchildren, etc.)
        $all_term_ids     = self::get_all_child_term_ids( array_map( 'intval', $category_ids ) );
        $term_placeholders = implode( ',', array_fill( 0, count( $all_term_ids ), '%d' ) );

        // Direct SQL — bypasses wc_get_products() and WP_Query entirely,
        // so no other plugin can filter or reduce the result set.
        $ids = $wpdb->get_col( $wpdb->prepare(
            "SELECT DISTINCT p.ID
             FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->term_relationships} tr_cat ON tr_cat.object_id = p.ID
             INNER JOIN {$wpdb->term_taxonomy} tt_cat ON tt_cat.term_taxonomy_id = tr_cat.term_taxonomy_id
                                                      AND tt_cat.taxonomy = 'product_cat'
                                                      AND tt_cat.term_id IN ({$term_placeholders})
             WHERE p.post_type   = 'product'
               AND p.post_status = 'publish'",
            $all_term_ids
        ) );

        return array_map( 'intval', $ids ?: [] );
    }

    // -------------------------------------------------------------------------
    // Process batch
    // -------------------------------------------------------------------------

    public static function process_batch(): void {
        global $wpdb;

        // Remove the scheduled event so we can schedule a new one after this batch
        $ts = wp_next_scheduled( self::PROCESS_HOOK );
        if ( $ts ) {
            wp_unschedule_event( $ts, self::PROCESS_HOOK );
        }

        $batch_size = max( 1, (int) Settings::get( 'batch_size', 10 ) );
        $qt = $wpdb->prefix . 'webshoptigercatalogsync_queue';

        $tasks = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->prepare(
                "SELECT * FROM `{$qt}` WHERE status = 'pending' ORDER BY queued_at ASC, id ASC LIMIT %d",
                $batch_size
            ),
            ARRAY_A
        );

        if ( empty( $tasks ) ) {
            self::finalize_completed_jobs();
            return;
        }

        // Mark as processing to prevent duplicate handling
        $ids          = array_column( $tasks, 'id' );
        $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
        $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->prepare(
                "UPDATE `{$qt}` SET status = 'processing', attempts = attempts + 1 WHERE id IN ({$placeholders})",
                ...$ids
            )
        );

        foreach ( $tasks as $i => $task ) {
            self::process_task( $task );
            // Small pause between products to avoid overloading the destination site
            if ( $i < count( $tasks ) - 1 ) {
                usleep( 300000 ); // 300ms
            }
        }

        // Check whether there are more pending tasks
        $remaining = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT COUNT(*) FROM `{$qt}` WHERE status = 'pending'"
        );

        if ( $remaining > 0 ) {
            // Trigger the next batch directly via HTTP
            self::schedule_next_batch();
        } else {
            self::finalize_completed_jobs();
        }
    }

    private static function process_task( array $task ): void {
        $site    = SiteManager::get( (int) $task['site_id'] );
        $product = wc_get_product( (int) $task['product_id'] );

        if ( ! $site || ! $product ) {
            self::mark_task( (int) $task['id'], $task['job_id'], 'error', 'Product or site not found.' );
            return;
        }

        try {
            Sync::push_single_product( $product, $site );
            self::mark_task( (int) $task['id'], $task['job_id'], 'done', '' );
        } catch ( \Throwable $e ) {
            self::mark_task( (int) $task['id'], $task['job_id'], 'error', substr( $e->getMessage(), 0, 255 ) );
        }
    }

    private static function mark_task( int $id, string $job_id, string $status, string $error ): void {
        global $wpdb;
        $wpdb->update( self::queue_table(), [
            'status'        => $status,
            'error_message' => $error,
            'processed_at'  => current_time( 'mysql' ),
        ], [ 'id' => $id ] );

        $jt = $wpdb->prefix . 'webshoptigercatalogsync_jobs';
        if ( $status === 'done' ) {
            $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->prepare( "UPDATE `{$jt}` SET done = done + 1 WHERE job_id = %s", $job_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            );
        } elseif ( $status === 'error' ) {
            $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->prepare( "UPDATE `{$jt}` SET done = done + 1, errors = errors + 1 WHERE job_id = %s", $job_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            );
        }
    }

    // -------------------------------------------------------------------------
    // Job management
    // -------------------------------------------------------------------------

    private static function create_job( string $job_id, int $site_id, int $total ): void {
        global $wpdb;
        $wpdb->insert( self::jobs_table(), [
            'job_id'     => $job_id,
            'site_id'    => $site_id,
            'total'      => $total,
            'done'       => 0,
            'errors'     => 0,
            'status'     => 'running',
            'started_at' => current_time( 'mysql' ),
        ] );
    }

    private static function finalize_completed_jobs(): void {
        global $wpdb;
        $jt = $wpdb->prefix . 'webshoptigercatalogsync_jobs';
        $qt = $wpdb->prefix . 'webshoptigercatalogsync_queue';

        $running_jobs = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT job_id FROM `{$jt}` WHERE status = 'running'"
        );

        foreach ( $running_jobs as $job_id ) {
            $pending = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->prepare(
                    "SELECT COUNT(*) FROM `{$qt}` WHERE job_id = %s AND status IN ('pending','processing')",
                    $job_id
                )
            );

            if ( $pending === 0 ) {
                $actual_done = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                    $wpdb->prepare(
                        "SELECT COUNT(*) FROM `{$qt}` WHERE job_id = %s AND status IN ('done','error')",
                        $job_id
                    )
                );
                $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                    $jt,
                    [ 'status' => 'completed', 'finished_at' => current_time( 'mysql' ), 'done' => $actual_done ],
                    [ 'job_id' => $job_id ]
                );
            }
        }
    }

    // -------------------------------------------------------------------------
    // Resume a stuck job
    // -------------------------------------------------------------------------

    public static function resume_job( string $job_id ): void {
        global $wpdb;

        // Move stuck 'processing' tasks back to 'pending'
        $wpdb->update(
            self::queue_table(),
            [ 'status' => 'pending' ],
            [ 'job_id' => $job_id, 'status' => 'processing' ]
        );

        // Schedule a new batch right away
        self::schedule_next_batch();
    }

    public static function cancel_job( string $job_id ): void {
        global $wpdb;
        $wpdb->update( self::jobs_table(),
            [ 'status' => 'cancelled', 'finished_at' => current_time( 'mysql' ) ],
            [ 'job_id' => $job_id ]
        );
        $wpdb->update( self::queue_table(),
            [ 'status' => 'cancelled' ],
            [ 'job_id' => $job_id, 'status' => 'pending' ]
        );
    }

    // -------------------------------------------------------------------------
    // Fetch status for AJAX polling
    // -------------------------------------------------------------------------

    public static function get_active_jobs(): array {
        global $wpdb;
        $jt = $wpdb->prefix . 'webshoptigercatalogsync_jobs';
        $st = $wpdb->prefix . 'webshoptigercatalogsync_sites';
        $qt = $wpdb->prefix . 'webshoptigercatalogsync_queue';

        $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT j.*,
                    s.label as site_label,
                    SUM(q.status = 'done')       as synced,
                    SUM(q.status = 'error')      as failed,
                    SUM(q.status = 'pending')    as pending_count,
                    SUM(q.status = 'processing') as processing_count,
                    MAX(q.processed_at)          as last_activity
             FROM `{$jt}` j
             LEFT JOIN `{$st}` s ON s.id = j.site_id
             LEFT JOIN `{$qt}` q ON q.job_id = j.job_id
             GROUP BY j.job_id
             ORDER BY j.started_at DESC
             LIMIT 20",
            ARRAY_A
        ) ?: [];

        foreach ( $rows as &$row ) {
            $total                    = (int) $row['total'];
            $done                     = (int) $row['done'];
            $row['total']             = $total;
            $row['done']              = $done;
            $row['synced']            = (int) $row['synced'];
            $row['failed']            = (int) $row['failed'];
            $row['errors']            = (int) $row['failed'];
            $row['pending_count']     = (int) $row['pending_count'];
            $row['processing_count']  = (int) $row['processing_count'];
            $row['pct']               = $total > 0 ? (int) round( $done / $total * 100 ) : 0;
            $row['is_running']        = $row['status'] === 'running';

            $last_activity  = $row['last_activity'];
            $minutes_idle   = $last_activity
                ? ( time() - strtotime( $last_activity ) ) / 60
                : ( time() - strtotime( $row['started_at'] ) ) / 60;

            $row['minutes_idle'] = (int) $minutes_idle;
            $row['is_stuck']     = $row['status'] === 'running'
                && ( $row['processing_count'] > 0 || $row['pending_count'] > 0 )
                && $minutes_idle > 5;
        }

        return $rows;
    }

    public static function clear_finished_jobs(): void {
        global $wpdb;
        $jt = $wpdb->prefix . 'webshoptigercatalogsync_jobs';
        $qt = $wpdb->prefix . 'webshoptigercatalogsync_queue';

        $finished = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT job_id FROM `{$jt}` WHERE status IN ('completed','cancelled')"
        );
        if ( empty( $finished ) ) return;

        $placeholders = implode( ',', array_fill( 0, count( $finished ), '%s' ) );
        $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->prepare( "DELETE FROM `{$qt}` WHERE job_id IN ({$placeholders})", ...$finished ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        );
        $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->prepare( "DELETE FROM `{$jt}` WHERE job_id IN ({$placeholders})", ...$finished ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        );
    }

    // -------------------------------------------------------------------------
    // Scheduling
    // -------------------------------------------------------------------------

    private static function schedule_next_batch(): void {
        // Primary method: trigger the runner directly via HTTP (non-blocking)
        // This works without page visits and without WP-Cron delay
        self::trigger_runner_async();

        // Fallback: WP-Cron if the HTTP call fails
        $ts = wp_next_scheduled( self::PROCESS_HOOK );
        if ( $ts ) {
            wp_unschedule_event( $ts, self::PROCESS_HOOK );
        }
        wp_schedule_single_event( time() + 30, self::PROCESS_HOOK );
    }

    /**
     * Send a non-blocking HTTP POST to the REST queue runner endpoint.
     * Uses the WordPress REST API endpoint instead of a standalone file,
     * which is more reliable across different hosting configurations.
     */
    private static function trigger_runner_async(): void {
        $token = self::get_runner_token();
        $url   = rest_url( 'webshoptigercatalogsync-sync/v1/run-queue' );

        wp_remote_post( $url, [
            'timeout'   => 1,
            'blocking'  => false,
            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
            'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
            'body'      => [ 'token' => $token ],
        ] );
    }

    public static function get_runner_token(): string {
        $token = get_option( 'webshoptigercatalogsync_runner_token', '' );
        if ( empty( $token ) ) {
            $token = wp_generate_password( 32, false );
            update_option( 'webshoptigercatalogsync_runner_token', $token );
        }
        return $token;
    }

    /**
     * Runner URL for display in the admin.
     */
    public static function get_runner_url(): string {
        return rest_url( 'webshoptigercatalogsync-sync/v1/run-queue' ) . '?token=' . self::get_runner_token();
    }

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

    private static function generate_uuid(): string {
        return sprintf(
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            wp_rand( 0, 0xffff ), wp_rand( 0, 0xffff ),
            wp_rand( 0, 0xffff ),
            wp_rand( 0, 0x0fff ) | 0x4000,
            wp_rand( 0, 0x3fff ) | 0x8000,
            wp_rand( 0, 0xffff ), wp_rand( 0, 0xffff ), wp_rand( 0, 0xffff )
        );
    }
}
