<?php
/**
 * Auto-reply sender for built-in forms.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\AutoReplies;

use SUBIN\Forms\FormFieldRepository;
use SUBIN\Forms\FormSubmissionContext;
use SUBIN\Leads\LeadRepository;
use SUBIN\MailLog\MailEventRepository;
use SUBIN\MailLog\MailErrorSanitizer;
use SUBIN\Notifications\NotificationEnvelope;

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

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

/**
 * Builds and sends visitor auto-replies after successful built-in submissions.
 */
class AutoReplySender {
	const SOURCE = 'submission_inbox_auto_reply';
	const PREHEADER_RENDER_BUDGET_BYTES = 2000;

	/**
	 * Template repository.
	 *
	 * @var AutoReplyTemplateRepository
	 */
	private $templates;

	/**
	 * Mail event repository.
	 *
	 * @var MailEventRepository
	 */
	private $events;

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

	/**
	 * Form field repository.
	 *
	 * @var FormFieldRepository
	 */
	private $fields;

	/**
	 * Rule repository.
	 *
	 * @var AutoReplyRuleRepository
	 */
	private $rules;

	/**
	 * Shared merge-tag renderer.
	 *
	 * @var AutoReplyMergeTagRenderer
	 */
	private $renderer;

	/**
	 * Constructor.
	 *
	 * @param AutoReplyTemplateRepository $templates Templates.
	 * @param MailEventRepository         $events Events.
	 * @param FormSubmissionContext       $context Context.
	 * @param FormFieldRepository|null    $fields Form fields.
	 * @param AutoReplyRuleRepository|null $rules Rules.
	 * @param AutoReplyMergeTagRenderer|null $renderer Merge-tag renderer.
	 */
	public function __construct( AutoReplyTemplateRepository $templates, MailEventRepository $events, FormSubmissionContext $context, ?FormFieldRepository $fields = null, ?AutoReplyRuleRepository $rules = null, ?AutoReplyMergeTagRenderer $renderer = null ) {
		$this->templates = $templates;
		$this->events    = $events;
		$this->context   = $context;
		$this->fields    = $fields ?: new FormFieldRepository();
		$this->rules     = $rules ?: new AutoReplyRuleRepository();
		$this->renderer  = $renderer ?: new AutoReplyMergeTagRenderer();
	}

	/**
	 * Whether a built-in form has an enabled auto-reply rule.
	 *
	 * @param array<string,mixed> $form Form row.
	 * @return bool
	 */
	public function has_enabled_rule_for_form( array $form ) {
		$form_id = absint( $form['id'] ?? 0 );

		return $form_id > 0 && null !== $this->rules->find_enabled_rule( AutoReplyRuleRepository::PROVIDER_SUBMISSION_INBOX_FORM, (string) $form_id );
	}

