<?php

namespace WPSN\Modules\SearchUrls;

use WPSN\Core\AbstractModule;
use WPSN\Domain\SeoDocumentContext;

defined('ABSPATH') || exit;

/**
 * Search URL canonicalization and query cleanup.
 *
 * Opt-in redirect layer that normalizes search result URLs to the site's canonical
 * search format, can optionally strip extra parameters to reduce crawl waste, and
 * can sanitize the search query itself (length, emoji, blocked patterns).
 */
final class SearchUrlsModule extends AbstractModule
{
    public const MODE_FORMAT = 'format';
    public const MODE_STRICT = 'strict';

    private const INVALID_ACTION_HOME = 'redirect_home';
    private const INVALID_ACTION_404 = '404';
    private const INVALID_ACTION_EMPTY_SEARCH = 'empty_search';

    public function id(): string
    {
        return 'search_urls';
    }

    public function name(): string
    {
        return __('Search URL Canonicalization & Query Cleanup', 'wp-seo-next');
    }

    public function description(): string
    {
        return __('Redirect search result URLs to the canonical search format, optionally strip noisy parameters, and sanitize the search query itself.', 'wp-seo-next');
    }

    public function defaultEnabled(): bool
    {
        return true;
    }

    public function register(): void
    {
        add_action('template_redirect', [$this, 'maybeRedirectCanonicalSearchUrl'], 1);
        add_action('template_redirect', [$this, 'maybeRedirectLegacyCategoryBaseUrl'], 2);
        add_filter('term_link', [$this, 'filterCategoryTermLink'], 20, 3);
        add_filter('request', [$this, 'filterRequestForStrippedCategoryBase'], 20);
    }

    public function maybeRedirectCanonicalSearchUrl(): void
    {
        if ($this->shouldSkipRequest()) {
            return;
        }

        $config = $this->config();
        if (empty($config['enabled']) && empty($config['sanitize_query'])) {
            return;
        }

        $resolver = $this->container->get('document_resolver');
        if (!is_object($resolver) || !method_exists($resolver, 'resolve')) {
            return;
        }

        $ctx = $resolver->resolve();
        if (!$ctx instanceof SeoDocumentContext) {
            return;
        }

        $currentUrl = $this->currentRequestUrl();
        if ($currentUrl === '') {
            return;
        }

        $queryAction = $this->determineSearchQueryAction($ctx, $currentUrl, $config);
        if ($queryAction['action'] === 'redirect' && $queryAction['target'] !== '') {
            if (!wp_safe_redirect($queryAction['target'], 301, 'WP SEO Next')) {
                return;
            }

            exit;
        }

        if ($queryAction['action'] === self::INVALID_ACTION_404) {
            $this->markCurrentRequestAs404();
            return;
        }

        $target = $this->determineRedirectTarget($ctx, $currentUrl);
        if ($target === '') {
            return;
        }

        if (!wp_safe_redirect($target, 301, 'WP SEO Next')) {
            return;
        }

        exit;
    }

    public function determineRedirectTarget(SeoDocumentContext $ctx, string $currentUrl = ''): string
    {
        $config = $this->config();
        if (empty($config['enabled'])) {
            return '';
        }

        if ($ctx->type !== SeoDocumentContext::TYPE_SEARCH) {
            return '';
        }

        $target = $this->resolveCanonicalSearchUrl($ctx);
        if ($target === '') {
            return '';
        }

        $currentUrl = trim($currentUrl);
        if ($currentUrl === '') {
            $currentUrl = $this->currentRequestUrl();
        }

        if ($currentUrl === '') {
            return '';
        }

        $target = $this->withPreservedArgs($target, $currentUrl, $config);
        if ($this->sameUrl($target, $currentUrl)) {
            return '';
        }

        return $target;
    }

