<?php
/**
 * Options Page Builder Module
 *
 * Create custom admin pages with settings panels. Runs only when module is enabled.
 * Options stored with wp_powersuite_opb_* prefix for cleanup on uninstall.
 *
 * @package OptionsPageBuilder
 * @since 1.0.0
 */

namespace OptionsPageBuilder;

use WP_PowerSuite\Core\AbstractModule;
class Module extends AbstractModule {

	private const MODULE_ID   = 'options-page-builder';
	private const MODULE_VER  = '1.2.1';
	private const PAGES_OPTION = 'wp_powersuite_opb_pages';
	private const BUILDER_SLUG = 'powersuite-options-page-builder';

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

	public function getName(): string {
		return __( 'Options Page Builder', 'wp-powersuite' );
	}

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

	public function getDescription(): string {
		return __( 'Custom settings screens with toggle, link, gallery, repeater, and 20 field types—ideal for theme options or client handoff.', 'wp-powersuite' );
	}

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

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

	private function isLicenseActive(): bool {
		return function_exists( 'wp_powersuite_is_license_valid' ) && wp_powersuite_is_license_valid();
	}

	protected function registerHooks(): void {
		if ( ! $this->isLicenseActive() ) {
			return;
		}

		require_once __DIR__ . '/includes/field-config.php';
		require_once __DIR__ . '/includes/helpers.php';

		add_action( 'elementor/loaded', [ $this, 'loadElementorIntegration' ] );

		if ( defined( 'ELEMENTOR_VERSION' ) || did_action( 'elementor/loaded' ) ) {
			$this->loadElementorIntegration();
		}

		add_action( 'plugins_loaded', [ $this, 'loadBricksIntegration' ], 25 );
		add_action( 'init', [ $this, 'loadBricksIntegration' ], 15 );
		if ( defined( 'BRICKS_VERSION' ) ) {
			$this->loadBricksIntegration();
		}

		$this->loadPublicIntegrations();

		if ( ! is_admin() ) {
			return;
		}

		add_action( 'admin_menu', [ $this, 'registerBuilderMenu' ], 20 );
		add_action( 'admin_menu', [ $this, 'registerOptionsPages' ], 99 );
		add_action( 'admin_init', [ $this, 'registerSettings' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueBuilderAssets' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueuePageAssets' ] );

		add_action( 'wp_ajax_wpps_opb_save_page', [ $this, 'ajaxSavePage' ] );
		add_action( 'wp_ajax_wpps_opb_delete_page', [ $this, 'ajaxDeletePage' ] );
		add_action( 'wp_ajax_wpps_opb_export_pages', [ $this, 'ajaxExportPages' ] );
		add_action( 'wp_ajax_wpps_opb_export_page', [ $this, 'ajaxExportPage' ] );
		add_action( 'wp_ajax_wpps_opb_import_pages', [ $this, 'ajaxImportPages' ] );
	}

	public function loadElementorIntegration(): void {
		static $loaded = false;
		if ( $loaded ) {
			return;
		}

		$path = __DIR__ . '/integrations/elementor.php';
		if ( ! file_exists( $path ) ) {
			return;
		}

		require_once $path;
		$loaded = true;

		if ( did_action( 'init' ) && function_exists( 'wpps_opb_init_elementor' ) ) {
			wpps_opb_init_elementor();
		}
	}

	public function loadPublicIntegrations(): void {
		static $loaded = false;
		if ( $loaded ) {
			return;
		}

		$path = __DIR__ . '/integrations/loader.php';
		if ( is_readable( $path ) ) {
			require_once $path;
			if ( function_exists( 'wpps_opb_load_public_integrations' ) ) {
				wpps_opb_load_public_integrations();
			}
		}

		$loaded = true;
	}

	public function loadBricksIntegration(): void {
		static $loaded = false;
		if ( $loaded || ! defined( 'BRICKS_VERSION' ) ) {
			return;
		}

		$path = __DIR__ . '/integrations/bricks.php';
		if ( ! file_exists( $path ) ) {
			return;
		}

		require_once $path;
		$loaded = true;

		if ( did_action( 'init' ) && function_exists( 'wpps_opb_init_bricks' ) ) {
			wpps_opb_init_bricks();
		}
	}


	public function registerBuilderMenu(): void {
		$this->registerPowerSuiteSubmenu(
			__( 'Options Page Builder', 'wp-powersuite' ),
			__( 'Options Page Builder', 'wp-powersuite' ),
			self::BUILDER_SLUG,
			[ $this, 'renderBuilderPage' ]
		);
	}

	public function renderBuilderPage(): void {
		if ( ! $this->canManagePlugin() ) {
			wp_die( esc_html__( 'You do not have sufficient permissions.', 'wp-powersuite' ) );
		}

		if ( ! $this->isLicenseActive() ) {
			$license_page_url = admin_url( 'admin.php?page=powersuite-license' );
			?>
			<div class="wrap">
				<div class="notice notice-warning">
					<p>
						<?php esc_html_e( 'Options Page Builder requires an active license. Activate your license to create custom admin settings pages.', 'wp-powersuite' ); ?>
						<a href="<?php echo esc_url( $license_page_url ); ?>"><?php esc_html_e( 'Activate license', 'wp-powersuite' ); ?></a>
					</p>
				</div>
			</div>
			<?php
			return;
		}

		$action  = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : '';
		$page_id = isset( $_GET['page_id'] ) ? sanitize_key( wp_unslash( $_GET['page_id'] ) ) : '';

		if ( ! in_array( $action, [ '', 'add', 'edit' ], true ) ) {
			$action = '';
		}

		if ( $action === 'add' ) {
			$page_data = $this->getDefaultPageData();
			$parent_options = $this->getParentMenuOptions();
			include __DIR__ . '/edit.php';
			return;
		}

		if ( $action === 'edit' && $page_id !== '' ) {
			$page_data = $this->getPageById( $page_id );
			if ( $page_data === null ) {
				wp_safe_redirect( admin_url( 'admin.php?page=' . self::BUILDER_SLUG ) );
				exit;
			}
			$parent_options = $this->getParentMenuOptions( $page_data['parent_slug'] ?? '' );
			include __DIR__ . '/edit.php';
			return;
		}

		$pages = $this->getPages();
		include __DIR__ . '/admin.php';
	}

	public function registerOptionsPages(): void {
		$pages = $this->getPages();
		if ( empty( $pages ) || ! is_array( $pages ) ) {
			return;
		}

		foreach ( $pages as $page ) {
			if ( ! is_array( $page ) || empty( $page['slug'] ) || empty( $page['page_title'] ) || empty( $page['menu_title'] ) ) {
				continue;
			}

			$cap = $this->sanitizeCapability( ! empty( $page['capability'] ) ? (string) $page['capability'] : 'manage_options' );
			$icon = ! empty( $page['icon'] ) ? sanitize_text_field( $page['icon'] ) : 'dashicons-admin-generic';
			$pos = isset( $page['position'] ) ? absint( $page['position'] ) : null;
			$parent = ! empty( $page['parent_slug'] ) ? sanitize_text_field( $page['parent_slug'] ) : '';

			$menu_slug = sanitize_key( $page['slug'] );
			if ( $parent ) {
				$page_hook = add_submenu_page(
					$parent,
					sanitize_text_field( $page['page_title'] ),
					sanitize_text_field( $page['menu_title'] ),
					$cap,
					$menu_slug,
					[ $this, 'renderOptionsPage' ]
				);
			} else {
				$page_hook = add_menu_page(
					sanitize_text_field( $page['page_title'] ),
					sanitize_text_field( $page['menu_title'] ),
					$cap,
					$menu_slug,
					[ $this, 'renderOptionsPage' ],
					$icon,
					$pos
				);
			}

			if ( is_string( $page_hook ) && $page_hook !== '' ) {
				add_action( 'load-' . $page_hook, [ $this, 'enqueueLivePageAssets' ] );
			}
		}
	}

	public function registerSettings(): void {
		$pages = $this->getPages();
		if ( empty( $pages ) || ! is_array( $pages ) ) {
			return;
		}

		foreach ( $pages as $page ) {
			if ( ! is_array( $page ) || empty( $page['slug'] ) ) {
				continue;
			}
			$slug = sanitize_key( $page['slug'] );
			register_setting(
				'wpps_opb_' . $slug,
				'wp_powersuite_opb_' . $slug,
				[
					'sanitize_callback' => function ( $value ) use ( $slug ) {
						return $this->sanitizeOptionValues( $value, $slug );
					},
				]
			);
		}
	}

	private function allowedCapabilities(): array {
		return [ 'manage_options', 'edit_others_posts', 'edit_pages', 'edit_posts', 'publish_posts', 'read' ];
	}

	/**
	 * Sanitizes a stored page capability. Weak capabilities ('read', 'exist',
	 * or empty) are upgraded to 'edit_posts' by default since they otherwise
	 * let any logged-in user (e.g. Subscribers) view/save settings pages and
	 * read their values over REST.
	 */
	private function sanitizeCapability( string $capability ): string {
		$capability = sanitize_key( $capability );

		if ( in_array( $capability, [ '', 'read', 'exist' ], true ) && ! apply_filters( 'wp_powersuite_opb_allow_read_capability', false ) ) {
			$capability = 'edit_posts';
		}

		return in_array( $capability, $this->allowedCapabilities(), true ) ? $capability : 'manage_options';
	}

	private function sanitizeFieldDefault( string $type, mixed $default ): string {
		$type = sanitize_key( $type );
		$value = is_scalar( $default ) ? (string) $default : '';

		switch ( $type ) {
			case 'email':
				return sanitize_email( $value );
			case 'url':
				return esc_url_raw( $value );
			case 'image':
			case 'gallery':
				$id = absint( $value );
				return $id > 0 ? (string) $id : '';
			case 'toggle':
				return in_array( strtolower( $value ), [ '1', 'on', 'yes', 'true' ], true ) ? '1' : '';
			case 'textarea':
				return sanitize_textarea_field( $value );
			case 'wysiwyg':
				return wp_kses_post( $value );
			case 'color':
				$color = sanitize_hex_color( $value );
				return $color ? $color : sanitize_text_field( $value );
			case 'date':
				$value = sanitize_text_field( $value );
				return preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ? $value : '';
			default:
				return sanitize_text_field( $value );
		}
	}

	/**
	 * @param array<int, mixed> $raw
	 * @return array<int, array<string, mixed>>
	 */
	private function sanitizeFieldsFromRaw( array $raw ): array {
		$fields = [];
		foreach ( $raw as $f ) {
			if ( ! is_array( $f ) ) {
				continue;
			}
			$fields[] = wpps_opb_normalize_field_record(
				$f,
				function ( string $type, mixed $default ): string {
					return $this->sanitizeFieldDefault( $type, $default );
				}
			);
		}

		return $fields;
	}

	/**
	 * @return array<string, array<string, mixed>>
	 */
	private function collectPageValuesForExport( array $pages ): array {
		$values = [];
		foreach ( $pages as $page ) {
			if ( ! is_array( $page ) || empty( $page['slug'] ) ) {
				continue;
			}
			$slug = sanitize_key( (string) $page['slug'] );
			$stored = get_option( 'wp_powersuite_opb_' . $slug, [] );
			if ( is_array( $stored ) && $stored !== [] ) {
				$values[ $slug ] = $stored;
			}
		}

		return $values;
	}

