<?php
/**
 * Read-only Design Studio readiness checks and safe submission simulation.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Forms;

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

/**
 * Audits the saved draft without creating leads, emails, jobs, or rate-limit data.
 */
class SubmissionPreflightService {
	const STATUS_PASS    = 'pass';
	const STATUS_WARNING = 'warning';
	const STATUS_FAIL    = 'fail';
	const STATUS_INFO    = 'info';

	/**
	 * Form repository.
	 *
	 * @var FormRepository
	 */
	private $forms;

	/**
	 * Settings repository.
	 *
	 * @var \SUBIN_SettingsRepository
	 */
	private $settings;

	/**
	 * Auto-reply rules.
	 *
	 * @var \SUBIN\AutoReplies\AutoReplyRuleRepository
	 */
	private $rules;

	/**
	 * Auto-reply templates.
	 *
	 * @var \SUBIN\AutoReplies\AutoReplyTemplateRepository
	 */
	private $templates;

	/**
	 * Constructor.
	 *
	 * @param FormRepository                                $forms Form repository.
	 * @param \SUBIN_SettingsRepository                     $settings Settings repository.
	 * @param \SUBIN\AutoReplies\AutoReplyRuleRepository    $rules Auto-reply rules.
	 * @param \SUBIN\AutoReplies\AutoReplyTemplateRepository $templates Auto-reply templates.
	 */
	public function __construct(
		FormRepository $forms,
		\SUBIN_SettingsRepository $settings,
		\SUBIN\AutoReplies\AutoReplyRuleRepository $rules,
		\SUBIN\AutoReplies\AutoReplyTemplateRepository $templates
	) {
		$this->forms     = $forms;
		$this->settings  = $settings;
		$this->rules     = $rules;
		$this->templates = $templates;
	}

	/**
	 * Audit the exact saved draft revision.
	 *
	 * @param int      $form_id Form id.
	 * @param int|null $expected_draft_revision Expected draft revision.
	 * @param int|null $expected_published_revision Expected published revision.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function run( $form_id, $expected_draft_revision = null, $expected_published_revision = null ) {
		$form_id = absint( $form_id );
		$form    = $this->forms->get( $form_id );
		if ( ! $form ) {
			return new \WP_Error(
				'form_not_found',
				__( 'Form not found.', 'submission-inbox' ),
				array( 'status' => 404 )
			);
		}

		$state              = $this->forms->get_design_studio_state( $form );
		$draft_revision     = max( 0, (int) ( $state['draft']['revision'] ?? 0 ) );
		$published_revision = max( 0, (int) ( $state['published']['revision'] ?? 0 ) );
		if (
			( null !== $expected_draft_revision && (int) $expected_draft_revision !== $draft_revision )
			|| ( null !== $expected_published_revision && (int) $expected_published_revision !== $published_revision )
		) {
			return new \WP_Error(
				'preflight_revision_conflict',
				__( 'The saved draft changed. Run the checks again.', 'submission-inbox' ),
				array(
					'status'             => 409,
					'revision'           => $draft_revision,
					'published_revision' => $published_revision,
				)
			);
		}

		$project         = is_array( $state['draft']['project'] ?? null ) ? $state['draft']['project'] : array();
		$analysis        = $this->analyze_project( $project );
		$form_settings   = $this->forms->decode_settings( $form );
		$global_settings = $this->settings->all();
		$checks          = array();

		$this->add_structure_checks( $checks, $analysis );
		$this->add_field_checks( $checks, $analysis );
		$this->add_conditional_logic_check( $checks, $analysis );
		$this->add_message_check( $checks, $analysis );

		$delivery = $this->delivery_plan( $form, $form_settings, $global_settings, $analysis );
		$this->add_delivery_checks( $checks, $delivery );
		$this->add_protection_checks( $checks, $form, $global_settings, $analysis );

		$counts = array(
			self::STATUS_PASS    => 0,
			self::STATUS_WARNING => 0,
			self::STATUS_FAIL    => 0,
			self::STATUS_INFO    => 0,
		);
		foreach ( $checks as $check ) {
			$status = sanitize_key( $check['status'] ?? self::STATUS_INFO );
			if ( array_key_exists( $status, $counts ) ) {
				++$counts[ $status ];
			}
		}

		$ready       = 0 === $counts[ self::STATUS_FAIL ];
		$fingerprint = hash(
			'sha256',
			(string) wp_json_encode(
				array(
					'form_id'            => $form_id,
					'draft_revision'     => $draft_revision,
					'published_revision' => $published_revision,
					'project'            => $project,
					'delivery'           => $delivery,
				)
			)
		);

		return array(
			'state'              => $ready ? ( $counts[ self::STATUS_WARNING ] > 0 ? 'ready_with_warnings' : 'ready' ) : 'blocked',
			'ready'              => $ready,
			'revision'           => $draft_revision,
			'published_revision' => $published_revision,
			'fingerprint'        => $fingerprint,
			'counts'             => $counts,
			'checks'             => $checks,
			'delivery_plan'      => $delivery,
			'transport'          => array(
				'configured' => ! empty( $delivery['smtp_configured'] ),
				'verified'   => false,
			),
		);
	}

	/**
	 * Validate generated sample values without invoking the submission pipeline.
	 *
	 * @param int      $form_id Form id.
	 * @param int|null $expected_draft_revision Expected draft revision.
	 * @param int|null $expected_published_revision Expected published revision.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function simulate( $form_id, $expected_draft_revision = null, $expected_published_revision = null ) {
		$preflight = $this->run( $form_id, $expected_draft_revision, $expected_published_revision );
		if ( is_wp_error( $preflight ) ) {
			return $preflight;
		}

		$form     = $this->forms->get( $form_id );
		$state    = $this->forms->get_design_studio_state( $form );
		$analysis = $this->analyze_project( is_array( $state['draft']['project'] ?? null ) ? $state['draft']['project'] : array() );
		$errors   = array();
		$tested   = array();

		foreach ( $analysis['fields'] as $field ) {
			$value = $this->sample_value( $field );
			$error = $this->validate_sample_value( $field, $value );
			if ( '' !== $error ) {
				$errors[ $field['key'] ] = $error;
			}
			$tested[] = array(
				'key'    => $field['key'],
				'label'  => $field['label'],
				'type'   => $field['type'],
				'status' => '' === $error ? self::STATUS_PASS : self::STATUS_FAIL,
			);
		}

		$passed = ! empty( $preflight['ready'] ) && empty( $errors );

		return array(
			'state'              => $passed ? 'passed' : 'blocked',
			'passed'             => $passed,
			'message'            => $passed
				? __( 'Safe simulation passed. No lead or email was created.', 'submission-inbox' )
				: __( 'Safe simulation found configuration errors.', 'submission-inbox' ),
			'revision'           => $preflight['revision'],
			'published_revision' => $preflight['published_revision'],
			'fingerprint'        => $preflight['fingerprint'],
			'field_count'        => count( $analysis['fields'] ),
			'fields'             => $tested,
			'errors'             => $errors,
			'delivery_plan'      => $preflight['delivery_plan'],
			'side_effects'       => array(
				'leads_created' => 0,
				'emails_sent'   => 0,
				'jobs_queued'   => 0,
			),
		);
	}

	/**
	 * Add a normalized check.
	 *
	 * @param array<int,array<string,mixed>> $checks Checks.
	 * @param string                         $id Check id.
	 * @param string                         $status Status.
	 * @param string                         $label Label.
	 * @param string                         $message Message.
	 * @return void
	 */
	private function add_check( array &$checks, $id, $status, $label, $message ) {
		$checks[] = array(
			'id'      => sanitize_key( $id ),
			'status'  => sanitize_key( $status ),
			'label'   => sanitize_text_field( $label ),
			'message' => sanitize_text_field( $message ),
		);
	}