	/**
	 * Snapshot the enabled visitor auto-reply into an immutable delivery draft.
	 *
	 * A disabled rule returns null. Misconfigured enabled rules return a
	 * no-email-generated draft so the accepted outcome remains auditable even if
	 * the rule or template changes before the worker runs.
	 *
	 * @param array<string,mixed>  $form Form row.
	 * @param array<int,array>     $lead_fields Accepted lead fields.
	 * @param array<string,string> $detected Detected lead values.
	 * @param string               $submitted_at Accepted submission time.
	 * @return array<string,mixed>|null|\WP_Error
	 */
	public function prepare_notification_envelope( array $form, array $lead_fields, array $detected, $submitted_at = '' ) {
		$form_id = absint( $form['id'] ?? 0 );
		$rule    = $form_id > 0
			? $this->rules->find_matching_rule( AutoReplyRuleRepository::PROVIDER_SUBMISSION_INBOX_FORM, (string) $form_id, $lead_fields )
			: null;
		if ( ! $rule ) {
			return null;
		}

		$template_id   = absint( $rule['template_id'] ?? 0 );
		$template      = $this->delivery_template( $template_id );
		$subject_source = '' !== trim( (string) ( $rule['subject'] ?? '' ) )
			? (string) $rule['subject']
			: (string) ( $template['subject'] ?? '' );
		$body_source = '' !== trim( (string) ( $rule['body'] ?? '' ) )
			? (string) $rule['body']
			: AutoReplyTemplateRepository::effective_body( $template );
		$preheader_source = '' !== trim( (string) ( $rule['preheader'] ?? '' ) )
			? (string) $rule['preheader']
			: (string) ( $template['preheader'] ?? '' );
		$lead_id_token = NotificationEnvelope::create_lead_id_token();
		$submitted_at  = sanitize_text_field( '' !== (string) $submitted_at ? $submitted_at : current_time( 'mysql' ) );
		$delay_seconds = AutoReplyRuleRepository::DELIVERY_DELAY === ( $rule['delivery_mode'] ?? '' )
			? min( AutoReplyRuleRepository::MAX_DELAY_SECONDS, absint( $rule['delay_seconds'] ?? 0 ) )
			: 0;
		$scheduled_at = $this->scheduled_at( $delay_seconds );
		$context       = array(
			'provider'     => AutoReplyRuleRepository::PROVIDER_SUBMISSION_INBOX_FORM,
			'form_id'      => (string) $form_id,
			'form_name'    => sanitize_text_field( $form['title'] ?? '' ),
			'submitted_at' => $submitted_at,
		);
		$meta = array(
			'rule_id'       => absint( $rule['id'] ?? 0 ),
			'rule_name'     => sanitize_text_field( $rule['name'] ?? '' ),
			'rule_version'  => max( 1, absint( $rule['rule_version'] ?? 1 ) ),
			'template_id'   => $template_id,
			'template_name' => sanitize_text_field( $template['name'] ?? '' ),
			'delivery_mode' => $delay_seconds > 0 ? AutoReplyRuleRepository::DELIVERY_DELAY : AutoReplyRuleRepository::DELIVERY_IMMEDIATE,
			'scheduled_at'  => $scheduled_at,
			'lead_id_token' => $lead_id_token,
		);
		$subject_result = $this->render_variables( $subject_source, $form, $lead_id_token, $lead_fields, $detected, false, AutoReplyDeliverySnapshot::MAX_SUBJECT_BYTES );
		$subject = (string) ( $subject_result['content'] ?? '' );
		$subject = sanitize_text_field( '' !== $subject ? $subject : __( 'We received your request', 'submission-inbox' ) );
		$from = is_email( $rule['from_email'] ?? '' )
			? array(
				'email' => sanitize_email( $rule['from_email'] ),
				'name'  => sanitize_text_field( $rule['from_name'] ?? '' ),
			)
			: null;
		$reply_to = is_email( $rule['reply_to_email'] ?? '' )
			? array(
				'email' => sanitize_email( $rule['reply_to_email'] ),
				'name'  => sanitize_text_field( $rule['reply_to_name'] ?? '' ),
			)
			: null;

		if ( '' === trim( $body_source ) ) {
			return NotificationEnvelope::create_draft(
				'visitor_auto_reply',
				self::SOURCE,
				array(
					'state'          => NotificationEnvelope::STATE_NO_EMAIL_GENERATED,
					'subject'        => $subject,
					'from'           => $from,
					'reply_to'       => $reply_to,
					'reason_code'    => 'missing_content',
					'reason_message' => __( 'Auto-reply content is missing.', 'submission-inbox' ),
				),
				$context,
				$meta
			);
		}

		$recipient_result = $this->resolve_visitor_email_for_rule( $form, $lead_fields, $rule );
		$recipient        = sanitize_email( $recipient_result['email'] ?? '' );
		if ( ! is_email( $recipient ) ) {
			return NotificationEnvelope::create_draft(
				'visitor_auto_reply',
				self::SOURCE,
				array(
					'state'          => NotificationEnvelope::STATE_NO_EMAIL_GENERATED,
					'subject'        => $subject,
					'from'           => $from,
					'reply_to'       => $reply_to,
					'reason_code'    => sanitize_key( $recipient_result['reason'] ?? 'auto_reply_email_invalid' ),
					'reason_message' => $this->auto_reply_reason_message( $recipient_result['reason'] ?? '' ),
				),
				$context,
				$meta
			);
		}
		if ( ! empty( $subject_result['overflowed'] ) ) {
			return NotificationEnvelope::create_draft(
				'visitor_auto_reply',
				self::SOURCE,
				array(
					'state'          => NotificationEnvelope::STATE_NO_EMAIL_GENERATED,
					'to'             => $recipient,
					'subject'        => $subject,
					'from'           => $from,
					'reply_to'       => $reply_to,
					'reason_code'    => 'subject_too_long',
					'reason_message' => __( 'Auto-reply subject is too long after merge tags are rendered.', 'submission-inbox' ),
				),
				$context,
				$meta
			);
		}

		$body_result = $this->render_auto_reply_email_for_sending( array( 'rendered_html' => $body_source ), $form, $lead_id_token, $lead_fields, $detected, NotificationEnvelope::MAX_BODY_BYTES );
		$body        = (string) ( $body_result['content'] ?? '' );
		if ( ! empty( $body_result['overflowed'] ) ) {
			return NotificationEnvelope::create_draft(
				'visitor_auto_reply',
				self::SOURCE,
				array(
					'state'          => NotificationEnvelope::STATE_NO_EMAIL_GENERATED,
					'to'             => $recipient,
					'subject'        => $subject,
					'from'           => $from,
					'reply_to'       => $reply_to,
					'reason_code'    => 'content_too_large',
					'reason_message' => __( 'Auto-reply content is too large after merge tags are rendered.', 'submission-inbox' ),
				),
				$context,
				$meta
			);
		}
		if ( ! AutoReplyPreheader::has_meaningful_content( $body ) ) {
			return NotificationEnvelope::create_draft(
				'visitor_auto_reply',
				self::SOURCE,
				array(
					'state'          => NotificationEnvelope::STATE_NO_EMAIL_GENERATED,
					'to'             => $recipient,
					'subject'        => $subject,
					'from'           => $from,
					'reply_to'       => $reply_to,
					'reason_code'    => 'empty_content',
					'reason_message' => __( 'Auto-reply template did not generate email content.', 'submission-inbox' ),
				),
				$context,
				$meta
			);
		}
		$preheader_result = $this->render_variables( $preheader_source, $form, $lead_id_token, $lead_fields, $detected, false, self::PREHEADER_RENDER_BUDGET_BYTES );
		$preheader = (string) ( $preheader_result['content'] ?? '' );
		$body      = AutoReplyPreheader::inject( $body, $preheader );
		if ( strlen( $body ) > NotificationEnvelope::MAX_BODY_BYTES ) {
			return NotificationEnvelope::create_draft(
				'visitor_auto_reply',
				self::SOURCE,
				array(
					'state'          => NotificationEnvelope::STATE_NO_EMAIL_GENERATED,
					'to'             => $recipient,
					'subject'        => $subject,
					'from'           => $from,
					'reply_to'       => $reply_to,
					'reason_code'    => 'content_too_large',
					'reason_message' => __( 'Auto-reply content is too large after preview text is added.', 'submission-inbox' ),
				),
				$context,
				$meta
			);
		}

		return NotificationEnvelope::create_draft(
			'visitor_auto_reply',
			self::SOURCE,
			array(
				'state'     => NotificationEnvelope::STATE_READY,
				'to'        => $recipient,
				'subject'   => $subject,
				'body_html' => $body,
				'from'      => $from,
				'reply_to'  => $reply_to,
			),
			$context,
			$meta
		);
	}

