<?php

/**
 * HTML Sitemap Module
 *
 * @package ProRank\SEO\Modules\TechnicalSEO
 * @since   0.1.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\TechnicalSEO;

use ProRank\SEO\Core\Config\Settings;
use ProRank\SEO\Modules\BaseModule;

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


/**
 * Class HtmlSitemapModule
 * 
 * Generates HTML sitemaps for users
 */


class HtmlSitemapModule extends BaseModule {
    /**
     * Module slug
     *
     * @var string
     */
    protected string $slug = 'html-sitemap';

    /**
     * Module name
     *
     * @var string
     */
    protected string $name = 'HTML Sitemap';

    /**
     * Module description
     *
     * @var string
     */
    protected string $description = 'Generate user-friendly HTML sitemaps';

    /**
     * Required tier
     *
     * @var string
     */
    protected string $feature_tier = 'free';

    /**
     * Parent module
     *
     * @var string|null
     */
    protected ?string $parent_slug = 'technical-seo';

    /**
     * Initialize module hooks
     *
     * @return void
     */
    public function init_hooks(): void {
        if (!$this->is_enabled()) {
            return;
        }

        // Register shortcode
        add_shortcode('prorank_sitemap', [$this, 'render_sitemap_shortcode']);
        add_shortcode('prorank_html_sitemap', [$this, 'render_sitemap_shortcode']);
        
        // Register block if Gutenberg is available
        add_action('init', [$this, 'register_sitemap_block']);
        
        // Add widget
        add_action('widgets_init', [$this, 'register_sitemap_widget']);
        
        // Enqueue styles
        add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_styles']);

        // Serve a fallback HTML sitemap route when no dedicated page exists.
        add_action('template_redirect', [$this, 'handle_virtual_sitemap_request'], 5);
    }

    /**
     * Render sitemap shortcode
     *
     * @param array $atts Shortcode attributes
     * @return string
     */
    public function render_sitemap_shortcode($atts): string {
        if (!$this->is_html_sitemap_enabled()) {
            return '';
        }

        $defaults = [
            'include' => 'pages,posts,categories,tags',
            'exclude' => '',
            'exclude_ids' => '',
            'depth' => '0',
            'exclude_empty' => 'true',
            'show_post_count' => 'true',
            'show_date' => 'false',
            'show_excerpt' => 'false',
            'columns' => '1',
            'style' => 'list',
            'heading_tag' => 'h3',
            'sort_order' => 'title',
            'class' => '',
        ];
        
        $atts = shortcode_atts($defaults, $atts, 'prorank_sitemap');
        
        // Parse includes
        $include_types = array_map('trim', explode(',', $atts['include']));
        $exclude_types = array_map('trim', explode(',', $atts['exclude']));
        $include_types = array_diff($include_types, $exclude_types);
        
        // Parse excluded IDs
        $exclude_ids = [];
        if (!empty($atts['exclude_ids'])) {
            $exclude_ids = array_map('intval', explode(',', $atts['exclude_ids']));
        }
        
        // Start output
        $output = '<div class="prorank-html-sitemap ' . esc_attr($atts['class']) . '">';
        
        // Apply columns if needed
        if ($atts['columns'] > 1) {
            $output .= '<div class="prorank-sitemap-columns" data-columns="' . esc_attr($atts['columns']) . '">';
        }
        
        // Generate sitemap sections
        foreach ($include_types as $type) {
            switch ($type) {
                case 'pages':
                    $output .= $this->render_pages_section($atts, $exclude_ids);
                    break;
                    
                case 'posts':
                    $output .= $this->render_posts_section($atts, $exclude_ids);
                    break;
                    
                case 'categories':
                    $output .= $this->render_categories_section($atts, $exclude_ids);
                    break;
                    
                case 'tags':
                    $output .= $this->render_tags_section($atts, $exclude_ids);
                    break;
                    
                case 'authors':
                    $output .= $this->render_authors_section($atts, $exclude_ids);
                    break;
                    
                default:
                    // Check if it's a custom post type
                    if (post_type_exists($type)) {
                        $output .= $this->render_custom_post_type_section($type, $atts, $exclude_ids);
                    }
                    // Check if it's a custom taxonomy
                    elseif (taxonomy_exists($type)) {
                        $output .= $this->render_custom_taxonomy_section($type, $atts, $exclude_ids);
                    }
                    break;
            }
        }
        
        if ($atts['columns'] > 1) {
            $output .= '</div>';
        }
        
        $output .= '</div>';
        
        return $output;
    }