	/**
	 * Add structural checks.
	 *
	 * @param array<int,array<string,mixed>> $checks Checks.
	 * @param array<string,mixed>            $analysis Project analysis.
	 * @return void
	 */
	private function add_structure_checks( array &$checks, array $analysis ) {
		if ( empty( $analysis['fields'] ) ) {
			$this->add_check(
				$checks,
				'form_fields',
				self::STATUS_FAIL,
				__( 'Form fields', 'submission-inbox' ),
				__( 'Add at least one field before publishing.', 'submission-inbox' )
			);
		} else {
			$this->add_check(
				$checks,
				'form_fields',
				self::STATUS_PASS,
				__( 'Form fields', 'submission-inbox' ),
				sprintf(
					/* translators: %d: number of form fields. */
					_n( '%d field is ready.', '%d fields are ready.', count( $analysis['fields'] ), 'submission-inbox' ),
					count( $analysis['fields'] )
				)
			);
		}

		if ( 0 === (int) $analysis['submit_count'] ) {
			$this->add_check(
				$checks,
				'submit_button',
				self::STATUS_FAIL,
				__( 'Submit action', 'submission-inbox' ),
				__( 'Add a submit button so visitors can send the form.', 'submission-inbox' )
			);
		} elseif ( 1 === (int) $analysis['submit_count'] ) {
			$this->add_check(
				$checks,
				'submit_button',
				self::STATUS_PASS,
				__( 'Submit action', 'submission-inbox' ),
				__( 'The form has one submit button.', 'submission-inbox' )
			);
		} else {
			$this->add_check(
				$checks,
				'submit_button',
				self::STATUS_WARNING,
				__( 'Submit action', 'submission-inbox' ),
				__( 'The form has multiple submit buttons. Confirm that this is intentional.', 'submission-inbox' )
			);
		}

		$this->add_check(
			$checks,
			'form_wrapper',
			! empty( $analysis['wrapper_found'] ) ? self::STATUS_PASS : self::STATUS_WARNING,
			__( 'Form container', 'submission-inbox' ),
			! empty( $analysis['wrapper_found'] )
				? __( 'The controlled form container is present.', 'submission-inbox' )
				: __( 'A legacy project container was detected. Publish will normalize it.', 'submission-inbox' )
		);
	}

	/**
	 * Add field identity and option checks.
	 *
	 * @param array<int,array<string,mixed>> $checks Checks.
	 * @param array<string,mixed>            $analysis Project analysis.
	 * @return void
	 */
	private function add_field_checks( array &$checks, array $analysis ) {
		$issues = array();
		$keys   = array();

		foreach ( $analysis['fields'] as $index => $field ) {
			$position = $index + 1;
			if ( empty( $field['type_valid'] ) ) {
				$issues[] = sprintf(
					/* translators: %d: field position. */
					__( 'Field %d has an unsupported type.', 'submission-inbox' ),
					$position
				);
			}
			if ( '' === $field['key'] ) {
				$issues[] = sprintf(
					/* translators: %d: field position. */
					__( 'Field %d needs a unique key.', 'submission-inbox' ),
					$position
				);
			} elseif ( isset( $keys[ $field['key'] ] ) ) {
				$issues[] = sprintf(
					/* translators: %s: duplicate field key. */
					__( 'Field key "%s" is used more than once.', 'submission-inbox' ),
					$field['key']
				);
			} else {
				$keys[ $field['key'] ] = true;
			}

			if ( 'hidden' !== $field['type'] && '' === $field['label'] ) {
				$issues[] = sprintf(
					/* translators: %d: field position. */
					__( 'Field %d needs a visible label.', 'submission-inbox' ),
					$position
				);
			}

			if ( in_array( $field['type'], array( 'select', 'radio', 'checkbox' ), true ) && empty( $field['options'] ) ) {
				$issues[] = sprintf(
					/* translators: %s: field label or key. */
					__( '"%s" needs at least one choice.', 'submission-inbox' ),
					'' !== $field['label'] ? $field['label'] : $field['key']
				);
			}
		}

		$this->add_check(
			$checks,
			'field_configuration',
			empty( $issues ) ? self::STATUS_PASS : self::STATUS_FAIL,
			__( 'Field configuration', 'submission-inbox' ),
			empty( $issues )
				? __( 'Field keys, labels, and choices are valid.', 'submission-inbox' )
				: implode( ' ', array_slice( $issues, 0, 3 ) )
		);

		$email_fields = array_filter(
			$analysis['fields'],
			static function ( $field ) {
				return 'email' === $field['type'];
			}
		);
		$this->add_check(
			$checks,
			'email_field',
			! empty( $email_fields ) ? self::STATUS_PASS : self::STATUS_WARNING,
			__( 'Visitor email', 'submission-inbox' ),
			! empty( $email_fields )
				? __( 'An Email field is available for Reply-To and auto-replies.', 'submission-inbox' )
				: __( 'No Email field was found. Reply-To and visitor auto-replies cannot use a submitted address.', 'submission-inbox' )
		);
	}

