<?php

/**
 * Enhanced Link Suggestion Service
 *
 * Integrates all internal linking components for intelligent suggestions
 *
 * @package ProRank\SEO\Modules\Content\InternalLinking
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Content\InternalLinking;

use ProRank\SEO\Modules\Content\InternalLinking\Parser\ContentParser;
use ProRank\SEO\Modules\Content\InternalLinking\Parser\PhraseExtractor;
use ProRank\SEO\Modules\Content\InternalLinking\Parser\ContextAnalyzer;
use ProRank\SEO\Modules\Content\InternalLinking\Parser\MultiLanguage\StemmersLoader;
use ProRank\SEO\Modules\Content\InternalLinking\AutoLink\RuleEngine;
use ProRank\SEO\Core\FocusKeywords;

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


/**
 * EnhancedLinkSuggestionService class
 */


class EnhancedLinkSuggestionService {
    
    /**
     * Content parser
     *
     * @var ContentParser
     */
    private ContentParser $parser;
    
    /**
     * Phrase extractor
     *
     * @var PhraseExtractor
     */
    private PhraseExtractor $phrase_extractor;
    
    /**
     * Context analyzer
     *
     * @var ContextAnalyzer
     */
    private ContextAnalyzer $context_analyzer;
    
    /**
     * Rule engine
     *
     * @var RuleEngine
     */
    private RuleEngine $rule_engine;
    
    
    /**
     * Constructor
     */
    public function __construct() {
        $this->parser = new ContentParser();
        $this->phrase_extractor = new PhraseExtractor();
        $this->context_analyzer = new ContextAnalyzer();
        $this->rule_engine = new RuleEngine();
    }
    
    /**
     * Cache duration in seconds (1 hour)
     */
    private const CACHE_DURATION = HOUR_IN_SECONDS;

    /**
     * Get enhanced link suggestions for a post
     *
     * @param int    $post_id Post ID
     * @param string $content Post content
     * @param array  $options Options
     * @return array Suggestions
     */
    public function get_suggestions(int $post_id, string $content, array $options = []): array {
        $limit = $options['limit'] ?? 10;
        $skip_cache = $options['skip_cache'] ?? false;

        $clean_content = wp_strip_all_tags($content);
        $linked_urls = $this->extract_linked_urls($content);
        $current_tokens = $this->get_content_tokens($clean_content);
        $current_terms = $this->get_post_term_ids($post_id);

        // Generate cache key based on post ID and content hash
        $cache_key = 'prorank_link_suggestions_' . $post_id . '_' . md5($content . serialize($options));

        // Check cache first (unless explicitly skipped)
        if (!$skip_cache) {
            $cached = get_transient($cache_key);
            if ($cached !== false) {
                return $cached;
            }
        }

        $suggestions = [];

        // Get different types of suggestions
        $suggestion_types = [];
        
        // Keyword-based suggestions
        $keyword_suggestions = $this->get_keyword_suggestions($post_id, $content, $linked_urls, $current_tokens, $current_terms);
        if (!empty($keyword_suggestions)) {
            $suggestion_types['keyword'] = $keyword_suggestions;
        }
        
        // 3. Auto-link rule suggestions
        $rule_suggestions = $this->get_rule_suggestions($content, $linked_urls, $current_tokens, $current_terms);
        if (!empty($rule_suggestions)) {
            $suggestion_types['rule'] = $rule_suggestions;
        }
        
        // Merge and rank suggestions
        $suggestions = $this->merge_and_rank_suggestions($suggestion_types);

        // Limit results
        $result = array_slice($suggestions, 0, $limit);

        // Cache the results for future requests
        set_transient($cache_key, $result, self::CACHE_DURATION);

        return $result;
    }

    /**
     * Clear cached suggestions for a post
     *
     * @param int $post_id Post ID
     * @return void
     */
    public function clear_cache(int $post_id): void {
        global $wpdb;

        // Delete all transients for this post
        $wpdb->query($wpdb->prepare(
            "DELETE FROM {$wpdb->options}
            WHERE option_name LIKE %s",
            '_transient_prorank_link_suggestions_' . $post_id . '_%'
        ));

        // Also delete timeout entries
        $wpdb->query($wpdb->prepare(
            "DELETE FROM {$wpdb->options}
            WHERE option_name LIKE %s",
            '_transient_timeout_prorank_link_suggestions_' . $post_id . '_%'
        ));
    }
    