    /**
     * Check whether a shortcode attribute is truthy.
     *
     * @param array  $atts Shortcode attributes.
     * @param string $key  Attribute key.
     * @return bool
     */
    private function is_true(array $atts, string $key): bool {
        return isset($atts[$key]) && filter_var($atts[$key], FILTER_VALIDATE_BOOLEAN);
    }

    /**
     * Resolve sort order settings for posts and custom post types.
     *
     * @param array $atts Shortcode attributes.
     * @return array<string, string>
     */
    private function get_post_sort_args(array $atts): array {
        $sort_order = (string) ($atts['sort_order'] ?? 'title');

        switch ($sort_order) {
            case 'date':
                return [
                    'orderby' => 'date',
                    'order' => 'DESC',
                ];

            case 'modified':
                return [
                    'orderby' => 'modified',
                    'order' => 'DESC',
                ];

            case 'menu_order':
                return [
                    'orderby' => 'menu_order title',
                    'order' => 'ASC',
                ];

            case 'title':
            default:
                return [
                    'orderby' => 'title',
                    'order' => 'ASC',
                ];
        }
    }

    /**
     * Resolve sort order settings for hierarchical pages.
     *
     * @param array $atts Shortcode attributes.
     * @return array<string, string>
     */
    private function get_page_sort_args(array $atts): array {
        $sort_order = (string) ($atts['sort_order'] ?? 'title');

        switch ($sort_order) {
            case 'date':
                return [
                    'sort_column' => 'post_date',
                    'sort_order' => 'DESC',
                ];

            case 'modified':
                return [
                    'sort_column' => 'post_modified',
                    'sort_order' => 'DESC',
                ];

            case 'menu_order':
                return [
                    'sort_column' => 'menu_order,post_title',
                    'sort_order' => 'ASC',
                ];

            case 'title':
            default:
                return [
                    'sort_column' => 'post_title',
                    'sort_order' => 'ASC',
                ];
        }
    }

    /**
     * Resolve taxonomy term sort arguments.
     *
     * @param array $atts Shortcode attributes.
     * @return array<string, string>
     */
    private function get_term_sort_args(array $atts): array {
        $sort_order = (string) ($atts['sort_order'] ?? 'title');

        switch ($sort_order) {
            case 'date':
            case 'modified':
                return [
                    'orderby' => 'term_id',
                    'order' => 'DESC',
                ];

            case 'menu_order':
            case 'title':
            default:
                return [
                    'orderby' => 'name',
                    'order' => 'ASC',
                ];
        }
    }

    /**
     * Resolve author sort arguments.
     *
     * @param array $atts Shortcode attributes.
     * @return array<string, string>
     */
    private function get_author_sort_args(array $atts): array {
        $sort_order = (string) ($atts['sort_order'] ?? 'title');

        switch ($sort_order) {
            case 'date':
            case 'modified':
                return [
                    'orderby' => 'registered',
                    'order' => 'DESC',
                ];

            case 'menu_order':
            case 'title':
            default:
                return [
                    'orderby' => 'display_name',
                    'order' => 'ASC',
                ];
        }
    }

