<?php

declare(strict_types=1);

namespace CliqThemes\StoreLocator\Data\Definition;

defined('ABSPATH') || exit;

use CliqThemes\ORM\Form\FieldStorageMap;
use CliqThemes\ORM\Form\SchemaPersistenceCoordinator;
use CliqThemes\StoreLocator\I18n\ContentTranslator;
use CliqThemes\StoreLocator\Models\Store;
use CliqThemes\StoreLocator\Repository\CustomFieldsRepository;
use CliqThemes\StoreLocator\Support\CountryOptions;
use CliqThemes\StoreLocator\Support\SettingsService;
use CliqThemes\UI\Data\DataProviderRegistry;

/**
 * Provider definition for the Store entity (schema-aware: meta_table fields, location, pivots, and admin-defined custom fields injected into their chosen tab/section).
 */
final class StoreProviderDefinition implements ProviderDefinition
{
    public function key(): string
    {
        return 'stores';
    }

    /**
     * @return array<string, mixed>
     */
    public function register(DataProviderRegistry $registry, ProviderContext $context): array
    {
        $brandOptions = $context->brandOptions();
        $categoryOptions = $context->categoryOptions();
        $storeTabSections = $context->storeTabSections();
        $slugSources = SettingsService::get('store_detail_slug_fields', ['city', 'name']);
        $slugSources = is_array($slugSources) ? $slugSources : ['city', 'name'];
        $slugSources = array_values(array_unique(array_filter(
            $slugSources,
            static fn (mixed $source): bool =>
                is_string($source)
                && in_array($source, ['name', 'city', 'state', 'postal_code', 'country'], true)
        )));
        $slugSources = $slugSources !== [] ? $slugSources : ['city', 'name'];

        global $wpdb;

        $storeMetaTable = $wpdb->prefix . 'cliq_sl_store_meta';
        $brandStoreTable = $wpdb->prefix . 'cliq_sl_brand_store';
        $storeLocationsTable = $wpdb->prefix . 'cliq_sl_store_locations';
        $storeCategoryTable = $wpdb->prefix . 'cliq_sl_store_category';

        // Single source of truth for the 5 store statuses — reused by the
        // form field's dropdown options below AND the grid column's badge
        // transformer (setColumnTransformer('status', ...) further down), so
        // the two label lists can never drift apart.
        $statusOptions = [
            ['value' => 'active', 'label' => __('Active', 'cliq-store-locator')],
            ['value' => 'pending', 'label' => __('Pending review', 'cliq-store-locator')],
            ['value' => 'relocated', 'label' => __('Relocated', 'cliq-store-locator')],
            ['value' => 'temporarily_closed', 'label' => __('Temporarily closed', 'cliq-store-locator')],
            ['value' => 'permanently_closed', 'label' => __('Permanently closed', 'cliq-store-locator')],
        ];

        $storeForm = [
            'primaryKey' => 'id',
            'label' => __('Store', 'cliq-store-locator'),
            'submitLabels' => [
                'create' => __('Add Store', 'cliq-store-locator'),
                'update' => __('Update Store', 'cliq-store-locator'),
            ],
            'sections' => [
                [
                    'id' => 'store-details',
                    'label' => __('Store Details', 'cliq-store-locator'),
                    'render' => 'regular',
                    'groups' => [
                        [
                            'id' => 'identity',
                            'label' => __('Identity', 'cliq-store-locator'),
                            'columns' => 2,
                            'fields' => [
                                [
                                    'name' => 'name',
                                    'label' => __('Name', 'cliq-store-locator'),
                                    'type' => 'text',
                                    'required' => true,
                                    'placeholder' => __('Flagship Store', 'cliq-store-locator'),
                                ],
                                [
                                    'name' => 'slug',
                                    'label' => __('Slug', 'cliq-store-locator'),
                                    'type' => 'text',
                                    'required' => true,
                                    'slugFrom' => $slugSources,
                                    'help' => __('Auto-filled from the configured core fields; edit to override. Existing store URLs are never changed automatically.', 'cliq-store-locator'),
                                ],
                                [
                                    'name' => 'brand_id',
                                    'label' => __('Brand', 'cliq-store-locator'),
                                    'type' => 'select',
                                    'storage' => [
                                        'target' => 'pivot',
                                        'table' => $brandStoreTable,
                                        'foreign_key' => 'store_id',
                                        'related_key' => 'brand_id',
                                    ],
                                    'options' => $brandOptions,
                                    'placeholder' => __('Select a brand', 'cliq-store-locator'),
                                ],
                                [
                                    'name' => 'status',
                                    'label' => __('Status', 'cliq-store-locator'),
                                    'type' => 'select',
                                    'default' => 'active',
                                    'options' => $statusOptions,
                                    'help' => __('Only Active stores appear on the public locator. New registrations arrive as Pending review.', 'cliq-store-locator'),
                                ],
                            ],
                        ],
                        [
                            'id' => 'overview',
                            'label' => __('Description', 'cliq-store-locator'),
                            'columns' => 1,
                            'fields' => [
                                [
                                    'name' => 'description',
                                    'label' => __('Description', 'cliq-store-locator'),
                                    'type' => 'wysiwyg',
                                    'help' => __('Rich description shown on the store detail layout.', 'cliq-store-locator'),
                                ],
                            ],
                        ],
                        [
                            'id' => 'classification',
                            'label' => __('Categories', 'cliq-store-locator'),
                            'columns' => 1,
                            'fields' => [
                                [
                                    'name' => 'categories',
                                    'label' => __('Categories', 'cliq-store-locator'),
                                    'type' => 'multiselect',
                                    'default' => [],
                                    'options' => $categoryOptions,
                                    'storage' => [
                                        'target' => 'pivot',
                                        'table' => $storeCategoryTable,
                                        'foreign_key' => 'store_id',
                                        'related_key' => 'category_id',
                                    ],
                                    'help' => __('Type to search and assign one or more categories. Category-specific custom fields appear in their own tab.', 'cliq-store-locator'),
                                ],
                            ],
                        ],
                        [
                            'id' => 'highlights',
                            'label' => __('Highlights', 'cliq-store-locator'),
                            'columns' => 2,
                            'fields' => [
                                [
                                    'name' => 'featured_store',
                                    'label' => __('Featured Store', 'cliq-store-locator'),
                                    'type' => 'toggle',
                                    'default' => false,
                                    'storage' => $context->storagePreset($storeMetaTable, 'featured_store'),
                                    'help' => __('Is it a featured store?', 'cliq-store-locator'),
                                ],
                                [
                                    'name' => 'hero_media',
                                    'label' => __('Hero Image', 'cliq-store-locator'),
                                    'type' => 'image',
                                    'default' => null,
                                    'storage' => $context->storagePreset($storeMetaTable, 'hero_media', ['cast' => 'media']),
                                    'mediaTitle' => __('Select a hero image', 'cliq-store-locator'),
                                    'mediaButtonLabel' => __('Use image', 'cliq-store-locator'),
                                    'mediaLibrary' => ['type' => 'image'],
                                ],
                                // Direct stores-table columns (no storage override) — power
                                // the builder's Rating element. Empty = the element renders
                                // nothing (no placeholder).
                                [
                                    'name' => 'rating',
                                    'label' => __('Rating', 'cliq-store-locator'),
                                    'type' => 'number',
                                    'min' => 0,
                                    'max' => 5,
                                    'step' => 0.1,
                                    'default' => null,
                                    'placeholder' => '4.6',
                                    'help' => __('Average rating 0–5. Leave empty to hide the Rating element.', 'cliq-store-locator'),
                                ],
                                [
                                    'name' => 'rating_count',
                                    'label' => __('Number of reviews', 'cliq-store-locator'),
                                    'type' => 'number',
                                    'min' => 0,
                                    'step' => 1,
                                    'default' => null,
                                    'help' => __('Optional — shown in parentheses next to the rating.', 'cliq-store-locator'),
                                ],
                            ],
                        ],
                    ],
                ],
                [
                    'id' => 'store-location',
                    'label' => __('Location', 'cliq-store-locator'),
                    'render' => 'regular',
                    'groups' => [
                        [
                            // Address fields first; the map below geocodes from them.
                            'id' => 'address',
                            'label' => __('Address', 'cliq-store-locator'),
                            'columns' => 2,
                            'fields' => [
                                [
                                    'name' => 'address_line1',
                                    'label' => __('Address line 1', 'cliq-store-locator'),
                                    'type' => 'text',
                                    'storage' => ['target' => 'location', 'table' => $storeLocationsTable, 'column' => 'address_line1'],
                                    'placeholder' => __('e.g. 10 Downing St', 'cliq-store-locator'),
                                ],
                                [
                                    'name' => 'address_line2',
                                    'label' => __('Address line 2', 'cliq-store-locator'),
                                    'type' => 'text',
                                    'storage' => ['target' => 'location', 'table' => $storeLocationsTable, 'column' => 'address_line2'],
                                ],
                                [
                                    'name' => 'city',
                                    'label' => __('City', 'cliq-store-locator'),
                                    'type' => 'text',
                                ],
                                [
                                    'name' => 'state',
                                    'label' => __('State / Region', 'cliq-store-locator'),
                                    'type' => 'text',
                                    'storage' => ['target' => 'location', 'table' => $storeLocationsTable, 'column' => 'state'],
                                ],
                                [
                                    'name' => 'postal_code',
                                    'label' => __('Postal code', 'cliq-store-locator'),
                                    'type' => 'text',
                                    'storage' => ['target' => 'location', 'table' => $storeLocationsTable, 'column' => 'postal_code'],
                                ],
                                [
                                    'name' => 'country',
                                    'label' => __('Country', 'cliq-store-locator'),
                                    'type' => 'select',
                                    'required' => true,
                                    'options' => CountryOptions::all(),
                                ],
                            ],
                        ],
                        [
                            'id' => 'geo',
                            'label' => __('Map & Coordinates', 'cliq-store-locator'),
                            'columns' => 1,
                            'fields' => [
                                [
                                    'name' => 'location',
                                    'label' => __('Pin Location', 'cliq-store-locator'),
                                    'type' => 'map',
                                    'default' => (object) [],
                                    'storage' => ['target' => 'location', 'table' => $storeLocationsTable],
                                    'defaultLocation' => ['lat' => 51.5074, 'lng' => -0.1278, 'zoom' => 12],
                                    // Match the editor map to the configured provider (Settings → Map).
                                    'provider' => (string) SettingsService::get('map_provider', 'leaflet'),
                                    'apiKey' => (string) SettingsService::get('google_maps_api_key', ''),
                                    'mapType' => (string) SettingsService::get('map_type', 'roadmap'),
                                    // Geocoding is generic in the framework: tell it which sibling
                                    // fields form the query, the endpoint, and which result component
                                    // fills which field (country-code casing handled by a JS hook).
                                    'geocode' => [
                                        'endpoint' => 'cliq-store-locator/v1/admin/geocode',
                                        'addressFields' => ['address_line1', 'address_line2', 'city', 'state', 'postal_code', 'country'],
                                        'fill' => [
                                            'city' => ['city', 'town', 'village', 'municipality'],
                                            'state' => ['state', 'region'],
                                            'postal_code' => ['postcode'],
                                            'country' => ['country_code'],
                                        ],
                                    ],
                                    'help' => __('Click "Locate on map" to find the address above, or click/drag the pin to set exact coordinates. This powers the radius search.', 'cliq-store-locator'),
                                ],
                            ],
                        ],
                    ],
                ],
                [
                    'id' => 'contact',
                    'label' => __('Contact', 'cliq-store-locator'),
                    'render' => 'regular',
                    'groups' => [
                        [
                            'id' => 'contact-details',
                            'label' => __('Contact Details', 'cliq-store-locator'),
                            'columns' => 2,
                            'fields' => [
                                ['name' => 'phone', 'label' => __('Phone', 'cliq-store-locator'), 'type' => 'tel'],
                                ['name' => 'email', 'label' => __('Email', 'cliq-store-locator'), 'type' => 'email'],
                                ['name' => 'website', 'label' => __('Website', 'cliq-store-locator'), 'type' => 'url'],
                            ],
                        ],
                        [
                            'id' => 'social',
                            'label' => __('Social Media', 'cliq-store-locator'),
                            'columns' => 2,
                            'fields' => [
                                [
                                    'name' => 'instagram_handle',
                                    'label' => __('Instagram', 'cliq-store-locator'),
                                    'type' => 'text',
                                    'storage' => $context->storagePreset($storeMetaTable, 'instagram_handle'),
                                    'placeholder' => __('@cliq_flagship', 'cliq-store-locator'),
                                ],
                                ['name' => 'facebook_url', 'label' => __('Facebook', 'cliq-store-locator'), 'type' => 'url', 'storage' => $context->storagePreset($storeMetaTable, 'facebook_url'), 'placeholder' => 'https://facebook.com/…'],
                                ['name' => 'x_url', 'label' => __('X (Twitter)', 'cliq-store-locator'), 'type' => 'url', 'storage' => $context->storagePreset($storeMetaTable, 'x_url'), 'placeholder' => 'https://x.com/…'],
                                ['name' => 'linkedin_url', 'label' => __('LinkedIn', 'cliq-store-locator'), 'type' => 'url', 'storage' => $context->storagePreset($storeMetaTable, 'linkedin_url'), 'placeholder' => 'https://linkedin.com/company/…'],
                                ['name' => 'youtube_url', 'label' => __('YouTube', 'cliq-store-locator'), 'type' => 'url', 'storage' => $context->storagePreset($storeMetaTable, 'youtube_url'), 'placeholder' => 'https://youtube.com/@…'],
                                ['name' => 'tiktok_url', 'label' => __('TikTok', 'cliq-store-locator'), 'type' => 'url', 'storage' => $context->storagePreset($storeMetaTable, 'tiktok_url'), 'placeholder' => 'https://tiktok.com/@…'],
                            ],
                        ],
                    ],
                ],
                [
                    'id' => 'store-operations',
                    'label' => __('Store Operations', 'cliq-store-locator'),
                    'render' => 'regular',
                    'groups' => [

                        [
                            'id' => 'operations',
                            'label' => __('Operations', 'cliq-store-locator'),
                            'columns' => 2,
                            'fields' => [
                                [
                                    'name' => 'manager_name',
                                    'label' => __('Store Manager', 'cliq-store-locator'),
                                    'type' => 'text',
                                    'default' => '',
                                    'storage' => $context->storagePreset($storeMetaTable, 'manager_name'),
                                    'placeholder' => __('Amelia Rose', 'cliq-store-locator'),
                                ],
                            ],
                        ],
                        [
                            // Full-width so each day renders as its own row in the list.
                            'id' => 'hours',
                            'label' => __('Opening Hours', 'cliq-store-locator'),
                            'columns' => 1,
                            'fields' => [
                                [
                                    'name' => 'opening_hours',
                                    'label' => __('Opening Hours', 'cliq-store-locator'),
                                    'type' => 'hours',
                                    'default' => (object) [],
                                    'storage' => $context->storagePreset($storeMetaTable, 'opening_hours', ['cast' => 'json']),
                                    'help' => __('Set open/close times per day. Add multiple intervals for split shifts.', 'cliq-store-locator'),
                                ],
                            ],
                        ],
                    ],
                ],
            ],
        ];


        // --- Admin-defined custom fields (dynamic) ---
        // Collect each store custom field with its admin-chosen placement
        // (target tab + section) and whether it is filterable. Categories now
        // live in the Store Details tab; custom fields default there too unless
        // they declare their own tab (custom fields can spawn tabs on the fly).
        $defaultTab = 'store-details';
        $placedFields = [];
        $customFilters = [];
        try {
            $customFieldsRepository = new CustomFieldsRepository();
            foreach ($customFieldsRepository->forEntity('store') as $customField) {
                // Skip incomplete definitions (no machine key) so a half-saved
                // custom field can never break the store form.
                if (empty($customField['field_key']) || empty($customField['type'])) {
                    continue;
                }

                $config = is_array($customField['config'] ?? null) ? $customField['config'] : [];
                $tab = (string) ($config['placement_tab'] ?? '') ?: $defaultTab;
                $sectionChoice = (string) ($config['placement_section__' . $tab] ?? '');

                $placedFields[] = [
                    'def' => $customFieldsRepository->toFormField($customField, $storeMetaTable),
                    'tab' => $tab,
                    'newTabLabel' => (string) ($config['placement_tab_label'] ?? ''),
                    'section' => $sectionChoice,
                    'newSection' => (string) ($customField['section'] ?? ''),
                ];

                if (! empty($config['is_filterable'])) {
                    $key = (string) $customField['field_key'];
                    $options = [];
                    foreach (($config['options'] ?? []) as $opt) {
                        $val = (string) ($opt['value'] ?? '');
                        if ($val !== '') {
                            $options[$val] = (string) ($opt['label'] ?? $val);
                        }
                    }
                    // Multi-value fields (checkboxes/multiselect) store an array, so
                    // their filter is a multi-select combobox (match ANY chosen value).
                    $isMulti = in_array((string) $customField['type'], ['checkboxes', 'multiselect'], true);
                    $customFilters[$key] = [
                        'label' => (string) $customField['label'],
                        'type' => $options !== [] ? ($isMulti ? 'multi-select' : 'select') : 'text',
                        'options' => $options,
                        // Values are added by the schema coordinator after the
                        // base Store query, so StoreProvider filters them after
                        // hydration rather than treating them as DB columns.
                        'source' => 'enriched',
                        'operator' => $options !== [] ? 'equals' : 'contains',
                    ];
                }
            }
        } catch (\Throwable $e) {
            $placedFields = [];
        }


        // Inject each custom field into its chosen tab + section, creating new
        // tabs/sections as needed.
        $injectField = function (array &$sections, string $tabId, string $tabLabel, string $groupId, string $groupLabel, array $def): void {
            foreach ($sections as &$section) {
                if (($section['id'] ?? '') !== $tabId) {
                    continue;
                }
                foreach ($section['groups'] as &$group) {
                    if (($group['id'] ?? '') === $groupId) {
                        $group['fields'][] = $def;
                        return;
                    }
                }
                unset($group);
                $section['groups'][] = ['id' => $groupId, 'label' => $groupLabel, 'columns' => 2, 'fields' => [$def]];
                return;
            }
            unset($section);

            // Target tab not found → create it (new tab).
            $sections[] = [
                'id' => $tabId,
                'label' => $tabLabel,
                'render' => 'regular',
                'groups' => [['id' => $groupId, 'label' => $groupLabel, 'columns' => 2, 'fields' => [$def]]],
            ];
        };

        foreach ($placedFields as $pf) {
            $tab = $pf['tab'];
            if ($tab === '__new' && trim($pf['newTabLabel']) !== '') {
                $tabId = 'cf-tab-' . sanitize_title($pf['newTabLabel']);
                $tabLabel = $pf['newTabLabel'];
                $heading = trim($pf['newSection']) !== '' ? $pf['newSection'] : __('Custom Fields', 'cliq-store-locator');
                $groupId = 'cf-' . sanitize_title($tabId . '-' . $heading);
                $injectField($storeForm['sections'], $tabId, $tabLabel, $groupId, $heading, $pf['def']);
                continue;
            }

            $tabLabel = $storeTabSections[$tab]['label'] ?? ucwords(str_replace('-', ' ', $tab));
            $section = $pf['section'];

            if ($section !== '' && $section !== '__new' && isset($storeTabSections[$tab]['groups'][$section])) {
                // Append into an existing section/group of the tab.
                $groupLabel = $storeTabSections[$tab]['groups'][$section];
                $injectField($storeForm['sections'], $tab, $tabLabel, $section, $groupLabel, $pf['def']);
            } else {
                // New section within the tab.
                $heading = trim($pf['newSection']) !== '' ? $pf['newSection'] : __('Custom Fields', 'cliq-store-locator');
                $groupId = 'cf-' . sanitize_title($tab . '-' . $heading);
                $injectField($storeForm['sections'], $tab, $tabLabel, $groupId, $heading, $pf['def']);
            }
        }

        /**
         * Filter the store form's sections before its flat field list is derived.
         *
         * @param array<int, array<string, mixed>> $sections
         * @param ProviderContext                  $context option sources
         */
        $storeForm['sections'] = apply_filters('cliq_sl_admin_store_form_sections', $storeForm['sections'], $context);

        $storeForm['fields'] = array_values(array_reduce(
            $storeForm['sections'],
            static function (array $carry, array $section): array {
                foreach ($section['groups'] ?? [] as $group) {
                    foreach ($group['fields'] ?? [] as $field) {
                        $carry[] = $field;
                    }
                }

                return $carry;
            },
            []
        ));

        $fieldByName = [];
        foreach ($storeForm['fields'] as $field) {
            if (! empty($field['name'])) {
                $fieldByName[(string) $field['name']] = $field;
            }
        }
        $customBulkFields = [];
        foreach ($placedFields as $placedField) {
            $definition = $placedField['def'] ?? null;
            if (is_array($definition) && ! empty($definition['name'])) {
                $customBulkFields[] = $definition;
            }
        }

        $storeForm['bulkActions'] = [
            [
                'id' => 'status',
                'label' => __('Update status', 'cliq-store-locator'),
                'field' => $fieldByName['status'] ?? [],
                'mode' => 'replace',
            ],
            [
                'id' => 'categories',
                'label' => __('Add categories', 'cliq-store-locator'),
                'field' => $fieldByName['categories'] ?? [],
                'mode' => 'append',
                'description' => __('The selected categories are added without removing categories already assigned to each store.', 'cliq-store-locator'),
            ],
            [
                'id' => 'custom_field',
                'label' => __('Update custom field', 'cliq-store-locator'),
                'kind' => 'custom-field',
                'fields' => $customBulkFields,
                'mode' => 'replace',
            ],
            [
                'id' => 'brand',
                'label' => __('Update brand', 'cliq-store-locator'),
                'field' => $fieldByName['brand_id'] ?? [],
                'mode' => 'replace',
            ],
        ];

        $storeColumns = [
            'name' => ['label' => __('Store', 'cliq-store-locator')],
            'status' => ['label' => __('Status', 'cliq-store-locator')],
            // Per-row translation status for the active language (rendered as a
            // badge by StoreProvider::overlay → langBadge).
            'lang' => ['label' => __('Language', 'cliq-store-locator')],
            'city' => ['label' => __('City', 'cliq-store-locator')],
            'country' => ['label' => __('Country', 'cliq-store-locator')],
            'manager_name' => ['label' => __('Manager', 'cliq-store-locator')],
            'brand_id' => ['label' => __('Brand', 'cliq-store-locator')],
            'hero_media_preview' => ['label' => __('Hero Image', 'cliq-store-locator')],
        ];

        $storeFilters = [
            // Free-text keyword search across the store's key text fields.
            'q' => [
                'label' => __('Search', 'cliq-store-locator'),
                'type' => 'text',
                'operator' => 'search',
                'fields' => ['name', 'city', 'slug'],
            ],
            'status' => [
                'label' => __('Status', 'cliq-store-locator'),
                'type' => 'select',
                'options' => [
                    'active' => __('Active', 'cliq-store-locator'),
                    'pending' => __('Pending review', 'cliq-store-locator'),
                    'relocated' => __('Relocated', 'cliq-store-locator'),
                    'temporarily_closed' => __('Temporarily closed', 'cliq-store-locator'),
                    'permanently_closed' => __('Permanently closed', 'cliq-store-locator'),
                ],
            ],
            'country' => [
                'label' => __('Country', 'cliq-store-locator'),
                'type' => 'select',
                'options' => [
                    'GB' => __('United Kingdom', 'cliq-store-locator'),
                    'DE' => __('Germany', 'cliq-store-locator'),
                    'FR' => __('France', 'cliq-store-locator'),
                    'US' => __('United States', 'cliq-store-locator'),
                ],
            ],
            'city' => [
                'label' => __('City', 'cliq-store-locator'),
                'type' => 'text',
                'operator' => 'contains',
            ],
            'categories' => [
                'label' => __('Category', 'cliq-store-locator'),
                'type' => 'multi-select',
                'options' => $categoryOptions,
                'source' => 'enriched',
                'operator' => 'equals',
            ],
            'brand_id' => [
                'label' => __('Brand', 'cliq-store-locator'),
                'type' => 'select',
                'options' => $brandOptions,
                'source' => 'enriched',
                'operator' => 'equals',
            ],
        ];

        $storageMap = FieldStorageMap::fromForm($storeForm);

        $strategyRegistry = $context->strategyRegistry();

        $coordinator = new SchemaPersistenceCoordinator(
            Store::class,
            $storageMap,
            $strategyRegistry
        );

        // The admin table renders every provider filter automatically. This
        // hook lets extensions add or alter store filters without changing the
        // table UI. Mark a filter `source => enriched` when its value comes from
        // a pivot/meta/custom coordinator field rather than the stores table.
        $storeFilters = array_merge($storeFilters, $customFilters);
        $storeFilters = apply_filters('cliq_sl_admin_store_filters', $storeFilters, $storeForm, $context);

        $provider = new \CliqThemes\StoreLocator\Data\StoreProvider(
            $coordinator,
            $storeColumns,
            $storeFilters,
            [
                'status' => [
                    'field' => 'status',
                    'mode' => 'replace',
                    'allowed' => array_column($statusOptions, 'value'),
                ],
                'categories' => [
                    'field' => 'categories',
                    'mode' => 'append',
                ],
                'custom_field' => [
                    'fields' => array_values(array_filter(array_map(
                        static fn (array $field): string => (string) ($field['name'] ?? ''),
                        $customBulkFields
                    ))),
                    'mode' => 'replace',
                ],
                'brand' => [
                    'field' => 'brand_id',
                    'mode' => 'replace',
                ],
            ],
        );

        // Colour-coded status badge (same CSS class family as the "Lang" column's
        // cliq-lang badge, see StoreProvider::langBadge()) — .cliq-status--{slug}
        // rules live in resources/admin-ext/src/style.scss.
        $provider->setColumnTransformer('status', static function (array $record) use ($statusOptions): array {
            $slug = (string) ($record['status'] ?? '');
            if ($slug === '') {
                return ['value' => '', 'isHtml' => false];
            }
            $label = $slug;
            foreach ($statusOptions as $opt) {
                if ($opt['value'] === $slug) {
                    $label = $opt['label'];
                    break;
                }
            }

            return ['value' => '<span class="cliq-status cliq-status--' . esc_attr($slug) . '">' . esc_html($label) . '</span>', 'isHtml' => true];
        });

        $provider->setColumnTransformer('hero_media_preview', static function (array $record): array {
            $media = $record['hero_media'] ?? null;

            if (is_array($media)) {
                $url = (string) ($media['url'] ?? '');

                if ($url === '' && isset($media['source_url'])) {
                    $url = (string) $media['source_url'];
                }

                if ($url !== '') {
                    $alt = (string) ($media['alt'] ?? 'Hero image');
                    $html = sprintf('<img src="%s" alt="%s" class="cliq-sl-thumb" />', esc_url($url), esc_attr($alt));
                    return ['value' => $html, 'isHtml' => true];
                }
            }

            if (is_string($media) && $media !== '') {
                $html = sprintf('<img src="%s" alt="Hero image" class="cliq-sl-thumb" />', esc_url($media));
                return ['value' => $html, 'isHtml' => true];
            }

            return ['value' => '', 'isHtml' => false];
        });

        // Resolve category IDs (pivot values) to names so the grid shows labels,
        // not raw ids. Map built once per request; falls back to the id if unknown.
        $categoryNames = [];
        try {
            foreach ((new \CliqThemes\StoreLocator\Repository\CategoryRepository())->all() as $cat) {
                if (isset($cat['id'])) {
                    $categoryNames[(string) $cat['id']] = (string) ($cat['name'] ?? $cat['id']);
                }
            }
        } catch (\Throwable $e) {
            $categoryNames = [];
        }
        $provider->setColumnTransformer('categories', static function (array $record) use ($categoryNames): array {
            $ids = $record['categories'] ?? [];
            if (! is_array($ids)) {
                $ids = ($ids === '' || $ids === null) ? [] : [$ids];
            }
            $names = [];
            foreach ($ids as $id) {
                if (is_array($id)) {
                    $names[] = (string) ($id['name'] ?? $id['id'] ?? '');
                } else {
                    $names[] = $categoryNames[(string) $id] ?? (string) $id;
                }
            }

            return array_values(array_filter($names, static fn ($n) => $n !== ''));
        });

        // Hide translation rows from the grid (overlay is applied in StoreProvider
        // after coordinator enrichment so it isn't overwritten).
        $provider->addListProcessor(ContentTranslator::listProcessor('cliq_sl_stores'));
        $registry->register('stores', $provider);


        // Publish the custom-field => token-format map (derived from form field
        // types) so DesignTokens::customFromMeta() resolves the SAME formats the
        // builder uses. NOTE: this whole register() method only runs when
        // Plugin::needsAdminLayer() is true, so this call alone never reaches a
        // public page load — FrontendModule calls the cheap publishCustomFieldFormats()
        // below directly before it builds tokens, so both paths agree.
        $formatMap = [];
        foreach (StoreCustomFields::fromForm($storeForm) as $field) {
            $formatMap[$field['key']] = \CliqThemes\StoreLocator\Support\DesignTokens::formatForType((string) ($field['type'] ?? 'text'));
        }
        \CliqThemes\StoreLocator\Support\DesignTokens::setCustomFormats($formatMap);

        return $storeForm;
    }