    /**
     * Get keyword-based suggestions
     *
     * @param int    $post_id Post ID
     * @param string $content Content
     * @return array Suggestions
     */
    private function get_keyword_suggestions(
        int $post_id,
        string $content,
        array $linked_urls,
        array $current_tokens,
        array $current_terms
    ): array {
        $suggestions = [];
        
        // Extract key phrases
        $phrases = $this->phrase_extractor->extract_key_phrases($content);
        
        foreach ($phrases as $phrase_data) {
            $phrase = $phrase_data['phrase'];
            if ($this->is_weak_anchor($phrase)) {
                continue;
            }
            
            // Find posts with this phrase in title or content
            $matching_posts = $this->find_posts_by_phrase($phrase, $post_id);
            
            foreach ($matching_posts as $match) {
                if (!$match instanceof \WP_Post) {
                    continue;
                }
                if ($match->ID === $post_id) {
                    continue;
                }
                if ($this->is_noindex_post($match->ID)) {
                    continue;
                }

                $target_url = get_permalink($match->ID);
                if ($target_url && $this->is_url_already_linked($target_url, $linked_urls)) {
                    continue;
                }

                $profile = $this->get_target_profile($match->ID, $match->post_title);
                $anchor_score = $this->score_anchor_against_profile($phrase, $profile);
                if ($anchor_score < 0.35) {
                    continue;
                }
                $content_score = $this->calculate_token_overlap($current_tokens, $profile['tokens']);
                $taxonomy_score = $this->calculate_taxonomy_overlap($current_terms, $profile['term_ids']);
                $context = $this->parser->extract_context($content, $phrase);
                
                $suggestions[] = [
                    'type' => 'keyword',
                    'post_id' => $match->ID,
                    'title' => $match->post_title,
                    'url' => $target_url,
                    'anchor_text' => $phrase,
                    'context' => $context,
                    'score' => $phrase_data['tf_idf_score'] ?? 0.5,
                    'anchor_score' => $anchor_score,
                    'content_score' => $content_score,
                    'taxonomy_score' => $taxonomy_score,
                    'match_type' => 'keyword',
                ];
            }
        }
        
        return $suggestions;
    }
    
    /**
     * Get rule-based suggestions
     *
     * @param string $content Content
     * @return array Suggestions
     */
    private function get_rule_suggestions(
        string $content,
        array $linked_urls,
        array $current_tokens,
        array $current_terms
    ): array {
        $suggestions = [];
        $rules = $this->rule_engine->get_active_rules();
        
        foreach ($rules as $rule) {
            // Check if rule URL points to a post
            $target_post_id = url_to_postid($rule['url']);
            if (!$target_post_id) {
                continue;
            }
            
            $target_post = get_post($target_post_id);
            if (!$target_post) {
                continue;
            }

            if ($this->is_noindex_post($target_post_id)) {
                continue;
            }
            
            // Check if keyword appears in content
            if ($this->is_weak_anchor($rule['keyword'])) {
                continue;
            }

            if (stripos($content, $rule['keyword']) !== false) {
                if ($rule['url'] && $this->is_url_already_linked($rule['url'], $linked_urls)) {
                    continue;
                }

                $profile = $this->get_target_profile($target_post_id, $target_post->post_title);
                $anchor_score = $this->score_anchor_against_profile($rule['keyword'], $profile);
                if ($anchor_score < 0.3) {
                    continue;
                }
                $content_score = $this->calculate_token_overlap($current_tokens, $profile['tokens']);
                $taxonomy_score = $this->calculate_taxonomy_overlap($current_terms, $profile['term_ids']);
                $context = $this->parser->extract_context($content, $rule['keyword']);
                
                $suggestions[] = [
                    'type' => 'rule',
                    'post_id' => $target_post_id,
                    'title' => $target_post->post_title,
                    'url' => $rule['url'],
                    'anchor_text' => $rule['keyword'],
                    'context' => $context,
                    'score' => ($rule['priority'] / 100),
                    'anchor_score' => $anchor_score,
                    'content_score' => $content_score,
                    'taxonomy_score' => $taxonomy_score,
                    'match_type' => $rule['pattern_type'],
                    'rule_id' => $rule['id'],
                ];
            }
        }
        
        return $suggestions;
    }
    
