<?php

declare(strict_types=1);

namespace FleekDash\API;

/**
 * Merges REST $menu / $submenu globals with persisted per-user snapshots for {@see MenuDataAPI}.
 * Extracted to keep MenuDataAPI orchestration readable and testable in isolation.
 */
final class MenuMergeStrategy
{
    /**
     * When comparing snapshot vs REST globals, only prefer globals if their submenu row count reaches
     * this ratio of the snapshot's count.
     */
    private const STALE_SNAPSHOT_SUBMENU_PARITY_RATIO = 0.7;

    /**
     * WordPress core top-level slugs (must match {@see \FleekDash\Admin\Menu\CoreMenuRestoreService}).
     *
     * @var array<int, string>
     */
    private const CORE_TOP_LEVEL_SLUGS = [
        'index.php',
        'edit.php',
        'upload.php',
        'edit.php?post_type=page',
        'edit-comments.php',
        'themes.php',
        'plugins.php',
        'users.php',
        'tools.php',
        'options-general.php',
    ];

    /**
     * Submenu parent file keys that belong to WordPress core (Dashboard, Posts, Media, CPT roots, …).
     *
     * @var array<string, true>
     */
    private const NATIVE_SUBMENU_PARENT_EXACT = [
        'index.php' => true,
        'edit.php' => true,
        'upload.php' => true,
        'edit.php?post_type=page' => true,
        'edit-comments.php' => true,
        'themes.php' => true,
        'plugins.php' => true,
        'users.php' => true,
        'profile.php' => true,
        'tools.php' => true,
        'options-general.php' => true,
    ];

    /**
     * @param array<int, mixed> $globalsMenu
     * @param array<string, mixed> $globalsSubmenu
     * @param array<int, mixed> $snapshotMenu
     * @param array<string, mixed> $snapshotSubmenu
     * @return array{0: array<int,mixed>, 1: array<string,mixed>, 2: string, 3: array<string, mixed>, 4: array<int,mixed>}
     */
    public function mergeGlobalsWithSnapshot(
        array $globalsMenu,
        array $globalsSubmenu,
        array $snapshotMenu,
        array $snapshotSubmenu,
        string $snapshotSource,
    ): array {
        $globalsSubmenuRows = $this->countSubmenuRows($globalsSubmenu);
        $globalsMenuCount = \count($globalsMenu);
        $snapshotSubmenuRows = $this->countSubmenuRows($snapshotSubmenu);

        $snapshotIsStaleSource = $snapshotSource === 'snapshot_stale';
        $snapshotIsFreshSource = $snapshotSource === 'snapshot_fresh';

        $useStaleSnapshotMerge = $snapshotIsStaleSource && $globalsMenuCount > 0;
        if ($useStaleSnapshotMerge) {
            $mergedSubmenu = $this->mergeSubmenuPreferringGlobalsForNativeParents(
                $globalsSubmenu,
                $snapshotSubmenu,
                true,
            );
            $mergedMenu = $this->mergeTopLevelMenusPerSlug($globalsMenu, $snapshotMenu, true);
        } else {
            $mergedSubmenu = $this->mergeSubmenuPreferringGlobalsForNativeParents(
                $globalsSubmenu,
                $snapshotSubmenu,
                false,
            );
            $mergedMenu = $this->mergeTopLevelMenusPerSlug($globalsMenu, $snapshotMenu, false);
        }
        $globalsSubmenuParityVersusSnapshot = $this->globalsSubmenuLooksCompleteVersusSnapshot(
            $globalsSubmenuRows,
            $snapshotSubmenuRows,
        );
        $preferGlobalsOverStale = $snapshotIsStaleSource
            && $globalsMenuCount > 0
            && $globalsSubmenuParityVersusSnapshot;

        $preferGlobalsOverFreshSnapshot = $snapshotIsFreshSource
            && $globalsMenuCount > 0
            && $snapshotSubmenuRows > $globalsSubmenuRows
            && $this->globalsSubmenuStrictlyBeatsParityThreshold(
                $globalsSubmenuRows,
                $snapshotSubmenuRows,
            );

        $preferGlobalsOverSnapshot = $preferGlobalsOverStale || $preferGlobalsOverFreshSnapshot;

        $resolutionMeta = [
            'snapshot_resolver_source' => $snapshotSource,
            'globals_menu_count' => $globalsMenuCount,
            'snapshot_menu_count' => \count($snapshotMenu),
            'merged_menu_count' => \count($mergedMenu),
            'globals_submenu_rows' => $globalsSubmenuRows,
            'snapshot_submenu_rows' => $snapshotSubmenuRows,
            'merged_submenu_rows' => $this->countSubmenuRows($mergedSubmenu),
            'globals_tree_empty' => $globalsMenuCount === 0 && $globalsSubmenuRows === 0,
            'snapshot_tree_empty' => empty($snapshotMenu) && $snapshotSubmenuRows === 0,
            'merged_tree_empty' => empty($mergedMenu) && $this->countSubmenuRows($mergedSubmenu) === 0,
            // Client uses all-or-nothing protection: never replace a non-empty menu with an empty payload.
            'all_or_nothing_empty_guard_expected' => true,
            'globals_submenu_parity_vs_snapshot' => $globalsSubmenuParityVersusSnapshot,
            'prefer_globals_over_stale_snapshot' => $preferGlobalsOverStale,
            'prefer_globals_over_fresh_snapshot' => $preferGlobalsOverFreshSnapshot,
            'prefer_globals_over_snapshot' => $preferGlobalsOverSnapshot,
            'merge_strategy' => $useStaleSnapshotMerge
                ? 'native_richer_wins_stale_plugin_globals_first_omit_snapshot_only'
                : 'native_submenu_tie_prefers_snapshot_nonnative_tie_globals',
            'top_level_merge_strategy' => $useStaleSnapshotMerge
                ? 'stale_snapshot_core_from_snapshot_non_core_globals_only'
                : 'globals_snapshot_per_slug_core_from_snapshot',
            'native_parent_submenu_rows' => [
                'edit.php' => $this->countSubmenuRowsForParent($mergedSubmenu, 'edit.php'),
                'themes.php' => $this->countSubmenuRowsForParent($mergedSubmenu, 'themes.php'),
            ],
        ];

        $chosenSource = 'merged_globals_snapshot';

        return [$mergedMenu, $mergedSubmenu, $chosenSource, $resolutionMeta, $snapshotMenu];
    }

