<?php
/**
 * Toolbar Hierarchy Formatter
 *
 * Formats WordPress Admin Bar nodes into a hierarchical structure matching admin menu format.
 * Extracted from ToolbarDataAPI to reduce complexity and method count.
 *
 * @package FleekDash\API
 * @since V2.2
 */

namespace FleekDash\API;

use FleekDash\Admin\Menu\RouteConverter;

/**
 * Formats toolbar nodes into hierarchical structure compatible with FleekDash menu format
 *
 * @package FleekDash\API
 * @since V2.2
 */
class ToolbarHierarchyFormatter
{
    /**
     * @var RouteConverter
     */
    private $routeConverter;

    public function __construct(RouteConverter $routeConverter)
    {
        $this->routeConverter = $routeConverter;
    }

    /**
     * Format toolbar nodes into hierarchical structure
     *
     * @param array|null $nodes All toolbar nodes from WP_Admin_Bar->get_nodes() or snapshot
     * @return array Hierarchical toolbar structure with root items and nested children
     */
    public function format($nodes): array
    {
        if (empty($nodes) || !is_array($nodes)) {
            return [];
        }

        $itemsById = [];
        $rootItems = [];
        $positionCounter = 0;
        $adminBaseUrl = \admin_url();
        $parentById = $this->buildParentMapFromNodes($nodes);

        foreach ($nodes as $nodeId => $node) {
            $canonicalId = $this->resolveCanonicalNodeId($node, $nodeId);
            $nodeData = $this->extractNodeData($node, $nodeId);
            if ($nodeData === null) {
                continue;
            }

            $metaRaw = $nodeData['meta'] ?? [];
            $metaArray = \is_array($metaRaw) ? $metaRaw : [];
            $nodeData['href'] = ToolbarHrefResolver::resolve(
                (string) ($nodeData['href'] ?? ''),
                (string) ($nodeData['id'] ?? ''),
                $metaArray,
                (string) ($nodeData['title'] ?? ''),
            );

            if (ToolbarExcludedNodes::shouldExcludeNode((string) $canonicalId, (string) ($nodeData['href'] ?? ''))) {
                continue;
            }

            $nodeIdStr = (string) ($nodeData['id'] ?? '');
            if ($nodeIdStr !== '' && ToolbarExcludedNodes::hasExcludedAncestor($nodeIdStr, $parentById)) {
                continue;
            }

            $processedUrls = $this->processToolbarUrl($nodeData['href'], $adminBaseUrl);
            $formattedItem = $this->buildFormattedItem($nodeData, $processedUrls, $positionCounter);
            $itemsById[$nodeData['id']] = $formattedItem;
        }

        $this->assignToParentOrRoot($itemsById, $rootItems);
        $this->sortRootItems($rootItems);

        return $rootItems;
    }

    /**
     * Build node id => parent id map for ancestor exclusion checks.
     *
     * @param array<string, mixed> $nodes
     * @return array<string, mixed>
     */
    private function buildParentMapFromNodes($nodes): array
    {
        $map = [];
        foreach ($nodes as $nodeId => $node) {
            $nodeData = $this->extractNodeData($node, $nodeId);
            if ($nodeData === null) {
                continue;
            }
            $id = (string) ($nodeData['id'] ?? '');
            if ($id === '') {
                continue;
            }
            $map[$id] = $nodeData['parent'] ?? false;
        }

        return $map;
    }

    /**
     * Admin bar node id for exclusion checks (matches extractNodeData id resolution).
     *
     * @param mixed $node
     * @param string $nodeId
     */
    private function resolveCanonicalNodeId($node, $nodeId): string
    {
        if (\is_array($node)) {
            return (string) ($node['id'] ?? $nodeId);
        }
        if (\is_object($node) && !empty($node->id)) {
            return (string) $node->id;
        }
        return (string) $nodeId;
    }

    /**
     * Extract node data from WP_Admin_Bar object or array snapshot
     *
     * @param mixed $node
     * @param string $nodeId
     * @return array|null Node data or null if invalid
     */
    private function extractNodeData($node, $nodeId): ?array
    {
        if (is_array($node)) {
            $nodeId = $node['id'] ?? $nodeId;
            $title = $node['title'] ?? '';
            $href = $node['href'] ?? '';
            $parent = $node['parent'] ?? false;
            $group = $node['group'] ?? '';
            $meta = $node['meta'] ?? [];
        } else {
            if (empty($node->id)) {
                return null;
            }
            $nodeId = $node->id;
            $title = $node->title ?? '';
            $href = $node->href ?? '';
            $parent = $node->parent ?? false;
            $group = $node->group ?? '';
            $meta = $node->meta ?? [];
        }

        if (empty($nodeId)) {
            return null;
        }

        return [
            'id' => $nodeId,
            'title' => $title,
            'href' => $href,
            'parent' => $parent,
            'group' => $group,
            'meta' => $meta,
        ];
    }

