<?php

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

/**
 * Extract serialized TOC blocks and shortcodes so they can be re-appended when replacing content.
 *
 * @param string $content Original post content prior to replacement.
 * @return string Serialized markup for manual TOC placements.
 */
function anchorkit_extract_manual_toc_placeholders($content)
{
    if (!is_string($content) || $content === '') {
        return '';
    }

    $segments = [];

    $add_segment = function ($markup, $offset) use (&$segments) {
        foreach ($segments as $segment) {
            if ($segment['offset'] === $offset && $segment['markup'] === $markup) {
                return;
            }
        }
        $segments[] = [
            'offset' => $offset,
            'markup' => $markup,
        ];
    };

    // Capture Gutenberg TOC blocks (both paired and self-closing)
    $block_patterns = [
        '/<!--\s+wp:anchorkit\/table-of-contents\b[^>]*-->.*?<!--\s+\/wp:anchorkit\/table-of-contents\s+-->/is',
        '/<!--\s+wp:anchorkit\/table-of-contents\b[^>]*\/-->/i',
    ];

    foreach ($block_patterns as $pattern) {
        if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
            foreach ($matches[0] as $match) {
                $add_segment($match[0], (int) $match[1]);
            }
        }
    }

    // Capture shortcode placements in their original order
    $shortcode_tags = [];
    if (shortcode_exists('anchorkit_toc')) {
        $shortcode_tags[] = 'anchorkit_toc';
    }

    if (!empty($shortcode_tags)) {
        $regex = get_shortcode_regex($shortcode_tags);
        if ($regex && preg_match_all('/' . $regex . '/s', $content, $matches, PREG_OFFSET_CAPTURE)) {
            foreach ($matches[0] as $match) {
                $add_segment($match[0], (int) $match[1]);
            }
        }
    }

    if (empty($segments)) {
        return '';
    }

    usort($segments, function ($a, $b) {
        if ($a['offset'] === $b['offset']) {
            return 0;
        }
        return ($a['offset'] < $b['offset']) ? -1 : 1;
    });

    $combined = '';
    foreach ($segments as $segment) {
        $combined .= trim($segment['markup']) . "\n\n";
    }

    return trim($combined);
}