	private function validateFieldIds( array $fields ): ?string {
		$seen = [];
		foreach ( $fields as $field ) {
			if ( ! is_array( $field ) || in_array( $field['type'] ?? '', [ 'group', 'message' ], true ) ) {
				continue;
			}
			$field_id = $field['field_id'] ?? '';
			if ( $field_id === '' ) {
				continue;
			}
			if ( isset( $seen[ $field_id ] ) ) {
				return sprintf(
					/* translators: %s: duplicate field ID */
					__( 'Duplicate field ID "%s". Each field must have a unique ID.', 'wp-powersuite' ),
					$field_id
				);
			}
			$seen[ $field_id ] = true;
		}
		return null;
	}

	private function sanitizeFieldWidth( $width ): string {
		$width = sanitize_key( (string) $width );

		return in_array( $width, [ 'full', '1-2', '1-3', '2-3' ], true ) ? $width : 'full';
	}

	private function addFieldRequiredError( string $settings_group, string $field_id, array $field ): void {
		$label = ! empty( $field['label'] ) ? (string) $field['label'] : $field_id;
		add_settings_error(
			$settings_group,
			'wpps_opb_required_' . $field_id,
			sprintf(
				/* translators: %s: field label */
				__( '%s is required.', 'wp-powersuite' ),
				$label
			),
			'error'
		);
	}

	private function isFieldValueEmpty( string $type, mixed $field_value ): bool {
		if ( in_array( $type, [ 'checkbox', 'toggle' ], true ) ) {
			return empty( $field_value ) || (string) $field_value === '0';
		}
		if ( $type === 'link' ) {
			if ( ! is_array( $field_value ) ) {
				return (string) $field_value === '';
			}

			return trim( (string) ( $field_value['url'] ?? '' ) ) === '';
		}
		if ( $type === 'repeater' ) {
			if ( ! is_array( $field_value ) || $field_value === [] ) {
				return true;
			}
			foreach ( $field_value as $row ) {
				if ( is_array( $row ) ) {
					foreach ( $row as $cell ) {
						if ( (string) $cell !== '' ) {
							return false;
						}
					}
				}
			}

			return true;
		}
		if ( in_array( $type, [ 'image', 'file', 'gallery' ], true ) ) {
			if ( is_array( $field_value ) ) {
				return $field_value === [];
			}
			$parts = array_filter( array_map( 'trim', explode( ',', (string) $field_value ) ) );

			return $parts === [];
		}

		return (string) $field_value === '';
	}

	/**
	 * @return string|false Sanitized value, or false when validation failed (existing value kept).
	 */
	private function sanitizeSubmittedFieldValue( array $field, mixed $field_value, string $settings_group ) {
		$type     = sanitize_key( (string) ( $field['type'] ?? 'text' ) );
		$field_id = isset( $field['field_id'] ) && $field['field_id'] !== ''
			? sanitize_key( (string) $field['field_id'] )
			: sanitize_key( (string) ( $field['id'] ?? '' ) );
		$label    = ! empty( $field['label'] ) ? (string) $field['label'] : $field_id;

		if ( $type === 'select' && ! empty( $field['multiple'] ) ) {
			$raw_values = is_array( $field_value ) ? $field_value : [ $field_value ];
			$opts       = $this->parseOptions( $field );
			$clean      = [];
			foreach ( $raw_values as $raw_choice ) {
				$choice = sanitize_text_field( (string) $raw_choice );
				if ( $choice === '' ) {
					continue;
				}
				if ( ! array_key_exists( $choice, $opts ) ) {
					add_settings_error(
						$settings_group,
						'wpps_opb_invalid_' . $field_id,
						sprintf(
							/* translators: %s: field label */
							__( '%s contains an invalid selection.', 'wp-powersuite' ),
							$label
						),
						'error'
					);

					return false;
				}
				$clean[] = $choice;
			}

			return implode( ',', array_unique( $clean ) );
		}

		if ( in_array( $type, [ 'select', 'radio' ], true ) ) {
			$choice = sanitize_text_field( (string) $field_value );
			if ( $choice === '' ) {
				return '';
			}
			$opts = $this->parseOptions( $field );
			if ( ! array_key_exists( $choice, $opts ) ) {
				add_settings_error(
					$settings_group,
					'wpps_opb_invalid_' . $field_id,
					sprintf(
						/* translators: %s: field label */
						__( '%s contains an invalid selection.', 'wp-powersuite' ),
						$label
					),
					'error'
				);

				return false;
			}

			return $choice;
		}

		switch ( $type ) {
			case 'password':
				$secret = (string) $field_value;
				// Empty or mask placeholder means "keep existing secret".
				if ( $secret === '' || $secret === '********' ) {
					return false;
				}

				return sanitize_text_field( $secret );
			case 'email':
				$email_raw = sanitize_text_field( (string) $field_value );
				if ( $email_raw === '' ) {
					return '';
				}
				$sanitized_email = sanitize_email( $email_raw );
				if ( $sanitized_email === '' ) {
					add_settings_error(
						$settings_group,
						'wpps_opb_invalid_email_' . $field_id,
						sprintf(
							/* translators: %s: field label */
							__( '%s must be a valid email address.', 'wp-powersuite' ),
							$label
						),
						'error'
					);

					return false;
				}

				return $sanitized_email;
			case 'url':
				return esc_url_raw( (string) $field_value );
			case 'gallery':
				$raw_ids = is_array( $field_value ) ? $field_value : explode( ',', (string) $field_value );
				$ids     = [];
				foreach ( $raw_ids as $raw_id ) {
					$id = absint( $raw_id );
					if ( $id > 0 ) {
						$ids[] = (string) $id;
					}
				}

				return implode( ',', array_unique( $ids ) );
			case 'image':
			case 'file':
				if ( ! empty( $field['multiple'] ) ) {
					$raw_ids = is_array( $field_value ) ? $field_value : explode( ',', (string) $field_value );
					$ids     = [];
					foreach ( $raw_ids as $raw_id ) {
						$id = absint( $raw_id );
						if ( $id > 0 ) {
							$ids[] = (string) $id;
						}
					}

					return implode( ',', array_unique( $ids ) );
				}
				$id = absint( $field_value );

				return $id > 0 ? (string) $id : '';
			case 'toggle':
				return ! empty( $field_value ) && (string) $field_value !== '0' ? '1' : '';
			case 'link':
				$link = is_array( $field_value ) ? $field_value : [];
				$url  = isset( $link['url'] ) ? esc_url_raw( (string) $link['url'] ) : '';

				return [
					'url'    => $url,
					'title'  => isset( $link['title'] ) ? sanitize_text_field( (string) $link['title'] ) : '',
					'target' => ! empty( $link['target'] ) && (string) $link['target'] === '_blank' ? '_blank' : '',
				];
			case 'repeater':
				return $this->sanitizeRepeaterValue( $field, $field_value );
			case 'textarea':
				return sanitize_textarea_field( (string) $field_value );
			case 'wysiwyg':
				return wp_kses_post( (string) $field_value );
			case 'checkbox':
				return ! empty( $field_value ) && (string) $field_value !== '0' ? '1' : '';
			case 'number':
				if ( ! is_numeric( $field_value ) ) {
					return '';
				}
				$num = (float) $field_value;
				if ( isset( $field['min'] ) && $field['min'] !== '' && $num < (float) $field['min'] ) {
					add_settings_error(
						$settings_group,
						'wpps_opb_min_' . $field_id,
						sprintf(
							/* translators: 1: field label, 2: minimum value */
							__( '%1$s must be at least %2$s.', 'wp-powersuite' ),
							$label,
							$field['min']
						),
						'error'
					);

					return false;
				}
				if ( isset( $field['max'] ) && $field['max'] !== '' && $num > (float) $field['max'] ) {
					add_settings_error(
						$settings_group,
						'wpps_opb_max_' . $field_id,
						sprintf(
							/* translators: 1: field label, 2: maximum value */
							__( '%1$s must be at most %2$s.', 'wp-powersuite' ),
							$label,
							$field['max']
						),
						'error'
					);

					return false;
				}

				return is_float( $num ) && floor( $num ) === $num ? (string) (int) $num : (string) $field_value;
			case 'color':
				$color = sanitize_hex_color( (string) $field_value );

				return $color ? $color : sanitize_text_field( (string) $field_value );
			case 'date':
				$date      = sanitize_text_field( (string) $field_value );
				$date_type = wpps_opb_sanitize_date_type( (string) ( $field['date_type'] ?? 'date' ) );
				if ( $date_type === 'time' ) {
					return preg_match( '/^\d{2}:\d{2}/', $date ) ? $date : '';
				}
				if ( $date_type === 'datetime-local' ) {
					return preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/', $date ) ? $date : '';
				}

				return preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ? $date : '';
			default:
				return sanitize_text_field( (string) $field_value );
		}
	}

	private function sanitizeOptionValues( mixed $value, string $page_slug ): array {
		$page = $this->getPageBySlug( $page_slug );
		$fields = is_array( $page ) && isset( $page['fields'] ) && is_array( $page['fields'] ) ? $page['fields'] : [];
		$raw = is_array( $value ) ? $value : [];
		$option_key = 'wp_powersuite_opb_' . sanitize_key( $page_slug );
		$existing   = get_option( $option_key, [] );
		$existing   = is_array( $existing ) ? $existing : [];
		$settings_group = 'wpps_opb_' . sanitize_key( $page_slug );
		$out = $existing;

		$eval_values = $existing;
		foreach ( $raw as $raw_key => $raw_val ) {
			$eval_values[ sanitize_key( (string) $raw_key ) ] = $raw_val;
		}

		foreach ( $fields as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}
			$type = sanitize_key( (string) ( $field['type'] ?? 'text' ) );
			if ( in_array( $type, [ 'group', 'message' ], true ) ) {
				continue;
			}
			$field_id = isset( $field['field_id'] ) && $field['field_id'] !== '' ? sanitize_key( (string) $field['field_id'] ) : sanitize_key( (string) ( $field['id'] ?? '' ) );
			if ( $field_id === '' ) {
				continue;
			}

			if ( ! wpps_opb_field_is_visible( $field, $eval_values, $fields ) ) {
				continue;
			}

			$is_required = ! empty( $field['required'] );
			$in_raw      = array_key_exists( $field_id, $raw );

			if ( ! $in_raw ) {
				if ( $is_required && in_array( $type, [ 'radio', 'select' ], true ) ) {
					$this->addFieldRequiredError( $settings_group, $field_id, $field );
				} elseif ( $type === 'radio' && ! $is_required ) {
					$out[ $field_id ] = '';
				} elseif ( $type === 'toggle' ) {
					$out[ $field_id ] = '';
				}
				continue;
			}

			$field_value = $raw[ $field_id ];

			if ( $is_required && $this->isFieldValueEmpty( $type, $field_value ) ) {
				$this->addFieldRequiredError( $settings_group, $field_id, $field );
				continue;
			}

			$sanitized = $this->sanitizeSubmittedFieldValue( $field, $field_value, $settings_group );
			if ( $sanitized === false ) {
				continue;
			}
			$out[ $field_id ] = $sanitized;
		}

		if ( is_array( $page ) && ! empty( $page['slug'] ) ) {
			$this->syncOptionAutoload( 'wp_powersuite_opb_' . sanitize_key( $page['slug'] ), ! empty( $page['autoload'] ) );
		}