    /**
     * @param array<string,mixed> $config
     * @return array{action:string,target:string,reason:string}
     */
    private function determineSearchQueryAction(SeoDocumentContext $ctx, string $currentUrl, array $config): array
    {
        if (empty($config['sanitize_query'])) {
            return ['action' => 'none', 'target' => '', 'reason' => ''];
        }

        if ($ctx->type !== SeoDocumentContext::TYPE_SEARCH) {
            return ['action' => 'none', 'target' => '', 'reason' => ''];
        }

        $query = $this->detectCurrentSearchQuery($ctx, $currentUrl);
        $sanitized = $this->sanitizeSearchQuery($query, $config);

        if ($sanitized['blocked'] || $sanitized['empty']) {
            $action = isset($config['invalid_action']) ? (string) $config['invalid_action'] : self::INVALID_ACTION_404;
            if ($action === self::INVALID_ACTION_404) {
                return ['action' => self::INVALID_ACTION_404, 'target' => '', 'reason' => $sanitized['reason']];
            }

            $target = $this->invalidSearchTarget($action);
            if ($target === '' || $this->sameUrl($target, $currentUrl)) {
                return ['action' => 'none', 'target' => '', 'reason' => $sanitized['reason']];
            }

            return ['action' => 'redirect', 'target' => $target, 'reason' => $sanitized['reason']];
        }

        if (!$sanitized['changed']) {
            return ['action' => 'none', 'target' => '', 'reason' => ''];
        }

        $target = $this->buildSearchUrl($sanitized['clean']);
        if ($target === '') {
            return ['action' => 'none', 'target' => '', 'reason' => ''];
        }

        $target = $this->withPreservedArgs($target, $currentUrl, $config);
        if ($this->sameUrl($target, $currentUrl)) {
            return ['action' => 'none', 'target' => '', 'reason' => ''];
        }

        return ['action' => 'redirect', 'target' => $target, 'reason' => 'clean'];
    }

    private function detectCurrentSearchQuery(SeoDocumentContext $ctx, string $currentUrl): string
    {
        $fromCurrentUrl = $this->searchQueryFromUrl($currentUrl);
        if ($fromCurrentUrl !== '') {
            return $fromCurrentUrl;
        }

        if (isset($_GET['s']) && is_scalar($_GET['s'])) {
            $query = (string) wp_unslash($_GET['s']);
            if (trim($query) !== '') {
                return $query;
            }
        }

        if (function_exists('get_search_query')) {
            $query = get_search_query();
            if (is_string($query)) {
                $query = html_entity_decode($query, ENT_QUOTES, 'UTF-8');
                $query = (string) wp_unslash($query);
                if (trim($query) !== '') {
                    return $query;
                }
            }
        }

        return is_string($ctx->url) ? $this->searchQueryFromUrl((string) $ctx->url) : '';
    }

    private function searchQueryFromUrl(string $url): string
    {
        $parts = wp_parse_url($url);
        if (!is_array($parts) || empty($parts['query']) || !is_string($parts['query'])) {
            return '';
        }

        $query = [];
        parse_str($parts['query'], $query);
        if (!is_array($query) || !isset($query['s']) || !is_scalar($query['s'])) {
            return '';
        }

        return trim((string) $query['s']);
    }

    /**
     * @param array<string,mixed> $config
     * @return array{raw:string,clean:string,changed:bool,blocked:bool,empty:bool,reason:string}
     */
    private function sanitizeSearchQuery(string $query, array $config): array
    {
        $input = trim(html_entity_decode((string) wp_unslash($query), ENT_QUOTES, 'UTF-8'));
        $raw = $this->normalizeSearchQueryText($query);
        $clean = $raw;

        if (!empty($config['strip_emoji'])) {
            $clean = $this->removeEmoji($clean);
            $clean = $this->normalizeSearchQueryText($clean);
        }

        $limit = isset($config['query_limit']) ? max(0, (int) $config['query_limit']) : 0;
        if ($limit > 0 && $this->stringLength($clean) > $limit) {
            $clean = $this->truncateString($clean, $limit);
            $clean = $this->normalizeSearchQueryText($clean);
        }

        $empty = ($clean === '');
        $blocked = false;
        $reason = '';

        if ($empty) {
            $reason = 'empty';
        } elseif (!empty($config['block_common_patterns']) && $this->matchesAnyPattern($raw, $this->commonBlockedPatterns())) {
            $blocked = true;
            $reason = 'common_pattern';
        } elseif ($this->matchesAnyPattern($raw, $this->customBlockedPatterns(isset($config['blacklist_patterns']) ? (string) $config['blacklist_patterns'] : ''))) {
            $blocked = true;
            $reason = 'custom_pattern';
        }

        return [
            'raw' => $raw,
            'clean' => $clean,
            'changed' => $clean !== $input,
            'blocked' => $blocked,
            'empty' => $empty,
            'reason' => $reason,
        ];
    }

