<?php
/**
 * Service Catalog for BoostBot.
 *
 * Provides a global catalog of all business services and products.
 * This is injected into every chat to give the AI awareness of all
 * available services regardless of the user's current page.
 *
 * @package BoostBot
 * @since   1.6.0 (Week 24)
 */

// Prevent direct access
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Class BoostBot_Service_Catalog
 *
 * Maintains and provides a global catalog of all business services.
 * Uses transient caching for performance (24 hour cache by default).
 *
 * @since 1.6.0
 */
class BoostBot_Service_Catalog {

    /**
     * Cache key for the service catalog.
     *
     * @var string
     */
    const CACHE_KEY = 'boostbot_service_catalog';

    /**
     * Cache expiration in seconds (24 hours).
     *
     * @var int
     */
    const CACHE_EXPIRATION = DAY_IN_SECONDS;

    /**
     * Maximum number of services to include in catalog.
     *
     * @var int
     */
    const MAX_SERVICES = 50;

    /**
     * Maximum characters for business summary.
     *
     * @var int
     */
    const MAX_SUMMARY_LENGTH = 500;

    /**
     * Week 27.7: Settings keys for overlay edits.
     */
    const OVERRIDES_KEY     = 'service_catalog_overrides_json';
    const OVERRIDES_VERSION = 'service_catalog_overrides_version';

    /**
     * Week 27.7: For now, prefer Hebrew-only catalog entries.
     * We treat /en/ URLs (and lang=en query) as English and exclude them from the catalog output.
     *
     * @param string $url
     * @return bool
     */
    private static function is_english_url( string $url ): bool {
        $url = trim( (string) $url );
        if ( '' === $url ) {
            return false;
        }
        $path = (string) wp_parse_url( $url, PHP_URL_PATH );
        $path = strtolower( $path );
        if ( $path === '/en' || 0 === strpos( $path, '/en/' ) ) {
            return true;
        }
        $query = (string) wp_parse_url( $url, PHP_URL_QUERY );
        $query = strtolower( $query );
        if ( $query && false !== strpos( $query, 'lang=en' ) ) {
            return true;
        }
        return false;
    }

    /**
     * Service categories with their detection keywords (dynamic, multi-language).
     * These are used as hints but actual categorization is done by AI analysis.
     *
     * @var array
     */
    private static $category_hints = array(
        'web_development' => array(
            'he' => array( 'אתר', 'אתרים', 'פיתוח', 'בניית', 'ווב', 'תכנות', 'קוד' ),
            'en' => array( 'website', 'web', 'development', 'programming', 'code', 'app' ),
        ),
        'hosting' => array(
            'he' => array( 'אחסון', 'שרת', 'דומיין', 'ענן', 'hosting' ),
            'en' => array( 'hosting', 'server', 'domain', 'cloud', 'storage' ),
        ),
        'marketing' => array(
            'he' => array( 'שיווק', 'פרסום', 'קידום', 'ממומן', 'סושיאל', 'דיגיטל' ),
            'en' => array( 'marketing', 'advertising', 'seo', 'ppc', 'social', 'digital' ),
        ),
        'design' => array(
            'he' => array( 'עיצוב', 'גרפי', 'לוגו', 'ברנדינג', 'מיתוג' ),
            'en' => array( 'design', 'graphic', 'logo', 'branding', 'ui', 'ux' ),
        ),
        'consulting' => array(
            'he' => array( 'ייעוץ', 'יועץ', 'אסטרטגיה', 'ליווי' ),
            'en' => array( 'consulting', 'consultant', 'strategy', 'advisory' ),
        ),
        'support' => array(
            'he' => array( 'תמיכה', 'שירות', 'עזרה', 'תחזוקה' ),
            'en' => array( 'support', 'service', 'help', 'maintenance' ),
        ),
        'ecommerce' => array(
            'he' => array( 'חנות', 'מסחר', 'מכירות', 'קנייה' ),
            'en' => array( 'ecommerce', 'shop', 'store', 'sales', 'commerce' ),
        ),
        'training' => array(
            'he' => array( 'הדרכה', 'קורס', 'לימוד', 'סדנה', 'הכשרה' ),
            'en' => array( 'training', 'course', 'workshop', 'education', 'learning' ),
        ),
        'photography' => array(
            'he' => array( 'צילום', 'וידאו', 'סרטון', 'מדיה' ),
            'en' => array( 'photography', 'video', 'media', 'production' ),
        ),
        'general' => array(
            'he' => array(),
            'en' => array(),
        ),
    );

