<?php
/**
 * Immutable notification envelope codec.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Notifications;

use SUBIN\AutoReplies\AutoReplyPreheader;

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

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

/**
 * Builds, finalizes, and validates self-contained queued email payloads.
 */
class NotificationEnvelope {
	const SCHEMA   = 'subin.notification-envelope';
	const VERSION  = 1;

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

	const MAX_BODY_BYTES     = 1048576;
	const MAX_JSON_STRING_EXPANSION = 6;
	const MAX_ENVELOPE_OVERHEAD_BYTES = 262144;
	const MAX_ENVELOPE_BYTES = ( self::MAX_BODY_BYTES * self::MAX_JSON_STRING_EXPANSION ) + self::MAX_ENVELOPE_OVERHEAD_BYTES;

	/**
	 * Create an unguessable marker for the system-owned lead id binding.
	 *
	 * The marker is stored in the draft metadata and removed during finalization,
	 * so marker-like visitor content can never be mistaken for a binding.
	 *
	 * @return string
	 */
	public static function create_lead_id_token() {
		return '__SUBIN_LEAD_ID_' . strtolower( str_replace( '-', '', wp_generate_uuid4() ) ) . '__';
	}

	/**
	 * Check whether a value is a generated lead id marker.
	 *
	 * @param mixed $value Candidate value.
	 * @return bool
	 */
	public static function is_lead_id_token( $value ) {
		return is_string( $value ) && 1 === preg_match( '/^__SUBIN_LEAD_ID_[a-f0-9]{32}__$/', $value );
	}

	/**
	 * Create a validated draft before receipt and lead ids are known.
	 *
	 * @param string              $email_type Notification type.
	 * @param string              $source Mail event source.
	 * @param array<string,mixed> $delivery Delivery snapshot.
	 * @param array<string,mixed> $context Acceptance context snapshot.
	 * @param array<string,mixed> $meta Optional immutable ids.
	 * @return array<string,mixed>|\WP_Error
	 */
	public static function create_draft( $email_type, $source, array $delivery, array $context, array $meta = array() ) {
		$subject_raw = self::scalar_string( $delivery['subject'] ?? '' );
		$reply_name_raw = is_array( $delivery['reply_to'] ?? null )
			? self::scalar_string( $delivery['reply_to']['name'] ?? '' )
			: '';
		$from_name_raw = is_array( $delivery['from'] ?? null )
			? self::scalar_string( $delivery['from']['name'] ?? '' )
			: '';
		if ( self::has_header_break( $subject_raw ) || self::has_header_break( $reply_name_raw ) || self::has_header_break( $from_name_raw ) ) {
			return self::invalid( 'subin_notification_envelope_header_invalid', __( 'The notification envelope contains an invalid header value.', 'submission-inbox' ) );
		}

		$from = null;
		if ( null !== ( $delivery['from'] ?? null ) ) {
			if ( ! is_array( $delivery['from'] ) ) {
				return self::invalid( 'subin_notification_envelope_from_invalid', __( 'The notification From value is invalid.', 'submission-inbox' ) );
			}
			$from = array(
				'email' => sanitize_email( self::scalar_string( $delivery['from']['email'] ?? '' ) ),
				'name'  => sanitize_text_field( $from_name_raw ),
			);
		}

		$reply_to = null;
		if ( null !== ( $delivery['reply_to'] ?? null ) ) {
			if ( ! is_array( $delivery['reply_to'] ) ) {
				return self::invalid( 'subin_notification_envelope_reply_to_invalid', __( 'The notification Reply-To value is invalid.', 'submission-inbox' ) );
			}
			$reply_to = array(
				'email' => sanitize_email( self::scalar_string( $delivery['reply_to']['email'] ?? '' ) ),
				'name'  => sanitize_text_field( $reply_name_raw ),
			);
		}
		$delivery_mode = 'delay' === sanitize_key( self::scalar_string( $meta['delivery_mode'] ?? '' ) ) ? 'delay' : 'immediate';
		$scheduled_at  = sanitize_text_field( self::scalar_string( $meta['scheduled_at'] ?? $context['submitted_at'] ?? current_time( 'mysql' ) ) );

		$envelope = array(
			'schema'     => self::SCHEMA,
			'version'    => self::VERSION,
			'email_type' => sanitize_key( self::scalar_string( $email_type ) ),
			'delivery'   => array(
				'state'          => sanitize_key( self::scalar_string( $delivery['state'] ?? '' ) ),
				'to'             => sanitize_email( self::scalar_string( $delivery['to'] ?? '' ) ),
				'subject'        => sanitize_text_field( $subject_raw ),
				'body_html'      => self::scalar_string( $delivery['body_html'] ?? '' ),
				'from'           => $from,
				'reply_to'       => $reply_to,
				'reason_code'    => sanitize_key( self::scalar_string( $delivery['reason_code'] ?? '' ) ),
				'reason_message' => sanitize_textarea_field( self::scalar_string( $delivery['reason_message'] ?? '' ) ),
			),
			'context'    => array(
				'source'         => sanitize_key( self::scalar_string( $source ) ),
				'provider'       => sanitize_key( self::scalar_string( $context['provider'] ?? '' ) ),
				'form_id'        => sanitize_text_field( self::scalar_string( $context['form_id'] ?? '' ) ),
				'form_name'      => sanitize_text_field( self::scalar_string( $context['form_name'] ?? '' ) ),
				'lead_id'        => 0,
				'receipt_id'     => 0,
				'submitted_at'   => sanitize_text_field( self::scalar_string( $context['submitted_at'] ?? '' ) ),
				'delivery_key'   => '',
			),
			'meta'       => array(
				'rule_id'       => absint( $meta['rule_id'] ?? 0 ),
				'rule_name'     => sanitize_text_field( self::scalar_string( $meta['rule_name'] ?? '' ) ),
				'rule_version'  => absint( $meta['rule_version'] ?? 0 ),
				'template_id'   => absint( $meta['template_id'] ?? 0 ),
				'template_name' => sanitize_text_field( self::scalar_string( $meta['template_name'] ?? '' ) ),
				'delivery_mode' => $delivery_mode,
				'scheduled_at'  => $scheduled_at,
				'lead_id_token' => self::scalar_string( $meta['lead_id_token'] ?? '' ),
				'body_sha256'   => '',
			),
		);

		$validated = self::validate( $envelope, false, $envelope['email_type'], 0, 0 );

		return is_wp_error( $validated ) ? $validated : $envelope;
	}

