<?php

if (!defined('WPINC')) {
    die;
}

/**
 * Internal Linking System
 * 
 * Automatic internal link insertion based on keyword associations
 * 
 * @package Aysa
 * @since 1.0.0
 */
class Aysa_Internal_Linking {
    
    private $settings;
    private $link_counts;
    private $processed_content = [];
    
    public function __construct() {
        $this->settings = get_option('aysa_internal_linking_settings', $this->get_default_settings());
        $this->link_counts = [];
        
        // Debug constructor settings only if needed
        // aysa_debug_log('[AYSA Internal Linking] Constructor - loaded settings: ' . aysa_debug_export($this->settings, true));
        
        // Initialize hooks
        add_action('init', array($this, 'init'));
        add_action('wp_ajax_aysa_internal_linking_preview', array($this, 'ajax_preview_content'));
        add_action('wp_ajax_aysa_internal_linking_process', array($this, 'ajax_process_links'));
        add_action('wp_ajax_aysa_internal_linking_get_report', array($this, 'ajax_get_report'));
        add_action('wp_ajax_aysa_internal_linking_save_settings', array($this, 'ajax_save_settings'));
        add_action('wp_ajax_aysa_internal_linking_get_settings', array($this, 'ajax_get_settings'));
        add_action('wp_ajax_aysa_internal_linking_clear_cache', array($this, 'ajax_clear_protection_cache'));
        add_action('wp_ajax_aysa_internal_linking_remove_links', array($this, 'ajax_remove_links_from_content'));
        add_action('wp_ajax_aysa_internal_linking_remove_all_links', array($this, 'ajax_remove_all_links_from_content'));
        add_action('wp_head', array($this, 'output_frontend_link_styles'), 99);
    }

    /**
     * Ensure AYSA inline internal links are clearly visible on frontend.
     */
    public function output_frontend_link_styles() {
        if (is_admin()) {
            return;
        }
        echo '<style id="aysa-inline-link-visibility">';
        echo 'a.aysa-internal-link{color:#1a0dab !important;text-decoration:underline !important;text-underline-offset:2px;font-weight:500;}';
        echo 'a.aysa-internal-link:visited{color:#681da8 !important;}';
        echo 'a.aysa-internal-link:hover,a.aysa-internal-link:focus{color:#174ea6 !important;text-decoration-thickness:2px;outline:none;}';
        echo '</style>';
    }
    
    /**
     * AJAX: Remove existing aysa-internal-link anchors for a keyword
     */
    public function ajax_remove_links_from_content() {
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Insufficient permissions'));
            return;
        }
        check_ajax_referer('aysa_internal_linking_nonce', 'nonce');

        $keyword = isset($_POST['keyword']) ? sanitize_text_field(wp_unslash($_POST['keyword'])) : '';
        $target_url = isset($_POST['target_url']) ? esc_url_raw(wp_unslash($_POST['target_url'])) : '';
        $source_url = isset($_POST['source_url']) ? esc_url_raw(wp_unslash($_POST['source_url'])) : '';
        $link_position = isset($_POST['link_position']) ? intval(wp_unslash($_POST['link_position'])) : 1;
        
        if (empty($keyword)) {
            wp_send_json_error(array('message' => 'Missing keyword'));
            return;
        }