	/**
	 * Send an auto-reply for a saved lead.
	 *
	 * @param array<string,mixed>  $form Form row.
	 * @param int                  $lead_id Lead id.
	 * @param array<int,array>     $lead_fields Saved lead fields.
	 * @param array<string,string> $detected Detected lead values.
	 * @param array<string,mixed>  $settings Form settings.
	 * @return string Lead email status.
	 */
	public function send_for_submission( array $form, $lead_id, array $lead_fields, array $detected, array $settings ) {
		$lead_id = absint( $lead_id );
		if ( $lead_id <= 0 ) {
			return LeadRepository::EMAIL_STATUS_NO_EMAIL_GENERATED;
		}

		unset( $settings );

		$rule = $this->rules->find_matching_rule( AutoReplyRuleRepository::PROVIDER_SUBMISSION_INBOX_FORM, (string) absint( $form['id'] ?? 0 ), $lead_fields );
		if ( ! $rule ) {
			return LeadRepository::EMAIL_STATUS_NO_EMAIL_GENERATED;
		}

		$template_id   = absint( $rule['template_id'] ?? 0 );
		$template      = $this->delivery_template( $template_id );
		$subject_source = '' !== trim( (string) ( $rule['subject'] ?? '' ) ) ? (string) $rule['subject'] : (string) ( $template['subject'] ?? '' );
		$body_source    = '' !== trim( (string) ( $rule['body'] ?? '' ) ) ? (string) $rule['body'] : AutoReplyTemplateRepository::effective_body( $template );
		$preheader_source = '' !== trim( (string) ( $rule['preheader'] ?? '' ) ) ? (string) $rule['preheader'] : (string) ( $template['preheader'] ?? '' );
		$delivery_metadata = array(
			'event_kind'    => MailEventRepository::EVENT_KIND_VISITOR_CONFIRMATION,
			'rule_id'       => absint( $rule['id'] ?? 0 ),
			'rule_name'     => sanitize_text_field( $rule['name'] ?? '' ),
			'rule_version'  => max( 1, absint( $rule['rule_version'] ?? 1 ) ),
			'template_id'   => $template_id,
			'template_name' => sanitize_text_field( $template['name'] ?? '' ),
			'provider'      => AutoReplyRuleRepository::PROVIDER_SUBMISSION_INBOX_FORM,
			'form_id'       => (string) absint( $form['id'] ?? 0 ),
			'form_name'     => sanitize_text_field( $form['title'] ?? '' ),
			'delivery_mode' => AutoReplyRuleRepository::DELIVERY_DELAY === ( $rule['delivery_mode'] ?? '' )
				? AutoReplyRuleRepository::DELIVERY_DELAY
				: AutoReplyRuleRepository::DELIVERY_IMMEDIATE,
			'scheduled_at'  => current_time( 'mysql' ),
		);
		if ( '' === trim( $body_source ) ) {
			$this->log_no_email_generated( $lead_id, '', '', __( 'Auto-reply content is missing.', 'submission-inbox' ), $delivery_metadata, 'missing_content' );
			return LeadRepository::EMAIL_STATUS_NO_EMAIL_GENERATED;
		}

		$subject_result = $this->render_variables( $subject_source, $form, $lead_id, $lead_fields, $detected, false, AutoReplyDeliverySnapshot::MAX_SUBJECT_BYTES );
		$subject = sanitize_text_field( (string) ( $subject_result['content'] ?? '' ) );
		$subject = '' !== $subject ? $subject : __( 'We received your request', 'submission-inbox' );
		if ( ! empty( $subject_result['overflowed'] ) ) {
			$this->log_no_email_generated(
				$lead_id,
				'',
				$subject,
				__( 'Auto-reply subject is too long after merge tags are rendered.', 'submission-inbox' ),
				$delivery_metadata,
				'subject_too_long'
			);
			return LeadRepository::EMAIL_STATUS_NO_EMAIL_GENERATED;
		}

		$recipient_result = $this->resolve_visitor_email_for_rule( $form, $lead_fields, $rule );
		$recipient        = $recipient_result['email'];
		if ( ! is_email( $recipient ) ) {
			$reason = $this->auto_reply_reason_message( $recipient_result['reason'] );
			$this->log_no_email_generated( $lead_id, '', $subject, $reason, $delivery_metadata, sanitize_key( $recipient_result['reason'] ) );
			return LeadRepository::EMAIL_STATUS_NO_EMAIL_GENERATED;
		}

		$body_result = $this->render_auto_reply_email_for_sending( array( 'rendered_html' => $body_source ), $form, $lead_id, $lead_fields, $detected, NotificationEnvelope::MAX_BODY_BYTES );
		$body        = (string) ( $body_result['content'] ?? '' );
		if ( ! empty( $body_result['overflowed'] ) ) {
			$this->log_no_email_generated(
				$lead_id,
				$recipient,
				$subject,
				__( 'Auto-reply content is too large after merge tags are rendered.', 'submission-inbox' ),
				$delivery_metadata,
				'content_too_large'
			);
			return LeadRepository::EMAIL_STATUS_NO_EMAIL_GENERATED;
		}
		if ( ! AutoReplyPreheader::has_meaningful_content( $body ) ) {
			$this->log_no_email_generated( $lead_id, $recipient, $subject, __( 'Auto-reply template did not generate email content.', 'submission-inbox' ), $delivery_metadata, 'empty_content' );
			return LeadRepository::EMAIL_STATUS_NO_EMAIL_GENERATED;
		}
		$preheader_result = $this->render_variables( $preheader_source, $form, $lead_id, $lead_fields, $detected, false, self::PREHEADER_RENDER_BUDGET_BYTES );
		$preheader = (string) ( $preheader_result['content'] ?? '' );
		$body      = AutoReplyPreheader::inject( $body, $preheader );
		if ( strlen( $body ) > NotificationEnvelope::MAX_BODY_BYTES ) {
			$this->log_no_email_generated(
				$lead_id,
				$recipient,
				$subject,
				__( 'Auto-reply content is too large after preview text is added.', 'submission-inbox' ),
				$delivery_metadata,
				'content_too_large'
			);
			return LeadRepository::EMAIL_STATUS_NO_EMAIL_GENERATED;
		}
		$correlation_id = str_replace( '-', '', wp_generate_uuid4() );
		$event_data = array_merge(
			array(
				'lead_id'        => $lead_id,
				'correlation_id' => $correlation_id,
				'source'         => self::SOURCE,
				'to_email'       => $recipient,
				'from_email'     => is_email( $rule['from_email'] ?? '' ) ? sanitize_email( $rule['from_email'] ) : '',
				'subject'        => $subject,
				'message_hash'   => hash( 'sha256', $body ),
			),
			$delivery_metadata
		);
		$headers = array( 'Content-Type: text/html; charset=UTF-8' );
		if ( is_email( $rule['from_email'] ?? '' ) ) {
			$from_name = sanitize_text_field( $rule['from_name'] ?? '' );
			$headers[] = 'From: ' . ( '' !== $from_name ? $from_name . ' ' : '' ) . '<' . sanitize_email( $rule['from_email'] ) . '>';
		}
		if ( is_email( $rule['reply_to_email'] ?? '' ) ) {
			$reply_name = sanitize_text_field( $rule['reply_to_name'] ?? '' );
			$headers[] = 'Reply-To: ' . ( '' !== $reply_name ? $reply_name . ' ' : '' ) . '<' . sanitize_email( $rule['reply_to_email'] ) . '>';
		}
		$this->context->set_current_lead_id( $lead_id );
		$this->context->set_current_source( self::SOURCE, true );
		$this->context->set_correlation_id( $correlation_id );
		$this->context->set_delivery_metadata( $delivery_metadata );
		$this->context->set_current_from( $rule['from_email'] ?? '', $rule['from_name'] ?? '' );
		add_filter( 'wp_mail_content_type', array( $this, 'html_mail_content_type' ) );
		$mail_error = '';
		try {
			$sent = wp_mail( $recipient, $subject, $body, $headers );
		} catch ( \Throwable $throwable ) {
			$mail_error = $this->sanitize_mail_error( $throwable->getMessage() );
			$sent       = false;
		} finally {
			remove_filter( 'wp_mail_content_type', array( $this, 'html_mail_content_type' ) );
			$this->context->clear();
		}

		$this->ensure_auto_reply_event(
			$event_data,
			(bool) $sent,
			'' !== $mail_error ? $mail_error : __( 'WordPress mail returned false.', 'submission-inbox' )
		);

		return $sent ? LeadRepository::EMAIL_STATUS_SENT : LeadRepository::EMAIL_STATUS_FAILED;
	}

