<?php

namespace WeDevs\AppBridge\Core;

use WeDevs\AppBridge\Core\Contracts\SettingsSchemaInterface;

/**
 * Base class for all integration settings schemas.
 *
 * Common pages (Appearance, Push Notifications) and their sections/fields are
 * defined here and automatically included in every integration's schema.
 * Subclasses extend by overriding any of the protected builder methods.
 *
 * Customisation hooks (coarsest → finest):
 *   1. WordPress filter  `pocket_store_{slug}_settings_schema`  — post-assembly
 *   2. Override  get_schema()          — full control of assembly
 *   3. Override  pages/sections/fields — add integration-specific content
 *   4. Override  individual common builders (e.g. notification_provider_fields)
 *   5. Override  general_page_id() / push_notifications_page_id()
 *                — change the IDs used by all common sections/fields
 */
abstract class AbstractSettingsSchema implements SettingsSchemaInterface {

	// ── Required ──────────────────────────────────────────────────────────────

	/**
	 * Short identifier for this integration.
	 * Used to derive the option key and the schema filter name.
	 * Examples: 'dokan', 'wperp', 'wppm', 'woocommerce'.
	 */
	abstract protected function slug(): string;

	/**
	 * Whether the integration's license is currently active.
	 * Override in concrete schemas to gate settings behind a valid license.
	 * Defaults to true (unlocked) so integrations without a license manager
	 * are unaffected.
	 */
	protected function is_licensed(): bool {
		return true;
	}

	// ── Page ID helpers (override to change IDs used by common sections) ──────

	protected function general_page_id(): string {
		return 'general';
	}

	protected function push_notifications_page_id(): string {
		return 'push-notifications';
	}

	// ── Common pages ──────────────────────────────────────────────────────────

	protected function general_page(): array {
		return [
			'id'          => $this->general_page_id(),
			'type'        => 'page',
			'label'       => __( 'General', 'app-bridge' ),
			'description' => __( 'Configure core app branding, welcome screen, languages, and shared identity used by the mobile app.', 'app-bridge' ),
			'icon'        => 'Settings',
			'priority'    => 10,
		];
	}

	protected function push_notifications_page(): array {
		return [
			'id'          => $this->push_notifications_page_id(),
			'type'        => 'page',
			'label'       => __( 'Push Notifications', 'app-bridge' ),
			'description' => __( 'Set up the notification provider, customise event templates, and send manual broadcasts to mobile users.', 'app-bridge' ),
			'icon'        => 'Bell',
			'priority'    => 120,
		];
	}

	/**
	 * Common Languages section under the General page. Returns a list (0 or 1)
	 * so subclasses can omit the section entirely by overriding to return [].
	 *
	 * @return array<int, array<string, mixed>>
	 */
	protected function languages_section(): array {
		return [
			[
				'id'      => 'app_languages',
				'type'    => 'section',
				'label'   => __( 'App Languages', 'app-bridge' ),
				'page_id' => $this->general_page_id(),
			],
		];
	}

	/**
	 * Normalize stored `app_languages` into a flat list of language codes.
	 * Tolerates legacy shapes (array of `{code, name}` objects) and arrays
	 * containing non-string entries.
	 *
	 * @param mixed $raw
	 * @return string[]
	 */
	public function normalize_language_codes( $raw ): array {
		if ( ! is_array( $raw ) ) {
			return [];
		}

		$codes = [];
		foreach ( $raw as $entry ) {
			if ( is_string( $entry ) ) {
				$codes[] = $entry;
			} elseif ( is_array( $entry ) && isset( $entry['code'] ) && is_string( $entry['code'] ) ) {
				$codes[] = $entry['code'];
			}
		}

		return array_values( array_unique( array_filter( array_map( 'sanitize_key', $codes ) ) ) );
	}

