<?php
/**
 * Cron wiring: daily snapshot, optional weekly re-scan, daily digest.
 *
 * @package Upgrade_Pilot
 */

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

/**
 * Class UPilot_Cron
 */
class UPilot_Cron {

	/**
	 * Scanner.
	 *
	 * @var UPilot_Scanner
	 */
	private $scanner;

	/**
	 * Snapshot engine.
	 *
	 * @var UPilot_Snapshot
	 */
	private $snapshot;

	/**
	 * Alerts.
	 *
	 * @var UPilot_Alerts
	 */
	private $alerts;

	/**
	 * Constructor.
	 *
	 * @param UPilot_Scanner  $scanner  Scanner.
	 * @param UPilot_Snapshot $snapshot Snapshot engine.
	 * @param UPilot_Alerts   $alerts   Alerts.
	 */
	public function __construct( UPilot_Scanner $scanner, UPilot_Snapshot $snapshot, UPilot_Alerts $alerts ) {
		$this->scanner  = $scanner;
		$this->snapshot = $snapshot;
		$this->alerts   = $alerts;

		add_action( 'init', array( $this, 'ensure_schedules' ) );
		add_action( 'upilot_daily_snapshot', array( $this, 'run_daily_snapshot' ) );
		add_action( 'upilot_weekly_rescan', array( $this, 'run_weekly_rescan' ) );
	}

	/**
	 * Make sure recurring events exist (exactly once each).
	 *
	 * MULTISITE: WP-Cron is per-site — each subsite keeps its own cron array. A
	 * network-activated plugin that schedules naively therefore ends up with one
	 * copy of every job on every subsite, and a 500-subsite network would run 500
	 * identical daily snapshots over one shared dataset. Since all of Upgrade
	 * Pilot's data is network-scoped, the work only needs doing once: scheduled
	 * jobs live on the MAIN SITE only, and every other subsite actively clears any
	 * stray events it is carrying.
	 */
	public function ensure_schedules() {
		if ( ! UPilot_Store::is_cron_site() ) {
			// Not the cron site. Drop any duplicate jobs (a network upgraded from an
			// earlier build may be carrying them), then stand down.
			foreach ( array( 'upilot_daily_snapshot', 'upilot_daily_digest', 'upilot_weekly_rescan' ) as $hook ) {
				if ( wp_next_scheduled( $hook ) ) {
					wp_clear_scheduled_hook( $hook );
				}
			}
			$this->heal_stale_network_snapshot();
			return;
		}

		if ( ! wp_next_scheduled( 'upilot_daily_snapshot' ) ) {
			// Overnight site-local time, with jitter so fleets don't stampede wp.org.
			wp_schedule_event( strtotime( 'tomorrow 3:00' ) + wp_rand( 0, 2 * HOUR_IN_SECONDS ), 'daily', 'upilot_daily_snapshot' );
		}
		if ( ! wp_next_scheduled( 'upilot_daily_digest' ) ) {
			wp_schedule_event( strtotime( 'tomorrow 8:00' ) + wp_rand( 0, HOUR_IN_SECONDS ), 'daily', 'upilot_daily_digest' );
		}

		$settings = UPilot_Store::get_option( 'upilot_settings', array() );
		$weekly   = ! empty( $settings['weekly_rescan'] );
		$next     = wp_next_scheduled( 'upilot_weekly_rescan' );
		if ( $weekly && ! $next ) {
			wp_schedule_event( strtotime( 'next sunday 4:00' ), 'weekly', 'upilot_weekly_rescan' );
		} elseif ( ! $weekly && $next ) {
			wp_unschedule_event( $next, 'upilot_weekly_rescan' );
		}

		// Self-heal a broken tick chain: a scan stuck in 'running' with no tick
		// scheduled (deactivate mid-scan, crashed worker after lock takeover)
		// would otherwise never finish.
		$scan_state = $this->scanner->get_state();
		if ( ! empty( $scan_state['status'] ) && 'running' === $scan_state['status'] && ! wp_next_scheduled( 'upilot_queue_tick' ) ) {
			wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'upilot_queue_tick' );
		}
	}

	/**
	 * Safety net for a network whose main site gets no traffic.
	 *
	 * WP-Cron only fires on page loads. If every visitor lands on subsites and
	 * nobody ever loads the main site, the main site's cron never runs and the
	 * daily snapshot silently stops. Any subsite that notices the snapshot is more
	 * than two days stale schedules a one-off catch-up ON THE MAIN SITE — cron
	 * arrays are per-site options, so switching blogs is what puts the job into the
	 * right queue.
	 */
	private function heal_stale_network_snapshot() {
		$last = (int) UPilot_Store::get_option( 'upilot_last_snapshot_run', 0 );
		if ( $last > 0 && ( time() - $last ) < 2 * DAY_IN_SECONDS ) {
			return;
		}

		switch_to_blog( get_main_site_id() );
		if ( ! wp_next_scheduled( 'upilot_daily_snapshot' ) ) {
			wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'upilot_daily_snapshot' );
		}
		restore_current_blog();
	}

	/**
	 * Daily: provenance snapshot + refresh the cached report.
	 *
	 * $fresh = true is deliberate. The 24h transient cache exists to keep RENDER
	 * paths off the network; this job IS the refresh, so it bypasses the cache and
	 * re-reads WordPress.org. Left cached, the snapshot would frequently diff a
	 * payload against itself and detect an ownership change up to two days late.
	 * With batching, "fresh" costs one HTTP request for the whole site.
	 */
	public function run_daily_snapshot() {
		$this->snapshot->run( true );
		UPilot_Store::delete_option( 'upilot_last_report' ); // Next dashboard view reassembles.
	}

	/**
	 * Weekly: kick a full re-scan. One weekly schedule ships in this plugin,
	 * on by default and toggleable in Settings.
	 */
	public function run_weekly_rescan() {
		$state = $this->scanner->get_state();
		if ( empty( $state['status'] ) || 'running' !== $state['status'] ) {
			$this->scanner->start_scan();
		}
	}
}