    /**
     * Build formatted item array for a single node
     *
     * @param array $nodeData
     * @param array $processedUrls
     * @param int $positionCounter
     * @return array
     */
    private function buildFormattedItem(array $nodeData, array $processedUrls, int &$positionCounter): array
    {
        $name = wp_strip_all_tags((string) $nodeData['title']);
        if (empty($name) && !empty($nodeData['meta']['aria-label'])) {
            $name = wp_strip_all_tags((string) $nodeData['meta']['aria-label']);
        }
        if (empty($name) && !empty($nodeData['id'])) {
            $name = $this->deriveNameFromNodeId($nodeData['id']);
        }

        $updateCount = 0;
        if (isset($nodeData['meta']['title']) && preg_match('/(\d+)/', $nodeData['meta']['title'], $countMatches)) {
            $updateCount = (int) $countMatches[1];
        }

        $positionCounter++;
        $pos = (float) $positionCounter;

        $metaTarget = $nodeData['meta']['target'] ?? '_self';
        $target = isset($processedUrls['target']) && \is_string($processedUrls['target']) && $processedUrls['target'] !== ''
            ? $processedUrls['target']
            : $metaTarget;

        return [
            'name' => $name,
            'title' => $name,
            'href' => $processedUrls['href'],
            'url' => $processedUrls['url'],
            'capability' => '',
            'icon' => 'default',
            'position' => $pos,
            'order' => $pos - 1,
            'update_count' => $updateCount,
            'is_wp_native' => $processedUrls['is_wp_native'],
            'route_type' => $processedUrls['route_type'],
            'target' => $target,
            'id' => $nodeData['id'],
            'unique_id' => $nodeData['id'],
            'submenu' => [],
            'meta' => $nodeData['meta'],
            '_original' => ['parent' => $nodeData['parent'], 'group' => $nodeData['group']],
        ];
    }

    /**
     * Assign items to parent or root based on parent reference
     *
     * @param array $itemsById
     * @param array $rootItems
     */
    private function assignToParentOrRoot(array &$itemsById, array &$rootItems): void
    {
        foreach ($itemsById as &$item) {
            $parentId = $item['_original']['parent'];
            unset($item['_original']);

            $isRoot = empty($parentId) || $parentId === false || !isset($itemsById[$parentId]);
            if ($isRoot) {
                $rootItems[] = &$item;
            } else {
                $itemsById[$parentId]['submenu'][] = &$item;
            }
        }
        unset($item);
    }

    private function sortRootItems(array &$rootItems): void
    {
        usort($rootItems, function ($a, $b) {
            return floatval($a['position']) <=> floatval($b['position']);
        });
    }

    private function processToolbarUrl(string $originalHref, string $adminBaseUrl): array
    {
        $emptyResult = ['href' => '', 'url' => '', 'is_wp_native' => true, 'route_type' => 'wordpress_iframe'];
        $externalResult = ['href' => $originalHref, 'url' => $originalHref, 'is_wp_native' => false, 'route_type' => 'external'];

        $result = $emptyResult;
        if (!empty($originalHref) && !$this->isSpecialToolbarUrl($originalHref)) {
            $parsedUrl = parse_url($originalHref);
            if (isset($parsedUrl['scheme'], $parsedUrl['host'])) {
                $absolute = $this->processAbsoluteToolbarUrl($parsedUrl, $adminBaseUrl);
                $result = $absolute ?? $externalResult;
            } else {
                $result = $this->processRelativeToolbarUrl($originalHref, $adminBaseUrl);
            }
        } elseif (!empty($originalHref)) {
            $result = $externalResult;
        }

        return $result;
    }

    private function isSpecialToolbarUrl(string $href): bool
    {
        return strpos($href, '#') === 0
            || strpos($href, 'mailto:') === 0
            || strpos($href, 'tel:') === 0
            || strpos($href, 'javascript:') === 0;
    }