    private function normalizeSearchQueryText(string $query): string
    {
        $query = html_entity_decode((string) wp_unslash($query), ENT_QUOTES, 'UTF-8');
        if (function_exists('wp_strip_all_tags')) {
            $query = wp_strip_all_tags($query);
        } else {
            $query = strip_tags($query);
        }

        $query = preg_replace('/[\x00-\x1F\x7F]+/u', ' ', $query) ?? $query;
        $query = preg_replace('/[\x{00AD}\x{200B}-\x{200D}\x{2060}\x{FEFF}]+/u', '', $query) ?? $query;
        $query = preg_replace('/\s+/u', ' ', $query) ?? $query;

        return trim($query);
    }

    private function removeEmoji(string $text): string
    {
        $pattern = '/[\x{1F1E6}-\x{1F1FF}\x{1F300}-\x{1F5FF}\x{1F600}-\x{1F64F}\x{1F680}-\x{1F6FF}\x{1F700}-\x{1F77F}\x{1F780}-\x{1F7FF}\x{1F800}-\x{1F8FF}\x{1F900}-\x{1F9FF}\x{1FA00}-\x{1FAFF}\x{2600}-\x{27BF}\x{FE0F}\x{200D}]+/u';
        $clean = preg_replace($pattern, '', $text);

        return is_string($clean) ? $clean : $text;
    }

    private function stringLength(string $value): int
    {
        if (function_exists('mb_strlen')) {
            return (int) mb_strlen($value, 'UTF-8');
        }

        return strlen($value);
    }

    private function truncateString(string $value, int $limit): string
    {
        if ($limit < 1) {
            return '';
        }

        if (function_exists('mb_substr')) {
            return (string) mb_substr($value, 0, $limit, 'UTF-8');
        }

        return substr($value, 0, $limit);
    }

    /**
     * @return array<int,string>
     */
    private function commonBlockedPatterns(): array
    {
        return [
            '(?:https?:\/\/|www\.)',
            '(?:<\s*(?:script|iframe)|%3c\s*(?:script|iframe))',
            '(?:javascript:|data:text\/html)',
            '(?:union\s+select|select\s+.+\s+from|drop\s+table|insert\s+into|benchmark\s*\(|sleep\s*\()',
        ];
    }

    /**
     * @return array<int,string>
     */
    private function customBlockedPatterns(string $raw): array
    {
        $lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
        $patterns = [];

        foreach ($lines as $line) {
            $line = trim((string) $line);
            if ($line === '') {
                continue;
            }

            $patterns[] = $line;
        }

        return $patterns;
    }

    /**
     * @param array<int,string> $patterns
     */
    private function matchesAnyPattern(string $query, array $patterns): bool
    {
        if ($query === '' || empty($patterns)) {
            return false;
        }

        foreach ($patterns as $pattern) {
            $regex = $this->normalizeRegexPattern($pattern);
            if ($regex === '') {
                continue;
            }

            $matched = @preg_match($regex, $query);
            if ($matched === 1) {
                return true;
            }
        }

        return false;
    }

    private function normalizeRegexPattern(string $pattern): string
    {
        $pattern = trim($pattern);
        if ($pattern === '') {
            return '';
        }

        if (preg_match('/^(.)(?:.*)\1[imsxuADSUXJ]*$/', $pattern) === 1) {
            $ok = @preg_match($pattern, '');
            return $ok === false ? '' : $pattern;
        }

        $regex = '~' . str_replace('~', '\\~', $pattern) . '~iu';
        $ok = @preg_match($regex, '');

        return $ok === false ? '' : $regex;
    }

    private function buildSearchUrl(string $query): string
    {
        if (function_exists('get_search_link')) {
            $url = get_search_link($query);
            if (is_string($url) && trim($url) !== '') {
                return trim(esc_url_raw($url));
            }
        }

        return trim((string) esc_url_raw(add_query_arg(['s' => $query], home_url('/'))));
    }