    /**
     * Snapshot rows keyed by source slug for CoreMenuRestoreService label fallback (wp-admin HTML titles).
     *
     * @param array<int, mixed> $snapshotMenu
     * @return array<string, array<int, mixed>>
     */
    public function buildSnapshotMenuRowBySlug(array $snapshotMenu): array
    {
        $out = [];
        foreach ($snapshotMenu as $item) {
            if (!\is_array($item) || !isset($item[2]) || !\is_string($item[2])) {
                continue;
            }
            $slug = \trim($item[2]);
            if ($slug !== '') {
                $out[$slug] = $item;
            }
        }
        return $out;
    }

    /**
     * @param array<int, mixed> $globalsMenu
     * @param array<int, mixed> $snapshotMenu
     * @return array<int, mixed>
     */
    private function mergeTopLevelMenusPerSlug(array $globalsMenu, array $snapshotMenu, bool $snapshotStale = false): array
    {
        $globBySlug = $this->indexMenuRowsBySlug($globalsMenu);
        $snapBySlug = $this->indexMenuRowsBySlug($snapshotMenu);
        $allSlugs = \array_unique(\array_merge(\array_keys($globBySlug), \array_keys($snapBySlug)));
        $pairs = [];
        foreach ($allSlugs as $slug) {
            $picked = $this->pickMergedTopLevelRow($slug, $globBySlug, $snapBySlug, $snapshotStale);
            if ($picked !== null) {
                $pairs[] = $picked;
            }
        }
        \usort($pairs, static function (array $a, array $b): int {
            $ka = isset($a['pos']) && \is_numeric($a['pos']) ? (float) $a['pos'] : 0.0;
            $kb = isset($b['pos']) && \is_numeric($b['pos']) ? (float) $b['pos'] : 0.0;
            return $ka <=> $kb;
        });
        $out = [];
        foreach ($pairs as $p) {
            $pos = $p['pos'];
            $item = $p['item'];
            $posKey = $pos;
            while (isset($out[$posKey])) {
                $posKey += 0.0001;
            }
            $out[$posKey] = $item;
        }
        \ksort($out, SORT_NUMERIC);
        return $out;
    }

    /**
     * @param array<int, mixed> $menu
     * @return array<string, array{key: int|float, item: array<int, mixed>}>
     */
    private function indexMenuRowsBySlug(array $menu): array
    {
        $map = [];
        foreach ($menu as $key => $item) {
            if (!\is_array($item) || !isset($item[2]) || !\is_string($item[2])) {
                continue;
            }
            $slug = \trim($item[2]);
            if ($slug === '') {
                continue;
            }
            $map[$slug] = ['key' => $key, 'item' => $item];
        }
        return $map;
    }

    /**
     * @param array<string, array{key: int|float, item: array<int, mixed>}> $globBySlug
     * @param array<string, array{key: int|float, item: array<int, mixed>}> $snapBySlug
     * @return array{pos: int|float, item: array<int, mixed>}|null
     */
    private function pickMergedTopLevelRow(string $slug, array $globBySlug, array $snapBySlug, bool $snapshotStale): ?array
    {
        $g = $globBySlug[$slug] ?? null;
        $s = $snapBySlug[$slug] ?? null;
        $core = \in_array($slug, self::CORE_TOP_LEVEL_SLUGS, true);
        $picked = null;
        if ($core) {
            if ($s !== null) {
                $picked = ['pos' => $s['key'], 'item' => $s['item']];
            } elseif ($g !== null) {
                $picked = ['pos' => $g['key'], 'item' => $g['item']];
            }
        } elseif ($snapshotStale) {
            if ($g !== null) {
                $picked = ['pos' => $g['key'], 'item' => $g['item']];
            }
        } elseif ($g !== null) {
            $picked = ['pos' => $g['key'], 'item' => $g['item']];
        } elseif ($s !== null) {
            $picked = ['pos' => $s['key'], 'item' => $s['item']];
        }
        return $picked;
    }