	/**
	 * Bind database identities and content hashes before transactional enqueue.
	 *
	 * @param array<string,mixed> $draft Draft envelope.
	 * @param int                 $receipt_id Receipt id.
	 * @param int                 $lead_id Lead id, or zero for no-store admin mail.
	 * @param string              $expected_email_type Expected job type.
	 * @return array<string,mixed>|\WP_Error
	 */
	public static function finalize( array $draft, $receipt_id, $lead_id, $expected_email_type ) {
		$receipt_id          = absint( $receipt_id );
		$lead_id             = absint( $lead_id );
		$expected_email_type = sanitize_key( self::scalar_string( $expected_email_type ) );
		$validated          = self::validate( $draft, false, $expected_email_type, 0, 0 );
		if ( is_wp_error( $validated ) ) {
			return $validated;
		}
		if ( $receipt_id <= 0 ) {
			return self::invalid( 'subin_notification_envelope_identity_invalid', __( 'The notification envelope identity is invalid.', 'submission-inbox' ) );
		}

		$envelope      = $draft;
		$lead_id_token = (string) ( $envelope['meta']['lead_id_token'] ?? '' );
		foreach ( array( 'subject', 'body_html', 'reason_message' ) as $key ) {
			if ( '' !== $lead_id_token ) {
				$envelope['delivery'][ $key ] = str_replace( $lead_id_token, (string) $lead_id, (string) $envelope['delivery'][ $key ] );
			}
		}
		$envelope['context']['lead_id']      = $lead_id;
		$envelope['context']['receipt_id']   = $receipt_id;
		$envelope['context']['delivery_key'] = hash( 'sha256', $receipt_id . ':' . $expected_email_type );
		$envelope['meta']['lead_id_token']   = '';
		$envelope['meta']['body_sha256']     = self::STATE_READY === $envelope['delivery']['state']
			? hash( 'sha256', (string) $envelope['delivery']['body_html'] )
			: '';

		$validated = self::validate( $envelope, true, $expected_email_type, $receipt_id, $lead_id );

		return is_wp_error( $validated ) ? $validated : $envelope;
	}