    /**
     * Get the complete service catalog.
     *
     * @param bool   $force_refresh Force refresh the cache.
     * @param string $language      Two-letter language code ('' = primary language).
     * @return array The service catalog.
     */
    public static function get_catalog( $force_refresh = false, string $language = '' ) {
        if ( '' === $language && class_exists( 'BoostBot_Language' ) ) {
            $language = BoostBot_Language::get_primary();
        }

        $cache_key = self::CACHE_KEY . ( $language ? '_' . $language : '' );

        if ( ! $force_refresh ) {
            $cached = get_transient( $cache_key );
            if ( false !== $cached ) {
                return $cached;
            }
        }

        $catalog = self::build_catalog( array( 'language' => $language ) );

        set_transient( $cache_key, $catalog, self::CACHE_EXPIRATION );

        return $catalog;
    }

    /**
     * Build the service catalog from the knowledge index.
     *
     * @return array The built catalog.
     */
    private static function build_catalog( array $options = array() ) {
        global $wpdb;

        $table_name = self::sanitize_identifier( $wpdb->prefix . 'boostbot_knowledge_index' );

        $include_menu_seed = isset( $options['include_menu_seed'] ) ? (bool) $options['include_menu_seed'] : true;
        $include_overrides = isset( $options['include_overrides'] ) ? (bool) $options['include_overrides'] : true;
        $language          = isset( $options['language'] ) ? (string) $options['language'] : '';
        $menu_seed = $include_menu_seed ? self::get_menu_seed_services() : array();

        // Language filter clause — scopes services to the requested language.
        $lang_clause = '';
        if ( '' !== $language ) {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $lang_clause = $wpdb->prepare( ' AND language = %s', $language );
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table identifier sanitized via sanitize_identifier(); values bind via prepare().
        $services = $wpdb->get_results(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix constant
                "SELECT id, post_id, title, summary, url, service_category, is_service_page, price_info
                 FROM {$table_name}
                 WHERE is_service_page = 1 {$lang_clause}
                 ORDER BY relevance_score DESC
                 LIMIT %d",
                self::MAX_SERVICES
            ),
            ARRAY_A
        );

        if ( empty( $services ) ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table identifier sanitized via sanitize_identifier(); values bind via prepare().
            $services = $wpdb->get_results(
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
                $wpdb->prepare(
                    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix constant
                    "SELECT id, post_id, title, summary, url, service_category, is_service_page, price_info
                     FROM {$table_name}
                     WHERE content_type IN ('page', 'post', 'product')
                     AND title IS NOT NULL
                     AND summary IS NOT NULL
                     {$lang_clause}
                     ORDER BY relevance_score DESC
                     LIMIT %d",
                    self::MAX_SERVICES
                ),
                ARRAY_A
            );
        }

        // Get WooCommerce products if available
        $products = self::get_woocommerce_products();

        // Get key pages (about, contact, etc.)
        $key_pages = self::get_key_pages();

        // Get business summary from site profile
        $business_summary = self::get_business_summary();

        // Format catalog
        $services_out = self::format_services( $services );

        // Week 27.7: Merge menu-seeded services first (stable order), then remaining.
        $services_out = self::merge_services_menu_first( $menu_seed, $services_out );

        // Week 27.7: Apply overlay edits (title/category/one_liner/hidden).
        if ( $include_overrides ) {
            $services_out = self::apply_service_overrides( $services_out, $language );
        }

        $catalog = array(
            'services'         => $services_out,
            'products'         => $products,
            'key_pages'        => $key_pages,
            'business_summary' => $business_summary,
            'categories'       => self::get_unique_categories( $services ),
            'built_at'         => current_time( 'mysql' ),
            'stats'            => array(
                'total_services' => count( $services ),
                'total_products' => count( $products ),
                'total_pages'    => count( $key_pages ),
            ),
        );

        return $catalog;
    }