    private function invalidSearchTarget(string $action): string
    {
        if ($action === self::INVALID_ACTION_HOME) {
            return trim((string) esc_url_raw(home_url('/')));
        }

        if ($action === self::INVALID_ACTION_EMPTY_SEARCH) {
            return trim((string) esc_url_raw(add_query_arg(['s' => ''], home_url('/'))));
        }

        return '';
    }

    private function markCurrentRequestAs404(): void
    {
        global $wp_query;

        if (isset($wp_query) && is_object($wp_query) && method_exists($wp_query, 'set_404')) {
            $wp_query->set_404();
        }

        if (function_exists('status_header')) {
            status_header(404);
        }

        if (function_exists('nocache_headers')) {
            nocache_headers();
        }
    }

    /**
     * Best-effort support for Yoast-style “strip category base”.
     *
     * When enabled we output category term links without the category base and
     * map matching front-end requests back to category_name, while letting more
     * specific page/post matches win first.
     *
     * @param array<string,mixed> $queryVars
     * @return array<string,mixed>
     */
    public function filterRequestForStrippedCategoryBase(array $queryVars): array
    {
        $config = $this->config();
        if (empty($config['strip_category_base']) || $this->shouldSkipRequest()) {
            return $queryVars;
        }

        if ($this->requestAlreadyTargetsContent($queryVars)) {
            return $queryVars;
        }

        $parsed = $this->parseCategoryArchiveRequestPath($this->currentRelativeRequestPath());
        $categoryPath = $parsed['path'];
        if ($categoryPath === '') {
            return $queryVars;
        }

        if ($this->requestPathHasCategoryBase($categoryPath)) {
            return $queryVars;
        }

        if ($this->pathConflictsWithPublicContent($categoryPath)) {
            return $queryVars;
        }

        $term = $this->resolveCategoryByPath($categoryPath);
        if (!$term instanceof \WP_Term || $term->taxonomy !== 'category') {
            return $queryVars;
        }

        $queryVars['category_name'] = $categoryPath;
        if ($parsed['paged'] > 1) {
            $queryVars['paged'] = $parsed['paged'];
        }
        if ($parsed['feed'] !== '') {
            $queryVars['feed'] = $parsed['feed'];
        }

        return $queryVars;
    }

    /**
     * @param mixed $term
     * @param mixed $taxonomy
     */
    public function filterCategoryTermLink(string $termlink, $term, $taxonomy): string
    {
        $config = $this->config();
        if (empty($config['strip_category_base'])) {
            return $termlink;
        }

        $taxonomy = is_string($taxonomy) ? $taxonomy : '';
        if ($taxonomy !== 'category') {
            return $termlink;
        }

        if (!$term instanceof \WP_Term) {
            return $termlink;
        }

        $path = $this->categoryPathFromTerm($term);
        if ($path === '') {
            return $termlink;
        }

        $url = $this->buildStrippedCategoryUrl($path);
        return $url !== '' ? $url : $termlink;
    }

    public function maybeRedirectLegacyCategoryBaseUrl(): void
    {
        if ($this->shouldSkipRequest() || !function_exists('is_category') || !is_category()) {
            return;
        }

        $config = $this->config();
        if (empty($config['strip_category_base'])) {
            return;
        }

        $requestPath = $this->currentRelativeRequestPath();
        if ($requestPath === '' || !$this->requestPathHasCategoryBase($requestPath)) {
            return;
        }

        $term = get_queried_object();
        if (!$term instanceof \WP_Term || $term->taxonomy !== 'category') {
            return;
        }

        $target = $this->buildStrippedCategoryUrl($this->categoryPathFromTerm($term));
        if ($target === '') {
            return;
        }

        $currentUrl = $this->currentRequestUrl();
        if ($currentUrl === '' || $this->sameUrl($target, $currentUrl)) {
            return;
        }

        wp_safe_redirect($target, 301, 'WP SEO Next');
        exit;
    }

    private function resolveCanonicalSearchUrl(SeoDocumentContext $ctx): string
    {
        $repo = $this->container->get('meta_repo');
        if (!is_object($repo) || !method_exists($repo, 'getForContext')) {
            return is_string($ctx->url) ? trim($ctx->url) : '';
        }

        $meta = $repo->getForContext($ctx);
        if (is_object($meta) && isset($meta->canonical) && is_string($meta->canonical) && trim($meta->canonical) !== '') {
            return trim($meta->canonical);
        }

        return is_string($ctx->url) ? trim($ctx->url) : '';
    }