	/**
	 * Decode and strictly validate a persisted v1 job payload.
	 *
	 * @param array<string,mixed> $payload Decoded job payload.
	 * @param string              $expected_email_type Job type.
	 * @param int                 $receipt_id Job receipt id.
	 * @param int                 $lead_id Job lead id.
	 * @return array<string,mixed>|\WP_Error
	 */
	public static function decode( array $payload, $expected_email_type, $receipt_id, $lead_id ) {
		if ( ! self::has_only_keys( $payload, array( 'envelope' ) ) || ! is_array( $payload['envelope'] ?? null ) ) {
			return self::invalid( 'subin_notification_envelope_payload_invalid', __( 'The queued notification envelope is invalid.', 'submission-inbox' ) );
		}

		$envelope = self::upgrade_legacy_envelope( $payload['envelope'] );
		$validated = self::validate(
			$envelope,
			true,
			sanitize_key( self::scalar_string( $expected_email_type ) ),
			absint( $receipt_id ),
			absint( $lead_id )
		);

		return is_wp_error( $validated ) ? $validated : $envelope;
	}

	/**
	 * Add the safe defaults introduced after the original v1 queue rollout.
	 *
	 * Existing immutable jobs remain deliverable; unknown extra keys are still
	 * rejected by the exact-schema validator.
	 *
	 * @param array<string,mixed> $envelope Stored envelope.
	 * @return array<string,mixed>
	 */
	private static function upgrade_legacy_envelope( array $envelope ) {
		if ( is_array( $envelope['delivery'] ?? null ) && ! array_key_exists( 'from', $envelope['delivery'] ) ) {
			$envelope['delivery']['from'] = null;
		}
		if ( is_array( $envelope['meta'] ?? null ) ) {
			$rule_id = absint( $envelope['meta']['rule_id'] ?? 0 );
			if ( ! array_key_exists( 'rule_version', $envelope['meta'] ) ) {
				$envelope['meta']['rule_version'] = $rule_id > 0 ? 1 : 0;
			}
			if ( ! array_key_exists( 'rule_name', $envelope['meta'] ) ) {
				$envelope['meta']['rule_name'] = '';
			}
			if ( ! array_key_exists( 'template_name', $envelope['meta'] ) ) {
				$envelope['meta']['template_name'] = '';
			}
			if ( ! array_key_exists( 'delivery_mode', $envelope['meta'] ) ) {
				$envelope['meta']['delivery_mode'] = 'immediate';
			}
			if ( ! array_key_exists( 'scheduled_at', $envelope['meta'] ) ) {
				$submitted_at = is_array( $envelope['context'] ?? null )
					? (string) ( $envelope['context']['submitted_at'] ?? '' )
					: '';
				$envelope['meta']['scheduled_at'] = self::valid_mysql_datetime( $submitted_at )
					? $submitted_at
					: current_time( 'mysql' );
			}
		}

		return $envelope;
	}

