<?php
if (!defined('ABSPATH')) exit;

class SS_AutoLinker {

    public function __construct() {
        add_filter('the_content', [$this, 'auto_link_terms'], 20);
    }

public function auto_link_terms($content) {

    if (is_admin()) {
        return $content;
    }

    global $post;

    if (!$post || empty($post->ID)) {
        return $content;
    }

    $post_id = (int) $post->ID;

    $terms = get_posts([
        'post_type'      => 'ss_glossary',
        'posts_per_page' => -1,
        'post_status'    => 'publish'
    ]);

    if (!$terms) {
        return $content;
    }

    libxml_use_internal_errors(true);

    $dom = new DOMDocument('1.0', 'UTF-8');

    $wrapped = '<div id="ss-glossary-wrapper">' .
        mb_convert_encoding($content, 'HTML-ENTITIES', 'UTF-8') .
        '</div>';

    if (!$dom->loadHTML($wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD)) {
        return $content;
    }

    $xpath = new DOMXPath($dom);
    $wrapper = $xpath->query('//*[@id="ss-glossary-wrapper"]')->item(0);

    if (!$wrapper) {
        return $content;
    }

    /*
     * Rules:
     * max 3 AUTO links
     * manual links unlimited
     * existing autos persist
     */
    $page_glossary_limit = 3;

    $existing_glossary_links = $xpath->query(
        './/a[contains(@href, "/term/")]',
        $wrapper
    );

    $manual_glossary_links_snapshot = [];
    $manual_glossary_count = 0;

    foreach ($existing_glossary_links as $link) {

        $is_auto = (
            $link->hasAttribute('class') &&
            strpos($link->getAttribute('class'), 'ss-glossary-term') !== false
        );

        if (!$is_auto) {
            $manual_glossary_count++;
            $manual_glossary_links_snapshot[] = $link;
        }
    }

    /*
     * Load saved autos
     */
    $saved_auto_terms = get_post_meta($post_id, '_ss_autolinked_terms', true);

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

    /*
     * Remove stale autos no longer in content
     */
    $page_plain_text = strtolower(wp_strip_all_tags($content));
    $validated_auto_terms = [];

    foreach ($saved_auto_terms as $term) {
        if (strpos($page_plain_text, strtolower($term)) !== false) {
            $validated_auto_terms[] = strtolower($term);
        }
    }

    $saved_auto_terms = array_values(array_unique($validated_auto_terms));

/*
 * Manual links count toward density.
 * Existing auto links persist and are never removed.
 * Manual links only block NEW automatic additions.
 */
$existing_auto_count = count($saved_auto_terms);

$new_auto_capacity = max(
    0,
    $page_glossary_limit - $manual_glossary_count
);

$remaining_auto_slots = max(
    $existing_auto_count,
    $new_auto_capacity
);

/*
 * If manual density is full and no autos exist, stop.
 * Existing autos must persist.
 */
if ($existing_auto_count === 0 && $remaining_auto_slots <= 0) {
    return $content;
}

    /*
     * Start render fresh
     */
    $page_glossary_count = 0;

    $paragraph_tags = ['p', 'li', 'blockquote'];

    $linked_terms = [];
    $priority_terms = [];

    /*
     * Saved autos should be rebuilt first
     */
    foreach ($saved_auto_terms as $saved_term) {
        $priority_terms[strtolower($saved_term)] = true;
    }

    /*
     * Reserve manual links
     */
    foreach ($manual_glossary_links_snapshot as $link) {
        $linked_terms[strtolower(trim($link->textContent))] = true;
    }

    foreach ($paragraph_tags as $tag) {
        foreach (iterator_to_array($wrapper->getElementsByTagName($tag)) as $paragraph) {

            $paragraph_link_count = 0;

            if ($page_glossary_count >= $remaining_auto_slots) {
                break 2;
            }

            self::processParagraph(
                $dom,
                $paragraph,
                $terms,
                $paragraph_link_count,
                $linked_terms,
                $saved_auto_terms,
                $priority_terms,
                $page_glossary_count,
                $remaining_auto_slots
            );
        }
    }

    $output = '';

    foreach ($wrapper->childNodes as $child) {
        $output .= $dom->saveHTML($child);
    }

    libxml_clear_errors();

    update_post_meta(
        $post_id,
        '_ss_autolinked_terms',
        array_values(array_unique($saved_auto_terms))
    );

    return $output;
}
private static function is_natural_sentence($sentence, $node) {

    $sentence = trim($sentence);

    if ($sentence === '') {
        return false;
    }

    /*
     * Minimum sentence length
     */
    if (str_word_count(wp_strip_all_tags($sentence)) < 4) {
        $word_count = preg_match_all('/\p{L}+/u', wp_strip_all_tags($sentence));

        if ($word_count < 4) {
            return false;
        }
    }

    /*
     * Reject keyword stuffing
     */
    if (substr_count($sentence, ',') >= 3) {
        return false;
    }

    /*
     * Reject separator blocks
     */
    if (preg_match('/(\||\/|•|»|>>)/u', $sentence)) {
        return false;
    }

    /*
     * Reject shortcode fragments
     */
    if (preg_match('/\[[^\]]+\]/', $sentence)) {
        return false;
    }

    /*
     * Reject CTA phrases
     */
    if (self::is_cta_phrase($sentence)) {
        return false;
    }

    /*
     * Require sentence-like structure
     */
    if (!self::contains_verb($sentence)) {
        return false;
    }

    /*
     * Reject metadata contexts
     */
    if (self::is_metadata_context($node)) {
        return false;
    }

    return true;
}

private static function is_cta_phrase($sentence) {

    $cta_words = [
        'buy',
        'contact',
        'call',
        'book',
        'signup',
        'sign up',
        'subscribe',
        'download',
        'get started',
        'learn more'
    ];

    $text = strtolower($sentence);

    foreach ($cta_words as $word) {
        if (strpos($text, $word) !== false) {
            return true;
        }
    }

    return false;
}


private static function contains_verb($sentence) {

    $verbs = [
        'is','are','was','were',
        'has','have','had',
        'does','do','did',
        'can','could','will','would',
        'should','may','might',
        'helps','help',
        'improves','improve',
        'supports','support',
        'provides','provide',
        'creates','create',
        'builds','build',
        'offers','offer',
        'enables','enable',
        'delivers','deliver',
        'powers','power',
        'uses','use',
        'detects','detect',
        'boosts','boost',
        'generates','generate',
        'optimizes','optimize'
    ];

    $text = strtolower($sentence);

    foreach ($verbs as $verb) {
        if (preg_match('/\b' . preg_quote($verb, '/') . '\b/u', $text)) {
            return true;
        }
    }

    return false;
}

private static function is_metadata_context($node) {

    $current = ($node->nodeType === XML_TEXT_NODE)
        ? $node->parentNode
        : $node;

    while ($current) {

        if ($current->nodeType === XML_ELEMENT_NODE) {

            $class = strtolower($current->getAttribute('class'));
            $id    = strtolower($current->getAttribute('id'));

            $meta_keywords = [
                'meta',
                'post-meta',
                'entry-meta',
                'author',
                'byline',
                'date',
                'timestamp',
                'breadcrumb',
                'sidebar',
                'widget',
                'recent-posts'
            ];

            foreach ($meta_keywords as $keyword) {
                if (
                    strpos($class, $keyword) !== false ||
                    strpos($id, $keyword) !== false
                ) {
                    return true;
                }
            }
        }

        $current = $current->parentNode;
    }

    return false;
}

private static function is_fallback_sentence($sentence, $node) {

    $sentence = trim($sentence);

    if ($sentence === '') {
        return false;
    }

    $clean = wp_strip_all_tags($sentence);
    $word_count = preg_match_all('/\p{L}+/u', $clean);

    if ($word_count < 4) {
        return false;
    }

    if (substr_count($sentence, ',') >= 3) {
        return false;
    }

    if (preg_match('/(\||\/|•|»|>>|>|›|→)/u', $sentence)) {
        return false;
    }

    if (preg_match('/\[[^\]]+\]/', $sentence)) {
        return false;
    }

    if (self::is_cta_phrase($sentence)) {
        return false;
    }

    if (self::is_metadata_context($node)) {
        return false;
    }

    if (!preg_match('/\p{L}/u', $sentence)) {
        return false;
    }

    preg_match_all('/\p{L}+/u', strtolower($clean), $matches);
    $words = $matches[0];


    if (count(array_unique($words)) < 4) {
        return false;
    }

    if (preg_match('/\b(and|or|for|with|to|of|in|on|at)$/iu', $sentence)) {
        return false;
    }

    if (!preg_match('/[.!?]/', $sentence) && $word_count < 8) {
        return false;
    }

    return true;
}

private static function get_paragraph_link_limit($node) {

    $current = ($node->nodeType === XML_TEXT_NODE)
        ? $node->parentNode
        : $node;

    while ($current && $current->nodeType === XML_ELEMENT_NODE) {

        if (in_array(strtolower($current->nodeName), ['p', 'li', 'blockquote'], true)) {
            break;
        }

        $current = $current->parentNode;
    }

    if (!$current) {
        return 1;
    }

    $text = trim(wp_strip_all_tags($current->textContent));

    if ($text === '') {
        return 1;
    }

    preg_match_all('/\p{L}+/u', $text, $matches);
    $word_count = count($matches[0]);
    /*
     * unusually long paragraph
     */
    if ($word_count > 300) {
        return 2;
    }

    return 1;
}
private static function processParagraph(
    $dom,
    $node,
    $terms,
    &$paragraph_link_count,
    &$linked_terms,
    &$saved_auto_terms,
    &$priority_terms,
    &$page_glossary_count,
    $remaining_auto_slots
) {

    $max_links = self::get_paragraph_link_limit($node);

    if ($page_glossary_count >= $remaining_auto_slots) {
        return;
    }

    if ($paragraph_link_count >= $max_links) {
        return;
    }

    if ($node->nodeType === XML_ELEMENT_NODE) {

        $tag = strtolower($node->nodeName);

        $excluded_tags = [
            'a',
            'h1','h2','h3','h4','h5','h6',
            'button',
            'nav',
            'footer',
            'form',
            'input',
            'textarea',
            'select',
            'option',
            'table',
            'thead',
            'tbody',
            'tfoot',
            'tr',
            'td',
            'th',
            'caption',
            'figcaption',
            'figure',
            'script',
            'style',
            'code',
            'pre',
            'svg'
        ];

        if (in_array($tag, $excluded_tags, true)) {
            return;
        }

        /*
         * Exclude by CSS classes / IDs
         * catches menus, testimonials, schema wrappers, captions, builders
         */
        $class = strtolower($node->getAttribute('class'));
        $id    = strtolower($node->getAttribute('id'));

        $excluded_keywords = [
            'menu',
            'nav',
            'navigation',
            'footer',
            'button',
            'btn',
            'testimonial',
            'review',
            'rating',
            'schema',
            'json-ld',
            'shortcode',
            'caption',
            'gallery',
            'widget',
            'breadcrumb',
            'elementor-button',
            'elementor-testimonial',
            'wp-caption'
        ];

        foreach ($excluded_keywords as $keyword) {
            if (
                strpos($class, $keyword) !== false ||
                strpos($id, $keyword) !== false
            ) {
                return;
            }
        }
    foreach (iterator_to_array($node->childNodes) as $child) {
        if ($page_glossary_count >= $remaining_auto_slots) {
                return;
            }

            if ($paragraph_link_count >= $max_links) {
                return;
            }
        self::processParagraph(
            $dom,
            $child,
            $terms,
            $paragraph_link_count,
            $linked_terms,
            $saved_auto_terms,
            $priority_terms,
            $page_glossary_count,
            $remaining_auto_slots
        );
    }

        return;
    }

    /*
     * Ignore comments / shortcode leftovers
     */
    if ($node->nodeType === XML_COMMENT_NODE) {
        return;
    }

    if ($node->nodeType !== XML_TEXT_NODE) {
        return;
    }

    $text = $node->nodeValue;

    if (trim($text) === '') {
        return;
    }

    /*
     * Skip shortcode fragments
     */
    if (preg_match('/\[[^\]]+\]/', $text)) {
        return;
    }

    /*
     * Skip JSON/schema-looking text
     */
    if (
        strpos($text, '@context') !== false ||
        strpos($text, '@type') !== false ||
        strpos($text, 'application/ld+json') !== false
    ) {
        return;
    }

    // sentence splitting
    $sentences = preg_split('/(?<=[.!?])\s+/u', $text);

    foreach ($sentences as $sentence) {

    $is_strict_natural = self::is_natural_sentence($sentence, $node);

    if (!$is_strict_natural) {
        $is_fallback_natural = self::is_fallback_sentence($sentence, $node);

        if (!$is_fallback_natural) {
            continue;
        }
    }

    $best_match = null;
    $best_position = null;

    foreach ($terms as $term) {

        $term_name = trim($term->post_title);

        if (!$term_name) {
            continue;
        }

        $key = strtolower(trim($term_name));
        $is_priority = isset($priority_terms[$key]);

       if (
    isset($linked_terms[$key]) &&
    !isset($priority_terms[$key])
) {
    continue;
}

        $pattern = '/\b' . preg_quote($term_name, '/') . '\b/iu';

        if (preg_match($pattern, $sentence, $matches, PREG_OFFSET_CAPTURE)) {

            $position = $matches[0][1];

            if (
                $best_match === null ||
                ($is_priority && !isset($priority_terms[strtolower(trim($best_match->post_title))])) ||
                ($position < $best_position && !$is_priority)
            ) {
                $best_position = $position;
                $best_match = $term;
            }
        }
    }

    if ($best_match) {

        $term_name = trim($best_match->post_title);
        $term_url = get_permalink($best_match->ID);

        $pattern = '/\b(' . preg_quote($term_name, '/') . ')\b/iu';

        $replacement = preg_replace(
            $pattern,
            '<a class="ss-glossary-term" href="' . esc_url($term_url) . '">$1</a>',
            $sentence,
            1
        );

        $new_text = preg_replace(
            '/' . preg_quote($sentence, '/') . '/u',
            $replacement,
            $text,
            1
        );

        $temp = new DOMDocument('1.0', 'UTF-8');

        @$temp->loadHTML(
            '<?xml encoding="utf-8" ?><div>' . $new_text . '</div>',
            LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
        );

        $fragment = $dom->createDocumentFragment();

        foreach ($temp->documentElement->childNodes as $child) {
            $fragment->appendChild(
                $dom->importNode($child, true)
            );
        }

  $parent = $node->parentNode;

if ($parent) {
    $parent->replaceChild($fragment, $node);
}

$term_key = strtolower(trim($best_match->post_title));

$linked_terms[$term_key] = true;

if (!in_array($term_key, $saved_auto_terms, true)) {
    $saved_auto_terms[] = $term_key;
}

unset($priority_terms[$term_key]);

$paragraph_link_count++;
$page_glossary_count++;

if ($page_glossary_count >= $remaining_auto_slots) {
    return;
}

/*
 * Re-process updated paragraph content
 * so long paragraphs can receive second allowed link
 */
foreach (iterator_to_array($parent->childNodes) as $child) {

    if ($page_glossary_count >= $remaining_auto_slots) {
        return;
    }

    if ($paragraph_link_count >= $max_links) {
        return;
    }

    self::processParagraph(
        $dom,
        $child,
        $terms,
        $paragraph_link_count,
        $linked_terms,
        $saved_auto_terms,
        $priority_terms,
        $page_glossary_count,
        $remaining_auto_slots
    );
}

return;
            }
        }

    
}
}

new SS_AutoLinker();