<?php
/**
 * Mailer asynchronous delivery helpers.
 *
 * @package MailDoctor
 */

namespace MailDoctor;

use WP_Error;

trait Mailer_Async_Trait {

	/**
	 * Determine if the current invocation should be deferred asynchronously.
	 *
	 * @return bool
	 */
	protected static function should_defer_async(): bool {
		if ( self::$bypass_plugin || self::$processing_async ) {
			return false;
		}

		if ( self::$force_override ) {
			return false;
		}

		if ( ! empty( self::$retry_context ) ) {
			return false;
		}

		$mode = Settings::async_mode();

		return in_array( $mode, array( 'shutdown', 'action_scheduler' ), true );
	}

	/**
	 * Queue the current message for asynchronous delivery.
	 *
	 * @param array<string, mixed> $mail_args Normalized mail arguments.
	 * @return void
	 */
	protected static function enqueue_async_delivery( array $mail_args ): void {
		$mode    = Settings::async_mode();
		$context = self::build_logging_context();

		$log_id = Logger::log_attempt( $mail_args, $context, 'queued' );
		self::debug_log(
			'enqueue_async_delivery',
			array(
				'log_id' => $log_id,
				'mode'   => $mode,
				'event'  => $context['event'] ?? null,
			)
		);

		if ( ! $log_id ) {
			self::debug_log( 'enqueue_async_delivery abort: failed to log attempt' );
			return;
		}

		if ( ! empty( self::$attachment_meta ) ) {
			Logger::annotate_attachments( $log_id, self::$attachment_meta, self::$attachment_total );
		}

		self::prepare_async_payload( $log_id, $mode );

		if ( 'action_scheduler' === $mode && Settings::action_scheduler_available() ) {
			if ( function_exists( 'as_enqueue_async_action' ) ) {
				as_enqueue_async_action( 'mail_doctor_async_send', array( 'log_id' => $log_id ), 'maildoctor' );
				self::debug_log( 'enqueue_async_delivery scheduled with Action Scheduler', array( 'log_id' => $log_id ) );
			}
		} else {
			self::$async_queue[] = (int) $log_id;
			self::debug_log(
				'enqueue_async_delivery queued for shutdown',
				array(
					'log_id'     => $log_id,
					'queue_size' => count( self::$async_queue ),
				)
			);
		}
	}

	/**
	 * Prepare payload metadata for asynchronous delivery.
	 *
	 * @param int    $log_id Log identifier.
	 * @param string $mode   Async mode slug.
	 * @return void
	 */
	protected static function prepare_async_payload( int $log_id, string $mode ): void {
		$payload = Logger::get_payload( $log_id );

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

		$payload['retry']['next_attempt_gmt'] = gmdate( 'Y-m-d H:i:s' );
		$payload['retry']['async']            = array(
			'mode'      => $mode,
			'queued_at' => gmdate( 'Y-m-d H:i:s' ),
		);
		$payload['retry']['manual']           = false;
		$payload['retry']['quarantined']      = false;

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

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

		$payload['meta']['async'] = array(
			'mode'      => $mode,
			'queued_at' => gmdate( 'Y-m-d H:i:s' ),
		);

		Logger::update_payload( $log_id, $payload );
		Logger::annotate_async( $log_id, $mode );
		self::debug_log(
			'prepare_async_payload',
			array(
				'log_id'     => $log_id,
				'mode'       => $mode,
				'next_retry' => $payload['retry']['next_attempt_gmt'],
			)
		);
	}

	/**
	 * Extract Mail Doctor log identifier from header set.
	 *
	 * @param mixed $headers Header collection.
	 * @return int
	 */
	protected static function resolve_log_id_from_headers( $headers ): int {
		if ( is_array( $headers ) ) {
			foreach ( $headers as $key => $value ) {
				if ( is_string( $key ) && stripos( $key, 'X-Order-Mail-Doctor-ID' ) === 0 ) {
					return (int) trim( (string) $value );
				}
				if ( is_string( $value ) && stripos( $value, 'X-Order-Mail-Doctor-ID:' ) === 0 ) {
					$parts = explode( ':', $value );
					return isset( $parts[1] ) ? (int) trim( $parts[1] ) : 0;
				}
			}
		}

		$normalized = Logger::normalize_headers( $headers );

		foreach ( $normalized as $header ) {
			if ( stripos( $header, 'X-Order-Mail-Doctor-ID:' ) === 0 ) {
				$parts = explode( ':', $header );
				return isset( $parts[1] ) ? (int) trim( $parts[1] ) : 0;
			}
		}

		return 0;
	}

