<?php

/**
 * URL & Permalink Cleanup Module
 *
 * @package ProRank\SEO\Modules\TechnicalSEO
 * @since   0.1.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\TechnicalSEO;

use ProRank\SEO\Modules\BaseModule;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}


/**
 * Class UrlCleanupModule
 *
 * Handles URL and permalink cleanup features:
 * - Strip category/tag base from URLs
 * - Remove replytocom parameters
 * - Redirect attachment pages
 * - Clean UTM parameters
 * - Control trailing slashes
 * - Convert URLs to lowercase
 * - Remove stop words from slugs
 * - Detect redirect chains
 */


class UrlCleanupModule extends BaseModule {
    /**
     * Guard to avoid starting the archive link output buffer multiple times.
     *
     * @var bool
     */
    private bool $archive_link_normalizer_started = false;

    /**
     * Module slug
     *
     * @var string
     */
    protected string $slug = 'url-cleanup';
    
    /**
     * Module name
     *
     * @var string
     */
    protected string $name = 'URL Cleanup';
    
    /**
     * Module description
     *
     * @var string
     */
    protected string $description = 'Clean and optimize URL structure';
    
    /**
     * Required tier
     *
     * @var string
     */
    protected string $feature_tier = 'free';
    
    /**
     * Parent module
     *
     * @var string|null
     */
    protected ?string $parent_slug = 'technical-seo';

    /**
     * Initialize module
     *
     * @return void
     */
    public function init(): void {
        parent::init();

        // Hook into WordPress
        $this->init_hooks();
    }

    /**
     * Initialize hooks
     *
     * @return void
     */
    public function init_hooks(): void {
        // Get settings
        $settings = $this->get_settings();
        
        // Strip category base
        if (!empty($settings['strip_category_base'])) {
            add_filter('category_rewrite_rules', [$this, 'strip_category_base_rewrite_rules']);
            add_filter('term_link', [$this, 'strip_category_base_term_link'], 10, 3);
            add_action('template_redirect', [$this, 'handle_category_base_redirects'], 5);
            add_action('created_category', [$this, 'flush_rewrite_rules_on_category_change']);
            add_action('edited_category', [$this, 'flush_rewrite_rules_on_category_change']);
            add_action('delete_category', [$this, 'flush_rewrite_rules_on_category_change']);
        }
        
        // Strip tag base
        if (!empty($settings['strip_tag_base'])) {
            add_filter('tag_rewrite_rules', [$this, 'strip_tag_base_rewrite_rules']);
            add_filter('term_link', [$this, 'strip_tag_base_term_link'], 10, 3);
            add_action('template_redirect', [$this, 'handle_tag_base_redirects'], 5);
        }
        
        // Remove replytocom parameters
        if (!empty($settings['remove_replytocom'])) {
            add_filter('comment_reply_link_args', [$this, 'remove_replytocom_parameter']);
            add_filter('comment_reply_link', [$this, 'filter_comment_reply_link']);
            add_action('template_redirect', [$this, 'redirect_replytocom_urls'], 5);
        }
        
        // Remove UTM parameters
        if (!empty($settings['remove_utm_parameters'])) {
            add_action('template_redirect', [$this, 'redirect_utm_urls'], 5);
        }
        
        // Force trailing slash
        if (!empty($settings['force_trailing_slash'])) {
            add_action('template_redirect', [$this, 'handle_trailing_slash'], 5);
        }
        
        // Remove trailing slash
        if (!empty($settings['remove_trailing_slash'])) {
            add_action('template_redirect', [$this, 'handle_trailing_slash'], 5);
        }

        // Process rendered archive links so builder HTML can be normalized or rewritten safely.
        if (
            !empty($settings['normalize_internal_archive_links']) ||
            (($settings['author_date_archive_link_policy'] ?? 'default') !== 'default')
        ) {
            add_action('wp', [$this, 'start_internal_archive_link_normalizer'], 0);
            add_action('template_redirect', [$this, 'start_internal_archive_link_normalizer'], 20);
            add_filter('the_content', [$this, 'normalize_internal_archive_links_html'], 999);
            add_filter('widget_text_content', [$this, 'normalize_internal_archive_links_html'], 999);
            add_filter('render_block', [$this, 'normalize_internal_archive_links_html'], 999);
        }
        
        // Convert URLs to lowercase
        if (!empty($settings['convert_to_lowercase'])) {
            add_action('template_redirect', [$this, 'handle_lowercase_urls'], 5);
        }
        
        // Remove stop words from new slugs
        if (!empty($settings['remove_stop_words'])) {
            add_filter('wp_unique_post_slug', [$this, 'remove_stop_words_from_slug'], 10, 4);
        }
        
        // Attachment redirects
        if (!empty($settings['attachment_redirect']) && $settings['attachment_redirect'] !== 'off') {
            add_action('template_redirect', [$this, 'handle_attachment_redirects'], 5);
        }

        // Author archive handling
        if (!empty($settings['author_archive_handling']) && $settings['author_archive_handling'] !== 'default') {
            add_action('template_redirect', [$this, 'handle_author_archive'], 5);
            if ($settings['author_archive_handling'] === 'noindex') {
                add_action('wp_head', [$this, 'add_author_archive_noindex'], 1);
            }
        }

        // Date archive handling
        if (!empty($settings['date_archive_handling']) && $settings['date_archive_handling'] !== 'default') {
            add_action('template_redirect', [$this, 'handle_date_archive'], 5);
            if ($settings['date_archive_handling'] === 'noindex') {
                add_action('wp_head', [$this, 'add_date_archive_noindex'], 1);
            }
        }

        // Auto-redirect on slug change (Pro+ feature - free like Rank Math)
        add_action('post_updated', [$this, 'maybe_create_redirect_on_slug_change'], 10, 3);

        // Head Cleanup features
        if (!empty($settings['remove_query_strings'])) {
            add_filter('script_loader_src', [$this, 'remove_query_strings_from_asset'], 15);
            add_filter('style_loader_src', [$this, 'remove_query_strings_from_asset'], 15);
        }

        if (!empty($settings['remove_feed_links'])) {
            remove_action('wp_head', 'feed_links', 2);
            remove_action('wp_head', 'feed_links_extra', 3);
        }

        if (!empty($settings['remove_shortlink'])) {
            remove_action('wp_head', 'wp_shortlink_wp_head', 10);
            remove_action('template_redirect', 'wp_shortlink_header', 11);
        }

        if (!empty($settings['remove_rest_api_links'])) {
            remove_action('wp_head', 'rest_output_link_wp_head', 10);
            remove_action('template_redirect', 'rest_output_link_header', 11);
        }

        if (!empty($settings['clean_permalinks'])) {
            remove_action('wp_head', 'wp_oembed_add_discovery_links', 10);
            remove_action('wp_head', 'wp_generator');
            remove_action('wp_head', 'rsd_link');
            remove_action('wp_head', 'wlwmanifest_link');
        }

        // Flush rewrite rules when settings change
        add_action('prorank_seo_settings_updated_url_cleanup', [$this, 'maybe_flush_rewrite_rules']);
    }

