<?php

declare(strict_types=1);

namespace FleekDash\Admin\Menu;

/**
 * Menu Item Formatter
 *
 * Formats WordPress menu arrays into FleekDash-compatible menu structure
 * Handles menu item processing; delegates route/URL logic to MenuRouteResolver
 *
 * @package FleekDash\Admin\Menu
 */
class MenuItemFormatter
{
    /** @deprecated Use MenuRouteResolver::NEW_TAB_URLS_OPTION */
    public const NEW_TAB_URLS_OPTION = MenuRouteResolver::NEW_TAB_URLS_OPTION;

    private const FLEEKDASH_ADMIN_PAGE = 'admin.php?page=fleekdash';

    private MenuRouteResolver $routeResolver;
    /** @var callable(string):bool */
    private $canAccess;
    private bool $includeDeniedItems = false;

    public function __construct(RouteConverter $routeConverter)
    {
        $this->routeResolver = new MenuRouteResolver($routeConverter);
        $this->canAccess = static fn(string $capability): bool => \current_user_can($capability);
    }

    /**
     * @param array $menu WordPress menu array
     * @param array $submenu WordPress submenu array
     * @param null|callable(string):bool $canAccess Capability gate (defaults to current_user_can)
     * @return array Formatted menu items
     */
    public function formatMenuItems(array $menu, array $submenu, ?callable $canAccess = null, bool $includeDeniedItems = false): array
    {
        if (empty($menu)) {
            return [];
        }
        $restoreCanAccess = $this->canAccess;
        $restoreIncludeDeniedItems = $this->includeDeniedItems;
        if ($canAccess !== null) {
            $this->canAccess = $canAccess;
        }
        $this->includeDeniedItems = $includeDeniedItems;

        try {
            $admin_base_url = \admin_url();
            $admin_base_path = trailingslashit(parse_url($admin_base_url, PHP_URL_PATH) ?? '');

            // Sort menu by keys (positions) to ensure correct order
            // WordPress uses array keys as positions, not sequential indices
            ksort($menu, SORT_NUMERIC);

            $formatted_menu = [];

            foreach ($menu as $menu_index => $menu_item) {
                $menuPosition = \is_numeric($menu_index) ? (float) $menu_index : 0.0;
                $formatted_item = $this->processMenuEntry($menu_item, $menuPosition, $submenu, $admin_base_url, $admin_base_path);
                if ($formatted_item !== null) {
                    $formatted_menu[] = $formatted_item;
                }
            }

            $canonicalizer = new MenuFormattedTopLevelCanonicalizer();
            $formatted_menu = $canonicalizer->canonicalize($formatted_menu);
            usort($formatted_menu, function ($a, $b) {
                $pos_a = isset($a['position']) ? floatval($a['position']) : 0;
                $pos_b = isset($b['position']) ? floatval($b['position']) : 0;
                return $pos_a <=> $pos_b;
            });
            return $formatted_menu;
        } finally {
            $this->canAccess = $restoreCanAccess;
            $this->includeDeniedItems = $restoreIncludeDeniedItems;
        }
    }

    /**
     * WordPress injects visual separators as fake $menu rows (slug {@code separator}, {@code separator1}, …
     * or class {@code wp-menu-separator}). They must not become sidebar links in FleekDash.
     *
     * @param array<int|string, mixed> $menuRow WordPress $menu item or $submenu row
     */
    private function isWordPressAdminMenuSeparator(array $menuRow): bool
    {
        $slug = isset($menuRow[2]) && \is_string($menuRow[2]) ? $menuRow[2] : '';
        $classes = isset($menuRow[4]) && \is_string($menuRow[4]) ? $menuRow[4] : '';
        $slugMatches = $slug !== '' && (
            $slug === 'wp-menu-separator'
            || \preg_match('/^separator\d*$/', $slug) === 1
        );
        $classMatches = $classes !== '' && \str_contains($classes, 'wp-menu-separator');

        return $slugMatches || $classMatches;
    }

    /**
     * Process a single menu entry (returns null if entry should be skipped)
     *
     * @param mixed $menuItem WordPress menu item
     * @param float $menuIndex Menu index (position)
     * @param array $submenu WordPress submenu array
     * @param string $adminBaseUrl Admin base URL
     * @param string $adminBasePath Admin base path
     * @return array|null Formatted menu item or null to skip
     */
    private function processMenuEntry($menuItem, float $menuIndex, array $submenu, string $adminBaseUrl, string $adminBasePath): ?array
    {
        if ($menuItem === null || !\is_array($menuItem)) {
            return null;
        }
        $rawSlug = $menuItem[2] ?? null;
        $skipEntry =
            $this->isWordPressAdminMenuSeparator($menuItem)
            || (($menuItem[0] ?? '') === '' && (!\is_string($rawSlug) || $rawSlug === ''));
        if ($skipEntry) {
            return null;
        }

        $formattedItem = $this->formatMenuItem($menuItem, $menuIndex, $adminBaseUrl, $adminBasePath);

        if ($rawSlug !== null && isset($submenu[$rawSlug])) {
            $formattedItem = $this->applyFormattedSubmenuToItem(
                $formattedItem,
                $submenu[$rawSlug],
                $adminBaseUrl,
                $adminBasePath,
            );
        }

        return $formattedItem;
    }

