<?php
/**
 * DBA_SEO_Sniper_Analyzer — Competitive intelligence engine with relevance filtering
 *
 * Takes SERP results and:
 *   1. AUTO-FILTERS irrelevant domains using snippet + title + business keywords
 *   2. Fetches only relevant competitor pages
 *   3. Produces competitive analysis: word counts, headings, density, gaps, blueprint
 *
 * Relevance scoring uses:
 *   - Damalion business context keywords (Luxembourg, corporate, fund, tax, etc.)
 *   - Theme-specific keywords from the router (e.g., RAIF → "reserved alternative investment fund")
 *   - Snippet + title matching
 *   - Domain TLD/country relevance (.lu, .eu, known financial/legal domains)
 *
 * @since 2.16.0
 */
if ( ! defined( 'ABSPATH' ) ) exit;

class DBA_SEO_Sniper_Analyzer {

    /**
     * Business context keywords — terms that indicate a result is about
     * the same kind of business/services as Damalion.
     */
    private static function get_business_keywords() {
        return array(
            // Core business
            'luxembourg', 'corporate', 'company', 'business', 'formation', 'incorporation',
            'structuring', 'advisory', 'consulting', 'domiciliation', 'management company',
            // Financial / Investment
            'fund', 'investment', 'private equity', 'venture capital', 'capital', 'asset management',
            'portfolio', 'investor', 'aifm', 'ucits', 'sicav', 'sicar', 'soparfi', 'spf',
            'holding', 'securitization', 'family office', 'wealth management',
            // Legal / Regulatory
            'regulation', 'compliance', 'cssf', 'tax', 'taxation', 'fiscal', 'legal',
            'legislation', 'directive', 'law', 'regime', 'jurisdiction', 'treaty',
            'substance', 'beneficial owner', 'anti-money laundering', 'aml',
            // Geography
            'europe', 'european', 'eu', 'cross-border', 'international',
            'offshore', 'onshore', 'residence', 'permit',
            // Industry
            'fintech', 'banking', 'insurance', 'real estate', 'intellectual property',
            'trademark', 'patent', 'accounting', 'audit', 'payroll',
            // Startup / Country-specific
            'startup', 'entrepreneur', 'visa', 'immigration', 'work permit',
        );
    }

    /** Known relevant TLDs */
    private static function get_trusted_tlds() {
        return array( '.lu', '.eu' );
    }

    /** Known relevant domains — get a relevance boost */
    private static function get_trusted_domains() {
        return array(
            'cssf.lu', 'alfi.lu', 'lhoft.com', 'abbl.lu', 'guichet.lu',
            'ogier.com', 'loyensloeff.com', 'arendt.com', 'elvinger.lu',
            'atoz.lu', 'deloitte.com', 'pwc.com', 'kpmg.com', 'ey.com',
        );
    }

    /** Known irrelevant domains — always exclude */
    private static function get_blacklist_domains() {
        return array(
            'wikipedia.org', 'youtube.com', 'facebook.com', 'twitter.com', 'x.com',
            'linkedin.com', 'reddit.com', 'instagram.com', 'tiktok.com',
            'amazon.com', 'ebay.com', 'pinterest.com',
        );
    }

    /** Minimum relevance score to include a competitor (0-100) */
    const RELEVANCE_THRESHOLD = 35;

    /* ══════════════════════════════════════════════════════════════
     *  MAIN ANALYSIS
     * ══════════════════════════════════════════════════════════════ */

