<?php
/**
 * Immutable queued-delivery snapshot for external auto-replies.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\AutoReplies;

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

if ( ! class_exists( AutoReplyPreheader::class ) ) {
	require_once __DIR__ . '/AutoReplyPreheader.php';
}

/**
 * Creates and validates the self-contained payload consumed by the mail worker.
 *
 * This codec intentionally has no WordPress runtime dependencies beyond the
 * WP_Error value returned at API boundaries. Persisted values are validated
 * again on every transition so corrupt or manually altered jobs fail closed.
 */
class AutoReplyDeliverySnapshot {
	const SCHEMA              = 'subin.auto-reply-delivery';
	const VERSION             = 2;
	const JOB_PAYLOAD_VERSION = 2;

	const EMAIL_TYPE = 'visitor_auto_reply';
	const SOURCE     = 'submission_inbox_auto_reply';

	const STATE_READY              = 'ready';
	const STATE_NO_EMAIL_GENERATED = 'no_email_generated';

	const DELIVERY_IMMEDIATE = 'immediate';
	const DELIVERY_DELAY     = 'delay';

	const MAX_SUBJECT_BYTES     = 255;
	const MAX_MAILBOX_NAME_BYTES = 255;
	const MAX_BODY_BYTES        = 1048576;
	const MAX_JSON_STRING_EXPANSION = 6;
	const MAX_SNAPSHOT_OVERHEAD_BYTES = 262144;
	const MAX_SNAPSHOT_BYTES    = ( self::MAX_BODY_BYTES * self::MAX_JSON_STRING_EXPANSION ) + self::MAX_SNAPSHOT_OVERHEAD_BYTES;

	/**
	 * Create a canonical immutable snapshot.
	 *
	 * Derived schema, source, identity key, and body hash values are never
	 * accepted from callers. This prevents stale caller data from becoming the
	 * authority for queued-delivery identity.
	 *
	 * @param array<string,mixed> $data Snapshot source fields.
	 * @return array<string,mixed>|\WP_Error
	 */
	public static function create( array $data ) {
		if ( ! self::has_exact_keys( $data, self::create_keys() ) ) {
			return self::invalid( 'subin_auto_reply_snapshot_input_invalid', 'The auto-reply snapshot input is invalid.' );
		}

		$lead_id      = self::canonical_integer( $data['lead_id'], 1, $lead_id_valid );
		$rule_id      = self::canonical_integer( $data['rule_id'], 1, $rule_id_valid );
		$rule_version = self::canonical_integer( $data['rule_version'], 1, $rule_version_valid );
		$template_id  = self::canonical_integer( $data['template_id'], 0, $template_id_valid );
		if ( ! $lead_id_valid || ! $rule_id_valid || ! $rule_version_valid || ! $template_id_valid ) {
			return self::invalid( 'subin_auto_reply_snapshot_identity_invalid', 'The auto-reply snapshot identity is invalid.' );
		}

		$form_id = $data['form_id'];
		if ( is_int( $form_id ) ) {
			$form_id = (string) $form_id;
		}

		$body_html = is_string( $data['body_html'] ) ? $data['body_html'] : '';
		$state     = is_string( $data['state'] ) ? $data['state'] : '';
		$snapshot  = array(
			'schema'         => self::SCHEMA,
			'version'        => self::VERSION,
			'state'          => $data['state'],
			'source'         => self::SOURCE,
			'provider'       => $data['provider'],
			'form_id'        => $form_id,
			'form_name'      => $data['form_name'],
			'lead_id'        => $lead_id,
			'rule_id'        => $rule_id,
			'rule_name'      => $data['rule_name'],
			'rule_version'   => $rule_version,
			'template_id'    => $template_id,
			'template_name'  => $data['template_name'],
			'delivery_mode'  => $data['delivery_mode'],
			'scheduled_at'   => $data['scheduled_at'],
			'to'             => $data['to'],
			'subject'        => $data['subject'],
			'body_html'      => $data['body_html'],
			'from'           => self::copy_mailbox( $data['from'] ),
			'reply_to'       => self::copy_mailbox( $data['reply_to'] ),
			'reason_code'    => $data['reason_code'],
			'reason_message' => $data['reason_message'],
			'delivery_key'   => self::delivery_key( $lead_id ),
			'body_sha256'    => self::STATE_READY === $state ? hash( 'sha256', $body_html ) : '',
		);

		$validated = self::validate_snapshot( $snapshot );

		return $validated instanceof \WP_Error ? $validated : $snapshot;
	}