    /**
     * @param array<string, mixed> $item
     * @param array<int|string, mixed> $submenuRows
     * @return array<string, mixed>
     */
    private function applyFormattedSubmenuToItem(
        array $item,
        array $submenuRows,
        string $adminBaseUrl,
        string $adminBasePath,
    ): array {
        $result = $item;
        $formattedSubmenu = $this->formatSubmenu($submenuRows, $adminBaseUrl, $adminBasePath);
        if ($formattedSubmenu === []) {
            return $result;
        }

        $result['submenu'] = $formattedSubmenu;
        if (\count($formattedSubmenu) === 1) {
            $onlyChild = $formattedSubmenu[0];
            if ($this->shouldPromoteRedundantSingleSubmenu($result, $onlyChild)) {
                $result = $this->promoteSingleSubmenu($result, $onlyChild);
            }
        }

        return $result;
    }

    /**
     * Format a single menu item
     *
     * @param array $menu_item WordPress menu item
     * @param float $menu_index Menu index (position)
     * @param string $admin_base_url Admin base URL
     * @param string $admin_base_path Admin base path
     * @return array Formatted menu item
     */
    private function formatMenuItem(array $menu_item, float $menu_index, string $admin_base_url, string $admin_base_path): array
    {
        $raw_slug = $menu_item[2];
        $decoded_slug = html_entity_decode($raw_slug, ENT_QUOTES | ENT_HTML5, 'UTF-8');

        $routeData = $this->routeResolver->resolveRouteData($raw_slug, $decoded_slug, $admin_base_url, $admin_base_path);

        $titleTop = isset($menu_item[0]) && (\is_string($menu_item[0]) || \is_numeric($menu_item[0]))
            ? (string) $menu_item[0]
            : '';
        $menu_name = $this->cleanMenuName($titleTop, $raw_slug);
        $href_base = $this->routeResolver->constructHref($routeData['target_path'], $routeData['target_hash']);
        $menu_position = (float) $menu_index;

        $url = $routeData['url'];
        if (empty($url) && !empty($routeData['target_path'])) {
            $url = \admin_url(self::FLEEKDASH_ADMIN_PAGE) . '#/wp/' . $routeData['target_path'];
        }

        $update_count = $this->getMenuItemUpdateCount($raw_slug, $decoded_slug, $titleTop);

        return [
            'name' => $menu_name,
            'href' => $href_base,
            'icon' => 'default',
            'capability' => $menu_item[1],
            'url' => $url,
            'position' => $menu_position,
            'order' => $menu_position,
            'update_count' => $update_count,
            'is_wp_native' => $routeData['is_wp_native'],
            'route_type' => $this->routeResolver->resolveRouteType($routeData['route_info'], $routeData['is_wp_native']),
            'target' => $routeData['should_open_new_tab'] ? '_blank' : '_self',
            'id' => $raw_slug . '_' . $menu_position,
            'unique_id' => $raw_slug . '_' . $menu_position,
            'source_slug' => $raw_slug,
        ];
    }

    /**
     * Get update count for a menu item (Updates vs other items)
     */
    private function getMenuItemUpdateCount(string $rawSlug, string $decodedSlug, string $menuItemHtml): int
    {
        if ($rawSlug === 'update-core.php' || $decodedSlug === 'update-core.php') {
            if (!\class_exists('\FleekDash\Services\UpdateCountersService')) {
                require_once FLEEKDASH_PLUGIN_DIR . 'includes/Services/UpdateCountersService.php';
            }
            return \FleekDash\Services\UpdateCountersService::getTotalUpdateCount();
        }
        return MenuHelpers::getUpdateCount($menuItemHtml);
    }

