<?php

namespace AIPT\Api\AutomationJobs;

use AIPT\Api\AutomationJobs\Interfaces\JobSchedulerInterface;
use AIPT\Api\AutomationJobs\Interfaces\JobLoggerInterface;
use AIPT\Api\AutomationJobs\Models\AutomationJob;
use AIPT\Api\Constants\AutomationConstants;

/**
 * Job Scheduler
 *
 * Handles job scheduling using WordPress cron system
 * Following Single Responsibility Principle
 *
 * @since 2.3.0
 */
class JobScheduler implements JobSchedulerInterface {

    /**
     * Job manager instance
     * @var AutomationJobManager
     */
    private $job_manager;

    /**
     * Logger instance
     * @var JobLoggerInterface
     */
    private $logger;

    const EXECUTION_EVENT_GRACE = AutomationConstants::EXECUTION_EVENT_GRACE_PERIOD;

    /**
     * Constructor
     *
     * @param AutomationJobManager $job_manager Job manager instance
     * @param JobLoggerInterface $logger Logger instance
     */
    public function __construct(AutomationJobManager $job_manager, JobLoggerInterface $logger) {
        $this->job_manager = $job_manager;
        $this->logger = $logger;
    }

    /**
     * Schedule a job for execution
     *
     * @param AutomationJob $job Job to schedule
     * @return bool Success status
     * @throws \Exception On scheduling failure
     */
    public function scheduleJob(AutomationJob $job) {
        try {
            $next_run = $this->calculateNextRun($job);

            if ($next_run) {
                $job->next_run = $next_run;
                $this->job_manager->updateJob($job);

                $timestamp = strtotime($next_run);
                $hook = 'aipt_execute_job_' . $job->id;

                \AIPT\Api\AutomationJobs\JobExecutionHandler::register_job_hook($job->id);

                wp_clear_scheduled_hook($hook, [$job->id]);

                $scheduled = wp_schedule_single_event($timestamp, $hook, [$job->id], true);

                if ($scheduled === false || is_wp_error($scheduled)) {
                    $error_detail = is_wp_error($scheduled) ? $scheduled->get_error_message() : 'unknown';
                    throw new \Exception('Failed to schedule WordPress cron event: ' . $error_detail);
                }

                $this->logger->info($job->id, 'Job scheduled for execution', [
                    'next_run' => $next_run,
                    'schedule_type' => $job->schedule_config['type'] ?? 'unknown'
                ]);

                return true;
            }

            return false;

        } catch (\Exception $e) {
            $this->logger->error($job->id, 'Failed to schedule job', [
                'error' => $e->getMessage()
            ]);
            throw $e;
        }
    }

    /**
     * Unschedule a job
     *
     * @param int $job_id Job ID to unschedule
     * @return bool Success status
     * @throws \Exception On unscheduling failure
     */
    public function unscheduleJob($job_id) {
        try {
            $hook = 'aipt_execute_job_' . $job_id;
            wp_clear_scheduled_hook($hook, [$job_id]);

            $this->logger->info($job_id, 'Job unscheduled');
            return true;

        } catch (\Exception $e) {
            $this->logger->error($job_id, 'Failed to unschedule job', [
                'error' => $e->getMessage()
            ]);
            throw $e;
        }
    }

    /**
     * Reschedule a job with new configuration
     *
     * @param AutomationJob $job Job to reschedule
     * @return bool Success status
     * @throws \Exception On rescheduling failure
     */
    public function rescheduleJob(AutomationJob $job) {

        $this->unscheduleJob($job->id);

        return $this->scheduleJob($job);
    }