	/**
	 * Validate the exact envelope schema and its draft/final identity.
	 *
	 * @param array<string,mixed> $envelope Envelope.
	 * @param bool                $final Whether ids and hashes must be final.
	 * @param string              $expected_email_type Expected type.
	 * @param int                 $expected_receipt_id Expected receipt id.
	 * @param int                 $expected_lead_id Expected lead id.
	 * @return true|\WP_Error
	 */
	private static function validate( array $envelope, $final, $expected_email_type, $expected_receipt_id, $expected_lead_id ) {
		if ( ! self::has_only_keys( $envelope, array( 'schema', 'version', 'email_type', 'delivery', 'context', 'meta' ) ) ) {
			return self::invalid( 'subin_notification_envelope_schema_invalid', __( 'The notification envelope schema is invalid.', 'submission-inbox' ) );
		}
		if ( self::SCHEMA !== ( $envelope['schema'] ?? null ) || self::VERSION !== ( $envelope['version'] ?? null ) ) {
			return self::invalid( 'subin_notification_envelope_version_unsupported', __( 'The notification envelope version is not supported.', 'submission-inbox' ) );
		}
		if ( ! is_array( $envelope['delivery'] ?? null ) || ! is_array( $envelope['context'] ?? null ) || ! is_array( $envelope['meta'] ?? null ) ) {
			return self::invalid( 'subin_notification_envelope_shape_invalid', __( 'The notification envelope shape is invalid.', 'submission-inbox' ) );
		}
		if ( ! self::has_only_keys( $envelope['delivery'], array( 'state', 'to', 'subject', 'body_html', 'from', 'reply_to', 'reason_code', 'reason_message' ) ) ) {
			return self::invalid( 'subin_notification_envelope_delivery_invalid', __( 'The notification delivery snapshot is invalid.', 'submission-inbox' ) );
		}
		if ( ! self::has_only_keys( $envelope['context'], array( 'source', 'provider', 'form_id', 'form_name', 'lead_id', 'receipt_id', 'submitted_at', 'delivery_key' ) ) ) {
			return self::invalid( 'subin_notification_envelope_context_invalid', __( 'The notification context snapshot is invalid.', 'submission-inbox' ) );
		}
		if ( ! self::has_only_keys( $envelope['meta'], array( 'rule_id', 'rule_name', 'rule_version', 'template_id', 'template_name', 'delivery_mode', 'scheduled_at', 'lead_id_token', 'body_sha256' ) ) ) {
			return self::invalid( 'subin_notification_envelope_meta_invalid', __( 'The notification envelope metadata is invalid.', 'submission-inbox' ) );
		}

		$email_type = $envelope['email_type'] ?? null;
		$sources = array(
			'admin_notification' => 'submission_inbox_form',
			'visitor_auto_reply'  => 'submission_inbox_auto_reply',
		);
		if ( ! is_string( $email_type ) || ! isset( $sources[ $email_type ] ) || $expected_email_type !== $email_type ) {
			return self::invalid( 'subin_notification_envelope_type_invalid', __( 'The notification envelope type is invalid.', 'submission-inbox' ) );
		}

		$delivery = $envelope['delivery'];
		$context  = $envelope['context'];
		$meta     = $envelope['meta'];
		foreach ( array( 'state', 'to', 'subject', 'body_html', 'reason_code', 'reason_message' ) as $key ) {
			if ( ! is_string( $delivery[ $key ] ?? null ) ) {
				return self::invalid( 'subin_notification_envelope_delivery_invalid', __( 'The notification delivery snapshot is invalid.', 'submission-inbox' ) );
			}
		}
		if ( self::has_header_break( $delivery['subject'] ) || $delivery['subject'] !== sanitize_text_field( $delivery['subject'] ) || strlen( $delivery['subject'] ) > 255 ) {
			return self::invalid( 'subin_notification_envelope_header_invalid', __( 'The notification subject is invalid.', 'submission-inbox' ) );
		}
		if ( strlen( $delivery['body_html'] ) > self::MAX_BODY_BYTES || self::unsafe_html( $delivery['body_html'] ) ) {
			return self::invalid( 'subin_notification_envelope_body_invalid', __( 'The notification body is invalid or too large.', 'submission-inbox' ) );
		}
		if ( null !== $delivery['from'] ) {
			if ( ! self::valid_mailbox( $delivery['from'] ) ) {
				return self::invalid( 'subin_notification_envelope_from_invalid', __( 'The notification From value is invalid.', 'submission-inbox' ) );
			}
		}
		if ( null !== $delivery['reply_to'] ) {
			if ( ! self::valid_mailbox( $delivery['reply_to'] ) ) {
				return self::invalid( 'subin_notification_envelope_reply_to_invalid', __( 'The notification Reply-To value is invalid.', 'submission-inbox' ) );
			}
		}

		$state = $delivery['state'];
		if ( ! in_array( $state, array( self::STATE_READY, self::STATE_NO_EMAIL_GENERATED ), true ) ) {
			return self::invalid( 'subin_notification_envelope_state_invalid', __( 'The notification delivery state is invalid.', 'submission-inbox' ) );
		}
		if ( self::STATE_READY === $state ) {
			if (
				! is_email( $delivery['to'] )
				|| sanitize_email( $delivery['to'] ) !== $delivery['to']
				|| '' === $delivery['subject']
				|| ! AutoReplyPreheader::has_meaningful_content( $delivery['body_html'] )
				|| '' !== $delivery['reason_code']
				|| '' !== $delivery['reason_message']
			) {
				return self::invalid( 'subin_notification_envelope_ready_invalid', __( 'The ready notification envelope is incomplete.', 'submission-inbox' ) );
			}
		} elseif (
			( '' !== $delivery['to'] && ( ! is_email( $delivery['to'] ) || sanitize_email( $delivery['to'] ) !== $delivery['to'] ) )
			|| '' !== $delivery['body_html']
			|| '' === $delivery['reason_code']
			|| '' === $delivery['reason_message']
		) {
			return self::invalid( 'subin_notification_envelope_no_email_invalid', __( 'The no-email notification outcome is incomplete.', 'submission-inbox' ) );
		}

		if (
			! is_string( $context['source'] ?? null )
			|| $sources[ $email_type ] !== $context['source']
			|| 'submission_inbox_form' !== ( $context['provider'] ?? null )
			|| ! is_string( $context['form_id'] ?? null )
			|| '' === $context['form_id']
			|| strlen( $context['form_id'] ) > 100
			|| sanitize_text_field( $context['form_id'] ) !== $context['form_id']
			|| ! is_string( $context['form_name'] ?? null )
			|| sanitize_text_field( $context['form_name'] ) !== $context['form_name']
			|| strlen( $context['form_name'] ) > 255
			|| ! is_string( $context['submitted_at'] ?? null )
			|| '' === $context['submitted_at']
			|| sanitize_text_field( $context['submitted_at'] ) !== $context['submitted_at']
			|| strlen( $context['submitted_at'] ) > 64
			|| ! is_int( $context['lead_id'] ?? null )
			|| ! is_int( $context['receipt_id'] ?? null )
			|| ! is_string( $context['delivery_key'] ?? null )
		) {
			return self::invalid( 'subin_notification_envelope_context_invalid', __( 'The notification context snapshot is invalid.', 'submission-inbox' ) );
		}
		if (
			! is_int( $meta['rule_id'] ?? null )
			|| ! is_string( $meta['rule_name'] ?? null )
			|| sanitize_text_field( $meta['rule_name'] ) !== $meta['rule_name']
			|| strlen( $meta['rule_name'] ) > 190
			|| ! is_int( $meta['rule_version'] ?? null )
			|| ! is_int( $meta['template_id'] ?? null )
			|| ! is_string( $meta['template_name'] ?? null )
			|| sanitize_text_field( $meta['template_name'] ) !== $meta['template_name']
			|| strlen( $meta['template_name'] ) > 255
			|| ! is_string( $meta['delivery_mode'] ?? null )
			|| ! in_array( $meta['delivery_mode'], array( 'immediate', 'delay' ), true )
			|| ! is_string( $meta['scheduled_at'] ?? null )
			|| ! self::valid_mysql_datetime( $meta['scheduled_at'] )
			|| ( $meta['rule_id'] > 0 && $meta['rule_version'] <= 0 )
			|| ! is_string( $meta['lead_id_token'] ?? null )
			|| ( '' !== $meta['lead_id_token'] && ! self::is_lead_id_token( $meta['lead_id_token'] ) )
			|| ! is_string( $meta['body_sha256'] ?? null )
		) {
			return self::invalid( 'subin_notification_envelope_meta_invalid', __( 'The notification envelope metadata is invalid.', 'submission-inbox' ) );
		}

		if ( $final ) {
			$expected_key  = hash( 'sha256', $expected_receipt_id . ':' . $expected_email_type );
			$expected_hash = self::STATE_READY === $state ? hash( 'sha256', $delivery['body_html'] ) : '';
			if (
				$expected_receipt_id <= 0
				|| $context['receipt_id'] !== $expected_receipt_id
				|| $context['lead_id'] !== $expected_lead_id
				|| $context['delivery_key'] !== $expected_key
				|| $meta['body_sha256'] !== $expected_hash
				|| '' !== $meta['lead_id_token']
			) {
				return self::invalid( 'subin_notification_envelope_identity_invalid', __( 'The finalized notification envelope identity is invalid.', 'submission-inbox' ) );
			}
		} elseif ( 0 !== $context['lead_id'] || 0 !== $context['receipt_id'] || '' !== $context['delivery_key'] || '' !== $meta['body_sha256'] ) {
			return self::invalid( 'subin_notification_envelope_draft_invalid', __( 'The notification envelope draft is already bound.', 'submission-inbox' ) );
		}

		$encoded = wp_json_encode( $envelope );
		if ( ! is_string( $encoded ) || strlen( $encoded ) > self::MAX_ENVELOPE_BYTES ) {
			return self::invalid( 'subin_notification_envelope_too_large', __( 'The notification envelope is too large.', 'submission-inbox' ) );
		}

		return true;
	}