function anchorkit_toc_auto_insert($content)
{
    /**
     * PERFORMANCE OPTIMIZATION OPPORTUNITY - TOC Caching
     * ===================================================
     * 
     * Currently, TOC is generated on every page load by parsing content and
     * extracting headings. For high-traffic sites, this can be optimized.
     * 
     * SUGGESTED IMPLEMENTATION:
     *   $cache_key = 'anchorkit_toc_' . $post_id . '_' . md5($content);
     *   $cached_toc = get_transient($cache_key);
     *   if ($cached_toc !== false) {
     *       return $content; // TOC already inserted
     *   }
     *   // ... generate TOC ...
     *   set_transient($cache_key, $modified_content, HOUR_IN_SECONDS);
     * 
     * CACHE INVALIDATION:
     *   - Clear on post update (save_post hook)
     *   - Clear on settings change (update_option hook)
     *   - Consider clearing on theme switch
     * 
     * CONSIDERATIONS:
     *   - Adds complexity for cache invalidation
     *   - Must handle dynamic content (shortcodes, ACF fields)
     *   - Not recommended for v1.0 - add in v1.1 if performance issues arise
     * 
     * CURRENT STATUS: Not implemented - premature optimization
     */

    // Add error handling to prevent fatal errors
    try {
        // Safety checks - don't run if we're not in a safe context
        if (!is_string($content) || empty($content))
            return $content;
        if (is_admin() || wp_doing_ajax() || wp_doing_cron())
            return $content;


        if (!anchorkit_get_option('anchorkit_toc_enabled', false))
            return $content;
        if (!anchorkit_get_option('anchorkit_toc_automatic_insertion', true))
            return $content;
        if (!is_singular())
            return $content;

        /**
         * Filter the allowed post types for TOC display.
         *
         * @since 1.0.0
         * @param array $post_types Array of post type slugs where TOC is enabled.
         */
        $post_types = apply_filters('anchorkit_post_types', anchorkit_get_option('anchorkit_toc_post_types', ['post', 'page']));

        if (!in_array(get_post_type(), $post_types, true))
            return $content;

        /**
         * Filter to exclude specific posts from displaying the TOC.
         *
         * @since 1.0.0
         * @param bool $exclude Whether to exclude this post. Default false.
         * @param int  $post_id The current post ID.
         */
        global $post;
        if (apply_filters('anchorkit_exclude_post', false, $post ? $post->ID : 0)) {
            return $content;
        }

        global $anchorkit_acf_merge_state;

        $acf_content_already_present = false;
        if ($post && isset($anchorkit_acf_merge_state['did_merge'], $anchorkit_acf_merge_state['post_id']) && $anchorkit_acf_merge_state['did_merge'] && (int) $anchorkit_acf_merge_state['post_id'] === (int) $post->ID) {
            $acf_content_already_present = true;
        } elseif (strpos($content, 'data-anchorkit-acf-content') !== false) {
            $acf_content_already_present = true;
        }

        if (anchorkit_post_contains_manual_toc($post)) {
            return $content;
        }

        // PRO: ACF Integration - free version defaults
        // This block is stripped by Freemius in the free version
        $acf_enabled = false;
        $acf_merge_mode = 'after';
        $acf_content = '';

        // Premium feature overrides - this entire block is stripped from free version
        if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
            if (anchorkit_fs()->can_use_premium_code()) {
                $acf_enabled = (bool) anchorkit_get_option('anchorkit_toc_acf_enabled', false);
                $acf_merge_mode = anchorkit_get_option('anchorkit_toc_acf_merge_mode', 'after');
            }
        }

        // Extract ACF content for heading parsing
        // Even if content is already merged, we need ACF content separately for "replace" mode
        if ($acf_enabled && $post && $post->ID) {
            // If content already merged but mode is "replace", we still need to extract ACF content
            // to use it as the heading source (not the merged content)
            if (!$acf_content_already_present || $acf_merge_mode === 'replace') {
                $acf_content = anchorkit_extract_acf_content($post->ID);
            }
        }

        // Merge ACF content with post content based on merge mode for heading extraction
        $heading_source = $content;
        if ($acf_enabled && !empty($acf_content)) {
            if ($acf_content_already_present && $acf_merge_mode === 'replace') {
                // Content already merged for display, but for TOC headings use ONLY ACF content
                $heading_source = $acf_content;
            } elseif (!$acf_content_already_present) {
                // Content not yet merged, merge it for heading extraction based on mode
                if ($acf_merge_mode === 'before') {
                    $heading_source = $acf_content . "\n\n" . $content;
                } elseif ($acf_merge_mode === 'after') {
                    $heading_source = $content . "\n\n" . $acf_content;
                } elseif ($acf_merge_mode === 'replace') {
                    $heading_source = $acf_content;
                }
            }
        }

        // Parse headings from the heading source (ACF + content merge)
        // But we'll inject IDs into the ORIGINAL content, not the merged source
        $include_tags = anchorkit_get_global_heading_levels();
        $exclude_sel = anchorkit_get_option('anchorkit_toc_exclude_selectors', '');

        /**
         * Filter the content before headings are parsed.
         *
         * @since 1.0.0
         * @param string $heading_source The content to parse for headings.
         * @param int    $post_id        The current post ID.
         */
        $heading_source = apply_filters('anchorkit_content_to_parse', $heading_source, $post ? $post->ID : 0);

        // Extract headings from merged source
        $heading_source_copy = $heading_source;
        $parsed_headings = anchorkit_toc_parse_headings($heading_source_copy, $include_tags, $exclude_sel);
        $headings = $parsed_headings['headings'];

        // Now inject IDs into the ORIGINAL content (not the merged source)
        // This ensures the actual page content isn't replaced
        $content_with_ids = $content;
        $parsed = anchorkit_toc_parse_headings($content_with_ids, $include_tags, $exclude_sel);
        $content_with_injected_ids = $parsed['content'];

        /**
         * Filter the minimum number of headings required to display the TOC.
         *
         * @since 1.0.0
         * @param int $min_headings The minimum number of headings required.
         * @param int $post_id      The current post ID.
         */
        $min_headings = apply_filters('anchorkit_min_headings', (int) anchorkit_get_option('anchorkit_toc_min_headings', 2), $post ? $post->ID : 0);

        if (count($headings) < $min_headings)
            return $content;

        /**
         * Master filter to control whether the TOC should be displayed.
         * This filter runs after all other checks have passed.
         *
         * @since 1.0.0
         * @param bool  $should_display Whether to display the TOC. Default true.
         * @param int   $post_id        The current post ID.
         * @param array $headings       The parsed headings array.
         */
        if (!apply_filters('anchorkit_should_display_toc', true, $post ? $post->ID : 0, $headings)) {
            return $content;
        }

        // Prepare headings with guaranteed IDs for TOC generation
        $headings_with_ids = anchorkit_prepare_amp_headings($headings);

        $position = anchorkit_get_option('anchorkit_toc_position', 'before_first_heading');
        $toc_context = [
            'source' => 'auto_insertion',
            'post_id' => ($post && isset($post->ID)) ? (int) $post->ID : 0,
            'data' => [
                'position' => $position,
                'automatic_insertion' => true,
                'acf_merge_mode' => $acf_merge_mode,
            ],
        ];

        // Generate TOC using headings from merged source, but with original content for reference
        $toc_html = anchorkit_toc_generate_html($headings_with_ids, $heading_source_copy, $toc_context);

        if ($toc_html === '')
            return $content;

        // TOC Positioning Logic
        // Several positioning options are available:
        // - before_first_heading: Place TOC before the first heading tag (default)
        // - after_first_paragraph: Place TOC after the first paragraph (supports both classic and block editor)
        // - top_of_content: Place TOC at the very top of the content
        // - bottom_of_content: Place TOC at the very bottom of the content

        switch ($position) {
            case 'before_first_heading':
                if (preg_match('/<(' . implode('|', $include_tags) . ')[^>]*>/i', $content_with_injected_ids, $m, PREG_OFFSET_CAPTURE)) {
                    return substr_replace($content_with_injected_ids, $toc_html, $m[0][1], 0);
                }
                return $toc_html . $content_with_injected_ids;
            case 'after_first_paragraph':
                // Log original content for debugging

                // Enhanced detection strategy to find real content paragraphs

                // Try multiple strategies in order of specificity to reliability

                // 1. Look for a specific text pattern from the screenshots (highest priority)
                if (preg_match('/<p[^>]*>I am a new paragraph.*?<\/p>/is', $content_with_injected_ids, $matches, PREG_OFFSET_CAPTURE)) {
                    $offset = $matches[0][1] + strlen($matches[0][0]);
                    return substr_replace($content_with_injected_ids, $toc_html, $offset, 0);
                }

                // 2. Find any paragraph with reasonable content length (excluding known metadata)
                $sizable_content = '/<p[^>]*>(?!.*?Reading Time:)(?!.*?Posted by)(?:.{15,}?)<\/p>/is';
                if (preg_match($sizable_content, $content_with_injected_ids, $matches, PREG_OFFSET_CAPTURE)) {
                    $offset = $matches[0][1] + strlen($matches[0][0]);
                    return substr_replace($content_with_injected_ids, $toc_html, $offset, 0);
                }

                // 3. Check for Gutenberg block paragraphs
                if (strpos($content_with_injected_ids, 'wp-block-paragraph') !== false) {
                    $gb_pattern = '/<div[^>]*class="[^\"]*wp-block-paragraph[^\"]*"[^>]*>.*?<\/div>/is';
                    if (preg_match($gb_pattern, $content_with_injected_ids, $matches, PREG_OFFSET_CAPTURE)) {
                        $offset = $matches[0][1] + strlen($matches[0][0]);
                        return substr_replace($content_with_injected_ids, $toc_html, $offset, 0);
                    }
                }

                // 4. Try standard paragraph pattern but exclude first paragraph if it contains "Reading Time"
                $parts = explode('</p>', $content_with_injected_ids, 3); // Get up to 2 paragraphs
                if (count($parts) >= 2) {
                    if (stripos($parts[0], 'Reading Time') !== false && count($parts) >= 3) {
                        // Skip the reading time paragraph and use the next one
                        return $parts[0] . '</p>' . $parts[1] . '</p>' . $toc_html . $parts[2];
                    } else {
                        // Use the first paragraph
                        return $parts[0] . '</p>' . $toc_html . $parts[1];
                    }
                }

                // 5. Last resort: DOM-based approach
                if (class_exists('DOMDocument')) {
                    $dom = new DOMDocument();
                    @$dom->loadHTML(mb_convert_encoding($content_with_injected_ids, 'HTML-ENTITIES', 'UTF-8'));
                    $paragraphs = $dom->getElementsByTagName('p');

                    // Skip the first paragraph if it contains "Reading Time"
                    $startIdx = 0;
                    if ($paragraphs->length > 1) {
                        $firstP = $paragraphs->item(0);
                        if ($firstP && stripos($firstP->textContent, 'Reading Time') !== false) {
                            $startIdx = 1;
                        }
                    }

                    if ($paragraphs->length > $startIdx) {
                        $targetP = $paragraphs->item($startIdx);
                        $innerHTML = '';
                        foreach ($targetP->childNodes as $child) {
                            $innerHTML .= $dom->saveHTML($child);
                        }

                        $pContent = '<p>' . $innerHTML . '</p>';
                        $pos = strpos($content_with_injected_ids, $pContent);

                        if ($pos !== false) {
                            $offset = $pos + strlen($pContent);
                            return substr_replace($content_with_injected_ids, $toc_html, $offset, 0);
                        }
                    }
                }

                // If all else fails, insert at the top
                return $toc_html . $content_with_injected_ids;
            case 'top_of_content':
                return $toc_html . $content_with_injected_ids;
            case 'bottom_of_content':
            default:
                return $content_with_injected_ids . $toc_html;
        }
    } catch (Exception $e) {
        // If there's any error, just return the original content
        // This prevents fatal errors from breaking the site
        return $content;
    } catch (Error $e) {
        // Handle PHP 7+ Error objects as well
        return $content;
    }
}

