<?php
/**
 * Bundle Builder Service for BoostBot.
 *
 * AI-powered service for building personalized product bundles.
 *
 * @package BoostBot
 * @since   1.0.0
 */

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Class BoostBot_Bundle_Builder_Service
 *
 * Handles the logic for building AI-powered product bundles.
 *
 * @since 1.0.0
 */
class BoostBot_Bundle_Builder_Service {

    /**
     * Last resolved category strategy for diagnostics.
     *
     * @var string
     */
    private $last_category_strategy = 'none';

    /**
     * Default settings.
     *
     * @var array
     */
    private $default_settings = array(
        // Budget ranges remain useful as conversational guidance.
        'budget_ranges'               => array(),

        // Product Settings (legacy).
        'min_products'                => 2,
        'max_products'                => 5,
        'min_products_in_bundle'      => 2,
        'max_products_in_bundle'      => 5,
        'bundle_discount_percent'     => 10,
        'discount_percent'            => 10,
        'include_on_sale'             => true,
        'include_variable'            => true,
        'include_featured_only'       => false,
        'min_stock'                   => 1,
        'min_rating'                  => 0,
        'max_single_product_percent'  => 50,
        'categories'                  => array(),
        'allowed_categories'          => array(),
        'exclude_categories'          => array(),
        'excluded_categories'         => array(),
        'specific_product_ids'        => array(),
        'preferred_product_ids'       => array(),
        'excluded_product_ids'        => array(),

        // Pricing Settings.
        'pricing_settings'            => array(
            'discount_type'           => 'percent',
            'discount_percent'        => 10,
            'discount_fixed'          => 0,
            'discount_tiers'          => array(),
            'max_discount_amount'     => null,
            'min_order_for_discount'  => 0,
            'show_savings'            => true,
            'show_original_prices'    => true,
            'show_per_product_savings' => false,
            'round_prices'            => true,
            'round_to'                => 0,
        ),

        // Coupon Settings.
        'coupon_settings'             => array(
            'enabled'                 => false,
            'mode'                    => 'none',
            'auto_coupon_id'          => null,
            'auto_select_best'        => false,
            'generate_unique'         => false,
            'generated_prefix'        => 'BUNDLE-',
            'generated_discount'      => 10,
            'generated_expiry_days'   => 7,
            'show_coupon_code'        => true,
            'show_coupon_value'       => true,
        ),

        // UI Settings.
        'allow_customization'         => true,
        'cart_redirect'               => true,

        // Messages.
        'intro_message'               => '',
        'loading_message'             => '',
        'success_message'             => '',
        'empty_message'               => '',
        'error_message'               => '',
    );

    /**
     * Get settings from WooCommerce preset or bot custom config.
     *
     * @since 1.3.0 (Week 25)
     * @param int   $bot_id          Bot ID (optional).
     * @param array $widget_settings Widget settings (fallback).
     * @return array Merged settings.
     */
    public function get_settings_from_preset( int $bot_id = 0, array $widget_settings = array() ): array {
        $settings = $this->default_settings;

        // Try to get bot-specific WooCommerce settings.
        if ( $bot_id > 0 ) {
            $bot_model = new BoostBot_Bot_Model();
            $bot       = $bot_model->get( $bot_id );

            if ( $bot && ! empty( $bot->woocommerce_preset_id ) ) {
                $preset_settings = $this->get_settings_from_preset_id( (int) $bot->woocommerce_preset_id );
                if ( $preset_settings ) {
                    $settings = wp_parse_args( $preset_settings, $settings );
                    return $settings;
                }
            }
        }

        // Fallback to widget settings.
        if ( ! empty( $widget_settings ) ) {
            $settings = wp_parse_args( $widget_settings, $settings );
        }

        return $settings;
    }

    /**
     * Get settings from preset ID.
     *
     * @since 1.3.0 (Week 25)
     * @param int $preset_id Preset ID.
     * @return array|null Settings array or null.
     */
    public function get_settings_from_preset_id( int $preset_id ): ?array {
        $preset_model = new BoostBot_WooCommerce_Preset_Model();
        $preset       = $preset_model->get( $preset_id );

        if ( ! $preset || ! $preset->bundle_enabled ) {
            return null;
        }

        $parse_json = function ( $value ) {
            if ( is_array( $value ) ) {
                return $value;
            }
            if ( is_string( $value ) && '' !== $value ) {
                $decoded = json_decode( $value, true );
                return is_array( $decoded ) ? $decoded : array();
            }
            return array();
        };

        // Two-tier product scope: prefer bundle-specific fields when set, fall back to global.
        $use_bundle_scope = ! empty( $preset->bundle_product_mode );

        $product_mode = $use_bundle_scope
            ? ( $preset->bundle_product_mode ?? 'all' )
            : ( $preset->product_mode ?? 'all' );

        $allowed_categories = $use_bundle_scope
            ? $parse_json( $preset->bundle_allowed_categories )
            : $parse_json( $preset->allowed_categories );

        $excluded_categories = $use_bundle_scope
            ? $parse_json( $preset->bundle_excluded_categories )
            : $parse_json( $preset->excluded_categories );

        $specific_product_ids = $use_bundle_scope
            ? $parse_json( $preset->bundle_specific_product_ids )
            : $parse_json( $preset->specific_product_ids );

        $excluded_product_ids = $use_bundle_scope
            ? $parse_json( $preset->bundle_excluded_product_ids )
            : $parse_json( $preset->excluded_product_ids );

        return array(
            'min_products'            => (int) $preset->bundle_min_products,
            'max_products'            => (int) $preset->bundle_max_products,
            'min_products_in_bundle'  => (int) $preset->bundle_min_products,
            'max_products_in_bundle'  => (int) $preset->bundle_max_products,
            'bundle_discount_percent' => (float) $preset->bundle_discount_value,
            'discount_percent'        => (float) $preset->bundle_discount_value,
            'discount_type'           => $preset->bundle_discount_type,
            'discount_tiers'          => $parse_json( $preset->discount_tiers ),
            'include_on_sale'         => (bool) $preset->include_on_sale,
            'include_variable'        => (bool) $preset->include_variable,
            'mode'                    => $product_mode,
            'allowed_categories'      => $allowed_categories,
            'excluded_categories'     => $excluded_categories,
            'categories'              => $allowed_categories,
            'exclude_categories'      => $excluded_categories,
            'specific_product_ids'    => $specific_product_ids,
            'preferred_product_ids'   => $parse_json( $preset->preferred_product_ids ),
            'excluded_product_ids'    => $excluded_product_ids,
            'show_savings'            => (bool) $preset->show_savings,
            'max_discount_amount'     => ( isset( $preset->max_discount_amount ) && '' !== $preset->max_discount_amount && null !== $preset->max_discount_amount )
                ? (float) $preset->max_discount_amount
                : null,
            'min_order_for_discount'  => (float) ( $preset->min_order_for_discount ?? 0 ),
            'success_message'         => $preset->success_message ?: '',
            'empty_message'           => $preset->empty_message ?: '',
            'preset_id'               => $preset_id,
        );
    }

