<?php
/**
 * Schema Detector
 *
 * Detecteert automatisch het beste schema type op basis van pagina inhoud
 * Werkt met de blok editor, Divi, Elementor en WooCommerce
 *
 * @package TamRank
 * @subpackage Schema\Detector
 * @since 1.0.0
 */

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

/**
 * Klasse TAMRANK_Schema_Detector
 *
 * Analyseert post inhoud en bepaalt het beste schema type
 */
class TAMRANK_Schema_Detector {

    /**
     * Minimum score drempel voor hoofd schema
     *
     * @var int
     */
    const HOOFD_SCHEMA_DREMPEL = 5;

    /**
     * Minimum score drempel voor extra schema
     *
     * @var int
     */
    const EXTRA_SCHEMA_DREMPEL = 4;

    /**
     * Maximum betrouwbaarheid bonus voor duidelijke detecties
     *
     * @var float
     */
    const MAX_BETROUWBAARHEID_BONUS = 0.3;

    /**
     * Cache voor gedetecteerde patronen
     *
     * @var array
     */
    private $patroon_cache = array();

    /**
     * Detecteer het schema type voor een post
     *
     * @param WP_Post $post Post object om te analyseren
     * @return TAMRANK_Schema_Detection_Result Detectie resultaat
     */
    public function detecteer( WP_Post $post ) {
        $resultaat = new TAMRANK_Schema_Detection_Result( $post->ID );

        // Reset cache voor nieuwe detectie
        $this->patroon_cache = array();

        // Haal ruwe en opgeschoonde content op
        $ruwe_content        = $post->post_content;
        $post_id             = $post->ID;

        // Probeer gerenderde content op te halen voor page builders (Divi, Elementor, etc.)
        $gerenderde_content = '';

        // Detecteer of dit een page builder pagina is
        $is_divi = strpos( $ruwe_content, '[et_pb_' ) !== false;
        $is_elementor = strpos( $ruwe_content, 'elementor' ) !== false || get_post_meta( $post_id, '_elementor_edit_mode', true );

        // Builder shortcodes nobody renders (Avada/WPBakery with the builder
        // switched off) survive the 'the_content' filter chain. Only then does the
        // front end need a separately fetched source - and we fetch it here, at
        // save time, never again from a visitor's request.
        $heeft_ongerenderde_builder_tags = tamrank_has_unregistered_builder_shortcodes( $ruwe_content );

        // Track whether a fetch was needed at all; if not, a previously stored
        // source is stale and may go.
        $fetch_nodig = $is_divi || $is_elementor || $heeft_ongerenderde_builder_tags;

        // Voor page builders: haal de gerenderde HTML op via HTTP request
        if ( $fetch_nodig && ! tamrank_rendered_source_fetch_blocked( $post_id ) ) {
            $permalink = get_permalink( $post_id );
            if ( $permalink ) {
                // Voeg skip parameter toe om schema output te skippen (voorkomt recursie)
                $permalink = add_query_arg( 'tamrank_skip_schema', '1', $permalink );
                $response = wp_remote_get( $permalink, array(
                    'timeout'   => 15,
                    // Self-fetch of this site's own permalink. Verify TLS in production; relax only on local WP_DEBUG dev.
                    'sslverify' => ! ( defined( 'WP_DEBUG' ) && WP_DEBUG ),
                    'user-agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ),
                    'cookies'   => array(),
                ) );
                $fetch_gelukt = ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200;
                if ( ! $fetch_gelukt ) {
                    // Fail silently, but remember it: a host that blocks loopbacks
                    // must not make every next save wait out the timeout again.
                    tamrank_mark_rendered_source_fetch_failed( $post_id );
                }
                if ( $fetch_gelukt ) {
                    $gerenderde_content = wp_remote_retrieve_body( $response );

                    // Filter content voor transient opslag (alleen relevante content area)
                    $filtered_content = $gerenderde_content;

                    // Divi: et-main-area bevat de hoofdcontent
                    if ( preg_match( '/<div[^>]*id=["\']et-main-area["\'][^>]*>(.*)/is', $gerenderde_content, $match ) ) {
                        $main_area = $match[1];
                        if ( preg_match( '/^(.*?)(?:<footer|<div[^>]*id=["\'](?:footer|et-footer)["\'])/is', $main_area, $footer_match ) ) {
                            $filtered_content = $footer_match[1];
                        } else {
                            $filtered_content = $main_area;
                        }
                    }
                    // Elementor: elementor content area
                    elseif ( preg_match( '/<div[^>]*class="[^"]*elementor[^"]*"[^>]*data-elementor-type=["\']wp-page["\'][^>]*>(.*?)<\/div>\s*(?:<footer|$)/is', $gerenderde_content, $match ) ) {
                        $filtered_content = $match[1];
                    }
                    // Fallback: DOMDocument extractie
                    else {
                        $extracted = $this->extract_main_content( $gerenderde_content );
                        if ( ! empty( $extracted ) ) {
                            $filtered_content = $extracted;
                        }
                    }

                    // Store the filtered source persistently; the front end only
                    // reads it, and never makes an HTTP call of its own.
                    if ( ! empty( $filtered_content ) ) {
                        tamrank_store_rendered_source( $post_id, $filtered_content );
                    }
                }
            }
        }

        // Voor custom themes zonder page builder: HTTP fetch als post_content te kort is
        if ( empty( $gerenderde_content ) && $post->post_status === 'publish' ) {
            $woord_check = str_word_count( wp_strip_all_tags( $ruwe_content ) );
            $fetch_nodig = $fetch_nodig || $woord_check < 50;
            if ( $woord_check < 50 && ! tamrank_rendered_source_fetch_blocked( $post_id ) ) {
                $permalink = get_permalink( $post_id );
                if ( $permalink ) {
                    $permalink = add_query_arg( 'tamrank_skip_schema', '1', $permalink );
                    $response = wp_remote_get( $permalink, array(
                        'timeout'   => 15,
                        // Self-fetch of this site's own permalink. Verify TLS in production; relax only on local WP_DEBUG dev.
                        'sslverify' => ! ( defined( 'WP_DEBUG' ) && WP_DEBUG ),
                        'user-agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ),
                        'cookies'   => array(),
                    ) );
                    $fetch_gelukt = ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200;
                    if ( ! $fetch_gelukt ) {
                        tamrank_mark_rendered_source_fetch_failed( $post_id );
                    }
                    if ( $fetch_gelukt ) {
                        $full_html = wp_remote_retrieve_body( $response );
                        $extracted = $this->extract_main_content( $full_html );
                        if ( ! empty( $extracted ) ) {
                            $gerenderde_content = $extracted;
                            tamrank_store_rendered_source( $post_id, $extracted );
                        }
                    }
                }
            }
        }

        // No fetch needed: a previously stored source is now stale and unused. A
        // FAILED fetch is different - there we keep the last known good source, so
        // the front end is never suddenly left without material.
        if ( ! $fetch_nodig ) {
            tamrank_delete_rendered_source( $post_id );
        }

        // Combineer ruwe en gerenderde content voor betere detectie
        $gecombineerde_content = $ruwe_content . "\n" . $gerenderde_content;

        $opgeschoonde_content = $this->schoon_content_op( $gecombineerde_content );

        // Bereken scores per schema type
        // Gebruik gecombineerde content zodat page builder elementen ook worden gedetecteerd
        $scores = array(
            'WebPage'       => $this->bereken_score_webpage( $post, $opgeschoonde_content, $gecombineerde_content ),
            'BlogPosting'   => $this->bereken_score_blogposting( $post, $opgeschoonde_content, $gecombineerde_content ),
            'Product'       => $this->bereken_score_product( $post, $opgeschoonde_content, $gecombineerde_content ),
            'FAQPage'       => $this->bereken_score_faq( $post, $opgeschoonde_content, $gecombineerde_content ),
            'HowTo'         => $this->bereken_score_howto( $post, $opgeschoonde_content, $gecombineerde_content ),
            'VideoObject'   => $this->bereken_score_video( $post, $opgeschoonde_content, $gecombineerde_content ),
            'ImageObject'   => $this->bereken_score_image( $post, $opgeschoonde_content, $gecombineerde_content ),
            'Event'         => $this->bereken_score_event( $post, $opgeschoonde_content, $gecombineerde_content ),
            'Recipe'        => $this->bereken_score_recipe( $post, $opgeschoonde_content, $gecombineerde_content ),
            'LocalBusiness' => $this->bereken_score_local_business( $post, $opgeschoonde_content, $gecombineerde_content ),
        );

        // Sla alle scores op in resultaat
        foreach ( $scores as $type => $score ) {
            $resultaat->stel_score_in( $type, $score );
        }

        // Definieer welke types als hoofd schema mogen dienen
        // Extra types (FAQ, Video, HowTo, Image) zijn altijd aanvullend, nooit het hoofd schema
        // WebPage subtypes: AboutPage, ContactPage, CollectionPage, ItemPage
        $primaire_types = array( 'WebPage', 'AboutPage', 'ContactPage', 'CollectionPage', 'ItemPage', 'BlogPosting', 'NewsArticle', 'Product', 'Event', 'Recipe', 'LocalBusiness', 'Course', 'Service', 'JobPosting', 'SoftwareApplication' );
        $extra_types = array( 'FAQPage', 'VideoObject', 'HowTo', 'ImageObject' );

        // Filter scores naar alleen primaire types voor hoofd schema bepaling
        $primaire_scores = array_filter( $scores, function( $type ) use ( $primaire_types ) {
            return in_array( $type, $primaire_types, true );
        }, ARRAY_FILTER_USE_KEY );

        // Sorteer primaire scores (hoogste eerst)
        arsort( $primaire_scores );

        // Bepaal hoofd schema uit primaire types
        $hoogste_score = reset( $primaire_scores );
        $hoofd_type    = key( $primaire_scores );

        // Controleer of score boven drempel is
        if ( $hoogste_score >= self::HOOFD_SCHEMA_DREMPEL ) {
            $resultaat->stel_hoofd_schema_in( $hoofd_type );
        } else {
            // Fallback op basis van post_type
            // BlogPosting voor blog posts, WebPage voor pagina's
            $post_type = $post->post_type;
            if ( $post_type === 'post' ) {
                $resultaat->stel_hoofd_schema_in( 'BlogPosting' );
            } elseif ( $post_type === 'product' ) {
                $resultaat->stel_hoofd_schema_in( 'Product' );
            } elseif ( $post_type === 'page' ) {
                // Detecteer specifiek pagina type op basis van slug/titel
                $pagina_type = $this->detecteer_pagina_type( $post );
                $resultaat->stel_hoofd_schema_in( $pagina_type );
            } else {
                $resultaat->stel_hoofd_schema_in( 'WebPage' );
            }
            $resultaat->voeg_detail_toe( 'fallback_reden', 'Geen primair schema type scoorde boven de drempel' );
        }

        // Bepaal extra schema's (FAQ, Video, HowTo, Image kunnen extra zijn)
        foreach ( $extra_types as $extra_type ) {
            if ( isset( $scores[ $extra_type ] ) && $scores[ $extra_type ] >= self::EXTRA_SCHEMA_DREMPEL ) {
                $resultaat->voeg_extra_schema_toe( $extra_type );
            }
        }

        // BreadcrumbList wordt altijd toegevoegd voor posts en pages
        if ( tamrank_post_type_has_breadcrumb_schema( $post->post_type ) ) {
            $resultaat->voeg_extra_schema_toe( 'BreadcrumbList' );
        }

        // Bereken betrouwbaarheid
        $betrouwbaarheid = $this->bereken_betrouwbaarheid( $scores, $resultaat->krijg_hoofd_schema() );
        $resultaat->stel_betrouwbaarheid_in( $betrouwbaarheid );

        // Voeg analyse details toe
        $resultaat->voeg_detail_toe( 'content_lengte', strlen( $opgeschoonde_content ) );
        $resultaat->voeg_detail_toe( 'post_type', $post->post_type );
        $resultaat->voeg_detail_toe( 'analyse_patronen', $this->patroon_cache );

        /**
         * Filter het detectie resultaat
         *
         * @param TAMRANK_Schema_Detection_Result $resultaat Detectie resultaat
         * @param WP_Post                         $post      Post object
         */
        return apply_filters( 'tamrank_schema_detectie_resultaat', $resultaat, $post );
    }