// Make sure the filter is hooked only once in the global scope
// Use priority 50 - after most plugins but before most theme modifications
if (!has_filter('the_content', 'anchorkit_toc_auto_insert')) {
    add_filter('the_content', 'anchorkit_toc_auto_insert', 50);
}

if (!function_exists('anchorkit_toc_auto_insert_placeholder')) {
    /**
     * This is just a placeholder to ensure backward compatibility.
     * The actual function is defined in the global scope above.
     */
    function anchorkit_toc_auto_insert_placeholder()
    {
        // This function is intentionally empty - the real function is defined above
    }
}

// Shortcode
/**
 * AnchorKit Table of Contents Shortcode
 *
 * Generates a table of contents from the current post/page content.
 *
 * Basic Usage:
 * [anchorkit_toc]
 *
 * Advanced Usage with Attributes:
 * [anchorkit_toc header_label="Contents" display_header_label="yes" toggle_view="yes" initial_view="expanded"]
 *
 * Display Control Attributes:
 *   header_label         - Custom TOC title (default: from settings)
 *   display_header_label - Show/hide title (yes/no, default: from settings)
 *   toggle_view          - Enable/disable collapsible toggle (yes/no, default: from settings)
 *   initial_view         - Initial state when collapsible (show/hide/expanded/collapsed, default: from settings)
 *
 * Content Filtering Attributes:
 *   heading_levels       - Heading levels to include (comma-separated: 1,2,3,4,5,6, default: from settings)
 *   exclude              - Exclude headings containing this text (default: none)
 *   post_types           - Limit to specific post types (comma-separated, default: all enabled)
 *   post_in              - Include only specific post IDs (comma-separated, default: all)
 *   post_not_in          - Exclude specific post IDs (comma-separated, default: none)
 *   min_headings         - Minimum headings required to show TOC (default: from settings)
 *
 * Appearance Attributes:
 *   display_counter      - Show/hide numbering (yes/no, default: from settings)
 *   class                - Custom CSS class for the TOC container (default: none)
 *   theme                - Theme selection (system/light/dark, default: from settings)
 *   preset               - Style preset (minimal/modern/clean, default: from settings)
 *
 * Behavior Attributes:
 *   hierarchical         - Enable hierarchical display (yes/no, default: from settings)
 *   smooth_scroll        - Enable smooth scrolling (yes/no, default: from settings)
 *   device_target        - Device-specific display (all/mobile/desktop, default: all)
 *
 * ACF Integration Attributes:
 *   acf_enabled          - Enable ACF field integration (yes/no, PRO only)
 *   acf_merge_mode       - How to merge ACF content (before/after/replace, PRO only)
 *   acf_field_names      - ACF field names to include (comma-separated, PRO only)
 *
 * PRO Features Attributes:
 *   view_more            - Limit initial headings shown (number, PRO only)
 *   sticky               - Enable sticky positioning (yes/no, PRO only)
 *   show_reading_time    - Display reading time (yes/no, PRO only)
 *   show_word_count      - Display word count (yes/no, PRO only)
 *
 * @param array $atts Shortcode attributes
 * @return string HTML output or empty string if conditions not met
 */