    /**
     * Strip category base from rewrite rules
     *
     * @param array $rules Category rewrite rules
     * @return array Modified rules
     */
    public function strip_category_base_rewrite_rules(array $rules): array {
        // Keep original category-base rules as fallback, then add safe stripped rules.
        // Avoid broad catch-all patterns (e.g. `(.+?)/?$`) that can hijack page URLs.
        $new_rules = $rules;

        $terms = get_terms([
            'taxonomy' => 'category',
            'hide_empty' => false,
        ]);
        if (is_wp_error($terms) || empty($terms)) {
            return $new_rules;
        }

        foreach ($terms as $term) {
            if (!($term instanceof \WP_Term)) {
                continue;
            }

            $path = $this->get_category_rewrite_path($term);
            if ($path === '' || $this->has_category_path_conflict($path)) {
                continue;
            }

            $quoted = preg_quote($path, '#');
            $category_name = str_replace('$', '\\$', $path);

            $new_rules["{$quoted}/?$"] = "index.php?category_name={$category_name}";
            $new_rules["{$quoted}/feed/(feed|rdf|rss|rss2|atom)/?$"] = "index.php?category_name={$category_name}&feed=\$matches[1]";
            $new_rules["{$quoted}/(feed|rdf|rss|rss2|atom)/?$"] = "index.php?category_name={$category_name}&feed=\$matches[1]";
            $new_rules["{$quoted}/page/?([0-9]{1,})/?$"] = "index.php?category_name={$category_name}&paged=\$matches[1]";
        }

        return $new_rules;
    }

