<?php
declare(strict_types=1);

namespace Celena\Plugin\Shop\Support;

/**
 * Нормализация и сериализация колонки `shop_attributes.options`.
 *
 * Старый формат (массив строк) и новый (массив объектов с цветом/картинкой)
 * сосуществуют. На чтении приводим к единому виду:
 *   [{label: string, color: ?string, image: ?string}, ...]
 *
 * При записи — кодируем JSON и сохраняем только поля, имеющие смысл
 * для конкретного типа атрибута (color → label+color, select/multiselect → label+image).
 */
final class AttributeOptions
{
    /**
     * Декодирует raw-значение колонки options в нормализованный список.
     * Принимает: JSON-строку, готовый массив, null.
     *
     * @return list<array{label: string, color: ?string, image: ?string}>
     */
    public static function decode(mixed $raw): array
    {
        if ($raw === null || $raw === '') {
            return [];
        }
        $arr = is_array($raw) ? $raw : json_decode((string) $raw, true);
        if (!is_array($arr)) {
            return [];
        }
        $out = [];
        foreach ($arr as $item) {
            if (is_string($item)) {
                $label = trim($item);
                if ($label === '') continue;
                $out[] = ['label' => $label, 'color' => null, 'image' => null];
                continue;
            }
            if (!is_array($item)) continue;
            $label = trim((string) ($item['label'] ?? ''));
            if ($label === '') continue;
            $color = isset($item['color']) ? trim((string) $item['color']) : '';
            $image = isset($item['image']) ? trim((string) $item['image']) : '';
            $out[] = [
                'label' => $label,
                'color' => $color !== '' ? $color : null,
                'image' => $image !== '' ? $image : null,
            ];
        }
        return $out;
    }

    /**
     * Сериализовать список вариантов в JSON для storage.
     * Поля color/image сохраняются только когда уместны по типу.
     *
     * @param array<int, array<string, mixed>> $options
     * @return string|null JSON-строка либо null если вариантов нет.
     */
    public static function encode(array $options, string $type): ?string
    {
        $items = [];
        $seen  = [];
        foreach ($options as $opt) {
            if (!is_array($opt)) continue;
            $label = trim((string) ($opt['label'] ?? ''));
            if ($label === '' || mb_strlen($label) > 100) continue;
            if (isset($seen[$label])) continue; // дубли по label отбрасываем
            $seen[$label] = true;

            $row = ['label' => $label];
            if ($type === 'color') {
                $color = self::sanitizeColor((string) ($opt['color'] ?? ''));
                if ($color !== null) {
                    $row['color'] = $color;
                }
            }
            if (in_array($type, ['select', 'multiselect', 'color'], true)) {
                $image = self::sanitizeImageUrl((string) ($opt['image'] ?? ''));
                if ($image !== null) {
                    $row['image'] = $image;
                }
            }
            $items[] = $row;
        }
        if ($items === []) {
            return null;
        }
        return json_encode($items, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    /**
     * Удобная мапа label → row для использования при обогащении фасетов.
     *
     * @param mixed $raw То же, что accepts decode().
     * @return array<string, array{label: string, color: ?string, image: ?string}>
     */
    public static function indexByLabel(mixed $raw): array
    {
        $out = [];
        foreach (self::decode($raw) as $opt) {
            $out[$opt['label']] = $opt;
        }
        return $out;
    }

    /** Просто список лейблов — для legacy-кода, ожидающего array of strings. */
    public static function labels(mixed $raw): array
    {
        return array_map(static fn ($o): string => $o['label'], self::decode($raw));
    }

    private static function sanitizeColor(string $value): ?string
    {
        $value = trim($value);
        if ($value === '') return null;
        // hex #fff / #ffffff / #ffffff80
        if (preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $value) === 1) {
            return strtolower($value);
        }
        // rgb()/rgba()/hsl()/hsla() — пропускаем без жёсткой валидации, но без скобок-инъекций
        if (preg_match('/^(rgb|rgba|hsl|hsla)\([0-9.,%\s]+\)$/i', $value) === 1) {
            return $value;
        }
        return null;
    }

    private static function sanitizeImageUrl(string $value): ?string
    {
        $value = trim($value);
        if ($value === '') return null;
        // Принимаем только относительные URL внутри сайта (uploads/) или абсолютные https
        // через MediaController. Внешние src запрещаем — это XSS-риск через style-инъекции.
        if (preg_match('#^/[A-Za-z0-9_./@-]+\.(?:jpg|jpeg|png|gif|webp)$#i', $value) === 1) {
            return $value;
        }
        return null;
    }
}