function anchorkit_toc_shortcode($atts = [])
{
    $atts = is_array($atts) ? $atts : [];
    $atts = shortcode_atts([
        // ACF Integration (existing)
        'acf_enabled' => null,
        'acf_merge_mode' => null,
        'acf_field_names' => null,

        // Display Control
        'header_label' => null,
        'display_header_label' => null,
        'toggle_view' => null,
        'initial_view' => null,

        // Content Filtering
        'heading_levels' => null,
        // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Shortcode attribute for heading keyword exclusion, not WP_Query
        'exclude' => null,
        'post_types' => null,
        'post_in' => null,
        'post_not_in' => null,

        // Appearance
        'display_counter' => null,
        'class' => null,
        'theme' => null,
        'preset' => null,

        // Behavior
        'hierarchical' => null,
        'smooth_scroll' => null,
        'device_target' => null,

        // Additional options
        'min_headings' => null,

        // PRO Features
        'view_more' => null,
        'sticky' => null,
        'show_reading_time' => null,
        'show_word_count' => null,
    ], $atts, 'anchorkit_toc');


    // ACF validation (existing)
    if (!is_null($atts['acf_enabled'])) {
        $atts['acf_enabled'] = filter_var($atts['acf_enabled'], FILTER_VALIDATE_BOOLEAN);
    }

    if (!is_null($atts['acf_merge_mode'])) {
        $atts['acf_merge_mode'] = sanitize_key($atts['acf_merge_mode']);
    }

    if (!is_null($atts['acf_field_names'])) {
        $atts['acf_field_names'] = sanitize_text_field($atts['acf_field_names']);
    }

    // Display Control validation
    if (!is_null($atts['header_label'])) {
        $atts['header_label'] = sanitize_text_field($atts['header_label']);
    }

    if (!is_null($atts['display_header_label'])) {
        $atts['display_header_label'] = in_array(strtolower($atts['display_header_label']), ['yes', 'no', 'true', 'false', '1', '0']) ?
            filter_var($atts['display_header_label'], FILTER_VALIDATE_BOOLEAN) : null;
    }

    if (!is_null($atts['toggle_view'])) {
        $atts['toggle_view'] = in_array(strtolower($atts['toggle_view']), ['yes', 'no', 'true', 'false', '1', '0']) ?
            filter_var($atts['toggle_view'], FILTER_VALIDATE_BOOLEAN) : null;
    }

    if (!is_null($atts['initial_view'])) {
        $atts['initial_view'] = in_array(strtolower($atts['initial_view']), ['show', 'hide', 'expanded', 'collapsed']) ?
            sanitize_key($atts['initial_view']) : null;
    }

    // Content Filtering validation
    if (!is_null($atts['heading_levels'])) {
        $levels = array_map('intval', array_filter(explode(',', $atts['heading_levels'])));
        $atts['heading_levels'] = implode(',', array_intersect($levels, [1, 2, 3, 4, 5, 6]));
    }

    if (!is_null($atts['exclude'])) {
        // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Shortcode attribute for heading keyword exclusion, not WP_Query.
        $atts['exclude'] = sanitize_text_field($atts['exclude']);
    }

    if (!is_null($atts['post_types'])) {
        $types = array_map('sanitize_key', explode(',', $atts['post_types']));
        $atts['post_types'] = implode(',', array_filter($types));
    }

    if (!is_null($atts['post_in'])) {
        $ids = array_map('intval', array_filter(explode(',', $atts['post_in'])));
        $atts['post_in'] = implode(',', $ids);
    }

    if (!is_null($atts['post_not_in'])) {
        $ids = array_map('intval', array_filter(explode(',', $atts['post_not_in'])));
        $atts['post_not_in'] = implode(',', $ids);
    }

    // Appearance validation
    if (!is_null($atts['display_counter'])) {
        $atts['display_counter'] = in_array(strtolower($atts['display_counter']), ['yes', 'no', 'true', 'false', '1', '0']) ?
            filter_var($atts['display_counter'], FILTER_VALIDATE_BOOLEAN) : null;
    }

    if (!is_null($atts['class'])) {
        $atts['class'] = sanitize_html_class($atts['class']);
    }

    if (!is_null($atts['theme'])) {
        $atts['theme'] = in_array($atts['theme'], ['system', 'light', 'dark']) ? $atts['theme'] : null;
    }

    if (!is_null($atts['preset'])) {
        $atts['preset'] = in_array($atts['preset'], ['minimal', 'modern', 'clean']) ? $atts['preset'] : null;
    }

    // Behavior validation
    if (!is_null($atts['hierarchical'])) {
        $atts['hierarchical'] = in_array(strtolower($atts['hierarchical']), ['yes', 'no', 'true', 'false', '1', '0']) ?
            filter_var($atts['hierarchical'], FILTER_VALIDATE_BOOLEAN) : null;
    }

    if (!is_null($atts['smooth_scroll'])) {
        $atts['smooth_scroll'] = in_array(strtolower($atts['smooth_scroll']), ['yes', 'no', 'true', 'false', '1', '0']) ?
            filter_var($atts['smooth_scroll'], FILTER_VALIDATE_BOOLEAN) : null;
    }

    if (!is_null($atts['device_target'])) {
        $atts['device_target'] = in_array(strtolower($atts['device_target']), ['all', 'mobile', 'desktop']) ?
            strtolower($atts['device_target']) : null;
    }

    if (!is_null($atts['min_headings'])) {
        $atts['min_headings'] = max(1, intval($atts['min_headings']));
    }

    // PRO Features validation - wrap each in is__premium_only() for stripping
    if (!is_null($atts['view_more'])) {
        if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
            if (anchorkit_fs()->can_use_premium_code()) {
                $atts['view_more'] = max(1, intval($atts['view_more']));
            } else {
                $atts['view_more'] = null;
            }
        } else {
            $atts['view_more'] = null; // Disable PRO feature for free users
        }
    }

    if (!is_null($atts['sticky'])) {
        if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
            if (anchorkit_fs()->can_use_premium_code()) {
                $atts['sticky'] = in_array(strtolower($atts['sticky']), ['yes', 'no', 'true', 'false', '1', '0']) ?
                    filter_var($atts['sticky'], FILTER_VALIDATE_BOOLEAN) : null;
            } else {
                $atts['sticky'] = null;
            }
        } else {
            $atts['sticky'] = null; // Disable PRO feature for free users
        }
    }

    if (!is_null($atts['show_reading_time'])) {
        if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
            if (anchorkit_fs()->can_use_premium_code()) {
                $atts['show_reading_time'] = in_array(strtolower($atts['show_reading_time']), ['yes', 'no', 'true', 'false', '1', '0']) ?
                    filter_var($atts['show_reading_time'], FILTER_VALIDATE_BOOLEAN) : null;
            } else {
                $atts['show_reading_time'] = null;
            }
        } else {
            $atts['show_reading_time'] = null; // Disable PRO feature for free users
        }
    }

    if (!is_null($atts['show_word_count'])) {
        if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
            if (anchorkit_fs()->can_use_premium_code()) {
                $atts['show_word_count'] = in_array(strtolower($atts['show_word_count']), ['yes', 'no', 'true', 'false', '1', '0']) ?
                    filter_var($atts['show_word_count'], FILTER_VALIDATE_BOOLEAN) : null;
            } else {
                $atts['show_word_count'] = null;
            }
        } else {
            $atts['show_word_count'] = null; // Disable PRO feature for free users
        }
    }

    if (!anchorkit_get_option('anchorkit_toc_enabled', false))
        return '';

    global $post;
    if (!$post)
        return '';

    $content_copy = $post->post_content;

    // PRO: ACF Integration - free version defaults
    // This block is stripped by Freemius in the free version
    $acf_enabled = false;
    // Premium feature overrides - this block is stripped from free version
    if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
        if (anchorkit_fs()->can_use_premium_code()) {
            $acf_enabled = (bool) anchorkit_get_option('anchorkit_toc_acf_enabled', false);
        }
    }
    if ($atts['acf_enabled'] !== null) {
        $acf_enabled = (bool) $atts['acf_enabled'];
    }

    $acf_merge_mode = 'after';
    // Premium feature overrides for merge mode
    if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
        if (anchorkit_fs()->can_use_premium_code()) {
            $acf_merge_mode = anchorkit_get_option('anchorkit_toc_acf_merge_mode', 'after');
        }
    }
    if (!empty($atts['acf_merge_mode'])) {
        $acf_merge_mode = sanitize_key($atts['acf_merge_mode']);
    }
    if (!in_array($acf_merge_mode, ['before', 'after', 'replace'], true)) {
        $acf_merge_mode = 'after';
    }

    $acf_field_names_override = $atts['acf_field_names'] !== null ? $atts['acf_field_names'] : null;
    $acf_content = '';

    if ($acf_enabled && $post->ID) {
        $acf_content = anchorkit_extract_acf_content($post->ID, $acf_field_names_override);
    }

    // Merge ACF content with post content based on merge mode
    // This is ONLY for extracting headings, not for TOC display
    $heading_source = $content_copy;
    if (!empty($acf_content)) {
        if ($acf_merge_mode === 'before') {
            $heading_source = $acf_content . "\n\n" . $content_copy;
        } elseif ($acf_merge_mode === 'after') {
            $heading_source = $content_copy . "\n\n" . $acf_content;
        } elseif ($acf_merge_mode === 'replace') {
            $heading_source = $acf_content;
        }
    }

    // Determine heading levels to include (shortcode override or global settings)
    $include_headings = anchorkit_get_global_heading_levels();
    if (!empty($atts['heading_levels'])) {
        $custom_levels = array_map('intval', explode(',', $atts['heading_levels']));
        $include_headings = array_intersect($custom_levels, [1, 2, 3, 4, 5, 6]);
        if (empty($include_headings)) {
            $include_headings = anchorkit_get_global_heading_levels(); // Fallback
        }
    }

    // Build exclude selectors from shortcode attributes
    $exclude_selectors = anchorkit_get_option('anchorkit_toc_exclude_selectors', '');

    // Extract headings from merged source
    $parsed = anchorkit_toc_parse_headings($heading_source, $include_headings, $exclude_selectors);

    // Apply shortcode-specific keyword exclusions
    if (!empty($atts['exclude']) && !empty($parsed['headings'])) {
        $exclude_keywords = array_map('trim', explode(',', $atts['exclude']));
        $filtered_headings = [];

        foreach ($parsed['headings'] as $heading) {
            $should_exclude = false;
            foreach ($exclude_keywords as $keyword) {
                if (!empty($keyword) && stripos($heading['text'], $keyword) !== false) {
                    $should_exclude = true;
                    break;
                }
            }
            if (!$should_exclude) {
                $filtered_headings[] = $heading;
            }
        }
        $parsed['headings'] = $filtered_headings;
    }
    $prepared_headings = anchorkit_prepare_amp_headings($parsed['headings']);

    // Check minimum headings (shortcode can override global minimum)
    $min_headings = (int) anchorkit_get_option('anchorkit_toc_min_headings', 2);
    if (isset($atts['min_headings']) && is_numeric($atts['min_headings'])) {
        $min_headings = max(1, (int) $atts['min_headings']);
    }

    if (count($prepared_headings) < $min_headings)
        return '';

    // Post type filtering
    if (!empty($atts['post_types'])) {
        $allowed_types = array_map('trim', explode(',', $atts['post_types']));
        if (!in_array($post->post_type, $allowed_types)) {
            return ''; // Post type not allowed
        }
    }

    // Post ID inclusion/exclusion
    if (!empty($atts['post_in'])) {
        $allowed_ids = array_map('intval', explode(',', $atts['post_in']));
        if (!in_array($post->ID, $allowed_ids)) {
            return ''; // Post ID not in include list
        }
    }

    if (!empty($atts['post_not_in'])) {
        $excluded_ids = array_map('intval', explode(',', $atts['post_not_in']));
        if (in_array($post->ID, $excluded_ids)) {
            return ''; // Post ID in exclude list
        }
    }

    // Device targeting
    if (!empty($atts['device_target']) && $atts['device_target'] !== 'all') {
        $is_mobile = wp_is_mobile();
        $target = $atts['device_target'];

        if ($target === 'mobile' && !$is_mobile) {
            return ''; // Desktop device, but mobile-only
        } elseif ($target === 'desktop' && $is_mobile) {
            return ''; // Mobile device, but desktop-only
        }
        // 'all' target always shows
    }

    $context = [
        'source' => 'shortcode',
        'post_id' => ($post && isset($post->ID)) ? (int) $post->ID : 0,
        'data' => [
            'shortcode_atts' => $atts,
            'acf_merge_mode' => $acf_merge_mode,
        ],
    ];

    // Generate TOC with headings from merged source
    return anchorkit_toc_generate_html($prepared_headings, $heading_source, $context);
}

