<?php

declare(strict_types=1);

namespace SiteFort\Modules\Scanner\Queue;

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

/**
 * Detects a non-responding scheduled worker.
 *
 * Observation, not diagnosis: an overdue fallback event with no recent
 * background run means background processing is not happening, whatever
 * the cause. Short-TTL flag; self-clears when any of our hooks run in a
 * real cron or loopback worker context.
 */
class WorkerLiveness {

	private const TRIP_FLAG = 'sitefort_worker_not_responding';

	private const GRACE_SECONDS = 90;
	private const MISS_THRESHOLD = 3;
	private const TRIP_TTL = 6 * HOUR_IN_SECONDS;
	private const MISS_TTL = HOUR_IN_SECONDS;
	private const BG_ACTIVITY_TTL = 15 * MINUTE_IN_SECONDS;

	/**
	 * Count a miss when the fallback event is overdue past the grace window
	 * and no background worker ran recently. last_ping is not consulted here:
	 * inline fallback refreshes it, masking exactly this state.
	 *
	 * @param string $scan_id
	 * @return void
	 */
	public static function observe( string $scan_id ): void {
		if ( '' === $scan_id || ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ) ) {
			return;
		}

		if ( self::is_tripped() ) {
			return;
		}

		$now = time();

		$next = wp_next_scheduled( 'sitefort_scan_worker_fallback', [ $scan_id ] );
		if ( ! $next || ( $now - (int) $next ) < self::GRACE_SECONDS ) {
			return;
		}

		$last_bg = (int) get_transient( self::bg_activity_key() );
		if ( $last_bg > 0 && ( $now - $last_bg ) < self::GRACE_SECONDS ) {
			return;
		}

		$state = get_transient( self::miss_key( $scan_id ) );
		if ( ! is_array( $state ) ) {
			$state = [
				'count'        => 0,
				'last_miss_at' => 0,
			];
		}

		// Background ran since the last miss; restart the streak.
		if ( $last_bg > (int) $state['last_miss_at'] ) {
			$state = [
				'count'        => 0,
				'last_miss_at' => 0,
			];
		}

		// Polls land every few seconds; space misses by the grace window.
		if ( ( $now - (int) $state['last_miss_at'] ) < self::GRACE_SECONDS ) {
			return;
		}

		$state['count']        = (int) $state['count'] + 1;
		$state['last_miss_at'] = $now;
		set_transient( self::miss_key( $scan_id ), $state, self::MISS_TTL );

		if ( $state['count'] >= self::MISS_THRESHOLD ) {
			self::trip( $scan_id );
		}
	}

	/**
	 * A background context executed (cron fallback, cron check, or HTTP
	 * loopback worker); clear the flag and counters.
	 *
	 * @param string $scan_id
	 * @return void
	 */
	public static function mark_background_alive( string $scan_id = '' ): void {
		set_transient( self::bg_activity_key(), time(), self::BG_ACTIVITY_TTL );

		if ( '' !== $scan_id ) {
			delete_transient( self::miss_key( $scan_id ) );
		}

		if ( false !== get_transient( self::TRIP_FLAG ) ) {
			delete_transient( self::TRIP_FLAG );
			if ( class_exists( '\\SiteFort\\Modules\\Scanner\\Service\\ScanLogger' ) ) {
				\SiteFort\Modules\Scanner\Service\ScanLogger::log( 'Background worker is responding again; resuming normal background scanning.' );
			}
		}
	}

	public static function is_tripped(): bool {
		return false !== get_transient( self::TRIP_FLAG );
	}

	/**
	 * Drops the scan's miss counter; the site-level trip flag stays because a
	 * scan ending does not heal the host.
	 */
	public static function clear_scan_state( string $scan_id ): void {
		if ( '' !== $scan_id ) {
			delete_transient( self::miss_key( $scan_id ) );
		}
	}

	private static function trip( string $scan_id ): void {
		set_transient(
			self::TRIP_FLAG,
			[
				'tripped_at' => time(),
				'scan_id'    => sanitize_text_field( $scan_id ),
			],
			self::TRIP_TTL
		);

		if ( class_exists( '\\SiteFort\\Modules\\Scanner\\Service\\ScanLogger' ) ) {
			\SiteFort\Modules\Scanner\Service\ScanLogger::log(
				'Scheduled background worker is not responding on this host. The scan continues at reduced speed from admin polling; configure a real server cron job for full-speed background scans.',
				3,
				'warning'
			);
		}
	}

	private static function miss_key( string $scan_id ): string {
		return 'sitefort_worker_miss_' . substr( hash( 'sha256', $scan_id ), 0, 24 );
	}

	private static function bg_activity_key(): string {
		return 'sitefort_worker_bg_activity';
	}
}