    /**
     * Render pages section
     *
     * @param array $atts       Shortcode attributes
     * @param array $exclude_ids Excluded IDs
     * @return string
     */
    private function render_pages_section(array $atts, array $exclude_ids): string {
        $args = [
            'post_type'   => 'page',
            'post_status' => 'publish',
            'exclude'     => implode(',', $exclude_ids),
            'echo'        => false,
            'title_li'    => '',
            'depth'       => intval($atts['depth']),
        ];

        $args = array_merge($args, $this->get_page_sort_args($atts));
        
        // Get hierarchical list
        $pages = wp_list_pages($args);
        
        if (empty($pages)) {
            return '';
        }
        
        $output = '<div class="prorank-sitemap-section prorank-sitemap-pages">';
        $output .= '<' . esc_html($atts['heading_tag']) . ' class="prorank-sitemap-heading">';
        $output .= esc_html__('Pages', 'prorank-seo');
        $output .= '</' . esc_html($atts['heading_tag']) . '>';
        
        if ($atts['style'] === 'list') {
            $output .= '<ul class="prorank-sitemap-list">' . $pages . '</ul>';
        } else {
            $output .= '<div class="prorank-sitemap-grid">' . $this->convert_list_to_grid($pages) . '</div>';
        }
        
        $output .= '</div>';
        
        return $output;
    }

    /**
     * Render posts section
     *
     * @param array $atts       Shortcode attributes
     * @param array $exclude_ids Excluded IDs
     * @return string
     */
    private function render_posts_section(array $atts, array $exclude_ids): string {
        $args = [
            'post_type'      => 'post',
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'post__not_in'   => $exclude_ids,
        ];

        $args = array_merge($args, $this->get_post_sort_args($atts));
        
        $posts = get_posts($args);
        
        if (empty($posts)) {
            return '';
        }
        
        $output = '<div class="prorank-sitemap-section prorank-sitemap-posts">';
        $output .= '<' . esc_html($atts['heading_tag']) . ' class="prorank-sitemap-heading">';
        $output .= esc_html__('Posts', 'prorank-seo');
        $output .= '</' . esc_html($atts['heading_tag']) . '>';
        
        if ($atts['style'] === 'list') {
            $output .= '<ul class="prorank-sitemap-list">';
            
            foreach ($posts as $post) {
                $output .= '<li>';
                $output .= '<a href="' . esc_url(get_permalink($post)) . '">' . esc_html($post->post_title) . '</a>';
                
                if ($atts['show_date'] === 'true') {
                    $output .= ' <span class="prorank-sitemap-date">(' . get_the_date('', $post) . ')</span>';
                }
                
                if ($atts['show_excerpt'] === 'true' && !empty($post->post_excerpt)) {
                    $output .= '<div class="prorank-sitemap-excerpt">' . wp_trim_words($post->post_excerpt, 20) . '</div>';
                }
                
                $output .= '</li>';
            }
            
            $output .= '</ul>';
        } else {
            $output .= '<div class="prorank-sitemap-grid">';
            
            foreach ($posts as $post) {
                $output .= '<div class="prorank-sitemap-item">';
                $output .= '<a href="' . esc_url(get_permalink($post)) . '">' . esc_html($post->post_title) . '</a>';
                
                if ($atts['show_date'] === 'true') {
                    $output .= '<div class="prorank-sitemap-meta">' . get_the_date('', $post) . '</div>';
                }
                
                $output .= '</div>';
            }
            
            $output .= '</div>';
        }
        
        $output .= '</div>';
        
        return $output;
    }