/**
 * Inject heading IDs into content (used when shortcode or block is present)
 * This runs at priority 5, before the auto-insert at priority 50
 */
if (!function_exists('anchorkit_inject_heading_ids')) {
    function anchorkit_inject_heading_ids($content)
    {
        if (!function_exists('anchorkit_toc_parse_headings')) {
            return $content;
        }

        // Get heading levels - use block-specific levels if available (via filter), otherwise global settings
        $heading_levels = apply_filters('anchorkit_inject_heading_levels', null);
        if (empty($heading_levels)) {
            $heading_levels = anchorkit_get_global_heading_levels();
        }

        // Parse headings and inject IDs (modifies $content by reference)
        $parsed = anchorkit_toc_parse_headings(
            $content,
            $heading_levels,
            anchorkit_get_option('anchorkit_toc_exclude_selectors', '')
        );

        // Return the modified content with IDs
        return $parsed['content'];
    }
}

// Register both new and old shortcodes for backward compatibility
if (!shortcode_exists('anchorkit_toc')) {
    add_shortcode('anchorkit_toc', 'anchorkit_toc_shortcode');
}

// Styles/tokens moved to includes/features/toc/styles.php

// Elementor preview extraction moved to includes/features/toc/integrations/elementor.php

// Elementor widget TOC generator moved to includes/features/toc/integrations/elementor.php

