<?php

declare(strict_types=1);

namespace CliqThemes\StoreLocator\Layouts;

defined('ABSPATH') || exit;

use CliqThemes\StoreLocator\Support\SettingsService;

/**
 * Builds the locator this plugin ships, by applying the site's settings to a
 * canonical preset.
 *
 * The settings are deliberately few and the preset is fixed, so the result is
 * predictable: one coherent locator a site adjusts, rather than a canvas.
 */
final class BundledLocatorBuilder
{
    /** @param array<string, mixed>|null $settings */
    public function resolve(?array $settings = null): array
    {
        $settings ??= SettingsService::all();
        $preset = BundledLocatorPreset::definition();

        $mapHeight = max(240, min(1200, (int) ($settings['locator_map_height'] ?? 560)));
        $perPage = max(1, min(100, (int) ($settings['locator_results_per_page'] ?? 20)));
        $zoom = max(1, min(22, (int) ($settings['map_default_zoom'] ?? $settings['locator_default_zoom'] ?? 5)));
        $clustering = (bool) ($settings['enable_clustering'] ?? $settings['locator_enable_clustering'] ?? true);
        $showLoadMore = (bool) ($settings['locator_show_load_more'] ?? true);
        $loadMoreLabel = trim((string) ($settings['locator_load_more_label'] ?? '')) ?: __('Load more', 'cliq-store-locator');
        $phoneLabel = trim((string) ($settings['locator_phone_label'] ?? ''));
        $emailLabel = trim((string) ($settings['locator_email_label'] ?? ''));
        $websiteLabel = trim((string) ($settings['locator_website_label'] ?? ''));
        $directionsLabel = trim((string) ($settings['locator_directions_button_label'] ?? '')) ?: __('Directions', 'cliq-store-locator');
        $detailsLabel = trim((string) ($settings['locator_details_button_label'] ?? '')) ?: __('View More', 'cliq-store-locator');
        $locationLabel = trim((string) ($settings['locator_location_label'] ?? '')) ?: __('Location', 'cliq-store-locator');
        $locationPlaceholder = trim((string) ($settings['locator_location_placeholder'] ?? '')) ?: __('City, postcode or address', 'cliq-store-locator');
        $categoryLabel = trim((string) ($settings['locator_category_label'] ?? '')) ?: __('Category', 'cliq-store-locator');
        $distanceLabel = trim((string) ($settings['locator_distance_label'] ?? '')) ?: __('Distance', 'cliq-store-locator');
        $searchButtonLabel = trim((string) ($settings['locator_search_button_label'] ?? '')) ?: __('Search', 'cliq-store-locator');
        $resultCountLabel = trim((string) ($settings['locator_result_count_label'] ?? '')) ?: __('Stores', 'cliq-store-locator');
        $emptyResultsMessage = trim((string) ($settings['locator_empty_results_message'] ?? '')) ?: __('No stores found.', 'cliq-store-locator');
        $showPrint = (bool) ($settings['locator_show_print'] ?? false);
        $requestedMapPosition = (string) ($settings['locator_map_position'] ?? 'right');
        $mapPosition = in_array($requestedMapPosition, ['left', 'right', 'none'], true)
            ? $requestedMapPosition
            : 'right';
        $mapColumns = max(4, min(9, (int) ($settings['locator_map_columns'] ?? 7)));
        $listColumns = max(1, min(6, (int) ($settings['locator_list_columns'] ?? 3)));
        $listMinWidth = max(160, min(600, (int) ($settings['locator_list_min_width'] ?? 260)));

        $model = $preset['model'];
        $this->patchWidget($model, 'results', static function (array $widget) use ($perPage, $showLoadMore, $loadMoreLabel, $emptyResultsMessage): array {
            $widget['widgetSettings'] = array_merge($widget['widgetSettings'], [
                'pageSize' => $perPage,
                'showLoadMore' => $showLoadMore,
                'loadMoreType' => 'button',
                'loadMoreLabel' => $loadMoreLabel,
                'emptyText' => $emptyResultsMessage,
            ]);

            return $widget;
        });
        $this->patchWidget($model, 'count', static function (array $widget) use ($showPrint, $resultCountLabel): array {
            $widget['widgetSettings']['showPrint'] = $showPrint;
            $widget['widgetSettings']['label'] = $resultCountLabel;

            return $widget;
        });
        $this->patchWidget($model, 'map', static function (array $widget) use ($mapHeight, $zoom, $clustering): array {
            $widget['widgetSettings']['height'] = $mapHeight;
            $widget['widgetSettings']['zoom'] = $zoom;
            $widget['widgetSettings']['clustering'] = $clustering;

            return $widget;
        });
        $this->applyContentLayout(
            $model,
            $mapPosition,
            $mapColumns,
            $mapHeight,
            $listColumns,
            $listMinWidth
        );
        if (! (bool) ($settings['locator_show_result_count'] ?? true)) {
            $this->removeWidget($model, 'count');
        }
        if (! (bool) ($settings['locator_show_search'] ?? true)) {
            $this->removeRow($model, 'locator-search-row');
        }

        $designs = $preset['designs'];
        $this->applyTemplateConfiguration($designs, $settings, [
            'phone' => $phoneLabel,
            'email' => $emailLabel,
            'website' => $websiteLabel,
            'directions' => $directionsLabel,
            'details' => $detailsLabel,
        ]);
        $search = $designs[(string) BundledLocatorPreset::SEARCH_ID]['model'];
        if (! (bool) ($settings['locator_show_category_filter'] ?? true)) {
            $this->removeItem($search, 'locator-category');
        }
        if (! (bool) ($settings['locator_show_radius_filter'] ?? true)) {
            $this->removeItem($search, 'locator-radius');
        }
        // Wire the "Enable address autocomplete" setting to the location field's
        // own flag — the frontend only mounts Google Places autocomplete when the
        // field itself opts in (searchWidgets.js: `f.mapTo === 'location' &&
        // f.autocomplete`). Autocomplete still requires a Google Maps/Places key;
        // with Leaflet/Nominatim the input degrades gracefully to plain text.
        $this->setItemProps($search, 'locator-location', [
            'autocomplete' => (bool) ($settings['enable_autocomplete'] ?? false),
            'label' => $locationLabel,
            'placeholder' => $locationPlaceholder,
        ]);
        $this->setItemProps($search, 'locator-category', ['label' => $categoryLabel]);
        $this->setItemProps($search, 'locator-radius', ['label' => $distanceLabel]);
        $this->setItemProps($search, 'locator-submit', ['text' => $searchButtonLabel]);
        $designs[(string) BundledLocatorPreset::SEARCH_ID]['model'] = $search;

        return [
            'id' => BundledLocatorPreset::TEMPLATE_ID,
            'key' => BundledLocatorPreset::KEY,
            'name' => $preset['name'],
            'model' => $model,
            'designs' => $designs,
        ];
    }

