<?php
/**
 * LuxStrat International — Inbound Link Scanner & No-Orphan Engine
 *
 * CRITICAL FIX: Keywords like "Holding familiale au Luxembourg : structuration,
 * fiscalité et transmission efficace" are broken into matchable fragments:
 *   -> "holding familiale", "structuration", "fiscalite", "transmission", "luxembourg"
 * These fragments actually appear across the site's French content.
 *
 * All injected links carry class="dba-lsi-link" + data-lsi-lang="xx" for safe stripping.
 */

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

class DBA_LuxStrat_Intl_Inbound {

    const LINK_CLASS              = 'dba-lsi-link';
    const LINK_PARA_CLASS         = 'dba-lsi-bridge';
    const MAX_LINKS_PER_POST      = 3;   // max outbound LSI links injected INTO a single source post
    const MAX_INBOUND_PER_TARGET  = 3;   // max inbound links pointing TO a single target post
    const MIN_PARAGRAPH_CHARS     = 80;
    const BATCH_SIZE              = 10;   // reduced from 25 — safe for shared hosting

    private static $noise = array(
        'fr' => array('le','la','les','un','une','des','du','de','au','aux','en','et','ou','est','sont','par','pour','dans','sur','avec','sans','que','qui','ce','cette','ces','son','sa','ses','leur','leurs','nos','vos','etre','avoir','faire','dit','plus','pas','nous','vous','ils','elles','tout','tous','tres','bien','mais','comme','aussi','entre','vers','peut','doit','dont','car','lors','sera','ete'),
        'de' => array('der','die','das','ein','eine','und','oder','ist','sind','von','zu','mit','fur','auf','an','in','dem','den','des','im','am','um','aus','bei','nach','uber','unter','vor','durch','gegen','ohne','bis','seit'),
        'es' => array('el','la','los','las','un','una','unos','unas','del','en','con','por','para','al','se','su','sus','mas','pero','como','entre','sin','sobre','hasta','desde','que'),
        'it' => array('il','lo','la','le','gli','un','una','dei','delle','del','in','con','per','da','su','al','alla','tra','fra','ma','come','anche','piu','non','si','che'),
    );

    /* ==============================================================
     *  BUILD KEYWORD MAP — FRAGMENT EXTRACTION
     *
     *  CRITICAL FIX: Theme configs may contain placeholder/fake URLs
     *  in pillar_urls[]. We now ALWAYS resolve to real get_permalink()
     *  when the post exists, mirroring the proven Startup Hub approach.
     *  Config URLs are only used as a lookup key for url_to_postid().
     * ============================================================== */

    /**
     * Validate that a URL is a real, reachable permalink (not a placeholder).
     * Returns the resolved permalink or false.
     */
    private static function resolve_real_url( $config_url ) {
        if ( empty( $config_url ) ) return false;

        // Try to find the post behind this URL
        $post_id = url_to_postid( $config_url );

        if ( $post_id && get_post_status( $post_id ) === 'publish' ) {
            // ALWAYS use the canonical permalink from WordPress, never the config string
            $real = get_permalink( $post_id );
            return $real ? array( 'url' => $real, 'post_id' => $post_id ) : false;
        }

        return false;
    }

