<?php
if ( ! defined( 'ABSPATH' ) ) { exit; }

/**
 * Defines every post meta key, its type, default value, sanitization,
 * whether it is overridable per-flipbook (vs. global-only), and
 * the JS config path it maps to.
 *
 * Convention:
 *   meta key  = TNC_FLIPBOOK_3D_PREFIX . '{key}'          e.g. _tncfb3d_flipDuration
 *   override  = TNC_FLIPBOOK_3D_PREFIX . '{key}_override'  e.g. _tncfb3d_flipDuration_override
 */
class TNC_Flipbook_3D_Meta_Schema {

    /** Cached schema array */
    private static $schema = null;

    /**
     * Returns the full schema.
     *
     * Each entry:
     *   'default'     => mixed   Plugin default value
     *   'type'        => string  bool|int|float|string|array|color
     *   'overridable' => bool    Whether per-flipbook can differ from global
     *   'global_key'  => string  Key in tnc_flipbook_3d_settings option
     *   'config_path' => array   Dot-path into the JS config object (null = not sent to JS)
     *   'sanitize'    => callable
     */
    public static function get() {
        if ( self::$schema !== null ) {
            return self::$schema;
        }

        $p = TNC_FLIPBOOK_3D_PREFIX; // _tncfb3d_

        self::$schema = array(

            // ── Content source ─────────────────────────────────────────────
            // (per-flipbook only, no global equivalent)
            "{$p}source_type" => array(
                'default'     => 'pdf',
                'type'        => 'string',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null, // handled by config builder separately
                'sanitize'    => function( $v ) { return in_array( $v, array( 'pdf', 'images', 'multi_pdf' ), true ) ? $v : 'pdf'; },
            ),
            "{$p}pdf_id" => array(
                'default'     => 0,
                'type'        => 'int',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => 'absint',
            ),
            "{$p}pdf_password" => array(
                'default'     => '',
                'type'        => 'string',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null, // Handled manually in config builder (Pro only)
                'sanitize'    => 'sanitize_text_field',
            ),
            "{$p}pdf_ids" => array(
                'default'     => array(),
                'type'        => 'array',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => array( __CLASS__, 'sanitize_pdf_ids' ),
            ),
            "{$p}image_ids" => array(
                'default'     => array(),
                'type'        => 'array',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => array( __CLASS__, 'sanitize_int_array' ),
            ),
            "{$p}page_count" => array(
                'default'     => 0,
                'type'        => 'int',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => 'absint',
            ),

            // ── Container dimensions ────────────────────────────────────────
            "{$p}width" => array(
                'default'     => '100%',
                'type'        => 'string',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => array( 'TNC_Flipbook_3D_Helpers', 'sanitize_dimension' ),
            ),
            "{$p}height" => array(
                'default'     => '600px',
                'type'        => 'string',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => array( 'TNC_Flipbook_3D_Helpers', 'sanitize_dimension' ),
            ),

            // ── Complex per-flipbook data ───────────────────────────────────
            "{$p}page_videos" => array(
                'default'     => array(),
                'type'        => 'array',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => array( __CLASS__, 'sanitize_page_videos' ),
            ),
            "{$p}hotspots" => array(
                'default'     => array(),
                'type'        => 'array',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => array( __CLASS__, 'sanitize_hotspots' ),
            ),
            "{$p}toc_custom" => array(
                'default'     => array(),
                'type'        => 'array',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null, // handled by config builder
                'sanitize'    => array( __CLASS__, 'sanitize_toc_entries' ),
            ),

            // ── General ─────────────────────────────────────────────────────
            "{$p}flipDuration" => array(
                'default'     => 1200,
                'type'        => 'int',
                'overridable' => true,
                'global_key'  => 'flipDuration',
                'config_path' => array( 'flipDuration' ),
                'sanitize'    => function( $v ) { return max( 100, min( 3000, absint( $v ) ) ); },
            ),
            "{$p}fastFlipDuration" => array(
                'default'     => 300,
                'type'        => 'int',
                'overridable' => true,
                'global_key'  => 'fastFlipDuration',
                'config_path' => array( 'fastFlipDuration' ),
                'sanitize'    => function( $v ) { return max( 50, min( 1000, absint( $v ) ) ); },
            ),
            "{$p}flipSound" => array(
                'default'     => '1',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'flipSound',
                'config_path' => array( 'flipSound' ),
                'sanitize'    => function( $v ) {
                    return in_array( $v, array( '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', 'custom', 'none' ), true ) ? $v : '1';
                },
            ),
            "{$p}flipSoundCustom_id" => array(
                'default'     => 0,
                'type'        => 'int',
                'overridable' => true,
                'global_key'  => 'flipSoundCustom_id',
                'config_path' => null, // resolved to URL by config builder
                'sanitize'    => 'absint',
            ),
            "{$p}curlIntensity" => array(
                'default'     => 1.0,
                'type'        => 'float',
                'overridable' => true,
                'global_key'  => 'curlIntensity',
                'config_path' => array( 'curlIntensity' ),
                'sanitize'    => function( $v ) { return max( 0.3, min( 1.5, round( (float) $v, 2 ) ) ); },
            ),
            "{$p}flipEffect" => array(
                'default'     => 'realistic',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'flipEffect',
                'config_path' => array( 'flipEffect' ),
                'sanitize'    => function( $v ) {
                    return in_array( $v, array( 'realistic', 'hard', 'slide', 'fade' ), true ) ? $v : 'realistic';
                },
            ),
            "{$p}renderScale" => array(
                'default'     => 2,
                'type'        => 'float',
                'overridable' => true,
                'global_key'  => 'renderScale',
                'config_path' => array( 'renderScale' ),
                'sanitize'    => function( $v ) { return max( 0.5, min( 4, (float) $v ) ); },
            ),
            "{$p}language" => array(
                'default'     => 'en',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'language',
                'config_path' => array( 'language' ),
                'sanitize'    => 'sanitize_text_field',
            ),
            "{$p}linkTarget" => array(
                'default'     => 'new',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'linkTarget',
                'config_path' => array( 'linkTarget' ),
                'sanitize'    => function( $v ) { return in_array( $v, array( 'new', 'same' ), true ) ? $v : 'new'; },
            ),
            "{$p}deepLinking" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'deepLinking',
                'config_path' => array( 'deepLinking' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),
            "{$p}pdfLinkOverlay" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'pdfLinkOverlay',
                'config_path' => array( 'pdfLinkOverlay' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),
            "{$p}renderMode" => array(
                'default'     => 'auto',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'renderMode',
                'config_path' => array( 'renderMode' ),
                'sanitize'    => function( $v ) { return in_array( $v, array( 'auto', '3d', '2d' ), true ) ? $v : 'auto'; },
            ),
            "{$p}defaultViewMode" => array(
                'default'     => 'flipbook',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'defaultViewMode',
                'config_path' => array( 'defaultViewMode' ),
                'sanitize'    => function( $v ) { return in_array( $v, array( 'flipbook', 'single', 'vertical', 'horizontal' ), true ) ? $v : 'flipbook'; },
            ),
            "{$p}pageMode" => array(
                'default'     => 'odd',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'pageMode',
                'config_path' => array( 'pageMode' ),
                'sanitize'    => function( $v ) { return in_array( $v, array( 'odd', 'even' ), true ) ? $v : 'odd'; },
            ),
            "{$p}forceSinglePageMobile" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'forceSinglePageMobile',
                'config_path' => array( 'forceSinglePageMobile' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),
            "{$p}defaultZoom" => array(
                'default'     => 'auto',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'defaultZoom',
                'config_path' => array( 'defaultZoom' ),
                'sanitize'    => function( $v ) { return in_array( $v, array( 'auto', 'page-fit', 'page-width', '50', '75', '100', '125', '150', '200', '250', '300', '400' ), true ) ? $v : 'auto'; },
            ),
            // ── Startup panel (Pro) — per-FlipBook auto-open of a sidebar on load ──
            // Per-FlipBook only (no global default): overridable=false, no global_key.
            // config_path is null because the Pro config builder emits these
            // explicitly (gated + cast); the Lite builder never emits them, which
            // is what keeps the feature Pro-only.
            "{$p}startupPanel" => array(
                'default'     => 'none',
                'type'        => 'string',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => function( $v ) { return in_array( $v, array( 'none', 'bookmarks', 'thumbnails', 'search' ), true ) ? $v : 'none'; },
            ),
            "{$p}startupSearchQuery" => array(
                'default'     => '',
                'type'        => 'string',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => function( $v ) { return sanitize_text_field( (string) $v ); },
            ),
            "{$p}startupPanelOnMobile" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),
            "{$p}ga4MeasurementId" => array(
                'default'     => '',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'ga4MeasurementId',
                'config_path' => array( 'ga4MeasurementId' ),
                'sanitize'    => 'sanitize_text_field',
            ),
            "{$p}loadingLogo_id" => array(
                'default'     => 0,
                'type'        => 'int',
                'overridable' => true,
                'global_key'  => 'loadingLogo_id',
                'config_path' => null, // resolved to URL by config builder
                'sanitize'    => 'absint',
            ),
            "{$p}loadingLogoDark_id" => array(
                'default'     => 0,
                'type'        => 'int',
                'overridable' => true,
                'global_key'  => 'loadingLogoDark_id',
                'config_path' => null, // resolved to URL by config builder
                'sanitize'    => 'absint',
            ),
            "{$p}loadingLogoMaxWidth" => array(
                'default'     => 150,
                'type'        => 'int',
                'overridable' => true,
                'global_key'  => 'loadingLogoMaxWidth',
                'config_path' => array( 'loadingLogoMaxWidth' ),
                'sanitize'    => function( $v ) { return max( 50, min( 500, absint( $v ) ) ); },
            ),

            // ── Appearance ──────────────────────────────────────────────────
            "{$p}toolbarLayout" => array(
                'default'     => 'bottom-center',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'toolbarLayout',
                'config_path' => array( 'toolbarLayout' ),
                'sanitize'    => function( $v ) {
                    return in_array( $v, array( 'bottom-center', 'top-center', 'bottom-fullwidth', 'top-fullwidth' ), true ) ? $v : 'bottom-center';
                },
            ),
            "{$p}flipbookStyle" => array(
                'default'     => 'default',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'flipbookStyle',
                'config_path' => array( 'flipbookStyle' ),
                'sanitize'    => function( $v ) {
                    return in_array( $v, array( 'default', 'hardcover', 'spiral' ), true ) ? $v : 'default';
                },
            ),
            "{$p}isRTL" => array(
                'default'     => false,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'isRTL',
                'config_path' => array( 'isRTL' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),
            "{$p}paperType" => array(
                'default'     => 'matte',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'paperType',
                'config_path' => array( 'paperType' ),
                'sanitize'    => function( $v ) {
                    return in_array( $v, array( 'matte', 'glossy', 'textured', 'newsprint' ), true ) ? $v : 'matte';
                },
            ),
            "{$p}paperTint" => array(
                'default'     => '#f0e6c8',
                'type'        => 'color',
                'overridable' => true,
                'global_key'  => 'paperTint',
                'config_path' => array( 'paperTint' ),
                'sanitize'    => array( 'TNC_Flipbook_3D_Helpers', 'sanitize_hex_color' ),
            ),
            "{$p}pageShininess" => array(
                'default'     => 0.1,
                'type'        => 'float',
                'overridable' => true,
                'global_key'  => 'pageShininess',
                'config_path' => array( 'pageShininess' ),
                'sanitize'    => function( $v ) { return max( 0.0, min( 1.0, (float) $v ) ); },
            ),
            "{$p}pageRoughness" => array(
                'default'     => 0.8,
                'type'        => 'float',
                'overridable' => true,
                'global_key'  => 'pageRoughness',
                'config_path' => array( 'pageRoughness' ),
                'sanitize'    => function( $v ) { return max( 0.0, min( 1.0, (float) $v ) ); },
            ),

            // ── Colors ──────────────────────────────────────────────────────
            // Chrome = surface palette (toolbar/sidebar/popup/page background).
            // Brand color = single hex that drives every accent (primary buttons,
            // active icons, focus rings). Hover/active shades are derived in JS.
            "{$p}chrome" => array(
                'default'     => 'slate',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'chrome',
                'config_path' => null, // goes into options.colors.chrome
                'sanitize'    => function( $v ) {
                    // Migrate legacy names from before the rename.
                    $migrate = array( 'dark' => 'slate', 'light' => 'paper' );
                    if ( isset( $migrate[ $v ] ) ) { $v = $migrate[ $v ]; }
                    $valid = array( 'slate', 'paper', 'glass', 'sepia', 'midnight', 'ocean', 'forest' );
                    return in_array( $v, $valid, true ) ? $v : 'slate';
                },
            ),
            "{$p}brandColor" => array(
                'default'     => '#4a9eff',
                'type'        => 'color',
                'overridable' => true,
                'global_key'  => 'brandColor',
                'config_path' => null, // goes into options.colors.brandColor
                'sanitize'    => 'sanitize_hex_color',
            ),
            // Hardcover wrap color. Empty = derive from active chrome polarity.
            // Spine and bevel are auto-derived shades of this color in JS.
            "{$p}coverColor" => array(
                'default'     => '',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'coverColor',
                'config_path' => array( 'coverColor' ),
                'sanitize'    => function( $v ) {
                    $v = trim( (string) $v );
                    if ( $v === '' ) return '';
                    return sanitize_hex_color( $v ) ?: '';
                },
            ),
            // Initial polarity. 'auto' uses the chrome's natural defaultMode;
            // 'dark'/'light' force a starting polarity. Reader can flip via toolbar.
            "{$p}colorMode" => array(
                'default'     => 'auto',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'colorMode',
                'config_path' => null, // goes into options.colors.colorMode
                'sanitize'    => function( $v ) {
                    $valid = array( 'auto', 'dark', 'light' );
                    return in_array( $v, $valid, true ) ? $v : 'auto';
                },
            ),

            // ── Camera ──────────────────────────────────────────────────────
            "{$p}cameraDistance" => array(
                'default'     => 8,
                'type'        => 'float',
                'overridable' => true,
                'global_key'  => 'cameraDistance',
                'config_path' => array( 'cameraDistance' ),
                'sanitize'    => function( $v ) { return max( 6, min( 20, (float) $v ) ); },
            ),
            "{$p}cameraAngle" => array(
                'default'     => 2,
                'type'        => 'float',
                'overridable' => true,
                'global_key'  => 'cameraAngle',
                'config_path' => array( 'cameraAngle' ),
                'sanitize'    => function( $v ) { return max( 0, min( 45, (float) $v ) ); },
            ),
            "{$p}cameraFOV" => array(
                'default'     => 50,
                'type'        => 'int',
                'overridable' => true,
                'global_key'  => 'cameraFOV',
                'config_path' => array( 'cameraFOV' ),
                'sanitize'    => function( $v ) { return max( 30, min( 75, absint( $v ) ) ); },
            ),
            "{$p}bookTilt" => array(
                'default'     => 9,
                'type'        => 'float',
                'overridable' => true,
                'global_key'  => 'bookTilt',
                'config_path' => array( 'bookTilt' ),
                'sanitize'    => function( $v ) { return max( 0, min( 25, (float) $v ) ); },
            ),

            // ── Lighting ────────────────────────────────────────────────────
            "{$p}ambientLightIntensity" => array(
                'default'     => 1.2,
                'type'        => 'float',
                'overridable' => true,
                'global_key'  => 'ambientLightIntensity',
                'config_path' => array( 'ambientLightIntensity' ),
                'sanitize'    => function( $v ) { return max( 0, min( 3, (float) $v ) ); },
            ),
            "{$p}mainLightIntensity" => array(
                'default'     => 0.5,
                'type'        => 'float',
                'overridable' => true,
                'global_key'  => 'mainLightIntensity',
                'config_path' => array( 'mainLightIntensity' ),
                'sanitize'    => function( $v ) { return max( 0, min( 3, (float) $v ) ); },
            ),
            "{$p}mainLightPosition" => array(
                'default'     => array( 0, 10, 10 ),
                'type'        => 'array',
                'overridable' => true,
                'global_key'  => 'mainLightPosition',
                'config_path' => array( 'mainLightPosition' ),
                'sanitize'    => array( __CLASS__, 'sanitize_light_position' ),
            ),
            "{$p}shadowsEnabled" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'shadowsEnabled',
                'config_path' => array( 'shadowsEnabled' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),
            "{$p}shadowSoftness" => array(
                'default'     => 'soft',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'shadowSoftness',
                'config_path' => array( 'shadowSoftness' ),
                'sanitize'    => function( $v ) {
                    return in_array( $v, array( 'sharp', 'soft', 'very-soft' ), true ) ? $v : 'soft';
                },
            ),
            "{$p}shadowOpacity" => array(
                'default'     => 1.0,
                'type'        => 'float',
                'overridable' => true,
                'global_key'  => 'shadowOpacity',
                'config_path' => array( 'shadowOpacity' ),
                'sanitize'    => function( $v ) { return max( 0, min( 1, (float) $v ) ); },
            ),
            "{$p}dropShadowEnabled" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'dropShadowEnabled',
                'config_path' => array( 'dropShadowEnabled' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),

            // ── Accessibility ────────────────────────────────────────────────
            "{$p}a11y_enabled" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'a11y_enabled',
                'config_path' => array( 'accessibility', 'enabled' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),
            "{$p}a11y_skipLinks" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'a11y_skipLinks',
                'config_path' => array( 'accessibility', 'skipLinks' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),
            "{$p}a11y_keyboardShortcuts" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'a11y_keyboardShortcuts',
                'config_path' => array( 'accessibility', 'keyboardShortcuts' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),
            "{$p}a11y_screenReaderAnnouncements" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'a11y_screenReaderAnnouncements',
                'config_path' => array( 'accessibility', 'screenReaderAnnouncements' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),
            "{$p}a11y_focusIndicators" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'a11y_focusIndicators',
                'config_path' => array( 'accessibility', 'focusIndicators' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),
            "{$p}a11y_reducedMotion" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'a11y_reducedMotion',
                'config_path' => array( 'accessibility', 'reducedMotion' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),
            "{$p}a11y_highContrastMode" => array(
                'default'     => false,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'a11y_highContrastMode',
                'config_path' => array( 'accessibility', 'highContrastMode' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),
            "{$p}a11y_keyboardShortcutsHelp" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => 'a11y_keyboardShortcutsHelp',
                'config_path' => array( 'accessibility', 'keyboardShortcutsHelp' ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            ),

            // ── TTS ──────────────────────────────────────────────────────────
            "{$p}tts_provider" => array(
                'default'     => 'browser',
                'type'        => 'string',
                'overridable' => true,
                'global_key'  => 'tts_defaultProvider',
                'config_path' => null, // goes into options.tts.defaultProvider
                'sanitize'    => function( $v ) {
                    return in_array( $v, array( 'browser', 'google', 'azure', 'amazon', 'elevenlabs', 'openai' ), true ) ? $v : 'browser';
                },
            ),
            "{$p}tts_speed" => array(
                'default'     => 1,
                'type'        => 'float',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => function( $v ) {
                    $v = (float) $v;
                    return ( $v >= 0.5 && $v <= 2 ) ? $v : 1;
                },
            ),
            "{$p}tts_browser_lang" => array(
                'default'     => 'en-US',
                'type'        => 'string',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => 'sanitize_text_field',
            ),
            "{$p}tts_autoflip" => array(
                'default'     => true,
                'type'        => 'bool',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => 'boolval',
            ),

            // ── WooCommerce / content restriction ──────────────────────────
            // config_path is null - these are NOT auto-mapped by the schema loop.
            // The gating block (step 7b in config builder) maps them explicitly
            // only when a restriction provider confirms 'preview' access status.
            "{$p}woo_product_id" => array(
                'default'     => '',
                'type'        => 'string',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => function( $v ) {
                    if ( is_array( $v ) ) { $v = implode( ',', $v ); }
                    return implode( ',', array_filter( array_map( 'absint', explode( ',', (string) $v ) ) ) );
                },
            ),
            "{$p}preview_pages" => array(
                'default'     => 0,
                'type'        => 'int',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => 'absint',
            ),
            "{$p}gating_message" => array(
                'default'     => 'Purchase to continue reading',
                'type'        => 'text',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => 'sanitize_text_field',
            ),
            "{$p}purchase_button_text" => array(
                'default'     => '',
                'type'        => 'text',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => 'sanitize_text_field',
            ),
            "{$p}purchase_url" => array(
                'default'     => '',
                'type'        => 'url',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => 'esc_url_raw',
            ),

            // ── Per-feature preview-mode policy (Phase 3 hardening) ───────
            // Each gated toolbar item gets a 3-state policy controlling how
            // it behaves when the visitor is in preview mode:
            //
            //   'overlay' (default) — show button + open gating overlay on click
            //   'hide'              — hide the button in preview mode entirely
            //   'partial'           — allow the feature to run on the already-
            //                         truncated content (server-side gates still
            //                         enforce that the content is only N pages)
            //
            // The config builder emits these into CONFIG.previewPolicy for the
            // client-side _checkPreviewAccess() helper to consume.
            "{$p}preview_policy_download" => array(
                'default'     => 'overlay',
                'type'        => 'string',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => function( $v ) { return in_array( $v, array( 'hide', 'overlay', 'partial' ), true ) ? $v : 'overlay'; },
            ),
            "{$p}preview_policy_print" => array(
                'default'     => 'overlay',
                'type'        => 'string',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => function( $v ) { return in_array( $v, array( 'hide', 'overlay', 'partial' ), true ) ? $v : 'overlay'; },
            ),
            "{$p}preview_policy_formViewer" => array(
                'default'     => 'overlay',
                'type'        => 'string',
                'overridable' => false,
                'global_key'  => null,
                'config_path' => null,
                'sanitize'    => function( $v ) { return in_array( $v, array( 'hide', 'overlay', 'partial' ), true ) ? $v : 'overlay'; },
            ),
        );

        // ── Toolbar flags (26 items) ─────────────────────────────────────────
        $toolbar_defaults = array(
            'share' => true, 'download' => true, 'print' => true,
            'search' => true, 'tilt' => true, 'rotate' => true, 'zoom' => true,
            'readingMode' => true, 'readAloud' => true, 'fullscreen' => true,
            'themeToggle' => true, 'moreMenu' => true, 'pageIndicator' => true,
            'logo' => true, 'autoPlay' => true,
            'sound' => true, 'singlePage' => true, 'bookmarks' => true,
            'notes' => true, 'thumbnails' => true, 'viewModes' => false,
            'formViewer' => true, 'flipbookStyles' => true, 'rtl' => true,
            'navigation' => true,
            'firstLast' => true,
        );

        foreach ( $toolbar_defaults as $item => $default ) {
            self::$schema[ "{$p}toolbar_{$item}" ] = array(
                'default'     => $default,
                'type'        => 'bool',
                'overridable' => true,
                'global_key'  => "toolbar_{$item}",
                'config_path' => array( 'toolbar', $item ),
                'sanitize'    => array( __CLASS__, 'sanitize_bool' ),
            );
        }

        // ── Content protection fields ────────────────────────────────────────
        $protection = array(
            'enabled'                         => array( false, 'bool', 'protection_enabled', array( 'contentProtection', 'enabled' ) ),
            'watermark_enabled'               => array( false, 'bool', 'protection_watermark_enabled', array( 'contentProtection', 'watermark', 'enabled' ) ),
            'watermark_type'                  => array( 'text', 'string', 'protection_watermark_type', array( 'contentProtection', 'watermark', 'type' ) ),
            'watermark_text'                  => array( 'CONFIDENTIAL', 'string', 'protection_watermark_text', array( 'contentProtection', 'watermark', 'text' ) ),
            'watermark_opacity'               => array( 0.08, 'float', 'protection_watermark_opacity', array( 'contentProtection', 'watermark', 'opacity' ) ),
            'watermark_fontSize'              => array( 48,   'int',   'protection_watermark_fontSize', array( 'contentProtection', 'watermark', 'fontSize' ) ),
            'watermark_color'                 => array( '#888888', 'color', 'protection_watermark_color', array( 'contentProtection', 'watermark', 'color' ) ),
            'watermark_rotation'              => array( -30,  'int',   'protection_watermark_rotation', array( 'contentProtection', 'watermark', 'rotation' ) ),
            'watermark_repeat'                => array( true,  'bool', 'protection_watermark_repeat', array( 'contentProtection', 'watermark', 'repeat' ) ),
            'watermark_position'              => array( 'center', 'string', 'protection_watermark_position', array( 'contentProtection', 'watermark', 'position' ) ),
            'watermark_image_id'              => array( 0,     'int',    'protection_watermark_image_id', null ),
            'watermark_imageWidth'            => array( 150,   'int',    'protection_watermark_imageWidth', array( 'contentProtection', 'watermark', 'imageWidth' ) ),
            'watermark_includeInDownload'     => array( false, 'bool', 'protection_watermark_includeInDownload', array( 'contentProtection', 'watermark', 'includeInDownload' ) ),
            'watermark_includeInPrint'        => array( false, 'bool', 'protection_watermark_includeInPrint', array( 'contentProtection', 'watermark', 'includeInPrint' ) ),
            'disableRightClick'               => array( true,  'bool', 'protection_disableRightClick', array( 'contentProtection', 'disableRightClick' ) ),
            'disableKeyboardShortcuts'        => array( true,  'bool', 'protection_disableKeyboardShortcuts', array( 'contentProtection', 'disableKeyboardShortcuts' ) ),
            'disableImageDrag'                => array( true,  'bool', 'protection_disableImageDrag', array( 'contentProtection', 'disableImageDrag' ) ),
            'disableSelection'                => array( true,  'bool', 'protection_disableSelection', array( 'contentProtection', 'disableSelection' ) ),
            'disableDownload'                 => array( true,  'bool', 'protection_disableDownload', array( 'contentProtection', 'disableDownload' ) ),
            'devTools_enabled'                => array( false, 'bool', 'protection_devTools_enabled', array( 'contentProtection', 'devToolsDetection', 'enabled' ) ),
            'devTools_action'                 => array( 'warn', 'string', 'protection_devTools_action', array( 'contentProtection', 'devToolsDetection', 'action' ) ),
            'devTools_message'                => array( 'Developer tools detected. Content protection is active.', 'string', 'protection_devTools_message', array( 'contentProtection', 'devToolsDetection', 'message' ) ),
            'print_enabled'                   => array( true,  'bool', 'protection_print_enabled', array( 'contentProtection', 'printProtection', 'enabled' ) ),
            'print_allowPrint'                => array( true,  'bool', 'protection_print_allowPrint', array( 'contentProtection', 'printProtection', 'allowPrint' ) ),
            'print_watermark'                 => array( 'PRINTED COPY - {date} {time}', 'string', 'protection_print_watermark', array( 'contentProtection', 'printProtection', 'printWatermark' ) ),
            'screenshot_enabled'              => array( false, 'bool', 'protection_screenshot_enabled', array( 'contentProtection', 'screenshotDeterrent', 'enabled' ) ),
            'screenshot_message'              => array( 'Screenshots are not permitted for this content.', 'string', 'protection_screenshot_message', array( 'contentProtection', 'screenshotDeterrent', 'message' ) ),
            'hideSourcePaths'                 => array( true,  'bool', 'protection_hideSourcePaths', array( 'contentProtection', 'hideSourcePaths' ) ),
        );

        foreach ( $protection as $key => list( $default, $type, $global_key, $config_path ) ) {
            self::$schema[ "{$p}protection_{$key}" ] = array(
                'default'     => $default,
                'type'        => $type,
                'overridable' => true,
                'global_key'  => $global_key,
                'config_path' => $config_path,
                'sanitize'    => self::sanitizer_for_type( $type, $key ),
            );
        }

        // ── SEO ──────────────────────────────────────────────────────
        self::$schema["{$p}seo_title"] = array(
            'default' => '', 'type' => 'string', 'overridable' => false,
            'config_path' => null, 'sanitize' => 'sanitize_text_field',
        );
        self::$schema["{$p}seo_description"] = array(
            'default' => '', 'type' => 'string', 'overridable' => false,
            'config_path' => null, 'sanitize' => 'sanitize_textarea_field',
        );
        self::$schema["{$p}seo_robots"] = array(
            'default' => 'index', 'type' => 'string', 'overridable' => false,
            'config_path' => null,
            'sanitize' => function($v) { return in_array($v, array('index', 'noindex'), true) ? $v : 'index'; },
        );
        self::$schema["{$p}seo_canonical"] = array(
            'default' => '', 'type' => 'string', 'overridable' => false,
            'config_path' => null, 'sanitize' => 'esc_url_raw',
        );
        self::$schema["{$p}seo_og_image_id"] = array(
            'default' => 0, 'type' => 'int', 'overridable' => false,
            'config_path' => null, 'sanitize' => 'absint',
        );

        /**
         * @hook tncfb3d_meta_schema
         * @param array $schema Full meta schema definition array.
         * @return array
         */
        self::$schema = apply_filters( 'tncfb3d_meta_schema', self::$schema );

        return self::$schema;
    }

    /**
     * Return only overridable schema entries (those with a global counterpart).
     */
    public static function get_overridable() {
        return array_filter( self::get(), function( $def ) {
            return ! empty( $def['overridable'] );
        } );
    }

    /**
     * Get default value for a given meta key.
     */
    public static function default_for( $meta_key ) {
        $schema = self::get();
        return isset( $schema[ $meta_key ] ) ? $schema[ $meta_key ]['default'] : null;
    }

    /**
     * Sanitize a posted value for a given meta key.
     */
    public static function sanitize( $meta_key, $value ) {
        $schema = self::get();
        if ( ! isset( $schema[ $meta_key ] ) ) {
            return sanitize_text_field( $value );
        }
        $cb = $schema[ $meta_key ]['sanitize'];
        if ( is_callable( $cb ) ) {
            return call_user_func( $cb, $value );
        }
        return sanitize_text_field( $value );
    }

    // ── Sanitizer helpers ────────────────────────────────────────────────────

    public static function sanitize_bool( $v ) {
        return filter_var( $v, FILTER_VALIDATE_BOOLEAN );
    }

    public static function sanitize_int_array( $arr ) {
        if ( ! is_array( $arr ) ) { return array(); }
        return array_values( array_map( 'absint', $arr ) );
    }

    public static function sanitize_light_position( $arr ) {
        if ( ! is_array( $arr ) || count( $arr ) < 3 ) {
            return array( 0, 10, 10 );
        }
        return array( (float) $arr[0], (float) $arr[1], (float) $arr[2] );
    }

    public static function sanitize_pdf_ids( $arr ) {
        if ( ! is_array( $arr ) ) { return array(); }
        $out = array();
        foreach ( $arr as $item ) {
            if ( is_array( $item ) ) {
                $out[] = array(
                    'id'   => absint( $item['id'] ?? 0 ),
                    'name' => sanitize_text_field( $item['name'] ?? '' ),
                );
            }
        }
        return $out;
    }

    public static function sanitize_page_videos( $arr ) {
        if ( ! is_array( $arr ) ) { return array(); }
        $out = array();
        foreach ( $arr as $item ) {
            if ( ! is_array( $item ) ) { continue; }
            $out[] = array(
                'page'      => absint( $item['page'] ?? 1 ),
                'video_id'  => absint( $item['video_id'] ?? 0 ),
                'poster_id' => absint( $item['poster_id'] ?? 0 ),
                'x'         => max( 0, min( 100, (float)( $item['x'] ?? 0 ) ) ),
                'y'         => max( 0, min( 100, (float)( $item['y'] ?? 0 ) ) ),
                'width'     => max( 1, min( 100, (float)( $item['width'] ?? 100 ) ) ),
                'height'    => max( 1, min( 100, (float)( $item['height'] ?? 100 ) ) ),
                'autoPlay'  => (bool)( $item['autoPlay'] ?? true ),
                'muted'     => (bool)( $item['muted'] ?? true ),
                'loop'      => (bool)( $item['loop'] ?? false ),
                'controls'  => (bool)( $item['controls'] ?? true ),
            );
        }
        return $out;
    }

    public static function sanitize_toc_entries( $arr ) {
        if ( ! is_array( $arr ) ) { return array(); }
        $out = array();
        foreach ( $arr as $item ) {
            if ( ! is_array( $item ) ) { continue; }
            $title = sanitize_text_field( $item['title'] ?? '' );
            $page  = absint( $item['page'] ?? 0 );
            if ( $title && $page ) {
                $out[] = array( 'title' => $title, 'page' => $page );
            }
        }
        return $out;
    }

    public static function sanitize_hotspots( $arr ) {
        // Accept JSON string from visual editor hidden input
        if ( is_string( $arr ) ) {
            $decoded = json_decode( $arr, true );
            $arr = is_array( $decoded ) ? $decoded : array();
        }
        if ( ! is_array( $arr ) ) { return array(); }
        $valid_types   = array( 'pin', 'area' );
        $valid_actions = array( 'popup', 'url', 'page', 'media' );
        $valid_icons   = array( 'pin', 'cart', 'info', 'star', 'tag', 'link', 'play' );
        $out = array();
        foreach ( $arr as $item ) {
            if ( ! is_array( $item ) ) { continue; }
            $out[] = array(
                'page'         => absint( $item['page'] ?? 1 ),
                'x'            => max( 0, min( 1, (float)( $item['x'] ?? 0.5 ) ) ),
                'y'            => max( 0, min( 1, (float)( $item['y'] ?? 0.5 ) ) ),
                'width'        => max( 0, min( 1, (float)( $item['width'] ?? 0.1 ) ) ),
                'height'       => max( 0, min( 1, (float)( $item['height'] ?? 0.1 ) ) ),
                'type'         => in_array( $item['type'] ?? '', $valid_types, true ) ? $item['type'] : 'pin',
                'action'       => in_array( $item['action'] ?? '', $valid_actions, true ) ? $item['action'] : 'popup',
                'icon'         => in_array( $item['icon'] ?? '', $valid_icons, true ) ? $item['icon'] : 'pin',
                'color'        => TNC_Flipbook_3D_Helpers::sanitize_hex_color( $item['color'] ?? '#3b82f6' ) ?: '#3b82f6',
                'title'        => sanitize_text_field( $item['title'] ?? '' ),
                'description'  => wp_kses_post( $item['description'] ?? '' ),
                'image_id'     => absint( $item['image_id'] ?? 0 ),
                'price'        => sanitize_text_field( $item['price'] ?? '' ),
                'cta'          => sanitize_text_field( $item['cta'] ?? '' ),
                'ctaUrl'       => esc_url_raw( $item['ctaUrl'] ?? '' ),
                'ctaTarget'    => in_array( $item['ctaTarget'] ?? '', array( 'new', 'same' ), true ) ? $item['ctaTarget'] : 'new',
                'url'          => esc_url_raw( $item['url'] ?? '' ),
                'target'       => in_array( $item['target'] ?? '', array( 'new', 'same' ), true ) ? $item['target'] : 'new',
                'targetPage'   => absint( $item['targetPage'] ?? 1 ),
                'mediaSrc_id'  => absint( $item['mediaSrc_id'] ?? 0 ),
                'mediaType'    => in_array( $item['mediaType'] ?? '', array( 'video', 'audio' ), true ) ? $item['mediaType'] : 'video',
                'mediaTitle'   => sanitize_text_field( $item['mediaTitle'] ?? '' ),
            );
        }
        return $out;
    }

    private static function sanitizer_for_type( $type, $key ) {
        switch ( $type ) {
            case 'bool':   return array( __CLASS__, 'sanitize_bool' );
            case 'int':    return 'absint';
            case 'float':  return 'floatval';
            case 'color':  return array( 'TNC_Flipbook_3D_Helpers', 'sanitize_hex_color' );
            default:       return 'sanitize_text_field';
        }
    }
}
