<?php
/**
 * Forms admin page.
 *
 * @package SubmissionInbox
 */

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

if ( ! class_exists( 'SUBIN_FormsCommandCenter' ) && defined( 'SUBIN_PLUGIN_DIR' ) ) {
	require_once SUBIN_PLUGIN_DIR . 'includes/Admin/Pages/FormsCommandCenter.php';
}

class SUBIN_FormsPage {
	const PAGE_SLUG = 'submission-inbox-forms';
	const GRAPESJS_PROJECT_MAX_BYTES = 2097152;

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

	/**
	 * Field repository.
	 *
	 * @var \SUBIN\Forms\FormFieldRepository
	 */
	private $fields;

	/**
	 * Template repository.
	 *
	 * @var \SUBIN\Forms\FormTemplateRepository
	 */
	private $templates;

	/**
	 * Auto-reply template repository.
	 *
	 * @var \SUBIN\AutoReplies\AutoReplyTemplateRepository
	 */
	private $auto_reply_templates;

	/**
	 * Auto-reply rule repository.
	 *
	 * @var \SUBIN\AutoReplies\AutoReplyRuleRepository
	 */
	private $auto_reply_rules;

	/**
	 * Auto-reply provider registry.
	 *
	 * @var \SUBIN\AutoReplies\AutoReplyFormProviderRegistry
	 */
	private $auto_reply_providers;

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

	/**
	 * Read-only Design Studio readiness service.
	 *
	 * @var \SUBIN\Forms\SubmissionPreflightService
	 */
	private $preflight;

	/**
	 * Forms portfolio command center.
	 *
	 * @var SUBIN_FormsCommandCenter
	 */
	private $command_center;