    private function isNativeSubmenuParent(string $parent): bool
    {
        if (isset(self::NATIVE_SUBMENU_PARENT_EXACT[$parent])) {
            return true;
        }

        return \preg_match('/^edit\.php\?post_type=[^&]+$/', $parent) === 1;
    }

    /**
     * @param array<string, mixed> $globalsSubmenu
     * @param array<string, mixed> $snapshotSubmenu
     * @return array<string, mixed>
     */
    private function mergeSubmenuPreferringGlobalsForNativeParents(
        array $globalsSubmenu,
        array $snapshotSubmenu,
        bool $staleSnapshotMode,
    ): array {
        $parents = \array_unique(\array_merge(
            \array_keys($globalsSubmenu),
            \array_keys($snapshotSubmenu),
        ));
        \sort($parents, SORT_STRING);
        $out = [];

        foreach ($parents as $parent) {
            $gExists = isset($globalsSubmenu[$parent]) && \is_array($globalsSubmenu[$parent]);
            $g = $gExists ? $globalsSubmenu[$parent] : [];
            $s = isset($snapshotSubmenu[$parent]) && \is_array($snapshotSubmenu[$parent])
                ? $snapshotSubmenu[$parent] : [];

            $picked = $this->pickSubmenuBranchForParent(
                $parent,
                $g,
                $s,
                $gExists,
                \count($g),
                \count($s),
                $staleSnapshotMode,
            );
            if ($picked !== null) {
                $out[$parent] = $picked;
            }
        }

        return $out;
    }

    /**
     * @param array<int, mixed> $g
     * @param array<int, mixed> $s
     * @return array<int, mixed>|null
     */
    private function pickSubmenuBranchForParent(
        string $parent,
        array $g,
        array $s,
        bool $gExists,
        int $cg,
        int $cs,
        bool $staleSnapshotMode,
    ): ?array {
        $result = null;
        $isStalePluginOnly = $staleSnapshotMode && !$this->isNativeSubmenuParent($parent);
        if ($isStalePluginOnly) {
            // Stale snapshot mode for plugin parents:
            // - Never resurrect snapshot-only parents (avoids ghost plugin trees after removal)
            // - Prefer current globals whenever they have rows (avoid downgrading with stale snapshot data)
            // - Only borrow snapshot when globals still expose the parent key but currently have zero rows
            if ($cg > 0) {
                $result = $g;
            } elseif ($gExists && $cs > 0) {
                $result = $s;
            }
        } elseif ($this->isNativeSubmenuParent($parent)) {
            // WordPress-native trees (Posts, Media, Appearance, …): snapshot comes from real wp-admin capture.
            // REST $submenu often matches or exceeds row count but can omit or differ on submenu items; on tie,
            // prefer snapshot so we do not replace a full wp-admin tree with an incomplete REST-built tree.
            $result = $cg > $cs ? $g : $s;
        } else {
            // Non-native (plugin) parents with fresh snapshot: richer branch wins; tie → globals (current REST).
            $result = $cg >= $cs ? $g : $s;
        }

        return $result;
    }

    /**
     * @param array<string,mixed> $rawSubmenu
     */
    private function countSubmenuRows(array $rawSubmenu): int
    {
        $rows = 0;
        foreach ($rawSubmenu as $submenuItems) {
            if (\is_array($submenuItems)) {
                $rows += \count($submenuItems);
            }
        }
        return $rows;
    }

    /**
     * @param array<string, mixed> $rawSubmenu
     */
    private function countSubmenuRowsForParent(array $rawSubmenu, string $parent): int
    {
        if (!isset($rawSubmenu[$parent]) || !\is_array($rawSubmenu[$parent])) {
            return 0;
        }
        return \count($rawSubmenu[$parent]);
    }

    private function globalsSubmenuLooksCompleteVersusSnapshot(
        int $globalsSubmenuRows,
        int $snapshotSubmenuRows,
    ): bool {
        $meetsRatio = false;
        if ($snapshotSubmenuRows <= 0) {
            $meetsRatio = true;
        } elseif ($globalsSubmenuRows > 0) {
            $threshold = (int) \ceil($snapshotSubmenuRows * self::STALE_SNAPSHOT_SUBMENU_PARITY_RATIO);
            $meetsRatio = $globalsSubmenuRows >= $threshold;
        }
        return $meetsRatio;
    }

    private function globalsSubmenuStrictlyBeatsParityThreshold(
        int $globalsSubmenuRows,
        int $snapshotSubmenuRows,
    ): bool {
        if ($snapshotSubmenuRows <= 0) {
            return true;
        }
        if ($globalsSubmenuRows <= 0) {
            return false;
        }
        $threshold = (int) \ceil($snapshotSubmenuRows * self::STALE_SNAPSHOT_SUBMENU_PARITY_RATIO);

        return $globalsSubmenuRows > $threshold;
    }
}