// TOC numbering helpers moved to includes/features/toc/render.php

// Elementor/Gutenberg heading ID injection moved to includes/features/toc/integrations/elementor.php

// Elementor schema + content-ID helpers moved to includes/features/toc/integrations/elementor.php

/**
 * Normalize heading data for AMP rendering
 * Ensures every heading has a valid ID and consistent level string
 *
 * @param array $headings
 * @return array
 */
function anchorkit_prepare_amp_headings($headings)
{
    if (empty($headings) || !is_array($headings)) {
        return [];
    }

    $prepared = [];
    $used_ids = [];

    foreach ($headings as $heading) {
        $text = isset($heading['text']) ? trim(wp_strip_all_tags($heading['text'])) : '';
        if ($text === '') {
            continue;
        }

        // Normalize heading level (accepts 'h2', 2, etc.) - keep as integer for consistency
        $level = 2;
        if (isset($heading['level'])) {
            if (is_numeric($heading['level'])) {
                $level = max(1, min(6, (int) $heading['level']));
            } else {
                $levelCandidate = strtolower($heading['level']);
                if (preg_match('/^h[1-6]$/', $levelCandidate)) {
                    $level = (int) str_replace('h', '', $levelCandidate);
                }
            }
        }

        // Ensure heading has an ID
        $id = $heading['id'] ?? ($heading['anchor'] ?? '');
        if ($id === '') {
            $id = sanitize_title($text);
        }
        if ($id === '') {
            $id = 'heading-' . uniqid();
        }

        // Guarantee uniqueness
        $base_id = $id;
        $counter = 1;
        while (isset($used_ids[$id])) {
            $id = $base_id . '-' . $counter;
            $counter++;
        }
        $used_ids[$id] = true;

        $prepared[] = [
            'level' => $level,
            'text' => $text,
            'id' => $id,
            'anchor' => $id,
        ];
    }

    return $prepared;
}

// Elementor content filter registration moved to includes/features/toc/integrations/elementor.php