	/**
	 * Process queued async deliveries on shutdown when configured.
	 *
	 * @return void
	 */
	public static function process_shutdown_queue(): void {
		if ( empty( self::$async_queue ) ) {
			self::debug_log( 'process_shutdown_queue: empty' );
			return;
		}

		$log_ids           = array_unique( array_map( 'intval', self::$async_queue ) );
		self::$async_queue = array();

		self::debug_log( 'process_shutdown_queue', array( 'log_ids' => $log_ids ) );

		self::$processing_async = true;

		foreach ( $log_ids as $log_id ) {
			self::process_async_log( $log_id, 'shutdown' );
		}

		self::$processing_async = false;
	}

	/**
	 * Handle async dispatch via Action Scheduler.
	 *
	 * @param mixed $log_id Log identifier provided by scheduler.
	 * @return void
	 */
	public static function handle_async_action( $log_id ): void {
		$log_id = (int) $log_id;

		if ( $log_id <= 0 ) {
			return;
		}

		self::debug_log( 'handle_async_action', array( 'log_id' => $log_id ) );

		self::$processing_async = true;
		self::process_async_log( $log_id, 'action_scheduler' );
		self::$processing_async = false;
	}

	/**
	 * Dispatch a queued log entry.
	 *
	 * @param int    $log_id Log identifier.
	 * @param string $source Trigger source.
	 * @return void
	 */
	protected static function process_async_log( int $log_id, string $source ): void {
		if ( $log_id <= 0 ) {
			return;
		}

		self::debug_log(
			'process_async_log start',
			array(
				'log_id' => $log_id,
				'source' => $source,
			)
		);

		$result = Retry::process_async_log( $log_id );

		self::debug_log(
			'process_async_log result',
			array(
				'log_id'     => $log_id,
				'dispatched' => $result,
			)
		);

		if ( ! $result ) {
			return;
		}

		Logger::annotate_async( $log_id, $source );
	}

	/**
	 * Queue a deferral entry for later retry handling.
	 *
	 * @param array<string, mixed> $mail_args Normalized mail arguments.
	 * @param int                  $delay     Delay in seconds.
	 * @param string               $reason    Reason for deferral.
	 * @return void
	 */
	protected static function queue_deferral( array $mail_args, int $delay, string $reason ): void {
		$context                    = self::build_logging_context();
		$context['deferred_reason'] = $reason;

		$log_id = Logger::log_attempt( $mail_args, $context, 'queued' );

		if ( ! $log_id ) {
			self::trace_event(
				'queue.defer_log_failure',
				array(
					'reason' => $reason,
					'delay'  => $delay,
				)
			);
			return;
		}

		if ( ! empty( self::$attachment_meta ) ) {
			Logger::annotate_attachments( $log_id, self::$attachment_meta, self::$attachment_total );
		}

		$result = Retry::enqueue( $log_id, max( 1, $delay ) );

		if ( $result instanceof WP_Error ) {
			Logger::mark_failed( $log_id, $result );
			self::trace_event(
				'queue.defer_error',
				array(
					'log_id' => $log_id,
					'error'  => $result->get_error_message(),
				)
			);
			return;
		}

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

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

		if ( isset( $payload['retry']['history'] ) && is_array( $payload['retry']['history'] ) && ! empty( $payload['retry']['history'] ) ) {
			$index = count( $payload['retry']['history'] ) - 1;
			$payload['retry']['history'][ $index ]['reason'] = $reason;
			$payload['retry']['history'][ $index ]['status'] = 'scheduled';
			$payload['retry']['history'][ $index ]['delay']  = max( 1, $delay );
		}

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

		self::trace_event(
			'queue.deferred',
			array(
				'log_id' => $log_id,
				'delay'  => $delay,
				'reason' => $reason,
			)
		);
	}
}