    public static function analyze( $keyword, $serp, $language = 'en' ) {
        $start = microtime( true );

        $analysis = array(
            'keyword'               => $keyword,
            'language'              => $language,
            'date'                  => current_time( 'mysql' ),
            'total_results'         => count( $serp ),
            'competitors'           => array(),
            'filtered_out'          => array(),
            'our_position'          => 0,
            'our_url'               => '',
            'metrics'               => array(),
            'heading_union'         => array(),
            'content_gaps'          => array(),
            'structural_advantages' => array(),
            'blueprint'             => array(),
        );

        // Check if damalion.com is in the results
        foreach ( $serp as $r ) {
            if ( stripos( $r['domain'] ?? $r['url'], 'damalion.com' ) !== false ) {
                $analysis['our_position'] = $r['position'];
                $analysis['our_url']      = $r['url'];
                break;
            }
        }

        // Get theme-specific keywords AND country context for relevance scoring
        $theme_keywords  = self::get_theme_keywords( $keyword, $language );
        $country_context = self::get_country_context( $keyword, $language );

        // ── Step 1: Score relevance of each SERP result ──
        $scored_results = array();
        foreach ( $serp as $r ) {
            $domain = $r['domain'] ?? wp_parse_url( $r['url'] ?? '', PHP_URL_HOST );
            $domain = preg_replace( '/^www\./', '', $domain ?? '' );

            // Skip our own domain
            if ( stripos( $domain, 'damalion.com' ) !== false ) continue;

            $relevance = self::score_relevance( $r, $keyword, $theme_keywords, $country_context );

            $scored_results[] = array_merge( $r, array(
                'domain'            => $domain,
                'relevance_score'   => $relevance['score'],
                'relevance_label'   => $relevance['label'],
                'relevance_reasons' => $relevance['reasons'],
                'is_relevant'       => $relevance['score'] >= self::RELEVANCE_THRESHOLD,
            ) );
        }

        // ── Step 2: Separate relevant from filtered ──
        $relevant = array_filter( $scored_results, function( $r ) { return $r['is_relevant']; } );
        $filtered = array_filter( $scored_results, function( $r ) { return ! $r['is_relevant']; } );

        foreach ( $filtered as $f ) {
            $analysis['filtered_out'][] = array(
                'position' => $f['position'],
                'domain'   => $f['domain'],
                'title'    => $f['title'] ?? '',
                'score'    => $f['relevance_score'],
                'reason'   => $f['relevance_label'],
            );
            error_log( 'DBA Sniper: ✕ Filtered #' . $f['position'] . ' ' . $f['domain'] . ' (score: ' . $f['relevance_score'] . ' — ' . $f['relevance_label'] . ')' );
        }

        error_log( 'DBA Sniper: Relevance filter — ' . count( $relevant ) . ' relevant, ' . count( $filtered ) . ' filtered for "' . $keyword . '"' );

        // ── Step 3: Fetch and analyze relevant competitor pages ──
        $pages = array();
        $fetched = 0;
        foreach ( $relevant as $r ) {
            if ( $fetched >= 10 ) break;

            $page_data = DBA_SEO_Sniper_Crawler::analyze_page( $r['url'], $keyword );
            if ( $page_data && $page_data['word_count'] > 100 ) {
                $page_data['position']        = $r['position'];
                $page_data['title']           = $r['title'];
                $page_data['domain']          = $r['domain'];
                $page_data['snippet']         = $r['snippet'] ?? '';
                $page_data['relevance_score'] = $r['relevance_score'];
                $page_data['relevance_label'] = $r['relevance_label'];
                $pages[] = $page_data;
                $fetched++;
            }

            usleep( 200000 );
        }

        $analysis['competitors'] = $pages;

        if ( empty( $pages ) ) {
            error_log( 'DBA Sniper: No relevant competitor pages for "' . $keyword . '"' );
            $analysis['metrics'] = self::default_metrics();
            return $analysis;
        }

        $analysis['metrics']                = self::compute_metrics( $pages, $keyword );
        $analysis['heading_union']          = self::build_heading_union( $pages );
        $analysis['structural_advantages']  = self::find_structural_advantages( $pages );
        $analysis['blueprint']              = self::build_blueprint( $analysis );

        $elapsed = round( microtime( true ) - $start, 1 );
        error_log( 'DBA Sniper: Analysis complete — ' . count( $pages ) . ' relevant pages in ' . $elapsed . 's' );

        // ── Persist analysis to dba_sniper_keywords for use by ANY bot at generation time ──
        self::persist_analysis( $keyword, $language, $analysis );

        return $analysis;
    }

