<?php
/**
 * Detection Job handler for ActionScheduler.
 *
 * @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\Cron\Schedulers\Handler as Scheduler;
use EDD\Pro\CartRecovery\Carts\Delete;
use EDD\Pro\CartRecovery\Journey\EventLogger;

/**
 * Detects abandoned carts and enqueues recovery jobs.
 */
final class Detection {

	/**
	 * Job entry point: scan and mark carts as abandoned using bulk operations.
	 *
	 * @since 3.6.5
	 */
	public static function handle(): void {
		$now              = time();
		$cutoff_seconds   = edd_get_option( 'acr_cutoff_minutes', 60 ) * MINUTE_IN_SECONDS;
		$buffer_seconds   = 30;
		$abandoned_before = $now - $cutoff_seconds - $buffer_seconds;

		// Clean up any empty tracking carts that slipped through (no products, no total).
		self::purge_empty_tracking_carts( $abandoned_before );

		$candidates = self::get_tracking_carts( $abandoned_before );
		if ( empty( $candidates ) ) {
			return;
		}

		// Extract cart IDs for bulk operations.
		$cart_ids = array();
		foreach ( $candidates as $cart ) {
			if ( isset( $cart->id ) ) {
				$cart_ids[] = (int) $cart->id;
			}
		}

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

		// Stamp abandoned date before bulk update so downstream jobs have timing information.
		$date_abandoned = edd_get_utc_date_string();

		global $wpdb;

		$placeholders = implode( ',', array_fill( 0, count( $cart_ids ), '%d' ) );
		$sql          = "
			UPDATE {$wpdb->prefix}edd_acr_carts
			SET status = %s,
				date_abandoned = CASE
					WHEN date_abandoned IS NULL OR date_abandoned = '0000-00-00 00:00:00'
						THEN %s
					ELSE date_abandoned
				END,
				date_modified = %s
			WHERE id IN ({$placeholders})
			  AND status = %s
		";

		$date_modified = edd_get_utc_date_string();
		$params        = array_merge(
			array( 'abandoned', $date_abandoned, $date_modified ),
			$cart_ids,
			array( 'tracking' )
		);

		// Validate parameters before preparing query.
		if ( ! is_array( $params ) || empty( $params ) ) {
			edd_debug_log( '[EDD ACR] DetectionJob failed: invalid query parameters.' );
			return;
		}

		$prepared = $wpdb->prepare( $sql, ...$params );

		if ( false === $prepared ) {
			edd_debug_log( '[EDD ACR] DetectionJob failed: unable to prepare bulk abandonment query.' );
			return;
		}

		$result = $wpdb->query( $prepared );

		if ( false === $result ) {
			edd_debug_log( '[EDD ACR] DetectionJob failed: bulk abandonment query execution error: ' . $wpdb->last_error );
			return;
		}

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

		// Log cart_abandoned journey events for each cart.
		$event_logger = new EventLogger();
		foreach ( $cart_ids as $cart_id ) {
			$event_logger->log_cart_event( 'cart_abandoned', $cart_id );
		}

		// Schedule a single batch recovery job instead of individual jobs.
		$batch_hash = md5( implode( ',', $cart_ids ) );
		$scheduler  = Scheduler::get_scheduler();

		$cart_ids_json = wp_json_encode( $cart_ids );
		$schedule_args = array( $batch_hash, $cart_ids_json );

		// Check if already scheduled to avoid duplicates.
		if ( ! $scheduler->next_scheduled( 'edd_acr_process_abandoned_batch', $schedule_args, 'edd_acr' ) ) {
			$scheduler->schedule_single(
				'edd_acr_process_abandoned_batch',
				time(),
				$schedule_args,
				'edd_acr'
			);
			edd_debug_log( sprintf( '[EDD ACR] Batch recovery job scheduled for %d carts (hash: %s)', count( $cart_ids ), $batch_hash ) );
		}
	}

	/**
	 * Finds tracking carts that became stale before the supplied timestamp.
	 *
	 * Uses date-only detection based on cart modification time. Session expiry is
	 * not checked because it's typically much longer (24+ hours) than the ACR
	 * abandonment threshold (typically 1 hour), which would prevent timely detection.
	 *
	 * @since 3.6.5
	 * @param int $timestamp Unix timestamp in seconds.
	 * @return array<int,object> Result set of cart rows.
	 */
	private static function get_tracking_carts( int $timestamp ): array {
		global $wpdb;

		$threshold = gmdate( 'Y-m-d H:i:s', $timestamp );
		if ( empty( $threshold ) ) {
			$threshold = edd_get_utc_date_string();
		}

		$limit = (int) apply_filters( 'edd_acr_detection_batch_size', 500 );
		$limit = max( 1, $limit );

		$sql = "SELECT id, session_id
	        FROM {$wpdb->prefix}edd_acr_carts
	        WHERE status = 'tracking'
	          AND date_unsubscribed IS NULL
	          AND date_modified <= %s
	        ORDER BY date_modified ASC
	        LIMIT %d";

		$params = array( $threshold, $limit );

		$prepared = $wpdb->prepare( $sql, ...$params );

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

		$results = $wpdb->get_results( $prepared );

		if ( ! empty( $wpdb->last_error ) ) {
			edd_debug_log( '[EDD ACR] DetectionJob failed: error fetching pending carts: ' . $wpdb->last_error );
		}

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

	/**
	 * Deletes stale tracking carts that have no cart contents or total.
	 *
	 * These carts were created by login events or other edge cases where the cart
	 * was persisted with identity data but no actual products. They should never
	 * be promoted to abandoned status.
	 *
	 * @since 3.6.5
	 * @param int $timestamp Unix timestamp — only purge carts modified before this time.
	 * @return void
	 */
	private static function purge_empty_tracking_carts( int $timestamp ): void {
		global $wpdb;

		$threshold = gmdate( 'Y-m-d H:i:s', $timestamp );
		if ( empty( $threshold ) ) {
			return;
		}

		$empty_cart_clause = Delete::get_empty_cart_where_clause();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$deleted = $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}edd_acr_carts
				WHERE status = 'tracking'
				  AND {$empty_cart_clause}
				  AND date_modified <= %s",
				$threshold
			)
		);

		if ( $deleted > 0 ) {
			edd_debug_log( sprintf( '[EDD ACR] Detection: purged %d empty tracking carts.', $deleted ) );
		}
	}
}