	/**
	 * Resolve visitor email from a rule-selected field.
	 *
	 * @param array<string,mixed> $form Form row.
	 * @param array<int,array>    $lead_fields Lead fields.
	 * @param array<string,mixed> $rule Rule row.
	 * @return array{email:string,reason:string,matched:bool}
	 */
	private function resolve_visitor_email_for_rule( array $form, array $lead_fields, array $rule ) {
		$selected_key = sanitize_key( (string) ( $rule['visitor_email_field'] ?? '' ) );
		if ( '' === $selected_key ) {
			return array(
				'email'   => '',
				'reason'  => 'auto_reply_email_field_not_selected',
				'matched' => false,
			);
		}

		$form_email_field = $this->selected_form_email_field( absint( $form['id'] ?? 0 ), $selected_key, 0 );
		if ( ! $form_email_field ) {
			return array(
				'email'   => '',
				'reason'  => 'auto_reply_email_field_not_found',
				'matched' => false,
			);
		}

		foreach ( $lead_fields as $field ) {
			if ( $selected_key !== sanitize_key( $field['field_key'] ?? '' ) ) {
				continue;
			}

			$email = sanitize_email( $field['field_value'] ?? '' );
			return array(
				'email'   => $email,
				'reason'  => is_email( $email ) ? 'resolved' : ( '' === $email ? 'auto_reply_email_empty' : 'auto_reply_email_invalid' ),
				'matched' => true,
			);
		}

		return array(
			'email'   => '',
			'reason'  => 'auto_reply_email_field_not_found',
			'matched' => false,
		);
	}

