<?php
/**
 * Maintenance Job handler for ActionScheduler.
 *
 * Scans for abandoned carts that should have received emails but haven't,
 * serving as a safety net for silent failures in the recovery workflow.
 *
 * @package EDD\Pro\CartRecovery\Jobs
 * @copyright Copyright (c) 2026, Sandhills Development, LLC
 * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0
 * @since 3.6.5
 */

namespace EDD\Pro\CartRecovery\Jobs;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore

use EDD\Pro\CartRecovery\Config\Emails;
use EDD\Pro\CartRecovery\Cron\SingleEventScheduler;
use EDD\Pro\CartRecovery\Traits\Debug;

/**
 * Maintenance Job Class
 *
 * Identifies and reschedules recovery emails for carts that fell through the cracks
 * due to silent failures, exceptions, or other issues.
 *
 * @since 3.6.5
 */
final class Maintenance {
	use Debug;

	/**
	 * Job entry point: scan for abandoned carts that need recovery emails scheduled.
	 *
	 * @since 3.6.5
	 */
	public static function handle(): void {
		$instance = new self();
		$instance->execute_sweep();
	}

	/**
	 * Execute the maintenance sweep.
	 *
	 * @since 3.6.5
	 * @return void
	 */
	private function execute_sweep(): void {
		$stalled_carts = $this->get_stalled_carts();

		if ( empty( $stalled_carts ) ) {
			$this->log( 'Maintenance sweep: No stalled carts found.' );
			return;
		}

		$scheduled = 0;
		$skipped   = 0;

		foreach ( $stalled_carts as $cart ) {
			$cart_id = (int) $cart->id;

			// Check if there's already a pending recovery job for this cart.
			if ( $this->has_pending_recovery_job( $cart_id ) ) {
				++$skipped;
				continue;
			}

			// Schedule a recovery email for this cart.
			try {
				if ( SingleEventScheduler::schedule_recovery_email( $cart_id, $cart ) ) {
					++$scheduled;
				} else {
					++$skipped;
				}
			} catch ( \Exception $e ) {
				$this->log(
					sprintf(
						'Maintenance sweep failed to schedule cart %d: %s',
						$cart_id,
						$e->getMessage()
					)
				);
				++$skipped;
			}
		}

		$this->log(
			sprintf(
				'Maintenance sweep completed: %d carts found, %d scheduled, %d skipped',
				count( $stalled_carts ),
				$scheduled,
				$skipped
			)
		);
	}

	/**
	 * Get carts that are abandoned but haven't received emails when they should have.
	 *
	 * A cart is considered stalled if:
	 * - Status is 'abandoned'
	 * - Has not been recovered or unsubscribed
	 * - Has been abandoned for longer than a reasonable threshold (2 hours by default)
	 * - Has not received any emails yet OR last email was sent more than 24 hours ago
	 * - Total attempts (emails + failures) hasn't exceeded the attempt cap
	 *
	 * @since 3.6.5
	 * @return array<int,object> Result set of stalled cart rows.
	 */
	private function get_stalled_carts(): array {
		global $wpdb;

		// Default threshold: carts abandoned more than 2 hours ago should have received first email.
		$first_email_threshold = gmdate( 'Y-m-d H:i:s', time() - ( 2 * HOUR_IN_SECONDS ) );

		// Default threshold: if last email was sent more than 24 hours ago, check if next is due.
		$max_delay_threshold = gmdate( 'Y-m-d H:i:s', time() - ( 24 * HOUR_IN_SECONDS ) );

		$params = array(
			$first_email_threshold,
			$max_delay_threshold,
		);

		// Build the base query.
		$sql = "
			SELECT id, recovery_email_count, fail_count, date_abandoned, last_recovery_email
			FROM {$wpdb->prefix}edd_acr_carts
			WHERE status = 'abandoned'
			  AND date_recovered IS NULL
			  AND date_unsubscribed IS NULL
			  AND (
				-- No emails sent yet and cart has been abandoned long enough.
				(recovery_email_count = 0 AND date_abandoned <= %s)
				OR
				-- Emails were sent but too long ago (might have stalled mid-sequence).
				(recovery_email_count > 0 AND last_recovery_email IS NOT NULL AND last_recovery_email <= %s)
			  )";

		$attempt_cap = Emails::get_max_recovery_attempts();
		if ( $attempt_cap > 0 ) {
			$params[] = $attempt_cap;
			$sql     .= ' AND (recovery_email_count + fail_count) < %d';
		}

		$sql .= ' ORDER BY date_abandoned ASC LIMIT %d';

		$params[] = max( 1, 50 );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared in the next line with $wpdb->prepare().
		$prepared = $wpdb->prepare( $sql, $params );

		if ( false === $prepared || ! is_string( $prepared ) ) {
			edd_debug_log( '[EDD ACR] Maintenance sweep failed: unable to prepare query.' );
			return array();
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above with $wpdb->prepare().
		$results = $wpdb->get_results( $prepared );

		if ( ! empty( $wpdb->last_error ) ) {
			edd_debug_log( '[EDD ACR] Maintenance sweep failed: ' . $wpdb->last_error );
		}

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

	/**
	 * Check if a cart has a pending recovery job scheduled.
	 *
	 * @since 3.6.5
	 * @param int $cart_id The cart ID.
	 * @return bool True if there's a pending job, false otherwise.
	 */
	private function has_pending_recovery_job( int $cart_id ): bool {
		$scheduler = \EDD\Cron\Schedulers\Handler::get_scheduler();
		$args      = array( $cart_id );

		// Check for pending recovery email job.
		if ( $scheduler->next_scheduled( 'edd_acr_recovery_email', $args, 'edd_acr' ) ) {
			return true;
		}

		// Check for pending retry job with any attempt number.
		// Since retry jobs include attempt_number as second arg, we can't check exact args.
		// We'll check with just the cart_id as the first arg by searching ActionScheduler directly.
		if ( class_exists( '\ActionScheduler_Store' ) ) {
			$store = \ActionScheduler_Store::instance();

			// Look for pending or in-progress actions for this cart.
			$pending_actions = $store->query_actions(
				array(
					'hook'     => 'edd_acr_recovery_email_retry',
					'status'   => array( \ActionScheduler_Store::STATUS_PENDING, \ActionScheduler_Store::STATUS_RUNNING ),
					'group'    => 'edd_acr',
					'per_page' => 100,
				)
			);

			foreach ( $pending_actions as $action_id ) {
				$action      = $store->fetch_action( $action_id );
				$action_args = $action->get_args();

				if ( isset( $action_args[0] ) && (int) $action_args[0] === $cart_id ) {
					return true;
				}
			}
		}

		return false;
	}
}
