<?php

declare(strict_types=1);

namespace FleekDash\Admin\Menu;

/**
 * Remaps role menu overrides when WordPress changes menu position suffixes (e.g. after a plugin update).
 *
 * MenuItemFormatter builds unique_id as "{source_slug}_{positionSuffix}" where positionSuffix comes from
 * WordPress $menu / $submenu array keys (see {@see MenuHelpers::submenuArrayKeyToUniqueIdSuffix()} for submenu).
 * Fractional submenu keys stay distinct (e.g. 59.5 vs 59.9) so React keys do not collide; role overrides
 * keep matching {@see MenuOverrideStaleKeyNormalizer::disambiguateByNumericSuffix()} patterns when positions shift.
 */
final class MenuOverrideStaleKeyNormalizer
{
    private bool $mutated = false;

    /**
     * @param array<int, array<string, mixed>> $items
     * @param array<string, mixed> $overrides
     * @return array<string, mixed>
     */
    public function normalizeForTree(array $items, array $overrides): array
    {
        $this->mutated = false;
        if ($overrides === []) {
            return $overrides;
        }

        $validIds = $this->collectValidIds($items);
        $slugIndex = $this->buildSourceSlugIndex($items);
        $result = [];

        foreach ($overrides as $key => $override) {
            if (!\is_string($key) || $key === '' || !\is_array($override)) {
                $this->mutated = true;
                continue;
            }

            $targetId = $this->resolveTargetId($key, $validIds, $slugIndex);
            if ($targetId === null || $targetId === '') {
                $this->mutated = true;
                continue;
            }

            if ($targetId !== $key) {
                $this->mutated = true;
            }

            if (isset($result[$targetId])) {
                $this->mutated = true;
                $result[$targetId] = \array_merge($result[$targetId], $override);
            } else {
                $result[$targetId] = $override;
            }

            $result[$targetId]['id'] = $targetId;
        }

        return $result;
    }

    public function didMutate(): bool
    {
        return $this->mutated;
    }

    /**
     * @param array<int, array<string, mixed>> $items
     * @return array<string, true>
     */
    private function collectValidIds(array $items): array
    {
        $ids = [];
        $this->walkItems($items, static function (array $item) use (&$ids): void {
            foreach (['unique_id', 'id'] as $field) {
                if (isset($item[$field]) && \is_string($item[$field]) && $item[$field] !== '') {
                    $ids[$item[$field]] = true;
                }
            }
        });

        return $ids;
    }

    /**
     * @param array<int, array<string, mixed>> $items
     * @return array<string, array<int, string>>
     */
    private function buildSourceSlugIndex(array $items): array
    {
        $index = [];
        $this->walkItems($items, static function (array $item) use (&$index): void {
            if (!isset($item['source_slug']) || !\is_string($item['source_slug'])) {
                return;
            }
            $slug = $item['source_slug'];
            if ($slug === '') {
                return;
            }
            $uid = '';
            if (isset($item['unique_id']) && \is_string($item['unique_id'])) {
                $uid = $item['unique_id'];
            } elseif (isset($item['id']) && \is_string($item['id'])) {
                $uid = $item['id'];
            }
            if ($uid === '') {
                return;
            }
            if (!isset($index[$slug])) {
                $index[$slug] = [];
            }
            $index[$slug][] = $uid;
        });

        foreach ($index as $slug => $list) {
            $index[$slug] = \array_values(\array_unique($list));
        }

        return $index;
    }

    /**
     * @param callable(array<string,mixed>):void $visitor
     * @param array<int, array<string, mixed>> $items
     */
    private function walkItems(array $items, callable $visitor): void
    {
        foreach ($items as $item) {
            if (!\is_array($item)) {
                continue;
            }
            $visitor($item);
            if (isset($item['submenu']) && \is_array($item['submenu'])) {
                $this->walkItems($item['submenu'], $visitor);
            }
            if (isset($item['children']) && \is_array($item['children'])) {
                $this->walkItems($item['children'], $visitor);
            }
        }
    }

    /**
     * @param array<string, true> $validIds
     * @param array<string, array<int, string>> $slugIndex
     */
    private function resolveTargetId(string $key, array $validIds, array $slugIndex): ?string
    {
        $target = null;
        if (isset($validIds[$key])) {
            $target = $key;
        } else {
            $bestSlug = $this->pickLongestMatchingSlug($key, $slugIndex);
            if ($bestSlug !== null) {
                $ids = $slugIndex[$bestSlug];
                if (\count($ids) === 1) {
                    $candidate = $ids[0];
                    $target = isset($validIds[$candidate]) ? $candidate : null;
                } else {
                    $target = $this->disambiguateByNumericSuffix($key, $ids, $validIds);
                }
            }
        }

        return $target;
    }

    /**
     * @param array<string, array<int, string>> $slugIndex
     */
    private function pickLongestMatchingSlug(string $key, array $slugIndex): ?string
    {
        $bestSlug = null;
        $bestLen = -1;
        foreach (\array_keys($slugIndex) as $slug) {
            if ($slug === '') {
                continue;
            }
            $matches = ($key === $slug) || \str_starts_with($key, $slug . '_');
            if (!$matches) {
                continue;
            }
            $len = \strlen($slug);
            if ($len > $bestLen) {
                $bestLen = $len;
                $bestSlug = $slug;
            }
        }

        return $bestSlug;
    }

    /**
     * @param array<int, string> $ids
     * @param array<string, true> $validIds
     */
    private function disambiguateByNumericSuffix(string $key, array $ids, array $validIds): ?string
    {
        if (!\preg_match('/_(\d+(?:\.\d+)?)$/', $key, $matches)) {
            return null;
        }

        $suffix = $matches[1];
        $needle = '_' . $suffix;
        $found = [];
        foreach ($ids as $id) {
            if (!isset($validIds[$id])) {
                continue;
            }
            if (\str_ends_with($id, $needle)) {
                $found[] = $id;
            }
        }

        if (\count($found) === 1) {
            return $found[0];
        }

        return null;
    }
}