    /**
     * Lay the store list out as a grid of cards.
     *
     * `auto-fit` plus a minimum track width means the configured column count is
     * a ceiling rather than a fixed number — the list drops to fewer columns on
     * its own when the container is too narrow, which is what lets the same two
     * settings serve both a full-width list and a column beside the map.
     *
     * @param array<string, mixed> $model
     */
    private function applyResultsGrid(array &$model, int $listColumns, int $listMinWidth, bool $scroll): void
    {
        $this->patchWidget(
            $model,
            'results',
            static function (array $widget) use ($listColumns, $listMinWidth, $scroll): array {
                $widget['widgetSettings'] = array_merge($widget['widgetSettings'], [
                    'layout' => 'grid',
                    'gridColumns' => $listColumns,
                    'minColWidth' => $listMinWidth,
                    'colMode' => 'auto-fit',
                    'scroll' => $scroll,
                ]);

                return $widget;
            }
        );
    }

    /** @param array<string, mixed> $model */
    private function applyContentLayout(
        array &$model,
        string $position,
        int $mapColumns,
        int $mapHeight,
        int $listColumns,
        int $listMinWidth
    ): void {
        foreach ($model['rows'] ?? [] as $rowIndex => $row) {
            if (($row['id'] ?? '') !== 'locator-content-row') {
                continue;
            }

            $results = null;
            $map = null;
            foreach ($row['columns'] ?? [] as $column) {
                if (($column['id'] ?? '') === 'locator-results-column') {
                    $results = $column;
                } elseif (($column['id'] ?? '') === 'locator-map-column') {
                    $map = $column;
                }
            }
            if (! is_array($results) || ! is_array($map)) {
                return;
            }

            if ($position === 'none') {
                $results['span'] = 12;
                $model['rows'][$rowIndex]['columns'] = [$results];
                unset(
                    $model['rows'][$rowIndex]['settings']['equalColumnHeight'],
                    $model['rows'][$rowIndex]['settings']['equalColumnHeightValue']
                );
                // Full width and no map to scroll alongside: the page scrolls.
                $this->applyResultsGrid($model, $listColumns, $listMinWidth, false);
                return;
            }

            $map['span'] = $mapColumns;
            $results['span'] = 12 - $mapColumns;
            $model['rows'][$rowIndex]['columns'] = $position === 'left'
                ? [$map, $results]
                : [$results, $map];
            $model['rows'][$rowIndex]['settings']['equalColumnHeight'] = true;
            $model['rows'][$rowIndex]['settings']['equalColumnHeightValue'] = $mapHeight . 'px';
            // Beside a map the list keeps its own scroll, but it is still a grid:
            // the column settings apply to both arrangements, and `auto-fit`
            // collapses to a single track whenever the narrower column cannot
            // fit two at the configured minimum width.
            $this->applyResultsGrid($model, $listColumns, $listMinWidth, true);
            return;
        }
    }