    /**
     * Schoon content op voor analyse
     *
     * Verwijdert shortcodes, HTML en normaliseert whitespace
     * Werkt met Divi, Elementor en standaard WordPress content
     *
     * @param string $content Ruwe content
     * @return string Opgeschoonde content
     */
    protected function schoon_content_op( $content ) {
        // Verwijder Gutenberg blok comments
        $content = preg_replace( '/<!--.*?-->/', '', $content );

        // Verwijder Elementor data attributen
        $content = preg_replace( '/data-elementor[^"]*="[^"]*"/', '', $content );

        // Verwijder Divi shortcode attributen maar behoud tekst
        $content = preg_replace( '/\[et_pb_[^\]]*\]/', '', $content );
        $content = preg_replace( '/\[\/et_pb_[^\]]*\]/', '', $content );

        // Verwijder overige shortcodes
        $content = strip_shortcodes( $content );

        // Verwijder HTML tags maar behoud structuur hints
        $content = wp_strip_all_tags( $content );

        // Normaliseer whitespace
        $content = preg_replace( '/\s+/', ' ', $content );
        $content = trim( $content );

        return $content;
    }

    /**
     * Bereken betrouwbaarheid van de detectie
     *
     * @param array  $scores      Scores per schema type
     * @param string $hoofd_type  Gekozen hoofd schema type
     * @return float Betrouwbaarheid tussen 0.0 en 1.0
     */
    protected function bereken_betrouwbaarheid( $scores, $hoofd_type ) {
        if ( empty( $scores ) || ! $hoofd_type ) {
            return 0.1;
        }

        $hoofd_score = isset( $scores[ $hoofd_type ] ) ? $scores[ $hoofd_type ] : 0;

        // Basis betrouwbaarheid op score
        if ( $hoofd_score >= 15 ) {
            $basis = 0.9;
        } elseif ( $hoofd_score >= 10 ) {
            $basis = 0.7;
        } elseif ( $hoofd_score >= 5 ) {
            $basis = 0.5;
        } else {
            $basis = 0.2;
        }

        // Bonus als er een duidelijk verschil is met de tweede beste
        $score_lijst = array_values( $scores );
        sort( $score_lijst, SORT_NUMERIC );
        $score_lijst = array_reverse( $score_lijst );

        if ( count( $score_lijst ) >= 2 && $score_lijst[0] > 0 ) {
            $verschil_ratio = ( $score_lijst[0] - $score_lijst[1] ) / $score_lijst[0];
            $basis += $verschil_ratio * self::MAX_BETROUWBAARHEID_BONUS;
        }

        return min( 1.0, $basis );
    }

