<?php
/**
 * Settings REST API Controller.
 *
 * Schema-driven approach: each settings sub-page (general, appearance,
 * company, checkout, etc.) has a schema defining its groups and fields.
 * The admin JS fetches the schema, renders the form dynamically, and
 * saves values back via PUT.
 *
 * Admin endpoints (require manage_options):
 *
 *   GET  /titancart/v1/settings/(?P<section>\w+)          Get schema + current values
 *   PUT  /titancart/v1/settings/(?P<section>\w+)          Bulk upsert values
 *
 * @package TitanCart\REST\Settings
 */

namespace TitanCart\REST\Settings;

use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use TitanCart\Core\LogService;
use TitanCart\Services\SettingsService;

defined( 'ABSPATH' ) || exit;

class SettingsController extends WP_REST_Controller {

    protected $namespace = 'titancart/v1';
    protected $rest_base = 'settings';

    // ══════════════════════════════════════════════════════════════════════════
    // ROUTE REGISTRATION
    // ══════════════════════════════════════════════════════════════════════════

    public function register_routes(): void {

        register_rest_route( $this->namespace, "/{$this->rest_base}/(?P<section>[a-z_]+)", [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_settings' ],
                'permission_callback' => [ $this, 'admin_permission' ],
            ],
            [
                'methods'             => 'PUT',
                'callback'            => [ $this, 'update_settings' ],
                'permission_callback' => [ $this, 'admin_permission' ],
            ],
        ] );
    }

    public function admin_permission(): bool {
        return current_user_can( 'manage_options' );
    }

    // ══════════════════════════════════════════════════════════════════════════
    // GET — Returns schema + current values for a section
    // ══════════════════════════════════════════════════════════════════════════

    public function get_settings( WP_REST_Request $request ): WP_REST_Response {
        $section = sanitize_key( $request->get_param( 'section' ) );

        $schema = self::get_schema( $section );
        if ( ! $schema ) {
            return new WP_REST_Response( [ 'code' => 'unknown_section', 'message' => 'Unknown settings section.' ], 404 );
        }

        $db     = titancart_db();
        $rows   = $db->getArray(
            "SELECT `key`, value FROM {$db->table('settings')} WHERE section = ?s",
            $section
        );
        $stored = [];
        foreach ( $rows as $row ) {
            $stored[ $row['key'] ] = $row['value'];
        }

        $values      = [];
        $secrets_set = [];
        foreach ( $schema['groups'] as $group ) {
            foreach ( $group['fields'] as $field ) {
                $key = $field['key'];

                // Never expose a stored secret to the browser. Report only whether
                // one is set so the UI can show "leave blank to keep current".
                if ( ( $field['type'] ?? 'text' ) === 'password' ) {
                    $secrets_set[ $key ] = ( isset( $stored[ $key ] ) && $stored[ $key ] !== '' );
                    $values[ $key ]      = '';
                    continue;
                }

                $values[ $key ] = isset( $stored[ $key ] ) ? $stored[ $key ] : ( $field['default'] ?? '' );
            }
        }

        return new WP_REST_Response( [
            'section'     => $section,
            'schema'      => $schema,
            'values'      => $values,
            'secrets_set' => $secrets_set,
        ] );
    }

    // ══════════════════════════════════════════════════════════════════════════
    // PUT — Bulk upsert settings for a section
    // ══════════════════════════════════════════════════════════════════════════

    public function update_settings( WP_REST_Request $request ): WP_REST_Response {
        $section = sanitize_key( $request->get_param( 'section' ) );

        $schema = self::get_schema( $section );
        if ( ! $schema ) {
            return new WP_REST_Response( [ 'code' => 'unknown_section', 'message' => 'Unknown settings section.' ], 404 );
        }

        $body = $request->get_json_params();
        if ( empty( $body ) || ! is_array( $body ) ) {
            return new WP_REST_Response( [ 'code' => 'invalid_body', 'message' => 'Request body must be a JSON object of key:value pairs.' ], 400 );
        }

        $valid_keys = [];
        foreach ( $schema['groups'] as $group ) {
            foreach ( $group['fields'] as $field ) {
                $valid_keys[ $field['key'] ] = $field;
            }
        }

        // Capture the active preset BEFORE saving so the shape sync below can
        // detect a genuine preset CHANGE (and not re-pin the global product image
        // shape on every theme_options save, e.g. when only a hero image changed).
        $prev_preset = ( $section === 'theme_options' )
            ? sanitize_key( (string) SettingsService::get( 'theme_options', 'active_preset', '' ) )
            : '';

        $db      = titancart_db();
        $table   = $db->table( 'settings' );
        $updated = 0;

        foreach ( $body as $key => $value ) {
            $key = sanitize_key( $key );
            if ( ! isset( $valid_keys[ $key ] ) ) {
                continue;
            }

            $field = $valid_keys[ $key ];

            // Password/secret fields: a blank submission means "keep the current
            // value". The GET never returns the stored secret, so an untouched
            // field arrives empty — only overwrite when a new value was entered.
            // This also makes re-saving the page unable to clobber the password.
            if ( ( $field['type'] ?? 'text' ) === 'password' && (string) $value === '' ) {
                continue;
            }

            $value = self::sanitize_value( $value, $field );

            $now = titancart_now_utc(); // KI-24: explicit UTC (Phase 3 drops the ON UPDATE/DEFAULT)
            $db->query(
                "INSERT INTO {$table} (section, `key`, value, autoload, updated_at)
                 VALUES (?s, ?s, ?s, 1, ?s)
                 ON DUPLICATE KEY UPDATE value = ?s, updated_at = ?s",
                $section, $key, $value, $now, $value, $now
            );
            $updated++;
        }

        LogService::info( 'general', 'update', 'Settings saved: ' . $section . ' (' . $updated . ' fields)', [
            'section'       => $section,
            'fields_updated'=> $updated,
        ] );

        // Flush LogService cache if logging settings were changed.
        if ( $section === 'logging' ) {
            LogService::flush_cache();
        }

        // Flush SettingsService cache so subsequent reads see the new values.
        SettingsService::flush( $section );

        // KI-113 N+2 (S474): let the active theme / extensions react to a save
        // (Prism drops its update-check cache when its licence key changes).
        // Keys only — never the values: a payment-gateway secret must not ride
        // an action every hooked callback can read.
        do_action( 'titancart_settings_saved', $section, array_map( 'sanitize_key', array_keys( $body ) ) );

        // When the active storefront preset changes (Theme Options page or the
        // dev preset switcher — both PUT the `theme_options` section), sync the
        // global product image shape to the newly selected preset's designed
        // shape. The active theme advertises its per-preset shape via the
        // `titancart_preset_image_shape` filter; here we just persist it to
        // thumbnails.product_image_shape. It stays a normal setting the user can
        // change afterward (no runtime override) — selecting a theme simply
        // moves the Thumbnails → Product Image Shape control to match.
        if ( $section === 'theme_options' && array_key_exists( 'active_preset', $body ) ) {
            $preset = sanitize_key( (string) $body['active_preset'] );
            // Only sync when the preset actually CHANGED (a new theme was selected),
            // so a manual Thumbnails change isn't re-pinned on unrelated saves.
            if ( $preset !== '' && $preset !== $prev_preset ) {
                $shape = (string) apply_filters( 'titancart_preset_image_shape', '', $preset );
                if ( in_array( $shape, [ 'tall', 'square', 'wide' ], true ) ) {
                    $now = titancart_now_utc();
                    $db->query(
                        "INSERT INTO {$table} (section, `key`, value, autoload, updated_at)
                         VALUES ('thumbnails', 'product_image_shape', ?s, 1, ?s)
                         ON DUPLICATE KEY UPDATE value = ?s, updated_at = ?s",
                        $shape, $now, $shape, $now
                    );
                    SettingsService::flush( 'thumbnails' );
                    LogService::info( 'general', 'update', 'Product image shape synced to preset "' . $preset . '" (' . $shape . ')', [
                        'preset' => $preset,
                        'shape'  => $shape,
                    ] );
                }

                // Couple the marketplace feature toggle to the active preset:
                // ON for the Marketplace preset, OFF for any other theme. The dev
                // switcher and the Theme Options page both land here, so the toggle
                // tracks the theme however it was switched. Guarded — a no-op when
                // the marketplace extension isn't installed. (is_active() still also
                // requires an active license, so this only drives the admin toggle.)
                if ( class_exists( '\\TitanCartMarketplace\\MarketplacePlugin' ) ) {
                    $mp_enabled = ( 'marketplace' === $preset ) ? '1' : '0';
                    $mp_now     = titancart_now_utc();
                    $db->query(
                        "INSERT INTO {$table} (section, `key`, value, autoload, updated_at)
                         VALUES ('marketplace', 'enabled', ?s, 1, ?s)
                         ON DUPLICATE KEY UPDATE value = ?s, updated_at = ?s",
                        $mp_enabled, $mp_now, $mp_enabled, $mp_now
                    );
                    SettingsService::flush( 'marketplace' );
                    LogService::info( 'general', 'update', 'Marketplace feature ' . ( '1' === $mp_enabled ? 'enabled' : 'disabled' ) . ' to match preset "' . $preset . '"', [
                        'preset'  => $preset,
                        'enabled' => $mp_enabled,
                    ] );
                }
            }
        }

        return new WP_REST_Response( [ 'success' => true, 'updated' => $updated ] );
    }

    // ══════════════════════════════════════════════════════════════════════════
    // VALUE SANITIZATION
    // ══════════════════════════════════════════════════════════════════════════

    private static function sanitize_value( $value, array $field ): string {
        $type = $field['type'] ?? 'text';

        switch ( $type ) {
            case 'checkbox':
                return ( $value === true || $value === '1' || $value === 1 ) ? '1' : '0';

            case 'number':
                return (string) floatval( $value );

            case 'select':
                $options = $field['options'] ?? [];
                $allowed = array_column( $options, 'value' );
                if ( in_array( (string) $value, $allowed, true ) ) {
                    return (string) $value;
                }
                return (string) ( $field['default'] ?? '' );

            case 'password':
                // Preserve the raw secret. sanitize_text_field() strips/encodes
                // characters (a lone "<", collapses whitespace runs, etc.) and
                // would corrupt valid passwords. Strip only control characters
                // (incl. a stray CR/LF a paste may carry); keep everything else.
                return preg_replace( '/[\x00-\x1F\x7F]/', '', (string) $value );

            case 'image':
                // Media picker yields a URL; store it sanitized. Empty clears it.
                $value = trim( (string) $value );
                return $value === '' ? '' : esc_url_raw( $value );

            case 'image_set':
                // Per-device art direction (0.2.27): one image + crop anchor for
                // desktop / tablet / phone, stored as compact JSON. The client
                // sends the JSON string (or an object); a plain URL — the value a
                // field carried while it was still type 'image' — is accepted as
                // the desktop image so a schema can change type in place with no
                // migration. All three blank → '' (clears the field).
                // KI-127 (0.2.28): a plain URL takes the field's `focus_default`
                // (the theme's historical anchor) so a no-change save persists
                // what the storefront already renders instead of 'center'.
                return \TitanCart\Storefront\ImageSet::sanitize(
                    $value,
                    (string) ( $field['focus_default'] ?? '' )
                );

            case 'multiselect':
                // Checkbox-list control (e.g. the Prism Theme Options category
                // pickers). The client submits a CSV of ids (or an array). Store a
                // clean, de-duplicated CSV of positive ints, kept to the field's
                // allowed option ids when the schema supplies them (mirrors 'select').
                if ( is_string( $value ) ) {
                    $value = ( $value === '' ) ? [] : explode( ',', $value );
                }
                if ( ! is_array( $value ) ) {
                    $value = [];
                }
                $ids = array_values( array_unique( array_filter( array_map( 'intval', $value ) ) ) );
                $allowed = array_map( 'intval', array_column( $field['options'] ?? [], 'value' ) );
                if ( ! empty( $allowed ) ) {
                    $ids = array_values( array_intersect( $ids, $allowed ) );
                }
                return implode( ',', $ids );

            case 'textarea':
                // Preserve line breaks. sanitize_text_field() collapses every
                // newline into a space, which flattens multi-line option lists
                // (e.g. Referral Source, where each line is one dropdown choice)
                // into a single unusable line. sanitize_textarea_field keeps the
                // newlines while still stripping tags/invalid UTF-8.
                return sanitize_textarea_field( (string) $value );

            case 'text':
            default:
                return sanitize_text_field( (string) $value );
        }
    }

    // ══════════════════════════════════════════════════════════════════════════
    // SCHEMA REGISTRY
    // ══════════════════════════════════════════════════════════════════════════

    public static function get_schema( string $section ): ?array {
        $method = 'schema_' . $section;
        if ( method_exists( self::class, $method ) ) {
            return self::$method();
        }
        return null;
    }

    // ══════════════════════════════════════════════════════════════════════════
    // 1 — GENERAL
    // ══════════════════════════════════════════════════════════════════════════

    private static function schema_general(): array {
        return [
            'section' => 'general',
            'title'   => 'General',
            'groups'  => [
                [
                    'id'     => 'units',
                    'title'  => 'Units & Measurement',
                    'fields' => [
                        [ 'key' => 'weight_symbol',        'label' => 'Weight symbol',                       'type' => 'text',   'default' => 'lbs',   'hint' => 'Shown next to weight values (lbs, kg, oz).' ],
                        [ 'key' => 'weight_in_grams',      'label' => 'Grams per weight unit',               'type' => 'number', 'default' => '453.6', 'hint' => 'lbs = 453.6, kg = 1000.' ],
                        [ 'key' => 'dimensions_unit',      'label' => 'Dimensions unit',                     'type' => 'select', 'default' => 'in',
                          'options' => [['value'=>'in','label'=>'Inches (in)'],['value'=>'cm','label'=>'Centimeters (cm)'],['value'=>'mm','label'=>'Millimeters (mm)']] ],
                    ],
                ],
                [
                    'id'     => 'catalog',
                    'title'  => 'Catalog',
                    'fields' => [
                        [ 'key' => 'catalog_allow_compare',             'label' => 'Allow customers to compare products',                          'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'catalog_track_inventory',           'label' => 'Track inventory',                                               'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'catalog_allow_negative_inventory',  'label' => 'Allow negative amount in inventory',                            'type' => 'checkbox', 'default' => '0', 'hint' => 'When enabled, stock can go below zero.' ],
                        [ 'key' => 'catalog_enable_downloads',          'label' => 'Enable downloadable products',                                  'type' => 'checkbox', 'default' => '1',  'hint' => 'Master toggle for the Downloadable Products feature. When on, customers can purchase products marked as downloadable, receive signed download links by email, and access their files from the My Account → Downloads page. When off, downloadable products cannot be added to the cart — file management and existing keys remain intact so you can flip the switch back without data loss.' ],
                        [ 'key' => 'catalog_download_key_ttl',          'label' => 'Download link TTL (hours)',                                     'type' => 'number',   'default' => '24', 'hint' => 'How many hours each download link stays active after purchase. Per-product "Time-unlimited download" overrides this for specific products.' ],
                        [ 'key' => 'catalog_revoke_on_refund',          'label' => 'Revoke download links on refund',                               'type' => 'checkbox', 'default' => '1',  'hint' => 'When ON, refunding or cancelling an order automatically deactivates that order\'s download links. When OFF, refunded orders keep working links until they expire normally — useful if your refund policy says "keep what you downloaded."' ],
                        [ 'key' => 'catalog_low_stock_threshold',       'label' => 'Low stock notification threshold',                              'type' => 'number',   'default' => '0',  'hint' => 'Alert when stock falls to this number. 0 = disabled.' ],
                        [ 'key' => 'catalog_show_subcategory_products', 'label' => 'Show products from subcategories of the selected category',     'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'catalog_display_option_modifiers',  'label' => 'Display modifiers for product options',                         'type' => 'checkbox', 'default' => '1', 'hint' => 'Show +/- price next to option values.' ],
                        [ 'key' => 'catalog_show_out_of_stock',         'label' => 'Show out of stock products',                                    'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'catalog_show_filter_counts',        'label' => 'Show product counts on filter options',                         'type' => 'checkbox', 'default' => '0', 'hint' => 'Show the matching product count (e.g. "Blue (3)") next to each option in the storefront filter sidebar — colours, sizes, availability and other facets. Off by default for a cleaner look.' ],
                    ],
                ],
                [
                    'id'     => 'product_code',
                    'title'  => 'Product Code Generation',
                    'fields' => [
                        [ 'key' => 'product_code_autogenerate', 'label' => 'Auto-generate product code', 'type' => 'checkbox', 'default' => '0', 'hint' => 'When ON, any product saved without a Product Code gets one generated automatically from the source below. Products that already have a code are never changed.' ],
                        [ 'key' => 'product_code_source', 'label' => 'Generate from', 'type' => 'select', 'default' => 'id',
                          'options' => [['value'=>'name','label'=>'Product name'],['value'=>'category','label'=>'Main category'],['value'=>'id','label'=>'Product ID'],['value'=>'prefix','label'=>'Custom prefix only']],
                          'hint' => 'What the generated code is based on. "Custom prefix only" uses your prefix plus the product ID so every code stays unique.' ],
                        [ 'key' => 'product_code_prefix', 'label' => 'Code prefix', 'type' => 'text', 'default' => '', 'hint' => 'Optional. Prepended to every generated code (e.g. "TC" gives TC-BLUE-T-SHIRT). Leave blank for no prefix.' ],
                    ],
                ],
                [
                    'id'     => 'promotions',
                    'title'  => 'Promotions',
                    'fields' => [
                        [ 'key' => 'promo_single_coupon_only', 'label' => 'Block applying a coupon when another is already on the cart', 'type' => 'checkbox', 'default' => '1',
                          'hint' => 'When enabled, customers must remove the current coupon before applying a new one. When disabled, the new coupon replaces the existing one. Multiple simultaneous coupons are not supported.' ],
                    ],
                ],
                [
                    'id'     => 'customers',
                    'title'  => 'Customers',
                    'fields' => [
                        [ 'key' => 'customers_allow_multiple_profiles', 'label' => 'Allow multiple address profiles per account',   'type' => 'checkbox', 'default' => '0', 'hint' => 'A profile is a combination of shipping and billing address.' ],
                        [ 'key' => 'customers_allow_group_signup',      'label' => 'Allow customer to sign up for customer group',  'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'customers_admin_activate',          'label' => 'Administrator must activate new user accounts', 'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'customers_quick_registration',      'label' => 'Quick registration',                           'type' => 'checkbox', 'default' => '1', 'hint' => 'Require only contact information for registration.' ],
                        [ 'key' => 'customers_show_delete_account',     'label' => 'Show "Delete my account" button',              'type' => 'checkbox', 'default' => '0', 'hint' => 'Customers will see it on their profile page on the site.' ],
                    ],
                ],
                [
                    'id'     => 'search',
                    'title'  => 'Search Options',
                    'fields' => [
                        [ 'key' => 'search_in_pages',    'label' => 'Search also in pages',           'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'search_min_length',  'label' => 'Minimum search query length',    'type' => 'number',   'default' => '1', 'hint' => 'Minimum characters to trigger a search.' ],
                    ],
                ],
            ],
        ];
    }

    // ══════════════════════════════════════════════════════════════════════════
    // 2 — APPEARANCE
    // ══════════════════════════════════════════════════════════════════════════

    private static function schema_appearance(): array {
        return [
            'section' => 'appearance',
            'title'   => 'Appearance',
            'groups'  => [
                [
                    'id'     => 'admin',
                    'title'  => 'Administrator Settings',
                    'fields' => [
                        [ 'key' => 'admin_elements_per_page', 'label' => 'Elements per page', 'type' => 'number', 'default' => '50', 'hint' => 'Number of items shown per page in admin lists.' ],
                    ],
                ],
                [
                    'id'     => 'customer',
                    'title'  => 'Customer Settings',
                    'fields' => [
                        [ 'key' => 'orders_per_page',            'label' => 'Orders per page',                                      'type' => 'number',   'default' => '10' ],
                        [ 'key' => 'products_per_page',          'label' => 'Products per page',                                    'type' => 'number',   'default' => '12' ],
                        [ 'key' => 'elements_per_page',          'label' => 'Elements per page',                                    'type' => 'number',   'default' => '10' ],
                        [ 'key' => 'columns_in_product_list',    'label' => 'Number of columns in the product list',                'type' => 'number',   'default' => '4' ],
                        [ 'key' => 'estimate_taxes_default_addr','label' => 'Estimate taxes using default address on cart/checkout', 'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'show_tax_on_catalog',        'label' => 'Display prices with taxes on category/product pages',  'type' => 'checkbox', 'default' => '0', 'hint' => 'Only if the tax calculation method is based on unit price.' ],
                        [ 'key' => 'show_tax_on_cart',           'label' => 'Display prices with taxes on cart/checkout pages',     'type' => 'checkbox', 'default' => '0', 'hint' => 'Only if the tax calculation method is based on unit price.' ],
                        [ 'key' => 'show_number_available',      'label' => 'Show number of available products',                    'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'thumbnail_gallery',          'label' => 'Display mini thumbnail images as a gallery',           'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'use_qty_value_changer',      'label' => 'Use "Value changer" for the Quantity field',           'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'pagination_on_top',          'label' => 'Display the "Pagination" section on top of the list',  'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'pagination_type',            'label' => 'Pagination style',                                     'type' => 'select',   'default' => 'numbers',
                          'options' => [['value'=>'numbers','label'=>'Numbered pages'],['value'=>'infinite_scroll','label'=>'Infinite scroll'],['value'=>'load_more','label'=>'"Load more" button']] ],
                        [ 'key' => 'show_videos_before_images',  'label' => 'Show videos before images',                            'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'autoplay_videos',            'label' => 'Autoplay videos',                                      'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'product_detail_layout',      'label' => 'Product detailed page view',                           'type' => 'select',   'default' => 'default',
                          'options' => [['value'=>'default','label'=>'Default template'],['value'=>'wide','label'=>'Wide layout'],['value'=>'gallery','label'=>'Gallery view']] ],
                        [ 'key' => 'product_details_in_tabs',    'label' => 'Display product details in tabs',                      'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'show_track_orders',          'label' => 'Display track my orders section',                      'type' => 'checkbox', 'default' => '1' ],
                    ],
                ],
                [
                    'id'     => 'product_list_layout',
                    'title'  => 'Product List Layout Settings',
                    'fields' => [
                        [ 'key' => 'view_grid_enabled',           'label' => 'Available view: Grid',                          'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'view_list_enabled',           'label' => 'Available view: List without options',           'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'view_compact_enabled',        'label' => 'Available view: Compact list',                   'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'product_list_default_view',   'label' => 'Product list default view',                      'type' => 'select',   'default' => 'grid',
                          'options' => [['value'=>'grid','label'=>'Grid'],['value'=>'list','label'=>'List without options'],['value'=>'compact','label'=>'Compact list']] ],
                        [ 'key' => 'view_selected_per_category',  'label' => 'Use the selected view for current category or search page only', 'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'sort_newest',                 'label' => 'Available sorting: Newest items first',         'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'sort_oldest',                 'label' => 'Available sorting: Oldest items first',         'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'sort_alpha_az',               'label' => 'Available sorting: Alphabetically A → Z',       'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'sort_alpha_za',               'label' => 'Available sorting: Alphabetically Z → A',       'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'sort_price_low',              'label' => 'Available sorting: Price low to high',          'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'sort_price_high',             'label' => 'Available sorting: Price high to low',          'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'sort_popularity',             'label' => 'Available sorting: Sort by popularity',         'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'sort_bestselling',            'label' => 'Available sorting: Sort by bestselling',        'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'sort_discount_low',           'label' => 'Available sorting: Sort by discount low to high','type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'sort_discount_high',          'label' => 'Available sorting: Sort by discount high to low','type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'product_list_default_sort',   'label' => 'Product list default sorting',                   'type' => 'select',   'default' => 'alpha_az',
                          'options' => [['value'=>'newest','label'=>'Newest items first'],['value'=>'oldest','label'=>'Oldest items first'],['value'=>'alpha_az','label'=>'Alphabetically A to Z'],['value'=>'alpha_za','label'=>'Alphabetically Z to A'],['value'=>'price_low','label'=>'Price: low to high'],['value'=>'price_high','label'=>'Price: high to low'],['value'=>'popularity','label'=>'Popularity'],['value'=>'bestselling','label'=>'Bestselling']] ],
                        [ 'key' => 'enable_quick_view',           'label' => 'Enable quick view',                              'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'filters_default_expanded',    'label' => 'Filters start expanded by default',                'type' => 'checkbox', 'default' => '1' ],
                      
                    ],
                ],
                [
                    'id'     => 'common',
                    'title'  => 'Common Settings',
                    'fields' => [
                        [ 'key' => 'date_format',      'label' => 'Date format',         'type' => 'select', 'default' => 'mdy',
                          'hint' => 'Authoritative across the admin panel, storefront, and emails.',
                          'options' => [['value'=>'mdy','label'=>'MM/DD/YYYY (US)'],['value'=>'dmy','label'=>'DD/MM/YYYY (EU)'],['value'=>'ymd','label'=>'YYYY-MM-DD (ISO)']] ],
                        [ 'key' => 'time_format',      'label' => 'Time format',         'type' => 'select', 'default' => '24',
                          'hint' => 'Authoritative across the admin panel, storefront, and emails.',
                          'options' => [['value'=>'12','label'=>'12-hour (AM/PM)'],['value'=>'24','label'=>'24-hour']] ],
                        [ 'key' => 'timezone',         'label' => 'Time zone',           'type' => 'select', 'default' => 'America/New_York',
                          'hint' => 'Display timezone for all dates shown in the admin, storefront, and emails. Database storage is unaffected.',
                          'options' => [
                              ['value'=>'America/New_York','label'=>'(GMT-05:00) Eastern Time'],
                              ['value'=>'America/Chicago','label'=>'(GMT-06:00) Central Time'],
                              ['value'=>'America/Denver','label'=>'(GMT-07:00) Mountain Time'],
                              ['value'=>'America/Los_Angeles','label'=>'(GMT-08:00) Pacific Time'],
                              ['value'=>'America/Anchorage','label'=>'(GMT-09:00) Alaska'],
                              ['value'=>'Pacific/Honolulu','label'=>'(GMT-10:00) Hawaii'],
                              ['value'=>'Europe/London','label'=>'(GMT+00:00) London'],
                              ['value'=>'Europe/Paris','label'=>'(GMT+01:00) Paris / Berlin'],
                              ['value'=>'Europe/Athens','label'=>'(GMT+02:00) Athens / Helsinki'],
                              ['value'=>'Europe/Moscow','label'=>'(GMT+03:00) Moscow'],
                              ['value'=>'Asia/Dubai','label'=>'(GMT+04:00) Dubai'],
                              ['value'=>'Asia/Kolkata','label'=>'(GMT+05:30) India'],
                              ['value'=>'Asia/Shanghai','label'=>'(GMT+08:00) Beijing / Singapore'],
                              ['value'=>'Asia/Tokyo','label'=>'(GMT+09:00) Tokyo'],
                              ['value'=>'Australia/Sydney','label'=>'(GMT+11:00) Sydney'],
                          ] ],
                    ],
                ],
                [
                    'id'     => 'product_ribbons',
                    'title'  => 'Product Ribbons',
                    'fields' => [
                        [ 'key' => 'ribbon_position', 'label' => 'Ribbon position', 'type' => 'select', 'default' => 'left',
                          'hint' => 'Corner of the product image where ribbons stack. Applies to every ribbon type below.',
                          'options' => [['value'=>'left','label'=>'Top left'],['value'=>'right','label'=>'Top right']] ],
                        [ 'key' => 'ribbon_shape', 'label' => 'Ribbon shape', 'type' => 'select', 'default' => 'rounded',
                          'options' => [['value'=>'rounded','label'=>'Rounded (pill)'],['value'=>'square','label'=>'Square'],['value'=>'corner','label'=>'Angled corner']] ],
                    ],
                ],
                [
                    'id'     => 'product_ribbons_sale',
                    'title'  => 'Sale Ribbon',
                    'fields' => [
                        [ 'key' => 'ribbon_sale_enabled', 'label' => 'Show "Sale" ribbon', 'type' => 'checkbox', 'default' => '1', 'hint' => 'Shown on products whose list price is higher than their selling price.' ],
                        [ 'key' => 'ribbon_sale_label', 'label' => 'Sale label', 'type' => 'text', 'default' => '-{pct}%', 'hint' => 'Use {pct} for the discount percentage — e.g. "-{pct}%" or "{pct}% OFF". Plain text with no {pct} (e.g. "Sale") is shown verbatim.' ],
                        [ 'key' => 'ribbon_sale_color', 'label' => 'Sale colour', 'type' => 'color', 'default' => '#DC2626' ],
                    ],
                ],
                [
                    'id'     => 'product_ribbons_new',
                    'title'  => 'New Ribbon',
                    'fields' => [
                        [ 'key' => 'ribbon_new_enabled', 'label' => 'Show "New" ribbon', 'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'ribbon_new_label', 'label' => 'New label', 'type' => 'text', 'default' => 'New' ],
                        [ 'key' => 'ribbon_new_color', 'label' => 'New colour', 'type' => 'color', 'default' => '#2563EB' ],
                        [ 'key' => 'ribbon_new_days', 'label' => 'Days considered "new"', 'type' => 'number', 'default' => '14', 'hint' => 'Products created within this many days show the New ribbon.' ],
                    ],
                ],
                [
                    'id'     => 'product_ribbons_featured',
                    'title'  => 'Featured Ribbon',
                    'fields' => [
                        [ 'key' => 'ribbon_featured_enabled', 'label' => 'Show "Featured" ribbon', 'type' => 'checkbox', 'default' => '0', 'hint' => 'Shown on products flagged as Featured.' ],
                        [ 'key' => 'ribbon_featured_label', 'label' => 'Featured label', 'type' => 'text', 'default' => 'Featured' ],
                        [ 'key' => 'ribbon_featured_color', 'label' => 'Featured colour', 'type' => 'color', 'default' => '#D97706' ],
                    ],
                ],
                [
                    'id'     => 'product_ribbons_bestseller',
                    'title'  => 'Best Seller Ribbon',
                    'fields' => [
                        [ 'key' => 'ribbon_bestseller_enabled', 'label' => 'Show "Best Seller" ribbon', 'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'ribbon_bestseller_label', 'label' => 'Best Seller label', 'type' => 'text', 'default' => 'Best Seller' ],
                        [ 'key' => 'ribbon_bestseller_color', 'label' => 'Best Seller colour', 'type' => 'color', 'default' => '#16A34A' ],
                        [ 'key' => 'ribbon_bestseller_min', 'label' => 'Best Seller minimum sales', 'type' => 'number', 'default' => '25', 'hint' => 'Products with at least this many sales show the Best Seller ribbon.' ],
                    ],
                ],
                [
                    'id'     => 'product_ribbons_toprated',
                    'title'  => 'Top Rated Ribbon',
                    'fields' => [
                        [ 'key' => 'ribbon_toprated_enabled', 'label' => 'Show "Top Rated" ribbon', 'type' => 'checkbox', 'default' => '0', 'hint' => 'Shown on products that meet the rating + review-count thresholds below.' ],
                        [ 'key' => 'ribbon_toprated_label', 'label' => 'Top Rated label', 'type' => 'text', 'default' => 'Top Rated' ],
                        [ 'key' => 'ribbon_toprated_color', 'label' => 'Top Rated colour', 'type' => 'color', 'default' => '#7C3AED' ],
                        [ 'key' => 'ribbon_toprated_min_rating', 'label' => 'Top Rated minimum rating', 'type' => 'number', 'default' => '4.5', 'hint' => 'Average star rating required, from 0 to 5.' ],
                        [ 'key' => 'ribbon_toprated_min_reviews', 'label' => 'Top Rated minimum reviews', 'type' => 'number', 'default' => '5', 'hint' => 'Number of reviews required before the ribbon can show.' ],
                    ],
                ],
                [
                    'id'     => 'product_ribbons_instock',
                    'title'  => 'In Stock Ribbon',
                    'fields' => [
                        [ 'key' => 'ribbon_instock_enabled', 'label' => 'Show "In Stock" ribbon', 'type' => 'checkbox', 'default' => '0', 'hint' => 'In stock and above the Low Stock threshold. Only one availability ribbon shows per product — precedence is Preorder, then Out of Stock, then Low Stock, then In Stock.' ],
                        [ 'key' => 'ribbon_instock_label', 'label' => 'In Stock label', 'type' => 'text', 'default' => 'In Stock' ],
                        [ 'key' => 'ribbon_instock_color', 'label' => 'In Stock colour', 'type' => 'color', 'default' => '#16A34A' ],
                    ],
                ],
                [
                    'id'     => 'product_ribbons_lowstock',
                    'title'  => 'Low Stock Ribbon',
                    'fields' => [
                        [ 'key' => 'ribbon_lowstock_enabled', 'label' => 'Show "Low Stock" ribbon', 'type' => 'checkbox', 'default' => '0', 'hint' => 'Shown when a tracked product has stock at or below the threshold below.' ],
                        [ 'key' => 'ribbon_lowstock_label', 'label' => 'Low Stock label', 'type' => 'text', 'default' => 'Only {qty} left', 'hint' => 'Use {qty} to insert the remaining quantity, e.g. "Only {qty} left". Plain text with no {qty} is shown verbatim.' ],
                        [ 'key' => 'ribbon_lowstock_color', 'label' => 'Low Stock colour', 'type' => 'color', 'default' => '#D97706' ],
                        [ 'key' => 'ribbon_lowstock_max', 'label' => 'Low Stock threshold', 'type' => 'number', 'default' => '5', 'hint' => 'Show the Low Stock ribbon when the remaining quantity is at or below this number.' ],
                    ],
                ],
                [
                    'id'     => 'product_ribbons_outofstock',
                    'title'  => 'Out of Stock Ribbon',
                    'fields' => [
                        [ 'key' => 'ribbon_outofstock_enabled', 'label' => 'Show "Out of Stock" ribbon', 'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'ribbon_outofstock_label', 'label' => 'Out of Stock label', 'type' => 'text', 'default' => 'Sold Out' ],
                        [ 'key' => 'ribbon_outofstock_color', 'label' => 'Out of Stock colour', 'type' => 'color', 'default' => '#6B7280' ],
                    ],
                ],
                [
                    'id'     => 'product_ribbons_preorder',
                    'title'  => 'Preorder Ribbon',
                    'fields' => [
                        [ 'key' => 'ribbon_preorder_enabled', 'label' => 'Show "Preorder" ribbon', 'type' => 'checkbox', 'default' => '0', 'hint' => 'Shown on out-of-stock products that allow buy-in-advance (Pre-order).' ],
                        [ 'key' => 'ribbon_preorder_label', 'label' => 'Preorder label', 'type' => 'text', 'default' => 'Preorder' ],
                        [ 'key' => 'ribbon_preorder_color', 'label' => 'Preorder colour', 'type' => 'color', 'default' => '#2563EB' ],
                    ],
                ],
            ],
        ];
    }

    // ══════════════════════════════════════════════════════════════════════════
    // 2b — THEME OPTIONS (storefront preset + per-preset design controls)
    //
    // Engine-agnostic: the active TitanCart theme advertises the selectable
    // presets (filter `titancart_theme_options_presets`) and the per-preset
    // control groups (filter `titancart_theme_options_schema`). With no theme
    // registering anything, only the preset dropdown (Default) + accent show.
    // The SPA renders the theme-provided groups as collapsible sections and opens
    // the one whose `preset` key matches the chosen active preset.
    // ══════════════════════════════════════════════════════════════════════════

    private static function schema_theme_options(): array {
        $preset_options = apply_filters( 'titancart_theme_options_presets', [
            [ 'value' => 'default', 'label' => 'Default' ],
        ] );

        // S472: the theme decides which preset a fresh install lands on (Prism →
        // 'apparel'; Indigo is dev-only until finished). Must be one of the
        // advertised options, else fall back to the first option so the select
        // never shows a value the picker can't display.
        $default_preset = sanitize_key( (string) apply_filters( 'titancart_theme_options_default_preset', 'default' ) );
        $option_values  = array_map( static fn( $o ) => (string) ( $o['value'] ?? '' ), (array) $preset_options );
        if ( ! in_array( $default_preset, $option_values, true ) ) {
            $default_preset = (string) ( $option_values[0] ?? 'default' );
        }

        $preset_groups = apply_filters( 'titancart_theme_options_schema', [] );
        if ( ! is_array( $preset_groups ) ) {
            $preset_groups = [];
        }

        $groups = [
            [
                'id'     => 'active',
                'title'  => 'Active Theme',
                // KI-113 N+2 (S474): the active theme may append controls to THIS
                // panel (not just per-preset groups) — Prism uses it for its free
                // update-channel licence key. Same producer→consumer contract as
                // the schema filter: well-formed field arrays only.
                'fields' => array_values( array_filter( (array) apply_filters( 'titancart_theme_options_active_fields', [
                    [ 'key' => 'active_preset', 'label' => 'Active preset', 'type' => 'select', 'default' => $default_preset,
                      'hint' => 'The storefront design preset applied by the active theme. Selecting one opens its settings below.',
                      'options' => $preset_options ],
                    [ 'key' => 'accent_override', 'label' => 'Accent color override', 'type' => 'text', 'default' => '',
                      'hint' => 'Optional. A hex color such as #2563EB that overrides the preset brand/accent. Leave blank to use the preset default.' ],
                ] ), static fn( $f ) => is_array( $f ) && ! empty( $f['key'] ) ) ),
            ],
        ];

        // Pass through theme-provided per-preset groups (only well-formed ones).
        foreach ( $preset_groups as $g ) {
            if ( is_array( $g ) && ! empty( $g['fields'] ) ) {
                $groups[] = $g;
            }
        }

        return [
            'section' => 'theme_options',
            'title'   => 'Theme Options',
            'groups'  => $groups,
        ];
    }

    // ══════════════════════════════════════════════════════════════════════════
    // 3 — COMPANY
    // ══════════════════════════════════════════════════════════════════════════

    private static function schema_company(): array {
        return [
            'section' => 'company',
            'title'   => 'Company',
            'groups'  => [
                [
                    'id'     => 'info',
                    'title'  => 'Company Information',
                    'fields' => [
                        [ 'key' => 'company_name',     'label' => 'Company name',           'type' => 'text', 'default' => '' ],
                        [ 'key' => 'company_address',  'label' => 'Company address',        'type' => 'text', 'default' => '', 'hint' => 'Together with city, state, zip, and country, renders as the address paragraph in transactional email footers (order confirmations, RMA notifications, gift card delivery, subscription emails).' ],
                        [ 'key' => 'company_city',     'label' => 'Company city',           'type' => 'text', 'default' => '', 'hint' => 'Part of the address paragraph in email footers.' ],
                        [ 'key' => 'company_country',  'label' => 'Company country',        'type' => 'text', 'default' => 'United States', 'hint' => 'Part of the address paragraph in email footers.' ],
                        [ 'key' => 'company_state',    'label' => 'Company state',          'type' => 'text', 'default' => '', 'hint' => 'Part of the address paragraph in email footers.' ],
                        [ 'key' => 'company_zip',      'label' => 'Company zip code',       'type' => 'text', 'default' => '', 'hint' => 'Part of the address paragraph in email footers.' ],
                        [ 'key' => 'company_phone',    'label' => 'Company phone',          'type' => 'text', 'default' => '' ],
                        [ 'key' => 'company_phone_2',  'label' => 'Company phone 2',        'type' => 'text', 'default' => '' ],
                        [ 'key' => 'company_website',  'label' => 'Company website',        'type' => 'text', 'default' => '' ],
                        [ 'key' => 'company_year',     'label' => 'Year the store started its operation', 'type' => 'text', 'default' => '' ],
                    ],
                ],
                [
                    'id'     => 'emails',
                    'title'  => 'Department E-mail Addresses',
                    'fields' => [
                        [ 'key' => 'email_user_dept',      'label' => 'User department e-mail address (coming soon)',          'type' => 'text', 'default' => '', 'hint' => 'When wired, account-related emails (registration, password reset, profile change confirmations) will be sent from this address. V1 uses the Order department address for all transactional email; per-department routing ships post-launch.' ],
                        [ 'key' => 'email_site_admin',     'label' => 'Site administrator e-mail address (coming soon)',       'type' => 'text', 'default' => '', 'hint' => 'When wired, admin-only notifications (new order alerts, low-stock warnings, error reports) will BCC this address. V1 uses the Order department address for all admin notifications; per-department routing ships post-launch.' ],
                        [ 'key' => 'email_order_dept',     'label' => 'Order department e-mail address',         'type' => 'text', 'default' => '', 'hint' => 'Used as the From address for all transactional emails (orders, RMA, gift cards, subscriptions). Falls back to the WordPress admin email when blank.' ],
                        [ 'key' => 'email_support_dept',   'label' => 'Help/Support department e-mail address (coming soon)',  'type' => 'text', 'default' => '', 'hint' => 'When wired, this address surfaces in customer-facing "Contact Support" UI (footer links, contact forms, RMA correspondence). V1 has no support-contact surface; per-department routing ships post-launch.' ],
                        [ 'key' => 'email_newsletter_reply','label' => 'Reply-To newsletter e-mail address (coming soon)',     'type' => 'text', 'default' => '', 'hint' => 'When wired, newsletter campaign emails will set Reply-To to this address so customer replies don\'t go to the order inbox. V1 newsletters inherit the WordPress default Reply-To; per-department routing ships post-launch.' ],
                    ],
                ],
            ],
        ];
    }

    // ══════════════════════════════════════════════════════════════════════════
    // 4 — CHECKOUT
    // ══════════════════════════════════════════════════════════════════════════

    private static function schema_checkout(): array {
        return [
            'section' => 'checkout',
            'title'   => 'Checkout',
            'groups'  => [
                [
                    'id'     => 'checkout_flow',
                    'title'  => 'Checkout Flow',
                    'fields' => [
                        [ 'key' => 'disable_anonymous_checkout',  'label' => 'Disable anonymous checkout',                           'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'offer_guest_account',         'label' => 'Offer guest to create an account after successful order','type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'initial_order_id',            'label' => 'Initial order ID value',                                'type' => 'number',   'default' => '',  'hint' => 'Use a number between 1 and 10000000 or leave empty for auto-filling.' ],
                        [ 'key' => 'order_number_prefix',         'label' => 'Order number prefix',                                   'type' => 'text',     'default' => 'TC', 'hint' => 'Prefix for new order numbers, e.g. TC gives TC-20260628-0001. Letters, numbers and hyphens only; leave empty for date-only numbers. Only affects orders placed from now on.' ],
                        [ 'key' => 'zero_price_action',           'label' => 'Zero price action',                                     'type' => 'select',   'default' => 'deny',
                          'options' => [['value'=>'deny','label'=>'Do not allow customers to add to cart'],['value'=>'allow','label'=>'Allow customers to add to cart'],['value'=>'ask','label'=>'Ask customer to enter the price']] ],
                        [ 'key' => 'min_qty_per_product',         'label' => 'Minimum quantity to buy per product',                    'type' => 'number',   'default' => '0', 'hint' => 'Sell no less than X units of a product in an order. 0 = no minimum.' ],
                        [ 'key' => 'max_qty_per_product',         'label' => 'Maximum quantity to buy per product',                    'type' => 'number',   'default' => '0', 'hint' => 'Sell no more than X units of a product in an order. 0 = no maximum.' ],
                        [ 'key' => 'qty_step',                    'label' => 'Quantity step',                                          'type' => 'number',   'default' => '0', 'hint' => 'Increase or decrease the quantity only by X. 0 = step of 1.' ],
                        [ 'key' => 'number_available_quantities', 'label' => 'Number of available quantities',                         'type' => 'number',   'default' => '0', 'hint' => 'Replace quantity selector with a dropdown list with X variants. 0 = standard input.' ],
                        [ 'key' => 'tax_calculation_basis',       'label' => 'Tax calculation method based on',                        'type' => 'select',   'default' => 'subtotal',
                          'options' => [['value'=>'subtotal','label'=>'Subtotal'],['value'=>'unit_price','label'=>'Unit price']] ],
                        [ 'key' => 'min_order_amount',            'label' => 'Minimum order amount',                                   'type' => 'number',   'default' => '0', 'hint' => 'Set 0 for no minimum.' ],
                        [ 'key' => 'allow_shopping_unlogged',     'label' => 'Allow shopping for unlogged customers',                  'type' => 'select',   'default' => 'allow',
                          'options' => [['value'=>'allow','label'=>'Allow'],['value'=>'hide_price','label'=>'Hide price and Add to cart button'],['value'=>'deny','label'=>'Deny']] ],
                        [ 'key' => 'checkout_only_from_cart',     'label' => 'Allow checkout only from cart',                           'type' => 'checkbox', 'default' => '0', 'hint' => 'Show the Checkout button only on the Cart Content page.' ],
                        [ 'key' => 'estimate_shipping_on_cart',   'label' => 'Estimate shipping cost on cart page',                     'type' => 'checkbox', 'default' => '1' ],
                    ],
                ],
                [
                    'id'     => 'address_settings',
                    'title'  => 'Address Settings',
                    'fields' => [
                        [ 'key' => 'ship_to_billing_only',         'label' => 'Ship to billing address only',                     'type' => 'checkbox', 'default' => '0', 'hint' => 'When enabled, the shipping address form is hidden at checkout. All orders ship to the billing address. Useful for digital-only stores or stores that require billing = shipping.' ],
                        [ 'key' => 'same_as_billing_default',      'label' => '"Same as billing" checked by default',             'type' => 'checkbox', 'default' => '1', 'hint' => 'Pre-checks the "Same as billing address" checkbox at checkout. Customers can still uncheck it (unless ship-to-billing-only is on).' ],
                        [ 'key' => 'address_autocomplete',         'label' => 'Enable address autocomplete',                        'type' => 'checkbox', 'default' => '0', 'hint' => 'Adds Google Places address suggestions to all checkout and customer address forms. Requires a Google Places API key configured below. When the toggle is on but the key is blank, the feature stays off.' ],
                        [ 'key' => 'address_autocomplete_api_key', 'label' => 'Google Places API key',                              'type' => 'password', 'default' => '', 'hint' => 'Get one at console.cloud.google.com → enable the "Places API (New)" → APIs & Services → Credentials → Create Credentials → API key. Recommended: restrict the key to your domain (HTTP referrers) so it can\'t be used by anyone else. The key is only loaded on checkout and account pages, never on the storefront.' ],
                    ],
                ],
                [
                    'id'     => 'default_location',
                    'title'  => 'Default Customer Location',
                    'fields' => [
                        [ 'key' => 'default_address',  'label' => 'Default address',  'type' => 'text', 'default' => '',              'hint' => 'Pre-fills the checkout address form for guests and logged-in users without a saved address. Useful for stores that primarily serve customers in one country/region — saves them a few clicks. Leave blank to disable. Country must be set for any of these to apply.' ],
                        [ 'key' => 'default_zipcode',  'label' => 'Default zipcode',  'type' => 'text', 'default' => '' ],
                        [ 'key' => 'default_city',     'label' => 'Default city',     'type' => 'text', 'default' => '' ],
                        [ 'key' => 'default_country',  'label' => 'Default country',  'type' => 'text', 'default' => 'United States', 'hint' => 'Required for any default-location pre-fill to apply. If empty, none of the default fields above/below are used.' ],
                        [ 'key' => 'default_state',    'label' => 'Default state',    'type' => 'text', 'default' => '' ],
                        [ 'key' => 'default_phone',    'label' => 'Default phone',    'type' => 'text', 'default' => '' ],
                    ],
                ],
                [
                    'id'     => 'referral_source',
                    'title'  => 'Referral Source',
                    'fields' => [
                        [ 'key' => 'referral_enabled', 'label' => 'Ask "How did you hear about us?" at checkout', 'type' => 'checkbox', 'default' => '0', 'hint' => 'Adds an optional dropdown to the checkout page. The customer\'s answer is saved on the order and aggregated in Revenue Reports → Referrals.' ],
                        [ 'key' => 'referral_label',   'label' => 'Question label',                              'type' => 'text',     'default' => 'How did you hear about us?', 'hint' => 'The label shown above the dropdown.' ],
                        [ 'key' => 'referral_options', 'label' => 'Options',                                     'type' => 'textarea', 'default' => "Facebook\nInstagram\nGoogle\nFriend or family\nYouTube\nTikTok\nOther", 'hint' => 'One option per line. Include "Other" to show a "please specify" text box when the customer picks it.' ],
                    ],
                ],
            ],
        ];
    }

    // ══════════════════════════════════════════════════════════════════════════
    // 5 — EMAILS
    // ══════════════════════════════════════════════════════════════════════════

    private static function schema_emails(): array {
        return [
            'section' => 'emails',
            'title'   => 'E-mails',
            'groups'  => [
                [
                    'id'     => 'method',
                    'title'  => 'Sending Method',
                    'fields' => [
                        [ 'key' => 'email_method', 'label' => 'Method of sending e-mails', 'type' => 'select', 'default' => 'php_mail',
                          'options' => [['value'=>'php_mail','label'=>'via php mail function'],['value'=>'smtp','label'=>'via SMTP server'],['value'=>'sendmail','label'=>'via Sendmail']] ],
                    ],
                ],
                [
                    'id'     => 'smtp',
                    'title'  => 'SMTP Server Settings',
                    'fields' => [
                        [ 'key' => 'smtp_host',       'label' => 'SMTP host',                 'type' => 'text',     'default' => '',     'hint' => 'You can specify the port after the domain like this: example.com:465' ],
                        [ 'key' => 'smtp_username',    'label' => 'SMTP username',             'type' => 'text',     'default' => '' ],
                        [ 'key' => 'smtp_password',    'label' => 'SMTP password',             'type' => 'password', 'default' => '' ],
                        [ 'key' => 'smtp_encryption',  'label' => 'Use Encrypted Connection',  'type' => 'select',   'default' => 'none',
                          'options' => [['value'=>'none','label'=>'None'],['value'=>'ssl','label'=>'SSL'],['value'=>'tls','label'=>'TLS']] ],
                        [ 'key' => 'smtp_auth',        'label' => 'Use SMTP authentication',   'type' => 'checkbox', 'default' => '0' ],
                    ],
                ],
                [
                    'id'     => 'sendmail',
                    'title'  => 'Sendmail Settings',
                    'fields' => [
                        [ 'key' => 'sendmail_path', 'label' => 'Path to sendmail program', 'type' => 'text', 'default' => '/usr/sbin/sendmail' ],
                    ],
                ],
            ],
        ];
    }

    // ══════════════════════════════════════════════════════════════════════════
    // 6 — THUMBNAILS
    // ══════════════════════════════════════════════════════════════════════════

    private static function schema_thumbnails(): array {
        return [
            'section' => 'thumbnails',
            'title'   => 'Thumbnails',
            'groups'  => [
                [
                    'id'     => 'general_img',
                    'title'  => 'General',
                    'fields' => [
                        [ 'key' => 'image_max_file_size',    'label' => 'Image file size (MB)',                  'type' => 'number', 'default' => '',   'hint' => 'Maximum size of an uploaded image in megabytes. Leave empty or set to 0 to use the server settings (PHP upload_max_filesize). Enforced via ImageService::validate_upload_size() at every image upload endpoint before WordPress handles the upload.' ],
                        [ 'key' => 'jpeg_quality',           'label' => 'JPEG format quality (0-100)',          'type' => 'number', 'default' => '80', 'hint' => 'Compression quality for JPEG thumbnails. Lower = smaller files, faster loads, more visible compression. 80 is the WP default and a good balance. Applied via the jpeg_quality / wp_editor_set_quality filters across all thumbnail generation.' ],
                    ],
                ],
                [
                    'id'     => 'image_shape',
                    'title'  => 'Product Image Shape',
                    'fields' => [
                        [ 'key' => 'product_image_shape', 'label' => 'Product image shape', 'type' => 'select', 'default' => 'tall',
                          'hint' => 'Controls the aspect ratio of product images across the entire store — shop grid, homepage, cart, admin product list, and all other product cards. Tall (3:4) suits apparel; Square (1:1) suits most catalogs; Wide (4:3) suits landscape products (electronics, firearms, vehicles).',
                          'options' => [['value'=>'tall','label'=>'Tall (3:4)'],['value'=>'square','label'=>'Square (1:1)'],['value'=>'wide','label'=>'Wide (4:3)']] ],
                    ],
                ],
            ],
        ];
    }

    // ══════════════════════════════════════════════════════════════════════════
    // 7 — SECURITY
    // ══════════════════════════════════════════════════════════════════════════

    private static function schema_security(): array {
        return [
            'section' => 'security',
            'title'   => 'Security',
            'groups'  => [
                [
                    'id'     => 'ssl',
                    'title'  => 'SSL / Secure Connections',
                    'fields' => [
                        [ 'key' => 'ssl_storefront',    'label' => 'Enable secure connection for the storefront',         'type' => 'checkbox', 'default' => '1', 'hint' => 'SSL certificate must be installed on your server.' ],
                        [ 'key' => 'ssl_admin',         'label' => 'Enable secure connection in the administration panel', 'type' => 'checkbox', 'default' => '1', 'hint' => 'SSL certificate must be installed on your server.' ],
                    ],
                ],
                [
                    'id'     => 'passwords',
                    'title'  => 'Password Policy',
                    'fields' => [
                        [ 'key' => 'min_password_length',        'label' => 'Minimum password length',                                    'type' => 'number',   'default' => '5' ],
                        [ 'key' => 'password_require_mixed',     'label' => 'Password must contain both letters and numbers',              'type' => 'checkbox', 'default' => '0' ],
                        [ 'key' => 'force_admin_password_change','label' => 'Force administrators to change password on the first login',  'type' => 'checkbox', 'default' => '0',
                          'hint' => 'Only applies to admin accounts created after this is enabled. Existing admins are not prompted.' ],
                        [ 'key' => 'password_validity_days',     'label' => 'Password validity period in days (0 = unlimited)',            'type' => 'number',   'default' => '0',
                          'hint' => 'Counted from each user\'s last password change. Enforced on next login after expiry.' ],
                    ],
                ],
                [
                    'id'     => 'security_headers',
                    'title'  => 'Security Headers',
                    'fields' => [
                        [ 'key' => 'security_headers_enabled', 'label' => 'Send recommended security headers', 'type' => 'checkbox', 'default' => '0',
                          'hint' => 'Adds safe, content-independent HTTP response headers that harden the store against clickjacking and MIME-type sniffing. Off by default — enable to apply the recommended set below. If your host or another plugin already sends one of these headers, TitanCart leaves the existing value untouched.' ],
                        [ 'key' => 'security_header_nosniff', 'label' => 'X-Content-Type-Options: nosniff', 'type' => 'checkbox', 'default' => '1',
                          'hint' => 'Stops browsers from second-guessing (MIME-sniffing) a response\'s declared content type.' ],
                        [ 'key' => 'security_header_frame', 'label' => 'X-Frame-Options: SAMEORIGIN', 'type' => 'checkbox', 'default' => '1',
                          'hint' => 'Blocks other sites from embedding your store in a frame (anti-clickjacking). Turn off only if you intentionally embed your storefront in an iframe on another domain.' ],
                        [ 'key' => 'security_header_referrer', 'label' => 'Referrer-Policy: strict-origin-when-cross-origin', 'type' => 'checkbox', 'default' => '1',
                          'hint' => 'Sends the full referring URL to same-origin requests but only the bare origin to other sites.' ],
                        [ 'key' => 'security_headers_scope', 'label' => 'Apply headers to', 'type' => 'select', 'default' => 'both',
                          'options' => [
                              [ 'value' => 'both',       'label' => 'Storefront and admin' ],
                              [ 'value' => 'storefront', 'label' => 'Storefront only' ],
                              [ 'value' => 'admin',      'label' => 'Admin only' ],
                          ],
                          'hint' => 'Which areas receive the headers. The admin panel benefits from frame protection regardless of this choice.' ],
                    ],
                ],
            ],
        ];
    }

    // ══════════════════════════════════════════════════════════════════════════
    // 8 — LOGGING
    // ══════════════════════════════════════════════════════════════════════════

    private static function schema_logging(): array {
        return [
            'section' => 'logging',
            'title'   => 'Logging',
            'groups'  => [
                [
                    'id'     => 'log_global',
                    'title'  => 'Global Settings',
                    'fields' => [
                        [ 'key' => 'log_min_level', 'label' => 'Minimum log level', 'type' => 'select', 'default' => 'info',
                          'hint' => 'Only events at this level or higher will be recorded. Errors and critical are always logged regardless.',
                          'options' => [
                              [ 'value' => 'debug',    'label' => 'Debug — everything (verbose)' ],
                              [ 'value' => 'info',     'label' => 'Info — standard activity (default)' ],
                              [ 'value' => 'notice',   'label' => 'Notice — notable events only' ],
                              [ 'value' => 'warning',  'label' => 'Warning — potential issues' ],
                              [ 'value' => 'error',    'label' => 'Error — failures only' ],
                              [ 'value' => 'critical', 'label' => 'Critical — severe failures only' ],
                          ],
                        ],
                        [ 'key' => 'log_retention_days', 'label' => 'Days before a log entry is removed', 'type' => 'number', 'default' => '90', 'hint' => "Enter 0 if you don't want any automatic removal of old logs." ],
                        [ 'key' => 'log_backtrace_on_errors', 'label' => 'Include backtrace on errors', 'type' => 'checkbox', 'default' => '1', 'hint' => 'Automatically capture the PHP call stack for error and critical level entries. Useful for debugging.' ],
                        [ 'key' => 'log_rich_context', 'label' => 'Include detailed context data', 'type' => 'checkbox', 'default' => '1', 'hint' => 'Store extra details (changed fields, before/after values, request data) with each log entry. Disable to reduce database size.' ],
                        [ 'key' => 'log_php_errors', 'label' => 'Capture PHP errors', 'type' => 'checkbox', 'default' => '1', 'hint' => 'Automatically log PHP warnings, notices, and fatal errors from TitanCart code into System Logs.' ],
                    ],
                ],
                [
                    'id'     => 'log_orders',
                    'title'  => 'Orders',
                    'fields' => [
                        [ 'key' => 'log_orders_create',  'label' => 'Order placed',       'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_orders_change',  'label' => 'Status changes',     'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_orders_update',  'label' => 'Order edits',        'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_orders_delete',  'label' => 'Order deleted',      'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_orders_refund',  'label' => 'Refunds',            'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_orders_note',    'label' => 'Notes added',        'type' => 'checkbox', 'default' => '0', 'hint' => 'Can be noisy on high-volume stores.' ],
                    ],
                ],
                [
                    'id'     => 'log_payments',
                    'title'  => 'Payments',
                    'fields' => [
                        [ 'key' => 'log_payments_success',  'label' => 'Successful payments',  'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_payments_failed',   'label' => 'Failed payments',      'type' => 'checkbox', 'default' => '1', 'hint' => 'Always logged at error level.' ],
                        [ 'key' => 'log_payments_refund',   'label' => 'Refund processed',     'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_payments_webhook',  'label' => 'Webhook received',     'type' => 'checkbox', 'default' => '1' ],
                    ],
                ],
                [
                    'id'     => 'log_checkout',
                    'title'  => 'Checkout',
                    'fields' => [
                        [ 'key' => 'log_checkout_started',     'label' => 'Checkout started',   'type' => 'checkbox', 'default' => '0', 'hint' => 'Can be very noisy — logs every checkout page load.' ],
                        [ 'key' => 'log_checkout_failed',      'label' => 'Checkout failures',  'type' => 'checkbox', 'default' => '1', 'hint' => 'Validation errors, stock issues, payment declines during checkout.' ],
                        [ 'key' => 'log_checkout_cart_errors',  'label' => 'Cart errors',       'type' => 'checkbox', 'default' => '1', 'hint' => 'Out-of-stock when adding to cart, invalid options, etc.' ],
                    ],
                ],
                [
                    'id'     => 'log_users',
                    'title'  => 'Users',
                    'fields' => [
                        [ 'key' => 'log_users_create',         'label' => 'Registration',     'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_users_delete',         'label' => 'Deleted',          'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_users_update',         'label' => 'Profile updated',  'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_users_session',        'label' => 'Login / logout',   'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_users_failed_logins',  'label' => 'Failed logins',    'type' => 'checkbox', 'default' => '1' ],
                    ],
                ],
                [
                    'id'     => 'log_products',
                    'title'  => 'Products',
                    'fields' => [
                        [ 'key' => 'log_products_create',     'label' => 'Created',     'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_products_delete',     'label' => 'Deleted',     'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_products_update',     'label' => 'Updated',     'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_products_low_stock',  'label' => 'Low stock warnings',  'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_products_import',     'label' => 'Import activity',     'type' => 'checkbox', 'default' => '1' ],
                    ],
                ],
                [
                    'id'     => 'log_categories',
                    'title'  => 'Categories',
                    'fields' => [
                        [ 'key' => 'log_categories_create',  'label' => 'Created',  'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_categories_delete',  'label' => 'Deleted',  'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_categories_update',  'label' => 'Updated',  'type' => 'checkbox', 'default' => '1' ],
                    ],
                ],
                [
                    'id'     => 'log_security',
                    'title'  => 'Security',
                    'fields' => [
                        [ 'key' => 'log_security_blocked',         'label' => 'Blocked IPs',           'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_security_permission',      'label' => 'Permission denied',     'type' => 'checkbox', 'default' => '1', 'hint' => 'Unauthorized API access attempts.' ],
                        [ 'key' => 'log_security_rate_limited',    'label' => 'Rate-limited requests',  'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_security_nonce_failed',    'label' => 'Nonce / CSRF failures',  'type' => 'checkbox', 'default' => '1' ],
                    ],
                ],
                [
                    'id'     => 'log_database',
                    'title'  => 'Database',
                    'fields' => [
                        [ 'key' => 'log_database_restore',   'label' => 'Restore',   'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_database_backup',    'label' => 'Backup',    'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_database_optimize',  'label' => 'Optimize',  'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_database_errors',    'label' => 'Query errors',  'type' => 'checkbox', 'default' => '1' ],
                    ],
                ],
                [
                    'id'     => 'log_requests',
                    'title'  => 'External Requests',
                    'fields' => [
                        [ 'key' => 'log_requests_http',          'label' => 'Outgoing API calls',       'type' => 'checkbox', 'default' => '1', 'hint' => 'Stripe, PayPal, shipping carrier API calls.' ],
                        [ 'key' => 'log_requests_shipping_err',  'label' => 'Shipping rate errors',     'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_requests_slow',          'label' => 'Slow requests (>5s)',      'type' => 'checkbox', 'default' => '0', 'hint' => 'Log external API calls that take more than 5 seconds.' ],
                    ],
                ],
                [
                    'id'     => 'log_general',
                    'title'  => 'General',
                    'fields' => [
                        [ 'key' => 'log_general_runtime',     'label' => 'Runtime events',       'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_general_deprecated',  'label' => 'Deprecated features',  'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_general_settings',    'label' => 'Settings changes',     'type' => 'checkbox', 'default' => '1' ],
                        [ 'key' => 'log_general_extensions',  'label' => 'Extension toggles',    'type' => 'checkbox', 'default' => '1' ],
                    ],
                ],
            ],
        ];
    }
}