    /** @param array<string, mixed> $model */
    private function patchWidget(array &$model, string $type, callable $patch): void
    {
        foreach ($model['rows'] ?? [] as $rowIndex => $row) {
            foreach ($row['columns'] ?? [] as $columnIndex => $column) {
                foreach ($column['items'] ?? [] as $itemIndex => $item) {
                    if (($item['type'] ?? '') === 'widget' && ($item['widget'] ?? '') === $type) {
                        $model['rows'][$rowIndex]['columns'][$columnIndex]['items'][$itemIndex] = $patch($item);
                    }
                }
            }
        }
    }

    /** @param array<string, mixed> $model */
    private function removeWidget(array &$model, string $type): void
    {
        foreach ($model['rows'] ?? [] as $rowIndex => $row) {
            foreach ($row['columns'] ?? [] as $columnIndex => $column) {
                $model['rows'][$rowIndex]['columns'][$columnIndex]['items'] = array_values(array_filter(
                    $column['items'] ?? [],
                    static fn (mixed $item): bool =>
                        ! is_array($item)
                        || ($item['type'] ?? '') !== 'widget'
                        || ($item['widget'] ?? '') !== $type
                ));
            }
        }
    }

    /** @param array<string, mixed> $model */
    private function removeRow(array &$model, string $id): void
    {
        $model['rows'] = array_values(array_filter(
            $model['rows'] ?? [],
            static fn (mixed $row): bool => ! is_array($row) || ($row['id'] ?? '') !== $id
        ));
    }

    /**
     * Merge properties onto the item with the given id, wherever it sits.
     *
     * @param array<string, mixed> $model
     * @param array<string, mixed> $props
     */
    private function setItemProps(array &$model, string $id, array $props): void
    {
        foreach ($model['rows'] ?? [] as $rowIndex => $row) {
            foreach ($row['columns'] ?? [] as $columnIndex => $column) {
                foreach ($column['items'] ?? [] as $itemIndex => $item) {
                    if (is_array($item) && ($item['id'] ?? '') === $id) {
                        $model['rows'][$rowIndex]['columns'][$columnIndex]['items'][$itemIndex] =
                            array_merge($item, $props);
                    }
                }
            }
        }
    }

    /** @param array<string, mixed> $model */
    private function removeItem(array &$model, string $id): void
    {
        foreach ($model['rows'] ?? [] as $rowIndex => $row) {
            $columns = [];
            foreach ($row['columns'] ?? [] as $column) {
                $column['items'] = array_values(array_filter(
                    $column['items'] ?? [],
                    static fn (mixed $item): bool => ! is_array($item) || ($item['id'] ?? '') !== $id
                ));
                if ($column['items'] !== []) {
                    $columns[] = $column;
                }
            }
            $model['rows'][$rowIndex]['columns'] = $columns;
        }
    }