    /**
     * Bereken score voor WebPage schema
     * WebPage is de standaard voor pagina's (diensten, over ons, contact, etc.)
     *
     * @param WP_Post $post                  Post object
     * @param string  $opgeschoonde_content  Opgeschoonde content
     * @param string  $ruwe_content          Ruwe content
     * @return int Score
     */
    protected function bereken_score_webpage( WP_Post $post, $opgeschoonde_content, $ruwe_content ) {
        $score = 0;

        // WebPage is voor pagina's, niet voor blog posts
        if ( $post->post_type === 'page' ) {
            $score += 6; // Hoge bonus voor pagina's
        } elseif ( $post->post_type === 'post' ) {
            // Geen WebPage voor posts - die gebruiken BlogPosting
            return 0;
        }

        // Content lengte
        $woord_aantal = str_word_count( $opgeschoonde_content );
        if ( $woord_aantal > 300 ) {
            $score += 2;
        } elseif ( $woord_aantal > 100 ) {
            $score += 1;
        }

        // Koppen structuur
        $kop_aantal = preg_match_all( '/<h[1-6][^>]*>/i', $ruwe_content );
        if ( $kop_aantal >= 2 ) {
            $score += 1;
        }

        $this->patroon_cache['webpage'] = array(
            'post_type'    => $post->post_type,
            'woord_aantal' => $woord_aantal,
            'kop_aantal'   => $kop_aantal,
            'total_score'  => $score,
        );

        return $score;
    }