    /**
     * Merge and rank suggestions from different sources
     *
     * @param array $suggestion_types Suggestions by type
     * @return array Merged and ranked suggestions
     */
    private function merge_and_rank_suggestions(array $suggestion_types): array {
        $all_suggestions = [];
        $seen_targets = [];
        
        // Weight factors for different types
        $type_weights = [
            'rule' => 1.0,    // Manual rules are trusted
            'keyword' => 0.9, // Keyword matching is baseline
        ];
        
        // Pillar content boost factor (25% boost for pillar content)
        $pillar_boost = 1.25;

        // Merge all suggestions
        foreach ($suggestion_types as $type => $suggestions) {
            foreach ($suggestions as $suggestion) {
                // Create unique key for deduplication
                $key = $suggestion['post_id'] . '_' . md5($suggestion['anchor_text']);

                if (!isset($seen_targets[$key])) {
                    $quality_score = (0.6 * ($suggestion['anchor_score'] ?? 0.0))
                        + (0.25 * ($suggestion['content_score'] ?? 0.0))
                        + (0.15 * ($suggestion['taxonomy_score'] ?? 0.0));

                    // Apply type weight to score
                    $base_score = ($suggestion['score'] ?? 0.0) * ($type_weights[$type] ?? 1.0);
                    $suggestion['final_score'] = ($base_score * 0.7) + ($quality_score * 0.3);

                    // Apply pillar content boost
                    $is_pillar = (bool) get_post_meta($suggestion['post_id'], '_prorank_is_pillar_content', true);
                    if ($is_pillar) {
                        $suggestion['final_score'] *= $pillar_boost;
                        $suggestion['is_pillar'] = true;
                    } else {
                        $suggestion['is_pillar'] = false;
                    }

                    $all_suggestions[] = $suggestion;
                    $seen_targets[$key] = true;
                }
            }
        }

        // Sort by final score
        usort($all_suggestions, function($a, $b) {
            return $b['final_score'] <=> $a['final_score'];
        });

        return $all_suggestions;
    }
    
    /**
     * Find best anchor text for target
     *
     * @param array  $phrases     Available phrases
     * @param string $target_title Target post title
     * @param string $content     Source content
     * @return array|null Best anchor data
     */
    private function find_best_anchor_for_target(
        array $phrases,
        string $target_title,
        string $content,
        array $profile
    ): ?array {
        $best_anchor = null;
        $best_score = 0;
        $target_phrases = array_values(array_unique(array_filter(array_merge([$target_title], $profile['phrases'] ?? []))));
        
        // Try exact title match first
        if (stripos($content, $target_title) !== false) {
            $anchor_score = $this->score_anchor_against_profile($target_title, $profile);
            if ($anchor_score >= 0.4 && !$this->is_weak_anchor($target_title)) {
            $context = $this->parser->extract_context($content, $target_title);
            return [
                'text' => $target_title,
                'context' => $context,
                    'score' => 1.0,
                    'anchor_score' => $anchor_score,
            ];
            }
        }
        
        // Try phrases
        foreach ($phrases as $phrase) {
            $phrase_text = $phrase['phrase'] ?? '';
            if ($phrase_text === '') {
                continue;
            }
            // Calculate relevance to target title
            $relevance = $this->calculate_phrase_relevance($phrase_text, $target_title);
            $anchor_score = $this->score_anchor_against_profile($phrase_text, $profile);
            if ($anchor_score < 0.25) {
                continue;
            }
            $phrase_score = $phrase['score'] ?? 0.5;
            $combined = ($anchor_score * 0.5) + ($relevance * 0.3) + ($phrase_score * 0.2);
            
            foreach ($target_phrases as $target_phrase) {
                if ($target_phrase !== '' && stripos($phrase_text, $target_phrase) !== false) {
                    $combined += 0.1;
                    break;
                }
            }

            if ($combined > $best_score) {
                $best_score = $combined;
                $best_anchor = [
                    'text' => $phrase_text,
                    'context' => $phrase['context'] ?? '',
                    'score' => $combined,
                    'anchor_score' => $anchor_score,
                ];
            }
        }
        
        return $best_anchor;
    }