	/**
	 * Wrap a validated snapshot in the exact v2 job payload shape.
	 *
	 * @param array<string,mixed> $snapshot Snapshot.
	 * @return array<string,mixed>|\WP_Error
	 */
	public static function payload( array $snapshot ) {
		$validated = self::validate_snapshot( $snapshot );
		if ( $validated instanceof \WP_Error ) {
			return $validated;
		}

		return array( 'auto_reply_snapshot' => $snapshot );
	}

	/**
	 * Decode and bind a persisted v2 job payload to its job identity.
	 *
	 * @param array<string,mixed> $payload Persisted decoded payload.
	 * @param mixed               $expected_lead_id Expected job lead id.
	 * @param string              $expected_email_type Expected job email type.
	 * @return array<string,mixed>|\WP_Error
	 */
	public static function decode( array $payload, $expected_lead_id, $expected_email_type = self::EMAIL_TYPE ) {
		if (
			! self::has_exact_keys( $payload, array( 'auto_reply_snapshot' ) )
			|| ! is_array( $payload['auto_reply_snapshot'] ?? null )
		) {
			return self::invalid( 'subin_auto_reply_snapshot_payload_invalid', 'The queued auto-reply snapshot payload is invalid.' );
		}
		if ( ! is_string( $expected_email_type ) || self::EMAIL_TYPE !== $expected_email_type ) {
			return self::invalid( 'subin_auto_reply_snapshot_email_type_invalid', 'The queued auto-reply email type is invalid.' );
		}

		$expected_lead_id = self::canonical_integer( $expected_lead_id, 1, $expected_lead_id_valid );
		if ( ! $expected_lead_id_valid ) {
			return self::invalid( 'subin_auto_reply_snapshot_identity_invalid', 'The queued auto-reply identity is invalid.' );
		}

		$snapshot  = self::upgrade_legacy_snapshot( $payload['auto_reply_snapshot'] );
		$validated = self::validate_snapshot( $snapshot );
		if ( $validated instanceof \WP_Error ) {
			return $validated;
		}
		if (
			$snapshot['lead_id'] !== $expected_lead_id
			|| ! hash_equals( self::delivery_key( $expected_lead_id ), $snapshot['delivery_key'] )
		) {
			return self::invalid( 'subin_auto_reply_snapshot_identity_invalid', 'The queued auto-reply identity is invalid.' );
		}

		return $snapshot;
	}

	/**
	 * Adapt a snapshot to the existing envelope delivery routine.
	 *
	 * The lead-based delivery key is intentionally different from the receipt-
	 * based NotificationEnvelope identity. The worker must call this only after
	 * decode() (or accept this method's equivalent full validation).
	 *
	 * @param array<string,mixed> $snapshot Snapshot.
	 * @return array<string,mixed>|\WP_Error
	 */
	public static function as_envelope( array $snapshot ) {
		$validated = self::validate_snapshot( $snapshot );
		if ( $validated instanceof \WP_Error ) {
			return $validated;
		}

		return array(
			'schema'     => 'subin.notification-envelope',
			'version'    => 1,
			'email_type' => self::EMAIL_TYPE,
			'delivery'   => array(
				'state'          => $snapshot['state'],
				'to'             => $snapshot['to'],
				'subject'        => $snapshot['subject'],
				'body_html'      => $snapshot['body_html'],
				'from'           => $snapshot['from'],
				'reply_to'       => $snapshot['reply_to'],
				'reason_code'    => $snapshot['reason_code'],
				'reason_message' => $snapshot['reason_message'],
			),
			'context'    => array(
				'source'       => $snapshot['source'],
				'provider'     => $snapshot['provider'],
				'form_id'      => $snapshot['form_id'],
				'form_name'    => $snapshot['form_name'],
				'lead_id'      => $snapshot['lead_id'],
				'receipt_id'   => 0,
				'submitted_at' => $snapshot['scheduled_at'],
				'delivery_key' => $snapshot['delivery_key'],
			),
				'meta'       => array(
					'rule_id'        => $snapshot['rule_id'],
					'rule_name'      => $snapshot['rule_name'],
					'rule_version'   => $snapshot['rule_version'],
					'template_id'    => $snapshot['template_id'],
					'template_name'  => $snapshot['template_name'],
				'delivery_mode'  => $snapshot['delivery_mode'],
				'scheduled_at'   => $snapshot['scheduled_at'],
				'lead_id_token'  => '',
				'body_sha256'    => $snapshot['body_sha256'],
			),
		);
	}