    /**
     * Build bundle with preset settings.
     *
     * Convenience method that automatically loads settings from preset.
     *
     * @since 1.3.0 (Week 25)
     * @param array $criteria  Customer criteria.
     * @param int   $bot_id    Bot ID (optional).
     * @param int   $preset_id Preset ID (optional, overrides bot's preset).
     * @return array Bundle data.
     */
    public function build_bundle_from_preset( array $criteria, int $bot_id = 0, int $preset_id = 0 ): array {
        $settings = array();

        // If preset ID is directly provided, use it.
        if ( $preset_id > 0 ) {
            $settings = $this->get_settings_from_preset_id( $preset_id ) ?? array();
        } elseif ( $bot_id > 0 ) {
            // Otherwise, get settings from bot's preset.
            $settings = $this->get_settings_from_preset( $bot_id );
        }

        // Merge with default settings.
        $settings = wp_parse_args( $settings, $this->default_settings );

        return $this->build_bundle( $criteria, $settings );
    }

    /**
     * Build a personalized bundle.
     *
     * @since 1.0.0
     * @param array $criteria Customer criteria (budget, preferences, etc.).
     * @param array $settings Widget settings.
     * @return array Bundle data including products, pricing, and savings.
     */
    public function build_bundle( array $criteria, array $settings = array(), string $language = 'he' ): array {
        $settings = wp_parse_args( $settings, $this->default_settings );

        $budget       = floatval( $criteria['budget'] ?? 100000 );
        $category     = $criteria['category'] ?? null;
        $preferences  = $criteria['preferences'] ?? array();
        $exclude      = $criteria['exclude'] ?? array();
        $max_single_product_percent = floatval( $settings['max_single_product_percent'] ?? 0 );

        $min_products = intval( $criteria['min_products'] ?? $settings['min_products_in_bundle'] ?? $settings['min_products'] ?? 2 );
        $max_products = intval( $criteria['max_products'] ?? $settings['max_products_in_bundle'] ?? $settings['max_products'] ?? 5 );

        BoostBot_Logger::debug( '[Bundle Build] Request with cart context', array(
            'budget'       => $budget,
            'category'     => $category,
            'language'     => $language,
            'preferences'  => $preferences,
            'excluded_ids' => $exclude,
            'min_max'      => $min_products . '/' . $max_products,
        ) );

        $products = $this->get_candidate_products( $category, $settings, $language );

        BoostBot_Logger::debug( 'Candidate products found: ' . count( $products ) );

        $products = $this->filter_excluded( $products, $exclude, $settings );

        $preferred_product_ids = array_map( 'intval', (array) ( $settings['preferred_product_ids'] ?? array() ) );
        $scored                = $this->score_products( $products, $preferences, $language, $preferred_product_ids );

        // Build optimal bundle within budget.
        $bundle = $this->optimize_bundle( $scored, $budget, $min_products, $max_products, $max_single_product_percent );

        // Build merged pricing settings from all sources.
        // Flat keys (from preset merge) take priority over nested defaults.
        $nested_pricing = $settings['pricing_settings'] ?? array();
        $merged_pricing = array(
            'discount_type'    => $settings['discount_type']
                ?? $nested_pricing['discount_type']
                ?? 'percent',
            'discount_percent' => floatval(
                $settings['discount_percent']
                ?? $settings['bundle_discount_percent']
                ?? $nested_pricing['discount_percent']
                ?? 10
            ),
            'discount_fixed'   => floatval( $settings['discount_fixed'] ?? $nested_pricing['discount_fixed'] ?? 0 ),
            'discount_tiers'   => ! empty( $settings['discount_tiers'] )
                ? $settings['discount_tiers']
                : ( $nested_pricing['discount_tiers'] ?? array() ),
            // Discount caps — carried through so apply_discount() can enforce them.
            'max_discount_amount'    => $settings['max_discount_amount']
                ?? $nested_pricing['max_discount_amount']
                ?? null,
            'min_order_for_discount' => floatval(
                $settings['min_order_for_discount']
                ?? $nested_pricing['min_order_for_discount']
                ?? 0
            ),
        );

        $bundle = $this->apply_discount( $bundle, $merged_pricing );
        $bundle = $this->enforce_post_discount_budget( $bundle, $budget, $merged_pricing );

        BoostBot_Logger::debug(
            '[Bundle Build] Budget check',
            array(
                'stated_budget'     => $budget,
                'bundle_total'      => floatval( $bundle['total_price'] ?? 0 ),
                'exceeded'          => floatval( $bundle['total_price'] ?? 0 ) > $budget,
                'category_strategy' => $this->last_category_strategy,
                'product_count'     => count( $bundle['products'] ?? array() ),
            )
        );

        return $bundle;
    }

    /**
     * Ensure the final displayed bundle total still respects the requested budget.
     *
     * The initial product selection uses live WooCommerce prices, but the final
     * widget display may be recalculated from regular prices when bundle pricing
     * rules are applied. Trim the most expensive displayed item until the final
     * total fits the shopper's stated budget.
     *
     * @param array $bundle Bundle data after discount application.
     * @param float $budget Shopper budget.
     * @param array $pricing_settings Pricing settings used for reapplication.
     * @return array
     */
    private function enforce_post_discount_budget( array $bundle, float $budget, array $pricing_settings ): array {
        if ( $budget <= 0 || empty( $bundle['products'] ) ) {
            return $bundle;
        }

        $final_total = floatval( $bundle['total_price'] ?? 0 );
        if ( $final_total <= $budget ) {
            return $bundle;
        }

        BoostBot_Logger::debug(
            '[Bundle Build] Post-discount budget exceeded',
            array(
                'budget'        => $budget,
                'final_total'   => $final_total,
                'product_count' => count( $bundle['products'] ),
            )
        );

        while ( $final_total > $budget && ! empty( $bundle['products'] ) ) {
            $highest_index = null;
            $highest_price = -1;

            foreach ( $bundle['products'] as $index => $product ) {
                $displayed_price = floatval( $product['price'] ?? $product['bundle_price'] ?? $product['original_price'] ?? 0 );
                if ( $displayed_price > $highest_price ) {
                    $highest_price = $displayed_price;
                    $highest_index = $index;
                }
            }

            if ( null === $highest_index ) {
                break;
            }

            $removed_product = $bundle['products'][ $highest_index ];
            array_splice( $bundle['products'], $highest_index, 1 );

            BoostBot_Logger::debug(
                '[Bundle Build] Removed item after post-discount overflow',
                array(
                    'removed_product_id'    => absint( $removed_product['id'] ?? 0 ),
                    'removed_product_name'  => $removed_product['name'] ?? '',
                    'removed_product_price' => floatval( $removed_product['price'] ?? $removed_product['bundle_price'] ?? $removed_product['original_price'] ?? 0 ),
                    'remaining_products'    => count( $bundle['products'] ),
                )
            );

            if ( empty( $bundle['products'] ) ) {
                $bundle['total_price']              = 0;
                $bundle['original_price']           = 0;
                $bundle['savings']                  = 0;
                $bundle['savings_percent']          = 0;
                $bundle['applied_discount_percent'] = 0;
                $bundle['applied_discount_type']    = $pricing_settings['discount_type'] ?? 'percent';
                break;
            }

            $bundle      = $this->apply_discount( $bundle, $pricing_settings );
            $final_total = floatval( $bundle['total_price'] ?? 0 );
        }

        BoostBot_Logger::debug(
            '[Bundle Build] Post-trim budget result',
            array(
                'budget'        => $budget,
                'final_total'   => floatval( $bundle['total_price'] ?? 0 ),
                'product_count' => count( $bundle['products'] ?? array() ),
                'within_budget' => floatval( $bundle['total_price'] ?? 0 ) <= $budget,
            )
        );

        return $bundle;
    }