		return $out;
	}

	/**
	 * @return array<string, string>
	 */
	public function getParentMenuOptions( string $selected = '' ): array {
		$options = [
			''                        => __( 'Top Level (root menu)', 'wp-powersuite' ),
			'index.php'               => __( 'Dashboard', 'wp-powersuite' ),
			'edit.php'                => __( 'Posts', 'wp-powersuite' ),
			'upload.php'              => __( 'Media', 'wp-powersuite' ),
			'edit.php?post_type=page' => __( 'Pages', 'wp-powersuite' ),
			'edit-comments.php'       => __( 'Comments', 'wp-powersuite' ),
			'themes.php'              => __( 'Appearance', 'wp-powersuite' ),
			'plugins.php'             => __( 'Plugins', 'wp-powersuite' ),
			'users.php'               => __( 'Users', 'wp-powersuite' ),
			'tools.php'               => __( 'Tools', 'wp-powersuite' ),
			'options-general.php'     => __( 'Settings', 'wp-powersuite' ),
			'powersuite'              => __( 'WP PowerSuite', 'wp-powersuite' ),
		];

		$skip = array_merge( array_keys( $options ), [ self::BUILDER_SLUG, 'powersuite-options-page-builder' ] );
		global $menu;
		if ( is_array( $menu ) ) {
			foreach ( $menu as $item ) {
				if ( ! is_array( $item ) || empty( $item[2] ) ) {
					continue;
				}
				$slug = (string) $item[2];
				if ( in_array( $slug, $skip, true ) ) {
					continue;
				}
				if ( ! isset( $options[ $slug ] ) ) {
					$title = wp_strip_all_tags( (string) ( $item[0] ?? $slug ) );
					$options[ $slug ] = $title . ' (' . $slug . ')';
				}
			}
		}

		if ( $selected !== '' && ! isset( $options[ $selected ] ) ) {
			$options[ $selected ] = $selected;
		}

		return $options;
	}

	private function syncOptionAutoload( string $option_name, bool $autoload ): void {
		global $wpdb;
		if ( ! isset( $wpdb ) ) {
			return;
		}

		$desired = $autoload ? 'yes' : 'no';
		$current = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT autoload FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
				$option_name
			)
		);

		if ( $current === null || $current === $desired ) {
			return;
		}

		$wpdb->update(
			$wpdb->options,
			[ 'autoload' => $desired ],
			[ 'option_name' => $option_name ],
			[ '%s' ],
			[ '%s' ]
		);
		wp_cache_delete( $option_name, 'options' );
		wp_cache_delete( 'alloptions', 'options' );
	}

	/**
	 * @return array{id: int, url: string}
	 */
	private function resolveImageField( mixed $value ): array {
		if ( is_numeric( $value ) && (int) $value > 0 ) {
			$id  = (int) $value;
			$url = wp_get_attachment_image_url( $id, 'medium' );
			return [
				'id'  => $id,
				'url' => is_string( $url ) ? $url : '',
			];
		}

		if ( is_string( $value ) && $value !== '' ) {
			if ( filter_var( $value, FILTER_VALIDATE_URL ) ) {
				return [
					'id'  => attachment_url_to_postid( $value ),
					'url' => $value,
				];
			}
		}

		return [ 'id' => 0, 'url' => '' ];
	}

	public function renderOptionsPage(): void {
		$page_slug = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
		if ( empty( $page_slug ) ) {
			echo '<div class="wrap"><p>' . esc_html__( 'Invalid page.', 'wp-powersuite' ) . '</p></div>';
			return;
		}

		$page = $this->getPageBySlug( $page_slug );
		if ( ! $page ) {
			echo '<div class="wrap"><p>' . esc_html__( 'Page not found.', 'wp-powersuite' ) . '</p></div>';
			return;
		}

		$cap = $this->sanitizeCapability( ! empty( $page['capability'] ) ? (string) $page['capability'] : 'manage_options' );
		if ( ! current_user_can( $cap ) ) {
			wp_die( esc_html__( 'You do not have sufficient permissions.', 'wp-powersuite' ) );
		}

		$option_name = 'wp_powersuite_opb_' . sanitize_key( $page['slug'] );
		$values = get_option( $option_name, [] );
		$values = is_array( $values ) ? $values : [];
		$fields = $page['fields'] ?? [];

		include __DIR__ . '/page-render.php';
	}

	/**
	 * @return array<int, string>
	 */
	private function getDashiconsList(): array {
		static $icons = null;

		if ( is_array( $icons ) ) {
			return $icons;
		}

		$css_path = ABSPATH . WPINC . '/css/dashicons.css';
		if ( is_readable( $css_path ) ) {
			$css = file_get_contents( $css_path );
			if ( is_string( $css ) && preg_match_all( '/\.(dashicons-[a-z0-9-]+):before/', $css, $matches ) ) {
				$icons = array_values( array_unique( $matches[1] ) );
				sort( $icons, SORT_STRING );

				return $icons;
			}
		}

		$icons = [
			'dashicons-admin-generic',
			'dashicons-admin-settings',
			'dashicons-admin-tools',
			'dashicons-admin-site',
			'dashicons-admin-appearance',
			'dashicons-admin-plugins',
			'dashicons-admin-users',
			'dashicons-admin-post',
			'dashicons-admin-page',
			'dashicons-admin-media',
			'dashicons-admin-comments',
			'dashicons-admin-home',
			'dashicons-format-image',
			'dashicons-format-gallery',
			'dashicons-calendar-alt',
			'dashicons-email',
			'dashicons-star-filled',
			'dashicons-heart',
			'dashicons-groups',
			'dashicons-yes',
		];

		return $icons;
	}

	public function enqueueBuilderAssets( string $hook ): void {
		$builder_hook = 'wp-powersuite_page_' . self::BUILDER_SLUG;
		if ( $hook !== $builder_hook && strpos( $hook, self::BUILDER_SLUG ) === false ) {
			return;
		}

		$base = wp_powersuite_module_assets_url( __FILE__ );
		wp_enqueue_style( 'dashicons' );
		wp_enqueue_script( 'jquery-ui-sortable' );
		wp_enqueue_style( 'wpps-opb-admin', $base . 'opb-admin.css', [ 'wp-powersuite-admin', 'dashicons' ], self::MODULE_VER );
		wp_enqueue_script( 'wpps-opb-field-builder', $base . 'opb-field-builder.js', [ 'jquery' ], self::MODULE_VER, true );
		wp_enqueue_script( 'wpps-opb-admin', $base . 'opb-admin.js', [ 'jquery', 'jquery-ui-sortable', 'wpps-opb-field-builder' ], self::MODULE_VER, true );

		wp_localize_script( 'wpps-opb-admin', 'wppsOpb', [
			'nonce'      => wp_create_nonce( 'wpps_opb' ),
			'ajaxUrl'    => admin_url( 'admin-ajax.php' ),
			'dashicons'  => $this->getDashiconsList(),
			'strings'    => [
				'saved'        => __( 'Saved successfully.', 'wp-powersuite' ),
				'saveLabel'    => __( 'Save Options Page', 'wp-powersuite' ),
				'error'        => __( 'An error occurred. Please try again.', 'wp-powersuite' ),
				'required'     => __( 'Page title, menu title, and slug are required.', 'wp-powersuite' ),
				'confirm'      => __( 'Are you sure you want to delete this page? This cannot be undone.', 'wp-powersuite' ),
				'searchIcons'  => __( 'Search icons…', 'wp-powersuite' ),
				'noIconsMatch' => __( 'No icons match your search.', 'wp-powersuite' ),
			],
		] );
	}

	public function enqueuePageAssets( string $hook ): void {
		if ( ! $this->isCurrentLiveOpbPage() ) {
			return;
		}

		$this->enqueueLivePageAssets();
	}

	/**
	 * Whether the current admin request is a registered OPB live options screen.
	 */
	private function isCurrentLiveOpbPage(): bool {
		$current = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( (string) $_GET['page'] ) ) : '';
		if ( $current === '' || $current === self::BUILDER_SLUG ) {
			return false;
		}

		foreach ( $this->getPages() as $page ) {
			if ( ! is_array( $page ) || empty( $page['slug'] ) ) {
				continue;
			}
			if ( sanitize_key( (string) $page['slug'] ) === $current ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Enqueue styles/scripts for live OPB options screens (outside the PowerSuite submenu).
	 */
	public function enqueueLivePageAssets(): void {
		static $enqueued = false;
		if ( $enqueued ) {
			return;
		}
		$enqueued = true;

		$base    = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		$version = defined( 'WP_POWERSUITE_VERSION' ) ? WP_POWERSUITE_VERSION : self::MODULE_VER;

		if ( $base === '' ) {
			return;
		}

		if ( ! wp_style_is( 'wp-powersuite-admin', 'registered' ) ) {
			wp_register_style(
				'wp-powersuite-admin',
				$base . 'assets/admin/app.css',
				[ 'wp-admin' ],
				$version
			);
		}
		wp_enqueue_style( 'wp-powersuite-admin' );

		if ( ! wp_style_is( 'wp-powersuite-settings', 'registered' ) ) {
			wp_register_style(
				'wp-powersuite-settings',
				$base . 'assets/admin/settings.css',
				[ 'wp-powersuite-admin' ],
				$version
			);
		}
		wp_enqueue_style( 'wp-powersuite-settings' );

		if ( ! wp_style_is( 'wp-powersuite-color-panel', 'registered' ) ) {
			wp_register_style(
				'wp-powersuite-color-panel',
				$base . 'assets/admin/color-panel.css',
				[ 'wp-powersuite-admin' ],
				$version
			);
		}
		wp_enqueue_style( 'wp-powersuite-color-panel' );

		if ( ! wp_script_is( 'wp-powersuite-color-panel', 'registered' ) ) {
			wp_register_script(
				'wp-powersuite-color-panel',
				$base . 'assets/admin/color-panel.js',
				[ 'jquery' ],
				$version,
				true
			);
		}
		wp_enqueue_script( 'wp-powersuite-color-panel' );

		wp_enqueue_media();
		wp_enqueue_editor();
		$mod = wp_powersuite_module_assets_url( __FILE__ );
		wp_enqueue_style(
			'wpps-opb-page',
			$mod . 'opb-admin.css',
			[ 'wp-powersuite-admin', 'wp-powersuite-settings', 'wp-powersuite-color-panel' ],
			self::MODULE_VER
		);
		wp_enqueue_script( 'jquery-ui-sortable' );
		wp_enqueue_script(
			'wpps-opb-repeater',
			$mod . 'opb-repeater.js',
			[ 'jquery', 'jquery-ui-sortable' ],
			self::MODULE_VER,
			true
		);
		$module_settings_ui = defined( 'WP_POWERSUITE_SRC_DIR' )
			? WP_POWERSUITE_SRC_DIR . 'Admin' . DIRECTORY_SEPARATOR . 'ModuleSettingsUi.php'
			: '';
		if ( $module_settings_ui !== '' && is_readable( $module_settings_ui ) ) {
			require_once $module_settings_ui;
			wpps_enqueue_accordion_tooltip_script();
		}
		add_action( 'admin_footer', [ $this, 'addMediaScript' ] );
	}

	/**
	 * Help tooltip after a live field label (Code Snippets pattern).
	 *
	 * @param string $description Field help text.
	 */
	public function renderFieldDescriptionTooltip( string $description ): void {
		$description = trim( $description );
		if ( $description === '' ) {
			return;
		}
		?>
		<span class="wpps-opb-accordion-tooltip" aria-label="<?php esc_attr_e( 'Help', 'wp-powersuite' ); ?>">
			<span class="wpps-opb-accordion-tooltip-trigger">?</span>
			<span class="wpps-opb-accordion-tooltip-panel"><?php echo esc_html( $description ); ?></span>
		</span>
		<?php
	}

	public function addMediaScript(): void {
		?>
		<script>
		jQuery(function($) {
			$('.wpps-opb-accordion-tooltip').on('click', function(e) {
				e.stopPropagation();
			});
			// How to use modal
			$('#wpps-opb-how-to-use-btn, .wpps-opb-help-btn').on('click', function() {
				$('#wpps-opb-how-to-modal').attr('aria-hidden', 'false').addClass('is-open');
				$('body').css('overflow', 'hidden');
			});
			$('#wpps-opb-how-to-modal .wpps-opb-modal-content').on('click', function(e) {
				if (!$(e.target).closest('.wpps-opb-modal-copy').length) {
					e.stopPropagation();
				}
			});
			$('.wpps-opb-modal-close, .wpps-opb-modal-backdrop').on('click', function() {
				$('#wpps-opb-how-to-modal').attr('aria-hidden', 'true').removeClass('is-open');
				$('body').css('overflow', '');
			});
			$(document).on('keydown', function(e) {
				if (e.key === 'Escape' && $('#wpps-opb-how-to-modal').hasClass('is-open')) {
					$('#wpps-opb-how-to-modal').attr('aria-hidden', 'true').removeClass('is-open');
					$('body').css('overflow', '');
				}
			});
			// Modal tabs
			$('#wpps-opb-how-to-modal .wpps-opb-modal-tab').on('click', function() {
				var tab = $(this).data('tab');
				var $modal = $('#wpps-opb-how-to-modal');
				$modal.find('.wpps-opb-modal-tab').removeClass('is-active').attr('aria-selected', 'false');
				$(this).addClass('is-active').attr('aria-selected', 'true');
				$modal.find('.wpps-opb-modal-panel').prop('hidden', true);
				$modal.find('.wpps-opb-modal-panel[data-panel="' + tab + '"]').prop('hidden', false);
			});
			function wppsOpbGetCopyButtonLabel($btn) {
				return $btn.data('opb-copy-label') || $.trim($btn.text()) || '<?php echo esc_js( __( 'Copy', 'wp-powersuite' ) ); ?>';
			}
			function wppsOpbCopyTextToClipboard(text, onSuccess, onFail) {
				text = String(text || '');
				if (!text) {
					if (typeof onFail === 'function') onFail();
					return;
				}
				var fallback = function() {
					var ta = document.createElement('textarea');
					ta.value = text;
					ta.setAttribute('readonly', '');
					ta.style.position = 'fixed';
					ta.style.top = '0';
					ta.style.left = '0';
					ta.style.width = '2em';
					ta.style.height = '2em';
					ta.style.padding = '0';
					ta.style.border = 'none';
					ta.style.outline = 'none';
					ta.style.boxShadow = 'none';
					ta.style.background = 'transparent';
					ta.style.opacity = '0';
					document.body.appendChild(ta);
					ta.focus();
					ta.select();
					ta.setSelectionRange(0, text.length);
					var ok = false;
					try {
						ok = document.execCommand('copy');
					} catch (err) {}
					document.body.removeChild(ta);
					if (ok && typeof onSuccess === 'function') {
						onSuccess();
					} else if (typeof onFail === 'function') {
						onFail();
					}
				};
				if (navigator.clipboard && navigator.clipboard.writeText) {
					navigator.clipboard.writeText(text).then(onSuccess).catch(fallback);
					return;
				}
				fallback();
			}
			function wppsOpbGetModalCopyText($btn) {
				var $wrap = $btn.closest('.wpps-opb-modal-code-wrap');
				if (!$wrap.length) return '';
				var codeEl = $wrap.find('code').get(0);
				if (codeEl) {
					return String(codeEl.textContent || codeEl.innerText || '').trim();
				}
				return String($btn.attr('data-copy') || '').trim();
			}
			function wppsOpbMarkCopyButtonCopied($btn) {
				var label = $btn.data('opb-copy-label');
				var copiedLabel = '<?php echo esc_js( __( 'Copied!', 'wp-powersuite' ) ); ?>';
				var isIconBtn = $btn.find('.dashicons').length > 0;
				if (isIconBtn) {
					$btn.addClass('is-copied').attr('aria-label', copiedLabel);
					setTimeout(function() {
						$btn.removeClass('is-copied').attr('aria-label', label);
					}, 2000);
					return;
				}
				$btn.text(copiedLabel);
				setTimeout(function() { $btn.text(label); }, 2000);
			}
			// Copy code buttons (bind on modal — document delegation is blocked by stopPropagation).
			$('#wpps-opb-how-to-modal').on('click', '.wpps-opb-modal-copy', function(e) {
				e.preventDefault();
				e.stopPropagation();
				var $btn = $(this);
				if (!$btn.data('opb-copy-label')) {
					$btn.data('opb-copy-label', $btn.attr('aria-label') || wppsOpbGetCopyButtonLabel($btn));
				}
				var code = wppsOpbGetModalCopyText($btn);
				if (!code) return;
				wppsOpbCopyTextToClipboard(code, function() {
					wppsOpbMarkCopyButtonCopied($btn);
				});
			});
			function wppsOpbOpenMediaPicker(btn) {
				var targetId = btn.data('target');
				var kind = btn.data('kind') || 'image';
				var multiple = btn.data('multiple') === 1 || btn.data('multiple') === '1';
				var input = $('#' + targetId);
				var preview = btn.siblings('.wpps-opb-image-preview');
				var frame = wp.media({
					title: kind === 'file' ? '<?php echo esc_js( __( 'Choose File', 'wp-powersuite' ) ); ?>' : '<?php echo esc_js( __( 'Choose Image', 'wp-powersuite' ) ); ?>',
					button: { text: '<?php echo esc_js( __( 'Use selected', 'wp-powersuite' ) ); ?>' },
					multiple: multiple
				});
				frame.on('select', function() {
					var selection = frame.state().get('selection');
					var ids = [], html = '';
					selection.each(function(att) {
						var json = att.toJSON();
						if (!json.id) return;
						ids.push(String(json.id));
						html += '<div class="wpps-opb-media-item" data-id="' + json.id + '">';
						html += '<button type="button" class="wpps-opb-image-remove" data-target="' + targetId + '" data-id="' + json.id + '" aria-label="<?php echo esc_attr( __( 'Remove', 'wp-powersuite' ) ); ?>"><span class="dashicons dashicons-no-alt"></span></button>';
						if (kind === 'image' && json.url) {
							html += '<img src="' + json.url + '" alt="">';
						} else {
							html += '<span class="wpps-opb-file-name">' + (json.title || json.filename || json.id) + '</span>';
						}
						html += '</div>';
					});
					input.val(ids.join(','));
					preview.html(html).toggle(ids.length > 0);
				});
				frame.open();
			}
			$(document).on('click', '.wpps-opb-upload-btn, .wpps-opb-file-upload-btn', function(e) {
				e.preventDefault();
				wppsOpbOpenMediaPicker($(this));
			});
			if ($.fn.sortable) {
				$('.wpps-opb-gallery-preview').sortable({
					items: '.wpps-opb-media-item',
					placeholder: 'wpps-opb-gallery-placeholder',
					update: function() {
						var $preview = $(this);
						var targetId = $preview.siblings('input[type="hidden"]').attr('id');
						if (!targetId) return;
						var ids = [];
						$preview.find('.wpps-opb-media-item').each(function() {
							var id = $(this).data('id');
							if (id) ids.push(String(id));
						});
						$('#' + targetId).val(ids.join(','));
					}
				});
			}
			$(document).on('click', '.wpps-opb-image-remove', function(e) {
				e.preventDefault();
				var btn = $(this), targetId = btn.data('target'), removeId = String(btn.data('id') || ''), wrap = btn.closest('.wpps-opb-media-wrap, .wpps-opb-image-wrap');
				var input = $('#' + targetId);
				if (removeId) {
					var ids = String(input.val() || '').split(',').filter(function(id) { return id && id !== removeId; });
					input.val(ids.join(','));
					btn.closest('.wpps-opb-media-item').remove();
					if (!wrap.find('.wpps-opb-media-item').length) wrap.find('.wpps-opb-image-preview').empty().hide();
				} else {
					input.val('');
					wrap.find('.wpps-opb-image-preview').empty().hide();
				}
			});
			$(document).on('click', '.wpps-opb-radio-clear', function(e) {
				e.preventDefault();
				var target = $(this).data('target');
				$('input[name*="' + target + '"][type="radio"]').prop('checked', false);
				wppsOpbSyncRadioButtons($(this).closest('.wpps-opb-live-field__control'));
				wppsOpbSyncConditionals();
			});
			$(document).on('click', '.wpps-opb-color-swatch', function(e) {
				e.preventDefault();
				var color = $(this).data('color'), target = $(this).data('target');
				if (target) {
					$('#' + target).val(color.length > 7 ? color.substring(0, 7) : color).trigger('change');
					$('.wpps-color-code-input[data-color-input="' + target + '"]').val(color).trigger('change');
				}
			});
			function wppsOpbNormalizeOperand(value, fieldType) {
				if (fieldType !== 'checkbox' && fieldType !== 'toggle') return String(value == null ? '' : value);
				var lower = String(value == null ? '' : value).toLowerCase().trim();
				if (lower === '1' || lower === 'on' || lower === 'yes' || lower === 'true' || lower === 'checked') return '1';
				return '0';
			}
			function wppsOpbGetFieldValue(form, fieldId) {
				var suffix = '[' + fieldId + ']';
				var $toggle = form.find('.wpps-opb-toggle > input[type="checkbox"][name$="' + suffix + '"]');
				if ($toggle.length) return $toggle.is(':checked') ? '1' : '0';
				var $checkbox = form.find('input[type="checkbox"][name$="' + suffix + '"]:not(.wpps-opb-toggle input)');
				if ($checkbox.length) return $checkbox.is(':checked') ? '1' : '0';
				var $radio = form.find('input[type="radio"][name$="' + suffix + '"]:checked');
				if ($radio.length) return String($radio.val() || '');
				var $multi = form.find('select[name$="' + suffix + '[]"]');
				if ($multi.length) {
					var multiVal = $multi.val();
					return Array.isArray(multiVal) ? multiVal.join(',') : String(multiVal || '');
				}
				var $selectValue = form.find('input.wpps-opb-select-value[name$="' + suffix + '"]');
				if ($selectValue.length) return String($selectValue.val() || '');
				var $select = form.find('select[name$="' + suffix + '"]');
				if ($select.length) return String($select.val() || '');
				var $text = form.find('input[name$="' + suffix + '"]:not([type="hidden"]), textarea[name$="' + suffix + '"]');
				if ($text.length) return String($text.first().val() || '');
				var $hidden = form.find('input[type="hidden"][name$="' + suffix + '"]');
				if ($hidden.length) return String($hidden.first().val() || '');
				return '';
			}
			function wppsOpbEvaluateConditional($field, form) {
				var op = $field.attr('data-opb-cond-operator') || 'equals';
				var dep = $field.attr('data-opb-cond-field') || '';
				var depType = $field.attr('data-opb-cond-dep-type') || 'text';
				var expected = wppsOpbNormalizeOperand($field.attr('data-opb-cond-value') || '', depType);
				var actual = dep ? wppsOpbNormalizeOperand(wppsOpbGetFieldValue(form, dep), depType) : '';
				if (op === 'not_equals') return actual !== expected;
				if (op === 'empty') return actual === '' || actual === '0';
				if (op === 'not_empty') return actual !== '' && actual !== '0';
				if (op === 'contains') return expected !== '' && actual.indexOf(expected) !== -1;
				return actual === expected;
			}
			function wppsOpbSetFieldInputsEnabled($field, enabled) {
				$field.find('input, select, textarea, button').each(function() {
					var $input = $(this);
					if ($input.is('.wpps-opb-radio-clear, .wpps-opb-upload-btn, .wpps-opb-file-upload-btn, .wpps-opb-image-remove, .wpps-opb-color-reset, .wpps-opb-color-swatch, .wpps-opb-select-item')) {
						return;
					}
					if (!enabled) {
						if ($input.prop('required')) {
							$input.data('opb-was-required', true);
							$input.prop('required', false);
						}
						$input.prop('disabled', true);
						return;
					}
					if ($input.data('opb-was-required')) {
						$input.prop('required', true);
					}
					$input.prop('disabled', false);
				});
			}
			function wppsOpbSyncConditionals() {
				var form = $('.wpps-opb-page-form');
				if (!form.length) return;
				form.find('[data-opb-conditional="1"]').each(function() {
					var $field = $(this);
					var visible = wppsOpbEvaluateConditional($field, form);
					$field.toggleClass('wpps-opb-live-field--hidden', !visible);
					wppsOpbSetFieldInputsEnabled($field, visible);
				});
			}
			wppsOpbSyncConditionals();
			$(document).on('change input', '.wpps-opb-page-form input, .wpps-opb-page-form select, .wpps-opb-page-form textarea', wppsOpbSyncConditionals);
			// Color field: sync picker <-> hex input (login customizer style)
			$(document).on('input change', '.wpps-color-picker-circle', function() {
				var colorInputId = $(this).attr('id');
				if (colorInputId) {
					var $textInput = $('.wpps-color-code-input[data-color-input="' + colorInputId + '"]');
					if ($textInput.length) $textInput.val($(this).val()).trigger('change');
				}
			});
			$(document).on('input change', '.wpps-color-code-input', function() {
				var hex = ($(this).val() || '').trim();
				if (hex.length > 0 && hex.indexOf('#') !== 0) hex = '#' + hex;
				if (/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(hex)) {
					var colorInputId = $(this).data('color-input');
					if (colorInputId) {
						var $colorInput = $('#' + colorInputId);
						if ($colorInput.length) $colorInput.val(hex.length > 7 ? hex.substring(0, 7) : hex);
					}
				}
			});
			$(document).on('click', '.wpps-opb-sidebar-nav .wpps-opb-nav-link', function(e) {
				e.preventDefault();
				var section = $(this).data('section');
				var $layout = $(this).closest('.wpps-opb-page-layout');
				$layout.find('.wpps-opb-nav-link').removeClass('is-active');
				$(this).addClass('is-active');
				$layout.find('.wpps-opb-section-card').removeClass('is-active');
				$layout.find('#opb-section-' + section).addClass('is-active');
			});
			function wppsOpbSyncRadioButtons($scope) {
				$scope.find('.wpps-opb-radio-group--buttons .wpps-opb-radio-item').each(function() {
					$(this).toggleClass('is-selected', $(this).find('input[type="radio"]').is(':checked'));
				});
			}
			wppsOpbSyncRadioButtons($('.wpps-opb-live-fields'));
			$(document).on('change', '.wpps-opb-radio-group--buttons input[type="radio"]', function() {
				wppsOpbSyncRadioButtons($(this).closest('.wpps-opb-radio-group'));
			});
			function wppsOpbSyncSelectButtons($scope) {
				$scope.find('.wpps-opb-select-wrap').each(function() {
					var $wrap = $(this), $input = $wrap.find('.wpps-opb-select-value'), val = String($input.val() || '');
					$wrap.find('.wpps-opb-select-item').each(function() {
						var selected = String($(this).data('value') || '') === val;
						$(this).toggleClass('is-selected', selected).attr('aria-selected', selected ? 'true' : 'false');
					});
				});
			}
			wppsOpbSyncSelectButtons($('.wpps-opb-live-fields'));
			$(document).on('click', '.wpps-opb-select-item:not(:disabled)', function(e) {
				e.preventDefault();
				var $item = $(this), $wrap = $item.closest('.wpps-opb-select-wrap');
				$wrap.find('.wpps-opb-select-value').val(String($item.data('value') || '')).trigger('change');
				wppsOpbSyncSelectButtons($wrap);
				wppsOpbSyncConditionals();
			});
			$(document).on('click', '.wpps-opb-color-reset', function(e) {
				e.preventDefault();
				var $resetButton = $(this);
				var defaultColor = $resetButton.data('default-color');
				var $textInput = $resetButton.prev('.wpps-color-code-input');
				var colorInputId = $textInput.data('color-input');
				var $colorInput = colorInputId ? $('#' + colorInputId) : $();
				$textInput.val(defaultColor);
				if ($colorInput.length) $colorInput.val(defaultColor.length > 7 ? defaultColor.substring(0, 7) : defaultColor);
				$textInput.trigger('change');
			});
		});
		</script>
		<?php
	}

	public function ajaxSavePage(): void {
		check_ajax_referer( 'wpps_opb', 'nonce' );
		if ( ! $this->canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}

		$page_id = isset( $_POST['page_id'] ) ? sanitize_key( $_POST['page_id'] ) : '';
		$page_title = isset( $_POST['page_title'] ) ? sanitize_text_field( wp_unslash( $_POST['page_title'] ) ) : '';
		$menu_title = isset( $_POST['menu_title'] ) ? sanitize_text_field( wp_unslash( $_POST['menu_title'] ) ) : '';
		$menu_slug = isset( $_POST['menu_slug'] ) ? sanitize_key( $_POST['menu_slug'] ) : '';
		$description = isset( $_POST['page_description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['page_description'] ) ) : '';
		$parent_slug = isset( $_POST['parent_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['parent_slug'] ) ) : '';
		$capability = $this->sanitizeCapability( isset( $_POST['capability'] ) ? (string) wp_unslash( $_POST['capability'] ) : 'manage_options' );
		$icon = isset( $_POST['icon'] ) ? sanitize_text_field( wp_unslash( $_POST['icon'] ) ) : 'dashicons-admin-generic';
		$position = isset( $_POST['position'] ) ? absint( $_POST['position'] ) : null;
		$autoload = ! empty( $_POST['autoload'] );
		$save_button = isset( $_POST['save_button'] ) ? sanitize_text_field( wp_unslash( $_POST['save_button'] ) ) : '';
		$updated_message = isset( $_POST['updated_message'] ) ? sanitize_text_field( wp_unslash( $_POST['updated_message'] ) ) : '';

		if ( empty( $page_title ) || empty( $menu_title ) || empty( $menu_slug ) ) {
			wp_send_json_error( [ 'message' => __( 'Page title, menu title, and slug are required.', 'wp-powersuite' ) ] );
		}

		$fields = [];
		if ( isset( $_POST['fields'] ) && is_string( $_POST['fields'] ) ) {
			$raw = json_decode( wp_unslash( $_POST['fields'] ), true );
			if ( is_array( $raw ) ) {
				$fields = $this->sanitizeFieldsFromRaw( $raw );
			}
		}

		$duplicate_error = $this->validateFieldIds( $fields );
		if ( $duplicate_error !== null ) {
			wp_send_json_error( [ 'message' => $duplicate_error ] );
		}

		$pages = $this->getPages();
		if ( empty( $page_id ) ) {
			$page_id = 'opb_' . time();
		} else {
			$existing_page = $this->getPageById( $page_id );
			if ( $existing_page !== null && ! empty( $existing_page['slug'] ) && sanitize_key( (string) $existing_page['slug'] ) !== $menu_slug ) {
				wp_send_json_error( [ 'message' => __( 'Menu slug cannot be changed after creation.', 'wp-powersuite' ) ] );
			}
		}

		foreach ( $pages as $p ) {
			if ( is_array( $p ) && isset( $p['slug'] ) && $p['slug'] === $menu_slug && ( $p['id'] ?? '' ) !== $page_id ) {
				wp_send_json_error( [ 'message' => __( 'A page with this slug already exists.', 'wp-powersuite' ) ] );
			}
		}

		$page_data = [
			'id'               => $page_id,
			'page_title'       => $page_title,
			'page_description' => $description,
			'menu_title'       => $menu_title,
			'slug'             => $menu_slug,
			'parent_slug'      => $parent_slug,
			'capability'       => $capability,
			'icon'             => $icon,
			'position'         => $position,
			'autoload'         => $autoload,
			'save_button'      => $save_button,
			'updated_message'  => $updated_message,
			'fields'           => $fields,
		];

		$found = false;
		foreach ( $pages as $k => $p ) {
			if ( is_array( $p ) && ( $p['id'] ?? '' ) === $page_id ) {
				$pages[ $k ] = $page_data;
				$found = true;
				break;
			}
		}
		if ( ! $found ) {
			$pages[] = $page_data;
		}

		update_option( self::PAGES_OPTION, $pages, false );
		$this->syncOptionAutoload( 'wp_powersuite_opb_' . $menu_slug, $autoload );
		wp_send_json_success( [ 'message' => __( 'Options page saved.', 'wp-powersuite' ), 'page_id' => $page_id ] );
	}

	public function ajaxDeletePage(): void {
		check_ajax_referer( 'wpps_opb', 'nonce' );
		if ( ! $this->canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}

		$page_id = isset( $_POST['page_id'] ) ? sanitize_key( $_POST['page_id'] ) : '';
		if ( empty( $page_id ) ) {
			wp_send_json_error( [ 'message' => __( 'No page ID.', 'wp-powersuite' ) ] );
		}

		$pages = $this->getPages();
		$slug = '';
		foreach ( $pages as $k => $p ) {
			if ( is_array( $p ) && ( $p['id'] ?? '' ) === $page_id ) {
				$slug = $p['slug'] ?? '';
				unset( $pages[ $k ] );
				break;
			}
		}

		$pages = array_values( $pages );
		update_option( self::PAGES_OPTION, $pages, false );
		if ( $slug ) {
			delete_option( 'wp_powersuite_opb_' . $slug );
		}
		wp_send_json_success( [ 'message' => __( 'Page deleted.', 'wp-powersuite' ) ] );
	}

	public function ajaxExportPages(): void {
		check_ajax_referer( 'wpps_opb', 'nonce' );
		if ( ! $this->canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}
		$pages = $this->getPages();
		$export = [
			'version'  => '1.1',
			'source'   => 'wp-powersuite-opb',
			'exported' => gmdate( 'Y-m-d H:i:s' ),
			'pages'    => $pages,
			'values'   => $this->collectPageValuesForExport( $pages ),
		];
		wp_send_json_success( [ 'data' => wp_json_encode( $export, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ] );
	}

	public function ajaxExportPage(): void {
		check_ajax_referer( 'wpps_opb', 'nonce' );
		if ( ! $this->canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}
		$page_id = isset( $_POST['page_id'] ) ? sanitize_key( wp_unslash( $_POST['page_id'] ) ) : '';
		if ( empty( $page_id ) ) {
			wp_send_json_error( [ 'message' => __( 'No page ID.', 'wp-powersuite' ) ] );
		}
		$pages = $this->getPages();
		$page = null;
		foreach ( $pages as $p ) {
			if ( is_array( $p ) && ( $p['id'] ?? '' ) === $page_id ) {
				$page = $p;
				break;
			}
		}
		if ( ! $page ) {
			wp_send_json_error( [ 'message' => __( 'Page not found.', 'wp-powersuite' ) ] );
		}
		$slug = sanitize_key( (string) ( $page['slug'] ?? 'page' ) );
		$stored = get_option( 'wp_powersuite_opb_' . $slug, [] );
		$values = [];
		if ( is_array( $stored ) && $stored !== [] ) {
			$values[ $slug ] = $stored;
		}
		$export = [
			'version'  => '1.1',
			'source'   => 'wp-powersuite-opb',
			'exported' => gmdate( 'Y-m-d H:i:s' ),
			'pages'    => [ $page ],
			'values'   => $values,
		];
		$filename = 'wp-powersuite-opb-' . sanitize_file_name( $slug );
		wp_send_json_success( [
			'data'     => wp_json_encode( $export, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ),
			'filename' => $filename,
		] );
	}

	public function ajaxImportPages(): void {
		check_ajax_referer( 'wpps_opb', 'nonce' );
		if ( ! $this->canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}
		$json = isset( $_POST['json'] ) ? wp_unslash( $_POST['json'] ) : '';
		$replace = ! empty( $_POST['replace'] );
		if ( $json === '' ) {
			wp_send_json_error( [ 'message' => __( 'No import data provided.', 'wp-powersuite' ) ] );
		}
		$data = json_decode( $json, true );
		if ( ! is_array( $data ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid JSON.', 'wp-powersuite' ) ] );
		}
		$imported = $data['pages'] ?? $data;
		if ( ! is_array( $imported ) ) {
			wp_send_json_error( [ 'message' => __( 'No pages in import data.', 'wp-powersuite' ) ] );
		}
		$existing = $replace ? [] : $this->getPages();
		if ( $replace ) {
			foreach ( $this->getPages() as $p ) {
				if ( is_array( $p ) && ! empty( $p['slug'] ) ) {
					delete_option( 'wp_powersuite_opb_' . sanitize_key( $p['slug'] ) );
				}
			}
		}
		$slug_map = [];
		foreach ( $existing as $p ) {
			if ( is_array( $p ) && ! empty( $p['slug'] ) ) {
				$slug_map[ $p['slug'] ] = $p;
			}
		}
		$import_values = isset( $data['values'] ) && is_array( $data['values'] ) ? $data['values'] : [];
		foreach ( $imported as $p ) {
			if ( ! is_array( $p ) || empty( $p['slug'] ) ) {
				continue;
			}
			$slug = sanitize_key( $p['slug'] );
			$raw_fields = isset( $p['fields'] ) && is_array( $p['fields'] ) ? $p['fields'] : [];
			$fields = $this->sanitizeFieldsFromRaw( $raw_fields );
			$duplicate_error = $this->validateFieldIds( $fields );
			if ( $duplicate_error !== null ) {
				wp_send_json_error( [ 'message' => $duplicate_error ] );
			}
			$p['slug'] = $slug;
			$p['id'] = $p['id'] ?? 'opb_' . time() . '_' . wp_rand( 1000, 9999 );
			$p['capability'] = $this->sanitizeCapability( isset( $p['capability'] ) ? (string) $p['capability'] : 'manage_options' );
			$p['page_title'] = isset( $p['page_title'] ) ? sanitize_text_field( (string) $p['page_title'] ) : '';
			$p['menu_title'] = isset( $p['menu_title'] ) ? sanitize_text_field( (string) $p['menu_title'] ) : '';
			$p['page_description'] = isset( $p['page_description'] ) ? sanitize_textarea_field( (string) $p['page_description'] ) : '';
			$p['fields'] = $fields;
			$slug_map[ $slug ] = $p;
		}
		$pages = array_values( $slug_map );
		update_option( self::PAGES_OPTION, $pages, false );
		foreach ( $import_values as $value_slug => $value_data ) {
			$value_slug = sanitize_key( (string) $value_slug );
			if ( $value_slug === '' || ! is_array( $value_data ) ) {
				continue;
			}
			$page_def = $this->getPageBySlug( $value_slug );
			if ( $page_def === null ) {
				continue;
			}
			$sanitized = $this->sanitizeOptionValues( $value_data, $value_slug );
			$autoload = ! empty( $page_def['autoload'] );
			update_option( 'wp_powersuite_opb_' . $value_slug, $sanitized, $autoload );
		}
		wp_send_json_success( [ 'message' => __( 'Import successful.', 'wp-powersuite' ), 'count' => count( $pages ) ] );
	}

	private function wrapFieldAffix( array $field, string $html ): void {
		$prefix = ! empty( $field['prefix'] ) ? (string) $field['prefix'] : '';
		$suffix = ! empty( $field['suffix'] ) ? (string) $field['suffix'] : '';
		if ( $prefix === '' && $suffix === '' ) {
			echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in caller.
			return;
		}
		echo '<div class="wpps-opb-affix-wrap">';
		if ( $prefix !== '' ) {
			echo '<span class="wpps-opb-affix wpps-opb-affix--prefix">' . esc_html( $prefix ) . '</span>';
		}
		echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		if ( $suffix !== '' ) {
			echo '<span class="wpps-opb-affix wpps-opb-affix--suffix">' . esc_html( $suffix ) . '</span>';
		}
		echo '</div>';
	}

	public function renderField( array $field, string $field_name, $value ): void {
		$type     = sanitize_key( $field['type'] ?? 'text' );
		$required = ! empty( $field['required'] ) ? 'required' : '';
		$name     = $field['field_id'] ?? $field['name'] ?? $field['id'] ?? '';
		$attrs    = wpps_opb_build_input_attributes( $field, $required );

		if ( $type === 'message' ) {
			echo '<div class="wpps-opb-message-field">' . wp_kses_post( (string) ( $field['message_content'] ?? '' ) ) . '</div>';
			return;
		}

		switch ( $type ) {
			case 'textarea':
				$rows = ! empty( $field['rows'] ) ? absint( $field['rows'] ) : 5;
				$html = '<textarea name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" rows="' . esc_attr( $rows ) . '" class="large-text" ' . $attrs . '>' . esc_textarea( (string) $value ) . '</textarea>';
				$this->wrapFieldAffix( $field, $html );
				break;
			case 'number':
				$min  = isset( $field['min'] ) && $field['min'] !== '' ? ' min="' . esc_attr( $field['min'] ) . '"' : '';
				$max  = isset( $field['max'] ) && $field['max'] !== '' ? ' max="' . esc_attr( $field['max'] ) . '"' : '';
				$step = isset( $field['step'] ) && $field['step'] !== '' ? ' step="' . esc_attr( $field['step'] ) . '"' : '';
				$html = '<input type="number" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $value ) . '" class="small-text"' . $min . $max . $step . ' ' . $attrs . '>';
				$this->wrapFieldAffix( $field, $html );
				break;
			case 'email':
				$html = '<input type="email" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $value ) . '" class="regular-text" ' . $attrs . '>';
				$this->wrapFieldAffix( $field, $html );
				break;
			case 'password':
				$configured = is_string( $value ) && $value !== '';
				$ph         = $configured
					? esc_attr__( '•••••••• (saved — enter a new value to replace)', 'wp-powersuite' )
					: '';
				$html       = '<input type="password" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="" autocomplete="new-password" class="regular-text" placeholder="' . $ph . '" ' . $attrs . '>';
				$this->wrapFieldAffix( $field, $html );
				break;
			case 'url':
				$html = '<input type="url" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $value ) . '" class="regular-text" ' . $attrs . '>';
				$this->wrapFieldAffix( $field, $html );
				break;
			case 'color':
				$color_id = 'wpps-opb-color-' . esc_attr( $name );
				$default_color = '#000000';
				if ( ! empty( $field['default'] ) ) {
					$d = trim( $field['default'] );
					if ( $d !== '' && $d[0] !== '#' ) {
						$d = '#' . $d;
					}
					$d = sanitize_hex_color( $d );
					if ( $d ) {
						$default_color = $d;
					}
				}
				$color_val = $value ?: $default_color;
				if ( ! preg_match( '/^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/', $color_val ) ) {
					$color_val = $default_color;
				}
				$color_val_circle = strlen( $color_val ) > 7 ? substr( $color_val, 0, 7 ) : $color_val;
				echo '<div class="wpps-color-field-row">';
				echo '<input type="color" id="' . $color_id . '" value="' . esc_attr( $color_val_circle ) . '" class="wpps-color-picker-circle" data-color-input="' . $color_id . '">';
				echo '<div class="wpps-color-code-wrap">';
				echo '<input type="text" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( $color_val ) . '" class="regular-text wpps-color-code-input" data-color-input="' . $color_id . '" data-default-color="' . esc_attr( $default_color ) . '" style="padding-right: 25px;" placeholder="' . esc_attr( $default_color ) . '" maxlength="9">';
				echo '<button type="button" class="wpps-color-reset button-link" data-default-color="' . esc_attr( $default_color ) . '" aria-label="' . esc_attr__( 'Reset to default', 'wp-powersuite' ) . '"><span class="dashicons dashicons-undo" style="font-size: 16px; width: 16px; height: 16px;"></span></button>';
				echo '</div>';
				if ( ! empty( $field['color_palette'] ) ) {
					echo '<div class="wpps-opb-color-palette">';
					foreach ( preg_split( '/\s*,\s*/', (string) $field['color_palette'] ) as $swatch ) {
						$swatch = trim( $swatch );
						if ( $swatch !== '' && $swatch[0] !== '#' ) {
							$swatch = '#' . $swatch;
						}
						$hex = sanitize_hex_color( $swatch );
						if ( $hex ) {
							echo '<button type="button" class="wpps-opb-color-swatch" data-color="' . esc_attr( $hex ) . '" data-target="' . esc_attr( $color_id ) . '" style="background:' . esc_attr( $hex ) . ';" aria-label="' . esc_attr( $hex ) . '"></button>';
						}
					}
					echo '</div>';
				}
				echo '</div>';
				break;
			case 'checkbox':
				$checked  = in_array( (string) $value, [ '1', 'on', 'yes', 'true' ], true );
				$cb_label = ! empty( $field['checkbox_label'] ) ? (string) $field['checkbox_label'] : '';
				echo '<input type="hidden" name="' . esc_attr( $field_name ) . '" value="0">';
				echo '<label class="wpps-opb-checkbox-label" for="' . esc_attr( $name ) . '">';
				echo '<input type="checkbox" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="1" ' . checked( $checked, true, false ) . ' ' . esc_attr( $required ) . '>';
				if ( $cb_label !== '' ) {
					echo '<span class="wpps-opb-checkbox-label-text">' . esc_html( $cb_label );
					if ( $required !== '' ) {
						echo '<span class="required">*</span>';
					}
					echo '</span>';
				}
				echo '</label>';
				break;
			case 'select':
				$this->renderSelectField( $field, $field_name, $value, $name, $attrs, $required );
				break;
			case 'radio':
				$opts   = $this->parseOptions( $field );
				$layout = wpps_opb_sanitize_layout( (string) ( $field['radio_layout'] ?? '' ) );
				if ( ! empty( $opts ) ) {
					$is_first = true;
					echo '<fieldset class="wpps-opb-radio-fieldset"><legend class="screen-reader-text">' . esc_html( $field['label'] ?? $name ) . '</legend>';
					echo '<div class="wpps-opb-radio-group wpps-opb-radio-group--' . esc_attr( $layout ) . '" role="radiogroup" aria-labelledby="' . esc_attr( $name ) . '-label">';
					foreach ( $opts as $ov => $ol ) {
						$is_checked = (string) $value === (string) $ov;
						$item_class = 'wpps-opb-radio-item' . ( $is_checked ? ' is-selected' : '' );
						$req_attr   = ( $is_first && $required !== '' ) ? $required : '';
						echo '<label class="' . esc_attr( $item_class ) . '">';
						echo '<input type="radio" name="' . esc_attr( $field_name ) . '" value="' . esc_attr( $ov ) . '" ' . checked( $value, $ov, false ) . ' ' . esc_attr( $req_attr ) . '>';
						echo '<span class="wpps-opb-radio-item-text">' . esc_html( $ol ) . '</span>';
						echo '</label>';
						$is_first = false;
					}
					echo '</div></fieldset>';
					if ( ! empty( $field['allow_none'] ) && $required === '' ) {
						echo '<button type="button" class="button button-link wpps-opb-radio-clear" data-target="' . esc_attr( $name ) . '">' . esc_html__( 'Clear selection', 'wp-powersuite' ) . '</button>';
					}
				}
				break;
			case 'wysiwyg':
				$ed_id = 'wpps_opb_' . preg_replace( '/[^a-z0-9_-]/i', '_', $name );
				$rows = ! empty( $field['rows'] ) ? absint( $field['rows'] ) : 12;
				$height = min( 5000, max( 150, $rows * 20 ) );
				echo '<div class="wpps-opb-wysiwyg-wrap">';
				wp_editor( $value, $ed_id, [
					'textarea_name'     => $field_name,
					'textarea_rows'     => $rows,
					'editor_height'     => $height,
					'media_buttons'     => ! isset( $field['media_buttons'] ) || ! empty( $field['media_buttons'] ),
					'drag_drop_upload'  => true,
					'editor_class'      => 'wpps-opb-wysiwyg',
					'teeny'             => ! empty( $field['teeny'] ),
					'quicktags'         => true,
					'wpautop'           => true,
					'tinymce'           => [
						'wp_autoresize_on' => true,
					],
				] );
				echo '</div>';
				break;
			case 'toggle':
				$checked = in_array( (string) $value, [ '1', 'on', 'yes', 'true' ], true );
				echo '<label class="wpps-opb-toggle">';
				echo '<input type="checkbox" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="1" ' . checked( $checked, true, false ) . ' ' . esc_attr( $required ) . '>';
				echo '<span class="wpps-opb-toggle__slider" aria-hidden="true"></span>';
				echo '<span class="wpps-opb-toggle__label">' . esc_html__( 'Enabled', 'wp-powersuite' ) . '</span>';
				echo '</label>';
				break;
			case 'link':
				$this->renderLinkField( $field, $field_name, $value, $name );
				break;
			case 'gallery':
				$this->renderGalleryField( $field, $field_name, $value, $name );
				break;
			case 'repeater':
				$this->renderRepeaterField( $field, $field_name, $value, $name );
				break;
			case 'image':
				$this->renderAttachmentField( $field, $field_name, $value, 'image' );
				break;
			case 'file':
				$this->renderAttachmentField( $field, $field_name, $value, 'file' );
				break;
			case 'date':
				$date_type = wpps_opb_sanitize_date_type( (string) ( $field['date_type'] ?? 'date' ) );
				$min_attr  = ! empty( $field['min_date'] ) ? ' min="' . esc_attr( (string) $field['min_date'] ) . '"' : '';
				$max_attr  = ! empty( $field['max_date'] ) ? ' max="' . esc_attr( (string) $field['max_date'] ) . '"' : '';
				$html      = '<input type="' . esc_attr( $date_type ) . '" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $value ) . '" class="regular-text"' . $min_attr . $max_attr . ' ' . $attrs . '>';
				$this->wrapFieldAffix( $field, $html );
				break;
			default:
				$html = '<input type="text" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $value ) . '" class="regular-text" ' . $attrs . '>';
				$this->wrapFieldAffix( $field, $html );
		}
	}

	private function renderSelectField( array $field, string $field_name, $value, string $name, string $attrs, string $required ): void {
		$opts        = $this->parseOptions( $field );
		$layout      = wpps_opb_sanitize_layout( (string) ( $field['select_layout'] ?? 'stacked' ) );
		$is_multiple = ! empty( $field['multiple'] );
		$allow_empty = ! empty( $field['allow_empty'] ) || $required === '';
		$readonly    = ! empty( $field['readonly'] );

		if ( $is_multiple || $layout === 'stacked' || $opts === [] ) {
			$select_name = $is_multiple ? $field_name . '[]' : $field_name;
			$selected    = $is_multiple ? array_map( 'trim', explode( ',', (string) $value ) ) : [ (string) $value ];
			echo '<div class="wpps-opb-select-wrap wpps-opb-select-wrap--' . esc_attr( $layout ) . '">';
			echo '<select name="' . esc_attr( $select_name ) . '" id="' . esc_attr( $name ) . '" class="regular-text" ' . ( $is_multiple ? 'multiple' : '' ) . ' ' . $attrs . '>';
			if ( $allow_empty && ! $is_multiple ) {
				echo '<option value="">' . esc_html__( '— Select —', 'wp-powersuite' ) . '</option>';
			}
			foreach ( $opts as $ov => $ol ) {
				$is_sel = $is_multiple ? in_array( (string) $ov, $selected, true ) : ( (string) $value === (string) $ov );
				echo '<option value="' . esc_attr( $ov ) . '" ' . selected( $is_sel, true, false ) . '>' . esc_html( $ol ) . '</option>';
			}
			echo '</select></div>';

			return;
		}

		$current = (string) $value;
		echo '<div class="wpps-opb-select-wrap wpps-opb-select-wrap--' . esc_attr( $layout ) . '">';
		echo '<input type="hidden" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( $current ) . '" class="wpps-opb-select-value" ' . $attrs . '>';
		echo '<div class="wpps-opb-select-group wpps-opb-select-group--' . esc_attr( $layout ) . '" role="listbox" aria-labelledby="' . esc_attr( $name ) . '-label">';

		if ( $allow_empty ) {
			$empty_selected = $current === '';
			echo '<button type="button" class="wpps-opb-select-item' . ( $empty_selected ? ' is-selected' : '' ) . '" data-value="" role="option" aria-selected="' . ( $empty_selected ? 'true' : 'false' ) . '"' . ( $readonly ? ' disabled' : '' ) . '>';
			echo esc_html__( '— Select —', 'wp-powersuite' );
			echo '</button>';
		}

		foreach ( $opts as $ov => $ol ) {
			$is_sel = $current === (string) $ov;
			echo '<button type="button" class="wpps-opb-select-item' . ( $is_sel ? ' is-selected' : '' ) . '" data-value="' . esc_attr( (string) $ov ) . '" role="option" aria-selected="' . ( $is_sel ? 'true' : 'false' ) . '"' . ( $readonly ? ' disabled' : '' ) . '>';
			echo esc_html( $ol );
			echo '</button>';
		}

		echo '</div></div>';
	}

	private function renderLinkField( array $field, string $field_name, $value, string $name ): void {
		$link    = is_array( $value ) ? $value : [];
		$url     = (string) ( $link['url'] ?? '' );
		$title   = (string) ( $link['title'] ?? '' );
		$new_tab = (string) ( $link['target'] ?? '' ) === '_blank';
		echo '<div class="wpps-opb-link-field">';
		echo '<p><label for="' . esc_attr( $name ) . '-url">' . esc_html__( 'URL', 'wp-powersuite' ) . '</label>';
		echo '<input type="url" class="regular-text" id="' . esc_attr( $name ) . '-url" name="' . esc_attr( $field_name ) . '[url]" value="' . esc_attr( $url ) . '"></p>';
		echo '<p><label for="' . esc_attr( $name ) . '-title">' . esc_html__( 'Link text', 'wp-powersuite' ) . '</label>';
		echo '<input type="text" class="regular-text" id="' . esc_attr( $name ) . '-title" name="' . esc_attr( $field_name ) . '[title]" value="' . esc_attr( $title ) . '"></p>';
		echo '<p><label><input type="checkbox" name="' . esc_attr( $field_name ) . '[target]" value="_blank" ' . checked( $new_tab, true, false ) . '> ';
		echo esc_html__( 'Open in new tab', 'wp-powersuite' ) . '</label></p>';
		echo '</div>';
	}

	private function renderGalleryField( array $field, string $field_name, $value, string $name ): void {
		$preview_size = sanitize_key( (string) ( $field['preview_size'] ?? 'thumbnail' ) );
		$ids          = array_filter( array_map( 'absint', explode( ',', (string) $value ) ) );
		$stored_value = implode( ',', $ids );

		echo '<div class="wpps-opb-image-wrap wpps-opb-media-wrap wpps-opb-gallery-wrap" data-media-kind="image" data-multiple="1">';
		echo '<input type="hidden" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( $stored_value ) . '">';
		echo '<div class="wpps-opb-image-preview wpps-opb-gallery-preview"' . ( $ids === [] ? ' style="display:none;"' : '' ) . '>';
		foreach ( $ids as $id ) {
			if ( $id <= 0 ) {
				continue;
			}
			$url = wp_get_attachment_image_url( $id, $preview_size );
			echo '<div class="wpps-opb-media-item" data-id="' . esc_attr( (string) $id ) . '">';
			echo '<button type="button" class="wpps-opb-image-remove" data-target="' . esc_attr( $name ) . '" data-id="' . esc_attr( (string) $id ) . '" aria-label="' . esc_attr__( 'Remove', 'wp-powersuite' ) . '"><span class="dashicons dashicons-no-alt"></span></button>';
			if ( is_string( $url ) && $url !== '' ) {
				echo '<img src="' . esc_url( $url ) . '" alt="">';
			}
			echo '</div>';
		}
		echo '</div>';
		echo '<button type="button" class="button wpps-opb-upload-btn" data-target="' . esc_attr( $name ) . '" data-kind="image" data-multiple="1">' . esc_html__( 'Add Images', 'wp-powersuite' ) . '</button>';
		echo '</div>';
	}

	/**
	 * @param array<string, mixed> $field
	 */
	private function renderRepeaterField( array $field, string $field_name, $value, string $name ): void {
		$sub_fields = isset( $field['sub_fields'] ) && is_array( $field['sub_fields'] ) ? $field['sub_fields'] : [];
		$rows       = is_array( $value ) ? array_values( $value ) : [];
		$min        = max( 0, (int) ( $field['min'] ?? 0 ) );
		$max        = (int) ( $field['max'] ?? 0 );
		$btn_label  = ! empty( $field['button_label'] ) ? (string) $field['button_label'] : __( 'Add Row', 'wp-powersuite' );

		if ( $rows === [] && $min > 0 ) {
			$rows = array_fill( 0, $min, [] );
		}

		echo '<div class="wpps-opb-repeater" data-min="' . esc_attr( (string) $min ) . '" data-max="' . esc_attr( (string) $max ) . '">';
		echo '<div class="wpps-opb-repeater__rows">';
		foreach ( $rows as $index => $row ) {
			$this->renderRepeaterRow( $sub_fields, $field_name, is_array( $row ) ? $row : [], $name, (int) $index );
		}
		echo '</div>';
		echo '<button type="button" class="button wpps-opb-repeater__add">' . esc_html( $btn_label ) . '</button>';
		echo '<script type="text/html" class="wpps-opb-repeater-row-template">';
		$this->renderRepeaterRow( $sub_fields, $field_name, [], $name, '__ROW__' );
		echo '</script>';
		echo '</div>';
	}

	/**
	 * @param array<int, array<string, mixed>> $sub_fields
	 * @param array<string, mixed>             $row
	 */
	private function renderRepeaterRow( array $sub_fields, string $field_name, array $row, string $name, int|string $index ): void {
		echo '<div class="wpps-opb-repeater__row" data-row="' . esc_attr( (string) $index ) . '">';
		echo '<div class="wpps-opb-repeater__row-head">';
		echo '<span class="wpps-opb-repeater__row-label">' . esc_html__( 'Row', 'wp-powersuite' ) . ' <span class="wpps-opb-repeater__row-num"></span></span>';
		echo '<button type="button" class="button-link wpps-opb-repeater__remove">' . esc_html__( 'Remove', 'wp-powersuite' ) . '</button>';
		echo '</div><div class="wpps-opb-repeater__row-body">';

		foreach ( $sub_fields as $sub ) {
			$sub_name = sanitize_key( (string) ( $sub['name'] ?? '' ) );
			if ( $sub_name === '' ) {
				continue;
			}
			$sub_label = (string) ( $sub['label'] ?? ucfirst( str_replace( '_', ' ', $sub_name ) ) );
			$sub_value = $row[ $sub_name ] ?? ( $sub['default'] ?? '' );
			$sub_id    = $name . '-' . sanitize_key( (string) $index ) . '-' . $sub_name;
			$input     = $field_name . '[' . $index . '][' . $sub_name . ']';
			echo '<div class="wpps-opb-repeater__subfield">';
			echo '<label class="wpps-opb-repeater__subfield-label" for="' . esc_attr( $sub_id ) . '">' . esc_html( $sub_label ) . '</label>';
			$this->renderRepeaterSubField( $sub, $input, $sub_value, $sub_id );
			echo '</div>';
		}

		echo '</div></div>';
	}

	/**
	 * @param array<string, mixed> $sub
	 */
	private function renderRepeaterSubField( array $sub, string $field_name, mixed $value, string $name ): void {
		$type = sanitize_key( (string) ( $sub['type'] ?? 'text' ) );
		$sub_field = array_merge( $sub, [ 'field_id' => $name, 'type' => $type ] );

		switch ( $type ) {
			case 'textarea':
				$rows = 3;
				echo '<textarea name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" rows="' . esc_attr( (string) $rows ) . '" class="large-text">' . esc_textarea( (string) $value ) . '</textarea>';
				break;
			case 'number':
				echo '<input type="number" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $value ) . '" class="small-text">';
				break;
			case 'email':
				echo '<input type="email" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $value ) . '" class="regular-text">';
				break;
			case 'url':
				echo '<input type="url" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $value ) . '" class="regular-text">';
				break;
			case 'image':
				$this->renderAttachmentField( $sub_field, $field_name, $value, 'image' );
				break;
			case 'select':
				$this->renderSelectField( $sub_field, $field_name, $value, $name, '', '' );
				break;
			case 'checkbox':
				$checked = in_array( (string) $value, [ '1', 'on', 'yes', 'true' ], true );
				echo '<input type="hidden" name="' . esc_attr( $field_name ) . '" value="0">';
				echo '<label><input type="checkbox" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="1" ' . checked( $checked, true, false ) . '> ';
				echo esc_html__( 'Yes', 'wp-powersuite' ) . '</label>';
				break;
			case 'toggle':
				$checked = in_array( (string) $value, [ '1', 'on', 'yes', 'true' ], true );
				echo '<label class="wpps-opb-toggle wpps-opb-toggle--compact">';
				echo '<input type="checkbox" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="1" ' . checked( $checked, true, false ) . '>';
				echo '<span class="wpps-opb-toggle__slider" aria-hidden="true"></span>';
				echo '</label>';
				break;
			default:
				echo '<input type="text" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $value ) . '" class="regular-text">';
		}
	}

	/**
	 * @param array<string, mixed> $field
	 * @return array<int, array<string, mixed>>
	 */
	private function sanitizeRepeaterValue( array $field, mixed $raw ): array {
		if ( ! is_array( $raw ) ) {
			return [];
		}

		$sub_fields = isset( $field['sub_fields'] ) && is_array( $field['sub_fields'] ) ? $field['sub_fields'] : [];
		$max        = (int) ( $field['max'] ?? 0 );
		$clean      = [];

		foreach ( array_values( $raw ) as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$item = [];
			foreach ( $sub_fields as $sub ) {
				$sub_name = sanitize_key( (string) ( $sub['name'] ?? '' ) );
				if ( $sub_name === '' ) {
					continue;
				}
				$item[ $sub_name ] = $this->sanitizeRepeaterSubFieldValue( $sub, $row[ $sub_name ] ?? null );
			}
			if ( $item !== [] ) {
				$clean[] = $item;
			}
			if ( $max > 0 && count( $clean ) >= $max ) {
				break;
			}
		}

		return $clean;
	}

	/**
	 * @param array<string, mixed> $sub
	 */
	private function sanitizeRepeaterSubFieldValue( array $sub, mixed $value ): string {
		$type = sanitize_key( (string) ( $sub['type'] ?? 'text' ) );

		switch ( $type ) {
			case 'url':
				return esc_url_raw( (string) $value );
			case 'email':
				return sanitize_email( (string) $value ) ?: '';
			case 'number':
				return is_numeric( $value ) ? (string) $value : '';
			case 'image':
				$id = absint( $value );

				return $id > 0 ? (string) $id : '';
			case 'textarea':
				return sanitize_textarea_field( (string) $value );
			case 'checkbox':
			case 'toggle':
				return ! empty( $value ) && (string) $value !== '0' ? '1' : '';
			case 'select':
				$choice = sanitize_text_field( (string) $value );
				if ( $choice === '' ) {
					return '';
				}
				$opts = $this->parseOptions( $sub );

				return array_key_exists( $choice, $opts ) ? $choice : '';
			default:
				return sanitize_text_field( (string) $value );
		}
	}

	private function renderAttachmentField( array $field, string $field_name, $value, string $kind ): void {
		$name          = $field['field_id'] ?? $field['name'] ?? $field['id'] ?? '';
		$is_multiple   = ! empty( $field['multiple'] );
		$preview_size  = sanitize_key( (string) ( $field['preview_size'] ?? 'medium' ) );
		$ids           = array_filter( array_map( 'absint', explode( ',', (string) $value ) ) );
		$stored_value  = $is_multiple ? implode( ',', $ids ) : (string) ( $ids[0] ?? '' );
		$upload_class  = $kind === 'file' ? 'wpps-opb-file-upload-btn' : 'wpps-opb-upload-btn';
		$upload_label  = $kind === 'file'
			? ( $is_multiple ? __( 'Choose Files', 'wp-powersuite' ) : __( 'Choose File', 'wp-powersuite' ) )
			: ( $is_multiple ? __( 'Choose Images', 'wp-powersuite' ) : __( 'Choose Image', 'wp-powersuite' ) );

		echo '<div class="wpps-opb-image-wrap wpps-opb-media-wrap" data-media-kind="' . esc_attr( $kind ) . '" data-multiple="' . esc_attr( $is_multiple ? '1' : '0' ) . '">';
		echo '<input type="hidden" name="' . esc_attr( $field_name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( $stored_value ) . '">';
		echo '<div class="wpps-opb-image-preview"' . ( $ids === [] ? ' style="display:none;"' : '' ) . '>';
		foreach ( $ids as $id ) {
			if ( $id <= 0 ) {
				continue;
			}
			$url = $kind === 'image' ? wp_get_attachment_image_url( $id, $preview_size ) : '';
			echo '<div class="wpps-opb-media-item" data-id="' . esc_attr( (string) $id ) . '">';
			echo '<button type="button" class="wpps-opb-image-remove" data-target="' . esc_attr( $name ) . '" data-id="' . esc_attr( (string) $id ) . '" aria-label="' . esc_attr__( 'Remove', 'wp-powersuite' ) . '"><span class="dashicons dashicons-no-alt"></span></button>';
			if ( $kind === 'image' && $url ) {
				echo '<img src="' . esc_url( $url ) . '" alt="">';
			} else {
				echo '<span class="wpps-opb-file-name">' . esc_html( get_the_title( $id ) ?: basename( (string) get_attached_file( $id ) ) ) . '</span>';
			}
			echo '</div>';
		}
		echo '</div>';
		echo '<button type="button" class="button ' . esc_attr( $upload_class ) . '" data-target="' . esc_attr( $name ) . '" data-kind="' . esc_attr( $kind ) . '" data-multiple="' . esc_attr( $is_multiple ? '1' : '0' ) . '">' . esc_html( $upload_label ) . '</button></div>';
	}

	private function sanitizeRadioLayout( $layout ): string {
		$layout = sanitize_key( (string) $layout );
		$allowed = [ 'stacked', 'inline', 'buttons', 'columns-2', 'columns-3' ];

		return in_array( $layout, $allowed, true ) ? $layout : 'stacked';
	}

	private function parseOptions( array $field ): array {
		$opts = $field['options'] ?? '';
		if ( is_array( $opts ) ) {
			return $opts;
		}
		$out = [];
		foreach ( explode( "\n", (string) $opts ) as $line ) {
			$line = trim( $line );
			if ( $line === '' ) {
				continue;
			}
			// Support both value:label and value|label notation
			if ( strpos( $line, '|' ) !== false ) {
				$p = explode( '|', $line, 2 );
				$out[ trim( $p[0] ) ] = trim( $p[1] ?? $p[0] );
			} elseif ( strpos( $line, ':' ) !== false ) {
				$p = explode( ':', $line, 2 );
				$out[ trim( $p[0] ) ] = trim( $p[1] ?? $p[0] );
			} else {
				$out[ $line ] = $line;
			}
		}
		return $out;
	}

	private function getPages(): array {
		$p = get_option( self::PAGES_OPTION, [] );
		return is_array( $p ) ? $p : [];
	}

	private function getPageById( string $id ): ?array {
		foreach ( $this->getPages() as $p ) {
			if ( is_array( $p ) && ( $p['id'] ?? '' ) === $id ) {
				return $p;
			}
		}
		return null;
	}

	private function getPageBySlug( string $slug ): ?array {
		$slug_normalized = str_replace( '-', '_', $slug );
		foreach ( $this->getPages() as $p ) {
			if ( ! is_array( $p ) || empty( $p['slug'] ) ) {
				continue;
			}
			$p_slug = $p['slug'];
			if ( $p_slug === $slug || str_replace( '-', '_', $p_slug ) === $slug_normalized ) {
				return $p;
			}
		}
		return null;
	}

	private function getDefaultPageData(): array {
		return [
			'id'               => '',
			'page_title'       => '',
			'page_description' => '',
			'menu_title'       => '',
			'slug'             => '',
			'parent_slug'      => '',
			'capability'       => 'manage_options',
			'icon'             => 'dashicons-admin-generic',
			'position'         => 30,
			'autoload'         => true,
			'save_button'      => '',
			'updated_message'  => '',
			'fields'           => [],
		];
	}


}