	/**
	 * Use HTML content type only while sending visitor auto-replies.
	 *
	 * @return string
	 */
	public function html_mail_content_type() {
		return 'text/html';
	}

	/**
	 * Sanitize mail errors before storing them.
	 *
	 * @param string $message Error message.
	 * @return string
	 */
	private function sanitize_mail_error( $message ) {
		return MailErrorSanitizer::sanitize( $message );
	}

	/**
	 * Ensure one attempt has one terminal event even when a later mail hook throws.
	 *
	 * MailLogger normally owns the lifecycle. This recovery path reuses its exact
	 * correlation/source row instead of creating a second event after an exception.
	 *
	 * @param array<string,mixed> $event_data Immutable attempt metadata.
	 * @param bool                $sent Whether WordPress accepted the message.
	 * @param string              $error Safe fallback failure.
	 * @return void
	 */
	private function ensure_auto_reply_event( array $event_data, $sent, $error ) {
		$event = $this->events->find_delivery_event( $event_data['correlation_id'] ?? '', self::SOURCE );
		if ( is_wp_error( $event ) ) {
			return;
		}

		$status = $event ? sanitize_key( (string) ( $event->status ?? '' ) ) : '';
		if ( in_array( $status, array( MailEventRepository::STATUS_SENT, MailEventRepository::STATUS_FAILED ), true ) ) {
			return;
		}

		$event_id = $event ? absint( $event->id ?? 0 ) : $this->events->create_pending( $event_data );
		if ( $event_id <= 0 ) {
			return;
		}

		if ( $sent ) {
			$this->events->mark_sent( $event_id );
			return;
		}

		$this->events->mark_failed( $event_id, $this->sanitize_mail_error( $error ) );
	}

