<?php
/**
 * Tam Rank™ Smart Redirect Suggestions v2
 *
 * Zelflerend redirect suggestie systeem met:
 * - Dynamische gewichten op basis van acceptatie/afwijzing
 * - Synoniemen detectie uit handmatige redirects
 * - Verbeterde matching met meerdere signalen
 * - Confidence scoring en match reasons
 */

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

class TamRank_Redirect_Suggestions {

    /**
     * Maximum aantal suggesties per 404 URL
     */
    const MAX_SUGGESTIONS_PER_URL = 3;

    /**
     * Dagen voordat pending suggesties ignored worden
     */
    const DAYS_UNTIL_IGNORED = 30;

    /**
     * Tuning configuratie (cached)
     *
     * @var array|null
     */
    private $tuning = null;


    /**
     * Constructor
     */
    public function __construct() {
        // AJAX handlers
        add_action( 'wp_ajax_tam_rank_get_suggestions', array( $this, 'ajax_get_suggestions' ) );
        add_action( 'wp_ajax_tam_rank_accept_suggestion', array( $this, 'ajax_accept_suggestion' ) );
        add_action( 'wp_ajax_tam_rank_dismiss_suggestion', array( $this, 'ajax_dismiss_suggestion' ) );
        add_action( 'wp_ajax_tam_rank_generate_suggestions', array( $this, 'ajax_generate_suggestions' ) );
        add_action( 'wp_ajax_tam_rank_run_learning', array( $this, 'ajax_run_learning' ) );

        // Cron voor auto-ignore en learning
        add_action( 'tamrank_weekly_learning', array( $this, 'run_weekly_learning' ) );
        add_action( 'tamrank_daily_cleanup', array( $this, 'mark_old_suggestions_ignored' ) );
        // Daily retention pass on the 404 log (TTL + hard row-cap).
        add_action( 'tamrank_daily_cleanup', array( $this, 'prune_404_log' ) );

        // Registreer cron schedules
        if ( ! wp_next_scheduled( 'tamrank_weekly_learning' ) ) {
            wp_schedule_event( time(), 'weekly', 'tamrank_weekly_learning' );
        }
        if ( ! wp_next_scheduled( 'tamrank_daily_cleanup' ) ) {
            wp_schedule_event( time(), 'daily', 'tamrank_daily_cleanup' );
        }
    }

    /**
     * Retentie-schoonmaak van de 404-log. Draait dagelijks op tamrank_daily_cleanup.
     *
     * De 404-log logt één rij per hit en kent zelf geen opschoning, dus op
     * drukke sites (bot-scans naar niet-bestaande URLs) zou de tabel monotoon
     * groeien tot een admin handmatig "Clear Log" drukt. Deze pass begrenst dat:
     *
     *   1. TTL-prune: rijen ouder dan de retentie-window verdwijnen. Omdat elke
     *      hit een verse timestamp schrijft, blijven URLs die NOG bezocht worden
     *      vanzelf in beeld; alleen "dode" 404's (al > window niet meer geraakt)
     *      vallen weg — precies wat een gebruiker wil zien.
     *   2. Harde row-cap als vangnet tegen bursts binnen de window.
     *
     * Beide defaults zijn filterbaar; days = 0 schakelt de TTL uit (alleen cap).
     * Schema-loos: gebruikt alleen de bestaande timestamp-kolom + KEY timestamp,
     * dus geen migratie/db_version-bump nodig.
     *
     * @since 1.2.9
     * @return void
     */
    public function prune_404_log() {
        global $wpdb;

        $table = $wpdb->prefix . 'tam_rank_404_log';

        // Table may not exist yet on a half-activated install.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared -- existence check on a custom plugin table; table name bound via prepare(); cron-context, caching not applicable
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            return;
        }