    /**
     * Week 27.7: Build an uncached preview catalog with configurable inputs.
     *
     * @param string $mode 'menus'|'classifier'|'both'
     * @return array
     */
    public static function build_preview( string $mode ): array {
        $mode = strtolower( trim( $mode ) );
        $include_menu = ( 'classifier' !== $mode );
        return self::build_catalog(
            array(
                'include_menu_seed' => $include_menu,
                'include_overrides' => false,
            )
        );
    }

    /**
     * Week 27.7: Extract internal menu URLs and build seed services in menu order.
     *
     * @return array [{name,url,category,source}]
     */
    private static function get_menu_seed_services(): array {
        if ( ! function_exists( 'get_nav_menu_locations' ) ) {
            return array();
        }

        $locations = (array) get_nav_menu_locations();
        if ( empty( $locations ) ) {
            return array();
        }

        // Try common location keys first; otherwise fall back to all menus.
        $preferred = array( 'primary', 'main', 'header', 'top', 'footer' );
        $menu_ids  = array();
        foreach ( $preferred as $k ) {
            if ( isset( $locations[ $k ] ) && $locations[ $k ] ) {
                $menu_ids[] = (int) $locations[ $k ];
            }
        }
        if ( empty( $menu_ids ) ) {
            foreach ( $locations as $mid ) {
                if ( $mid ) {
                    $menu_ids[] = (int) $mid;
                }
            }
        }
        $menu_ids = array_values( array_unique( array_filter( $menu_ids ) ) );
        if ( empty( $menu_ids ) ) {
            return array();
        }

        $home = home_url( '/' );
        $home_host = wp_parse_url( $home, PHP_URL_HOST );
        if ( empty( $home_host ) ) {
            return array();
        }

        $out = array();
        foreach ( $menu_ids as $mid ) {
            $items = wp_get_nav_menu_items( $mid );
            if ( ! is_array( $items ) ) {
                continue;
            }
            foreach ( $items as $it ) {
                if ( ! ( $it instanceof WP_Post ) ) {
                    continue;
                }
                $url = isset( $it->url ) ? (string) $it->url : '';
                $label = isset( $it->title ) ? (string) $it->title : '';
                $url = trim( $url );
                if ( '' === $url ) {
                    continue;
                }
                $host = wp_parse_url( $url, PHP_URL_HOST );
                if ( $host && strtolower( $host ) !== strtolower( (string) $home_host ) ) {
                    continue; // external
                }
                // Normalize weird "/https://..." strings (seen in some menu exports).
                if ( 0 === strpos( $url, '/http' ) ) {
                    $url = ltrim( $url, '/' );
                }

                // Canonicalize internal relative URLs.
                if ( 0 === strpos( $url, '/' ) ) {
                    $url = rtrim( $home, '/' ) . $url;
                }
                $url = esc_url_raw( $url );
                if ( empty( $url ) ) {
                    continue;
                }

                // Week 27.7: Prefer Hebrew-only menu items (exclude /en/).
                if ( self::is_english_url( $url ) ) {
                    continue;
                }
                // Dedup by URL while keeping first occurrence (menu order).
                $key = strtolower( $url );
                if ( isset( $out[ $key ] ) ) {
                    continue;
                }

                $name = trim( wp_strip_all_tags( $label ) );
                if ( '' === $name ) {
                    $name = $url;
                }
                $out[ $key ] = array(
                    'name'     => $name,
                    'url'      => $url,
                    'category' => 'general',
                    'source'   => 'menu',
                );
            }
        }

        return array_values( $out );
    }

