<?php
/**
 * Auto-reply admin policy application service.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\AutoReplies;

use SUBIN\Forms\FormSubmissionContext;

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

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

/**
 * Owns the normalized admin contract for policy editing and safe testing.
 */
class AutoReplyPolicyService {
	const SCHEMA_VERSION = 1;
	const DESIGN_SCHEMA_VERSION = 2;
	const DESIGN_TYPE_STARTER = 'starter';
	const DESIGN_TYPE_SAVED = 'saved';
	const DESIGN_LIST_DEFAULT_PER_PAGE = 20;
	const DESIGN_LIST_MAX_PER_PAGE = 50;
	const DESIGN_LIST_MAX_PAGE = 1000;
	const DESIGN_SEARCH_MAX_LENGTH = 100;
	const PREHEADER_RENDER_MAX_BYTES = 2000;
	const TEST_RATE_LIMIT = 3;
	const TEST_RATE_WINDOW = 60;

	/** @var AutoReplyRuleRepository */
	private $rules;

	/** @var AutoReplyTemplateRepository */
	private $templates;

	/** @var AutoReplyFormProviderRegistry */
	private $providers;

	/** @var AutoReplyConditionEngine */
	private $conditions;

	/** @var AutoReplyMergeTagRenderer */
	private $renderer;

	/** @var FormSubmissionContext */
	private $context;

	/** @var AutoReplyTemplateLibrary */
	private $library;

	/**
	 * Constructor.
	 *
	 * @param AutoReplyRuleRepository|null      $rules Rules.
	 * @param AutoReplyTemplateRepository|null  $templates Templates.
	 * @param AutoReplyFormProviderRegistry|null $providers Providers.
	 * @param AutoReplyConditionEngine|null     $conditions Conditions.
	 * @param AutoReplyMergeTagRenderer|null    $renderer Renderer.
	 * @param FormSubmissionContext|null        $context Mail context.
	 * @param AutoReplyTemplateLibrary|null     $library Starter design library.
	 */
	public function __construct(
		?AutoReplyRuleRepository $rules = null,
		?AutoReplyTemplateRepository $templates = null,
		?AutoReplyFormProviderRegistry $providers = null,
		?AutoReplyConditionEngine $conditions = null,
		?AutoReplyMergeTagRenderer $renderer = null,
		?FormSubmissionContext $context = null,
		?AutoReplyTemplateLibrary $library = null
	) {
		$this->rules      = $rules ?: new AutoReplyRuleRepository();
		$this->templates  = $templates ?: new AutoReplyTemplateRepository();
		$this->providers  = $providers ?: new AutoReplyFormProviderRegistry();
		$this->conditions = $conditions ?: new AutoReplyConditionEngine();
		$this->renderer   = $renderer ?: new AutoReplyMergeTagRenderer();
		$this->context    = $context ?: new FormSubmissionContext();
		$this->library    = $library ?: new AutoReplyTemplateLibrary();
	}

	/**
	 * Build the editor's server-owned initial state.
	 *
	 * @param int    $rule_id Optional rule id.
	 * @param string $provider Requested provider.
	 * @param string $form_id Requested form.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function initial_state( $rule_id = 0, $provider = '', $form_id = '' ) {
		$rule_id = absint( $rule_id );
		$rule    = $rule_id > 0 ? $this->rules->find( $rule_id ) : null;
		if ( $rule_id > 0 && ! $rule ) {
			return $this->error( 'rule_not_found', __( 'Auto-reply rule not found.', 'submission-inbox' ), 404 );
		}

		$requested_provider = $this->rules->sanitize_provider( $provider );
		$requested_form_id  = sanitize_text_field( (string) $form_id );
		if ( $rule ) {
			$provider = '' !== $requested_provider ? $requested_provider : (string) $rule['provider'];
			$form_id  = '' !== $requested_form_id
				? $requested_form_id
				: ( $provider === (string) $rule['provider'] ? (string) $rule['form_id'] : '' );
		} else {
			$provider = '' !== $requested_provider ? $requested_provider : AutoReplyRuleRepository::PROVIDER_SUBMISSION_INBOX_FORM;
			$form_id  = $requested_form_id;
		}

		$forms  = $this->providers->list_forms( $provider );
		$fields = '' !== $form_id && $this->providers->get_form( $provider, $form_id )
			? $this->providers->list_fields( $provider, $form_id )
			: array();
		$starter_designs = $this->starter_design_choices();

		return array(
			'code'           => 'initial_state',
			'message'        => __( 'Auto-reply editor state loaded.', 'submission-inbox' ),
			'schema_version' => self::SCHEMA_VERSION,
			'design_schema_version' => self::DESIGN_SCHEMA_VERSION,
			'rule'           => $rule ? $this->resource_from_rule( $rule ) : $this->empty_resource( $provider, $form_id ),
			'providers'      => $this->providers->providers(),
			'forms'          => $forms,
			'fields'         => $fields,
			'email_fields'   => array_values(
				array_filter(
					$fields,
					function ( $field ) {
						return 'email' === ( $field['type'] ?? '' );
					}
				)
			),
			'templates'      => array(),
			'designs'        => $starter_designs,
			'operators'      => $this->operator_choices(),
			'merge_tags'     => $this->merge_tags( $fields ),
			'limits'         => array(
				'max_conditions'    => AutoReplyConditionEngine::MAX_CONDITIONS,
				'max_priority'      => AutoReplyRuleRepository::MAX_PRIORITY,
				'max_delay_seconds' => AutoReplyRuleRepository::MAX_DELAY_SECONDS,
				'max_body_bytes'    => AutoReplyDeliverySnapshot::MAX_BODY_BYTES,
			),
		);
	}

	/**
	 * Load one starter or saved design after it is selected in the picker.
	 *
	 * Initial-state choices intentionally contain metadata only. This method is
	 * the single bounded content read used by the editor, and re-sanitizes saved
	 * rows defensively in case a database value bypassed repository writes.
	 *
	 * @param string $type Design type: starter or saved.
	 * @param int    $id Saved template id.
	 * @param string $key Starter template key.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function load_design( $type, $id = 0, $key = '' ) {
		$type = sanitize_key( (string) $type );
		$id   = absint( $id );
		$key  = sanitize_key( (string) $key );

		if ( self::DESIGN_TYPE_STARTER === $type ) {
			if ( '' === $key ) {
				return $this->error( 'invalid_design_key', __( 'Choose a starter design.', 'submission-inbox' ), 400 );
			}
			$source = $this->library->get( $key );
			if ( ! is_array( $source ) ) {
				return $this->error( 'design_not_found', __( 'The selected email design was not found.', 'submission-inbox' ), 404 );
			}
			$design = array(
				'type' => self::DESIGN_TYPE_STARTER,
				'id'   => 0,
				'key'  => sanitize_key( (string) ( $source['key'] ?? $key ) ),
				'name' => sanitize_text_field( (string) ( $source['name'] ?? '' ) ),
				'body' => $this->sanitize_design_body( $source['html'] ?? '' ),
			);
		} elseif ( self::DESIGN_TYPE_SAVED === $type ) {
			if ( $id <= 0 ) {
				return $this->error( 'invalid_design_id', __( 'Choose a saved design.', 'submission-inbox' ), 400 );
			}
			$source = $this->templates->get_design_content( $id, AutoReplyDeliverySnapshot::MAX_BODY_BYTES );
			if ( ! is_array( $source ) ) {
				return $this->error( 'design_not_found', __( 'The selected email design was not found.', 'submission-inbox' ), 404 );
			}
			$body_source = AutoReplyTemplateRepository::effective_body( $source );
			$design = array(
				'type' => self::DESIGN_TYPE_SAVED,
				'id'   => $id,
				'key'  => sanitize_key( (string) ( $source['template_key'] ?? '' ) ),
				'name' => sanitize_text_field( (string) ( $source['name'] ?? '' ) ),
				'body' => $this->sanitize_design_body( $body_source ),
			);
		} else {
			return $this->error( 'invalid_design_type', __( 'Choose a starter or saved email design.', 'submission-inbox' ), 400 );
		}

		if ( strlen( $design['body'] ) > AutoReplyDeliverySnapshot::MAX_BODY_BYTES ) {
			return $this->error( 'design_too_large', __( 'The selected design is too large to load safely.', 'submission-inbox' ), 413 );
		}

		return array(
			'code'                  => 'design_loaded',
			'message'               => __( 'Email design loaded.', 'submission-inbox' ),
			'design_schema_version' => self::DESIGN_SCHEMA_VERSION,
			'design'                => $design,
		);
	}

	/**
	 * List a bounded, searchable page of saved design metadata.
	 *
	 * @param string $search Search term.
	 * @param int    $page Page number, starting at one.
	 * @param int    $per_page Page size.
	 * @return array<string,mixed>
	 */
	public function list_saved_designs( $search = '', $page = 1, $per_page = self::DESIGN_LIST_DEFAULT_PER_PAGE ) {
		$search = sanitize_text_field( $this->scalar( $search ) );
		if ( $this->length( $search ) > self::DESIGN_SEARCH_MAX_LENGTH ) {
			$search = function_exists( 'mb_substr' )
				? mb_substr( $search, 0, self::DESIGN_SEARCH_MAX_LENGTH )
				: substr( $search, 0, self::DESIGN_SEARCH_MAX_LENGTH );
		}

		$page     = min( self::DESIGN_LIST_MAX_PAGE, max( 1, absint( $page ) ) );
		$per_page = absint( $per_page );
		if ( $per_page <= 0 ) {
			$per_page = self::DESIGN_LIST_DEFAULT_PER_PAGE;
		}
		$per_page = min( self::DESIGN_LIST_MAX_PER_PAGE, $per_page );
		$filters  = array(
			'per_page' => $per_page,
			'offset'   => ( $page - 1 ) * $per_page,
		);
		if ( '' !== $search ) {
			$filters['search'] = $search;
		}

		$count_filters = '' !== $search ? array( 'search' => $search ) : array();
		$total         = $this->templates->count( $count_filters );

		return array(
			'code'                  => 'designs_listed',
			'message'               => __( 'Saved email designs loaded.', 'submission-inbox' ),
			'design_schema_version' => self::DESIGN_SCHEMA_VERSION,
			'designs'               => $this->saved_design_choices( $filters ),
			'pagination'            => array(
				'page'        => $page,
				'per_page'    => $per_page,
				'total'       => $total,
				'total_pages' => $total > 0 ? (int) ceil( $total / $per_page ) : 0,
			),
		);
	}