    /**
     * Get candidate products from WooCommerce.
     *
     * @since 1.0.0
     * @param string|null $category Specific category to filter.
     * @param array       $settings Widget settings.
     * @param string      $language Language code.
     * @return array Array of WC_Product objects.
     */
    private function get_candidate_products( ?string $category, array $settings, string $language = 'he' ): array {
        if ( ! class_exists( 'WooCommerce' ) ) {
            return array();
        }

        $this->last_category_strategy = 'none';
        $category_name_match    = null;
        $category_slug_match    = null;
        $resolved_category      = '';
        $resolved_category_id   = 0;
        $category_strategy      = 'none';
        $args = array(
            'status'  => 'publish',
            'limit'   => 100,
            'orderby' => 'popularity',
            'order'   => 'DESC',
        );

        // Honor an explicit "specific products" scope from the preset: restrict the query
        // to the curated product list instead of scanning the whole catalog. Without this,
        // a preset limited to specific products still built bundles from arbitrary products.
        $scope_mode   = $settings['mode'] ?? '';
        $specific_ids = array_values( array_filter( array_map( 'intval', (array) ( $settings['specific_product_ids'] ?? array() ) ) ) );
        if ( 'specific' === $scope_mode && ! empty( $specific_ids ) ) {
            $args['include'] = $specific_ids;
            // A curated product list takes precedence over category narrowing.
            $category = null;
        }

        // Filter by specific category (only if provided).
        if ( $category ) {
            $category_name_match = get_term_by( 'name', $category, 'product_cat' );
            $category_slug_match = get_term_by( 'slug', $category, 'product_cat' );

            if ( $category_name_match && ! is_wp_error( $category_name_match ) ) {
                $resolved_category    = $category_name_match->slug;
                $resolved_category_id = (int) $category_name_match->term_id;
                $category_strategy    = 'name';
                BoostBot_Logger::debug(
                    '[Bundle Build] Category resolved by name',
                    array(
                        'requested_category' => $category,
                        'term_id'            => (int) $category_name_match->term_id,
                        'term_name'          => $category_name_match->name,
                        'resolved_slug'      => $resolved_category,
                    )
                );
            } elseif ( $category_slug_match && ! is_wp_error( $category_slug_match ) ) {
                $resolved_category    = $category_slug_match->slug;
                $resolved_category_id = (int) $category_slug_match->term_id;
                $category_strategy    = 'slug';
                BoostBot_Logger::debug(
                    '[Bundle Build] Category resolved by slug',
                    array(
                        'requested_category' => $category,
                        'term_id'            => (int) $category_slug_match->term_id,
                        'term_name'          => $category_slug_match->name,
                        'resolved_slug'      => $resolved_category,
                    )
                );
            } else {
                $fuzzy_matches = get_terms(
                    array(
                        'taxonomy'   => 'product_cat',
                        'name__like' => $category,
                        'hide_empty' => true,
                        'number'     => 1,
                    )
                );

                if ( ! is_wp_error( $fuzzy_matches ) && ! empty( $fuzzy_matches ) ) {
                    $resolved_category    = $fuzzy_matches[0]->slug;
                    $resolved_category_id = (int) $fuzzy_matches[0]->term_id;
                    $category_strategy    = 'fuzzy_name';
                    BoostBot_Logger::debug(
                        '[Bundle Build] Category fuzzy-matched',
                        array(
                            'requested_category' => $category,
                            'term_id'            => (int) $fuzzy_matches[0]->term_id,
                            'term_name'          => $fuzzy_matches[0]->name,
                            'resolved_slug'      => $resolved_category,
                        )
                    );
                } else {
                    $category_strategy = 'fallback_no_filter';
                    BoostBot_Logger::warning(
                        '[Bundle Build] Category not found, removing category filter',
                        array(
                            'requested_category' => $category,
                            'language'           => $language,
                        )
                    );
                }
            }

            if ( $resolved_category_id > 0 ) {
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- WooCommerce product category filtering
                $args['tax_query'] = array(
                    array(
                        'taxonomy' => 'product_cat',
                        'field'    => 'term_id',
                        'terms'    => array( $resolved_category_id ),
                    ),
                );
            }
        }

        // Filter by widget categories setting (support both old and new keys).
        $widget_categories = ! empty( $settings['categories'] ) ? $settings['categories'] : ( $settings['allowed_categories'] ?? array() );
        if ( ! empty( $widget_categories ) && is_array( $widget_categories ) ) {
            if ( is_numeric( $widget_categories[0] ) ) {
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- WooCommerce widget category filtering
                $args['tax_query'] = array( array(
                    'taxonomy' => 'product_cat',
                    'field'    => 'term_id',
                    'terms'    => array_map( 'absint', $widget_categories ),
                ) );
            } else {
                $args['category'] = $widget_categories;
            }
        }

        BoostBot_Logger::debug(
            '[Bundle Build] Candidate query prepared',
            array(
                'requested_category'    => $category,
                'requested_category_name_match' => $category_name_match ? array(
                    'term_id' => (int) $category_name_match->term_id,
                    'name'    => $category_name_match->name,
                    'slug'    => $category_name_match->slug,
                ) : null,
                'requested_category_slug_match' => $category_slug_match ? array(
                    'term_id' => (int) $category_slug_match->term_id,
                    'name'    => $category_slug_match->name,
                    'slug'    => $category_slug_match->slug,
                ) : null,
                'resolved_category'     => $resolved_category,
                'resolved_category_id'  => $resolved_category_id,
                'category_strategy'     => $category_strategy,
                'query_category_arg'    => $args['category'] ?? array(),
                'has_tax_query'         => ! empty( $args['tax_query'] ),
                'query_tax_terms'       => ! empty( $args['tax_query'][0]['terms'] ) ? $args['tax_query'][0]['terms'] : array(),
                'widget_categories'     => $widget_categories,
                'language'              => $language,
            )
        );

        // Query products inside the requested Polylang context when available.
        $product_context = class_exists( 'BoostBot_Polylang_Context' ) ? new BoostBot_Polylang_Context() : null;
        $did_switch_language = false;

        if ( $language && $product_context instanceof BoostBot_Polylang_Context ) {
            $did_switch_language = $product_context->switch_to( $language );
        }

        try {
            $products = wc_get_products( $args );
        } finally {
            if ( $did_switch_language ) {
                $product_context->restore();
            }
        }

        if ( $resolved_category_id > 0 && empty( $products ) ) {
            $term_product_ids = get_objects_in_term( $resolved_category_id, 'product_cat' );

            if ( ! is_wp_error( $term_product_ids ) && ! empty( $term_product_ids ) ) {
                $term_product_ids = array_values(
                    array_filter(
                        array_map( 'absint', $term_product_ids ),
                        static function ( $product_id ) {
                            return 'product' === get_post_type( $product_id ) && 'publish' === get_post_status( $product_id );
                        }
                    )
                );

                if ( ! empty( $term_product_ids ) ) {
                    $fallback_args = $args;
                    unset( $fallback_args['category'], $fallback_args['tax_query'] );
                    $fallback_args['include'] = $term_product_ids;
                    $fallback_args['orderby'] = 'include';
                    $fallback_args['limit']   = min( count( $term_product_ids ), 100 );

                    $products = wc_get_products( $fallback_args );

                    if ( empty( $products ) ) {
                        $products = array_values(
                            array_filter(
                                array_map( 'wc_get_product', $term_product_ids )
                            )
                        );
                    }

                    BoostBot_Logger::debug(
                        '[Bundle Build] Recovered category products via direct term lookup',
                        array(
                            'requested_category'   => $category,
                            'resolved_category_id' => $resolved_category_id,
                            'fallback_product_ids' => array_slice( $term_product_ids, 0, 20 ),
                            'fallback_count'       => count( $products ),
                            'language'             => $language,
                        )
                    );
                }
            }
        }

        $raw_product_count = count( $products );

        BoostBot_Logger::debug(
            '[Bundle Build] Raw candidate query result',
            array(
                'requested_category' => $category,
                'resolved_category'  => $resolved_category,
                'resolved_category_id' => $resolved_category_id,
                'category_strategy'  => $category_strategy,
                'raw_product_count'  => $raw_product_count,
                'language'           => $language,
            )
        );

        // Resolve products to the target language via Polylang translations.
        // The 'lang' WC query param is unreliable when pll_set_current_language()
        // is absent, so we translate each product individually.
        if ( $language && ! empty( $products ) && function_exists( 'pll_get_post_language' ) && function_exists( 'pll_get_post' ) ) {
            $resolved = array();
            $seen_ids = array();
            foreach ( $products as $product ) {
                $pid  = $product->get_id();
                $lang = pll_get_post_language( $pid );

                if ( $lang && $lang !== $language ) {
                    $translated_id = pll_get_post( $pid, $language );
                    if ( $translated_id && $translated_id !== $pid ) {
                        $translated = wc_get_product( $translated_id );
                        if ( $translated ) {
                            $product = $translated;
                            $pid     = $translated_id;
                        }
                    }
                }

                if ( in_array( $pid, $seen_ids, true ) ) {
                    continue;
                }
                $seen_ids[] = $pid;
                $resolved[] = $product;
            }
            $products = $resolved;
        }
        $language_resolved_count = count( $products );

        // Filter by settings.
        $filtered_products = array();
        foreach ( $products as $product ) {
            // Must be in stock.
            if ( ! $product->is_in_stock() ) {
                continue;
            }
            // Check variable products setting.
            if ( $product->is_type( 'variable' ) && empty( $settings['include_variable'] ) ) {
                continue;
            }
            // Must have a price.
            if ( ! $product->get_price() ) {
                continue;
            }
            
            $filtered_products[] = $product;
        }

        BoostBot_Logger::debug(
            '[Bundle Build] Candidate filter result',
            array(
                'requested_category'      => $category,
                'resolved_category'       => $resolved_category,
                'resolved_category_id'    => $resolved_category_id,
                'category_strategy'       => $category_strategy,
                'raw_product_count'       => $raw_product_count,
                'language_resolved_count' => $language_resolved_count,
                'filtered_product_count'  => count( $filtered_products ),
                'language'                => $language,
            )
        );

        if ( $category && empty( $filtered_products ) ) {
            BoostBot_Logger::warning(
                '[Bundle Build] No candidate products after category filtering',
                array(
                    'requested_category'          => $category,
                    'resolved_category'           => $resolved_category,
                    'resolved_category_id'        => $resolved_category_id,
                    'requested_category_strategy' => $category_strategy,
                    'requested_category_name_match' => $category_name_match ? array(
                        'term_id' => (int) $category_name_match->term_id,
                        'name'    => $category_name_match->name,
                        'slug'    => $category_name_match->slug,
                    ) : null,
                    'requested_category_slug_match' => $category_slug_match ? array(
                        'term_id' => (int) $category_slug_match->term_id,
                        'name'    => $category_slug_match->name,
                        'slug'    => $category_slug_match->slug,
                    ) : null,
                    'query_category_arg'          => $args['category'] ?? array(),
                    'query_tax_terms'             => ! empty( $args['tax_query'][0]['terms'] ) ? $args['tax_query'][0]['terms'] : array(),
                    'widget_categories'           => $widget_categories,
                    'raw_product_count'           => $raw_product_count,
                    'language_resolved_count'     => $language_resolved_count,
                    'language'                    => $language,
                )
            );
        }

        $this->last_category_strategy = $category_strategy;

        return $filtered_products;
    }