    /**
     * Week 27.7: Merge menu services first, then add DB services not already present.
     *
     * @param array $menu_seed [{name,url,category,source}]
     * @param array $db_services formatted services
     * @return array
     */
    private static function merge_services_menu_first( array $menu_seed, array $db_services ): array {
        $by_url = array();
        foreach ( $db_services as $s ) {
            $u = isset( $s['url'] ) ? strtolower( (string) $s['url'] ) : '';
            if ( '' !== $u ) {
                $by_url[ $u ] = $s;
            }
        }

        $out = array();
        foreach ( $menu_seed as $m ) {
            $u = strtolower( (string) ( $m['url'] ?? '' ) );
            if ( '' === $u ) {
                continue;
            }
            if ( self::is_english_url( (string) ( $m['url'] ?? '' ) ) ) {
                continue;
            }
            if ( isset( $by_url[ $u ] ) ) {
                // Prefer DB metadata, but keep menu label for name if present.
                $s = $by_url[ $u ];
                if ( ! empty( $m['name'] ) ) {
                    $s['name'] = (string) $m['name'];
                }
                $s['source'] = 'menu';
                $out[] = $s;
                unset( $by_url[ $u ] );
            } else {
                // Menu-only item: keep minimal.
                $out[] = array(
                    'id'        => 0,
                    'name'      => (string) ( $m['name'] ?? '' ),
                    'summary'   => '',
                    'url'       => (string) ( $m['url'] ?? '' ),
                    'category'  => (string) ( $m['category'] ?? 'general' ),
                    'has_price' => false,
                    'source'    => 'menu',
                );
            }
            if ( count( $out ) >= self::MAX_SERVICES ) {
                return $out;
            }
        }

        foreach ( $db_services as $s ) {
            $u = isset( $s['url'] ) ? strtolower( (string) $s['url'] ) : '';
            if ( '' === $u ) {
                continue;
            }
            if ( self::is_english_url( (string) ( $s['url'] ?? '' ) ) ) {
                continue;
            }
            if ( isset( $by_url[ $u ] ) ) {
                $s['source'] = 'classifier';
                $out[] = $s;
            }
            if ( count( $out ) >= self::MAX_SERVICES ) {
                break;
            }
        }

        return $out;
    }

    /**
     * Week 27.7: Apply overlay edits + hidden flags.
     *
     * Overlay JSON schema:
     * { "overrides": { "<url>": {title?,category?,one_liner?,hidden?} }, "hidden": { "<url>": true } }
     *
     * @param array $services
     * @return array
     */
    private static function apply_service_overrides( array $services, string $language = '' ): array {
        $raw = (string) BoostBot_Settings::get_for_language( self::OVERRIDES_KEY, $language, '' );
        $raw = trim( $raw );
        if ( '' === $raw ) {
            return $services;
        }
        $decoded = json_decode( $raw, true );
        if ( ! is_array( $decoded ) ) {
            return $services;
        }
        $ov = isset( $decoded['overrides'] ) && is_array( $decoded['overrides'] ) ? $decoded['overrides'] : array();
        $hidden = isset( $decoded['hidden'] ) && is_array( $decoded['hidden'] ) ? $decoded['hidden'] : array();

        $out = array();
        foreach ( $services as $s ) {
            $url = isset( $s['url'] ) ? (string) $s['url'] : '';
            $key = strtolower( trim( $url ) );
            if ( '' === $key ) {
                continue;
            }
            if ( isset( $hidden[ $key ] ) && $hidden[ $key ] ) {
                continue;
            }
            if ( isset( $ov[ $key ] ) && is_array( $ov[ $key ] ) ) {
                $o = $ov[ $key ];
                if ( isset( $o['title'] ) ) {
                    $s['name'] = (string) $o['title'];
                }
                if ( isset( $o['category'] ) ) {
                    $s['category'] = (string) $o['category'];
                }
                if ( isset( $o['one_liner'] ) ) {
                    $s['summary'] = (string) $o['one_liner'];
                }
            }
            $out[] = $s;
        }

        return $out;
    }

    /**
     * Format services for the catalog.
     *
     * @param array $services Raw services from database.
     * @return array Formatted services.
     */
    private static function format_services( $services ) {
        $formatted = array();

        foreach ( $services as $service ) {
            $formatted[] = array(
                'id'       => (int) $service['id'],
                'name'     => $service['title'],
                'summary'  => self::truncate_text( $service['summary'], 200 ),
                'url'      => $service['url'],
                'category' => $service['service_category'] ?: 'general',
                'has_price' => ! empty( $service['price_info'] ),
            );
        }

        return $formatted;
    }