    /**
     * Process pending jobs
     *
     * @return int Number of jobs processed
     */
    public function processPendingJobs() {
        $processed_count = 0;

        try {

            $this->buildPoolsForPreparingJobs(AutomationConstants::MAX_PREPARING_JOBS_TO_BUILD, AutomationConstants::MAX_POOL_BUILD_CHUNKS);

            $running_jobs = $this->getRunningJobsCount();
            if ($running_jobs >= AutomationConstants::MAX_CONCURRENT_JOBS) {
                return 0;
            }

            $available_slots = AutomationConstants::MAX_CONCURRENT_JOBS - $running_jobs;
            $jobs = $this->getJobsReadyForExecution($available_slots);

            foreach ($jobs as $job) {
                try {

                    \AIPT\Api\AutomationJobs\JobExecutionHandler::register_job_hook($job->id);

                    $hook = 'aipt_execute_job_' . $job->id;

                    $existing_schedule = wp_next_scheduled( $hook, [$job->id] );
                    if ( false !== $existing_schedule ) {
                        $this->logger->info($job->id, 'Job already scheduled - skipping duplicate enqueue', [
                            'hook' => $hook,
                            'existing_run_at' => gmdate('Y-m-d H:i:s', (int) $existing_schedule),
                        ]);

                        $processed_count++;
                        continue;
                    }

                    $scheduled = wp_schedule_single_event(time() + AutomationConstants::IMMEDIATE_EXECUTION_DELAY, $hook, [$job->id], true);

                    if ($scheduled === false || is_wp_error($scheduled)) {
                        $error_detail = is_wp_error($scheduled) ? $scheduled->get_error_message() : 'unknown';
                        $this->logger->error($job->id, 'Failed to queue job for immediate execution', [
                            'hook' => $hook,
                            'wp_error' => $error_detail,
                        ]);
                    } else {
                        $this->logger->info($job->id, 'Job queued for immediate execution', [
                            'hook' => $hook,
                            'scheduled_time' => gmdate('Y-m-d H:i:s', time() + AutomationConstants::IMMEDIATE_EXECUTION_DELAY)
                        ]);
                    }

                    $processed_count++;

                } catch (\Exception $e) {
                    $this->logger->error($job->id, 'Failed to queue job for execution', [
                        'error' => $e->getMessage()
                    ]);
                }
            }

        } catch (\Exception $e) {

        }

        return $processed_count;
    }

    /**
     * Monitor running jobs for timeouts and issues
     *
     * @return int Number of jobs monitored
     */
    public function monitorRunningJobs() {
        $monitored_count = 0;

            try {
                $reclaimed = (new \AIPT\Api\AutomationJobs\ClaimBasedQueue())->releaseStaleClaims(AutomationConstants::STALE_CLAIM_MINUTES);
                if ($reclaimed > 0) {
                }
            } catch (\Throwable $t) {
            }

        try {
            $running_jobs = $this->job_manager->getJobs(['status' => 'running']);

            foreach ($running_jobs as $job) {
                $monitored_count++;

                if ($this->isJobTimedOut($job)) {
                    $snapshot = $this->getJobProductSnapshot((int) $job->id);
                    $this->logger->warning($job->id, 'Job timeout detected', array_merge($snapshot, [
                        'started_at' => $job->started_at,
                        'timeout_seconds' => AutomationConstants::JOB_TIMEOUT,
                    ]));
                    $this->handleJobTimeout($job->id);
                }

                $this->updateJobProgressIfNeeded($job);

                $snapshot = $this->getJobProductSnapshot((int) $job->id);
                $this->ensureJobExecutionScheduled($job, $snapshot);
            }

        } catch (\Exception $e) {

        }

        return $monitored_count;
    }

    /**
     * Calculate next run time for a job
     *
     * @param AutomationJob $job Job to calculate next run for
     * @return string|null Next run datetime or null if one-time job
     */
    public function calculateNextRun(AutomationJob $job) {
        $schedule_config = $job->schedule_config;

        if (empty($schedule_config['type'])) {
            return null;
        }

        $base_time = time();

        switch ($schedule_config['type']) {
            case 'once':

                $runDate = $schedule_config['runDate'] ?? null;

                if (isset($schedule_config['runDate'])) {

                    $runDate = $schedule_config['runDate'];
                    if (strpos($runDate, 'T') !== false) {

                        $runDate = str_replace('T', ' ', $runDate) . ':00';
                    }
                    return $runDate;
                } else {
                    $fallback = gmdate('Y-m-d H:i:s', $base_time + 60);
                    return $fallback;
                }

            case 'daily':
                $time = $schedule_config['time'] ?? '00:00';
                $next_run = gmdate('Y-m-d', $base_time + 86400) . ' ' . $time . ':00';
                return $next_run;

            case 'weekly':
                $day = $schedule_config['day'] ?? 'monday';
                $time = $schedule_config['time'] ?? '00:00';
                $next_run = gmdate('Y-m-d', strtotime("next {$day}", $base_time)) . ' ' . $time . ':00';
                return $next_run;

            case 'monthly':
                $day = $schedule_config['day'] ?? 1;
                $time = $schedule_config['time'] ?? '00:00';
                $next_month = gmdate('Y-m', strtotime('+1 month', $base_time));
                $next_run = $next_month . '-' . str_pad($day, 2, '0', STR_PAD_LEFT) . ' ' . $time . ':00';
                return $next_run;

            default:
                return null;
        }
    }