    /**
     * @param array<string,mixed> $config
     */
    private function withPreservedArgs(string $target, string $currentUrl, array $config): string
    {
        $currentParts = wp_parse_url($currentUrl);
        if (!is_array($currentParts)) {
            return $target;
        }

        $currentQuery = [];
        if (isset($currentParts['query']) && is_string($currentParts['query']) && $currentParts['query'] !== '') {
            parse_str($currentParts['query'], $currentQuery);
        }

        if (!is_array($currentQuery) || empty($currentQuery)) {
            return $target;
        }

        $targetParts = wp_parse_url($target);
        if (!is_array($targetParts)) {
            return $target;
        }

        $targetQuery = [];
        if (isset($targetParts['query']) && is_string($targetParts['query']) && $targetParts['query'] !== '') {
            parse_str($targetParts['query'], $targetQuery);
        }

        $mode = isset($config['mode']) ? (string) $config['mode'] : self::MODE_FORMAT;
        $preserveMap = $this->preserveArgMap(isset($config['preserve_args']) ? (string) $config['preserve_args'] : '');
        $extra = [];

        foreach ($currentQuery as $key => $value) {
            $normalized = strtolower(sanitize_key((string) $key));
            if ($normalized === '') {
                continue;
            }

            if ($this->isSearchControlArg($normalized)) {
                continue;
            }

            if ($this->isTrackingArg($normalized)) {
                continue;
            }

            if ($mode === self::MODE_STRICT && !isset($preserveMap[$normalized])) {
                continue;
            }

            $extra[$key] = $value;
        }

        if (empty($extra)) {
            return $target;
        }

        foreach ($extra as $key => $value) {
            if (!array_key_exists($key, $targetQuery)) {
                $targetQuery[$key] = $value;
            }
        }

        $targetParts['query'] = http_build_query($targetQuery, '', '&', PHP_QUERY_RFC3986);

        return $this->buildUrl($targetParts);
    }

    /**
     * @return array<string,bool>
     */
    private function preserveArgMap(string $raw): array
    {
        $items = preg_split('/[\s,]+/', strtolower($raw)) ?: [];
        $map = [];

        foreach ($items as $item) {
            $key = sanitize_key((string) $item);
            if ($key !== '') {
                $map[$key] = true;
            }
        }

        return $map;
    }

    private function isSearchControlArg(string $key): bool
    {
        return in_array($key, ['s', 'paged', 'page'], true);
    }

    private function isTrackingArg(string $key): bool
    {
        if (strpos($key, 'utm_') === 0) {
            return true;
        }

        return in_array($key, [
            'gclid',
            'dclid',
            'fbclid',
            'msclkid',
            'yclid',
            'mc_cid',
            'mc_eid',
            '_ga',
            '_gl',
            '_gac',
            '_gid',
            'igshid',
            'mkt_tok',
            'amp',
            'replytocom',
        ], true);
    }

    /**
     * @return array{enabled:bool,mode:string,preserve_args:string,strip_category_base:bool,sanitize_query:bool,query_limit:int,strip_emoji:bool,block_common_patterns:bool,blacklist_patterns:string,invalid_action:string}
     */
    private function config(): array
    {
        $config = $this->settings->get('search_urls', []);
        if (!is_array($config)) {
            $config = [];
        }

        $mode = isset($config['mode']) ? sanitize_key((string) $config['mode']) : self::MODE_FORMAT;
        if (!in_array($mode, [self::MODE_FORMAT, self::MODE_STRICT], true)) {
            $mode = self::MODE_FORMAT;
        }

        $invalidAction = isset($config['invalid_action']) ? sanitize_key((string) $config['invalid_action']) : self::INVALID_ACTION_404;
        if (!in_array($invalidAction, [self::INVALID_ACTION_HOME, self::INVALID_ACTION_404, self::INVALID_ACTION_EMPTY_SEARCH], true)) {
            $invalidAction = self::INVALID_ACTION_404;
        }

        return [
            'enabled' => !empty($config['enabled']),
            'mode' => $mode,
            'preserve_args' => isset($config['preserve_args']) ? (string) $config['preserve_args'] : '',
            'strip_category_base' => !empty($config['strip_category_base']),
            'sanitize_query' => !empty($config['sanitize_query']),
            'query_limit' => max(0, min(200, (int) ($config['query_limit'] ?? 50))),
            'strip_emoji' => !empty($config['strip_emoji']),
            'block_common_patterns' => !empty($config['block_common_patterns']),
            'blacklist_patterns' => isset($config['blacklist_patterns']) ? (string) $config['blacklist_patterns'] : '',
            'invalid_action' => $invalidAction,
        ];
    }