	/**
	 * Validate an exact canonical snapshot.
	 *
	 * @param array<string,mixed> $snapshot Snapshot.
	 * @return true|\WP_Error
	 */
	private static function validate_snapshot( array $snapshot ) {
		if ( ! self::has_exact_keys( $snapshot, self::snapshot_keys() ) ) {
			return self::invalid( 'subin_auto_reply_snapshot_schema_invalid', 'The auto-reply snapshot schema is invalid.' );
		}
		if ( self::SCHEMA !== $snapshot['schema'] || self::VERSION !== $snapshot['version'] ) {
			return self::invalid( 'subin_auto_reply_snapshot_version_unsupported', 'The auto-reply snapshot version is not supported.' );
		}

		foreach ( array( 'state', 'source', 'provider', 'form_id', 'form_name', 'rule_name', 'template_name', 'delivery_mode', 'scheduled_at', 'to', 'subject', 'body_html', 'reason_code', 'reason_message', 'delivery_key', 'body_sha256' ) as $key ) {
			if ( ! is_string( $snapshot[ $key ] ) ) {
				return self::invalid( 'subin_auto_reply_snapshot_type_invalid', 'The auto-reply snapshot contains an invalid value type.' );
			}
		}

		if (
			! is_int( $snapshot['lead_id'] )
			|| $snapshot['lead_id'] <= 0
			|| ! is_int( $snapshot['rule_id'] )
			|| $snapshot['rule_id'] <= 0
			|| ! is_int( $snapshot['rule_version'] )
			|| $snapshot['rule_version'] <= 0
			|| ! is_int( $snapshot['template_id'] )
			|| $snapshot['template_id'] < 0
		) {
			return self::invalid( 'subin_auto_reply_snapshot_identity_invalid', 'The auto-reply snapshot identity is invalid.' );
		}

		if (
			self::SOURCE !== $snapshot['source']
			|| ! self::valid_provider( $snapshot['provider'] )
				|| ! self::valid_single_line( $snapshot['form_id'], 190, false )
				|| ! self::valid_single_line( $snapshot['form_name'], 255, true )
				|| ! self::valid_single_line( $snapshot['rule_name'], 190, true )
				|| ! self::valid_single_line( $snapshot['template_name'], 255, true )
		) {
			return self::invalid( 'subin_auto_reply_snapshot_context_invalid', 'The auto-reply snapshot context is invalid.' );
		}

		if (
			! in_array( $snapshot['delivery_mode'], array( self::DELIVERY_IMMEDIATE, self::DELIVERY_DELAY ), true )
			|| ! self::valid_datetime( $snapshot['scheduled_at'] )
		) {
			return self::invalid( 'subin_auto_reply_snapshot_schedule_invalid', 'The auto-reply snapshot schedule is invalid.' );
		}

		if (
			! in_array( $snapshot['state'], array( self::STATE_READY, self::STATE_NO_EMAIL_GENERATED ), true )
			|| ! self::valid_single_line( $snapshot['subject'], self::MAX_SUBJECT_BYTES, true )
			|| strlen( $snapshot['body_html'] ) > self::MAX_BODY_BYTES
			|| self::unsafe_html( $snapshot['body_html'] )
		) {
			return self::invalid( 'subin_auto_reply_snapshot_delivery_invalid', 'The auto-reply snapshot delivery is invalid.' );
		}

		if ( '' !== $snapshot['to'] && ! self::valid_email( $snapshot['to'] ) ) {
			return self::invalid( 'subin_auto_reply_snapshot_recipient_invalid', 'The auto-reply recipient is invalid.' );
		}
		if ( ! self::valid_mailbox( $snapshot['from'] ) ) {
			return self::invalid( 'subin_auto_reply_snapshot_from_invalid', 'The auto-reply From address is invalid.' );
		}
		if ( ! self::valid_mailbox( $snapshot['reply_to'] ) ) {
			return self::invalid( 'subin_auto_reply_snapshot_reply_to_invalid', 'The auto-reply Reply-To address is invalid.' );
		}

		if (
			strlen( $snapshot['reason_code'] ) > 100
			|| ( '' !== $snapshot['reason_code'] && 1 !== preg_match( '/^[a-z0-9_:-]+$/', $snapshot['reason_code'] ) )
			|| ! self::valid_message( $snapshot['reason_message'], 4096, true )
		) {
			return self::invalid( 'subin_auto_reply_snapshot_reason_invalid', 'The auto-reply snapshot reason is invalid.' );
		}

		if ( self::STATE_READY === $snapshot['state'] ) {
			if (
				! self::valid_email( $snapshot['to'] )
				|| '' === trim( $snapshot['subject'] )
				|| ! self::has_rendered_content( $snapshot['body_html'] )
				|| '' !== $snapshot['reason_code']
				|| '' !== $snapshot['reason_message']
			) {
				return self::invalid( 'subin_auto_reply_snapshot_ready_invalid', 'The ready auto-reply snapshot is incomplete.' );
			}
		} elseif (
			'' !== $snapshot['body_html']
			|| '' === $snapshot['reason_code']
			|| '' === trim( $snapshot['reason_message'] )
		) {
			return self::invalid( 'subin_auto_reply_snapshot_no_email_invalid', 'The no-email auto-reply snapshot is incomplete.' );
		}

		$expected_key  = self::delivery_key( $snapshot['lead_id'] );
		$expected_hash = self::STATE_READY === $snapshot['state'] ? hash( 'sha256', $snapshot['body_html'] ) : '';
		if (
			1 !== preg_match( '/^[a-f0-9]{64}$/', $snapshot['delivery_key'] )
			|| ! hash_equals( $expected_key, $snapshot['delivery_key'] )
			|| ( '' !== $snapshot['body_sha256'] && 1 !== preg_match( '/^[a-f0-9]{64}$/', $snapshot['body_sha256'] ) )
			|| ! hash_equals( $expected_hash, $snapshot['body_sha256'] )
		) {
			return self::invalid( 'subin_auto_reply_snapshot_integrity_invalid', 'The auto-reply snapshot integrity check failed.' );
		}

		$encoded = json_encode( $snapshot );
		if ( ! is_string( $encoded ) || strlen( $encoded ) > self::MAX_SNAPSHOT_BYTES ) {
			return self::invalid( 'subin_auto_reply_snapshot_size_invalid', 'The auto-reply snapshot is invalid or too large.' );
		}

		return true;
	}