    private static function build_keyword_map( $lang, $lang_config ) {
        $prefix = DBA_LuxStrat_Intl_Dedupe::meta_prefix( $lang );
        $themes = ! empty( $lang_config['themes'] ) ? $lang_config['themes'] : array();

        $map = array();
        $skipped_urls = array(); // track fake/unresolvable URLs for logging

        /* ── 1. Entries from pillar URLs in theme config ── */
        foreach ( $themes as $theme_slug => $tc ) {
            $pillar_urls = ! empty( $tc['pillar_urls'] ) ? $tc['pillar_urls'] : array();
            if ( empty( $pillar_urls ) ) continue;

            // Resolve REAL permalink — never trust the config URL blindly
            $resolved   = false;
            $config_url = $pillar_urls[0]; // Raw config value (may be fake)

            foreach ( $pillar_urls as $candidate ) {
                $resolved = self::resolve_real_url( $candidate );
                if ( $resolved ) break;
            }

            if ( ! $resolved ) {
                // None of the pillar_urls resolved to a real published post — skip this theme
                $skipped_urls[] = $theme_slug . ': ' . $config_url;
                continue;
            }

            $pillar_url   = $resolved['url'];     // Real permalink from get_permalink()
            $pillar_id    = $resolved['post_id'];  // Verified post ID
            $pillar_title = get_the_title( $pillar_id );

            $raw = array();
            if ( ! empty( $tc['keywords'] ) )    $raw = array_merge( $raw, $tc['keywords'] );
            if ( ! empty( $tc['short_label'] ) ) $raw[] = $tc['short_label'];
            if ( ! empty( $tc['label'] ) )       $raw[] = $tc['label'];

            $fragments = self::extract_fragments( $raw, $lang );

            foreach ( $fragments as $frag ) {
                $map[] = array(
                    'keyword' => $frag,
                    'url'     => $pillar_url,
                    'post_id' => $pillar_id,
                    'theme'   => $theme_slug,
                    'title'   => $pillar_title,
                );
            }
        }

        if ( ! empty( $skipped_urls ) ) {
            error_log( 'LuxStrat[' . $lang . '] Inbound WARNING: Skipped ' . count( $skipped_urls ) . ' themes with unresolvable pillar URLs: ' . implode( ', ', $skipped_urls ) );
        }

        /* ── 2. Entries from published LuxStrat auto-created posts ── */
        $wpml = self::wpml_switch( $lang );
        $posts = get_posts( array(
            'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => -1,
            'meta_query' => array( array( 'key' => $prefix . 'auto_created', 'compare' => 'EXISTS' ) ),
            'suppress_filters' => false,
        ) );
        self::wpml_restore( $wpml );

        foreach ( $posts as $post ) {
            $theme_slug = get_post_meta( $post->ID, $prefix . 'theme', true );
            $tc   = isset( $themes[ $theme_slug ] ) ? $themes[ $theme_slug ] : null;
            $url  = get_permalink( $post->ID );
            if ( ! $url || $url === false || strpos( $url, '?p=' ) !== false ) continue; // Skip draft/broken URLs
            $title = wp_strip_all_tags( $post->post_title );

            $raw = array();
            if ( $tc && ! empty( $tc['keywords'] ) )   $raw = array_merge( $raw, $tc['keywords'] );
            if ( $tc && ! empty( $tc['short_label'] ) ) $raw[] = $tc['short_label'];
            if ( $tc && ! empty( $tc['label'] ) )       $raw[] = $tc['label'];
            if ( ! empty( $title ) )                     $raw[] = $title;
            $path = trim( wp_parse_url( $url, PHP_URL_PATH ) ?: '', '/' );
            $raw[] = str_replace( array( '-', '_', '/' ), ' ', $path );

            $fragments = self::extract_fragments( $raw, $lang );

            foreach ( $fragments as $frag ) {
                $map[] = array(
                    'keyword' => $frag,
                    'url'     => $url,
                    'post_id' => $post->ID,
                    'theme'   => $theme_slug ?: '',
                    'title'   => $title,
                );
            }
        }

        // Deduplicate — prefer auto-created post URL over pillar if same keyword
        $seen = array(); $deduped = array();
        foreach ( $map as $e ) {
            $key = mb_strtolower( $e['keyword'] ) . '|' . $e['url'];
            if ( isset( $seen[ $key ] ) ) continue;
            $seen[ $key ] = true;
            $deduped[] = $e;
        }

        // Longer first
        usort( $deduped, function( $a, $b ) { return mb_strlen( $b['keyword'] ) - mb_strlen( $a['keyword'] ); } );

        $unique_kws = array_unique( array_column( $deduped, 'keyword' ) );
        error_log( 'LuxStrat[' . $lang . '] Inbound: ' . count( $deduped ) . ' entries, ' . count( $unique_kws ) . ' unique fragments, ' . count( $posts ) . ' auto-created posts, ' . count( $themes ) . ' themes'
            . ( ! empty( $skipped_urls ) ? ' (' . count( $skipped_urls ) . ' themes skipped — fake/unresolvable URLs)' : '' )
            . '.' );

        return $deduped;
    }

    /**
     * Extract matchable fragments from raw phrases.
     * "Holding familiale au Luxembourg : structuration, fiscalite"
     * -> "holding familiale", "structuration", "fiscalite", "luxembourg", etc.
     */
    private static function extract_fragments( $phrases, $lang ) {
        $stop = isset( self::$noise[ $lang ] ) ? self::$noise[ $lang ] : self::$noise['fr'];
        $stop = array_merge( $stop, array( 'lang', 'www', 'com', 'https', 'http', 'html', 'php' ) );

        $fragments = array();

        foreach ( $phrases as $phrase ) {
            if ( empty( $phrase ) ) continue;

            $clean = mb_strtolower( trim( $phrase ) );
            $clean = preg_replace( '/[:\?\!\.,;\"\x27\xC2\xAB\xC2\xBB\(\)\[\]\{\}\/\\\\\x{2013}\x{2014}\-]+/u', ' ', $clean );
            $clean = preg_replace( '/\s+/', ' ', trim( $clean ) );

            $words = explode( ' ', $clean );
            $sig = array();
            foreach ( $words as $w ) {
                $w = trim( $w );
                if ( mb_strlen( $w ) >= 3 && ! in_array( $w, $stop, true ) && ! is_numeric( $w ) ) {
                    $sig[] = $w;
                }
            }
            if ( empty( $sig ) ) continue;

            // Single words 6+ chars
            foreach ( $sig as $w ) {
                if ( mb_strlen( $w ) >= 6 ) $fragments[] = $w;
            }
            // 2-word bigrams
            for ( $i = 0; $i < count( $sig ) - 1; $i++ ) {
                $b = $sig[ $i ] . ' ' . $sig[ $i + 1 ];
                if ( mb_strlen( $b ) >= 10 ) $fragments[] = $b;
            }
            // 3-word trigrams
            for ( $i = 0; $i < count( $sig ) - 2; $i++ ) {
                $t = $sig[ $i ] . ' ' . $sig[ $i + 1 ] . ' ' . $sig[ $i + 2 ];
                if ( mb_strlen( $t ) >= 14 ) $fragments[] = $t;
            }
            // Full phrase if 2-4 sig words
            if ( count( $sig ) >= 2 && count( $sig ) <= 4 ) {
                $fragments[] = implode( ' ', $sig );
            }
        }

        $fragments = array_unique( $fragments );
        $fragments = array_filter( $fragments, function( $f ) { return mb_strlen( $f ) >= 5 && mb_strlen( $f ) <= 60; } );
        return array_values( $fragments );
    }