    private function currentRequestUrl(): string
    {
        $requestUri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '';
        if ($requestUri === '') {
            $fallback = home_url(add_query_arg([]));
            return is_string($fallback) ? trim($fallback) : '';
        }

        if (strpos($requestUri, 'http://') === 0 || strpos($requestUri, 'https://') === 0) {
            return trim(esc_url_raw($requestUri));
        }

        if ($requestUri[0] !== '/') {
            $requestUri = '/' . ltrim($requestUri, '/');
        }

        return trim((string) esc_url_raw(home_url($requestUri)));
    }

    private function sameUrl(string $left, string $right): bool
    {
        return $this->normalizeUrlForCompare($left) === $this->normalizeUrlForCompare($right);
    }

    private function normalizeUrlForCompare(string $url): string
    {
        $parts = wp_parse_url(trim($url));
        if (!is_array($parts)) {
            return rtrim(trim($url), '/');
        }

        $scheme = isset($parts['scheme']) ? strtolower((string) $parts['scheme']) . '://' : '';
        $host = isset($parts['host']) ? strtolower((string) $parts['host']) : '';
        $port = isset($parts['port']) ? ':' . (string) ((int) $parts['port']) : '';
        $path = isset($parts['path']) ? rtrim((string) $parts['path'], '/') : '';

        $query = [];
        if (isset($parts['query']) && is_string($parts['query']) && $parts['query'] !== '') {
            parse_str($parts['query'], $query);
            if (is_array($query)) {
                ksort($query);
            } else {
                $query = [];
            }
        }

        $qs = '';
        if (!empty($query)) {
            $qs = '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
        }

        return $scheme . $host . $port . $path . $qs;
    }

    /**
     * @param array<string,mixed> $parts
     */
    private function buildUrl(array $parts): string
    {
        $scheme = isset($parts['scheme']) ? (string) $parts['scheme'] . '://' : '';
        $host = isset($parts['host']) ? (string) $parts['host'] : '';
        $port = isset($parts['port']) ? ':' . (string) ((int) $parts['port']) : '';
        $user = isset($parts['user']) ? (string) $parts['user'] : '';
        $pass = isset($parts['pass']) ? ':' . (string) $parts['pass'] : '';
        $auth = $user !== '' ? $user . $pass . '@' : '';
        $path = isset($parts['path']) ? (string) $parts['path'] : '';
        $query = isset($parts['query']) && (string) $parts['query'] !== '' ? '?' . (string) $parts['query'] : '';
        $fragment = isset($parts['fragment']) && (string) $parts['fragment'] !== '' ? '#' . (string) $parts['fragment'] : '';

        $url = $scheme . $auth . $host . $port . $path . $query . $fragment;
        $url = esc_url_raw($url);

        return $url !== '' ? $url : '';
    }

    private function shouldSkipRequest(): bool
    {
        if (is_admin()) {
            return true;
        }

        if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
            return true;
        }

        if (defined('REST_REQUEST') && REST_REQUEST) {
            return true;
        }