    /**
     * Detecteer specifiek WebPage subtype op basis van pagina slug en titel
     *
     * @param WP_Post $post Post object
     * @return string WebPage subtype (AboutPage, ContactPage, CollectionPage, ItemPage, of WebPage)
     */
    protected function detecteer_pagina_type( WP_Post $post ) {
        $slug  = $post->post_name;
        $titel = strtolower( $post->post_title );

        // AboutPage - "Over ons" pagina's
        $about_patterns = array( 'over-ons', 'over-mij', 'about', 'about-us', 'about-me', 'wie-zijn-wij', 'ons-verhaal', 'ons-team', 'team' );
        foreach ( $about_patterns as $pattern ) {
            if ( strpos( $slug, $pattern ) !== false || strpos( $titel, str_replace( '-', ' ', $pattern ) ) !== false ) {
                return 'AboutPage';
            }
        }

        // ContactPage - Contact pagina's
        $contact_patterns = array( 'contact', 'neem-contact', 'contacteer', 'bereik-ons', 'locatie', 'adres' );
        foreach ( $contact_patterns as $pattern ) {
            if ( strpos( $slug, $pattern ) !== false || strpos( $titel, str_replace( '-', ' ', $pattern ) ) !== false ) {
                return 'ContactPage';
            }
        }

        // CollectionPage - Overzichtspagina's
        $collection_patterns = array( 'diensten', 'services', 'producten', 'products', 'portfolio', 'projecten', 'cases', 'werk', 'aanbod', 'overzicht' );
        foreach ( $collection_patterns as $pattern ) {
            if ( strpos( $slug, $pattern ) !== false || strpos( $titel, str_replace( '-', ' ', $pattern ) ) !== false ) {
                return 'CollectionPage';
            }
        }

        // Standaard: WebPage
        return 'WebPage';
    }

    /**
     * Bereken score voor BlogPosting schema
     * BlogPosting is specifiek voor blog posts (post_type = 'post')
     * WebPage is voor algemene pagina's
     *
     * @param WP_Post $post                  Post object
     * @param string  $opgeschoonde_content  Opgeschoonde content
     * @param string  $ruwe_content          Ruwe content
     * @return int Score
     */
    protected function bereken_score_blogposting( WP_Post $post, $opgeschoonde_content, $ruwe_content ) {
        $score = 0;

        // Hoge bonus voor post_type 'post' - dit is de primaire indicator
        if ( $post->post_type === 'post' ) {
            $score += 6; // Hoge score zodat BlogPosting wint van Article voor posts
        } else {
            // Geen BlogPosting voor pages of andere post types
            return 0;
        }

        // Content lengte
        $woord_aantal = str_word_count( $opgeschoonde_content );
        if ( $woord_aantal > 500 ) {
            $score += 3;
        } elseif ( $woord_aantal > 200 ) {
            $score += 2;
        } elseif ( $woord_aantal > 100 ) {
            $score += 1;
        }

        // Koppen structuur
        $kop_aantal = preg_match_all( '/<h[1-6][^>]*>/i', $ruwe_content );
        if ( $kop_aantal >= 3 ) {
            $score += 2;
        } elseif ( $kop_aantal >= 1 ) {
            $score += 1;
        }

        // Categorie/tags (typisch voor blog posts)
        $categories = wp_get_post_categories( $post->ID );
        $tags       = wp_get_post_tags( $post->ID );
        if ( ! empty( $categories ) && ! empty( $tags ) ) {
            $score += 2; // Extra bonus als beide aanwezig zijn
        } elseif ( ! empty( $categories ) || ! empty( $tags ) ) {
            $score += 1;
        }

        // Publicatiedatum (blog posts hebben typisch een datum)
        if ( $post->post_date ) {
            $score += 1;
        }

        $this->patroon_cache['blogposting'] = array(
            'post_type'    => $post->post_type,
            'woord_aantal' => $woord_aantal,
            'kop_aantal'   => $kop_aantal,
            'categories'   => count( $categories ),
            'tags'         => count( $tags ),
            'total_score'  => $score,
        );

        return $score;
    }

    /**
     * Bereken score voor Product schema
     *
     * @param WP_Post $post                  Post object
     * @param string  $opgeschoonde_content  Opgeschoonde content
     * @param string  $ruwe_content          Ruwe content
     * @return int Score
     */
    protected function bereken_score_product( WP_Post $post, $opgeschoonde_content, $ruwe_content ) {
        $score = 0;

        // WooCommerce product post type krijgt hoge score
        if ( $post->post_type === 'product' ) {
            $score += 10;
        }

        // Prijs patronen
        $prijs_patronen = array(
            '/€\s*\d+[,.]?\d*/i',
            '/\d+[,.]?\d*\s*€/i',
            '/EUR\s*\d+/i',
            '/\$\s*\d+/i',
            '/prijs[:\s]+\d+/i',
            '/price[:\s]+\d+/i',
        );
        $prijs_matches = 0;
        foreach ( $prijs_patronen as $patroon ) {
            $prijs_matches += preg_match_all( $patroon, $opgeschoonde_content );
        }
        if ( $prijs_matches > 0 ) {
            $score += min( 3, $prijs_matches );
        }

        // Product gerelateerde woorden
        $product_woorden = array(
            'koop', 'bestel', 'winkelmand', 'winkelwagen', 'cart',
            'prijs', 'price', 'sku', 'voorraad', 'stock',
            'maat', 'size', 'kleur', 'color', 'merk', 'brand',
            'product', 'artikel', 'item', 'specificaties',
            'beschikbaar', 'available', 'in stock', 'op voorraad',
            'add to cart', 'toevoegen', 'afrekenen', 'checkout',
        );
        $product_matches = $this->tel_woord_matches( $opgeschoonde_content, $product_woorden );
        if ( $product_matches >= 5 ) {
            $score += 3;
        } elseif ( $product_matches >= 2 ) {
            $score += 2;
        } elseif ( $product_matches >= 1 ) {
            $score += 1;
        }

        // WooCommerce shortcodes
        if ( preg_match( '/\[product|add_to_cart|products\]/i', $ruwe_content ) ) {
            $score += 2;
        }

        $this->patroon_cache['product'] = array(
            'prijs_matches'   => $prijs_matches,
            'product_matches' => $product_matches,
            'is_woo_product'  => $post->post_type === 'product',
        );

        return $score;
    }