    /**
     * Render categories section
     *
     * @param array $atts       Shortcode attributes
     * @param array $exclude_ids Excluded IDs
     * @return string
     */
    private function render_categories_section(array $atts, array $exclude_ids): string {
        $sort_args = $this->get_term_sort_args($atts);
        $args = [
            'taxonomy'   => 'category',
            'hide_empty' => $this->is_true($atts, 'exclude_empty'),
            'echo'       => false,
            'title_li'   => '',
            'depth'      => intval($atts['depth']),
            'show_count' => $this->is_true($atts, 'show_post_count'),
            'orderby'    => $sort_args['orderby'],
            'order'      => $sort_args['order'],
        ];
        
        $categories = wp_list_categories($args);
        
        if (empty($categories)) {
            return '';
        }
        
        $output = '<div class="prorank-sitemap-section prorank-sitemap-categories">';
        $output .= '<' . esc_html($atts['heading_tag']) . ' class="prorank-sitemap-heading">';
        $output .= esc_html__('Categories', 'prorank-seo');
        $output .= '</' . esc_html($atts['heading_tag']) . '>';
        
        if ($atts['style'] === 'list') {
            $output .= '<ul class="prorank-sitemap-list">' . $categories . '</ul>';
        } else {
            $output .= '<div class="prorank-sitemap-grid">' . $this->convert_list_to_grid($categories) . '</div>';
        }
        
        $output .= '</div>';
        
        return $output;
    }

    /**
     * Render tags section
     *
     * @param array $atts       Shortcode attributes
     * @param array $exclude_ids Excluded IDs
     * @return string
     */
    private function render_tags_section(array $atts, array $exclude_ids): string {
        $sort_args = $this->get_term_sort_args($atts);
        $show_post_count = $this->is_true($atts, 'show_post_count');
        $tags = get_tags([
            'hide_empty' => $this->is_true($atts, 'exclude_empty'),
            'orderby'    => $sort_args['orderby'],
            'order'      => $sort_args['order'],
        ]);
        
        if (empty($tags)) {
            return '';
        }
        
        $output = '<div class="prorank-sitemap-section prorank-sitemap-tags">';
        $output .= '<' . esc_html($atts['heading_tag']) . ' class="prorank-sitemap-heading">';
        $output .= esc_html__('Tags', 'prorank-seo');
        $output .= '</' . esc_html($atts['heading_tag']) . '>';
        
        if ($atts['style'] === 'list') {
            $output .= '<ul class="prorank-sitemap-list">';
            
            foreach ($tags as $tag) {
                $output .= '<li>';
                $output .= '<a href="' . esc_url(get_tag_link($tag)) . '">' . esc_html($tag->name) . '</a>';
                if ($show_post_count) {
                    $output .= ' <span class="prorank-sitemap-count">(' . $tag->count . ')</span>';
                }
                $output .= '</li>';
            }
            
            $output .= '</ul>';
        } else {
            $output .= '<div class="prorank-sitemap-grid prorank-sitemap-tags-cloud">';
            
            foreach ($tags as $tag) {
                $output .= '<a href="' . esc_url(get_tag_link($tag)) . '" class="prorank-sitemap-tag">';
                $output .= esc_html($tag->name);
                if ($show_post_count) {
                    $output .= ' (' . $tag->count . ')';
                }
                $output .= '</a>';
            }
            
            $output .= '</div>';
        }
        
        $output .= '</div>';
        
        return $output;
    }

