<?php

// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedNamespaceFound -- APD is the established namespace shared with the Pro add-on and retained for backward compatibility.
namespace APD\Runtime;

defined('ABSPATH') || exit;

class RuntimeCatalogMapper
{
    public function __construct(private readonly ?RuntimeShapeMapper $shapeMapper = null)
    {
    }

    public function buildCatalog(array $catalogPayload, array $normalized, array $colors, array $printingMethods) : array
    {
        $shapeMapper = $this->shapeMapper ?? new RuntimeShapeMapper();

        return [
            'designs' => $this->mergeById((array) ($catalogPayload['designs'] ?? []), (array) (($normalized['catalog']['designs'] ?? []))),
            'elements' => $shapeMapper->normalizeItems($this->mergeById((array) ($catalogPayload['shapes'] ?? []), (array) (($normalized['catalog']['elements'] ?? [])))),
            'colors' => $colors,
            'printingMethods' => $printingMethods,
        ];
    }

    public function mergeModuleConfigCatalog(array $moduleConfig, array $catalog, array $assetFilters = []) : array
    {
        $catalogDesigns = array_values(array_filter((array) ($catalog['designs'] ?? []), 'is_array'));
        $replaceDesigns = $this->shouldReplaceCatalogFamily($assetFilters, 'designs');
        $designItems = $replaceDesigns
            ? $catalogDesigns
            : $this->mergeById(
                (array) (($moduleConfig['designs']['items'] ?? [])),
                $catalogDesigns
            );

        if (! isset($moduleConfig['designs']) || ! is_array($moduleConfig['designs'])) {
            $moduleConfig['designs'] = [];
        }

        $moduleConfig['designs']['items'] = array_values(array_filter(array_map(
            static fn($item) => is_array($item) ? $item : null,
            $designItems
        )));

        $catalogShapes = array_values(array_filter((array) ($catalog['elements'] ?? []), 'is_array'));
        if ($this->shouldReplaceCatalogFamily($assetFilters, 'shapes')) {
            if (! isset($moduleConfig['shapes']) || ! is_array($moduleConfig['shapes'])) {
                $moduleConfig['shapes'] = [];
            }
            $moduleConfig['shapes']['svgAssets'] = $catalogShapes;

            return $moduleConfig;
        }

        $shapeAssets = [];
        if (is_array($moduleConfig['shapes']['svgAssets'] ?? null)) {
            $shapeAssets = array_values(array_filter($moduleConfig['shapes']['svgAssets'], 'is_array'));
        }
        $shapeAssets = $this->mergeById($shapeAssets, $catalogShapes);
        if ($shapeAssets !== []) {
            if (! isset($moduleConfig['shapes']) || ! is_array($moduleConfig['shapes'])) {
                $moduleConfig['shapes'] = [];
            }
            $moduleConfig['shapes']['svgAssets'] = $shapeAssets;
        }

        return $moduleConfig;
    }

    public function mergeInitialStateCatalog(mixed $state, array $catalog, array $assetFilters = []) : mixed
    {
        if (! is_array($state)) {
            return $state;
        }

        $productElements = $this->extractElementDefinitionsFromPayload($state);
        $elements = $this->mergeCatalogFamily((array) (($state['catalog']['elements'] ?? [])), (array) ($catalog['elements'] ?? []), $assetFilters, 'shapes');
        if ($productElements !== []) {
            $elements = $this->mergeElementDefinitions($elements, $productElements);
        }

        $state['catalog'] = [
            'designs' => $this->mergeCatalogFamily((array) (($state['catalog']['designs'] ?? [])), (array) ($catalog['designs'] ?? []), $assetFilters, 'designs'),
            'elements' => $elements,
            'colors' => $this->mergeCatalogFamily((array) (($state['catalog']['colors'] ?? [])), (array) ($catalog['colors'] ?? []), $assetFilters, 'colors'),
            'printingMethods' => $this->mergeCatalogFamily((array) (($state['catalog']['printingMethods'] ?? [])), (array) ($catalog['printingMethods'] ?? []), $assetFilters, 'printingMethods'),
        ];

        return $state;
    }


    public function extractRuntimeElementDefinitions(mixed ...$payloads) : array
    {
        $groups = [];
        foreach ($payloads as $payload) {
            $groups[] = $this->extractElementDefinitionsFromPayload($payload);
        }

        return $this->mergeElementDefinitions(...$groups);
    }