    /**
     * Build hierarchical category path for rewrite mapping.
     *
     * @param \WP_Term $term Category term.
     * @return string
     */
    private function get_category_rewrite_path(\WP_Term $term): string {
        $parts = [];
        $cursor = $term;

        while ($cursor instanceof \WP_Term) {
            if (!empty($cursor->slug)) {
                array_unshift($parts, $cursor->slug);
            }

            if (empty($cursor->parent)) {
                break;
            }

            $cursor = get_term((int) $cursor->parent, 'category');
            if (is_wp_error($cursor)) {
                break;
            }
        }

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

    /**
     * Check if a stripped category path conflicts with an existing public post path.
     *
     * @param string $path Requested path (no leading/trailing slash).
     * @return bool
     */
    private function has_category_path_conflict(string $path): bool {
        $path = trim($path, '/');
        if ($path === '') {
            return true;
        }

        $public_types = array_values(get_post_types(['public' => true], 'names'));
        if (empty($public_types)) {
            return false;
        }

        $existing = get_page_by_path($path, OBJECT, $public_types);
        return $existing instanceof \WP_Post;
    }

    /**
     * Strip category base from term links
     *
     * @param string $term_link Term link URL
     * @param object $term      Term object
     * @param string $taxonomy  Taxonomy slug
     * @return string Modified term link
     */
    public function strip_category_base_term_link(string $term_link, object $term, string $taxonomy): string {
        // Only modify category links
        if ($taxonomy !== 'category') {
            return $term_link;
        }

        // Get category base
        $category_base = get_option('category_base', 'category');
        if (empty($category_base)) {
            $category_base = 'category';
        }
        
        // Remove category base from URL
        $term_link = str_replace('/' . $category_base . '/', '/', $term_link);
        
        return $term_link;
    }

    /**
     * Handle redirects for old category URLs
     *
     * @return void
     */
    public function handle_category_base_redirects(): void {
        // Only run on 404 pages
        if (!is_404()) {
            return;
        }

        // Get the request URI
        $request_uri = prorank_get_server_var( 'REQUEST_URI' );
        if (empty($request_uri)) {
            return;
        }

        // Check if this looks like an old category URL
        $category_base = get_option('category_base', 'category');
        if (empty($category_base)) {
            $category_base = 'category';
        }

        if (strpos($request_uri, '/' . $category_base . '/') !== false) {
            // Remove category base and try to find the category
            $new_uri = str_replace('/' . $category_base . '/', '/', $request_uri);
            
            // Check if the new URL exists
            $category_slug = trim($new_uri, '/');
            $category = get_category_by_slug($category_slug);
            
            if ($category) {
                // Redirect to new URL
                wp_safe_redirect(home_url($new_uri), 301);
                exit;
            }
        }
    }

    /**
     * Strip tag base from rewrite rules
     *
     * @param array $rules Tag rewrite rules
     * @return array Modified rules
     */
    public function strip_tag_base_rewrite_rules(array $rules): array {
        $new_rules = [];
        
        foreach ($rules as $rule => $rewrite) {
            // Remove 'tag/' from the rule
            $new_rule = str_replace('tag/', '', $rule);
            $new_rules[$new_rule] = $rewrite;
        }
        
        return $new_rules;
    }

    /**
     * Strip tag base from term links
     *
     * @param string $term_link Term link URL
     * @param object $term      Term object
     * @param string $taxonomy  Taxonomy slug
     * @return string Modified term link
     */
    public function strip_tag_base_term_link(string $term_link, object $term, string $taxonomy): string {
        // Only modify tag links
        if ($taxonomy !== 'post_tag') {
            return $term_link;
        }

        // Get tag base
        $tag_base = get_option('tag_base', 'tag');
        if (empty($tag_base)) {
            $tag_base = 'tag';
        }
        
        // Remove tag base from URL
        $term_link = str_replace('/' . $tag_base . '/', '/', $term_link);
        
        return $term_link;
    }

    /**
     * Handle redirects for old tag URLs
     *
     * @return void
     */
    public function handle_tag_base_redirects(): void {
        // Only run on 404 pages
        if (!is_404()) {
            return;
        }

        // Get the request URI
        $request_uri = prorank_get_server_var( 'REQUEST_URI' );
        if (empty($request_uri)) {
            return;
        }

        // Check if this looks like an old tag URL
        $tag_base = get_option('tag_base', 'tag');
        if (empty($tag_base)) {
            $tag_base = 'tag';
        }

        if (strpos($request_uri, '/' . $tag_base . '/') !== false) {
            // Remove tag base and try to find the tag
            $new_uri = str_replace('/' . $tag_base . '/', '/', $request_uri);
            
            // Check if the new URL exists
            $tag_slug = trim($new_uri, '/');
            $tag = get_term_by('slug', $tag_slug, 'post_tag');
            
            if ($tag) {
                // Redirect to new URL
                wp_safe_redirect(home_url($new_uri), 301);
                exit;
            }
        }
    }

    /**
     * Remove replytocom parameter from comment reply links
     *
     * @param array $args Comment reply link arguments
     * @return array Modified arguments
     */
    public function remove_replytocom_parameter(array $args): array {
        // Remove the replytocom parameter
        $args['add_below'] = 'comment';
        $args['respond_id'] = 'respond';
        
        return $args;
    }

    /**
     * Filter comment reply link HTML
     *
     * @param string $link Comment reply link HTML
     * @return string Modified link HTML
     */
    public function filter_comment_reply_link(string $link): string {
        // Remove replytocom parameter from link
        $link = preg_replace('/\?replytocom=\d+#respond/', '#respond', $link);
        $link = preg_replace('/&replytocom=\d+#respond/', '#respond', $link);
        
        return $link;
    }

    /**
     * Redirect URLs with replytocom parameter
     *
     * @return void
     */
    public function redirect_replytocom_urls(): void {
        // Check for replytocom parameter
        $replytocom = filter_input(INPUT_GET, 'replytocom', FILTER_VALIDATE_INT);
        if (!$replytocom || empty($replytocom)) {
            return;
        }

        // Build clean URL without replytocom
        $redirect_url = remove_query_arg('replytocom');
        
        // Add fragment for the comment
        $redirect_url .= '#comment-' . absint($replytocom);
        
        // Redirect
        wp_safe_redirect($redirect_url, 301);
        exit;
    }

    /**
     * Handle attachment page redirects
     *
     * @return void
     */
    public function handle_attachment_redirects(): void {
        // Only run on attachment pages
        if (!is_attachment()) {
            return;
        }

        // Get settings
        $settings = $this->get_settings();
        $redirect_type = $settings['attachment_redirect'] ?? 'off';
        
        if ($redirect_type === 'off') {
            return;
        }

        global $post;
        if (!$post) {
            return;
        }

        $redirect_url = '';
        
        switch ($redirect_type) {
            case 'parent':
                // Redirect to parent post
                $parent_id = $post->post_parent;
                if ($parent_id) {
                    $redirect_url = get_permalink($parent_id);
                } else {
                    // No parent, redirect to home
                    $redirect_url = home_url('/');
                }
                break;
                
            case 'file':
                // Redirect to attachment file URL
                $redirect_url = wp_get_attachment_url($post->ID);
                break;
        }

        if (!empty($redirect_url)) {
            wp_safe_redirect($redirect_url, 301);
            exit;
        }
    }

    /**
     * Flush rewrite rules when category is changed
     *
     * @param int $term_id Term ID
     * @return void
     */
    public function flush_rewrite_rules_on_category_change(int $term_id): void {
        // Set transient to flush rules on next page load
        set_transient('prorank_seo_flush_rules', true, 60);
    }

    /**
     * Maybe flush rewrite rules after settings update
     *
     * @param array $settings Updated settings
     * @return void
     */
    public function maybe_flush_rewrite_rules(array $settings): void {
        // Check if strip_category_base setting changed
        $old_settings = $this->get_settings();
        
        if (isset($settings['strip_category_base']) && 
            $settings['strip_category_base'] !== ($old_settings['strip_category_base'] ?? false)) {
            flush_rewrite_rules();
        }
    }

    /**
     * Redirect URLs with UTM parameters
     *
     * @return void
     */
    public function redirect_utm_urls(): void {
        $settings = $this->get_settings();

        if (empty($settings['remove_utm_parameters']) && empty($settings['utm_to_fragment'])) {
            return;
        }

        $request_uri = (string) ($_SERVER['REQUEST_URI'] ?? '');
        if ($request_uri === '') {
            return;
        }

        $parsed = wp_parse_url($request_uri);
        if (!is_array($parsed)) {
            return;
        }

        $query_params = [];
        wp_parse_str((string) ($parsed['query'] ?? ''), $query_params);

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

        [$remaining_params, $removed_params] = $this->partition_tracking_query_params($query_params, $settings);

        if (empty($removed_params)) {
            return;
        }

        $path = (string) ($parsed['path'] ?? '/');
        $redirect_url = home_url($path === '' ? '/' : $path);

        if (!empty($remaining_params)) {
            $redirect_url = add_query_arg($remaining_params, $redirect_url);
        }

        if (!empty($settings['utm_to_fragment'])) {
            $fragment = http_build_query($removed_params);
            if ($fragment !== '') {
                $redirect_url .= '#' . $fragment;
            }
        }

        wp_safe_redirect($redirect_url, 301);
        exit;
    }
    
    /**
     * Handle trailing slash requirements
     *
     * @return void
     */
    public function handle_trailing_slash(): void {
        // Skip admin, login, and file requests
        if (is_admin() || $GLOBALS['pagenow'] === 'wp-login.php') {
            return;
        }
        
        $request_uri = prorank_get_server_var( 'REQUEST_URI' );
        if (empty($request_uri)) {
            return;
        }
        
        // Parse URL
        $parsed = wp_parse_url($request_uri);
        $path = $parsed['path'] ?? '/';
        
        // Skip if it's a file
        if (preg_match('/\.[a-zA-Z0-9]+$/', $path)) {
            return;
        }
        
        // Skip home page
        if ($path === '/' || $path === '') {
            return;
        }
        
        $settings = $this->get_settings();
        $has_trailing = substr($path, -1) === '/';
        $needs_redirect = false;
        $new_path = $path;
        
        if (!empty($settings['force_trailing_slash']) && !$has_trailing) {
            $new_path = $path . '/';
            $needs_redirect = true;
        } elseif (!empty($settings['remove_trailing_slash']) && $has_trailing) {
            $new_path = rtrim($path, '/');
            $needs_redirect = true;
        }
        
        if ($needs_redirect) {
            $redirect_url = home_url($new_path);
            if (!empty($parsed['query'])) {
                $redirect_url .= '?' . $parsed['query'];
            }
            wp_safe_redirect($redirect_url, 301);
            exit;
        }
    }
    
    /**
     * Handle lowercase URL requirements
     *
     * @return void
     */
    public function handle_lowercase_urls(): void {
        $request_uri = prorank_get_server_var( 'REQUEST_URI' );
        if (empty($request_uri)) {
            return;
        }
        
        // Parse URL
        $parsed = wp_parse_url($request_uri);
        $path = $parsed['path'] ?? '/';
        
        // Check if path contains uppercase letters
        if ($path !== strtolower($path)) {
            $redirect_url = home_url(strtolower($path));
            if (!empty($parsed['query'])) {
                $redirect_url .= '?' . $parsed['query'];
            }
            wp_safe_redirect($redirect_url, 301);
            exit;
        }
    }

    /**
     * Start the output buffer used to normalize rendered internal archive links.
     *
     * @return void
     */
    public function start_internal_archive_link_normalizer(): void {
        if ($this->archive_link_normalizer_started || !$this->should_normalize_archive_links_for_request()) {
            return;
        }

        $this->archive_link_normalizer_started = true;
        ob_start([$this, 'normalize_internal_archive_links_html']);
    }

    /**
     * Normalize same-site archive links in final HTML output.
     *
     * @param string $html Final HTML output
     * @return string
     */
    public function normalize_internal_archive_links_html(string $html): string {
        if ($html === '' || stripos($html, 'href=') === false) {
            return $html;
        }

        $settings = $this->get_settings();
        $policy = (string) ($settings['author_date_archive_link_policy'] ?? 'default');
        $should_normalize = !empty($settings['normalize_internal_archive_links']);
        $updated = $html;

        if ($should_normalize) {
            $updated = preg_replace_callback(
                '/\bhref\s*=\s*(["\'])([^"\']+)\1/i',
                function (array $matches): string {
                    $normalized = $this->normalize_internal_archive_link_url($matches[2]);

                    if ($normalized === null || $normalized === $matches[2]) {
                        return $matches[0];
                    }

                    return 'href=' . $matches[1] . esc_attr($normalized) . $matches[1];
                },
                $updated
            );

            if (!is_string($updated)) {
                $updated = $html;
            }
        }

        if ($policy !== 'default' && stripos($updated, '<a') !== false) {
            if ($policy === 'nofollow') {
                $policy_updated = preg_replace_callback(
                    '#<a\b([^>]*?)href\s*=\s*(["\'])([^"\']+)\2([^>]*)>#i',
                    function (array $matches): string {
                        $link_type = $this->get_internal_archive_link_type($matches[3]);

                        if (!in_array($link_type, ['author', 'date'], true)) {
                            return $matches[0];
                        }

                        $attributes = trim($matches[1] . ' href=' . $matches[2] . $matches[3] . $matches[2] . $matches[4]);
                        return '<a' . $this->ensure_anchor_rel_token(' ' . $attributes, 'nofollow') . '>';
                    },
                    $updated
                );
            } else {
                $policy_updated = preg_replace_callback(
                    '#<a\b([^>]*\bhref\s*=\s*(["\'])([^"\']+)\2[^>]*)>(.*?)</a>#is',
                    function (array $matches): string {
                        $link_type = $this->get_internal_archive_link_type($matches[3]);

                        if (!in_array($link_type, ['author', 'date'], true)) {
                            return $matches[0];
                        }

                        return $matches[4];
                    },
                    $updated
                );
            }

            if (is_string($policy_updated)) {
                $updated = $policy_updated;
            }
        }

        return $updated;
    }
    
    /**
     * Remove stop words from slug
     *
     * @param string $slug          The post slug
     * @param int    $post_ID       Post ID
     * @param string $post_status   Post status
     * @param string $post_type     Post type
     * @return string Modified slug
     */
    public function remove_stop_words_from_slug(string $slug, int $post_ID, string $post_status, string $post_type): string {
        // Only process published posts and pages
        if (!in_array($post_type, ['post', 'page']) || $post_status !== 'publish') {
            return $slug;
        }
        
        // List of stop words to remove
        $stop_words = [
            'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from',
            'has', 'he', 'in', 'is', 'it', 'its', 'of', 'on', 'that', 'the',
            'to', 'was', 'will', 'with', 'the', 'this', 'but', 'they', 'have',
            'had', 'what', 'when', 'where', 'who', 'which', 'why', 'how'
        ];
        
        // Split slug into words
        $words = explode('-', $slug);
        
        // Remove stop words
        $filtered_words = array_filter($words, function($word) use ($stop_words) {
            return !in_array(strtolower($word), $stop_words);
        });
        
        // Rebuild slug
        $new_slug = implode('-', $filtered_words);
        
        // Ensure we have a valid slug
        if (empty($new_slug)) {
            return $slug;
        }
        
        return $new_slug;
    }
    
    /**
     * Handle author archive pages based on settings
     *
     * @return void
     */
    public function handle_author_archive(): void {
        if (!is_author()) {
            return;
        }

        $settings = $this->get_settings();
        $handling = $settings['author_archive_handling'] ?? 'default';

        switch ($handling) {
            case 'redirect_home':
                wp_safe_redirect(home_url('/'), 301);
                exit;

            case 'disable_404':
                global $wp_query;
                $wp_query->set_404();
                status_header(404);
                nocache_headers();
                break;
        }
    }

    /**
     * Handle date archive pages based on settings
     *
     * @return void
     */
    public function handle_date_archive(): void {
        if (!is_date()) {
            return;
        }

        $settings = $this->get_settings();
        $handling = $settings['date_archive_handling'] ?? 'default';

        switch ($handling) {
            case 'redirect_home':
                wp_safe_redirect(home_url('/'), 301);
                exit;

            case 'disable_404':
                global $wp_query;
                $wp_query->set_404();
                status_header(404);
                nocache_headers();
                break;
        }
    }

    /**
     * Add noindex meta tag to author archive pages
     *
     * @return void
     */
    public function add_author_archive_noindex(): void {
        if (is_author()) {
            echo '<meta name="robots" content="noindex, follow" />' . "\n";
        }
    }

    /**
     * Add noindex meta tag to date archive pages
     *
     * @return void
     */
    public function add_date_archive_noindex(): void {
        if (is_date()) {
            echo '<meta name="robots" content="noindex, follow" />' . "\n";
        }
    }

    /**
     * Check whether the current request should have archive-link normalization.
     *
     * @return bool
     */
    private function should_normalize_archive_links_for_request(): bool {
        if (is_admin() || wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) {
            return false;
        }

        if (defined('DOING_CRON') && DOING_CRON) {
            return false;
        }

        if (is_feed() || is_trackback() || is_preview() || is_robots()) {
            return false;
        }

        $request_uri = (string) prorank_get_server_var('REQUEST_URI');
        if ($request_uri === '') {
            return false;
        }

        $path = (string) wp_parse_url($request_uri, PHP_URL_PATH);
        if ($path !== '' && preg_match('/\.(?:xml|xsl|json|txt|map|ico|css|js)$/i', $path)) {
            return false;
        }

        return true;
    }

    /**
     * Normalize a same-site archive URL to the configured slash format.
     *
     * @param string $url URL from rendered HTML
     * @return string|null
     */
    private function normalize_internal_archive_link_url(string $url): ?string {
        $trimmed_url = trim($url);

        if ($trimmed_url === '' || $trimmed_url[0] === '#') {
            return null;
        }

        if (preg_match('#^(?:mailto:|tel:|javascript:|data:)#i', $trimmed_url)) {
            return null;
        }

        $decoded_url = html_entity_decode($trimmed_url, ENT_QUOTES);
        $parsed_url = wp_parse_url($decoded_url);

        if (!is_array($parsed_url) || !$this->is_same_site_archive_link($parsed_url)) {
            return null;
        }

        $path = (string) ($parsed_url['path'] ?? '');
        if ($path === '' || !$this->is_normalizable_archive_path($path)) {
            return null;
        }

        $normalized_path = $this->normalize_archive_path($path);
        if ($normalized_path === $path) {
            return null;
        }

        return $this->rebuild_archive_link_url($trimmed_url, $parsed_url, $normalized_path);
    }

    /**
     * Determine the supported archive-link type for a rendered URL.
     *
     * @param string $url URL from rendered HTML
     * @return string|null
     */
    private function get_internal_archive_link_type(string $url): ?string {
        $trimmed_url = trim($url);

        if ($trimmed_url === '' || $trimmed_url[0] === '#') {
            return null;
        }

        if (preg_match('#^(?:mailto:|tel:|javascript:|data:)#i', $trimmed_url)) {
            return null;
        }

        $decoded_url = html_entity_decode($trimmed_url, ENT_QUOTES);
        $parsed_url = wp_parse_url($decoded_url);

        if (!is_array($parsed_url) || !$this->is_same_site_archive_link($parsed_url)) {
            return null;
        }

        $path = (string) ($parsed_url['path'] ?? '');
        if ($path === '') {
            return null;
        }

        return $this->get_archive_link_type_from_path($path);
    }

    /**
     * Determine whether a parsed URL points to the current site.
     *
     * @param array $parsed_url Parsed URL
     * @return bool
     */
    private function is_same_site_archive_link(array $parsed_url): bool {
        if (empty($parsed_url['host'])) {
            return !empty($parsed_url['path']) && strpos((string) $parsed_url['path'], '/') === 0;
        }

        $home_host = (string) wp_parse_url(home_url('/'), PHP_URL_HOST);
        if ($home_host === '') {
            return false;
        }

        return strtolower((string) $parsed_url['host']) === strtolower($home_host);
    }

    /**
     * Check whether the path is a supported archive link that can be normalized safely.
     *
     * @param string $path URL path
     * @return bool
     */
    private function is_normalizable_archive_path(string $path): bool {
        if ($path === '' || $path === '/') {
            return false;
        }

        if (preg_match('/\.[a-z0-9]{1,8}$/i', $path)) {
            return false;
        }

        if (preg_match('#^/(?:wp-admin|wp-json|wp-content|wp-includes)(?:/|$)#i', $path)) {
            return false;
        }

        if (preg_match('#/(?:feed|embed|amp)/?$#i', $path)) {
            return false;
        }

        return $this->get_archive_link_type_from_path($path) !== null;
    }

    /**
     * Return the supported archive type for a path.
     *
     * @param string $path URL path
     * @return string|null
     */
    private function get_archive_link_type_from_path(string $path): ?string {
        $normalized_path = untrailingslashit($path);

        if (preg_match('~^/author/[^/?#]+(?:/page/\d+)?$~i', $normalized_path)) {
            return 'author';
        }

        if (preg_match('~^/(?:\d{4})(?:/\d{2}){0,2}(?:/page/\d+)?$~i', $normalized_path)) {
            return 'date';
        }

        if (preg_match('~^/category/[^?#]+$~i', $normalized_path)) {
            return 'category';
        }

        if (preg_match('~^/tag/[^?#]+$~i', $normalized_path)) {
            return 'tag';
        }

        return null;
    }

    /**
     * Apply the configured slash preference to a supported archive path.
     *
     * @param string $path Archive path
     * @return string
     */
    private function normalize_archive_path(string $path): string {
        $settings = $this->get_settings();
        $normalized_path = untrailingslashit($path);

        if (!empty($settings['remove_trailing_slash'])) {
            return $normalized_path;
        }

        if (!empty($settings['force_trailing_slash'])) {
            return trailingslashit($normalized_path);
        }

        return user_trailingslashit($normalized_path);
    }

    /**
     * Rebuild the normalized URL while preserving query strings and fragments.
     *
     * @param string $original_url Original URL string
     * @param array  $parsed_url   Parsed URL parts
     * @param string $path         Normalized path
     * @return string
     */
    private function rebuild_archive_link_url(string $original_url, array $parsed_url, string $path): string {
        $rebuilt_url = $path;

        if (!empty($parsed_url['host'])) {
            $host = (string) $parsed_url['host'];
            $port = isset($parsed_url['port']) ? ':' . (string) $parsed_url['port'] : '';

            if (strpos($original_url, '//') === 0 && empty($parsed_url['scheme'])) {
                $rebuilt_url = '//' . $host . $port . $path;
            } else {
                $scheme = (string) ($parsed_url['scheme'] ?? wp_parse_url(home_url('/'), PHP_URL_SCHEME) ?: 'https');
                $rebuilt_url = $scheme . '://' . $host . $port . $path;
            }
        }

        if (isset($parsed_url['query']) && $parsed_url['query'] !== '') {
            $rebuilt_url .= '?' . $parsed_url['query'];
        }

        if (isset($parsed_url['fragment']) && $parsed_url['fragment'] !== '') {
            $rebuilt_url .= '#' . $parsed_url['fragment'];
        }

        return $rebuilt_url;
    }

    /**
     * Ensure a rendered anchor has the required rel token.
     *
     * @param string $attributes Raw anchor attributes
     * @param string $token      Rel token to enforce
     * @return string
     */
    private function ensure_anchor_rel_token(string $attributes, string $token): string {
        if (preg_match('/\brel\s*=\s*(["\'])(.*?)\1/i', $attributes, $matches)) {
            $rel_tokens = preg_split('/\s+/', trim((string) $matches[2])) ?: [];
            $rel_tokens = array_values(array_unique(array_filter($rel_tokens)));

            if (!in_array($token, $rel_tokens, true)) {
                $rel_tokens[] = $token;
            }

            $replacement = 'rel=' . $matches[1] . esc_attr(implode(' ', $rel_tokens)) . $matches[1];
            return (string) preg_replace('/\brel\s*=\s*(["\'])(.*?)\1/i', $replacement, $attributes, 1);
        }

        return rtrim($attributes) . ' rel="nofollow"';
    }

    /**
     * Auto-create redirect when post/page slug changes (Pro+ feature, free like Rank Math)
     *
     * @param int     $post_id      Post ID
     * @param WP_Post $post_after   Post object after the update
     * @param WP_Post $post_before  Post object before the update
     * @return void
     */
    public function maybe_create_redirect_on_slug_change($post_id, $post_after, $post_before): void {
        // Only for published posts/pages
        if ($post_after->post_status !== 'publish' || $post_before->post_status !== 'publish') {
            return;
        }

        // Skip autosaves
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return;
        }

        // Check if slug actually changed
        if ($post_before->post_name === $post_after->post_name) {
            return;
        }

        // Don't create redirect if slug is empty
        if (empty($post_before->post_name) || empty($post_after->post_name)) {
            return;
        }

        // Get old and new URLs
        // We need to temporarily restore the old slug to get the old URL
        $old_slug = $post_before->post_name;
        $new_slug = $post_after->post_name;

        // Build the old URL path
        $old_permalink = str_replace($new_slug, $old_slug, get_permalink($post_id));
        $new_permalink = get_permalink($post_id);

        // Parse URLs to get paths
        $old_path = wp_parse_url($old_permalink, PHP_URL_PATH);
        $new_path = wp_parse_url($new_permalink, PHP_URL_PATH);

        if (!$old_path || !$new_path || $old_path === $new_path) {
            return;
        }

        // Get existing redirects
        $redirects = get_option('prorank_seo_redirects', []);

        // Check if redirect already exists
        foreach ($redirects as $redirect) {
            if (($redirect['source'] ?? '') === $old_path) {
                return; // Redirect already exists
            }
        }

        // Add the new redirect
        $redirects[] = [
            'id' => uniqid('redirect_'),
            'source' => $old_path,
            'destination' => $new_path,
            'type' => '301',
            'enabled' => true,
            'created' => current_time('mysql'),
            'auto_created' => true,
            'reason' => sprintf(
/* translators: %1$: value, %2$: value */
                __('Auto-created when slug changed from "%1$s" to "%2$s"', 'prorank-seo'),
                $old_slug,
                $new_slug
            ),
        ];

        update_option('prorank_seo_redirects', $redirects);
    }