    /**
     * Get WooCommerce products for the catalog.
     *
     * @return array Products list.
     */
    private static function get_woocommerce_products() {
        if ( ! class_exists( 'WooCommerce' ) ) {
            return array();
        }

        $products = array();

        $args = array(
            'post_type'      => 'product',
            'post_status'    => 'publish',
            'posts_per_page' => 20,
            'orderby'        => 'menu_order date',
            'order'          => 'ASC',
        );

        $query = new WP_Query( $args );

        if ( $query->have_posts() ) {
            while ( $query->have_posts() ) {
                $query->the_post();
                $product_id = get_the_ID();
                $product = wc_get_product( $product_id );

                if ( $product ) {
                    $products[] = array(
                        'id'       => $product_id,
                        'name'     => $product->get_name(),
                        'price'    => $product->get_price_html(),
                        'url'      => urldecode( get_permalink( $product_id ) ),
                        'category' => 'product',
                    );
                }
            }
            wp_reset_postdata();
        }

        return $products;
    }

    /**
     * Get key pages (about, contact, etc.).
     *
     * @return array Key pages list.
     */
    private static function get_key_pages() {
        $key_pages = array();

        // Common key page slugs (multi-language)
        $key_slugs = array(
            'about'       => array( 'about', 'about-us', 'אודות', 'אודותינו' ),
            'contact'     => array( 'contact', 'contact-us', 'צור-קשר', 'יצירת-קשר' ),
            'services'    => array( 'services', 'שירותים', 'our-services' ),
            'pricing'     => array( 'pricing', 'prices', 'מחירון', 'מחירים' ),
            'faq'         => array( 'faq', 'faqs', 'שאלות-נפוצות' ),
            'terms'       => array( 'terms', 'terms-of-service', 'תנאי-שימוש' ),
            'privacy'     => array( 'privacy', 'privacy-policy', 'מדיניות-פרטיות' ),
        );

        foreach ( $key_slugs as $type => $slugs ) {
            foreach ( $slugs as $slug ) {
                $page = get_page_by_path( $slug );
                if ( $page && 'publish' === $page->post_status ) {
                    $key_pages[ $type ] = array(
                        'id'    => $page->ID,
                        'title' => $page->post_title,
                        'url'   => urldecode( get_permalink( $page->ID ) ),
                    );
                    break; // Found one, move to next type
                }
            }
        }

        return $key_pages;
    }

    /**
     * Get business summary from site profile.
     *
     * @return string Business summary.
     */
    private static function get_business_summary() {
        global $wpdb;

        $table_name = self::sanitize_identifier( $wpdb->prefix . 'boostbot_site_profile' );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table identifier sanitized via sanitize_identifier(); values bind via prepare().
        $profile = $wpdb->get_row(
            "SELECT business_description, business_type, main_topics FROM {$table_name} LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            ARRAY_A
        );

        if ( ! $profile ) {
            return '';
        }

        $summary = $profile['business_description'] ?: '';

        if ( ! empty( $profile['business_type'] ) ) {
            $summary = $profile['business_type'] . ': ' . $summary;
        }

        return self::truncate_text( $summary, self::MAX_SUMMARY_LENGTH );
    }

    /**
     * Get unique categories from services.
     *
     * @param array $services Services list.
     * @return array Unique categories.
     */
    private static function get_unique_categories( $services ) {
        $categories = array();

        foreach ( $services as $service ) {
            $category = $service['service_category'] ?: 'general';
            if ( ! in_array( $category, $categories, true ) ) {
                $categories[] = $category;
            }
        }

        return $categories;
    }

    /**
     * Truncate text to a maximum length.
     *
     * @param string $text The text to truncate.
     * @param int    $max_length Maximum length.
     * @return string Truncated text.
     */
    private static function truncate_text( $text, $max_length ) {
        if ( mb_strlen( $text ) <= $max_length ) {
            return $text;
        }

        return mb_substr( $text, 0, $max_length - 3 ) . '...';
    }