    /**
     * Bereken score voor FAQ schema
     *
     * @param WP_Post $post                  Post object
     * @param string  $opgeschoonde_content  Opgeschoonde content
     * @param string  $ruwe_content          Ruwe content
     * @return int Score
     */
    protected function bereken_score_faq( WP_Post $post, $opgeschoonde_content, $ruwe_content ) {
        $score = 0;

        // Tel koppen met vraagteken - dit is de sterkste indicator
        $vraag_koppen = preg_match_all( '/<h[1-6][^>]*>[^<]*\?[^<]*<\/h[1-6]>/i', $ruwe_content );
        if ( $vraag_koppen >= 5 ) {
            $score += 6; // Veel vragen = duidelijke FAQ
        } elseif ( $vraag_koppen >= 3 ) {
            $score += 5; // 3+ vragen = waarschijnlijk FAQ
        } elseif ( $vraag_koppen >= 2 ) {
            $score += 4; // 2 vragen = mogelijk FAQ (haalt drempel)
        }
        // Geen punten voor 1 vraagkop - dat is niet genoeg voor een FAQ

        // Expliciete FAQ indicatoren (strenger - alleen duidelijke FAQ patronen)
        $faq_patronen = array(
            '/\bfaq\b/i',
            '/veelgestelde vragen/i',
            '/frequently asked/i',
            '/vraag\s*&\s*antwoord/i',
            '/vragen\s*en\s*antwoorden/i',
        );
        $faq_matches = 0;
        foreach ( $faq_patronen as $patroon ) {
            $faq_matches += preg_match_all( $patroon, $opgeschoonde_content );
        }
        if ( $faq_matches >= 1 ) {
            $score += 4; // Expliciete FAQ vermelding haalt drempel
        }

        // FAQ blokken / accordions - zeer sterke indicator.
        // Naast plugin-specifieke blokken herkennen we ook schema.org FAQPage
        // microdata, zodat een pagina die zichzelf al als FAQPage markeert direct
        // wordt opgepikt (theme-agnostisch, geen vaste klassenamen).
        if ( preg_match( '~wp:yoast-seo/faq|wp:rank-math/faq|faq-block|et_pb_accordion|fusion-faqs-wrapper|fusion-accordian|itemtype=["\']https?://schema\.org/FAQPage["\']~i', $ruwe_content ) ) {
            $score += 5;
        }

        // HTML5 <details>/<summary> accordions zijn dé theme-agnostische
        // FAQ-bouwsteen: een <summary> met een vraagteken is een sterk signaal,
        // ongeacht het theme of de klassenamen. Tempered-greedy zodat elke match
        // binnen één summary blijft (telt geen vraagteken uit een volgende item).
        $summary_vragen = preg_match_all(
            '/<summary\b[^>]*>(?:(?!<\/summary>)[\s\S])*?\?(?:(?!<\/summary>)[\s\S])*?<\/summary>/i',
            $ruwe_content
        );
        if ( $summary_vragen >= 3 ) {
            $score += 6; // Veel accordion-vragen = duidelijke FAQ
        } elseif ( $summary_vragen >= 2 ) {
            $score += 4; // 2 accordion-vragen = mogelijk FAQ (haalt drempel)
        }

        // Definitielijst-FAQ: <dt>-termen die een vraag stellen. Klassiek
        // semantisch FAQ-patroon dat sommige themes en plugins gebruiken.
        $dt_vragen = preg_match_all(
            '/<dt\b[^>]*>(?:(?!<\/dt>)[\s\S])*?\?(?:(?!<\/dt>)[\s\S])*?<\/dt>/i',
            $ruwe_content
        );
        if ( $dt_vragen >= 3 ) {
            $score += 5;
        } elseif ( $dt_vragen >= 2 ) {
            $score += 4;
        }

        // Alleen als er al andere FAQ indicatoren zijn, tel ook vraagwoorden
        // Dit voorkomt false positives op normale tekst
        if ( $score >= 4 ) {
            $vraagwoord_patroon = '/(?:^|[.!?]\s*)(wat|hoe|waarom|wanneer|waar|wie|welke|waarmee|hoeveel)\s[^.?!]*\?/im';
            $vraagwoorden = preg_match_all( $vraagwoord_patroon, $opgeschoonde_content );
            if ( $vraagwoorden >= 3 ) {
                $score += 1;
            }
        }

        $this->patroon_cache['faq'] = array(
            'vraag_koppen'   => $vraag_koppen,
            'faq_matches'    => $faq_matches,
            'summary_vragen' => $summary_vragen,
            'dt_vragen'      => $dt_vragen,
            'total_score'    => $score,
        );

        return $score;
    }