	/**
	 * Whether state-changing request handling has already run.
	 *
	 * @var bool
	 */
	private $request_handled = false;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->forms          = new \SUBIN\Forms\FormRepository();
		$this->fields         = new \SUBIN\Forms\FormFieldRepository();
		$this->templates      = new \SUBIN\Forms\FormTemplateRepository( $this->forms, $this->fields );
		$this->auto_reply_templates = new \SUBIN\AutoReplies\AutoReplyTemplateRepository();
		$this->auto_reply_rules = new \SUBIN\AutoReplies\AutoReplyRuleRepository();
		$this->auto_reply_providers = new \SUBIN\AutoReplies\AutoReplyFormProviderRegistry( $this->forms, $this->fields );
		$this->settings       = new \SUBIN_SettingsRepository();
		$this->preflight      = new \SUBIN\Forms\SubmissionPreflightService(
			$this->forms,
			$this->settings,
			$this->auto_reply_rules,
			$this->auto_reply_templates
		);
		$this->command_center = new SUBIN_FormsCommandCenter( $this->forms );
	}

	/**
	 * Render page.
	 *
	 * @return void
	 */
	public function render() {
		if ( ! current_user_can( SUBIN_AdminMenu::CAPABILITY ) ) {
			wp_die( esc_html__( 'You do not have permission to perform this action.', 'submission-inbox' ) );
		}

		$this->handle_request();

		$view = SUBIN_Request::get_key( 'view' );

		if ( 'templates' === $view ) {
			$this->render_templates_view();
			return;
		}

		if ( 'template-preview' === $view ) {
			$this->render_template_preview_view();
			return;
		}

		if ( 'edit' === $view ) {
			$this->render_edit_view();
			return;
		}

		if ( 'preview' === $view ) {
			$this->render_preview_view();
			return;
		}

		$this->render_default_view();
	}

	/**
	 * Handle redirects and state changes before admin HTML is sent.
	 *
	 * @return void
	 */
	public function handle_request() {
		if ( $this->request_handled ) {
			return;
		}

		$this->request_handled = true;

		if ( ! current_user_can( SUBIN_AdminMenu::CAPABILITY ) ) {
			wp_die( esc_html__( 'You do not have permission to perform this action.', 'submission-inbox' ) );
		}

		if ( $this->is_legacy_auto_replies_tab_request() ) {
			$this->safe_admin_redirect( $this->auto_replies_url() );
		}

		$this->command_center->handle_post();
		$this->handle_post_action();
		$this->handle_action();
	}

	/**
	 * Save GrapesJS project data through the legacy endpoint as an unpublished draft.
	 *
	 * The endpoint remains registered for older cached editor assets, but it must
	 * never reconcile fields or update the public form. Field reconciliation is
	 * deliberately deferred to ajax_publish_grapesjs_draft().
	 *
	 * @return void
	 */
	public function ajax_save_grapesjs_project() {
		if ( ! current_user_can( SUBIN_AdminMenu::CAPABILITY ) ) {
			$this->send_grapesjs_project_error( __( 'Design Studio could not be saved.', 'submission-inbox' ), 403 );
		}

		$form_id = isset( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0;
		$nonce   = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';

		if ( $form_id <= 0 || ! wp_verify_nonce( $nonce, 'subin_save_grapesjs_project_' . $form_id ) ) {
			$this->send_grapesjs_project_error( __( 'Design Studio could not be saved.', 'submission-inbox' ), 403 );
		}

		$form = $this->forms->get( $form_id );
		if ( ! $form ) {
			$this->send_grapesjs_project_error( __( 'Form not found.', 'submission-inbox' ), 404 );
		}

		$raw_project = isset( $_POST['project_json'] ) ? wp_unslash( $_POST['project_json'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw JSON is decoded and sanitized recursively below.
		if ( ! is_string( $raw_project ) || '' === trim( $raw_project ) ) {
			$this->send_grapesjs_project_error( __( 'Project data is missing.', 'submission-inbox' ), 400 );
		}
		if ( $this->grapesjs_project_exceeds_size_limit( $raw_project ) ) {
			$this->send_grapesjs_project_error( __( 'Project data is too large.', 'submission-inbox' ), 413, array( 'code' => 'project_too_large' ) );
		}

		$project = json_decode( $raw_project, true, 64 );
		if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $project ) ) {
			$this->send_grapesjs_project_error( __( 'Project data is not valid JSON.', 'submission-inbox' ), 400 );
		}

		$project = $this->sanitize_grapesjs_project_for_draft( $project );
		if ( is_wp_error( $project ) ) {
			$this->send_grapesjs_project_error(
				$project->get_error_message(),
				400,
				array( 'code' => $project->get_error_code() )
			);
		}

		$raw_revision = isset( $_POST['revision'] ) ? wp_unslash( $_POST['revision'] ) : '0';
		if ( ! is_scalar( $raw_revision ) || ! preg_match( '/^\d+$/', (string) $raw_revision ) ) {
			$this->send_grapesjs_project_error(
				__( 'Project revision is invalid.', 'submission-inbox' ),
				400,
				array( 'code' => 'invalid_revision' )
			);
		}

		$state                   = $this->forms->get_design_studio_state( $form );
		$expected_draft_revision = (int) $raw_revision;

		// Before the draft/publish workflow, clients sent the public project revision.
		// Translate that initial legacy value to the first draft revision without
		// weakening CAS once a draft exists.
		if (
			empty( $state['has_draft'] ) &&
			$expected_draft_revision === (int) $state['published']['revision']
		) {
			$expected_draft_revision = 0;
		}

		$result = $this->forms->save_design_studio_draft(
			$form_id,
			$expected_draft_revision,
			(int) $state['published']['revision'],
			$project
		);

		if ( is_wp_error( $result ) ) {
			$error_data = (array) $result->get_error_data();
			$status     = absint( $error_data['status'] ?? 500 );
			$response   = array(
				'code' => $result->get_error_code(),
			);

			if ( is_array( $error_data['project'] ?? null ) ) {
				$response['project'] = $error_data['project'];
			}
			if ( array_key_exists( 'revision', $error_data ) ) {
				$response['revision'] = max( 0, (int) $error_data['revision'] );
			}

			$this->send_grapesjs_project_error(
				$result->get_error_message(),
				$status > 0 ? $status : 500,
				$response
			);
		}

		wp_send_json_success(
			array(
				'message'            => __( 'Draft saved.', 'submission-inbox' ),
				'project'            => $result['project'],
				'revision'           => $result['revision'],
				'published_revision' => $result['published_revision'],
				'is_dirty'           => ! empty( $result['is_dirty'] ),
			)
		);
	}

	/**
	 * Autosave a Design Studio draft without changing the public form.
	 *
	 * @return void
	 */
	public function ajax_save_grapesjs_draft() {
		$form_id = $this->validate_design_studio_ajax_request( 'subin_save_grapesjs_draft' );
		$project = $this->grapesjs_project_from_request();
		$result  = $this->forms->save_design_studio_draft(
			$form_id,
			$this->grapesjs_revision_from_request( 'revision' ),
			$this->grapesjs_revision_from_request( 'published_revision' ),
			$project
		);

		if ( is_wp_error( $result ) ) {
			$this->send_design_studio_repository_error( $result );
		}

		wp_send_json_success(
			array(
				'message'            => __( 'Draft saved.', 'submission-inbox' ),
				'project'            => $result['project'],
				'revision'           => $result['revision'],
				'published_revision' => $result['published_revision'],
				'is_dirty'           => ! empty( $result['is_dirty'] ),
			)
		);
	}

	/**
	 * Run read-only checks against the exact saved draft revision.
	 *
	 * @return void
	 */
	public function ajax_design_studio_preflight() {
		$form_id = $this->validate_design_studio_ajax_request( 'subin_form_preflight' );
		$result  = $this->preflight->run(
			$form_id,
			$this->grapesjs_revision_from_request( 'revision' ),
			$this->grapesjs_revision_from_request( 'published_revision' )
		);

		if ( is_wp_error( $result ) ) {
			$this->send_design_studio_repository_error( $result );
		}

		wp_send_json_success( $result );
	}

	/**
	 * Run a side-effect-free sample validation and delivery simulation.
	 *
	 * @return void
	 */
	public function ajax_design_studio_sandbox() {
		$form_id = $this->validate_design_studio_ajax_request( 'subin_form_sandbox' );
		$result  = $this->preflight->simulate(
			$form_id,
			$this->grapesjs_revision_from_request( 'revision' ),
			$this->grapesjs_revision_from_request( 'published_revision' )
		);

		if ( is_wp_error( $result ) ) {
			$this->send_design_studio_repository_error( $result );
		}

		wp_send_json_success( $result );
	}

	/**
	 * Publish the current Design Studio draft.
	 *
	 * @return void
	 */
	public function ajax_publish_grapesjs_draft() {
		$form_id            = $this->validate_design_studio_ajax_request( 'subin_publish_grapesjs_draft' );
		$draft_revision     = $this->grapesjs_revision_from_request( 'revision' );
		$published_revision = $this->grapesjs_revision_from_request( 'published_revision' );
		$preflight          = $this->preflight->run( $form_id, $draft_revision, $published_revision );
		if ( is_wp_error( $preflight ) ) {
			$this->send_design_studio_repository_error( $preflight );
		}
		if ( empty( $preflight['ready'] ) ) {
			$this->send_grapesjs_project_error(
				__( 'Resolve the failed pre-publish checks before publishing.', 'submission-inbox' ),
				422,
				array(
					'code'      => 'preflight_failed',
					'preflight' => $preflight,
				)
			);
		}

		$result  = $this->forms->publish_design_studio_draft(
			$form_id,
			$draft_revision,
			$published_revision,
			function ( $locked_form_id, array $candidate ) {
				return $this->reconcile_grapesjs_project_for_publish( $locked_form_id, $candidate );
			}
		);

		if ( is_wp_error( $result ) ) {
			$this->send_design_studio_repository_error( $result );
		}

		wp_send_json_success(
			array(
				'message'            => __( 'Form published.', 'submission-inbox' ),
				'project'            => $result['project'],
				'revision'           => $result['revision'],
				'published_revision' => $result['published_revision'],
				'published_at'       => $result['published_at'],
				'history'            => $result['history'],
				'is_dirty'           => false,
			)
		);
	}

	/**
	 * Restore a published revision into a new draft.
	 *
	 * @return void
	 */
	public function ajax_restore_grapesjs_revision() {
		$form_id = $this->validate_design_studio_ajax_request( 'subin_restore_grapesjs_revision' );
		$result  = $this->forms->restore_design_studio_revision(
			$form_id,
			$this->grapesjs_revision_from_request( 'target_revision' ),
			$this->grapesjs_revision_from_request( 'revision' ),
			$this->grapesjs_revision_from_request( 'published_revision' )
		);

		if ( is_wp_error( $result ) ) {
			$this->send_design_studio_repository_error( $result );
		}

		wp_send_json_success(
			array(
				'message'            => __( 'Revision restored as a draft.', 'submission-inbox' ),
				'project'            => $result['project'],
				'revision'           => $result['revision'],
				'published_revision' => $result['published_revision'],
				'restored_revision'  => $result['restored_revision'],
				'is_dirty'           => ! empty( $result['is_dirty'] ),
			)
		);
	}

	/**
	 * Validate a Design Studio AJAX request and return its form id.
	 *
	 * @param string $action Nonce action prefix.
	 * @return int
	 */
	private function validate_design_studio_ajax_request( $action ) {
		if ( ! current_user_can( SUBIN_AdminMenu::CAPABILITY ) ) {
			$this->send_grapesjs_project_error( __( 'You do not have permission to edit this form.', 'submission-inbox' ), 403 );
		}

		$form_id = isset( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0;
		$nonce   = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
		if ( $form_id <= 0 || ! wp_verify_nonce( $nonce, sanitize_key( $action ) . '_' . $form_id ) ) {
			$this->send_grapesjs_project_error( __( 'Design Studio request could not be verified.', 'submission-inbox' ), 403 );
		}
		if ( ! $this->forms->get( $form_id ) ) {
			$this->send_grapesjs_project_error( __( 'Form not found.', 'submission-inbox' ), 404 );
		}

		return $form_id;
	}

	/**
	 * Decode and sanitize project JSON from the current request.
	 *
	 * @return array<string,mixed>
	 */
	private function grapesjs_project_from_request() {
		$raw_project = isset( $_POST['project_json'] ) ? wp_unslash( $_POST['project_json'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw JSON is decoded and sanitized recursively below.
		if ( ! is_string( $raw_project ) || '' === trim( $raw_project ) ) {
			$this->send_grapesjs_project_error( __( 'Project data is missing.', 'submission-inbox' ), 400 );
		}
		if ( $this->grapesjs_project_exceeds_size_limit( $raw_project ) ) {
			$this->send_grapesjs_project_error( __( 'Project data is too large.', 'submission-inbox' ), 413, array( 'code' => 'project_too_large' ) );
		}

		$project = json_decode( $raw_project, true, 64 );
		if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $project ) ) {
			$this->send_grapesjs_project_error( __( 'Project data is not valid JSON.', 'submission-inbox' ), 400 );
		}
		$project = $this->sanitize_grapesjs_project_for_draft( $project );
		if ( is_wp_error( $project ) ) {
			$this->send_grapesjs_project_error(
				$project->get_error_message(),
				400,
				array( 'code' => $project->get_error_code() )
			);
		}

		return $project;
	}

	/**
	 * Check the raw project payload before JSON decoding and recursive sanitization.
	 *
	 * @param mixed $raw_project Raw project JSON.
	 * @return bool
	 */
	private function grapesjs_project_exceeds_size_limit( $raw_project ) {
		return is_string( $raw_project ) && strlen( $raw_project ) > self::GRAPESJS_PROJECT_MAX_BYTES;
	}

	/**
	 * Read a non-negative revision integer from the current request.
	 *
	 * @param string $key Request key.
	 * @return int
	 */
	private function grapesjs_revision_from_request( $key ) {
		$raw = isset( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : '0';
		if ( ! is_scalar( $raw ) || ! preg_match( '/^\d+$/', (string) $raw ) ) {
			$this->send_grapesjs_project_error(
				__( 'Project revision is invalid.', 'submission-inbox' ),
				400,
				array( 'code' => 'invalid_revision' )
			);
		}
		return max( 0, (int) $raw );
	}

	/**
	 * Send a normalized repository error to the editor.
	 *
	 * @param \WP_Error $error Error.
	 * @return void
	 */
	private function send_design_studio_repository_error( \WP_Error $error ) {
		$error_data = (array) $error->get_error_data();
		$status     = absint( $error_data['status'] ?? 500 );
		$response   = array( 'code' => $error->get_error_code() );
		foreach ( array( 'project', 'revision', 'published_revision' ) as $key ) {
			if ( array_key_exists( $key, $error_data ) ) {
				$response[ $key ] = $error_data[ $key ];
			}
		}
		$this->send_grapesjs_project_error( $error->get_error_message(), $status > 0 ? $status : 500, $response );
	}

	/**
	 * Reconcile a draft into canonical fields, messages, and form columns.
	 *
	 * @param int                 $form_id Form id.
	 * @param array<string,mixed> $candidate Draft project.
	 * @return array<string,mixed>|\WP_Error
	 */
	private function reconcile_grapesjs_project_for_publish( $form_id, array $candidate ) {
		$candidate = $this->sanitize_grapesjs_project( $candidate );
		if ( is_wp_error( $candidate ) ) {
			return $candidate;
		}

		$sync = $this->sync_grapesjs_project_fields( $form_id, $candidate );
		if ( is_wp_error( $sync ) ) {
			return $sync;
		}
		if ( true !== $sync ) {
			return new \WP_Error(
				'design_studio_sync_failed',
				__( 'Design Studio fields could not be published.', 'submission-inbox' ),
				array( 'status' => 500 )
			);
		}

		$candidate = $this->sanitize_grapesjs_project( $candidate );
		if ( is_wp_error( $candidate ) ) {
			return $candidate;
		}
		$messages_update = $this->messages_from_grapesjs_project( $candidate );
		$columns         = array();
		foreach ( array( 'success_message', 'header_title', 'header_description' ) as $column ) {
			if ( array_key_exists( $column, $messages_update ) ) {
				$columns[ $column ] = $messages_update[ $column ];
			}
		}
		return array(
			'project'  => $candidate,
			'messages' => is_array( $messages_update['messages'] ?? null ) ? $messages_update['messages'] : array(),
			'columns'  => $columns,
		);
	}

	/**
	 * Send a normalized GrapesJS project AJAX error.
	 *
	 * @param string $message Error message.
	 * @param int    $status_code HTTP status code.
	 * @param array<string,mixed> $data Error data.
	 * @return void
	 */
	private function send_grapesjs_project_error( $message, $status_code = 400, array $data = array() ) {
		wp_send_json_error(
			array_merge( array( 'message' => $message ), $data ),
			absint( $status_code )
		);
	}

	/**
	 * Extract saved global messages from the controlled GrapesJS form wrapper.
	 *
	 * @param array<string,mixed> $project Project data.
	 * @return array{messages?:array<string,string>,success_message?:string}
	 */
	private function messages_from_grapesjs_project( array $project ) {
		$attributes = $this->grapesjs_form_wrapper_attributes( $project );
		$messages   = array();
		$result     = array();

		if ( isset( $attributes['data-subin-validation-error-message'] ) ) {
			$messages['validation_error'] = sanitize_textarea_field( $attributes['data-subin-validation-error-message'] );
		}
		if ( isset( $attributes['data-subin-required-error-message'] ) ) {
			$messages['required_field_error'] = sanitize_text_field( $attributes['data-subin-required-error-message'] );
		}
		if ( isset( $attributes['data-subin-spam-error-message'] ) ) {
			$messages['spam_protection_error'] = sanitize_textarea_field( $attributes['data-subin-spam-error-message'] );
		}
		if ( isset( $attributes['data-subin-success-message'] ) ) {
			$result['success_message'] = sanitize_textarea_field( $attributes['data-subin-success-message'] );
		}
		if ( isset( $attributes['data-subin-form-title'] ) ) {
			$result['header_title'] = sanitize_text_field( $attributes['data-subin-form-title'] );
		}
		if ( isset( $attributes['data-subin-form-description'] ) ) {
			$result['header_description'] = sanitize_textarea_field( $attributes['data-subin-form-description'] );
		}
		if ( ! empty( $messages ) ) {
			$result['messages'] = $messages;
		}

		return $result;
	}

	/**
	 * Find form wrapper attributes in native GrapesJS project data.
	 *
	 * @param mixed $node Project node.
	 * @return array<string,string>
	 */
	private function grapesjs_form_wrapper_attributes( $node ) {
		if ( ! is_array( $node ) ) {
			return array();
		}

		if ( $this->is_grapesjs_component_array( $node ) ) {
			$attributes = is_array( $node['attributes'] ?? null ) ? $node['attributes'] : array();
			$type       = sanitize_key( (string) ( $attributes['data-subin-component'] ?? $this->grapesjs_component_key_from_type( $node['type'] ?? '' ) ) );
			if ( 'form-wrapper' === $type ) {
				return $attributes;
			}
		}

		foreach ( $node as $child ) {
			$attributes = $this->grapesjs_form_wrapper_attributes( $child );
			if ( ! empty( $attributes ) ) {
				return $attributes;
			}
		}

		return array();
	}

	/**
	 * Attach new controlled GrapesJS field blocks to real form fields.
	 *
	 * @param int                 $form_id Form ID.
	 * @param array<string,mixed> $project Project data.
	 * @return true|WP_Error
	 */
	private function sync_grapesjs_project_fields( $form_id, array &$project ) {
		$existing_fields = $this->fields->get_by_form_id_for_update( $form_id );
		if ( is_wp_error( $existing_fields ) ) {
			return $existing_fields;
		}

		$field_map       = array();
		$used_field_ids  = array();
		$field_key_remap = array();

		foreach ( $existing_fields as $field ) {
			$field_id  = absint( $field['id'] ?? 0 );
			$field_key = sanitize_key( $field['field_key'] ?? '' );
			if ( '' !== $field_key && $field_id > 0 ) {
				$field_map[ $field_key ] = $field;
			}
		}

		$result = $this->sync_grapesjs_components_fields( $form_id, $project, $field_map, $used_field_ids, $field_key_remap );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$this->remap_grapesjs_conditional_logic_field_keys( $project, $field_key_remap );

		foreach ( $existing_fields as $field ) {
			$field_id = absint( $field['id'] ?? 0 );
			if ( $field_id > 0 && empty( $used_field_ids[ $field_id ] ) && ! $this->fields->delete_for_form( $form_id, $field_id ) ) {
				return new WP_Error(
					'design_studio_field_delete_failed',
					__( 'Design Studio fields could not be saved.', 'submission-inbox' ),
					array( 'status' => 500 )
				);
			}
		}

		return true;
	}

	/**
	 * Recursively attach controlled GrapesJS field components to fields.
	 *
	 * @param int                                      $form_id Form ID.
	 * @param mixed                                    $node Project node.
	 * @param array<string,array<string,mixed>>        $field_map Existing fields by key.
	 * @param array<int,bool>                          $used_field_ids Field IDs already referenced.
	 * @param array<string,string>                     $field_key_remap Temporary identities by canonical field key.
	 * @return true|WP_Error
	 */
	private function sync_grapesjs_components_fields( $form_id, &$node, array &$field_map, array &$used_field_ids, array &$field_key_remap ) {
		if ( ! is_array( $node ) ) {
			return true;
		}

		if ( $this->is_grapesjs_component_array( $node ) ) {
			$result = $this->sync_grapesjs_field_component( $form_id, $node, $field_map, $used_field_ids, $field_key_remap );
			if ( is_wp_error( $result ) ) {
				return $result;
			}
		}

		foreach ( $node as &$child ) {
			if ( is_array( $child ) ) {
				$result = $this->sync_grapesjs_components_fields( $form_id, $child, $field_map, $used_field_ids, $field_key_remap );
				if ( is_wp_error( $result ) ) {
					unset( $child );
					return $result;
				}
			}
		}
		unset( $child );

		return true;
	}

	/**
	 * Attach one controlled GrapesJS field component to a stored form field.
	 *
	 * @param int                               $form_id Form ID.
	 * @param array<string,mixed>               $component Component.
	 * @param array<string,array<string,mixed>> $field_map Existing fields by key.
	 * @param array<int,bool>                   $used_field_ids Field IDs already referenced.
	 * @param array<string,string>              $field_key_remap Temporary identities by canonical field key.
	 * @return true|WP_Error
	 */
	private function sync_grapesjs_field_component( $form_id, array &$component, array &$field_map, array &$used_field_ids, array &$field_key_remap ) {
		$attributes = is_array( $component['attributes'] ?? null ) ? $component['attributes'] : array();
		$subin_type   = sanitize_key( (string) ( $attributes['data-subin-component'] ?? $this->grapesjs_component_key_from_type( $component['type'] ?? '' ) ) );

		if ( 'field' !== $subin_type ) {
			return true;
		}

		$previous_logic_key = $this->grapesjs_field_logic_identity( $attributes );

		$field_id = absint( $attributes['data-subin-field-id'] ?? 0 );
		if ( $field_id > 0 ) {
			if ( ! empty( $used_field_ids[ $field_id ] ) ) {
				return new WP_Error(
					'duplicate_field_reference',
					__( 'The project references the same field more than once.', 'submission-inbox' ),
					array( 'status' => 409 )
				);
			}

			$existing = $this->fields->get_for_form( $form_id, $field_id );
			if ( ! $existing ) {
				return new WP_Error(
					'field_ownership_conflict',
					__( 'The project references a field that does not belong to this form.', 'submission-inbox' ),
					array( 'status' => 409 )
				);
			}

			$field_data = $this->grapesjs_field_defaults_from_attributes( $attributes, $form_id, $existing );
			if ( ! $this->fields->update_for_form( $form_id, $field_id, $field_data ) ) {
				return new WP_Error(
					'design_studio_field_update_failed',
					__( 'Design Studio fields could not be saved.', 'submission-inbox' ),
					array( 'status' => 500 )
				);
			}

			$updated = $this->fields->get_for_form( $form_id, $field_id );
			if ( ! $updated ) {
				return new WP_Error(
					'design_studio_field_reload_failed',
					__( 'Design Studio fields could not be saved.', 'submission-inbox' ),
					array( 'status' => 500 )
				);
			}

			$updated_key = sanitize_key( $updated['field_key'] ?? $field_data['field_key'] );
			$component['attributes']['data-subin-field-id']  = (string) $field_id;
			$component['attributes']['data-subin-field-key'] = $updated_key;
			$component['attributes']['data-subin-style-id']  = 'field-' . $field_id;
			$field_map[ $updated_key ]                       = $updated;
			$used_field_ids[ $field_id ]                     = true;
			$this->record_grapesjs_field_key_remap( $field_key_remap, $previous_logic_key, $updated_key );
			return true;
		}

		$field_key = sanitize_key( $attributes['data-subin-field-key'] ?? '' );
		if ( '' !== $field_key && isset( $field_map[ $field_key ] ) ) {
			$existing_id = absint( $field_map[ $field_key ]['id'] ?? 0 );
			if ( $existing_id > 0 && empty( $used_field_ids[ $existing_id ] ) ) {
				$field_data = $this->grapesjs_field_defaults_from_attributes( $attributes, $form_id, $field_map[ $field_key ] );
				if ( ! $this->fields->update_for_form( $form_id, $existing_id, $field_data ) ) {
					return new WP_Error(
						'design_studio_field_update_failed',
						__( 'Design Studio fields could not be saved.', 'submission-inbox' ),
						array( 'status' => 500 )
					);
				}

				$updated = $this->fields->get_for_form( $form_id, $existing_id );
				if ( ! $updated ) {
					return new WP_Error(
						'design_studio_field_reload_failed',
						__( 'Design Studio fields could not be saved.', 'submission-inbox' ),
						array( 'status' => 500 )
					);
				}

				$updated_key = sanitize_key( $updated['field_key'] ?? $field_data['field_key'] );
				$component['attributes']['data-subin-field-id']  = (string) $existing_id;
				$component['attributes']['data-subin-field-key'] = $updated_key;
				$component['attributes']['data-subin-style-id']  = 'field-' . $existing_id;
				$used_field_ids[ $existing_id ]                  = true;
				$this->record_grapesjs_field_key_remap( $field_key_remap, $previous_logic_key, $updated_key );
				return true;
			}
		}

		$field_data = $this->grapesjs_field_defaults_from_attributes( $attributes, $form_id );
		$new_id     = $this->fields->create( $form_id, $field_data );
		if ( $new_id <= 0 ) {
			return new WP_Error(
				'design_studio_field_create_failed',
				__( 'Design Studio fields could not be saved.', 'submission-inbox' ),
				array( 'status' => 500 )
			);
		}

		$new_field = $this->fields->get_for_form( $form_id, $new_id );
		if ( ! $new_field ) {
			return new WP_Error(
				'design_studio_field_reload_failed',
				__( 'Design Studio fields could not be saved.', 'submission-inbox' ),
				array( 'status' => 500 )
			);
		}

		$new_key = sanitize_key( $new_field['field_key'] ?? $field_data['field_key'] );
		$component['attributes']['data-subin-field-id'] = (string) $new_id;
		$component['attributes']['data-subin-field-key'] = $new_key;
		$component['attributes']['data-subin-style-id'] = 'field-' . $new_id;
		$field_map[ $new_key ] = $new_field;
		$used_field_ids[ $new_id ] = true;
		$this->record_grapesjs_field_key_remap( $field_key_remap, $previous_logic_key, $new_key );

		return true;
	}

	/**
	 * Return the field identity used by the Logic editor before persistence.
	 *
	 * @param array<string,mixed> $attributes Component attributes.
	 * @return string
	 */
	private function grapesjs_field_logic_identity( array $attributes ) {
		foreach ( array( 'data-subin-field-key', 'data-subin-field-id', 'data-subin-temp-id' ) as $attribute ) {
			$value = trim( (string) ( $attributes[ $attribute ] ?? '' ) );
			if ( '' !== $value ) {
				return $value;
			}
		}

		return '';
	}

	/**
	 * Record a temporary-to-canonical field-key mapping.
	 *
	 * @param array<string,string> $field_key_remap Field-key mappings.
	 * @param string               $previous_key Previous editor identity.
	 * @param string               $canonical_key Canonical persisted key.
	 * @return void
	 */
	private function record_grapesjs_field_key_remap( array &$field_key_remap, $previous_key, $canonical_key ) {
		$previous_key  = trim( (string) $previous_key );
		$canonical_key = sanitize_key( $canonical_key );

		if ( '' !== $previous_key && '' !== $canonical_key && $previous_key !== $canonical_key ) {
			$field_key_remap[ $previous_key ] = $canonical_key;
		}
	}

	/**
	 * Replace temporary source and field-target identities in conditional logic.
	 *
	 * @param array<string,mixed>  $project Project data.
	 * @param array<string,string> $field_key_remap Temporary identities by canonical field key.
	 * @return void
	 */
	private function remap_grapesjs_conditional_logic_field_keys( array &$project, array $field_key_remap ) {
		if ( empty( $field_key_remap ) || ! is_array( $project['subinLogic']['rules'] ?? null ) ) {
			return;
		}

		foreach ( $project['subinLogic']['rules'] as &$rule ) {
			if ( ! is_array( $rule ) ) {
				continue;
			}

			$target_type = (string) ( $rule['target']['type'] ?? '' );
			$target_id   = (string) ( $rule['target']['id'] ?? '' );
			if ( 'field' === $target_type && isset( $field_key_remap[ $target_id ] ) ) {
				$rule['target']['id'] = $field_key_remap[ $target_id ];
			}

			if ( ! is_array( $rule['groups'] ?? null ) ) {
				continue;
			}

			foreach ( $rule['groups'] as &$group ) {
				if ( ! is_array( $group['conditions'] ?? null ) ) {
					continue;
				}

				foreach ( $group['conditions'] as &$condition ) {
					if ( ! is_array( $condition ) ) {
						continue;
					}

					$field_key = (string) ( $condition['field'] ?? '' );
					if ( isset( $field_key_remap[ $field_key ] ) ) {
						$condition['field'] = $field_key_remap[ $field_key ];
					}
				}
				unset( $condition );
			}
			unset( $group );
		}
		unset( $rule );
	}

	/**
	 * Build form field defaults from controlled GrapesJS field metadata.
	 *
	 * @param array<string,mixed> $attributes Component attributes.
	 * @param int                 $form_id Form ID.
	 * @param array<string,mixed> $existing Existing field row.
	 * @return array<string,mixed>
	 */
	private function grapesjs_field_defaults_from_attributes( array $attributes, $form_id, array $existing = array() ) {
		$type       = $this->sanitize_choice( $attributes['data-subin-field-type'] ?? 'text', array_keys( $this->field_type_labels() ), 'text' );
		$field_data = empty( $existing ) ? $this->library_field_defaults( $type, $form_id ) : array(
			'field_type'    => $type,
			'field_key'     => sanitize_key( $existing['field_key'] ?? '' ),
			'field_label'   => sanitize_text_field( $existing['field_label'] ?? '' ),
			'placeholder'   => sanitize_text_field( $existing['placeholder'] ?? '' ),
			'help_text'     => sanitize_textarea_field( $existing['help_text'] ?? '' ),
			'is_required'   => ! empty( $existing['is_required'] ),
			'field_options' => is_string( $existing['field_options'] ?? null ) ? $existing['field_options'] : array(),
			'sort_order'    => absint( $existing['sort_order'] ?? 0 ),
		);
		$label      = sanitize_text_field( $attributes['data-subin-label'] ?? ( $attributes['data-subin-field-label'] ?? '' ) );
		$key        = sanitize_key( $attributes['data-subin-field-key'] ?? '' );

		if ( '' !== $label ) {
			$field_data['field_label'] = $label;
		}
		if ( '' !== $key ) {
			$field_data['field_key'] = $key;
		}
		if ( isset( $attributes['data-subin-placeholder'] ) ) {
			$field_data['placeholder'] = sanitize_text_field( $attributes['data-subin-placeholder'] );
		}
		if ( isset( $attributes['data-subin-help-text'] ) ) {
			$field_data['help_text'] = sanitize_textarea_field( $attributes['data-subin-help-text'] );
		}
		if ( 'consent' === $type && isset( $attributes['data-subin-consent-text'] ) ) {
			$field_data['field_label'] = sanitize_text_field( $attributes['data-subin-consent-text'] );
		}

		if ( isset( $attributes['data-subin-required'] ) ) {
			$field_data['is_required'] = '1' === (string) $attributes['data-subin-required'];
		}

		if ( in_array( $type, array( 'select', 'radio', 'checkbox' ), true ) && isset( $attributes['data-subin-options'] ) ) {
			$options = $this->grapesjs_options_from_attribute( $attributes['data-subin-options'] );
			if ( ! empty( $options ) ) {
				$field_data['field_options'] = $options;
			}
		}

		return $field_data;
	}

	/**
	 * Handle POST editor actions.
	 *
	 * @return void
	 */
	private function handle_post_action() {
		$request_method = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? '' ) );
		if ( 'POST' !== strtoupper( $request_method ) ) {
			return;
		}

		if ( ! current_user_can( SUBIN_AdminMenu::CAPABILITY ) ) {
			$this->redirect_with_notice( 'permission_denied' );
		}

		$action  = isset( $_POST['subin_editor_action'] ) ? sanitize_key( wp_unslash( $_POST['subin_editor_action'] ) ) : '';
		if ( '' === $action ) {
			return;
		}

		if ( 'use_template' === $action ) {
			$this->handle_use_template_post();
			return;
		}

		$form_id = isset( $_POST['form_id'] ) ? absint( wp_unslash( $_POST['form_id'] ) ) : 0;
		$tab     = isset( $_POST['tab'] ) ? sanitize_key( wp_unslash( $_POST['tab'] ) ) : 'fields';
		$field_id = isset( $_POST['field_id'] ) ? absint( wp_unslash( $_POST['field_id'] ) ) : 0;
		$nonce   = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';

		if ( ! wp_verify_nonce( $nonce, $this->editor_nonce_name( $action, $form_id, $field_id ) ) ) {
			$this->redirect_to_edit( $form_id, $tab, 'security_failed' );
		}

		$form = $this->forms->get( $form_id );
		if ( ! $form ) {
			$this->redirect_with_notice( 'form_not_found' );
		}

		if ( in_array( $action, array( 'save_notification', 'save_settings' ), true ) ) {
			$settings = $this->forms->decode_settings( $form );
			$notify = is_array( $settings['notification'] ?? null ) ? $settings['notification'] : array();
			$recipient_mode = $this->sanitize_notification_recipient_mode(
				sanitize_text_field( wp_unslash( $_POST['notification_recipient_mode'] ?? ( $notify['recipient_mode'] ?? $this->default_notification_recipient_mode_for_form( $form ) ) ) )
			);
			$custom_recipient = sanitize_email( wp_unslash( $_POST['notification_email_preserved'] ?? ( $form['notification_email'] ?? '' ) ) );
			if ( array_key_exists( 'notification_email', $_POST ) ) {
				$custom_recipient = sanitize_email( wp_unslash( $_POST['notification_email'] ) );
			}

			if ( ! empty( $_POST['notification_enabled'] ) ) {
				if ( 'default' === $recipient_mode && ! is_email( $this->default_admin_notification_recipient() ) ) {
					$this->redirect_to_edit( $form_id, 'settings', 'default_notification_recipient_missing' );
				}

				if ( 'custom' === $recipient_mode && ! is_email( $custom_recipient ) ) {
					$this->redirect_to_edit( $form_id, 'settings', 'custom_notification_recipient_invalid' );
				}
			}

			$settings_patch = array(
				'notification' => array(
					'use_lead_email_reply_to' => ! empty( $_POST['use_lead_email_reply_to'] ),
					'recipient_mode'          => $recipient_mode,
				),
			);
			if ( array_key_exists( 'auto_reply_enabled', $_POST ) || array_key_exists( 'auto_reply_template_id', $_POST ) || array_key_exists( 'auto_reply_recipient_field_key', $_POST ) || array_key_exists( 'auto_reply_recipient_field_key_preserved', $_POST ) ) {
				$auto_reply = is_array( $settings['auto_reply'] ?? null ) ? $settings['auto_reply'] : array();
				$posted_auto_reply_recipient = $auto_reply['recipient_field_key'] ?? '';
				if ( array_key_exists( 'auto_reply_recipient_field_key_preserved', $_POST ) ) {
					$posted_auto_reply_recipient = sanitize_text_field( wp_unslash( $_POST['auto_reply_recipient_field_key_preserved'] ) );
				}
				if ( array_key_exists( 'auto_reply_recipient_field_key', $_POST ) ) {
					$posted_auto_reply_recipient = sanitize_text_field( wp_unslash( $_POST['auto_reply_recipient_field_key'] ) );
				}
				$auto_reply_enabled   = ! empty( $_POST['auto_reply_enabled'] );
				$auto_reply_recipient = $this->sanitize_auto_reply_recipient_field_key( $form_id, $posted_auto_reply_recipient );
				if ( $auto_reply_enabled && '' === $auto_reply_recipient ) {
					$this->redirect_to_edit( $form_id, 'settings', 'auto_reply_email_field_required' );
				}
				$settings_patch['auto_reply'] = array(
					'enabled'             => $auto_reply_enabled,
					'template_id'         => $this->sanitize_auto_reply_template_id( absint( $_POST['auto_reply_template_id'] ?? ( $auto_reply['template_id'] ?? 0 ) ) ),
					'recipient_field_key' => $auto_reply_recipient,
				);
			}
			$updates = array(
				'notification_enabled' => ! empty( $_POST['notification_enabled'] ),
				'notification_email'   => $custom_recipient,
				'notification_subject' => sanitize_text_field( wp_unslash( $_POST['notification_subject'] ?? '' ) ),
			);

			if ( 'save_settings' === $action ) {
				$updates['slug']       = sanitize_title( wp_unslash( $_POST['slug'] ?? '' ) );
				$updates['status']     = sanitize_key( wp_unslash( $_POST['status'] ?? '' ) );
				$updates['save_leads'] = ! empty( $_POST['save_leads'] );
			}

			$updated = $this->forms->patch_settings_and_columns( $form_id, $settings_patch, $updates );
			$this->redirect_to_edit( $form_id, 'settings', $updated ? ( 'save_settings' === $action ? 'form_settings_saved' : 'form_email_saved' ) : 'form_save_failed' );
		}

		if ( 'save_publish' === $action ) {
			$updated = $this->forms->update_columns(
				$form_id,
				array(
					'slug'       => sanitize_title( wp_unslash( $_POST['slug'] ?? '' ) ),
					'status'     => sanitize_key( wp_unslash( $_POST['status'] ?? '' ) ),
					'save_leads' => ! empty( $_POST['save_leads'] ),
				)
			);
			$this->redirect_to_edit( $form_id, 'settings', $updated ? 'form_publish_saved' : 'form_save_failed' );
		}

	}

	/**
	 * Handle template creation from the redesigned chooser.
	 *
	 * @return void
	 */
	private function handle_use_template_post() {
		$template_key = isset( $_POST['template_key'] ) ? sanitize_key( wp_unslash( $_POST['template_key'] ) ) : '';
		$nonce        = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';

		if ( ! $this->templates->get_template( $template_key ) ) {
			$this->redirect_to_templates( 'template_not_found' );
		}

		if ( ! wp_verify_nonce( $nonce, 'subin_use_template_' . $template_key ) ) {
			$this->redirect_to_templates( 'security_failed' );
		}

		$form_id = $this->templates->create_form_from_template( $template_key );
		if ( $form_id > 0 ) {
			$this->redirect_to_edit( $form_id, 'design_studio', 'form_created' );
		}

		$this->redirect_to_templates( 'form_create_failed' );
	}

	/**
	 * Handle nonce-protected GET actions.
	 *
	 * @return void
	 */
	private function handle_action() {
		$action = SUBIN_Request::action_key( 'subin_action' );
		if ( '' === $action ) {
			return;
		}

		if ( ! current_user_can( SUBIN_AdminMenu::CAPABILITY ) ) {
			$this->redirect_with_notice( 'permission_denied' );
		}

		if ( 'create_from_template' === $action ) {
			$nonce        = SUBIN_Request::action_text( '_wpnonce' );
			$template_key = SUBIN_Request::action_key( 'template_key' );
			if ( ! $this->templates->get_template( $template_key ) ) {
				$this->redirect_to_templates( 'template_not_found' );
			}

			if ( ! wp_verify_nonce( $nonce, 'subin_use_template_' . $template_key ) ) {
				$this->redirect_to_templates( 'security_failed' );
			}

			$form_id = $this->templates->create_form_from_template( $template_key );
			if ( $form_id > 0 ) {
				$this->redirect_to_edit( $form_id, 'design_studio', 'form_created' );
			}

			$this->redirect_to_templates( 'form_create_failed' );
		}

		$form_id  = SUBIN_Request::action_absint( 'form_id' );
		$field_id = SUBIN_Request::action_absint( 'field_id' );
		$nonce    = SUBIN_Request::action_text( '_wpnonce' );

		if ( ! wp_verify_nonce( $nonce, $this->action_nonce_name( $action, $form_id, $field_id ) ) ) {
			$this->redirect_with_notice( 'security_failed' );
		}

		if ( 'duplicate' === $action ) {
			$new_form_id = $this->forms->duplicate( $form_id );
			$this->redirect_with_notice( $new_form_id > 0 ? 'form_duplicated' : 'form_duplicate_failed' );
		}

		if ( 'activate' === $action || 'deactivate' === $action ) {
			$status  = 'activate' === $action ? \SUBIN\Forms\FormRepository::STATUS_ACTIVE : \SUBIN\Forms\FormRepository::STATUS_INACTIVE;
			$updated = $this->forms->update_status( $form_id, $status );
			$this->redirect_with_notice( $updated ? 'form_' . $status : 'form_status_failed' );
		}

		$this->redirect_with_notice( 'unknown_action' );
	}

	/**
	 * Render default Forms view.
	 *
	 * @return void
	 */
	private function render_default_view() {
		$this->command_center->render();
	}

	/**
	 * Render My Forms tab content.
	 *
	 * @param array<int,array<string,mixed>> $forms Forms.
	 * @return void
	 */
	private function render_my_forms_tab( array $forms ) {
		?>
		<div class="subin-forms-section">
			<div class="subin-section-heading">
				<h2><?php esc_html_e( 'My Forms', 'submission-inbox' ); ?></h2>
				<p><?php esc_html_e( 'Manage your forms and check the most important setup details.', 'submission-inbox' ); ?></p>
			</div>

			<?php if ( empty( $forms ) ) : ?>
				<div class="subin-forms-empty-state subin-empty-state">
					<h2><?php esc_html_e( 'No forms yet', 'submission-inbox' ); ?></h2>
					<p><?php esc_html_e( 'Create your first form from a ready-made template and customize it in Design Studio.', 'submission-inbox' ); ?></p>
					<div class="subin-forms-empty-state__actions">
						<a class="button button-primary subin-btn subin-btn-primary" href="<?php echo esc_url( $this->templates_url() ); ?>"><?php esc_html_e( 'Create Form', 'submission-inbox' ); ?></a>
						<a class="button subin-btn" href="<?php echo esc_url( $this->templates_url() ); ?>"><?php esc_html_e( 'Browse Templates', 'submission-inbox' ); ?></a>
					</div>
				</div>
			<?php else : ?>
				<?php $this->render_forms_table( $forms ); ?>
			<?php endif; ?>
		</div>
		<?php
	}

	/**
	 * Render template chooser.
	 *
	 * @return void
	 */
	private function render_templates_view() {
		SUBIN_View::page_header(
			__( 'Choose a form template', 'submission-inbox' ),
			__( 'Start with a ready-made form and customize it before publishing.', 'submission-inbox' ),
			array(
				'wrapper_class' => 'subin-forms-page subin-form-templates-page',
			)
		);

		$this->render_notice();
		?>
		<a class="subin-back-link" href="<?php echo esc_url( $this->forms_url() ); ?>"><?php esc_html_e( 'Back to Forms', 'submission-inbox' ); ?></a>

		<div class="subin-template-grid">
			<?php foreach ( $this->templates->get_templates() as $template_key => $template ) : ?>
				<?php
				$fields      = is_array( $template['fields'] ?? null ) ? $template['fields'] : array();
				$field_names = $this->template_field_labels( $template );
				?>
				<div class="subin-template-card">
					<div class="subin-template-card__badge"><?php echo esc_html( $this->template_category( $template_key, $template ) ); ?></div>
					<h2 class="subin-template-card__title"><?php echo esc_html( $template['title'] ?? '' ); ?></h2>
					<p class="subin-template-card__description"><?php echo esc_html( $this->template_description( $template_key, $template ) ); ?></p>
					<div class="subin-template-card__count">
						<?php echo esc_html( $this->format_template_field_count( count( $fields ) ) ); ?>
					</div>
					<p class="subin-template-card__fields">
						<strong><?php esc_html_e( 'Fields:', 'submission-inbox' ); ?></strong>
						<?php echo esc_html( implode( ', ', $field_names ) ); ?>
					</p>
					<?php $this->render_template_visual_preview( $template ); ?>
					<div class="subin-template-card__actions">
						<?php $this->render_use_template_form( $template_key ); ?>
						<a class="button subin-btn subin-btn-secondary" href="<?php echo esc_url( $this->template_preview_url( $template_key ) ); ?>"><?php esc_html_e( 'Preview', 'submission-inbox' ); ?></a>
					</div>
				</div>
			<?php endforeach; ?>
		</div>
		<?php
		SUBIN_View::page_footer();
	}

	/**
	 * Render a larger view-only template preview.
	 *
	 * @return void
	 */
	private function render_template_preview_view() {
		$template_key = SUBIN_Request::get_key( 'template' );
		$template     = $this->templates->get_template( $template_key );

		SUBIN_View::page_header(
			$template ? sprintf(
				/* translators: %s: Template title. */
				__( 'Preview Template: %s', 'submission-inbox' ),
				$template['title']
			) : __( 'Preview Template', 'submission-inbox' ),
			__( 'Review the starter form before creating it.', 'submission-inbox' ),
			array(
				'wrapper_class' => 'subin-forms-page subin-form-templates-page',
			)
		);

		if ( ! $template ) {
			$this->render_notice( 'template_not_found' );
			echo '<p><a class="button subin-btn subin-btn-secondary" href="' . esc_url( $this->templates_url() ) . '">' . esc_html__( 'Back to Templates', 'submission-inbox' ) . '</a></p>';
			SUBIN_View::page_footer();
			return;
		}

		$field_names = $this->template_field_labels( $template );
		$fields      = is_array( $template['fields'] ?? null ) ? $template['fields'] : array();
		?>
		<div class="subin-page-actions">
			<?php $this->render_use_template_form( $template_key ); ?>
			<a class="button subin-btn subin-btn-secondary" href="<?php echo esc_url( $this->templates_url() ); ?>"><?php esc_html_e( 'Back to Templates', 'submission-inbox' ); ?></a>
		</div>
		<div class="subin-notice-box">
			<?php esc_html_e( 'This is a template preview. Create the form before placing it on a page.', 'submission-inbox' ); ?>
		</div>

		<div class="subin-template-preview-screen">
			<div class="subin-template-preview-screen__content">
				<div class="subin-template-card__badge"><?php echo esc_html( $this->template_category( $template_key, $template ) ); ?></div>
				<h2><?php echo esc_html( $template['title'] ?? '' ); ?></h2>
				<p><?php echo esc_html( $this->template_description( $template_key, $template ) ); ?></p>
				<p class="subin-template-card__fields">
					<strong><?php esc_html_e( 'Fields:', 'submission-inbox' ); ?></strong>
					<?php echo esc_html( implode( ', ', $field_names ) ); ?>
				</p>
				<dl class="subin-template-details">
					<dt><?php esc_html_e( 'Field count', 'submission-inbox' ); ?></dt>
					<dd><?php echo esc_html( $this->format_template_field_count( count( $fields ) ) ); ?></dd>
					<?php if ( ! empty( $template['header_title'] ) ) : ?>
						<dt><?php esc_html_e( 'Header title', 'submission-inbox' ); ?></dt>
						<dd><?php echo esc_html( $template['header_title'] ); ?></dd>
					<?php endif; ?>
					<?php if ( ! empty( $template['header_description'] ) ) : ?>
						<dt><?php esc_html_e( 'Header description', 'submission-inbox' ); ?></dt>
						<dd><?php echo esc_html( $template['header_description'] ); ?></dd>
					<?php endif; ?>
					<?php if ( ! empty( $template['footer_note'] ) ) : ?>
						<dt><?php esc_html_e( 'Footer note', 'submission-inbox' ); ?></dt>
						<dd><?php echo esc_html( $template['footer_note'] ); ?></dd>
					<?php endif; ?>
					<?php if ( ! empty( $template['success_message'] ) ) : ?>
						<dt><?php esc_html_e( 'Success message', 'submission-inbox' ); ?></dt>
						<dd><?php echo esc_html( $template['success_message'] ); ?></dd>
					<?php endif; ?>
				</dl>
			</div>
			<div class="subin-template-preview-screen__visual">
				<?php $this->render_template_visual_preview( $template, true ); ?>
			</div>
		</div>
		<?php
		SUBIN_View::page_footer();
	}

	/**
	 * Render admin preview screen.
	 *
	 * @return void
	 */
	private function render_preview_view() {
		$form_id = SUBIN_Request::get_absint( 'form_id' );
		$form    = $this->forms->get( $form_id );

		if ( ! $form ) {
			SUBIN_View::page_header( __( 'Form Preview', 'submission-inbox' ), __( 'Form was not found.', 'submission-inbox' ) );
			$this->render_notice( 'form_not_found' );
			echo '<p><a class="button" href="' . esc_url( $this->forms_url() ) . '">' . esc_html__( 'Back to Forms', 'submission-inbox' ) . '</a></p>';
			SUBIN_View::page_footer();
			return;
		}

		$fields   = $this->fields->get_by_form_id( $form_id );
		$renderer = new \SUBIN\Forms\FormRenderer();
		$shortcode = '[submission_inbox_form id="' . $form_id . '"]';
		$status    = sanitize_key( $form['status'] ?? '' );
		$is_active = \SUBIN\Forms\FormRepository::STATUS_ACTIVE === $status;

		SUBIN_View::page_header(
			sprintf(
				/* translators: %s: Form title. */
				__( 'Preview Form: %s', 'submission-inbox' ),
				$form['title']
			),
			__( 'Preview the saved form before placing it on a public page.', 'submission-inbox' )
		);
		?>
		<div class="subin-preview-page">
			<div class="subin-preview-page__bar">
				<div>
					<?php SUBIN_View::badge( $is_active ? __( 'Active', 'submission-inbox' ) : __( 'Inactive', 'submission-inbox' ), $is_active ? 'success' : 'muted' ); ?>
					<?php if ( ! $is_active ) : ?>
						<p class="description"><?php esc_html_e( 'This form is inactive. Regular visitors will not see it.', 'submission-inbox' ); ?></p>
					<?php endif; ?>
				</div>
				<div class="subin-editor-actions">
					<a class="button" href="<?php echo esc_url( $this->edit_url( $form_id ) ); ?>"><?php esc_html_e( 'Back to editor', 'submission-inbox' ); ?></a>
					<a class="button" href="<?php echo esc_url( $this->forms_url() ); ?>"><?php esc_html_e( 'Back to Forms', 'submission-inbox' ); ?></a>
				</div>
			</div>

			<?php $this->render_shortcode_box( $shortcode, __( 'Shortcode', 'submission-inbox' ), __( 'Copy this shortcode and paste it into any page or post.', 'submission-inbox' ) ); ?>

			<div class="subin-notice-box subin-preview-notice">
				<?php esc_html_e( 'This is an admin preview. To collect leads, place the shortcode on a public page.', 'submission-inbox' ); ?>
			</div>
			<div class="subin-preview-frame">
				<?php echo $renderer->render( $form, $fields, array( 'preview' => true ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
			</div>
		</div>
		<?php
		SUBIN_View::page_footer();
	}

	/**
	 * Render edit screen.
	 *
	 * @return void
	 */
	private function render_edit_view() {
		$form_id = SUBIN_Request::get_absint( 'form_id' );
		$form    = $this->forms->get( $form_id );

		if ( ! $form ) {
			SUBIN_View::page_header( __( 'Edit Form', 'submission-inbox' ), __( 'Form was not found.', 'submission-inbox' ) );
			$this->render_notice( 'form_not_found' );
			echo '<p><a class="button" href="' . esc_url( $this->forms_url() ) . '">' . esc_html__( 'Back to Forms', 'submission-inbox' ) . '</a></p>';
			SUBIN_View::page_footer();
			return;
		}

		$requested_tab = SUBIN_Request::get_key( 'tab' );
		$visible_tabs  = array( 'design_studio', 'settings' );
		$legacy_tabs   = array( 'content', 'fields', 'design', 'messages', 'advanced_html' );
		$tab           = '' === $requested_tab ? 'design_studio' : $requested_tab;
		if ( in_array( $tab, array( 'email', 'publish' ), true ) ) {
			$this->safe_admin_redirect( $this->edit_url( $form_id, 'settings' ) );
		}
		if ( in_array( $tab, $legacy_tabs, true ) ) {
			$this->safe_admin_redirect( $this->edit_url( $form_id, 'design_studio' ) );
		}
		$tab = in_array( $tab, $visible_tabs, true ) ? $tab : 'design_studio';
		$shortcode = '[submission_inbox_form id="' . absint( $form['id'] ) . '"]';

		SUBIN_View::page_header(
			sprintf(
				/* translators: %s: Form title. */
				__( 'Edit Form: %s', 'submission-inbox' ),
				$form['title']
			),
			__( 'Customize your form, review the saved preview, and copy the shortcode when you are ready to publish.', 'submission-inbox' ),
			array( 'wrapper_class' => 'subin-forms-page subin-form-editor-page' )
		);

		$this->render_notice();
		$editor_classes = array( 'subin-form-editor' );
		if ( 'design_studio' === $tab ) {
			$editor_classes[] = 'subin-form-editor--design-studio';
		}
		?>
		<div class="<?php echo esc_attr( implode( ' ', $editor_classes ) ); ?>">
			<div class="subin-form-editor__body">
				<div class="subin-form-editor__main">
					<?php if ( 'design_studio' !== $tab ) : ?>
						<nav class="nav-tab-wrapper subin-editor-tabs" aria-label="<?php esc_attr_e( 'Form editor tabs', 'submission-inbox' ); ?>">
							<?php $this->editor_tab( $form_id, 'design_studio', __( 'Design Studio', 'submission-inbox' ), $tab ); ?>
							<?php $this->editor_tab( $form_id, 'settings', __( 'Settings', 'submission-inbox' ), $tab ); ?>
						</nav>
					<?php endif; ?>

					<?php
					if ( 'design_studio' === $tab ) {
						$this->render_design_studio_tab( $form );
					} elseif ( 'settings' === $tab ) {
						$this->render_settings_tab( $form, $shortcode );
					} else {
						$this->render_design_studio_tab( $form );
					}
					?>
				</div>
			</div>
		</div>
		<?php

		SUBIN_View::page_footer();
	}

	/**
	 * Render Design Studio foundation tab.
	 *
	 * @param array<string,mixed> $form Form.
	 * @return void
	 */
	private function render_design_studio_tab( array $form ) {
		$form_id     = absint( $form['id'] );
		$shortcode   = '[submission_inbox_form id="' . $form_id . '"]';
		$status      = sanitize_key( $form['status'] ?? '' );
		$is_active   = \SUBIN\Forms\FormRepository::STATUS_ACTIVE === $status;
		$editor_data = $this->grapesjs_editor_form_data( $form );
		?>
		<div class="subin-panel subin-editor-panel subin-design-studio-panel subin-studio-shell-v2" data-subin-studio-shell>
			<div class="subin-design-studio subin-design-studio--grapesjs-shell">
				<header class="subin-design-studio__toolbar">
					<div class="subin-design-toolbar__left">
						<a class="subin-studio-icon-button subin-design-back-link" href="<?php echo esc_url( $this->forms_url() ); ?>" aria-label="<?php esc_attr_e( 'Back to Forms', 'submission-inbox' ); ?>" title="<?php esc_attr_e( 'Back to Forms', 'submission-inbox' ); ?>">
							<span class="dashicons dashicons-arrow-left-alt2" aria-hidden="true"></span>
						</a>
						<div class="subin-design-toolbar__identity">
							<div>
								<span class="subin-studio-eyebrow"><?php esc_html_e( 'Form editor', 'submission-inbox' ); ?></span>
								<h2><?php echo esc_html( $form['title'] ?? '' ); ?></h2>
							</div>
							<?php SUBIN_View::badge( $is_active ? __( 'Active', 'submission-inbox' ) : __( 'Inactive', 'submission-inbox' ), $is_active ? 'success' : 'muted' ); ?>
						</div>
					</div>
					<div class="subin-design-toolbar__center" aria-live="polite">
						<span class="subin-studio-save-indicator" aria-hidden="true"></span>
						<span class="subin-grapesjs-save-status" data-subin-grapesjs-save-status></span>
					</div>
					<div class="subin-design-toolbar__right">
						<div class="subin-studio-history-actions" role="group" aria-label="<?php esc_attr_e( 'Edit history', 'submission-inbox' ); ?>">
							<button type="button" class="subin-studio-icon-button" data-subin-studio-undo aria-label="<?php esc_attr_e( 'Undo', 'submission-inbox' ); ?>" title="<?php esc_attr_e( 'Undo', 'submission-inbox' ); ?>" disabled><span class="dashicons dashicons-undo" aria-hidden="true"></span></button>
							<button type="button" class="subin-studio-icon-button" data-subin-studio-redo aria-label="<?php esc_attr_e( 'Redo', 'submission-inbox' ); ?>" title="<?php esc_attr_e( 'Redo', 'submission-inbox' ); ?>" disabled><span class="dashicons dashicons-redo" aria-hidden="true"></span></button>
						</div>
						<button type="button" class="button subin-studio-preview-button" data-subin-studio-preview-open><span class="dashicons dashicons-visibility subin-studio-button-icon" aria-hidden="true"></span><?php esc_html_e( 'Preview', 'submission-inbox' ); ?></button>
						<button type="button" class="button button-primary" data-subin-studio-publish><?php esc_html_e( 'Publish', 'submission-inbox' ); ?></button>
						<button type="button" data-subin-grapesjs-save hidden tabindex="-1" aria-hidden="true"><?php esc_html_e( 'Save draft', 'submission-inbox' ); ?></button>
					</div>
				</header>

				<div class="subin-studio-preview-toolbar" data-subin-studio-preview-toolbar hidden>
					<button type="button" class="button subin-studio-preview-back" data-subin-studio-preview-close><span class="dashicons dashicons-arrow-left-alt2 subin-studio-button-icon" aria-hidden="true"></span><?php esc_html_e( 'Back to editor', 'submission-inbox' ); ?></button>
					<div class="subin-studio-preview-toolbar__center">
						<div class="subin-gjs-preview-state-switcher" role="group" aria-label="<?php esc_attr_e( 'Preview state', 'submission-inbox' ); ?>">
							<button type="button" class="button button-small subin-gjs-preview-state-button is-active" data-subin-gjs-preview-state="normal" aria-pressed="true"><?php esc_html_e( 'Normal', 'submission-inbox' ); ?></button>
							<button type="button" class="button button-small subin-gjs-preview-state-button" data-subin-gjs-preview-state="error" aria-pressed="false"><?php esc_html_e( 'Error', 'submission-inbox' ); ?></button>
							<button type="button" class="button button-small subin-gjs-preview-state-button" data-subin-gjs-preview-state="success" aria-pressed="false"><?php esc_html_e( 'Success', 'submission-inbox' ); ?></button>
						</div>
					</div>
					<div class="subin-studio-device-switcher" role="group" aria-label="<?php esc_attr_e( 'Preview device', 'submission-inbox' ); ?>">
						<button type="button" class="subin-studio-device-button is-active" data-subin-studio-device="Desktop" aria-pressed="true"><span class="dashicons dashicons-desktop" aria-hidden="true"></span><span><?php esc_html_e( 'Desktop', 'submission-inbox' ); ?></span></button>
						<button type="button" class="subin-studio-device-button" data-subin-studio-device="Tablet" aria-pressed="false"><span class="dashicons dashicons-tablet" aria-hidden="true"></span><span><?php esc_html_e( 'Tablet', 'submission-inbox' ); ?></span></button>
						<button type="button" class="subin-studio-device-button" data-subin-studio-device="Mobile" aria-pressed="false"><span class="dashicons dashicons-smartphone" aria-hidden="true"></span><span><?php esc_html_e( 'Mobile', 'submission-inbox' ); ?></span></button>
					</div>
				</div>

				<div class="subin-studio-workspace">
					<aside class="subin-studio-library" aria-label="<?php esc_attr_e( 'Form tools', 'submission-inbox' ); ?>">
						<nav class="subin-studio-rail" aria-label="<?php esc_attr_e( 'Design Studio sections', 'submission-inbox' ); ?>">
							<button type="button" class="subin-studio-rail-button is-active" data-subin-studio-nav="build" aria-pressed="true"><span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span><span><?php esc_html_e( 'Build', 'submission-inbox' ); ?></span></button>
							<button type="button" class="subin-studio-rail-button" data-subin-studio-nav="structure" aria-pressed="false"><span class="dashicons dashicons-list-view" aria-hidden="true"></span><span><?php esc_html_e( 'Structure', 'submission-inbox' ); ?></span></button>
							<button type="button" class="subin-studio-rail-button" data-subin-studio-nav="theme" aria-pressed="false"><span class="dashicons dashicons-art" aria-hidden="true"></span><span><?php esc_html_e( 'Theme', 'submission-inbox' ); ?></span></button>
							<button type="button" class="subin-studio-rail-button" data-subin-studio-nav="settings" aria-pressed="false"><span class="dashicons dashicons-admin-generic" aria-hidden="true"></span><span><?php esc_html_e( 'Settings', 'submission-inbox' ); ?></span></button>
							<button type="button" class="subin-studio-rail-button" data-subin-studio-nav="logic" aria-pressed="false"><span class="dashicons dashicons-randomize" aria-hidden="true"></span><span><?php esc_html_e( 'Logic', 'submission-inbox' ); ?></span></button>
						</nav>
						<div class="subin-studio-library-drawer">
							<section class="subin-studio-library-panel is-active" data-subin-studio-panel="build">
								<div class="subin-studio-panel-heading"><div><span class="subin-studio-eyebrow"><?php esc_html_e( 'Build', 'submission-inbox' ); ?></span><h3><?php esc_html_e( 'Fields & blocks', 'submission-inbox' ); ?></h3></div><p><?php esc_html_e( 'Drag an item onto the form.', 'submission-inbox' ); ?></p></div>
								<label class="subin-studio-search"><span class="dashicons dashicons-search" aria-hidden="true"></span><span class="screen-reader-text"><?php esc_html_e( 'Search fields and blocks', 'submission-inbox' ); ?></span><input type="search" data-subin-studio-block-search placeholder="<?php esc_attr_e( 'Search fields...', 'submission-inbox' ); ?>" /></label>
								<div class="subin-studio-blocks-host" data-subin-studio-blocks></div>
							</section>
							<section class="subin-studio-library-panel" data-subin-studio-panel="structure" hidden>
								<div class="subin-studio-panel-heading"><div><span class="subin-studio-eyebrow"><?php esc_html_e( 'Structure', 'submission-inbox' ); ?></span><h3><?php esc_html_e( 'Form outline', 'submission-inbox' ); ?></h3></div><p><?php esc_html_e( 'Select, reorder, and inspect form elements.', 'submission-inbox' ); ?></p></div>
								<div class="subin-studio-layers-host" data-subin-studio-layers></div>
							</section>
							<section class="subin-studio-library-panel" data-subin-studio-panel="theme" hidden>
								<div class="subin-studio-panel-heading"><div><span class="subin-studio-eyebrow"><?php esc_html_e( 'Theme', 'submission-inbox' ); ?></span><h3><?php esc_html_e( 'Design system', 'submission-inbox' ); ?></h3></div><p><?php esc_html_e( 'Choose the part you want to style.', 'submission-inbox' ); ?></p></div>
								<div class="subin-studio-jump-list">
									<button type="button" data-subin-studio-select="form-wrapper"><span class="dashicons dashicons-feedback" aria-hidden="true"></span><span><strong><?php esc_html_e( 'Form container', 'submission-inbox' ); ?></strong><small><?php esc_html_e( 'Width, background, spacing', 'submission-inbox' ); ?></small></span></button>
									<button type="button" data-subin-studio-select="field"><span class="dashicons dashicons-editor-textcolor" aria-hidden="true"></span><span><strong><?php esc_html_e( 'Form fields', 'submission-inbox' ); ?></strong><small><?php esc_html_e( 'Labels, inputs, validation', 'submission-inbox' ); ?></small></span></button>
									<button type="button" data-subin-studio-select="submit-button"><span class="dashicons dashicons-button" aria-hidden="true"></span><span><strong><?php esc_html_e( 'Submit button', 'submission-inbox' ); ?></strong><small><?php esc_html_e( 'Text, color, alignment', 'submission-inbox' ); ?></small></span></button>
								</div>
							</section>
							<section class="subin-studio-library-panel" data-subin-studio-panel="settings" hidden>
								<div class="subin-studio-panel-heading"><div><span class="subin-studio-eyebrow"><?php esc_html_e( 'Settings', 'submission-inbox' ); ?></span><h3><?php esc_html_e( 'Form setup', 'submission-inbox' ); ?></h3></div><p><?php esc_html_e( 'Messages, delivery, and embedding.', 'submission-inbox' ); ?></p></div>
								<div class="subin-studio-jump-list">
									<button type="button" data-subin-studio-select="form-wrapper"><span class="dashicons dashicons-format-status" aria-hidden="true"></span><span><strong><?php esc_html_e( 'Content & messages', 'submission-inbox' ); ?></strong><small><?php esc_html_e( 'Title, description, success and error states', 'submission-inbox' ); ?></small></span></button>
									<a href="<?php echo esc_url( $this->edit_url( $form_id, 'settings' ) ); ?>"><span class="dashicons dashicons-email-alt" aria-hidden="true"></span><span><strong><?php esc_html_e( 'Delivery settings', 'submission-inbox' ); ?></strong><small><?php esc_html_e( 'Notifications, storage, and auto-replies', 'submission-inbox' ); ?></small></span></a>
								</div>
								<div class="subin-studio-embed-card">
									<span class="subin-studio-eyebrow"><?php esc_html_e( 'Embed', 'submission-inbox' ); ?></span>
									<p><?php esc_html_e( 'Paste this shortcode into any page or post.', 'submission-inbox' ); ?></p>
									<code class="subin-shortcode-pill"><?php echo esc_html( $shortcode ); ?></code>
									<button type="button" class="button subin-shortcode-copy subin-copy-shortcode" data-subin-copy="<?php echo esc_attr( $shortcode ); ?>"><?php esc_html_e( 'Copy shortcode', 'submission-inbox' ); ?></button>
									<span class="subin-copy-feedback" aria-live="polite"></span>
								</div>
							</section>
							<section class="subin-studio-library-panel" data-subin-studio-panel="logic" hidden>
								<div class="subin-studio-panel-heading"><div><span class="subin-studio-eyebrow"><?php esc_html_e( 'Logic', 'submission-inbox' ); ?></span><h3><?php esc_html_e( 'Conditional rules', 'submission-inbox' ); ?></h3></div><p><?php esc_html_e( 'Show or hide fields and blocks based on visitor answers.', 'submission-inbox' ); ?></p></div>
							</section>
						</div>
					</aside>

					<main class="subin-studio-canvas" aria-label="<?php esc_attr_e( 'Form canvas', 'submission-inbox' ); ?>">
						<div class="subin-studio-canvas-label"><span class="subin-studio-canvas-label__dot"></span><span data-subin-studio-canvas-label><?php esc_html_e( 'Editing desktop layout', 'submission-inbox' ); ?></span></div>
						<div id="subin-grapesjs-editor" class="subin-grapesjs-editor"></div>
					</main>

					<div class="subin-studio-inspector-host" data-subin-studio-inspector-host aria-label="<?php esc_attr_e( 'Element settings', 'submission-inbox' ); ?>"></div>
				</div>

				<div class="subin-studio-publish-layer" data-subin-studio-publish-layer hidden>
					<button type="button" class="subin-studio-publish-backdrop" data-subin-studio-publish-close aria-label="<?php esc_attr_e( 'Close publish panel', 'submission-inbox' ); ?>"></button>
					<aside class="subin-studio-publish-panel" role="dialog" aria-modal="true" aria-labelledby="subin-studio-publish-title">
						<header class="subin-studio-publish-panel__header">
							<div><span class="subin-studio-eyebrow"><?php esc_html_e( 'Publish & embed', 'submission-inbox' ); ?></span><h2 id="subin-studio-publish-title"><?php esc_html_e( 'Publish this form', 'submission-inbox' ); ?></h2></div>
							<button type="button" class="subin-studio-icon-button" data-subin-studio-publish-close aria-label="<?php esc_attr_e( 'Close publish panel', 'submission-inbox' ); ?>"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button>
						</header>
						<div class="subin-studio-publish-panel__body">
							<div class="subin-studio-publish-summary" data-subin-studio-publish-summary>
								<span class="dashicons dashicons-cloud-saved" aria-hidden="true"></span>
								<div><strong><?php esc_html_e( 'Your draft is saved separately', 'submission-inbox' ); ?></strong><p><?php esc_html_e( 'Visitors keep seeing the published version until you confirm below.', 'submission-inbox' ); ?></p></div>
							</div>
							<section class="subin-studio-publish-section subin-studio-preflight">
								<div class="subin-studio-preflight__heading">
									<div>
										<h3><?php esc_html_e( 'Pre-publish checks', 'submission-inbox' ); ?></h3>
										<p><?php esc_html_e( 'Checks the saved draft, delivery setup, protection, and privacy before it goes live.', 'submission-inbox' ); ?></p>
									</div>
									<span class="subin-studio-preflight__badge" data-subin-studio-preflight-badge data-status="idle"><?php esc_html_e( 'Not checked', 'submission-inbox' ); ?></span>
								</div>
								<div class="subin-studio-preflight__actions">
									<button type="button" class="button" data-subin-studio-preflight-run><?php esc_html_e( 'Run checks', 'submission-inbox' ); ?></button>
									<button type="button" class="button" data-subin-studio-sandbox-run disabled><?php esc_html_e( 'Run safe simulation', 'submission-inbox' ); ?></button>
								</div>
								<div class="subin-studio-preflight__results" data-subin-studio-preflight-results aria-live="polite">
									<p class="subin-studio-preflight__empty"><?php esc_html_e( 'Checks run automatically when this panel opens.', 'submission-inbox' ); ?></p>
								</div>
								<div class="subin-studio-sandbox-result" data-subin-studio-sandbox-result aria-live="polite" hidden></div>
								<p class="subin-studio-preflight__note"><?php esc_html_e( 'Safe simulation validates generated sample values and the delivery plan. It never creates a lead, sends email, or queues background work.', 'submission-inbox' ); ?></p>
							</section>
							<section class="subin-studio-publish-section">
								<h3><?php esc_html_e( 'Published revisions', 'submission-inbox' ); ?></h3>
								<p><?php esc_html_e( 'Restore creates a new draft first; it never changes the live form without Publish.', 'submission-inbox' ); ?></p>
								<div class="subin-studio-revision-list" data-subin-studio-history></div>
							</section>
							<section class="subin-studio-publish-section">
								<h3><?php esc_html_e( 'Embed after publishing', 'submission-inbox' ); ?></h3>
								<div class="subin-studio-publish-shortcode">
									<code><?php echo esc_html( $shortcode ); ?></code>
									<button type="button" class="button subin-copy-shortcode" data-subin-copy="<?php echo esc_attr( $shortcode ); ?>"><?php esc_html_e( 'Copy', 'submission-inbox' ); ?></button>
									<span class="subin-copy-feedback" aria-live="polite"></span>
								</div>
							</section>
						</div>
						<footer class="subin-studio-publish-panel__footer">
							<span data-subin-studio-publish-status aria-live="polite"></span>
							<div><button type="button" class="button" data-subin-studio-publish-close><?php esc_html_e( 'Cancel', 'submission-inbox' ); ?></button><button type="button" class="button button-primary" data-subin-studio-publish-confirm><?php esc_html_e( 'Publish changes', 'submission-inbox' ); ?></button></div>
						</footer>
					</aside>
				</div>

				<script type="application/json" id="subin-grapesjs-form-data"><?php echo str_replace( '</script', '<\/script', wp_json_encode( $editor_data ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON is encoded and script-close escaped for an admin-only data island. ?></script>
				<?php $this->render_grapesjs_html_editor_modal(); ?>
			</div>
		</div>
		<?php
	}

	/**
	 * Render the WordPress editor modal used by the controlled GrapesJS HTML block.
	 *
	 * @return void
	 */
	private function render_grapesjs_html_editor_modal() {
		$editor_id = 'subin_gjs_html_editor';
		?>
		<div class="subin-gjs-html-modal" data-subin-gjs-html-modal hidden>
			<div class="subin-gjs-html-modal__backdrop" data-subin-gjs-html-close></div>
			<div class="subin-gjs-html-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="subin-gjs-html-modal-title">
				<div class="subin-gjs-html-modal__header">
					<h2 id="subin-gjs-html-modal-title"><?php esc_html_e( 'HTML block editor', 'submission-inbox' ); ?></h2>
					<button type="button" class="button-link subin-gjs-html-modal__close" data-subin-gjs-html-close aria-label="<?php esc_attr_e( 'Close editor', 'submission-inbox' ); ?>">&times;</button>
				</div>
				<div class="subin-gjs-html-modal__body">
					<?php
					wp_editor(
						'',
						$editor_id,
						array(
							'textarea_name' => 'subin_gjs_html_editor_content',
							'textarea_rows' => 14,
							'media_buttons' => false,
							'teeny'         => false,
							'quicktags'     => true,
							'tinymce'       => array(
								'toolbar1' => 'formatselect,bold,italic,bullist,numlist,blockquote,alignleft,aligncenter,alignright,link,unlink,undo,redo',
								'toolbar2' => '',
							),
						)
					);
					?>
					<p class="description"><?php esc_html_e( 'Scripts, forms, embeds, iframes, shortcodes, and unsafe attributes are stripped when the block is saved or rendered.', 'submission-inbox' ); ?></p>
				</div>
				<div class="subin-gjs-html-modal__footer">
					<button type="button" class="button" data-subin-gjs-html-close><?php esc_html_e( 'Cancel', 'submission-inbox' ); ?></button>
					<button type="button" class="button button-primary" data-subin-gjs-html-apply><?php esc_html_e( 'Apply HTML', 'submission-inbox' ); ?></button>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Build the Design Studio GrapesJS editor preview data.
	 *
	 * @param array<string,mixed> $form Form.
	 * @return array<string,mixed>
	 */
	private function grapesjs_editor_form_data( array $form ) {
		$form_id     = absint( $form['id'] ?? 0 );
		$fields      = array();
		$title       = sanitize_text_field( $form['header_title'] ?? '' );
		$description = sanitize_textarea_field( $form['header_description'] ?? '' );
		$settings     = $this->forms->decode_settings( $form );
		$designer     = new \SUBIN\Forms\DesignSettings();
		$design       = $designer->get_for_settings( $settings );
		$messages     = is_array( $settings['messages'] ?? null ) ? $settings['messages'] : array();
		$field_styles = $designer->sanitize_field_styles( is_array( $settings['design_studio_field_styles'] ?? null ) ? $settings['design_studio_field_styles'] : array() );
		$studio_state = $this->forms->get_design_studio_state_from_settings( $settings, $form );
		$project      = $this->saved_grapesjs_project_for_editor( $settings );
		$history      = array();
		foreach ( $studio_state['history'] as $snapshot ) {
			$history[] = array(
				'revision'     => max( 0, (int) ( $snapshot['revision'] ?? 0 ) ),
				'published_at' => sanitize_text_field( $snapshot['published_at'] ?? '' ),
				'published_by' => absint( $snapshot['published_by'] ?? 0 ),
			);
		}

		if ( '' === $title ) {
			$title = sanitize_text_field( $form['title'] ?? '' );
		}

		if ( '' === $description ) {
			$description = sanitize_textarea_field( $form['description'] ?? '' );
		}

		foreach ( $this->fields->get_by_form_id( $form_id ) as $field ) {
			$fields[] = array(
				'id'          => absint( $field['id'] ?? 0 ),
				'key'         => sanitize_key( $field['field_key'] ?? '' ),
				'type'        => sanitize_key( $field['field_type'] ?? 'text' ),
				'label'       => sanitize_text_field( $field['field_label'] ?? '' ),
				'placeholder' => sanitize_text_field( $field['placeholder'] ?? '' ),
				'help_text'   => sanitize_textarea_field( $field['help_text'] ?? '' ),
				'required'    => ! empty( $field['is_required'] ),
				'options'     => $this->get_preview_options( $field ),
			);
		}

		return array(
			'id'                 => $form_id,
			'revision'           => max( 0, (int) $studio_state['draft']['revision'] ),
			'published_revision' => max( 0, (int) $studio_state['published']['revision'] ),
			'published_at'       => sanitize_text_field( $studio_state['published']['published_at'] ?? '' ),
			'is_dirty'           => ! empty( $studio_state['is_dirty'] ),
			'history'            => $history,
			'title'              => $title,
			'description'        => $description,
			'footer_note'        => sanitize_textarea_field( $form['footer_note'] ?? '' ),
			'privacy_text'       => sanitize_textarea_field( $form['privacy_text'] ?? '' ),
			'submit_button_text' => sanitize_text_field( $form['submit_button_text'] ?? __( 'Submit', 'submission-inbox' ) ),
			'fields'             => $fields,
			'design_styles'      => $design,
			'field_styles'       => $field_styles,
			'messages'           => array(
				'validation_error'      => sanitize_textarea_field( $messages['validation_error'] ?? __( 'Please check the highlighted fields and try again.', 'submission-inbox' ) ),
				'required_field_error'  => sanitize_text_field( $messages['required_field_error'] ?? __( 'This field is required.', 'submission-inbox' ) ),
				'spam_protection_error' => sanitize_textarea_field( $messages['spam_protection_error'] ?? __( 'Your submission could not be accepted.', 'submission-inbox' ) ),
				'success_message'       => sanitize_textarea_field( $form['success_message'] ?: __( 'Thanks, your message has been sent.', 'submission-inbox' ) ),
			),
			'grapesjs_project'   => $project,
			'ajax'               => array(
				'action'           => 'subin_save_grapesjs_draft',
				'url'              => admin_url( 'admin-ajax.php' ),
				'nonce'            => wp_create_nonce( 'subin_save_grapesjs_draft_' . $form_id ),
				'publish_action'   => 'subin_publish_grapesjs_draft',
				'publish_nonce'    => wp_create_nonce( 'subin_publish_grapesjs_draft_' . $form_id ),
				'restore_action'   => 'subin_restore_grapesjs_revision',
				'restore_nonce'    => wp_create_nonce( 'subin_restore_grapesjs_revision_' . $form_id ),
				'preflight_action' => 'subin_form_preflight',
				'preflight_nonce'  => wp_create_nonce( 'subin_form_preflight_' . $form_id ),
				'sandbox_action'   => 'subin_form_sandbox',
				'sandbox_nonce'    => wp_create_nonce( 'subin_form_sandbox_' . $form_id ),
				'media_action'     => 'subin_upload_design_studio_media',
				'media_nonce'      => wp_create_nonce( 'subin_upload_design_studio_media_' . $form_id ),
			),
		);
	}

	/**
	 * Upload a Design Studio image into the WordPress Media Library.
	 *
	 * @return void
	 */
	public function ajax_upload_design_studio_media() {
		if ( ! current_user_can( SUBIN_AdminMenu::CAPABILITY ) || ! current_user_can( 'upload_files' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Image could not be uploaded.', 'submission-inbox' ),
				),
				403
			);
		}

		$form_id = isset( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0;
		$nonce   = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';

		if ( $form_id <= 0 || ! wp_verify_nonce( $nonce, 'subin_upload_design_studio_media_' . $form_id ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Image could not be uploaded.', 'submission-inbox' ),
				),
				403
			);
		}

		if ( ! $this->forms->get( $form_id ) || empty( $_FILES['file'] ) || ! is_array( $_FILES['file'] ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Image file is missing.', 'submission-inbox' ),
				),
				400
			);
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/image.php';
		require_once ABSPATH . 'wp-admin/includes/media.php';

		$attachment_id = media_handle_upload( 'file', 0, array(), array( 'test_form' => false ) );
		if ( is_wp_error( $attachment_id ) ) {
			wp_send_json_error(
				array(
					'message' => $attachment_id->get_error_message(),
				),
				400
			);
		}

		$url = wp_get_attachment_image_url( $attachment_id, 'full' );
		if ( ! $url ) {
			$url = wp_get_attachment_url( $attachment_id );
		}

		if ( ! $url ) {
			wp_send_json_error(
				array(
					'message' => __( 'Uploaded image URL could not be processed.', 'submission-inbox' ),
				),
				500
			);
		}

		wp_send_json_success(
			array(
				'id'  => absint( $attachment_id ),
				'url' => esc_url_raw( $url ),
			)
		);
	}

	/**
	 * Get saved GrapesJS project data for the admin editor.
	 *
	 * @param array<string,mixed> $settings Decoded settings.
	 * @return array<string,mixed>
	 */
	private function saved_grapesjs_project_for_editor( array $settings ) {
		$studio   = is_array( $settings['design_studio'] ?? null ) ? $settings['design_studio'] : array();
		$draft    = is_array( $studio['draft']['project'] ?? null ) ? $studio['draft']['project'] : null;
		$is_draft = is_array( $draft );
		$published = is_array( $studio['published']['project'] ?? null ) ? $studio['published']['project'] : null;
		$legacy    = is_array( $studio['grapesjs_project'] ?? null ) ? $studio['grapesjs_project'] : array();
		$project   = $is_draft ? $draft : ( is_array( $published ) ? $published : $legacy );
		if ( empty( $project ) ) {
			return array();
		}

		$project = $is_draft ? $this->sanitize_grapesjs_project_for_draft( $project ) : $this->sanitize_grapesjs_project( $project );
		return is_wp_error( $project ) ? array() : $project;
	}

	/**
	 * Sanitize an unpublished project without dropping incomplete Logic rows.
	 *
	 * Draft Logic uses a deliberately small, bounded schema. Empty identifiers,
	 * selectors, groups, and values are retained so autosave cannot erase an
	 * in-progress rule. The strict sanitizer remains the publish boundary.
	 *
	 * @param array<string,mixed> $project Project data.
	 * @return array<string,mixed>|\WP_Error
	 */
	private function sanitize_grapesjs_project_for_draft( array $project ) {
		$has_logic = array_key_exists( 'subinLogic', $project );
		$logic     = $project['subinLogic'] ?? null;
		unset( $project['subinLogic'] );

		$clean = $this->sanitize_grapesjs_project( $project );
		if ( is_wp_error( $clean ) || ! $has_logic ) {
			return $clean;
		}

		$draft_logic = $this->sanitize_grapesjs_logic_draft( $logic );
		if ( is_wp_error( $draft_logic ) ) {
			return $draft_logic;
		}

		$clean['subinLogic'] = $draft_logic;
		return $clean;
	}

	/**
	 * Preserve a bounded Conditional Logic draft, including incomplete entries.
	 *
	 * @param mixed $logic Raw Logic draft.
	 * @return array{version:int,rules:array<int,array<string,mixed>>}|\WP_Error
	 */
	private function sanitize_grapesjs_logic_draft( $logic ) {
		if ( ! is_array( $logic ) || ( isset( $logic['rules'] ) && ! is_array( $logic['rules'] ) ) ) {
			return $this->grapesjs_logic_draft_error();
		}

		$version = isset( $logic['version'] ) && is_scalar( $logic['version'] ) ? (int) $logic['version'] : \SUBIN\Forms\ConditionalLogicEvaluator::VERSION;
		if ( \SUBIN\Forms\ConditionalLogicEvaluator::VERSION !== $version ) {
			return new \WP_Error(
				'subin_grapesjs_logic_version_invalid',
				__( 'Conditional logic version is not supported.', 'submission-inbox' )
			);
		}

		$rules = $logic['rules'] ?? array();
		if ( ! $this->is_grapesjs_logic_list( $rules ) ) {
			return $this->grapesjs_logic_draft_error();
		}
		if ( count( $rules ) > \SUBIN\Forms\ConditionalLogicEvaluator::MAX_RULES ) {
			return $this->grapesjs_logic_draft_error( true );
		}

		$clean            = array( 'version' => $version, 'rules' => array() );
		$total_conditions = 0;
		foreach ( $rules as $rule ) {
			if ( ! is_array( $rule ) ) {
				return $this->grapesjs_logic_draft_error();
			}

			$id      = $this->sanitize_grapesjs_logic_draft_identifier( $rule['id'] ?? '' );
			$action  = $this->sanitize_grapesjs_logic_draft_token( $rule['action'] ?? '' );
			$enabled = $rule['enabled'] ?? true;
			if ( is_wp_error( $id ) || is_wp_error( $action ) || ! is_scalar( $enabled ) ) {
				return $this->grapesjs_logic_draft_error();
			}

			$target = $rule['target'] ?? array();
			$groups = $rule['groups'] ?? array();
			if ( ! is_array( $target ) || ! is_array( $groups ) || ! $this->is_grapesjs_logic_list( $groups ) ) {
				return $this->grapesjs_logic_draft_error();
			}
			if ( count( $groups ) > \SUBIN\Forms\ConditionalLogicEvaluator::MAX_GROUPS_PER_RULE ) {
				return $this->grapesjs_logic_draft_error( true );
			}

			$target_type = $this->sanitize_grapesjs_logic_draft_token( $target['type'] ?? '' );
			$target_id   = $this->sanitize_grapesjs_logic_draft_identifier( $target['id'] ?? '' );
			if ( is_wp_error( $target_type ) || is_wp_error( $target_id ) ) {
				return $this->grapesjs_logic_draft_error();
			}

			$clean_rule = array(
				'id'      => $id,
				'enabled' => $this->normalize_grapesjs_logic_draft_boolean( $enabled ),
				'action'  => $action,
				'target'  => array(
					'type' => $target_type,
					'id'   => $target_id,
				),
				'groups'  => array(),
			);

			foreach ( $groups as $group ) {
				if ( ! is_array( $group ) ) {
					return $this->grapesjs_logic_draft_error();
				}

				$match      = $this->sanitize_grapesjs_logic_draft_token( $group['match'] ?? '' );
				$conditions = $group['conditions'] ?? array();
				if ( is_wp_error( $match ) || ! is_array( $conditions ) || ! $this->is_grapesjs_logic_list( $conditions ) ) {
					return $this->grapesjs_logic_draft_error();
				}
				if ( count( $conditions ) > \SUBIN\Forms\ConditionalLogicEvaluator::MAX_CONDITIONS_PER_GROUP ) {
					return $this->grapesjs_logic_draft_error( true );
				}

				$total_conditions += count( $conditions );
				if ( $total_conditions > \SUBIN\Forms\ConditionalLogicEvaluator::MAX_TOTAL_CONDITIONS ) {
					return $this->grapesjs_logic_draft_error( true );
				}

				$clean_group = array( 'match' => $match, 'conditions' => array() );
				foreach ( $conditions as $condition ) {
					if ( ! is_array( $condition ) ) {
						return $this->grapesjs_logic_draft_error();
					}

					$field    = $this->sanitize_grapesjs_logic_draft_identifier( $condition['field'] ?? '' );
					$operator = $this->sanitize_grapesjs_logic_draft_token( $condition['operator'] ?? '' );
					$nodes    = 0;
					$value    = $this->sanitize_grapesjs_logic_draft_value( $condition['value'] ?? '', 0, $nodes );
					if ( is_wp_error( $field ) || is_wp_error( $operator ) ) {
						return $this->grapesjs_logic_draft_error();
					}
					if ( is_wp_error( $value ) ) {
						return $value;
					}

					$clean_group['conditions'][] = array(
						'field'    => $field,
						'operator' => $operator,
						'value'    => $value,
					);
				}
				$clean_rule['groups'][] = $clean_group;
			}
			$clean['rules'][] = $clean_rule;
		}

		return $clean;
	}

	/**
	 * Build a consistent draft schema error.
	 *
	 * @param bool $limits Whether a hard size/count limit was exceeded.
	 * @return \WP_Error
	 */
	private function grapesjs_logic_draft_error( $limits = false ) {
		return new \WP_Error(
			$limits ? 'subin_grapesjs_logic_limits_exceeded' : 'subin_grapesjs_logic_invalid',
			$limits
				? __( 'Conditional logic data exceeds the allowed limits.', 'submission-inbox' )
				: __( 'Conditional logic data is invalid.', 'submission-inbox' )
		);
	}

	/**
	 * Sanitize one possibly empty Logic identifier without truncation.
	 *
	 * @param mixed $value Raw identifier.
	 * @return string|\WP_Error
	 */
	private function sanitize_grapesjs_logic_draft_identifier( $value ) {
		if ( ! is_scalar( $value ) ) {
			return $this->grapesjs_logic_draft_error();
		}

		$value = trim( (string) $value );
		if (
			strlen( $value ) > \SUBIN\Forms\ConditionalLogicEvaluator::MAX_IDENTIFIER_LENGTH ||
			! preg_match( '/^[A-Za-z0-9_.:-]*$/', $value )
		) {
			return $this->grapesjs_logic_draft_error();
		}

		return $value;
	}

	/**
	 * Sanitize one possibly empty Logic enum token without choosing a default.
	 *
	 * @param mixed $value Raw token.
	 * @return string|\WP_Error
	 */
	private function sanitize_grapesjs_logic_draft_token( $value ) {
		if ( ! is_scalar( $value ) ) {
			return $this->grapesjs_logic_draft_error();
		}

		$value = strtolower( trim( (string) $value ) );
		if (
			strlen( $value ) > \SUBIN\Forms\ConditionalLogicEvaluator::MAX_IDENTIFIER_LENGTH ||
			! preg_match( '/^[a-z_]*$/', $value )
		) {
			return $this->grapesjs_logic_draft_error();
		}

		return $value;
	}

	/**
	 * Normalize the serialized boolean forms accepted by the strict evaluator.
	 *
	 * @param mixed $value Raw boolean.
	 * @return bool
	 */
	private function normalize_grapesjs_logic_draft_boolean( $value ) {
		if ( true === $value || 1 === $value ) {
			return true;
		}

		return is_string( $value ) && in_array( strtolower( trim( $value ) ), array( '1', 'true', 'yes', 'on' ), true );
	}

	/**
	 * Preserve a bounded scalar/list condition value.
	 *
	 * @param mixed $value Raw value.
	 * @param int   $depth Current list depth.
	 * @param int   $nodes Total nested nodes.
	 * @return mixed|\WP_Error
	 */
	private function sanitize_grapesjs_logic_draft_value( $value, $depth, &$nodes ) {
		if ( $depth > 10 ) {
			return $this->grapesjs_logic_draft_error( true );
		}

		if ( is_array( $value ) ) {
			if ( ! $this->is_grapesjs_logic_list( $value ) ) {
				return $this->grapesjs_logic_draft_error();
			}
			$clean = array();
			foreach ( $value as $item ) {
				++$nodes;
				if ( $nodes > \SUBIN\Forms\ConditionalLogicEvaluator::MAX_VALUES ) {
					return $this->grapesjs_logic_draft_error( true );
				}
				$clean_item = $this->sanitize_grapesjs_logic_draft_value( $item, $depth + 1, $nodes );
				if ( is_wp_error( $clean_item ) ) {
					return $clean_item;
				}
				$clean[] = $clean_item;
			}
			return $clean;
		}

		if ( null === $value || is_bool( $value ) || is_int( $value ) ) {
			return $value;
		}
		if ( is_float( $value ) ) {
			return is_finite( $value ) ? $value : $this->grapesjs_logic_draft_error();
		}
		if ( ! is_string( $value ) ) {
			return $this->grapesjs_logic_draft_error();
		}
		if ( $this->grapesjs_logic_draft_string_length( $value ) > \SUBIN\Forms\ConditionalLogicEvaluator::MAX_VALUE_LENGTH ) {
			return $this->grapesjs_logic_draft_error( true );
		}

		return $value;
	}

	/**
	 * Count UTF-8 code points consistently with the evaluator and JS runtime.
	 *
	 * @param string $value Value.
	 * @return int
	 */
	private function grapesjs_logic_draft_string_length( $value ) {
		if ( function_exists( 'mb_strlen' ) ) {
			return mb_strlen( $value, 'UTF-8' );
		}

		$length = preg_match_all( '/./us', $value, $characters );
		if ( false !== $length ) {
			return $length;
		}

		return strlen( $value );
	}

	/**
	 * Check whether a decoded JSON value is a zero-indexed list.
	 *
	 * @param array<mixed> $value Value.
	 * @return bool
	 */
	private function is_grapesjs_logic_list( array $value ) {
		$index = 0;
		foreach ( $value as $key => $item ) {
			if ( $key !== $index ) {
				return false;
			}
			++$index;
		}
		return true;
	}

	/**
	 * Sanitize GrapesJS project data while preserving GrapesJS key names.
	 *
	 * @param array<string,mixed> $project Project data.
	 * @return array<string,mixed>|\WP_Error
	 */
	private function sanitize_grapesjs_project( array $project ) {
		$has_logic = array_key_exists( 'subinLogic', $project );
		$logic     = $project['subinLogic'] ?? null;
		unset( $project['subinLogic'] );

		$clean = $this->sanitize_grapesjs_value( $project );
		if ( is_wp_error( $clean ) ) {
			return $clean;
		}

		if ( ! is_array( $clean ) ) {
			return new \WP_Error( 'subin_grapesjs_project_invalid', __( 'Project data is invalid.', 'submission-inbox' ) );
		}

		if ( $has_logic ) {
			if ( ! is_array( $logic ) ) {
				return new \WP_Error(
					'subin_grapesjs_logic_invalid',
					__( 'Conditional logic data is invalid.', 'submission-inbox' )
				);
			}

			$logic_evaluator = new \SUBIN\Forms\ConditionalLogicEvaluator();
			$logic_errors    = $logic_evaluator->validate( $logic );
			if ( ! empty( $logic_errors ) ) {
				return new \WP_Error(
					'subin_grapesjs_logic_invalid',
					sanitize_text_field( $logic_errors[0]['message'] ?? __( 'Conditional logic data is invalid.', 'submission-inbox' ) )
				);
			}

			$clean['subinLogic'] = $logic_evaluator->sanitize( $logic );
		}

		return $this->forms->compact_design_studio_project( $clean );
	}

	/**
	 * Sanitize a GrapesJS project value.
	 *
	 * @param mixed $value Value.
	 * @return mixed|\WP_Error
	 */
	private function sanitize_grapesjs_value( $value ) {
		if ( is_array( $value ) ) {
			if ( $this->is_grapesjs_component_array( $value ) ) {
				return $this->sanitize_grapesjs_component( $value );
			}

			$clean = array();
			foreach ( $value as $key => $item ) {
				$clean_key = is_int( $key ) ? $key : $this->sanitize_grapesjs_project_key( $key );
				if ( '' === $clean_key && ! is_int( $key ) ) {
					continue;
				}

				$clean_item = $this->sanitize_grapesjs_value( $item );
				if ( is_wp_error( $clean_item ) ) {
					return $clean_item;
				}

				$clean[ $clean_key ] = $clean_item;
			}

			return $clean;
		}

		if ( is_bool( $value ) || is_int( $value ) || is_float( $value ) || null === $value ) {
			return $value;
		}

		$value = (string) $value;
		if ( $this->grapesjs_string_is_unsafe( $value ) ) {
			return '';
		}

		return sanitize_text_field( $value );
	}

	/**
	 * Check whether an array looks like a GrapesJS component.
	 *
	 * @param array<mixed> $value Value.
	 * @return bool
	 */
	private function is_grapesjs_component_array( array $value ) {
		return isset( $value['tagName'] ) || isset( $value['type'] ) || isset( $value['attributes'] ) || isset( $value['components'] );
	}

	/**
	 * Sanitize one GrapesJS component.
	 *
	 * @param array<string,mixed> $component Component.
	 * @return array<string,mixed>|\WP_Error
	 */
	private function sanitize_grapesjs_component( array $component ) {
		$type       = sanitize_key( (string) ( $component['type'] ?? '' ) );
		$tag_name   = strtolower( sanitize_key( (string) ( $component['tagName'] ?? '' ) ) );
		$attributes = is_array( $component['attributes'] ?? null ) ? $this->sanitize_grapesjs_attributes( $component['attributes'] ) : array();
		$subin_type   = sanitize_key( (string) ( $attributes['data-subin-component'] ?? $this->grapesjs_component_key_from_type( $type ) ) );

		if ( in_array( $tag_name, array( 'script', 'iframe', 'object', 'embed', 'form' ), true ) ) {
			return new \WP_Error( 'subin_grapesjs_project_unsafe', __( 'Project data contains unsupported markup.', 'submission-inbox' ) );
		}

		if ( in_array( $tag_name, array( 'input', 'select', 'textarea' ), true ) && '' === sanitize_key( $attributes['data-subin-preview-control'] ?? '' ) ) {
			return new \WP_Error( 'subin_grapesjs_project_unsafe', __( 'Project data contains unsupported markup.', 'submission-inbox' ) );
		}

		if ( 'button' === $tag_name && 'submit-button' !== $subin_type ) {
			return new \WP_Error( 'subin_grapesjs_project_unsafe', __( 'Project data contains unsupported markup.', 'submission-inbox' ) );
		}

		if ( in_array( $type, array( 'script', 'iframe', 'object', 'embed', 'form', 'button' ), true ) && 'submit-button' !== $subin_type ) {
			return new \WP_Error( 'subin_grapesjs_project_unsafe', __( 'Project data contains unsupported components.', 'submission-inbox' ) );
		}

		$clean = array();
		foreach ( $component as $key => $value ) {
			$key = $this->sanitize_grapesjs_project_key( $key );
			if ( '' === $key ) {
				continue;
			}

			if ( 'attributes' === $key ) {
				$clean[ $key ] = $attributes;
				continue;
			}

			if ( 'components' === $key ) {
				$children = $this->sanitize_grapesjs_components( $value );
				if ( is_wp_error( $children ) ) {
					return $children;
				}
				$clean[ $key ] = $children;
				continue;
			}

			if ( 'style' === $key && is_array( $value ) ) {
				$clean[ $key ] = $this->sanitize_grapesjs_style_map( $value );
				continue;
			}

			$clean_value = $this->sanitize_grapesjs_value( $value );
			if ( is_wp_error( $clean_value ) ) {
				return $clean_value;
			}
			$clean[ $key ] = $clean_value;
		}

		return $clean;
	}

	/**
	 * Sanitize component children.
	 *
	 * @param mixed $components Components.
	 * @return mixed|\WP_Error
	 */
	private function sanitize_grapesjs_components( $components ) {
		if ( is_string( $components ) ) {
			return wp_kses( $components, $this->grapesjs_allowed_preview_tags(), array() );
		}

		if ( ! is_array( $components ) ) {
			return array();
		}

		$clean = array();
		foreach ( $components as $component ) {
			$clean_component = is_array( $component ) ? $this->sanitize_grapesjs_component( $component ) : $this->sanitize_grapesjs_value( $component );
			if ( is_wp_error( $clean_component ) ) {
				return $clean_component;
			}
			$clean[] = $clean_component;
		}

		return $clean;
	}

	/**
	 * Sanitize GrapesJS component attributes.
	 *
	 * @param array<string,mixed> $attributes Attributes.
	 * @return array<string,string>
	 */
	private function sanitize_grapesjs_attributes( array $attributes ) {
		$clean = array();
		foreach ( $attributes as $key => $value ) {
			$key = strtolower( (string) $key );
			if ( '' === $key || preg_match( '/^on[a-z]/', $key ) ) {
				continue;
			}

			$key = preg_replace( '/[^a-z0-9_:\\.-]/', '', $key );
			if ( '' === $key ) {
				continue;
			}

			if ( 'data-subin-options' === $key ) {
				$value = $this->sanitize_grapesjs_options_attribute( $value );
				if ( '' !== $value ) {
					$clean[ $key ] = $value;
				}
				continue;
			}

			if ( 'data-subin-field-key' === $key ) {
				$value = sanitize_key( (string) $value );
				if ( '' !== $value ) {
					$clean[ $key ] = $value;
				}
				continue;
			}

			if ( in_array( $key, array( 'data-subin-help-text', 'data-subin-consent-text', 'data-subin-default-value', 'data-subin-required-message', 'data-subin-form-description', 'data-subin-validation-error-message', 'data-subin-required-error-message', 'data-subin-spam-error-message', 'data-subin-success-message' ), true ) ) {
				$value = sanitize_textarea_field( (string) $value );
				if ( $this->grapesjs_string_is_unsafe( $value ) ) {
					continue;
				}
				$clean[ $key ] = $value;
				continue;
			}

			if ( 'data-subin-form-title' === $key ) {
				$value = sanitize_text_field( (string) $value );
				if ( $this->grapesjs_string_is_unsafe( $value ) ) {
					continue;
				}
				$clean[ $key ] = $value;
				continue;
			}

			if ( in_array( $key, array( 'data-subin-content', 'data-subin-html-content' ), true ) ) {
				$value = wp_kses( (string) $value, $this->grapesjs_allowed_content_tags(), array( 'http', 'https', 'mailto', 'tel' ) );
				if ( $this->grapesjs_string_is_unsafe( $value ) ) {
					continue;
				}
				$clean[ $key ] = $value;
				continue;
			}

			if ( 'data-subin-bg-image-id' === $key ) {
				$clean[ $key ] = (string) absint( $value );
				continue;
			}

			if ( 'data-subin-bg-image-url' === $key ) {
				$value = esc_url_raw( (string) $value, array( 'http', 'https' ) );
				if ( '' !== $value && ! preg_match( '/^(javascript|data):/i', $value ) && ! preg_match( '/\\.(svg|svgz)(?:[?#].*)?$/i', $value ) && preg_match( '/\\.(jpe?g|png|gif|webp|avif)(?:[?#].*)?$/i', $value ) ) {
					$clean[ $key ] = $value;
				}
				continue;
			}

			if ( 'data-subin-bg-size' === $key ) {
				$value = $this->sanitize_choice( $value, array( 'cover', 'contain', 'auto' ), 'cover' );
				$clean[ $key ] = $value;
				continue;
			}

			if ( 'data-subin-bg-position' === $key ) {
				$value = $this->sanitize_choice( $value, array( 'center', 'top', 'bottom', 'left', 'right' ), 'center' );
				$clean[ $key ] = $value;
				continue;
			}

			if ( 'data-subin-bg-repeat' === $key ) {
				$value = $this->sanitize_choice( $value, array( 'no-repeat', 'repeat', 'repeat-x', 'repeat-y' ), 'no-repeat' );
				$clean[ $key ] = $value;
				continue;
			}

			$value = sanitize_text_field( (string) $value );
			if ( $this->grapesjs_string_is_unsafe( $value ) ) {
				continue;
			}

			$clean[ $key ] = $value;
		}

		return $clean;
	}

	/**
	 * Sanitize a serialized GrapesJS options attribute.
	 *
	 * @param mixed $value Attribute value.
	 * @return string
	 */
	private function sanitize_grapesjs_options_attribute( $value ) {
		$options = $this->grapesjs_options_from_attribute( $value );
		if ( empty( $options ) ) {
			return '';
		}

		$encoded = wp_json_encode( $options );

		return is_string( $encoded ) ? $encoded : '';
	}

	/**
	 * Decode and sanitize choice field options from a GrapesJS attribute.
	 *
	 * @param mixed $value Attribute value.
	 * @return array<int,array{label:string,value:string}>
	 */
	private function grapesjs_options_from_attribute( $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();
		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;
			}

			if ( preg_match( '/^\\s*(javascript|data)\\s*:/i', $raw_value ) ) {
				$raw_value = '';
			}

			$value_key = sanitize_key( '' !== $raw_value ? $raw_value : $label );
			if ( '' === $value_key ) {
				$value_key = 'option_' . ( absint( $index ) + 1 );
			}

			if ( '' === $label ) {
				$label = $value_key;
			}

			$options[] = array(
				'label' => $label,
				'value' => $value_key,
			);
		}

		return $options;
	}

	/**
	 * Sanitize CSS style map values.
	 *
	 * @param array<string,mixed> $style Style declarations.
	 * @return array<string,string>
	 */
	private function sanitize_grapesjs_style_map( array $style ) {
		$clean = array();
		foreach ( $style as $property => $value ) {
			$property = strtolower( preg_replace( '/[^a-z0-9_-]/', '', (string) $property ) );
			$value    = sanitize_text_field( (string) $value );
			if ( '' === $property || $this->grapesjs_string_is_unsafe( $value ) ) {
				continue;
			}

			$clean[ $property ] = $value;
		}

		return $clean;
	}

	/**
	 * Sanitize a GrapesJS project key without forcing lowercase.
	 *
	 * @param mixed $key Key.
	 * @return string
	 */
	private function sanitize_grapesjs_project_key( $key ) {
		return preg_replace( '/[^A-Za-z0-9_\\-:]/', '', (string) $key );
	}

	/**
	 * Detect unsafe strings in project data.
	 *
	 * @param string $value Value.
	 * @return bool
	 */
	private function grapesjs_string_is_unsafe( $value ) {
		return (bool) preg_match( '/(<\\s*\\/?\\s*(script|iframe|object|embed|form|input)\\b|on[a-z]+\\s*=|javascript\\s*:|data\\s*:|expression\\s*\\(|@import|<\\s*\\/?\\s*style\\b)/i', $value );
	}

	/**
	 * Map GrapesJS type names to InboxMend component keys.
	 *
	 * @param string $type Type.
	 * @return string
	 */
	private function grapesjs_component_key_from_type( $type ) {
		$map = array(
			'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',
			'wrapper'           => 'form-wrapper',
		);

		return $map[ sanitize_key( (string) $type ) ] ?? '';
	}

	/**
	 * Get allowed markup for text-like GrapesJS component content.
	 *
	 * @return array<string,array<string,bool>>
	 */
	private function grapesjs_allowed_preview_tags() {
		$global = array(
			'class'                    => true,
			'data-subin-component'       => true,
			'data-subin-preview-control' => true,
			'data-subin-style-id'        => true,
			'aria-hidden'              => true,
		);
		$control = array_merge(
			$global,
			array(
				'disabled'    => true,
				'readonly'    => true,
				'type'        => true,
				'value'       => true,
				'placeholder' => true,
			)
		);

		return array(
			'div'      => $global,
			'p'        => $global,
			'h2'       => $global,
			'label'    => $global,
			'select'   => $control,
			'option'   => $control,
			'input'    => $control,
			'textarea' => $control,
			'span'     => $global,
			'strong'   => $global,
			'em'       => $global,
			'br'       => array(),
			'hr'       => $global,
		);
	}

	/**
	 * Get allowed markup for controlled content block attributes.
	 *
	 * @return array<string,array<string,bool>>
	 */
	private function grapesjs_allowed_content_tags() {
		return array(
			'p'      => array( 'class' => true, 'aria-label' => true ),
			'br'     => array(),
			'strong' => array( 'class' => true ),
			'em'     => array( 'class' => true ),
			'b'      => array( 'class' => true ),
			'i'      => array( 'class' => true ),
			'span'   => array( 'class' => true, 'aria-label' => true ),
			'div'    => array( 'class' => true, 'aria-label' => true ),
			'small'  => array( 'class' => true ),
			'ul'     => array( 'class' => true ),
			'ol'     => array( 'class' => true ),
			'li'     => array( 'class' => true ),
			'a'      => array(
				'href'   => true,
				'title'  => true,
				'target' => true,
				'rel'    => true,
				'class'  => true,
			),
			'hr'     => array( 'class' => true ),
		);
	}

	/**
	 * Render merged Settings tab.
	 *
	 * @param array<string,mixed> $form Form.
	 * @param string              $shortcode Shortcode.
	 * @return void
	 */
	private function render_settings_tab( array $form, $shortcode ) {
		$form_id      = absint( $form['id'] );
		$settings     = $this->forms->decode_settings( $form );
		$notify       = is_array( $settings['notification'] ?? null ) ? $settings['notification'] : array();
		$default_notification_recipient = $this->default_admin_notification_recipient();
		$notification_recipient_mode    = $this->sanitize_notification_recipient_mode( $notify['recipient_mode'] ?? $this->default_notification_recipient_mode_for_form( $form ) );
		$notification_email_value       = 'custom' === $notification_recipient_mode ? sanitize_email( $form['notification_email'] ?? '' ) : $default_notification_recipient;
		$auto_reply_rules   = $this->auto_reply_rules->list_for_form( 'submission_inbox_form', (string) $form_id );
		$auto_reply_summary = $this->summarize_auto_reply_rules( $auto_reply_rules, $form_id );
		?>
		<div class="subin-form-settings">
			<form id="subin-editor-form-settings" method="post" action="<?php echo esc_url( $this->edit_url( $form_id, 'settings' ) ); ?>">
				<input type="hidden" name="subin_editor_action" value="save_settings" />
				<input type="hidden" name="form_id" value="<?php echo esc_attr( (string) $form_id ); ?>" />
				<input type="hidden" name="tab" value="settings" />
				<?php wp_nonce_field( $this->editor_nonce_name( 'save_settings', $form_id ) ); ?>

				<section class="subin-settings-card">
					<header class="subin-settings-card__header">
						<div>
							<h2><?php esc_html_e( 'Publish & shortcode', 'submission-inbox' ); ?></h2>
							<p><?php esc_html_e( 'Copy the shortcode and place this form on any page, post, or widget area that supports shortcodes.', 'submission-inbox' ); ?></p>
						</div>
						<a class="button subin-settings-button" href="<?php echo esc_url( $this->preview_url( $form_id ) ); ?>"><?php esc_html_e( 'Preview form', 'submission-inbox' ); ?></a>
					</header>
					<div class="subin-settings-card__body">
						<div class="subin-settings-field">
							<label><?php esc_html_e( 'Shortcode', 'submission-inbox' ); ?></label>
							<div class="subin-settings-shortcode-row">
								<code class="subin-shortcode-pill"><?php echo esc_html( $shortcode ); ?></code>
								<button type="button" class="button subin-settings-button subin-copy-shortcode" data-subin-copy="<?php echo esc_attr( $shortcode ); ?>"><?php esc_html_e( 'Copy shortcode', 'submission-inbox' ); ?></button>
								<span class="subin-copy-feedback" aria-live="polite"></span>
							</div>
						</div>
						<div class="subin-settings-grid">
							<div class="subin-settings-field">
								<label for="subin-form-status">
									<?php esc_html_e( 'Status', 'submission-inbox' ); ?>
									<?php SUBIN_View::help_tooltip( __( 'Inactive forms are not shown to regular visitors.', 'submission-inbox' ) ); ?>
								</label>
								<select id="subin-form-status" name="status">
									<option value="active" <?php selected( $form['status'] ?? '', 'active' ); ?>><?php esc_html_e( 'Active', 'submission-inbox' ); ?></option>
									<option value="inactive" <?php selected( $form['status'] ?? '', 'inactive' ); ?>><?php esc_html_e( 'Inactive', 'submission-inbox' ); ?></option>
								</select>
							</div>
							<div class="subin-settings-field">
								<label for="subin-form-slug"><?php esc_html_e( 'Slug', 'submission-inbox' ); ?></label>
								<input id="subin-form-slug" type="text" name="slug" value="<?php echo esc_attr( $form['slug'] ?? '' ); ?>" />
							</div>
						</div>
					</div>
				</section>

				<section class="subin-settings-card">
					<header class="subin-settings-card__header">
						<div>
							<h2><?php esc_html_e( 'Lead storage', 'submission-inbox' ); ?></h2>
							<p><?php esc_html_e( 'Choose whether form submissions should be stored in InboxMend.', 'submission-inbox' ); ?></p>
						</div>
					</header>
					<div class="subin-settings-card__body">
						<label class="subin-settings-toggle">
							<input type="checkbox" name="save_leads" value="1" <?php checked( ! empty( $form['save_leads'] ) ); ?> />
							<span></span>
							<strong><?php esc_html_e( 'Save submissions in InboxMend', 'submission-inbox' ); ?></strong>
						</label>
						<p class="subin-settings-help"><?php esc_html_e( 'When enabled, submissions are stored and can be reviewed later.', 'submission-inbox' ); ?></p>
					</div>
				</section>

				<section class="subin-settings-card">
					<header class="subin-settings-card__header">
						<div>
							<h2><?php esc_html_e( 'Admin notification', 'submission-inbox' ); ?></h2>
							<p><?php esc_html_e( 'Send an email to the site owner when a new lead is submitted.', 'submission-inbox' ); ?></p>
						</div>
					</header>
					<div class="subin-settings-card__body">
						<label class="subin-settings-toggle">
							<input type="checkbox" name="notification_enabled" value="1" <?php checked( ! empty( $form['notification_enabled'] ) ); ?> />
							<span></span>
							<strong><?php esc_html_e( 'Send admin notification', 'submission-inbox' ); ?></strong>
						</label>
						<div class="subin-settings-grid">
							<div class="subin-settings-field">
								<label><?php esc_html_e( 'Send to', 'submission-inbox' ); ?></label>
								<label class="subin-settings-radio">
									<input type="radio" name="notification_recipient_mode" value="default" <?php checked( $notification_recipient_mode, 'default' ); ?> data-subin-notification-recipient-mode />
									<span>
										<?php
										if ( is_email( $default_notification_recipient ) ) {
											printf(
												/* translators: %s: default notification recipient email. */
												esc_html__( 'Default recipient: %s', 'submission-inbox' ),
												esc_html( $default_notification_recipient )
											);
										} else {
											esc_html_e( 'Default admin notification recipient is not configured.', 'submission-inbox' );
										}
										?>
									</span>
								</label>
								<label class="subin-settings-radio">
									<input type="radio" name="notification_recipient_mode" value="custom" <?php checked( $notification_recipient_mode, 'custom' ); ?> data-subin-notification-recipient-mode />
									<span><?php esc_html_e( 'Custom recipient for this form', 'submission-inbox' ); ?></span>
								</label>
								<p><?php esc_html_e( 'The default recipient is configured in Settings → Sending.', 'submission-inbox' ); ?></p>
							</div>
						</div>
						<div class="subin-settings-grid">
							<div class="subin-settings-field" data-subin-notification-custom-recipient data-subin-default-notification-recipient="<?php echo esc_attr( $default_notification_recipient ); ?>">
								<label for="subin-notification-email"><?php esc_html_e( 'Recipient email', 'submission-inbox' ); ?></label>
								<input type="hidden" name="notification_email_preserved" value="<?php echo esc_attr( $notification_email_value ); ?>" data-subin-notification-email-preserved />
								<input id="subin-notification-email" type="email" name="notification_email" value="<?php echo esc_attr( $notification_email_value ); ?>" <?php disabled( 'custom' !== $notification_recipient_mode ); ?> data-subin-notification-email />
							</div>
							<div class="subin-settings-field">
								<label for="subin-notification-subject">
									<?php esc_html_e( 'Email subject', 'submission-inbox' ); ?>
									<?php SUBIN_View::help_tooltip( __( 'You can use {form_name}. InboxMend replaces it with this form’s title before sending the admin notification.', 'submission-inbox' ) ); ?>
								</label>
								<input id="subin-notification-subject" type="text" name="notification_subject" value="<?php echo esc_attr( $form['notification_subject'] ?: __( 'New lead from {form_name}', 'submission-inbox' ) ); ?>" />
							</div>
						</div>
						<label class="subin-settings-checkbox">
							<input type="checkbox" name="use_lead_email_reply_to" value="1" <?php checked( ! empty( $notify['use_lead_email_reply_to'] ) ); ?> />
							<span><?php esc_html_e( 'Use submitted email as Reply-To when available', 'submission-inbox' ); ?></span>
						</label>
					</div>
				</section>

				<section class="subin-settings-card">
					<header class="subin-settings-card__header">
						<div>
							<h2><?php esc_html_e( 'Visitor auto-reply', 'submission-inbox' ); ?></h2>
							<p><?php esc_html_e( 'Visitor confirmations are managed from Auto-reply Rules.', 'submission-inbox' ); ?></p>
						</div>
					</header>
					<div class="subin-settings-card__body">
						<?php if ( $auto_reply_summary['total_count'] > 0 ) : ?>
							<div class="subin-auto-reply-status-block">
								<?php $auto_reply_status = $this->auto_reply_summary_status( $auto_reply_summary ); ?>
								<?php SUBIN_View::badge( $auto_reply_status['status_label'], $auto_reply_status['badge_type'] ); ?>
								<dl class="subin-compact-details">
									<div>
										<dt><?php esc_html_e( 'Rules', 'submission-inbox' ); ?></dt>
										<?php /* translators: %1$d: total configured rules, %2$d: active rules. */ ?>
										<dd><?php echo esc_html( sprintf( __( '%1$d configured · %2$d active', 'submission-inbox' ), $auto_reply_summary['total_count'], $auto_reply_summary['active_count'] ) ); ?></dd>
									</div>
									<div>
										<dt><?php esc_html_e( 'Drafts', 'submission-inbox' ); ?></dt>
										<dd><?php echo esc_html( (string) $auto_reply_summary['draft_count'] ); ?></dd>
									</div>
								</dl>
								<?php if ( $auto_reply_summary['attention_count'] > 0 ) : ?>
									<p class="subin-settings-help"><?php esc_html_e( 'At least one active rule has an invalid recipient, content, or condition.', 'submission-inbox' ); ?></p>
								<?php endif; ?>
								<a class="button subin-settings-button" href="<?php echo esc_url( $this->auto_reply_manage_url( $auto_reply_summary, $form_id ) ); ?>"><?php echo esc_html( 1 === absint( $auto_reply_summary['total_count'] ) ? __( 'Edit auto-reply', 'submission-inbox' ) : __( 'Manage auto-replies', 'submission-inbox' ) ); ?></a>
							</div>
						<?php else : ?>
							<div class="subin-auto-reply-status-block">
								<p><?php esc_html_e( 'No auto-reply rule configured for this form.', 'submission-inbox' ); ?></p>
								<a class="button button-primary subin-btn subin-btn-primary" href="<?php echo esc_url( $this->auto_reply_rule_url( 0, $form_id ) ); ?>"><?php esc_html_e( 'Create rule', 'submission-inbox' ); ?></a>
							</div>
						<?php endif; ?>
					</div>
				</section>

				<div class="subin-settings-actions">
					<?php submit_button( __( 'Save settings', 'submission-inbox' ), 'primary', 'submit', false ); ?>
				</div>
			</form>

		</div>
		<?php
	}

	/**
	 * Render selected auto-reply template card.
	 *
	 * @param array<string,mixed>|null $template Template.
	 * @param int                      $template_count Template count.
	 * @param bool                     $disabled Whether controls should be disabled.
	 * @return void
	 */
	private function render_selected_auto_reply_template_card( $template, $template_count, $disabled = false ) {
		if ( ! $template ) {
			?>
			<div class="subin-auto-reply-template-empty<?php echo $disabled ? ' is-disabled' : ''; ?>">
				<h3><?php esc_html_e( 'No template selected', 'submission-inbox' ); ?></h3>
				<p><?php esc_html_e( 'Choose which auto-reply template should be sent after a successful submission.', 'submission-inbox' ); ?></p>
				<?php if ( $template_count > 0 ) : ?>
					<div class="subin-auto-reply-template-empty__actions">
						<button type="button" class="button button-primary" data-subin-open-template-picker <?php disabled( $disabled ); ?>><?php esc_html_e( 'Change template', 'submission-inbox' ); ?></button>
						<?php if ( $disabled ) : ?>
							<span class="button disabled" aria-disabled="true"><?php esc_html_e( 'Edit template', 'submission-inbox' ); ?></span>
						<?php else : ?>
							<a class="button" href="<?php echo esc_url( $this->auto_replies_create_url() ); ?>"><?php esc_html_e( 'Edit template', 'submission-inbox' ); ?></a>
						<?php endif; ?>
					</div>
				<?php else : ?>
					<?php if ( $disabled ) : ?>
						<span class="button button-primary disabled" aria-disabled="true"><?php esc_html_e( 'Change template', 'submission-inbox' ); ?></span>
					<?php else : ?>
						<a class="button button-primary" href="<?php echo esc_url( $this->auto_replies_create_url() ); ?>"><?php esc_html_e( 'Change template', 'submission-inbox' ); ?></a>
					<?php endif; ?>
				<?php endif; ?>
			</div>
			<?php
			return;
		}

		$template_id = absint( $template['id'] ?? 0 );
		?>
		<div class="subin-auto-reply-template-selected<?php echo $disabled ? ' is-disabled' : ''; ?>" data-subin-selected-template-card="<?php echo esc_attr( (string) $template_id ); ?>">
			<div class="subin-auto-reply-template-selected__preview">
				<?php $this->render_auto_reply_template_mini_preview( $template ); ?>
			</div>
			<div class="subin-auto-reply-template-selected__details">
				<div class="subin-auto-reply-template-selected__top">
					<div>
						<h3><?php echo esc_html( $template['name'] ?? __( 'Auto-reply template', 'submission-inbox' ) ); ?></h3>
					</div>
					<div class="subin-auto-reply-template-selected__actions">
						<button type="button" class="button" data-subin-open-template-picker <?php disabled( $disabled ); ?>><?php esc_html_e( 'Change template', 'submission-inbox' ); ?></button>
						<a class="button<?php echo $disabled ? ' disabled' : ''; ?>" href="<?php echo esc_url( $this->auto_reply_edit_url( $template_id ) ); ?>" data-subin-auto-reply-link-control <?php echo $disabled ? 'aria-disabled="true" tabindex="-1"' : ''; ?>><?php esc_html_e( 'Edit template', 'submission-inbox' ); ?></a>
					</div>
				</div>
				<dl class="subin-template-meta-list">
					<dt><?php esc_html_e( 'Subject', 'submission-inbox' ); ?></dt>
					<dd><?php echo esc_html( $template['subject'] ?? __( 'We received your request', 'submission-inbox' ) ); ?></dd>
					<dt><?php esc_html_e( 'Preheader', 'submission-inbox' ); ?></dt>
					<dd><?php echo esc_html( $template['preheader'] ?? __( 'Not set', 'submission-inbox' ) ); ?></dd>
					<dt><?php esc_html_e( 'Used for', 'submission-inbox' ); ?></dt>
					<dd><?php esc_html_e( 'Visitor confirmation after successful submission', 'submission-inbox' ); ?></dd>
					<dt><?php esc_html_e( 'Variables', 'submission-inbox' ); ?></dt>
					<dd><?php esc_html_e( 'Supports form and submission variables.', 'submission-inbox' ); ?></dd>
				</dl>
			</div>
		</div>
		<?php
	}

	/**
	 * Render auto-reply picker modal.
	 *
	 * @param array<int,array<string,mixed>> $templates Templates.
	 * @return void
	 */
	private function render_auto_reply_template_picker_modal( array $templates ) {
		?>
		<div class="subin-template-picker-modal" data-subin-template-picker-modal hidden>
			<div class="subin-template-picker-modal__backdrop" data-subin-close-template-picker></div>
			<div class="subin-template-picker-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="subin-template-picker-title">
				<header class="subin-template-picker-modal__header">
					<div>
						<h2 id="subin-template-picker-title"><?php esc_html_e( 'Choose auto-reply template', 'submission-inbox' ); ?></h2>
						<p><?php esc_html_e( 'Preview the email before attaching it to this form.', 'submission-inbox' ); ?></p>
					</div>
					<div class="subin-template-picker-modal__header-actions">
						<a class="button" href="<?php echo esc_url( $this->auto_replies_create_url() ); ?>"><?php esc_html_e( 'Create template', 'submission-inbox' ); ?></a>
						<button type="button" class="button-link" data-subin-close-template-picker><?php esc_html_e( 'Close', 'submission-inbox' ); ?></button>
					</div>
				</header>
				<div class="subin-template-picker-modal__grid">
					<?php if ( empty( $templates ) ) : ?>
						<div class="subin-template-picker-modal__empty">
							<p><?php esc_html_e( 'No auto-reply templates are available yet.', 'submission-inbox' ); ?></p>
							<a class="button button-primary" href="<?php echo esc_url( $this->auto_replies_create_url() ); ?>"><?php esc_html_e( 'Create template', 'submission-inbox' ); ?></a>
						</div>
					<?php else : ?>
						<?php foreach ( $templates as $template ) : ?>
							<?php $this->render_auto_reply_template_picker_card( $template ); ?>
						<?php endforeach; ?>
					<?php endif; ?>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Render one picker card.
	 *
	 * @param array<string,mixed> $template Template.
	 * @return void
	 */
	private function render_auto_reply_template_picker_card( array $template ) {
		$template_id = absint( $template['id'] ?? 0 );
		?>
		<article class="subin-template-picker-card" data-subin-template-picker-card="<?php echo esc_attr( (string) $template_id ); ?>">
			<?php $this->render_auto_reply_template_mini_preview( $template ); ?>
			<div class="subin-template-picker-card__body">
				<div class="subin-template-picker-card__title">
					<h3><?php echo esc_html( $template['name'] ?? __( 'Auto-reply template', 'submission-inbox' ) ); ?></h3>
				</div>
				<p><?php echo esc_html( $template['preheader'] ?? __( 'Confirmation email template.', 'submission-inbox' ) ); ?></p>
				<div class="subin-template-picker-card__subject"><?php echo esc_html( $template['subject'] ?? __( 'We received your request', 'submission-inbox' ) ); ?></div>
				<button type="button" class="button button-primary" data-subin-use-template="<?php echo esc_attr( (string) $template_id ); ?>"><?php esc_html_e( 'Use this template', 'submission-inbox' ); ?></button>
			</div>
		</article>
		<?php
	}

	/**
	 * Render safe compact email preview.
	 *
	 * @param array<string,mixed> $template Template.
	 * @return void
	 */
	private function render_auto_reply_template_mini_preview( array $template ) {
		$srcdoc = $this->auto_reply_template_preview_srcdoc( $template );
		if ( '' !== $srcdoc ) {
			?>
			<div class="subin-template-mini-preview">
				<iframe
					<?php /* translators: %s: auto-reply template name. */ ?>
					title="<?php echo esc_attr( sprintf( __( 'Preview: %s', 'submission-inbox' ), $template['name'] ?? __( 'Auto-reply template', 'submission-inbox' ) ) ); ?>"
					sandbox=""
					loading="lazy"
					scrolling="no"
					srcdoc="<?php echo esc_attr( $srcdoc ); ?>"
				></iframe>
			</div>
			<?php
			return;
		}

		?>
		<div class="subin-template-mini-preview subin-template-mini-preview--fallback" aria-hidden="true">
			<div class="subin-template-mini-preview__hero"></div>
			<div class="subin-template-mini-preview__card">
				<span></span>
				<span></span>
				<span></span>
			</div>
			<div class="subin-template-mini-preview__subject"><?php echo esc_html( wp_trim_words( (string) ( $template['subject'] ?? __( 'We received your request', 'submission-inbox' ) ), 7 ) ); ?></div>
		</div>
		<?php
	}

	/**
	 * Build sandboxed srcdoc for a real auto-reply preview.
	 *
	 * @param array<string,mixed> $template Template.
	 * @return string
	 */
	private function auto_reply_template_preview_srcdoc( array $template ) {
		$html = \SUBIN\AutoReplies\AutoReplyTemplateRepository::effective_body( $template );
		$css  = (string) ( $template['rendered_css'] ?? '' );

		if ( '' === trim( $html ) ) {
			return '';
		}

		$html = wp_kses(
			$html,
			\SUBIN\AutoReplies\AutoReplyTemplateRepository::allowed_email_html(),
			array( 'http', 'https', 'mailto', 'tel' )
		);

		if ( '' === trim( $html ) ) {
			return '';
		}

		$css = wp_strip_all_tags( $css );
		$css = preg_replace( '/@import[^;]+;/i', '', $css );
		$css = preg_replace( '/(?:expression|javascript|data|behavior|-moz-binding)\s*[:(][^;}]+[;}]?/i', '', (string) $css );

		$preview_css = '
			html,body{margin:0;padding:0;background:#eef3f8;overflow:hidden;}
			body{width:620px;min-height:420px;font-family:Arial,sans-serif;transform-origin:top left;}
			a{pointer-events:none;}
			*{box-sizing:border-box;}
		';

		return '<!doctype html><html><head><meta charset="utf-8"><base target="_blank"><style>' . esc_html( $preview_css . "\n" . $css ) . '</style></head><body>' . $html . '</body></html>';
	}

	/**
	 * Find template in a list.
	 *
	 * @param array<int,array<string,mixed>> $templates Templates.
	 * @param int                           $template_id Template id.
	 * @return array<string,mixed>|null
	 */
	private function find_auto_reply_template( array $templates, $template_id ) {
		$template_id = absint( $template_id );
		foreach ( $templates as $template ) {
			if ( absint( $template['id'] ?? 0 ) === $template_id ) {
				return $template;
			}
		}

		return null;
	}

	/**
	 * Build auto-reply edit URL.
	 *
	 * @param int $template_id Template id.
	 * @return string
	 */
	private function auto_reply_edit_url( $template_id ) {
		return add_query_arg(
			array(
				'page'                 => 'submission-inbox-auto-replies',
				'subin_auto_replies_tab' => 'templates',
				'view'                 => 'edit',
				'id'                   => absint( $template_id ),
			),
			admin_url( 'admin.php' )
		);
	}

	/**
	 * Build auto-reply rule URL.
	 *
	 * @param int $rule_id Rule id.
	 * @param int $form_id Form id.
	 * @return string
	 */
	private function auto_reply_rule_url( $rule_id, $form_id ) {
		$args = array(
			'page'                 => 'submission-inbox-auto-replies',
			'subin_auto_replies_tab' => 'rules',
			'rule_view'            => $rule_id > 0 ? 'edit' : 'new',
			'provider'             => 'submission_inbox_form',
			'form_id'              => absint( $form_id ),
		);
		if ( $rule_id > 0 ) {
			$args['id'] = absint( $rule_id );
		}

		return add_query_arg( $args, admin_url( 'admin.php' ) );
	}

	/** Build the Auto-replies list URL scoped to one built-in form. */
	private function auto_reply_rules_url( $form_id ) {
		return add_query_arg(
			array(
				'page'                   => 'submission-inbox-auto-replies',
				'subin_auto_replies_tab' => 'rules',
				'provider'               => 'submission_inbox_form',
				'form_id'                => absint( $form_id ),
			),
			admin_url( 'admin.php' )
		);
	}

	/**
	 * Summarize all policies for the compact Forms settings card.
	 *
	 * @param array<int,array<string,mixed>> $rules Rules in delivery order.
	 * @param int                            $form_id Built-in form id.
	 * @return array<string,int>
	 */
	private function summarize_auto_reply_rules( array $rules, $form_id ) {
		$summary = array(
			'total_count'     => count( $rules ),
			'active_count'    => 0,
			'draft_count'     => 0,
			'attention_count' => 0,
			'single_rule_id'  => 1 === count( $rules ) ? absint( $rules[0]['id'] ?? 0 ) : 0,
		);
		$schema_fields = $this->auto_reply_providers->list_fields( 'submission_inbox_form', (string) absint( $form_id ) );
		$template_ids = array();
		foreach ( $rules as $rule ) {
			if ( absint( $rule['template_id'] ?? 0 ) > 0 ) {
				$template_ids[] = absint( $rule['template_id'] );
			}
		}
		$templates = $this->auto_reply_templates->get_by_ids( $template_ids );
		$readiness = new \SUBIN\AutoReplies\AutoReplyRuleReadiness();

		foreach ( $rules as $rule ) {
			$status = sanitize_key( $rule['status'] ?? \SUBIN\AutoReplies\AutoReplyRuleRepository::STATUS_PUBLISHED );
			if ( \SUBIN\AutoReplies\AutoReplyRuleRepository::STATUS_DRAFT === $status ) {
				++$summary['draft_count'];
			}
			if ( \SUBIN\AutoReplies\AutoReplyRuleRepository::STATUS_PUBLISHED !== $status || empty( $rule['enabled'] ) ) {
				continue;
			}

			++$summary['active_count'];
			$template = $templates[ absint( $rule['template_id'] ?? 0 ) ] ?? null;
			$analysis = $readiness->analyze( $rule, $schema_fields, true, $template );
			if ( empty( $analysis['ready'] ) ) {
				++$summary['attention_count'];
			}
		}

		return $summary;
	}

	/**
	 * Build the compact Forms status from the effective readiness of all rules.
	 *
	 * @param array<string,int> $summary Auto-reply rule counts.
	 * @return array{status_label:string,badge_type:string,text:string}
	 */
	private function auto_reply_summary_status( array $summary ) {
		$total     = absint( $summary['total_count'] ?? 0 );
		$active    = absint( $summary['active_count'] ?? 0 );
		$drafts    = absint( $summary['draft_count'] ?? 0 );
		$attention = absint( $summary['attention_count'] ?? 0 );

		if ( 0 === $total ) {
			return array(
				'status_label' => __( 'Off', 'submission-inbox' ),
				'badge_type'   => 'muted',
				'text'         => __( 'No auto-reply rule is configured for this form.', 'submission-inbox' ),
			);
		}

		if ( $attention > 0 ) {
			return array(
				'status_label' => __( 'Needs attention', 'submission-inbox' ),
				'badge_type'   => 'warning',
				'text'         => sprintf(
					/* translators: %d: Number of active auto-reply rules that need attention. */
					_n( '%d active auto-reply needs attention.', '%d active auto-replies need attention.', $attention, 'submission-inbox' ),
					$attention
				),
			);
		}

		if ( $active > 0 ) {
			return array(
				'status_label' => __( 'Active', 'submission-inbox' ),
				'badge_type'   => 'success',
				'text'         => sprintf(
					/* translators: %d: Number of active auto-reply rules. */
					_n( '%d auto-reply is active.', '%d auto-replies are active.', $active, 'submission-inbox' ),
					$active
				),
			);
		}

		if ( $drafts > 0 ) {
			return array(
				'status_label' => __( 'Draft', 'submission-inbox' ),
				'badge_type'   => 'warning',
				'text'         => sprintf(
					/* translators: %d: Number of draft auto-reply rules. */
					_n( '%d draft is not sending yet.', '%d drafts are not sending yet.', $drafts, 'submission-inbox' ),
					$drafts
				),
			);
		}

		return array(
			'status_label' => __( 'Disabled', 'submission-inbox' ),
			'badge_type'   => 'muted',
			'text'         => __( 'Configured auto-replies are not currently sending.', 'submission-inbox' ),
		);
	}

	/**
	 * Open a lone rule directly; keep multiple rules in their delivery-order list.
	 *
	 * @param array<string,int> $summary Auto-reply rule counts.
	 * @param int               $form_id Built-in form id.
	 * @return string
	 */
	private function auto_reply_manage_url( array $summary, $form_id ) {
		$rule_id = 1 === absint( $summary['total_count'] ?? 0 ) ? absint( $summary['single_rule_id'] ?? 0 ) : 0;
		if ( $rule_id > 0 ) {
			return $this->auto_reply_rule_url( $rule_id, $form_id );
		}

		return $this->auto_reply_rules_url( $form_id );
	}

	/**
	 * Build auto-reply create URL.
	 *
	 * @return string
	 */
	private function auto_replies_create_url() {
		return add_query_arg(
			array(
				'page'                 => 'submission-inbox-auto-replies',
				'subin_auto_replies_tab' => 'templates',
				'view'                 => 'new',
			),
			admin_url( 'admin.php' )
		);
	}

	/**
	 * Render a shortcode copy block.
	 *
	 * @param string $shortcode Shortcode.
	 * @param string $title Title.
	 * @param string $description Description.
	 * @return void
	 */
	private function render_shortcode_box( $shortcode, $title, $description = '' ) {
		?>
		<div class="subin-shortcode-box">
			<div class="subin-shortcode-box__header">
				<h3><?php echo esc_html( $title ); ?></h3>
				<?php if ( '' !== $description ) : ?>
					<p><?php echo esc_html( $description ); ?></p>
				<?php endif; ?>
			</div>
			<div class="subin-shortcode-box__copy">
				<code class="subin-shortcode-pill"><?php echo esc_html( $shortcode ); ?></code>
				<button type="button" class="button button-primary subin-shortcode-copy subin-copy-shortcode" data-subin-copy="<?php echo esc_attr( $shortcode ); ?>"><?php esc_html_e( 'Copy shortcode', 'submission-inbox' ); ?></button>
				<span class="subin-copy-feedback" aria-live="polite"></span>
			</div>
		</div>
		<?php
	}

	/**
	 * Get preview options from a saved field.
	 *
	 * @param array<string,mixed> $field Field.
	 * @return array<int,array{label:string,value:string}>
	 */
	private function get_preview_options( array $field ) {
		if ( empty( $field['field_options'] ) || ! is_string( $field['field_options'] ) ) {
			return array();
		}

		$decoded = json_decode( $field['field_options'], true );
		if ( ! is_array( $decoded ) ) {
			return array();
		}

		$options = array();
		foreach ( $decoded as $option ) {
			if ( ! is_array( $option ) ) {
				continue;
			}

			$label = sanitize_text_field( $option['label'] ?? '' );
			$value = sanitize_key( $option['value'] ?? $label );
			if ( '' === $label || '' === $value ) {
				continue;
			}

			$options[] = array(
				'label' => $label,
				'value' => $value,
			);
		}

		return $options;
	}

	/**
	 * Get email fields for auto-reply recipient choices.
	 *
	 * @param int $form_id Form id.
	 * @return array<int,array{id:int,key:string,label:string}>
	 */
	private function email_field_choices( $form_id ) {
		$choices = array();
		foreach ( $this->fields->get_by_form_id( $form_id ) as $field ) {
			if ( 'email' !== sanitize_key( $field['field_type'] ?? '' ) ) {
				continue;
			}

			$key = sanitize_key( $field['field_key'] ?? '' );
			if ( '' === $key ) {
				continue;
			}

			$label = sanitize_text_field( $field['field_label'] ?? '' );
			$choices[] = array(
				'id'    => absint( $field['id'] ?? 0 ),
				'key'   => $key,
				'label' => '' !== $label ? $label : $key,
			);
		}

		return $choices;
	}

	/**
	 * Normalize saved auto-reply recipient selection for the editor.
	 *
	 * @param array<int,array{id:int,key:string,label:string}> $email_fields Real email field choices.
	 * @param mixed                                     $selected Selected key or legacy id.
	 * @return string
	 */
	private function normalize_auto_reply_recipient_field_key( array $email_fields, $selected ) {
		if ( empty( $email_fields ) ) {
			return '';
		}

		$selected_key = sanitize_key( (string) $selected );
		$selected_id  = absint( $selected );
		foreach ( $email_fields as $field ) {
			if ( $selected_key === $field['key'] || ( $selected_id > 0 && $selected_id === absint( $field['id'] ?? 0 ) ) ) {
				return $field['key'];
			}
		}

		if ( '' !== $selected_key || 1 === count( $email_fields ) ) {
			return $email_fields[0]['key'];
		}

		return '';
	}

	/**
	 * Format date.
	 *
	 * @param string $date Date.
	 * @return string
	 */
	private function format_date( $date ) {
		return $date ? mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $date ) : '';
	}

	/**
	 * Format template key.
	 *
	 * @param string $template_key Template key.
	 * @return string
	 */
	private function format_template_label( $template_key ) {
		$template = $this->templates->get_template( sanitize_key( $template_key ) );

		return $template ? (string) $template['title'] : __( 'Custom', 'submission-inbox' );
	}

	/**
	 * Template description for chooser cards.
	 *
	 * @param string              $template_key Template key.
	 * @param array<string,mixed> $template Template.
	 * @return string
	 */
	private function template_description( $template_key, array $template ) {
		$descriptions = array(
			'contact_us'       => __( 'A simple form for general messages.', 'submission-inbox' ),
			'request_quote'    => __( 'Collect project or service requests with helpful details.', 'submission-inbox' ),
			'callback_request' => __( 'Let visitors request a phone call.', 'submission-inbox' ),
			'support_request'  => __( 'Collect support questions with a subject and message.', 'submission-inbox' ),
			'booking_inquiry'  => __( 'Collect booking requests or appointment inquiries.', 'submission-inbox' ),
			'job_application'  => __( 'Collect basic job application details without file uploads.', 'submission-inbox' ),
		);

		return $descriptions[ sanitize_key( $template_key ) ] ?? sanitize_text_field( $template['header_description'] ?? '' );
	}

	/**
	 * Recommended use text for a template.
	 *
	 * @param string $template_key Template key.
	 * @return string
	 */
	private function template_recommended_use( $template_key ) {
		$uses = array(
			'contact_us'       => __( 'General inquiries', 'submission-inbox' ),
			'request_quote'    => __( 'Service requests', 'submission-inbox' ),
			'callback_request' => __( 'Phone follow-up', 'submission-inbox' ),
			'support_request'  => __( 'Support intake', 'submission-inbox' ),
			'booking_inquiry'  => __( 'Booking requests', 'submission-inbox' ),
			'job_application'  => __( 'Hiring inquiries', 'submission-inbox' ),
		);

		return $uses[ sanitize_key( $template_key ) ] ?? __( 'Custom form', 'submission-inbox' );
	}

	/**
	 * Template category label.
	 *
	 * @param string              $template_key Template key.
	 * @param array<string,mixed> $template Template.
	 * @return string
	 */
	private function template_category( $template_key, array $template ) {
		if ( ! empty( $template['category'] ) ) {
			return sanitize_text_field( $template['category'] );
		}

		return $this->template_recommended_use( $template_key );
	}

	/**
	 * Get readable template field labels.
	 *
	 * @param array<string,mixed> $template Template.
	 * @return array<int,string>
	 */
	private function template_field_labels( array $template ) {
		$fields = is_array( $template['fields'] ?? null ) ? $template['fields'] : array();
		$labels = array();

		foreach ( $fields as $field ) {
			if ( ! empty( $field['field_label'] ) ) {
				$labels[] = sanitize_text_field( $field['field_label'] );
			}
		}

		return $labels;
	}

	/**
	 * Format template field count.
	 *
	 * @param int $count Field count.
	 * @return string
	 */
	private function format_template_field_count( $count ) {
		$count = absint( $count );

		return sprintf(
			/* translators: %d: Field count. */
			_n( '%d field', '%d fields', $count, 'submission-inbox' ),
			$count
		);
	}

	/**
	 * Render a real, non-submitting form template preview.
	 *
	 * @param array<string,mixed> $template Template.
	 * @param bool                $large Whether to render the larger version.
	 * @return void
	 */
	private function render_template_visual_preview( array $template, $large = false ) {
		$renderer = new \SUBIN\Forms\FormRenderer();
		$form     = $this->template_preview_form( $template );
		$fields   = $this->template_preview_fields( $template );
		?>
		<div class="subin-template-preview subin-template-preview--real<?php echo $large ? ' subin-template-preview--large' : ''; ?>" aria-hidden="true">
			<div class="subin-template-preview__viewport">
				<div class="subin-template-preview__scale">
					<?php echo $renderer->render( $form, $fields, array( 'preview' => true ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FormRenderer escapes fields and outputs sanitized preview markup. ?>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Build a temporary form row for template preview rendering.
	 *
	 * @param array<string,mixed> $template Template.
	 * @return array<string,mixed>
	 */
	private function template_preview_form( array $template ) {
		$seed = absint( sprintf( '%u', crc32( (string) ( $template['title'] ?? 'template' ) ) ) );
		$form_id = 900000 + ( $seed % 90000 );

		return array(
			'id'                 => $form_id,
			'title'              => sanitize_text_field( $template['title'] ?? __( 'Form template', 'submission-inbox' ) ),
			'slug'               => sanitize_title( $template['title'] ?? 'form-template' ),
			'header_title'       => sanitize_text_field( $template['header_title'] ?? $template['title'] ?? '' ),
			'header_description' => sanitize_textarea_field( $template['header_description'] ?? '' ),
			'footer_note'        => sanitize_textarea_field( $template['footer_note'] ?? '' ),
			'success_message'    => sanitize_textarea_field( $template['success_message'] ?? '' ),
			'submit_button_text' => sanitize_text_field( $template['submit_button_text'] ?? __( 'Send', 'submission-inbox' ) ),
			'layout_template'    => \SUBIN\Forms\FormRepository::LAYOUT_CLEAN_CARD,
			'style_preset'       => \SUBIN\Forms\FormRepository::STYLE_DEFAULT,
			'settings_json'      => '',
		);
	}

	/**
	 * Build temporary field rows for template preview rendering.
	 *
	 * @param array<string,mixed> $template Template.
	 * @return array<int,array<string,mixed>>
	 */
	private function template_preview_fields( array $template ) {
		$fields = is_array( $template['fields'] ?? null ) ? $template['fields'] : array();
		$rows   = array();
		$order  = 0;

		foreach ( $fields as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}

			++$order;
			$options = $field['field_options'] ?? array();
			if ( is_array( $options ) ) {
				$options = wp_json_encode( $options );
				$options = is_string( $options ) ? $options : '';
			}

			$rows[] = array(
				'id'            => $order,
				'field_key'     => sanitize_key( $field['field_key'] ?? 'field_' . $order ),
				'field_type'    => sanitize_key( $field['field_type'] ?? 'text' ),
				'field_label'   => sanitize_text_field( $field['field_label'] ?? '' ),
				'placeholder'   => sanitize_text_field( $field['placeholder'] ?? '' ),
				'help_text'     => sanitize_textarea_field( $field['help_text'] ?? '' ),
				'is_required'   => ! empty( $field['is_required'] ) ? 1 : 0,
				'field_options' => $options,
				'sort_order'    => $order,
			);
		}

		return $rows;
	}

	/**
	 * Render Use Template form.
	 *
	 * @param string $template_key Template key.
	 * @return void
	 */
	private function render_use_template_form( $template_key ) {
		$template_key = sanitize_key( $template_key );
		?>
		<form class="subin-template-use-form" method="post" action="<?php echo esc_url( $this->templates_url() ); ?>">
			<input type="hidden" name="subin_editor_action" value="use_template" />
			<input type="hidden" name="template_key" value="<?php echo esc_attr( $template_key ); ?>" />
			<?php wp_nonce_field( 'subin_use_template_' . $template_key ); ?>
			<button type="submit" class="button button-primary subin-btn subin-btn-primary"><?php esc_html_e( 'Use Template', 'submission-inbox' ); ?></button>
		</form>
		<?php
	}

	/**
	 * Sanitize an allowlisted choice.
	 *
	 * @param mixed             $value Value.
	 * @param array<int,string> $allowed Allowed values.
	 * @param string            $fallback Fallback.
	 * @return string
	 */
	private function sanitize_choice( $value, array $allowed, $fallback ) {
		$value = sanitize_key( (string) $value );
		return in_array( $value, $allowed, true ) ? $value : $fallback;
	}

	/**
	 * Render built-in forms table.
	 *
	 * @param array<int,array<string,mixed>> $forms Forms.
	 * @return void
	 */
	private function render_forms_table( array $forms ) {
		$plugin_settings = $this->settings->all();
		?>
		<div class="subin-form-list">
			<?php foreach ( $forms as $form ) : ?>
				<?php
				$form_id       = absint( $form['id'] ?? 0 );
				$fields        = $this->fields->get_by_form_id( $form_id );
				$settings      = $this->forms->decode_settings( $form );
				$auto_reply_rules = $this->auto_reply_rules->list_for_form( 'submission_inbox_form', (string) $form_id );
				$auto_reply_summary = $this->summarize_auto_reply_rules( $auto_reply_rules, $form_id );
				$auto_reply_status  = $this->auto_reply_summary_status( $auto_reply_summary );
				$indicators    = $this->form_setup_indicators( $form, $fields, $settings, $plugin_settings );
				$status        = sanitize_key( $form['status'] ?? '' );
				$is_active     = \SUBIN\Forms\FormRepository::STATUS_ACTIVE === $status;
				$shortcode     = '[submission_inbox_form id="' . $form_id . '"]';
				$last_edited   = ! empty( $form['updated_at'] ) ? $form['updated_at'] : ( $form['created_at'] ?? '' );
				$field_count   = count( $fields );
				$email_found   = $this->form_has_email_field( $fields );
				$save_leads    = ! empty( $form['save_leads'] );
				?>
				<div class="subin-form-card<?php echo $is_active ? ' subin-form-card--active' : ' subin-form-card--inactive'; ?>">
					<div class="subin-form-main">
						<div class="subin-card-top">
							<span class="subin-card-top__icon dashicons dashicons-feedback" aria-hidden="true"></span>
							<div class="subin-card-top__content">
								<div class="subin-title-line">
									<h3 class="subin-form-card__title"><?php echo esc_html( $form['title'] ?? '' ); ?></h3>
									<?php SUBIN_View::badge( $is_active ? __( 'Active', 'submission-inbox' ) : __( 'Inactive', 'submission-inbox' ), $is_active ? 'success' : 'muted' ); ?>
								</div>
								<div class="subin-meta">
									<span><?php echo esc_html( $form['slug'] ?? '' ); ?></span>
									<?php if ( $last_edited ) : ?>
										<span><?php esc_html_e( 'Last edited', 'submission-inbox' ); ?>: <?php echo esc_html( $this->format_date( $last_edited ) ); ?></span>
									<?php endif; ?>
								</div>
							</div>
							<a class="button button-primary subin-btn subin-btn-primary" href="<?php echo esc_url( $this->edit_url( $form_id ) ); ?>"><?php esc_html_e( 'Edit in Design Studio', 'submission-inbox' ); ?></a>
						</div>

						<div class="subin-shortcode-row">
							<span class="subin-shortcode-label"><?php esc_html_e( 'Shortcode', 'submission-inbox' ); ?></span>
							<span class="subin-shortcode subin-shortcode-pill"><?php echo esc_html( $shortcode ); ?></span>
							<?php SUBIN_View::help_tooltip( __( 'Copy this shortcode and paste it into a page or post to display the form.', 'submission-inbox' ) ); ?>
							<button type="button" class="button button-small subin-shortcode-copy subin-copy-shortcode" data-subin-copy="<?php echo esc_attr( $shortcode ); ?>"><?php esc_html_e( 'Copy', 'submission-inbox' ); ?></button>
							<span class="subin-copy-feedback" aria-live="polite"></span>
						</div>

						<div class="subin-card-grid">
							<section class="subin-panel">
								<h4 class="subin-panel-title"><?php esc_html_e( 'Form setup', 'submission-inbox' ); ?></h4>
								<div class="subin-setup-list">
									<?php foreach ( $indicators as $indicator ) : ?>
										<div class="subin-setup-row subin-setup-row-<?php echo esc_attr( $indicator['type'] ); ?>">
											<span class="subin-dot" aria-hidden="true"></span>
											<div>
												<strong>
													<?php echo esc_html( $indicator['label'] ); ?>
													<?php if ( ! empty( $indicator['help'] ) ) : ?>
														<?php SUBIN_View::help_tooltip( $indicator['help'] ); ?>
													<?php endif; ?>
												</strong>
												<span><?php echo esc_html( $indicator['text'] ); ?></span>
											</div>
										</div>
									<?php endforeach; ?>
								</div>
							</section>

							<section class="subin-panel subin-autoreply">
								<div class="subin-panel-title-row">
									<h4 class="subin-panel-title"><?php esc_html_e( 'Visitor auto-reply', 'submission-inbox' ); ?></h4>
									<?php SUBIN_View::badge( $auto_reply_status['status_label'], $auto_reply_status['badge_type'] ); ?>
								</div>
								<p><?php echo esc_html( $auto_reply_status['text'] ); ?></p>
								<?php if ( $auto_reply_summary['total_count'] > 0 ) : ?>
									<a class="subin-action-chip" href="<?php echo esc_url( $this->auto_reply_manage_url( $auto_reply_summary, $form_id ) ); ?>"><?php echo esc_html( 1 === absint( $auto_reply_summary['total_count'] ) ? __( 'Edit auto-reply', 'submission-inbox' ) : __( 'Manage auto-replies', 'submission-inbox' ) ); ?></a>
								<?php else : ?>
									<a class="subin-action-chip" href="<?php echo esc_url( $this->auto_reply_rule_url( 0, $form_id ) ); ?>"><?php esc_html_e( 'Create auto-reply', 'submission-inbox' ); ?></a>
								<?php endif; ?>
							</section>
						</div>

						<div class="subin-info-row">
							<span class="subin-info-pill"><strong><?php esc_html_e( 'Fields', 'submission-inbox' ); ?>:</strong> <?php echo esc_html( (string) $field_count ); ?></span>
							<span class="subin-info-pill"><strong><?php esc_html_e( 'Email field', 'submission-inbox' ); ?>:</strong> <?php echo esc_html( $email_found ? __( 'Found', 'submission-inbox' ) : __( 'Missing', 'submission-inbox' ) ); ?></span>
							<span class="subin-info-pill"><strong><?php esc_html_e( 'Save leads', 'submission-inbox' ); ?>:</strong> <?php echo esc_html( $save_leads ? __( 'Enabled', 'submission-inbox' ) : __( 'Disabled', 'submission-inbox' ) ); ?></span>
						</div>

						<div class="subin-card-actions">
							<a class="subin-action-chip" href="<?php echo esc_url( $this->edit_url( $form_id, 'settings' ) ); ?>"><?php esc_html_e( 'Settings', 'submission-inbox' ); ?></a>
							<a class="subin-action-chip" href="<?php echo esc_url( $this->action_url( 'duplicate', $form_id ) ); ?>"><?php esc_html_e( 'Duplicate', 'submission-inbox' ); ?></a>
							<?php if ( $is_active ) : ?>
								<a class="subin-action-chip subin-action-chip-warning" href="<?php echo esc_url( $this->action_url( 'deactivate', $form_id ) ); ?>"><?php esc_html_e( 'Deactivate', 'submission-inbox' ); ?></a>
							<?php else : ?>
								<a class="subin-action-chip subin-action-chip-warning" href="<?php echo esc_url( $this->action_url( 'activate', $form_id ) ); ?>"><?php esc_html_e( 'Activate', 'submission-inbox' ); ?></a>
							<?php endif; ?>
						</div>
					</div>
				</div>
			<?php endforeach; ?>
		</div>
		<?php
	}

	/**
	 * Get attached auto-reply template for form settings.
	 *
	 * @param array<string,mixed>          $settings Form settings.
	 * @param array<int,array<string,mixed>> $templates Templates.
	 * @return array<string,mixed>|null
	 */
	private function auto_reply_template_for_form_settings( array $settings, array $templates ) {
		$auto_reply = is_array( $settings['auto_reply'] ?? null ) ? $settings['auto_reply'] : array();
		$template_id = absint( $auto_reply['template_id'] ?? 0 );

		if ( $template_id <= 0 ) {
			return null;
		}

		$template = $this->find_auto_reply_template( $templates, $template_id );
		if ( $template ) {
			return $template;
		}

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

	/**
	 * Render attached auto-reply template info.
	 *
	 * @param int                       $form_id Form id.
	 * @param array<string,mixed>       $settings Form settings.
	 * @param array<string,mixed>|null  $template Attached template.
	 * @param bool                      $inside_setup Whether the row is nested inside a setup item.
	 * @return void
	 */
	private function render_form_auto_reply_template_line( $form_id, array $settings, $template, $inside_setup = false ) {
		$auto_reply   = is_array( $settings['auto_reply'] ?? null ) ? $settings['auto_reply'] : array();
		$template_id  = absint( $auto_reply['template_id'] ?? 0 );
		$settings_url = $this->edit_url( $form_id, 'settings' );
		?>
		<div class="subin-form-auto-reply-line<?php echo $inside_setup ? ' subin-form-auto-reply-line--inside' : ''; ?>">
			<span class="subin-form-auto-reply-line__label"><?php echo esc_html( $inside_setup ? __( 'Template', 'submission-inbox' ) : __( 'Auto-reply template', 'submission-inbox' ) ); ?></span>
			<?php if ( $template ) : ?>
				<a class="subin-form-auto-reply-line__template" href="<?php echo esc_url( $this->auto_reply_edit_url( absint( $template['id'] ?? 0 ) ) ); ?>">
					<span><?php echo esc_html( (string) ( $template['name'] ?? __( 'Untitled template', 'submission-inbox' ) ) ); ?></span>
				</a>
			<?php elseif ( $template_id > 0 ) : ?>
				<a class="subin-form-auto-reply-line__missing" href="<?php echo esc_url( $settings_url ); ?>"><?php esc_html_e( 'Selected template was not found', 'submission-inbox' ); ?></a>
			<?php else : ?>
				<a class="subin-form-auto-reply-line__empty" href="<?php echo esc_url( $settings_url ); ?>"><?php esc_html_e( 'No template attached', 'submission-inbox' ); ?></a>
			<?php endif; ?>
		</div>
		<?php
	}

	/**
	 * Check if a form has an email field.
	 *
	 * @param array<int,array<string,mixed>> $fields Fields.
	 * @return bool
	 */
	private function form_has_email_field( array $fields ) {
		foreach ( $fields as $field ) {
			if ( \SUBIN\Forms\FormFieldRepository::TYPE_EMAIL === sanitize_key( $field['field_type'] ?? '' ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Build setup indicators for a form card.
	 *
	 * @param array<string,mixed> $form Form row.
	 * @param array<int,array<string,mixed>> $fields Fields.
	 * @param array<string,mixed> $settings Form settings.
	 * @param array<string,mixed> $plugin_settings Plugin settings.
	 * @return array<int,array<string,mixed>>
	 */
	private function form_setup_indicators( array $form, array $fields, array $settings, array $plugin_settings ) {
		$indicators       = array();
		$email_found      = $this->form_has_email_field( $fields );
		$notify_enabled   = ! empty( $form['notification_enabled'] );
		$notify_recipient = sanitize_email( $form['notification_email'] ?? '' );
		if ( '' === $notify_recipient ) {
			$notify_recipient = sanitize_email( $plugin_settings['notification_recipient_email'] ?? '' );
		}
		$smtp_enabled           = ! empty( $plugin_settings['enable_smtp'] );
		$smtp_configured        = $smtp_enabled && '' !== (string) ( $plugin_settings['smtp_host'] ?? '' );
		if ( $smtp_configured && ! empty( $plugin_settings['smtp_auth'] ) && '' === (string) ( $plugin_settings['smtp_username'] ?? '' ) ) {
			$smtp_configured = false;
		}

		$indicators[] = ! empty( $form['save_leads'] )
			? array( 'label' => __( 'Leads are saved', 'submission-inbox' ), 'text' => __( 'Submissions are stored in InboxMend.', 'submission-inbox' ), 'type' => 'success', 'help' => __( 'When enabled, submissions are stored in InboxMend.', 'submission-inbox' ) )
			: array( 'label' => __( 'Lead storage disabled', 'submission-inbox' ), 'text' => __( 'Submissions are not stored in InboxMend.', 'submission-inbox' ), 'type' => 'warning', 'help' => __( 'Enable lead storage if you want submissions saved even when email notification fails.', 'submission-inbox' ) );

		if ( $notify_enabled && is_email( $notify_recipient ) ) {
			$indicators[] = array( 'label' => __( 'Admin notification enabled', 'submission-inbox' ), 'text' => __( 'Admin notification has a recipient.', 'submission-inbox' ), 'type' => 'success', 'help' => __( 'Sends an email notification when this form is submitted.', 'submission-inbox' ) );
		} elseif ( $notify_enabled ) {
			$indicators[] = array( 'label' => __( 'Notification needs recipient', 'submission-inbox' ), 'text' => __( 'Add a recipient before using admin notifications.', 'submission-inbox' ), 'type' => 'warning', 'help' => __( 'Sends an email notification when this form is submitted.', 'submission-inbox' ) );
		} else {
			$indicators[] = array( 'label' => __( 'Notification disabled', 'submission-inbox' ), 'text' => __( 'No admin notification is configured.', 'submission-inbox' ), 'type' => 'muted', 'help' => __( 'Sends an email notification when this form is submitted.', 'submission-inbox' ) );
		}

		$indicators[] = $smtp_configured
			? array( 'label' => __( 'SMTP configured', 'submission-inbox' ), 'text' => __( 'WordPress uses the configured SMTP connection.', 'submission-inbox' ), 'type' => 'success', 'help' => __( 'Controls how WordPress sends email from this site.', 'submission-inbox' ) )
			: array( 'label' => __( 'SMTP not configured', 'submission-inbox' ), 'text' => __( 'WordPress will use the default mail method.', 'submission-inbox' ), 'type' => 'warning', 'help' => __( 'WordPress will use the default mail method until SMTP is configured.', 'submission-inbox' ) );

		$indicators[] = $email_found
			? array( 'label' => __( 'Email field found', 'submission-inbox' ), 'text' => __( 'Reply-To and auto-reply can use a submitted email.', 'submission-inbox' ), 'type' => 'success', 'help' => __( 'Auto-replies and Reply-To use the submitted email field.', 'submission-inbox' ) )
			: array( 'label' => __( 'Email field missing', 'submission-inbox' ), 'text' => __( 'Reply-To and auto-reply may not work as expected.', 'submission-inbox' ), 'type' => 'warning', 'help' => __( 'Auto-replies and Reply-To may not work without an email field.', 'submission-inbox' ) );

		return $indicators;
	}

	/**
	 * Render auto-reply templates section.
	 *
	 * @return void
	 */
	private function render_auto_reply_templates_section() {
		$templates = $this->auto_reply_templates->get_paginated(
			array(
				'per_page' => 50,
				'offset'   => 0,
			)
		);
		?>
		<div class="subin-forms-section">
			<div class="subin-section-heading">
				<h2><?php esc_html_e( 'Auto-reply Templates', 'submission-inbox' ); ?></h2>
				<p><?php esc_html_e( 'Create reusable visitor auto-reply messages and attach them to forms from the form editor.', 'submission-inbox' ); ?></p>
			</div>
			<div class="subin-auto-reply-manager">
				<div class="subin-auto-reply-manager__header">
					<p><?php esc_html_e( 'Create reusable visitor auto-reply templates and attach them to forms from the form editor.', 'submission-inbox' ); ?></p>
					<a class="button button-primary subin-btn subin-btn-primary" href="<?php echo esc_url( $this->auto_replies_new_url() ); ?>"><?php esc_html_e( 'Create auto-reply template', 'submission-inbox' ); ?></a>
				</div>
				<?php if ( empty( $templates ) ) : ?>
					<div class="subin-forms-empty-state subin-empty-state">
						<h2><?php esc_html_e( 'No auto-reply templates yet', 'submission-inbox' ); ?></h2>
						<p><?php esc_html_e( 'Create a reusable confirmation message for visitors after they submit a form.', 'submission-inbox' ); ?></p>
						<div class="subin-forms-empty-state__actions">
							<a class="button button-primary subin-btn subin-btn-primary" href="<?php echo esc_url( $this->auto_replies_new_url() ); ?>"><?php esc_html_e( 'Create auto-reply template', 'submission-inbox' ); ?></a>
						</div>
					</div>
				<?php else : ?>
					<div class="subin-auto-reply-card-list">
						<?php foreach ( $templates as $template ) : ?>
							<?php
							$template_id = absint( $template['id'] ?? 0 );
							?>
							<div class="subin-auto-reply-card">
								<div class="subin-auto-reply-card__main">
									<div class="subin-auto-reply-card__title-row">
										<h3><a href="<?php echo esc_url( $this->auto_replies_edit_url( $template_id ) ); ?>"><?php echo esc_html( $template['name'] ?? '' ); ?></a></h3>
									</div>
									<p class="subin-auto-reply-card__subject"><?php echo esc_html( $template['subject'] ?? '' ); ?></p>
									<?php if ( ! empty( $template['updated_at'] ) ) : ?>
										<p class="subin-auto-reply-card__meta"><?php esc_html_e( 'Last edited', 'submission-inbox' ); ?>: <?php echo esc_html( $this->format_date( $template['updated_at'] ) ); ?></p>
									<?php endif; ?>
								</div>
								<div class="subin-action-row subin-auto-reply-card__actions">
									<a class="button button-primary subin-btn subin-btn-primary" href="<?php echo esc_url( $this->auto_replies_edit_url( $template_id ) ); ?>"><?php esc_html_e( 'Edit', 'submission-inbox' ); ?></a>
									<a class="subin-action-chip" href="<?php echo esc_url( $this->auto_replies_action_url( 'duplicate', $template_id ) ); ?>"><?php esc_html_e( 'Duplicate', 'submission-inbox' ); ?></a>
									<a class="subin-action-chip subin-action-chip-danger" href="<?php echo esc_url( $this->auto_replies_action_url( 'delete', $template_id ) ); ?>" onclick="return confirm('<?php echo esc_js( __( 'Delete this auto-reply template?', 'submission-inbox' ) ); ?>');"><?php esc_html_e( 'Delete', 'submission-inbox' ); ?></a>
								</div>
							</div>
						<?php endforeach; ?>
					</div>
				<?php endif; ?>
			</div>
		</div>
		<?php
	}

	/**
	 * Build auto-reply template new URL.
	 *
	 * @return string
	 */
	private function auto_replies_new_url() {
		return add_query_arg( array( 'page' => 'submission-inbox-auto-replies', 'view' => 'new' ), admin_url( 'admin.php' ) );
	}

	/**
	 * Build auto-reply template edit URL.
	 *
	 * @param int $template_id Template id.
	 * @return string
	 */
	private function auto_replies_edit_url( $template_id ) {
		return add_query_arg( array( 'page' => 'submission-inbox-auto-replies', 'view' => 'edit', 'id' => absint( $template_id ) ), admin_url( 'admin.php' ) );
	}

	/**
	 * Build auto-reply template action URL.
	 *
	 * @param string $action Action.
	 * @param int    $template_id Template id.
	 * @return string
	 */
	private function auto_replies_action_url( $action, $template_id ) {
		$action      = sanitize_key( $action );
		$template_id = absint( $template_id );

		return wp_nonce_url(
			add_query_arg(
				array(
					'page'       => 'submission-inbox-auto-replies',
					'subin_action' => $action,
					'id'         => $template_id,
				),
				admin_url( 'admin.php' )
			),
			'subin_auto_reply_' . $action . '_' . $template_id
		);
	}

	/**
	 * Render notice from query arg.
	 *
	 * @param string $forced_notice Optional forced notice.
	 * @return void
	 */
	private function render_notice( $forced_notice = '' ) {
		$notice = $forced_notice ? sanitize_key( $forced_notice ) : SUBIN_Request::get_key( 'subin_notice' );
		if ( '' === $notice ) {
			return;
		}

		$messages = array(
			'form_created'          => array( 'success', __( 'Form created. You can now customize it.', 'submission-inbox' ) ),
			'form_create_failed'    => array( 'error', __( 'Form could not be created.', 'submission-inbox' ) ),
			'template_not_found'    => array( 'error', __( 'Template was not found.', 'submission-inbox' ) ),
			'form_duplicated'       => array( 'success', __( 'Form duplicated.', 'submission-inbox' ) ),
			'form_duplicate_failed' => array( 'error', __( 'Form could not be duplicated.', 'submission-inbox' ) ),
			'form_active'           => array( 'success', __( 'Form activated.', 'submission-inbox' ) ),
			'form_inactive'         => array( 'success', __( 'Form deactivated.', 'submission-inbox' ) ),
			'form_status_failed'    => array( 'error', __( 'Form status could not be updated.', 'submission-inbox' ) ),
			'form_saved'            => array( 'success', __( 'Form saved.', 'submission-inbox' ) ),
			'form_content_saved'    => array( 'success', __( 'Form content saved.', 'submission-inbox' ) ),
			'header_title_saved'    => array( 'success', __( 'Header title saved.', 'submission-inbox' ) ),
			'header_description_saved' => array( 'success', __( 'Header description saved.', 'submission-inbox' ) ),
			'footer_note_saved'     => array( 'success', __( 'Footer note saved.', 'submission-inbox' ) ),
			'privacy_text_saved'    => array( 'success', __( 'Privacy text saved.', 'submission-inbox' ) ),
			'button_text_saved'     => array( 'success', __( 'Button text saved.', 'submission-inbox' ) ),
			'design_content_invalid' => array( 'error', __( 'Content could not be saved.', 'submission-inbox' ) ),
			'form_design_saved'     => array( 'success', __( 'Form design saved.', 'submission-inbox' ) ),
			'form_style_applied'    => array( 'success', __( 'Form style applied.', 'submission-inbox' ) ),
			'form_style_not_found'  => array( 'error', __( 'Form style was not found.', 'submission-inbox' ) ),
			'form_style_apply_failed' => array( 'error', __( 'Form style could not be applied.', 'submission-inbox' ) ),
			'background_applied'       => array( 'success', __( 'Background applied.', 'submission-inbox' ) ),
			'background_preset_not_found' => array( 'error', __( 'Background preset was not found.', 'submission-inbox' ) ),
			'background_apply_failed'  => array( 'error', __( 'Background could not be applied.', 'submission-inbox' ) ),
			'gradient_applied'         => array( 'success', __( 'Gradient applied.', 'submission-inbox' ) ),
			'gradient_preset_not_found' => array( 'error', __( 'Gradient preset was not found.', 'submission-inbox' ) ),
			'gradient_apply_failed'    => array( 'error', __( 'Gradient could not be applied.', 'submission-inbox' ) ),
			'button_applied'           => array( 'success', __( 'Button style applied.', 'submission-inbox' ) ),
			'button_preset_not_found'  => array( 'error', __( 'Button style was not found.', 'submission-inbox' ) ),
			'button_apply_failed'      => array( 'error', __( 'Button style could not be applied.', 'submission-inbox' ) ),
			'input_applied'            => array( 'success', __( 'Input style applied.', 'submission-inbox' ) ),
			'input_preset_not_found'   => array( 'error', __( 'Input style was not found.', 'submission-inbox' ) ),
			'input_apply_failed'       => array( 'error', __( 'Input style could not be applied.', 'submission-inbox' ) ),
			'design_element_saved'     => array( 'success', __( 'Element style saved.', 'submission-inbox' ) ),
			'design_element_cleared'   => array( 'success', __( 'Element style cleared.', 'submission-inbox' ) ),
			'design_element_invalid'   => array( 'error', __( 'Element could not be customized.', 'submission-inbox' ) ),
			'design_element_save_failed' => array( 'error', __( 'Element style could not be saved.', 'submission-inbox' ) ),
			'form_email_saved'      => array( 'success', __( 'Email notification settings saved.', 'submission-inbox' ) ),
			'default_notification_recipient_missing' => array( 'error', __( 'Default admin notification recipient is not configured. Add it in Settings → Sending or use a custom recipient for this form.', 'submission-inbox' ) ),
			'custom_notification_recipient_invalid' => array( 'error', __( 'Enter a valid custom recipient email address.', 'submission-inbox' ) ),
			'auto_reply_email_field_required' => array( 'error', __( 'Visitor auto-reply requires an Email field. Add an Email field to the form or turn auto-reply off.', 'submission-inbox' ) ),
			'form_messages_saved'   => array( 'success', __( 'Form messages saved.', 'submission-inbox' ) ),
			'form_publish_saved'       => array( 'success', __( 'Publish settings saved.', 'submission-inbox' ) ),
			'form_settings_saved'      => array( 'success', __( 'Form settings saved.', 'submission-inbox' ) ),
			'form_advanced_html_saved' => array( 'success', __( 'Advanced HTML settings saved.', 'submission-inbox' ) ),
			'custom_css_invalid'       => array( 'error', __( 'Custom CSS must include .subin-form-{id} so it only affects this form.', 'submission-inbox' ) ),
			'form_save_failed'         => array( 'error', __( 'Form could not be saved.', 'submission-inbox' ) ),
			'form_not_found'           => array( 'error', __( 'Form was not found.', 'submission-inbox' ) ),
			'field_added'              => array( 'success', __( 'Field added.', 'submission-inbox' ) ),
			'field_saved'              => array( 'success', __( 'Field saved.', 'submission-inbox' ) ),
			'field_updated'            => array( 'success', __( 'Field updated.', 'submission-inbox' ) ),
			'field_duplicated'         => array( 'success', __( 'Field duplicated.', 'submission-inbox' ) ),
			'field_deleted'            => array( 'success', __( 'Field deleted.', 'submission-inbox' ) ),
			'field_order_updated'      => array( 'success', __( 'Field order updated.', 'submission-inbox' ) ),
			'field_not_found'          => array( 'error', __( 'Field was not found.', 'submission-inbox' ) ),
			'field_save_failed'        => array( 'error', __( 'Field could not be saved.', 'submission-inbox' ) ),
			'block_added'              => array( 'success', __( 'Block added.', 'submission-inbox' ) ),
			'block_updated'            => array( 'success', __( 'Block updated.', 'submission-inbox' ) ),
			'block_deleted'            => array( 'success', __( 'Block deleted.', 'submission-inbox' ) ),
			'block_order_updated'      => array( 'success', __( 'Block order updated.', 'submission-inbox' ) ),
			'block_status_updated'     => array( 'success', __( 'Block status updated.', 'submission-inbox' ) ),
			'block_save_failed'        => array( 'error', __( 'Block could not be saved.', 'submission-inbox' ) ),
			'permission_denied'        => array( 'error', __( 'You do not have permission to perform this action.', 'submission-inbox' ) ),
			'security_failed'          => array( 'error', __( 'Security check failed. Please try again.', 'submission-inbox' ) ),
		);

		if ( empty( $messages[ $notice ] ) ) {
			return;
		}

		$type    = $messages[ $notice ][0];
		$message = $messages[ $notice ][1];
		?>
		<div class="notice notice-<?php echo esc_attr( $type ); ?> is-dismissible">
			<p><?php echo esc_html( $message ); ?></p>
		</div>
		<?php
	}

	/**
	 * Get field type labels.
	 *
	 * @return array<string,string>
	 */
	private function field_type_labels() {
		return array(
			'text'     => __( 'Text', 'submission-inbox' ),
			'email'    => __( 'Email', 'submission-inbox' ),
			'phone'    => __( 'Phone', 'submission-inbox' ),
			'date'     => __( 'Date field', 'submission-inbox' ),
			'textarea' => __( 'Textarea', 'submission-inbox' ),
			'select'   => __( 'Select', 'submission-inbox' ),
			'radio'    => __( 'Radio', 'submission-inbox' ),
			'checkbox' => __( 'Checkbox', 'submission-inbox' ),
			'consent'  => __( 'Consent checkbox', 'submission-inbox' ),
			'hidden'   => __( 'Hidden', 'submission-inbox' ),
		);
	}

	/**
	 * Get the next sort order for a form field.
	 *
	 * @param int $form_id Form ID.
	 * @return int
	 */
	private function next_field_sort_order( $form_id ) {
		$max = -1;
		foreach ( $this->fields->get_by_form_id( $form_id ) as $field ) {
			$max = max( $max, absint( $field['sort_order'] ?? 0 ) );
		}

		return $max + 1;
	}

	/**
	 * Get Elements Library field defaults.
	 *
	 * @param string $field_type Field type.
	 * @param int    $form_id Form ID.
	 * @return array<string,mixed>
	 */
	private function library_field_defaults( $field_type, $form_id ) {
		$field_type = $this->sanitize_choice( $field_type, array_keys( $this->field_type_labels() ), 'text' );
		$defaults   = array(
			'text'     => array(
				'field_label'   => __( 'Text field', 'submission-inbox' ),
				'field_key'     => 'text_field',
				'placeholder'   => '',
				'is_required'   => false,
				'field_options' => array(),
			),
			'email'    => array(
				'field_label'   => __( 'Email', 'submission-inbox' ),
				'field_key'     => 'email',
				'placeholder'   => 'email@example.com',
				'is_required'   => true,
				'field_options' => array(),
			),
			'phone'    => array(
				'field_label'   => __( 'Phone', 'submission-inbox' ),
				'field_key'     => 'phone',
				'placeholder'   => '',
				'is_required'   => false,
				'field_options' => array(),
			),
			'date'     => array(
				'field_label'   => __( 'Date', 'submission-inbox' ),
				'field_key'     => 'date',
				'placeholder'   => '',
				'is_required'   => false,
				'field_options' => array(),
			),
			'textarea' => array(
				'field_label'   => __( 'Message', 'submission-inbox' ),
				'field_key'     => 'message',
				'placeholder'   => '',
				'is_required'   => true,
				'field_options' => array(),
			),
			'select'   => array(
				'field_label'   => __( 'Select field', 'submission-inbox' ),
				'field_key'     => 'select_field',
				'placeholder'   => '',
				'is_required'   => false,
				'field_options' => $this->default_choice_options(),
			),
			'radio'    => array(
				'field_label'   => __( 'Radio field', 'submission-inbox' ),
				'field_key'     => 'radio_field',
				'placeholder'   => '',
				'is_required'   => false,
				'field_options' => $this->default_choice_options(),
			),
			'checkbox' => array(
				'field_label'   => __( 'Checkbox field', 'submission-inbox' ),
				'field_key'     => 'checkbox_field',
				'placeholder'   => '',
				'is_required'   => false,
				'field_options' => $this->default_choice_options(),
			),
			'consent'  => array(
				'field_label'   => __( 'I agree to be contacted about this request.', 'submission-inbox' ),
				'field_key'     => 'consent',
				'placeholder'   => '',
				'is_required'   => false,
				'field_options' => array(),
			),
			'hidden'   => array(
				'field_label'   => __( 'Hidden field', 'submission-inbox' ),
				'field_key'     => 'hidden_field',
				'placeholder'   => '',
				'is_required'   => false,
				'field_options' => array(),
			),
		);

		$field = $defaults[ $field_type ];
		$field['field_type'] = $field_type;
		$field['help_text']  = '';
		$field['sort_order'] = $this->next_field_sort_order( $form_id );

		return $field;
	}

	/**
	 * Get default options for library choice fields.
	 *
	 * @return array<int,array{label:string,value:string}>
	 */
	private function default_choice_options() {
		return array(
			array(
				'label' => __( 'Option 1', 'submission-inbox' ),
				'value' => 'option_1',
			),
			array(
				'label' => __( 'Option 2', 'submission-inbox' ),
				'value' => 'option_2',
			),
		);
	}

	/**
	 * Render editor tab.
	 *
	 * @param int    $form_id Form id.
	 * @param string $tab Tab key.
	 * @param string $label Tab label.
	 * @param string $active Active tab.
	 * @return void
	 */
	private function editor_tab( $form_id, $tab, $label, $active ) {
		$is_active = $tab === $active;

		printf(
			'<a class="nav-tab subin-editor-tab %1$s" href="%2$s"%3$s>%4$s</a>',
			$is_active ? 'nav-tab-active is-active subin-editor-tab--active' : '',
			esc_url( $this->edit_url( $form_id, $tab ) ),
			$is_active ? ' aria-current="page"' : '',
			esc_html( $label )
		);
	}

	/**
	 * Get the active internal Forms tab.
	 *
	 * @return string
	 */
	private function current_forms_tab() {
		$tab          = SUBIN_Request::get_key( 'subin_forms_tab', 'forms' );
		$allowed_tabs = array( 'forms' );

		return in_array( $tab, $allowed_tabs, true ) ? $tab : 'forms';
	}

	/**
	 * Check if an old internal auto-replies tab URL was requested.
	 *
	 * @return bool
	 */
	private function is_legacy_auto_replies_tab_request() {
		return 'auto_replies' === SUBIN_Request::get_key( 'subin_forms_tab' );
	}

	/**
	 * Render Forms product navigation.
	 *
	 * @param string $active Active tab key.
	 * @return void
	 */
	private function render_management_tabs( $active ) {
		SUBIN_View::admin_tabs(
			array(
				array(
					'key'   => 'forms',
					'label' => __( 'Forms', 'submission-inbox' ),
					'url'   => $this->forms_url(),
				),
			),
			$active
		);
	}

	/**
	 * Build base Forms URL.
	 *
	 * @return string
	 */
	private function forms_url() {
		return add_query_arg( array( 'page' => self::PAGE_SLUG ), admin_url( 'admin.php' ) );
	}

	/**
	 * Build auto-reply templates manager URL.
	 *
	 * @return string
	 */
	private function auto_replies_url() {
		return add_query_arg( array( 'page' => 'submission-inbox-auto-replies' ), admin_url( 'admin.php' ) );
	}

	/**
	 * Build an internal Forms tab URL.
	 *
	 * @param string $tab Tab key.
	 * @return string
	 */
	private function forms_tab_url( $tab ) {
		$allowed_tabs = array( 'forms' );
		$tab          = in_array( $tab, $allowed_tabs, true ) ? $tab : 'forms';

		return add_query_arg(
			array(
				'page'          => self::PAGE_SLUG,
				'subin_forms_tab' => $tab,
			),
			admin_url( 'admin.php' )
		);
	}

	/**
	 * Build template chooser URL.
	 *
	 * @return string
	 */
	private function templates_url() {
		return add_query_arg(
			array(
				'page' => self::PAGE_SLUG,
				'view' => 'templates',
			),
			admin_url( 'admin.php' )
		);
	}

	/**
	 * Build template preview URL.
	 *
	 * @param string $template_key Template key.
	 * @return string
	 */
	private function template_preview_url( $template_key ) {
		return add_query_arg(
			array(
				'page'     => self::PAGE_SLUG,
				'view'     => 'template-preview',
				'template' => sanitize_key( $template_key ),
			),
			admin_url( 'admin.php' )
		);
	}

	/**
	 * Build preview URL.
	 *
	 * @param int $form_id Form id.
	 * @return string
	 */
	private function preview_url( $form_id ) {
		return add_query_arg(
			array(
				'page'    => self::PAGE_SLUG,
				'view'    => 'preview',
				'form_id' => absint( $form_id ),
			),
			admin_url( 'admin.php' )
		);
	}

	/**
	 * Build edit URL.
	 *
	 * @param int                 $form_id Form id.
	 * @param string              $tab Tab key.
	 * @param array<string,mixed> $extra Extra args.
	 * @return string
	 */
	private function edit_url( $form_id, $tab = 'content', array $extra = array() ) {
		$tab = sanitize_key( $tab );
		if ( 'content' === $tab ) {
			$tab = 'design_studio';
		}
		if ( in_array( $tab, array( 'email', 'publish' ), true ) ) {
			$tab = 'settings';
		}

		return add_query_arg(
			array_merge(
				array(
					'page'    => self::PAGE_SLUG,
					'view'    => 'edit',
					'form_id' => absint( $form_id ),
					'tab'     => $tab,
				),
				$extra
			),
			admin_url( 'admin.php' )
		);
	}

	/**
	 * Build action URL.
	 *
	 * @param string              $action Action.
	 * @param int                 $form_id Form id.
	 * @param array<string,mixed> $extra Extra args.
	 * @return string
	 */
	private function action_url( $action, $form_id = 0, array $extra = array() ) {
		$action = sanitize_key( $action );
		$args   = array_merge(
			array(
				'page'       => self::PAGE_SLUG,
				'subin_action' => $action,
			),
			$extra
		);

		if ( $form_id > 0 ) {
			$args['form_id'] = absint( $form_id );
		}

		$field_id = absint( $extra['field_id'] ?? 0 );
		$nonce_action = $this->action_nonce_name( $action, $form_id, $field_id );
		if ( 'create_from_template' === $action && ! empty( $extra['template_key'] ) ) {
			$nonce_action = 'subin_use_template_' . sanitize_key( $extra['template_key'] );
		}

		return wp_nonce_url( add_query_arg( $args, admin_url( 'admin.php' ) ), $nonce_action );
	}

	/**
	 * Get nonce action name for state-changing admin links.
	 *
	 * @param string $action Action.
	 * @param int    $form_id Form id.
	 * @param int    $field_id Field id.
	 * @return string
	 */
	private function action_nonce_name( $action, $form_id = 0, $field_id = 0 ) {
		$action   = sanitize_key( $action );
		$form_id  = absint( $form_id );
		$field_id = absint( $field_id );

		if ( 'create_from_template' === $action ) {
			return 'subin_create_form_from_template';
		}

		if ( 'duplicate' === $action ) {
			return 'subin_duplicate_form_' . $form_id;
		}

		if ( in_array( $action, array( 'activate', 'deactivate' ), true ) ) {
			return 'subin_update_form_' . $form_id;
		}

		return 'subin_forms_' . $action;
	}

	/**
	 * Get nonce action name for editor POST actions.
	 *
	 * @param string $action Action.
	 * @param int    $form_id Form id.
	 * @param int    $field_id Field id.
	 * @return string
	 */
	private function editor_nonce_name( $action, $form_id, $field_id = 0 ) {
		$action   = sanitize_key( $action );
		$form_id  = absint( $form_id );
		$field_id = absint( $field_id );

		if ( in_array( $action, array( 'save_notification', 'save_publish', 'save_settings' ), true ) ) {
			return 'subin_update_form_' . $form_id;
		}

		return 'subin_form_editor_' . $form_id;
	}

	/**
	 * Sanitize auto-reply recipient field against this form's email fields.
	 *
	 * @param int    $form_id Form id.
	 * @param string $field_key Field key.
	 * @return string
	 */
	private function sanitize_auto_reply_recipient_field_key( $form_id, $field_key ) {
		$field_key = sanitize_key( $field_key );
		$field_id  = absint( $field_key );
		if ( '' === $field_key ) {
			return '';
		}

		foreach ( $this->email_field_choices( $form_id ) as $field ) {
			if ( $field_key === $field['key'] || ( $field_id > 0 && $field_id === absint( $field['id'] ?? 0 ) ) ) {
				return $field['key'];
			}
		}

		return '';
	}

	/**
	 * Sanitize selected auto-reply template id.
	 *
	 * @param int $template_id Template id.
	 * @return int
	 */
	private function sanitize_auto_reply_template_id( $template_id ) {
		$template_id = absint( $template_id );
		if ( $template_id <= 0 ) {
			return 0;
		}

		return $this->auto_reply_templates->get( $template_id ) ? $template_id : 0;
	}

	/**
	 * Sanitize admin notification recipient mode.
	 *
	 * @param string $mode Recipient mode.
	 * @return string
	 */
	private function sanitize_notification_recipient_mode( $mode ) {
		$mode = sanitize_key( $mode );

		return in_array( $mode, array( 'default', 'custom' ), true ) ? $mode : 'default';
	}

	/**
	 * Determine default notification recipient mode for existing forms.
	 *
	 * @param array<string,mixed> $form Form row.
	 * @return string
	 */
	private function default_notification_recipient_mode_for_form( array $form ) {
		$recipient = sanitize_email( $form['notification_email'] ?? '' );

		return is_email( $recipient ) ? 'custom' : 'default';
	}

	/**
	 * Get the global default admin notification recipient.
	 *
	 * @return string
	 */
	private function default_admin_notification_recipient() {
		$settings  = get_option( \SUBIN_SettingsRepository::OPTION_KEY, array() );
		$recipient = is_array( $settings ) ? sanitize_email( $settings['notification_recipient_email'] ?? '' ) : '';
		if ( is_email( $recipient ) ) {
			return $recipient;
		}

		$admin_email = sanitize_email( (string) get_option( 'admin_email' ) );
		return is_email( $admin_email ) ? $admin_email : '';
	}

	/**
	 * Redirect to Forms page with notice.
	 *
	 * @param string $notice Notice key.
	 * @return void
	 */
	private function redirect_with_notice( $notice ) {
		$this->safe_admin_redirect(
			add_query_arg(
				array(
					'page'       => self::PAGE_SLUG,
					'subin_notice' => sanitize_key( $notice ),
				),
				admin_url( 'admin.php' )
			)
		);
	}

	/**
	 * Redirect to template chooser with notice.
	 *
	 * @param string $notice Notice key.
	 * @return void
	 */
	private function redirect_to_templates( $notice ) {
		$this->safe_admin_redirect(
			add_query_arg(
				array(
					'subin_notice' => sanitize_key( $notice ),
				),
				$this->templates_url()
			)
		);
	}

	/**
	 * Redirect to edit screen with notice.
	 *
	 * @param int    $form_id Form id.
	 * @param string $tab Tab key.
	 * @param string $notice Notice key.
	 * @return void
	 */
	private function redirect_to_edit( $form_id, $tab, $notice ) {
		$this->safe_admin_redirect(
			add_query_arg(
				array(
					'subin_notice' => sanitize_key( $notice ),
				),
				$this->edit_url( $form_id, $tab )
			)
		);
	}

	/**
	 * Redirect from an admin page callback without header warnings.
	 *
	 * @param string $url Target URL.
	 * @return void
	 */
	private function safe_admin_redirect( $url ) {
		$url = esc_url_raw( $url );
		if ( ! headers_sent() ) {
			wp_safe_redirect( $url );
			exit;
		}

		printf(
			'<script>window.location.href=%1$s;</script><noscript><meta http-equiv="refresh" content="0;url=%2$s" /></noscript>',
			wp_json_encode( $url ),
			esc_url( $url )
		);
		exit;
	}
}