    /**
     * Render authors section
     *
     * @param array $atts       Shortcode attributes
     * @param array $exclude_ids Excluded IDs
     * @return string
     */
    private function render_authors_section(array $atts, array $exclude_ids): string {
        $sort_args = $this->get_author_sort_args($atts);
        $show_post_count = $this->is_true($atts, 'show_post_count');
        $args = [
            'who'         => 'authors',
            'orderby'     => $sort_args['orderby'],
            'order'       => $sort_args['order'],
            'has_published_posts' => true,
        ];
        
        $authors = get_users($args);
        
        if (empty($authors)) {
            return '';
        }
        
        $output = '<div class="prorank-sitemap-section prorank-sitemap-authors">';
        $output .= '<' . esc_html($atts['heading_tag']) . ' class="prorank-sitemap-heading">';
        $output .= esc_html__('Authors', 'prorank-seo');
        $output .= '</' . esc_html($atts['heading_tag']) . '>';
        
        if ($atts['style'] === 'list') {
            $output .= '<ul class="prorank-sitemap-list">';
            
            foreach ($authors as $author) {
                $post_count = count_user_posts($author->ID);
                $output .= '<li>';
                $output .= '<a href="' . esc_url(get_author_posts_url($author->ID)) . '">';
                $output .= esc_html($author->display_name);
                $output .= '</a>';
                if ($show_post_count) {
/* translators: %1$d: number of posts. */
                    $output .= ' <span class="prorank-sitemap-count">(' . sprintf(_n('%1$d post', '%1$d posts', $post_count, 'prorank-seo'), $post_count) . ')</span>';
                }
                $output .= '</li>';
            }
            
            $output .= '</ul>';
        } else {
            $output .= '<div class="prorank-sitemap-grid">';
            
            foreach ($authors as $author) {
                $output .= '<div class="prorank-sitemap-author">';
                $output .= get_avatar($author->ID, 48);
                $output .= '<div class="prorank-sitemap-author-info">';
                $output .= '<a href="' . esc_url(get_author_posts_url($author->ID)) . '">';
                $output .= esc_html($author->display_name);
                $output .= '</a>';
                if ($show_post_count) {
                    $author_post_count = count_user_posts($author->ID);
/* translators: %1$d: number of posts. */
                    $output .= '<div class="prorank-sitemap-meta">' . sprintf(_n('%1$d post', '%1$d posts', $author_post_count, 'prorank-seo'), $author_post_count) . '</div>';
                }
                $output .= '</div>';
                $output .= '</div>';
            }
            
            $output .= '</div>';
        }
        
        $output .= '</div>';
        
        return $output;
    }

    /**
     * Render custom post type section
     *
     * @param string $post_type  Post type
     * @param array  $atts       Shortcode attributes
     * @param array  $exclude_ids Excluded IDs
     * @return string
     */
    private function render_custom_post_type_section(string $post_type, array $atts, array $exclude_ids): string {
        $post_type_obj = get_post_type_object($post_type);
        
        if (!$post_type_obj || !$post_type_obj->public) {
            return '';
        }
        
        $args = [
            'post_type'      => $post_type,
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'post__not_in'   => $exclude_ids,
        ];

        $args = array_merge($args, $this->get_post_sort_args($atts));
        
        $posts = get_posts($args);
        
        if (empty($posts)) {
            return '';
        }
        
        $output = '<div class="prorank-sitemap-section prorank-sitemap-' . esc_attr($post_type) . '">';
        $output .= '<' . esc_html($atts['heading_tag']) . ' class="prorank-sitemap-heading">';
        $output .= esc_html($post_type_obj->labels->name);
        $output .= '</' . esc_html($atts['heading_tag']) . '>';
        
        if ($atts['style'] === 'list') {
            $output .= '<ul class="prorank-sitemap-list">';
            
            foreach ($posts as $post) {
                $output .= '<li>';
                $output .= '<a href="' . esc_url(get_permalink($post)) . '">' . esc_html($post->post_title) . '</a>';
                
                if ($atts['show_date'] === 'true') {
                    $output .= ' <span class="prorank-sitemap-date">(' . get_the_date('', $post) . ')</span>';
                }

                if ($atts['show_excerpt'] === 'true' && !empty($post->post_excerpt)) {
                    $output .= '<div class="prorank-sitemap-excerpt">' . wp_trim_words($post->post_excerpt, 20) . '</div>';
                }
                
                $output .= '</li>';
            }
            
            $output .= '</ul>';
        } else {
            $output .= '<div class="prorank-sitemap-grid">';
            
            foreach ($posts as $post) {
                $output .= '<div class="prorank-sitemap-item">';
                $output .= '<a href="' . esc_url(get_permalink($post)) . '">' . esc_html($post->post_title) . '</a>';

                if ($atts['show_date'] === 'true') {
                    $output .= '<div class="prorank-sitemap-meta">' . get_the_date('', $post) . '</div>';
                }

                if ($atts['show_excerpt'] === 'true' && !empty($post->post_excerpt)) {
                    $output .= '<div class="prorank-sitemap-excerpt">' . wp_trim_words($post->post_excerpt, 20) . '</div>';
                }
                $output .= '</div>';
            }
            
            $output .= '</div>';
        }
        
        $output .= '</div>';
        
        return $output;
    }