    /* ══════════════════════════════════════════════════════════════
     *  PERSIST ANALYSIS — store to dba_sniper_keywords for cron use
     *
     *  When Sniper analyzes a keyword, it stores the competitive
     *  intelligence persistently so ANY bot can read it during
     *  regular cron-triggered generation (not just Sniper-triggered).
     * ══════════════════════════════════════════════════════════════ */

    private static function persist_analysis( $keyword, $language, $analysis ) {
        $kw_key   = sanitize_key( $keyword . '_' . $language );
        $keywords = get_option( 'dba_sniper_keywords', array() );

        if ( ! isset( $keywords[ $kw_key ] ) ) {
            // Keyword not tracked — nothing to persist to
            return;
        }

        // Store the full competitive intel that bots need at generation time
        $keywords[ $kw_key ]['last_analysis'] = array(
            'date'                  => current_time( 'mysql' ),
            'our_position'          => $analysis['our_position'] ?? 0,
            'competitor_count'      => count( $analysis['competitors'] ?? array() ),
            'metrics'               => $analysis['metrics'] ?? array(),
            'heading_union'         => array_slice( $analysis['heading_union'] ?? array(), 0, 10 ),
            'structural_advantages' => $analysis['structural_advantages'] ?? array(),
            'blueprint'             => $analysis['blueprint'] ?? array(),
            // Store top 5 competitor summaries (not full page data — too big)
            'top_competitors'       => array_map( function( $c ) {
                return array(
                    'position'        => $c['position'] ?? 0,
                    'domain'          => $c['domain'] ?? '',
                    'word_count'      => $c['word_count'] ?? 0,
                    'h2_count'        => count( $c['headings']['h2'] ?? array() ),
                    'keyword_density' => $c['keyword_density'] ?? 0,
                    'has_faq'         => ! empty( $c['has_faq'] ),
                    'has_table'       => ! empty( $c['has_table'] ),
                    'has_schema'      => ! empty( $c['has_schema'] ),
                );
            }, array_slice( $analysis['competitors'] ?? array(), 0, 5 ) ),
        );

        update_option( 'dba_sniper_keywords', $keywords, false );
        error_log( 'DBA Sniper: Persisted analysis for "' . $keyword . '" (' . $language . ') → ' . count( $analysis['structural_advantages'] ?? array() ) . ' weaknesses stored' );
    }

    /* ══════════════════════════════════════════════════════════════
     *  RELEVANCE SCORING
     * ══════════════════════════════════════════════════════════════ */