	/**
	 * Return true when an associative array has exactly the allowed keys.
	 *
	 * @param array<string,mixed> $value Value.
	 * @param array<int,string>   $keys Expected keys.
	 * @return bool
	 */
	private static function has_only_keys( array $value, array $keys ) {
		$actual = array_keys( $value );
		sort( $actual );
		sort( $keys );

		return $actual === $keys;
	}

	/**
	 * Get a string only from scalar input.
	 *
	 * @param mixed $value Raw value.
	 * @return string
	 */
	private static function scalar_string( $value ) {
		return is_scalar( $value ) ? (string) $value : '';
	}

	/**
	 * Detect line breaks that could create additional mail headers.
	 *
	 * @param string $value Header value.
	 * @return bool
	 */
	private static function has_header_break( $value ) {
		return false !== strpos( (string) $value, "\r" ) || false !== strpos( (string) $value, "\n" );
	}

	/**
	 * Validate one immutable From/Reply-To mailbox.
	 *
	 * @param mixed $mailbox Mailbox value.
	 * @return bool
	 */
	private static function valid_mailbox( $mailbox ) {
		if ( ! is_array( $mailbox ) || ! self::has_only_keys( $mailbox, array( 'email', 'name' ) ) ) {
			return false;
		}

		$email = $mailbox['email'] ?? null;
		$name  = $mailbox['name'] ?? null;

		return is_string( $email )
			&& is_email( $email )
			&& sanitize_email( $email ) === $email
			&& is_string( $name )
			&& ! self::has_header_break( $name )
			&& sanitize_text_field( $name ) === $name
			&& strlen( $name ) <= 255;
	}

