<?php
/**
 * Email Sequence Management Service
 *
 * Manages the ordering and scheduling of recovery email sequences.
 *
 * @package EDD\Pro\CartRecovery\Emails
 * @copyright Copyright (c) 2026, Sandhills Development, LLC
 * @license https://opensource.org/licenses/GPL-2.0 GNU Public License
 * @since 3.6.5
 */

namespace EDD\Pro\CartRecovery\Emails;

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

use EDD\Pro\CartRecovery\Carts\Cart;
use EDD\Emails\Email;

/**
 * Email Sequence Service Class
 *
 * Provides methods for determining which recovery email should be sent next
 * based on the cart state and enabled email templates.
 *
 * @since 3.6.5
 */
final class Sequence {

	/**
	 * Timing tolerance in seconds.
	 *
	 * Allows emails to be sent this many seconds before the calculated send time
	 * to account for cron timing variations.
	 *
	 * @since 3.6.5
	 * @var int
	 */
	private const TIMING_TOLERANCE_SECONDS = 30;

	/**
	 * Gets the next email template that should be sent for a cart.
	 *
	 * Returns null if no more emails should be sent (sequence complete or no emails enabled).
	 *
	 * @since 3.6.5
	 * @param Cart $cart Cart object from database.
	 * @return Email|null Email object or null if no more emails to send.
	 */
	public function get_next_email_in_sequence( Cart $cart ): ?Email {
		if ( ! empty( $cart->date_unsubscribed ) || 'unsubscribed' === $cart->status ) {
			return null;
		}

		$enabled_emails   = $this->get_all_enabled_emails();
		$emails_sent      = isset( $cart->recovery_email_count ) ? (int) $cart->recovery_email_count : 0;
		$next_email_index = $emails_sent;

		// Check if we have a next email in the sequence.
		if ( ! isset( $enabled_emails[ $next_email_index ] ) ) {
			return null;
		}

		return $enabled_emails[ $next_email_index ];
	}

	/**
	 * Calculates the timestamp when an email should be sent for a cart.
	 *
	 * For the first email (IR), uses minutes from detection.
	 * For all subsequent emails, uses hours from cart abandonment.
	 *
	 * @since 3.6.5
	 * @param Cart  $cart  Cart object from database.
	 * @param Email $email Email object.
	 * @return int Unix timestamp when the email should be sent.
	 */
	public function calculate_send_time( Cart $cart, Email $email ): int {
		// Get the email delay in seconds (already stored in seconds in metadata).
		$delay_seconds = $this->get_email_delay_seconds( $email );

		// All emails (including first) should use cart abandonment time as the base.
		// This ensures consistent timing whether called at scheduling or execution time.
		$date_abandoned = $cart->date_abandoned ?? null;

		if ( empty( $date_abandoned ) ) {
			// Fallback: if no abandonment date, use creation date or now.
			$date_created = $cart->date_created ?? null;
			if ( ! empty( $date_created ) ) {
				$base_timestamp = strtotime( $date_created );
			} else {
				return time() + $delay_seconds;
			}
		} else {
			$base_timestamp = strtotime( $date_abandoned );
		}

		if ( false === $base_timestamp ) {
			// Invalid date, fallback to now.
			return time() + $delay_seconds;
		}

		// Calculate send time from abandonment/creation time.
		$send_time = $base_timestamp + $delay_seconds;

		// Ensure we don't schedule in the past.
		return max( time(), $send_time );
	}

	/**
	 * Gets the time difference in seconds until an email should be sent.
	 *
	 * Returns 0 if the email is ready to send now, or positive seconds if it's too early.
	 *
	 * @since 3.6.5
	 * @param Cart  $cart  Cart object from database.
	 * @param Email $email Email object.
	 * @return int Seconds until ready (0 if ready now).
	 */
	public function get_seconds_until_ready( Cart $cart, Email $email ): int {
		$send_time = $this->calculate_send_time( $cart, $email );
		$time_diff = $send_time - time();

		return max( 0, $time_diff );
	}

	/**
	 * Gets all enabled recovery emails, ordered by delay.
	 *
	 * @since 3.6.5
	 * @return array Array of email objects, ordered by delay (ascending).
	 */
	protected function get_all_enabled_emails(): array {
		$emails = edd_get_emails(
			array(
				'sender'  => 'edd',
				'context' => 'abandoned_cart',
				'status'  => 1,
				'number'  => 9999,
			)
		);

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

		// Sort by delay metadata (ascending).
		usort(
			$emails,
			function ( $a, $b ) {
				$delay_a = $this->get_email_delay_seconds( $a );
				$delay_b = $this->get_email_delay_seconds( $b );

				return $delay_a <=> $delay_b;
			}
		);

		return $emails;
	}

	/**
	 * Checks if a cart is ready to send a specific email.
	 *
	 * @since 3.6.5
	 * @param Cart  $cart  Cart object from database.
	 * @param Email $email Email object.
	 * @return bool Whether the cart is ready for this email.
	 */
	protected function is_cart_ready_for_email( Cart $cart, Email $email ): bool {
		// Check cart status.
		$status = $cart->status ?? '';
		if ( 'abandoned' !== $status ) {
			return false;
		}

		// Check if cart was recovered or customer unsubscribed.
		$date_recovered    = $cart->date_recovered ?? null;
		$date_unsubscribed = $cart->date_unsubscribed ?? null;

		if ( ! empty( $date_recovered ) || ! empty( $date_unsubscribed ) ) {
			return false;
		}

		// Check if it's time to send this email.
		$send_time    = $this->calculate_send_time( $cart, $email );
		$current_time = time();
		$time_diff    = $send_time - $current_time;

		// Allow a small tolerance for cron timing variations.
		// If we're within the tolerance window, consider it ready.
		if ( $time_diff > self::TIMING_TOLERANCE_SECONDS ) {
			return false;
		}

		return true;
	}

	/**
	 * Gets the delay in seconds for an email.
	 *
	 * @since 3.6.5
	 * @param Email $email Email object.
	 * @return int Delay in seconds.
	 */
	private function get_email_delay_seconds( Email $email ): int {
		$template = $email->get_template();
		if ( ! $template ) {
			return 0;
		}

		$delay = $template->get_metadata( 'delay' );

		return is_numeric( $delay ) ? (int) $delay : 0;
	}
}
