<?php
/**
 * Toolbar nodes excluded from FleekDash toolbar REST JSON.
 *
 * WordPress admin bar node IDs (see WP_Admin_Bar) that should not appear in
 * `/wp-json/fleekdash/v1/toolbar*` responses because they are redundant in FleekDash.
 *
 * @package FleekDash\API
 * @since V2.2
 */

namespace FleekDash\API;

/**
 * Central registry for toolbar exclusions (scalable + filterable).
 */
final class ToolbarExcludedNodes
{
    /**
     * Default exclusions (WordPress core / common noise).
     *
     * @var list<string>
     */
    private const DEFAULT_IDS = [
        'about', // About WordPress (about.php)
        'wp-logo', // WordPress logo root (redundant in FleekDash chrome)
        'contribute', // Get Involved (contribute.php)
        'wp-logo-external', // External links group under WP logo (wordpress.org, docs, etc.)
    ];

    /**
     * Default href slugs to exclude (normalized relative href/path).
     *
     * @var list<string>
     */
    private const DEFAULT_HREFS = [
        'about.php',
        'contribute.php',
    ];

    /**
     * Cached IDs for the current request (filter runs once).
     *
     * @var list<string>|null
     */
    private static $cachedIds = null;

    /**
     * Node IDs to strip from formatted toolbar output.
     *
     * Filter: `fleekdash_toolbar_excluded_node_ids`
     * Pass an array of admin bar node id strings (e.g. `about`, `contribute`).
     *
     * @return list<string>
     */
    public static function getIds(): array
    {
        if (self::$cachedIds !== null) {
            return self::$cachedIds;
        }

        $ids = \apply_filters('fleekdash_toolbar_excluded_node_ids', self::DEFAULT_IDS);
        if (!\is_array($ids)) {
            $ids = self::DEFAULT_IDS;
        }

        $normalized = [];
        foreach ($ids as $id) {
            $s = \is_string($id) ? \trim($id) : '';
            if ($s !== '') {
                $normalized[] = $s;
            }
        }

        self::$cachedIds = \array_values(\array_unique($normalized));
        return self::$cachedIds;
    }

    /**
     * Fast lookup map: id => true
     *
     * @return array<string, true>
     */
    public static function getIdLookup(): array
    {
        $lookup = [];
        foreach (self::getIds() as $id) {
            $lookup[$id] = true;
        }
        return $lookup;
    }

    /**
     * Href slugs to strip from formatted toolbar output.
     *
     * Filter: `fleekdash_toolbar_excluded_hrefs`
     *
     * @return list<string>
     */
    public static function getHrefs(): array
    {
        $hrefs = \apply_filters('fleekdash_toolbar_excluded_hrefs', self::DEFAULT_HREFS);
        if (!\is_array($hrefs)) {
            $hrefs = self::DEFAULT_HREFS;
        }

        $normalized = [];
        foreach ($hrefs as $href) {
            $s = \is_string($href) ? \trim($href) : '';
            if ($s !== '') {
                $normalized[] = $s;
            }
        }
        return \array_values(\array_unique($normalized));
    }

    /**
     * Fast lookup map: href => true
     *
     * @return array<string, true>
     */
    public static function getHrefLookup(): array
    {
        $lookup = [];
        foreach (self::getHrefs() as $href) {
            $lookup[$href] = true;
        }
        return $lookup;
    }

    /**
     * Central exclusion rule matcher for toolbar nodes.
     *
     * @param string $id
     * @param string $href
     */
    public static function shouldExcludeNode(string $id, string $href): bool
    {
        $shouldExclude = false;

        $idLookup = self::getIdLookup();
        if ($id !== '' && isset($idLookup[$id])) {
            $shouldExclude = true;
        }

        if (!$shouldExclude && $href !== '') {
            $hrefLookup = self::getHrefLookup();

            // Normalize absolute/relative href and compare by path+query.
            $normalizedHref = $href;
            $parsed = \wp_parse_url($href);
            if (\is_array($parsed)) {
                $path = isset($parsed['path']) ? \ltrim((string) $parsed['path'], '/') : '';
                $query = isset($parsed['query']) ? '?' . (string) $parsed['query'] : '';
                $normalizedHref = $path . $query;
            }

            if ($normalizedHref !== '' && isset($hrefLookup[$normalizedHref])) {
                $shouldExclude = true;
            }
        }

        return $shouldExclude;
    }

    /**
     * True if any ancestor admin bar node id is in the exclusion list.
     * Used to drop descendants when a parent (e.g. wp-logo-external) is excluded.
     *
     * @param array<string, mixed> $parentById node id => parent id or false
     */
    public static function hasExcludedAncestor(string $nodeId, array $parentById): bool
    {
        $excludedLookup = self::getIdLookup();
        $current = $nodeId;
        $guard = 0;
        $found = false;
        while ($guard++ < 50) {
            if (!isset($parentById[$current])) {
                break;
            }
            $p = $parentById[$current];
            if ($p === '' || $p === false) {
                break;
            }
            $pStr = (string) $p;
            if (isset($excludedLookup[$pStr])) {
                $found = true;
                break;
            }
            $current = $pStr;
        }

        return $found;
    }
}