    /**
     * Format submenu items
     *
     * @param array $submenu_items Submenu items array
     * @param string $admin_base_url Admin base URL
     * @param string $admin_base_path Admin base path
     * @return array Formatted submenu items
     */
    private function formatSubmenu(array $submenu_items, string $admin_base_url, string $admin_base_path): array
    {
        $working = $submenu_items;
        $this->uksortSubmenuByMenuPositionKeys($working);

        $formatted_submenu = [];
        $stableOrdinal = 0;

        foreach ($working as $sub_key => $sub_item) {
            $fromKey = $this->coerceSubmenuPositionKeyToFloatOrNull($sub_key);
            $wpPositionOrder = $fromKey !== null ? $fromKey : (100000.0 + (float) $stableOrdinal);
            $uniqueIdSuffix = MenuHelpers::submenuArrayKeyToUniqueIdSuffix($sub_key);
            $formatted = $this->processSubmenuItem(
                $sub_item,
                $uniqueIdSuffix,
                $wpPositionOrder,
                $admin_base_url,
                $admin_base_path,
            );
            if ($formatted !== null) {
                $formatted_submenu[] = $formatted;
            }
            $stableOrdinal += 1;
        }

        return $formatted_submenu;
    }

    /**
     * Match WordPress admin submenu ordering: numeric position keys (int, float, or numeric strings
     * including fractional positions) first; non-numeric keys after, compared as strings.
     *
     * @param array<int|string, mixed> $rows
     */
    private function uksortSubmenuByMenuPositionKeys(array &$rows): void
    {
        uksort($rows, [$this, 'compareSubmenuPositionKeys']);
    }

    private function compareSubmenuPositionKeys(mixed $keyA, mixed $keyB): int
    {
        $posA = $this->coerceSubmenuPositionKeyToFloatOrNull($keyA);
        $posB = $this->coerceSubmenuPositionKeyToFloatOrNull($keyB);
        $cmp = \strcmp((string) $keyA, (string) $keyB);
        if ($posA !== null && $posB !== null) {
            $cmp = $posA <=> $posB;
        } elseif ($posA !== null) {
            $cmp = -1;
        } elseif ($posB !== null) {
            $cmp = 1;
        }

        return $cmp;
    }

    private function coerceSubmenuPositionKeyToFloatOrNull(mixed $key): ?float
    {
        $out = null;
        if (\is_int($key) || \is_float($key) || (\is_string($key) && $key !== '' && is_numeric($key))) {
            $out = (float) $key;
        }

        return $out;
    }

    /**
     * Attach WordPress submenu rows to a synthetic top-level item (e.g. restored core/CPT).
     * Uses the same formatting and single-child promotion as processMenuEntry.
     *
     * @param array<string, mixed> $parent Pre-built item (source_slug must match $submenu key in WP global)
     * @param array<int|string, mixed> $submenuRows WordPress $submenu[$parentSlug] rows
     * @return array<string, mixed>
     */
    public function attachSubmenuRowsToTopLevel(array $parent, array $submenuRows): array
    {
        $result = $parent;
        $formattedSubmenu = [];
        if ($submenuRows !== []) {
            $adminBaseUrl = \admin_url();
            $adminBasePath = trailingslashit(parse_url($adminBaseUrl, PHP_URL_PATH) ?? '');
            $formattedSubmenu = $this->formatSubmenu($submenuRows, $adminBaseUrl, $adminBasePath);
        }
        if ($formattedSubmenu !== []) {
            if (\count($formattedSubmenu) === 1) {
                $onlyChild = $formattedSubmenu[0];
                if ($this->shouldPromoteRedundantSingleSubmenu($parent, $onlyChild)) {
                    $result = $this->promoteSingleSubmenu($parent, $onlyChild);
                } else {
                    $result = $parent;
                    $result['submenu'] = $formattedSubmenu;
                }
            } else {
                $result = $parent;
                $result['submenu'] = $formattedSubmenu;
            }
        }

        return $result;
    }

    /**
     * Validate submenu item; returns raw slug if valid, null if should be skipped
     *
     * @param mixed $subItem WordPress submenu item
     * @return string|null Raw slug or null
     */
    private function validateSubmenuItem($subItem): ?string
    {
        if ($subItem === null || !\is_array($subItem)) {
            return null;
        }
        if (
            !$this->includeDeniedItems
            && !empty($subItem[1])
            && \is_string($subItem[1])
            && !($this->canAccess)($subItem[1])
        ) {
            return null;
        }
        $rawSlug = $subItem[2] ?? null;
        return \is_string($rawSlug) && $rawSlug !== '' ? $rawSlug : null;
    }