    /**
     * Filter excluded products.
     *
     * @since 1.0.0
     * @param array $products Array of products.
     * @param array $exclude  Array of product IDs to exclude.
     * @param array $settings Widget settings.
     * @return array Filtered products.
     */
    private function filter_excluded( array $products, array $exclude, array $settings ): array {
        // Get excluded category IDs.
        $excluded_categories = $settings['exclude_categories'] ?? array();

        // Merchant-configured product exclusions from the preset/widget settings. These
        // were previously ignored here, so explicitly-excluded products could still be
        // scored and placed into (and sold in) a bundle.
        $excluded_product_ids = array_map( 'intval', (array) ( $settings['excluded_product_ids'] ?? array() ) );

        return array_filter(
            $products,
            function ( $product ) use ( $exclude, $excluded_categories, $excluded_product_ids ) {
                $product_id = (int) $product->get_id();

                // Check runtime excluded IDs (e.g. items already in the cart).
                if ( in_array( $product->get_id(), $exclude, true ) ) {
                    return false;
                }

                // Check merchant-configured excluded product IDs.
                if ( $excluded_product_ids && in_array( $product_id, $excluded_product_ids, true ) ) {
                    return false;
                }

                // Check excluded categories.
                if ( ! empty( $excluded_categories ) ) {
                    $product_cats = wp_get_post_terms( $product->get_id(), 'product_cat', array( 'fields' => 'slugs' ) );
                    if ( array_intersect( $product_cats, $excluded_categories ) ) {
                        return false;
                    }
                }

                return true;
            }
        );
    }