    /**
     * Bereken score voor HowTo schema
     *
     * @param WP_Post $post                  Post object
     * @param string  $opgeschoonde_content  Opgeschoonde content
     * @param string  $ruwe_content          Ruwe content
     * @return int Score
     */
    protected function bereken_score_howto( WP_Post $post, $opgeschoonde_content, $ruwe_content ) {
        $score = 0;

        // HowTo schema is ALLEEN voor echte handleidingen/tutorials
        // Niet voor elke pagina met een lijst

        // Titel moet expliciet aangeven dat het een handleiding is (sterke indicator)
        $titel = strtolower( $post->post_title );
        $titel_is_howto = preg_match( '/hoe (je|u|men)|how to|stappenplan|handleiding|tutorial|guide/i', $titel );
        if ( $titel_is_howto ) {
            $score += 4; // Sterke indicator
        }

        // Alleen verder kijken als titel al aangeeft dat het een howto is
        // OF als er expliciete stap-patronen zijn
        $nummer_woorden = 'een|één|twee|drie|vier|vijf|zes|zeven|acht|negen|tien|eerste|tweede|derde|vierde|vijfde|zesde|zevende|achtste|negende|tiende';
        $stap_patronen = array(
            '/stap\s*\d+/i',                                    // Stap 1, Stap 2
            '/step\s*\d+/i',                                    // Step 1, Step 2
            '/stap\s+(' . $nummer_woorden . ')/iu',             // Stap één, Stap twee, etc.
        );
        $stap_matches = 0;
        foreach ( $stap_patronen as $patroon ) {
            $stap_matches += preg_match_all( $patroon, $opgeschoonde_content );
        }

        // Meerdere expliciete stappen nodig
        if ( $stap_matches >= 3 ) {
            $score += 3;
        }

        // Geordende lijsten alleen tellen als er ook andere HowTo indicatoren zijn
        if ( $score >= 3 ) {
            $ol_matches = preg_match_all( '/<ol[^>]*>/i', $ruwe_content );
            if ( $ol_matches >= 1 ) {
                $score += 1;
            }
        }

        $this->patroon_cache['howto'] = array(
            'titel_is_howto' => $titel_is_howto ? true : false,
            'stap_matches'   => $stap_matches,
            'total_score'    => $score,
        );

        return $score;
    }

    /**
     * Bereken score voor Video schema
     *
     * @param WP_Post $post                  Post object
     * @param string  $opgeschoonde_content  Opgeschoonde content
     * @param string  $ruwe_content          Ruwe content
     * @return int Score
     */
    protected function bereken_score_video( WP_Post $post, $opgeschoonde_content, $ruwe_content ) {
        $score = 0;

        // Video embeds (YouTube, Vimeo, etc.)
        $embed_patronen = array(
            '/youtube\.com\/embed/i',
            '/youtube\.com\/watch/i',
            '/youtu\.be/i',
            '/vimeo\.com/i',
            '/wistia\.com/i',
            '/dailymotion\.com/i',
            '/player\.vimeo\.com/i',
        );
        $embed_matches = 0;
        foreach ( $embed_patronen as $patroon ) {
            $embed_matches += preg_match_all( $patroon, $ruwe_content );
        }
        if ( $embed_matches >= 1 ) {
            $score += 5;
        }

        // HTML5 video tag
        if ( preg_match( '/<video[^>]*>/i', $ruwe_content ) ) {
            $score += 5;
        }

        // Iframe met video
        if ( preg_match( '/<iframe[^>]*(youtube|vimeo|video)[^>]*>/i', $ruwe_content ) ) {
            $score += 4;
        }

        // WordPress video blok
        if ( preg_match( '/wp:video|wp:embed|wp:core-embed/i', $ruwe_content ) ) {
            $score += 3;
        }

        // Video shortcodes
        if ( preg_match( '/\[video|embed|youtube|vimeo\]/i', $ruwe_content ) ) {
            $score += 3;
        }

        // Divi/Elementor video modules
        if ( preg_match( '/et_pb_video|elementor-widget-video/i', $ruwe_content ) ) {
            $score += 3;
        }

        $this->patroon_cache['video'] = array(
            'embed_matches' => $embed_matches,
        );

        return $score;
    }

    /**
     * Bereken score voor Image schema
     *
     * @param WP_Post $post                  Post object
     * @param string  $opgeschoonde_content  Opgeschoonde content
     * @param string  $ruwe_content          Ruwe content
     * @return int Score
     */
    protected function bereken_score_image( WP_Post $post, $opgeschoonde_content, $ruwe_content ) {
        $score = 0;

        // ImageObject schema voor pagina's met significante afbeeldingen

        // Featured image
        $has_featured = has_post_thumbnail( $post->ID );
        if ( $has_featured ) {
            $score += 2;
        }

        // Tel img tags in content
        $img_matches = preg_match_all( '/<img[^>]*>/i', $ruwe_content );
        if ( $img_matches === false ) {
            $img_matches = 0;
        }
        if ( $img_matches >= 3 ) {
            $score += 3; // 3+ afbeeldingen = haalt drempel met featured image
        } elseif ( $img_matches >= 1 ) {
            $score += 2;
        }

        // Gallery shortcodes - sterke indicator
        $has_gallery = preg_match( '/\[gallery/i', $ruwe_content );
        if ( $has_gallery ) {
            $score += 4;
        }

        // WordPress gallery blokken
        $wp_gallery_blocks = preg_match_all( '/wp:gallery/i', $ruwe_content );
        if ( $wp_gallery_blocks >= 1 ) {
            $score += 4;
        }

        // Divi/Elementor image/gallery modules
        if ( preg_match( '/et_pb_image|et_pb_gallery|elementor-widget-image/i', $ruwe_content ) ) {
            $score += 2;
        }

        $this->patroon_cache['image'] = array(
            'has_featured'     => $has_featured,
            'img_matches'      => $img_matches,
            'has_gallery'      => $has_gallery ? true : false,
            'wp_gallery_blocks'=> $wp_gallery_blocks,
            'total_score'      => $score,
        );

        return $score;
    }