    /**
     * Get catalog formatted for injection into AI prompt.
     *
     * @return string Formatted catalog for prompt.
     */
    public static function get_formatted_for_prompt( string $language = '' ) {
        $catalog = self::get_catalog( false, $language );
        $ps      = BoostBot_Prompt_Strings::get( $language );

        $output = '';

        // Business summary
        if ( ! empty( $catalog['business_summary'] ) ) {
            $output .= $ps['catalog_business_desc'] . "\n";
            $output .= $catalog['business_summary'] . "\n\n";
        }

        // Services
        if ( ! empty( $catalog['services'] ) ) {
            $output .= $ps['catalog_services'] . "\n";
            foreach ( $catalog['services'] as $service ) {
                $output .= "• {$service['name']}";
                if ( ! empty( $service['summary'] ) ) {
                    $output .= ": {$service['summary']}";
                }
                if ( ! empty( $service['url'] ) ) {
                    $output .= " ({$service['url']})";
                }
                $output .= "\n";
            }
            $output .= "\n";
        }

        // Products
        if ( ! empty( $catalog['products'] ) ) {
            $output .= $ps['catalog_products'] . "\n";
            foreach ( $catalog['products'] as $product ) {
                $output .= "• {$product['name']}";
                if ( ! empty( $product['price'] ) ) {
                    $output .= " - " . wp_strip_all_tags( $product['price'] );
                }
                if ( ! empty( $product['url'] ) ) {
                    $output .= " ({$product['url']})";
                }
                $output .= "\n";
            }
            $output .= "\n";
        }

        // Key pages
        if ( ! empty( $catalog['key_pages'] ) ) {
            $output .= $ps['catalog_key_pages'] . "\n";
            foreach ( $catalog['key_pages'] as $type => $page ) {
                $type_labels = array(
                    'about'    => $ps['page_about'],
                    'contact'  => $ps['page_contact'],
                    'services' => $ps['page_services'],
                    'pricing'  => $ps['page_pricing'],
                    'faq'      => $ps['page_faq'],
                    'terms'    => $ps['page_terms'],
                    'privacy'  => $ps['page_privacy'],
                );
                $label = isset( $type_labels[ $type ] ) ? $type_labels[ $type ] : $type;
                $output .= "• {$label}: {$page['title']} ({$page['url']})\n";
            }
            $output .= "\n";
        }

        return $output;
    }

    /**
     * Get a compact catalog formatted for AI prompt injection.
     *
     * Week 28: used to keep always-on context small (names + links only).
     *
     * @return string
     */
    public static function get_compact_for_prompt( string $language = '' ) {
        $catalog = self::get_catalog( false, $language );
        $ps      = BoostBot_Prompt_Strings::get( $language );

        $lines = array();

        // Services (names + urls only)
        if ( ! empty( $catalog['services'] ) ) {
            $lines[] = $ps['catalog_services_compact'];
            foreach ( $catalog['services'] as $service ) {
                $name = isset( $service['name'] ) ? (string) $service['name'] : '';
                $url  = isset( $service['url'] ) ? (string) $service['url'] : '';
                if ( '' === trim( $name ) ) {
                    continue;
                }
                $lines[] = "• {$name}" . ( $url ? " ({$url})" : '' );
            }
            $lines[] = '';
        }

        // Key pages (titles + urls)
        if ( ! empty( $catalog['key_pages'] ) ) {
            $lines[] = $ps['catalog_key_pages'];
            foreach ( $catalog['key_pages'] as $type => $page ) {
                $title = isset( $page['title'] ) ? (string) $page['title'] : '';
                $url   = isset( $page['url'] ) ? (string) $page['url'] : '';
                if ( '' === trim( $title ) ) {
                    continue;
                }
                $lines[] = "• {$title}" . ( $url ? " ({$url})" : '' );
            }
            $lines[] = '';
        }

        // Products: include only names + urls (no prices) to keep it small
        if ( ! empty( $catalog['products'] ) ) {
            $lines[] = $ps['catalog_products_compact'];
            foreach ( $catalog['products'] as $product ) {
                $name = isset( $product['name'] ) ? (string) $product['name'] : '';
                $url  = isset( $product['url'] ) ? (string) $product['url'] : '';
                if ( '' === trim( $name ) ) {
                    continue;
                }
                $lines[] = "• {$name}" . ( $url ? " ({$url})" : '' );
            }
            $lines[] = '';
        }

        return trim( implode( "\n", $lines ) );
    }

    /**
     * Invalidate the service catalog cache.
     *
     * Called when content is indexed or updated.
     *
     * @return bool True on success.
     */
    public static function invalidate_cache() {
        delete_transient( self::CACHE_KEY );

        // Clear per-language caches.
        if ( class_exists( 'BoostBot_Language_Manager' ) ) {
            $languages = BoostBot_Language_Manager::get_instance()->get_enabled_languages();
        } elseif ( class_exists( 'BoostBot_Language' ) ) {
            $languages = array( BoostBot_Language::get_primary() );
        } else {
            $languages = array();
        }

        if ( ! empty( $languages ) ) {
            foreach ( $languages as $lang ) {
                delete_transient( self::CACHE_KEY . '_' . $lang );
            }
        }

        return true;
    }