    /* ==============================================================
     *  SCAN BATCH
     * ============================================================== */

    /* ==============================================================
     *  SCAN BATCH — inject internal links INTO posts
     *
     *  LOGIC:
     *    1. Build keyword_map from ALL LuxStrat posts
     *    2. If target_ids given: modify THOSE posts only
     *       If no target_ids:   modify ALL LuxStrat posts (batched)
     *    3. For each post to modify, inject links to OTHER articles
     *       (self-link prevention is in inject_links_into_post)
     *    4. Max 3 auto-injected links per post
     * ============================================================== */

    public static function scan_batch( $lang, $lang_config, $offset = 0, $target_ids = array() ) {
        @set_time_limit( 60 );

        /* ── Build or retrieve cached keyword_map from ALL LuxStrat posts ── */
        $cache_key   = 'dba_lsi_kwmap_' . $lang;
        $keyword_map = ( $offset > 0 ) ? get_transient( $cache_key ) : false;
        if ( ! is_array( $keyword_map ) ) {
            $keyword_map = self::build_keyword_map( $lang, $lang_config );
            set_transient( $cache_key, $keyword_map, 600 );
        }

        if ( empty( $keyword_map ) ) {
            return array( 'done' => true, 'total' => 0, 'offset' => 0, 'linked' => 0,
                          'keywords' => 0, 'message' => 'No LuxStrat posts found.' );
        }

        /* ── Determine which posts to modify ── */
        if ( ! empty( $target_ids ) ) {
            // "Scan Selected" — modify only these specific posts (no batching needed)
            $posts_to_modify = $target_ids;
            $total           = count( $posts_to_modify );

            $linked = 0;
            foreach ( $posts_to_modify as $pid ) {
                $pid = absint( $pid );
                // Use full keyword_map; inject_links_into_post skips self-links
                $linked += self::inject_links_into_post( $pid, $keyword_map, $lang );
            }

            delete_transient( $cache_key );

            return array(
                'done'     => true,
                'total'    => $total,
                'offset'   => $total,
                'linked'   => $linked,
                'keywords' => count( array_unique( array_column( $keyword_map, 'keyword' ) ) ),
            );

        } else {
            // "Scan All" — modify all LuxStrat posts in batches
            $prefix = DBA_LuxStrat_Intl_Dedupe::meta_prefix( $lang );

            $wpml = self::wpml_switch( $lang );
            $all_lsi_ids = get_posts( array(
                'post_type'        => 'post',
                'post_status'      => 'publish',
                'posts_per_page'   => -1,
                'fields'           => 'ids',
                'meta_query'       => array( array( 'key' => $prefix . 'auto_created', 'compare' => 'EXISTS' ) ),
                'suppress_filters' => false,
                'orderby'          => 'ID',
                'order'            => 'ASC',
            ) );
            self::wpml_restore( $wpml );

            $total = count( $all_lsi_ids );

            if ( $offset >= $total ) {
                delete_transient( $cache_key );
                self::save_stats( $lang, $total, array( 'orphans' => 0, 'fixed' => 0 ) );
                return array(
                    'done' => true, 'total' => $total, 'offset' => $total, 'linked' => 0,
                    'keywords' => count( array_unique( array_column( $keyword_map, 'keyword' ) ) ),
                );
            }

            $batch  = array_slice( $all_lsi_ids, $offset, self::BATCH_SIZE );
            $linked = 0;
            foreach ( $batch as $pid ) {
                $linked += self::inject_links_into_post( $pid, $keyword_map, $lang );
            }

            $new_offset = $offset + self::BATCH_SIZE;
            return array(
                'done'     => $new_offset >= $total,
                'total'    => $total,
                'offset'   => min( $new_offset, $total ),
                'linked'   => $linked,
                'keywords' => count( array_unique( array_column( $keyword_map, 'keyword' ) ) ),
            );
        }
    }

    /* ==============================================================
     *  INJECT LINKS INTO POST — safe-zone approach (proven by GeoBot)
     * ============================================================== */