    /**
     * Remove version query strings from static assets
     *
     * @param string $src Asset URL
     * @return string Modified URL
     */
    public function remove_query_strings_from_asset($src) {
        if (!is_string($src)) {
            return $src;
        }
        if ($this->is_prorank_build_asset_url($src)) {
            return $src;
        }
        if (strpos($src, '?ver=') !== false) {
            $src = remove_query_arg('ver', $src);
        }
        return $src;
    }

    /**
     * Keep version query strings for assets that are updated in-place.
     * This avoids stale caches on long-lived immutable cache policies.
     *
     * @param string $url
     * @return bool
     */
    private function is_prorank_build_asset_url(string $url): bool {
        $path = (string) wp_parse_url($url, PHP_URL_PATH);
        if ($path === '') {
            return false;
        }

        return strpos($path, '/wp-content/plugins/prorank-seo/build/') !== false
            || strpos($path, '/wp-content/plugins/prorank-seo/build/') !== false
            || strpos($path, '/wp-content/uploads/elementor/css/') !== false;
    }

    /**
     * Get module settings
     *
     * @return array
     */
    private function get_settings(): array {
        // Get individual settings with defaults
        $defaults = [
            'strip_category_base' => false,
            'strip_tag_base' => false,
            'remove_replytocom' => false,
            'remove_utm_parameters' => false,
            'utm_to_fragment' => false,
            'force_trailing_slash' => false,
            'remove_trailing_slash' => false,
            'normalize_internal_archive_links' => false,
            'author_date_archive_link_policy' => 'default',
            'convert_to_lowercase' => false,
            'remove_stop_words' => false,
            'attachment_redirect' => 'off',
            'author_archive_handling' => 'default',
            'date_archive_handling' => 'default',
            // Head Cleanup settings
            'remove_query_strings' => false,
            'remove_feed_links' => false,
            'remove_shortlink' => false,
            'remove_rest_api_links' => false,
            'clean_permalinks' => false,
            'param_whitelist' => 'gclid,fbclid,msclkid,_ga',
        ];

        $grouped_settings = [];

        try {
            $grouped_settings = $this->plugin()->settings()->get_settings('url_cleanup');
        } catch (\Throwable $e) {
            $grouped_settings = [];
        }

        if (!is_array($grouped_settings)) {
            $grouped_settings = [];
        }

        $settings = array_merge($defaults, array_intersect_key($grouped_settings, $defaults));

        foreach ($defaults as $key => $default) {
            $settings[$key] = get_option('prorank_seo_url_cleanup_' . $key, $settings[$key] ?? $default);
        }

        return $settings;
    }