    /**
     * Render custom taxonomy section
     *
     * @param string $taxonomy   Taxonomy
     * @param array  $atts       Shortcode attributes
     * @param array  $exclude_ids Excluded IDs
     * @return string
     */
    private function render_custom_taxonomy_section(string $taxonomy, array $atts, array $exclude_ids): string {
        $taxonomy_obj = get_taxonomy($taxonomy);
        
        if (!$taxonomy_obj || !$taxonomy_obj->public) {
            return '';
        }
        
        $sort_args = $this->get_term_sort_args($atts);
        $show_post_count = $this->is_true($atts, 'show_post_count');
        $terms = get_terms([
            'taxonomy'   => $taxonomy,
            'hide_empty' => $this->is_true($atts, 'exclude_empty'),
            'orderby'    => $sort_args['orderby'],
            'order'      => $sort_args['order'],
        ]);
        
        if (empty($terms) || is_wp_error($terms)) {
            return '';
        }
        
        $output = '<div class="prorank-sitemap-section prorank-sitemap-' . esc_attr($taxonomy) . '">';
        $output .= '<' . esc_html($atts['heading_tag']) . ' class="prorank-sitemap-heading">';
        $output .= esc_html($taxonomy_obj->labels->name);
        $output .= '</' . esc_html($atts['heading_tag']) . '>';
        
        if ($atts['style'] === 'list') {
            $output .= '<ul class="prorank-sitemap-list">';
            
            foreach ($terms as $term) {
                $output .= '<li>';
                $output .= '<a href="' . esc_url(get_term_link($term)) . '">' . esc_html($term->name) . '</a>';
                if ($show_post_count) {
                    $output .= ' <span class="prorank-sitemap-count">(' . $term->count . ')</span>';
                }
                $output .= '</li>';
            }
            
            $output .= '</ul>';
        } else {
            $output .= '<div class="prorank-sitemap-grid">';
            
            foreach ($terms as $term) {
                $output .= '<div class="prorank-sitemap-item">';
                $output .= '<a href="' . esc_url(get_term_link($term)) . '">' . esc_html($term->name) . '</a>';
                $output .= '</div>';
            }
            
            $output .= '</div>';
        }
        
        $output .= '</div>';
        
        return $output;
    }

    /**
     * Convert list HTML to grid
     *
     * @param string $html List HTML
     * @return string
     */
    private function convert_list_to_grid(string $html): string {
        // Convert <li> tags to divs
        $html = str_replace('<li>', '<div class="prorank-sitemap-item">', $html);
        $html = str_replace('</li>', '</div>', $html);
        
        // Remove <ul> tags
        $html = preg_replace('/<\/?ul[^>]*>/', '', $html);
        
        return $html;
    }

    /**
     * Register sitemap block
     *
     * @return void
     */
    public function register_sitemap_block(): void {
        if (!function_exists('register_block_type')) {
            return;
        }
        
        register_block_type('prorank-seo/html-sitemap', [
            'api_version' => 3,
            'render_callback' => [$this, 'render_sitemap_block'],
            'attributes' => [
                'include' => [
                    'type' => 'string',
                    'default' => 'pages,posts,categories,tags',
                ],
                'exclude' => [
                    'type' => 'string',
                    'default' => '',
                ],
                'excludeIds' => [
                    'type' => 'string',
                    'default' => '',
                ],
                'depth' => [
                    'type' => 'number',
                    'default' => 0,
                ],
                'showDate' => [
                    'type' => 'boolean',
                    'default' => false,
                ],
                'showExcerpt' => [
                    'type' => 'boolean',
                    'default' => false,
                ],
                'columns' => [
                    'type' => 'number',
                    'default' => 1,
                ],
                'style' => [
                    'type' => 'string',
                    'default' => 'list',
                ],
                'headingTag' => [
                    'type' => 'string',
                    'default' => 'h3',
                ],
            ],
        ]);
    }

