<?php
/**
 * Cron Scheduler Service
 *
 * Handles WordPress cron job registration and execution for plugin features.
 *
 * @package AIPT\Core\Services
 * @since 2.4.0
 */

namespace AIPT\Core\Services;

use AIPT\Api\AutomationJobs\AutomationJobManager;
use AIPT\Api\AutomationJobs\JobLogger;
use AIPT\Api\AutomationJobs\JobScheduler;

/**
 * Manages WP-Cron schedules for plugin
 *
 * @internal Used by Init
 */
class CronScheduler {

	/**
	 * Init instance
	 *
	 * @var \AIPT\Core\Init
	 */
	private $init;

	/**
	 * Constructor
	 *
	 * @param \AIPT\Core\Init $init Init instance.
	 */
	public function __construct( \AIPT\Core\Init $init ) {
		$this->init = $init;
	}

	/**
	 * Add automation jobs cron intervals
	 *
	 * @param array $schedules Existing schedules.
	 * @return array Modified schedules.
	 */
	public function addAutomationIntervals( array $schedules ): array {

		if ( ! isset( $schedules['aipt_two_minutes'] ) ) {
			$schedules['aipt_two_minutes'] = array(
				'interval' => 120,
				'display'  => __( 'Every 2 Minutes', 'ai-product-tools' ),
			);
		}

		if ( ! isset( $schedules['aipt_five_minutes'] ) ) {
			$schedules['aipt_five_minutes'] = array(
				'interval' => 300,
				'display'  => __( 'Every 5 Minutes', 'ai-product-tools' ),
			);
		}

		return $schedules;
	}

	/**
	 * Prevent the could_not_set noise from no-op reschedule races (@since 2.6.2).
	 *
	 * Hooks into pre_reschedule_event. When WP core's wp_cron() auto-reschedules
	 * one of our recurring hooks and the next scheduled time computed by core
	 * lands on the exact timestamp that already holds an entry, the subsequent
	 * update_option('cron', ...) call returns false (no-op detection) and WP
	 * core emits a could_not_set WP_Error that wp_cron() writes to error_log.
	 * That noise is what surfaces in customer site monitors as "the plugin
	 * caused this error". See https://core.trac.wordpress.org/ticket/57271.
	 *
	 * Returns `true` (success short-circuit) ONLY when both apply:
	 *   - The hook is one of our two recurring automation hooks (foreign hooks
	 *     including action_scheduler_run_queue are intentionally not in scope).
	 *   - wp_next_scheduled() reports a collision within 1s of the timestamp
	 *     WP core would target ($event->timestamp + $event->interval). The 1s
	 *     tolerance covers clock skew between concurrent wp-cron.php hits.
	 *
	 * All other cases return null so WP core's normal reschedule path runs.
	 *
	 * @param null|bool|\WP_Error $pre   Prior filter result. If non-null, passthrough.
	 * @param object              $event Event object (hook, timestamp, interval, args, schedule).
	 * @return null|bool|\WP_Error       `true` to short-circuit, null to let WP proceed.
	 */
	public function preventNoopRescheduleRace( $pre, $event ) {

		if ( null !== $pre ) {
			return $pre;
		}

		if ( ! is_object( $event ) || empty( $event->hook ) || empty( $event->interval ) ) {
			return null;
		}

		if ( ! $this->isOwnedCronHook( (string) $event->hook ) ) {
			return null;
		}

		$args = isset( $event->args ) && is_array( $event->args ) ? $event->args : array();

		$now       = (int) time();
		$timestamp = (int) $event->timestamp;
		$interval  = (int) $event->interval;
		if ( $timestamp >= $now ) {
			$target_ts = $now + $interval;
		} else {
			$target_ts = $now + ( $interval - ( ( $now - $timestamp ) % $interval ) );
		}

		$existing = wp_next_scheduled( $event->hook, $args );
		if ( false === $existing ) {

			return null;
		}

		if ( abs( (int) $existing - $target_ts ) <= 1 ) {
			return true;
		}

		return null;
	}

	/**
	 * Check whether the given hook is one of ours subject to the reschedule guard.
	 *
	 * Hardcoded list of exactly two recurring hooks. Single-event hooks
	 * (aipt_execute_job_{id}) are NOT in scope because pre_reschedule_event only
	 * fires for recurring events; their duplicate-scheduling race is fixed via
	 * wp_next_scheduled guards inside JobScheduler::processPendingJobs.
	 *
	 * @since 2.6.2
	 *
	 * @param string $hook Hook name.
	 * @return bool        True if this filter is authorized to intervene.
	 */
	private function isOwnedCronHook( string $hook ): bool {
		return 'aipt_process_automation_jobs' === $hook
			|| 'aipt_monitor_automation_jobs' === $hook;
	}

	/**
	 * Process automation jobs cron callback
	 */
	public function processAutomationJobs(): void {
		try {

			if ( ! class_exists( '\AIPT\Api\AutomationJobs\AutomationJobManager' ) ) {
				if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				}
				return;
			}

			$job_manager = new AutomationJobManager();
			$logger      = new JobLogger();
			$scheduler   = new JobScheduler( $job_manager, $logger );

			$processed = $scheduler->processPendingJobs();

		} catch ( \Exception $e ) {

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			}
		}
	}

	/**
	 * Monitor automation jobs cron callback
	 */
	public function monitorAutomationJobs(): void {
		try {

			if ( ! class_exists( '\AIPT\Api\AutomationJobs\AutomationJobManager' ) ) {
				if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				}
				return;
			}

			$job_manager = new AutomationJobManager();
			$logger      = new JobLogger();
			$scheduler   = new JobScheduler( $job_manager, $logger );

			$monitored = $scheduler->monitorRunningJobs();

		} catch ( \Exception $e ) {

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			}
		}
	}

	/**
	 * Setup automation jobs cron schedules
	 */
	public function setupAutomationSchedules(): void {
		try {
			JobScheduler::setupCronSchedules();
		} catch ( \Exception $e ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			}
		}
	}
}