	/**
	 * Exact caller-owned fields accepted by create().
	 *
	 * @return array<int,string>
	 */
	private static function create_keys() {
		return array(
			'state',
			'provider',
			'form_id',
			'form_name',
			'lead_id',
				'rule_id',
				'rule_name',
				'rule_version',
				'template_id',
				'template_name',
			'delivery_mode',
			'scheduled_at',
			'to',
			'subject',
			'body_html',
			'from',
			'reply_to',
			'reason_code',
			'reason_message',
		);
	}

	/**
	 * Exact canonical snapshot fields.
	 *
	 * @return array<int,string>
	 */
	private static function snapshot_keys() {
		return array_merge(
			array( 'schema', 'version' ),
			self::create_keys(),
			array( 'source', 'delivery_key', 'body_sha256' )
		);
	}

	/**
	 * Upgrade an already-queued v1 snapshot without inventing historical names.
	 *
	 * @param array<string,mixed> $snapshot Stored snapshot.
	 * @return array<string,mixed>
	 */
	private static function upgrade_legacy_snapshot( array $snapshot ) {
		if ( self::SCHEMA === ( $snapshot['schema'] ?? null ) && 1 === ( $snapshot['version'] ?? null ) ) {
			$snapshot['version']       = self::VERSION;
			$snapshot['rule_name']     = '';
			$snapshot['template_name'] = '';
		}

		return $snapshot;
	}