	/**
	 * Create or update a draft/published policy.
	 *
	 * @param array<string,mixed> $resource Canonical API resource.
	 * @param string              $intent save_draft or publish.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function save( array $resource, $intent ) {
		$intent = sanitize_key( (string) $intent );
		if ( ! in_array( $intent, array( 'save_draft', 'publish' ), true ) ) {
			return $this->error( 'invalid_intent', __( 'Choose whether to save a draft or publish the rule.', 'submission-inbox' ), 400 );
		}

		$normalized = $this->normalize_resource( $resource, $intent, true );
		if ( is_wp_error( $normalized ) ) {
			return $normalized;
		}

		$rule_id = absint( $normalized['id'] );
		if ( $rule_id > 0 ) {
			$current = $this->rules->find( $rule_id );
			if ( ! $current ) {
				return $this->error( 'rule_not_found', __( 'Auto-reply rule not found.', 'submission-inbox' ), 404 );
			}
			$expected = $this->strict_non_negative_integer( $resource['expected_version'] ?? null );
			if ( null === $expected ) {
				return $this->validation_error(
					array(
						'expected_version' => $this->field_error( 'required', __( 'Reload the rule before saving it.', 'submission-inbox' ) ),
					)
				);
			}
			if ( $expected !== absint( $current['rule_version'] ?? 1 ) ) {
				return $this->error(
					'rule_conflict',
					__( 'This rule changed in another browser session. Reload it before saving.', 'submission-inbox' ),
					409,
					array( 'current' => $this->resource_from_rule( $current ) )
				);
			}

			$updated = $this->rules->update_if_version( $rule_id, $expected, $normalized['storage'] );
			if ( is_wp_error( $updated ) ) {
				if ( 'rule_conflict' === $updated->get_error_code() ) {
					$latest = $this->rules->find( $rule_id );
					return $this->error(
						'rule_conflict',
						__( 'This rule changed in another browser session. Reload it before saving.', 'submission-inbox' ),
						409,
						array( 'current' => $latest ? $this->resource_from_rule( $latest ) : null )
					);
				}
				return $this->error( $updated->get_error_code(), $updated->get_error_message(), 'rule_not_found' === $updated->get_error_code() ? 404 : 500 );
			}
		} else {
			$rule_id = $this->rules->create( $normalized['storage'] );
			if ( $rule_id <= 0 ) {
				return $this->error( 'rule_not_saved', __( 'Auto-reply rule could not be saved.', 'submission-inbox' ), 500 );
			}
		}

		$saved = $this->rules->find( $rule_id );
		if ( ! $saved ) {
			return $this->error( 'rule_not_readable', __( 'The saved auto-reply rule could not be reloaded.', 'submission-inbox' ), 500 );
		}

		return array(
			'code'    => 'publish' === $intent ? 'rule_published' : 'rule_draft_saved',
			'message' => 'publish' === $intent
				? __( 'Auto-reply rule published.', 'submission-inbox' )
				: __( 'Auto-reply draft saved.', 'submission-inbox' ),
			'rule'    => $this->resource_from_rule( $saved ),
		);
	}

	/**
	 * Render a side-effect-free preview from unsaved resource data.
	 *
	 * @param array<string,mixed> $resource Resource.
	 * @param array<string,mixed> $sample Sample values.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function preview( array $resource, array $sample = array() ) {
		$prepared = $this->prepare_preview( $resource, $sample );
		if ( is_wp_error( $prepared ) ) {
			return $prepared;
		}

		return array_merge(
			array(
				'code'    => 'preview_ready',
				'message' => __( 'Auto-reply preview generated.', 'submission-inbox' ),
			),
			$prepared['preview']
		);
	}

	/**
	 * Evaluate conditions with server-owned form field types.
	 *
	 * @param array<string,mixed> $resource Resource.
	 * @param array<string,mixed> $sample Sample values.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function simulate( array $resource, array $sample = array() ) {
		$prepared = $this->prepare_preview( $resource, $sample );
		if ( is_wp_error( $prepared ) ) {
			return $prepared;
		}

		$result = $this->conditions->evaluate( $prepared['conditions'], $prepared['submitted_fields'] );

		return array(
			'code'            => 'simulation_ready',
			'message'         => __( 'Auto-reply conditions simulated without sending email.', 'submission-inbox' ),
			'matched'         => ! empty( $result['matched'] ),
			'valid'           => ! empty( $result['valid'] ),
			'mode'            => $result['mode'],
			'condition_count' => $result['condition_count'],
			'conditions'      => $result['conditions'],
			'errors'          => $result['errors'],
			'recipient_valid' => ! empty( $prepared['preview']['recipient_valid'] ),
		);
	}

	/**
	 * Send a manually requested test using the exact preview renderer.
	 *
	 * @param array<string,mixed> $resource Resource.
	 * @param array<string,mixed> $sample Sample values.
	 * @param string              $recipient Forced test recipient.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function send_test( array $resource, array $sample, $recipient ) {
		$recipient = sanitize_email( is_scalar( $recipient ) ? trim( (string) $recipient ) : '' );
		if ( ! is_email( $recipient ) ) {
			return $this->validation_error(
				array(
					'to' => $this->field_error( 'invalid_email', __( 'Enter a valid test recipient.', 'submission-inbox' ) ),
				)
			);
		}

		$prepared = $this->prepare_preview( $resource, $sample );
		if ( is_wp_error( $prepared ) ) {
			return $prepared;
		}
		$body = (string) $prepared['preview']['body_html'];
		if ( ! AutoReplyPreheader::has_meaningful_content( $body ) ) {
			return $this->validation_error(
				array(
					'body' => $this->field_error( 'missing_content', __( 'Add email content before sending a test.', 'submission-inbox' ) ),
				)
			);
		}
		if ( ! $this->claim_test_send() ) {
			return $this->error( 'test_rate_limited', __( 'Too many test emails were requested. Wait a minute and try again.', 'submission-inbox' ), 429 );
		}

		$storage = $prepared['storage'];
		$subject = sprintf(
			/* translators: %s: rendered email subject. */
			__( '[Test] %s', 'submission-inbox' ),
			(string) $prepared['preview']['subject']
		);
		$headers = array( 'Content-Type: text/html; charset=UTF-8' );
		if ( is_email( $storage['from_email'] ) ) {
			$headers[] = 'From: ' . $this->format_mailbox( $storage['from_email'], $storage['from_name'] );
		}
		if ( is_email( $storage['reply_to_email'] ) ) {
			$headers[] = 'Reply-To: ' . $this->format_mailbox( $storage['reply_to_email'], $storage['reply_to_name'] );
		}

		$mail_error = null;
		$failed_hook = function ( $error ) use ( &$mail_error ) {
			if ( is_wp_error( $error ) ) {
				$mail_error = $error;
			}
		};
		add_action( 'wp_mail_failed', $failed_hook );

		try {
			$this->context->set_current_lead_id( null );
			$this->context->set_current_source( 'submission_inbox_auto_reply', true );
			$this->context->set_delivery_metadata(
					array(
						'rule_id'        => absint( $prepared['id'] ),
						'rule_name'      => sanitize_text_field( $prepared['rule_name'] ?? '' ),
						'template_id'    => absint( $storage['template_id'] ),
						'template_name'  => sanitize_text_field( $prepared['template_name'] ?? '' ),
						'rule_version'   => $this->actual_rule_version( $prepared['id'] ),
						'provider'       => $storage['provider'],
						'form_id'        => $storage['form_id'],
						'form_name'      => sanitize_text_field( $prepared['form_name'] ?? '' ),
					'delivery_mode'  => $storage['delivery_mode'],
					'scheduled_at'   => current_time( 'mysql' ),
					'reason_code'    => 'manual_test',
				)
			);
			$this->context->set_current_from( $storage['from_email'], $storage['from_name'] );
			$sent = wp_mail( $recipient, $subject, $body, $headers );
		} catch ( \Throwable $throwable ) {
			$sent       = false;
			$mail_error = new \WP_Error( 'test_mail_exception', $throwable->getMessage() );
		} finally {
			remove_action( 'wp_mail_failed', $failed_hook );
			$this->context->clear();
		}

		if ( ! $sent ) {
			unset( $mail_error );
			return $this->error(
				'test_failed',
				__( 'The test email could not be sent. Open Email Log for the delivery error.', 'submission-inbox' ),
				502
			);
		}

		return array(
			'code'      => 'test_sent',
			'message'   => __( 'Test email sent. Delivery details are available in Email Log.', 'submission-inbox' ),
			'recipient' => $recipient,
		);
	}

	/**
	 * Normalize and validate one API resource.
	 *
	 * @param array<string,mixed> $resource Resource.
	 * @param string              $intent Intent.
	 * @param bool                $for_save Require persistence-specific fields.
	 * @return array<string,mixed>|\WP_Error
	 */
	private function normalize_resource( array $resource, $intent, $for_save ) {
		$errors   = array();
		$id_value = $this->strict_non_negative_integer( $resource['id'] ?? 0 );
		$id       = null === $id_value ? 0 : $id_value;
		$name     = sanitize_text_field( $this->scalar( $resource['name'] ?? '' ) );
		$provider = $this->rules->sanitize_provider( $this->scalar( $resource['provider'] ?? '' ) );
		$form_id  = sanitize_text_field( $this->scalar( $resource['form_id'] ?? '' ) );

		if ( $for_save && '' === $name ) {
			$errors['name'] = $this->field_error( 'required', __( 'Enter a rule name.', 'submission-inbox' ) );
		} elseif ( $this->length( $name ) > 190 ) {
			$errors['name'] = $this->field_error( 'too_long', __( 'Rule name must be 190 characters or fewer.', 'submission-inbox' ) );
		}
		if ( null === $id_value ) {
			$errors['id'] = $this->field_error( 'invalid_id', __( 'Rule id is invalid.', 'submission-inbox' ) );
		}
		if ( '' === $provider ) {
			$errors['provider'] = $this->field_error( 'invalid_provider', __( 'Select a supported form provider.', 'submission-inbox' ) );
		}
		$form = '' !== $provider && '' !== $form_id ? $this->providers->get_form( $provider, $form_id ) : null;
		if ( ! $form ) {
			$errors['form_id'] = $this->field_error( 'invalid_form', __( 'Select an existing form.', 'submission-inbox' ) );
		}

		$email_key   = sanitize_key( $this->scalar( $resource['visitor_email_field'] ?? '' ) );
		$email_field = $form ? $this->providers->get_email_field( $provider, $form_id, $email_key ) : null;
		if ( ! $email_field && ( 'publish' === $intent || '' !== $email_key ) ) {
			$errors['visitor_email_field'] = $this->field_error( 'invalid_email_field', __( 'Select an Email field from this form.', 'submission-inbox' ) );
		}

		$template_value = $this->strict_non_negative_integer( $resource['template_id'] ?? 0 );
		$template_id    = null === $template_value ? 0 : $template_value;
		if ( null === $template_value ) {
			$errors['template_id'] = $this->field_error( 'invalid_template', __( 'Template id is invalid.', 'submission-inbox' ) );
		}
		$template    = $template_id > 0
			? $this->templates->get_design_content( $template_id, AutoReplyDeliverySnapshot::MAX_BODY_BYTES )
			: null;
		if ( $template_id > 0 && ! $template ) {
			$errors['template_id'] = $this->field_error( 'template_not_found', __( 'The selected auto-reply template no longer exists.', 'submission-inbox' ) );
		}

		$priority = $this->strict_non_negative_integer( $resource['priority'] ?? AutoReplyRuleRepository::DEFAULT_PRIORITY );
		if ( null === $priority || $priority > AutoReplyRuleRepository::MAX_PRIORITY ) {
			$errors['priority'] = $this->field_error( 'invalid_priority', __( 'Priority must be a whole number within the supported range.', 'submission-inbox' ) );
			$priority = AutoReplyRuleRepository::DEFAULT_PRIORITY;
		}

		$condition_match = sanitize_key( $this->scalar( $resource['condition_match'] ?? AutoReplyRuleRepository::MATCH_ALL ) );
		$raw_conditions  = $resource['conditions'] ?? array();
		if ( is_array( $raw_conditions ) && isset( $raw_conditions['mode'] ) ) {
			$condition_match = sanitize_key( $this->scalar( $raw_conditions['mode'] ) );
		}
		$condition_document = is_array( $raw_conditions ) && array_key_exists( 'conditions', $raw_conditions )
			? $raw_conditions
			: array(
				'version'    => AutoReplyConditionEngine::VERSION,
				'mode'       => $condition_match,
				'conditions' => is_array( $raw_conditions ) ? $raw_conditions : null,
			);
		foreach ( $this->conditions->validate_conditions( $condition_document, $condition_match ) as $condition_error ) {
			$path = (string) ( $condition_error['path'] ?? 'conditions' );
			$errors[ $path ] = $this->field_error(
				(string) ( $condition_error['code'] ?? 'invalid_condition' ),
				(string) ( $condition_error['message'] ?? __( 'Condition is invalid.', 'submission-inbox' ) )
			);
		}
		$clean_conditions = $this->conditions->sanitize_conditions( $condition_document, $condition_match );

		$schema_fields = array();
		if ( $form ) {
			foreach ( $this->providers->list_fields( $provider, $form_id ) as $schema_field ) {
				$schema_fields[ $schema_field['key'] ] = $schema_field;
			}
		}
		foreach ( $clean_conditions['conditions'] as $index => $condition ) {
			$field = $schema_fields[ $condition['field'] ] ?? null;
			if ( ! $field ) {
				$errors[ 'conditions.' . $index . '.field' ] = $this->field_error( 'field_not_found', __( 'Select a field from this form.', 'submission-inbox' ) );
			} elseif ( $field['type'] !== $condition['type'] ) {
				$errors[ 'conditions.' . $index . '.type' ] = $this->field_error( 'field_type_mismatch', __( 'The condition field type does not match the saved form schema.', 'submission-inbox' ) );
			} elseif ( ! empty( $field['options'] ) && in_array( $field['type'], array( 'choice', 'select', 'radio', 'checkbox' ), true ) ) {
				$allowed = wp_list_pluck( $field['options'], 'value' );
				$values  = is_array( $condition['value'] ) ? $condition['value'] : array( $condition['value'] );
				foreach ( $values as $value ) {
					if ( null !== $value && ! in_array( (string) $value, $allowed, true ) ) {
						$errors[ 'conditions.' . $index . '.value' ] = $this->field_error( 'option_not_found', __( 'Select a value that still exists in the form field.', 'submission-inbox' ) );
						break;
					}
				}
			}
		}

		$from     = is_array( $resource['from'] ?? null ) ? $resource['from'] : array();
		$reply_to = is_array( $resource['reply_to'] ?? null ) ? $resource['reply_to'] : array();
		$from_name       = $this->validate_header_name( $from['name'] ?? $resource['from_name'] ?? '', 'from.name', $errors );
		$from_email      = $this->validate_header_email( $from['email'] ?? $resource['from_email'] ?? '', 'from.email', $errors );
		$reply_to_name   = $this->validate_header_name( $reply_to['name'] ?? $resource['reply_to_name'] ?? '', 'reply_to.name', $errors );
		$reply_to_email  = $this->validate_header_email( $reply_to['email'] ?? $resource['reply_to_email'] ?? '', 'reply_to.email', $errors );
		if ( '' !== $from_name && '' === $from_email ) {
			$errors['from.email'] = $this->field_error( 'email_required', __( 'Enter a From email when a From name is set.', 'submission-inbox' ) );
		}
		if ( '' !== $reply_to_name && '' === $reply_to_email ) {
			$errors['reply_to.email'] = $this->field_error( 'email_required', __( 'Enter a Reply-To email when a Reply-To name is set.', 'submission-inbox' ) );
		}

		$delivery      = is_array( $resource['delivery'] ?? null ) ? $resource['delivery'] : array();
		$delivery_mode = sanitize_key( $this->scalar( $delivery['mode'] ?? $resource['delivery_mode'] ?? AutoReplyRuleRepository::DELIVERY_IMMEDIATE ) );
		$delay_raw     = $delivery['delay_seconds'] ?? $resource['delay_seconds'] ?? 0;
		$delay_seconds = $this->strict_non_negative_integer( $delay_raw );
		if ( AutoReplyRuleRepository::DELIVERY_DELAY === $delivery_mode ) {
			if ( null === $delay_seconds || $delay_seconds <= 0 || $delay_seconds > AutoReplyRuleRepository::MAX_DELAY_SECONDS ) {
				$errors['delivery.delay_seconds'] = $this->field_error( 'invalid_delay', __( 'Delay must be between 1 second and 7 days.', 'submission-inbox' ) );
				$delay_seconds = 0;
			}
		} elseif ( AutoReplyRuleRepository::DELIVERY_IMMEDIATE === $delivery_mode ) {
			$delay_seconds = 0;
		} else {
			$errors['delivery.mode'] = $this->field_error( 'invalid_delivery_mode', __( 'Choose immediate or delayed delivery.', 'submission-inbox' ) );
			$delivery_mode = AutoReplyRuleRepository::DELIVERY_IMMEDIATE;
			$delay_seconds = 0;
		}

		$subject   = sanitize_text_field( $this->scalar( $resource['subject'] ?? '' ) );
		$preheader = sanitize_textarea_field( $this->scalar( $resource['preheader'] ?? '' ) );
		$body      = $this->scalar( $resource['body'] ?? '' );
		if ( strlen( $subject ) > AutoReplyDeliverySnapshot::MAX_SUBJECT_BYTES ) {
			$errors['subject'] = $this->field_error( 'too_long', __( 'Subject must be 255 bytes or fewer.', 'submission-inbox' ) );
		}
		if ( AutoReplyPreheader::length( $preheader ) > AutoReplyPreheader::MAX_CHARACTERS ) {
			$errors['preheader'] = $this->field_error( 'too_long', __( 'Preview text must be 500 characters or fewer.', 'submission-inbox' ) );
		}
		if ( strlen( $body ) > AutoReplyDeliverySnapshot::MAX_BODY_BYTES ) {
			$errors['body'] = $this->field_error( 'too_large', __( 'The email body is too large to save safely.', 'submission-inbox' ) );
		}
		$status  = 'publish' === $intent ? AutoReplyRuleRepository::STATUS_PUBLISHED : AutoReplyRuleRepository::STATUS_DRAFT;
		$enabled = 'publish' === $intent && ! empty( $resource['enabled'] ) ? 1 : 0;

		$storage = $this->rules->sanitize_data(
			array(
				'name'                      => $name,
				'provider'                  => $provider,
				'form_id'                   => $form ? $form['id'] : $form_id,
				'form_name'                 => $form ? $form['label'] : '',
				'visitor_email_field'       => $email_field ? $email_field['key'] : $email_key,
				'visitor_email_field_label' => $email_field ? $email_field['label'] : '',
				'template_id'               => $template_id,
				'enabled'                   => $enabled,
				'status'                    => $status,
				'priority'                  => $priority,
				'condition_match'           => $clean_conditions['mode'],
				'conditions'                => $clean_conditions,
				'subject'                   => $subject,
				'preheader'                 => $preheader,
				'body'                      => $body,
				'from_name'                 => $from_name,
				'from_email'                => $from_email,
				'reply_to_name'             => $reply_to_name,
				'reply_to_email'            => $reply_to_email,
				'delivery_mode'             => $delivery_mode,
				'delay_seconds'             => $delay_seconds,
			)
		);
		if ( strlen( (string) ( $storage['body'] ?? '' ) ) > AutoReplyDeliverySnapshot::MAX_BODY_BYTES ) {
			$errors['body'] = $this->field_error( 'too_large', __( 'The sanitized email body is too large to save safely.', 'submission-inbox' ) );
		}

		if ( 'publish' === $intent ) {
			$subject_source = '' !== trim( (string) $storage['subject'] )
				? (string) $storage['subject']
				: (string) ( $template['subject'] ?? '' );
			$body_source = '' !== trim( (string) $storage['body'] )
				? (string) $storage['body']
				: AutoReplyTemplateRepository::effective_body( $template );
			$preheader_source = '' !== trim( (string) ( $storage['preheader'] ?? '' ) )
				? (string) $storage['preheader']
				: (string) ( $template['preheader'] ?? '' );
			$delivery_body = AutoReplyPreheader::inject( $body_source, AutoReplyPreheader::normalize_text( $preheader_source ) );
			if ( strlen( sanitize_text_field( $subject_source ) ) > AutoReplyDeliverySnapshot::MAX_SUBJECT_BYTES ) {
				$errors['subject'] = $this->field_error( 'too_long', __( 'The effective email subject must be 255 bytes or fewer.', 'submission-inbox' ) );
			}
			if ( strlen( $delivery_body ) > AutoReplyDeliverySnapshot::MAX_BODY_BYTES ) {
				$errors['body'] = $this->field_error( 'too_large', __( 'The effective email body is too large to deliver safely.', 'submission-inbox' ) );
			}
			if ( ! AutoReplyPreheader::has_meaningful_content( $body_source ) ) {
				$errors['body'] = $this->field_error( 'missing_content', __( 'Add rule content or choose a template before publishing.', 'submission-inbox' ) );
			}
		}

		if ( ! empty( $errors ) ) {
			return $this->validation_error( $errors );
		}

		return array(
			'id'         => $id,
			'storage'    => $storage,
			'form'       => $form,
			'template'   => $template,
			'fields'     => array_values( $schema_fields ),
			'conditions' => $clean_conditions,
		);
	}

	/**
	 * Build shared preview data.
	 *
	 * @param array<string,mixed> $resource Resource.
	 * @param array<string,mixed> $sample Sample.
	 * @return array<string,mixed>|\WP_Error
	 */
	private function prepare_preview( array $resource, array $sample ) {
		$normalized = $this->normalize_resource( $resource, 'preview', false );
		if ( is_wp_error( $normalized ) ) {
			return $normalized;
		}

		$storage      = $normalized['storage'];
		$template     = $normalized['template'];
		$sample_map   = is_array( $sample['fields'] ?? null ) ? $sample['fields'] : array();
		$field_values = array();
		$submitted    = array();
		foreach ( $normalized['fields'] as $field ) {
			$key = $field['key'];
			if ( ! array_key_exists( $key, $sample_map ) ) {
				continue;
			}
			$value = $this->sanitize_sample_value( $sample_map[ $key ] );
			$field_values[ $key ] = $value;
			$submitted[] = array(
				'field_key'   => $key,
				'field_type'  => $field['type'],
				'field_value' => $value,
			);
		}

		$recipient_value = $field_values[ $storage['visitor_email_field'] ] ?? $sample['visitor_email'] ?? '';
		$recipient       = sanitize_email( $this->scalar_value( $recipient_value ) );
		$context         = array(
			'site_name'       => get_bloginfo( 'name' ),
			'form_name'       => (string) $normalized['form']['label'],
			'submission_date' => wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), current_time( 'timestamp' ) ),
			'lead_id'         => __( 'Preview', 'submission-inbox' ),
			'visitor_email'   => is_email( $recipient ) ? $recipient : '',
		);
		$subject_source = '' !== trim( (string) $storage['subject'] )
			? (string) $storage['subject']
			: (string) ( $template['subject'] ?? '' );
		$body_source = '' !== trim( (string) $storage['body'] )
			? (string) $storage['body']
			: AutoReplyTemplateRepository::effective_body( $template );
		$preheader_source = '' !== trim( (string) ( $storage['preheader'] ?? '' ) )
			? (string) $storage['preheader']
			: (string) ( $template['preheader'] ?? '' );
		$subject_result = $this->renderer->render_subject( $subject_source, $context, $field_values, AutoReplyDeliverySnapshot::MAX_SUBJECT_BYTES );
		$preheader_result = $this->renderer->render_subject( $preheader_source, $context, $field_values, self::PREHEADER_RENDER_MAX_BYTES );
		$body_result    = $this->renderer->render_html( $body_source, $context, $field_values, AutoReplyDeliverySnapshot::MAX_BODY_BYTES );
		$render_errors  = array();
		if ( ! empty( $subject_result['overflowed'] ) ) {
			$render_errors['subject'] = $this->field_error( 'rendered_too_long', __( 'The rendered subject is too long. Shorten its text or sample variables.', 'submission-inbox' ) );
		}
		if ( ! empty( $preheader_result['overflowed'] ) ) {
			$render_errors['preheader'] = $this->field_error( 'rendered_too_long', __( 'The rendered preview text is too long. Shorten its text or sample variables.', 'submission-inbox' ) );
		}
		if ( ! empty( $body_result['overflowed'] ) ) {
			$render_errors['body'] = $this->field_error( 'rendered_too_large', __( 'The rendered email body is too large to deliver safely.', 'submission-inbox' ) );
		}
		if ( ! empty( $render_errors ) ) {
			return $this->validation_error( $render_errors );
		}
		$subject        = (string) ( $subject_result['content'] ?? '' );
		$preheader      = AutoReplyPreheader::normalize_text( (string) ( $preheader_result['content'] ?? '' ) );
		$rendered_body  = AutoReplyPreheader::inject( (string) ( $body_result['content'] ?? '' ), $preheader );
		if ( '' === $subject ) {
			$subject = __( 'We received your request', 'submission-inbox' );
		}
		if ( strlen( $subject ) > AutoReplyDeliverySnapshot::MAX_SUBJECT_BYTES ) {
			return $this->validation_error(
				array(
					'subject' => $this->field_error( 'rendered_too_long', __( 'The rendered subject is too long. Shorten its text or sample variables.', 'submission-inbox' ) ),
				)
			);
		}
		if ( strlen( $rendered_body ) > AutoReplyDeliverySnapshot::MAX_BODY_BYTES ) {
			return $this->validation_error(
				array(
					'body' => $this->field_error( 'rendered_too_large', __( 'The rendered email body is too large to deliver safely.', 'submission-inbox' ) ),
				)
			);
		}

		$warnings = array();
		if ( ! is_email( $recipient ) ) {
			$warnings[] = array( 'code' => 'invalid_recipient', 'message' => __( 'The sample visitor email is not valid.', 'submission-inbox' ) );
		}
		foreach ( array( 'missing', 'unknown' ) as $diagnostic ) {
			$tokens = array_merge(
				(array) ( $subject_result['diagnostics'][ $diagnostic ] ?? array() ),
				(array) ( $preheader_result['diagnostics'][ $diagnostic ] ?? array() ),
				(array) ( $body_result['diagnostics'][ $diagnostic ] ?? array() )
			);
			if ( ! empty( $tokens ) ) {
				$warnings[] = array(
					'code'    => 'merge_tags_' . $diagnostic,
					'message' => implode( ', ', array_values( array_unique( $tokens ) ) ),
				);
			}
		}

		return array(
			'id'               => $normalized['id'],
			'rule_name'        => sanitize_text_field( $storage['name'] ?? '' ),
			'template_name'    => sanitize_text_field( $template['name'] ?? '' ),
			'form_name'        => sanitize_text_field( $normalized['form']['label'] ?? '' ),
			'storage'          => $storage,
			'conditions'       => $normalized['conditions'],
			'submitted_fields' => $submitted,
			'preview'          => array(
				'subject'         => $subject,
				'preheader'       => $preheader,
				'body_html'       => $rendered_body,
				'recipient'       => is_email( $recipient ) ? $recipient : '',
				'recipient_valid' => is_email( $recipient ),
				'diagnostics'     => array(
					'subject' => $subject_result['diagnostics'],
					'preheader' => $preheader_result['diagnostics'],
					'body'    => $body_result['diagnostics'],
				),
				'warnings'        => $warnings,
				'delivery'        => array(
					'mode'          => $storage['delivery_mode'],
					'delay_seconds' => absint( $storage['delay_seconds'] ),
				),
			),
		);
	}

	/**
	 * Convert a repository row to the public resource contract.
	 *
	 * @param array<string,mixed> $rule Rule.
	 * @return array<string,mixed>
	 */
	public function resource_from_rule( array $rule ) {
		$conditions = $rule['conditions_document'] ?? array(
			'version'    => AutoReplyConditionEngine::VERSION,
			'mode'       => $rule['condition_match'] ?? AutoReplyRuleRepository::MATCH_ALL,
			'conditions' => $rule['conditions'] ?? array(),
		);
		unset( $conditions['valid'] );

		return array(
			'id'                    => absint( $rule['id'] ?? 0 ),
			'expected_version'      => max( 1, absint( $rule['rule_version'] ?? 1 ) ),
			'name'                  => (string) ( $rule['name'] ?? '' ),
			'provider'              => (string) ( $rule['provider'] ?? '' ),
			'form_id'               => (string) ( $rule['form_id'] ?? '' ),
			'visitor_email_field'   => (string) ( $rule['visitor_email_field'] ?? '' ),
			'template_id'           => absint( $rule['template_id'] ?? 0 ),
			'status'                => (string) ( $rule['status'] ?? AutoReplyRuleRepository::STATUS_DRAFT ),
			'enabled'               => ! empty( $rule['enabled'] ),
			'priority'              => absint( $rule['priority'] ?? AutoReplyRuleRepository::DEFAULT_PRIORITY ),
			'condition_match'       => (string) ( $rule['condition_match'] ?? AutoReplyRuleRepository::MATCH_ALL ),
			'conditions'            => $conditions,
			'subject'               => (string) ( $rule['subject'] ?? '' ),
			'preheader'             => (string) ( $rule['preheader'] ?? '' ),
			'body'                  => (string) ( $rule['body'] ?? '' ),
			'from'                  => array(
				'name'  => (string) ( $rule['from_name'] ?? '' ),
				'email' => (string) ( $rule['from_email'] ?? '' ),
			),
			'reply_to'              => array(
				'name'  => (string) ( $rule['reply_to_name'] ?? '' ),
				'email' => (string) ( $rule['reply_to_email'] ?? '' ),
			),
			'delivery'              => array(
				'mode'          => (string) ( $rule['delivery_mode'] ?? AutoReplyRuleRepository::DELIVERY_IMMEDIATE ),
				'delay_seconds' => absint( $rule['delay_seconds'] ?? 0 ),
			),
		);
	}

	/**
	 * Default unsaved resource.
	 *
	 * @param string $provider Provider.
	 * @param string $form_id Form id.
	 * @return array<string,mixed>
	 */
	private function empty_resource( $provider, $form_id ) {
		return array(
			'id'                  => 0,
			'expected_version'    => 0,
			'name'                => '',
			'provider'            => $provider,
			'form_id'             => $form_id,
			'visitor_email_field' => '',
			'template_id'         => 0,
			'status'              => AutoReplyRuleRepository::STATUS_DRAFT,
			'enabled'             => false,
			'priority'            => AutoReplyRuleRepository::DEFAULT_PRIORITY,
			'condition_match'     => AutoReplyRuleRepository::MATCH_ALL,
			'conditions'          => array(
				'version'    => AutoReplyConditionEngine::VERSION,
				'mode'       => AutoReplyRuleRepository::MATCH_ALL,
				'conditions' => array(),
			),
			'subject'             => '',
			'preheader'           => '',
			'body'                => '',
			'from'                => array( 'name' => '', 'email' => '' ),
			'reply_to'            => array( 'name' => '', 'email' => '' ),
			'delivery'            => array( 'mode' => AutoReplyRuleRepository::DELIVERY_IMMEDIATE, 'delay_seconds' => 0 ),
		);
	}

	/** @return array<int,array<string,mixed>> */
	private function saved_design_choices( array $args = array() ) {
		$args = array_merge( array( 'per_page' => self::DESIGN_LIST_DEFAULT_PER_PAGE, 'offset' => 0 ), $args );
		$choices = array();
		foreach ( $this->templates->get_choice_metadata( $args ) as $template ) {
			$choices[] = array(
				'type'        => self::DESIGN_TYPE_SAVED,
				'id'          => absint( $template['id'] ?? 0 ),
				'key'         => sanitize_key( (string) ( $template['template_key'] ?? '' ) ),
				'name'        => sanitize_text_field( (string) ( $template['name'] ?? '' ) ),
				'description' => '',
				'status'      => sanitize_key( (string) ( $template['status'] ?? '' ) ),
			);
		}
		return $choices;
	}

	/** @return array<int,array<string,mixed>> */
	private function starter_design_choices() {
		$choices = array();
		foreach ( $this->library->all() as $template ) {
			$choices[] = array(
				'type'        => self::DESIGN_TYPE_STARTER,
				'id'          => 0,
				'key'         => sanitize_key( (string) ( $template['key'] ?? '' ) ),
				'name'        => sanitize_text_field( (string) ( $template['name'] ?? '' ) ),
				'description' => sanitize_text_field( (string) ( $template['description'] ?? '' ) ),
				'status'      => self::DESIGN_TYPE_STARTER,
			);
		}
		return $choices;
	}

	/**
	 * Sanitize one selected design body with the delivery allowlist.
	 *
	 * @param mixed $body Raw HTML.
	 * @return string
	 */
	private function sanitize_design_body( $body ) {
		$body = is_string( $body ) ? $body : '';
		if ( strlen( $body ) > AutoReplyDeliverySnapshot::MAX_BODY_BYTES ) {
			return $body;
		}
		$body = preg_replace( '/<\?(?:php)?[\s\S]*?\?>/i', '', $body );
		$body = strip_shortcodes( (string) $body );
		$body = wp_kses( $body, AutoReplyTemplateRepository::allowed_email_html(), array( 'http', 'https', 'mailto', 'tel' ) );

		return AutoReplyTemplateRepository::sanitize_svg_paint_attributes( $body );
	}

	/** @return array<string,array<int,array<string,mixed>>> */
	private function operator_choices() {
		$choices = array();
		foreach ( array( 'text', 'email', 'url', 'tel', 'textarea', 'choice', 'select', 'radio', 'checkbox', 'consent', 'number', 'date', 'datetime', 'time' ) as $type ) {
			$choices[ $type ] = array();
			foreach ( $this->conditions->operators_for_type( $type ) as $operator ) {
				$choices[ $type ][] = array(
					'key'         => $operator,
					'label'       => $this->operator_label( $operator ),
					'needs_value' => ! in_array( $operator, array( 'is_empty', 'is_not_empty' ), true ),
					'value_shape' => in_array( $operator, array( 'between', 'not_between' ), true ) ? 'range' : 'single',
				);
			}
		}
		return $choices;
	}

	/** @param string $operator Operator key. @return string */
	private function operator_label( $operator ) {
		$labels = array(
			'equals'                => __( 'is', 'submission-inbox' ),
			'not_equals'            => __( 'is not', 'submission-inbox' ),
			'contains'              => __( 'contains', 'submission-inbox' ),
			'not_contains'          => __( 'does not contain', 'submission-inbox' ),
			'starts_with'           => __( 'starts with', 'submission-inbox' ),
			'ends_with'             => __( 'ends with', 'submission-inbox' ),
			'is_empty'              => __( 'is empty', 'submission-inbox' ),
			'is_not_empty'          => __( 'is not empty', 'submission-inbox' ),
			'in'                    => __( 'is one of', 'submission-inbox' ),
			'not_in'                => __( 'is not one of', 'submission-inbox' ),
			'greater_than'          => __( 'is greater than', 'submission-inbox' ),
			'greater_than_or_equal' => __( 'is at least', 'submission-inbox' ),
			'less_than'             => __( 'is less than', 'submission-inbox' ),
			'less_than_or_equal'    => __( 'is at most', 'submission-inbox' ),
			'before'                => __( 'is before', 'submission-inbox' ),
			'after'                 => __( 'is after', 'submission-inbox' ),
			'on_or_before'          => __( 'is on or before', 'submission-inbox' ),
			'on_or_after'           => __( 'is on or after', 'submission-inbox' ),
			'between'               => __( 'is between', 'submission-inbox' ),
			'not_between'           => __( 'is not between', 'submission-inbox' ),
		);

		return $labels[ $operator ] ?? sanitize_text_field( ucwords( str_replace( '_', ' ', $operator ) ) );
	}

	/** @param array<int,array<string,mixed>> $fields Fields. @return array<string,array<int,array<string,string>>> */
	private function merge_tags( array $fields ) {
		$tags = array(
			'general' => array(
				array( 'tag' => '{site_name}', 'label' => __( 'Site name', 'submission-inbox' ) ),
				array( 'tag' => '{form_name}', 'label' => __( 'Form name', 'submission-inbox' ) ),
				array( 'tag' => '{submission_date}', 'label' => __( 'Submission date', 'submission-inbox' ) ),
				array( 'tag' => '{lead_id}', 'label' => __( 'Lead ID', 'submission-inbox' ) ),
				array( 'tag' => '{visitor_email}', 'label' => __( 'Visitor email', 'submission-inbox' ) ),
			),
			'fields'  => array(),
		);
		foreach ( $fields as $field ) {
			$tags['fields'][] = array(
				'tag'   => '{field:' . $field['key'] . '}',
				'label' => $field['label'],
			);
		}
		return $tags;
	}

	/** @param mixed $value Raw. @param string $path Path. @param array<string,mixed> $errors Errors. @return string */
	private function validate_header_name( $value, $path, array &$errors ) {
		$value = $this->scalar( $value );
		if ( false !== strpos( $value, "\r" ) || false !== strpos( $value, "\n" ) ) {
			$errors[ $path ] = $this->field_error( 'invalid_header', __( 'Email header names cannot contain line breaks.', 'submission-inbox' ) );
			return '';
		}
		$value = sanitize_text_field( $value );
		if ( strlen( $value ) > AutoReplyDeliverySnapshot::MAX_MAILBOX_NAME_BYTES ) {
			$errors[ $path ] = $this->field_error( 'too_long', __( 'Email header names must be 255 bytes or fewer.', 'submission-inbox' ) );
			return '';
		}
		return $value;
	}

	/** @param mixed $value Raw. @param string $path Path. @param array<string,mixed> $errors Errors. @return string */
	private function validate_header_email( $value, $path, array &$errors ) {
		$value = trim( $this->scalar( $value ) );
		if ( '' === $value ) {
			return '';
		}
		if ( false !== strpos( $value, "\r" ) || false !== strpos( $value, "\n" ) || ! is_email( $value ) ) {
			$errors[ $path ] = $this->field_error( 'invalid_email', __( 'Enter a valid email address without line breaks.', 'submission-inbox' ) );
			return '';
		}
		return sanitize_email( $value );
	}

	/** @param mixed $value Value. @return int|null */
	private function strict_non_negative_integer( $value ) {
		if ( ! is_int( $value ) && ! is_string( $value ) ) {
			return null;
		}
		$value = trim( (string) $value );
		if ( '' === $value || ! preg_match( '/^[0-9]+$/', $value ) ) {
			return null;
		}
		return (int) $value;
	}

	/** @param mixed $value Value. @return string */
	private function scalar( $value ) {
		return is_scalar( $value ) ? (string) $value : '';
	}

	/** @param string $value Value. @return int */
	private function length( $value ) {
		return function_exists( 'mb_strlen' ) ? mb_strlen( (string) $value ) : strlen( (string) $value );
	}

	/** @param mixed $value Value. @return string */
	private function scalar_value( $value ) {
		if ( is_scalar( $value ) ) {
			return (string) $value;
		}
		if ( is_array( $value ) ) {
			return implode( ', ', array_map( array( $this, 'scalar_value' ), array_slice( $value, 0, 100 ) ) );
		}
		return '';
	}

	/** @param mixed $value Value. @param int $depth Depth. @return mixed */
	private function sanitize_sample_value( $value, $depth = 0 ) {
		if ( is_scalar( $value ) ) {
			$value = sanitize_textarea_field( (string) $value );
			return function_exists( 'mb_substr' ) ? mb_substr( $value, 0, 4096 ) : substr( $value, 0, 4096 );
		}
		if ( ! is_array( $value ) || $depth >= 3 ) {
			return '';
		}
		$clean = array();
		foreach ( array_slice( $value, 0, 100 ) as $item ) {
			$clean[] = $this->sanitize_sample_value( $item, $depth + 1 );
		}
		return $clean;
	}

	/** Resolve Email Log metadata from persisted state instead of client input. @param int $rule_id Rule id. @return int */
	private function actual_rule_version( $rule_id ) {
		$rule_id = absint( $rule_id );
		if ( $rule_id <= 0 ) {
			return 1;
		}
		$rule = $this->rules->find( $rule_id );

		return $rule ? max( 1, absint( $rule['rule_version'] ?? 1 ) ) : 1;
	}

	/** @param string $email Email. @param string $name Name. @return string */
	private function format_mailbox( $email, $name ) {
		$email = sanitize_email( $email );
		$name  = sanitize_text_field( $name );
		return '' !== $name ? $name . ' <' . $email . '>' : $email;
	}

	/** @return bool */
	private function claim_test_send() {
		$user_id = get_current_user_id();
		$prefix  = 'subin_ar_test_' . absint( $user_id ) . '_';
		for ( $slot = 1; $slot <= self::TEST_RATE_LIMIT; $slot++ ) {
			$key = $prefix . $slot;
			if ( false !== get_transient( $key ) ) {
				continue;
			}
			if ( $this->add_atomic_test_transient( $key ) ) {
				return true;
			}
		}
		return false;
	}

	/** @param string $key Transient key. @return bool */
	private function add_atomic_test_transient( $key ) {
		if ( wp_using_ext_object_cache() ) {
			return (bool) wp_cache_add( $key, 1, 'transient', self::TEST_RATE_WINDOW );
		}

		$timeout_name = '_transient_timeout_' . $key;
		$value_name   = '_transient_' . $key;
		if ( ! add_option( $timeout_name, time() + self::TEST_RATE_WINDOW, '', false ) ) {
			return false;
		}
		if ( add_option( $value_name, 1, '', false ) ) {
			return true;
		}

		delete_option( $timeout_name );
		return false;
	}

	/** @param string $code Code. @param string $message Message. @return array{code:string,message:string} */
	private function field_error( $code, $message ) {
		return array( 'code' => sanitize_key( $code ), 'message' => sanitize_text_field( $message ) );
	}

	/** @param array<string,mixed> $fields Fields. @return \WP_Error */
	private function validation_error( array $fields ) {
		return $this->error(
			'validation_failed',
			__( 'Review the highlighted auto-reply settings.', 'submission-inbox' ),
			422,
			array( 'fields' => $fields )
		);
	}

	/** @param string $code Code. @param string $message Message. @param int $status Status. @param array<string,mixed> $extra Extra. @return \WP_Error */
	private function error( $code, $message, $status, array $extra = array() ) {
		return new \WP_Error(
			sanitize_key( $code ),
			$message,
			array_merge( array( 'status' => absint( $status ) ), $extra )
		);
	}
}