    /**
     * Process a single submenu item (returns null if item should be skipped)
     *
     * @param array|null $subItem WordPress submenu item
     * @param string $uniqueIdSuffix From {@see MenuHelpers::submenuArrayKeyToUniqueIdSuffix()} (WP $submenu key).
     * @param float $wpPositionOrder WordPress submenu position key as float (fractional positions preserved).
     * @param string $adminBaseUrl Admin base URL
     * @param string $adminBasePath Admin base path
     * @return array|null Formatted submenu item or null
     */
    private function processSubmenuItem(
        $subItem,
        string $uniqueIdSuffix,
        float $wpPositionOrder,
        string $adminBaseUrl,
        string $adminBasePath,
    ): ?array {
        if (\is_array($subItem) && $this->isWordPressAdminMenuSeparator($subItem)) {
            return null;
        }
        $rawSlug = $this->validateSubmenuItem($subItem);
        if ($rawSlug === null) {
            return null;
        }

        $decodedSlug = html_entity_decode($rawSlug, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $routeData = $this->routeResolver->resolveRouteData($rawSlug, $decodedSlug, $adminBaseUrl, $adminBasePath);

        $titleRaw = isset($subItem[0]) && (\is_string($subItem[0]) || \is_numeric($subItem[0]))
            ? (string) $subItem[0]
            : '';
        $submenuName = $this->cleanMenuName($titleRaw, $rawSlug);
        $subHrefBase = $this->routeResolver->constructHref($routeData['target_path'], $routeData['target_hash']);
        $subUpdateCount = $this->getMenuItemUpdateCount($rawSlug, $decodedSlug, $titleRaw);

        return [
            'name' => $submenuName,
            'href' => $subHrefBase,
            'capability' => $subItem[1],
            'url' => $routeData['url'],
            'order' => $wpPositionOrder,
            'update_count' => $subUpdateCount,
            'is_wp_native' => $routeData['is_wp_native'],
            'route_type' => $this->routeResolver->resolveRouteType($routeData['route_info'], $routeData['is_wp_native']),
            'target' => $routeData['should_open_new_tab'] ? '_blank' : '_self',
            'id' => $rawSlug . '_' . $uniqueIdSuffix,
            'unique_id' => $rawSlug . '_' . $uniqueIdSuffix,
            'source_slug' => $rawSlug,
        ];
    }

    /**
     * Clean menu name from HTML tags and entities
     *
     * @param string $raw_name Raw menu name
     * @param string $slug Menu slug for fallback
     * @return string Cleaned menu name
     */
    private function cleanMenuName(string $raw_name, string $slug): string
    {
        $menuName = preg_replace('/<span[^>]*>.*?<\/span>/i', '', $raw_name);
        $menuName = strip_tags($menuName);
        $menuName = html_entity_decode($menuName, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $menuName = trim($menuName);

        if (empty($menuName)) {
            $menuName = $this->getFallbackMenuName($raw_name, $slug);
        }

        return $menuName;
    }

    /**
     * Get fallback menu name when primary cleaning yields empty
     */
    private function getFallbackMenuName(string $rawName, string $slug): string
    {
        if (empty($rawName)) {
            return 'Untitled Menu';
        }

        $cleaned = preg_replace('/<[^>]*>/', '', $rawName);
        $cleaned = html_entity_decode($cleaned, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $cleaned = trim($cleaned);

        if (!empty($cleaned)) {
            return $cleaned;
        }

        $slugParts = explode('-', $slug);
        $lastPart = end($slugParts);
        return !empty($lastPart) ? ucfirst(str_replace('_', ' ', $lastPart)) : 'Untitled Menu';
    }

    /**
     * Promote only when the sole visible submenu is the same admin file as the parent (WordPress repeats
     * the parent slug as the first submenu). If the only child is another screen (e.g. a single "Edit post type"
     * entry), keep the top-level label and submenu tree.
     *
     * @param array<string, mixed> $parent
     * @param array<string, mixed> $singleChild
     */
    private function shouldPromoteRedundantSingleSubmenu(array $parent, array $singleChild): bool
    {
        $parentSlug = isset($parent['source_slug']) && \is_string($parent['source_slug']) ? $parent['source_slug'] : '';
        $childSlug = isset($singleChild['source_slug']) && \is_string($singleChild['source_slug']) ? $singleChild['source_slug'] : '';

        return $parentSlug !== '' && $childSlug !== '' && $parentSlug === $childSlug;
    }

    /**
     * Promote single submenu to main item
     *
     * @param array $main_item Main menu item
     * @param array $submenu_item Submenu item
     * @return array Updated main item
     */
    private function promoteSingleSubmenu(array $main_item, array $submenu_item): array
    {
        $main_item['name'] = $submenu_item['name'];
        $main_item['href'] = $submenu_item['href'];
        $main_item['capability'] = $submenu_item['capability'];
        $main_item['url'] = $submenu_item['url'];
        $main_item['update_count'] = $submenu_item['update_count'];
        $main_item['is_wp_native'] = $submenu_item['is_wp_native'];
        $main_item['route_type'] = $submenu_item['route_type'];
        $main_item['target'] = $submenu_item['target'];
        $main_item['unique_id'] = $submenu_item['unique_id'];
        unset($main_item['submenu']);

        return $main_item;
    }

    /** Get default new tab URLs (for API exposure) */
    public function getDefaultNewTabUrls(): array
    {
        return $this->routeResolver->getDefaultNewTabUrls();
    }
}