	/**
	 * Resolve the visitor email address for an auto-reply.
	 *
	 * Form settings currently store the selected visitor email field as field_key
	 * in auto_reply.recipient_field_key. Legacy data may contain a numeric field id,
	 * so this resolver supports both field_key and field_id matching.
	 *
	 * @param array<string,mixed> $form Form row.
	 * @param array<int,array>    $lead_fields Lead fields.
	 * @param array<string,mixed> $settings Form settings.
	 * @return array{email:string,reason:string,matched:bool}
	 */
	private function resolve_visitor_email_for_auto_reply( array $form, array $lead_fields, array $settings ) {
		$config = is_array( $settings['auto_reply'] ?? null ) ? $settings['auto_reply'] : array();
		$raw_selected = $config['recipient_field_key']
			?? $config['email_field_key']
			?? $config['auto_reply_email_field_key']
			?? $config['recipient_field_id']
			?? $config['email_field_id']
			?? $config['auto_reply_email_field_id']
			?? '';

		$selected_key = sanitize_key( (string) $raw_selected );
		$selected_id  = absint( $raw_selected );

		if ( '' === $selected_key && $selected_id <= 0 ) {
			return array(
				'email'   => '',
				'reason'  => 'auto_reply_email_field_not_selected',
				'matched' => false,
			);
		}

		$form_email_field = $this->selected_form_email_field( absint( $form['id'] ?? 0 ), $selected_key, $selected_id );
		if ( ! $form_email_field ) {
			return array(
				'email'   => '',
				'reason'  => 'auto_reply_email_field_not_found',
				'matched' => false,
			);
		}

		$form_field_key = sanitize_key( $form_email_field['field_key'] ?? '' );
		$form_field_id  = absint( $form_email_field['id'] ?? 0 );
		foreach ( $lead_fields as $field ) {
			$field_key = sanitize_key( $field['field_key'] ?? '' );
			$field_id  = absint( $field['field_id'] ?? 0 );
			$matches_key = '' !== $form_field_key && $form_field_key === $field_key;
			$matches_id  = $form_field_id > 0 && $form_field_id === $field_id;

			if ( ! $matches_key && ! $matches_id ) {
				continue;
			}

			if ( 'email' !== sanitize_key( $field['field_type'] ?? '' ) ) {
				return array(
					'email'   => '',
					'reason'  => 'auto_reply_email_field_not_found',
					'matched' => true,
				);
			}

			$email = sanitize_email( $field['field_value'] ?? '' );
			if ( '' === $email ) {
				return array(
					'email'   => '',
					'reason'  => 'auto_reply_email_empty',
					'matched' => true,
				);
			}

			if ( ! is_email( $email ) ) {
				return array(
					'email'   => '',
					'reason'  => 'auto_reply_email_invalid',
					'matched' => true,
				);
			}

			return array(
				'email'   => $email,
				'reason'  => 'resolved',
				'matched' => true,
			);
		}

		return array(
			'email'   => '',
			'reason'  => 'auto_reply_email_field_not_found',
			'matched' => false,
		);
	}

	/**
	 * Find the selected real email field on the current form.
	 *
	 * @param int    $form_id Form id.
	 * @param string $selected_key Selected field key.
	 * @param int    $selected_id Selected field id.
	 * @return array<string,mixed>|null
	 */
	private function selected_form_email_field( $form_id, $selected_key, $selected_id ) {
		if ( $form_id <= 0 ) {
			return null;
		}

		foreach ( $this->fields->get_by_form_id( $form_id ) as $field ) {
			if ( 'email' !== sanitize_key( $field['field_type'] ?? '' ) ) {
				continue;
			}

			$field_key = sanitize_key( $field['field_key'] ?? '' );
			$field_id  = absint( $field['id'] ?? 0 );
			if ( ( '' !== $selected_key && $selected_key === $field_key ) || ( $selected_id > 0 && $selected_id === $field_id ) ) {
				return $field;
			}
		}

		return null;
	}