        $removed_from_count = $this->remove_links_from_content($keyword, $target_url, $source_url, $link_position);
        wp_send_json_success(array('removedFrom' => $removed_from_count));
    }

    /**
     * AJAX: Remove ALL aysa-internal-link anchors from content
     */
    public function ajax_remove_all_links_from_content() {
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Insufficient permissions'));
            return;
        }
        check_ajax_referer('aysa_internal_linking_nonce', 'nonce');

        $removed_from_count = $this->remove_all_links_from_content();
        wp_send_json_success(array('removedFrom' => $removed_from_count));
    }

    /**
     * Remove links from content for a specific keyword - simple position-based removal
     */
    private function remove_links_from_content($keyword, $target_url = '', $source_url = '', $link_position = 1) {
        $content_items = $this->get_all_content_for_scanning();
        $updated_count = 0;
        
        foreach ($content_items as $item) {
            if (empty($item['content'])) continue;
            
            // If source_url is specified, only process that specific page
            if (!empty($source_url)) {
                $item_url = $item['url'] ?? '';
                // Compare URLs - both with and without trailing slashes
                $source_url_clean = rtrim($source_url, '/');
                $item_url_clean = rtrim($item_url, '/');
                
                if ($source_url_clean !== $item_url_clean) {
                    continue; // Skip this item, not the target page
                }
            }
            
            $original_content = $item['content'];
            $updated_content = $original_content;
            
            // Create diacritics-aware pattern for the keyword
            $keyword_pattern = $this->create_diacritics_pattern($keyword);
            
            // Simple pattern to find aysa-internal-link anchors with this keyword
            $pattern = '/<a[^>]*class="[^"]*aysa-internal-link[^"]*"[^>]*>\s*' . $keyword_pattern . '\s*<\/a>/iu';
            
            // Find all matches
            if (preg_match_all($pattern, $original_content, $matches, PREG_OFFSET_CAPTURE)) {
                // If we have multiple matches, remove the one at the specified position (1-based)
                $match_index = $link_position - 1; // Convert to 0-based index
                
                if (isset($matches[0][$match_index])) {
                    $match_to_remove = $matches[0][$match_index];
                    $actual_text = trim(wp_strip_all_tags($match_to_remove[0]));
                    
                    // Replace only this specific occurrence
                    $before = substr($original_content, 0, $match_to_remove[1]);
                    $after = substr($original_content, $match_to_remove[1] + strlen($match_to_remove[0]));
                    $updated_content = $before . $actual_text . $after;
                }
            }
            
            if ($updated_content !== $original_content) {
                // Update content in WordPress
                if ($item['type'] === 'term') {
                    wp_update_term($item['id'], $item['taxonomy'], ['description' => $updated_content]);
                } else {
                    wp_update_post(['ID' => $item['id'], 'post_content' => $updated_content]);
                }
                $updated_count++;
            }
        }
        
        return $updated_count;
    }

    /**
     * Remove ALL aysa-internal-link anchors from content
     */
    private function remove_all_links_from_content() {
        $content_items = $this->get_all_content_for_scanning();
        $updated_count = 0;
        
        foreach ($content_items as $item) {
            if (empty($item['content'])) continue;
            
            $original_content = $item['content'];
            $updated_content = $original_content;
            
            // Remove ALL aysa-internal-link anchors, regardless of keyword
            $pattern = '/<a[^>]*class="[^"]*aysa-internal-link[^"]*"[^>]*>(.*?)<\/a>/ius';
            
            // Replace each link with its text content
            $updated_content = preg_replace_callback($pattern, function($matches) {
                return wp_strip_all_tags($matches[1]); // Return just the text inside the link
            }, $updated_content);
            
            if ($updated_content !== $original_content) {
                // Update content in WordPress
                if ($item['type'] === 'term') {
                    wp_update_term($item['id'], $item['taxonomy'], ['description' => $updated_content]);
                } else {
                    wp_update_post(['ID' => $item['id'], 'post_content' => $updated_content]);
                }
                $updated_count++;
            }
        }
        
        return $updated_count;
    }

    /**
     * Initialize the internal linking system
     */
    public function init() {
        // Only process if enabled
        // Initialize internal linking system
        
        if (!$this->is_enabled()) {
            // Internal linking is disabled
            return;
        }
        
        // Hook on both frontend and admin for testing
        if (!is_admin()) {
            // Hook later in WordPress lifecycle when query is properly set
            add_action('wp', array($this, 'setup_content_filters'));
            add_action('template_redirect', array($this, 'setup_content_filters'));
        } else {
            // Also allow in admin for testing purposes
            add_action('admin_init', array($this, 'setup_content_filters'));
        }
    }
    
    /**
     * Setup content filters after WP query is established
     */
    public function setup_content_filters() {
        aysa_debug_log('[AYSA Internal Linking] setup_content_filters() called');
        
        // Only setup once
        static $setup_done = false;
        if ($setup_done) {
            aysa_debug_log('[AYSA Internal Linking] Content filters already setup, skipping');
            return;
        }
        
        // Verify we have a proper query context
        global $wp_query;
        
        if (!$wp_query || !$wp_query->is_main_query()) {
            aysa_debug_log('[AYSA Internal Linking] Not main query, skipping filter setup');
            return;
        }
        
        // Only add filters when we can detect context
        if (is_single() || is_page() || is_category() || is_tag() || is_tax()) {
            aysa_debug_log('[AYSA Internal Linking] Setting up content filters for valid context');
            
            // Hook into content filters based on settings
            if ($this->settings['process_posts'] || $this->settings['process_pages']) {
                add_filter('the_content', array($this, 'process_content'), 999);
            }
        
            if ($this->settings['process_categories']) {
                add_filter('category_description', array($this, 'process_content'), 999);
                add_filter('tag_description', array($this, 'process_content'), 999);
                add_filter('term_description', array($this, 'process_content'), 999);
            }
            
            if ($this->settings['process_products'] && class_exists('WooCommerce')) {
                add_filter('woocommerce_product_get_description', array($this, 'process_content'), 999);
                add_filter('woocommerce_product_get_short_description', array($this, 'process_content'), 999);
            }
        } else {
            aysa_debug_log('[AYSA Internal Linking] Invalid context for filters - not setting up content filters');
        }
        
        $setup_done = true;
    }
    
    /**
     * Get default settings
     */
    private function get_default_settings() {
        return array(
            'enabled' => false,
            'max_links_per_keyword' => 1, // Conservative default: one occurrence per keyword in a source entity
            'max_links_per_entity' => 3, // Article cap; dynamic rules apply in the main processor
            'matching_type' => 'both', // exact, partial, both
            'process_posts' => true,
            'process_pages' => true,
            'process_categories' => true,
            'process_products' => true,
            'excluded_post_ids' => array(),
            'excluded_post_types' => array(),
            'excluded_url_patterns' => array(
                // WooCommerce system pages
                '/cart/',
                '/checkout/',
                '/my-account/',
                '/wishlist/',
                // WordPress internal archives
                '/tag/',
                '/author/',
                '/date/',
                '/\d{4}/\d{2}/', // date archives like /2025/04/
                // Search parameters and internal queries
                '/?s=',
                '/search/',
                '/?add-to-cart=',
                '/?wc-ajax=',
                '/?orderby=',
                '/?filter_',
                // Feeds and technical pages
                '/feed/',
                '/comments/feed/',
                '/rss/',
                '/atom/',
                // WordPress admin and system
                '/wp-admin/',
                '/wp-content/',
                '/wp-includes/',
                '/wp-json/',
                // Common technical patterns
                '.xml',
                '.json',
                '.css',
                '.js',
                '.ico'
            ),
            'case_sensitive' => false,
            'ignore_existing_links' => true,
            'priority_by_authority' => true
        );
    }
    
    /**
     * Check if internal linking is enabled
     */
    public function is_enabled() {
        return !empty($this->settings['enabled']);
    }
    
    /**
     * Process content and add internal links
     */
    public function process_content($content) {
        // CRITICAL: Skip if in admin context
        if (is_admin()) {
            return $content;
        }
        
        // Skip if not enabled
        if (!$this->is_enabled()) {
            return $content;
        }
        
        // Skip if content already processed
        $content_hash = md5($content);
        if (isset($this->processed_content[$content_hash])) {
            aysa_debug_log('[AYSA Internal Linking] Content already processed, skipping');
            return $content;
        }
        
        // Get current post/term context
        $current_entity = $this->get_current_entity();
        if (!$current_entity) {
            aysa_debug_log('[AYSA Internal Linking] No current entity found');
            return $content;
        }
        
        aysa_debug_log('[AYSA Internal Linking] Current entity: ' . aysa_debug_export($current_entity, true));
        
        // Skip if current entity is excluded
        if ($this->is_entity_excluded($current_entity)) {
            aysa_debug_log('[AYSA Internal Linking] Current entity is excluded');
            return $content;
        }
        
        // Get all keyword associations
        $keyword_associations = $this->get_keyword_associations();
        if (empty($keyword_associations)) {
            aysa_debug_log('[AYSA Internal Linking] No keyword associations found');
            return $content;
        }
        
        aysa_debug_log('[AYSA Internal Linking] Found ' . count($keyword_associations) . ' keyword associations');
        
        // Process content for internal links
        $link_result = $this->insert_internal_links($content, $keyword_associations, $current_entity);
        
        // Handle new return format
        if (is_array($link_result)) {
            $processed_content = $link_result['content'];
        } else {
            // Backward compatibility
            $processed_content = $link_result;
        }
        
        // Mark as processed
        $this->processed_content[$content_hash] = true;
        
        return $processed_content;
    }
    
    /**
     * Get current entity (post, page, category, product)
     */
    private function get_current_entity() {
        global $post, $wp_query;
        
        aysa_debug_log('[AYSA Internal Linking] get_current_entity called - is_single: ' . (is_single() ? 'yes' : 'no') . ', is_page: ' . (is_page() ? 'yes' : 'no') . ', post ID: ' . (isset($post->ID) ? $post->ID : 'none'));
        aysa_debug_log('[AYSA Internal Linking] WP Query vars: ' . aysa_debug_export($wp_query->query_vars ?? [], true));
        
        // Method 1: Try the standard WordPress query checks (works after 'wp' action)
        if (is_single() || is_page()) {
            $entity = array(
                'type' => get_post_type(),
                'id' => get_the_ID(),
                'title' => get_the_title(),
                'url' => get_permalink()
            );
            aysa_debug_log('[AYSA Internal Linking] Found entity via is_single/is_page: ' . aysa_debug_export($entity, true));
            return $entity;
        }
        
        if (is_category() || is_tag() || is_tax()) {
            $term = get_queried_object();
            if ($term && isset($term->term_id)) {
                $entity = array(
                    'type' => 'term',
                    'id' => $term->term_id,
                    'taxonomy' => $term->taxonomy,
                    'title' => $term->name,
                    'url' => get_term_link($term)
                );
                aysa_debug_log('[AYSA Internal Linking] Found entity via is_category/is_tag/is_tax: ' . aysa_debug_export($entity, true));
                return $entity;
            }
        }
        
        // Method 2: Get queried object directly (reliable in most contexts)
        $queried_object = get_queried_object();
        if ($queried_object) {
            if (isset($queried_object->ID) && isset($queried_object->post_type)) {
                // It's a post object
                $entity = array(
                    'type' => $queried_object->post_type,
                    'id' => $queried_object->ID,
                    'title' => $queried_object->post_title,
                    'url' => get_permalink($queried_object->ID)
                );
                aysa_debug_log('[AYSA Internal Linking] Found entity via get_queried_object (post): ' . aysa_debug_export($entity, true));
                return $entity;
            } elseif (isset($queried_object->term_id)) {
                // It's a term object
                $entity = array(
                    'type' => 'term',
                    'id' => $queried_object->term_id,
                    'taxonomy' => $queried_object->taxonomy,
                    'title' => $queried_object->name,
                    'url' => get_term_link($queried_object)
                );
                aysa_debug_log('[AYSA Internal Linking] Found entity via get_queried_object (term): ' . aysa_debug_export($entity, true));
                return $entity;
            }
        }
        
        // Method 3: Fallback to global $post if we're in the loop
        if (isset($post) && is_object($post) && isset($post->ID) && $post->ID > 0) {
            // Check if this is a published post/page/product
            if (in_array($post->post_status, ['publish', 'private']) && in_array($post->post_type, ['post', 'page', 'product'])) {
                $entity = array(
                    'type' => $post->post_type,
                    'id' => $post->ID,
                    'title' => $post->post_title,
                    'url' => get_permalink($post->ID)
                );
                aysa_debug_log('[AYSA Internal Linking] Found entity via global $post fallback: ' . aysa_debug_export($entity, true));
                return $entity;
            }
        }
        
        // Method 4: Try to extract from WP query vars
        if (isset($wp_query->query_vars['page_id']) && $wp_query->query_vars['page_id'] > 0) {
            $page_id = $wp_query->query_vars['page_id'];
            $page = get_post($page_id);
            if ($page) {
                $entity = array(
                    'type' => $page->post_type,
                    'id' => $page->ID,
                    'title' => $page->post_title,
                    'url' => get_permalink($page->ID)
                );
                aysa_debug_log('[AYSA Internal Linking] Found entity via query_vars page_id: ' . aysa_debug_export($entity, true));
                return $entity;
            }
        }
        
        if (isset($wp_query->query_vars['p']) && $wp_query->query_vars['p'] > 0) {
            $post_id = $wp_query->query_vars['p'];
            $post_obj = get_post($post_id);
            if ($post_obj) {
                $entity = array(
                    'type' => $post_obj->post_type,
                    'id' => $post_obj->ID,
                    'title' => $post_obj->post_title,
                    'url' => get_permalink($post_obj->ID)
                );
                aysa_debug_log('[AYSA Internal Linking] Found entity via query_vars p: ' . aysa_debug_export($entity, true));
                return $entity;
            }
        }
        
        aysa_debug_log('[AYSA Internal Linking] No entity found with any method');
        return false;
    }
    
    /**
     * Check if entity is excluded from processing
     */
    private function is_entity_excluded($entity) {
        // Check excluded post IDs
        if (in_array($entity['id'], $this->settings['excluded_post_ids'])) {
            return true;
        }
        
        // Check excluded post types
        if (in_array($entity['type'], $this->settings['excluded_post_types'])) {
            return true;
        }
        
        // Check excluded URL patterns
        if (isset($entity['url']) && !empty($entity['url'])) {
            if ($this->is_url_excluded($entity['url'])) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check if URL matches any excluded patterns
     */
    private function is_url_excluded($url) {
        aysa_debug_log("[AYSA Internal Linking] Checking URL exclusion for: $url");
        
        // Use global exclusions first
        if (class_exists('Aysa_Global_Exclusions')) {
            $is_globally_excluded = Aysa_Global_Exclusions::is_url_excluded($url);
            aysa_debug_log("[AYSA Internal Linking] Global exclusions check for '$url': " . ($is_globally_excluded ? 'EXCLUDED' : 'ALLOWED'));
            if ($is_globally_excluded) {
                return true;
            }
        } else {
            aysa_debug_log("[AYSA Internal Linking] ⚠️ Aysa_Global_Exclusions class not available");
        }
        
        // Check internal linking specific exclusions
        $excluded_patterns = $this->settings['excluded_url_patterns'] ?? array();
        
        if (empty($excluded_patterns)) {
            aysa_debug_log("[AYSA Internal Linking] No custom exclusion patterns defined");
            return false;
        }
        
        // Parse URL to get path and query
        $parsed_url = wp_parse_url($url);
        $path = $parsed_url['path'] ?? '';
        $query = $parsed_url['query'] ?? '';
        $full_url = $path . ($query ? '?' . $query : '');
        
        foreach ($excluded_patterns as $pattern) {
            $pattern = trim($pattern);
            if (empty($pattern)) continue;
            
            // Handle regex patterns (those starting with / and containing regex chars)
            if (strpos($pattern, '/') === 0 && preg_match('/[.*+?^${}()|[\]\\\\]/', $pattern)) {
                try {
                    if (preg_match('#' . $pattern . '#i', $full_url)) {
                        aysa_debug_log("[AYSA Internal Linking] URL '$url' excluded by custom regex pattern: $pattern");
                        return true;
                    }
                } catch (Exception $e) {
                    aysa_debug_log("[AYSA Internal Linking] Invalid regex pattern: $pattern");
                    continue;
                }
            } else {
                // Handle simple string patterns
                if (strpos($full_url, $pattern) !== false) {
                    aysa_debug_log("[AYSA Internal Linking] URL '$url' excluded by custom pattern: $pattern");
                    return true;
                }
            }
        }
        
        return false;
    }
    
    /**
     * Get keyword associations from API
     */
    private function get_keyword_associations() {
        // Try to get from cache first
        $cache_key = 'aysa_internal_linking_keywords';
        $cached_keywords = get_transient($cache_key);
        
        if ($cached_keywords !== false) {
            aysa_debug_log('[AYSA Internal Linking] Using cached keywords: ' . count($cached_keywords) . ' found');
            return $cached_keywords;
        }
        
        aysa_debug_log('[AYSA Internal Linking] Cache miss, fetching keywords from local WordPress data');
        $keywords = array();
        $project_id = intval(get_option('aysa_project_id', 0));
        $associations = $this->get_local_internal_linking_associations($project_id);
        aysa_debug_log('[AYSA Internal Linking] Found ' . count($associations) . ' local associations');

        foreach ($associations as $association) {
            if (!is_array($association) || empty($association['keyword'])) {
                continue;
            }

            $keyword_data = array(
                'keyword' => $association['keyword'],
                'url' => $association['link'] ?? '',
                'title' => $association['value'] ?? '',
                'entity_id' => intval($association['idIntern'] ?? 0),
                'entity_type' => (string) ($association['associatedOn'] ?? ''),
                'is_main' => !empty($association['isMain']),
                'priority' => $this->calculate_priority($association)
            );

            $keywords[] = $keyword_data;

            if ($this->settings['matching_type'] === 'partial' || $this->settings['matching_type'] === 'both') {
                $partial_keywords = $this->generate_partial_matches($association['keyword']);
                foreach ($partial_keywords as $partial) {
                    $partial_data = $keyword_data;
                    $partial_data['keyword'] = $partial;
                    $partial_data['is_partial'] = true;
                    $keywords[] = $partial_data;
                }
            }
        }
        
        // Sort by priority
        if ($this->settings['priority_by_authority']) {
            usort($keywords, function($a, $b) {
                return $b['priority'] - $a['priority'];
            });
        }
        
        // Cache for 1 hour
        set_transient($cache_key, $keywords, HOUR_IN_SECONDS);
        
        aysa_debug_log('[AYSA Internal Linking] Final keywords count: ' . count($keywords));
        if (count($keywords) > 0) {
            aysa_debug_log('[AYSA Internal Linking] Sample keywords: ' . aysa_debug_export(array_slice($keywords, 0, 3), true));
        }
        
        return $keywords;
    }

    /**
     * Collect keyword associations from local WordPress storage only.
     * Sources:
     * 1) local association option cache
     * 2) local keyword discovery table (target_url)
     */
    private function get_local_internal_linking_associations($project_id = 0) {
        global $wpdb;

        $project_id = intval($project_id);
        if ($project_id <= 0) {
            $project_id = intval(get_option('aysa_project_id', 0));
        }

        $associations = array();
        $seen = array();

        // 1) Local association option cache.
        $option_key = 'aysa_keyword_discovery_local_associations_' . $project_id;
        $local_rows = get_option($option_key, array());
        if (is_array($local_rows)) {
            foreach ($local_rows as $row) {
                if (!is_array($row)) {
                    continue;
                }
                $keyword = trim((string) ($row['keyword'] ?? ''));
                if ($keyword === '') {
                    continue;
                }

                $assoc = array(
                    'keyword' => $keyword,
                    'associatedOn' => (string) ($row['associatedOn'] ?? $row['associated_on'] ?? ''),
                    'idIntern' => intval($row['idIntern'] ?? $row['id_intern'] ?? 0),
                    'value' => (string) ($row['value'] ?? $keyword),
                    'link' => (string) ($row['link'] ?? $row['url'] ?? $row['target_url'] ?? ''),
                    'isMain' => isset($row['isMain']) ? intval($row['isMain']) : 1,
                    'secondKeyword' => (string) ($row['secondKeyword'] ?? $row['second_keyword'] ?? ''),
                );

                $dedupe_key = strtolower($assoc['keyword']) . '|' . strtolower((string) $assoc['link']) . '|' . strtolower((string) $assoc['associatedOn']) . '|' . intval($assoc['idIntern']);
                if (!isset($seen[$dedupe_key])) {
                    $seen[$dedupe_key] = true;
                    $associations[] = $assoc;
                }
            }
        }

        // 2) Local keyword discovery table with matched target URL.
        $table = esc_sql($wpdb->prefix . 'aysa_keyword_discovery');
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table));
        if ($table_exists === $table && $project_id > 0) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $rows = $wpdb->get_results(
                $wpdb->prepare(
                    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to prepare. Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to prepare.
                    "SELECT keyword, target_url
" .
                    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: the interpolated identifier is an escaped or allowlisted plugin-owned table name; request-derived values remain prepared.
                    "                     FROM {$table}
                     WHERE project_id = %d
                       AND keyword IS NOT NULL
                       AND TRIM(keyword) <> ''
                       AND target_url IS NOT NULL
                       AND TRIM(target_url) <> ''
                     ORDER BY updated_at DESC, id DESC
                     LIMIT 10000",
                    $project_id
                ),
                ARRAY_A
            );

            if (is_array($rows)) {
                foreach ($rows as $row) {
                    $keyword = trim((string) ($row['keyword'] ?? ''));
                    $target_url = trim((string) ($row['target_url'] ?? ''));
                    if ($keyword === '' || $target_url === '') {
                        continue;
                    }

                    $assoc = array(
                        'keyword' => $keyword,
                        'associatedOn' => '',
                        'idIntern' => 0,
                        'value' => $keyword,
                        'link' => $target_url,
                        'isMain' => 1,
                        'secondKeyword' => '',
                    );

                    $dedupe_key = strtolower($assoc['keyword']) . '|' . strtolower($assoc['link']);
                    if (!isset($seen[$dedupe_key])) {
                        $seen[$dedupe_key] = true;
                        $associations[] = $assoc;
                    }
                }
            }
        }

        return $associations;
    }
    
    /**
     * Calculate priority for keyword based on entity authority
     */
    private function calculate_priority($association) {
        $priority = 1;
        
        // Higher priority for main keywords
        if ($association['isMain'] ?? false) {
            $priority += 10;
        }
        
        // Priority based on entity type - NEW ORDER: posts → categories → products → pages
        $entity_type = $association['associatedOn'] ?? '';
        switch (strtolower($entity_type)) {
            case 'post':
                $priority += 8;  // HIGHEST: Posts/Articles
                break;
            case 'category':
                $priority += 6;  // SECOND: Categories
                break;
            case 'product':
                $priority += 4;  // THIRD: Products
                break;
            case 'page':
                $priority += 2;  // LOWEST: Pages
                break;
        }
        
        return $priority;
    }
    
    /**
     * Generate partial matches for a keyword
     */
    private function generate_partial_matches($keyword) {
        $partials = array();
        $words = explode(' ', trim($keyword));
        
        if (count($words) <= 1) {
            return $partials;
        }
        
        // Generate combinations
        for ($i = 0; $i < count($words); $i++) {
            for ($j = $i + 1; $j <= count($words); $j++) {
                $partial = implode(' ', array_slice($words, $i, $j - $i));
                if (strlen($partial) > 3 && $partial !== $keyword) { // Only meaningful partials
                    $partials[] = $partial;
                }
            }
        }
        
        return array_unique($partials);
    }
    
    /**
     * Get effective maximum links per entity (hard-capped at 3 regardless of UI setting)
     * @return int Maximum links per entity (never more than 3)
     */
    private function get_effective_max_links_per_entity() {
        $configured = intval($this->settings['max_links_per_entity'] ?? 3);
        // Enforce a hard upper bound of 3 per entity
        return min($configured, 3);
    }

    /**
     * Insert internal links into content
     */
    private function insert_internal_links($content, $keyword_associations, $current_entity, $preview_mode = false) {
        if (empty($keyword_associations)) {
            return $content;
        }
        
        // Initialize link counters for this content
        $entity_link_count = 0;
        $keyword_global_counts = $this->get_keyword_global_counts();
        
        // In preview mode, create a copy of counts to simulate without affecting real cache
        if ($preview_mode) {
            aysa_debug_log('[AYSA Internal Linking] Preview mode: using temporary keyword counts (will not save to cache)');
            // Create temporary copy for preview
            $keyword_global_counts = $keyword_global_counts;
        }
        $skipped_links = [
            'existing_links' => 0,
            'self_links' => 0,
            'limit_reached' => 0
        ];
        
        // Parse content to avoid existing links
        $dom = new DOMDocument();
        $dom->loadHTML('<?xml encoding="UTF-8">' . $content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOERROR | LIBXML_NOWARNING);
        
        foreach ($keyword_associations as $keyword_data) {
            // Skip if we've reached limits
            if ($entity_link_count >= $this->get_effective_max_links_per_entity()) {
                $skipped_links['limit_reached']++;
                aysa_debug_log('[AYSA Internal Linking] SKIP: Entity link limit reached (' . $this->get_effective_max_links_per_entity() . ')');
                break;
            }
            
            // Check if keyword has reached its usage limit per entity (max_links_per_keyword = max occurrences per keyword in same entity)
            $keyword_entity_usage = $this->get_keyword_entity_usage($keyword_data['keyword']);
            $current_entity_key = $current_entity['type'] . '_' . $current_entity['id'];
            
            // Count how many times this keyword already appears in current entity
            $keyword_count_in_entity = 0;
            if (in_array($current_entity_key, $keyword_entity_usage)) {
                // If keyword already exists in this entity, count occurrences
                $keyword_count_in_entity = array_count_values($keyword_entity_usage)[$current_entity_key] ?? 0;
            }
            
            // Skip if keyword has reached its limit in this specific entity
            if ($keyword_count_in_entity >= $this->settings['max_links_per_keyword']) {
                $skipped_links['limit_reached']++;
                aysa_debug_log("[AYSA Internal Linking] SKIP: Keyword '{$keyword_data['keyword']}' already used {$keyword_count_in_entity} times in this entity (max: {$this->settings['max_links_per_keyword']})");
                continue;
            }
            
            // Skip self-linking
            if ($current_entity['id'] == $keyword_data['entity_id'] && 
                $current_entity['type'] == $keyword_data['entity_type']) {
                $skipped_links['self_links']++;
                aysa_debug_log("[AYSA Internal Linking] SKIP: Self-linking detected for '{$keyword_data['keyword']}'");
                continue;
            }
            
            // Find and replace keyword with link (now with enhanced protection)
            $replaced = $this->replace_keyword_with_link($content, $keyword_data, $current_entity, $skipped_links, $entity_link_count);
            
            if ($replaced) {
                $content = $replaced;
                $entity_link_count++;
                
                // Add this entity to keyword usage tracking (only if not in preview mode)
                if (!$preview_mode) {
                    $this->add_keyword_entity_usage($keyword_data['keyword'], $current_entity['id'], $current_entity['type']);
                }
                
                // If we've hit the entity limit, stop further processing
                if ($entity_link_count >= $this->get_effective_max_links_per_entity()) {
                    aysa_debug_log("[AYSA Internal Linking] Entity limit reached after processing '{$keyword_data['keyword']}'. Stopping further replacements.");
                    break;
                }
                
                // Log the replacement
                $this->log_link_replacement($keyword_data, $current_entity);
                
                aysa_debug_log("[AYSA Internal Linking] SUCCESS: Link created for '{$keyword_data['keyword']}' -> '{$keyword_data['url']}'");
            }
        }
        
        // Log summary of skipped links
        $total_skipped = array_sum($skipped_links);
        if ($total_skipped > 0) {
            aysa_debug_log("[AYSA Internal Linking] SKIPPED SUMMARY: " . aysa_debug_export($skipped_links, true));
        }
        
        // Note: Entity usage tracking is updated individually for each keyword in the loop above
        if ($preview_mode) {
            aysa_debug_log('[AYSA Internal Linking] Preview mode: entity usage tracking was skipped');
        } else {
            aysa_debug_log('[AYSA Internal Linking] Entity usage tracking updated for processed keywords');
        }
        
        // Return both content and statistics
        return [
            'content' => $content,
            'links_added' => $entity_link_count,
            'skipped_stats' => $skipped_links
        ];
    }
    
    /**
     * Normalize text by removing diacritics and optionally converting to lowercase
     */
    private function normalize_text($text, $case_sensitive = false) {
        // Remove diacritics - map Romanian diacritics to regular letters
        $diacritics_map = array(
            'ă' => 'a', 'Ă' => 'A',
            'â' => 'a', 'Â' => 'A', 
            'î' => 'i', 'Î' => 'I',
            'ș' => 's', 'Ș' => 'S',
            'ț' => 't', 'Ț' => 'T',
            // Also handle other common diacritics
            'à' => 'a', 'á' => 'a', 'ä' => 'a', 'ã' => 'a',
            'À' => 'A', 'Á' => 'A', 'Ä' => 'A', 'Ã' => 'A',
            'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',
            'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E',
            'ì' => 'i', 'í' => 'i', 'ï' => 'i',
            'Ì' => 'I', 'Í' => 'I', 'Ï' => 'I',
            'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o',
            'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O',
            'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u',
            'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U',
            'ç' => 'c', 'Ç' => 'C',
            'ñ' => 'n', 'Ñ' => 'N'
        );
        
        $normalized = strtr($text, $diacritics_map);
        
        if (!$case_sensitive) {
            $normalized = mb_strtolower($normalized, 'UTF-8');
        }
        
        return $normalized;
    }

    /**
     * Check if keyword is inside excluded HTML tags (H1-H6, A, alt, title, href attributes)
     * This function now returns an array of valid positions where keyword can be replaced
     */
    private function get_valid_keyword_positions($content, $keyword) {
        $case_sensitive = $this->settings['case_sensitive'] ?? false;
        $flags = $case_sensitive ? 'u' : 'iu';
        $valid_positions = [];
        
        // Create flexible pattern for the keyword with word boundaries
        $keyword_pattern = $this->create_diacritics_pattern($keyword);
        $word_boundary_pattern = '\\b' . $keyword_pattern . '\\b';
        
        // Find all occurrences of the keyword in content
        if (preg_match_all('/' . $word_boundary_pattern . '/' . $flags, $content, $matches, PREG_OFFSET_CAPTURE)) {
            foreach ($matches[0] as $match) {
                $keyword_text = $match[0];
                $keyword_offset = $match[1];
                
                // Check if this specific keyword occurrence is inside excluded tags
                if ($this->is_keyword_occurrence_in_excluded_context($content, $keyword_offset, strlen($keyword_text))) {
                    aysa_debug_log("[AYSA Internal Linking] SKIP: Keyword '$keyword' occurrence at offset $keyword_offset found in excluded context");
                } else {
                    // This position is valid for replacement
                    $valid_positions[] = [
                        'text' => $keyword_text,
                        'offset' => $keyword_offset,
                        'length' => strlen($keyword_text)
                    ];
                    aysa_debug_log("[AYSA Internal Linking] VALID: Keyword '$keyword' occurrence at offset $keyword_offset can be replaced");
                }
            }
        }
        
        return $valid_positions;
    }
    
    /**
     * Legacy function for backward compatibility - now checks if ANY valid positions exist
     */
    private function is_keyword_in_excluded_tags($content, $keyword) {
        $valid_positions = $this->get_valid_keyword_positions($content, $keyword);
        // Return true only if NO valid positions exist (all occurrences are in excluded contexts)
        return empty($valid_positions);
    }
    
    /**
     * Check if a specific keyword occurrence (by position) is within excluded HTML contexts
     */
    private function is_keyword_occurrence_in_excluded_context($content, $keyword_offset, $keyword_length) {
        $keyword_end = $keyword_offset + $keyword_length;
        
        aysa_debug_log("[AYSA Internal Linking] DEBUG: Checking keyword at offset $keyword_offset-$keyword_end");
        
        // Find all H1-H6 tags and their ranges
        if (preg_match_all('/<h[1-6][^>]*>(.*?)<\/h[1-6]>/is', $content, $h_matches, PREG_OFFSET_CAPTURE)) {
            foreach ($h_matches[0] as $h_match) {
                $h_start = $h_match[1];
                $h_end = $h_start + strlen($h_match[0]);
                
                // Check if keyword is inside this heading tag
                if ($keyword_offset >= $h_start && $keyword_end <= $h_end) {
                    aysa_debug_log("[AYSA Internal Linking] DEBUG: Keyword found inside H1-H6 tag (range $h_start-$h_end)");
                    return true;
                }
            }
        }
        
        // Find all A tags and their ranges
        if (preg_match_all('/<a[^>]*>(.*?)<\/a>/is', $content, $a_matches, PREG_OFFSET_CAPTURE)) {
            foreach ($a_matches[0] as $a_match) {
                $a_start = $a_match[1];
                $a_end = $a_start + strlen($a_match[0]);
                
                // Check if keyword is inside this anchor tag
                if ($keyword_offset >= $a_start && $keyword_end <= $a_end) {
                    aysa_debug_log("[AYSA Internal Linking] DEBUG: Keyword found inside A tag (range $a_start-$a_end)");
                    return true;
                }
            }
        }
        
        // Check for attributes (alt, title, href) by finding tag boundaries
        $char_before = $keyword_offset > 0 ? $content[$keyword_offset - 1] : '';
        $chars_before_20 = substr($content, max(0, $keyword_offset - 20), 20);
        
        // Look for attribute patterns before the keyword
        if (preg_match('/(alt|title|href)=["\'][^"\']*$/i', $chars_before_20)) {
            aysa_debug_log("[AYSA Internal Linking] DEBUG: Keyword found in HTML attribute");
            return true;
        }
        
        aysa_debug_log("[AYSA Internal Linking] DEBUG: Keyword is in valid context (not excluded)");
        return false;
    }
    
    /**
     * Check if keyword matches content considering diacritics and case sensitivity
     */
    private function keyword_matches_content($content, $keyword) {
        $case_sensitive = $this->settings['case_sensitive'] ?? false;
        
        aysa_debug_log("[AYSA Internal Linking] keyword_matches_content: case_sensitive = " . ($case_sensitive ? 'TRUE' : 'FALSE'));
        
        // Normalize both content and keyword
        $normalized_content = $this->normalize_text($content, $case_sensitive);
        $normalized_keyword = $this->normalize_text($keyword, $case_sensitive);
        
        // Use word boundary matching
        $escaped_keyword = preg_quote($normalized_keyword, '/');
        $pattern = '/\b' . $escaped_keyword . '\b/';
        
        $match_result = preg_match($pattern, $normalized_content);
        
        if ($match_result) {
            aysa_debug_log("[AYSA Internal Linking] KEYWORD MATCH: '$keyword' -> normalized: '$normalized_keyword' found in content");
        } else {
            aysa_debug_log("[AYSA Internal Linking] NO MATCH: '$keyword' -> normalized: '$normalized_keyword' NOT found in normalized content");
            aysa_debug_log("[AYSA Internal Linking] Content sample: " . substr($normalized_content, 0, 200) . "...");
        }
        
        return $match_result;
    }

    /**
     * Find the actual keyword as it appears in content (preserving original diacritics and case)
     */
    private function find_actual_keyword_in_content($content, $keyword) {
        $case_sensitive = $this->settings['case_sensitive'] ?? false;
        
        // Create a flexible pattern that handles diacritics
        $keyword_pattern = $this->create_diacritics_pattern($keyword);
        $flags = $case_sensitive ? 'u' : 'iu';
        $pattern = '/\b' . $keyword_pattern . '\b/' . $flags;
        
        if (preg_match($pattern, $content, $matches)) {
            return $matches[0];
        }
        
        return false;
    }
    
    /**
     * Create a regex pattern that matches words with or without diacritics
     */
    private function create_diacritics_pattern($keyword) {
        // Map of characters to their diacritic variants
        $diacritics_patterns = array(
            'a' => '[aăâàáäã]',
            'A' => '[AĂÂÀÁÄÃ]',
            'e' => '[eèéêë]',
            'E' => '[EÈÉÊË]',
            'i' => '[iîìí]',
            'I' => '[IÎÌÍ]',
            'o' => '[oòóôõö]',
            'O' => '[OÒÓÔÕÖ]',
            'u' => '[uùúûü]',
            'U' => '[UÙÚÛÜ]',
            's' => '[sș]',
            'S' => '[SȘ]',
            't' => '[tț]',
            'T' => '[TȚ]',
            'c' => '[cç]',
            'C' => '[CÇ]',
            'n' => '[nñ]',
            'N' => '[NÑ]'
        );
        
        // Escape the keyword for regex
        $escaped = preg_quote($keyword, '/');
        
        // Replace each character with its diacritic pattern
        foreach ($diacritics_patterns as $char => $pattern) {
            $escaped = str_replace(preg_quote($char, '/'), $pattern, $escaped);
        }
        
        return $escaped;
    }

    /**
     * Replace keyword with link in content (using valid positions only)
     */
    private function replace_keyword_with_link($content, $keyword_data, $current_entity, &$skipped_links = null, $current_entity_link_count = 0) {
        $keyword = $keyword_data['keyword'];
        $url = $keyword_data['url'];
        $title = $keyword_data['title'];
        
        // Check if keyword matches content (with diacritics and case handling)
        if (!$this->keyword_matches_content($content, $keyword)) {
            return false;
        }
        
        // CRITICAL: Check if any instance of the keyword is already linked
        // This is more thorough than just checking inside the replace function
        if ($this->keyword_already_linked_anywhere($content, $keyword)) {
            if ($skipped_links) $skipped_links['existing_links']++;
            aysa_debug_log("[AYSA Internal Linking] SKIP: Keyword '$keyword' has existing links anywhere in content");
            return false;
        }
        
        // Get valid positions where keyword can be replaced (not in excluded contexts)
        $valid_positions = $this->get_valid_keyword_positions($content, $keyword);
        
        if (empty($valid_positions)) {
            aysa_debug_log("[AYSA Internal Linking] SKIP: No valid positions found for keyword '$keyword' (all in excluded contexts)");
            return false;
        }
        
        // FIXED: Respect global entity limit - calculate how many links we can still add
        $max_entity_links = $this->get_effective_max_links_per_entity();
        $remaining_entity_links = $max_entity_links - $current_entity_link_count;
        
        if ($remaining_entity_links <= 0) {
            aysa_debug_log("[AYSA Internal Linking] SKIP: Entity link limit already reached ($current_entity_link_count/$max_entity_links)");
            return false;
        }
        
        // For this keyword, we can add at most 1 link (not multiple per keyword)
        $max_replacements = 1;
        $replacements_made = 0;
        $new_content = $content;
        
        // Process valid positions in reverse order to maintain correct offsets
        $valid_positions = array_reverse($valid_positions);
        
        foreach ($valid_positions as $position) {
            if ($replacements_made >= $max_replacements) {
                break;
            }
            
            $keyword_text = $position['text'];
            $offset = $position['offset'];
            $length = $position['length'];
            
            // Create the link HTML (using keyword as title instead of target page title)
            $link_html = sprintf(
                '<a href="%s" title="%s" class="aysa-internal-link">%s</a>',
                esc_url($url),
                esc_attr($keyword_text),
                $keyword_text
            );
            
            // Replace this specific occurrence
            $new_content = substr_replace($new_content, $link_html, $offset, $length);
            $replacements_made++;
            
            aysa_debug_log("[AYSA Internal Linking] REPLACED: Keyword '$keyword' at offset $offset with link");
        }
        
        aysa_debug_log("[AYSA Internal Linking] replace_keyword_with_link: '$keyword' replaced $replacements_made times (entity count: $current_entity_link_count/$max_entity_links)");
        
        return $replacements_made > 0 ? $new_content : false;
    }
    
    /**
     * Enhanced check for existing links in content
     */
    private function has_existing_links($content, $keyword, $target_url) {
        // Method 1: Check if keyword is already inside any link tag (improved regex)
        $escaped_keyword = preg_quote($keyword, '/');
        $link_with_keyword_pattern = '/<a[^>]*>.*?' . $escaped_keyword . '.*?<\/a>/is';
        if (preg_match($link_with_keyword_pattern, $content)) {
            aysa_debug_log("[AYSA Internal Linking] SKIP: Keyword '$keyword' found inside existing link tag");
            return true;
        }
        
        // Method 2: Use DOMDocument for more accurate parsing
        if (function_exists('libxml_use_internal_errors')) {
            libxml_use_internal_errors(true);
            $dom = new DOMDocument();
            $dom->loadHTML('<?xml encoding="UTF-8">' . $content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOERROR | LIBXML_NOWARNING);
            
            $links = $dom->getElementsByTagName('a');
            foreach ($links as $link) {
                $linkText = $link->textContent;
                if (stripos($linkText, $keyword) !== false) {
                    aysa_debug_log("[AYSA Internal Linking] SKIP: Keyword '$keyword' found in existing link text: '$linkText'");
                    return true;
                }
            }
            
            libxml_clear_errors();
        }
        
        // Method 3: Simple string position check as fallback
        $pattern_variations = [
            '/<a[^>]*>[^<]*' . $escaped_keyword . '[^<]*<\/a>/i',
            '/<a[^>]*>' . $escaped_keyword . '<\/a>/i',
            '/href="[^"]*"[^>]*>' . $escaped_keyword . '<\/a>/i'
        ];
        
        foreach ($pattern_variations as $pattern) {
            if (preg_match($pattern, $content)) {
                aysa_debug_log("[AYSA Internal Linking] SKIP: Keyword '$keyword' detected in existing link (pattern match)");
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check if keyword is already linked anywhere in content (simplified check)
     */
    private function keyword_already_linked_anywhere($content, $keyword) {
        // Remove any WordPress shortcodes and other complex content first
        $clean_content = strip_shortcodes($content);
        
        // Create diacritics-aware pattern for the keyword
        $keyword_pattern = $this->create_diacritics_pattern($keyword);
        
        // SIMPLIFIED: Only check for our own internal links (aysa-internal-link class)
        // This prevents blocking when other types of links exist
        $aysa_link_pattern = '/<a[^>]*class="[^"]*aysa-internal-link[^"]*"[^>]*>\s*' . $keyword_pattern . '\s*<\/a>/iu';
        
        if (preg_match($aysa_link_pattern, $clean_content)) {
            aysa_debug_log("[AYSA Internal Linking] SKIP: Keyword '$keyword' already has AYSA internal link");
            return true;
        }
        
        aysa_debug_log("[AYSA Internal Linking] OK: Keyword '$keyword' has no existing AYSA internal links");
        return false;
    }

    
    /**
     * Get current global keyword entity counts (tracks entities where each keyword appears)
     */
    private function get_keyword_global_counts() {
        $counts = get_option('aysa_internal_linking_entity_counts', array());
        return $counts;
    }
    
    /**
     * Update global keyword entity counts
     */
    private function update_keyword_global_counts($counts) {
        update_option('aysa_internal_linking_entity_counts', $counts);
    }
    
    /**
     * Get entities where a keyword has been used (for max_links_per_keyword limit)
     */
    private function get_keyword_entity_usage($keyword) {
        $entity_counts = $this->get_keyword_global_counts();
        return isset($entity_counts[$keyword]) ? $entity_counts[$keyword] : array();
    }
    
    /**
     * Add entity to keyword usage tracking (allows multiple occurrences of same keyword in same entity)
     */
    private function add_keyword_entity_usage($keyword, $entity_id, $entity_type) {
        $entity_counts = $this->get_keyword_global_counts();
        $entity_key = $entity_type . '_' . $entity_id;
        
        if (!isset($entity_counts[$keyword])) {
            $entity_counts[$keyword] = array();
        }
        
        // Always add the entity_key to allow multiple occurrences of same keyword in same entity
        $entity_counts[$keyword][] = $entity_key;
        
        $this->update_keyword_global_counts($entity_counts);
    }
    
    /**
     * Log link replacement for reporting
     */
    private function log_link_replacement($keyword_data, $current_entity) {
        $log_entry = array(
            'timestamp' => current_time('mysql'),
            'keyword' => $keyword_data['keyword'],
            'target_url' => $keyword_data['url'],
            'target_title' => $keyword_data['title'],
            'source_entity_id' => $current_entity['id'],
            'source_entity_type' => $current_entity['type'],
            'source_title' => $current_entity['title'],
            'source_url' => isset($current_entity['url']) ? (string) $current_entity['url'] : '',
            'is_partial' => $keyword_data['is_partial'] ?? false
        );
        
        $log = get_option('aysa_internal_linking_log', array());
        $log[] = $log_entry;
        
        // Keep only last 1000 entries
        if (count($log) > 1000) {
            $log = array_slice($log, -1000);
        }
        
        update_option('aysa_internal_linking_log', $log);
    }

    /**
     * Persist mappings generated by main bulk processor into local report log.
     */
    private function persist_bulk_mappings_to_log($link_mappings) {
        if (!is_array($link_mappings) || empty($link_mappings)) {
            return;
        }

        $log = get_option('aysa_internal_linking_log', array());
        if (!is_array($log)) {
            $log = array();
        }

        foreach ($link_mappings as $mapping) {
            if (!is_array($mapping)) {
                continue;
            }
            $keyword = trim((string) ($mapping['keyword'] ?? ''));
            $target_url = trim((string) ($mapping['target_url'] ?? ''));
            if ($keyword === '' || $target_url === '') {
                continue;
            }

            $log[] = array(
                'timestamp' => (string) ($mapping['timestamp'] ?? current_time('mysql')),
                'keyword' => $keyword,
                'target_url' => $target_url,
                'target_title' => (string) ($mapping['target_page'] ?? ''),
                'source_entity_id' => intval($mapping['source_content_id'] ?? 0),
                'source_entity_type' => sanitize_key((string) ($mapping['source_content_type'] ?? '')),
                'source_title' => (string) ($mapping['source_content_title'] ?? ''),
                'source_url' => (string) ($mapping['source_content_url'] ?? ''),
                'is_partial' => !empty($mapping['is_partial']),
            );
        }

        if (count($log) > 5000) {
            $log = array_slice($log, -5000);
        }

        update_option('aysa_internal_linking_log', $log, false);
    }

    /**
     * Resolve a front-end URL for a logged source entity.
     */
    private function resolve_source_front_url($entity_id, $entity_type, $fallback_url = '') {
        if (!empty($fallback_url) && filter_var($fallback_url, FILTER_VALIDATE_URL)) {
            return $fallback_url;
        }

        $entity_id = (int) $entity_id;
        $entity_type = sanitize_key((string) $entity_type);
        if ($entity_id <= 0) {
            return '';
        }

        if (in_array($entity_type, array('post', 'page', 'product'), true)) {
            $url = get_permalink($entity_id);
            return is_string($url) ? $url : '';
        }

        $taxonomy = '';
        if (in_array($entity_type, array('category', 'categories'), true)) {
            $taxonomy = 'category';
        } elseif (in_array($entity_type, array('product_cat', 'product-category'), true)) {
            $taxonomy = 'product_cat';
        }

        if ($taxonomy !== '') {
            $term_link = get_term_link($entity_id, $taxonomy);
            if (!is_wp_error($term_link) && is_string($term_link)) {
                return $term_link;
            }
        }

        return '';
    }

    /**
     * Rebuild internal linking report log from existing anchors in content when the log is sparse.
     * This recovers visibility after historical cleanup without requiring external APIs.
     */
    private function maybe_rebuild_internal_linking_log($existing_log) {
        if (!is_array($existing_log)) {
            $existing_log = array();
        }

        // If we already have enough entries, avoid expensive scans.
        if (count($existing_log) >= 25) {
            return $existing_log;
        }

        $seen = array();
        foreach ($existing_log as $entry) {
            $sig = md5(
                strtolower(trim((string)($entry['keyword'] ?? ''))) . '|' .
                strtolower(trim((string)($entry['target_url'] ?? ''))) . '|' .
                intval($entry['source_entity_id'] ?? 0) . '|' .
                strtolower((string)($entry['source_entity_type'] ?? ''))
            );
            $seen[$sig] = true;
        }

        $post_types = array('post', 'page');
        if (class_exists('WooCommerce')) {
            $post_types[] = 'product';
        }

        $posts = get_posts(array(
            'numberposts' => 500,
            'post_status' => 'publish',
            'post_type' => $post_types,
            'orderby' => 'modified',
            'order' => 'DESC',
        ));

        $rebuilt_count = 0;
        foreach ($posts as $post) {
            $content = (string)($post->post_content ?? '');
            if ($content === '' || strpos($content, 'aysa-internal-link') === false) {
                continue;
            }

            if (!preg_match_all('/<a\\b[^>]*class=["\\\'][^"\\\']*aysa-internal-link[^"\\\']*["\\\'][^>]*>.*?<\\/a>/isu', $content, $matches)) {
                continue;
            }

            $source_url = get_permalink($post->ID);
            foreach ($matches[0] as $anchor_html) {
                $target_url = $this->extract_html_attribute($anchor_html, 'href');
                if (empty($target_url)) {
                    continue;
                }

                $keyword = $this->extract_html_attribute($anchor_html, 'title');
                if ($keyword === '') {
                    $anchor_text = wp_strip_all_tags($anchor_html);
                    $keyword = trim((string)$anchor_text);
                }
                if ($keyword === '') {
                    continue;
                }

                $target_title = '';
                $target_post_id = url_to_postid($target_url);
                if ($target_post_id > 0) {
                    $target_title = get_the_title($target_post_id);
                }
                if ($target_title === '') {
                    $path = (string) wp_parse_url($target_url, PHP_URL_PATH);
                    $target_title = trim(ucwords(str_replace(array('-', '_', '/'), ' ', $path)));
                }

                $sig = md5(
                    strtolower(trim((string)$keyword)) . '|' .
                    strtolower(trim((string)$target_url)) . '|' .
                    intval($post->ID) . '|' .
                    strtolower((string)$post->post_type)
                );
                if (isset($seen[$sig])) {
                    continue;
                }
                $seen[$sig] = true;

                $existing_log[] = array(
                    'timestamp' => current_time('mysql'),
                    'keyword' => (string)$keyword,
                    'target_url' => (string)$target_url,
                    'target_title' => (string)$target_title,
                    'source_entity_id' => intval($post->ID),
                    'source_entity_type' => sanitize_key((string)$post->post_type),
                    'source_title' => (string)$post->post_title,
                    'source_url' => is_string($source_url) ? $source_url : '',
                    'is_partial' => false,
                );
                $rebuilt_count++;
            }
        }

        if ($rebuilt_count > 0) {
            if (count($existing_log) > 5000) {
                $existing_log = array_slice($existing_log, -5000);
            }
            update_option('aysa_internal_linking_log', $existing_log, false);
            aysa_debug_log('[AYSA Internal Linking] Rebuilt log entries from existing content: ' . $rebuilt_count);
        }

        return $existing_log;
    }

    /**
     * Extract a single HTML attribute value from a tag snippet.
     */
    private function extract_html_attribute($html, $attribute) {
        $attribute = preg_quote((string)$attribute, '/');
        if (preg_match('/\\b' . $attribute . '\\s*=\\s*([\'"])(.*?)\\1/isu', (string)$html, $m)) {
            return trim((string)$m[2]);
        }
        if (preg_match('/\\b' . $attribute . '\\s*=\\s*([^\\s>]+)/isu', (string)$html, $m)) {
            return trim((string)$m[1], "\"' \t\n\r\0\x0B");
        }
        return '';
    }
    
    /**
     * Clear internal linking cache (for debugging)
     */
    public function clear_duplicate_protection_cache() {
        delete_option('aysa_internal_linking_counts');
        delete_option('aysa_internal_linking_log');
        
        aysa_debug_log("[AYSA Internal Linking] Internal linking cache cleared");
        
        return [
            'success' => true, 
            'message' => 'Internal linking cache cleared successfully'
        ];
    }
    
    /**
     * AJAX: Clear duplicate protection cache
     */
    public function ajax_clear_protection_cache() {
        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Insufficient permissions'));
            return;
        }
        
        check_ajax_referer('aysa_internal_linking_nonce', 'nonce');
        
        $result = $this->clear_duplicate_protection_cache();
        
        if ($result['success']) {
            wp_send_json_success($result);
        } else {
            wp_send_json_error($result);
        }
    }
    
    /**
     * AJAX: Preview internal links
     */
    public function ajax_preview_links() {
        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Insufficient permissions'));
            return;
        }
        
        check_ajax_referer('aysa_internal_linking_nonce', 'nonce');
        
        $entity_id = isset($_POST['entity_id']) ? intval(wp_unslash($_POST['entity_id'])) : 0;
        $entity_type = isset($_POST['entity_type']) ? sanitize_text_field(wp_unslash($_POST['entity_type'])) : '';
        $content = isset($_POST['content']) ? wp_kses_post(wp_unslash($_POST['content'])) : '';
        
        // Get keyword associations
        $keyword_associations = $this->get_keyword_associations();
        
        // Create mock entity
        $current_entity = array(
            'id' => $entity_id,
            'type' => $entity_type,
            'title' => 'Preview Entity',
            'url' => '#'
        );
        
        // Process content (in preview mode)
        $link_result = $this->insert_internal_links($content, $keyword_associations, $current_entity, true);
        
        // Handle new return format
        if (is_array($link_result)) {
            $processed_content = $link_result['content'];
            $links_added = $link_result['links_added'];
            $skipped_stats = $link_result['skipped_stats'];
        } else {
            // Backward compatibility
            $processed_content = $link_result;
            $links_added = substr_count($processed_content, 'aysa-internal-link');
            $skipped_stats = [];
        }
        
        wp_send_json_success(array(
            'original_content' => $content,
            'processed_content' => $processed_content,
            'links_found' => $links_added,
            'links_added' => $links_added,
            'skipped_stats' => $skipped_stats
        ));
    }
    
    /**
     * AJAX: Process all content for internal links
     */
    public function ajax_process_links() {
        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Insufficient permissions'));
            return;
        }
        
        check_ajax_referer('aysa_internal_linking_nonce', 'nonce');
        
        // Keep historical report data between runs.
        // Full cleanup is handled by explicit user actions (Clear Cache / Delete All Links).
        // Here we only clear transient keyword cache for fresh association reads.
        delete_transient('aysa_internal_linking_keywords');
        
        // NEW LOGIC: Use MAIN keywords from API
        $result = $this->process_all_content_with_main_keywords();
        
        if ($result['success']) {
            wp_send_json_success($result);
        } else {
            wp_send_json_error($result);
        }
    }
    
    /**
     * Process ALL content and add internal links based on MAIN keywords
     * This is the CORRECT logic as requested by user
     */
    public function process_all_content_with_main_keywords($preview_mode = false, $force_run = false) {
        // `enabled` controls legacy runtime injection. Agent8 Technical SEO may
        // force a read-only preview, but real processing stays behind the flag.
        $allow_forced_preview = (bool) $preview_mode && (bool) $force_run;
        if (!$allow_forced_preview && !$this->is_enabled()) {
            return ['success' => false, 'message' => 'Internal linking is disabled'];
        }
        
        // TIMEOUT PROTECTION: Set start time and max execution time
        $start_time = microtime(true);
        $max_execution_time = $preview_mode ? 45 : 120; // 45 seconds for preview, 2 minutes for full processing
        
        // Get MAIN keywords from local WordPress data only.
        $project_id = get_option('aysa_project_id', 0);
        $associations = $this->get_local_internal_linking_associations($project_id);
        aysa_debug_log('[AYSA Internal Linking] Local associations count: ' . count($associations));
        
        // Initialize global keyword counts for tracking limits
        $keyword_global_counts = $this->get_keyword_global_counts();
        if ($preview_mode) {
            aysa_debug_log('[AYSA Internal Linking] Preview mode: using temporary keyword counts (will not save to cache)');
        }
        
        if (!$associations || !is_array($associations)) {
            return ['success' => false, 'message' => 'No local keyword associations found'];
        }
        
        // Extract MAIN keywords first (isMain = 1), then SECONDARY as fallback
        $main_keywords = [];
        $secondary_keywords = [];
        
        foreach ($associations as $association) {
            if (!empty($association['keyword'])) {
                
                // Get or generate link for this keyword
                $link = $this->get_or_generate_link($association);
                
                if ($link) {
                    $keyword_data = [
                        'keyword' => $association['keyword'],
                        'link' => $link,
                        'target_page' => $association['value'],
                        'target_type' => $association['associatedOn'],
                        'has_generated_link' => empty($association['link']),
                        'is_main' => isset($association['isMain']) && $association['isMain'] == 1,
                        'secondary_keyword' => $association['secondKeyword'] ?? null
                    ];
                    
                    // Prioritize MAIN keywords
                    if (isset($association['isMain']) && $association['isMain'] == 1) {
                        $main_keywords[] = $keyword_data;
                    } else {
                        $secondary_keywords[] = $keyword_data;
                    }
                    
                    // Also process SECONDARY keyword if present
                    if (!empty($association['secondKeyword'])) {
                        $secondary_keyword_data = [
                            'keyword' => $association['secondKeyword'],
                            'link' => $link, // Same link as main keyword
                            'target_page' => $association['value'],
                            'target_type' => $association['associatedOn'],
                            'has_generated_link' => empty($association['link']),
                            'is_main' => false, // Secondary keywords are never main
                            'is_secondary' => true
                        ];
                        $secondary_keywords[] = $secondary_keyword_data;
                    }
                }
            }
        }
        
        // Combine: MAIN keywords first, then SECONDARY
        $all_keywords = array_merge($main_keywords, $secondary_keywords);
        
        // Add partial matches if enabled
        if ($this->settings['matching_type'] === 'partial' || $this->settings['matching_type'] === 'both') {
            $partial_keywords = [];
            
            foreach ($all_keywords as $keyword_data) {
                $partial_matches = $this->generate_partial_matches($keyword_data['keyword']);
                
                foreach ($partial_matches as $partial) {
                    $partial_data = $keyword_data;
                    $partial_data['keyword'] = $partial;
                    $partial_data['is_partial'] = true;
                    $partial_keywords[] = $partial_data;
                }
            }
            
            // Add partial keywords to the main list (main/secondary keywords have priority)
            $all_keywords = array_merge($all_keywords, $partial_keywords);
            
            aysa_debug_log('[AYSA Internal Linking] Added ' . count($partial_keywords) . ' partial keyword variations');
        }

        // Strong preference order for matching in a source page:
        // 1) main keywords, 2) secondary keywords, 3) exact before partial variations.
        usort($all_keywords, function($a, $b) {
            $scoreA = 0;
            $scoreB = 0;

            if (!empty($a['is_main'])) { $scoreA += 30; }
            if (!empty($b['is_main'])) { $scoreB += 30; }
            if (!empty($a['is_secondary'])) { $scoreA += 20; }
            if (!empty($b['is_secondary'])) { $scoreB += 20; }
            if (empty($a['is_partial'])) { $scoreA += 10; }
            if (empty($b['is_partial'])) { $scoreB += 10; }

            if ($scoreA !== $scoreB) {
                return $scoreB - $scoreA;
            }

            // Prefer longer phrases when scores are equal (usually more specific anchors).
            return mb_strlen((string)($b['keyword'] ?? ''), 'UTF-8') - mb_strlen((string)($a['keyword'] ?? ''), 'UTF-8');
        });
        
        aysa_debug_log('[AYSA Internal Linking] Keywords extracted - Main: ' . count($main_keywords) . ', Secondary: ' . count($secondary_keywords) . ', Total (with partials): ' . count($all_keywords));
        aysa_debug_log('[AYSA Internal Linking] Sample keywords: ' . aysa_debug_export(array_slice($all_keywords, 0, 5), true));
        
        if (empty($all_keywords)) {
            return ['success' => false, 'message' => 'No keywords found (main or secondary)', 'debug' => 'Total associations: ' . count($associations)];
        }
        
        // Get all content to scan (posts, pages, products, categories)
        $all_content = $this->get_all_content_for_scanning();
        
        aysa_debug_log('[AYSA Internal Linking] Content items found: ' . count($all_content));
        aysa_debug_log('[AYSA Internal Linking] Sample content: ' . aysa_debug_export(array_slice($all_content, 0, 2), true));
        
        $results = [
            'success' => true,
            'main_keywords_found' => count($main_keywords),
            'secondary_keywords_found' => count($secondary_keywords),
            'total_keywords_found' => count($all_keywords),
            'content_items_scanned' => count($all_content),
            'links_added' => 0,
            'links_skipped_existing' => 0,
            'links_skipped_limits' => 0,
            'links_skipped_self' => 0,
            'links_skipped_same_target' => 0,
            'links_skipped_same_keyword' => 0,
            'links_removed_inactive_targets' => 0,
            'processed_items' => [],
            'keywords_processed' => [],
            'summary' => '',
            'details' => [],
            'link_mappings' => [], // NEW: Exact keyword -> target mappings
            'timestamp' => current_time('mysql')
        ];
        
        // Prepare keywords list for display (both main and secondary)
        foreach ($all_keywords as $kw) {
            $results['keywords_processed'][] = [
                'keyword' => $kw['keyword'],
                'target_page' => $kw['target_page'],
                'target_type' => $kw['target_type'],
                'link' => $kw['link'],
                'is_main' => $kw['is_main'] ?? false,
                'is_secondary' => $kw['is_secondary'] ?? false
            ];
        }
        
        // TIMEOUT OPTIMIZATION: Limit keywords processing in preview mode
        if ($preview_mode && count($all_keywords) > 100) {
            $all_keywords = array_slice($all_keywords, 0, 100);
            aysa_debug_log('[AYSA Internal Linking] Preview mode: Limited to first 100 keywords to prevent timeout');
        }
        
        // Process each piece of content
        foreach ($all_content as $content_item) {
            // TIMEOUT CHECK: Stop processing if we're running out of time
            $current_time = microtime(true);
            $elapsed_time = $current_time - $start_time;
            
            if ($elapsed_time > $max_execution_time) {
                aysa_debug_log('[AYSA Internal Linking] TIMEOUT: Stopping processing after ' . round($elapsed_time, 2) . ' seconds');
                $results['timeout_reached'] = true;
                $results['processing_stopped_early'] = true;
                $results['summary'] = 'Processing stopped due to timeout after ' . round($elapsed_time, 2) . ' seconds. Processed ' . count($results['processed_items']) . ' items.';
                break;
            }
            
            // Check if this is Elementor content - skip automatic processing but track it
            if (!empty($content_item['is_elementor']) && $content_item['is_elementor']) {
                aysa_debug_log("[AYSA Internal Linking] 🎨 SKIPPING ELEMENTOR CONTENT: {$content_item['type']} ID {$content_item['id']} - '{$content_item['title']}'");
                
                // Add to processed items for reporting, but mark as skipped
                $results['processed_items'][] = [
                    'item_id' => $content_item['id'],
                    'item_type' => $content_item['type'],
                    'item_title' => $content_item['title'],
                    'links_added' => 0,
                    'status' => 'skipped_elementor',
                    'message' => 'Elementor content - manual linking required'
                ];
                
                continue; // Skip to next content item
            }
            
            $links_added_to_item = 0;
            $original_content = $content_item['content'];
            $updated_content = $original_content;
            $inactive_cleanup = $this->remove_inactive_aysa_links_from_content($updated_content);
            if ($inactive_cleanup['removed'] > 0) {
                $updated_content = $inactive_cleanup['content'];
                $results['links_removed_inactive_targets'] += intval($inactive_cleanup['removed']);
            }
            $linked_targets_in_item = $this->extract_link_targets_from_content($updated_content);
            $linked_keywords_in_item = $this->extract_linked_keywords_from_content($updated_content);
            $max_links_for_item = $this->get_dynamic_entity_link_limit($content_item, $updated_content);
            
            // TIMEOUT PROTECTION: Skip empty content
            if (empty(trim($original_content))) {
                continue;
            }
            
            // Check each keyword (main and secondary, in priority order)
            foreach ($all_keywords as $keyword_data) {
                $keyword = $keyword_data['keyword'];
                $link = $keyword_data['link'];
                $target_key = $this->normalize_url_for_matching($link);
                $keyword_key = $this->normalize_keyword_for_matching($keyword);

                // ENTITY LIMIT CHECK: Stop if we've reached the maximum links per entity (using hard-capped value)
                $max_links_per_entity = $max_links_for_item;
                
                if ($links_added_to_item >= $max_links_per_entity) {
                    aysa_debug_log("[AYSA Internal Linking] SKIP: Entity link limit reached for content item ({$max_links_per_entity} links max)");
                    $results['links_skipped_limits']++;
                    break; // Stop processing more keywords for this content item
                }
                
                // Check keyword usage limit per entity (max_links_per_keyword = max occurrences per keyword in same entity)
                $keyword_entity_usage = $this->get_keyword_entity_usage($keyword);
                $current_entity_key = $content_item['type'] . '_' . $content_item['id'];
                
                // Count how many times this keyword already appears in current entity
                $keyword_count_in_entity = 0;
                if (in_array($current_entity_key, $keyword_entity_usage)) {
                    // If keyword already exists in this entity, count occurrences
                    $keyword_count_in_entity = array_count_values($keyword_entity_usage)[$current_entity_key] ?? 0;
                }
                
                // Skip if keyword has reached its limit in this specific entity
                if ($keyword_count_in_entity >= $this->settings['max_links_per_keyword']) {
                    aysa_debug_log("[AYSA Internal Linking] SKIP: Keyword '$keyword' already used {$keyword_count_in_entity} times in this entity (max: {$this->settings['max_links_per_keyword']})");
                    continue;
                }
                
                // Skip if linking to same page (avoid self-linking)
                if ($this->is_same_page($content_item, $link, $keyword_data['target_type'])) {
                    continue;
                }

                // Rule: only one link per target page in a given source content item.
                if ($target_key !== '' && isset($linked_targets_in_item[$target_key])) {
                    $results['links_skipped_same_target']++;
                    aysa_debug_log("[AYSA Internal Linking] SKIP: Target already linked in source item for '{$keyword}' -> '{$link}'");
                    continue;
                }

                // Rule: do not reuse same keyword combination in the same source content item.
                if ($keyword_key !== '' && isset($linked_keywords_in_item[$keyword_key])) {
                    $results['links_skipped_same_keyword']++;
                    aysa_debug_log("[AYSA Internal Linking] SKIP: Keyword already linked in source item for '{$keyword}'");
                    continue;
                }
                
                // ENHANCED DUPLICATE PROTECTION: Check if keyword already has links in current content
                if ($this->keyword_already_linked_anywhere($updated_content, $keyword)) {
                    $results['links_skipped_existing']++;
                    aysa_debug_log("[AYSA Internal Linking] SKIP: Keyword '$keyword' already has links in content (main process)");
                    continue;
                }
                
                // Check if keyword exists in content using new diacritics-aware method
                if ($this->keyword_matches_content($updated_content, $keyword)) {
                    aysa_debug_log("[AYSA Internal Linking] MATCH FOUND for keyword '$keyword'");
                    
                    // IMPORTANT: Check if keyword is in excluded HTML tags BEFORE processing
                    if ($this->is_keyword_in_excluded_tags($updated_content, $keyword)) {
                        aysa_debug_log("[AYSA Internal Linking] SKIP: Keyword '$keyword' found in excluded HTML tags (H1-H6, A, alt, title, href)");
                        continue; // Skip this keyword but continue with others
                    }
                    
                    // NEW: Use valid positions and enforce entity limit to avoid linking in excluded contexts (e.g., headings)
                    $valid_positions = $this->get_valid_keyword_positions($updated_content, $keyword);
                    if (empty($valid_positions)) {
                        aysa_debug_log("[AYSA Internal Linking] SKIP: No valid positions for '$keyword' in this content item (all occurrences in excluded contexts)");
                        continue;
                    }
                    
                    // Determine how many replacements we may perform using dynamic limit for current source item
                    $max_links_per_entity = $max_links_for_item;
                    $remaining_slots = max(0, $max_links_per_entity - $links_added_to_item);
                    if ($remaining_slots <= 0) {
                        $results['links_skipped_limits']++;
                        aysa_debug_log('[AYSA Internal Linking] SKIP: No remaining slots for entity (limit ' . $max_links_per_entity . ')');
                        break; // stop processing more keywords for this item
                    }
                    $max_per_keyword = intval($this->settings['max_links_per_keyword'] ?? 1);
                    // Keep insertion conservative: one insertion per keyword pass.
                    $max_replacements = min(1, $max_per_keyword, $remaining_slots, count($valid_positions));
                    
                    // Process from the end to preserve offsets
                    $valid_positions = array_reverse($valid_positions);
                    $replacements_made = 0;
                    $new_content = $updated_content;
                    foreach ($valid_positions as $pos) {
                        if ($replacements_made >= $max_replacements) { break; }
                        $kw_text = $pos['text'];
                        $offset = $pos['offset'];
                        $length = $pos['length'];
                        $replacement = '<a href="' . esc_url($link) . '" class="aysa-internal-link" title="' . esc_attr($keyword) . '">' . $kw_text . '</a>';
                        $new_content = substr_replace($new_content, $replacement, $offset, $length);
                        $replacements_made++;
                    }
                    aysa_debug_log("[AYSA Internal Linking] Keyword '$keyword' replaced $replacements_made times (allowed this pass: $max_replacements)");
                    
                    if ($replacements_made > 0) {
                        // Update content and counters
                        $updated_content = $new_content;
                        $actual_replacements = $replacements_made;
                        $links_added_to_item += $actual_replacements;
                        $results['links_added'] += $actual_replacements;
                        if ($target_key !== '') {
                            $linked_targets_in_item[$target_key] = true;
                        }
                        if ($keyword_key !== '') {
                            $linked_keywords_in_item[$keyword_key] = true;
                        }
                        
                        // Add this entity to keyword usage tracking (only if not in preview mode)
                        if (!$preview_mode) {
                            $this->add_keyword_entity_usage($keyword, $content_item['id'], $content_item['type']);
                        }
                        
                        // Store detailed link mapping for each replacement made
                        for ($i = 0; $i < $actual_replacements; $i++) {
                            $results['link_mappings'][] = [
                                'keyword' => $keyword,
                                'target_page' => $keyword_data['target_page'],
                                'target_type' => $keyword_data['target_type'],
                                'target_url' => $link,
                                'source_content_type' => ucfirst($content_item['type']),
                                'source_content_id' => $content_item['id'],
                                'source_content_title' => $content_item['title'],
                                'source_content_url' => $content_item['url'],
                                'has_generated_link' => $keyword_data['has_generated_link'] ?? false,
                                'is_main' => $keyword_data['is_main'] ?? false,
                                'is_secondary' => $keyword_data['is_secondary'] ?? false,
                                'replacement_number' => $i + 1,
                                'total_replacements' => $actual_replacements,
                                'timestamp' => current_time('mysql')
                            ];
                        }
                        
                        aysa_debug_log("[AYSA Internal Linking] SUCCESS: Link(s) created in main process for '$keyword' -> '$link'. Content updated to prevent duplicates.");
                        
                        // PROTECTION: mark that this keyword now has a link
                        if ($this->keyword_already_linked_anywhere($updated_content, $keyword)) {
                            aysa_debug_log("[AYSA Internal Linking] PROTECTION: Keyword '$keyword' now has links, will be skipped in future iterations");
                        }
                        
                        // MALFORMED LINK DETECTION
                        if (preg_match('/<a[^>]*>[^<]*<a[^>]*>/i', $updated_content) || preg_match('/<\/a>[^<]*<\/a>/i', $updated_content)) {
                            aysa_debug_log("[AYSA Internal Linking] WARNING: Detected potential malformed/nested links after processing '$keyword'");
                        }
                        
                        // Stop if entity limit reached
                        if ($links_added_to_item >= $max_links_per_entity) {
                            aysa_debug_log("[AYSA Internal Linking] Entity limit reached for this content item ($links_added_to_item/$max_links_per_entity). Stopping further keywords.");
                            $results['links_skipped_limits']++;
                            break; // break out of keywords foreach
                        }
                    }
                }
            }
            
            // Update content if links were added
            if ($links_added_to_item > 0 || $updated_content !== $original_content) {
                $this->update_content_in_database($content_item, $updated_content, $preview_mode);
                
                $results['processed_items'][] = [
                    'type' => ucfirst($content_item['type']),
                    'id' => $content_item['id'],
                    'title' => $content_item['title'],
                    'links_added' => $links_added_to_item,
                    'inactive_links_removed' => $inactive_cleanup['removed'] ?? 0,
                    'url' => $content_item['url']
                ];
            }
        }
        
        // Create summary message with skip information
        $skip_summary = '';
        $total_skipped = $results['links_skipped_existing'] + $results['links_skipped_limits'] + $results['links_skipped_self'] + $results['links_skipped_same_target'] + $results['links_skipped_same_keyword'];
        
        if ($total_skipped > 0) {
            $skip_details = [];
            if ($results['links_skipped_existing'] > 0) {
                $skip_details[] = "{$results['links_skipped_existing']} already linked";
            }
            if ($results['links_skipped_limits'] > 0) {
                $skip_details[] = "{$results['links_skipped_limits']} limit reached";
            }
            if ($results['links_skipped_self'] > 0) {
                $skip_details[] = "{$results['links_skipped_self']} self-linking";
            }
            if ($results['links_skipped_same_target'] > 0) {
                $skip_details[] = "{$results['links_skipped_same_target']} target already linked in source";
            }
            if ($results['links_skipped_same_keyword'] > 0) {
                $skip_details[] = "{$results['links_skipped_same_keyword']} keyword already linked in source";
            }
            $skip_summary = " ($total_skipped skipped: " . implode(', ', $skip_details) . ")";
        }

        if ($results['links_removed_inactive_targets'] > 0) {
            $skip_summary .= " [cleaned {$results['links_removed_inactive_targets']} inactive target links]";
        }
        
        // Count Elementor pages that were skipped
        $elementor_skipped = 0;
        foreach ($results['processed_items'] as $item) {
            if (isset($item['status']) && $item['status'] === 'skipped_elementor') {
                $elementor_skipped++;
            }
        }
        
        if ($results['links_added'] > 0) {
            $elementor_note = $elementor_skipped > 0 ? " Note: {$elementor_skipped} Elementor pages were skipped and require manual linking." : "";
            $results['summary'] = sprintf(
                'Successfully processed %d keywords (%d main, %d secondary) across %d content items and added %d internal links%s.%s',
                $results['total_keywords_found'],
                $results['main_keywords_found'],
                $results['secondary_keywords_found'],
                $results['content_items_scanned'],
                $results['links_added'],
                $skip_summary,
                $elementor_note
            );
        } else {
            $elementor_note = $elementor_skipped > 0 ? " Note: {$elementor_skipped} Elementor pages were skipped and require manual linking." : "";
            $results['summary'] = sprintf(
                'Processed %d keywords (%d main, %d secondary) across %d content items, but no new links were added%s.%s',
                $results['total_keywords_found'],
                $results['main_keywords_found'],
                $results['secondary_keywords_found'],
                $results['content_items_scanned'],
                $skip_summary,
                $elementor_note
            );
        }
        
        // Add Elementor statistics to results
        $results['elementor_pages_skipped'] = $elementor_skipped;
        
        // Calculate final execution time
        $final_time = microtime(true);
        $total_execution_time = round($final_time - $start_time, 2);
        
        // Add execution time info to results
        $results['execution_time'] = $total_execution_time;
        $results['max_execution_time'] = $max_execution_time;
        
        // Update summary with execution time if not already set by timeout
        if (!isset($results['timeout_reached'])) {
            $current_summary = $results['summary'];
            $results['summary'] = $current_summary . " (Completed in {$total_execution_time}s)";
        }

        // Persist generated mappings for report view in local WordPress DB.
        if (!$preview_mode && !empty($results['link_mappings'])) {
            $this->persist_bulk_mappings_to_log($results['link_mappings']);
        }
        
        // Note: Entity usage tracking is updated individually for each keyword during processing
        if ($preview_mode) {
            aysa_debug_log('[AYSA Internal Linking] Preview mode: entity usage tracking was skipped');
        } else {
            aysa_debug_log('[AYSA Internal Linking] Entity usage tracking updated for processed keywords');
        }
        
        aysa_debug_log('[AYSA Internal Linking] FINAL EXECUTION TIME: ' . $total_execution_time . ' seconds');
        
        return $results;
    }
    
    /**
     * Get all content for scanning (posts, pages, products, categories)
     */
    private function get_all_content_for_scanning() {
        $all_content = [];
        
        // Build post types array based on settings
        $post_types = [];
        if ($this->settings['process_posts']) {
            $post_types[] = 'post';
        }
        if ($this->settings['process_pages']) {
            $post_types[] = 'page';
        }
        if ($this->settings['process_products'] && class_exists('WooCommerce')) {
            $post_types[] = 'product';
        }
        
        aysa_debug_log('[AYSA Internal Linking] Enabled post types: ' . implode(', ', $post_types));
        
        // Get posts only if we have enabled post types
        if (!empty($post_types)) {
            // Fetch per type to guarantee coverage and true priority for posts.
            $per_type_limits = $this->is_preview_mode()
                ? array('post' => 40, 'page' => 20, 'product' => 20)
                : array('post' => 400, 'page' => 200, 'product' => 200);

            $post_pool = array();
            foreach ($post_types as $single_type) {
                $type_limit = isset($per_type_limits[$single_type]) ? intval($per_type_limits[$single_type]) : 100;
                $type_items = get_posts([
                    'numberposts' => $type_limit,
                    'post_status' => 'publish',
                    'post_type' => $single_type,
                    'orderby' => 'modified',
                    'order' => 'DESC'
                ]);
                $post_pool = array_merge($post_pool, $type_items);
            }

            foreach ($post_pool as $post) {
                // Skip homepage - we don't want to add internal links to homepage
                if ($this->is_homepage($post->ID)) {
                    aysa_debug_log("[AYSA Internal Linking] SKIPPING HOMEPAGE: Post ID {$post->ID} - '{$post->post_title}'");
                    continue;
                }
                
                // Get URL and check if it should be excluded
                $post_url = get_permalink($post->ID);
                if ($this->is_url_excluded($post_url)) {
                    aysa_debug_log("[AYSA Internal Linking] SKIPPING EXCLUDED URL: Post ID {$post->ID} - '{$post->post_title}' - URL: {$post_url}");
                    continue;
                }
                
                // Check if this is an Elementor page/post
                $content = $this->get_effective_content($post);
                
                $all_content[] = [
                    'type' => $post->post_type,
                    'id' => $post->ID,
                    'title' => $post->post_title,
                    'content' => $content,
                    'url' => $post_url,
                    'is_elementor' => $this->is_elementor_content($post->ID)
                ];
            }
            
            aysa_debug_log('[AYSA Internal Linking] Found ' . count($post_pool) . ' posts/pages/products (per-type pooled)');
        } else {
            aysa_debug_log('[AYSA Internal Linking] No post types enabled - skipping posts/pages/products');
        }
        
        // Get categories and terms only if enabled
        if ($this->settings['process_categories']) {
            $taxonomies = ['category', 'post_tag'];
            if ($this->settings['process_products'] && class_exists('WooCommerce')) {
                $taxonomies[] = 'product_cat';
            }
            
            $terms = get_terms([
                'taxonomy' => $taxonomies,
                'hide_empty' => false
            ]);
            
            foreach ($terms as $term) {
                $term_url = get_term_link($term);
                
                // Check if URL should be excluded
                if (is_wp_error($term_url) || $this->is_url_excluded($term_url)) {
                    aysa_debug_log("[AYSA Internal Linking] SKIPPING EXCLUDED TERM: Term ID {$term->term_id} - '{$term->name}' - URL: " . (is_wp_error($term_url) ? 'ERROR' : $term_url));
                    continue;
                }
                
                $all_content[] = [
                    'type' => 'term',
                    'id' => $term->term_id,
                    'title' => $term->name,
                    'content' => $term->description,
                    'taxonomy' => $term->taxonomy,
                    'url' => $term_url
                ];
            }
            
            aysa_debug_log('[AYSA Internal Linking] Found ' . count($terms) . ' categories/terms');
        } else {
            aysa_debug_log('[AYSA Internal Linking] Categories processing disabled - skipping terms');
        }
        
        aysa_debug_log('[AYSA Internal Linking] Total content items for scanning: ' . count($all_content));
        
        // DEBUG: Log first few items to see what's being processed
        foreach (array_slice($all_content, 0, 5) as $index => $item) {
            $url = $item['url'] ?? 'NO URL';
            $title = $item['title'] ?? 'NO TITLE';
            $type = $item['type'] ?? 'NO TYPE';
            aysa_debug_log("[AYSA Internal Linking] Content item $index: $type - '$title' - $url");
            
            // Test if this specific URL would be excluded
            if (isset($item['url'])) {
                $would_be_excluded = $this->is_url_excluded($item['url']);
                aysa_debug_log("[AYSA Internal Linking] URL exclusion test for '{$item['url']}': " . ($would_be_excluded ? 'EXCLUDED' : 'ALLOWED'));
            }
        }
        
        // TIMEOUT PROTECTION: Log if we're processing a large number of items
        if (count($all_content) > 100) {
            aysa_debug_log('[AYSA Internal Linking] WARNING: Processing ' . count($all_content) . ' items - this may take time');
        }

        // Processing priority required by product flow:
        // posts/articles first, then pages, then products, then terms.
        usort($all_content, function($a, $b) {
            $priority_a = $this->get_source_priority_for_internal_linking($a['type'] ?? '');
            $priority_b = $this->get_source_priority_for_internal_linking($b['type'] ?? '');

            if ($priority_a !== $priority_b) {
                return $priority_a - $priority_b;
            }

            // Secondary order: recently modified/published content first.
            return 0;
        });
        
        return $all_content;
    }

    /**
     * Source processing priority for internal linking.
     * Lower value = higher priority.
     */
    private function get_source_priority_for_internal_linking($type) {
        $type = strtolower((string) $type);
        switch ($type) {
            case 'post':
                return 1;
            case 'page':
                return 2;
            case 'product':
                return 3;
            case 'term':
                return 4;
            default:
                return 9;
        }
    }

    public function process_site_audit_scoped_targets($package, $preview_mode = false) {
        $package = is_array($package) ? $package : array();
        $candidates = isset($package['candidates']) && is_array($package['candidates']) ? $package['candidates'] : array();
        $results = array();
        $applied = 0;
        $skipped = 0;
        $failed = 0;
        $manual = 0;

        foreach ($candidates as $candidate) {
            if (!is_array($candidate)) {
                continue;
            }
            $target_url = esc_url_raw((string) ($candidate['url'] ?? $candidate['target_url'] ?? ''));
            $anchor = sanitize_text_field((string) ($candidate['suggested_anchor_text'] ?? $candidate['anchor_text'] ?? ''));
            $sources = isset($candidate['suggested_source_pages']) && is_array($candidate['suggested_source_pages']) ? $candidate['suggested_source_pages'] : array();

            if (empty($candidate['safe_to_apply'])) {
                $manual++;
                $results[] = array(
                    'target_url' => $target_url,
                    'status' => 'manual_needed',
                    'reason' => sanitize_text_field((string) ($candidate['reason'] ?? 'candidate_not_safe_for_scoped_auto_apply')),
                );
                continue;
            }
            if ($target_url === '' || $anchor === '' || empty($sources)) {
                $manual++;
                $results[] = array(
                    'target_url' => $target_url,
                    'status' => 'manual_needed',
                    'reason' => 'target_anchor_or_source_missing',
                );
                continue;
            }

            $target_post_id = url_to_postid($target_url);
            $candidate_applied = false;
            foreach ($sources as $source) {
                $source_url = is_array($source) ? esc_url_raw((string) ($source['url'] ?? $source['source_url'] ?? '')) : esc_url_raw((string) $source);
                $source_anchor = is_array($source) ? sanitize_text_field((string) ($source['anchor_text'] ?? $anchor)) : $anchor;
                if ($source_url === '' || $source_anchor === '') {
                    $skipped++;
                    $results[] = array('target_url' => $target_url, 'source_url' => $source_url, 'status' => 'skipped', 'reason' => 'source_url_or_anchor_missing');
                    continue;
                }

                $source_post_id = url_to_postid($source_url);
                if ($source_post_id <= 0) {
                    $skipped++;
                    $results[] = array('target_url' => $target_url, 'source_url' => $source_url, 'status' => 'skipped', 'reason' => 'source_post_mapping_missing');
                    continue;
                }
                if ($target_post_id > 0 && $source_post_id === $target_post_id) {
                    $skipped++;
                    $results[] = array('target_url' => $target_url, 'source_url' => $source_url, 'source_post_id' => $source_post_id, 'status' => 'skipped', 'reason' => 'self_link_blocked');
                    continue;
                }

                $post = get_post($source_post_id);
                if (!$post || !isset($post->post_content)) {
                    $skipped++;
                    $results[] = array('target_url' => $target_url, 'source_url' => $source_url, 'source_post_id' => $source_post_id, 'status' => 'skipped', 'reason' => 'source_content_unavailable');
                    continue;
                }

                $current_entity = array('id' => $source_post_id, 'type' => 'post');
                $keyword_data = array(
                    'keyword' => $source_anchor,
                    'url' => $target_url,
                    'title' => $source_anchor,
                    'entity_id' => $target_post_id,
                    'entity_type' => 'post',
                );
                $link_result = $this->insert_internal_links((string) $post->post_content, array($keyword_data), $current_entity, $preview_mode);
                $links_added = is_array($link_result) ? intval($link_result['links_added'] ?? 0) : 0;
                $new_content = is_array($link_result) ? (string) ($link_result['content'] ?? '') : '';
                if ($links_added <= 0 || $new_content === '' || $new_content === (string) $post->post_content) {
                    $skipped++;
                    $results[] = array('target_url' => $target_url, 'source_url' => $source_url, 'source_post_id' => $source_post_id, 'status' => 'skipped', 'reason' => 'anchor_not_found_or_already_linked');
                    continue;
                }

                if (!$preview_mode) {
                    $updated = wp_update_post(array('ID' => $source_post_id, 'post_content' => $new_content), true);
                    if (is_wp_error($updated)) {
                        $failed++;
                        $results[] = array('target_url' => $target_url, 'source_url' => $source_url, 'source_post_id' => $source_post_id, 'status' => 'failed', 'reason' => $updated->get_error_message());
                        continue;
                    }
                }
                $applied++;
                $candidate_applied = true;
                $results[] = array('target_url' => $target_url, 'source_url' => $source_url, 'source_post_id' => $source_post_id, 'status' => $preview_mode ? 'preview_ready' : 'applied', 'links_added' => $links_added);
                break;
            }

            if (!$candidate_applied && !empty($candidate['safe_to_apply'])) {
                // Per-source skipped rows already explain why this candidate was not changed.
            }
        }

        $manual_results = 0;
        foreach ($results as $result_row) {
            if (is_array($result_row) && sanitize_key((string) ($result_row['status'] ?? '')) === 'manual_needed') {
                $manual_results++;
            }
        }

        return array(
            'success' => $failed === 0,
            'shared_executor' => 'Aysa_Internal_Linking::process_site_audit_scoped_targets',
            'global_mode_used' => false,
            'package_id' => sanitize_text_field((string) ($package['package_id'] ?? '')),
            'total_candidates' => intval($package['total_candidates'] ?? count($candidates)),
            'safe_auto_apply_count' => intval($package['safe_auto_apply_count'] ?? 0),
            'manual_needed_count' => $manual_results,
            'manual_review_needed_count' => $manual_results,
            'package_manual_review_needed_count' => intval($package['manual_review_needed_count'] ?? 0),
            'no_valid_source_count' => intval($package['no_valid_source_count'] ?? 0),
            'applied_count' => $applied,
            'skipped_count' => $skipped,
            'failed_count' => $failed,
            'results' => $results,
            'reason' => $failed > 0 ? 'scoped_internal_linking_failed_for_some_rows' : '',
        );
    }
    
    /**
     * Check if we're in preview mode
     */
    private function is_preview_mode() {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reviewed callback inventory: this request read is downstream of the handler nonce and capability guard, a shared guarded delegate, or the authenticated WordPress core save/query flow.
        return defined('DOING_AJAX') && isset($_POST['action']) && $_POST['action'] === 'aysa_internal_linking_preview';
    }

    /**
     * Normalize URL for target matching in internal-linking rules.
     */
    private function normalize_url_for_matching($url) {
        $url = trim((string) $url);
        if ($url === '') {
            return '';
        }

        $parsed = wp_parse_url($url);
        if (!is_array($parsed) || empty($parsed['host'])) {
            return rtrim($url, '/');
        }

        $scheme = isset($parsed['scheme']) ? strtolower($parsed['scheme']) : 'https';
        $host = strtolower($parsed['host']);
        $path = isset($parsed['path']) ? rtrim($parsed['path'], '/') : '';

        if ($path === '') {
            $path = '/';
        }

        return $scheme . '://' . $host . $path;
    }

    /**
     * Extract currently linked targets from content (for one-target-per-source enforcement).
     */
    private function extract_link_targets_from_content($content) {
        $targets = array();
        $content = (string) $content;
        if ($content === '') {
            return $targets;
        }

        if (preg_match_all('/<a[^>]+href=(["\'])(.*?)\\1[^>]*>/iu', $content, $matches)) {
            foreach ($matches[2] as $href) {
                $normalized = $this->normalize_url_for_matching($href);
                if ($normalized !== '') {
                    $targets[$normalized] = true;
                }
            }
        }

        return $targets;
    }

    /**
     * Normalize keyword for duplicate checks inside one source content item.
     */
    private function normalize_keyword_for_matching($keyword) {
        $keyword = $this->normalize_text((string) $keyword, false);
        $keyword = preg_replace('/\s+/u', ' ', trim($keyword));
        return (string) $keyword;
    }

    /**
     * Extract already linked AYSA keywords from content.
     */
    private function extract_linked_keywords_from_content($content) {
        $keywords = array();
        $content = (string) $content;
        if ($content === '') {
            return $keywords;
        }

        if (preg_match_all('/<a[^>]*class="[^"]*aysa-internal-link[^"]*"[^>]*>(.*?)<\/a>/iu', $content, $matches)) {
            foreach ($matches[1] as $anchor_text) {
                $normalized = $this->normalize_keyword_for_matching(wp_strip_all_tags($anchor_text));
                if ($normalized !== '') {
                    $keywords[$normalized] = true;
                }
            }
        }

        return $keywords;
    }

    /**
     * Dynamic max links per source content item.
     * Rules:
     * - Posts/articles: always max 3 links
     * - Pages/products: <=300 chars => 1, 301-900 => 2, >=901 => 3
     * - Other types: conservative fallback 1
     */
    private function get_dynamic_entity_link_limit($content_item, $content) {
        $type = strtolower((string)($content_item['type'] ?? ''));
        if ($type === 'post') {
            return 3;
        }

        if ($type === 'page' || $type === 'product') {
            $plain_text = wp_strip_all_tags((string) $content);
            $plain_text = preg_replace('/\s+/u', ' ', trim($plain_text));
            $length = mb_strlen((string) $plain_text, 'UTF-8');

            if ($length <= 300) {
                return 1;
            }
            if ($length <= 900) {
                return 2;
            }
            return 3;
        }

        return 1;
    }
    
    /**
     * Check if we're trying to link to the same page (enhanced self-linking detection)
     */
    private function is_same_page($content_item, $link, $target_type) {
        // Extract ID from link
        $link_id = $this->extract_id_from_link($link);
        
        // Enhanced self-linking detection - cover all entity types
        $content_type = strtolower($content_item['type']);
        $target_type_lower = strtolower($target_type);
        
        aysa_debug_log("[AYSA Internal Linking] Self-link check: Content type='{$content_type}', Target type='{$target_type_lower}', Content ID={$content_item['id']}, Link ID={$link_id}");
        
        // Direct type matching
        if ($content_type === $target_type_lower && $content_item['id'] == $link_id) {
            aysa_debug_log("[AYSA Internal Linking] SELF-LINK DETECTED: Direct match - {$content_type} ID {$content_item['id']}");
            return true;
        }
        
        // Handle special cases and synonyms
        $type_mappings = [
            'term' => ['category', 'product_cat', 'post_tag'],
            'page' => ['page'],
            'post' => ['post'],
            'product' => ['product']
        ];
        
        foreach ($type_mappings as $content_base_type => $target_types) {
            if ($content_type === $content_base_type && in_array($target_type_lower, $target_types)) {
                if ($content_item['id'] == $link_id) {
                    aysa_debug_log("[AYSA Internal Linking] SELF-LINK DETECTED: Type mapping - {$content_type} -> {$target_type_lower} ID {$content_item['id']}");
                    return true;
                }
            }
        }
        
        // URL-based comparison as fallback (in case ID extraction fails)
        if (empty($link_id) && !empty($content_item['url']) && !empty($link)) {
            $content_url = rtrim($content_item['url'], '/');
            $target_url = rtrim($link, '/');
            
            if ($content_url === $target_url) {
                aysa_debug_log("[AYSA Internal Linking] SELF-LINK DETECTED: URL match - {$content_url}");
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check if a post/page is the homepage
     */
    private function is_homepage($post_id) {
        // Method 1: Check if it's the front page
        if (get_option('page_on_front') == $post_id) {
            return true;
        }
        
        // Method 2: Check if URL matches home URL
        $post_url = get_permalink($post_id);
        $home_url = home_url('/');
        
        // Normalize URLs for comparison
        $post_url_clean = rtrim($post_url, '/');
        $home_url_clean = rtrim($home_url, '/');
        
        if ($post_url_clean === $home_url_clean) {
            return true;
        }
        
        // Method 3: Check for common homepage slugs
        $post = get_post($post_id);
        if ($post) {
            $homepage_slugs = ['home', 'homepage', 'index', 'main'];
            if (in_array(strtolower($post->post_name), $homepage_slugs)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check if content is built with Elementor
     */
    private function is_elementor_content($post_id) {
        // Check if Elementor plugin is active
        if (!class_exists('\Elementor\Plugin')) {
            return false;
        }
        
        // Check if this post has Elementor data
        $elementor_data = get_post_meta($post_id, '_elementor_data', true);
        return !empty($elementor_data);
    }
    
    /**
     * Get effective content for a post (handles Elementor content)
     */
    private function get_effective_content($post) {
        // First check if it's Elementor content
        if ($this->is_elementor_content($post->ID)) {
            aysa_debug_log("[AYSA Internal Linking] Elementor content detected for post {$post->ID} - '{$post->post_title}'");
            
            // Try to get rendered Elementor content
            $elementor_content = $this->get_elementor_rendered_content($post->ID);
            
            if (!empty($elementor_content)) {
                aysa_debug_log("[AYSA Internal Linking] Successfully extracted Elementor content (" . strlen($elementor_content) . " chars) for post {$post->ID}");
                return $elementor_content;
            } else {
                aysa_debug_log("[AYSA Internal Linking] Failed to extract Elementor content for post {$post->ID}, using post_content as fallback");
                // Fallback to regular content if Elementor extraction fails
                return $post->post_content;
            }
        }
        
        // For non-Elementor content, return regular post content
        return $post->post_content;
    }
    
    /**
     * Get rendered content from Elementor
     */
    private function get_elementor_rendered_content($post_id) {
        // Check if Elementor plugin is active
        if (!class_exists('\Elementor\Plugin')) {
            return '';
        }
        
        try {
            // Try to get the Elementor frontend instance
            if (class_exists('\Elementor\Frontend')) {
                $frontend = \Elementor\Plugin::instance()->frontend;
                
                // Get the rendered content
                $content = $frontend->get_builder_content($post_id, true);
                
                if (!empty($content)) {
                    // Strip HTML tags to get plain text for keyword matching
                    $plain_content = wp_strip_all_tags($content);
                    
                    // Clean up whitespace
                    $plain_content = preg_replace('/\s+/', ' ', trim($plain_content));
                    
                    return $plain_content;
                }
            }
            
            // Alternative method: try to parse Elementor data manually
            $elementor_data = get_post_meta($post_id, '_elementor_data', true);
            if (!empty($elementor_data)) {
                $data = json_decode($elementor_data, true);
                if ($data) {
                    $extracted_text = $this->extract_text_from_elementor_data($data);
                    if (!empty($extracted_text)) {
                        return $extracted_text;
                    }
                }
            }
            
        } catch (Exception $e) {
            aysa_debug_log("[AYSA Internal Linking] Error extracting Elementor content for post {$post_id}: " . $e->getMessage());
        }
        
        return '';
    }
    
    /**
     * Extract text content from Elementor data structure
     */
    private function extract_text_from_elementor_data($data) {
        $text_content = '';
        
        if (is_array($data)) {
            foreach ($data as $element) {
                if (is_array($element)) {
                    // Check for text content in various Elementor element types
                    if (isset($element['settings'])) {
                        $settings = $element['settings'];
                        
                        // Text/heading elements
                        if (isset($settings['text'])) {
                            $text_content .= ' ' . wp_strip_all_tags($settings['text']);
                        }
                        
                        // Title/heading elements
                        if (isset($settings['title'])) {
                            $text_content .= ' ' . wp_strip_all_tags($settings['title']);
                        }
                        
                        // Description elements
                        if (isset($settings['description'])) {
                            $text_content .= ' ' . wp_strip_all_tags($settings['description']);
                        }
                        
                        // Editor content (rich text)
                        if (isset($settings['editor'])) {
                            $text_content .= ' ' . wp_strip_all_tags($settings['editor']);
                        }
                        
                        // Button text
                        if (isset($settings['button_text'])) {
                            $text_content .= ' ' . wp_strip_all_tags($settings['button_text']);
                        }
                    }
                    
                    // Recursively process nested elements
                    if (isset($element['elements']) && is_array($element['elements'])) {
                        $text_content .= ' ' . $this->extract_text_from_elementor_data($element['elements']);
                    }
                }
            }
        }
        
        // Clean up and return
        return preg_replace('/\s+/', ' ', trim($text_content));
    }
    
    /**
     * Extract ID from WordPress link
     */
    private function extract_id_from_link($link) {
        // Handle various WordPress link formats
        if (preg_match('/\/product\/([^\/]+)\/?$/', $link, $matches)) {
            // Product permalink
            $product = get_page_by_path($matches[1], OBJECT, 'product');
            return $product ? $product->ID : null;
        }
        
        if (preg_match('/\?p=(\d+)/', $link, $matches)) {
            return intval($matches[1]);
        }
        
        if (preg_match('/\?page_id=(\d+)/', $link, $matches)) {
            return intval($matches[1]);
        }
        
        if (preg_match('/\?cat=(\d+)/', $link, $matches)) {
            return intval($matches[1]);
        }
        
        return null;
    }
    
    /**
     * Get or generate link for association based on VALUE (the target page/product)
     */
    private function get_or_generate_link($association) {
        $entity_type = strtolower((string)($association['associatedOn'] ?? ''));
        $entity_id = intval($association['idIntern'] ?? 0);

        // If link already exists, use it only if target is still active.
        if (!empty($association['link'])) {
            $candidate = esc_url_raw($association['link']);
            if ($this->is_target_url_active($candidate, $entity_type, $entity_id)) {
                return $candidate;
            }
            aysa_debug_log("[AYSA Internal Linking] SKIP: Inactive target URL from API link '{$candidate}'");
            return null;
        }
        
        // Use VALUE to find the target entity
        $entity_value = $association['value'] ?? '';
        
        if (empty($entity_value)) {
            return null;
        }
        
        // Search based on entity type and VALUE (target name)
        switch (strtolower($entity_type)) {
            case 'product':
                // PRIORITY 1: Use idIntern if available (most reliable)
                if ($entity_id > 0) {
                    $post = get_post($entity_id);
                    if ($post && $post->post_type === 'product' && $post->post_status === 'publish') {
                        $candidate = get_permalink($entity_id);
                        return $this->is_target_url_active($candidate, 'product', $entity_id) ? $candidate : null;
                    }
                }
                
                // PRIORITY 2: Search for product by title (value contains product name)
                $products = get_posts([
                    'post_type' => 'product',
                    'post_status' => 'publish',
                    'title' => $entity_value,
                    'numberposts' => 1
                ]);
                
                if (!empty($products)) {
                    $candidate = get_permalink($products[0]->ID);
                    return $this->is_target_url_active($candidate, 'product', $products[0]->ID) ? $candidate : null;
                }
                break;
                
            case 'page':
                // PRIORITY 1: Use idIntern if available (most reliable)
                if ($entity_id > 0) {
                    $post = get_post($entity_id);
                    if ($post && $post->post_type === 'page' && $post->post_status === 'publish') {
                        $candidate = get_permalink($entity_id);
                        return $this->is_target_url_active($candidate, 'page', $entity_id) ? $candidate : null;
                    }
                }
                
                // PRIORITY 2: Search for page by title (value contains page name)
                $pages = get_posts([
                    'post_type' => 'page',
                    'post_status' => 'publish',
                    'title' => $entity_value,
                    'numberposts' => 1
                ]);
                
                if (!empty($pages)) {
                    $candidate = get_permalink($pages[0]->ID);
                    return $this->is_target_url_active($candidate, 'page', $pages[0]->ID) ? $candidate : null;
                }
                break;
                
            case 'post':
                // Search for post by title (value contains post name)
                $posts = get_posts([
                    'post_type' => 'post',
                    'post_status' => 'publish',
                    'title' => $entity_value,
                    'numberposts' => 1
                ]);
                
                if (!empty($posts)) {
                    $candidate = get_permalink($posts[0]->ID);
                    return $this->is_target_url_active($candidate, 'post', $posts[0]->ID) ? $candidate : null;
                }
                break;
                
            case 'category':
                // Search for category by name (value contains category name)
                $term = get_term_by('name', $entity_value, 'category');
                if ($term && !is_wp_error($term)) {
                    $candidate = get_term_link($term);
                    return $this->is_target_url_active($candidate, 'category', intval($term->term_id)) ? $candidate : null;
                }
                
                // Also try product categories
                $term = get_term_by('name', $entity_value, 'product_cat');
                if ($term && !is_wp_error($term)) {
                    $candidate = get_term_link($term);
                    return $this->is_target_url_active($candidate, 'product_cat', intval($term->term_id)) ? $candidate : null;
                }
                
                // Fallback: search by slug
                $term = get_term_by('slug', sanitize_title($entity_value), 'product_cat');
                if ($term && !is_wp_error($term)) {
                    $candidate = get_term_link($term);
                    return $this->is_target_url_active($candidate, 'product_cat', intval($term->term_id)) ? $candidate : null;
                }
                break;
        }
        
        // Generic fallback: search across all post types
        $posts = get_posts([
            'post_type' => ['post', 'page', 'product'],
            'post_status' => 'publish',
            'title' => $entity_value,
            'numberposts' => 1
        ]);
        
        if (!empty($posts)) {
            $candidate = get_permalink($posts[0]->ID);
            $candidate_type = get_post_type($posts[0]->ID);
            return $this->is_target_url_active($candidate, (string)$candidate_type, intval($posts[0]->ID)) ? $candidate : null;
        }
        
        return null; // Could not find target entity
    }

    /**
     * Validate that a target URL is still active and eligible for internal linking.
     */
    private function is_target_url_active($url, $entity_type = '', $entity_id = 0) {
        static $cache = array();
        $url = esc_url_raw((string) $url);
        if ($url === '') {
            return false;
        }

        $cache_key = md5($url . '|' . strtolower((string)$entity_type) . '|' . intval($entity_id));
        if (isset($cache[$cache_key])) {
            return $cache[$cache_key];
        }

        $url_parts = wp_parse_url($url);
        $home_parts = wp_parse_url(home_url('/'));
        $url_host = strtolower((string)($url_parts['host'] ?? ''));
        $home_host = strtolower((string)($home_parts['host'] ?? ''));
        $path = strtolower((string)($url_parts['path'] ?? ''));
        $query = strtolower((string)($url_parts['query'] ?? ''));

        // Internal linking must only target same-site public URLs.
        if ($url_host !== '' && $home_host !== '' && $url_host !== $home_host) {
            $cache[$cache_key] = false;
            return false;
        }

        // Block admin/auth/edit endpoints from being linked.
        if (
            strpos($path, '/wp-admin/') === 0 ||
            strpos($path, '/wp-login.php') === 0 ||
            strpos($path, '/xmlrpc.php') === 0 ||
            strpos($query, 'action=edit') !== false ||
            strpos($query, 'post=') !== false ||
            strpos($query, 'page=') !== false
        ) {
            $cache[$cache_key] = false;
            return false;
        }

        $type = strtolower((string) $entity_type);
        $id = intval($entity_id);

        // If we have a known post ID, validate status directly.
        if ($id > 0 && in_array($type, array('post', 'page', 'product'), true)) {
            $post = get_post($id);
            $is_active = ($post && $post->post_status === 'publish');
            $cache[$cache_key] = $is_active;
            return $is_active;
        }

        // Validate internal post/page/product by URL.
        $post_id = url_to_postid($url);
        if ($post_id > 0) {
            $post = get_post($post_id);
            $is_active = ($post && $post->post_status === 'publish');
            $cache[$cache_key] = $is_active;
            return $is_active;
        }

        // Validate common taxonomy archives by path.
        $path_trimmed = trim((string)($url_parts['path'] ?? ''), '/');
        $segments = $path_trimmed === '' ? array() : explode('/', $path_trimmed);

        if (count($segments) >= 2) {
            $base = strtolower($segments[0]);
            $slug = sanitize_title(end($segments));
            $taxonomy = '';

            if ($base === 'category') {
                $taxonomy = 'category';
            } elseif ($base === 'tag') {
                $taxonomy = 'post_tag';
            } elseif ($base === 'product-category') {
                $taxonomy = 'product_cat';
            }

            if ($taxonomy !== '') {
                $term = get_term_by('slug', $slug, $taxonomy);
                $is_active = ($term && !is_wp_error($term));
                $cache[$cache_key] = $is_active;
                return $is_active;
            }
        }

        // Fallback for same-host public paths.
        $cache[$cache_key] = true;
        return true;
    }

    /**
     * Remove AYSA internal links that target inactive URLs.
     */
    private function remove_inactive_aysa_links_from_content($content) {
        $content = (string) $content;
        if ($content === '') {
            return array('content' => $content, 'removed' => 0);
        }

        $removed = 0;
        $updated = preg_replace_callback(
            '/<a([^>]*class="[^"]*aysa-internal-link[^"]*"[^>]*)href=(["\'])(.*?)\\2([^>]*)>(.*?)<\/a>/iu',
            function($m) use (&$removed) {
                $href = esc_url_raw($m[3]);
                if (!$this->is_target_url_active($href)) {
                    $removed++;
                    return wp_strip_all_tags($m[5]);
                }
                return $m[0];
            },
            $content
        );

        if (!is_string($updated)) {
            return array('content' => $content, 'removed' => 0);
        }

        return array('content' => $updated, 'removed' => $removed);
    }
    
    /**
     * Update content in database (Enhanced for Elementor support)
     */
    private function update_content_in_database($content_item, $updated_content, $preview_mode = false) {
        if ($preview_mode) {
            // In preview mode, don't actually update the database
            aysa_debug_log('[AYSA Internal Linking] Preview mode: skipping database update for ' . $content_item['type'] . ' ID ' . $content_item['id']);
            return;
        }
        
        if ($content_item['type'] == 'term') {
            wp_update_term($content_item['id'], $content_item['taxonomy'], [
                'description' => $updated_content
            ]);
            aysa_debug_log("[AYSA Internal Linking] ✅ Updated term {$content_item['id']} description");
        } else {
            // Check if this is Elementor content
            if (!empty($content_item['is_elementor']) && $content_item['is_elementor']) {
                // For Elementor content, we cannot safely update the Elementor data structure
                aysa_debug_log("[AYSA Internal Linking] 🎨 ELEMENTOR CONTENT DETECTED for post {$content_item['id']} - SKIPPING AUTOMATIC UPDATE");
                aysa_debug_log("[AYSA Internal Linking] ⚠️ ELEMENTOR LIMITATION: Automatic internal linking not supported for Elementor pages");
                aysa_debug_log("[AYSA Internal Linking] 💡 SOLUTION: Use Elementor's text editor to manually add internal links");
                aysa_debug_log("[AYSA Internal Linking] 🔗 SUGGESTED LINKS: Check the Internal Linking report for keyword suggestions");
                
                // Don't update Elementor content automatically to avoid breaking the page
                return;
                
            } else {
                // Regular WordPress content
                wp_update_post([
                    'ID' => $content_item['id'],
                    'post_content' => $updated_content
                ]);
                aysa_debug_log("[AYSA Internal Linking] ✅ Updated regular post content for post {$content_item['id']}");
            }
        }
    }
    
    /**
     * AJAX: Get internal linking report
     */
    public function ajax_get_report() {
        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Insufficient permissions'));
            return;
        }
        
        check_ajax_referer('aysa_internal_linking_nonce', 'nonce');
        
        $log = get_option('aysa_internal_linking_log', array());
        $log = $this->maybe_rebuild_internal_linking_log($log);
        $counts = get_option('aysa_internal_linking_counts', array());
        
        // Group by keyword
        $keyword_stats = array();
        foreach ($log as $entry) {
            $keyword = $entry['keyword'];
            if (!isset($keyword_stats[$keyword])) {
                $keyword_stats[$keyword] = array(
                    'keyword' => $keyword,
                    'total_links' => 0,
                    'target_url' => $entry['target_url'],
                    'target_title' => $entry['target_title'],
                    'placements' => array()
                );
            }
            
            $keyword_stats[$keyword]['total_links']++;
            $source_url = $this->resolve_source_front_url(
                $entry['source_entity_id'] ?? 0,
                $entry['source_entity_type'] ?? '',
                $entry['source_url'] ?? ''
            );
            $keyword_stats[$keyword]['placements'][] = array(
                'source_title' => $entry['source_title'],
                'source_type' => $entry['source_entity_type'],
                'source_url' => $source_url,
                'timestamp' => $entry['timestamp']
            );
        }
        
        wp_send_json_success(array(
            'keyword_stats' => array_values($keyword_stats),
            'total_keywords' => count($keyword_stats),
            'total_links' => count($log),
            'settings' => $this->settings
        ));
    }
    
    /**
     * AJAX: Get settings
     */
    public function ajax_get_settings() {
        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Insufficient permissions'));
            return;
        }
        
        check_ajax_referer('aysa_internal_linking_nonce', 'nonce');
        
        // Get saved settings
        $settings = get_option('aysa_internal_linking_settings', array());
        
        // Ensure all expected fields exist with defaults
        $default_settings = array(
            'enabled' => false,
            'max_links_per_keyword' => 1, // Conservative default: one occurrence per keyword in a source entity
            'max_links_per_entity' => 3, // Article cap; dynamic rules apply in the main processor
            'matching_type' => 'both',
            'process_posts' => true,
            'process_pages' => true,
            'process_categories' => true,
            'process_products' => true,
            'case_sensitive' => false,
            'ignore_existing_links' => true,
            'priority_by_authority' => true,
            'excluded_post_ids' => ''
        );
        
        $settings = wp_parse_args($settings, $default_settings);

        // Enforce current UX/business preset in settings panel.
        $settings['max_links_per_keyword'] = 1;
        $settings['max_links_per_entity'] = 3;
        
        aysa_debug_log('[AYSA Internal Linking] Loaded settings: ' . aysa_debug_export($settings, true));
        
        wp_send_json_success($settings);
    }

    /**
     * AJAX: Save settings
     */
    public function ajax_save_settings() {
        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Insufficient permissions'));
            return;
        }
        
        check_ajax_referer('aysa_internal_linking_nonce', 'nonce');

        $post_data = wp_unslash($_POST);
        aysa_debug_log('[AYSA Internal Linking] SAVE SETTINGS request received');
        
        $settings = $this->settings;
        
        // Update settings from POST data
        // Handle boolean values correctly - JavaScript sends boolean values
        $settings['enabled'] = (isset($post_data['enabled']) && ($post_data['enabled'] === 'true' || $post_data['enabled'] === true));
        // Keep enforced preset to match agreed business rules.
        $settings['max_links_per_keyword'] = 1;
        $settings['max_links_per_entity'] = 3;
        $settings['matching_type'] = sanitize_text_field(isset($post_data['matching_type']) ? (string) $post_data['matching_type'] : 'both');
        $settings['process_posts'] = (isset($post_data['process_posts']) && ($post_data['process_posts'] === 'true' || $post_data['process_posts'] === true));
        $settings['process_pages'] = (isset($post_data['process_pages']) && ($post_data['process_pages'] === 'true' || $post_data['process_pages'] === true));
        $settings['process_categories'] = (isset($post_data['process_categories']) && ($post_data['process_categories'] === 'true' || $post_data['process_categories'] === true));
        $settings['process_products'] = (isset($post_data['process_products']) && ($post_data['process_products'] === 'true' || $post_data['process_products'] === true));
        $settings['case_sensitive'] = (isset($post_data['case_sensitive']) && ($post_data['case_sensitive'] === 'true' || $post_data['case_sensitive'] === true));
        $settings['ignore_existing_links'] = (isset($post_data['ignore_existing_links']) && ($post_data['ignore_existing_links'] === 'true' || $post_data['ignore_existing_links'] === true));
        $settings['priority_by_authority'] = (isset($post_data['priority_by_authority']) && ($post_data['priority_by_authority'] === 'true' || $post_data['priority_by_authority'] === true));
        
        // Handle excluded IDs
        $excluded_ids = sanitize_text_field(isset($post_data['excluded_post_ids']) ? (string) $post_data['excluded_post_ids'] : '');
        $settings['excluded_post_ids'] = array_filter(array_map('intval', explode(',', $excluded_ids)));
        
        // Handle excluded URL patterns
        if (isset($post_data['excluded_url_patterns'])) {
            $excluded_patterns = sanitize_textarea_field((string) $post_data['excluded_url_patterns']);
            $patterns_array = array_filter(array_map('trim', explode("\n", $excluded_patterns)));
            $settings['excluded_url_patterns'] = $patterns_array;
        }
        
        // Save settings
        aysa_debug_log('[AYSA Internal Linking] SAVING settings: ' . aysa_debug_export($settings, true));
        update_option('aysa_internal_linking_settings', $settings);
        $this->settings = $settings;
        
        // Verify what was actually saved
        $saved_settings = get_option('aysa_internal_linking_settings');
        aysa_debug_log('[AYSA Internal Linking] VERIFIED saved settings: ' . aysa_debug_export($saved_settings, true));
        
        // Clear cache
        delete_transient('aysa_internal_linking_keywords');
        
        wp_send_json_success(array(
            'message' => __('Settings saved successfully.', 'aysaaiseo'),
            'saved_settings' => $saved_settings,
            'case_sensitive_value' => $saved_settings['case_sensitive'] ?? 'NOT SET'
        ));
    }

    /**
     * AJAX: Preview content changes without applying them
     */
    public function ajax_preview_content() {
        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Insufficient permissions'));
            return;
        }
        
        check_ajax_referer('aysa_internal_linking_nonce', 'nonce');
        
        try {
            aysa_debug_log('[AYSA Internal Linking] Starting preview...');
            
            // Reload settings to ensure we have the latest values
            $this->settings = get_option('aysa_internal_linking_settings', $this->get_default_settings());
            aysa_debug_log('[AYSA Internal Linking] Reloaded settings for preview: ' . aysa_debug_export($this->settings, true));
            
            // Process content but don't save changes - just return what would be changed
            $preview_results = $this->process_all_content_with_main_keywords(true); // true = preview mode
            
            aysa_debug_log('[AYSA Internal Linking] Preview results: ' . aysa_debug_export($preview_results, true));
            
            // Format results for preview modal
            $preview_data = array(
                'total_keywords_found' => $preview_results['total_keywords_found'] ?? 0,
                'content_items_scanned' => $preview_results['content_items_scanned'] ?? 0,
                'links_to_be_added' => $preview_results['links_added'] ?? 0,
                'preview_changes' => $this->format_preview_changes($preview_results['link_mappings'] ?? array()),
                'summary' => $preview_results['summary'] ?? 'Preview completed'
            );
            
            wp_send_json_success($preview_data);
            
        } catch (Exception $e) {
            aysa_debug_log('[AYSA Internal Linking] Preview error: ' . $e->getMessage());
            wp_send_json_error(array('message' => $e->getMessage()));
        }
    }

    /**
     * Format link mappings for preview display
     */
    private function format_preview_changes($link_mappings) {
        $changes_by_content = array();
        
        foreach ($link_mappings as $mapping) {
            $content_id = $mapping['source_content_id'];
            $content_title = $mapping['source_content_title'] ?? "Content #{$content_id}";
            
            if (!isset($changes_by_content[$content_id])) {
                $changes_by_content[$content_id] = array(
                    'content_title' => $content_title,
                    'content_id' => $content_id,
                    'content_type' => $mapping['source_content_type'] ?? 'post',
                    'links_count' => 0,
                    'changes' => array()
                );
            }
            
            $changes_by_content[$content_id]['links_count']++;
            $changes_by_content[$content_id]['changes'][] = array(
                'keyword' => $mapping['keyword'],
                'target_title' => $mapping['target_page'] ?? 'Unknown',
                'target_url' => $mapping['target_url'] ?? '#'
            );
        }
        
        return array_values($changes_by_content);
    }
}

// Internal linking system will be initialized from class-aysa.php