    private function processAbsoluteToolbarUrl(array $parsedUrl, string $adminBaseUrl): ?array
    {
        $siteParsed = parse_url(\site_url());
        if (($parsedUrl['host'] ?? '') !== ($siteParsed['host'] ?? '')) {
            return null;
        }

        $fullRequestPath = (string) ($parsedUrl['path'] ?? '/');
        if (!$this->isRequestPathUnderAdminUrl($fullRequestPath, $adminBaseUrl)) {
            return $this->buildSameSitePublicToolbarRoute($parsedUrl);
        }

        $path = $parsedUrl['path'] ?? '/';
        $query = isset($parsedUrl['query']) ? '?' . $parsedUrl['query'] : '';
        $fragment = isset($parsedUrl['fragment']) ? '#' . $parsedUrl['fragment'] : '';
        $siteBasePath = trailingslashit($siteParsed['path'] ?? '');
        if (strpos($path, $siteBasePath) === 0) {
            $path = substr($path, strlen($siteBasePath));
        }
        $adminBasePath = trailingslashit(parse_url($adminBaseUrl, PHP_URL_PATH) ?? '');
        if (strpos($path, ltrim($adminBasePath, '/')) === 0) {
            $path = substr($path, strlen(ltrim($adminBasePath, '/')));
        }
        $targetPath = ltrim($path, '/') . $query;
        $targetHash = ltrim($fragment, '#');
        $routeInfo = $this->routeConverter->convertWordPressRoute($targetPath, $targetHash);

        return [
            'href' => $targetPath . ($targetHash ? '#' . $targetHash : ''),
            'url' => $routeInfo['url'],
            'is_wp_native' => $routeInfo['is_wp_native'],
            'route_type' => $routeInfo['route_type'],
        ];
    }

    private function processRelativeToolbarUrl(string $originalHref, string $adminBaseUrl): array
    {
        if ($originalHref !== '' && $originalHref[0] === '/') {
            $absolute = \site_url($originalHref);
            $parsedAbs = \parse_url($absolute);
            if (\is_array($parsedAbs) && isset($parsedAbs['scheme'], $parsedAbs['host'])) {
                $fromAbsolute = $this->processAbsoluteToolbarUrl($parsedAbs, $adminBaseUrl);
                if ($fromAbsolute !== null) {
                    return $fromAbsolute;
                }
            }
        }

        $slugParts = explode('#', $originalHref, 2);
        $relativeSlugMain = $slugParts[0];
        $targetHash = $slugParts[1] ?? '';
        $targetPath = (strpos($relativeSlugMain, '.php') === false && $relativeSlugMain !== '')
            ? 'admin.php?page=' . $relativeSlugMain
            : $relativeSlugMain;

        $routeInfo = $this->routeConverter->convertWordPressRoute($targetPath, $targetHash);
        return [
            'href' => $targetPath . ($targetHash ? '#' . $targetHash : ''),
            'url' => $routeInfo['url'],
            'is_wp_native' => $routeInfo['is_wp_native'],
            'route_type' => $routeInfo['route_type'],
        ];
    }

    /**
     * Frontend and other non-wp-admin same-site toolbar links (e.g. "Visit site", WooCommerce storefront).
     */
    private function buildSameSitePublicToolbarRoute(array $parsedUrl): array
    {
        $full = $this->rebuildAbsoluteUrlFromParts($parsedUrl);

        return [
            'href' => $full,
            'url' => $full,
            'is_wp_native' => false,
            'route_type' => 'external',
            'target' => '_blank',
        ];
    }

    /**
     * @param array<string, mixed> $parsedUrl
     */
    private function rebuildAbsoluteUrlFromParts(array $parsedUrl): string
    {
        $scheme = isset($parsedUrl['scheme']) ? (string) $parsedUrl['scheme'] : 'https';
        $host = isset($parsedUrl['host']) ? (string) $parsedUrl['host'] : '';
        $port = isset($parsedUrl['port']) ? ':' . (string) (int) $parsedUrl['port'] : '';
        $path = isset($parsedUrl['path']) ? (string) $parsedUrl['path'] : '/';
        $query = isset($parsedUrl['query']) ? '?' . (string) $parsedUrl['query'] : '';
        $fragment = isset($parsedUrl['fragment']) ? '#' . (string) $parsedUrl['fragment'] : '';

        return $scheme . '://' . $host . $port . $path . $query . $fragment;
    }

    private function isRequestPathUnderAdminUrl(string $requestPath, string $adminBaseUrl): bool
    {
        $adminPath = \parse_url($adminBaseUrl, \PHP_URL_PATH);
        if (!\is_string($adminPath) || $adminPath === '' || $adminPath === '/') {
            return false;
        }

        $adminNorm = \rtrim($adminPath, '/');
        $req = \rtrim($requestPath, '/');
        if ($req === $adminNorm) {
            return true;
        }

        return \str_starts_with($req . '/', $adminNorm . '/');
    }

    private function deriveNameFromNodeId(string $id): string
    {
        if (empty($id)) {
            return '';
        }
        $clean = preg_replace('/-?(menu|node|bar|toolbar|admin[-_]bar|btn|button|action|widget)$/i', '', $id);
        $clean = str_replace(['-', '_'], ' ', $clean);
        return ucwords(trim($clean));
    }
}
