<?php
/**
 * Retry + fallback orchestration for a single outbound message.
 *
 * Never retries or falls back after a provider has accepted the message.
 *
 * @package EmailSmtp
 * @since 1.4.0
 */

namespace EmailSmtp\Includes;

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

final class DeliveryOrchestrator {

	/**
	 * @param array<string, mixed> $settings Full module settings.
	 * @return array{
	 *   result: DeliveryResult,
	 *   provider: ProviderInterface,
	 *   retry_count: int,
	 *   fallback_used: int
	 * }
	 */
	public static function send( EmailMessage $message, ProviderInterface $primary, array $settings ): array {
		$max_retries = max( 0, min( 2, (int) ( $settings['retry_max_attempts'] ?? 0 ) ) );
		$retry_count = 0;
		$fallback_used = 0;

		$result = self::attempt( $primary, $message, $settings );
		$active = $primary;

		while ( ! $result->is_success() && $result->is_retry_safe() && $retry_count < $max_retries ) {
			++$retry_count;
			// Brief pause only — avoid blocking PHP workers for long waits (cron handles deferred retries).
			usleep( min( 250000, 50000 * $retry_count ) );
			$result = self::attempt( $primary, $message, $settings );
			$active = $primary;
		}

		if ( ! $result->is_success() && self::should_use_fallback( $settings, $primary, $result ) ) {
			$attempts = 0;
			foreach ( self::fallback_queue( $settings, $primary ) as $candidate ) {
				$fb_provider   = $candidate['provider'];
				$fb_settings   = $candidate['settings'];
				// Never fall back to local PHP mail / wordpress provider.
				if ( $fb_provider->get_id() === 'wordpress' ) {
					continue;
				}
				$result        = self::attempt( $fb_provider, $message, $fb_settings );
				$active        = $fb_provider;
				$fallback_used = 1;
				++$attempts;
				if ( $result->is_success() || $attempts >= 2 ) {
					break;
				}
			}
		}

		return [
			'result'        => $result,
			'provider'      => $active,
			'retry_count'   => $retry_count,
			'fallback_used' => $fallback_used,
		];
	}

	/**
	 * Fallback connections: designated fallback only (plus legacy flat fallback_provider).
	 *
	 * @param array<string, mixed> $settings
	 * @return list<array{provider: ProviderInterface, settings: array<string, mixed>}>
	 */
	private static function fallback_queue( array $settings, ProviderInterface $primary ): array {
		$queue        = [];
		$primary_conn = sanitize_key( (string) ( $settings['id'] ?? '' ) );
		$seen         = [];
		if ( $primary_conn !== '' ) {
			$seen[ $primary_conn ] = true;
		}
		$fb_conn_id  = sanitize_key( (string) ( $settings['fallback_connection_id'] ?? '' ) );
		$connections = ConnectionStore::all( $settings );

		$push = static function ( string $conn_id ) use ( &$queue, &$seen, $settings, $connections ): void {
			if ( $conn_id === '' || ! isset( $connections[ $conn_id ] ) || isset( $seen[ $conn_id ] ) ) {
				return;
			}
			$slice    = ConnectionStore::settings_for_connection( $settings, $conn_id );
			$provider = ProviderRegistry::get( (string) ( $slice['provider'] ?? '' ) );
			if ( ! $provider instanceof ProviderInterface || $provider->get_id() === 'wordpress' ) {
				return;
			}
			$seen[ $conn_id ] = true;
			$queue[]          = [
				'provider' => $provider,
				'settings' => $slice,
			];
		};

		// Only the explicitly designated fallback — never fan out to every other connection.
		if ( $fb_conn_id !== '' ) {
			$push( $fb_conn_id );
		}

		// Legacy flat fallback_provider when no multi-connection fallback is set.
		if ( $queue === [] ) {
			$fallback_type = sanitize_key( (string) ( $settings['fallback_provider'] ?? '' ) );
			$fallback      = ProviderRegistry::get( $fallback_type );
			if ( $fallback instanceof ProviderInterface && $fallback->get_id() !== $primary->get_id() ) {
				$queue[] = [
					'provider' => $fallback,
					'settings' => $settings,
				];
			}
		}

		return $queue;
	}

	/**
	 * Send via HTTP API or PHPMailer SMTP depending on connection transport.
	 *
	 * @param array<string, mixed> $settings
	 */
	private static function attempt( ProviderInterface $provider, EmailMessage $message, array $settings ): DeliveryResult {
		if ( SmtpRelayProfiles::connection_uses_smtp( $settings, $provider ) ) {
			$settings = SmtpRelayProfiles::merge_smtp_settings( $settings, $provider->get_id() );
			$smtp     = ProviderRegistry::get( 'smtp' );
			if ( ! $smtp instanceof ProviderInterface ) {
				return DeliveryResult::failure(
					__( 'SMTP transport is unavailable.', 'wp-powersuite' ),
					'missing_smtp_provider',
					'configuration'
				);
			}
			$bag = array_merge( $settings, SettingsSchema::provider_bag( $settings, 'smtp' ) );
			return $smtp->send( $message, $bag );
		}

		$bag = SettingsSchema::provider_bag( $settings, $provider->get_id() );
		return $provider->send( $message, $bag );
	}

	/**
	 * Fallback only on pre-accept failures that are retry-safe (or explicitly enabled for all failures).
	 *
	 * @param array<string, mixed> $settings
	 */
	private static function should_use_fallback( array $settings, ProviderInterface $primary, DeliveryResult $result ): bool {
		if ( empty( $settings['fallback_enabled'] ) || (string) $settings['fallback_enabled'] === '0' ) {
			return false;
		}

		if ( self::fallback_queue( $settings, $primary ) === [] ) {
			return false;
		}

		// Never fallback after accept (success path never reaches here).
		$mode = sanitize_key( (string) ( $settings['fallback_on'] ?? 'retry_safe' ) );
		if ( $mode === 'always' ) {
			return true;
		}

		return $result->is_retry_safe();
	}
}