	/**
	 * Validate a local MySQL-style datetime without timezone coercion.
	 *
	 * @param mixed $value Datetime value.
	 * @return bool
	 */
	private static function valid_mysql_datetime( $value ) {
		if ( ! is_string( $value ) || 1 !== preg_match( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $value ) ) {
			return false;
		}

		$date = \DateTime::createFromFormat( '!Y-m-d H:i:s', $value );

		return $date instanceof \DateTime && $date->format( 'Y-m-d H:i:s' ) === $value;
	}

	/**
	 * Reject executable or active content if storage is corrupted upstream.
	 *
	 * @param string $html HTML body.
	 * @return bool
	 */
	private static function unsafe_html( $html ) {
		$html = (string) $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)\b/i', $tag )
				|| 1 === preg_match( '/\son[a-z][a-z0-9:_-]*\s*=/i', $tag )
			) {
				return true;
			}

			$url_matches = array();
			preg_match_all(
				'/\s(?:href|src|xlink:href|action|formaction|srcset)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+))/i',
				$tag,
				$url_matches,
				PREG_SET_ORDER
			);
			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 ( 1 === preg_match( '/(?:^|,)(?:javascript|data):/i', (string) $value ) ) {
					return true;
				}
			}

			$style_matches = array();
			preg_match_all( '/\sstyle\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+))/i', $tag, $style_matches, PREG_SET_ORDER );
			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*\(|url\s*\(\s*[\'"]?\s*(?:javascript|data)\s*:/i', $value ) ) {
					return true;
				}
			}
		}

		return false;
	}

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