    /**
     * Bereken score voor Event schema
     *
     * @param WP_Post $post                  Post object
     * @param string  $opgeschoonde_content  Opgeschoonde content
     * @param string  $ruwe_content          Ruwe content
     * @return int Score
     */
    protected function bereken_score_event( WP_Post $post, $opgeschoonde_content, $ruwe_content ) {
        $score = 0;

        // Event post types
        $event_post_types = array( 'tribe_events', 'event', 'events', 'em_event' );
        if ( in_array( $post->post_type, $event_post_types, true ) ) {
            $score += 10;
        }

        // Datum patronen (toekomstige data)
        $datum_patronen = array(
            '/\d{1,2}[-\/]\d{1,2}[-\/]20\d{2}/i',
            '/\d{1,2}\s+(jan|feb|mrt|apr|mei|jun|jul|aug|sep|okt|nov|dec)[a-z]*\s+20\d{2}/i',
            '/20\d{2}[-\/]\d{1,2}[-\/]\d{1,2}/i',
        );
        $datum_matches = 0;
        foreach ( $datum_patronen as $patroon ) {
            $datum_matches += preg_match_all( $patroon, $opgeschoonde_content );
        }
        if ( $datum_matches >= 2 ) {
            $score += 2;
        } elseif ( $datum_matches >= 1 ) {
            $score += 1;
        }

        // Event woorden
        $event_woorden = array(
            'evenement', 'event', 'workshop', 'seminar', 'webinar',
            'conferentie', 'conference', 'bijeenkomst', 'meeting',
            'training', 'cursus', 'festival', 'concert',
            'aanmelden', 'register', 'inschrijven', 'tickets',
            'locatie', 'location', 'venue', 'wanneer', 'when',
            'deuren open', 'start', 'einde', 'end',
        );
        $event_matches = $this->tel_woord_matches( $opgeschoonde_content, $event_woorden );
        if ( $event_matches >= 5 ) {
            $score += 3;
        } elseif ( $event_matches >= 3 ) {
            $score += 2;
        } elseif ( $event_matches >= 1 ) {
            $score += 1;
        }

        // Tijdstippen
        $tijd_matches = preg_match_all( '/\d{1,2}[:.]\d{2}(\s*(uur|u|am|pm))?/i', $opgeschoonde_content );
        if ( $tijd_matches >= 2 ) {
            $score += 1;
        }

        $this->patroon_cache['event'] = array(
            'datum_matches' => $datum_matches,
            'event_matches' => $event_matches,
            'tijd_matches'  => $tijd_matches,
        );

        return $score;
    }

    /**
     * Bereken score voor Recipe schema
     *
     * @param WP_Post $post                  Post object
     * @param string  $opgeschoonde_content  Opgeschoonde content
     * @param string  $ruwe_content          Ruwe content
     * @return int Score
     */
    protected function bereken_score_recipe( WP_Post $post, $opgeschoonde_content, $ruwe_content ) {
        $score = 0;

        // Recept post types
        if ( in_array( $post->post_type, array( 'recipe', 'recipes', 'wprm_recipe' ), true ) ) {
            $score += 10;
        }

        // Ingrediënten woorden
        $ingredient_woorden = array(
            'ingrediënt', 'ingredient', 'gram', 'kilo', 'liter', 'ml',
            'eetlepel', 'theelepel', 'snufje', 'mespunt',
            'cup', 'tablespoon', 'teaspoon', 'tbsp', 'tsp',
        );
        $ingredient_matches = $this->tel_woord_matches( $opgeschoonde_content, $ingredient_woorden );
        if ( $ingredient_matches >= 5 ) {
            $score += 3;
        } elseif ( $ingredient_matches >= 2 ) {
            $score += 2;
        }

        // Recept woorden
        $recept_woorden = array(
            'recept', 'recipe', 'bereid', 'bak', 'kook',
            'ingrediënten', 'ingredients', 'bereiding', 'preparation',
            'porties', 'servings', 'bereidingstijd', 'prep time',
            'voedingswaarde', 'nutrition', 'calorieën', 'calories',
        );
        $recept_matches = $this->tel_woord_matches( $opgeschoonde_content, $recept_woorden );
        if ( $recept_matches >= 4 ) {
            $score += 3;
        } elseif ( $recept_matches >= 2 ) {
            $score += 2;
        }

        // Recept plugin blokken
        if ( preg_match( '/wprm-recipe|recipe-card|tasty-recipe/i', $ruwe_content ) ) {
            $score += 4;
        }

        $this->patroon_cache['recipe'] = array(
            'ingredient_matches' => $ingredient_matches,
            'recept_matches'     => $recept_matches,
        );

        return $score;
    }

    /**
     * Bereken score voor LocalBusiness schema
     *
     * @param WP_Post $post                  Post object
     * @param string  $opgeschoonde_content  Opgeschoonde content
     * @param string  $ruwe_content          Ruwe content
     * @return int Score
     */
    protected function bereken_score_local_business( WP_Post $post, $opgeschoonde_content, $ruwe_content ) {
        $score = 0;

        // Adres patronen
        $adres_patronen = array(
            '/\d{4}\s*[A-Z]{2}/i',  // Nederlandse postcode
            '/\d{5}/i',             // Cijfer postcode
            '/straat|street|weg|laan|plein|avenue/i',
        );
        $adres_matches = 0;
        foreach ( $adres_patronen as $patroon ) {
            $adres_matches += preg_match_all( $patroon, $opgeschoonde_content );
        }
        if ( $adres_matches >= 2 ) {
            $score += 2;
        }

        // Telefoonnummers
        $telefoon_matches = preg_match_all( '/(\+31|0031|0)\d{2}[\s-]?\d{7}/i', $opgeschoonde_content );
        if ( $telefoon_matches >= 1 ) {
            $score += 2;
        }

        // Openingstijden
        $tijd_woorden = array(
            'openingstijden', 'opening hours', 'geopend', 'open',
            'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag',
            'monday', 'tuesday', 'wednesday', 'thursday', 'friday',
        );
        $tijd_matches = $this->tel_woord_matches( $opgeschoonde_content, $tijd_woorden );
        if ( $tijd_matches >= 3 ) {
            $score += 2;
        }

        // Lokale business woorden
        $business_woorden = array(
            'contact', 'adres', 'address', 'locatie', 'location',
            'bezoek', 'visit', 'ons bedrijf', 'our company',
            'route', 'directions', 'parkeren', 'parking',
        );
        $business_matches = $this->tel_woord_matches( $opgeschoonde_content, $business_woorden );
        if ( $business_matches >= 3 ) {
            $score += 2;
        }

        $this->patroon_cache['local_business'] = array(
            'adres_matches'    => $adres_matches,
            'telefoon_matches' => $telefoon_matches,
            'tijd_matches'     => $tijd_matches,
            'business_matches' => $business_matches,
        );

        return $score;
    }