	/**
	 * Get a safe no-email-generated reason message.
	 *
	 * @param string $reason Reason code.
	 * @return string
	 */
	private function auto_reply_reason_message( $reason ) {
		$messages = array(
			'auto_reply_email_field_not_selected' => __( 'Auto-reply visitor email field is not selected.', 'submission-inbox' ),
			'auto_reply_email_field_not_found'    => __( 'Visitor email field is missing.', 'submission-inbox' ),
			'auto_reply_email_empty'              => __( 'Auto-reply visitor email field was empty.', 'submission-inbox' ),
			'auto_reply_email_invalid'            => __( 'Auto-reply visitor email field did not contain a valid email address.', 'submission-inbox' ),
		);

		return $messages[ $reason ] ?? __( 'Auto-reply requires a valid submitted email field.', 'submission-inbox' );
	}

	/**
	 * Render a self-contained, email-safe HTML body for sending.
	 *
	 * @param array<string,mixed>  $template Template.
	 * @param array<string,mixed>  $form Form.
	 * @param int                  $lead_id Lead id.
	 * @param array<int,array>     $lead_fields Lead fields.
	 * @param array<string,string> $detected Detected values.
	 * @param int                  $max_bytes Maximum rendered bytes.
	 * @return array{content:string,overflowed:bool}
	 */
	private function render_auto_reply_email_for_sending( array $template, array $form, $lead_id, array $lead_fields, array $detected, $max_bytes ) {
		return $this->build_compiled_template_email_body( $template, $form, $lead_id, $lead_fields, $detected, $max_bytes );
	}

	/**
	 * Build the send email from final compiled HTML.
	 *
	 * @param array<string,mixed>  $template Template.
	 * @param array<string,mixed>  $form Form.
	 * @param int                  $lead_id Lead id.
	 * @param array<int,array>     $lead_fields Lead fields.
	 * @param array<string,string> $detected Detected values.
	 * @param int                  $max_bytes Maximum rendered bytes.
	 * @return array{content:string,overflowed:bool}
	 */
	private function build_compiled_template_email_body( array $template, array $form, $lead_id, array $lead_fields, array $detected, $max_bytes ) {
		$html = AutoReplyTemplateRepository::effective_body( $template );
		$html = preg_replace( '/<\?(?:php)?[\s\S]*?\?>/i', '', $html );
		$html = strip_shortcodes( $html );
		$result = $this->render_variables( $html, $form, $lead_id, $lead_fields, $detected, true, $max_bytes );
		$html = (string) ( $result['content'] ?? '' );
		$html = $this->remove_unresolved_variables( $html );
		$html = $this->sanitize_email_html( $html );

		return array(
			'content'    => AutoReplyTemplateRepository::sanitize_svg_paint_attributes( $html ),
			'overflowed' => ! empty( $result['overflowed'] ),
		);
	}

	/**
	 * Get a submitted field value by key.
	 *
	 * @param array<int,array> $lead_fields Lead fields.
	 * @param string           $key Field key.
	 * @return string
	 */
	private function field_value( array $lead_fields, $key ) {
		$key = sanitize_key( $key );
		foreach ( $lead_fields as $field ) {
			if ( $key === sanitize_key( $field['field_key'] ?? '' ) ) {
				return sanitize_textarea_field( $field['field_value'] ?? '' );
			}
		}

		return '';
	}

	/**
	 * Get safe visitor email from detected lead data or saved email fields.
	 *
	 * @param array<int,array>     $lead_fields Lead fields.
	 * @param array<string,string> $detected Detected values.
	 * @return string
	 */
	private function visitor_email( array $lead_fields, array $detected ) {
		$email = sanitize_email( $detected['customer_email'] ?? '' );
		if ( is_email( $email ) ) {
			return $email;
		}

		foreach ( $lead_fields as $field ) {
			if ( 'email' !== sanitize_key( $field['field_type'] ?? '' ) ) {
				continue;
			}

			$email = sanitize_email( $field['field_value'] ?? '' );
			if ( is_email( $email ) ) {
				return $email;
			}
		}

		return '';
	}

	/**
	 * Get safe form name.
	 *
	 * @param array<string,mixed> $form Form.
	 * @return string
	 */
	private function form_name( array $form ) {
		$name = sanitize_text_field( $form['title'] ?? '' );
		return '' !== $name ? $name : __( 'this form', 'submission-inbox' );
	}

	/**
	 * Get formatted submission date.
	 *
	 * @return string
	 */
	private function submission_date() {
		return sanitize_text_field( current_time( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) );
	}

	/**
	 * Remove unresolved placeholders from final email HTML.
	 *
	 * @param string $html HTML.
	 * @return string
	 */
	private function remove_unresolved_variables( $html ) {
		$html = preg_replace( '/\{field:[A-Za-z0-9_-]+\}/', '', (string) $html );
		$html = preg_replace( '/\{(?:form_name|submission_date|visitor_email|site_name|lead_id)\}/', '', (string) $html );
		$html = preg_replace( '/\{[A-Za-z0-9_:-]+\}/', '', (string) $html );

		return (string) $html;
	}