	/**
	 * Compare associative keys without depending on their insertion order.
	 *
	 * @param array<string,mixed> $value Value.
	 * @param array<int,string>   $expected Expected keys.
	 * @return bool
	 */
	private static function has_exact_keys( array $value, array $expected ) {
		$actual = array_keys( $value );
		sort( $actual, SORT_STRING );
		sort( $expected, SORT_STRING );

		return $actual === $expected;
	}

	/**
	 * Canonicalize a database-compatible non-negative integer.
	 *
	 * @param mixed $value Value.
	 * @param int   $minimum Minimum value.
	 * @param bool  $valid Validity reference.
	 * @return int
	 */
	private static function canonical_integer( $value, $minimum, &$valid ) {
		$valid = false;
		if ( is_int( $value ) ) {
			$number = $value;
		} elseif ( is_string( $value ) && 1 === preg_match( '/^(?:0|[1-9][0-9]*)$/', $value ) ) {
			$max = (string) PHP_INT_MAX;
			if ( strlen( $value ) > strlen( $max ) || ( strlen( $value ) === strlen( $max ) && strcmp( $value, $max ) > 0 ) ) {
				return 0;
			}
			$number = (int) $value;
		} else {
			return 0;
		}

		$valid = $number >= $minimum;

		return $valid ? $number : 0;
	}

	/**
	 * Copy a nullable exact mailbox without retaining caller references.
	 *
	 * @param mixed $mailbox Mailbox.
	 * @return mixed
	 */
	private static function copy_mailbox( $mailbox ) {
		if ( ! is_array( $mailbox ) ) {
			return $mailbox;
		}

		return array(
			'name'  => $mailbox['name'] ?? null,
			'email' => $mailbox['email'] ?? null,
		) + $mailbox;
	}

	/**
	 * Validate a nullable mailbox with exact name/email keys.
	 *
	 * @param mixed $mailbox Mailbox.
	 * @return bool
	 */
	private static function valid_mailbox( $mailbox ) {
		if ( null === $mailbox ) {
			return true;
		}
		if ( ! is_array( $mailbox ) || ! self::has_exact_keys( $mailbox, array( 'name', 'email' ) ) ) {
			return false;
		}

		return is_string( $mailbox['name'] )
			&& self::valid_single_line( $mailbox['name'], self::MAX_MAILBOX_NAME_BYTES, true )
			&& is_string( $mailbox['email'] )
			&& self::valid_email( $mailbox['email'] );
	}

	/**
	 * Strict RFC-style mailbox validation suitable for a generated header.
	 *
	 * @param string $email Address.
	 * @return bool
	 */
	private static function valid_email( $email ) {
		return is_string( $email )
			&& '' !== $email
			&& strlen( $email ) <= 254
			&& trim( $email ) === $email
			&& ! self::has_header_break( $email )
			&& false !== filter_var( $email, FILTER_VALIDATE_EMAIL );
	}

	/**
	 * Validate a safe provider storage key.
	 *
	 * @param string $provider Provider.
	 * @return bool
	 */
	private static function valid_provider( $provider ) {
		return '' !== $provider
			&& strlen( $provider ) <= 64
			&& 1 === preg_match( '/^[a-z0-9_-]+$/', $provider );
	}

	/**
	 * Validate a trimmed single-line header/context value.
	 *
	 * @param string $value Value.
	 * @param int    $maximum Maximum bytes.
	 * @param bool   $allow_empty Whether empty is valid.
	 * @return bool
	 */
	private static function valid_single_line( $value, $maximum, $allow_empty ) {
		if ( ! is_string( $value ) || strlen( $value ) > $maximum || trim( $value ) !== $value ) {
			return false;
		}
		if ( ! $allow_empty && '' === $value ) {
			return false;
		}

		return 1 !== preg_match( '/[\x00-\x1F\x7F]/', $value );
	}