    /**
     * Render sitemap block
     *
     * @param array $attributes Block attributes
     * @return string
     */
    public function render_sitemap_block($attributes): string {
        $atts = [
            'include' => $attributes['include'] ?? 'pages,posts,categories,tags',
            'exclude' => $attributes['exclude'] ?? '',
            'exclude_ids' => $attributes['excludeIds'] ?? '',
            'depth' => $attributes['depth'] ?? 0,
            'show_date' => $attributes['showDate'] ? 'true' : 'false',
            'show_excerpt' => $attributes['showExcerpt'] ? 'true' : 'false',
            'columns' => $attributes['columns'] ?? 1,
            'style' => $attributes['style'] ?? 'list',
            'heading_tag' => $attributes['headingTag'] ?? 'h3',
        ];
        
        return $this->render_sitemap_shortcode($atts);
    }

    /**
     * Register sitemap widget
     *
     * @return void
     */
    public function register_sitemap_widget(): void {
        if (!$this->is_html_sitemap_enabled() || !$this->is_widget_enabled()) {
            return;
        }

        register_widget('\ProRank\SEO\Widgets\HtmlSitemapWidget');
    }

    /**
     * Enqueue frontend styles
     *
     * @return void
     */
    public function enqueue_frontend_styles(): void {
        if (!$this->is_html_sitemap_enabled()) {
            return;
        }

        $content = '';
        if (get_the_ID()) {
            $content = (string) get_post_field('post_content', get_the_ID());
        }

        if (!is_admin() && (
            $this->is_virtual_sitemap_request() ||
            has_shortcode($content, 'prorank_sitemap') ||
            has_shortcode($content, 'prorank_html_sitemap')
        )) {
            
            wp_enqueue_style(
                'prorank-html-sitemap',
                PRORANK_SEO_URL . 'assets/css/html-sitemap.css',
                [],
                PRORANK_SEO_VERSION
            );

            $custom_css = (string) Settings::get('modules.html_sitemap.custom_css', '');
            if ($custom_css !== '') {
                wp_add_inline_style(
                    'prorank-html-sitemap',
                    function_exists('prorank_sanitize_inline_css')
                        ? prorank_sanitize_inline_css($custom_css)
                        : trim(wp_strip_all_tags($custom_css))
                );
            }
        }
    }

    /**
     * Render a virtual /html-sitemap/ page when no dedicated page exists.
     *
     * @return void
     */
    public function handle_virtual_sitemap_request(): void {
        if (
            !$this->is_html_sitemap_enabled() ||
            !$this->is_virtual_sitemap_request() ||
            is_admin() ||
            wp_doing_ajax() ||
            is_feed()
        ) {
            return;
        }

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

        // Let a real WordPress object win if this path is already mapped intentionally.
        if (!is_404() && get_queried_object_id()) {
            return;
        }

        global $wp_query;
        if ($wp_query instanceof \WP_Query) {
            $wp_query->is_404 = false;
        }

        status_header(200);
        nocache_headers();

        $title = __('HTML Sitemap', 'prorank-seo');
        $description = __('Browse the main sections of this site in one place.', 'prorank-seo');
        $content = $this->render_sitemap_shortcode($this->build_virtual_sitemap_atts($this->get_html_sitemap_settings()));

        if (function_exists('get_header') && function_exists('get_footer')) {
            get_header();
            echo '<main id="primary" class="site-main prorank-html-sitemap-page">';
            echo '<article class="prorank-html-sitemap-page__article">';
            echo '<header class="prorank-html-sitemap-page__header">';
            echo '<h1>' . esc_html($title) . '</h1>';
            echo '<p>' . esc_html($description) . '</p>';
            echo '</header>';
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Shortcode output is already escaped internally.
            echo $content;
            echo '</article>';
            echo '</main>';
            get_footer();
            exit;
        }

        echo '<!doctype html><html><head><meta charset="utf-8"><title>' . esc_html($title) . '</title>';
        wp_head();
        echo '</head><body class="prorank-html-sitemap-page">';
        echo '<main class="prorank-html-sitemap-page__article"><h1>' . esc_html($title) . '</h1><p>' . esc_html($description) . '</p>';
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Shortcode output is already escaped internally.
        echo $content;
        echo '</main>';
        wp_footer();
        echo '</body></html>';
        exit;
    }