	/**
	 * Replace supported variables.
	 *
	 * @param string               $content Content.
	 * @param array<string,mixed>  $form Form.
	 * @param int                  $lead_id Lead id.
	 * @param array<int,array>     $lead_fields Lead fields.
	 * @param array<string,string> $detected Detected values.
	 * @param bool                 $html Whether replacements are for HTML output.
	 * @return string
	 */
	private function replace_variables( $content, array $form, $lead_id, array $lead_fields, array $detected, $html ) {
		$result = $this->render_variables( $content, $form, $lead_id, $lead_fields, $detected, $html, 0 );

		return (string) ( $result['content'] ?? '' );
	}

	/**
	 * Replace supported variables under a fixed output budget.
	 *
	 * @param string               $content Content.
	 * @param array<string,mixed>  $form Form.
	 * @param int|string           $lead_id Lead id or immutable token.
	 * @param array<int,array>     $lead_fields Lead fields.
	 * @param array<string,string> $detected Detected values.
	 * @param bool                 $html Whether replacements are for HTML output.
	 * @param int                  $max_bytes Maximum rendered bytes.
	 * @return array{content:string,diagnostics:array<string,array<int,string>>,overflowed:bool}
	 */
	private function render_variables( $content, array $form, $lead_id, array $lead_fields, array $detected, $html, $max_bytes ) {
		$lead_id_value = NotificationEnvelope::is_lead_id_token( $lead_id )
			? (string) $lead_id
			: (string) absint( $lead_id );
		$context = array(
			'site_name'       => get_bloginfo( 'name' ),
			'form_name'       => sanitize_text_field( $form['title'] ?? '' ),
			'submission_date' => current_time( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ),
			'lead_id'         => $lead_id_value,
			'visitor_email'   => $this->visitor_email( $lead_fields, $detected ),
		);

		return $html
			? $this->renderer->render_html( $content, $context, $lead_fields, $max_bytes )
			: $this->renderer->render_subject( $content, $context, $lead_fields, $max_bytes );
	}

	/**
	 * Calculate the earliest local queue datetime for a policy delay.
	 *
	 * @param int $delay_seconds Delay in seconds.
	 * @return string
	 */
	private function scheduled_at( $delay_seconds ) {
		$timestamp = current_time( 'timestamp' ) + min( AutoReplyRuleRepository::MAX_DELAY_SECONDS, absint( $delay_seconds ) );

		return gmdate( 'Y-m-d H:i:s', $timestamp );
	}

	/**
	 * Log an auto-reply that was intentionally not generated.
	 *
	 * @param int    $lead_id Lead id.
	 * @param string $recipient Recipient.
	 * @param string $subject Subject.
	 * @param string              $reason Reason.
	 * @param array<string,mixed> $metadata Delivery metadata.
	 * @param string              $reason_code Machine-readable reason.
	 * @return void
	 */
	private function log_no_email_generated( $lead_id, $recipient, $subject, $reason, array $metadata = array(), $reason_code = '' ) {
		$this->events->create_no_email_generated(
			array_merge(
				array(
					'lead_id'       => $lead_id,
					'source'        => self::SOURCE,
					'event_kind'    => \SUBIN\MailLog\MailEventRepository::EVENT_KIND_VISITOR_CONFIRMATION,
					'to_email'      => $recipient,
					'subject'       => $subject,
					'error_message' => $reason,
				),
				$metadata,
				'' !== $reason_code ? array( 'reason_code' => sanitize_key( $reason_code ) ) : array()
			)
		);
	}

	/**
	 * Load only delivery-owned template fields with a bounded body projection.
	 *
	 * @param int $template_id Template id.
	 * @return array<string,mixed>|null
	 */
	private function delivery_template( $template_id ) {
		$template_id = absint( $template_id );
		if ( $template_id <= 0 ) {
			return null;
		}
		if ( method_exists( $this->templates, 'get_design_content' ) ) {
			return $this->templates->get_design_content( $template_id, NotificationEnvelope::MAX_BODY_BYTES );
		}

		return $this->templates->get( $template_id );
	}

	/**
	 * Sanitize email HTML after variable replacement.
	 *
	 * @param string $html HTML.
	 * @return string
	 */
	private function sanitize_email_html( $html ) {
		$html = preg_replace( '/<\?(?:php)?[\s\S]*?\?>/i', '', (string) $html );
		$html = strip_shortcodes( $html );
		$html = wp_kses( $html, AutoReplyTemplateRepository::allowed_email_html(), array( 'http', 'https', 'mailto', 'tel' ) );

		return AutoReplyTemplateRepository::sanitize_svg_paint_attributes( $html );
	}
}