        // 1) TTL-prune. Default 90 days; filter to extend/shorten, 0 = keep forever.
        //    timestamp is written with current_time('mysql') (site-local time), so
        //    the cutoff is built on the same basis.
        $days = (int) apply_filters( 'tamrank_404_retention_days', 90 );
        if ( $days > 0 ) {
            $cutoff = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - ( $days * DAY_IN_SECONDS ) );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- delete on a custom plugin table; only the {$table} identifier is interpolated, the cutoff value uses a %s placeholder; cron-context write, caching not applicable
            $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE timestamp < %s", $cutoff ) );
        }

        // 2) Hard row-cap safety-net. Default 5000; filter to raise/lower, 0 = no cap.
        $cap = (int) apply_filters( 'tamrank_404_max_rows', 5000 );
        if ( $cap > 0 ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- count on a custom plugin table; only the {$table} identifier is interpolated, no user values; cron-context read, caching not applicable
            $total  = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
            $excess = $total - $cap;
            if ( $excess > 0 ) {
                // Drop the oldest rows above the cap. DELETE ... ORDER BY ... LIMIT
                // keeps this bounded to the overflow on each daily run.
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- delete on a custom plugin table; only the {$table} identifier is interpolated, the limit uses a %d placeholder; cron-context write, caching not applicable
                $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} ORDER BY timestamp ASC LIMIT %d", $excess ) );
            }
        }
    }

    /**
     * Haal tuning configuratie op
     *
     * @return array Tuning weights en thresholds
     */
    private function get_tuning() {
        if ( $this->tuning === null ) {
            $this->tuning = get_option( 'tamrank_redirect_tuning', array() );

            // Fallback defaults
            $defaults = array(
                'levenshtein_weight'   => 0.40,
                'slug_term_weight'     => 0.35,  // Verhoogd voor betere term matching
                'title_term_weight'    => 0.20,
                'similar_text_weight'  => 0.05,
                'exact_match_bonus'    => 100,
                'multi_token_bonus'    => 10,   // Verhoogd
                'category_match_bonus' => 5,
                'post_type_bonus'      => 3,
                'short_slug_penalty'   => -10,  // Verlaagd
                'date_slug_penalty'    => -8,   // Verlaagd
                'numeric_slug_penalty' => -3,   // Verlaagd
                'min_similarity_score' => 25,   // Verlaagd van 40 naar 25 voor betere matching
                'high_confidence_threshold' => 75,
                'medium_confidence_threshold' => 50,
                'acceptance_boost'     => 1.1,
                'dismissal_penalty'    => 0.9,
            );

            $this->tuning = wp_parse_args( $this->tuning, $defaults );
        }
        return $this->tuning;
    }


    /**
     * Check of een URL moet worden overgeslagen (bots, assets, systeem URLs)
     *
     * @param string $path URL path
     * @return bool True als URL moet worden overgeslagen
     */
    private function should_skip_url( $path ) {
        $path_lower = strtolower( $path );

        // Skip asset bestanden (CSS, JS, afbeeldingen, fonts)
        $asset_extensions = array( '.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.map' );
        foreach ( $asset_extensions as $ext ) {
            if ( substr( $path_lower, -strlen( $ext ) ) === $ext ) {
                return true;
            }
        }

        // Skip WordPress system paths and bot/scanner URLs
        // Loaded from JSON to keep sensitive strings out of PHP source (WAF compatibility)
        static $skip_filters = null;
        if ( $skip_filters === null ) {
            $json_path = TAM_RANK_PLUGIN_PATH . 'assets/data/404-noise-filters.json';
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading a bundled local JSON asset, not a remote resource.
            $skip_filters = file_exists( $json_path ) ? json_decode( file_get_contents( $json_path ), true ) : array();
        }
        $skip_patterns = isset( $skip_filters['ignore_patterns'] ) ? $skip_filters['ignore_patterns'] : array();
        foreach ( $skip_patterns as $pattern ) {
            if ( strpos( $path_lower, $pattern ) !== false ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Genereer suggesties voor een 404 URL
     *
     * @param string $url_404 De 404 URL
     * @return array Array met suggesties
     */
    public function generate_suggestions( $url_404 ) {
        $suggestions = array();
        $tuning = $this->get_tuning();

        // Parse URL
        $parsed = wp_parse_url( $url_404 );
        $path = isset( $parsed['path'] ) ? $parsed['path'] : $url_404;
        $path = trim( $path, '/' );

        // Skip URLs die we niet willen verwerken (bots, assets, systeem)
        if ( $this->should_skip_url( $path ) ) {
            return $suggestions;
        }

        // Splits path in delen
        $path_parts = explode( '/', $path );
        $slug = end( $path_parts );

        // Verwijder extensies
        $slug = preg_replace( '/\.(html?|php|aspx?|jsp)$/i', '', $slug );

        // Check voor te korte of ongeldige slugs
        if ( strlen( $slug ) < 2 ) {
            return $suggestions;
        }

        // Analyseer de 404 URL
        $url_analysis = $this->analyze_url( $slug, $path );

        // Skip als het een datum-achtige URL is met hoge zekerheid
        if ( $url_analysis['is_date_url'] && $url_analysis['date_confidence'] > 0.8 ) {
            return $suggestions;
        }

        // Maak zoektermen van de slug
        $search_terms = $this->extract_search_terms( $slug );

        if ( empty( $search_terms ) ) {
            return $suggestions;
        }

        // Zoek matching posts/pages
        $matches = $this->find_matching_content( $search_terms, $slug, $path, $url_analysis );

        // Sorteer op confidence score en limiteer
        usort( $matches, function( $a, $b ) {
            return $b['confidence_score'] - $a['confidence_score'];
        });

        $suggestions = array_slice( $matches, 0, self::MAX_SUGGESTIONS_PER_URL );

        return $suggestions;
    }

    /**
     * Analyseer een URL voor speciale patronen
     *
     * @param string $slug De URL slug
     * @param string $path Het volledige pad
     * @return array Analyse resultaten
     */
    private function analyze_url( $slug, $path ) {
        $analysis = array(
            'is_date_url'       => false,
            'date_confidence'   => 0,
            'is_numeric'        => false,
            'is_short'          => false,
            'token_count'       => 0,
            'detected_post_type' => null,
            'detected_category' => null,
        );

        // Check voor datum patronen
        $date_patterns = array(
            '/^\d{4}\/\d{2}\/\d{2}/',           // 2024/01/15
            '/^\d{4}-\d{2}-\d{2}/',             // 2024-01-15
            '/\d{4}\/\d{2}\//',                 // 2024/01/
            '/^\d{8}$/',                         // 20240115
        );

        foreach ( $date_patterns as $pattern ) {
            if ( preg_match( $pattern, $path ) || preg_match( $pattern, $slug ) ) {
                $analysis['is_date_url'] = true;
                $analysis['date_confidence'] = 0.9;
                break;
            }
        }

        // Minder sterke datum indicatoren
        if ( ! $analysis['is_date_url'] && preg_match( '/\d{4}/', $slug ) ) {
            $analysis['is_date_url'] = true;
            $analysis['date_confidence'] = 0.5;
        }

        // Check voor puur numerieke slug
        $analysis['is_numeric'] = preg_match( '/^\d+$/', $slug );

        // Check voor korte slug
        $clean_slug = str_replace( array( '-', '_' ), '', $slug );
        $analysis['is_short'] = strlen( $clean_slug ) < 4;

        // Tel tokens
        $tokens = preg_split( '/[-_]/', $slug );
        $analysis['token_count'] = count( array_filter( $tokens, function( $t ) {
            return strlen( $t ) >= 2;
        }));

        // Probeer post type te detecteren uit path
        $post_type_indicators = array(
            'blog'      => 'post',
            'nieuws'    => 'post',
            'news'      => 'post',
            'artikel'   => 'post',
            'article'   => 'post',
            'pagina'    => 'page',
            'page'      => 'page',
            'product'   => 'product',
            'products'  => 'product',
            'shop'      => 'product',
        );

        foreach ( explode( '/', $path ) as $part ) {
            $part = strtolower( $part );
            if ( isset( $post_type_indicators[ $part ] ) ) {
                $analysis['detected_post_type'] = $post_type_indicators[ $part ];
                break;
            }
        }

        // Probeer categorie te detecteren
        $path_parts = explode( '/', trim( $path, '/' ) );
        if ( count( $path_parts ) > 1 ) {
            $potential_cat = $path_parts[0];
            $category = get_category_by_slug( $potential_cat );
            if ( $category ) {
                $analysis['detected_category'] = $category->term_id;
            }
        }

        return $analysis;
    }

    /**
     * Extract zoektermen uit een slug
     *
     * @param string $slug De URL slug
     * @return array Array met zoektermen
     */
    private function extract_search_terms( $slug ) {
        // Vervang scheidingstekens door spaties
        $slug = str_replace( array( '-', '_', '+', '%20', '.' ), ' ', $slug );

        // Split op spaties
        $words = preg_split( '/\s+/', $slug );

        // Stopwoorden en test-achtige woorden
        $stopwords = array(
            // Nederlands
            'de', 'het', 'een', 'van', 'in', 'op', 'en', 'is', 'zijn', 'voor',
            'met', 'naar', 'aan', 'om', 'te', 'bij', 'door', 'over', 'uit',
            'deze', 'die', 'dat', 'wat', 'wie', 'waar', 'hoe', 'als',
            // Engels
            'the', 'a', 'an', 'of', 'to', 'and', 'for', 'is', 'on', 'with',
            'in', 'at', 'by', 'from', 'or', 'as', 'be', 'this', 'that',
            'it', 'are', 'was', 'were', 'been', 'being', 'have', 'has',
            // Test/dev woorden (niet zoeken maar ook niet de hele URL afkeuren)
            'test', 'testing', 'demo', 'example', 'sample', 'tmp', 'temp',
        );

        $terms = array();
        foreach ( $words as $word ) {
            $word = strtolower( trim( $word ) );
            // Skip te korte woorden, stopwoorden, en puur numerieke waarden
            if ( strlen( $word ) < 2 ) {
                continue;
            }
            if ( in_array( $word, $stopwords, true ) ) {
                continue;
            }
            // Skip puur numerieke waarden (maar houd alfanumerieke)
            if ( preg_match( '/^\d+$/', $word ) ) {
                continue;
            }
            $terms[] = $word;
        }

        return $terms;
    }

    /**
     * Zoek content die matcht met de zoektermen
     *
     * @param array  $search_terms Zoektermen
     * @param string $original_slug Originele slug
     * @param string $original_path Origineel path
     * @param array  $url_analysis URL analyse resultaten
     * @return array Array met matches
     */
    private function find_matching_content( $search_terms, $original_slug, $original_path, $url_analysis ) {
        global $wpdb;

        $matches = array();
        $tuning = $this->get_tuning();

        // Bouw LIKE clauses
        $like_clauses = array();
        $like_values = array();

        foreach ( $search_terms as $term ) {
            $like_clauses[] = "(post_title LIKE %s OR post_name LIKE %s)";
            $like_values[] = '%' . $wpdb->esc_like( $term ) . '%';
            $like_values[] = '%' . $wpdb->esc_like( $term ) . '%';
        }

        if ( empty( $like_clauses ) ) {
            return $matches;
        }

        $like_sql = implode( ' OR ', $like_clauses );

        // Bepaal post types om te zoeken
        $post_types = array( 'post', 'page' );
        $public_post_types = get_post_types( array( 'public' => true ), 'names' );
        $post_types = array_merge( $post_types, array_values( $public_post_types ) );
        $post_types = array_unique( $post_types );
        $post_types_sql = "'" . implode( "','", array_map( 'esc_sql', $post_types ) ) . "'";

        // Query posts
        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only $wpdb->posts, an esc_sql()'d+quoted post-type list and runtime-built literal %s LIKE clauses are interpolated; all user values bound via prepare() placeholders; admin-context read, caching not applicable
        $sql = $wpdb->prepare(
            "SELECT ID, post_title, post_name, post_type
             FROM {$wpdb->posts}
             WHERE post_status = 'publish'
             AND post_type IN ({$post_types_sql})
             AND ({$like_sql})
             LIMIT 100",
            ...$like_values
        );
        $results = $wpdb->get_results( $sql );
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter

        foreach ( $results as $post ) {
            $permalink = get_permalink( $post->ID );

            // Bereken score met alle signalen
            $score_result = $this->calculate_smart_score(
                $original_slug,
                $original_path,
                $post,
                $search_terms,
                $url_analysis
            );

            if ( $score_result['score'] >= $tuning['min_similarity_score'] ) {
                $matches[] = array(
                    'post_id'          => $post->ID,
                    'title'            => $post->post_title,
                    'slug'             => $post->post_name,
                    'url'              => $permalink,
                    'post_type'        => $post->post_type,
                    'score'            => $score_result['score'],
                    'confidence_score' => $score_result['confidence'],
                    'match_reasons'    => $score_result['reasons'],
                );
            }
        }

        return $matches;
    }

    /**
     * Bereken smart score met alle signalen
     *
     * @param string   $original_slug Originele slug
     * @param string   $original_path Origineel path
     * @param WP_Post  $post Target post object
     * @param array    $search_terms Zoektermen
     * @param array    $url_analysis URL analyse
     * @return array Score, confidence en reasons
     */
    private function calculate_smart_score( $original_slug, $original_path, $post, $search_terms, $url_analysis ) {
        $tuning = $this->get_tuning();
        $score = 0;
        $reasons = array();
        $positive_signals = 0;
        $negative_signals = 0;

        // Clean strings
        $original_clean = strtolower( str_replace( array( '-', '_' ), ' ', $original_slug ) );
        $target_slug_clean = strtolower( str_replace( array( '-', '_' ), ' ', $post->post_name ) );
        $target_title_clean = strtolower( $post->post_title );

        // ========================================
        // 1. EXACTE MATCH (hoogste score)
        // ========================================
        if ( $original_slug === $post->post_name ) {
            return array(
                'score'      => 100,
                'confidence' => 1.0,
                'reasons'    => array(
                    array( 'type' => 'positive', 'text' => __( 'Exact slug match', 'tam-rank' ) ),
                ),
            );
        }

        // ========================================
        // 2. LEVENSHTEIN DISTANCE
        // ========================================
        $slug_distance = levenshtein( $original_clean, $target_slug_clean );
        $max_len = max( strlen( $original_clean ), strlen( $target_slug_clean ) );

        if ( $max_len > 0 ) {
            $slug_similarity = ( 1 - ( $slug_distance / $max_len ) ) * 100;
            if ( $slug_similarity > 60 ) {
                $levenshtein_score = $slug_similarity * $tuning['levenshtein_weight'];
                $score += $levenshtein_score;
                $positive_signals++;
                $reasons[] = array(
                    'type' => 'positive',
                    // translators: %.0f%% is the slug similarity percentage between two URLs.
                    'text' => sprintf( __( 'Slug similarity: %.0f%%', 'tam-rank' ), $slug_similarity ),
                );
            }
        }

        // ========================================
        // 3. ZOEKTERM MATCHES IN SLUG
        // ========================================
        $slug_term_matches = 0;
        $matched_terms_slug = array();
        foreach ( $search_terms as $term ) {
            if ( stripos( $target_slug_clean, $term ) !== false ) {
                $slug_term_matches++;
                $matched_terms_slug[] = $term;
            }
        }

        if ( count( $search_terms ) > 0 && $slug_term_matches > 0 ) {
            $term_match_ratio = $slug_term_matches / count( $search_terms );
            $score += $term_match_ratio * 100 * $tuning['slug_term_weight'];
            $positive_signals++;
            $reasons[] = array(
                'type' => 'positive',
                'text' => sprintf( /* translators: %s: matched search terms */ __( 'Overlap in slug: "%s"', 'tam-rank' ), implode( '", "', $matched_terms_slug ) ),
            );
        }

        // ========================================
        // 4. ZOEKTERM MATCHES IN TITEL
        // ========================================
        $title_term_matches = 0;
        $matched_terms_title = array();
        foreach ( $search_terms as $term ) {
            if ( stripos( $target_title_clean, $term ) !== false ) {
                $title_term_matches++;
                $matched_terms_title[] = $term;
            }
        }

        if ( count( $search_terms ) > 0 && $title_term_matches > 0 ) {
            $title_match_ratio = $title_term_matches / count( $search_terms );
            $score += $title_match_ratio * 100 * $tuning['title_term_weight'];
            $positive_signals++;
            if ( count( $matched_terms_title ) > 0 ) {
                $reasons[] = array(
                    'type' => 'positive',
                    'text' => sprintf( /* translators: %s: matched search terms */ __( 'Overlap in title: "%s"', 'tam-rank' ), implode( '", "', $matched_terms_title ) ),
                );
            }
        }

        // ========================================
        // 5. SIMILAR_TEXT BONUS
        // ========================================
        similar_text( $original_clean, $target_slug_clean, $similar_percent );
        if ( $similar_percent > 50 ) {
            $similar_bonus = ( $similar_percent - 50 ) * $tuning['similar_text_weight'];
            $score += $similar_bonus;
        }

        // ========================================
        // 6. MULTI-TOKEN BONUS (>= 3 tokens match)
        // ========================================
        $total_matches = count( array_unique( array_merge( $matched_terms_slug, $matched_terms_title ) ) );
        if ( $total_matches >= 3 ) {
            $score += $tuning['multi_token_bonus'];
            $positive_signals++;
            $reasons[] = array(
                'type' => 'positive',
                'text' => sprintf( /* translators: %d: number of matching words */ __( '%d+ words match', 'tam-rank' ), $total_matches ),
            );
        }

        // ========================================
        // 8. POST TYPE MATCH BONUS
        // ========================================
        if ( $url_analysis['detected_post_type'] !== null &&
             $url_analysis['detected_post_type'] === $post->post_type ) {
            $score += $tuning['post_type_bonus'];
            $positive_signals++;
            $reasons[] = array(
                'type' => 'positive',
                'text' => __( 'Post type matches', 'tam-rank' ),
            );
        } elseif ( $url_analysis['detected_post_type'] !== null &&
                   $url_analysis['detected_post_type'] !== $post->post_type ) {
            $negative_signals++;
            $reasons[] = array(
                'type' => 'negative',
                'text' => sprintf( /* translators: 1: detected post type, 2: actual post type */ __( 'Different post type (%1$s vs %2$s)', 'tam-rank' ),
                    $url_analysis['detected_post_type'], $post->post_type ),
            );
        }

        // ========================================
        // 9. CATEGORIE MATCH BONUS
        // ========================================
        if ( $url_analysis['detected_category'] !== null ) {
            $post_categories = wp_get_post_categories( $post->ID );
            if ( in_array( $url_analysis['detected_category'], $post_categories, true ) ) {
                $score += $tuning['category_match_bonus'];
                $positive_signals++;
                $reasons[] = array(
                    'type' => 'positive',
                    'text' => __( 'Category matches', 'tam-rank' ),
                );
            }
        }

        // ========================================
        // 10. PENALTIES
        // ========================================

        // Korte slug penalty
        if ( $url_analysis['is_short'] ) {
            $score += $tuning['short_slug_penalty'];
            $negative_signals++;
            $reasons[] = array(
                'type' => 'negative',
                'text' => __( 'Short original slug (less reliable)', 'tam-rank' ),
            );
        }

        // Datum slug penalty
        if ( $url_analysis['is_date_url'] ) {
            $penalty = $tuning['date_slug_penalty'] * $url_analysis['date_confidence'];
            $score += $penalty;
            $negative_signals++;
            $reasons[] = array(
                'type' => 'warning',
                'text' => __( 'Date detected in URL', 'tam-rank' ),
            );
        }

        // Numerieke slug penalty
        if ( $url_analysis['is_numeric'] ) {
            $score += $tuning['numeric_slug_penalty'];
            $negative_signals++;
            $reasons[] = array(
                'type' => 'negative',
                'text' => __( 'Numeric original slug', 'tam-rank' ),
            );
        }

        // ========================================
        // CONFIDENCE BEREKENING
        // ========================================
        $score = max( 0, min( 99, round( $score ) ) );

        // Confidence is gebaseerd op score + aantal positieve/negatieve signalen
        $signal_ratio = $positive_signals / max( 1, $positive_signals + $negative_signals );
        $base_confidence = $score / 100;
        $confidence = ( $base_confidence * 0.7 ) + ( $signal_ratio * 0.3 );
        $confidence = round( $confidence, 2 );

        return array(
            'score'      => $score,
            'confidence' => $confidence,
            'reasons'    => $reasons,
        );
    }

    /**
     * Sla een suggestie op in de database
     *
     * @param string $url_404     De 404 URL
     * @param int    $target_id   Target post ID
     * @param string $target_url  Target URL
     * @param int    $score       Similarity score
     * @param float  $confidence  Confidence score
     * @param array  $reasons     Match reasons
     * @return int|false Insert ID of false bij fout
     */
    public function save_suggestion( $url_404, $target_id, $target_url, $score, $confidence = 0, $reasons = array() ) {
        global $wpdb;

        $table_name = $wpdb->prefix . 'tam_rank_redirect_suggestions';

        // Check of suggestie al bestaat
        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the prefix-built table identifier (tam_rank_redirect_suggestions) is interpolated; user values bound via prepare() placeholders; admin-context read, caching not applicable
        $existing = $wpdb->get_var( $wpdb->prepare(
            "SELECT id FROM $table_name WHERE source_url = %s AND target_url = %s",
            $url_404,
            $target_url
        ) );
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter

        if ( $existing ) {
            return $existing;
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- write operation on a custom plugin table (tam_rank_redirect_suggestions); admin-context insert, caching not applicable
        $result = $wpdb->insert(
            $table_name,
            array(
                'source_url'       => $url_404,
                'target_id'        => $target_id,
                'target_url'       => $target_url,
                'score'            => $score,
                'confidence_score' => $confidence,
                'status'           => 'pending',
                'match_reasons'    => wp_json_encode( $reasons ),
                'created_at'       => current_time( 'mysql' ),
            ),
            array( '%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s' )
        );

        // Update statistieken
        $this->update_stats( $score, 'suggested' );

        return $result ? $wpdb->insert_id : false;
    }

    /**
     * Haal suggesties op voor weergave
     *
     * @param int    $limit  Maximum aantal resultaten
     * @param string $status Filter op status
     * @param string $quality Filter op kwaliteit ('high', 'low', 'all')
     * @return array Array met suggesties
     */
    public function get_suggestions( $limit = 50, $status = 'pending', $quality = 'all' ) {
        global $wpdb;

        $table_name = $wpdb->prefix . 'tam_rank_redirect_suggestions';
        $tuning = $this->get_tuning();

        $where_clauses = array( "s.status = %s" );
        $where_values = array( $status );

        // Filter op kwaliteit
        if ( $quality === 'high' ) {
            $where_clauses[] = "s.score >= %d";
            $where_values[] = $tuning['medium_confidence_threshold'];
        } elseif ( $quality === 'low' ) {
            $where_clauses[] = "s.score < %d";
            $where_values[] = $tuning['medium_confidence_threshold'];
        }

        $where_sql = implode( ' AND ', $where_clauses );

        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the prefix-built table name, $wpdb->posts and a runtime-built WHERE clause of literal %s/%d placeholders are interpolated; all user values bound via prepare() placeholders; admin-context read, caching not applicable
        $sql = $wpdb->prepare(
            "SELECT s.*, p.post_title as target_title
             FROM $table_name s
             LEFT JOIN {$wpdb->posts} p ON s.target_id = p.ID
             WHERE $where_sql
             ORDER BY s.confidence_score DESC, s.score DESC, s.created_at DESC
             LIMIT %d",
            array_merge( $where_values, array( $limit ) )
        );
        $results = $wpdb->get_results( $sql );
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter

        // Decode match_reasons
        foreach ( $results as &$row ) {
            if ( ! empty( $row->match_reasons ) ) {
                $row->match_reasons = json_decode( $row->match_reasons, true );
            } else {
                $row->match_reasons = array();
            }
        }

        return $results;
    }

    /**
     * Tel suggesties per status
     *
     * @return array Counts per status + quality
     */
    public function count_suggestions() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'tam_rank_redirect_suggestions';
        $tuning = $this->get_tuning();

        $counts = array(
            'pending'      => 0,
            'accepted'     => 0,
            'dismissed'    => 0,
            'ignored'      => 0,
            'high_quality' => 0,
            'low_quality'  => 0,
        );

        // Basis counts per status
        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the prefix-built table identifier (tam_rank_redirect_suggestions) is interpolated; no user values; admin-context read, caching not applicable
        $results = $wpdb->get_results(
            "SELECT status, COUNT(*) as count FROM $table_name GROUP BY status"
        );
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter

        foreach ( $results as $row ) {
            if ( isset( $counts[ $row->status ] ) ) {
                $counts[ $row->status ] = (int) $row->count;
            }
        }

        // Quality counts voor pending
        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the prefix-built table identifier (tam_rank_redirect_suggestions) is interpolated; user values bound via prepare() placeholders; admin-context read, caching not applicable
        $counts['high_quality'] = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM $table_name WHERE status = 'pending' AND score >= %d",
            $tuning['medium_confidence_threshold']
        ) );
        $counts['low_quality'] = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM $table_name WHERE status = 'pending' AND score < %d",
            $tuning['medium_confidence_threshold']
        ) );
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter

        return $counts;
    }

    /**
     * Accepteer een suggestie en maak redirect aan
     *
     * @param int $suggestion_id Suggestie ID
     * @return bool|WP_Error True bij succes
     */
    public function accept_suggestion( $suggestion_id ) {
        global $wpdb;

        $table_name = $wpdb->prefix . 'tam_rank_redirect_suggestions';

        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the prefix-built table identifier (tam_rank_redirect_suggestions) is interpolated; user value bound via prepare() placeholder; admin-context read, caching not applicable
        $suggestion = $wpdb->get_row( $wpdb->prepare(
            "SELECT * FROM $table_name WHERE id = %d",
            $suggestion_id
        ) );
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter

        if ( ! $suggestion ) {
            return new WP_Error( 'not_found', __( 'Suggestion not found', 'tam-rank' ) );
        }

        // Maak redirect aan via redirect manager
        $redirect_manager = new TamRank_Redirect_Manager();
        $result = $redirect_manager->create_redirect(
            $suggestion->source_url,
            $suggestion->target_url,
            '301',
            true // auto_generated = true
        );

        if ( is_wp_error( $result ) ) {
            return $result;
        }

        // Update redirect met created_by
        $redirects_table = $wpdb->prefix . 'tam_rank_redirects';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- write operation on a custom plugin table (tam_rank_redirects); admin-context update, caching not applicable
        $wpdb->update(
            $redirects_table,
            array( 'created_by' => 'suggestion' ),
            array( 'source_url' => $suggestion->source_url ),
            array( '%s' ),
            array( '%s' )
        );

        // Update suggestie status
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- write operation on a custom plugin table (tam_rank_redirect_suggestions); admin-context update, caching not applicable
        $wpdb->update(
            $table_name,
            array(
                'status'      => 'accepted',
                'decision_at' => current_time( 'mysql' ),
            ),
            array( 'id' => $suggestion_id ),
            array( '%s', '%s' ),
            array( '%d' )
        );

        // Update statistieken
        $this->update_stats( $suggestion->score, 'accepted' );

        return true;
    }

    /**
     * Wijs een suggestie af
     *
     * @param int $suggestion_id Suggestie ID
     * @return bool True bij succes
     */
    public function dismiss_suggestion( $suggestion_id ) {
        global $wpdb;

        $table_name = $wpdb->prefix . 'tam_rank_redirect_suggestions';

        // Haal score op voor statistieken
        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the prefix-built table identifier (tam_rank_redirect_suggestions) is interpolated; user value bound via prepare() placeholder; admin-context read, caching not applicable
        $suggestion = $wpdb->get_row( $wpdb->prepare(
            "SELECT score FROM $table_name WHERE id = %d",
            $suggestion_id
        ) );
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- write operation on a custom plugin table (tam_rank_redirect_suggestions); admin-context update, caching not applicable
        $result = $wpdb->update(
            $table_name,
            array(
                'status'      => 'dismissed',
                'decision_at' => current_time( 'mysql' ),
            ),
            array( 'id' => $suggestion_id ),
            array( '%s', '%s' ),
            array( '%d' )
        );

        if ( $result !== false && $suggestion ) {
            $this->update_stats( $suggestion->score, 'dismissed' );
        }

        return $result !== false;
    }

    /**
     * Update statistieken voor learning
     *
     * @param int    $score Score van de suggestie
     * @param string $action 'suggested', 'accepted', of 'dismissed'
     */
    private function update_stats( $score, $action ) {
        global $wpdb;

        $stats_table = $wpdb->prefix . 'tam_rank_redirect_stats';

        // Check of tabel bestaat
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery --admin-context existence check, caching not applicable
        $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $stats_table ) );
        if ( ! $table_exists ) {
            return;
        }

        // Bepaal score range
        if ( $score >= 80 ) {
            $range = '80-100';
        } elseif ( $score >= 60 ) {
            $range = '60-79';
        } else {
            $range = '40-59';
        }

        $column = $action . '_count';

        // Upsert statistiek
        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the prefix-built table identifier (tam_rank_redirect_stats) is interpolated; user value bound via prepare() placeholder; admin-context read, caching not applicable
        $existing = $wpdb->get_var( $wpdb->prepare(
            "SELECT id FROM $stats_table WHERE score_range = %s AND match_type = 'overall'",
            $range
        ) );
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter

        if ( $existing ) {
            // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the prefix-built table identifier (tam_rank_redirect_stats) and an internally-derived column name (<action>_count, where <action> is one of the hardcoded literals 'suggested'/'accepted'/'dismissed') are interpolated; user values bound via prepare() placeholders; admin-context update, caching not applicable
            $wpdb->query( $wpdb->prepare(
                "UPDATE $stats_table SET {$column} = {$column} + 1, last_updated = %s WHERE id = %d",
                current_time( 'mysql' ),
                $existing
            ) );
            // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
        } else {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- write operation on a custom plugin table (tam_rank_redirect_stats); admin-context insert, caching not applicable
            $wpdb->insert(
                $stats_table,
                array(
                    'score_range'     => $range,
                    'match_type'      => 'overall',
                    $column           => 1,
                    'last_updated'    => current_time( 'mysql' ),
                ),
                array( '%s', '%s', '%d', '%s' )
            );
        }
    }

    /**
     * Markeer oude suggesties als ignored
     */
    public function mark_old_suggestions_ignored() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'tam_rank_redirect_suggestions';
        $cutoff_date = gmdate( 'Y-m-d H:i:s', strtotime( '-' . self::DAYS_UNTIL_IGNORED . ' days' ) );

        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the prefix-built table identifier (tam_rank_redirect_suggestions) is interpolated; values bound via prepare() placeholders; cron/admin-context update, caching not applicable
        $affected = $wpdb->query( $wpdb->prepare(
            "UPDATE $table_name
             SET status = 'ignored', decision_at = %s
             WHERE status = 'pending' AND created_at < %s",
            current_time( 'mysql' ),
            $cutoff_date
        ) );
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter

        return $affected;
    }


    /**
     * Run wekelijkse learning op basis van statistieken
     */
    public function run_weekly_learning() {
        global $wpdb;

        $stats_table = $wpdb->prefix . 'tam_rank_redirect_stats';
        $tuning = $this->get_tuning();

        // Check of tabel bestaat
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery --cron/admin-context existence check, caching not applicable
        $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $stats_table ) );
        if ( ! $table_exists ) {
            return;
        }

        // Haal statistieken op
        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the prefix-built table identifier (tam_rank_redirect_stats) is interpolated; no user values; cron/admin-context read, caching not applicable
        $stats = $wpdb->get_results(
            "SELECT score_range, suggested_count, accepted_count, dismissed_count
             FROM $stats_table WHERE match_type = 'overall'"
        );
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter

        $updated = false;

        foreach ( $stats as $stat ) {
            $total_decisions = $stat->accepted_count + $stat->dismissed_count;

            if ( $total_decisions < 5 ) {
                continue; // Niet genoeg data
            }

            $acceptance_rate = $stat->accepted_count / $total_decisions;

            // Pas minimum score aan op basis van lage score range
            if ( $stat->score_range === '40-59' && $acceptance_rate < 0.3 ) {
                // Lage scores worden vaak afgewezen → verhoog minimum
                $tuning['min_similarity_score'] = min( 50, $tuning['min_similarity_score'] + 2 );
                $updated = true;
            }

            // Pas gewichten aan
            if ( $stat->score_range === '80-100' && $acceptance_rate > 0.8 ) {
                // Hoge scores worden vaak geaccepteerd → boost confidence thresholds
                $tuning['high_confidence_threshold'] = max( 75, $tuning['high_confidence_threshold'] - 1 );
                $updated = true;
            }
        }

        if ( $updated ) {
            update_option( 'tamrank_redirect_tuning', $tuning );
            $this->tuning = null; // Clear cache
        }
    }

    /**
     * Genereer suggesties voor alle 404 URLs zonder suggesties
     *
     * @param int $limit Maximum aantal te verwerken 404s
     * @return int Aantal gegenereerde suggesties
     */
    public function generate_suggestions_for_404s( $limit = 50 ) {
        global $wpdb;

        $log_table = $wpdb->prefix . 'tam_rank_404_log';
        $suggestions_table = $wpdb->prefix . 'tam_rank_redirect_suggestions';
        $redirects_table = $wpdb->prefix . 'tam_rank_redirects';

        // Haal 404 URLs op die nog geen suggestie of redirect hebben
        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only prefix-built table identifiers are interpolated; the %d limit is bound via prepare(); admin-context read, caching not applicable
        $sql = $wpdb->prepare(
            "SELECT DISTINCT l.url
             FROM $log_table l
             LEFT JOIN $suggestions_table s ON l.url = s.source_url
             LEFT JOIN $redirects_table r ON l.url = r.source_url
             WHERE s.id IS NULL AND r.id IS NULL
             ORDER BY l.timestamp DESC
             LIMIT %d",
            $limit
        );
        $urls = $wpdb->get_col( $sql );
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $total_suggestions = 0;

        foreach ( $urls as $url ) {
            $suggestions = $this->generate_suggestions( $url );

            foreach ( $suggestions as $suggestion ) {
                $saved = $this->save_suggestion(
                    $url,
                    $suggestion['post_id'],
                    $suggestion['url'],
                    $suggestion['score'],
                    $suggestion['confidence_score'],
                    $suggestion['match_reasons']
                );

                if ( $saved ) {
                    $total_suggestions++;
                }
            }
        }

        return $total_suggestions;
    }


    // ========================================
    // AJAX HANDLERS
    // ========================================

    /**
     * AJAX: Haal suggesties op
     */
    public function ajax_get_suggestions() {
        check_ajax_referer( 'tam_rank_nonce', 'nonce' );

        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        $status = isset( $_POST['status'] ) ? sanitize_text_field(wp_unslash($_POST['status'])) : 'pending';
        $quality = isset( $_POST['quality'] ) ? sanitize_text_field(wp_unslash($_POST['quality'])) : 'all';

        $suggestions = $this->get_suggestions( 50, $status, $quality );
        $counts = $this->count_suggestions();

        wp_send_json_success( array(
            'suggestions' => $suggestions,
            'counts'      => $counts,
        ) );
    }

    /**
     * AJAX: Accepteer suggestie
     */
    public function ajax_accept_suggestion() {
        check_ajax_referer( 'tam_rank_nonce', 'nonce' );

        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        // Basis-actie: FREE-feature (redirect_suggestions_basic)
        if ( ! tam_rank_can_use_feature( 'redirect_suggestions_basic' ) ) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        $suggestion_id = isset( $_POST['suggestion_id'] ) ? intval( $_POST['suggestion_id'] ) : 0;

        if ( ! $suggestion_id ) {
            wp_send_json_error(array('message' => tamrank__('error_invalid_suggestion', 'Invalid suggestion ID.'), 'code' => 'not_found'));
        }

        $result = $this->accept_suggestion( $suggestion_id );

        if ( is_wp_error( $result ) ) {
            wp_send_json_error(array('message' => $result->get_error_message(), 'code' => $result->get_error_code() ?: 'request_failed'));
        }

        // Trigger automatische learning
        $this->maybe_trigger_learning();

        wp_send_json_success( __( 'Redirect created', 'tam-rank' ) );
    }

    /**
     * AJAX: Wijs suggestie af
     */
    public function ajax_dismiss_suggestion() {
        check_ajax_referer( 'tam_rank_nonce', 'nonce' );

        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        // Basis-actie: FREE
        if ( ! tam_rank_can_use_feature( 'redirect_suggestions_basic' ) ) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        $suggestion_id = isset( $_POST['suggestion_id'] ) ? intval( $_POST['suggestion_id'] ) : 0;

        if ( ! $suggestion_id ) {
            wp_send_json_error(array('message' => tamrank__('error_invalid_suggestion', 'Invalid suggestion ID.'), 'code' => 'not_found'));
        }

        $result = $this->dismiss_suggestion( $suggestion_id );

        if ( ! $result ) {
            wp_send_json_error(array('message' => tamrank__('error_suggestion_reject', 'Could not reject the suggestion.'), 'code' => 'save_failed'));
        }

        // Trigger automatische learning
        $this->maybe_trigger_learning();

        wp_send_json_success( __( 'Suggestion rejected', 'tam-rank' ) );
    }

    /**
     * AJAX: Genereer nieuwe suggesties
     */
    public function ajax_generate_suggestions() {
        check_ajax_referer( 'tam_rank_nonce', 'nonce' );

        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        // Basis-actie: FREE
        if ( ! tam_rank_can_use_feature( 'redirect_suggestions_basic' ) ) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        global $wpdb;

        // Debug: check hoeveel 404s er zijn
        $log_table = $wpdb->prefix . 'tam_rank_404_log';
        $suggestions_table = $wpdb->prefix . 'tam_rank_redirect_suggestions';
        $redirects_table = $wpdb->prefix . 'tam_rank_redirects';

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the prefix-built table identifier (tam_rank_404_log) is interpolated; no user values; admin-context read, caching not applicable
        $total_404s = $wpdb->get_var( "SELECT COUNT(*) FROM $log_table" );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the prefix-built table identifier (tam_rank_404_log) is interpolated; no user values; admin-context read, caching not applicable
        $unique_404s = $wpdb->get_var( "SELECT COUNT(DISTINCT url) FROM $log_table" );

        // Check welke 404s nog geen suggesties hebben
        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only prefix-built table identifiers are interpolated; no user values; admin-context read, caching not applicable
        $unprocessed = $wpdb->get_results(
            "SELECT DISTINCT l.url
             FROM $log_table l
             LEFT JOIN $suggestions_table s ON l.url = s.source_url
             LEFT JOIN $redirects_table r ON l.url = r.source_url
             WHERE s.id IS NULL AND r.id IS NULL
             LIMIT 20"
        );
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter

        // Filter URLs die worden overgeslagen en sorteer op kwaliteit
        $filtered_urls = array();
        $valid_urls_with_terms = array();
        foreach ( $unprocessed as $row ) {
            $path = trim( $row->url, '/' );
            if ( $this->should_skip_url( $path ) ) {
                $filtered_urls[] = $row->url;
            } else {
                // Extract zoektermen om kwaliteit te bepalen
                $path_parts = explode( '/', $path );
                $slug = end( $path_parts );
                $slug = preg_replace( '/\.(html?|php|aspx?|jsp)$/i', '', $slug );
                $terms = $this->extract_search_terms( $slug );
                $valid_urls_with_terms[] = array(
                    'url'   => $row->url,
                    'terms' => $terms,
                    'count' => count( $terms ),
                );
            }
        }

        // Sorteer op kwaliteit: aantal termen, dan totale lengte van termen
        usort( $valid_urls_with_terms, function( $a, $b ) {
            // Eerst op aantal termen
            if ( $a['count'] !== $b['count'] ) {
                return $b['count'] - $a['count'];
            }
            // Dan op totale lengte van alle termen (langere woorden = betere kwaliteit)
            $len_a = array_sum( array_map( 'strlen', $a['terms'] ) );
            $len_b = array_sum( array_map( 'strlen', $b['terms'] ) );
            return $len_b - $len_a;
        });

        $valid_urls = array_column( $valid_urls_with_terms, 'url' );

        $debug_info = array(
            'total_404_entries' => (int) $total_404s,
            'unique_404_urls'   => (int) $unique_404s,
            'filtered_urls'     => $filtered_urls,
            'valid_urls'        => $valid_urls,
        );

        // Check of we verouderde tuning hebben (migratiefix)
        $current_tuning = $this->get_tuning();
        if ( $current_tuning['min_similarity_score'] > 30 ) {
            // Oude tuning detecteerd, reset naar nieuwe defaults
            delete_option( 'tamrank_redirect_tuning' );
            $this->tuning = null;
            $debug_info['tuning_reset'] = true;
        }

        // Genereer suggesties
        $generated = $this->generate_suggestions_for_404s( 100 );

        // Als er geldige URLs waren maar geen suggesties gegenereerd, test handmatig
        if ( $generated === 0 && ! empty( $valid_urls_with_terms ) ) {
            // Pak de URL met de meeste zoektermen
            $best_url_data = $valid_urls_with_terms[0];
            $test_url = $best_url_data['url'];
            $test_suggestions = $this->generate_suggestions( $test_url );
            $debug_info['test_url'] = $test_url;
            $debug_info['test_result'] = $test_suggestions;

            // Extra debug: zoek termen en potentiële matches
            $path = trim( $test_url, '/' );
            $path_parts = explode( '/', $path );
            $slug = end( $path_parts );
            $slug = preg_replace( '/\.(html?|php|aspx?|jsp)$/i', '', $slug );
            $search_terms = $this->extract_search_terms( $slug );
            $debug_info['extracted_terms'] = $search_terms;

            // Check of er überhaupt posts zijn met deze termen
            if ( ! empty( $search_terms ) ) {
                $like_clauses = array();
                $like_values = array();
                foreach ( $search_terms as $term ) {
                    $like_clauses[] = "(post_title LIKE %s OR post_name LIKE %s)";
                    $like_values[] = '%' . $wpdb->esc_like( $term ) . '%';
                    $like_values[] = '%' . $wpdb->esc_like( $term ) . '%';
                }
                $like_sql = implode( ' OR ', $like_clauses );
                // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only $wpdb->posts and runtime-built literal %s LIKE clauses are interpolated; all search values bound via prepare() placeholders; admin-context read, caching not applicable
                $sql = $wpdb->prepare(
                    "SELECT ID, post_title, post_name FROM {$wpdb->posts}
                     WHERE post_status = 'publish' AND ({$like_sql}) LIMIT 5",
                    ...$like_values
                );
                $potential_matches = $wpdb->get_results( $sql );
                // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
                $debug_info['potential_matches'] = $potential_matches;
            }

            // Extra debug: check tuning
            $debug_info['tuning'] = $this->get_tuning();
        }

        $response = array(
            'generated' => $generated,
            'message'   => sprintf( /* translators: %d: number of suggestions */ __( '%d new suggestions generated', 'tam-rank' ), $generated ),
        );
        // Internal diagnostics only in debug builds, never in production responses.
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            $response['debug'] = $debug_info;
        }
        wp_send_json_success( $response );
    }

    /**
     * AJAX: Run learning handmatig
     */
    public function ajax_run_learning() {
        check_ajax_referer( 'tam_rank_nonce', 'nonce' );

        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        // Reset tuning naar optimale defaults (optioneel via parameter)
        $reset = isset( $_POST['reset_tuning'] ) && 'true' === sanitize_text_field( wp_unslash( $_POST['reset_tuning'] ) );
        if ( $reset ) {
            delete_option( 'tamrank_redirect_tuning' );
            $this->tuning = null; // Clear cache
        }

        // Run learning
        $this->run_weekly_learning();

        // Mark old as ignored
        $ignored = $this->mark_old_suggestions_ignored();

        $message = sprintf( /* translators: %d: number of old suggestions */ __( 'Learning completed. %d old suggestions marked as ignored.', 'tam-rank' ), $ignored );
        if ( $reset ) {
            $message .= ' ' . __( 'Tuning reset to optimal defaults.', 'tam-rank' );
        }

        wp_send_json_success( array(
            'message' => $message,
            'tuning'  => $this->get_tuning(),
        ) );
    }

    /**
     * Reset tuning naar defaults
     */
    public function reset_tuning() {
        delete_option( 'tamrank_redirect_tuning' );
        $this->tuning = null;
        return $this->get_tuning();
    }

    /**
     * Haal learning statistieken op
     *
     * @return array Learning stats
     */
    public function get_learning_stats() {
        global $wpdb;

        $table = $wpdb->prefix . 'tam_rank_redirect_suggestions';

        // Tel totaal aantal beslissingen
        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the prefix-built table identifier (tam_rank_redirect_suggestions) is interpolated; no user values; admin-context read, caching not applicable
        $total_decisions = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM $table WHERE status IN ('accepted', 'dismissed')"
        );
        $accepted = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM $table WHERE status = 'accepted'"
        );
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter

        // Bereken acceptance rate
        $acceptance_rate = $total_decisions > 0 ? round( ( $accepted / $total_decisions ) * 100 ) : 0;

        // Haal laatste learning timestamp op
        $last_learning = get_option( 'tamrank_last_learning_run', 0 );

        return array(
            'total_decisions'  => $total_decisions,
            'accepted'         => $accepted,
            'acceptance_rate'  => $acceptance_rate,
            'last_learning'    => $last_learning,
        );
    }

    /**
     * Trigger learning als nodig (elke 5 beslissingen)
     */
    private function maybe_trigger_learning() {
        $stats = $this->get_learning_stats();

        // Run learning elke 5 beslissingen
        if ( $stats['total_decisions'] > 0 && $stats['total_decisions'] % 5 === 0 ) {
            $this->run_weekly_learning();
            update_option( 'tamrank_last_learning_run', time() );
        }
    }
}