    /**
     * Check if job should run now
     *
     * @param AutomationJob $job Job to check
     * @return bool Whether job should run
     */
    public function shouldJobRun(AutomationJob $job) {
        if ($job->status !== 'ready') {
            return false;
        }

        if (empty($job->next_run)) {
            return false;
        }

        return strtotime($job->next_run) <= time();
    }

    /**
     * Get jobs ready for execution
     *
     * @param int $limit Maximum number of jobs to return
     * @return array Jobs ready for execution
     */
    public function getJobsReadyForExecution($limit = 10) {
        global $wpdb;

        $jobs_table = $wpdb->prefix . 'aipt_automation_jobs';

        $results = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$jobs_table}
                WHERE status = 'ready'
                AND (next_run IS NULL OR next_run <= %s)
                ORDER BY created_at ASC
                LIMIT %d",
                current_time('mysql'),
                $limit
            ),
            ARRAY_A
        );

        $jobs = [];
        foreach ($results as $result) {
            $jobs[] = new AutomationJob($result);
        }
        return $jobs;
    }
    /**
     * Build pools for jobs in 'preparing' status.
     *
     * @param int $max_jobs   Max number of preparing jobs to advance
     * @param int $max_chunks Max chunks per job to build in this tick
     */
    private function buildPoolsForPreparingJobs($max_jobs = AutomationConstants::MAX_PREPARING_JOBS_TO_BUILD, $max_chunks = AutomationConstants::MAX_POOL_BUILD_CHUNKS) {
        try {
            $preparing_jobs = $this->job_manager->getJobs(['status' => 'preparing'], $max_jobs, 0, 'created_at', 'ASC');
            if (empty($preparing_jobs)) {
                return;
            }

            global $wpdb;
            $jobs_table = $wpdb->prefix . 'aipt_automation_jobs';
            $job_products_table = $wpdb->prefix . 'aipt_job_products';

            foreach ($preparing_jobs as $job) {
                try {
                    $pool_builder = new \AIPT\Api\AutomationJobs\PoolBuilder();
                    $result = $pool_builder->buildPool((int)$job->id, $job->product_filter ?? [], AutomationConstants::POOL_BUILD_CHUNK_SIZE, $max_chunks);

                    $current_total = (int) $wpdb->get_var(
                        $wpdb->prepare("SELECT COUNT(*) FROM {$job_products_table} WHERE job_id = %d", $job->id)
                    );

                    $wpdb->update(
                        $jobs_table,
                        [
                            'total_products' => $current_total,
                            'updated_at' => current_time('mysql'),
                        ],
                        ['id' => $job->id],
                        ['%d','%s'],
                        ['%d']
                    );

                    $target_total = isset($result['total']) ? (int)$result['total'] : $current_total;

                    if ($current_total >= $target_total) {
                        if ($current_total === 0) {

                            $this->job_manager->updateJobStatus($job->id, 'completed');
                            $this->logger->info($job->id, 'Job completed immediately (no matching products)');
                        } else {

                            $this->job_manager->updateJobStatus($job->id, 'ready');
                            $this->logger->info($job->id, 'Job pool prepared and marked as ready', [
                                'total_products' => $current_total,
                            ]);
                        }
                    } else {
                        $this->logger->info($job->id, 'Pool building in progress', [
                            'built_so_far' => $current_total,
                            'target_total' => $target_total,
                        ]);
                    }
                } catch (\Exception $e) {
                    $this->logger->error($job->id, 'Pool building error', ['error' => $e->getMessage()]);
                }
            }
        } catch (\Exception $e) {
        }
    }

    /**
     * Handle job timeout
     *
     * @param int $job_id Job ID that timed out
     * @return bool Success status
     */
    public function handleJobTimeout($job_id) {
        try {
            $this->job_manager->updateJobStatus($job_id, 'failed', 'Job timed out');
            $this->logger->error($job_id, 'Job timed out and was marked as failed');

            $released = (new \AIPT\Api\AutomationJobs\ClaimBasedQueue())->forceReleaseAllProcessing((int) $job_id);
            $postState = $this->getJobProductSnapshot((int) $job_id);
            $cleanupContext = array_merge($postState, [
                'released_processing' => (int) $released,
            ]);
            if ($this->shouldLogSnapshot((int) $job_id, 'timeout_cleanup', $cleanupContext, 600)) {
                $this->logger->debug($job_id, 'Timeout cleanup executed', $cleanupContext);
            }

            $this->unscheduleJob($job_id);

            return true;

        } catch (\Exception $e) {
            $this->logger->error($job_id, 'Failed to handle job timeout', [
                'error' => $e->getMessage()
            ]);
            return false;
        }
    }

    /**
     * Get count of running jobs
     *
     * @return int Number of running jobs
     */
    private function getRunningJobsCount() {
        return $this->job_manager->getJobCount(['status' => 'running']);
    }

    /**
     * Check if job is timed out
     *
     * @param AutomationJob $job Job to check
     * @return bool Whether job is timed out
     */
    private function isJobTimedOut(AutomationJob $job) {
        $lastActivity = $this->getLastActivityTimestamp($job);
        if ($lastActivity === null) {
            return false;
        }

        return (time() - $lastActivity) > AutomationConstants::JOB_TIMEOUT;
    }

    /**
     * Update job progress if needed (throttled via last_progress_logged_at)
     *
     * @param AutomationJob $job Job to update
     */
    private function updateJobProgressIfNeeded(AutomationJob $job) {
        global $wpdb;
        $jobs_table = $wpdb->prefix . 'aipt_automation_jobs';

        $started_ts = $this->convertDatetimeToTimestamp($job->started_at);
        if ($started_ts === null) {
            return; 
        }

        $now_ts = time();
        $elapsed_time = max(0, $now_ts - $started_ts);

        $logThrottleKey = 'aipt_job_monitor_log_' . (int) $job->id;
        $lastLoggedTs = null;
        if (function_exists('get_transient')) {
            $cached = get_transient($logThrottleKey);
            if (is_numeric($cached)) {
                $lastLoggedTs = (int) $cached;
            }
        }

        $should_log = false;

        if ($lastLoggedTs === null) {

            $should_log = true;
        } elseif (($now_ts - $lastLoggedTs) >= AutomationConstants::LOG_THROTTLE_CADENCE_SECONDS) {
            $should_log = true;
        }

        if ($should_log) {

            $last_progress = $wpdb->get_var(
                $wpdb->prepare("SELECT last_progress_logged_at FROM {$jobs_table} WHERE id = %d", $job->id)
            );

            $snapshot = $this->getJobProductSnapshot((int) $job->id);
            $lastActivity = $this->getLastActivityTimestamp($job);
            $sinceLastProgress = null;
            if (!empty($last_progress)) {
                $last_ts = $this->convertDatetimeToTimestamp($last_progress);
                if ($last_ts !== null) {
                    $sinceLastProgress = max(0, $now_ts - $last_ts);
                }
            } else {
                $sinceLastProgress = max(0, $now_ts - $started_ts);
            }

            $logContext = array_merge([
                'elapsed_time' => $elapsed_time,
                'progress' => $job->progress,
                'seconds_since_last_progress' => $sinceLastProgress,
                'last_activity_at' => $lastActivity ? \wp_date('c', $lastActivity) : null,
            ], $snapshot);

            if ($this->shouldLogSnapshot((int) $job->id, 'job_still_running', $logContext, AutomationConstants::SNAPSHOT_LOG_TTL)) {
                $this->logger->info($job->id, 'Job still running', $logContext);
            }

            if (function_exists('set_transient')) {
                set_transient($logThrottleKey, $now_ts, AutomationConstants::LOG_THROTTLE_CADENCE_SECONDS);
            }
        }
    }

    /**
     * Snapshot queue state for diagnostics/logging.
     *
     * @param int $job_id
     * @return array<string,mixed>
     */
    private function getJobProductSnapshot(int $job_id): array {
        global $wpdb;
        $table = $wpdb->prefix . 'aipt_job_products';

        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT
                    SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending,
                    SUM(CASE WHEN status = 'processing' THEN 1 ELSE 0 END) AS processing,
                    SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
                    SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
                    MIN(CASE WHEN status = 'processing' THEN processing_started_at END) AS oldest_processing_started_at
                FROM {$table}
                WHERE job_id = %d",
                $job_id
            ),
            ARRAY_A
        );

        $snapshot = [
            'pending' => 0,
            'processing' => 0,
            'completed' => 0,
            'failed' => 0,
        ];

        if (is_array($row)) {
            $snapshot['pending'] = (int) ($row['pending'] ?? 0);
            $snapshot['processing'] = (int) ($row['processing'] ?? 0);
            $snapshot['completed'] = (int) ($row['completed'] ?? 0);
            $snapshot['failed'] = (int) ($row['failed'] ?? 0);
            $snapshot['total_rows'] = array_sum($snapshot);

            if (!empty($row['oldest_processing_started_at'])) {
                $ts = strtotime($row['oldest_processing_started_at']);
                if ($ts !== false) {
                    $snapshot['oldest_processing_started_at'] = $row['oldest_processing_started_at'];
                    $snapshot['oldest_processing_age_sec'] = max(0, time() - $ts);
                }
            }
        } else {
            $snapshot['total_rows'] = 0;
        }

        return $snapshot;
    }

    /**
     * Prevent duplicate log noise by caching snapshot hashes.
     */
    private function shouldLogSnapshot(int $job_id, string $slug, array $context, int $ttl = AutomationConstants::SNAPSHOT_LOG_TTL): bool {
        if (!function_exists('get_transient') || !function_exists('set_transient')) {
            return true;
        }

        try {
            ksort($context);
        } catch (\Throwable $t) {

        }

        $hash = md5(wp_json_encode($context));
        $key = 'aipt_joblog_' . $slug . '_' . $job_id;
        $stored = get_transient($key);
        if ($stored === $hash) {
            return false;
        }

        set_transient($key, $hash, $ttl);
        return true;
    }

    /**
     * Ensure there is an upcoming execution event for a running job.
     *
     * Acts as a safety net when WP-Cron misses/loses the scheduled single event,
     * avoiding "stuck" jobs that never schedule the next tick.
     *
     * @param AutomationJob         $job
     * @param array<string, mixed>  $snapshot
     */
    private function ensureJobExecutionScheduled(AutomationJob $job, array $snapshot): void {
        if (!function_exists('wp_next_scheduled') || !function_exists('wp_schedule_single_event')) {
            return;
        }

        $remaining = (int) ($snapshot['pending'] ?? 0) + (int) ($snapshot['processing'] ?? 0);
        if ($remaining <= 0) {
            return;
        }

        $job_id = (int) $job->id;

        $lockKey  = 'aipt_job_lock_' . $job_id;
        $lock_raw = get_option($lockKey);
        if ($lock_raw) {
            $lock_data = is_string($lock_raw) ? json_decode($lock_raw, true) : $lock_raw;
            if (is_array($lock_data) && isset($lock_data['expires']) && time() <= (int) $lock_data['expires']) {
                return;
            }
        }

        $hook = 'aipt_execute_job_' . $job_id;
        $args = [$job_id];
        $now = time();
        $next = wp_next_scheduled($hook, $args);

        $reason = null;
        if ($next === false) {
            $reason = 'missing_event';
        } elseif ($next <= ($now - self::EXECUTION_EVENT_GRACE)) {
            $reason = 'event_overdue';
        }

        if ($reason === null) {
            return;
        }

        \AIPT\Api\AutomationJobs\JobExecutionHandler::register_job_hook($job_id);

        if ($next !== false && function_exists('wp_unschedule_event')) {
            wp_unschedule_event($next, $hook, $args);
        }

        $scheduled_time = $now + AutomationConstants::rescheduleDelay();
        $scheduled = wp_schedule_single_event($scheduled_time, $hook, $args, true);

        $schedule_failed = ($scheduled === false) || is_wp_error($scheduled);
        $wp_error_msg    = is_wp_error($scheduled) ? $scheduled->get_error_message() : null;

        $logContext = array_merge($snapshot, [
            'reason' => $reason,
            'previous_event_at' => $next ? gmdate('c', (int) $next) : null,
            'rescheduled_for' => gmdate('c', $scheduled_time),
            'schedule_result' => $schedule_failed ? 'failed' : 'scheduled',
            'wp_error' => $wp_error_msg,
            'remaining_items' => $remaining,
        ]);

        if ($schedule_failed) {
            $this->logger->error($job_id, 'Job monitor failed to reschedule execution event', $logContext);
        } else {
            $this->logger->warning($job_id, 'Job monitor rescheduled missing execution event', $logContext);
        }
    }

    /**
     * Determine the most recent activity timestamp for a running job.
     *
     * @param AutomationJob $job
     * @return int|null
     */
    private function getLastActivityTimestamp(AutomationJob $job): ?int {
        $candidates = [];

        $started = $this->convertDatetimeToTimestamp($job->started_at);
        if ($started !== null) {
            $candidates[] = $started;
        }

        if (!empty($job->updated_at)) {
            $updated = $this->convertDatetimeToTimestamp($job->updated_at);
            if ($updated !== null) {
                $candidates[] = $updated;
            }
        }

        global $wpdb;
        $jobs_table = $wpdb->prefix . 'aipt_automation_jobs';

        $last_progress = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT last_progress_logged_at FROM {$jobs_table} WHERE id = %d",
                (int) $job->id
            )
        );

        if (!empty($last_progress)) {
            $progress_ts = $this->convertDatetimeToTimestamp($last_progress);
            if ($progress_ts !== null) {
                $candidates[] = $progress_ts;
            }
        }

        if (empty($candidates)) {
            return null;
        }

        return max($candidates);
    }

    /**
     * Convert a WordPress-stored datetime (site timezone) to a Unix timestamp.
     *
     * @param string|null $datetime Datetime string in Y-m-d H:i:s format.
     * @return int|null Timestamp or null on failure.
     */
    private function convertDatetimeToTimestamp($datetime) {
        if (empty($datetime)) {
            return null;
        }

        try {
            $timezone = wp_timezone();
            $date = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $datetime, $timezone);
            if ($date instanceof \DateTimeImmutable) {
                return $date->getTimestamp();
            }
        } catch (\Throwable $e) {

        }

        $fallback = strtotime($datetime);
        return $fallback !== false ? $fallback : null;
    }

    /**
     * Setup cron schedules
     *
     * Called during plugin initialization
     */
    public static function setupCronSchedules() {

        if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
            return;
        }

        if (!wp_next_scheduled('aipt_process_automation_jobs')) {
            $scheduled = wp_schedule_event(time() + 60, 'aipt_five_minutes', 'aipt_process_automation_jobs', [], true);
            if ($scheduled === false || is_wp_error($scheduled)) {
                $detail = is_wp_error($scheduled) ? $scheduled->get_error_message() : 'unknown';
            } else {
            }
        }

        if (!wp_next_scheduled('aipt_monitor_automation_jobs')) {
            $scheduled = wp_schedule_event(time() + 30, 'aipt_two_minutes', 'aipt_monitor_automation_jobs', [], true);
            if ($scheduled === false || is_wp_error($scheduled)) {
                $detail = is_wp_error($scheduled) ? $scheduled->get_error_message() : 'unknown';
            } else {
            }
        }
    }

    /**
     * Clear cron schedules
     *
     * Called during plugin deactivation
     */
    public static function clearCronSchedules() {
        wp_clear_scheduled_hook('aipt_process_automation_jobs');
        wp_clear_scheduled_hook('aipt_monitor_automation_jobs');

        global $wpdb;
        $jobs_table = $wpdb->prefix . 'aipt_automation_jobs';

        $job_ids = $wpdb->get_col("SELECT id FROM {$jobs_table}");

        foreach ($job_ids as $job_id) {
            wp_clear_scheduled_hook('aipt_execute_job_' . $job_id, [$job_id]);
        }
    }
}