    /**
     * Score products by relevance.
     *
     * IMPORTANT: Preference matching is the PRIMARY scoring factor.
     * Products that match customer preferences get MASSIVE bonus points
     * to ensure they are selected over random popular products.
     *
     * @since 1.0.0
     * @param array $products              Array of products.
     * @param array $preferences           Customer preferences/keywords.
     * @param string $language             Language code.
     * @param array $preferred_product_ids Merchant-preferred product IDs to boost.
     * @return array Array of scored products.
     */
    private function score_products( array $products, array $preferences, string $language = 'he', array $preferred_product_ids = array() ): array {
        $scored = array();
        $has_preferences = ! empty( $preferences );
        $preferred       = array_map( 'intval', $preferred_product_ids );

        BoostBot_Logger::debug( sprintf(
            'Scoring %d products with preferences: %s (lang=%s)',
            count( $products ),
            wp_json_encode( $preferences ),
            $language
        ) );

        foreach ( $products as $product ) {
            $score = 0;
            $preference_matches = 0;

            // Merchant-preferred products get a large boost so they surface first
            // (mirrors BoostBot_Bundle_Product_Selector::score_products).
            if ( $preferred && in_array( (int) $product->get_id(), $preferred, true ) ) {
                $score += 100;
            }

            if ( $has_preferences ) {
                foreach ( $preferences as $pref ) {
                    if ( $this->matches_preference( $product, $pref, $language ) ) {
                        $preference_matches++;
                        $score += 500; // MASSIVE bonus for relevance
                    }
                }
            }

            // ============================================
            // SECONDARY SCORES (only matter when no preferences or equal matches)
            // ============================================
            
            // Popularity score (sales count) - max 30 points.
            $sales = $product->get_total_sales();
            $score += min( $sales / 10, 30 );

            // Rating score - max 25 points.
            $rating = floatval( $product->get_average_rating() );
            $score += $rating * 5;

            // Review count score - max 20 points.
            $reviews = $product->get_review_count();
            $score  += min( $reviews, 20 );

            // On sale bonus - 15 points.
            if ( $product->is_on_sale() ) {
                $score += 15;
            }

            // Featured bonus - 10 points.
            if ( $product->is_featured() ) {
                $score += 10;
            }

            $scored[] = array(
                'product'            => $product,
                'score'              => $score,
                'preference_matches' => $preference_matches,
            );
        }

        // Sort by score descending.
        usort(
            $scored,
            function ( $a, $b ) {
                return $b['score'] - $a['score'];
            }
        );

        // ============================================
        // Prefer exact preference matches when available, but if a generic
        // request like "gift bundle" matches nothing directly, fall back to
        // the scored catalog instead of returning an empty set.
        // ============================================
        if ( $has_preferences ) {
            $matching_products = array_filter(
                $scored,
                function ( $item ) {
                    return $item['preference_matches'] > 0;
                }
            );

            // If we have matching products, use only those
            if ( ! empty( $matching_products ) ) {
                $product_names = array_map( function( $item ) {
                    return $item['product']->get_name();
                }, $matching_products );
                BoostBot_Logger::debug( 'Found ' . count( $matching_products ) . ' products matching preferences', array(
                    'preferences' => $preferences,
                    'products'    => $product_names,
                ) );
                return array_values( $matching_products );
            }
            
            BoostBot_Logger::debug( 'No products matched preferences — falling back to scored catalog', array(
                'preferences' => $preferences,
            ) );
        }

        return $scored;
    }

    /**
     * Check if product matches a preference.
     *
     * Enhanced matching with:
     * - Partial word matching
     * - Hebrew synonyms support
     * - Category and tag matching
     *
     * @since 1.0.0
     * @param WC_Product $product    Product to check.
     * @param string     $preference Preference keyword.
     * @return bool True if matches.
     */
    private function matches_preference( $product, string $preference, string $language = 'he' ): bool {
        $preference = mb_strtolower( trim( $preference ) );

        if ( empty( $preference ) ) {
            return false;
        }

        $keywords = $this->get_preference_keywords( $preference, $language );
        
        // Get all product text to search in
        $product_text = $this->get_product_searchable_text( $product );
        $product_text = mb_strtolower( $product_text );

        // Check if ANY keyword matches
        foreach ( $keywords as $keyword ) {
            if ( mb_strpos( $product_text, $keyword ) !== false ) {
                BoostBot_Logger::debug( sprintf(
                    'Product "%s" matches keyword "%s" (from preference "%s")',
                    $product->get_name(),
                    $keyword,
                    $preference
                ) );
                return true;
            }
        }

        return false;
    }
    
    /**
     * Get preference keywords for matching.
     *
     * DYNAMIC APPROACH: This function does NOT use hardcoded synonyms!
     * Instead, it returns the keyword as-is and relies on substring matching
     * against actual WooCommerce product data (names, descriptions, categories).
     *
     * This works for ANY business type - the matching happens dynamically
     * based on what products actually exist in the store.
     *
     * @since 1.0.0
     * @param string $preference Original preference keyword.
     * @return array Array of keywords to match (the keyword itself).
     */
    private function get_preference_keywords( string $preference, string $language = 'he' ): array {
        $preference = mb_strtolower( trim( $preference ) );

        if ( empty( $preference ) ) {
            return array();
        }

        $keywords = array( $preference );

        if ( $language === 'en' ) {
            // No prefix stripping or suffix expansion for English
            return $keywords;
        }

        // Hebrew prefix stripping
        $prefixes = array( 'ל', 'ב', 'ה', 'ו', 'מ', 'כ', 'ש' );
        foreach ( $prefixes as $prefix ) {
            if ( mb_strpos( $preference, $prefix ) === 0 && mb_strlen( $preference ) > 2 ) {
                $without_prefix = mb_substr( $preference, 1 );
                if ( mb_strlen( $without_prefix ) >= 2 ) {
                    $keywords[] = $without_prefix;
                }
                break;
            }
        }

        // Hebrew suffix expansion
        $base = $keywords[0];
        $last_char = mb_substr( $base, -1 );

        if ( $last_char !== 'ם' && $last_char !== 'ת' && $last_char !== 'י' ) {
            $keywords[] = $base . 'ים';
            $keywords[] = $base . 'ות';
            $keywords[] = $base . 'י';
        }

        return array_unique( $keywords );
    }
    
    /**
     * Get all searchable text from a product.
     *
     * @param WC_Product $product Product object.
     * @return string All product text concatenated.
     */
    private function get_product_searchable_text( $product ): string {
        $parts = array();
        
        // Name
        $parts[] = $product->get_name();
        
        // Short description
        $parts[] = $product->get_short_description();
        
        // Description
        $parts[] = $product->get_description();
        
        // Categories
        $categories = wp_get_post_terms( $product->get_id(), 'product_cat', array( 'fields' => 'names' ) );
        $parts = array_merge( $parts, $categories );
        
        // Tags
        $tags = wp_get_post_terms( $product->get_id(), 'product_tag', array( 'fields' => 'names' ) );
        $parts = array_merge( $parts, $tags );
        
        // SKU
        $parts[] = $product->get_sku();
        
        return implode( ' ', array_filter( $parts ) );
    }

