<?php
/**
 * Basic Forms Module
 *
 * Create custom contact and lead forms with file uploads, email notifications,
 * entry management, spam protection, and optional AI-assisted form building.
 *
 * @package BasicForms
 * @since 1.0.0
 */

namespace BasicForms;

use WP_PowerSuite\Core\Contracts\DataLocationsInterface;
use WP_PowerSuite\Core\AbstractModule;
use WP_PowerSuite\Email\EmailComponents;
use WP_PowerSuite\Email\EmailDesign;
use WP_PowerSuite\Email\EmailRenderer;
use WP_PowerSuite\AI\ConnectionSettings;
use WP_PowerSuite\Core\DataRegistry;

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

require_once __DIR__ . '/includes/FormsListTable.php';
require_once __DIR__ . '/includes/EntriesListTable.php';
require_once __DIR__ . '/includes/DesignDefaults.php';
require_once __DIR__ . '/includes/FormTemplates.php';
require_once __DIR__ . '/includes/ConditionalFields.php';
require_once __DIR__ . '/includes/AiRateLimiter.php';
require_once __DIR__ . '/includes/AiFormGenerator.php';
require_once __DIR__ . '/includes/AiSettings.php';
require_once __DIR__ . '/includes/FeatureGate.php';
require_once __DIR__ . '/includes/FormAiModels.php';
require_once __DIR__ . '/includes/FormFileStorage.php';
require_once __DIR__ . '/includes/SubmissionValidator.php';
require_once __DIR__ . '/includes/PrivacyService.php';

class Module extends AbstractModule implements DataLocationsInterface {

	private const MODULE_ID = 'basic-forms';
	private const MODULE_VERSION = '1.0.0';
	private const CPT = 'wpps_form';
	private const TABLE = 'wpps_form_entries';
	private const FILE_UPLOAD_LIMIT       = 5;
	private const FILE_UPLOAD_WINDOW      = MINUTE_IN_SECONDS;
	private const SUBMISSION_LIMIT        = 10;
	private const SUBMISSION_WINDOW       = MINUTE_IN_SECONDS;

	private string $table_name = '';
	private int $upload_form_id = 0;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'AI Forms', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	/**
	 * Cache-busting asset version: combines the module version with the file's
	 * modification time so CSS/JS changes are picked up without a manual bump.
	 *
	 * @param string $relative Path relative to this module directory, e.g. "assets/frontend.js".
	 */
	private function assetVersion( string $relative ): string {
		$path = __DIR__ . '/' . ltrim( $relative, '/' );
		if ( file_exists( $path ) ) {
			return self::MODULE_VERSION . '.' . (string) filemtime( $path );
		}
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Build contact and lead forms manually or with AI assistance. Uploads, notifications, entries, and spam protection—no external form SaaS.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return [ 'admin', 'frontend', 'ajax' ];
	}

	public function getDependencies(): array {
		return [];
	}

	public function getDataLocations(): array {
		return [
			'option_store_keys' => [ self::MODULE_ID ],
			'options'          => [ 'wpps_forms_rewrite_flushed' ],
			'tables'            => [ self::TABLE ],
			'post_meta'         => [ '_form_fields', '_form_settings', '_form_design' ],
		];
	}


	protected function registerHooks(): void {
		global $wpdb;
		$this->table_name = $wpdb->prefix . self::TABLE;


		DataRegistry::registerTable( self::TABLE );add_action( 'init', [ $this, 'registerCpt' ], 5 );
		add_action( 'admin_init', [ $this, 'maybeCreateTable' ], 999 );
		add_action( 'admin_init', [ $this, 'maybeMigrateLegacyFileEntries' ], 1000 );
		add_action( 'admin_init', [ $this, 'handleCsvExport' ] );
		add_action( 'load-post-new.php', [ $this, 'redirectPostNewToCustomPage' ] );
		add_action( 'load-post.php', [ $this, 'redirectPostEditToCustomPage' ] );
		add_action( 'admin_init', [ $this, 'handleFormEditorSave' ] );
		add_action( 'admin_init', [ $this, 'handleTemplateFormCreate' ] );
		add_action( 'admin_menu', [ $this, 'addAdminMenu' ], 20 );
		add_action( 'admin_menu', [ $this, 'removeFormsMenu' ], 999 );
		add_action( 'parent_file', [ $this, 'fixParentMenu' ] );
		add_action( 'submenu_file', [ $this, 'fixSubmenuFile' ] );
		add_shortcode( 'basic_form', [ $this, 'renderFormShortcode' ] );
		add_action( 'wp_ajax_submit_basic_form', [ $this, 'handleFormSubmission' ] );
		add_action( 'wp_ajax_nopriv_submit_basic_form', [ $this, 'handleFormSubmission' ] );
		add_action( 'wp_ajax_wpps_basic_forms_view_entry', [ $this, 'ajaxViewEntry' ] );
		add_action( 'wp_ajax_wpps_basic_forms_download_file', [ $this, 'ajaxDownloadFile' ] );
		add_action( 'wp_ajax_wpps_basic_forms_save_design', [ $this, 'ajaxSaveDesign' ] );
		add_action( 'wp_ajax_wpps_basic_forms_ai_generate', [ $this, 'ajaxAiGenerateForm' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAdminAssets' ] );
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueueFrontendAssets' ] );
		PrivacyService::registerHooks();
	}


	public function registerCpt(): void {
		$labels = [
			'name'               => __( 'Forms', 'wp-powersuite' ),
			'singular_name'      => __( 'Form', 'wp-powersuite' ),
			'add_new'            => __( 'Add New Form', 'wp-powersuite' ),
			'add_new_item'       => __( 'Add New Form', 'wp-powersuite' ),
			'edit_item'          => __( 'Edit Form', 'wp-powersuite' ),
			'new_item'           => __( 'New Form', 'wp-powersuite' ),
			'view_item'          => __( 'View Form', 'wp-powersuite' ),
			'search_items'       => __( 'Search Forms', 'wp-powersuite' ),
			'not_found'           => __( 'No forms found', 'wp-powersuite' ),
			'not_found_in_trash' => __( 'No forms found in Trash', 'wp-powersuite' ),
			'all_items'          => __( 'All Forms', 'wp-powersuite' ),
		];

		register_post_type( self::CPT, [
			'labels'              => $labels,
			'public'              => false,
			'publicly_queryable'  => false,
			'show_ui'             => true,
			'show_in_menu'        => true,
			'query_var'           => true,
			'rewrite'             => [ 'slug' => 'wpps-form' ],
			'menu_icon'           => 'dashicons-feedback',
			'menu_position'       => 100,
			'supports'            => [ 'title' ],
			'capability_type'     => 'post',
			'capabilities'        => [
				'edit_post'          => 'manage_options',
				'edit_posts'         => 'manage_options',
				'edit_others_posts'  => 'manage_options',
				'publish_posts'      => 'manage_options',
				'read_post'          => 'manage_options',
				'read_private_posts' => 'manage_options',
				'delete_post'        => 'manage_options',
			],
		] );
	}

	public function maybeCreateTable(): void {
		if ( ! $this->isEnabled() ) {
			return;
		}
		$exists = $GLOBALS['wpdb']->get_var( $GLOBALS['wpdb']->prepare( 'SHOW TABLES LIKE %s', $this->table_name ) ) === $this->table_name;
		if ( ! $exists ) {
			$this->createEntriesTable();
		}
		if ( ! get_option( 'wpps_forms_rewrite_flushed' ) ) {
			add_action( 'init', 'flush_rewrite_rules', 999 );
			update_option( 'wpps_forms_rewrite_flushed', 1, false );
		}
	}

	/**
	 * Migrate entries that still store public upload URLs (batched on admin loads).
	 */
	public function maybeMigrateLegacyFileEntries(): void {
		if ( ! $this->isEnabled() || ! current_user_can( 'manage_options' ) ) {
			return;
		}

		if ( get_option( 'wpps_bf_legacy_files_migrated', false ) ) {
			return;
		}

		FormFileStorage::migrateLegacyEntries( $this->table_name );
	}

	private function deleteEntryById( int $entry_id ): bool {
		global $wpdb;

		if ( $entry_id <= 0 ) {
			return false;
		}

		$row = $wpdb->get_row( $wpdb->prepare( "SELECT payload FROM {$this->table_name} WHERE id = %d", $entry_id ) );
		if ( $row ) {
			$payload = json_decode( $row->payload ?? '{}', true );
			if ( is_array( $payload ) ) {
				FormFileStorage::deleteFilesForPayload( $payload );
			}
		}

		return (bool) $wpdb->delete( $this->table_name, [ 'id' => $entry_id ], [ '%d' ] );
	}

	private function deleteEntriesForFormId( int $form_id ): void {
		global $wpdb;

		if ( $form_id <= 0 ) {
			return;
		}

		$entries = $wpdb->get_results( $wpdb->prepare( "SELECT payload FROM {$this->table_name} WHERE form_id = %d", $form_id ) );
		if ( is_array( $entries ) ) {
			foreach ( $entries as $entry ) {
				$payload = json_decode( $entry->payload ?? '{}', true );
				if ( is_array( $payload ) ) {
					FormFileStorage::deleteFilesForPayload( $payload );
				}
			}
		}

		$wpdb->delete( $this->table_name, [ 'form_id' => $form_id ], [ '%d' ] );
	}

	private function createEntriesTable(): void {
		global $wpdb;
		$charset = $wpdb->get_charset_collate();
		$sql = "CREATE TABLE IF NOT EXISTS {$this->table_name} (
			id bigint(20) NOT NULL AUTO_INCREMENT,
			form_id bigint(20) NOT NULL,
			date_created datetime DEFAULT CURRENT_TIMESTAMP,
			status varchar(20) DEFAULT 'unread',
			user_ip varchar(100),
			user_agent text,
			payload longtext,
			user_email varchar(255),
			user_name varchar(255),
			PRIMARY KEY (id),
			KEY form_id (form_id),
			KEY date_created (date_created),
			KEY status (status),
			KEY user_email (user_email)
		) {$charset};";
		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );
	}

	public function removeFormsMenu(): void {
		remove_menu_page( 'edit.php?post_type=' . self::CPT );
	}

	/** @param string|null $parent_file */
	public function fixParentMenu( $parent_file ): string {
		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( $screen && $screen->post_type === self::CPT ) {
			return 'powersuite';
		}
		// Expand WP PowerSuite menu when on Basic Forms custom page
		if ( isset( $_GET['page'] ) && $_GET['page'] === 'powersuite-basic-forms' ) {
			return 'powersuite';
		}
		return $parent_file ?? '';
	}

	/** @param string|null $submenu_file */
	public function fixSubmenuFile( $submenu_file ): string {
		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( $screen && $screen->post_type === self::CPT ) {
			return $screen->base === 'post' ? 'post-new.php?post_type=' . self::CPT : 'edit.php?post_type=' . self::CPT;
		}
		// Highlight Basic Forms submenu when on the custom page (forms list or entries)
		if ( isset( $_GET['page'] ) && $_GET['page'] === 'powersuite-basic-forms' ) {
			return 'powersuite-basic-forms';
		}
		return $submenu_file ?? '';
	}

	public function addAdminMenu(): void {
		$parent_slug = class_exists( 'WP_PowerSuite\Core\AdminMenu', false )
			? \WP_PowerSuite\Core\AdminMenu::getInstance()->getPageSlug()
			: 'powersuite';
		$capability = apply_filters( 'wpps_basic_forms_capability', 'manage_options' );
		add_submenu_page(
			$parent_slug,
			__( 'AI Forms', 'wp-powersuite' ),
			__( 'AI Forms', 'wp-powersuite' ),
			$capability,
			'powersuite-basic-forms',
			[ $this, 'renderAdminPage' ]
		);
	}

