<?php
/**
 * Retry queue worker for Mail Doctor.
 *
 * @package MailDoctor
 */

namespace MailDoctor;

use DateTimeImmutable;
use DateTimeZone;
use MailDoctor\Helpers\Settings;
use WP_Error;

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

/**
 * Handles scheduling and executing retry attempts.
 */
class Retry {

	/**
	 * Emit debug output when enabled.
	 *
	 * @param string              $message Log message.
	 * @param array<string,mixed> $context Context data.
	 * @return void
	 */
	protected static function debug_log( string $message, array $context = array() ): void {
		if ( ! Settings::debug_logging_enabled() ) {
			return;
		}

		$line = '[MailDoctor Retry] ' . $message;

		if ( ! empty( $context ) ) {
			$encoded = wp_json_encode( $context );
			if ( false !== $encoded ) {
				$line .= ' :: ' . $encoded;
			}
		}

		error_log( $line ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
	}

	/**
	 * Cron event slug.
	 */
	const CRON_HOOK = 'mail_doctor_retry_cron';

	/**
	 * Queue a failed log entry for another retry attempt.
	 *
	 * @param int $log_id Log identifier.
	 * @param int $delay  Delay in seconds before the next attempt.
	 * @return WP_Error|true
	 */
	public static function enqueue( int $log_id, int $delay = 0 ) {
		if ( $log_id <= 0 ) {
			return new WP_Error( 'mail_doctor_invalid_log', __( 'Invalid log entry identifier.', 'mail-doctor' ) );
		}

		$row = Logger::get_entry( $log_id );

		if ( ! $row ) {
			return new WP_Error( 'mail_doctor_missing_log', __( 'Email log entry not found.', 'mail-doctor' ) );
		}

		$payload = json_decode( $row['payload'] ?? '', true );

		if ( ! is_array( $payload ) || empty( $payload ) ) {
			$payload = Logger::get_payload( $log_id );
		}

		if ( empty( $payload ) || empty( $payload['mail_args'] ) ) {
			return new WP_Error( 'mail_doctor_empty_payload', __( 'The email payload is no longer available for retry.', 'mail-doctor' ) );
		}

		if ( ! empty( $payload['retry']['disabled'] ) ) {
			return new WP_Error( 'mail_doctor_retry_disabled', __( 'Retries are disabled for this email.', 'mail-doctor' ) );
		}

		if ( ! empty( $payload['retry']['quarantined'] ) ) {
			return new WP_Error( 'mail_doctor_retry_quarantined', __( 'This email is quarantined. Release it before retrying.', 'mail-doctor' ) );
		}

		$delay = max( 0, $delay );

		$next_attempt = ( new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ) )
			->modify( sprintf( '+%d seconds', $delay ) )
			->format( 'Y-m-d H:i:s' );

		if ( ! isset( $payload['retry'] ) || ! is_array( $payload['retry'] ) ) {
			$payload['retry'] = array();
		}

		$payload['retry']['next_attempt_gmt'] = $next_attempt;
		$payload['retry']['manual_trigger']   = true;

		if ( ! isset( $payload['retry']['history'] ) || ! is_array( $payload['retry']['history'] ) ) {
			$payload['retry']['history'] = array();
		}

		$payload['retry']['history'][] = array(
			'attempt'          => isset( $payload['retry']['attempts'] ) ? (int) $payload['retry']['attempts'] : 0,
			'status'           => 'scheduled',
			'occurred_at_gmt'  => gmdate( 'Y-m-d H:i:s' ),
			'reason'           => 'manual',
			'next_attempt_gmt' => $next_attempt,
			'delay'            => $delay,
		);

		Logger::update_payload( $log_id, $payload );

		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->update(
			Logger::table_name(),
			array( 'status' => 'queued' ),
			array( 'id' => $log_id ),
			array( '%s' ),
			array( '%d' )
		);

		/**
		 * Fires after a log entry has been queued for retry.
		 *
		 * @param int                  $log_id  Log identifier.
		 * @param array<string, mixed> $payload Updated payload.
		 * @param int                  $delay   Delay in seconds before the retry.
		 */
		do_action( 'mail_doctor_retry_enqueued', $log_id, $payload, $delay );

		return true;
	}