    /**
     * Optimize bundle for budget.
     *
     * Uses a greedy algorithm to select the best products within budget.
     *
     * @since 1.0.0
     * @param array $scored Scored products.
     * @param float $budget Maximum budget.
     * @param int   $min    Minimum products.
     * @param int   $max    Maximum products.
     * @return array Bundle data.
     */
    private function optimize_bundle( array $scored, float $budget, int $min, int $max, float $max_single_product_percent = 0 ): array {
        $bundle = array();
        $total  = 0;

        if ( $budget <= 0 ) {
            return array(
                'products'        => array(),
                'total_price'     => 0,
                'original_price'  => 0,
                'savings'         => 0,
                'savings_percent' => 0,
            );
        }

        // Pick a random target number of products between min and max
        // This creates variety in bundle sizes
        $target = wp_rand( $min, $max );

        foreach ( $scored as $item ) {
            $product = $item['product'];
            $price   = floatval( $product->get_price() );

            if ( $price <= 0 || $price > $budget ) {
                continue;
            }

            if ( $max_single_product_percent > 0 && $price > ( $budget * $max_single_product_percent / 100 ) ) {
                continue;
            }

            // Never exceed the shopper's requested budget.
            if ( ( $total + $price ) > $budget ) {
                continue;
            }

            // Stop when we reach our random target.
            if ( count( $bundle ) >= $target ) {
                break;
            }

            // Add product to bundle.
            $bundle[] = array(
                'id'             => $product->get_id(),
                'name'           => $product->get_name(),
                'price'          => $price,
                'regular_price'  => floatval( $product->get_regular_price() ),
                'original_price' => floatval( $product->get_regular_price() ?: $price ),
                'image'          => wp_get_attachment_url( $product->get_image_id() ) ?: '',
                'permalink'      => urldecode( $product->get_permalink() ),
                'rating'         => floatval( $product->get_average_rating() ),
                'on_sale'        => $product->is_on_sale(),
                'score'          => $item['score'],
            );

            $total += $price;
        }

        // Calculate totals.
        $original_total = array_sum( array_column( $bundle, 'original_price' ) );

        return array(
            'products'        => $bundle,
            'total_price'     => round( $total, 2 ),
            'original_price'  => round( $original_total, 2 ),
            'savings'         => 0,
            'savings_percent' => 0,
        );
    }

    /**
     * Apply bundle discount.
     *
     * Supports three discount types:
     * - percent/percentage: flat percentage off each product's original price
     * - tiered: percentage determined by bundle total (via BoostBot_Bundle_Discount_Calculator)
     * - fixed: fixed currency amount subtracted proportionally across products
     *
     * @since 1.0.0
     * @param array       $bundle           Bundle data.
     * @param array|float $pricing_settings  Pricing settings array or legacy float percentage.
     * @return array Bundle with discount applied.
     */
    private function apply_discount( array $bundle, $pricing_settings ): array {
        if ( empty( $bundle['products'] ) ) {
            return $bundle;
        }

        // Backward compatibility: accept a plain float as simple percentage.
        if ( is_numeric( $pricing_settings ) ) {
            $pricing_settings = array(
                'discount_type'    => 'percent',
                'discount_percent' => floatval( $pricing_settings ),
            );
        }

        $discount_type = $pricing_settings['discount_type'] ?? 'percent';

        // Normalize 'percentage' → 'percent' for consistency.
        if ( 'percentage' === $discount_type ) {
            $discount_type = 'percent';
        }

        // Enforce the minimum-order threshold: when the qualifying subtotal (live
        // selling prices) is below the configured minimum, skip the bundle discount
        // entirely and leave the live pricing untouched.
        $min_order = floatval( $pricing_settings['min_order_for_discount'] ?? 0 );
        if ( $min_order > 0 ) {
            $subtotal_live = 0;
            foreach ( $bundle['products'] as $p ) {
                $subtotal_live += floatval( $p['price'] ?? $p['original_price'] ?? 0 );
            }
            if ( $subtotal_live < $min_order ) {
                return $bundle;
            }
        }

        $total_original = 0;
        foreach ( $bundle['products'] as $p ) {
            $total_original += floatval( $p['original_price'] );
        }

        // Resolve the effective discount rate/amount based on type.
        if ( 'tiered' === $discount_type ) {
            $tiers = $this->normalize_tiers( $pricing_settings['discount_tiers'] ?? array() );
            $calculator = new BoostBot_Bundle_Discount_Calculator( array(
                'discount_type'  => 'tiered',
                'discount_tiers' => $tiers,
            ) );

            $calc_result    = $calculator->calculate( $bundle );
            $discount_percent = $calc_result['discount_percent'] ?? 0;

        } elseif ( 'fixed' === $discount_type ) {
            $fixed_amount = floatval( $pricing_settings['discount_fixed'] ?? 0 );
            $discount_percent = $total_original > 0
                ? min( 100, ( $fixed_amount / $total_original ) * 100 )
                : 0;

        } else {
            $discount_percent = floatval( $pricing_settings['discount_percent'] ?? 10 );
        }

        // Clamp to 0-100.
        $discount_percent = max( 0, min( 100, $discount_percent ) );
        $discount_multiplier = 1 - ( $discount_percent / 100 );

        $total_original   = 0;
        $total_live       = 0;
        $total_discounted = 0;
        $live_prices      = array();

        foreach ( $bundle['products'] as $idx => &$product ) {
            $original = floatval( $product['original_price'] );
            // The live selling price already reflects any active WooCommerce sale.
            $live       = floatval( $product['price'] ?? $original );
            // Base the discount on the regular price, but never charge above the
            // live price — otherwise on-sale items would be priced above their sale
            // price and reported savings would be wrong.
            $discounted = round( min( $original * $discount_multiplier, $live ), 2 );

            $product['price']        = $discounted;
            $product['bundle_price'] = $discounted;
            // Savings measured against the price the shopper would actually be charged.
            $product['savings']      = round( $live - $discounted, 2 );

            $live_prices[ $idx ] = $live;
            $total_original     += $original;
            $total_live         += $live;
            $total_discounted   += $discounted;
        }
        unset( $product );

        // Enforce the maximum total discount cap: if the aggregate savings exceed
        // the configured cap, scale each line's savings back proportionally so the
        // total discount equals the cap (keeps per-line cart prices consistent).
        $max_discount  = isset( $pricing_settings['max_discount_amount'] ) && null !== $pricing_settings['max_discount_amount']
            ? floatval( $pricing_settings['max_discount_amount'] )
            : 0;
        $total_savings = $total_live - $total_discounted;
        if ( $max_discount > 0 && $total_savings > $max_discount ) {
            $ratio            = $max_discount / $total_savings;
            $total_discounted = 0;

            foreach ( $bundle['products'] as $idx => &$product ) {
                $live        = $live_prices[ $idx ] ?? floatval( $product['price'] );
                $new_savings = round( floatval( $product['savings'] ) * $ratio, 2 );
                $discounted  = round( $live - $new_savings, 2 );

                $product['price']        = $discounted;
                $product['bundle_price'] = $discounted;
                $product['savings']      = $new_savings;

                $total_discounted += $discounted;
            }
            unset( $product );
        }

        $bundle['original_price']  = round( $total_original, 2 );
        $bundle['total_price']     = round( $total_discounted, 2 );
        $bundle['savings']         = round( $total_live - $total_discounted, 2 );
        $bundle['savings_percent'] = $total_live > 0
            ? round( ( ( $total_live - $total_discounted ) / $total_live ) * 100, 1 )
            : 0;

        $bundle['applied_discount_percent'] = $bundle['savings_percent'];
        $bundle['applied_discount_type']    = $discount_type;

        return $bundle;
    }