    /**
     * Calculate phrase relevance to target
     *
     * @param string $phrase Phrase text
     * @param string $target Target text
     * @return float Relevance score
     */
    private function calculate_phrase_relevance(string $phrase, string $target): float {
        $phrase_words = array_map('strtolower', preg_split('/\s+/', $phrase));
        $target_words = array_map('strtolower', preg_split('/\s+/', $target));

        $stemmer = StemmersLoader::get_current_stemmer();
        if ($stemmer) {
            $phrase_words = $stemmer->stem_words($phrase_words);
            $target_words = $stemmer->stem_words($target_words);
        }
        
        $common_words = array_intersect($phrase_words, $target_words);
        
        if (empty($common_words)) {
            return 0.0;
        }
        
        // Calculate Jaccard similarity
        $union = array_unique(array_merge($phrase_words, $target_words));
        return count($common_words) / count($union);
    }

    /**
     * Build target profile tokens and taxonomy.
     *
     * @param int    $post_id Post ID.
     * @param string $title   Post title.
     * @return array
     */
    private function get_target_profile(int $post_id, string $title): array {
        $tokens = $this->get_tokens($title);
        $phrases = $this->get_title_phrases($title, 6);

        $focus_keyword = FocusKeywords::get_primary($post_id);
        if (empty($focus_keyword)) {
            $focus_keyword = get_post_meta($post_id, '_yoast_wpseo_focuskw', true);
        }
        if (!empty($focus_keyword)) {
            $phrases[] = (string) $focus_keyword;
            $tokens = array_merge($tokens, $this->get_tokens((string) $focus_keyword));
        }

        $slug = get_post_field('post_name', $post_id);
        if (!empty($slug)) {
            $tokens = array_merge($tokens, $this->get_tokens(str_replace('-', ' ', (string) $slug)));
        }

        $term_names = [];
        foreach (['category', 'post_tag'] as $taxonomy) {
            $terms = get_the_terms($post_id, $taxonomy);
            if (!empty($terms) && !is_wp_error($terms)) {
                foreach ($terms as $term) {
                    $term_names[] = $term->name;
                }
            }
        }
        foreach ($term_names as $name) {
            $tokens = array_merge($tokens, $this->get_tokens($name));
        }

        $tokens = array_values(array_unique($tokens));
        $phrases = array_values(array_unique(array_filter(array_map('trim', $phrases))));

        return [
            'tokens' => $tokens,
            'phrases' => $phrases,
            'term_ids' => $this->get_post_term_ids($post_id),
        ];
    }

    /**
     * Score anchor text against target profile.
     *
     * @param string $anchor Anchor text.
     * @param array  $profile Target profile.
     * @return float
     */
    private function score_anchor_against_profile(string $anchor, array $profile): float {
        $anchor_tokens = $this->get_tokens($anchor);
        $profile_tokens = $profile['tokens'] ?? [];
        if (empty($anchor_tokens) || empty($profile_tokens)) {
            return 0.0;
        }

        $overlap = array_intersect($anchor_tokens, $profile_tokens);
        $overlap_count = count($overlap);
        if ($overlap_count === 0) {
            return 0.0;
        }

        $target_score = $overlap_count / max(1, count($profile_tokens));
        $anchor_score = $overlap_count / max(1, count($anchor_tokens));
        $score = ($target_score * 0.65) + ($anchor_score * 0.35);

        $anchor_lower = strtolower($anchor);
        foreach ($profile['phrases'] ?? [] as $phrase) {
            if ($phrase === '') {
                continue;
            }
            $phrase_lower = strtolower($phrase);
            if ($anchor_lower === $phrase_lower) {
                $score += 0.35;
                break;
            }
            if (stripos($anchor_lower, $phrase_lower) !== false) {
                $score += 0.15;
            }
        }

        $word_count = str_word_count($anchor_lower);
        if ($word_count > 6) {
            $score *= 0.8;
        } elseif ($word_count >= 2 && $word_count <= 5) {
            $score += 0.1;
        } elseif ($word_count === 1) {
            $score -= 0.1;
        }

        return max(0.0, min(1.0, $score));
    }