	/**
	 * Initialize hooks.
	 *
	 * @return int Number of dispatched retries.
	 */
	public static function init(): void {
		add_action( self::CRON_HOOK, array( __CLASS__, 'run' ) );
		add_filter( 'cron_schedules', array( __CLASS__, 'register_cron_interval' ) );
		add_action( 'mail_doctor_log_failed', array( __CLASS__, 'handle_failure' ), 10, 2 );

		if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
			$schedule   = Settings::get( 'backoff_schedule', array( 300, 900, 3600, 14400, 43200 ) );
			$first_slot = 300;

			if ( is_array( $schedule ) && ! empty( $schedule ) ) {
				$first_slot = max( 60, (int) $schedule[0] );
			}

			wp_schedule_event( time() + $first_slot, 'mail_doctor_five_minutes', self::CRON_HOOK );
		}
	}

	/**
	 * On activation, schedule cron event.
	 *
	 * @return void
	 */
	public static function activate(): void {
		add_filter( 'cron_schedules', array( __CLASS__, 'register_cron_interval' ) );

		if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
			wp_schedule_event( time() + 300, 'mail_doctor_five_minutes', self::CRON_HOOK );
		}

		remove_filter( 'cron_schedules', array( __CLASS__, 'register_cron_interval' ) );
	}

	/**
	 * Clear scheduled cron on deactivation.
	 *
	 * @return void
	 */
	public static function deactivate(): void {
		wp_clear_scheduled_hook( self::CRON_HOOK );
	}

	/**
	 * Register custom cron interval.
	 *
	 * @param array<string, array<string, mixed>> $schedules WP schedules.
	 * @return array<string, array<string, mixed>>
	 */
	public static function register_cron_interval( array $schedules ): array {
		if ( ! isset( $schedules['mail_doctor_five_minutes'] ) ) {
			$schedules['mail_doctor_five_minutes'] = array(
				'interval' => 300,
				'display'  => __( 'Every Five Minutes (Mail Doctor)', 'mail-doctor' ),
			);
		}

		return $schedules;
	}

	/**
	 * Handle a failed log event by scheduling retry if available.
	 *
	 * @param int   $log_id Log identifier.
	 * @param array $payload Log payload.
	 * @return void
	 */
	public static function handle_failure( int $log_id, array $payload ): void {
		$retry    = $payload['retry'] ?? array();
		$attempts = (int) ( $retry['attempts'] ?? 0 );
		$max      = (int) Settings::get( 'max_attempts', 4 );
		$log_row  = Logger::get_entry( $log_id );

		if ( $attempts >= $max ) {
			Mailer::trace_maildbg(
				'retry.abandoned',
				array(
					'log_id'   => $log_id,
					'attempts' => $attempts,
					'max'      => $max,
				)
			);
			$payload['retry']['next_attempt_gmt'] = null;
			Logger::update_payload( $log_id, $payload );
			return;
		}

		if ( self::should_quarantine( $payload ) ) {
			$latest = self::latest_history_entry( $payload );

			$payload['retry']['quarantined']       = true;
			$payload['retry']['quarantine_reason'] = $latest['error'] ?? __( 'Repeated failure detected. Manual release required.', 'mail-doctor' );
			$payload['retry']['quarantine_at']     = gmdate( 'Y-m-d H:i:s' );
			$payload['retry']['next_attempt_gmt']  = null;

			Logger::update_payload( $log_id, $payload );
			Mailer::trace_maildbg(
				'retry.quarantined',
				array(
					'log_id' => $log_id,
					'reason' => $payload['retry']['quarantine_reason'],
				)
			);

			return;
		}

		$delay = self::calculate_backoff( $attempts, $log_row );

		if ( ! $delay ) {
			Mailer::trace_maildbg(
				'retry.no_schedule',
				array(
					'log_id'   => $log_id,
					'attempts' => $attempts,
				)
			);
			return;
		}

		$next_attempt = ( new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ) )
			->modify( sprintf( '+%d seconds', $delay ) )
			->format( 'Y-m-d H:i:s' );

		$payload['retry']['next_attempt_gmt'] = $next_attempt;

		self::annotate_history_with_schedule( $payload, $next_attempt, $delay );

		Logger::update_payload( $log_id, $payload );
		Mailer::trace_maildbg(
			'retry.scheduled',
			array(
				'log_id'       => $log_id,
				'delay'        => $delay,
				'next_attempt' => $next_attempt,
			)
		);
	}

	/**
	 * Calculate delay for next attempt using exponential backoff data.
	 *
	 * @param int                       $attempts Attempts so far.
	 * @param array<string, mixed>|null $log_row  Optional log row context.
	 * @return int Delay in seconds.
	 */
	public static function calculate_backoff( int $attempts, ?array $log_row = null ): int {
		$schedule = Settings::get( 'backoff_schedule', array( 300, 900, 3600, 14400, 43200 ) );

		if ( ! is_array( $schedule ) ) {
			$schedule = array();
		}

		/**
		 * Filter the retry backoff schedule before the next attempt is scheduled.
		 *
		 * @param array<int, int|string>      $schedule Configured schedule values.
		 * @param int                         $attempts Attempt number (1-indexed).
		 * @param array<string, mixed>|null   $log_row  Raw database row for the log entry.
		 */
		$schedule = apply_filters( 'mail_doctor_retry_backoff', $schedule, $attempts, $log_row );

		if ( ! is_array( $schedule ) || empty( $schedule ) ) {
			return 0;
		}

		$normalized = array();

		foreach ( $schedule as $value ) {
			if ( is_numeric( $value ) ) {
				$value = (int) $value;
			} else {
				$value = (int) sanitize_text_field( (string) $value );
			}

			if ( $value > 0 ) {
				$normalized[] = $value;
			}
		}

		if ( empty( $normalized ) ) {
			return 0;
		}

		$index = max( 0, $attempts - 1 );

		if ( $index >= count( $normalized ) ) {
			return (int) end( $normalized );
		}

		return (int) $normalized[ $index ];
	}

	/**
	 * Execute retry queue worker.
	 *
	 * @return void
	 */
	public static function run(): int {
		global $wpdb;

		$table  = Logger::table_name();
		$now_gm = ( new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ) )->format( 'Y-m-d H:i:s' );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
				"SELECT * FROM {$table} WHERE status IN (%s,%s) ORDER BY created_at ASC LIMIT 25",
				'failed',
				'queued'
			),
			ARRAY_A
		);

		if ( empty( $rows ) ) {
			return 0;
		}

		$processed = 0;

		foreach ( $rows as $row ) {
			$payload = json_decode( $row['payload'] ?? '', true );

			if ( empty( $payload ) || empty( $payload['mail_args'] ) ) {
				continue;
			}

			$retry_meta = $payload['retry'] ?? array();

			if ( ! empty( $retry_meta['quarantined'] ) ) {
				continue;
			}

			$next_attempt = $retry_meta['next_attempt_gmt'] ?? null;
			if ( empty( $next_attempt ) || $next_attempt > $now_gm ) {
				continue;
			}

			$attempts = (int) ( $retry_meta['attempts'] ?? 0 );

			if ( $attempts >= (int) Settings::get( 'max_attempts', 4 ) ) {
				continue;
			}

			self::dispatch_retry( (int) $row['id'], $payload, false );
			++$processed;
		}

		return $processed;
	}

	/**
	 * Process a specific log entry for asynchronous dispatch.
	 *
	 * @param int $log_id Log identifier.
	 * @return bool True when dispatched.
	 */
	public static function process_async_log( int $log_id ): bool {
		if ( $log_id <= 0 ) {
			return false;
		}

		global $wpdb;

		$table_name = Logger::table_name();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$row = $wpdb->get_row(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
				"SELECT * FROM {$table_name} WHERE id = %d",
				$log_id
			),
			ARRAY_A
		);

		if ( ! $row ) {
			return false;
		}

		$payload = json_decode( $row['payload'] ?? '', true );

		if ( empty( $payload ) || empty( $payload['mail_args'] ) ) {
			return false;
		}

		$retry_meta = $payload['retry'] ?? array();

		if ( ! empty( $retry_meta['quarantined'] ) ) {
			return false;
		}

		$attempts = (int) ( $retry_meta['attempts'] ?? 0 );
		$max      = (int) Settings::get( 'max_attempts', 4 );

		if ( $attempts >= $max ) {
			return false;
		}

		if ( isset( $payload['retry']['disabled'] ) && $payload['retry']['disabled'] ) {
			$payload['retry']['disabled'] = false;
		}

		self::dispatch_retry( (int) $row['id'], $payload, false );

		return true;
	}

	/**
	 * Manually trigger retry from admin action.
	 *
	 * @param int $log_id Log identifier.
	 * @return WP_Error|true
	 */
	public static function manual_retry( int $log_id ) {
		global $wpdb;

		$table_name = Logger::table_name();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$row = $wpdb->get_row(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
				"SELECT * FROM {$table_name} WHERE id = %d",
				$log_id
			),
			ARRAY_A
		);

		if ( ! $row ) {
			return new WP_Error( 'mail_doctor_missing_log', __( 'Email log entry not found.', 'mail-doctor' ) );
		}

		$payload = json_decode( $row['payload'] ?? '', true );

		if ( empty( $payload['mail_args'] ) ) {
			return new WP_Error( 'mail_doctor_empty_payload', __( 'The email payload is no longer available for retry.', 'mail-doctor' ) );
		}

		self::dispatch_retry( (int) $row['id'], $payload, true );

		return true;
	}

	/**
	 * Resend a log entry to a different recipient.
	 *
	 * @param int    $log_id    Log identifier.
	 * @param string $recipient Target email address.
	 * @return WP_Error|true
	 */
	public static function manual_resend_to( int $log_id, string $recipient ) {
		$email = sanitize_email( $recipient );

		if ( ! $email || ! is_email( $email ) ) {
			return new WP_Error( 'mail_doctor_invalid_recipient', __( 'Enter a valid email address before resending.', 'mail-doctor' ) );
		}

		$row = Logger::get_entry( $log_id );

		if ( ! $row ) {
			return new WP_Error( 'mail_doctor_missing_log', __( 'Email log entry not found.', 'mail-doctor' ) );
		}

		$payload = Logger::get_payload( $log_id );

		if ( empty( $payload['mail_args'] ) ) {
			return new WP_Error( 'mail_doctor_empty_payload', __( 'The email payload is no longer available for resend.', 'mail-doctor' ) );
		}

		$mail_args = $payload['mail_args'];

		if ( empty( $mail_args['message'] ) ) {
			return new WP_Error( 'mail_doctor_missing_body', __( 'This email body was redacted and cannot be resent.', 'mail-doctor' ) );
		}

		$mail_args['to'] = $email;

		$context = array(
			'attempt'       => 0,
			'origin_log_id' => $log_id,
			'manual'        => true,
			'smtp_profile'  => isset( $payload['meta']['smtp_profile'] ) ? $payload['meta']['smtp_profile'] : null,
		);

		if ( isset( $payload['meta']['correlation_id'] ) && $payload['meta']['correlation_id'] ) {
			$context['correlation_id'] = $payload['meta']['correlation_id'];
		}

		Mailer::set_retry_context( $context );

		$result = wp_mail(
			$mail_args['to'],
			$mail_args['subject'] ?? '',
			$mail_args['message'] ?? '',
			$mail_args['headers'] ?? array(),
			$mail_args['attachments'] ?? array()
		);

		if ( ! $result ) {
			return new WP_Error( 'mail_doctor_resend_failed', __( 'Unable to resend the email. Check the logs for details.', 'mail-doctor' ) );
		}

		if ( ! isset( $payload['retry'] ) || ! is_array( $payload['retry'] ) ) {
			$payload['retry'] = array();
		}

		if ( ! isset( $payload['retry']['history'] ) || ! is_array( $payload['retry']['history'] ) ) {
			$payload['retry']['history'] = array();
		}

		$payload['retry']['history'][] = array(
			'attempt'         => (int) ( $payload['retry']['attempts'] ?? 0 ),
			'status'          => 'manual_resend',
			'occurred_at_gmt' => gmdate( 'Y-m-d H:i:s' ),
			'email_addresses' => array( $email ),
		);

		Logger::update_payload( $log_id, $payload );

		return true;
	}

	/**
	 * Release a quarantined log back into the retry queue.
	 *
	 * @param int $log_id Log identifier.
	 * @return WP_Error|true
	 */
	public static function release_quarantined( int $log_id ) {
		global $wpdb;

		$table_name = Logger::table_name();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$row = $wpdb->get_row(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
				"SELECT * FROM {$table_name} WHERE id = %d",
				$log_id
			),
			ARRAY_A
		);

		if ( ! $row ) {
			return new WP_Error( 'mail_doctor_missing_log', __( 'Email log entry not found.', 'mail-doctor' ) );
		}

		$payload = json_decode( $row['payload'] ?? '', true );

		if ( empty( $payload['retry'] ) || empty( $payload['retry']['quarantined'] ) ) {
			return new WP_Error( 'mail_doctor_not_quarantined', __( 'This log is not quarantined.', 'mail-doctor' ) );
		}

		$payload['retry']['quarantined']       = false;
		$payload['retry']['quarantine_reason'] = '';
		$payload['retry']['quarantine_at']     = null;

		$next_attempt                         = ( new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ) )->format( 'Y-m-d H:i:s' );
		$payload['retry']['next_attempt_gmt'] = $next_attempt;

		if ( ! isset( $payload['retry']['history'] ) || ! is_array( $payload['retry']['history'] ) ) {
			$payload['retry']['history'] = array();
		}

		$payload['retry']['history'][] = array(
			'attempt'          => (int) ( $payload['retry']['attempts'] ?? 0 ),
			'status'           => 'released',
			'reason'           => 'manual',
			'occurred_at_gmt'  => gmdate( 'Y-m-d H:i:s' ),
			'next_attempt_gmt' => $next_attempt,
		);

		Logger::update_payload( $log_id, $payload );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->update(
			Logger::table_name(),
			array( 'status' => 'failed' ),
			array( 'id' => $log_id ),
			array( '%s' ),
			array( '%d' )
		);

		return true;
	}

	/**
	 * Dispatch retry attempt.
	 *
	 * @param int   $log_id Log identifier.
	 * @param array $payload Stored payload.
	 * @param bool  $manual Whether this was triggered manually.
	 * @return void
	 */
	protected static function dispatch_retry( int $log_id, array $payload, bool $manual ): void {
		$mail_args = $payload['mail_args'];
		$retry     = $payload['retry'] ?? array();

		if ( ! empty( $retry['disabled'] ) || empty( $mail_args['to'] ) || empty( $mail_args['message'] ) ) {
			self::debug_log(
				'dispatch_retry skipped',
				array(
					'log_id'   => $log_id,
					'disabled' => ! empty( $retry['disabled'] ),
					'has_to'   => ! empty( $mail_args['to'] ),
					'has_body' => ! empty( $mail_args['message'] ),
				)
			);
			return;
		}

		$retry['attempts'] = (int) ( $retry['attempts'] ?? 0 );

		$context = array(
			'attempt'       => $retry['attempts'] + 1,
			'origin_log_id' => $log_id,
			'manual'        => $manual,
			'smtp_profile'  => isset( $payload['meta']['smtp_profile'] ) ? $payload['meta']['smtp_profile'] : null,
		);

		if ( isset( $payload['meta']['correlation_id'] ) && $payload['meta']['correlation_id'] ) {
			$context['correlation_id'] = $payload['meta']['correlation_id'];
		}

		self::debug_log(
			'dispatch_retry',
			array(
				'log_id'  => $log_id,
				'attempt' => $context['attempt'],
				'manual'  => $manual,
				'profile' => $context['smtp_profile'],
			)
		);
		Mailer::trace_maildbg(
			'retry.dispatch',
			array(
				'log_id'  => $log_id,
				'attempt' => $context['attempt'],
				'manual'  => $manual,
			)
		);

		Mailer::set_retry_context( $context );

		// Prevent repeated dispatch until result is known.
		$payload['retry']['next_attempt_gmt'] = null;
		$payload['retry']['attempts']         = $retry['attempts'] + 1;
		Logger::update_payload( $log_id, $payload );

		$result = wp_mail(
			$mail_args['to'] ?? '',
			$mail_args['subject'] ?? '',
			$mail_args['message'] ?? '',
			$mail_args['headers'] ?? array(),
			$mail_args['attachments'] ?? array()
		);

		self::debug_log(
			'dispatch_retry wp_mail result',
			array(
				'log_id' => $log_id,
				'result' => $result,
			)
		);
		Mailer::trace_maildbg(
			'retry.dispatch_result',
			array(
				'log_id' => $log_id,
				'result' => is_bool( $result ) ? ( $result ? 'true' : 'false' ) : ( $result instanceof WP_Error ? $result->get_error_code() : gettype( $result ) ),
			)
		);
	}

	/**
	 * Determine if a log entry should be quarantined due to repeated failures.
	 *
	 * @param array<string, mixed> $payload Log payload.
	 * @return bool
	 */
	protected static function should_quarantine( array $payload ): bool {
		if ( ! empty( $payload['retry']['quarantined'] ) ) {
			return false;
		}

		$threshold = (int) Settings::get( 'poison_attempt_threshold', 3 );
		if ( $threshold <= 0 ) {
			return false;
		}

		$history = isset( $payload['retry']['history'] ) && is_array( $payload['retry']['history'] ) ? $payload['retry']['history'] : array();
		if ( empty( $history ) ) {
			return false;
		}

		$failed_entries = array();
		for ( $index = count( $history ) - 1; $index >= 0 && count( $failed_entries ) < $threshold; $index-- ) {
			$entry = $history[ $index ];
			if ( ! isset( $entry['status'] ) || 'failed' !== $entry['status'] ) {
				continue;
			}

			$failed_entries[] = $entry;
		}

		if ( count( $failed_entries ) < $threshold ) {
			return false;
		}

		$failed_entries = array_reverse( $failed_entries );

		$signature = null;
		$earliest  = null;
		$latest    = null;
		$window    = max( 300, (int) Settings::get( 'poison_match_window', 900 ) );

		foreach ( $failed_entries as $entry ) {
			if ( empty( $entry['occurred_at_gmt'] ) ) {
				return false;
			}

			$timestamp = self::parse_gmt_timestamp( (string) $entry['occurred_at_gmt'] );

			if ( null === $timestamp ) {
				return false;
			}

			if ( null === $earliest || $timestamp < $earliest ) {
				$earliest = $timestamp;
			}

			if ( null === $latest || $timestamp > $latest ) {
				$latest = $timestamp;
			}

			$current_signature = self::history_signature( $entry );

			if ( null === $signature ) {
				$signature = $current_signature;
			} elseif ( $signature !== $current_signature ) {
				return false;
			}
		}

		if ( null === $earliest || null === $latest ) {
			return false;
		}

		return ( $latest - $earliest ) <= $window;
	}

	/**
	 * Retrieve latest history entry from payload.
	 *
	 * @param array<string, mixed> $payload Log payload.
	 * @return array<string, mixed>
	 */
	protected static function latest_history_entry( array $payload ): array {
		$history = isset( $payload['retry']['history'] ) && is_array( $payload['retry']['history'] ) ? $payload['retry']['history'] : array();

		if ( empty( $history ) ) {
			return array();
		}

		$last = end( $history );
		if ( false === $last ) {
			return array();
		}

		return is_array( $last ) ? $last : array();
	}

	/**
	 * Annotate the most recent history entry with scheduling information.
	 *
	 * @param array<string, mixed> $payload Payload by reference.
	 * @param string               $next_attempt Next attempt timestamp.
	 * @param int                  $delay Delay in seconds.
	 * @return void
	 */
	protected static function annotate_history_with_schedule( array &$payload, string $next_attempt, int $delay ): void {
		if ( empty( $payload['retry']['history'] ) || ! is_array( $payload['retry']['history'] ) ) {
			return;
		}

		$keys = array_keys( $payload['retry']['history'] );

		if ( empty( $keys ) ) {
			return;
		}

		$last_key = $keys[ count( $keys ) - 1 ];

		if ( ! isset( $payload['retry']['history'][ $last_key ] ) || ! is_array( $payload['retry']['history'][ $last_key ] ) ) {
			return;
		}

		$payload['retry']['history'][ $last_key ]['next_attempt_gmt'] = $next_attempt;
		$payload['retry']['history'][ $last_key ]['delay']            = $delay;
	}

	/**
	 * Generate a deterministic signature for a history entry.
	 *
	 * @param array<string, mixed> $entry History detail.
	 * @return string
	 */
	protected static function history_signature( array $entry ): string {
		$code    = isset( $entry['provider_code'] ) ? strtolower( (string) $entry['provider_code'] ) : '';
		$message = isset( $entry['error'] ) ? strtolower( preg_replace( '/\s+/', ' ', (string) $entry['error'] ) ) : '';

		return md5( $code . '|' . $message );
	}

	/**
	 * Convert a GMT datetime string to timestamp.
	 *
	 * @param string $value Date time string.
	 * @return int|null
	 */
	protected static function parse_gmt_timestamp( string $value ): ?int {
		$value = trim( $value );

		if ( '' === $value ) {
			return null;
		}

		$timestamp = strtotime( $value . ' UTC' );

		if ( false === $timestamp ) {
			return null;
		}

		return (int) $timestamp;
	}
}
