<?php

declare(strict_types=1);

namespace CliqThemes\StoreLocator\Support;

defined('ABSPATH') || exit;

/**
 * Resolves Google Maps preset/custom styles before public rendering.
 *
 * The full preset gallery is useful in wp-admin, but a public locator needs
 * only the style array selected by its global settings or individual Map
 * widget. Keeping resolution here removes the gallery from the public bundle.
 */
final class MapStyleResolver
{
    private const MAX_CUSTOM_BYTES = 262144;
    private const MAX_RULES = 500;
    private const MAX_STYLERS = 50;

    /** @var array<string, array<int, array<string, mixed>>>|null */
    private static ?array $presets = null;

    /**
     * Resolve one preset key or custom JSON value to a safe Google styles array.
     *
     * @return array<int, array<string, mixed>>
     */
    public static function resolve(string $styleKey, mixed $customJson = ''): array
    {
        $styleKey = self::key($styleKey);
        if ($styleKey === 'custom') {
            if (is_string($customJson)) {
                if ($customJson === '' || strlen($customJson) > self::MAX_CUSTOM_BYTES) {
                    return [];
                }
                $decoded = json_decode($customJson, true, 64);
            } else {
                $decoded = $customJson;
            }

            return is_array($decoded) ? self::normalize($decoded) : [];
        }

        return self::presets()[$styleKey] ?? [];
    }

    /**
     * Add the exact resolved style array to every explicitly-themed Map widget.
     *
     * A widget without mapStyle intentionally receives no property so the
     * frontend can inherit the globally resolved Settings style. An explicitly
     * selected Standard theme receives an empty array and therefore overrides
     * a non-standard global theme correctly.
     *
     * @param array<string, mixed> $model
     *
     * @return array<string, mixed>
     */
    public static function resolveModel(array $model): array
    {
        self::walk($model);

        return $model;
    }

    /** @param array<string, mixed> $node */
    private static function walk(array &$node): void
    {
        if (
            ($node['type'] ?? null) === 'widget'
            && ($node['widget'] ?? null) === 'map'
            && is_array($node['widgetSettings'] ?? null)
        ) {
            $settings = &$node['widgetSettings'];
            $styleKey = trim((string) ($settings['mapStyle'] ?? ''));
            if ($styleKey !== '') {
                $settings['resolvedMapStyles'] = self::resolve(
                    $styleKey,
                    $settings['mapStyleCustom'] ?? ''
                );
            }
            unset($settings);
        }

        foreach ($node as &$value) {
            if (is_array($value)) {
                self::walk($value);
            }
        }
        unset($value);
    }

    /** @return array<string, array<int, array<string, mixed>>> */
    private static function presets(): array
    {
        if (self::$presets !== null) {
            return self::$presets;
        }

        self::$presets = [];
        $file = CLIQ_SL_PATH . 'assets/map-style-presets.json';
        if (! is_readable($file)) {
            return self::$presets;
        }

        $decoded = json_decode((string) file_get_contents($file), true, 64);
        if (! is_array($decoded)) {
            return self::$presets;
        }

        foreach ($decoded as $preset) {
            if (! is_array($preset)) {
                continue;
            }
            $key = self::key((string) ($preset['key'] ?? ''));
            $styles = is_array($preset['json'] ?? null) ? self::normalize($preset['json']) : [];
            if ($key !== '') {
                self::$presets[$key] = $styles;
            }
        }

        return self::$presets;
    }

    private static function key(string $value): string
    {
        return strtolower((string) preg_replace('/[^a-z0-9_-]/i', '', $value));
    }

    /**
     * Validate and normalize Google Maps JSON styling rules.
     *
     * @param array<mixed> $styles
     *
     * @return array<int, array<string, mixed>>
     */
    private static function normalize(array $styles): array
    {
        if (! array_is_list($styles) || count($styles) > self::MAX_RULES) {
            return [];
        }

        $normalized = [];
        foreach ($styles as $rule) {
            if (! is_array($rule)) {
                return [];
            }

            $cleanRule = [];
            foreach (['featureType', 'elementType'] as $key) {
                if (! isset($rule[$key])) {
                    continue;
                }
                $value = (string) $rule[$key];
                if ($value === '' || strlen($value) > 128 || ! preg_match('/^[a-zA-Z0-9._-]+$/', $value)) {
                    return [];
                }
                $cleanRule[$key] = $value;
            }

            $stylers = $rule['stylers'] ?? null;
            if (! is_array($stylers) || ! array_is_list($stylers) || count($stylers) > self::MAX_STYLERS) {
                return [];
            }
            $cleanStylers = [];
            foreach ($stylers as $styler) {
                if (! is_array($styler) || count($styler) !== 1) {
                    return [];
                }
                $property = (string) array_key_first($styler);
                $value = $styler[$property];
                if (
                    $property === ''
                    || ! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $property)
                    || ! is_scalar($value)
                    || is_string($value) && strlen($value) > 128
                ) {
                    return [];
                }
                $cleanStylers[] = [$property => $value];
            }

            if ($cleanStylers === []) {
                return [];
            }
            $cleanRule['stylers'] = $cleanStylers;
            $normalized[] = $cleanRule;
        }

        return $normalized;
    }
}