    /**
     * Tokenize text into significant tokens.
     *
     * @param string $text Input text.
     * @return array
     */
    private function get_tokens(string $text): array {
        $normalized = $this->normalize_text($text);
        if ($normalized === '') {
            return [];
        }
        $raw_words = preg_split('/\s+/', $normalized);
        $tokens = [];
        foreach ($raw_words as $word) {
            if ($word === '') {
                continue;
            }
            if (!$this->is_significant_word($word)) {
                continue;
            }
            $tokens[] = $word;
        }
        return array_values(array_unique($tokens));
    }

    /**
     * Determine if a word is significant for matching.
     *
     * @param string $word Word to check.
     * @return bool
     */
    private function is_significant_word(string $word): bool {
        if ($this->get_text_length($word) < 3) {
            return false;
        }
        $stop_words = $this->get_stop_words();
        $weak_words = $this->get_weak_words();
        if (in_array($word, $stop_words, true) || in_array($word, $weak_words, true)) {
            return false;
        }
        return true;
    }

    /**
     * Get title phrases (2-6 word chunks).
     *
     * @param string $title Title to extract phrases from.
     * @param int    $max_words Maximum words per phrase.
     * @return array
     */
    private function get_title_phrases(string $title, int $max_words = 6): array {
        $raw_words = preg_split('/\s+/', $title);
        $words = [];
        foreach ($raw_words as $word) {
            $word = trim($word, ".,!?;:\"'()-");
            if ($word !== '') {
                $words[] = $word;
            }
        }

        $count = count($words);
        if ($count < 2) {
            return [];
        }

        $phrases = [];
        $max_words = min($max_words, $count);
        for ($length = $max_words; $length >= 2; $length--) {
            for ($i = 0; $i <= $count - $length; $i++) {
                $slice = array_slice($words, $i, $length);
                $significant_count = 0;
                foreach ($slice as $word) {
                    $lower = strtolower($word);
                    if ($this->is_significant_word($lower)) {
                        $significant_count++;
                    }
                }
                if ($significant_count < 2) {
                    continue;
                }
                $phrases[] = implode(' ', $slice);
            }
        }

        return array_values(array_unique($phrases));
    }

    /**
     * Get content tokens ranked by frequency.
     *
     * @param string $content Clean content.
     * @param int    $limit   Max tokens.
     * @return array
     */
    private function get_content_tokens(string $content, int $limit = 80): array {
        $tokens = $this->get_tokens($content);
        if (empty($tokens)) {
            return [];
        }
        $counts = array_count_values($tokens);
        arsort($counts);
        return array_slice(array_keys($counts), 0, $limit);
    }

    /**
     * Calculate overlap score between token sets.
     *
     * @param array $tokens_a Tokens A.
     * @param array $tokens_b Tokens B.
     * @return float
     */
    private function calculate_token_overlap(array $tokens_a, array $tokens_b): float {
        if (empty($tokens_a) || empty($tokens_b)) {
            return 0.0;
        }
        $overlap = array_intersect($tokens_a, $tokens_b);
        return count($overlap) / max(1, count(array_unique(array_merge($tokens_a, $tokens_b))));
    }

    /**
     * Get term IDs for categories and tags.
     *
     * @param int $post_id Post ID.
     * @return array
     */
    private function get_post_term_ids(int $post_id): array {
        $term_ids = [];
        foreach (['category', 'post_tag'] as $taxonomy) {
            $terms = get_the_terms($post_id, $taxonomy);
            if (!empty($terms) && !is_wp_error($terms)) {
                foreach ($terms as $term) {
                    $term_ids[] = (int) $term->term_id;
                }
            }
        }
        return array_values(array_unique(array_filter($term_ids)));
    }