    private static function score_relevance( $result, $keyword, $theme_keywords = array(), $country_context = array() ) {
        $score   = 0;
        $reasons = array();
        $domain  = $result['domain'] ?? '';
        $title   = mb_strtolower( $result['title'] ?? '' );
        $snippet = mb_strtolower( $result['snippet'] ?? '' );
        $url_str = mb_strtolower( $result['url'] ?? '' );
        $text    = $title . ' ' . $snippet;
        $kw_lower = mb_strtolower( $keyword );

        // ── Blacklist check (instant exclude) ──
        foreach ( self::get_blacklist_domains() as $bl ) {
            if ( stripos( $domain, $bl ) !== false ) {
                return array( 'score' => 0, 'label' => 'Blacklisted (social/marketplace)', 'reasons' => array( 'Social/marketplace domain' ) );
            }
        }

        // ══════════════════════════════════════════════════════════
        //  COUNTRY CONTEXT — the hardest filter
        //  If we know the country (e.g., Luxembourg), results WITHOUT
        //  country mentions get a heavy penalty.
        // ══════════════════════════════════════════════════════════

        $country_matched = false;
        $country_name    = $country_context['country'] ?? '';
        $country_terms   = $country_context['terms'] ?? array(); // e.g. ['luxembourg', 'lux', 'luxemburg', 'luxembourgeois']

        if ( ! empty( $country_terms ) ) {
            foreach ( $country_terms as $ct ) {
                if ( stripos( $text, $ct ) !== false || stripos( $url_str, $ct ) !== false || stripos( $domain, $ct ) !== false ) {
                    $country_matched = true;
                    break;
                }
            }

            if ( $country_matched ) {
                $score += 25;
                $reasons[] = 'Country match (' . $country_name . ')';
            } else {
                // NO country mention → heavy penalty
                $score -= 20;
                $reasons[] = 'NO country context (' . $country_name . ')';
            }
        }

        // ── Trusted domain bonus ──
        foreach ( self::get_trusted_domains() as $td ) {
            if ( stripos( $domain, $td ) !== false ) {
                $score += 30;
                $reasons[] = 'Trusted domain';
                break;
            }
        }

        // ── Trusted TLD bonus ──
        $country_tlds = $country_context['tlds'] ?? array();
        $all_tlds = array_merge( self::get_trusted_tlds(), $country_tlds );
        foreach ( $all_tlds as $tld ) {
            if ( substr( $domain, -strlen( $tld ) ) === $tld ) {
                $score += 15;
                $reasons[] = 'Relevant TLD (' . $tld . ')';
                break;
            }
        }

        // ── Keyword in title ──
        if ( stripos( $title, $kw_lower ) !== false ) {
            $score += 15;
            $reasons[] = 'Keyword in title';
        }

        // ── Keyword in snippet ──
        if ( stripos( $snippet, $kw_lower ) !== false ) {
            $score += 10;
            $reasons[] = 'Keyword in snippet';
        }

        // ── Business context keywords in title + snippet ──
        $biz_hits = 0;
        foreach ( self::get_business_keywords() as $bk ) {
            if ( stripos( $text, $bk ) !== false ) {
                $biz_hits++;
            }
        }
        if ( $biz_hits >= 4 ) {
            $score += 25;
            $reasons[] = $biz_hits . ' business keywords';
        } elseif ( $biz_hits >= 2 ) {
            $score += 15;
            $reasons[] = $biz_hits . ' business keywords';
        } elseif ( $biz_hits >= 1 ) {
            $score += 5;
            $reasons[] = $biz_hits . ' business keyword';
        }

        // ── Theme-specific keywords ──
        $theme_hits = 0;
        foreach ( $theme_keywords as $tk ) {
            if ( stripos( $text, mb_strtolower( $tk ) ) !== false ) {
                $theme_hits++;
            }
        }
        if ( $theme_hits >= 2 ) {
            $score += 20;
            $reasons[] = $theme_hits . ' theme keywords';
        } elseif ( $theme_hits >= 1 ) {
            $score += 10;
            $reasons[] = '1 theme keyword';
        }

        // ── Domain contains keyword — only if ALSO has country + business ──
        if ( stripos( $domain, str_replace( ' ', '', $kw_lower ) ) !== false ) {
            if ( $country_matched && $biz_hits >= 1 ) {
                $score += 10;
                $reasons[] = 'Domain + country + business';
            } elseif ( $biz_hits >= 2 ) {
                $score += 5;
                $reasons[] = 'Domain + some business context';
            } else {
                // Domain matches but NO country, NO business → likely false positive
                $reasons[] = 'Domain matches but NO country/business context';
            }
        }

        // ── Financial/legal domain pattern ──
        $legal_patterns = array( 'law', 'legal', 'avocats', 'rechts', 'notaire', 'fiducia', 'tax', 'audit', 'consult' );
        foreach ( $legal_patterns as $lp ) {
            if ( stripos( $domain, $lp ) !== false ) {
                $score += 10;
                $reasons[] = 'Legal/financial domain';
                break;
            }
        }

        // ── Floor at 0 ──
        $score = max( 0, min( 100, $score ) );

        // ── Label ──
        if ( $score >= 60 )      $label = 'Highly relevant';
        elseif ( $score >= 40 )  $label = 'Relevant';
        elseif ( $score >= 35 )  $label = 'Marginally relevant';
        else                     $label = 'Not relevant — filtered';

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

    /**
     * Get theme-specific keywords from the registry for relevance matching.
     */
    private static function get_theme_keywords( $keyword, $language ) {
        $extra = array();

        if ( class_exists( 'DBA_SEO_Sniper_Router' ) ) {
            $route = DBA_SEO_Sniper_Router::resolve( $keyword, $language );
            if ( ! is_wp_error( $route ) && ! empty( $route['bot_id'] ) ) {
                $bot_id     = $route['bot_id'];
                $theme_slug = $route['theme_slug'] ?? '';

                if ( $bot_id === 'luxstrat' && class_exists( 'DBA_LuxStrat_Registry' ) ) {
                    $theme = DBA_LuxStrat_Registry::get_theme( $theme_slug );
                    if ( $theme ) {
                        $extra = array_merge( $extra, $theme['keywords'] ?? array() );
                        if ( ! empty( $theme['label'] ) ) $extra[] = $theme['label'];
                    }
                }

                if ( strpos( $bot_id, 'luxstrat-intl-' ) === 0 && class_exists( 'DBA_LuxStrat_Intl_Loader' ) ) {
                    $lang_code = str_replace( 'luxstrat-intl-', '', $bot_id );
                    $lang_cfg  = DBA_LuxStrat_Intl_Loader::get_language( $lang_code );
                    if ( $lang_cfg && isset( $lang_cfg['themes'][ $theme_slug ] ) ) {
                        $t = $lang_cfg['themes'][ $theme_slug ];
                        $extra = array_merge( $extra, $t['keywords'] ?? array() );
                        if ( ! empty( $t['label'] ) ) $extra[] = $t['label'];
                    }
                }
            }
        }

        return array_unique( array_filter( $extra ) );
    }

    /**
     * Determine country context from bot routing.
     *
     * Maps bot_id + theme → country name + search terms + TLDs.
     * This is the HARD filter: results without country context get penalized.
     *
     * @return array { country, terms[], tlds[] }
     */
    private static function get_country_context( $keyword, $language ) {
        $context = array( 'country' => '', 'terms' => array(), 'tlds' => array() );

        // Country database
        $countries = array(
            'luxembourg' => array(
                'terms' => array( 'luxembourg', 'luxemburg', 'lux', 'luxembourgeois', 'grand duchy', 'grand-duché', 'cssf' ),
                'tlds'  => array( '.lu' ),
            ),
            'france' => array(
                'terms' => array( 'france', 'français', 'french', 'paris' ),
                'tlds'  => array( '.fr' ),
            ),
            'italy' => array(
                'terms' => array( 'italy', 'italia', 'italian', 'italiano', 'rome', 'roma', 'milan', 'milano' ),
                'tlds'  => array( '.it' ),
            ),
            'spain' => array(
                'terms' => array( 'spain', 'españa', 'spanish', 'español', 'madrid', 'barcelona' ),
                'tlds'  => array( '.es' ),
            ),
            'portugal' => array(
                'terms' => array( 'portugal', 'portuguese', 'português', 'lisbon', 'lisboa' ),
                'tlds'  => array( '.pt' ),
            ),
            'germany' => array(
                'terms' => array( 'germany', 'deutschland', 'german', 'deutsch', 'berlin', 'munich', 'münchen' ),
                'tlds'  => array( '.de' ),
            ),
            'netherlands' => array(
                'terms' => array( 'netherlands', 'dutch', 'holland', 'amsterdam', 'nederland' ),
                'tlds'  => array( '.nl' ),
            ),
            'belgium' => array(
                'terms' => array( 'belgium', 'belge', 'belgian', 'belgique', 'brussels', 'bruxelles' ),
                'tlds'  => array( '.be' ),
            ),
            'czech_republic' => array(
                'terms' => array( 'czech', 'czech republic', 'czechia', 'prague', 'praha' ),
                'tlds'  => array( '.cz' ),
            ),
            'poland' => array(
                'terms' => array( 'poland', 'polish', 'polska', 'warsaw', 'warszawa' ),
                'tlds'  => array( '.pl' ),
            ),
            'romania' => array(
                'terms' => array( 'romania', 'romanian', 'bucharest', 'bucurești' ),
                'tlds'  => array( '.ro' ),
            ),
            'hungary' => array(
                'terms' => array( 'hungary', 'hungarian', 'budapest', 'magyar' ),
                'tlds'  => array( '.hu' ),
            ),
            'brazil' => array(
                'terms' => array( 'brazil', 'brasil', 'brazilian', 'são paulo', 'rio' ),
                'tlds'  => array( '.br' ),
            ),
            'mexico' => array(
                'terms' => array( 'mexico', 'méxico', 'mexican', 'mexicano' ),
                'tlds'  => array( '.mx' ),
            ),
            'argentina' => array(
                'terms' => array( 'argentina', 'argentino', 'buenos aires' ),
                'tlds'  => array( '.ar' ),
            ),
            'united_states' => array(
                'terms' => array( 'united states', 'usa', 'u.s.', 'american', 'delaware', 'new york', 'california' ),
                'tlds'  => array( '.us' ),
            ),
        );

        // Resolve bot → country
        if ( class_exists( 'DBA_SEO_Sniper_Router' ) ) {
            $route = DBA_SEO_Sniper_Router::resolve( $keyword, $language );
            if ( ! is_wp_error( $route ) && ! empty( $route['bot_id'] ) ) {
                $bot_id     = $route['bot_id'];
                $theme_slug = $route['theme_slug'] ?? '';

                // LuxStrat EN/FR/IT → always Luxembourg
                if ( $bot_id === 'luxstrat' || strpos( $bot_id, 'luxstrat-intl-' ) === 0 ) {
                    $context = array_merge( $context, array(
                        'country' => 'Luxembourg',
                    ), $countries['luxembourg'] );
                }

                // GeoBot → theme IS the country
                if ( $bot_id === 'geobot' && isset( $countries[ $theme_slug ] ) ) {
                    $context = array_merge( $context, array(
                        'country' => ucfirst( str_replace( '_', ' ', $theme_slug ) ),
                    ), $countries[ $theme_slug ] );
                }

                // EEurope → theme IS the country
                if ( $bot_id === 'eeurope' && isset( $countries[ $theme_slug ] ) ) {
                    $context = array_merge( $context, array(
                        'country' => ucfirst( str_replace( '_', ' ', $theme_slug ) ),
                    ), $countries[ $theme_slug ] );
                }

                // LATAM → theme IS the country
                if ( $bot_id === 'latam' && isset( $countries[ $theme_slug ] ) ) {
                    $context = array_merge( $context, array(
                        'country' => ucfirst( str_replace( '_', ' ', $theme_slug ) ),
                    ), $countries[ $theme_slug ] );
                }

                // Startup Hub → US
                if ( $bot_id === 'startup-hub' ) {
                    $context = array_merge( $context, array(
                        'country' => 'United States',
                    ), $countries['united_states'] );
                }
            }
        }

        // Fallback: check if keyword itself contains a country name
        if ( empty( $context['country'] ) ) {
            $kw_lower = mb_strtolower( $keyword );
            foreach ( $countries as $slug => $c ) {
                foreach ( $c['terms'] as $term ) {
                    if ( stripos( $kw_lower, $term ) !== false ) {
                        $context = array_merge( $context, array(
                            'country' => ucfirst( str_replace( '_', ' ', $slug ) ),
                        ), $c );
                        break 2;
                    }
                }
            }
        }

        error_log( 'DBA Sniper: Country context for "' . $keyword . '" → ' . ( $context['country'] ?: 'NONE (no penalty applied)' ) );
        return $context;
    }

    /* ══════════════════════════════════════════════════════════════
     *  METRICS
     * ══════════════════════════════════════════════════════════════ */

    private static function compute_metrics( $pages, $keyword ) {
        $word_counts = array_column( $pages, 'word_count' );
        $densities   = array_column( $pages, 'keyword_density' );
        $h2_counts   = array_map( function( $p ) { return count( $p['headings']['h2'] ?? array() ); }, $pages );

        $top5_wc      = array_slice( $word_counts, 0, 5 );
        $top5_density = array_slice( $densities, 0, 5 );

        $avg_wc_top5 = ! empty( $top5_wc ) ? round( array_sum( $top5_wc ) / count( $top5_wc ) ) : 1000;
        $avg_density = ! empty( $top5_density ) ? round( array_sum( $top5_density ) / count( $top5_density ), 2 ) : 1.0;
        $avg_h2      = ! empty( $h2_counts ) ? round( array_sum( $h2_counts ) / count( $h2_counts ) ) : 5;

        $kw_in_title = count( array_filter( $pages, function( $p ) { return ! empty( $p['keyword_in_title'] ); } ) );
        $kw_in_h1    = count( array_filter( $pages, function( $p ) { return ! empty( $p['keyword_in_h1'] ); } ) );
        $kw_in_h2    = count( array_filter( $pages, function( $p ) { return ! empty( $p['keyword_in_h2'] ); } ) );
        $kw_in_first = count( array_filter( $pages, function( $p ) { return ! empty( $p['keyword_in_first_p'] ); } ) );

        return array(
            'avg_word_count_top5'    => $avg_wc_top5,
            'target_word_count'      => round( $avg_wc_top5 * 1.2 ),
            'max_word_count'         => ! empty( $word_counts ) ? max( $word_counts ) : 2000,
            'min_word_count'         => ! empty( $word_counts ) ? min( $word_counts ) : 500,
            'avg_keyword_density'    => $avg_density,
            'target_density'         => $avg_density,
            'avg_h2_count'           => $avg_h2,
            'target_h2_count'        => max( $avg_h2, 5 ),
            'keyword_in_title_pct'   => round( ( $kw_in_title / max( count( $pages ), 1 ) ) * 100 ),
            'keyword_in_h1_pct'      => round( ( $kw_in_h1 / max( count( $pages ), 1 ) ) * 100 ),
            'keyword_in_h2_pct'      => round( ( $kw_in_h2 / max( count( $pages ), 1 ) ) * 100 ),
            'keyword_in_first_p_pct' => round( ( $kw_in_first / max( count( $pages ), 1 ) ) * 100 ),
            'competitor_count'       => count( $pages ),
        );
    }

    /* ══════════════════════════════════════════════════════════════
     *  HEADING UNION
     * ══════════════════════════════════════════════════════════════ */

    private static function build_heading_union( $pages ) {
        $all_h2s = array();
        foreach ( $pages as $p ) {
            foreach ( $p['headings']['h2'] ?? array() as $h2 ) {
                $h2_clean = trim( preg_replace( '/\s+/', ' ', $h2 ) );
                if ( mb_strlen( $h2_clean ) > 5 && mb_strlen( $h2_clean ) < 200 ) {
                    $all_h2s[] = $h2_clean;
                }
            }
        }

        $unique = array();
        $seen   = array();
        foreach ( $all_h2s as $h2 ) {
            $key = mb_strtolower( $h2 );
            $is_dup = false;
            foreach ( $seen as $s ) {
                similar_text( $key, $s, $pct );
                if ( $pct > 75 ) { $is_dup = true; break; }
            }
            if ( ! $is_dup ) {
                $unique[] = $h2;
                $seen[]   = $key;
            }
        }

        $scored = array();
        foreach ( $unique as $h2 ) {
            $count = 0;
            foreach ( $all_h2s as $raw ) {
                similar_text( mb_strtolower( $h2 ), mb_strtolower( $raw ), $pct );
                if ( $pct > 60 ) $count++;
            }
            $scored[] = array( 'heading' => $h2, 'frequency' => $count );
        }
        usort( $scored, function( $a, $b ) { return $b['frequency'] - $a['frequency']; } );

        return array_slice( $scored, 0, 20 );
    }

    /* ══════════════════════════════════════════════════════════════
     *  STRUCTURAL ADVANTAGES
     * ══════════════════════════════════════════════════════════════ */

    private static function find_structural_advantages( $pages ) {
        $advantages = array();
        $total = count( $pages );

        $with_faq    = count( array_filter( $pages, function( $p ) { return ! empty( $p['has_faq'] ); } ) );
        $with_table  = count( array_filter( $pages, function( $p ) { return ! empty( $p['has_table'] ); } ) );
        $with_schema = count( array_filter( $pages, function( $p ) { return ! empty( $p['has_schema'] ); } ) );

        if ( $with_faq < $total * 0.5 ) {
            $advantages[] = array( 'type' => 'faq_schema', 'impact' => 'high',
                'label' => 'Add FAQ section with schema', 'detail' => $with_faq . '/' . $total . ' have FAQ. Earn featured snippets.' );
        }
        if ( $with_table < $total * 0.5 ) {
            $advantages[] = array( 'type' => 'comparison_table', 'impact' => 'medium',
                'label' => 'Add comparison table', 'detail' => $with_table . '/' . $total . ' use tables.' );
        }
        if ( $with_schema < $total * 0.5 ) {
            $advantages[] = array( 'type' => 'schema_markup', 'impact' => 'high',
                'label' => 'Add Article/HowTo schema', 'detail' => $with_schema . '/' . $total . ' have structured data.' );
        }

        $avg_images = array_sum( array_column( $pages, 'image_count' ) ) / max( $total, 1 );
        if ( $avg_images < 3 ) {
            $advantages[] = array( 'type' => 'more_images', 'impact' => 'medium',
                'label' => 'Add 3+ relevant images', 'detail' => 'Average only ' . round( $avg_images, 1 ) . ' images.' );
        }

        return $advantages;
    }

    /* ══════════════════════════════════════════════════════════════
     *  BLUEPRINT
     * ══════════════════════════════════════════════════════════════ */

    private static function build_blueprint( $analysis ) {
        $m = $analysis['metrics'];

        $recommended_h2s = array();
        foreach ( array_slice( $analysis['heading_union'], 0, 8 ) as $h ) {
            $recommended_h2s[] = $h['heading'];
        }

        $structure_requirements = array();
        foreach ( $analysis['structural_advantages'] as $a ) {
            if ( $a['impact'] === 'high' ) $structure_requirements[] = $a['label'];
        }

        return array(
            'target_word_count'      => $m['target_word_count'],
            'min_word_count'         => max( $m['avg_word_count_top5'], 800 ),
            'keyword_density'        => $m['target_density'],
            'recommended_h2s'        => $recommended_h2s,
            'target_h2_count'        => $m['target_h2_count'],
            'must_include'           => array(
                'keyword_in_title'   => ( $m['keyword_in_title_pct'] >= 70 ),
                'keyword_in_h1'      => ( $m['keyword_in_h1_pct'] >= 50 ),
                'keyword_in_h2'      => ( $m['keyword_in_h2_pct'] >= 40 ),
                'keyword_in_first_p' => ( $m['keyword_in_first_p_pct'] >= 50 ),
            ),
            'structure_requirements' => $structure_requirements,
            'competitor_weaknesses'  => $analysis['structural_advantages'],
        );
    }

    private static function default_metrics() {
        return array(
            'avg_word_count_top5'    => 1500, 'target_word_count' => 1800,
            'max_word_count'         => 2500, 'min_word_count'    => 500,
            'avg_keyword_density'    => 1.0,  'target_density'    => 1.0,
            'avg_h2_count'           => 5,    'target_h2_count'   => 6,
            'keyword_in_title_pct'   => 80,   'keyword_in_h1_pct' => 70,
            'keyword_in_h2_pct'      => 50,   'keyword_in_first_p_pct' => 60,
            'competitor_count'       => 0,
        );
    }
}