	/**
	 * Validate a non-header message while permitting ordinary line breaks.
	 *
	 * @param string $value Value.
	 * @param int    $maximum Maximum bytes.
	 * @param bool   $allow_empty Whether empty is valid.
	 * @return bool
	 */
	private static function valid_message( $value, $maximum, $allow_empty ) {
		if ( ! is_string( $value ) || strlen( $value ) > $maximum ) {
			return false;
		}
		if ( ! $allow_empty && '' === trim( $value ) ) {
			return false;
		}

		return 1 !== preg_match( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $value );
	}

	/**
	 * Validate a canonical UTC/MySQL datetime without environmental timezone use.
	 *
	 * @param string $value Datetime.
	 * @return bool
	 */
	private static function valid_datetime( $value ) {
		if ( ! is_string( $value ) || 1 !== preg_match( '/^(\d{4})-(\d{2})-(\d{2}) ([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/', $value, $parts ) ) {
			return false;
		}

		return checkdate( (int) $parts[2], (int) $parts[3], (int) $parts[1] );
	}

	/**
	 * Detect CR/LF mail header injection.
	 *
	 * @param string $value Value.
	 * @return bool
	 */
	private static function has_header_break( $value ) {
		return false !== strpos( $value, "\r" ) || false !== strpos( $value, "\n" );
	}

	/**
	 * Determine whether an HTML body has visible content or a usable image.
	 *
	 * @param string $html HTML.
	 * @return bool
	 */
	private static function has_rendered_content( $html ) {
		return AutoReplyPreheader::has_meaningful_content( $html );
	}

	/**
	 * Reject executable or active content if persisted data was corrupted.
	 *
	 * @param string $html HTML.
	 * @return bool
	 */
	private static function unsafe_html( $html ) {
		if ( 1 === preg_match( '/<\?(?:php)?/i', $html ) ) {
			return true;
		}

		$matched = preg_match_all( '/<[^>]*>/s', $html, $matches );
		if ( false === $matched ) {
			return true;
		}

		foreach ( $matches[0] as $tag ) {
			$tag = html_entity_decode( $tag, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
			if (
				1 === preg_match( '/^<\s*\/?\s*(?:script|iframe|object|embed|form|base|meta)\b/i', $tag )
				|| 1 === preg_match( '/\son[a-z][a-z0-9:_-]*\s*=/i', $tag )
				|| 1 === preg_match( '/\ssrcdoc\s*=/i', $tag )
			) {
				return true;
			}

			$url_matches = array();
			$url_result  = preg_match_all(
				'/\s(?:href|src|xlink:href|action|formaction|srcset)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+))/i',
				$tag,
				$url_matches,
				PREG_SET_ORDER
			);
			if ( false === $url_result ) {
				return true;
			}
			foreach ( $url_matches as $url_match ) {
				$value = (string) ( $url_match[1] ?: ( $url_match[2] ?: ( $url_match[3] ?? '' ) ) );
				$value = preg_replace( '/[\x00-\x20]+/', '', html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
				if ( null === $value || 1 === preg_match( '/(?:^|,)(?:javascript|data):/i', $value ) ) {
					return true;
				}
			}

			$style_matches = array();
			$style_result  = preg_match_all( '/\sstyle\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+))/i', $tag, $style_matches, PREG_SET_ORDER );
			if ( false === $style_result ) {
				return true;
			}
			foreach ( $style_matches as $style_match ) {
				$value = html_entity_decode( (string) ( $style_match[1] ?: ( $style_match[2] ?: ( $style_match[3] ?? '' ) ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
				if ( 1 === preg_match( '/(?:expression\s*\(|@import|url\s*\(\s*[\'\"]?\s*(?:javascript|data)\s*:)/i', $value ) ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Build the deterministic lead/type delivery identity.
	 *
	 * @param int $lead_id Lead id.
	 * @return string
	 */
	private static function delivery_key( $lead_id ) {
		return hash( 'sha256', 'lead:' . $lead_id . ':' . self::EMAIL_TYPE );
	}

	/**
	 * Build a permanent codec error.
	 *
	 * @param string $code Error code.
	 * @param string $message Safe message.
	 * @return \WP_Error
	 */
	private static function invalid( $code, $message ) {
		return new \WP_Error( $code, $message, array( 'retryable' => false ) );
	}
}