    /**
     * Product-specific Asuka Elements are saved inside the exported state as an
     * `elements` bucket. Do not let the global WordPress shape catalog replace
     * those entries, otherwise placed elements can reload without their selectable
     * variation tree on the frontend.
     */
    private function extractElementDefinitionsFromPayload(mixed $payload) : array
    {
        if (! is_array($payload) || $payload === []) {
            return [];
        }

        $items = [];
        foreach (['elements', 'runtimeElements'] as $bucketKey) {
            if (! isset($payload[$bucketKey]) || ! is_array($payload[$bucketKey])) {
                continue;
            }
            foreach ($payload[$bucketKey] as $item) {
                if (is_array($item)) {
                    $items[] = $item;
                }
            }
        }

        if (isset($payload['catalog']['elements']) && is_array($payload['catalog']['elements'])) {
            foreach ($payload['catalog']['elements'] as $item) {
                if (is_array($item) && $this->looksLikeReusableElement($item)) {
                    $items[] = $item;
                }
            }
        }

        foreach (['initialState', 'initialDocument', 'documentState', 'document', 'state'] as $key) {
            if (isset($payload[$key]) && is_array($payload[$key])) {
                $items = array_merge($items, $this->extractElementDefinitionsFromPayload($payload[$key]));
            }
        }

        if (isset($payload['legacy']) && is_array($payload['legacy'])) {
            $items = array_merge($items, $this->extractElementDefinitionsFromPayload($payload['legacy']));
        }

        return array_values(array_filter($items, 'is_array'));
    }

    private function looksLikeReusableElement(array $item) : bool
    {
        if (isset($item['variations']) && is_array($item['variations'])) {
            return true;
        }
        if (isset($item['children']) && is_array($item['children'])) {
            return true;
        }
        foreach (['familyId', '_asukaElementFamilyId', 'asukaElementId', '_asukaElementId'] as $key) {
            if (isset($item[$key]) && trim((string) $item[$key]) !== '') {
                return true;
            }
        }
        $kind = strtolower(trim((string) ($item['itemType'] ?? $item['elementType'] ?? $item['kind'] ?? '')));
        return in_array($kind, ['element', 'elements', 'reusable-element'], true);
    }

    private function elementDefinitionKey(array $item) : string
    {
        foreach (['id', 'familyId', 'name', 'label', '_asukaElementId', 'asukaElementId'] as $key) {
            $value = sanitize_key((string) ($item[$key] ?? ''));
            if ($value !== '') {
                return $value;
            }
        }

        return '';
    }

    private function countElementVariations(array $item) : int
    {
        $variations = isset($item['variations']) && is_array($item['variations']) ? $item['variations'] : [];
        $count = count($variations);
        foreach ($variations as $variation) {
            if (is_array($variation)) {
                $count += $this->countElementVariations($variation);
            }
        }

        return $count;
    }

    private function preferElementDefinition(array $current, array $incoming) : array
    {
        $currentVariationCount = $this->countElementVariations($current);
        $incomingVariationCount = $this->countElementVariations($incoming);
        if ($incomingVariationCount > $currentVariationCount) {
            return array_replace($current, $incoming);
        }
        if ($currentVariationCount > $incomingVariationCount) {
            return array_replace($incoming, $current);
        }

        $currentChildren = isset($current['children']) && is_array($current['children']) ? count($current['children']) : 0;
        $incomingChildren = isset($incoming['children']) && is_array($incoming['children']) ? count($incoming['children']) : 0;
        if ($incomingChildren > $currentChildren) {
            return array_replace($current, $incoming);
        }

        return array_replace($incoming, $current);
    }

    private function mergeElementDefinitions(array ...$groups) : array
    {
        $keyed = [];
        $loose = [];
        foreach ($groups as $group) {
            foreach ($group as $item) {
                if (! is_array($item)) {
                    continue;
                }
                $key = $this->elementDefinitionKey($item);
                if ($key === '') {
                    $loose[] = $item;
                    continue;
                }
                $keyed[$key] = isset($keyed[$key]) && is_array($keyed[$key])
                    ? $this->preferElementDefinition($keyed[$key], $item)
                    : $item;
            }
        }

        return array_values(array_merge($loose, array_values($keyed)));
    }

    private function mergeCatalogFamily(array $existing, array $filtered, array $assetFilters, string $family) : array
    {
        $filtered = array_values(array_filter($filtered, 'is_array'));

        if ($this->shouldReplaceCatalogFamily($assetFilters, $family)) {
            return $filtered;
        }

        return $this->mergeById($existing, $filtered);
    }

    private function shouldReplaceCatalogFamily(array $assetFilters, string $family) : bool
    {
        $filter = $assetFilters[$family] ?? [];
        if (! is_array($filter)) {
            return false;
        }

        $mode = (string) ($filter['mode'] ?? 'all');

        return in_array($mode, ['selected', 'none'], true);
    }

    public function mergeById(array $first, array $second) : array
    {
        $merged = [];
        foreach ([$first, $second] as $group) {
            foreach ($group as $item) {
                if (! is_array($item)) {
                    continue;
                }
                $id = sanitize_key((string) ($item['id'] ?? ''));
                if ($id === '') {
                    $merged[] = $item;
                    continue;
                }
                $merged[$id] = isset($merged[$id]) && is_array($merged[$id]) ? array_replace($merged[$id], $item) : $item;
            }
        }
        return array_values($merged);
    }
}