    /**
     * Apply the settings that shape the bundled layout. This is deliberately
     * data-driven: the public renderer receives ordinary design models and does
     * not need a separate Free-only rendering path.
     *
     * @param array<string, mixed> $designs
     * @param array<string, mixed> $settings
     */
    private function applyTemplateConfiguration(array &$designs, array $settings, array $labels): void
    {
        $templates = [
            [
                'design' => BundledLocatorPreset::CARD_ID,
                'column' => 'locator-card-column',
                'scope' => 'card',
                'setting' => 'locator_card_fields',
            ],
            [
                'design' => BundledLocatorPreset::INFOBOX_ID,
                'column' => 'locator-infobox-column',
                'scope' => 'infobox',
                'setting' => 'locator_infobox_fields',
            ],
            [
                'design' => BundledLocatorPreset::DETAIL_ID,
                'column' => 'locator-detail-content',
                'scope' => 'detail',
                'setting' => 'locator_detail_fields',
            ],
        ];

        foreach ($templates as $template) {
            $designKey = (string) $template['design'];
            $model = $designs[$designKey]['model'] ?? null;
            if (! is_array($model)) {
                continue;
            }
            $configured = $settings[$template['setting']] ?? SettingsService::defaults()[$template['setting']];
            $configured = is_array($configured) ? $configured : [];
            $items = $this->configuredItems(
                (string) $template['scope'],
                $configured,
                $labels
            );
            $columnSetting = 'locator_' . $template['scope'] . '_columns';
            $columnCount = in_array($template['scope'], ['card', 'detail'], true)
                ? max(1, min(2, (int) ($settings[$columnSetting] ?? 1)))
                : 1;
            if ($columnCount === 2) {
                $columnItems = [1 => [], 2 => []];
                foreach ($configured as $entry) {
                    if (! is_array($entry) || ! (bool) ($entry['enabled'] ?? false)) {
                        continue;
                    }
                    $field = (string) ($entry['field'] ?? '');
                    foreach ($items as $item) {
                        if (($item['token'] ?? '') === (str_starts_with($field, 'custom:') ? substr($field, 7) : $field)) {
                            $columnItems[(int) ($entry['column'] ?? 1) === 2 ? 2 : 1][] = $item;
                            break;
                        }
                    }
                }
                $this->replaceColumnWithColumns(
                    $model,
                    (string) $template['column'],
                    $columnItems
                );
            } else {
                $this->replaceColumnItems($model, (string) $template['column'], $items);
            }
            $designs[$designKey]['model'] = $model;
        }

        // The Free detail editor treats the hero as an ordinary reorderable
        // template item. Remove the old fixed media column and let the configured
        // `logo` item render wherever the user placed it.
        $detailKey = (string) BundledLocatorPreset::DETAIL_ID;
        $detail = $designs[$detailKey]['model'] ?? null;
        if (is_array($detail)) {
            foreach ($detail['rows'] ?? [] as $rowIndex => $row) {
                $columns = array_values(array_filter(
                    $row['columns'] ?? [],
                    static fn (mixed $column): bool =>
                        ! is_array($column) || ($column['id'] ?? '') !== 'locator-detail-media'
                ));
                foreach ($columns as &$column) {
                    if (($column['id'] ?? '') === 'locator-detail-content') {
                        $column['span'] = 12;
                    }
                }
                unset($column);
                $detail['rows'][$rowIndex]['columns'] = $columns;
            }
            $designs[$detailKey]['model'] = $detail;
        }
    }

    /**
     * @param array<int, mixed> $configuration
     *
     * @return array<int, array<string, mixed>>
     */
    private function configuredItems(string $scope, array $configuration, array $labels): array
    {
        $items = [];
        foreach ($configuration as $entry) {
            if (! is_array($entry) || ! (bool) ($entry['enabled'] ?? false)) {
                continue;
            }
            $field = (string) ($entry['field'] ?? '');
            $icon = (string) ($entry['icon'] ?? '');
            $item = $this->templateField($scope, $field, $icon);
            if ($item === null) {
                continue;
            }
            if (in_array($field, ['directions', 'details'], true)) {
                $item['buttonLabel'] = (string) ($labels[$field] ?? '');
            } elseif (in_array($field, ['phone', 'email', 'website'], true)) {
                $displayLabel = (string) ($labels[$field] ?? '');
                if ($displayLabel !== '') {
                    $item['displayLabel'] = $displayLabel;
                }
            }
            $items[] = $item;
        }

        return $items;
    }