    /**
     * Store-form fields baked directly into the $storeForm scaffold above (not
     * sourced from CustomFieldsRepository) whose type resolves to a non-'text'
     * token format, isn't already covered by a dedicated core token
     * (DesignTokens::core()), and isn't excluded by StoreCustomFields' $core /
     * $skipTypes lists (e.g. 'opening_hours' is type 'hours', but 'hours' is a
     * skipped structural type, and 'hero_media'/'gallery_media' are both in
     * StoreCustomFields' $core list) — so 'featured_store' (type 'toggle') is
     * currently the only entry. Kept as a small static list: deriving it from
     * the live $storeForm would require running register()'s full
     * $context-dependent option resolution, the exact DB cost
     * Plugin::needsAdminLayer() exists to avoid on public requests.
     *
     * @var array<string, string>
     */
    private const BAKED_IN_META_FIELD_TYPES = [
        'featured_store' => 'toggle',
    ];

    /**
     * Publish the same custom-field => token-format map as register() above, but
     * cheaply — the static list above plus one CustomFieldsRepository query, none
     * of register()'s category resolution / pivot / storage-map / tab-injection
     * work — so it's safe to call unconditionally on every public request.
     * register() itself only runs when Plugin::needsAdminLayer() is true (a
     * deliberate perf gate), which used to mean a public page's custom fields
     * (e.g. a 'toggle' field like "Featured Store") always resolved to the
     * generic 'text' format and rendered their raw stored value.
     */
    public static function publishCustomFieldFormats(): void
    {
        $fields = [];
        foreach (self::BAKED_IN_META_FIELD_TYPES as $key => $type) {
            $fields[] = ['name' => $key, 'type' => $type];
        }
        try {
            foreach ((new CustomFieldsRepository())->forEntity('store') as $customField) {
                if (empty($customField['field_key']) || empty($customField['type'])) {
                    continue;
                }
                $fields[] = ['name' => (string) $customField['field_key'], 'type' => (string) $customField['type']];
            }
        } catch (\Throwable $e) {
            // Keep the baked-in fields even if the repository query fails.
        }

        $formatMap = [];
        foreach (StoreCustomFields::fromForm(['sections' => [['groups' => [['fields' => $fields]]]]]) as $field) {
            $formatMap[$field['key']] = \CliqThemes\StoreLocator\Support\DesignTokens::formatForType((string) ($field['type'] ?? 'text'));
        }
        \CliqThemes\StoreLocator\Support\DesignTokens::setCustomFormats($formatMap);
    }