	/**
	 * Validate conditional-logic schema, references, and dependency direction.
	 *
	 * Conditional visibility may only depend on fields that precede the target.
	 * That rule keeps evaluation deterministic and makes the dependency graph
	 * acyclic, including when a source field lives inside a row or section that
	 * is itself controlled by another rule.
	 *
	 * @param array<int,array<string,mixed>> $checks Checks.
	 * @param array<string,mixed>            $analysis Project analysis.
	 * @return void
	 */
	private function add_conditional_logic_check( array &$checks, array $analysis ) {
		if ( empty( $analysis['logic_present'] ) ) {
			$this->add_check(
				$checks,
				'conditional_logic',
				self::STATUS_INFO,
				__( 'Conditional logic', 'submission-inbox' ),
				__( 'No conditional visibility rules are configured.', 'submission-inbox' )
			);
			return;
		}

		if ( empty( $analysis['logic_valid'] ) ) {
			$this->add_check(
				$checks,
				'conditional_logic',
				self::STATUS_FAIL,
				__( 'Conditional logic', 'submission-inbox' ),
				__( 'Conditional logic data is malformed.', 'submission-inbox' )
			);
			return;
		}

		$logic         = is_array( $analysis['logic'] ?? null ) ? $analysis['logic'] : array();
		$known_targets = array(
			'field' => array_keys( $analysis['target_positions']['field'] ?? array() ),
			'row'   => array_keys( $analysis['target_positions']['row'] ?? array() ),
			'block' => array_keys( $analysis['target_positions']['block'] ?? array() ),
		);
		$evaluator = new ConditionalLogicEvaluator();
		$clean     = $evaluator->sanitize( $logic );
		$active_rules = array_values(
			array_filter(
				$clean['rules'],
				static function ( $rule ) {
					return ! empty( $rule['enabled'] );
				}
			)
		);
		$active_logic = array(
			'version' => ConditionalLogicEvaluator::VERSION,
			'rules'   => $active_rules,
		);
		$errors = array_merge(
			$evaluator->validate( $logic ),
			$evaluator->validate(
				$active_logic,
				array_keys( $analysis['field_positions'] ?? array() ),
				$known_targets
			)
		);
		$issues    = array();
		$fields_by_key = array();
		foreach ( $analysis['fields'] as $field ) {
			if ( '' !== (string) ( $field['key'] ?? '' ) ) {
				$fields_by_key[ $field['key'] ] = $field;
			}
		}

		foreach ( $errors as $error ) {
			$issues[] = sprintf(
				/* translators: %s: conditional logic validation message. */
				__( 'Conditional logic is invalid: %s', 'submission-inbox' ),
				sanitize_text_field( $error['message'] ?? '' )
			);
		}

		$rule_targets = array();
		foreach ( $active_rules as $rule ) {
			$target_type = (string) ( $rule['target']['type'] ?? '' );
			$target_id   = (string) ( $rule['target']['id'] ?? '' );
			$target_key  = $this->conditional_logic_target_key( $target_type, $target_id );
			$rule_targets[ $target_key ] = true;

			if ( (int) ( $analysis['target_counts'][ $target_type ][ $target_id ] ?? 0 ) > 1 ) {
				$issues[] = sprintf(
					/* translators: 1: target type, 2: target identifier. */
					__( 'Conditional logic target %1$s:%2$s is assigned to more than one component.', 'submission-inbox' ),
					$target_type,
					$target_id
				);
			}
		}

		$graph = array();
		foreach ( $active_rules as $rule ) {
			$target_type     = (string) ( $rule['target']['type'] ?? '' );
			$target_id       = (string) ( $rule['target']['id'] ?? '' );
			$target_key      = $this->conditional_logic_target_key( $target_type, $target_id );
			$target_position = $analysis['target_positions'][ $target_type ][ $target_id ] ?? null;
			$graph[ $target_key ] = $graph[ $target_key ] ?? array();

			foreach ( $rule['groups'] as $group ) {
				foreach ( $group['conditions'] as $condition ) {
					$source_id       = (string) ( $condition['field'] ?? '' );
					$source_position = $analysis['field_positions'][ $source_id ] ?? null;
					$target_fields   = $analysis['target_fields'][ $target_key ] ?? array();

					if ( in_array( $source_id, $target_fields, true ) ) {
						$issues[] = sprintf(
							/* translators: %s: conditional logic rule identifier. */
							__( 'Rule %s depends on a field inside its own target.', 'submission-inbox' ),
							(string) ( $rule['id'] ?? '' )
						);
					} elseif (
						null !== $source_position
						&& null !== $target_position
						&& (int) $source_position >= (int) $target_position
					) {
						$issues[] = sprintf(
							/* translators: %s: conditional logic rule identifier. */
							__( 'Rule %s must use a source field placed before its target.', 'submission-inbox' ),
							(string) ( $rule['id'] ?? '' )
						);
					}

					$source_field = $fields_by_key[ $source_id ] ?? null;
					if (
						is_array( $source_field )
						&& in_array( $source_field['type'], array( 'select', 'radio', 'checkbox' ), true )
						&& in_array( $condition['operator'], array( 'is', 'is_not' ), true )
					) {
						$allowed_values = array_map(
							'strval',
							wp_list_pluck( $source_field['options'], 'value' )
						);
						foreach ( $evaluator->normalize_value( $condition['value'] ?? null ) as $expected_value ) {
							if ( ! in_array( $expected_value, $allowed_values, true ) ) {
								$issues[] = sprintf(
									/* translators: 1: rule identifier, 2: removed choice value. */
									__( 'Rule %1$s uses a choice that no longer exists: %2$s.', 'submission-inbox' ),
									(string) ( $rule['id'] ?? '' ),
									$expected_value
								);
							}
						}
					}

					foreach ( $analysis['field_visibility_targets'][ $source_id ] ?? array() as $source_target_key ) {
						if ( isset( $rule_targets[ $source_target_key ] ) ) {
							$graph[ $target_key ][ $source_target_key ] = true;
						}
					}
				}
			}
		}

		if ( $this->conditional_logic_has_cycle( $graph ) ) {
			$issues[] = __( 'Conditional logic contains a dependency cycle.', 'submission-inbox' );
		}

		$issues = array_values( array_unique( array_filter( $issues ) ) );
		if ( ! empty( $issues ) ) {
			$this->add_check(
				$checks,
				'conditional_logic',
				self::STATUS_FAIL,
				__( 'Conditional logic', 'submission-inbox' ),
				implode( ' ', array_slice( $issues, 0, 3 ) )
			);
			return;
		}

		$rule_count = count( $active_rules );
		$this->add_check(
			$checks,
			'conditional_logic',
			$rule_count > 0 ? self::STATUS_PASS : self::STATUS_INFO,
			__( 'Conditional logic', 'submission-inbox' ),
			$rule_count > 0
				? sprintf(
					/* translators: %d: number of conditional logic rules. */
					_n( '%d conditional visibility rule is valid.', '%d conditional visibility rules are valid.', $rule_count, 'submission-inbox' ),
					$rule_count
				)
				: __( 'No conditional visibility rules are configured.', 'submission-inbox' )
		);
	}