    /**
     * Get catalog statistics.
     *
     * @return array Statistics about the catalog.
     */
    public static function get_stats() {
        $catalog = self::get_catalog();

        return array(
            'total_services'   => count( $catalog['services'] ),
            'total_products'   => count( $catalog['products'] ),
            'total_key_pages'  => count( $catalog['key_pages'] ),
            'categories'       => $catalog['categories'],
            'has_summary'      => ! empty( $catalog['business_summary'] ),
            'last_built'       => $catalog['built_at'] ?? null,
            'cache_key'        => self::CACHE_KEY,
            'cache_exists'     => false !== get_transient( self::CACHE_KEY ),
        );
    }

    /**
     * Detect service category for a piece of content.
     *
     * Uses keyword hints to suggest a category.
     * This is a helper function for content processing.
     *
     * @param string $title The content title.
     * @param string $content The content body.
     * @param string $language The content language (he/en).
     * @return string The detected category.
     */
    public static function detect_category( $title, $content, $language = '' ) {
        if ( '' === $language ) {
            $language = BoostBot_Language::get_primary();
        }
        $combined_text = mb_strtolower( $title . ' ' . $content );

        $best_category = 'general';
        $best_score = 0;

        foreach ( self::$category_hints as $category => $langs ) {
            if ( 'general' === $category ) {
                continue;
            }

            $score = 0;
            $keywords = isset( $langs[ $language ] ) ? $langs[ $language ] : array();

            // Also check English keywords as fallback
            if ( 'he' === $language && isset( $langs['en'] ) ) {
                $keywords = array_merge( $keywords, $langs['en'] );
            }

            foreach ( $keywords as $keyword ) {
                if ( mb_strpos( $combined_text, mb_strtolower( $keyword ) ) !== false ) {
                    $score++;
                }
            }

            if ( $score > $best_score ) {
                $best_score = $score;
                $best_category = $category;
            }
        }

        return $best_category;
    }

    /**
     * Check if content appears to be a service page.
     *
     * Uses heuristics to determine if content describes a service.
     *
     * @param string $title The content title.
     * @param string $content The content body.
     * @param array  $keywords The content keywords.
     * @return bool True if this appears to be a service page.
     */
    public static function is_service_content( $title, $content, $keywords = array() ) {
        // Service indicator words
        $service_indicators = array(
            'he' => array(
                'שירות', 'שירותים', 'פתרון', 'פתרונות', 'חבילה', 'חבילות',
                'מסלול', 'מסלולים', 'הצעה', 'מחיר', 'עלות', 'תעריף',
                'יתרונות', 'כולל', 'מה כלול', 'מומלץ', 'מתאים ל',
            ),
            'en' => array(
                'service', 'services', 'solution', 'solutions', 'package', 'packages',
                'plan', 'plans', 'offer', 'price', 'pricing', 'cost', 'rate',
                'benefits', 'includes', 'features', 'recommended', 'suitable for',
            ),
        );

        $combined_text = mb_strtolower( $title . ' ' . $content . ' ' . implode( ' ', $keywords ) );
        $score = 0;

        foreach ( $service_indicators as $lang => $indicators ) {
            foreach ( $indicators as $indicator ) {
                if ( mb_strpos( $combined_text, mb_strtolower( $indicator ) ) !== false ) {
                    $score++;
                }
            }
        }

        // Threshold for considering as service page
        return $score >= 3;
    }

    /**
     * Restrict a SQL identifier to [A-Za-z0-9_] characters.
     *
     * DDL cannot use $wpdb->prepare() for identifiers, so any identifier
     * that ends up interpolated into a query first passes through this
     * allow-list.
     *
     * @param string $identifier Raw identifier.
     * @return string Identifier with disallowed characters removed.
     */
    private static function sanitize_identifier( $identifier ) {
        return preg_replace( '/[^A-Za-z0-9_]/', '', (string) $identifier );
    }
}