        return false;
    }

    /**
     * @param array<string,mixed> $queryVars
     */
    private function requestAlreadyTargetsContent(array $queryVars): bool
    {
        foreach (['category_name', 'cat', 'taxonomy', 'term', 'name', 'pagename', 'attachment', 'attachment_id', 'page_id', 'p', 's', 'post_type', 'feed', 'rest_route', 'preview', 'error'] as $key) {
            if (!array_key_exists($key, $queryVars)) {
                continue;
            }

            $value = $queryVars[$key];
            if (is_scalar($value) && trim((string) $value) !== '') {
                return true;
            }

            if (is_array($value) && !empty($value)) {
                return true;
            }
        }

        return false;
    }

    private function currentRelativeRequestPath(): string
    {
        $requestUri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '';
        if ($requestUri === '') {
            return '';
        }

        $parts = wp_parse_url($requestUri);
        if (!is_array($parts)) {
            return '';
        }

        $path = isset($parts['path']) ? (string) $parts['path'] : '';
        if ($path === '') {
            return '';
        }

        $homePath = (string) wp_parse_url(home_url('/'), PHP_URL_PATH);
        $homePath = trim($homePath, '/');
        $path = trim($path, '/');

        if ($homePath !== '' && strpos($path . '/', $homePath . '/') === 0) {
            $path = trim(substr($path, strlen($homePath)), '/');
        }

        return $path;
    }

    /**
     * @return array{path:string,paged:int,feed:string}
     */
    private function parseCategoryArchiveRequestPath(string $requestPath): array
    {
        $requestPath = trim($requestPath, '/');
        $feed = '';
        $paged = 0;

        if ($requestPath === '') {
            return ['path' => '', 'paged' => 0, 'feed' => ''];
        }

        if (preg_match('#^(.*?)/(feed|rdf|rss|rss2|atom)$#i', $requestPath, $m)) {
            $requestPath = trim((string) $m[1], '/');
            $feed = strtolower((string) $m[2]);
        }

        if (preg_match('#^(.*?)/page/([0-9]+)$#i', $requestPath, $m)) {
            $requestPath = trim((string) $m[1], '/');
            $paged = max(0, (int) $m[2]);
        }

        return [
            'path' => $requestPath,
            'paged' => $paged,
            'feed' => $feed,
        ];
    }

    private function requestPathHasCategoryBase(string $path): bool
    {
        $base = trim($this->categoryBasePath(), '/');
        if ($base === '') {
            return false;
        }

        $path = trim($path, '/');
        return $path === $base || strpos($path . '/', $base . '/') === 0;
    }

    private function categoryBasePath(): string
    {
        global $wp_rewrite;

        $base = '';
        if (isset($wp_rewrite) && is_object($wp_rewrite) && isset($wp_rewrite->category_base) && is_string($wp_rewrite->category_base)) {
            $base = trim($wp_rewrite->category_base, '/');
        }

        if ($base === '') {
            $base = 'category';
        }

        return $base;
    }

    private function pathConflictsWithPublicContent(string $path): bool
    {
        $path = trim($path, '/');
        if ($path === '') {
            return false;
        }

        $postTypes = get_post_types(['public' => true], 'names');
        if (!is_array($postTypes) || empty($postTypes)) {
            return false;
        }

        $match = get_page_by_path($path, OBJECT, array_values($postTypes));
        return $match instanceof \WP_Post;
    }

    private function resolveCategoryByPath(string $path): ?\WP_Term
    {
        $path = trim($path, '/');
        if ($path === '') {
            return null;
        }

        if (function_exists('get_category_by_path')) {
            $term = get_category_by_path($path, false);
            if ($term instanceof \WP_Term) {
                return $term;
            }
        }

        $slug = basename($path);
        if ($slug === '') {
            return null;
        }

        $term = get_term_by('slug', $slug, 'category');
        return $term instanceof \WP_Term ? $term : null;
    }

    private function categoryPathFromTerm(\WP_Term $term): string
    {
        if ($term->taxonomy !== 'category') {
            return '';
        }

        $ancestors = get_ancestors((int) $term->term_id, 'category', 'taxonomy');
        $ancestors = is_array($ancestors) ? array_reverse($ancestors) : [];
        $parts = [];

        foreach ($ancestors as $ancestorId) {
            $ancestor = get_term((int) $ancestorId, 'category');
            if (!$ancestor instanceof \WP_Term || $ancestor->slug === '') {
                continue;
            }
            $parts[] = $ancestor->slug;
        }

        if ($term->slug !== '') {
            $parts[] = $term->slug;
        }

        return trim(implode('/', $parts), '/');
    }

    private function buildStrippedCategoryUrl(string $path): string
    {
        $path = trim($path, '/');
        if ($path === '') {
            return '';
        }

        $url = home_url(user_trailingslashit($path, 'category'));
        return is_string($url) ? trim(esc_url_raw($url)) : '';
    }
}