	/**
	 * Detect a cycle in the target dependency graph.
	 *
	 * @param array<string,array<string,bool>> $graph Target dependency graph.
	 * @return bool
	 */
	private function conditional_logic_has_cycle( array $graph ) {
		$visiting = array();
		$visited  = array();

		foreach ( array_keys( $graph ) as $node ) {
			if ( $this->conditional_logic_node_has_cycle( $node, $graph, $visiting, $visited ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Depth-first cycle check for one conditional target.
	 *
	 * @param string                           $node Target key.
	 * @param array<string,array<string,bool>> $graph Target dependency graph.
	 * @param array<string,bool>               $visiting Active path.
	 * @param array<string,bool>               $visited Completed nodes.
	 * @return bool
	 */
	private function conditional_logic_node_has_cycle( $node, array $graph, array &$visiting, array &$visited ) {
		if ( isset( $visiting[ $node ] ) ) {
			return true;
		}
		if ( isset( $visited[ $node ] ) ) {
			return false;
		}

		$visiting[ $node ] = true;
		foreach ( array_keys( $graph[ $node ] ?? array() ) as $dependency ) {
			if ( $this->conditional_logic_node_has_cycle( $dependency, $graph, $visiting, $visited ) ) {
				return true;
			}
		}

		unset( $visiting[ $node ] );
		$visited[ $node ] = true;
		return false;
	}

	/**
	 * Add global message check.
	 *
	 * @param array<int,array<string,mixed>> $checks Checks.
	 * @param array<string,mixed>            $analysis Project analysis.
	 * @return void
	 */
	private function add_message_check( array &$checks, array $analysis ) {
		$required = array(
			'data-subin-validation-error-message',
			'data-subin-required-error-message',
			'data-subin-spam-error-message',
			'data-subin-success-message',
		);
		$missing = 0;
		foreach ( $required as $key ) {
			if ( '' === trim( (string) ( $analysis['wrapper_attributes'][ $key ] ?? '' ) ) ) {
				++$missing;
			}
		}

		$this->add_check(
			$checks,
			'form_messages',
			0 === $missing ? self::STATUS_PASS : self::STATUS_WARNING,
			__( 'Visitor messages', 'submission-inbox' ),
			0 === $missing
				? __( 'Success, validation, and spam messages are configured.', 'submission-inbox' )
				: __( 'Some visitor messages use safe product defaults.', 'submission-inbox' )
		);
	}

	/**
	 * Build the delivery plan used by checks and sandbox output.
	 *
	 * @param array<string,mixed> $form Form row.
	 * @param array<string,mixed> $form_settings Form settings.
	 * @param array<string,mixed> $global_settings Global settings.
	 * @param array<string,mixed> $analysis Project analysis.
	 * @return array<string,mixed>
	 */
	private function delivery_plan( array $form, array $form_settings, array $global_settings, array $analysis ) {
		$storage_enabled = ! empty( $global_settings['store_leads'] ) && ! empty( $form['save_leads'] );
		$notify          = is_array( $form_settings['notification'] ?? null ) ? $form_settings['notification'] : array();
		$recipient_mode  = sanitize_key( $notify['recipient_mode'] ?? '' );
		if ( ! in_array( $recipient_mode, array( 'default', 'custom' ), true ) ) {
			$recipient_mode = is_email( sanitize_email( $form['notification_email'] ?? '' ) ) ? 'custom' : 'default';
		}
		$recipient = 'custom' === $recipient_mode
			? sanitize_email( $form['notification_email'] ?? '' )
			: sanitize_email( $global_settings['notification_recipient_email'] ?? '' );
		$notification_enabled = ! empty( $form['notification_enabled'] );
		$notification_ready   = $notification_enabled && is_email( $recipient );

		$rule = $this->rules->find_enabled_rule(
			\SUBIN\AutoReplies\AutoReplyRuleRepository::PROVIDER_SUBMISSION_INBOX_FORM,
			(string) absint( $form['id'] ?? 0 )
		);
		$template = $rule ? $this->templates->get( absint( $rule['template_id'] ?? 0 ) ) : null;
		$email_key = $rule ? sanitize_key( $rule['visitor_email_field'] ?? '' ) : '';
		$email_field_found = false;
		foreach ( $analysis['fields'] as $field ) {
			if ( 'email' === $field['type'] && ( '' === $email_key || $email_key === $field['key'] ) ) {
				$email_field_found = true;
				break;
			}
		}

		$smtp_enabled    = ! empty( $global_settings['enable_smtp'] );
		$smtp_port       = absint( $global_settings['smtp_port'] ?? 0 );
		$smtp_encryption = sanitize_key( $global_settings['smtp_encryption'] ?? '' );
		$smtp_configured = $smtp_enabled
			&& '' !== trim( (string) ( $global_settings['smtp_host'] ?? '' ) )
			&& $smtp_port > 0
			&& $smtp_port <= 65535
			&& in_array( $smtp_encryption, array( 'none', 'ssl', 'tls' ), true );
		if ( $smtp_configured && ! empty( $global_settings['smtp_auth'] ) ) {
			$smtp_configured = '' !== trim( (string) ( $global_settings['smtp_username'] ?? '' ) )
				&& '' !== trim( (string) ( $global_settings['smtp_password'] ?? '' ) );
		}

		return array(
			'storage_enabled'       => $storage_enabled,
			'notification_enabled'  => $notification_enabled,
			'notification_ready'    => $notification_ready,
			'recipient_mode'        => $recipient_mode,
			'recipient'             => $this->mask_email( $recipient ),
			'auto_reply_enabled'    => ! empty( $rule ),
			'auto_reply_ready'      => ! empty( $rule ) && $storage_enabled && ! empty( $template ) && $email_field_found,
			'auto_reply_template'   => $template ? sanitize_text_field( $template['name'] ?? '' ) : '',
			'auto_reply_email_key'  => $email_key,
			'auto_reply_email_found' => $email_field_found,
			'smtp_enabled'          => $smtp_enabled,
			'smtp_configured'       => $smtp_configured,
			'has_destination'       => $storage_enabled || $notification_ready,
		);
	}

	/**
	 * Add delivery destination, notification, and transport checks.
	 *
	 * @param array<int,array<string,mixed>> $checks Checks.
	 * @param array<string,mixed>            $delivery Delivery plan.
	 * @return void
	 */
	private function add_delivery_checks( array &$checks, array $delivery ) {
		$this->add_check(
			$checks,
			'lead_storage',
			! empty( $delivery['storage_enabled'] ) ? self::STATUS_PASS : self::STATUS_WARNING,
			__( 'Lead storage', 'submission-inbox' ),
			! empty( $delivery['storage_enabled'] )
				? __( 'Accepted submissions will be stored in InboxMend.', 'submission-inbox' )
				: __( 'Lead storage is disabled for this form or globally.', 'submission-inbox' )
		);

		if ( ! empty( $delivery['notification_ready'] ) ) {
			$this->add_check(
				$checks,
				'admin_notification',
				self::STATUS_PASS,
				__( 'Admin notification', 'submission-inbox' ),
				sprintf(
					/* translators: %s: masked email recipient. */
					__( 'Notifications will be prepared for %s.', 'submission-inbox' ),
					$delivery['recipient']
				)
			);
		} elseif ( ! empty( $delivery['notification_enabled'] ) ) {
			$this->add_check(
				$checks,
				'admin_notification',
				self::STATUS_FAIL,
				__( 'Admin notification', 'submission-inbox' ),
				__( 'Notification is enabled, but its recipient is missing or invalid.', 'submission-inbox' )
			);
		} else {
			$this->add_check(
				$checks,
				'admin_notification',
				self::STATUS_INFO,
				__( 'Admin notification', 'submission-inbox' ),
				__( 'Admin notification is disabled.', 'submission-inbox' )
			);
		}

		$this->add_check(
			$checks,
			'delivery_destination',
			! empty( $delivery['has_destination'] ) ? self::STATUS_PASS : self::STATUS_FAIL,
			__( 'Submission destination', 'submission-inbox' ),
			! empty( $delivery['has_destination'] )
				? __( 'The form has at least one durable delivery destination.', 'submission-inbox' )
				: __( 'Enable lead storage or configure a valid admin notification recipient.', 'submission-inbox' )
		);

		if ( ! empty( $delivery['auto_reply_enabled'] ) && ! empty( $delivery['auto_reply_ready'] ) ) {
			$this->add_check(
				$checks,
				'auto_reply',
				self::STATUS_PASS,
				__( 'Visitor auto-reply', 'submission-inbox' ),
				__( 'The enabled auto-reply has a template and visitor Email field.', 'submission-inbox' )
			);
		} elseif ( ! empty( $delivery['auto_reply_enabled'] ) ) {
			$this->add_check(
				$checks,
				'auto_reply',
				self::STATUS_WARNING,
				__( 'Visitor auto-reply', 'submission-inbox' ),
				__( 'The enabled auto-reply needs lead storage, a template, and its selected Email field.', 'submission-inbox' )
			);
		} else {
			$this->add_check(
				$checks,
				'auto_reply',
				self::STATUS_INFO,
				__( 'Visitor auto-reply', 'submission-inbox' ),
				__( 'No visitor auto-reply is enabled.', 'submission-inbox' )
			);
		}

		$email_delivery_used = ! empty( $delivery['notification_ready'] ) || ! empty( $delivery['auto_reply_ready'] );
		if ( ! $email_delivery_used ) {
			$status  = self::STATUS_INFO;
			$message = __( 'This delivery plan does not require email.', 'submission-inbox' );
		} elseif ( ! empty( $delivery['smtp_configured'] ) ) {
			$status  = self::STATUS_PASS;
			$message = __( 'SMTP is configured. Connection health is not tested during preflight.', 'submission-inbox' );
		} elseif ( ! empty( $delivery['smtp_enabled'] ) ) {
			$status  = self::STATUS_WARNING;
			$message = __( 'SMTP is enabled but incomplete. WordPress mail delivery may fail.', 'submission-inbox' );
		} else {
			$status  = self::STATUS_INFO;
			$message = __( 'Email will use the default WordPress mail transport.', 'submission-inbox' );
		}
		$this->add_check( $checks, 'email_transport', $status, __( 'Email transport', 'submission-inbox' ), $message );
	}

	/**
	 * Add spam-protection and privacy checks.
	 *
	 * @param array<int,array<string,mixed>> $checks Checks.
	 * @param array<string,mixed>            $form Form row.
	 * @param array<string,mixed>            $global_settings Global settings.
	 * @param array<string,mixed>            $analysis Project analysis.
	 * @return void
	 */
	private function add_protection_checks( array &$checks, array $form, array $global_settings, array $analysis ) {
		$recaptcha_enabled = ! empty( $global_settings['recaptcha_enabled'] );
		$recaptcha_ready   = $recaptcha_enabled
			&& '' !== trim( (string) ( $global_settings['recaptcha_site_key'] ?? '' ) )
			&& '' !== trim( (string) ( $global_settings['recaptcha_secret_key'] ?? '' ) );
		if ( $recaptcha_ready ) {
			$status  = self::STATUS_PASS;
			$message = __( 'reCAPTCHA v3 keys are configured. Network health is not tested here.', 'submission-inbox' );
		} elseif ( $recaptcha_enabled ) {
			$status  = self::STATUS_FAIL;
			$message = __( 'reCAPTCHA is enabled but its site or secret key is missing. Complete the protection setup before publishing.', 'submission-inbox' );
		} else {
			$status  = self::STATUS_INFO;
			$message = __( 'Built-in nonce, honeypot, timing, replay, and rate-limit protection remain active.', 'submission-inbox' );
		}
		$this->add_check( $checks, 'spam_protection', $status, __( 'Spam protection', 'submission-inbox' ), $message );

		$has_privacy = ! empty( $analysis['privacy_found'] )
			|| '' !== trim( (string) ( $form['privacy_text'] ?? '' ) );
		foreach ( $analysis['fields'] as $field ) {
			if ( 'consent' === $field['type'] ) {
				$has_privacy = true;
				break;
			}
		}
		$this->add_check(
			$checks,
			'privacy_notice',
			$has_privacy ? self::STATUS_PASS : self::STATUS_WARNING,
			__( 'Privacy context', 'submission-inbox' ),
			$has_privacy
				? __( 'The form includes consent or privacy context.', 'submission-inbox' )
				: __( 'Consider adding a consent field or privacy note before collecting personal data.', 'submission-inbox' )
		);

		$this->add_check(
			$checks,
			'form_availability',
			FormRepository::STATUS_ACTIVE === sanitize_key( $form['status'] ?? '' ) ? self::STATUS_PASS : self::STATUS_INFO,
			__( 'Form availability', 'submission-inbox' ),
			FormRepository::STATUS_ACTIVE === sanitize_key( $form['status'] ?? '' )
				? __( 'The published form is active.', 'submission-inbox' )
				: __( 'The form is inactive and will stay unavailable until activated.', 'submission-inbox' )
		);
	}

	/**
	 * Extract controlled components from native GrapesJS project data.
	 *
	 * @param array<string,mixed> $project Project.
	 * @return array<string,mixed>
	 */
	private function analyze_project( array $project ) {
		$analysis = array(
			'fields'             => array(),
			'submit_count'       => 0,
			'wrapper_found'      => false,
			'wrapper_attributes' => array(),
			'privacy_found'      => false,
			'logic_present'      => array_key_exists( 'subinLogic', $project ),
			'logic_valid'        => ! array_key_exists( 'subinLogic', $project ) || is_array( $project['subinLogic'] ),
			'logic'              => is_array( $project['subinLogic'] ?? null ) ? $project['subinLogic'] : array(),
			'sequence'           => 0,
			'field_positions'    => array(),
			'target_positions'   => array(
				'field' => array(),
				'row'   => array(),
				'block' => array(),
			),
			'target_counts'      => array(
				'field' => array(),
				'row'   => array(),
				'block' => array(),
			),
			'target_fields'      => array(),
			'field_visibility_targets' => array(),
		);
		$this->walk_project( $project, $analysis, array() );

		return $analysis;
	}

	/**
	 * Walk project nodes recursively.
	 *
	 * @param mixed                $node Project node.
	 * @param array<string,mixed>  $analysis Mutable analysis.
	 * @param array<int,string>    $ancestor_targets Row/block targets containing this node.
	 * @return void
	 */
	private function walk_project( $node, array &$analysis, array $ancestor_targets = array() ) {
		if ( ! is_array( $node ) ) {
			return;
		}

		$attributes = is_array( $node['attributes'] ?? null ) ? $node['attributes'] : array();
		$type       = sanitize_key( (string) ( $attributes['data-subin-component'] ?? $this->component_name_from_type( $node['type'] ?? '' ) ) );
		$next_ancestor_targets = $ancestor_targets;

		if ( 'form-wrapper' === $type ) {
			$analysis['wrapper_found']      = true;
			$analysis['wrapper_attributes'] = array_merge( $analysis['wrapper_attributes'], $attributes );
		} elseif ( 'field' === $type ) {
			$field = $this->field_from_component( $attributes );
			$analysis['fields'][] = $field;
			++$analysis['sequence'];

			$field_id = (string) $field['key'];
			if ( '' !== $field_id ) {
				if ( ! isset( $analysis['field_positions'][ $field_id ] ) ) {
					$analysis['field_positions'][ $field_id ] = $analysis['sequence'];
				}

				$field_target = $this->conditional_logic_target_key( 'field', $field_id );
				$this->register_conditional_logic_target( $analysis, 'field', $field_id, $analysis['sequence'] );
				$analysis['field_visibility_targets'][ $field_id ] = array_values(
					array_unique( array_merge( $ancestor_targets, array( $field_target ) ) )
				);
				foreach ( $analysis['field_visibility_targets'][ $field_id ] as $target_key ) {
					$analysis['target_fields'][ $target_key ]   = $analysis['target_fields'][ $target_key ] ?? array();
					$analysis['target_fields'][ $target_key ][] = $field_id;
				}
			}
		} elseif ( 'row' === $type ) {
			++$analysis['sequence'];
			$target_id = $this->conditional_logic_identifier(
				$attributes['data-subin-row-id'] ?? ( $attributes['data-subin-style-id'] ?? '' )
			);
			if ( '' !== $target_id ) {
				$this->register_conditional_logic_target( $analysis, 'row', $target_id, $analysis['sequence'] );
				$next_ancestor_targets[] = $this->conditional_logic_target_key( 'row', $target_id );
			}
		} elseif ( 'submit-button' === $type ) {
			++$analysis['submit_count'];
		} elseif ( 'privacy' === $type ) {
			$analysis['privacy_found'] = true;
			++$analysis['sequence'];
			$target_id = $this->conditional_logic_block_identifier( $attributes );
			if ( '' !== $target_id ) {
				$this->register_conditional_logic_target( $analysis, 'block', $target_id, $analysis['sequence'] );
				$next_ancestor_targets[] = $this->conditional_logic_target_key( 'block', $target_id );
			}
		} elseif ( $this->is_conditional_logic_block_type( $type ) ) {
			++$analysis['sequence'];
			$target_id = $this->conditional_logic_block_identifier( $attributes );
			if ( '' !== $target_id ) {
				$this->register_conditional_logic_target( $analysis, 'block', $target_id, $analysis['sequence'] );
				$next_ancestor_targets[] = $this->conditional_logic_target_key( 'block', $target_id );
			}
		}

		foreach ( $node as $key => $child ) {
			if ( in_array( $key, array( 'attributes', 'style', 'traits', 'subinLogic' ), true ) ) {
				continue;
			}
			if ( is_array( $child ) ) {
				$child_ancestors = 'components' === $key ? $next_ancestor_targets : $ancestor_targets;
				$this->walk_project( $child, $analysis, $child_ancestors );
			}
		}
	}

	/**
	 * Register one target identity and its first form-order position.
	 *
	 * @param array<string,mixed> $analysis Mutable project analysis.
	 * @param string              $type Target type.
	 * @param string              $id Target identifier.
	 * @param int                 $position Component position.
	 * @return void
	 */
	private function register_conditional_logic_target( array &$analysis, $type, $id, $position ) {
		$analysis['target_counts'][ $type ][ $id ] = (int) ( $analysis['target_counts'][ $type ][ $id ] ?? 0 ) + 1;
		if ( ! isset( $analysis['target_positions'][ $type ][ $id ] ) ) {
			$analysis['target_positions'][ $type ][ $id ] = (int) $position;
		}
	}

	/**
	 * Build the namespaced graph key for one visibility target.
	 *
	 * @param string $type Target type.
	 * @param string $id Target identifier.
	 * @return string
	 */
	private function conditional_logic_target_key( $type, $id ) {
		return (string) $type . ':' . (string) $id;
	}

	/**
	 * Normalize a Conditional Logic identifier using the evaluator contract.
	 *
	 * @param mixed $value Raw identifier.
	 * @return string
	 */
	private function conditional_logic_identifier( $value ) {
		if ( ! is_scalar( $value ) ) {
			return '';
		}

		$value = preg_replace( '/[^A-Za-z0-9_.:-]/', '', trim( (string) $value ) );
		return substr( (string) $value, 0, ConditionalLogicEvaluator::MAX_IDENTIFIER_LENGTH );
	}

	/**
	 * Resolve a block identity using the same preference as public rendering.
	 *
	 * @param array<string,mixed> $attributes Component attributes.
	 * @return string
	 */
	private function conditional_logic_block_identifier( array $attributes ) {
		return $this->conditional_logic_identifier(
			$attributes['data-subin-logic-id'] ?? ( $attributes['data-subin-style-id'] ?? '' )
		);
	}

	/**
	 * Whether a controlled component is a Conditional Logic block target.
	 *
	 * @param string $type Controlled component name.
	 * @return bool
	 */
	private function is_conditional_logic_block_type( $type ) {
		return in_array(
			$type,
			array( 'text-block', 'html-block', 'notice', 'divider', 'section', 'spacer', 'image-block' ),
			true
		);
	}

	/**
	 * Map GrapesJS type names to controlled component names.
	 *
	 * @param mixed $type Component type.
	 * @return string
	 */
	private function component_name_from_type( $type ) {
		$map = array(
			'wrapper'             => 'form-wrapper',
			'subin-row'           => 'row',
			'subin-field'         => 'field',
			'subin-submit-button' => 'submit-button',
			'subin-text-block'    => 'text-block',
			'subin-html-block'    => 'html-block',
			'subin-notice-block'  => 'notice',
			'subin-divider-block' => 'divider',
			'subin-privacy-block' => 'privacy',
			'subin-section-block' => 'section',
			'subin-spacer-block'  => 'spacer',
			'subin-image-block'   => 'image-block',
		);
		$type = sanitize_key( (string) $type );

		return $map[ $type ] ?? '';
	}

	/**
	 * Normalize one controlled field component.
	 *
	 * @param array<string,mixed> $attributes Component attributes.
	 * @return array<string,mixed>
	 */
	private function field_from_component( array $attributes ) {
		$allowed = array( 'text', 'email', 'phone', 'date', 'textarea', 'select', 'radio', 'checkbox', 'consent', 'hidden' );
		$type    = sanitize_key( $attributes['data-subin-field-type'] ?? 'text' );
		$label   = sanitize_text_field( $attributes['data-subin-label'] ?? ( $attributes['data-subin-field-label'] ?? '' ) );
		if ( 'consent' === $type && isset( $attributes['data-subin-consent-text'] ) ) {
			$label = sanitize_text_field( $attributes['data-subin-consent-text'] );
		}

		return array(
			'id'            => absint( $attributes['data-subin-field-id'] ?? 0 ),
			'key'           => sanitize_key( $attributes['data-subin-field-key'] ?? '' ),
			'type'          => in_array( $type, $allowed, true ) ? $type : '',
			'type_valid'    => in_array( $type, $allowed, true ),
			'label'         => $label,
			'required'      => '1' === (string) ( $attributes['data-subin-required'] ?? '0' ),
			'options'       => $this->choice_options( $attributes['data-subin-options'] ?? array() ),
			'default_value' => sanitize_text_field( $attributes['data-subin-default-value'] ?? '' ),
		);
	}

	/**
	 * Decode controlled choice options.
	 *
	 * @param mixed $value Options attribute.
	 * @return array<int,array{label:string,value:string}>
	 */
	private function choice_options( $value ) {
		if ( is_string( $value ) ) {
			$decoded = json_decode( wp_unslash( $value ), true );
			$value   = is_array( $decoded ) ? $decoded : array();
		}
		if ( ! is_array( $value ) ) {
			return array();
		}

		$options = array();
		$seen    = array();
		foreach ( $value as $index => $option ) {
			if ( is_scalar( $option ) ) {
				$label     = sanitize_text_field( wp_strip_all_tags( (string) $option ) );
				$raw_value = $label;
			} elseif ( is_array( $option ) ) {
				$label     = sanitize_text_field( wp_strip_all_tags( (string) ( $option['label'] ?? '' ) ) );
				$raw_value = (string) ( $option['value'] ?? $label );
			} else {
				continue;
			}
			$option_value = sanitize_key( $raw_value );
			if ( '' === $option_value ) {
				$option_value = 'option_' . ( absint( $index ) + 1 );
			}
			if ( isset( $seen[ $option_value ] ) ) {
				continue;
			}
			$seen[ $option_value ] = true;
			$options[]             = array(
				'label' => '' !== $label ? $label : $option_value,
				'value' => $option_value,
			);
		}

		return $options;
	}

	/**
	 * Generate a harmless representative value for one field.
	 *
	 * @param array<string,mixed> $field Field.
	 * @return mixed
	 */
	private function sample_value( array $field ) {
		switch ( $field['type'] ) {
			case 'email':
				return 'preview@example.test';
			case 'phone':
				return '+1 555 010 1234';
			case 'date':
				return gmdate( 'Y-m-d' );
			case 'textarea':
				return 'Safe form simulation message.';
			case 'select':
			case 'radio':
				return (string) ( $field['options'][0]['value'] ?? '' );
			case 'checkbox':
				$value = (string) ( $field['options'][0]['value'] ?? '' );
				return '' !== $value ? array( $value ) : array();
			case 'consent':
				return '1';
			case 'hidden':
				return (string) ( $field['default_value'] ?? '' );
			default:
				return 'Sample value';
		}
	}

	/**
	 * Validate a generated sample against the controlled field contract.
	 *
	 * @param array<string,mixed> $field Field.
	 * @param mixed               $value Sample value.
	 * @return string
	 */
	private function validate_sample_value( array $field, $value ) {
		$empty = is_array( $value ) ? empty( $value ) : '' === trim( (string) $value );
		if ( ! empty( $field['required'] ) && $empty ) {
			return __( 'Required field has no valid sample value.', 'submission-inbox' );
		}
		if ( 'email' === $field['type'] && ! $empty && ! is_email( (string) $value ) ) {
			return __( 'Email sample is invalid.', 'submission-inbox' );
		}
		if ( 'date' === $field['type'] && ! $empty && ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', (string) $value ) ) {
			return __( 'Date sample is invalid.', 'submission-inbox' );
		}
		if ( 'consent' === $field['type'] && '1' !== (string) $value ) {
			return __( 'Consent sample is invalid.', 'submission-inbox' );
		}
		if ( in_array( $field['type'], array( 'select', 'radio', 'checkbox' ), true ) ) {
			$allowed = wp_list_pluck( $field['options'], 'value' );
			$values  = is_array( $value ) ? $value : array( $value );
			foreach ( $values as $selected ) {
				if ( '' !== (string) $selected && ! in_array( (string) $selected, $allowed, true ) ) {
					return __( 'Choice sample is invalid.', 'submission-inbox' );
				}
			}
		}

		return '';
	}

	/**
	 * Mask an email address for admin UI output.
	 *
	 * @param string $email Email.
	 * @return string
	 */
	private function mask_email( $email ) {
		$email = sanitize_email( $email );
		if ( ! is_email( $email ) ) {
			return '';
		}

		list( $local, $domain ) = array_pad( explode( '@', $email, 2 ), 2, '' );
		$prefix = '' !== $local ? substr( $local, 0, 1 ) : '';

		return $prefix . '***@' . $domain;
	}
}