    /**
     * Determine whether the current request targets the virtual sitemap route.
     *
     * @return bool
     */
    private function is_virtual_sitemap_request(): bool {
        $request_uri = function_exists('prorank_get_server_var')
            ? (string) prorank_get_server_var('REQUEST_URI')
            : (string) ($_SERVER['REQUEST_URI'] ?? '');
        $request_path = untrailingslashit((string) wp_parse_url($request_uri, PHP_URL_PATH));

        return $request_path === '/html-sitemap';
    }

    /**
     * Read the HTML sitemap settings used by the admin UI.
     *
     * @return array
     */
    private function get_html_sitemap_settings(): array {
        $defaults = [
            'enable_html_sitemap' => true,
            'include_sections' => ['pages', 'posts', 'categories', 'tags'],
            'exclude_empty' => true,
            'show_post_count' => true,
            'show_date' => false,
            'show_excerpt' => false,
            'columns' => 1,
            'style' => 'list',
            'heading_tag' => 'h3',
            'sort_order' => 'title',
            'exclude_ids' => '',
            'custom_css' => '',
            'widget_enabled' => true,
        ];

        $settings = [];
        foreach ($defaults as $key => $default) {
            $settings[$key] = Settings::get('modules.html_sitemap.' . $key, $default);
        }

        return $settings;
    }

    /**
     * Build shortcode attributes from saved module settings.
     *
     * @param array $settings HTML sitemap settings.
     * @return array
     */
    private function build_virtual_sitemap_atts(array $settings): array {
        return [
            'include' => implode(',', (array) ($settings['include_sections'] ?? ['pages', 'posts', 'categories', 'tags'])),
            'exclude_ids' => (string) ($settings['exclude_ids'] ?? ''),
            'exclude_empty' => !empty($settings['exclude_empty']) ? 'true' : 'false',
            'show_post_count' => !empty($settings['show_post_count']) ? 'true' : 'false',
            'show_date' => !empty($settings['show_date']) ? 'true' : 'false',
            'show_excerpt' => !empty($settings['show_excerpt']) ? 'true' : 'false',
            'columns' => max(1, (int) ($settings['columns'] ?? 1)),
            'style' => (string) ($settings['style'] ?? 'list'),
            'heading_tag' => (string) ($settings['heading_tag'] ?? 'h3'),
            'sort_order' => (string) ($settings['sort_order'] ?? 'title'),
        ];
    }

    /**
     * Check whether HTML sitemap output is enabled in module settings.
     *
     * @return bool
     */
    private function is_html_sitemap_enabled(): bool {
        return (bool) Settings::get('modules.html_sitemap.enable_html_sitemap', true);
    }

    /**
     * Check whether the widget should be registered.
     *
     * @return bool
     */
    private function is_widget_enabled(): bool {
        return (bool) Settings::get('modules.html_sitemap.widget_enabled', true);
    }
}