    /**
     * Normalize tier arrays to the format expected by BoostBot_Bundle_Discount_Calculator.
     *
     * Handles both admin-UI format (min_amount/max_amount/discount) and
     * alternate format (min_total/max_total/discount_percent).
     *
     * @param array $tiers Raw tiers from settings.
     * @return array Normalized tiers.
     */
    private function normalize_tiers( array $tiers ): array {
        if ( is_string( $tiers ) ) {
            $tiers = json_decode( $tiers, true );
            if ( ! is_array( $tiers ) ) {
                return array();
            }
        }

        return array_map( function ( $tier ) {
            return array(
                'min_amount' => floatval( $tier['min_amount'] ?? $tier['min_total'] ?? 0 ),
                'max_amount' => isset( $tier['max_amount'] )
                    ? ( null === $tier['max_amount'] ? null : floatval( $tier['max_amount'] ) )
                    : ( isset( $tier['max_total'] )
                        ? ( null === $tier['max_total'] ? null : floatval( $tier['max_total'] ) )
                        : null ),
                'discount'   => floatval( $tier['discount'] ?? $tier['discount_percent'] ?? 0 ),
            );
        }, $tiers );
    }

    /**
     * Get WooCommerce categories for discovery.
     *
     * @since 1.0.0
     * @param array $include Optional category slugs to include.
     * @return array Array of categories.
     */
    public function get_categories( array $include = array() ): array {
        if ( ! class_exists( 'WooCommerce' ) ) {
            return array();
        }

        $args = array(
            'taxonomy'   => 'product_cat',
            'hide_empty' => true,
            'orderby'    => 'count',
            'order'      => 'DESC',
            'number'     => 20,
        );

        if ( ! empty( $include ) ) {
            $args['slug'] = $include;
        }

        $categories = get_terms( $args );

        if ( is_wp_error( $categories ) ) {
            return array();
        }

        return array_map(
            function ( $cat ) {
                return array(
                    'id'    => $cat->term_id,
                    'slug'  => $cat->slug,
                    'name'  => $cat->name,
                    'count' => $cat->count,
                );
            },
            $categories
        );
    }

    /**
     * Get budget range suggestions.
     *
     * @since 1.0.0
     * @param array $settings Widget settings.
     * @return array Array of budget ranges.
     */
    public function get_budget_ranges( array $settings = array(), string $language = '' ): array {
        // Return custom ranges if set.
        if ( ! empty( $settings['budget_ranges'] ) ) {
            return $settings['budget_ranges'];
        }

        $symbol = $language
            ? BoostBot_Polylang_Context::get_currency_for_language( $language )['symbol']
            : ( function_exists( 'get_woocommerce_currency_symbol' )
                ? html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8' )
                : '₪' );

        return array(
            array(
                /* translators: %s: currency symbol */
                'label' => sprintf( __( 'עד %s200', 'boostbot' ), $symbol ),
                'min'   => 0,
                'max'   => 200,
            ),
            array(
                'label' => sprintf( '%s200-500', $symbol ),
                'min'   => 200,
                'max'   => 500,
            ),
            array(
                'label' => sprintf( '%s500-1000', $symbol ),
                'min'   => 500,
                'max'   => 1000,
            ),
            array(
                'label' => sprintf( '%s1000+', $symbol ),
                'min'   => 1000,
                'max'   => null,
            ),
        );
    }

    /**
     * Generate add-to-cart URL for bundle.
     *
     * @since 1.0.0
     * @param array $bundle Bundle data.
     * @return string Add to cart URL.
     */
    public function get_bundle_cart_url( array $bundle ): string {
        if ( empty( $bundle['products'] ) || ! function_exists( 'wc_get_cart_url' ) ) {
            return '';
        }

        $wc_service = class_exists( 'BoostBot_WooCommerce_Service' ) ? new BoostBot_WooCommerce_Service() : null;
        $cart_url   = $wc_service instanceof BoostBot_WooCommerce_Service
            ? $wc_service->get_store_page_url( 'cart', (string) ( $bundle['language'] ?? '' ) )
            : wc_get_cart_url();

        // For multiple products, use a custom endpoint that we'll create
        // Build comma-separated list of product IDs
        $product_ids = array();
        foreach ( $bundle['products'] as $product ) {
            $product_ids[] = $product['id'];
        }

        // Use custom URL with all product IDs
        $url = add_query_arg(
            array(
                'boostbot_bundle_add' => implode( ',', $product_ids ),
                'bundle_id'           => $bundle['bundle_id'] ?? 0,
            ),
            $cart_url
        );

        return $url;
    }