    /**
     * The store form's tabs and their section (group) ids/labels. Drives the
     * custom-field placement picker and the store form's field injection.
     *
     * @return array<string, array{label: string, groups: array<string, string>}>
     */
    public static function tabSectionMap(): array
    {
        return [
            'store-details' => ['label' => __('Store Details', 'cliq-store-locator'), 'groups' => [
                'identity' => __('Identity', 'cliq-store-locator'),
                'overview' => __('Description', 'cliq-store-locator'),
                'classification' => __('Categories', 'cliq-store-locator'),
                'highlights' => __('Highlights', 'cliq-store-locator'),
                'gallery' => __('Gallery', 'cliq-store-locator'),
            ]],
            'store-location' => ['label' => __('Location', 'cliq-store-locator'), 'groups' => [
                'geo' => __('Map & Coordinates', 'cliq-store-locator'),
                'address' => __('Address', 'cliq-store-locator'),
            ]],
            'contact' => ['label' => __('Contact', 'cliq-store-locator'), 'groups' => [
                'contact-details' => __('Contact Details', 'cliq-store-locator'),
                'social' => __('Social Media', 'cliq-store-locator'),
            ]],
            'store-operations' => ['label' => __('Store Operations', 'cliq-store-locator'), 'groups' => [
                'operations' => __('Operations', 'cliq-store-locator'),
                'hours' => __('Opening Hours', 'cliq-store-locator'),
            ]],
        ];
    }
}