	public function handleCsvExport(): void {
		if ( isset( $_GET['page'] ) && $_GET['page'] === 'powersuite-basic-forms'
			&& isset( $_GET['action'] ) && $_GET['action'] === 'export_csv'
			&& isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'export_entries_csv' )
			&& $this->canManageForms() ) {
			$this->exportEntriesCsv();
			exit;
		}
	}

	public function redirectPostNewToCustomPage(): void {
		if ( isset( $_GET['post_type'] ) && $_GET['post_type'] === self::CPT ) {
			wp_safe_redirect( admin_url( 'admin.php?page=powersuite-basic-forms&action=add' ) );
			exit;
		}
	}

	public function redirectPostEditToCustomPage(): void {
		if ( ! isset( $_GET['post'] ) ) {
			return;
		}
		$post = get_post( (int) $_GET['post'] );
		if ( $post && $post->post_type === self::CPT ) {
			wp_safe_redirect( admin_url( 'admin.php?page=powersuite-basic-forms&action=edit&form_id=' . $post->ID ) );
			exit;
		}
	}

	public function handleTemplateFormCreate(): void {
		if ( ! isset( $_GET['page'] ) || 'powersuite-basic-forms' !== $_GET['page'] ) {
			return;
		}
		if ( ! isset( $_GET['action'] ) || 'add' !== $_GET['action'] ) {
			return;
		}
		if ( ! isset( $_GET['template'] ) ) {
			return;
		}

		$capability = apply_filters( 'wpps_basic_forms_capability', 'manage_options' );
		if ( ! current_user_can( $capability ) ) {
			return;
		}

		$template_id = sanitize_key( wp_unslash( (string) $_GET['template'] ) );
		if ( '' === $template_id ) {
			return;
		}

		if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wpps_bf_use_template_' . $template_id ) ) {
			wp_die( esc_html__( 'Invalid template request.', 'wp-powersuite' ), esc_html__( 'Basic Forms', 'wp-powersuite' ), [ 'response' => 403 ] );
		}

		$form_id = $this->createFormFromTemplate( $template_id );
		if ( is_wp_error( $form_id ) ) {
			$code = $form_id->get_error_code();
			wp_safe_redirect(
				add_query_arg(
					[
						'page'            => 'powersuite-basic-forms',
						'action'          => 'add',
						'template_error'  => 'invalid' === $code ? 'invalid' : 'failed',
					],
					admin_url( 'admin.php' )
				)
			);
			exit;
		}

		wp_safe_redirect( admin_url( 'admin.php?page=powersuite-basic-forms&action=edit&form_id=' . (int) $form_id . '&saved=1' ) );
		exit;
	}

	/**
	 * Create a published form from a ready-made template.
	 *
	 * @return int|\WP_Error
	 */
	public function createFormFromTemplate( string $template_id ) {
		$template = FormTemplates::getById( $template_id );
		if ( ! is_array( $template ) ) {
			return new \WP_Error( 'invalid', __( 'Template not found.', 'wp-powersuite' ) );
		}

		$title = isset( $template['name'] ) ? sanitize_text_field( (string) $template['name'] ) : __( 'New Form', 'wp-powersuite' );
		$form_id = wp_insert_post(
			[
				'post_type'   => self::CPT,
				'post_title'  => $title,
				'post_status' => 'publish',
			]
		);
		if ( is_wp_error( $form_id ) || ! $form_id ) {
			return is_wp_error( $form_id ) ? $form_id : new \WP_Error( 'failed', __( 'Could not create form.', 'wp-powersuite' ) );
		}

		$fields = isset( $template['fields'] ) && is_array( $template['fields'] ) ? $template['fields'] : [];
		update_post_meta( $form_id, '_form_fields', $this->sanitizeFormFields( $fields ) );

		$settings_defaults = [
			'recipient'          => get_option( 'admin_email' ),
			'subject'            => sprintf( __( 'New form submission from %s', 'wp-powersuite' ), get_bloginfo( 'name' ) ),
			'success_message'    => __( 'Thank you! Your submission has been received.', 'wp-powersuite' ),
			'redirect_url'       => '',
			'submit_button_text' => __( 'Submit', 'wp-powersuite' ),
			'captcha_provider'   => 'simple',
			'enable_autoreply'   => '',
			'autoreply_subject'  => __( 'Thank you for your submission', 'wp-powersuite' ),
			'autoreply_message'  => __( 'We have received your submission and will get back to you soon.', 'wp-powersuite' ),
			'store_ip'           => '1',
			'store_user_agent'   => '1',
		];
		$raw_settings = isset( $template['settings'] ) && is_array( $template['settings'] ) ? $template['settings'] : [];
		update_post_meta( $form_id, '_form_settings', $this->sanitizeFormSettings( wp_parse_args( $raw_settings, $settings_defaults ) ) );

		$raw_design = isset( $template['design'] ) && is_array( $template['design'] ) ? $template['design'] : [];
		$design     = DesignDefaults::normalizeForAdminForm( wp_parse_args( $raw_design, DesignDefaults::get() ) );
		update_post_meta( $form_id, '_form_design', $design );

		/**
		 * Fires after a form is created from a ready-made template.
		 *
		 * @param int    $form_id     New form post ID.
		 * @param string $template_id Template slug.
		 */
		do_action( 'wpps_basic_forms_form_created_from_template', (int) $form_id, $template_id );

		return (int) $form_id;
	}

	/**
	 * Create a published form from an AI-generated payload.
	 *
	 * @param array<string, mixed> $payload
	 * @return int|\WP_Error
	 */
	public function createFormFromAiPayload( array $payload ) {
		$title = isset( $payload['title'] ) ? sanitize_text_field( (string) $payload['title'] ) : __( 'New Form', 'wp-powersuite' );
		$form_id = wp_insert_post(
			[
				'post_type'   => self::CPT,
				'post_title'  => $title !== '' ? $title : __( 'New Form', 'wp-powersuite' ),
				'post_status' => 'publish',
			]
		);
		if ( is_wp_error( $form_id ) || ! $form_id ) {
			return is_wp_error( $form_id ) ? $form_id : new \WP_Error( 'failed', __( 'Could not create form.', 'wp-powersuite' ) );
		}

		$fields = isset( $payload['fields'] ) && is_array( $payload['fields'] ) ? $payload['fields'] : [];
		update_post_meta( $form_id, '_form_fields', $this->sanitizeFormFields( $fields ) );

		$settings_defaults = [
			'recipient'          => get_option( 'admin_email' ),
			'subject'            => sprintf( __( 'New form submission from %s', 'wp-powersuite' ), get_bloginfo( 'name' ) ),
			'success_message'    => __( 'Thank you! Your submission has been received.', 'wp-powersuite' ),
			'redirect_url'       => '',
			'submit_button_text' => __( 'Submit', 'wp-powersuite' ),
			'captcha_provider'   => 'simple',
			'enable_autoreply'   => '',
			'autoreply_subject'  => __( 'Thank you for your submission', 'wp-powersuite' ),
			'autoreply_message'  => __( 'We have received your submission and will get back to you soon.', 'wp-powersuite' ),
			'store_ip'           => '1',
			'store_user_agent'   => '1',
		];
		$raw_settings = isset( $payload['settings'] ) && is_array( $payload['settings'] ) ? $payload['settings'] : [];
		update_post_meta( $form_id, '_form_settings', $this->sanitizeFormSettings( wp_parse_args( $raw_settings, $settings_defaults ) ) );

		$design_defaults = DesignDefaults::normalizeForAdminForm( DesignDefaults::get() );
		if ( isset( $payload['design'] ) && is_array( $payload['design'] ) ) {
			$design = $this->sanitizeAndNormalizeFormDesign( (int) $form_id, $payload['design'] );
		} else {
			$design = $design_defaults;
		}
		update_post_meta( $form_id, '_form_design', $design );

		return (int) $form_id;
	}

	public function ajaxAiGenerateForm(): void {
		check_ajax_referer( 'wpps_basic_forms_ai_generate', 'nonce' );

		$capability = apply_filters( 'wpps_basic_forms_capability', 'manage_options' );
		if ( ! current_user_can( $capability ) ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ], 403 );
		}

		if ( ! FeatureGate::isPro() ) {
			wp_send_json_error(
				[
					'message' => __( 'The AI Form Builder is a Premium feature. Activate your license to generate and modify forms with AI.', 'wp-powersuite' ),
					'code'    => 'premium_required',
				],
				403
			);
		}

		if ( ! AiSettings::isEnabled() ) {
			wp_send_json_error( [ 'message' => __( 'The AI assistant is disabled. Enable it in AI Forms settings.', 'wp-powersuite' ) ], 403 );
		}

		$rate_check = AiRateLimiter::check();
		if ( is_wp_error( $rate_check ) ) {
			wp_send_json_error( [ 'message' => $rate_check->get_error_message(), 'code' => $rate_check->get_error_code() ], 429 );
		}

		$prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( (string) $_POST['prompt'] ) ) : '';
		$mode   = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( (string) $_POST['mode'] ) ) : 'create';

		$module_ai = AiSettings::get();
		$provider  = ConnectionSettings::sanitizeProvider( (string) ( $module_ai['provider'] ?? 'openai' ) );

		$context = [];
		if ( isset( $_POST['form_context'] ) ) {
			$decoded = json_decode( wp_unslash( (string) $_POST['form_context'] ), true );
			if ( is_array( $decoded ) ) {
				$context = $decoded;
			}
		}

		$history = [];
		if ( isset( $_POST['history'] ) ) {
			$decoded_history = json_decode( wp_unslash( (string) $_POST['history'] ), true );
			if ( is_array( $decoded_history ) ) {
				$history = AiFormGenerator::normalizeHistory( $decoded_history );
			}
		}

		$has_context = [] !== $context && (
			! empty( $context['fields'] )
			|| ! empty( $context['title'] )
			|| ! empty( $context['design'] )
		);
		$intent        = 'edit' === $mode || 'fields' === $mode
			? AiFormGenerator::inferIntentFromPrompt( $prompt, $has_context, $history )
			: 'create';

		/**
		 * Filter the inferred AI form builder intent.
		 *
		 * @param string               $intent
		 * @param string               $prompt
		 * @param array<string, mixed> $context
		 */
		$intent = (string) apply_filters( 'wpps_basic_forms_ai_inferred_intent', $intent, $prompt, $context );

		$generator = new AiFormGenerator();
		$result    = $generator->generate(
			$prompt,
			$provider,
			[
				'intent'  => $intent,
				'context' => $context,
				'model'   => (string) ( $module_ai['model'] ?? '' ),
				'history' => $history,
			]
		);
		if ( is_wp_error( $result ) ) {
			wp_send_json_error(
				[
					'message'      => $result->get_error_message(),
					'code'         => $result->get_error_code(),
					'settings_url' => admin_url( 'admin.php?page=powersuite-settings&tab=ai-connections' ),
				]
			);
		}

		$title    = (string) ( $result['title'] ?? __( 'New Form', 'wp-powersuite' ) );
		$fields   = $this->sanitizeFormFields( is_array( $result['fields'] ?? null ) ? $result['fields'] : [] );
		$settings = $this->sanitizeFormSettings(
			wp_parse_args(
				is_array( $result['settings'] ?? null ) ? $result['settings'] : [],
				[
					'recipient'          => get_option( 'admin_email' ),
					'subject'            => sprintf( __( 'New form submission: %s', 'wp-powersuite' ), $title ),
					'success_message'    => __( 'Thank you! Your submission has been received.', 'wp-powersuite' ),
					'redirect_url'       => '',
					'submit_button_text' => __( 'Submit', 'wp-powersuite' ),
					'captcha_provider'   => 'simple',
					'enable_autoreply'   => '',
					'autoreply_subject'  => __( 'Thank you for your submission', 'wp-powersuite' ),
					'autoreply_message'  => __( 'We have received your submission and will get back to you soon.', 'wp-powersuite' ),
					'store_ip'           => '1',
					'store_user_agent'   => '1',
				]
			)
		);

		if ( 'edit' === $mode || 'fields' === $mode ) {
			$changed = isset( $result['changed'] ) && is_array( $result['changed'] ) ? $result['changed'] : [];

			$summary = AiFormGenerator::buildAssistantSummary(
				[
					'title'      => $title,
					'fields'     => $fields,
					'changed'    => $changed,
					'ai_message' => $result['ai_message'] ?? '',
				],
				$intent
			);

			$response = [
				'title'              => $title,
				'fields'             => $fields,
				'settings'           => $settings,
				'intent'             => $intent,
				'changed'            => $changed,
				'assistant_message'  => $summary,
				'intent_label'       => AiFormGenerator::intentLabel( $intent ),
			];
			if ( isset( $result['design'] ) && is_array( $result['design'] ) ) {
				$response['design'] = $this->sanitizeAndNormalizeFormDesign(
					isset( $context['form_id'] ) ? (int) $context['form_id'] : 0,
					$result['design']
				);
			}
			wp_send_json_success( $response );
		}

		$create_payload = [
			'title'    => $title,
			'fields'   => $fields,
			'settings' => $settings,
		];
		if ( isset( $result['design'] ) && is_array( $result['design'] ) ) {
			$create_payload['design'] = $result['design'];
		}

		$form_id = $this->createFormFromAiPayload( $create_payload );
		if ( is_wp_error( $form_id ) ) {
			wp_send_json_error( [ 'message' => $form_id->get_error_message() ] );
		}

		/**
		 * Fires after a form is created from the AI form builder.
		 *
		 * @param int    $form_id Form post ID.
		 * @param string $prompt  User prompt.
		 */
		do_action( 'wpps_basic_forms_ai_form_generated', (int) $form_id, $prompt );

		wp_send_json_success(
			[
				'form_id'             => (int) $form_id,
				'redirect_url'        => admin_url( 'admin.php?page=powersuite-basic-forms&action=edit&form_id=' . (int) $form_id . '&saved=1' ),
				'title'               => $title,
				'assistant_message'   => AiFormGenerator::buildAssistantSummary(
					[
						'title'  => $title,
						'fields' => $fields,
					],
					'create'
				),
			]
		);
	}

	private function renderTemplatePickerView(): void {
		$base = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		$deps = wp_style_is( 'wp-powersuite-admin', 'registered' ) ? [ 'wp-powersuite-admin' ] : [];
		wp_enqueue_style( 'wpps-bf-admin', $base . 'modules/basic-forms/assets/admin.css', $deps, $this->assetVersion( 'assets/admin.css' ) );
		$this->enqueueAiFormBuilderAssets( 'create' );
		include __DIR__ . '/views/form-template-picker.php';
	}

	public function handleFormEditorSave(): void {
		// Only react to genuine editor-save POSTs; normal admin page loads fall through.
		if ( ! isset( $_POST['wpps_bf_editor_nonce'] ) ) {
			return;
		}
		if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'powersuite-basic-forms' ) {
			return;
		}

		$form_id = isset( $_POST['wpps_bf_form_id'] ) ? (int) $_POST['wpps_bf_form_id'] : 0;

		// From here on, surface every failure to the user instead of silently dropping the save.
		if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['wpps_bf_editor_nonce'] ) ), 'wpps_basic_forms_save' ) ) {
			$this->redirectAfterEditorSave( $form_id, 'nonce' );
		}
		$capability = apply_filters( 'wpps_basic_forms_capability', 'manage_options' );
		if ( ! current_user_can( $capability ) ) {
			$this->redirectAfterEditorSave( $form_id, 'cap' );
		}
		$title = isset( $_POST['wpps_bf_form_title'] ) ? sanitize_text_field( wp_unslash( $_POST['wpps_bf_form_title'] ) ) : '';
		if ( '' === $title ) {
			$this->redirectAfterEditorSave( $form_id, 'title' );
		}
		if ( $form_id > 0 ) {
			$form = get_post( $form_id );
			if ( ! $form || $form->post_type !== self::CPT || ! current_user_can( 'edit_post', $form_id ) ) {
				$this->redirectAfterEditorSave( $form_id, 'invalid' );
			}
			$result = wp_update_post(
				[
					'ID'          => $form_id,
					'post_title'  => $title,
					'post_status' => 'publish',
				],
				true
			);
			if ( is_wp_error( $result ) ) {
				$this->redirectAfterEditorSave( $form_id, 'save' );
			}
		} else {
			$form_id = wp_insert_post(
				[
					'post_type'   => self::CPT,
					'post_title'  => $title,
					'post_status' => 'publish',
				],
				true
			);
			if ( is_wp_error( $form_id ) ) {
				$this->redirectAfterEditorSave( 0, 'save' );
			}
		}
		if ( isset( $_POST['form_fields'] ) ) {
			$fields = json_decode( stripslashes( (string) $_POST['form_fields'] ), true );
			$existing_fields = $form_id > 0 ? get_post_meta( $form_id, '_form_fields', true ) : [];
			$existing_fields = is_array( $existing_fields ) ? $existing_fields : [];
			update_post_meta(
				$form_id,
				'_form_fields',
				is_array( $fields ) ? $this->sanitizeFormFields( $fields, $existing_fields ) : []
			);
		}
		if ( isset( $_POST['form_settings'] ) && is_array( $_POST['form_settings'] ) ) {
			$existing_settings = $form_id > 0 ? get_post_meta( $form_id, '_form_settings', true ) : [];
			$existing_settings = is_array( $existing_settings ) ? $existing_settings : [];
			$settings          = $this->sanitizeFormSettings( wp_unslash( $_POST['form_settings'] ) );
			update_post_meta( $form_id, '_form_settings', FeatureGate::preservePremiumFormSettingsOnSave( $settings, $existing_settings ) );
		}
		if ( isset( $_POST['form_design'] ) && is_array( $_POST['form_design'] ) ) {
			$design = $this->sanitizeAndNormalizeFormDesign( $form_id, wp_unslash( $_POST['form_design'] ) );
			update_post_meta( $form_id, '_form_design', $design );
		}
		wp_safe_redirect( admin_url( 'admin.php?page=powersuite-basic-forms&action=edit&form_id=' . $form_id . '&saved=1' ) );
		exit;
	}

	/**
	 * Redirect back to the editor with an error code so the user sees why a save failed.
	 *
	 * @param int    $form_id Form being edited (0 for a new form).
	 * @param string $code    Error code understood by the editor view.
	 */
	private function redirectAfterEditorSave( int $form_id, string $code ): void {
		$args = [
			'page' => 'powersuite-basic-forms',
		];
		if ( $form_id > 0 ) {
			$args['action']  = 'edit';
			$args['form_id'] = $form_id;
		} else {
			$args['action'] = 'add';
			$args['blank']  = '1';
		}
		$args['bf_error'] = $code;
		wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) );
		exit;
	}

	/**
	 * Merge posted design with stored meta and defaults, then clamp so admin inputs stay valid.
	 *
	 * @param array<string, mixed> $raw_design Raw POST form_design array.
	 * @return array<string, string>
	 */
	private function sanitizeAndNormalizeFormDesign( int $form_id, array $raw_design ): array {
		$incoming = [];
		foreach ( $raw_design as $k => $v ) {
			if ( ! is_string( $k ) ) {
				continue;
			}
			if ( 'custom_css' === $k ) {
				$incoming[ $k ] = $this->sanitizeCustomCss( is_scalar( $v ) ? (string) $v : '' );
				continue;
			}
			$incoming[ $k ] = sanitize_text_field( is_scalar( $v ) ? (string) $v : '' );
		}
		$existing = get_post_meta( $form_id, '_form_design', true );
		$existing_clean = [];
		if ( is_array( $existing ) ) {
			foreach ( $existing as $k => $v ) {
				if ( ! is_string( $k ) || is_array( $v ) ) {
					continue;
				}
				if ( 'custom_css' === $k ) {
					$existing_clean[ $k ] = $this->sanitizeCustomCss( (string) $v );
					continue;
				}
				$existing_clean[ $k ] = sanitize_text_field( (string) $v );
			}
		}
		$merged = wp_parse_args( $incoming, wp_parse_args( $existing_clean, DesignDefaults::get() ) );

		return FeatureGate::preserveFormDesignOnSave(
			DesignDefaults::normalizeForAdminForm( $merged ),
			$existing_clean
		);
	}

	/**
	 * Strip dangerous content from per-form custom CSS while preserving valid rules.
	 */
	private function sanitizeCustomCss( string $css ): string {
		if ( $css === '' ) {
			return '';
		}

		$css = preg_replace( '/<script\b[^>]*>(.*?)<\/script>/is', '', $css );
		$css = wp_strip_all_tags( $css, true );
		$css = preg_replace( '/javascript\s*:/i', '', $css );
		$css = preg_replace( '/expression\s*\(/i', '', $css );
		$css = preg_replace( '/@import/i', '', $css );
		$css = preg_replace( '/behavior\s*:/i', '', $css );
		$css = preg_replace( '/<\/?style\b/i', '', $css );

		$css = trim( $css );
		if ( strlen( $css ) > 10000 ) {
			$css = substr( $css, 0, 10000 );
		}

		return $css;
	}

	/**
	 * Sanitize the stored form schema before it can affect frontend output or validation messages.
	 *
	 * @param array<int, mixed> $fields Raw decoded field schema.
	 * @return array<int, array<string, mixed>>
	 */
	/**
	 * @param array<string, mixed> $raw_settings
	 * @return array<string, string>
	 */
	private function sanitizeFormSettings( array $raw_settings ): array {
		$text_fields = [
			'recipient',
			'subject',
			'redirect_url',
			'submit_button_text',
			'captcha_provider',
			'autoreply_subject',
		];
		$textarea_fields = [
			'success_message',
			'autoreply_message',
		];
		$checkbox_fields = [
			'enable_autoreply',
			'store_ip',
			'store_user_agent',
		];

		$settings = [];
		foreach ( $text_fields as $key ) {
			if ( isset( $raw_settings[ $key ] ) ) {
				$settings[ $key ] = sanitize_text_field( (string) $raw_settings[ $key ] );
			}
		}
		foreach ( $textarea_fields as $key ) {
			if ( isset( $raw_settings[ $key ] ) ) {
				$settings[ $key ] = sanitize_textarea_field( (string) $raw_settings[ $key ] );
			}
		}
		foreach ( $checkbox_fields as $key ) {
			$settings[ $key ] = ! empty( $raw_settings[ $key ] ) ? '1' : '';
		}

		$provider = $settings['captcha_provider'] ?? '';
		if ( ! in_array( $provider, [ '', 'simple', 'google', 'turnstile' ], true ) ) {
			$provider = '';
			$settings['captcha_provider'] = '';
		}

		// Honeypot + timing always enabled; CAPTCHA is optional via provider.
		$settings['enable_spam_protection'] = '1';

		if ( ! isset( $settings['store_ip'] ) ) {
			$settings['store_ip'] = '1';
		}
		if ( ! isset( $settings['store_user_agent'] ) ) {
			$settings['store_user_agent'] = '1';
		}

		return $settings;
	}

	private function sanitizeFormFields( array $fields, array $existing = [] ): array {
		$allowed_types = [ 'text', 'textarea', 'email', 'phone', 'number', 'select', 'radio', 'checkbox', 'file', 'date', 'time', 'datetime', 'url', 'hidden', 'password', 'html', 'separator', 'group' ];
		$sanitized     = [];

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

			$type = isset( $field['type'] ) ? sanitize_key( (string) $field['type'] ) : 'text';
			if ( ! in_array( $type, $allowed_types, true ) ) {
				$type = 'text';
			}

			$item = [
				'id'          => isset( $field['id'] ) ? sanitize_key( (string) $field['id'] ) : '',
				'type'        => $type,
				'label'       => isset( $field['label'] ) ? sanitize_text_field( (string) $field['label'] ) : '',
				'placeholder' => isset( $field['placeholder'] ) ? sanitize_text_field( (string) $field['placeholder'] ) : '',
				'description' => isset( $field['description'] ) ? sanitize_textarea_field( (string) $field['description'] ) : '',
				'options'     => isset( $field['options'] ) ? sanitize_textarea_field( (string) $field['options'] ) : '',
				'required'    => ! empty( $field['required'] ),
			];

			foreach ( $field as $key => $value ) {
				$key = sanitize_key( (string) $key );
				if ( $key === '' || array_key_exists( $key, $item ) || $key === 'content' || $key === 'show_if' ) {
					continue;
				}

				if ( is_bool( $value ) ) {
					$item[ $key ] = $value;
				} elseif ( is_int( $value ) || is_float( $value ) ) {
					$item[ $key ] = $value;
				} elseif ( is_string( $value ) ) {
					$item[ $key ] = in_array( $key, [ 'options', 'description' ], true )
						? sanitize_textarea_field( $value )
						: sanitize_text_field( $value );
				} elseif ( is_array( $value ) ) {
					$item[ $key ] = $this->sanitizeScalarArray( $value );
				}
			}

			if ( $type === 'html' ) {
				$item['content'] = isset( $field['content'] ) ? wp_kses_post( (string) $field['content'] ) : '';
			}

			if ( isset( $field['show_if'] ) && is_array( $field['show_if'] ) ) {
				$normalized_show_if = ConditionalFields::normalizeShowIf( $field['show_if'] );
				if ( $normalized_show_if !== null ) {
					$item['show_if'] = $normalized_show_if;
				}
			}

			$sanitized[] = $item;
		}

		return FeatureGate::applyFieldTierLimits( $sanitized, $existing );
	}

	/**
	 * @param array<mixed> $values
	 * @return array<mixed>
	 */
	private function sanitizeScalarArray( array $values ): array {
		$out = [];
		foreach ( $values as $key => $value ) {
			$clean_key = is_string( $key ) ? sanitize_key( $key ) : (int) $key;
			if ( is_array( $value ) ) {
				$out[ $clean_key ] = $this->sanitizeScalarArray( $value );
			} elseif ( is_bool( $value ) || is_int( $value ) || is_float( $value ) ) {
				$out[ $clean_key ] = $value;
			} elseif ( is_scalar( $value ) ) {
				$out[ $clean_key ] = sanitize_text_field( (string) $value );
			}
		}
		return $out;
	}

	private function renderFormEditorView( ?\WP_Post $form ): void {
		$fields = [];
		$settings = [];
		$design = [];
		if ( $form && $form->ID > 0 ) {
			$fields = get_post_meta( $form->ID, '_form_fields', true );
			$fields = is_array( $fields ) ? $fields : [];
			$settings = get_post_meta( $form->ID, '_form_settings', true );
			$design = get_post_meta( $form->ID, '_form_design', true );
		}
		$base = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		$deps = wp_style_is( 'wp-powersuite-admin', 'registered' ) ? [ 'wp-powersuite-admin' ] : [];
		wp_enqueue_style( 'wpps-bf-admin', $base . 'modules/basic-forms/assets/admin.css', $deps, $this->assetVersion( 'assets/admin.css' ) );
		include __DIR__ . '/views/form-editor.php';
	}

	public function renderAdminPage(): void {
		$capability = apply_filters( 'wpps_basic_forms_capability', 'manage_options' );
		if ( ! current_user_can( $capability ) ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-powersuite' ) );
		}

		$view = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : 'forms';

		if ( $view === 'entries' ) {
			$this->renderEntriesView();
			return;
		}

		if ( $view === 'settings' ) {
			$this->renderSettingsView();
			return;
		}

		if ( isset( $_GET['action'] ) && $_GET['action'] === 'delete' && isset( $_GET['form_id'] ) ) {
			$form_id = (int) $_GET['form_id'];
			if ( wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'delete_form_' . $form_id ) ) {
				wp_delete_post( $form_id, true );
				$this->deleteEntriesForFormId( $form_id );
				echo '<div class="notice notice-success"><p>' . esc_html__( 'Form deleted successfully.', 'wp-powersuite' ) . '</p></div>';
			}
		}

		// Dedicated form editor page (not WordPress post editor)
		if ( isset( $_GET['action'] ) && $_GET['action'] === 'add' ) {
			if ( isset( $_GET['blank'] ) && '1' === $_GET['blank'] ) {
				$this->renderFormEditorView( null );
				return;
			}
			$this->renderTemplatePickerView();
			return;
		}
		if ( isset( $_GET['action'] ) && $_GET['action'] === 'edit' && isset( $_GET['form_id'] ) ) {
			$form_id = (int) $_GET['form_id'];
			if ( $form_id > 0 ) {
				$form = get_post( $form_id );
				if ( $form && $form->post_type === self::CPT ) {
					$this->renderFormEditorView( $form );
					return;
				}
			}
		}

		$this->renderFormsView();
	}

	private function renderSettingsView(): void {
		$base = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		$deps = wp_style_is( 'wp-powersuite-admin', 'registered' ) ? [ 'wp-powersuite-admin' ] : [];
		wp_enqueue_style( 'wpps-bf-admin', $base . 'modules/basic-forms/assets/admin.css', $deps, $this->assetVersion( 'assets/admin.css' ) );
		include __DIR__ . '/views/admin-settings.php';
	}

	private function renderFormsView(): void {
		global $wpdb;

		$bulk_action = isset( $_REQUEST['action'] ) && $_REQUEST['action'] !== '-1' ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : ( isset( $_REQUEST['action2'] ) && $_REQUEST['action2'] !== '-1' ? sanitize_text_field( wp_unslash( $_REQUEST['action2'] ) ) : '' );
		$bulk_ids = isset( $_REQUEST['form'] ) && is_array( $_REQUEST['form'] ) ? array_map( 'absint', wp_unslash( $_REQUEST['form'] ) ) : [];
		$bulk_ids = array_filter( $bulk_ids );
		$bulk_nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';

		if ( $bulk_action && ! empty( $bulk_ids ) && wp_verify_nonce( $bulk_nonce, 'bulk-forms' ) && current_user_can( 'manage_options' ) ) {
			$done = 0;
			foreach ( $bulk_ids as $form_id ) {
				if ( $bulk_action === 'delete' && $form_id > 0 ) {
					$post = get_post( $form_id );
					if ( $post && $post->post_type === self::CPT ) {
						wp_delete_post( $form_id, true );
						$this->deleteEntriesForFormId( $form_id );
						$done++;
					}
				}
			}
			$redirect = add_query_arg( [ 'page' => 'powersuite-basic-forms', 'forms_deleted' => $done ], admin_url( 'admin.php' ) );
			wp_safe_redirect( $redirect );
			exit;
		}

		$exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $this->table_name ) ) === $this->table_name;
		if ( ! $exists ) {
			$this->createEntriesTable();
		}

		// Count every form shown in the list below (any status except trash /
		// auto-draft), so the headline number matches the table.
		$total_forms = (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status NOT IN ( 'trash', 'auto-draft' )",
			self::CPT
		) );
		$total_entries = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$this->table_name}" );
		$entries_today = (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$this->table_name} WHERE DATE(date_created) = %s",
			current_time( 'Y-m-d' )
		) );
		// date_created is stored in site-local time, so anchor the window to local time too.
		$entries_week = (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$this->table_name} WHERE date_created >= %s",
			gmdate( 'Y-m-d H:i:s', strtotime( '-7 days', strtotime( current_time( 'mysql' ) ) ) )
		) );

		$recent_entries = $wpdb->get_results( $wpdb->prepare(
			"SELECT e.*, p.post_title as form_name FROM {$this->table_name} e LEFT JOIN {$wpdb->posts} p ON e.form_id = p.ID ORDER BY e.date_created DESC LIMIT %d",
			5
		) );

		$top_forms = $wpdb->get_results(
			"SELECT p.ID, p.post_title, COUNT(e.id) as entry_count FROM {$wpdb->posts} p LEFT JOIN {$this->table_name} e ON p.ID = e.form_id WHERE p.post_type = '" . esc_sql( self::CPT ) . "' AND p.post_status = 'publish' GROUP BY p.ID ORDER BY entry_count DESC LIMIT 5"
		);

		$base = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		$deps = wp_style_is( 'wp-powersuite-admin', 'registered' ) ? [ 'wp-powersuite-admin' ] : [];
		wp_enqueue_style( 'wpps-bf-admin', $base . 'modules/basic-forms/assets/admin.css', $deps, $this->assetVersion( 'assets/admin.css' ) );

		$forms_deleted = isset( $_GET['forms_deleted'] ) ? (int) $_GET['forms_deleted'] : 0;

		include __DIR__ . '/views/admin-forms.php';
	}

	private function renderEntriesView(): void {
		global $wpdb;

		$bulk_action = isset( $_REQUEST['action'] ) && $_REQUEST['action'] !== '-1' ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : ( isset( $_REQUEST['action2'] ) && $_REQUEST['action2'] !== '-1' ? sanitize_text_field( wp_unslash( $_REQUEST['action2'] ) ) : '' );
		$bulk_ids = isset( $_REQUEST['entry'] ) && is_array( $_REQUEST['entry'] ) ? array_map( 'absint', wp_unslash( $_REQUEST['entry'] ) ) : [];
		$bulk_ids = array_filter( $bulk_ids );
		$bulk_nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';

		if ( $bulk_action && ! empty( $bulk_ids ) && wp_verify_nonce( $bulk_nonce, 'bulk-entries' ) && current_user_can( 'manage_options' ) ) {
			$filter_form = isset( $_REQUEST['form_filter'] ) ? (int) $_REQUEST['form_filter'] : 0;
			$filter_status = isset( $_REQUEST['status_filter'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['status_filter'] ) ) : '';
			$done = 0;
			foreach ( $bulk_ids as $entry_id ) {
				if ( $entry_id > 0 ) {
					if ( $bulk_action === 'delete' ) {
						$this->deleteEntryById( $entry_id );
						$done++;
					} elseif ( $bulk_action === 'mark_read' ) {
						$wpdb->update( $this->table_name, [ 'status' => 'read' ], [ 'id' => $entry_id ], [ '%s' ], [ '%d' ] );
						$done++;
					} elseif ( $bulk_action === 'mark_unread' ) {
						$wpdb->update( $this->table_name, [ 'status' => 'unread' ], [ 'id' => $entry_id ], [ '%s' ], [ '%d' ] );
						$done++;
					}
				}
			}
			$redirect = add_query_arg( [ 'page' => 'powersuite-basic-forms', 'view' => 'entries' ], admin_url( 'admin.php' ) );
			if ( $filter_form ) {
				$redirect = add_query_arg( 'form_filter', $filter_form, $redirect );
			}
			if ( $filter_status ) {
				$redirect = add_query_arg( 'status_filter', $filter_status, $redirect );
			}
			$redirect = add_query_arg( 'entries_updated', $done . '_' . $bulk_action, $redirect );
			wp_safe_redirect( $redirect );
			exit;
		}

		if ( isset( $_GET['mark_read_entry'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'mark_read_entry_' . (int) $_GET['mark_read_entry'] ) ) {
			$wpdb->update( $this->table_name, [ 'status' => 'read' ], [ 'id' => (int) $_GET['mark_read_entry'] ], [ '%s' ], [ '%d' ] );
			echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Entry marked as read.', 'wp-powersuite' ) . '</p></div>';
		}
		if ( isset( $_GET['mark_unread_entry'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'mark_unread_entry_' . (int) $_GET['mark_unread_entry'] ) ) {
			$wpdb->update( $this->table_name, [ 'status' => 'unread' ], [ 'id' => (int) $_GET['mark_unread_entry'] ], [ '%s' ], [ '%d' ] );
			echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Entry marked as unread.', 'wp-powersuite' ) . '</p></div>';
		}
		if ( isset( $_GET['delete_entry'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'delete_entry_' . (int) $_GET['delete_entry'] ) ) {
			$this->deleteEntryById( (int) $_GET['delete_entry'] );
			echo '<div class="notice notice-success"><p>' . esc_html__( 'Entry deleted.', 'wp-powersuite' ) . '</p></div>';
		}

		$base = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		$deps = wp_style_is( 'wp-powersuite-admin', 'registered' ) ? [ 'wp-powersuite-admin' ] : [];
		wp_enqueue_style( 'wpps-bf-admin', $base . 'modules/basic-forms/assets/admin.css', $deps, $this->assetVersion( 'assets/admin.css' ) );
		wp_localize_script( 'jquery', 'wppsBasicFormsData', [
			'nonce'           => wp_create_nonce( 'wpps_basic_forms_view_entry' ),
			'ajaxUrl'         => admin_url( 'admin-ajax.php' ),
		] );

		$entries_updated = isset( $_GET['entries_updated'] ) ? sanitize_text_field( wp_unslash( $_GET['entries_updated'] ) ) : '';

		include __DIR__ . '/views/admin-entries.php';
	}

	/** @param array $atts */
	public function renderFormShortcode( $atts ): string {
		$atts = shortcode_atts( [ 'id' => 0 ], $atts, 'basic_form' );
		$form_id = (int) $atts['id'];

		if ( ! $form_id ) {
			return '<p>' . esc_html__( 'Invalid form ID.', 'wp-powersuite' ) . '</p>';
		}

		$form = get_post( $form_id );
		if ( ! $form || $form->post_type !== self::CPT || $form->post_status !== 'publish' ) {
			return '<p>' . esc_html__( 'Form not found.', 'wp-powersuite' ) . '</p>';
		}

		$fields = get_post_meta( $form_id, '_form_fields', true );
		if ( empty( $fields ) || ! is_array( $fields ) ) {
			return '<p>' . esc_html__( 'This form has no fields yet.', 'wp-powersuite' ) . '</p>';
		}

		$settings = get_post_meta( $form_id, '_form_settings', true );
		$defaults = [
			'submit_button_text'     => __( 'Submit', 'wp-powersuite' ),
			'enable_spam_protection'  => true,
			'captcha_provider'       => 'simple',
		];
		$settings = wp_parse_args( is_array( $settings ) ? $settings : [], $defaults );

		$design = get_post_meta( $form_id, '_form_design', true );
		$design = wp_parse_args( is_array( $design ) ? $design : [], \BasicForms\DesignDefaults::get() );

		$base = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		wp_enqueue_style( 'wpps-bf-frontend', $base . 'modules/basic-forms/assets/frontend.css', [], $this->assetVersion( 'assets/frontend.css' ) );

		$custom_css = isset( $design['custom_css'] ) ? $this->sanitizeCustomCss( (string) $design['custom_css'] ) : '';
		if ( $custom_css !== '' ) {
			wp_add_inline_style( 'wpps-bf-frontend', $custom_css );
		}


		if ( class_exists( 'WP_PowerSuite\Admin\TypographyHelper' ) ) {
			$fonts_to_load = [];
			foreach ( [ 'label_font_family', 'field_font_family', 'placeholder_font_family' ] as $key ) {
				$stack = $design[ $key ] ?? '';
				if ( $stack !== '' ) {
					$name = \WP_PowerSuite\Admin\TypographyHelper::getGoogleFontNameFromStack( $stack );
					if ( $name !== null ) {
						$fonts_to_load[ $name ] = true;
					}
				}
			}
			if ( ! empty( $fonts_to_load ) ) {
				$families = array_map( function ( $n ) {
					return 'family=' . str_replace( ' ', '+', $n ) . ':wght@300;400;500;600;700';
				}, array_keys( $fonts_to_load ) );
				$url = 'https://fonts.googleapis.com/css2?' . implode( '&', $families ) . '&display=swap';
				wp_enqueue_style( 'wpps-bf-google-fonts', $url, [ 'wpps-bf-frontend' ], null );
			}
		}
		wp_enqueue_script( 'jquery' );
		wp_enqueue_script(
			'wpps-bf-frontend',
			$base . 'modules/basic-forms/assets/frontend.js',
			[ 'jquery' ],
			$this->assetVersion( 'assets/frontend.js' ),
			true
		);

		static $localized = false;
		if ( ! $localized ) {
			wp_localize_script(
				'wpps-bf-frontend',
				'wppsBfFrontendData',
				[
					'ajaxUrl' => admin_url( 'admin-ajax.php' ),
				]
			);
			$localized = true;
		}

		wp_add_inline_script(
			'wpps-bf-frontend',
			'window.wppsBfFrontendData=window.wppsBfFrontendData||{};window.wppsBfFrontendData[' . (int) $form_id . ']=' . wp_json_encode(
				[
					'ajaxUrl'          => admin_url( 'admin-ajax.php' ),
					'submitText'       => $settings['submit_button_text'] ?? __( 'Submit', 'wp-powersuite' ),
					'sendingText'      => __( 'Submitting...', 'wp-powersuite' ),
					'redirectText'     => __( 'Submitting...', 'wp-powersuite' ),
					'errorText'        => __( 'Unable to submit the form. Please check your entries and try again.', 'wp-powersuite' ),
					'networkErrorText' => __( 'Unable to reach the server. Check your connection and try again.', 'wp-powersuite' ),
				]
			) . ';',
			'before'
		);

		ob_start();
		include __DIR__ . '/views/form-frontend.php';
		return ob_get_clean();
	}

	public function handleFormSubmission(): void {
		$form_id = isset( $_POST['form_id'] ) ? (int) $_POST['form_id'] : 0;
		if ( ! $form_id ) {
			wp_send_json_error( [ 'message' => __( 'Invalid form.', 'wp-powersuite' ) ] );
		}

		if ( ! isset( $_POST['basic_form_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['basic_form_nonce'] ) ), 'submit_basic_form_' . $form_id ) ) {
			wp_send_json_error( [ 'message' => __( 'Security check failed.', 'wp-powersuite' ) ] );
		}

		$form = get_post( $form_id );
		if ( ! $form || $form->post_type !== self::CPT || $form->post_status !== 'publish' ) {
			wp_send_json_error( [ 'message' => __( 'Form not found.', 'wp-powersuite' ) ] );
		}

		if ( $this->isSubmissionRateLimited( $form_id ) ) {
			wp_send_json_error( [ 'message' => __( 'Too many submissions. Please try again later.', 'wp-powersuite' ) ] );
		}

		$settings = get_post_meta( $form_id, '_form_settings', true );
		$fields = get_post_meta( $form_id, '_form_fields', true );
		$settings = wp_parse_args( is_array( $settings ) ? $settings : [], [ 'enable_spam_protection' => '1' ] );
		$fields = is_array( $fields ) ? $fields : [];

		$has_file_fields = $this->formHasFileFields( $fields );
		if ( $has_file_fields ) {
			$captcha_provider = $settings['captcha_provider'] ?? '';
			if ( $captcha_provider === '' ) {
				wp_send_json_error(
					[
						'message' => __( 'File uploads require a CAPTCHA provider on this form. Configure one in the form settings.', 'wp-powersuite' ),
					]
				);
			}

			$verified = apply_filters( 'wpps_verify_basic_forms_captcha', false, $form_id, $settings );
			if ( ! $verified ) {
				$module_id_for_msg = match ( $captcha_provider ) {
					'google' => 'google-recaptcha',
					'turnstile' => 'cloudflare-turnstile',
					'simple' => 'simple-captcha',
					default => '',
				};
				$msg = apply_filters(
					'wpps_captcha_error_message',
					__( 'Please complete the verification before uploading files.', 'wp-powersuite' ),
					'basic_forms',
					$module_id_for_msg
				);
				wp_send_json_error( [ 'message' => $msg ] );
			}

			if ( $this->isFileUploadRateLimited( $form_id ) ) {
				wp_send_json_error(
					[
						'message' => __( 'Too many upload attempts. Please try again later.', 'wp-powersuite' ),
					]
				);
			}

			$this->recordFileUploadAttempt( $form_id );
		}

		if ( ! empty( $settings['enable_spam_protection'] ) ) {
			// Require honeypot + timing fields so bots cannot bypass by omitting them.
			$hp_name = isset( $_POST['wpps_bf_honeypot_name'] ) ? sanitize_text_field( wp_unslash( $_POST['wpps_bf_honeypot_name'] ) ) : '';
			if ( $hp_name === '' || ! preg_match( '/^wpps_bf_\d+_url$/', $hp_name ) ) {
				wp_send_json_error( [ 'message' => __( 'Security check failed. Please refresh the page and try again.', 'wp-powersuite' ) ] );
			}

			$hp_value = isset( $_POST[ $hp_name ] ) ? trim( (string) wp_unslash( $_POST[ $hp_name ] ) ) : '';
			if ( $hp_value !== '' ) {
				wp_send_json_error( [ 'message' => __( 'Security check failed.', 'wp-powersuite' ) ] );
			}

			$timing_token = isset( $_POST['wpps_bf_timing_token'] ) ? sanitize_text_field( wp_unslash( $_POST['wpps_bf_timing_token'] ) ) : '';
			if ( $timing_token === '' ) {
				wp_send_json_error( [ 'message' => __( 'Security check failed. Please refresh the page and try again.', 'wp-powersuite' ) ] );
			}

			$render_time = get_transient( 'wpps_bf_t_' . $timing_token );
			if ( $render_time === false ) {
				wp_send_json_error( [ 'message' => __( 'Security check failed. Please refresh the page and try again.', 'wp-powersuite' ) ] );
			}
			$min_seconds = apply_filters( 'wpps_basic_forms_min_submit_seconds', 3, $form_id );
			if ( is_numeric( $render_time ) && ( time() - (int) $render_time ) < $min_seconds ) {
				wp_send_json_error( [ 'message' => __( 'Please wait a moment before submitting.', 'wp-powersuite' ) ] );
			}

			$captcha_provider = $settings['captcha_provider'] ?? '';
			if ( $captcha_provider !== '' ) {
				$verified = apply_filters( 'wpps_verify_basic_forms_captcha', false, $form_id, $settings );
				if ( ! $verified ) {
					$module_id_for_msg = match ( $captcha_provider ) {
						'google' => 'google-recaptcha',
						'turnstile' => 'cloudflare-turnstile',
						'simple' => 'simple-captcha',
						default => '',
					};
					$msg = apply_filters(
						'wpps_captcha_error_message',
						__( 'Please complete the verification.', 'wp-powersuite' ),
						'basic_forms',
						$module_id_for_msg
					);
					wp_send_json_error( [ 'message' => $msg ] );
				}
			}
		}

		$data = [];
		$user_email = '';
		$user_name = '';
		$errors = [];
		$post_data  = isset( $_POST ) && is_array( $_POST ) ? wp_unslash( $_POST ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized per field below.

		foreach ( $fields as $field ) {
			if ( in_array( (string) ( $field['type'] ?? '' ), [ 'separator', 'group', 'html' ], true ) ) {
				continue;
			}

			if ( ! ConditionalFields::isVisible( $field, $fields, $post_data ) ) {
				continue;
			}

			$field_id   = 'field_' . ( $field['id'] ?? '' );
			$field_type = (string) ( $field['type'] ?? 'text' );
			$value      = '';

			if ( $field_type === 'file' && ! empty( $_FILES[ $field_id ]['name'] ) ) {
				$uploaded = $this->handleFileUpload( $form_id, $field_id, $field );
				if ( is_wp_error( $uploaded ) ) {
					$errors[] = ( $field['label'] ?? '' ) . ': ' . $uploaded->get_error_message();
					continue;
				}
				$value = $uploaded;
			} elseif ( $field_type === 'file' ) {
				if ( ! empty( $field['required'] ) ) {
					$errors[] = sprintf( __( '%s is required.', 'wp-powersuite' ), sanitize_text_field( (string) ( $field['label'] ?? '' ) ) );
				}
				continue;
			} elseif ( $field_type === 'phone' ) {
				$value = SubmissionValidator::resolvePhoneValue( $field, $field_id );
				[ $value, $error ] = SubmissionValidator::validateField( $field, $value );
				if ( $error ) {
					$errors[] = $error;
					continue;
				}
			} else {
				$raw = isset( $_POST[ $field_id ] ) ? $_POST[ $field_id ] : '';
				[ $value, $error ] = SubmissionValidator::validateField( $field, $raw );
				if ( $error ) {
					$errors[] = $error;
					continue;
				}
			}

			if ( $field_type === 'email' && empty( $user_email ) && is_string( $value ) && is_email( $value ) ) {
				$user_email = sanitize_email( $value );
			}
			if ( strtolower( (string) ( $field['label'] ?? '' ) ) === 'name' && empty( $user_name ) && is_string( $value ) ) {
				$user_name = sanitize_text_field( $value );
			}

			$base_label = $field['label'] ?? '';
			if ( $base_label === '' ) {
				$base_label = sprintf( __( 'Field %s', 'wp-powersuite' ), $field['id'] ?? '' );
			}
			$key = sanitize_text_field( (string) $base_label );
			$n = 1;
			while ( array_key_exists( $key, $data ) ) {
				$n++;
				$key = sanitize_text_field( (string) $base_label ) . ' (' . $n . ')';
			}
			$data[ $key ] = $value;
		}

		$data = apply_filters( 'wpps_basic_forms_entry_data', $data, $form_id, $settings );

		if ( ! empty( $errors ) ) {
			wp_send_json_error( [ 'message' => implode( "\n", array_map( 'wp_strip_all_tags', $errors ) ) ] );
		}

		$this->recordSubmissionAttempt( $form_id );

		$new_timing_token = '';
		if ( ! empty( $settings['enable_spam_protection'] ) ) {
			$timing_token = isset( $_POST['wpps_bf_timing_token'] ) ? sanitize_text_field( wp_unslash( $_POST['wpps_bf_timing_token'] ) ) : '';
			if ( $timing_token !== '' ) {
				delete_transient( 'wpps_bf_t_' . $timing_token );
			}
			// Issue a fresh token so AJAX success + form.reset() can resubmit without a page refresh.
			$new_timing_token = $this->mintTimingToken();
		}

		$store_ip = ! isset( $settings['store_ip'] ) || ! empty( $settings['store_ip'] );
		$store_ua = ! isset( $settings['store_user_agent'] ) || ! empty( $settings['store_user_agent'] );
		$user_agent = $store_ua && isset( $_SERVER['HTTP_USER_AGENT'] )
			? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
			: '';

		$GLOBALS['wpdb']->insert(
			$this->table_name,
			[
				'form_id'      => $form_id,
				'date_created' => current_time( 'mysql' ),
				'status'       => 'unread',
				'user_ip'      => $store_ip ? $this->getClientIp() : '',
				'user_agent'   => $user_agent,
				'payload'      => wp_json_encode( $data ),
				'user_email'   => $user_email,
				'user_name'    => $user_name,
			],
			[ '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ]
		);

		$entry_id = (int) $GLOBALS['wpdb']->insert_id;
		$this->sendAdminNotification( $form_id, $entry_id, $data, $settings );
		if ( ! empty( $settings['enable_autoreply'] ) && $user_email ) {
			$this->sendAutoreply( $user_email, $settings );
		}

		$success_message = $settings['success_message'] ?? __( 'Thank you! Your submission has been received.', 'wp-powersuite' );
		$redirect_url = ! empty( $settings['redirect_url'] )
			? wp_validate_redirect( esc_url_raw( $settings['redirect_url'] ), '' )
			: '';

		$payload = [
			'message'      => wp_strip_all_tags( $success_message ),
			'redirect_url' => $redirect_url,
		];
		if ( $new_timing_token !== '' ) {
			$payload['timing_token'] = $new_timing_token;
		}

		wp_send_json_success( $payload );
	}

	/**
	 * Create a one-time timing token used by spam protection.
	 */
	private function mintTimingToken(): string {
		$timing_token = wp_generate_password( 20, false, false );
		set_transient( 'wpps_bf_t_' . $timing_token, time(), HOUR_IN_SECONDS );

		return $timing_token;
	}

	public function ajaxViewEntry(): void {
		check_ajax_referer( 'wpps_basic_forms_view_entry', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}

		$entry_id = isset( $_POST['entry_id'] ) ? (int) $_POST['entry_id'] : 0;
		$entry = $GLOBALS['wpdb']->get_row( $GLOBALS['wpdb']->prepare( "SELECT * FROM {$this->table_name} WHERE id = %d", $entry_id ) );
		if ( ! $entry ) {
			wp_send_json_error( [ 'message' => __( 'Entry not found.', 'wp-powersuite' ) ] );
		}

		$GLOBALS['wpdb']->update( $this->table_name, [ 'status' => 'read' ], [ 'id' => $entry_id ], [ '%s' ], [ '%d' ] );

		$form = get_post( (int) $entry->form_id );
		$data = json_decode( $entry->payload ?? '{}', true );
		$data = is_array( $data ) ? $data : [];

		ob_start();
		include __DIR__ . '/views/entry-detail.php';
		$html = ob_get_clean();

		wp_send_json_success( [ 'html' => $html ] );
	}

	public function ajaxSaveDesign(): void {
		if ( ! $this->canManageForms() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}
		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'wpps_basic_forms_save' ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid security token.', 'wp-powersuite' ) ] );
		}
		$form_id = isset( $_POST['form_id'] ) ? (int) $_POST['form_id'] : 0;
		if ( $form_id <= 0 ) {
			wp_send_json_error( [ 'message' => __( 'Please save the form first.', 'wp-powersuite' ) ] );
		}
		$form = get_post( $form_id );
		if ( ! $form || $form->post_type !== self::CPT || ! current_user_can( 'edit_post', $form_id ) ) {
			wp_send_json_error( [ 'message' => __( 'Form not found or access denied.', 'wp-powersuite' ) ] );
		}
		if ( ! FeatureGate::formDesignEnabled() ) {
			wp_send_json_error(
				[
					'message' => __( 'Form Design is a Premium feature. Activate your license to customize colors, layout, and styling.', 'wp-powersuite' ),
					'code'    => 'premium_required',
				],
				403
			);
		}
		if ( ! isset( $_POST['form_design'] ) || ! is_array( $_POST['form_design'] ) ) {
			wp_send_json_error( [ 'message' => __( 'No design data received.', 'wp-powersuite' ) ] );
		}
		$design = $this->sanitizeAndNormalizeFormDesign( $form_id, wp_unslash( $_POST['form_design'] ) );
		update_post_meta( $form_id, '_form_design', $design );
		wp_send_json_success( [
			'message'     => __( 'Design saved.', 'wp-powersuite' ),
			'form_design' => $design,
		] );
	}

	/**
	 * @param array<int, array<string, mixed>> $fields
	 */
	private function formHasFileFields( array $fields ): bool {
		foreach ( $fields as $field ) {
			if ( ( $field['type'] ?? '' ) === 'file' ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @param array<string, string> $uploads WordPress upload directory array.
	 * @return array<string, string>
	 */
	public function filterFormUploadDir( array $uploads ): array {
		return FormFileStorage::filterUploadDir( $uploads, $this->upload_form_id );
	}

	public function ajaxDownloadFile(): void {
		FormFileStorage::handleDownloadRequest();
	}

	private function handleFileUpload( int $form_id, string $field_id, array $field ): \WP_Error|array {
		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/image.php';
		require_once ABSPATH . 'wp-admin/includes/media.php';

		FormFileStorage::ensureUploadRoot();

		$max_size = ( ! empty( $field['max_size'] ) ? (int) $field['max_size'] : 5 ) * 1048576;
		$allowed_mimes = $this->getAllowedMimesForField( $field );
		$uploaded = [];

		$this->upload_form_id = $form_id;
		add_filter( 'upload_dir', [ $this, 'filterFormUploadDir' ] );

		if ( is_array( $_FILES[ $field_id ]['name'] ?? null ) ) {
			foreach ( array_keys( $_FILES[ $field_id ]['name'] ) as $i ) {
				if ( empty( $_FILES[ $field_id ]['name'][ $i ] ) ) {
					continue;
				}
				$file = [
					'name'     => $_FILES[ $field_id ]['name'][ $i ],
					'type'     => $_FILES[ $field_id ]['type'][ $i ],
					'tmp_name' => $_FILES[ $field_id ]['tmp_name'][ $i ],
					'error'    => $_FILES[ $field_id ]['error'][ $i ],
					'size'     => $_FILES[ $field_id ]['size'][ $i ],
				];
				if ( $file['size'] > $max_size ) {
					remove_filter( 'upload_dir', [ $this, 'filterFormUploadDir' ] );
					$this->upload_form_id = 0;
					return new \WP_Error( 'file_too_large', __( 'File size exceeds limit.', 'wp-powersuite' ) );
				}
				$result = wp_handle_upload( $file, [ 'test_form' => false, 'mimes' => $allowed_mimes ] );
				if ( isset( $result['error'] ) ) {
					remove_filter( 'upload_dir', [ $this, 'filterFormUploadDir' ] );
					$this->upload_form_id = 0;
					return new \WP_Error( 'upload_failed', $result['error'] );
				}
				$uploaded[] = FormFileStorage::createReference(
					$result['file'],
					$file['name'] ?? basename( $result['file'] )
				);
			}
		} else {
			if ( ( $_FILES[ $field_id ]['size'] ?? 0 ) > $max_size ) {
				remove_filter( 'upload_dir', [ $this, 'filterFormUploadDir' ] );
				$this->upload_form_id = 0;
				return new \WP_Error( 'file_too_large', __( 'File size exceeds limit.', 'wp-powersuite' ) );
			}
			$result = wp_handle_upload( $_FILES[ $field_id ], [ 'test_form' => false, 'mimes' => $allowed_mimes ] );
			if ( isset( $result['error'] ) ) {
				remove_filter( 'upload_dir', [ $this, 'filterFormUploadDir' ] );
				$this->upload_form_id = 0;
				return new \WP_Error( 'upload_failed', $result['error'] );
			}
			$uploaded[] = FormFileStorage::createReference(
				$result['file'],
				(string) ( $_FILES[ $field_id ]['name'] ?? basename( $result['file'] ) )
			);
		}

		remove_filter( 'upload_dir', [ $this, 'filterFormUploadDir' ] );
		$this->upload_form_id = 0;

		return $uploaded;
	}

	private function sendAdminNotification( int $form_id, int $entry_id, array $data, array $settings ): void {
		$form = get_post( $form_id );
		$recipient = ! empty( $settings['recipient'] ) && is_email( $settings['recipient'] ) ? $settings['recipient'] : get_option( 'admin_email' );
		$subject = $settings['subject'] ?? sprintf( __( 'New form submission from %s', 'wp-powersuite' ), get_bloginfo( 'name' ) );

		$content  = EmailComponents::paragraph(
			esc_html( $form->post_title ?? '' ),
			[ 'size' => EmailDesign::FONT_SUBHEAD, 'muted' => true, 'margin' => '0 0 14px' ]
		);
		foreach ( $data as $label => $value ) {
			$display = FormFileStorage::normalizeReferences( $value ) !== []
				? FormFileStorage::formatValueForAdmin( $value, $entry_id )
				: nl2br( esc_html( is_array( $value ) ? implode( ', ', $value ) : (string) $value ) );
			$content .= EmailComponents::formFieldRow( (string) $label, $display );
		}
		$content .= EmailComponents::paragraph(
			esc_html( sprintf( __( 'Submitted on %s', 'wp-powersuite' ), current_time( 'F j, Y g:i a' ) ) ),
			[ 'size' => EmailDesign::FONT_SMALL, 'muted' => true, 'margin' => '14px 0 0' ]
		);

		$message = EmailRenderer::wrap(
			[
				'title'        => __( 'New Form Submission', 'wp-powersuite' ),
				'content'      => $content,
				'footer_brand' => sprintf(
					/* translators: 1: site name, 2: plugin name */
					__( 'Sent from %1$s via %2$s Basic Forms', 'wp-powersuite' ),
					get_bloginfo( 'name' ),
					function_exists( 'wp_powersuite_get_plugin_name' ) ? wp_powersuite_get_plugin_name() : __( 'WP PowerSuite', 'wp-powersuite' )
				),
			]
		);

		$headers = EmailRenderer::headers();
		foreach ( $data as $v ) {
			if ( is_string( $v ) && is_email( $v ) ) {
				$headers[] = 'Reply-To: ' . sanitize_email( $v );
				break;
			}
		}
		wp_mail( $recipient, $subject, $message, $headers );
	}

	private function sendAutoreply( string $email, array $settings ): void {
		$subject      = $settings['autoreply_subject'] ?? __( 'Thank you for your submission', 'wp-powersuite' );
		$plain_body   = $settings['autoreply_message'] ?? __( 'We have received your submission and will get back to you soon.', 'wp-powersuite' );
		$content      = EmailComponents::paragraph( nl2br( esc_html( $plain_body ) ) );
		$html_body    = EmailRenderer::wrap(
			[
				'title'        => __( 'Thank you', 'wp-powersuite' ),
				'content'      => $content,
				'footer_brand' => sprintf(
					/* translators: 1: site name, 2: plugin name */
					__( 'Sent from %1$s via %2$s Basic Forms', 'wp-powersuite' ),
					get_bloginfo( 'name' ),
					function_exists( 'wp_powersuite_get_plugin_name' ) ? wp_powersuite_get_plugin_name() : __( 'WP PowerSuite', 'wp-powersuite' )
				),
			]
		);
		$headers = EmailRenderer::headers();

		EmailRenderer::withPlainAltBody(
			$plain_body,
			static function () use ( $email, $subject, $html_body, $headers ): bool {
				return wp_mail( $email, $subject, $html_body, $headers );
			}
		);
	}

	private function getFileUploadRateLimitKey( int $form_id ): string {
		$ip = $this->getClientIp();
		$ip = (string) apply_filters( 'wp_powersuite_basic_forms_upload_rate_limit_ip', $ip, $form_id );

		return 'wpps_bf_up_' . md5( $form_id . '|' . $ip );
	}

	private function getSubmissionRateLimitKey( int $form_id ): string {
		$ip = $this->getClientIp();
		$ip = (string) apply_filters( 'wp_powersuite_basic_forms_submission_rate_limit_ip', $ip, $form_id );

		return 'wpps_bf_sub_' . md5( $form_id . '|' . $ip );
	}

	private function isSubmissionRateLimited( int $form_id ): bool {
		$limit = (int) apply_filters( 'wp_powersuite_basic_forms_submission_limit', self::SUBMISSION_LIMIT, $form_id );
		if ( $limit <= 0 ) {
			return false;
		}

		return (int) get_transient( $this->getSubmissionRateLimitKey( $form_id ) ) >= $limit;
	}

	private function recordSubmissionAttempt( int $form_id ): void {
		$key      = $this->getSubmissionRateLimitKey( $form_id );
		$window   = (int) apply_filters( 'wp_powersuite_basic_forms_submission_window', self::SUBMISSION_WINDOW, $form_id );
		$attempts = (int) get_transient( $key );
		set_transient( $key, $attempts + 1, max( 1, $window ) );
	}

	private function canManageForms(): bool {
		$capability = apply_filters( 'wpps_basic_forms_capability', 'manage_options' );
		return current_user_can( $capability );
	}

	private static function escapeCsvCell( string $value ): string {
		if ( preg_match( '/^[=\+\-@]/', $value ) ) {
			return "'" . $value;
		}
		return $value;
	}

	private function isFileUploadRateLimited( int $form_id ): bool {
		$attempts = (int) get_transient( $this->getFileUploadRateLimitKey( $form_id ) );

		return $attempts >= self::FILE_UPLOAD_LIMIT;
	}

	private function recordFileUploadAttempt( int $form_id ): void {
		$key      = $this->getFileUploadRateLimitKey( $form_id );
		$attempts = (int) get_transient( $key );
		set_transient( $key, $attempts + 1, self::FILE_UPLOAD_WINDOW );
	}

	private function getClientIp(): string {
		$keys = [ 'HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR' ];
		foreach ( $keys as $key ) {
			if ( ! empty( $_SERVER[ $key ] ) ) {
				$ip = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
				if ( strpos( $ip, ',' ) !== false ) {
					$ip = trim( explode( ',', $ip )[0] );
				}
				if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
					return $ip;
				}
			}
		}
		return '0.0.0.0';
	}

	private function exportEntriesCsv(): void {
		if ( ! $this->canManageForms() ) {
			wp_die( esc_html__( 'You do not have permission to export entries.', 'wp-powersuite' ) );
		}

		global $wpdb;
		$filter_form = isset( $_GET['form_filter'] ) ? (int) $_GET['form_filter'] : 0;
		$filter_status = isset( $_GET['status_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['status_filter'] ) ) : '';

		$where = [];
		if ( $filter_form ) {
			$where[] = $wpdb->prepare( 'form_id = %d', $filter_form );
		}
		if ( $filter_status && in_array( $filter_status, [ 'read', 'unread' ], true ) ) {
			$where[] = $wpdb->prepare( 'status = %s', $filter_status );
		}
		$where_sql = ! empty( $where ) ? ' WHERE ' . implode( ' AND ', $where ) : '';

		$max_export_rows = (int) apply_filters( 'wp_powersuite_basic_forms_max_csv_rows', 5000 );
		if ( $max_export_rows > 0 ) {
			$total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$this->table_name}{$where_sql}" );
			if ( $total > $max_export_rows ) {
				wp_die( esc_html__( 'This export is too large. Please filter entries before exporting.', 'wp-powersuite' ) );
			}
		}

		$entries = $wpdb->get_results( "SELECT * FROM {$this->table_name}{$where_sql} ORDER BY date_created DESC" );
		if ( empty( $entries ) ) {
			wp_die( esc_html__( 'No entries to export.', 'wp-powersuite' ) );
		}

		$all_fields = [];
		foreach ( $entries as $e ) {
			$data = json_decode( $e->payload ?? '{}', true );
			if ( is_array( $data ) ) {
				$all_fields = array_merge( $all_fields, array_keys( $data ) );
			}
		}
		$all_fields = array_unique( $all_fields );

		$filename = 'form-entries-' . gmdate( 'Y-m-d-His' ) . '.csv';
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename=' . $filename );
		header( 'Pragma: no-cache' );
		header( 'Expires: 0' );

		$out = fopen( 'php://output', 'w' );
		$header = array_merge(
			[ 'Entry ID', 'Form', 'Status', 'Date', 'IP', 'Name', 'Email' ],
			$all_fields
		);
		fputcsv( $out, $header );

		foreach ( $entries as $e ) {
			$form = get_post( (int) $e->form_id );
			$data = json_decode( $e->payload ?? '{}', true );
			$data = is_array( $data ) ? $data : [];
			$row = [
				$e->id,
				$form ? $form->post_title : '',
				$e->status ?? '',
				$e->date_created ?? '',
				$e->user_ip ?? '',
				$e->user_name ?? '',
				$e->user_email ?? '',
			];
			foreach ( $all_fields as $f ) {
				if ( ! isset( $data[ $f ] ) ) {
					$row[] = '';
					continue;
				}
				$field_value = $data[ $f ];
				if ( FormFileStorage::normalizeReferences( $field_value ) !== [] ) {
					$row[] = self::escapeCsvCell( FormFileStorage::formatValuePlain( $field_value, (int) $e->id ) );
				} else {
					$plain = is_array( $field_value ) ? implode( ', ', $field_value ) : (string) $field_value;
					$row[] = self::escapeCsvCell( $plain );
				}
			}
			fputcsv( $out, $row );
		}
		fclose( $out );
	}

	public function enqueueAdminAssets( string $hook ): void {
		$is_forms_page = isset( $_GET['page'] ) && $_GET['page'] === 'powersuite-basic-forms';
		$action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : '';
		$form_id = isset( $_GET['form_id'] ) ? (int) $_GET['form_id'] : 0;
		$is_blank_editor = $is_forms_page && $action === 'add' && isset( $_GET['blank'] ) && '1' === $_GET['blank'];
		$is_form_editor_page = $is_blank_editor || ( $is_forms_page && $action === 'edit' && $form_id > 0 );

		if ( ! $is_forms_page ) {
			return;
		}

		$base = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		$deps = wp_style_is( 'wp-powersuite-admin', 'registered' ) ? [ 'wp-powersuite-admin' ] : [];
		wp_enqueue_style( 'wpps-bf-admin', $base . 'modules/basic-forms/assets/admin.css', $deps, $this->assetVersion( 'assets/admin.css' ) );
		wp_enqueue_style( 'dashicons' );

		if ( $is_form_editor_page ) {
			wp_enqueue_script( 'jquery' );
			wp_enqueue_script( 'jquery-ui-sortable' );
			wp_enqueue_script( 'wpps-bf-form-builder', $base . 'modules/basic-forms/assets/form-builder.js', [ 'jquery', 'jquery-ui-sortable' ], $this->assetVersion( 'assets/form-builder.js' ), true );
			wp_add_inline_script( 'wpps-bf-form-builder', "function wppsBfCopyShortcode(el){el.select();el.setSelectionRange(0,99999);try{navigator.clipboard.writeText(el.value).then(function(){var m=el.parentElement.querySelector('.wpps-bf-shortcode-copied');if(m){m.classList.add('show');setTimeout(function(){m.classList.remove('show')},2000)}})}catch(e){document.execCommand('copy')}}", 'before' );
			wp_localize_script( 'wpps-bf-form-builder', 'wppsBfEditorData', [
				'ajaxUrl'           => admin_url( 'admin-ajax.php' ),
				'saveNonce'         => wp_create_nonce( 'wpps_basic_forms_save' ),
				'isPro'             => FeatureGate::isPro(),
				'formDesignEnabled' => FeatureGate::formDesignEnabled(),
				'licenseUrl'        => admin_url( 'admin.php?page=powersuite-license' ),
				'strings'           => [
					'premiumDesign' => __( 'Form Design is a Premium feature. Activate your license to customize styling.', 'wp-powersuite' ),
				],
			] );
			$this->enqueueAiFormBuilderAssets( 'edit' );
		}

		if ( $is_forms_page && isset( $_GET['view'] ) && $_GET['view'] === 'entries' ) {
			wp_enqueue_script( 'jquery' );
			wp_localize_script( 'jquery', 'wppsBasicFormsData', [
				'nonce'   => wp_create_nonce( 'wpps_basic_forms_view_entry' ),
				'ajaxUrl' => admin_url( 'admin-ajax.php' ),
			] );
			wp_add_inline_script( 'jquery', $this->getEntriesPageJs() );
		}
	}

	public function enqueueFrontendAssets(): void {
		// Frontend CSS is enqueued in shortcode when form is rendered
	}

	/**
	 * @param 'create'|'edit' $mode
	 */
	private function enqueueAiFormBuilderAssets( string $mode ): void {
		if ( ! FeatureGate::aiFormBuilderEnabled() ) {
			return;
		}
		$base = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		wp_enqueue_script( 'jquery' );
		wp_enqueue_script(
			'wpps-bf-ai-form-builder',
			$base . 'modules/basic-forms/assets/ai-form-builder.js',
			[ 'jquery' ],
			$this->assetVersion( 'assets/ai-form-builder.js' ),
			true
		);

		$ai_public = AiSettings::getPublic();
		wp_localize_script(
			'wpps-bf-ai-form-builder',
			'wppsBfAiData',
			[
				'ajaxUrl'      => admin_url( 'admin-ajax.php' ),
				'nonce'        => wp_create_nonce( 'wpps_basic_forms_ai_generate' ),
				'mode'         => $mode,
				'hasApiKey'    => ! empty( $ai_public['has_api_key'] ),
				'provider'     => (string) ( $ai_public['provider'] ?? 'openai' ),
				'model'        => (string) ( $ai_public['model'] ?? '' ),
				'providers'    => ConnectionSettings::providerLabels(),
				'settingsUrl'  => admin_url( 'admin.php?page=powersuite-basic-forms&view=settings' ),
				'connectionsUrl' => admin_url( 'admin.php?page=powersuite-settings&tab=ai-connections' ),
				'strings'      => [
					'generating'    => __( 'Generating…', 'wp-powersuite' ),
					'sending'       => __( 'Sending…', 'wp-powersuite' ),
					'send'          => __( 'Send', 'wp-powersuite' ),
					'generate'      => __( 'Generate form', 'wp-powersuite' ),
					'applyChanges'  => __( 'Apply changes', 'wp-powersuite' ),
					'networkError'  => __( 'Network error. Please try again.', 'wp-powersuite' ),
					'noApiKey'      => __( 'Configure an AI provider and model in AI Forms settings, and add an API key under Settings → AI Connections.', 'wp-powersuite' ),
					'openSettings'  => __( 'Open AI Connections', 'wp-powersuite' ),
					'applied'       => __( 'AI changes applied. Review and save your form.', 'wp-powersuite' ),
					'appliedAdd'    => __( 'New fields added. Review and save your form.', 'wp-powersuite' ),
					'appliedDesign' => __( 'Form design updated. Open Form Design to review changes.', 'wp-powersuite' ),
					'emptyPrompt'   => __( 'Describe the changes you want.', 'wp-powersuite' ),
					'welcomeCreate' => __( 'Describe the form you need. I will draft fields, labels, notifications, and styling you can edit before publishing.', 'wp-powersuite' ),
					'welcomeEdit'   => __( 'Tell me what to change — add fields, update labels, adjust settings, design, or custom CSS.', 'wp-powersuite' ),
					'clearChat'     => __( 'Clear chat', 'wp-powersuite' ),
					'clearConfirm'  => __( 'Clear this conversation?', 'wp-powersuite' ),
					'openChat'      => __( 'Open AI assistant', 'wp-powersuite' ),
					'closeChat'     => __( 'Minimize', 'wp-powersuite' ),
					'thinking'      => __( 'Thinking…', 'wp-powersuite' ),
					'charCount'     => __( '/ 8000 characters', 'wp-powersuite' ),
					'tryExample'    => __( 'Try an example', 'wp-powersuite' ),
					'you'           => __( 'You', 'wp-powersuite' ),
					'assistant'     => __( 'Assistant', 'wp-powersuite' ),
					'retry'         => __( 'Retry', 'wp-powersuite' ),
					'rateLimited'   => __( 'Too many AI requests. Please wait a minute and try again.', 'wp-powersuite' ),
				],
			]
		);
	}

	private function getFormBuilderJs(): string {
		$js_file = __DIR__ . '/assets/form-builder.js';
		if ( file_exists( $js_file ) ) {
			return file_get_contents( $js_file );
		}
		return '/* Form builder JS */';
	}

	private function getEntriesPageJs(): string {
		return "
		jQuery(document).ready(function(\$){
			var \$modal = \$('#wpps-bf-entry-modal');
			\$(document).on('click','.wpps-bf-view-entry',function(e){
				e.preventDefault();
				var id=\$(this).data('entry-id');
				var row=\$(this).closest('tr');
				\$modal.addClass('show').attr('aria-hidden','false');
				\$('#wpps-bf-entry-modal-content').html('<div class=\"wpps-bf-loading\">" . esc_js( __( 'Loading...', 'wp-powersuite' ) ) . "</div>');
				\$.post(wppsBasicFormsData.ajaxUrl,{action:'wpps_basic_forms_view_entry',entry_id:id,nonce:wppsBasicFormsData.nonce},function(r){
					if(r.success){
						\$('#wpps-bf-entry-modal-content').html(r.data.html);
						row.removeClass('entry-unread').find('.wpps-bf-status-badge').removeClass('status-unread').addClass('status-read').text('" . esc_js( __( 'Read', 'wp-powersuite' ) ) . "');
					}else{
						\$('#wpps-bf-entry-modal-content').html('<div style=\"padding:20px;color:#dc2626;\">'+(r.data&&r.data.message?r.data.message:'Error')+'</div>');
					}
				}).fail(function(){
					\$('#wpps-bf-entry-modal-content').html('<div style=\"padding:20px;color:#dc2626;\">" . esc_js( __( 'Network error.', 'wp-powersuite' ) ) . "</div>');
				});
			});
			\$modal.on('click',function(e){if(e.target.classList.contains('wpps-bf-modal-overlay')){\$modal.removeClass('show').attr('aria-hidden','true');}});
			\$('.wpps-bf-modal-close').on('click',function(){\$modal.removeClass('show').attr('aria-hidden','true');});
			\$(document).on('keydown',function(e){if(e.key==='Escape'){\$modal.removeClass('show').attr('aria-hidden','true');}});
		});
		";
	}

	/**
	 * Get country dial codes for phone field dropdown.
	 *
	 * @return array<int, array{code: string, iso: string, name: string}> List of countries.
	 */
	public static function getCountryCodes(): array {
		$countries = [
			[ 'code' => '1', 'iso' => 'US', 'name' => 'United States' ],
			[ 'code' => '44', 'iso' => 'GB', 'name' => 'United Kingdom' ],
			[ 'code' => '1', 'iso' => 'CA', 'name' => 'Canada' ],
			[ 'code' => '91', 'iso' => 'IN', 'name' => 'India' ],
			[ 'code' => '61', 'iso' => 'AU', 'name' => 'Australia' ],
			[ 'code' => '49', 'iso' => 'DE', 'name' => 'Germany' ],
			[ 'code' => '33', 'iso' => 'FR', 'name' => 'France' ],
			[ 'code' => '34', 'iso' => 'ES', 'name' => 'Spain' ],
			[ 'code' => '39', 'iso' => 'IT', 'name' => 'Italy' ],
			[ 'code' => '31', 'iso' => 'NL', 'name' => 'Netherlands' ],
			[ 'code' => '55', 'iso' => 'BR', 'name' => 'Brazil' ],
			[ 'code' => '52', 'iso' => 'MX', 'name' => 'Mexico' ],
			[ 'code' => '81', 'iso' => 'JP', 'name' => 'Japan' ],
			[ 'code' => '86', 'iso' => 'CN', 'name' => 'China' ],
			[ 'code' => '82', 'iso' => 'KR', 'name' => 'South Korea' ],
			[ 'code' => '7', 'iso' => 'RU', 'name' => 'Russia' ],
			[ 'code' => '27', 'iso' => 'ZA', 'name' => 'South Africa' ],
			[ 'code' => '234', 'iso' => 'NG', 'name' => 'Nigeria' ],
			[ 'code' => '92', 'iso' => 'PK', 'name' => 'Pakistan' ],
			[ 'code' => '880', 'iso' => 'BD', 'name' => 'Bangladesh' ],
			[ 'code' => '63', 'iso' => 'PH', 'name' => 'Philippines' ],
			[ 'code' => '62', 'iso' => 'ID', 'name' => 'Indonesia' ],
			[ 'code' => '90', 'iso' => 'TR', 'name' => 'Turkey' ],
			[ 'code' => '48', 'iso' => 'PL', 'name' => 'Poland' ],
			[ 'code' => '966', 'iso' => 'SA', 'name' => 'Saudi Arabia' ],
			[ 'code' => '971', 'iso' => 'AE', 'name' => 'United Arab Emirates' ],
			[ 'code' => '972', 'iso' => 'IL', 'name' => 'Israel' ],
			[ 'code' => '46', 'iso' => 'SE', 'name' => 'Sweden' ],
			[ 'code' => '32', 'iso' => 'BE', 'name' => 'Belgium' ],
			[ 'code' => '41', 'iso' => 'CH', 'name' => 'Switzerland' ],
			[ 'code' => '43', 'iso' => 'AT', 'name' => 'Austria' ],
			[ 'code' => '47', 'iso' => 'NO', 'name' => 'Norway' ],
			[ 'code' => '45', 'iso' => 'DK', 'name' => 'Denmark' ],
			[ 'code' => '358', 'iso' => 'FI', 'name' => 'Finland' ],
			[ 'code' => '353', 'iso' => 'IE', 'name' => 'Ireland' ],
			[ 'code' => '351', 'iso' => 'PT', 'name' => 'Portugal' ],
			[ 'code' => '30', 'iso' => 'GR', 'name' => 'Greece' ],
			[ 'code' => '420', 'iso' => 'CZ', 'name' => 'Czech Republic' ],
			[ 'code' => '40', 'iso' => 'RO', 'name' => 'Romania' ],
			[ 'code' => '36', 'iso' => 'HU', 'name' => 'Hungary' ],
			[ 'code' => '64', 'iso' => 'NZ', 'name' => 'New Zealand' ],
			[ 'code' => '65', 'iso' => 'SG', 'name' => 'Singapore' ],
			[ 'code' => '60', 'iso' => 'MY', 'name' => 'Malaysia' ],
			[ 'code' => '66', 'iso' => 'TH', 'name' => 'Thailand' ],
			[ 'code' => '84', 'iso' => 'VN', 'name' => 'Vietnam' ],
			[ 'code' => '852', 'iso' => 'HK', 'name' => 'Hong Kong' ],
			[ 'code' => '886', 'iso' => 'TW', 'name' => 'Taiwan' ],
			[ 'code' => '54', 'iso' => 'AR', 'name' => 'Argentina' ],
			[ 'code' => '56', 'iso' => 'CL', 'name' => 'Chile' ],
			[ 'code' => '57', 'iso' => 'CO', 'name' => 'Colombia' ],
			[ 'code' => '51', 'iso' => 'PE', 'name' => 'Peru' ],
			[ 'code' => '20', 'iso' => 'EG', 'name' => 'Egypt' ],
			[ 'code' => '254', 'iso' => 'KE', 'name' => 'Kenya' ],
			[ 'code' => '233', 'iso' => 'GH', 'name' => 'Ghana' ],
			[ 'code' => '212', 'iso' => 'MA', 'name' => 'Morocco' ],
			[ 'code' => '213', 'iso' => 'DZ', 'name' => 'Algeria' ],
			[ 'code' => '380', 'iso' => 'UA', 'name' => 'Ukraine' ],
			[ 'code' => '98', 'iso' => 'IR', 'name' => 'Iran' ],
			[ 'code' => '964', 'iso' => 'IQ', 'name' => 'Iraq' ],
			[ 'code' => '93', 'iso' => 'AF', 'name' => 'Afghanistan' ],
			[ 'code' => '355', 'iso' => 'AL', 'name' => 'Albania' ],
			[ 'code' => '376', 'iso' => 'AD', 'name' => 'Andorra' ],
			[ 'code' => '374', 'iso' => 'AM', 'name' => 'Armenia' ],
			[ 'code' => '994', 'iso' => 'AZ', 'name' => 'Azerbaijan' ],
			[ 'code' => '973', 'iso' => 'BH', 'name' => 'Bahrain' ],
			[ 'code' => '375', 'iso' => 'BY', 'name' => 'Belarus' ],
			[ 'code' => '387', 'iso' => 'BA', 'name' => 'Bosnia and Herzegovina' ],
			[ 'code' => '359', 'iso' => 'BG', 'name' => 'Bulgaria' ],
			[ 'code' => '385', 'iso' => 'HR', 'name' => 'Croatia' ],
			[ 'code' => '357', 'iso' => 'CY', 'name' => 'Cyprus' ],
			[ 'code' => '372', 'iso' => 'EE', 'name' => 'Estonia' ],
			[ 'code' => '995', 'iso' => 'GE', 'name' => 'Georgia' ],
			[ 'code' => '354', 'iso' => 'IS', 'name' => 'Iceland' ],
			[ 'code' => '962', 'iso' => 'JO', 'name' => 'Jordan' ],
			[ 'code' => '7', 'iso' => 'KZ', 'name' => 'Kazakhstan' ],
			[ 'code' => '965', 'iso' => 'KW', 'name' => 'Kuwait' ],
			[ 'code' => '371', 'iso' => 'LV', 'name' => 'Latvia' ],
			[ 'code' => '961', 'iso' => 'LB', 'name' => 'Lebanon' ],
			[ 'code' => '370', 'iso' => 'LT', 'name' => 'Lithuania' ],
			[ 'code' => '352', 'iso' => 'LU', 'name' => 'Luxembourg' ],
			[ 'code' => '356', 'iso' => 'MT', 'name' => 'Malta' ],
			[ 'code' => '373', 'iso' => 'MD', 'name' => 'Moldova' ],
			[ 'code' => '382', 'iso' => 'ME', 'name' => 'Montenegro' ],
			[ 'code' => '977', 'iso' => 'NP', 'name' => 'Nepal' ],
			[ 'code' => '389', 'iso' => 'MK', 'name' => 'North Macedonia' ],
			[ 'code' => '968', 'iso' => 'OM', 'name' => 'Oman' ],
			[ 'code' => '974', 'iso' => 'QA', 'name' => 'Qatar' ],
			[ 'code' => '381', 'iso' => 'RS', 'name' => 'Serbia' ],
			[ 'code' => '421', 'iso' => 'SK', 'name' => 'Slovakia' ],
			[ 'code' => '386', 'iso' => 'SI', 'name' => 'Slovenia' ],
			[ 'code' => '94', 'iso' => 'LK', 'name' => 'Sri Lanka' ],
			[ 'code' => '249', 'iso' => 'SD', 'name' => 'Sudan' ],
			[ 'code' => '216', 'iso' => 'TN', 'name' => 'Tunisia' ],
			[ 'code' => '998', 'iso' => 'UZ', 'name' => 'Uzbekistan' ],
			[ 'code' => '263', 'iso' => 'ZW', 'name' => 'Zimbabwe' ],
		];
		return $countries;
	}

	/**
	 * Build HTML input attributes from field settings.
	 *
	 * @param array<string, mixed> $field
	 */
	private static function fieldInputAttributes( array $field, string $required ): string {
		$attrs   = $required;
		$type    = (string) ( $field['type'] ?? 'text' );

		if ( ! empty( $field['min'] ) && $field['min'] !== '' ) {
			$attrs .= ' min="' . esc_attr( (string) $field['min'] ) . '"';
		}
		if ( ! empty( $field['max'] ) && $field['max'] !== '' ) {
			$attrs .= ' max="' . esc_attr( (string) $field['max'] ) . '"';
		}
		if ( ! empty( $field['maxlength'] ) ) {
			$attrs .= ' maxlength="' . esc_attr( (string) (int) $field['maxlength'] ) . '"';
		}
		if ( ! empty( $field['step'] ) && $type === 'number' ) {
			$attrs .= ' step="' . esc_attr( (string) $field['step'] ) . '"';
		}
		if ( ! empty( $field['default_value'] ) && in_array( $type, [ 'text', 'email', 'url', 'number', 'hidden' ], true ) ) {
			$attrs .= ' value="' . esc_attr( (string) $field['default_value'] ) . '"';
		}
		if ( $type === 'password' && ! empty( $field['minlength'] ) ) {
			$attrs .= ' minlength="' . esc_attr( (string) (int) $field['minlength'] ) . '"';
		}
		if ( $type === 'email' ) {
			$attrs .= ' autocomplete="email"';
		}
		if ( $type === 'url' ) {
			$attrs .= ' autocomplete="url"';
		}
		if ( $type === 'password' ) {
			$attrs .= ' autocomplete="new-password"';
		}

		return $attrs;
	}

	/**
	 * Resolve allowed upload MIME map from field accept string.
	 *
	 * @param array<string, mixed> $field
	 * @return array<string, string>
	 */
	private function getAllowedMimesForField( array $field ): array {
		$all = [
			'jpg|jpeg|jpe' => 'image/jpeg',
			'png'          => 'image/png',
			'gif'          => 'image/gif',
			'webp'         => 'image/webp',
			'pdf'          => 'application/pdf',
			'doc'          => 'application/msword',
			'docx'         => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
			'txt'          => 'text/plain',
			'csv'          => 'text/csv',
		];

		$file_types = trim( (string) ( $field['file_types'] ?? '' ) );
		if ( $file_types === '' || $file_types === '*' ) {
			return $all;
		}

		$allowed = [];
		$parts   = array_map( 'trim', explode( ',', $file_types ) );

		foreach ( $parts as $part ) {
			if ( $part === '' ) {
				continue;
			}
			if ( $part === 'image/*' ) {
				foreach ( $all as $ext => $mime ) {
					if ( str_starts_with( $mime, 'image/' ) ) {
						$allowed[ $ext ] = $mime;
					}
				}
				continue;
			}
			if ( str_starts_with( $part, '.' ) ) {
				$ext = ltrim( $part, '.' );
				foreach ( $all as $ext_pattern => $mime ) {
					if ( preg_match( '/(^|\|)' . preg_quote( $ext, '/' ) . '(\||$)/', $ext_pattern ) ) {
						$allowed[ $ext_pattern ] = $mime;
					}
				}
				continue;
			}
			if ( str_contains( $part, '/' ) ) {
				foreach ( $all as $ext_pattern => $mime ) {
					if ( $mime === $part ) {
						$allowed[ $ext_pattern ] = $mime;
					}
				}
			}
		}

		return $allowed !== [] ? $allowed : $all;
	}

	/**
	 * Render a single form field (static for use in view).
	 *
	 * @param array<string, mixed> $field   Field config.
	 * @param int                  $form_id Form ID (for unique HTML ids when multiple forms on page).
	 * @return string HTML output.
	 */
	public static function renderField( array $field, int $form_id = 0 ): string {
		$type = $field['type'] ?? 'text';
		$name = 'field_' . ( $field['id'] ?? '' );
		$html_id = $form_id > 0 ? 'wpps-bf-' . $form_id . '-field-' . ( $field['id'] ?? 0 ) : $name;
		$field_id = ! empty( $field['field_id'] ) ? sanitize_html_class( $field['field_id'] ) : '';
		$required = ! empty( $field['required'] ) ? ' required' : '';
		$label = esc_html( $field['label'] ?? '' );
		$placeholder = esc_attr( $field['placeholder'] ?? '' );
		$help = ! empty( $field['help'] ) ? '<small class="wpps-bf-help-text">' . esc_html( $field['help'] ) . '</small>' : '';

		if ( $type === 'separator' ) {
			return '<div class="wpps-bf-field wpps-bf-field-separator"><hr></div>';
		}
		if ( $type === 'html' ) {
			$content = ! empty( $field['content'] ) ? (string) $field['content'] : '';
			return '<div class="wpps-bf-field wpps-bf-field-html"><div class="wpps-bf-html-content">' . $content . '</div></div>';
		}
		if ( $type === 'group' ) {
			$desc = ! empty( $field['description'] ) ? '<p class="wpps-bf-group-desc">' . esc_html( $field['description'] ) . '</p>' : '';
			return '<div class="wpps-bf-field wpps-bf-field-group">' . ( $label ? '<h3 class="wpps-bf-group-title">' . $label . '</h3>' : '' ) . $desc . '</div>';
		}

		$data_attr = $field_id ? ' data-field-id="' . esc_attr( $field_id ) . '"' : '';
		$wrapper_class = 'wpps-bf-field wpps-bf-field-' . esc_attr( $type );
		if ( $field_id ) {
			$wrapper_class .= ' wpps-bf-field-id-' . esc_attr( $field_id );
		}
		$wrapper_attrs = $data_attr;
		if ( ! empty( $field['show_if'] ) && is_array( $field['show_if'] ) ) {
			$normalized_show_if = ConditionalFields::normalizeShowIf( $field['show_if'] );
			if ( $normalized_show_if !== null ) {
				$wrapper_class .= ' wpps-bf-field-conditional';
				$wrapper_attrs  .= ' data-show-if="' . esc_attr( wp_json_encode( $normalized_show_if ) ) . '"';
			}
		}
		$html = '<div class="' . $wrapper_class . '"' . $wrapper_attrs . '>';
		if ( $type !== 'hidden' ) {
			$html .= '<label for="' . esc_attr( $html_id ) . '">' . $label . ( $required ? ' <span class="wpps-bf-required">*</span>' : '' ) . '</label>';
		}

		switch ( $type ) {
			case 'phone':
				$ph_placeholder = $placeholder ?: __( 'e.g. 555 123 4567', 'wp-powersuite' );
				$countries = self::getCountryCodes();
				$flag_url = 'https://flagcdn.com/w40/%s.png';
				$default_code = ! empty( $field['default_country'] ) ? preg_replace( '/\D/', '', (string) $field['default_country'] ) : '1';
				$default = [ 'code' => '1', 'iso' => 'US' ];
				foreach ( $countries as $c ) {
					if ( (string) $c['code'] === $default_code ) {
						$default = $c;
						break;
					}
				}
				$html .= '<div class="wpps-bf-phone-wrap" data-field-name="' . esc_attr( $name ) . '">';
				$html .= '<input type="hidden" name="' . esc_attr( $name ) . '" class="wpps-bf-phone-value" value="">';
				$html .= '<div class="wpps-bf-phone-country wpps-bf-input">';
				$html .= '<input type="hidden" class="wpps-bf-phone-country-value" value="' . esc_attr( $default['code'] ) . '">';
				$default_iso = strtolower( $default['iso'] );
				$html .= '<button type="button" class="wpps-bf-phone-country-trigger" aria-label="' . esc_attr__( 'Country code', 'wp-powersuite' ) . '" aria-haspopup="listbox" aria-expanded="false" data-iso="' . esc_attr( $default_iso ) . '">';
				$html .= '<img src="' . esc_url( sprintf( $flag_url, $default_iso ) ) . '" alt="" width="20" height="15" class="wpps-bf-phone-flag">';
				$html .= '<span class="wpps-bf-phone-code">+' . esc_html( $default['code'] ) . '</span>';
				$html .= '</button>';
				$html .= '<div class="wpps-bf-phone-country-list" role="listbox" hidden>';
				$html .= '<input type="text" class="wpps-bf-phone-country-search" placeholder="' . esc_attr__( 'Search country...', 'wp-powersuite' ) . '" autocomplete="off" aria-label="' . esc_attr__( 'Search country', 'wp-powersuite' ) . '">';
				$html .= '<div class="wpps-bf-phone-country-options">';
				foreach ( $countries as $item ) {
					$code = $item['code'];
					$iso_lower = strtolower( $item['iso'] );
					$name = $item['name'] ?? '';
					$is_selected = ( $code === $default['code'] && $iso_lower === $default_iso );
					$html .= '<div class="wpps-bf-phone-country-option' . ( $is_selected ? ' is-selected' : '' ) . '" role="option" data-code="' . esc_attr( $code ) . '" data-iso="' . esc_attr( $iso_lower ) . '" data-name="' . esc_attr( $name ) . '" data-search="' . esc_attr( strtolower( $name . ' ' . $code . ' +' . $code ) ) . '" tabindex="0"' . ( $is_selected ? ' aria-selected="true"' : '' ) . '>';
					$html .= '<img src="' . esc_url( sprintf( $flag_url, $iso_lower ) ) . '" alt="" width="20" height="15" class="wpps-bf-phone-flag">';
					$html .= '<span>+' . esc_html( $code ) . '</span>';
					$html .= '</div>';
				}
				$html .= '</div></div></div>';
				$html .= '<input type="tel" class="wpps-bf-phone-number wpps-bf-input wpps-bf-input-tel" id="' . esc_attr( $html_id ) . '" placeholder="' . esc_attr( $ph_placeholder ) . '" inputmode="tel" autocomplete="tel"' . $required . '>';
				$html .= '</div>';
				break;
			case 'text':
			case 'email':
			case 'url':
			case 'date':
			case 'time':
			case 'datetime':
			case 'datetime-local':
			case 'hidden':
			case 'password':
				$input_type = ( $type === 'datetime' ) ? 'datetime-local' : $type;
				$attrs      = self::fieldInputAttributes( $field, $required );
				$html      .= '<input type="' . esc_attr( $input_type ) . '" class="wpps-bf-input" id="' . esc_attr( $html_id ) . '" name="' . esc_attr( $name ) . '" placeholder="' . $placeholder . '"' . $attrs . '>';
				break;
			case 'number':
				$html .= '<div class="wpps-bf-number-wrap">';
				$html .= '<input type="number" class="wpps-bf-input" id="' . esc_attr( $html_id ) . '" name="' . esc_attr( $name ) . '" placeholder="' . $placeholder . '"' . self::fieldInputAttributes( $field, $required ) . '>';
				$html .= '</div>';
				break;
			case 'textarea':
				$rows   = ! empty( $field['rows'] ) ? max( 2, min( 20, (int) $field['rows'] ) ) : 5;
				$maxlen = ! empty( $field['maxlength'] ) ? ' maxlength="' . esc_attr( (string) (int) $field['maxlength'] ) . '"' : '';
				$html  .= '<textarea id="' . esc_attr( $html_id ) . '" name="' . esc_attr( $name ) . '" placeholder="' . $placeholder . '" rows="' . esc_attr( (string) $rows ) . '"' . $maxlen . $required . '></textarea>';
				break;
			case 'select':
				$opts  = ! empty( $field['options'] ) ? array_filter( array_map( 'trim', explode( "\n", $field['options'] ) ) ) : [];
				$empty = ! empty( $field['empty_label'] ) ? (string) $field['empty_label'] : __( 'Select', 'wp-powersuite' );
				$html .= '<select id="' . esc_attr( $html_id ) . '" name="' . esc_attr( $name ) . '"' . $required . '><option value="">-- ' . esc_html( $empty ) . ' --</option>';
				foreach ( $opts as $o ) {
					$html .= '<option value="' . esc_attr( $o ) . '">' . esc_html( $o ) . '</option>';
				}
				$html .= '</select>';
				break;
			case 'radio':
				$opts = ! empty( $field['options'] ) ? array_filter( array_map( 'trim', explode( "\n", $field['options'] ) ) ) : [];
				$inline = ! empty( $field['inline'] ) || ( isset( $field['inline'] ) && ( $field['inline'] === true || $field['inline'] === 'true' || $field['inline'] === '1' ) );
				$cols = isset( $field['cols'] ) ? (int) $field['cols'] : 3;
				$cols = in_array( $cols, [ 2, 3, 4, 5 ], true ) ? $cols : 3;
				$group_class = 'wpps-bf-radio-group' . ( $inline ? ' wpps-bf-radio-inline wpps-bf-cols-' . $cols : '' );
				$html .= '<div class="' . esc_attr( $group_class ) . '">';
				foreach ( $opts as $i => $o ) {
					$oid = $html_id . '_' . $i;
					$html .= '<label class="wpps-bf-radio-label"><input type="radio" id="' . esc_attr( $oid ) . '" name="' . esc_attr( $name ) . '" value="' . esc_attr( $o ) . '"' . $required . '> ' . esc_html( $o ) . '</label>';
				}
				$html .= '</div>';
				break;
			case 'checkbox':
				$opts = ! empty( $field['options'] ) ? array_filter( array_map( 'trim', explode( "\n", $field['options'] ) ) ) : [];
				if ( count( $opts ) > 1 ) {
					$inline = ! empty( $field['inline'] ) || ( isset( $field['inline'] ) && ( $field['inline'] === true || $field['inline'] === 'true' || $field['inline'] === '1' ) );
					$cols = isset( $field['cols'] ) ? (int) $field['cols'] : 3;
					$cols = in_array( $cols, [ 2, 3, 4, 5 ], true ) ? $cols : 3;
					$group_class = 'wpps-bf-checkbox-group' . ( $inline ? ' wpps-bf-checkbox-inline wpps-bf-cols-' . $cols : '' );
					$html .= '<div class="' . esc_attr( $group_class ) . '">';
					foreach ( $opts as $i => $o ) {
						$oid = $html_id . '_' . $i;
						$html .= '<label class="wpps-bf-checkbox-label"><input type="checkbox" id="' . esc_attr( $oid ) . '" name="' . esc_attr( $name ) . '[]" value="' . esc_attr( $o ) . '"> ' . esc_html( $o ) . '</label>';
					}
					$html .= '</div>';
				} else {
					$cb_label = ! empty( $field['checkbox_label'] ) ? $field['checkbox_label'] : ( ! empty( $field['label'] ) ? $field['label'] : __( 'I agree', 'wp-powersuite' ) );
					$html .= '<label class="wpps-bf-checkbox-label"><input type="checkbox" id="' . esc_attr( $html_id ) . '" name="' . esc_attr( $name ) . '" value="1"' . $required . '> ' . esc_html( $cb_label ) . '</label>';
				}
				break;
			case 'file':
				$accept = ! empty( $field['file_types'] ) ? esc_attr( $field['file_types'] ) : 'image/*,.pdf';
				$max = ! empty( $field['max_size'] ) ? (int) $field['max_size'] : 5;
				$mult = ! empty( $field['multiple'] ) ? ' multiple' : '';
				$html .= '<div class="wpps-bf-file-upload">';
				$html .= '<input type="file" id="' . esc_attr( $html_id ) . '" name="' . esc_attr( $name ) . ( $mult ? '[]' : '' ) . '" accept="' . $accept . '"' . $required . $mult . ' class="wpps-bf-file-input">';
				$html .= '<div class="wpps-bf-file-dropzone" data-for="' . esc_attr( $html_id ) . '">';
				$html .= '<span class="wpps-bf-file-icon" aria-hidden="true">&#x2191;</span>';
				$html .= '<span class="wpps-bf-file-text">' . esc_html__( 'Drop files here or click to browse', 'wp-powersuite' ) . '</span>';
				$html .= '<small class="wpps-bf-file-hint">' . sprintf( esc_html__( 'Max %d MB per file', 'wp-powersuite' ), $max ) . '</small>';
				$html .= '</div>';
				$html .= '<div class="wpps-bf-file-preview"></div>';
				$html .= '</div>';
				break;
			default:
				$html .= '<input type="text" id="' . esc_attr( $html_id ) . '" name="' . esc_attr( $name ) . '" placeholder="' . $placeholder . '"' . $required . '>';
		}
		$html .= $help . '</div>';
		return $html;
	}


}