    /**
     * Split query parameters into remaining and removed tracking parameters.
     *
     * @param array $query_params
     * @param array $settings
     * @return array{0: array, 1: array}
     */
    private function partition_tracking_query_params(array $query_params, array $settings): array {
        $remaining = $query_params;
        $removed = [];
        $whitelist = $this->get_param_whitelist($settings);
        $tracking_keys = $this->get_tracking_param_names();

        foreach ($query_params as $key => $value) {
            $normalized_key = strtolower((string) $key);
            $is_tracking = str_starts_with($normalized_key, 'utm_') || in_array($normalized_key, $tracking_keys, true);

            if (!$is_tracking || in_array($normalized_key, $whitelist, true)) {
                continue;
            }

            $removed[$key] = $value;
            unset($remaining[$key]);
        }

        return [$remaining, $removed];
    }

    /**
     * Parse the saved whitelist into normalized lowercase keys.
     *
     * @param array $settings
     * @return array<int, string>
     */
    private function get_param_whitelist(array $settings): array {
        $raw = (string) ($settings['param_whitelist'] ?? '');
        if ($raw === '') {
            return [];
        }

        $parts = array_map('trim', explode(',', $raw));
        $parts = array_filter($parts, static fn($part): bool => $part !== '');

        return array_values(array_unique(array_map('strtolower', $parts)));
    }

    /**
     * Get supported tracking parameter names in addition to the generic utm_* family.
     *
     * @return array<int, string>
     */
    private function get_tracking_param_names(): array {
        return [
            '_ga',
            'dclid',
            'fbclid',
            'gbraid',
            'gclid',
            'mc_cid',
            'mc_eid',
            'msclkid',
            'twclid',
            'wbraid',
            'yclid',
        ];
    }

    /**
     * Get module details
     *
     * @return array
     */
    public function get_details(): array {
        return [
            'name'        => __('URL & Permalink Cleanup', 'prorank-seo'),
            'description' => __('Clean up your URLs by removing category base, reply parameters, and managing attachment pages.', 'prorank-seo'),
            'tier'        => 'free',
            'parent'      => 'technical-seo',
        ];
    }

    /**
     * Check if module meets requirements
     *
     * @return bool
     */
    public function meets_requirements(): bool {
        return true;
    }
}