    private static function inject_links_into_post( $post_id, $keyword_map, $lang ) {
        $post = get_post( $post_id );
        if ( ! $post || empty( trim( $post->post_content ) ) ) return 0;

        $content  = $post->post_content;
        $original = $content;

        // Count our injected links specifically
        $our_count = substr_count( $content, 'class="' . self::LINK_CLASS . '"' );
        if ( $our_count >= self::MAX_LINKS_PER_POST ) return 0;

        $remaining      = self::MAX_LINKS_PER_POST - $our_count;
        $injected       = 0;
        $linked_urls    = array(); // track URLs we've linked to (1 link per target)

        // This post's own URL — never link to self
        $self_url = strtolower( rtrim( get_permalink( $post_id ) ?: '', '/' ) );

        // Collect all existing hrefs to avoid duplicate linking
        preg_match_all( '/<a[^>]+href=["\']([^"\']+)["\']/i', $content, $hm );
        $existing_hrefs = ! empty( $hm[1] )
            ? array_map( function( $u ) { return strtolower( rtrim( $u, '/' ) ); }, $hm[1] )
            : array();

        foreach ( $keyword_map as $entry ) {
            if ( $injected >= $remaining ) break;

            // VALIDATE: Skip entries with obviously fake/broken URLs
            if ( empty( $entry['url'] ) || strpos( $entry['url'], '?p=' ) !== false ) continue;
            // URL must start with http(s)
            if ( strncmp( $entry['url'], 'http', 4 ) !== 0 ) continue;

            $entry_url_lower = strtolower( rtrim( $entry['url'], '/' ) );

            // Don't self-link (by ID or by URL)
            if ( $entry['post_id'] === $post_id ) continue;
            if ( $entry_url_lower === $self_url ) continue;

            // Don't link to same URL twice in one post
            if ( in_array( $entry_url_lower, $linked_urls, true ) ) continue;

            // Don't link to a URL already present in the post
            if ( in_array( $entry_url_lower, $existing_hrefs, true ) ) continue;

            $phrase = $entry['keyword'];
            if ( mb_strlen( $phrase ) < 4 ) continue;

            // Quick check: does keyword even appear?
            if ( mb_stripos( $content, $phrase ) === false ) continue;

            // Skip CTA zone: don't link near "contact" + "Damalion"
            // (handled inside link_first_safe_mention by excluding <a> zones)

            // Inject at first safe occurrence
            $new_content = self::link_first_safe_mention(
                $content, $phrase, $entry['url'], $entry['title'], $lang
            );

            if ( $new_content !== $content ) {
                $content = $new_content;
                $injected++;
                $linked_urls[]    = $entry_url_lower;
                $existing_hrefs[] = $entry_url_lower;
                error_log( 'LuxStrat[' . $lang . '] Linked "' . $phrase . '" in #' . $post_id . ' -> ' . $entry['url'] );
            }
        }

        if ( $content !== $original && $injected > 0 ) {
            wp_update_post( array( 'ID' => $post_id, 'post_content' => $content ) );
            update_post_meta( $post_id, '_dba_lsi_' . $lang . '_linked', current_time( 'mysql' ) );
            update_post_meta( $post_id, '_dba_lsi_' . $lang . '_count', $our_count + $injected );
        }
        return $injected;
    }

    /* ==============================================================
     *  SAFE HTML LINK INJECTION (ported from GeoBot — proven method)
     *
     *  Splits HTML into safe text vs unsafe zones:
     *    UNSAFE = <a>…</a>, <h1>–<h4>, <script>, <style>, comments, tags
     *    SAFE   = everything else (plain text between tags)
     *
     *  Links the first occurrence in a SAFE zone only.
     * ============================================================== */

    private static function link_first_safe_mention( $content, $phrase, $url, $title, $lang ) {
        if ( empty( $phrase ) || mb_strlen( $phrase ) < 4 ) return $content;

        $escaped = preg_quote( $phrase, '/' );

        // Unicode word-boundary pattern
        $pattern = '/(?<![\w\p{L}\p{M}])(' . $escaped . ')(?![\w\p{L}\p{M}])/iu';

        // Split into safe text vs unsafe zones
        $unsafe_pattern = '/'
            . '(<a\s[^>]*>.*?<\/a>'           // existing links (full anchor)
            . '|<h[1-4][^>]*>.*?<\/h[1-4]>'   // headings h1-h4
            . '|<script[\s>].*?<\/script>'     // scripts
            . '|<style[\s>].*?<\/style>'       // styles
            . '|<!--.*?-->'                     // comments
            . '|<[^>]+>'                        // any HTML tag
            . ')/is';

        $parts = preg_split( $unsafe_pattern, $content, -1, PREG_SPLIT_DELIM_CAPTURE );
        if ( ! is_array( $parts ) ) return $content;

        $linked  = false;
        $rebuilt = '';

        $safe_url   = esc_url( $url );
        $safe_title = esc_attr( $title );

        foreach ( $parts as $part ) {
            if ( $linked ) {
                $rebuilt .= $part;
                continue;
            }

            // Detect unsafe zone — pass through unchanged
            if ( preg_match( '/^<(?:a\s|\/a>|h[1-4]|\/h[1-4]|script|\/script|style|\/style|!--)/i', $part )
                 || preg_match( '/^<[^>]+>$/s', $part ) ) {
                $rebuilt .= $part;
            } else {
                // Safe text zone — try to link first match
                // Skip if this text chunk is a CTA zone
                if ( stripos( $part, 'contact' ) !== false && stripos( $part, 'Damalion' ) !== false ) {
                    $rebuilt .= $part;
                    continue;
                }
                if ( stripos( $part, 'lets-connect' ) !== false ) {
                    $rebuilt .= $part;
                    continue;
                }

                $link_html = '<a href="' . $safe_url . '" class="' . self::LINK_CLASS
                           . '" data-lsi-lang="' . esc_attr( $lang )
                           . '" title="' . $safe_title . '">${1}</a>';

                $new_part = preg_replace( $pattern, $link_html, $part, 1, $count );

                if ( $count > 0 && $new_part !== null ) {
                    $rebuilt .= $new_part;
                    $linked = true;
                } else {
                    $rebuilt .= $part;
                }
            }
        }

        return $rebuilt;
    }

    /* ==============================================================
     *  ORPHAN FIX
     * ============================================================== */