    /**
     * Calculate taxonomy overlap between current and target.
     *
     * @param array $current_terms Current term IDs.
     * @param array $target_terms  Target term IDs.
     * @return float
     */
    private function calculate_taxonomy_overlap(array $current_terms, array $target_terms): float {
        if (empty($current_terms) || empty($target_terms)) {
            return 0.0;
        }
        $overlap = array_intersect($current_terms, $target_terms);
        return count($overlap) / max(1, count($current_terms));
    }

    /**
     * Extract linked URLs from content.
     *
     * @param string $content Raw content.
     * @return array
     */
    private function extract_linked_urls(string $content): array {
        $urls = [];
        if (preg_match_all('/href=[\"\\\']([^\"\\\']+)[\"\\\']/i', $content, $matches)) {
            foreach ($matches[1] as $url) {
                $normalized = $this->normalize_url((string) $url);
                if ($normalized) {
                    $urls[] = $normalized;
                }
            }
        }
        return array_values(array_unique($urls));
    }

    /**
     * Check if a URL is already linked in content.
     *
     * @param string $url   Target URL.
     * @param array  $links Linked URLs.
     * @return bool
     */
    private function is_url_already_linked(string $url, array $links): bool {
        $normalized = $this->normalize_url($url);
        if (!$normalized) {
            return false;
        }
        return in_array($normalized, $links, true);
    }

    /**
     * Normalize URL to host + path.
     *
     * @param string $url URL to normalize.
     * @return string|null
     */
    private function normalize_url(string $url): ?string {
        $parsed = wp_parse_url($url);
        if (empty($parsed)) {
            return null;
        }
        $host = $parsed['host'] ?? '';
        $path = $parsed['path'] ?? '';
        if ($host === '' && $path === '') {
            return null;
        }
        $normalized = strtolower($host . $path);
        return rtrim($normalized, '/');
    }

    /**
     * Determine if a post is noindex.
     *
     * @param int $post_id Post ID.
     * @return bool
     */
    private function is_noindex_post(int $post_id): bool {
        $robots = get_post_meta($post_id, '_prorank_meta_robots', true);
        if (is_array($robots) && in_array('noindex', $robots, true)) {
            return true;
        }
        $prorank_noindex = get_post_meta($post_id, '_prorank_seo_noindex', true);
        if ($prorank_noindex === '1' || $prorank_noindex === 'on') {
            return true;
        }
        $robots_noindex = get_post_meta($post_id, '_prorank_seo_robots_noindex', true);
        if ($robots_noindex === '1') {
            return true;
        }
        $yoast_noindex = get_post_meta($post_id, '_yoast_wpseo_meta-robots-noindex', true);
        if ($yoast_noindex === '1') {
            return true;
        }
        return false;
    }