    /**
     * Handle bundle add to cart from URL parameter.
     * This is called via WordPress init hook when boostbot_bundle_add param is present.
     *
     * @since 1.0.0
     */
    public static function handle_bundle_add_from_url(): void {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Shareable add-to-cart URL (mirrors WooCommerce's own ?add-to-cart=X pattern); inputs absint()-coerced, no state injection possible.
        if ( ! isset( $_GET['boostbot_bundle_add'] ) ) {
            return;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Shareable add-to-cart URL (mirrors WooCommerce's own ?add-to-cart=X pattern); inputs absint()-coerced, no state injection possible.
        $product_ids = array_filter( array_map( 'absint', explode( ',', sanitize_text_field( wp_unslash( $_GET['boostbot_bundle_add'] ) ) ) ) );

        if ( empty( $product_ids ) || ! function_exists( 'WC' ) ) {
            return;
        }

        $added = 0;
        foreach ( $product_ids as $product_id ) {
            $result = WC()->cart->add_to_cart( $product_id, 1 );
            if ( $result ) {
                ++$added;
            }
        }

        // Add success notice
        if ( $added > 0 ) {
            wc_add_notice(
                sprintf(
                    /* translators: %d: number of products */
                    _n( '%d מוצר נוסף לסל', '%d מוצרים נוספו לסל', $added, 'boostbot' ),
                    $added
                ),
                'success'
            );
        }

        // Redirect to cart without the parameter
        wp_safe_redirect( wc_get_cart_url() );
        exit;
    }

    /**
     * Add bundle products to cart.
     *
     * @since 1.0.0
     * @param array $bundle Bundle data.
     * @return array Result with success status and messages.
     */
    public function add_bundle_to_cart( array $bundle, float $discount_percent = 0, int $bundle_id = 0, string $language = '', string $discount_method = 'coupon' ): array {
        if ( empty( $bundle['products'] ) || ! function_exists( 'WC' ) ) {
            return array(
                'success' => false,
                'message' => __( 'אין מוצרים להוספה', 'boostbot' ),
            );
        }

        $resolved_language = 'en' === strtolower( $language ) ? 'en' : 'he';
        $is_english        = 'en' === $resolved_language;
        $session_cookie_name = 'wp_woocommerce_session_' . COOKIEHASH;

        // Ensure WooCommerce session is initialized
        if ( null === WC()->session ) {
            WC()->session = new \WC_Session_Handler();
            WC()->session->init();
        }

        // Ensure customer is initialized
        if ( null === WC()->customer ) {
            WC()->customer = new \WC_Customer( get_current_user_id(), true );
        }

        // Ensure cart is initialized
        if ( null === WC()->cart ) {
            WC()->cart = new \WC_Cart();
            WC()->cart->get_cart();
        }

        $added   = 0;
        $errors  = array();
        $cart    = WC()->cart;
        $product_ids = array_values(
            array_filter(
                array_map(
                    static function ( $product ) {
                        return is_array( $product ) ? absint( $product['id'] ?? 0 ) : absint( $product );
                    },
                    $bundle['products']
                )
            )
        );

        BoostBot_Logger::debug(
            '[Cart] bundle add_to_cart',
            array(
                'bundle_id'                        => $bundle_id,
                'product_ids'                      => $product_ids,
                'wc_cart_initialized'             => null !== WC()->cart,
                'cart_count_before'               => $cart ? count( $cart->get_cart() ) : null,
                'has_woocommerce_cart_hash_cookie'=> isset( $_COOKIE['woocommerce_cart_hash'] ),
                'has_wp_session_cookie'           => isset( $_COOKIE[ $session_cookie_name ] ),
            )
        );
        
        // Build product price map for bundle discount
        $product_prices = array();
        foreach ( $bundle['products'] as $product ) {
            if ( is_array( $product ) && isset( $product['id'] ) ) {
                $product_prices[ $product['id'] ] = array(
                    'original_price' => $product['original_price'] ?? $product['price'] ?? 0,
                    'bundle_price'   => $product['price'] ?? 0,
                );
            }
        }

        foreach ( $bundle['products'] as $product ) {
            $product_id = is_array( $product ) ? ( $product['id'] ?? 0 ) : $product;

            if ( ! $product_id ) {
                continue;
            }

            try {
                // Add cart item data for bundle pricing
                $cart_item_data = array(
                    'boostbot_bundle_id'              => $bundle_id,
                    'boostbot_bundle_price'           => $product_prices[ $product_id ]['bundle_price'] ?? 0,
                    'boostbot_original_price'         => $product_prices[ $product_id ]['original_price'] ?? 0,
                    'boostbot_bundle_discount_method' => $discount_method,
                );
                
                $result = $cart->add_to_cart( intval( $product_id ), 1, 0, array(), $cart_item_data );
                if ( $result ) {
                    ++$added;
                }
            } catch ( \Exception $e ) {
                $product_name = is_array( $product ) ? ( $product['name'] ?? "מוצר #$product_id" ) : "מוצר #$product_id";
                $errors[] = $product_name . ': ' . $e->getMessage();
            }
        }

        // Products already added to cart above, just redirect to cart (no boostbot_bundle_add param)
        $redirect_url = wc_get_cart_url();

        BoostBot_Logger::debug(
            '[Cart] bundle add_to_cart result',
            array(
                'bundle_id'                        => $bundle_id,
                'success'                          => $added > 0,
                'added'                            => $added,
                'expected'                         => count( $bundle['products'] ),
                'cart_count_after'                 => $cart ? count( $cart->get_cart() ) : null,
                'cart_contents_count'              => $cart ? $cart->get_cart_contents_count() : null,
                'cart_hash'                        => $cart ? $cart->get_cart_hash() : null,
                'has_woocommerce_cart_hash_cookie' => isset( $_COOKIE['woocommerce_cart_hash'] ),
                'has_wp_session_cookie'            => isset( $_COOKIE[ $session_cookie_name ] ),
                'error_count'                      => count( $errors ),
            )
        );

        if ( $added === count( $bundle['products'] ) ) {
            return array(
                'success'      => true,
                'message'      => sprintf(
                    /* translators: %d: number of products */
                    $is_english ? __( '%d products added to cart', 'boostbot' ) : __( 'נוספו %d מוצרים לסל', 'boostbot' ),
                    $added
                ),
                'cart_url'     => wc_get_cart_url(),
                'redirect_url' => $redirect_url,
                'cart_count'   => $cart->get_cart_contents_count(),
                'cart'         => ( new BoostBot_WooCommerce_Service() )->get_cart_info( $resolved_language ),
            );
        }

        return array(
            'success'      => $added > 0,
            'message'      => sprintf(
                /* translators: 1: added count, 2: total count */
                $is_english ? __( 'Added %1$d of %2$d products', 'boostbot' ) : __( 'נוספו %1$d מתוך %2$d מוצרים', 'boostbot' ),
                $added,
                count( $bundle['products'] )
            ),
            'redirect_url' => $redirect_url,
            'errors'       => $errors,
            'cart'         => ( new BoostBot_WooCommerce_Service() )->get_cart_info( $resolved_language ),
        );
    }

    /**
     * Get active preset coupons that are allowed for bundle usage.
     *
     * @param int $preset_id Preset ID.
     * @return array
     */
    public function get_bundle_purpose_coupons( int $preset_id, string $language = '' ): array {
        if ( $preset_id <= 0 || ! class_exists( 'BoostBot_WooCommerce_Coupon_Model' ) ) {
            return array();
        }

        $coupon_model = new BoostBot_WooCommerce_Coupon_Model();

        return $coupon_model->get_active_by_preset_and_purpose(
            $preset_id,
            BoostBot_WooCommerce_Coupon_Model::PURPOSE_BUNDLE,
            $language ?: null
        );
    }

    /**
     * Find the best bundle-purpose coupon for a preset and bundle total.
     *
     * @param int   $preset_id     Preset ID.
     * @param float $bundle_total  Bundle total.
     * @return int|null
     */
    public function find_best_bundle_coupon_id( int $preset_id, float $bundle_total, string $language = '' ): ?int {
        if ( ! class_exists( 'WooCommerce' ) ) {
            return null;
        }

        $best_coupon_id = null;
        $best_discount  = 0.0;
        $preset_coupons = $this->get_bundle_purpose_coupons( $preset_id, $language );

        foreach ( $preset_coupons as $preset_coupon ) {
            if ( empty( $preset_coupon->wc_coupon_id ) || empty( $preset_coupon->wc_is_valid ) ) {
                continue;
            }

            $coupon = new WC_Coupon( (int) $preset_coupon->wc_coupon_id );
            if ( ! $coupon->get_id() ) {
                continue;
            }

            $minimum = (float) $coupon->get_minimum_amount();
            $maximum = (float) $coupon->get_maximum_amount();
            if ( $minimum > 0 && $bundle_total < $minimum ) {
                continue;
            }
            if ( $maximum > 0 && $bundle_total > $maximum ) {
                continue;
            }

            $discount = $this->calculate_coupon_discount_value( $coupon, $bundle_total );
            if ( $discount > $best_discount ) {
                $best_discount  = $discount;
                $best_coupon_id = (int) $coupon->get_id();
            }
        }

        return $best_coupon_id;
    }

    /**
     * Estimate the monetary value of a coupon for a bundle total.
     *
     * @param WC_Coupon $coupon       Coupon object.
     * @param float     $bundle_total Bundle total.
     * @return float
     */
    private function calculate_coupon_discount_value( WC_Coupon $coupon, float $bundle_total ): float {
        $discount_type = $coupon->get_discount_type();
        $amount        = (float) $coupon->get_amount();

        if ( 'percent' === $discount_type ) {
            return $bundle_total * ( $amount / 100 );
        }

        if ( 'fixed_cart' === $discount_type || 'fixed_product' === $discount_type ) {
            return $amount;
        }

        return 0.0;
    }
}