    public static function fix_orphans( $lang, $lang_config, $keyword_map = null, $target_ids = array() ) {
        if ( ! $keyword_map ) $keyword_map = self::build_keyword_map( $lang, $lang_config );

        $prefix = DBA_LuxStrat_Intl_Dedupe::meta_prefix( $lang );
        $themes = ! empty( $lang_config['themes'] ) ? $lang_config['themes'] : array();

        $wpml = self::wpml_switch( $lang );
        $lsi_posts = get_posts( array(
            'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => -1,
            'meta_query' => array( array( 'key' => $prefix . 'auto_created', 'compare' => 'EXISTS' ) ),
            'suppress_filters' => false,
        ) );
        self::wpml_restore( $wpml );

        if ( empty( $lsi_posts ) ) return array( 'orphans' => 0, 'fixed' => 0, 'details' => array() );

        $lsi_ids = wp_list_pluck( $lsi_posts, 'ID' );

        $orphans = array();
        foreach ( $lsi_posts as $p ) {
            if ( ! empty( $target_ids ) && ! in_array( $p->ID, $target_ids, true ) ) continue;
            if ( self::count_inbound( $p->ID ) === 0 ) $orphans[] = $p;
        }

        if ( empty( $orphans ) ) return array( 'orphans' => 0, 'fixed' => 0, 'details' => array() );

        $wpml = self::wpml_switch( $lang );
        $sources = get_posts( array(
            'post_type' => array( 'post', 'page' ), 'post_status' => 'publish',
            'posts_per_page' => 300, 'post__not_in' => $lsi_ids,
            'suppress_filters' => false, 'orderby' => 'date', 'order' => 'DESC',
        ) );
        self::wpml_restore( $wpml );

        $fixed = 0; $details = array();

        foreach ( $orphans as $orphan ) {
            $orphan_url = get_permalink( $orphan->ID );
            if ( ! $orphan_url || strpos( $orphan_url, '?p=' ) !== false ) continue;

            $orphan_kws = array_filter( $keyword_map, function( $e ) use ( $orphan ) { return $e['post_id'] === $orphan->ID; } );

            $best_src = null; $best_score = 0; $best_anchor = '';

            foreach ( $sources as $src ) {
                if ( empty( $src->post_content ) ) continue;
                if ( substr_count( $src->post_content, 'class="' . self::LINK_CLASS . '"' ) >= self::MAX_LINKS_PER_POST ) continue;
                if ( strpos( $src->post_content, $orphan_url ) !== false ) continue;

                $src_lower = mb_strtolower( wp_strip_all_tags( $src->post_content ) );
                $score = 0; $anchor = '';

                foreach ( $orphan_kws as $e ) {
                    if ( mb_strpos( $src_lower, mb_strtolower( $e['keyword'] ) ) !== false ) {
                        $s = mb_strlen( $e['keyword'] );
                        $score += $s;
                        if ( $s > mb_strlen( $anchor ) ) $anchor = $e['keyword'];
                    }
                }
                if ( $score > $best_score ) { $best_score = $score; $best_src = $src; $best_anchor = $anchor; }
            }

            if ( ! $best_src ) {
                foreach ( $sources as $src ) {
                    if ( substr_count( $src->post_content, 'class="' . self::LINK_CLASS . '"' ) < self::MAX_LINKS_PER_POST && strpos( $src->post_content, $orphan_url ) === false ) {
                        $best_src = $src; break;
                    }
                }
            }

            if ( ! $best_src ) { $details[] = '#' . $orphan->ID . ': no source'; continue; }

            if ( ! empty( $best_anchor ) ) {
                $single = array( array( 'keyword' => $best_anchor, 'url' => $orphan_url, 'post_id' => $orphan->ID, 'theme' => '', 'title' => wp_strip_all_tags( $orphan->post_title ) ) );
                if ( self::inject_links_into_post( $best_src->ID, $single, $lang ) > 0 ) {
                    $fixed++; $details[] = '#' . $orphan->ID . ' -> #' . $best_src->ID . ' ("' . $best_anchor . '")'; continue;
                }
            }

            // Bridge paragraph
            $bridge = self::build_bridge( $lang, $orphan->post_title, $orphan_url, $best_anchor ?: $orphan->post_title );
            wp_update_post( array( 'ID' => $best_src->ID, 'post_content' => $best_src->post_content . "\n\n" . $bridge ) );
            $fixed++; $details[] = '#' . $orphan->ID . ' -> bridge #' . $best_src->ID;
        }

        return array( 'orphans' => count( $orphans ), 'fixed' => $fixed, 'details' => $details );
    }

    private static function build_bridge( $lang, $title, $url, $anchor ) {
        $t = array(
            'fr' => array(
                'Pour approfondir ce sujet, consultez notre analyse : <a href="%s" class="dba-lsi-link" data-lsi-lang="%s" title="%s">%s</a>.',
                'Decouvrez notre guide complet : <a href="%s" class="dba-lsi-link" data-lsi-lang="%s" title="%s">%s</a>.',
            ),
            'de' => array( 'Lesen Sie unsere Analyse: <a href="%s" class="dba-lsi-link" data-lsi-lang="%s" title="%s">%s</a>.' ),
            'es' => array( 'Consulte nuestro analisis: <a href="%s" class="dba-lsi-link" data-lsi-lang="%s" title="%s">%s</a>.' ),
            'it' => array( 'Scopra la nostra analisi: <a href="%s" class="dba-lsi-link" data-lsi-lang="%s" title="%s">%s</a>.' ),
        );
        $pool = isset( $t[ $lang ] ) ? $t[ $lang ] : $t['fr'];
        $display = mb_strlen( $anchor ) >= 5 ? $anchor : wp_strip_all_tags( $title );
        $s = sprintf( $pool[ array_rand( $pool ) ], esc_url( $url ), esc_attr( $lang ), esc_attr( $title ), esc_html( $display ) );
        return '<p class="' . self::LINK_PARA_CLASS . '" data-lsi-lang="' . esc_attr( $lang ) . '">' . $s . '</p>';
    }