    /**
     * Check if anchor text is too weak or generic
     *
     * @param string $anchor Anchor text.
     * @return bool
     */
    private function is_weak_anchor(string $anchor): bool {
        $normalized = $this->normalize_text($anchor);
        if ($normalized === '') {
            return true;
        }

        $words = preg_split('/\s+/', $normalized, -1, PREG_SPLIT_NO_EMPTY);
        if (empty($words)) {
            return true;
        }

        $stop_words = $this->get_stop_words();
        $weak_words = $this->get_weak_words();
        $non_weak = 0;

        foreach ($words as $word) {
            if (preg_match('/^\d+$/', $word)) {
                continue;
            }
            if (in_array($word, $stop_words, true) || in_array($word, $weak_words, true)) {
                continue;
            }
            $non_weak++;
        }

        if ($non_weak === 0) {
            return true;
        }

        if (count($words) >= 2 && $non_weak < 2) {
            return true;
        }

        if (count($words) === 1) {
            $word = $words[0];
            if ($this->get_text_length($word) < 4) {
                return true;
            }
            if (in_array($word, $stop_words, true) || in_array($word, $weak_words, true)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Normalize text for matching
     *
     * @param string $text Raw text.
     * @return string
     */
    private function normalize_text(string $text): string {
        $text = wp_strip_all_tags($text);
        $text = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $text);
        $text = trim($text);

        if (function_exists('mb_strtolower')) {
            return mb_strtolower($text);
        }

        return strtolower($text);
    }

    /**
     * Get stop words list
     *
     * @return array
     */
    private function get_stop_words(): array {
        static $cache = null;
        if (is_array($cache) && !empty($cache)) {
            return $cache;
        }

        $defaults = [
            'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for',
            'from', 'has', 'he', 'in', 'is', 'it', 'its', 'of', 'on',
            'that', 'the', 'to', 'was', 'will', 'with', 'this', 'these',
            'those', 'your', 'you', 'we', 'our', 'they', 'them', 'their',
            'or', 'but', 'not', 'if', 'then', 'than', 'also',
            'how', 'when', 'why', 'what', 'where', 'who', 'which', 'whom', 'whose',
            'can', 'could', 'should', 'would', 'may', 'might', 'do', 'does', 'did',
            'doing', 'done', 'make', 'makes', 'made', 'get', 'gets', 'got',
        ];

        $settings = SettingsStore::get(['language' => 'en']);
        $language = isset($settings['language']) ? sanitize_key((string) $settings['language']) : 'en';
        $language = strtolower(substr($language, 0, 2));
        if ($language === '') {
            $language = 'en';
        }

        $file = __DIR__ . '/Parser/StopWords/' . $language . '_stop_words.txt';
        if (!file_exists($file)) {
            $file = __DIR__ . '/Parser/StopWords/en_stop_words.txt';
        }

        if (file_exists($file)) {
            $words = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
            if (is_array($words) && !empty($words)) {
                $cache = array_values(
                    array_unique(
                        array_filter(
                            array_map(
                                static function ($word): string {
                                    $clean = trim((string) $word);
                                    if ($clean === '') {
                                        return '';
                                    }
                                    if (function_exists('mb_strtolower')) {
                                        return mb_strtolower($clean);
                                    }
                                    return strtolower($clean);
                                },
                                $words
                            )
                        )
                    )
                );

                if (!empty($cache)) {
                    return $cache;
                }
            }
        }

        $cache = $defaults;
        return $cache;
    }

    /**
     * Extra weak words beyond stop words
     *
     * @return array
     */
    private function get_weak_words(): array {
        return [
            'about', 'after', 'before', 'during', 'because', 'into', 'over', 'under',
            'between', 'within', 'without', 'very', 'more', 'most', 'less',
        ];
    }

    /**
     * Get text length with multibyte support
     *
     * @param string $text Text to measure.
     * @return int
     */
    private function get_text_length(string $text): int {
        if (function_exists('mb_strlen')) {
            return mb_strlen($text);
        }

        return strlen($text);
    }
    
    /**
     * Find posts by search query
     *
     * @param string $query      Search query
     * @param int    $exclude_id Post to exclude
     * @return array Posts
     */
    private function find_posts_by_query(string $query, int $exclude_id): array {
        $args = [
            's' => $query,
            'post_type' => ['post', 'page'],
            'post_status' => 'publish',
            'posts_per_page' => 5,
            'post__not_in' => [$exclude_id],
        ];
        
        $query_obj = new \WP_Query($args);
        return $query_obj->posts;
    }
    
    /**
     * Find posts by phrase
     *
     * @param string $phrase     Phrase to search
     * @param int    $exclude_id Post to exclude
     * @return array Posts
     */
    private function find_posts_by_phrase(string $phrase, int $exclude_id): array {
        global $wpdb;
        
        $like_phrase = '%' . $wpdb->esc_like($phrase) . '%';
        
        $post_ids = $wpdb->get_col($wpdb->prepare(
            "SELECT ID FROM {$wpdb->posts} 
            WHERE post_status = 'publish' 
            AND post_type IN ('post', 'page')
            AND ID != %d
            AND (post_title LIKE %s OR post_content LIKE %s)
            ORDER BY post_modified DESC
            LIMIT 5",
            $exclude_id,
            $like_phrase,
            $like_phrase
        ));
        
        $posts = [];
        foreach ($post_ids as $post_id) {
            $post = get_post($post_id);
            if ($post) {
                $posts[] = $post;
            }
        }
        
        return $posts;
    }
    
}