	/**
	 * Resolve a language code into a `{code, name}` object using the shared
	 * picker list. Returns the English fallback when the code is unknown.
	 *
	 * @param string $code
	 * @return array{code:string, name:string}
	 */
	public function resolve_language( string $code ): array {
		$code = $code ?? 'en';
		foreach ( $this->language_options() as $opt ) {
			if ( ( $opt['value'] ?? '' ) === $code ) {
				return [
					'code' => $code,
					'name' => (string) ( $opt['label'] ?? $code ),
				];
			}
		}

		return [
			'code' => $code,
			'name' => $code,
		];
	}

	/**
	 * Common list of languages offered by the default-language picker.
	 *
	 * @return array<int, array{label:string, value:string}>
	 */
	protected function language_options(): array {
		return [
			[
				'label' => __( 'English', 'app-bridge' ),
				'value' => 'en',
			],
			[
				'label' => __( 'Spanish', 'app-bridge' ),
				'value' => 'es',
			],
			[
				'label' => __( 'French', 'app-bridge' ),
				'value' => 'fr',
			],
			[
				'label' => __( 'German', 'app-bridge' ),
				'value' => 'de',
			],
			[
				'label' => __( 'Italian', 'app-bridge' ),
				'value' => 'it',
			],
			[
				'label' => __( 'Portuguese', 'app-bridge' ),
				'value' => 'pt',
			],
			[
				'label' => __( 'Russian', 'app-bridge' ),
				'value' => 'ru',
			],
			[
				'label' => __( 'Arabic', 'app-bridge' ),
				'value' => 'ar',
			],
			[
				'label' => __( 'Chinese', 'app-bridge' ),
				'value' => 'zh',
			],
			[
				'label' => __( 'Japanese', 'app-bridge' ),
				'value' => 'ja',
			],
			[
				'label' => __( 'Hindi', 'app-bridge' ),
				'value' => 'hi',
			],
			[
				'label' => __( 'Bengali', 'app-bridge' ),
				'value' => 'bn',
			],
		];
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<int, array<string, mixed>>
	 */
	protected function languages_fields( array $settings ): array {
		$saved_app_langs = $this->normalize_language_codes( $settings['app_languages'] ?? null );
		if ( empty( $saved_app_langs ) ) {
			$saved_app_langs = [ 'en' ];
		}

		return [
			[
				'id'             => 'app_languages',
				'type'           => 'field',
				'variant'        => 'multicheck',
				'label'          => __( 'App Languages', 'app-bridge' ),
				'section_id'     => 'app_languages',
				'dependency_key' => 'app_languages',
				'value'          => $saved_app_langs,
				'default'        => [ 'en' ],
				'options'        => $this->language_options(),
				'description'    => __( 'Languages available inside the mobile app.', 'app-bridge' ),
			],
			[
				'id'             => 'default_app_language',
				'type'           => 'field',
				'variant'        => 'select',
				'label'          => __( 'Default Language', 'app-bridge' ),
				'section_id'     => 'app_languages',
				'dependency_key' => 'default_app_language',
				'value'          => (string) ( $settings['default_app_language'] ?? 'en' ),
				'default'        => 'en',
				'options'        => $this->language_options(),
				'description'    => __( 'Language used by the mobile app on first launch.', 'app-bridge' ),
			],
		];
	}

	/**
	 * Common Sliders page. Subclasses opt in by including the result of
	 * {@see self::sliders_page()} and {@see self::sliders_field()} in their
	 * own {@see self::pages()} / {@see self::fields()} overrides. The page
	 * uses `hide_save` since the embedded `sliders_manager` field renders its
	 * own save button.
	 */
	protected function sliders_page(): array {
		return [
			'id'          => 'sliders',
			'type'        => 'page',
			'label'       => __( 'Sliders', 'app-bridge' ),
			'description' => __( 'Manage the home-screen carousel: add slides, link them to products or pages, and reorder how they appear.', 'app-bridge' ),
			'icon'        => 'Images',
			'hide_save'   => true,
			'priority'    => 100,
		];
	}

	protected function sliders_field(): array {
		return [
			'id'      => 'sliders_manager',
			'type'    => 'field',
			'variant' => 'sliders_manager',
			'page_id' => 'sliders',
			'no_wrap' => true,
			'display' => true,
		];
	}

	protected function license_page(): array {
		return [
			'id'           => 'license',
			'type'         => 'page',
			'label'        => __( 'License', 'app-bridge' ),
			'description'  => __( 'Activate or manage your license to unlock the mobile app integration and receive updates.', 'app-bridge' ),
			'icon'         => 'KeyRound',
			'hide_save'    => true,
			'hide_heading' => true,
			'priority'     => 999,
		];
	}

	protected function license_field(): array {
		return [
			'id'      => 'license_field',
			'type'    => 'field',
			'variant' => 'license',
			'page_id' => 'license',
			'no_wrap' => true,
		];
	}

	// ── Common sections ───────────────────────────────────────────────────────

	protected function app_identity_section(): array {
		return [
			'id' => 'app_identity',
			'type' => 'section',
			'label' => __( 'App Identity', 'app-bridge' ),
			'page_id' => $this->general_page_id(),
		];
	}

	protected function welcome_screen_section(): array {
		return [
			'id' => 'welcome_screen',
			'type' => 'section',
			'label' => __( 'Welcome Screen', 'app-bridge' ),
			'page_id' => $this->general_page_id(),
		];
	}

	/**
	 * Returns a list of section definitions (0 or 1) for the Button Colors
	 * section. Override and return [] in subclasses that should not render it.
	 *
	 * @return array<int, array<string, mixed>>
	 */
	protected function button_colors_section(): array {
		return [
			[
				'id'      => 'button_colors',
				'type'    => 'section',
				'label'   => __( 'Button Colors', 'app-bridge' ),
				'page_id' => $this->general_page_id(),
			],
		];
	}

	protected function notification_provider_section(): array {
		return [
			'id' => 'notification_provider',
			'type' => 'section',
			'label' => __( 'Provider Configuration', 'app-bridge' ),
			'page_id' => $this->push_notifications_page_id(),
		];
	}

	// ── Common fields ─────────────────────────────────────────────────────────

	/**
	 * @param array<string, mixed> $settings
	 * @return array<int, array<string, mixed>>
	 */
	protected function app_identity_fields( array $settings ): array {
		return $this->app_logo_field( $settings );
	}

	/**
	 * App-logo field. Returns a list (0 or 1) so subclasses can omit it
	 * entirely by overriding to return [].
	 *
	 * @param array<string, mixed> $settings
	 * @return array<int, array<string, mixed>>
	 */
	protected function app_logo_field( array $settings ): array {
		return [
			[
				'id'             => 'app_logo',
				'type'           => 'field',
				'variant'        => 'wp_media_upload',
				'label'          => __( 'App Logo', 'app-bridge' ),
				'section_id'     => 'app_identity',
				'dependency_key' => 'app_logo',
				'value'          => $settings['app_logo'] ?? '',
				'description'    => __( 'Upload your app logo image.', 'app-bridge' ),
			],
		];
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<int, array<string, mixed>>
	 */
	protected function welcome_screen_fields( array $settings ): array {
		return [
			[
				'id' => 'welcome_screen_image',
				'type' => 'field',
				'variant' => 'wp_media_upload',
				'label' => __( 'Image', 'app-bridge' ),
				'section_id' => 'welcome_screen',
				'dependency_key' => 'welcome_screen_image',
				'value' => $settings['welcome_screen_image'] ?? '',
			],
			[
				'id' => 'welcome_screen_text_1',
				'type' => 'field',
				'variant' => 'text',
				'label' => __( 'Heading', 'app-bridge' ),
				'section_id' => 'welcome_screen',
				'dependency_key' => 'welcome_screen_text_1',
				'value' => $settings['welcome_screen_text_1'] ?? 'WELCOME',
			],
			[
				'id' => 'welcome_screen_text_2',
				'type' => 'field',
				'variant' => 'text',
				'label' => __( 'Subheading', 'app-bridge' ),
				'section_id' => 'welcome_screen',
				'dependency_key' => 'welcome_screen_text_2',
				'value' => $settings['welcome_screen_text_2'] ?? 'To Mobile App',
			],
			[
				'id' => 'welcome_screen_color_1',
				'type' => 'field',
				'variant' => 'color_picker',
				'label' => __( 'Gradient Color 1', 'app-bridge' ),
				'section_id' => 'welcome_screen',
				'dependency_key' => 'welcome_screen_color_1',
				'value' => $settings['welcome_screen_color_1'] ?? '#FF9472',
			],
			[
				'id' => 'welcome_screen_color_2',
				'type' => 'field',
				'variant' => 'color_picker',
				'label' => __( 'Gradient Color 2', 'app-bridge' ),
				'section_id' => 'welcome_screen',
				'dependency_key' => 'welcome_screen_color_2',
				'value' => $settings['welcome_screen_color_2'] ?? '#FF4B7A',
			],
		];
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<int, array<string, mixed>>
	 */
	protected function button_color_fields( array $settings ): array {
		return [
			[
				'id' => 'primary_button_color_1',
				'type' => 'field',
				'variant' => 'color_picker',
				'label' => __( 'Primary Gradient Color 1', 'app-bridge' ),
				'section_id' => 'button_colors',
				'dependency_key' => 'primary_button_color_1',
				'value' => $settings['primary_button_color_1'] ?? '#FF9472',
			],
			[
				'id' => 'primary_button_color_2',
				'type' => 'field',
				'variant' => 'color_picker',
				'label' => __( 'Primary Gradient Color 2', 'app-bridge' ),
				'section_id' => 'button_colors',
				'dependency_key' => 'primary_button_color_2',
				'value' => $settings['primary_button_color_2'] ?? '#F2709C',
			],
			[
				'id' => 'text_color_on_primary',
				'type' => 'field',
				'variant' => 'color_picker',
				'label' => __( 'Text Color On Primary', 'app-bridge' ),
				'section_id' => 'button_colors',
				'dependency_key' => 'text_color_on_primary',
				'value' => $settings['text_color_on_primary'] ?? '#FFFFFF',
			],
		];
	}

	/**
	 * Provider options for the push-notification service dropdown.
	 * Override in subclasses to add integration-specific providers.
	 *
	 * @return array<int, array{label:string, value:string}>
	 */
	protected function notification_provider_options(): array {
		return [
			[
				'label' => __( 'OneSignal', 'app-bridge' ),
				'value' => 'onesignal',
			],
		];
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<int, array<string, mixed>>
	 */
	protected function notification_provider_fields( array $settings ): array {
		$provider              = $settings['notification_provider'] ?? 'onesignal';
		$onesignal_visible_dep = [
			[
				'key'        => 'notification_provider',
				'value'      => 'onesignal',
				'comparison' => '===',
				'effect'     => 'show',
			],
		];

		return [
			[
				'id'             => 'provider',
				'type'           => 'field',
				'variant'        => 'select',
				'label'          => __( 'Provider', 'app-bridge' ),
				'section_id'     => 'notification_provider',
				'dependency_key' => 'notification_provider',
				'value'          => $provider,
				'default'        => 'onesignal',
				'options'        => $this->notification_provider_options(),
				'description'    => __( 'Select the service that will deliver push notifications. More providers coming soon.', 'app-bridge' ),
				'priority'       => 5,
			],
			[
				'id'             => 'app_id',
				'type'           => 'field',
				'variant'        => 'text',
				'label'          => __( 'OneSignal App ID', 'app-bridge' ),
				'section_id'     => 'notification_provider',
				'dependency_key' => 'onesignal.app_id',
				'value'          => $settings['onesignal']['app_id'] ?? '',
				'description'    => __( 'Your OneSignal application ID.', 'app-bridge' ),
				'dependencies'   => $onesignal_visible_dep,
				'priority'       => 10,
			],
			[
				'id'             => 'rest_api_key',
				'type'           => 'field',
				'variant'        => 'show_hide',
				'label'          => __( 'OneSignal REST API Key', 'app-bridge' ),
				'section_id'     => 'notification_provider',
				'dependency_key' => 'onesignal.rest_api_key',
				'value'          => $settings['onesignal']['rest_api_key'] ?? '',
				'description'    => __( 'Your OneSignal REST API key.', 'app-bridge' ),
				'dependencies'   => $onesignal_visible_dep,
				'priority'       => 20,
			],
		];
	}

	// ── Integration-specific extension points ─────────────────────────────────

	/**
	 * Additional pages beyond the common Appearance + Push Notifications.
	 *
	 * @return array<int, array<string, mixed>>
	 */
	protected function pages(): array {
		return [];
	}

	/**
	 * Additional sections beyond the common ones.
	 *
	 * @return array<int, array<string, mixed>>
	 */
	protected function sections(): array {
		return [];
	}

	/**
	 * Additional fields beyond the common ones.
	 * Receives the current saved option array for value pre-population.
	 *
	 * @param array<string, mixed> $settings Current saved option values.
	 * @return array<int, array<string, mixed>>
	 */
	protected function fields( array $settings ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
		return [];
	}

	// ── Sanitization ──────────────────────────────────────────────────────────

	/**
	 * Sanitizes the common fields shared by all integrations.
	 * Called from save() — override to modify sanitization rules.
	 *
	 * @param array<string, mixed> $data
	 * @return array<string, mixed>
	 */
	protected function sanitize_common( array $data ): array {
		$result = [];

		// The frontend groups fields by section_id (e.g. app_identity, welcome_screen).
		// Only sections present in $data are updated; absent sections are left unchanged
		// by the partial-merge in save().

		if ( isset( $data['app_identity'] ) ) {
			$s                 = $data['app_identity'];
			$result['app_logo'] = esc_url_raw( $s['app_logo'] ?? '' );
			$result['tag_line'] = sanitize_text_field( $s['tag_line'] ?? '' );
		}

		if ( isset( $data['app_languages'] ) && is_array( $data['app_languages'] ) ) {
			$s = $data['app_languages'];

			if ( isset( $s['app_languages'] ) ) {
				$result['app_languages'] = $this->normalize_language_codes( $s['app_languages'] );
			}

			if ( isset( $s['default_app_language'] ) ) {
				$result['default_app_language'] = sanitize_key( (string) $s['default_app_language'] );
			}
		}

		if ( isset( $data['welcome_screen'] ) ) {
			$s                                  = $data['welcome_screen'];
			$result['welcome_screen_image']     = esc_url_raw( $s['welcome_screen_image'] ?? '' );
			$result['welcome_screen_text_1']    = sanitize_text_field( $s['welcome_screen_text_1'] ?? 'WELCOME' );
			$result['welcome_screen_text_2']    = sanitize_text_field( $s['welcome_screen_text_2'] ?? 'To Mobile App' );
			$result['welcome_screen_color_1']   = sanitize_hex_color( $s['welcome_screen_color_1'] ?? '#FF9472' );
			$result['welcome_screen_color_2']   = sanitize_hex_color( $s['welcome_screen_color_2'] ?? '#FF4B7A' );
		}

		if ( isset( $data['button_colors'] ) ) {
			$s                                       = $data['button_colors'];
			$result['primary_button_color_1']        = sanitize_hex_color( $s['primary_button_color_1'] ?? '#FF9472' );
			$result['primary_button_color_2']        = sanitize_hex_color( $s['primary_button_color_2'] ?? '#F2709C' );
			$result['text_color_on_primary']     = sanitize_hex_color( $s['text_color_on_primary'] ?? '#FFFFFF' );
		}

		// The Notification Provider section bundles both the provider selector
		// and the OneSignal credentials. The plugin-ui formatter nests fields
		// under their full parent chain, so depending on whether the section
		// lives directly on the page or under a tab, the form posts either
		// `data.notification_provider.{...}` or `data.<tab>.notification_provider.{...}`.
		// `find_section()` handles both shapes.
		$np = self::find_section( $data, 'notification_provider' );

		if ( is_array( $np ) ) {
			if ( array_key_exists( 'provider', $np ) ) {
				$result['notification_provider'] = sanitize_key( (string) $np['provider'] );
			}

			if ( array_key_exists( 'app_id', $np ) || array_key_exists( 'rest_api_key', $np ) ) {
				$result['onesignal'] = [
					'app_id'       => sanitize_text_field( (string) ( $np['app_id'] ?? '' ) ),
					'rest_api_key' => sanitize_text_field( (string) ( $np['rest_api_key'] ?? '' ) ),
				];
			}
		}

		if ( isset( $data['notification_provider'] ) ) {
			$result['notification_provider'] = sanitize_key( (string) $data['notification_provider'] );
		}

		return $result;
	}

	/**
	 * Sanitize push-notification event overrides out of a settings save payload.
	 *
	 * Each event is rendered by the custom `notification_event` field which
	 * stores its full state as a single object value at
	 * `events.events_<cat_id>.<event_id>`:
	 *   { enabled: bool, title: string, body: string }
	 *
	 * @param array<string, mixed>                                                                           $data       Incoming POST data.
	 * @param array<string, array{events: array<int, array{id:string, title?:string, body?:string}>}>       $categories Catalog defining valid event IDs.
	 *
	 * @return array<string, array{enabled:bool, title:string, body:string}>
	 */
	protected static function sanitize_push_events_data( array $data, array $categories ): array {
		$out = [];

		foreach ( $categories as $cat_id => $cat ) {
			$section = self::find_section( $data, 'events_' . $cat_id );

			if ( ! is_array( $section ) ) {
				continue;
			}

			foreach ( ( $cat['events'] ?? [] ) as $event ) {
				$event_id = (string) ( $event['id'] ?? '' );
				if ( '' === $event_id ) {
					continue;
				}

				$val = $section[ $event_id ] ?? null;
				if ( ! is_array( $val ) ) {
					continue;
				}

				$out[ sanitize_key( $event_id ) ] = [
					'enabled' => (bool) ( $val['enabled'] ?? true ),
					'title'   => sanitize_text_field( (string) ( $val['title'] ?? '' ) ),
					'body'    => sanitize_textarea_field( (string) ( $val['body'] ?? '' ) ),
				];
			}
		}

		return $out;
	}

	/**
	 * Locate a section's flat field map inside a settings save payload.
	 *
	 * The plugin-ui formatter rewrites each field's `dependency_key` to be the
	 * dot-joined chain of every ancestor element ID, so the incoming data is
	 * nested by `tab → section → field` (or just `section → field` when the
	 * section is placed directly on a page). This walks the tree to return
	 * the section's `{ field_id => value }` block from either depth.
	 *
	 * @param array<string, mixed> $data
	 * @return array<string, mixed>|null
	 */
	protected static function find_section( array $data, string $section_id ): ?array {
		if ( isset( $data[ $section_id ] ) && is_array( $data[ $section_id ] ) ) {
			return $data[ $section_id ];
		}

		foreach ( $data as $value ) {
			if ( is_array( $value ) ) {
				$nested = self::find_section( $value, $section_id );
				if ( null !== $nested ) {
					return $nested;
				}
			}
		}

		return null;
	}

	/**
	 * Sanitizes the full payload before saving.
	 * Merges common sanitization with any integration-specific rules.
	 * Override to add or replace integration-specific field sanitization.
	 *
	 * @param array<string, mixed> $data
	 * @return array<string, mixed>
	 */
	protected function sanitize( array $data ): array {
		return $this->sanitize_common( $data );
	}

	// ── SettingsSchemaInterface ───────────────────────────────────────────────

	/**
	 * The wp_option key for this integration.
	 * Override when a legacy or custom option name is required.
	 */
	public function get_option_key(): string {
		return 'app_bridge_' . $this->slug() . '_settings';
	}

	/**
	 * Assembles the full flat schema: common + integration-specific, then
	 * applies the `app_bridge_{slug}_settings_schema` filter.
	 *
	 * @return array<int, array<string, mixed>>
	 */
	public function get_schema(): array {
		$settings = get_option( $this->get_option_key(), [] );

		$schema = array_merge(
			// Pages — sorted by priority after assembly
			[ $this->general_page() ],
			[ $this->push_notifications_page() ],
			[ $this->license_page() ],
			$this->pages(),
			// Common sections
			[ $this->app_identity_section() ],
			$this->languages_section(),
			$this->button_colors_section(),
			[ $this->notification_provider_section() ],
			// Integration-specific sections
			$this->sections(),
			// Common fields
			$this->app_identity_fields( $settings ),
			$this->languages_fields( $settings ),
			$this->welcome_screen_fields( $settings ),
			$this->button_color_fields( $settings ),
			$this->notification_provider_fields( $settings ),
			[ $this->license_field() ],
			// Integration-specific fields
			$this->fields( $settings ),
		);

		// When the license is not active, replace the content of every non-license
		// page with a single `license_cta` field.  The field-renderer dispatches
		// unknown variants through applyFilters, so each app can supply its own
		// CTA component without requiring any plugin-ui changes.
		if ( ! $this->is_licensed() ) {
			$locked_page_ids = [];

			foreach ( $schema as &$element ) {
				if ( 'page' === $element['type'] && 'license' !== $element['id'] ) {
					$locked_page_ids[]    = $element['id'];
					$element['hide_save'] = true;
				}
			}
			unset( $element );

			if ( ! empty( $locked_page_ids ) ) {
				// Resolve the owning page for every section/tab so we can
				// reliably strip nested fields (which reference `tab_id` or
				// `section_id`, not `page_id` directly).
				$page_of = [];
				foreach ( $schema as $element ) {
					if ( in_array( $element['type'], [ 'section', 'tab' ], true ) && isset( $element['page_id'], $element['id'] ) ) {
						$page_of[ $element['id'] ] = $element['page_id'];
					}
				}

				$resolve_page = static function ( array $element ) use ( $page_of ): string {
					if ( isset( $element['page_id'] ) ) {
						return (string) $element['page_id'];
					}
					if ( isset( $element['tab_id'], $page_of[ $element['tab_id'] ] ) ) {
						return (string) $page_of[ $element['tab_id'] ];
					}
					if ( isset( $element['section_id'], $page_of[ $element['section_id'] ] ) ) {
						return (string) $page_of[ $element['section_id'] ];
					}
					return '';
				};

				$schema = array_filter(
					$schema,
					function ( $element ) use ( $locked_page_ids, $resolve_page ) {
						if ( 'page' === $element['type'] ) {
							return true;
						}
						$page_id = $resolve_page( $element );
						return '' === $page_id || ! in_array( $page_id, $locked_page_ids, true );
					}
				);

				// Add one license_cta placeholder per locked page.
				foreach ( $locked_page_ids as $page_id ) {
					$schema[] = [
						'id'      => 'license_cta_' . $page_id,
						'type'    => 'field',
						'variant' => 'license_cta',
						'page_id' => $page_id,
						'no_wrap' => true,
					];
				}
			}
		}

		// Drop any empty elements left by builders that opted out of a common
		// page/section/field (e.g. an integration that omits Push Notifications
		// by overriding those builders to return []).
		$schema = array_filter(
			$schema,
			static function ( $element ) {
				return is_array( $element ) && ! empty( $element );
			}
		);

		return apply_filters( 'app_bridge_' . $this->slug() . '_settings_schema', array_values( $schema ) );
	}

	/**
	 * Persists the payload. Override when saving spans multiple option keys.
	 *
	 * @param array<string, mixed> $data
	 */
	public function save( array $data ): void {
		$existing  = get_option( $this->get_option_key(), [] );
		$sanitized = $this->sanitize( $data );
		update_option( $this->get_option_key(), array_merge( $existing, $sanitized ), false );
	}
}