    /** @return array<string, mixed>|null */
    private function templateField(string $scope, string $field, string $icon): ?array
    {
        $allowed = [
            'card' => ['logo', 'name', 'address', 'category', 'brand', 'brand_logo', 'phone', 'email', 'website', 'opening_hours', 'distance', 'details'],
            'infobox' => ['logo', 'name', 'address', 'category', 'phone', 'email', 'website', 'directions', 'details'],
            'detail' => ['logo', 'name', 'address', 'category', 'brand', 'brand_logo', 'description', 'phone', 'email', 'website', 'opening_hours'],
        ];
        $isCustom = str_starts_with($field, 'custom:')
            && preg_match('/^custom:[a-z0-9_-]+$/', $field) === 1;
        if (! in_array($field, $allowed[$scope] ?? [], true) && ! $isCustom) {
            return null;
        }

        $css = ['color' => 'var(--cliq-sl-text)', 'fontSize' => $scope === 'infobox' ? '12px' : '14px'];
        if (in_array($field, ['logo', 'brand_logo'], true)) {
            $isBrandLogo = $field === 'brand_logo';
            $css = [
                'width' => '100%',
                'height' => $isBrandLogo
                    ? ($scope === 'detail' ? '90px' : '58px')
                    : ($scope === 'detail' ? '320px' : ($scope === 'card' ? '170px' : '110px')),
                'objectFit' => $isBrandLogo ? 'contain' : 'cover',
                'borderRadius' => $scope === 'detail' ? '12px' : '8px',
            ];
        } elseif ($field === 'name') {
            $css = [
                'fontSize' => $scope === 'detail' ? '32px' : ($scope === 'card' ? '18px' : '15px'),
                'fontWeight' => '700',
                'color' => 'var(--cliq-sl-heading)',
            ];
        } elseif ($field === 'address') {
            $css = [
                'fontSize' => $scope === 'detail' ? '15px' : ($scope === 'card' ? '13px' : '12px'),
                'color' => 'var(--cliq-sl-muted)',
            ];
        } elseif ($field === 'description') {
            $css = ['fontSize' => '15px', 'color' => 'var(--cliq-sl-text)'];
        } elseif (in_array($field, ['phone', 'email', 'website'], true)) {
            if (in_array($field, ['email', 'website'], true)) {
                $css['color'] = 'var(--cliq-sl-primary)';
            }
            $css['linkType'] = $field;
            if ($field === 'website') {
                $css['linkNewTab'] = true;
            }
        } elseif ($field === 'distance') {
            $css = [
                'color' => 'var(--cliq-sl-primary)',
                'fontSize' => '12px',
                'fontWeight' => '600',
            ];
        } elseif (in_array($field, ['details', 'directions'], true)) {
            $css = [
                'fit' => 'auto',
                'background' => 'var(--cliq-sl-primary)',
                'color' => 'var(--cliq-sl-on-primary)',
                'borderRadius' => $scope === 'infobox' ? '7px' : '8px',
                'padding' => $scope === 'infobox' ? '7px 12px' : '8px 14px',
                'fontWeight' => '600',
            ];
        }

        $token = $isCustom ? substr($field, strlen('custom:')) : $field;
        $item = [
            'id' => 'locator-' . $scope . '-' . str_replace(['_', ':'], '-', $field),
            'type' => 'field',
            'token' => $token,
            'colSpan' => 12,
            'css' => $css,
        ];
        if ($icon !== '') {
            $item['icon'] = $icon;
        }

        return $item;
    }

    /**
     * @param array<string, mixed> $model
     * @param array<int, array<string, mixed>> $items
     */
    private function replaceColumnItems(array &$model, string $columnId, array $items): void
    {
        foreach ($model['rows'] ?? [] as $rowIndex => $row) {
            foreach ($row['columns'] ?? [] as $columnIndex => $column) {
                if (($column['id'] ?? '') === $columnId) {
                    $model['rows'][$rowIndex]['columns'][$columnIndex]['items'] = $items;
                }
            }
        }
    }

    /**
     * Replace one canonical content column with two equal responsive columns.
     *
     * @param array<string, mixed> $model
     * @param array<int, array<int, array<string, mixed>>> $columnItems
     */
    private function replaceColumnWithColumns(array &$model, string $columnId, array $columnItems): void
    {
        foreach ($model['rows'] ?? [] as $rowIndex => $row) {
            foreach ($row['columns'] ?? [] as $columnIndex => $column) {
                if (($column['id'] ?? '') !== $columnId) {
                    continue;
                }

                $replacement = [];
                foreach ([1, 2] as $number) {
                    $next = $column;
                    $next['id'] = $columnId . '-' . $number;
                    $next['span'] = 6;
                    $next['spanMobile'] = 12;
                    $next['items'] = $columnItems[$number] ?? [];
                    $replacement[] = $next;
                }
                array_splice(
                    $model['rows'][$rowIndex]['columns'],
                    $columnIndex,
                    1,
                    $replacement
                );

                return;
            }
        }
    }
}