    private static function count_inbound( $post_id ) {
        global $wpdb;
        $url = get_permalink( $post_id );
        if ( ! $url || strpos( $url, '?p=' ) !== false ) return 0;
        return (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status='publish' AND post_type IN ('post','page') AND ID != %d AND post_content LIKE %s",
            $post_id, '%' . $wpdb->esc_like( $url ) . '%'
        ) );
    }

    /**
     * Count only AUTO-INJECTED inbound links (dba-lsi-link class).
     * Used for the 3-link cap — manual editorial links don't count against it.
     */
    private static function count_auto_inbound( $post_id ) {
        global $wpdb;
        $url = get_permalink( $post_id );
        if ( ! $url ) return 0;
        return (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM {$wpdb->posts}
             WHERE post_status = 'publish'
             AND post_type IN ('post','page')
             AND ID != %d
             AND post_content LIKE %s
             AND post_content LIKE %s",
            $post_id,
            '%' . $wpdb->esc_like( self::LINK_CLASS ) . '%',
            '%' . $wpdb->esc_like( $url ) . '%'
        ) );
    }

    /* ==============================================================
     *  STRIP ALL
     * ============================================================== */

    public static function strip_all_links( $lang, $lang_config ) {
        global $wpdb;
        $ids = $wpdb->get_col( $wpdb->prepare(
            "SELECT ID FROM {$wpdb->posts} WHERE post_status IN ('publish','draft','pending') AND post_type IN ('post','page') AND (post_content LIKE %s OR post_content LIKE %s)",
            '%' . $wpdb->esc_like( self::LINK_CLASS ) . '%', '%' . $wpdb->esc_like( self::LINK_PARA_CLASS ) . '%'
        ) );
        $cleaned = 0;
        foreach ( $ids as $pid ) {
            $p = get_post( $pid );
            if ( ! $p ) continue;
            $c = $p->post_content; $orig = $c;
            $c = preg_replace( '/<a\b[^>]*class="[^"]*' . preg_quote( self::LINK_CLASS, '/' ) . '[^"]*"[^>]*>(.*?)<\/a>/is', '$1', $c );
            $c = preg_replace( '/<p\b[^>]*class="[^"]*' . preg_quote( self::LINK_PARA_CLASS, '/' ) . '[^"]*"[^>]*>.*?<\/p>/is', '', $c );
            $c = preg_replace( '/\n{3,}/', "\n\n", $c );
            if ( $c !== $orig ) {
                wp_update_post( array( 'ID' => $pid, 'post_content' => $c ) );
                delete_post_meta( $pid, '_dba_lsi_' . $lang . '_linked' );
                delete_post_meta( $pid, '_dba_lsi_' . $lang . '_count' );
                $cleaned++;
            }
        }
        return $cleaned;
    }

    /* ==============================================================
     *  STRIP SELECTED — remove inbound links pointing to specific target posts
     * ============================================================== */

    public static function strip_selected_links( $lang, $target_post_ids ) {
        $cleaned = 0;

        foreach ( $target_post_ids as $tid ) {
            $tid = absint( $tid );
            $p = get_post( $tid );
            if ( ! $p || empty( $p->post_content ) ) continue;

            $c    = $p->post_content;
            $orig = $c;

            // Remove our auto-injected <a> links (keep anchor text)
            $c = preg_replace(
                '/<a\b[^>]*class="[^"]*' . preg_quote( self::LINK_CLASS, '/' ) . '[^"]*"[^>]*>(.*?)<\/a>/is',
                '$1', $c
            );
            // Remove our bridge paragraphs
            $c = preg_replace(
                '/<p\b[^>]*class="[^"]*' . preg_quote( self::LINK_PARA_CLASS, '/' ) . '[^"]*"[^>]*>.*?<\/p>/is',
                '', $c
            );
            $c = preg_replace( '/\n{3,}/', "\n\n", $c );

            if ( $c !== $orig ) {
                wp_update_post( array( 'ID' => $tid, 'post_content' => $c ) );
                delete_post_meta( $tid, '_dba_lsi_' . $lang . '_linked' );
                delete_post_meta( $tid, '_dba_lsi_' . $lang . '_count' );
                $cleaned++;
            }
        }

        return $cleaned;
    }

    /* ==============================================================
     *  ENRICH POSTS
     * ============================================================== */

    public static function enrich_posts( $lang, $lang_config, $post_ids ) {
        @set_time_limit( 300 );
        $prefix = DBA_LuxStrat_Intl_Dedupe::meta_prefix( $lang );
        $themes = ! empty( $lang_config['themes'] ) ? $lang_config['themes'] : array();
        $settings = get_option( 'dba_luxstrat_' . $lang . '_settings', array() );
        $images = 0; $seo = 0;
        foreach ( $post_ids as $pid ) {
            if ( ! has_post_thumbnail( $pid ) ) {
                $ts = get_post_meta( $pid, $prefix . 'theme', true );
                $tc = isset( $themes[ $ts ] ) ? $themes[ $ts ] : array( 'short_label' => 'Luxembourg', 'keywords' => array() );
                DBA_LuxStrat_Intl_Generator::generate_featured_image_for_post( $pid, get_the_title( $pid ), $tc, $settings, $lang );
                if ( has_post_thumbnail( $pid ) ) $images++;
            }
            $has_seo = ! empty( get_post_meta( $pid, '_yoast_wpseo_metadesc', true ) ) || ! empty( get_post_meta( $pid, 'rank_math_description', true ) );
            if ( ! $has_seo ) {
                $ex = get_the_excerpt( $pid );
                if ( ! empty( $ex ) ) {
                    $d = mb_strlen( $ex ) > 135 ? mb_substr( $ex, 0, 132 ) . '...' : $ex;
                    update_post_meta( $pid, '_yoast_wpseo_metadesc', sanitize_text_field( $d ) );
                    update_post_meta( $pid, 'rank_math_description', sanitize_text_field( $d ) );
                    $seo++;
                }
            }
        }
        return array( 'total' => count( $post_ids ), 'images_added' => $images, 'seo_fixed' => $seo );
    }

    public static function enrich_all( $lang, $lang_config ) {
        $prefix = DBA_LuxStrat_Intl_Dedupe::meta_prefix( $lang );
        $scope = get_transient( 'dba_luxstrat_' . $lang . '_enrich_scope' );
        if ( $scope && ! empty( $scope['post_ids'] ) ) return self::enrich_posts( $lang, $lang_config, $scope['post_ids'] );
        $ids = get_posts( array(
            'post_type' => 'post', 'post_status' => array( 'publish', 'draft', 'pending' ),
            'posts_per_page' => -1, 'fields' => 'ids',
            'meta_query' => array( array( 'key' => $prefix . 'auto_created', 'compare' => 'EXISTS' ) ),
            'date_query' => array( array( 'after' => '7 days ago' ) ),
        ) );
        return self::enrich_posts( $lang, $lang_config, $ids );
    }

    private static function save_stats( $lang, $total, $orphan_result ) {
        update_option( 'dba_luxstrat_' . $lang . '_inbound_stats', array(
            'last_run' => current_time( 'Y-m-d H:i:s' ),
            'posts_scanned' => $total, 'orphans_found' => $orphan_result['orphans'], 'orphans_fixed' => $orphan_result['fixed'],
        ), false );
    }

    private static function wpml_switch( $lang ) {
        if ( ! function_exists( 'icl_get_languages' ) ) return null;
        global $sitepress;
        if ( ! $sitepress ) return null;
        $o = $sitepress->get_current_language();
        $sitepress->switch_lang( $lang );
        return $o;
    }

    private static function wpml_restore( $o ) {
        if ( $o === null ) return;
        global $sitepress;
        if ( $sitepress ) $sitepress->switch_lang( $o );
    }

    /* ==============================================================
     *  SCAN BATCH TARGETED — wrapper for admin AJAX
     *  Reorders params to match ajax_scan_targeted() call signature:
     *    scan_batch_targeted( $lang, $lang_config, $target_ids, $offset )
     * ============================================================== */

    public static function scan_batch_targeted( $lang, $lang_config, $target_ids, $offset = 0 ) {
        return self::scan_batch( $lang, $lang_config, $offset, $target_ids );
    }

    /* ==============================================================
     *  INBOUND LINK REPORT — for admin UI table
     *  Returns array of [ post_id => inbound_count ] for all
     *  published LuxStrat posts in the target language.
     * ============================================================== */

    public static function get_link_report( $lang, $lang_config ) {
        $prefix = DBA_LuxStrat_Intl_Dedupe::meta_prefix( $lang );

        $wpml = self::wpml_switch( $lang );

        $posts = get_posts( array(
            'post_type'        => 'post',
            'post_status'      => 'publish',
            'posts_per_page'   => -1,
            'fields'           => 'ids',
            'meta_query'       => array( array( 'key' => $prefix . 'auto_created', 'compare' => 'EXISTS' ) ),
            'suppress_filters' => false,
        ) );

        self::wpml_restore( $wpml );

        $report = array();
        foreach ( $posts as $pid ) {
            $report[ $pid ] = self::count_inbound( $pid );
        }

        return $report;
    }

    /**
     * Rich inbound report for admin UI — returns full post data with inbound count.
     */
    public static function get_inbound_report( $lang, $lang_config ) {
        $prefix = DBA_LuxStrat_Intl_Dedupe::meta_prefix( $lang );
        $themes = ! empty( $lang_config['themes'] ) ? $lang_config['themes'] : array();

        $wpml = self::wpml_switch( $lang );

        $posts = get_posts( array(
            'post_type'        => 'post',
            'post_status'      => 'publish',
            'posts_per_page'   => -1,
            'meta_query'       => array( array( 'key' => $prefix . 'auto_created', 'compare' => 'EXISTS' ) ),
            'suppress_filters' => false,
            'orderby'          => 'date',
            'order'            => 'DESC',
        ) );

        self::wpml_restore( $wpml );

        $report = array();
        foreach ( $posts as $post ) {
            $ts = get_post_meta( $post->ID, $prefix . 'theme', true );
            $tc = isset( $themes[ $ts ] ) ? $themes[ $ts ] : null;
            $report[] = array(
                'post_id'   => $post->ID,
                'title'     => $post->post_title,
                'date'      => get_the_date( 'Y-m-d', $post ),
                'theme'     => $tc ? ( $tc['short_label'] ?? $ts ) : $ts,
                'inbound'   => substr_count( $post->post_content, 'class="' . self::LINK_CLASS . '"' ),
                'has_image' => has_post_thumbnail( $post->ID ),
            );
        }
        return $report;
    }

    /* ==============================================================
     *  BOOST SELECTED POSTS — force inbound links to specific posts
     *  Called by ajax_boost: scans entire site, focuses on selected
     *  target post IDs, uses bridge paragraphs as last resort.
     * ============================================================== */

    public static function boost_posts( $lang, $lang_config, $target_post_ids ) {
        @set_time_limit( 120 );

        $prefix = DBA_LuxStrat_Intl_Dedupe::meta_prefix( $lang );
        $themes = ! empty( $lang_config['themes'] ) ? $lang_config['themes'] : array();
        $boosted = 0;
        $details = array();

        // Get all LuxStrat IDs (to exclude from sources)
        $all_lsi_ids = get_posts( array(
            'post_type' => 'post', 'post_status' => 'publish',
            'posts_per_page' => -1, 'fields' => 'ids',
            'meta_query' => array( array( 'key' => $prefix . 'auto_created', 'compare' => 'EXISTS' ) ),
        ) );

        $wpml = self::wpml_switch( $lang );
        $sources = get_posts( array(
            'post_type' => array( 'post', 'page' ), 'post_status' => 'publish',
            'posts_per_page' => 200, 'post__not_in' => $all_lsi_ids,
            'suppress_filters' => false, 'orderby' => 'date', 'order' => 'DESC',
        ) );
        self::wpml_restore( $wpml );

        foreach ( $target_post_ids as $tid ) {
            $tid  = absint( $tid );
            $post = get_post( $tid );
            if ( ! $post ) continue;

            // ── Enforce inbound cap (only count auto-injected, not manual links) ──
            $current_inbound = self::count_auto_inbound( $tid );
            $inbound_remaining = self::MAX_INBOUND_PER_TARGET - $current_inbound;
            if ( $inbound_remaining <= 0 ) {
                $details[] = '#' . $tid . ': already at cap (' . $current_inbound . ' inbound)';
                continue;
            }

            $ts  = get_post_meta( $tid, $prefix . 'theme', true );
            $tc  = isset( $themes[ $ts ] ) ? $themes[ $ts ] : null;
            $kws = ( $tc && ! empty( $tc['keywords'] ) ) ? $tc['keywords'] : array();
            $url = get_permalink( $tid );
            if ( ! $url || $url === false || strpos( $url, '?p=' ) !== false ) continue;
            // URL must be a real permalink (not a placeholder)
            if ( strncmp( $url, 'http', 4 ) !== 0 ) continue;

            $title_clean = wp_strip_all_tags( $post->post_title );
            if ( mb_strlen( $title_clean ) >= 10 ) $kws[] = $title_clean;
            if ( $tc && ! empty( $tc['short_label'] ) ) $kws[] = $tc['short_label'];

            // Also use extracted fragments for better matching
            $fragments = self::extract_fragments( $kws, $lang );
            $all_kws = array_unique( array_merge( $kws, $fragments ) );

            // Score sources
            $scored = array();
            foreach ( $sources as $src ) {
                if ( empty( $src->post_content ) ) continue;
                if ( substr_count( $src->post_content, 'class="' . self::LINK_CLASS . '"' ) >= self::MAX_LINKS_PER_POST ) continue;
                if ( strpos( $src->post_content, $url ) !== false ) continue;

                $src_lower = mb_strtolower( wp_strip_all_tags( $src->post_content ) );
                $score = 0; $anchor = '';
                foreach ( $all_kws as $kw ) {
                    if ( mb_strlen( $kw ) < 5 ) continue;
                    if ( mb_strpos( $src_lower, mb_strtolower( $kw ) ) !== false ) {
                        $s = mb_strlen( $kw );
                        $score += $s;
                        if ( $s > mb_strlen( $anchor ) ) $anchor = $kw;
                    }
                }
                if ( $score > 0 ) $scored[] = array( 'src' => $src, 'score' => $score, 'anchor' => $anchor );
            }

            usort( $scored, function( $a, $b ) { return $b['score'] - $a['score']; } );

            $added = 0;
            foreach ( array_slice( $scored, 0, $inbound_remaining ) as $e ) {
                if ( $added >= $inbound_remaining ) break;
                $r = self::inject_links_into_post( $e['src']->ID, array( array(
                    'keyword' => $e['anchor'], 'url' => $url, 'post_id' => $tid,
                    'theme' => $ts ?: '', 'title' => $title_clean,
                ) ), $lang );
                if ( $r > 0 ) { $added++; $details[] = '#' . $tid . ' ← from #' . $e['src']->ID; }
            }

            // Fallback: bridge
            if ( $added === 0 ) {
                foreach ( $sources as $src ) {
                    if ( substr_count( $src->post_content, 'class="' . self::LINK_CLASS . '"' ) < self::MAX_LINKS_PER_POST
                        && strpos( $src->post_content, $url ) === false ) {
                        $a = ! empty( $all_kws[0] ) ? $all_kws[0] : $title_clean;
                        $bridge = self::build_bridge( $lang, $post->post_title, $url, $a );
                        wp_update_post( array( 'ID' => $src->ID, 'post_content' => $src->post_content . "\n\n" . $bridge ) );
                        $added++; $details[] = '#' . $tid . ' ← bridge to #' . $src->ID;
                        break;
                    }
                }
            }

            if ( $added > 0 ) $boosted++;
        }

        return array( 'boosted' => $boosted, 'total' => count( $target_post_ids ), 'details' => $details );
    }
}