    /**
     * Tel het aantal matches voor een lijst met woorden
     *
     * @param string $content Content om te doorzoeken
     * @param array  $woorden Lijst met te zoeken woorden
     * @return int Aantal matches
     */
    protected function tel_woord_matches( $content, $woorden ) {
        $content = strtolower( $content );
        $matches = 0;

        foreach ( $woorden as $woord ) {
            $matches += substr_count( $content, strtolower( $woord ) );
        }

        return $matches;
    }


    /**
     * Extraheer main content uit volledige pagina HTML via DOMDocument + XPath.
     *
     * Twee passes, theme-agnostisch:
     *   1. Bekende content-containers (entry-content, article, main, etc.).
     *   2. Body-fallback voor custom/block-themes zonder herkenbare container:
     *      de hele <body> minus chrome (nav/header/footer/aside/script/svg).
     * Zo bereikt ook accordion-content (FAQ via <details>/<summary>) de scorer,
     * ongeacht welke klassenamen het theme gebruikt.
     *
     * @param string $html Volledige pagina HTML
     * @return string Geëxtraheerde content of lege string
     */
    private function extract_main_content( $html ) {
        if ( empty( $html ) ) {
            return '';
        }

        $doc = new DOMDocument();
        @$doc->loadHTML( mb_encode_numericentity( $html, array( 0x80, 0x10FFFF, 0, 0x1FFFFF ), 'UTF-8' ), LIBXML_NOERROR | LIBXML_NOWARNING );
        $xpath = new DOMXPath( $doc );

        $selectors = array(
            '//*[contains(@class,"entry-content")]',
            '//*[contains(@class,"post-content")]',
            '//*[contains(@class,"page-content")]',
            '//*[contains(@class,"the-content")]',
            '//*[contains(@class,"article-content")]',
            '//*[contains(@class,"site-content")]',
            '//*[contains(@class,"content-area")]',
            '//*[@id="content"]',
            '//*[@role="main"]',
            '//article',
            '//main',
        );

        // Pass 1: bekende content-containers, op specificiteit-volgorde. De eerste
        // container met substantiële content (>= 100 woorden) wint meteen, zodat
        // de smalle .entry-content vóór de brede <main>/<article> blijft gaan.
        // We onthouden ook de rijkste magere match: sommige themes hebben kleine
        // decoy-<article>/<main> elementen (bijv. hulp-kaartjes onderaan een
        // FAQ-pagina) die wél matchen maar niet de hoofdcontent zijn.
        $best_known = '';
        $best_words = 0;
        foreach ( $selectors as $selector ) {
            $nodes = $xpath->query( $selector );
            if ( ! $nodes ) {
                continue;
            }
            foreach ( $nodes as $node ) {
                $candidate = $this->strip_chrome( $doc->saveHTML( $node ) );
                $words     = str_word_count( wp_strip_all_tags( $candidate ) );
                if ( $words >= 100 ) {
                    return $candidate;
                }
                if ( $words > $best_words ) {
                    $best_words = $words;
                    $best_known = $candidate;
                }
            }
        }

        // Pass 2: body-fallback (theme-agnostisch). We komen hier alleen als geen
        // enkele bekende container substantiële content had — typisch een custom/
        // block-theme met een eigen klassenaam, of een FAQ-pagina waar de
        // <details>/<summary> accordions buiten elke herkenbare container staan.
        // We nemen dan de hele <body> minus chrome (nav/header/footer/aside/script/
        // style/svg) en gebruiken die als hij meer bevat dan de magere Pass 1-match.
        // Content-rijke standaard-themes komen hier nooit: die zijn al teruggegeven.
        $body = $xpath->query( '//body' );
        if ( $body && $body->length > 0 ) {
            $body_html  = $this->strip_chrome( $doc->saveHTML( $body->item( 0 ) ) );
            $body_words = str_word_count( wp_strip_all_tags( $body_html ) );
            if ( $body_words > $best_words && $body_words > 20 ) {
                return $body_html;
            }
        }

        if ( $best_words > 20 ) {
            return $best_known;
        }

        return '';
    }

    /**
     * Strip niet-content chrome uit een HTML-fragment.
     *
     * Theme-agnostisch: werkt op semantische tags (nav/header/footer/aside) en
     * scripts/styles/svg, niet op specifieke klassenamen. Draait op DOM-genormaliseerde
     * HTML (saveHTML), dus de tags zijn correct gesloten en de regex strip betrouwbaar.
     *
     * @param string $html HTML-fragment
     * @return string Opgeschoond fragment
     */
    private function strip_chrome( $html ) {
        $patterns = array(
            '/<header[^>]*>.*?<\/header>/si',
            '/<nav[^>]*>.*?<\/nav>/si',
            '/<aside[^>]*>.*?<\/aside>/si',
            '/<footer[^>]*>.*?<\/footer>/si',
            '/<form[^>]*>.*?<\/form>/si',
            '/<script[^>]*>.*?<\/script>/si',
            '/<style[^>]*>.*?<\/style>/si',
            '/<noscript[^>]*>.*?<\/noscript>/si',
            '/<svg[^>]*>.*?<\/svg>/si',
        );

        return preg_replace( $patterns, '', $html );
    }
}
