<?php
/**
 * Widgets REST API Endpoint for BoostBot.
 *
 * @package BoostBot
 * @since   1.0.0
 */

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

/**
 * Class BoostBot_Widgets_Endpoint
 *
 * REST API endpoints for widget management.
 *
 * @since 1.0.0
 */
class BoostBot_Widgets_Endpoint extends BoostBot_Base_Endpoint {

    /**
     * REST base for this endpoint.
     *
     * @var string
     */
    protected $rest_base = 'widgets';

    /**
     * Register REST routes.
     *
     * @since 1.0.0
     */
    public function register() {
        // GET /widgets - List all widgets.
        $this->register_route(
            '',
            'GET',
            array( $this, 'get_widgets' ),
            array( $this, 'admin_access' )
        );

        // POST /widgets - Create a new widget.
        $this->register_route(
            '',
            'POST',
            array( $this, 'create_widget' ),
            array( $this, 'admin_access' )
        );

        // GET /widgets/types - Get available widget types.
        $this->register_route(
            '/types',
            'GET',
            array( $this, 'get_widget_types' ),
            array( $this, 'admin_access' )
        );

        // GET /widgets/{id} - Get a single widget (admin).
        $this->register_route(
            '/(?P<id>\d+)',
            'GET',
            array( $this, 'get_widget' ),
            array( $this, 'admin_access' )
        );

        // GET /widgets/{id}/public - Get widget config for chat (public).
        $this->register_route(
            '/(?P<id>\d+)/public',
            'GET',
            array( $this, 'get_widget_public' ),
            array( $this, 'public_with_api_rate_limit' )  // Passive public config read
        );

        // PUT /widgets/{id} - Update a widget.
        $this->register_route(
            '/(?P<id>\d+)',
            'PUT',
            array( $this, 'update_widget' ),
            array( $this, 'admin_access' )
        );

        // DELETE /widgets/{id} - Delete a widget.
        $this->register_route(
            '/(?P<id>\d+)',
            'DELETE',
            array( $this, 'delete_widget' ),
            array( $this, 'admin_access' )
        );

        // POST /widgets/{id}/fields - Add a field to a form widget.
        $this->register_route(
            '/(?P<id>\d+)/fields',
            'POST',
            array( $this, 'add_field' ),
            array( $this, 'admin_access' )
        );

        // PUT /widgets/{widget_id}/fields/{id} - Update a field.
        $this->register_route(
            '/(?P<widget_id>\d+)/fields/(?P<id>\d+)',
            'PUT',
            array( $this, 'update_field' ),
            array( $this, 'admin_access' )
        );

        // DELETE /widgets/{widget_id}/fields/{id} - Delete a field.
        $this->register_route(
            '/(?P<widget_id>\d+)/fields/(?P<id>\d+)',
            'DELETE',
            array( $this, 'delete_field' ),
            array( $this, 'admin_access' )
        );

        // POST /widgets/{id}/reorder-fields - Reorder fields.
        $this->register_route(
            '/(?P<id>\d+)/reorder-fields',
            'POST',
            array( $this, 'reorder_fields' ),
            array( $this, 'admin_access' )
        );

        // POST /widgets/{id}/assign/{chatbot_id} - Assign widget to chatbot.
        $this->register_route(
            '/(?P<id>\d+)/assign/(?P<chatbot_id>\d+)',
            'POST',
            array( $this, 'assign_to_chatbot' ),
            array( $this, 'admin_access' )
        );

        // DELETE /widgets/{id}/assign/{chatbot_id} - Unassign widget from chatbot.
        $this->register_route(
            '/(?P<id>\d+)/assign/(?P<chatbot_id>\d+)',
            'DELETE',
            array( $this, 'unassign_from_chatbot' ),
            array( $this, 'admin_access' )
        );

        // GET /widgets/for-chatbot/{chatbot_id} - Get widgets for a chatbot.
        $this->register_route(
            '/for-chatbot/(?P<chatbot_id>\d+)',
            'GET',
            array( $this, 'get_widgets_for_chatbot' ),
            array( $this, 'admin_access' )
        );

        // GET /widgets/{id}/linked-bots - Get bots using this widget.
        $this->register_route(
            '/(?P<id>\d+)/linked-bots',
            'GET',
            array( $this, 'get_linked_bots' ),
            array( $this, 'admin_access' )
        );

        // POST /widgets/{id}/unlink-bot/{bot_id} - Unlink bot from widget.
        $this->register_route(
            '/(?P<id>\d+)/unlink-bot/(?P<bot_id>\d+)',
            'POST',
            array( $this, 'unlink_bot' ),
            array( $this, 'admin_access' )
        );

        // POST /widgets/{id}/link-bot/{bot_id} - Link bot to widget (alias for assign).
        $this->register_route(
            '/(?P<id>\d+)/link-bot/(?P<bot_id>\d+)',
            'POST',
            array( $this, 'link_bot' ),
            array( $this, 'admin_access' )
        );

        // POST /widgets/{id}/track-usage/{bot_id} - Track widget usage by bot.
        $this->register_route(
            '/(?P<id>\d+)/track-usage/(?P<bot_id>\d+)',
            'POST',
            array( $this, 'track_usage' ),
            array( $this, 'public_with_api_rate_limit' )  // Passive usage tracking
        );

        // GET /widgets/{id}/usage-stats - Get widget usage statistics.
        $this->register_route(
            '/(?P<id>\d+)/usage-stats',
            'GET',
            array( $this, 'get_usage_stats' ),
            array( $this, 'admin_access' )
        );

        // POST /widgets/bundle-builder/build - Build a bundle (public).
        $this->register_route(
            '/bundle-builder/build',
            'POST',
            array( $this, 'build_bundle' ),
            array( $this, 'public_with_commerce_rate_limit' )  // Bundle build is a transactional user action
        );

        // POST /widgets/bundle-builder/swap-item - Swap one bundle item (public).
        $this->register_route(
            '/bundle-builder/swap-item',
            'POST',
            array( $this, 'swap_bundle_item' ),
            array( $this, 'public_with_commerce_rate_limit' )  // Bundle swap is a transactional user action
        );

        // POST /widgets/bundle-builder/add-to-cart - Add bundle to cart (public).
        $this->register_route(
            '/bundle-builder/add-to-cart',
            'POST',
            array( $this, 'add_bundle_to_cart' ),
            array( $this, 'public_with_commerce_rate_limit' )  // Bundle add-to-cart is a transactional user action
        );

        // GET /widgets/bundle-builder/config - Get bundle builder config (public).
        $this->register_route(
            '/bundle-builder/config',
            'GET',
            array( $this, 'get_bundle_builder_config' ),
            array( $this, 'public_with_api_rate_limit' )  // Passive bundle builder bootstrap
        );

        // GET /widgets/bundles/analytics - Get bundle analytics (admin).
        $this->register_route(
            '/bundles/analytics',
            'GET',
            array( $this, 'get_bundle_analytics' ),
            array( $this, 'admin_access' )
        );

        // GET /widgets/woocommerce/coupons - Get WooCommerce coupons (admin).
        $this->register_route(
            '/woocommerce/coupons',
            'GET',
            array( $this, 'get_wc_coupons' ),
            array( $this, 'admin_access' )
        );

        // POST /widgets/{id}/duplicate - Duplicate a widget (including form fields).
        $this->register_route(
            '/(?P<id>\d+)/duplicate',
            'POST',
            array( $this, 'duplicate_widget' ),
            array( $this, 'admin_access' )
        );
    }

    /**
     * Get all widgets.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function get_widgets( $request ) {
        $model = new BoostBot_Widget_Model();
        $type  = $request->get_param( 'type' );

        // Get pagination.
        $pagination = $this->get_pagination_params( $request, 50 );

        $args = array(
            'limit'   => $pagination['per_page'],
            'offset'  => $pagination['offset'],
            'orderby' => 'created_at',
            'order'   => 'DESC',
            'where'   => array(),
        );

        if ( $request->has_param( 'language' ) ) {
            $args['where']['language'] = $request->get_param( 'language' );
        }

        if ( $type ) {
            $args['where']['type'] = $type;
        }

        // Use get_all_with_stats to include linked_bots_count (supports type + language filters).
        $widgets = $model->get_all_with_stats( $args );

        // Add fields for form widgets.
        $field_model = new BoostBot_Form_Field_Model();
        foreach ( $widgets as $widget ) {
            if ( BoostBot_Widget_Model::TYPE_FORM === $widget->type ) {
                $widget->fields = $field_model->get_by_widget( $widget->id );
            }
            // Ensure config is decoded.
            if ( ! empty( $widget->config ) && is_string( $widget->config ) ) {
                $widget->config = json_decode( $widget->config, true );
            }
            // Ensure linked_bots_count exists
            if ( ! isset( $widget->linked_bots_count ) ) {
                $widget->linked_bots_count = $model->get_linked_bots_count( $widget->id );
            }
        }

        $total = $model->count( $args['where'] );

        return $this->success(
            array(
                'widgets'    => $widgets,
                'pagination' => $this->build_pagination_response(
                    $pagination['page'],
                    $pagination['per_page'],
                    $total
                ),
            )
        );
    }

    /**
     * Get available widget types.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response Response object.
     */
    public function get_widget_types( $request ) {
        $widget_types = BoostBot_Widget_Model::get_widget_types();
        $field_types  = BoostBot_Form_Field_Model::get_field_types();

        return $this->success(
            array(
                'widget_types' => $widget_types,
                'field_types'  => $field_types,
            )
        );
    }

    /**
     * Get a single widget.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function get_widget( $request ) {
        $id    = absint( $request->get_param( 'id' ) );
        $model = new BoostBot_Widget_Model();

        $widget = $model->get_with_fields( $id );

        if ( ! $widget ) {
            return $this->not_found( __( 'וידג\'ט', 'boostbot' ) );
        }

        return $this->success( array( 'widget' => $widget ) );
    }

    /**
     * Get widget config for public/chat use.
     * Returns only the necessary config for rendering the widget.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function get_widget_public( $request ) {
        $id    = absint( $request->get_param( 'id' ) );
        $model = new BoostBot_Widget_Model();

        $widget = $model->get_with_fields( $id );

        if ( ! $widget || ! $widget->is_active ) {
            return $this->not_found( __( 'וידג\'ט', 'boostbot' ) );
        }

        // Prepare public-safe response.
        $config = is_string( $widget->config ) ? json_decode( $widget->config, true ) : (array) $widget->config;

        // SECURITY: strip server-only integration secrets before returning to the public
        // (unauthenticated) caller. The webhook URL embeds a secret token and the
        // notification recipient emails are internal — neither belongs in a public response.
        if ( is_array( $config ) ) {
            unset( $config['webhook_url'], $config['notification_emails'], $config['recipient_emails'] );
        }

        $public_bot = $this->resolve_public_widget_bot( $request, (int) $widget->id );
        $wc_preset  = $this->get_bot_woocommerce_preset( $public_bot );

        $response = array(
            'widget' => array(
                'id'     => $widget->id,
                'name'   => $widget->name,
                'type'   => $widget->type,
                'config' => $config ?: array(),
            ),
        );

        // Add fields for form widgets.
        if ( BoostBot_Widget_Model::TYPE_FORM === $widget->type && ! empty( $widget->fields ) ) {
            $response['widget']['fields'] = array_map( function( $field ) {
                return array(
                    'id'          => $field->id,
                    'name'        => $field->name,
                    'label'       => $field->label,
                    'field_type'  => $field->field_type,
                    'placeholder' => $field->placeholder,
                    'is_required' => (bool) $field->is_required,
                    'options'     => is_string( $field->options ) ? json_decode( $field->options, true ) : $field->options,
                );
            }, $widget->fields );
        }

        // For contact card widgets, always return a concrete chat-ready contact config.
        // This prevents preview placeholders from leaking to live users when the frontend
        // receives an empty contact payload.
        if ( BoostBot_Widget_Model::TYPE_CONTACT_CARD === $widget->type && class_exists( 'BoostBot_Contact_Card_Widget' ) ) {
            $contact_widget = new BoostBot_Contact_Card_Widget();
            $chat_config    = $contact_widget->get_chat_config( (int) $widget->id );
            if ( is_array( $chat_config ) && ! empty( $chat_config ) ) {
                $response['widget']['config'] = $chat_config;
            }
        }

        // Resolve currency via direct term-meta (reliable in all contexts).
        $pll_widget_lang = $public_bot->language ?? $widget->language ?? 'he';
        $widget_currency = BoostBot_Polylang_Context::get_currency_for_language( $pll_widget_lang );

        // For coupon widgets, enrich with available WooCommerce coupons.
        if ( BoostBot_Widget_Model::TYPE_COUPON === $widget->type && class_exists( 'WooCommerce' ) ) {
            $coupons    = array();
            $wc_service = new BoostBot_WooCommerce_Service();

            if ( $wc_preset && class_exists( 'BoostBot_WooCommerce_Coupon_Model' ) ) {
                $coupon_model    = new BoostBot_WooCommerce_Coupon_Model();
                $preset_coupons  = $coupon_model->get_active_by_preset_and_purpose(
                    (int) $wc_preset->id,
                    BoostBot_WooCommerce_Coupon_Model::PURPOSE_WIDGET,
                    $pll_widget_lang
                );

                foreach ( $preset_coupons as $pc ) {
                    if ( empty( $pc->wc_coupon_id ) || empty( $pc->wc_is_valid ) ) {
                        continue;
                    }
                    $wc_coupon = new \WC_Coupon( $pc->wc_coupon_id );
                    $expiry    = $wc_coupon->get_date_expires();
                    $coupon_applicability = $wc_service->get_coupon_applicability_data( $wc_coupon, $pll_widget_lang );

                    $coupons[] = array(
                        'code'                  => $wc_coupon->get_code(),
                        'description'           => ! empty( $pc->description ) ? $pc->description : ( $wc_coupon->get_description() ?: '' ),
                        'display_name'          => ! empty( $pc->display_name ) ? $pc->display_name : $wc_coupon->get_code(),
                        'discount_type'         => $wc_coupon->get_discount_type(),
                        'amount'                => (float) $wc_coupon->get_amount(),
                        'expiry_date'           => $coupon_applicability['expiry_date'] ?? ( $expiry ? $expiry->format( 'Y-m-d' ) : null ),
                        'minimum_amount'        => (float) $wc_coupon->get_minimum_amount(),
                        'maximum_amount'        => '' !== $wc_coupon->get_maximum_amount() ? (float) $wc_coupon->get_maximum_amount() : null,
                        'applicable'            => (bool) ( $coupon_applicability['applicable'] ?? false ),
                        'reason'                => $coupon_applicability['reason'] ?? null,
                        'reason_code'           => $coupon_applicability['reason_code'] ?? null,
                        'min_spend'             => $coupon_applicability['min_spend'],
                        'min_spend_display'     => $coupon_applicability['min_spend_display'],
                        'max_spend'             => $coupon_applicability['max_spend'],
                        'max_spend_display'     => $coupon_applicability['max_spend_display'],
                        'spend_gap'             => $coupon_applicability['spend_gap'],
                        'spend_gap_display'     => $coupon_applicability['spend_gap_display'],
                        'eligible_products'     => $coupon_applicability['eligible_products'],
                        'eligible_categories'   => $coupon_applicability['eligible_categories'],
                        'usage_remaining'       => $coupon_applicability['usage_remaining'],
                        'is_applied'            => (bool) ( $coupon_applicability['is_applied'] ?? false ),
                        'free_shipping'         => (bool) ( $coupon_applicability['free_shipping'] ?? false ),
                        'expires_soon'          => (bool) ( $coupon_applicability['expires_soon'] ?? false ),
                    );
                }
            }

            $response['widget']['config']['coupons'] = $coupons;
        }

        // For product_card widgets, enrich with product data when product_id is provided.
        if ( BoostBot_Widget_Model::TYPE_PRODUCT_CARD === $widget->type && class_exists( 'WooCommerce' ) ) {
            $product_id = absint( $request->get_param( 'product_id' ) );
            if ( $product_id ) {
                // Safety net: if the product is in the wrong language, resolve translation.
                if ( function_exists( 'pll_get_post_language' ) && function_exists( 'pll_get_post' ) ) {
                    $product_lang = pll_get_post_language( $product_id );
                    if ( $product_lang && $product_lang !== $pll_widget_lang ) {
                        $translated_id = pll_get_post( $product_id, $pll_widget_lang );
                        if ( $translated_id && $translated_id !== $product_id ) {
                            $product_id = $translated_id;
                        }
                    }
                }

                $product = wc_get_product( $product_id );
                if ( $product && $product->is_visible() ) {
                    $wc_service = new BoostBot_WooCommerce_Service();
                    $formatted_product = $wc_service->get_product( $product_id );

                    if ( ! empty( $formatted_product ) ) {
                        $formatted_product['currency'] = $widget_currency['symbol'];
                        $formatted_product['currency_code'] = $widget_currency['code'];
                        $response['widget']['config']['product'] = $formatted_product;
                    }
                }
            }
        }

        return $this->success( $response );
    }

    /**
     * Resolve the requesting bot context for public widget rendering.
     *
     * @param WP_REST_Request $request   REST request.
     * @param int             $widget_id Widget ID.
     * @return object|null
     */
    private function resolve_public_widget_bot( $request, int $widget_id ) {
        $bot_id = absint( $request->get_param( 'bot_id' ) ?: $request->get_param( 'chatbot_id' ) );
        if ( $bot_id <= 0 || ! class_exists( 'BoostBot_Bot_Model' ) ) {
            return null;
        }

        if ( ! $this->widget_is_assigned_to_bot( $widget_id, $bot_id ) ) {
            return null;
        }

        return ( new BoostBot_Bot_Model() )->get( $bot_id );
    }

    /**
     * Check whether a widget is assigned to the given bot.
     *
     * @param int $widget_id Widget ID.
     * @param int $bot_id    Bot ID.
     * @return bool
     */
    private function widget_is_assigned_to_bot( int $widget_id, int $bot_id ): bool {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        return (bool) $wpdb->get_var(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                "SELECT id FROM {$wpdb->prefix}boostbot_chatbot_widgets WHERE widget_id = %d AND chatbot_id = %d LIMIT 1",
                $widget_id,
                $bot_id
            )
        );
    }

    /**
     * Load the WooCommerce preset for a bot when present and valid.
     *
     * @param object|null $bot Bot object.
     * @return object|null
     */
    private function get_bot_woocommerce_preset( $bot ) {
        if ( ! $bot || empty( $bot->woocommerce_preset_id ) || ! class_exists( 'BoostBot_WooCommerce_Preset_Model' ) ) {
            return null;
        }

        return ( new BoostBot_WooCommerce_Preset_Model() )->get( (int) $bot->woocommerce_preset_id );
    }

    /**
     * Create a new widget.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function create_widget( $request ) {
        $data  = $request->get_json_params();
        $model = new BoostBot_Widget_Model();

        // Validate required fields.
        if ( empty( $data['name'] ) ) {
            return $this->validation_error( array( 'name' => __( 'שם הווידג\'ט הוא שדה חובה', 'boostbot' ) ) );
        }

        if ( empty( $data['type'] ) ) {
            return $this->validation_error( array( 'type' => __( 'סוג הווידג\'ט הוא שדה חובה', 'boostbot' ) ) );
        }

        // Check if type is valid.
        $valid_types = array_keys( BoostBot_Widget_Model::get_widget_types() );
        if ( ! in_array( $data['type'], $valid_types, true ) ) {
            return $this->validation_error( array( 'type' => __( 'סוג ווידג\'ט לא תקין', 'boostbot' ) ) );
        }

        // Create widget (with fields if it's a form).
        $id = $model->create_with_fields( $data );

        if ( ! $id ) {
            return $this->error( __( 'שגיאה ביצירת הווידג\'ט', 'boostbot' ), 500 );
        }

        // Get the created widget.
        $widget = $model->get_with_fields( $id );

        BoostBot_Logger::info(
            'Widget created',
            array(
                'id'   => $id,
                'type' => $data['type'],
                'name' => $data['name'],
            )
        );

        return $this->success( array( 'widget' => $widget ), 201 );
    }

    /**
     * Update a widget.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function update_widget( $request ) {
        $id    = absint( $request->get_param( 'id' ) );
        $data  = $request->get_json_params();
        $model = new BoostBot_Widget_Model();

        // Debug log incoming data
        BoostBot_Logger::debug(
            'Widget update request',
            array(
                'id'   => $id,
                'data' => $data,
            )
        );

        // Check if widget exists.
        $existing = $model->get( $id );
        if ( ! $existing ) {
            return $this->not_found( __( 'וידג\'ט', 'boostbot' ) );
        }

        // Don't allow changing type.
        unset( $data['type'] );

        // Fix #10 — Validate associated_contacts: remove IDs that don't exist in the DB.
        if ( isset( $data['config'] ) ) {
            $config_data = $data['config'];
            if ( is_string( $config_data ) ) {
                $config_data = json_decode( $config_data, true );
            }
            if ( is_array( $config_data ) && ! empty( $config_data['associated_contacts'] ) && is_array( $config_data['associated_contacts'] ) ) {
                $contact_model = new BoostBot_Contact_Model();
                $valid_ids     = array();
                foreach ( $config_data['associated_contacts'] as $cid ) {
                    $cid = absint( $cid );
                    if ( $cid > 0 && $contact_model->get( $cid ) ) {
                        $valid_ids[] = $cid;
                    }
                }
                $config_data['associated_contacts'] = $valid_ids;
                $data['config'] = $config_data;
            }
        }

        // Prevent activating a widget that is not fully configured.
        if ( isset( $data['is_active'] ) ) {
            $is_trying_to_activate = false;
            if ( is_bool( $data['is_active'] ) ) {
                $is_trying_to_activate = $data['is_active'];
            } else {
                $is_trying_to_activate = (bool) absint( $data['is_active'] );
            }
            if ( $is_trying_to_activate ) {
                // Build a temporary widget object merging existing + new data for validation.
                $check_widget = clone $existing;
                if ( isset( $data['config'] ) ) {
                    $check_widget->config = $data['config'];
                }
                if ( isset( $data['fields'] ) ) {
                    $check_widget->fields = $data['fields'];
                }
                if ( ! BoostBot_Widget_Model::is_fully_configured( $check_widget ) ) {
                    return $this->error(
                        __( 'לא ניתן להפעיל וידג\'ט שלא הוגדר במלואו. יש להשלים את כל שדות החובה.', 'boostbot' ),
                        400
                    );
                }
            }
        }

        // Update widget.
        $result = $model->update_with_fields( $id, $data );
        
        // Debug log result
        BoostBot_Logger::debug(
            'Widget update result',
            array(
                'id'     => $id,
                'result' => $result,
            )
        );

        if ( ! $result ) {
            return $this->error( __( 'שגיאה בעדכון הווידג\'ט', 'boostbot' ), 500 );
        }

        // Get updated widget.
        $widget = $model->get_with_fields( $id );

        BoostBot_Logger::info(
            'Widget updated',
            array(
                'id'   => $id,
                'name' => $widget->name,
            )
        );

        return $this->success( array( 'widget' => $widget ) );
    }

    /**
     * Delete a widget.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function delete_widget( $request ) {
        $id    = absint( $request->get_param( 'id' ) );
        $model = new BoostBot_Widget_Model();

        // Check if widget exists.
        $existing = $model->get( $id );
        if ( ! $existing ) {
            return $this->not_found( __( 'וידג\'ט', 'boostbot' ) );
        }

        // Delete widget and its fields.
        $result = $model->delete_with_fields( $id );

        if ( ! $result ) {
            return $this->error( __( 'שגיאה במחיקת הווידג\'ט', 'boostbot' ), 500 );
        }

        BoostBot_Logger::info(
            'Widget deleted',
            array(
                'id'   => $id,
                'name' => $existing->name,
            )
        );

        return $this->success( array( 'deleted' => true ) );
    }

    /**
     * Add a field to a form widget.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function add_field( $request ) {
        $widget_id   = absint( $request->get_param( 'id' ) );
        $data        = $request->get_json_params();
        $model       = new BoostBot_Widget_Model();
        $field_model = new BoostBot_Form_Field_Model();

        // Check if widget exists and is a form.
        $widget = $model->get( $widget_id );
        if ( ! $widget ) {
            return $this->not_found( __( 'וידג\'ט', 'boostbot' ) );
        }

        if ( BoostBot_Widget_Model::TYPE_FORM !== $widget->type ) {
            return $this->error( __( 'ניתן להוסיף שדות רק לווידג\'ט מסוג טופס', 'boostbot' ), 400 );
        }

        // Add widget_id to data.
        $data['widget_id'] = $widget_id;

        // Create field.
        $field_id = $field_model->create( $data );

        if ( ! $field_id ) {
            return $this->error( __( 'שגיאה ביצירת השדה', 'boostbot' ), 500 );
        }

        // Get the created field.
        $field = $field_model->get( $field_id );

        return $this->success( array( 'field' => $field ), 201 );
    }

    /**
     * Update a field.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function update_field( $request ) {
        $widget_id   = absint( $request->get_param( 'widget_id' ) );
        $field_id    = absint( $request->get_param( 'id' ) );
        $data        = $request->get_json_params();
        $field_model = new BoostBot_Form_Field_Model();

        // Check if field exists and belongs to widget.
        $field = $field_model->get( $field_id );
        if ( ! $field || (int) $field->widget_id !== $widget_id ) {
            return $this->not_found( __( 'שדה', 'boostbot' ) );
        }

        // Don't allow changing widget_id.
        unset( $data['widget_id'] );

        // Update field.
        $result = $field_model->update( $field_id, $data );

        if ( ! $result ) {
            return $this->error( __( 'שגיאה בעדכון השדה', 'boostbot' ), 500 );
        }

        // Get updated field.
        $field = $field_model->get( $field_id );

        return $this->success( array( 'field' => $field ) );
    }

    /**
     * Delete a field.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function delete_field( $request ) {
        $widget_id   = absint( $request->get_param( 'widget_id' ) );
        $field_id    = absint( $request->get_param( 'id' ) );
        $field_model = new BoostBot_Form_Field_Model();

        // Check if field exists and belongs to widget.
        $field = $field_model->get( $field_id );
        if ( ! $field || (int) $field->widget_id !== $widget_id ) {
            return $this->not_found( __( 'שדה', 'boostbot' ) );
        }

        // Delete field.
        $result = $field_model->delete( $field_id );

        if ( ! $result ) {
            return $this->error( __( 'שגיאה במחיקת השדה', 'boostbot' ), 500 );
        }

        return $this->success( array( 'deleted' => true ) );
    }

    /**
     * Reorder fields.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function reorder_fields( $request ) {
        $widget_id   = absint( $request->get_param( 'id' ) );
        $data        = $request->get_json_params();
        $field_ids   = isset( $data['field_ids'] ) ? array_map( 'absint', $data['field_ids'] ) : array();
        $field_model = new BoostBot_Form_Field_Model();
        $model       = new BoostBot_Widget_Model();

        // Check if widget exists.
        $widget = $model->get( $widget_id );
        if ( ! $widget ) {
            return $this->not_found( __( 'וידג\'ט', 'boostbot' ) );
        }

        // Reorder fields.
        $field_model->reorder_fields( $widget_id, $field_ids );

        // Get updated fields.
        $fields = $field_model->get_by_widget( $widget_id );

        return $this->success( array( 'fields' => $fields ) );
    }

    /**
     * Assign widget to chatbot.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function assign_to_chatbot( $request ) {
        $widget_id   = absint( $request->get_param( 'id' ) );
        $chatbot_id  = absint( $request->get_param( 'chatbot_id' ) );
        $data        = $request->get_json_params();
        $model       = new BoostBot_Widget_Model();

        // Check if widget exists.
        $widget = $model->get( $widget_id );
        if ( ! $widget ) {
            return $this->not_found( __( 'וידג\'ט', 'boostbot' ) );
        }

        // Check if chatbot exists.
        $bot_model = new BoostBot_Bot_Model();
        $chatbot   = $bot_model->get( $chatbot_id );
        if ( ! $chatbot ) {
            return $this->not_found( __( 'צ\'אטבוט', 'boostbot' ) );
        }

        $assignment_error = $this->validate_widget_assignment_for_bot( $widget, $chatbot );
        if ( $assignment_error ) {
            return $assignment_error;
        }

        // Assign widget.
        $custom_config = isset( $data['custom_config'] ) ? $data['custom_config'] : null;
        $result        = $model->assign_to_chatbot( $widget_id, $chatbot_id, $custom_config );

        if ( ! $result ) {
            return $this->error( __( 'שגיאה בשיוך הווידג\'ט', 'boostbot' ), 500 );
        }

        BoostBot_Logger::info(
            'Widget assigned to chatbot',
            array(
                'widget_id'  => $widget_id,
                'chatbot_id' => $chatbot_id,
            )
        );

        return $this->success( array( 'assigned' => true ) );
    }

    /**
     * Unassign widget from chatbot.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function unassign_from_chatbot( $request ) {
        $widget_id  = absint( $request->get_param( 'id' ) );
        $chatbot_id = absint( $request->get_param( 'chatbot_id' ) );
        $model      = new BoostBot_Widget_Model();

        // Unassign widget.
        $result = $model->unassign_from_chatbot( $widget_id, $chatbot_id );

        if ( ! $result ) {
            return $this->error( __( 'שגיאה בהסרת שיוך הווידג\'ט', 'boostbot' ), 500 );
        }

        BoostBot_Logger::info(
            'Widget unassigned from chatbot',
            array(
                'widget_id'  => $widget_id,
                'chatbot_id' => $chatbot_id,
            )
        );

        return $this->success( array( 'unassigned' => true ) );
    }

    /**
     * Get widgets for a chatbot.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function get_widgets_for_chatbot( $request ) {
        $chatbot_id = absint( $request->get_param( 'chatbot_id' ) );
        $model      = new BoostBot_Widget_Model();

        // Check if chatbot exists.
        $bot_model = new BoostBot_Bot_Model();
        $chatbot   = $bot_model->get( $chatbot_id );
        if ( ! $chatbot ) {
            return $this->not_found( __( 'צ\'אטבוט', 'boostbot' ) );
        }

        // Get widgets — include incompletely-configured widgets so the admin
        // panel can display every assignment (with an is_fully_configured flag).
        $widgets = $model->get_for_chatbot( $chatbot_id, true );

        // Decode configs and add fields for form widgets.
        // Note: get_for_chatbot() already decodes config to array, so only
        // decode if still a JSON string (defensive for backward compat).
        $field_model = new BoostBot_Form_Field_Model();
        foreach ( $widgets as $widget ) {
            if ( ! empty( $widget->config ) && is_string( $widget->config ) ) {
                $widget->config = json_decode( $widget->config, true );
            }
            if ( ! empty( $widget->custom_config ) && is_string( $widget->custom_config ) ) {
                $widget->custom_config = json_decode( $widget->custom_config, true );
            }
            if ( BoostBot_Widget_Model::TYPE_FORM === $widget->type && empty( $widget->fields ) ) {
                $widget->fields = $field_model->get_by_widget( $widget->id );
            }
        }

        return $this->success( array( 'widgets' => $widgets ) );
    }

    /**
     * Get bots that are using this widget.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function get_linked_bots( $request ) {
        $widget_id = absint( $request->get_param( 'id' ) );

        global $wpdb;
        $junction_table = self::sanitize_identifier( $wpdb->prefix . 'boostbot_chatbot_widgets' );
        $bots_table     = self::sanitize_identifier( $wpdb->prefix . 'boostbot_bots' );

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        // 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().
        $bots = $wpdb->get_results(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $wpdb->prepare(
                "SELECT b.id, b.bot_name 
                 FROM {$bots_table} b
                 INNER JOIN {$junction_table} cw ON b.id = cw.chatbot_id
                 WHERE cw.widget_id = %d
                 ORDER BY b.bot_name ASC",
                $widget_id
            )
        );
        // phpcs:enable

        return $this->success( array( 'bots' => $bots ) );
    }

    /**
     * Unlink a bot from this widget.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function unlink_bot( $request ) {
        $widget_id = absint( $request->get_param( 'id' ) );
        $bot_id    = absint( $request->get_param( 'bot_id' ) );

        $model  = new BoostBot_Widget_Model();
        $result = $model->unassign_from_chatbot( $widget_id, $bot_id );

        if ( ! $result ) {
            return $this->error( __( 'שגיאה בניתוק הבוט מהווידג\'ט', 'boostbot' ), 500 );
        }

        BoostBot_Logger::info(
            'Bot unlinked from widget',
            array(
                'widget_id' => $widget_id,
                'bot_id'    => $bot_id,
            )
        );

        return $this->success( array( 'message' => __( 'הבוט נותק מהווידג\'ט בהצלחה', 'boostbot' ) ) );
    }

    /**
     * Link a bot to this widget (alias for assign_to_chatbot).
     *
     * Uses the same underlying assign logic via the junction table.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function link_bot( $request ) {
        $widget_id = absint( $request->get_param( 'id' ) );
        $bot_id    = absint( $request->get_param( 'bot_id' ) );
        $model     = new BoostBot_Widget_Model();

        // Check if widget exists.
        $widget = $model->get( $widget_id );
        if ( ! $widget ) {
            return $this->not_found( __( 'וידג\'ט', 'boostbot' ) );
        }

        // Check if bot exists.
        $bot_model = new BoostBot_Bot_Model();
        $bot       = $bot_model->get( $bot_id );
        if ( ! $bot ) {
            return $this->not_found( __( 'בוט', 'boostbot' ) );
        }

        $assignment_error = $this->validate_widget_assignment_for_bot( $widget, $bot );
        if ( $assignment_error ) {
            return $assignment_error;
        }

        // Assign widget to bot.
        $result = $model->assign_to_chatbot( $widget_id, $bot_id );

        if ( ! $result ) {
            return $this->error( __( 'שגיאה בשיוך הווידג\'ט לבוט', 'boostbot' ), 500 );
        }

        BoostBot_Logger::info(
            'Widget linked to bot',
            array(
                'widget_id' => $widget_id,
                'bot_id'    => $bot_id,
            )
        );

        return $this->success( array(
            'message' => __( 'הווידג\'ט שויך לבוט בהצלחה', 'boostbot' ),
        ) );
    }

    /**
     * Validate language and WooCommerce preset compatibility before linking a widget.
     *
     * @param object $widget Widget record.
     * @param object $bot    Bot record.
     * @return WP_REST_Response|null
     */
    private function validate_widget_assignment_for_bot( $widget, $bot ) {
        $widget_language = (string) ( $widget->language ?? '' );
        $bot_language    = (string) ( $bot->language ?? '' );

        if ( '' !== $widget_language && '' !== $bot_language && $widget_language !== $bot_language ) {
            return $this->error( __( 'Widget language must match bot language.', 'boostbot' ), 400 );
        }

        $widget_type = (string) ( $widget->type ?? '' );
        if ( class_exists( 'BoostBot_Widget_Model' )
            && BoostBot_Widget_Model::is_woocommerce_widget_type( $widget_type )
            && ! $this->get_bot_woocommerce_preset( $bot ) ) {
            return $this->error( __( 'WooCommerce widgets require a WooCommerce preset on the bot.', 'boostbot' ), 400 );
        }

        return null;
    }

    /**
     * Track widget usage by a bot.
     * Called from the chat widget when a widget is displayed/used.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function track_usage( $request ) {
        $widget_id = absint( $request->get_param( 'id' ) );
        $bot_id    = absint( $request->get_param( 'bot_id' ) );
        $language = BoostBot_Language::with_fallback( $request->get_param( 'language' ) );

        $model = new BoostBot_Widget_Model();

        // Check if widget exists
        $widget = $model->get( $widget_id );
        if ( ! $widget ) {
            return $this->not_found( __( 'וידג\'ט', 'boostbot' ) );
        }

        // Track the usage
        $model->track_usage( $widget_id, $bot_id, $language );

        return $this->success( array( 'tracked' => true ) );
    }

    /**
     * Get usage statistics for a widget.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function get_usage_stats( $request ) {
        $widget_id = absint( $request->get_param( 'id' ) );

        $model = new BoostBot_Widget_Model();

        // Check if widget exists
        $widget = $model->get( $widget_id );
        if ( ! $widget ) {
            return $this->not_found( __( 'וידג\'ט', 'boostbot' ) );
        }

        // Get usage stats
        $stats = $model->get_usage_stats( $widget_id );

        return $this->success( array( 
            'widget_id' => $widget_id,
            'stats'     => $stats,
        ) );
    }

    /**
     * Build a product bundle.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function build_bundle( $request ) {
        // Check WooCommerce.
        if ( ! class_exists( 'WooCommerce' ) ) {
            BoostBot_Logger::warning(
                '[Widget] bundle-builder/build error: WooCommerce unavailable',
                array(
                    'endpoint' => 'bundle-builder/build',
                )
            );
            return $this->error(
                __( 'נדרש WooCommerce להפעלת פיצ\'ר זה', 'boostbot' ),
                400
            );
        }

        $params = $request->get_json_params();

        $budget       = floatval( $params['budget'] ?? 100000 );
        $category     = sanitize_text_field( $params['category'] ?? '' );
        $preferences  = array_map( 'sanitize_text_field', (array) ( $params['preferences'] ?? array() ) );
        $widget_id    = absint( $params['widget_id'] ?? 0 );
        $conversation_id = sanitize_text_field( $params['conversation_id'] ?? '' );
        $chatbot_id   = absint( $params['chatbot_id'] ?? 0 );
        $language     = sanitize_text_field( $params['language'] ?? 'he' );

        $customer_message = sanitize_text_field( $params['customer_message'] ?? '' );

        // Override WooCommerce currency filter for the request's language so
        // wc_price() / get_woocommerce_currency_symbol() return correct values.
        $currency_override = BoostBot_Polylang_Context::override_currency_for_language( $language );

        BoostBot_Logger::debug( 'Bundle build API called', array(
            'customer_message' => $customer_message ?: '(empty)',
            'category'         => $category,
            'budget'           => $budget,
            'preferences'      => $preferences,
            'widget_id'        => $widget_id,
            'chatbot_id'       => $chatbot_id,
            'language'          => $language,
        ) );

        if ( ! empty( $customer_message ) && empty( $preferences ) ) {
            $preferences = $this->extract_keywords_from_message( $customer_message, $language );
        }

        // Build settings: preset as base, widget config as override.
        $service  = new BoostBot_Bundle_Builder_Service();
        $settings = array(
            'include_variable' => true,
            'include_on_sale'  => true,
            'product_settings' => array(),
            'pricing_settings' => array(
                'discount_type'    => 'percent',
                'discount_percent' => 10,
            ),
            'coupon_settings'  => array(),
        );

        // 1. Try loading preset settings from the bot's linked preset.
        $preset_settings = null;
        if ( $chatbot_id ) {
            $preset_settings = $service->get_settings_from_preset( $chatbot_id );
        }

        // 2. Load widget config.
        $widget_config = array();
        if ( $widget_id ) {
            $widget_model = new BoostBot_Widget_Model();
            $widget       = $widget_model->get( $widget_id );
            if ( $widget && ! empty( $widget->config ) ) {
                $widget_config = is_string( $widget->config ) ? json_decode( $widget->config, true ) : (array) $widget->config;

                // If bot had no preset but widget config references one, use it.
                if ( ! $preset_settings && ! empty( $widget_config['woocommerce_preset_id'] ) ) {
                    $wc_preset_id  = absint( $widget_config['woocommerce_preset_id'] );
                    $preset_settings = $service->get_settings_from_preset_id( $wc_preset_id );
                }
            }
        }

        // 3. Merge: defaults ← preset ← widget config.
        if ( is_array( $preset_settings ) && ! empty( $preset_settings ) ) {
            $settings = array_merge( $settings, $preset_settings );
        }

        if ( ! empty( $widget_config ) ) {
            // Merge top-level settings from widget config.
            $settings = array_merge( $settings, $widget_config['settings'] ?? array() );

            if ( ! empty( $widget_config['product_settings'] ) ) {
                $settings['product_settings'] = array_merge(
                    $settings['product_settings'],
                    $widget_config['product_settings']
                );
            }
            if ( ! empty( $widget_config['pricing_settings'] ) ) {
                $settings['pricing_settings'] = array_merge(
                    $settings['pricing_settings'],
                    $widget_config['pricing_settings']
                );
            }
            if ( ! empty( $widget_config['coupon_settings'] ) ) {
                $settings['coupon_settings'] = array_merge(
                    $settings['coupon_settings'],
                    $widget_config['coupon_settings']
                );
            }

            // Lift product_settings to top level — build_bundle() reads categories/allowed_categories/exclude_categories there.
            $settings = array_merge( $settings, $widget_config['product_settings'] ?? array() );
        }

        // Collect cart item IDs to exclude from the bundle.
        $cart_item_ids = array();
        if ( ! empty( $params['cart_item_ids'] ) && is_array( $params['cart_item_ids'] ) ) {
            $cart_item_ids = array_map( 'absint', $params['cart_item_ids'] );
            $cart_item_ids = array_filter( $cart_item_ids );
        }

        // Merge with server-side WooCommerce cart (belt and suspenders).
        if ( class_exists( 'WooCommerce' ) && WC()->cart ) {
            try {
                foreach ( WC()->cart->get_cart() as $cart_item ) {
                    $pid = absint( $cart_item['product_id'] );
                    if ( $pid > 0 && ! in_array( $pid, $cart_item_ids, true ) ) {
                        $cart_item_ids[] = $pid;
                    }
                }
            } catch ( \Throwable $e ) {
                BoostBot_Logger::debug( '[Bundle Build] Could not read server-side cart', array(
                    'error' => $e->getMessage(),
                ) );
            }
        }

        // Build criteria.
        $criteria = array(
            'budget'       => $budget,
            'category'     => $category ?: null,
            'preferences'  => $preferences,
            'exclude'      => array_values( array_unique( $cart_item_ids ) ),
            'min_products' => $settings['min_products_in_bundle'] ?? $settings['min_products'] ?? 2,
            'max_products' => $settings['max_products_in_bundle'] ?? $settings['max_products'] ?? 5,
        );

        BoostBot_Logger::debug( '[Bundle Build] Cart exclusions applied', array(
            'cart_item_ids'  => $cart_item_ids,
            'total_excluded' => count( $criteria['exclude'] ),
        ) );

        $bundle  = $service->build_bundle( $criteria, $settings, $language );
        $bundle['language'] = $language;

        BoostBot_Logger::debug(
            '[Bundle Build] Service result',
            array(
                'category'        => $category,
                'budget'          => $budget,
                'language'        => $language,
                'preferences'     => $preferences,
                'product_count'   => count( $bundle['products'] ?? array() ),
                'total_price'     => $bundle['total_price'] ?? null,
                'original_price'  => $bundle['original_price'] ?? null,
            )
        );

        if ( empty( $bundle['products'] ) ) {
            BoostBot_Polylang_Context::restore_currency( $currency_override );

            // Always return debug info for now to diagnose the issue
            // Count total products in WooCommerce
            $all_products = wc_get_products( array( 'status' => 'publish', 'limit' => -1, 'return' => 'ids' ) );
            
            // Get products with details for debugging
            $sample_products = wc_get_products( array( 'status' => 'publish', 'limit' => 5 ) );
            $sample_info = array();
            foreach ( $sample_products as $p ) {
                $sample_info[] = array(
                    'id' => $p->get_id(),
                    'name' => $p->get_name(),
                    'price' => $p->get_price(),
                    'in_stock' => $p->is_in_stock(),
                    'type' => $p->get_type(),
                );
            }
            
            // Check bundle widgets
            global $wpdb;
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $widgets = $wpdb->get_results( "SELECT id, name, type, is_active FROM {$wpdb->prefix}boostbot_widgets WHERE type = 'bundle_builder' LIMIT 5" );

            BoostBot_Logger::warning(
                '[Widget] bundle-builder/build error: no products returned',
                array(
                    'endpoint'          => 'bundle-builder/build',
                    'category'          => $category,
                    'budget'            => $budget,
                    'language'          => $language,
                    'preferences'       => $preferences,
                    'widget_id'         => $widget_id,
                    'chatbot_id'        => $chatbot_id,
                    'criteria'          => $criteria,
                    'total_wc_products' => count( $all_products ),
                    'sample_product_ids' => array_map(
                        static function ( $product ) {
                            return (int) $product['id'];
                        },
                        $sample_info
                    ),
                )
            );
            
            return new WP_REST_Response(
                array(
                    'success' => false,
                    'message' => $language === 'en'
                        ? __( 'No matching products found. Try broadening your criteria.', 'boostbot' )
                        : __( 'לא מצאנו מוצרים מתאימים. נסה להרחיב את הקריטריונים.', 'boostbot' ),
                    'debug' => array(
                        'total_wc_products' => count( $all_products ),
                        'sample_products' => $sample_info,
                        'bundle_widgets' => $widgets,
                        'criteria' => $criteria,
                        'settings' => $settings,
                        'widget_id_used' => $widget_id,
                    ),
                ),
                404
            );
        }

        // Save bundle.
        $bundle_model = new BoostBot_Bundle_Model();
        $bundle_id    = $bundle_model->create(
            array(
                'conversation_id'   => $conversation_id ?: null,
                'chatbot_id'        => $chatbot_id ?: null,
                'widget_id'         => $widget_id ?: null,
                'customer_criteria' => $criteria,
                'products'          => $bundle['products'],
                'total_price'       => $bundle['total_price'],
                'original_price'    => $bundle['original_price'],
                'discount_amount'   => $bundle['savings'],
                'discount_percent'  => $bundle['savings_percent'],
                'status'            => 'suggested',
            )
        );

        BoostBot_Logger::info(
            'Bundle created via API',
            array(
                'bundle_id'      => $bundle_id,
                'products_count' => count( $bundle['products'] ),
                'total_price'    => $bundle['total_price'],
            )
        );

        // Prepare response data
        $response_data = array(
            'bundle_id'       => $bundle_id,
            'products'        => $bundle['products'],
            'total_price'     => $bundle['total_price'],
            'original_price'  => $bundle['original_price'],
            'savings'         => $bundle['savings'],
            'savings_percent' => $bundle['savings_percent'],
            'add_to_cart_url' => $service->get_bundle_cart_url( $bundle ),
            'message'         => $settings['success_message'] ?? ( $language === 'en'
                ? __( "Here's your recommended bundle!", 'boostbot' )
                : __( 'הנה החבילה המומלצת!', 'boostbot' ) ),
            // Debug info - always include for now
            '_debug' => array(
                'preferences_used' => $preferences,
                'customer_message' => $customer_message,
                'criteria'         => $criteria,
            ),
        );

        // Add coupon info if enabled
        $coupon_settings = $settings['coupon_settings'] ?? array();
        $preset_id       = (int) ( $settings['preset_id'] ?? 0 );
        if ( ! empty( $coupon_settings['enabled'] ) ) {
            $coupon_id = $coupon_settings['auto_coupon_id'] ?? null;
            
            // If auto_select_best is enabled, find the best coupon
            if ( ! empty( $coupon_settings['auto_select_best'] ) ) {
                $coupon_id = $this->find_best_coupon_for_bundle( $bundle['total_price'], $preset_id );
            }
            
            if ( $coupon_id ) {
                $coupon = new WC_Coupon( $coupon_id );
                if ( $coupon->get_id() ) {
                    $response_data['coupon'] = array(
                        'id'            => $coupon->get_id(),
                        'code'          => $coupon->get_code(),
                        'discount_type' => $coupon->get_discount_type(),
                        'amount'        => $coupon->get_amount(),
                    );
                    
                    // Add coupon to cart URL
                    $response_data['add_to_cart_url'] = add_query_arg(
                        'coupon_code',
                        $coupon->get_code(),
                        $response_data['add_to_cart_url']
                    );
                }
            }
        }

        BoostBot_Polylang_Context::restore_currency( $currency_override );

        return $this->success( $response_data );
    }

    /**
     * Swap a single bundle item for a server-selected replacement.
     *
     * @since 1.9.0
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function swap_bundle_item( $request ) {
        if ( ! class_exists( 'WooCommerce' ) ) {
            return $this->error(
                __( 'נדרש WooCommerce להפעלת פיצ\'ר זה', 'boostbot' ),
                400
            );
        }

        $params      = $request->get_json_params();
        $bundle_id   = absint( $params['bundle_id'] ?? 0 );
        $swap_out_id = absint( $params['swap_out_id'] ?? 0 );
        $language    = sanitize_text_field( $params['language'] ?? $this->get_request_language( $request ) ?: 'he' );

        if ( ! $swap_out_id ) {
            return $this->error( __( 'מזהה מוצר להחלפה חסר', 'boostbot' ), 400 );
        }

        $bundle_model  = new BoostBot_Bundle_Model();
        $stored_bundle = $bundle_id ? $bundle_model->get_with_products( $bundle_id ) : null;
        $widget_id     = absint( $params['widget_id'] ?? ( $stored_bundle->widget_id ?? 0 ) );
        $chatbot_id    = absint( $params['chatbot_id'] ?? ( $stored_bundle->chatbot_id ?? 0 ) );

        $currency_override = BoostBot_Polylang_Context::override_currency_for_language( $language );

        try {
            $settings       = $this->get_bundle_runtime_settings( $widget_id, $chatbot_id );
            $current_items  = $this->normalize_bundle_products_input( $params['bundle_items'] ?? array(), $language );
            $current_bundle = ! empty( $current_items )
                ? $current_items
                : ( is_object( $stored_bundle ) ? $this->normalize_bundle_products_input( $stored_bundle->products ?? array(), $language ) : array() );

            if ( empty( $current_bundle ) ) {
                BoostBot_Polylang_Context::restore_currency( $currency_override );
                return $this->error( __( 'לא נמצאה חבילה פעילה להחלפה', 'boostbot' ), 400 );
            }

            $swap_index = null;
            foreach ( $current_bundle as $index => $product ) {
                if ( (int) ( $product['id'] ?? 0 ) === $swap_out_id ) {
                    $swap_index = $index;
                    break;
                }
            }

            if ( null === $swap_index ) {
                BoostBot_Polylang_Context::restore_currency( $currency_override );
                return $this->not_found( __( 'מוצר בחבילה', 'boostbot' ) );
            }

            $stored_criteria = is_object( $stored_bundle ) && is_array( $stored_bundle->customer_criteria ?? null )
                ? $stored_bundle->customer_criteria
                : array();

            $budget = isset( $params['budget'] )
                ? floatval( $params['budget'] )
                : floatval( $stored_criteria['budget'] ?? array_sum( array_map(
                    static function ( $product ) {
                        return (float) ( $product['original_price'] ?? $product['price'] ?? 0 );
                    },
                    $current_bundle
                ) ) );

            $category = sanitize_text_field( $params['category'] ?? ( $stored_criteria['category'] ?? '' ) );
            $preferences = ! empty( $params['preferences'] )
                ? array_values( array_filter( array_map( 'sanitize_text_field', (array) $params['preferences'] ) ) )
                : array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $stored_criteria['preferences'] ?? array() ) ) ) );

            $kept_products = array_values(
                array_filter(
                    $current_bundle,
                    static function ( $product ) use ( $swap_out_id ) {
                        return (int) ( $product['id'] ?? 0 ) !== $swap_out_id;
                    }
                )
            );

            $kept_original_total = array_sum(
                array_map(
                    static function ( $product ) {
                        return (float) ( $product['original_price'] ?? $product['price'] ?? 0 );
                    },
                    $kept_products
                )
            );

            $remaining_budget = max( 0, $budget - $kept_original_total );
            $exclude_ids      = array_map(
                static function ( $product ) {
                    return (int) ( $product['id'] ?? 0 );
                },
                $current_bundle
            );

            $selector    = new BoostBot_Bundle_Product_Selector( $settings );
            $replacement = $selector->get_replacement_candidate(
                $swap_out_id,
                $exclude_ids,
                $remaining_budget,
                $category ?: null,
                $preferences,
                $language
            );

            if ( ! $replacement ) {
                BoostBot_Polylang_Context::restore_currency( $currency_override );
                return new WP_REST_Response(
                    array(
                        'success' => false,
                        'message' => $language === 'en'
                            ? __( 'No replacement product matched this bundle. Try rebuilding instead.', 'boostbot' )
                            : __( 'לא נמצא מוצר חלופי מתאים. נסה לבנות חבילה חדשה.', 'boostbot' ),
                    ),
                    404
                );
            }

            $updated_products              = $current_bundle;
            $updated_products[ $swap_index ] = $replacement;
            $updated_products              = array_values( $updated_products );

            $priced_bundle = $this->price_bundle_products(
                $updated_products,
                $settings,
                is_object( $stored_bundle ) ? floatval( $stored_bundle->discount_percent ?? 0 ) : null
            );
            $priced_bundle['language'] = $language;

            $coupon          = $this->build_bundle_coupon_payload( $settings, $priced_bundle );
            $service         = new BoostBot_Bundle_Builder_Service();
            $add_to_cart_url = $service->get_bundle_cart_url( $priced_bundle );

            if ( $coupon && ! empty( $coupon['code'] ) ) {
                $add_to_cart_url = add_query_arg( 'coupon_code', $coupon['code'], $add_to_cart_url );
                $priced_bundle['coupon'] = $coupon;
            }

            if ( $bundle_id ) {
                $bundle_model->update(
                    $bundle_id,
                    array(
                        'products'         => $priced_bundle['products'],
                        'total_price'      => $priced_bundle['total_price'],
                        'original_price'   => $priced_bundle['original_price'],
                        'discount_amount'  => $priced_bundle['savings'],
                        'discount_percent' => $priced_bundle['savings_percent'],
                        'status'           => 'customized',
                    )
                );
            }

            BoostBot_Logger::info(
                'Bundle item swapped via API',
                array(
                    'bundle_id'         => $bundle_id,
                    'swap_out_id'       => $swap_out_id,
                    'replacement_id'    => (int) ( $replacement['id'] ?? 0 ),
                    'remaining_budget'  => $remaining_budget,
                    'products_count'    => count( $priced_bundle['products'] ),
                )
            );

            BoostBot_Polylang_Context::restore_currency( $currency_override );

            return $this->success(
                array(
                    'bundle_id'           => $bundle_id,
                    'swap_out_id'         => $swap_out_id,
                    'replacement_product' => $replacement,
                    'products'            => $priced_bundle['products'],
                    'total_price'         => $priced_bundle['total_price'],
                    'original_price'      => $priced_bundle['original_price'],
                    'savings'             => $priced_bundle['savings'],
                    'savings_percent'     => $priced_bundle['savings_percent'],
                    'add_to_cart_url'     => $add_to_cart_url,
                    'coupon'              => $coupon,
                )
            );
        } catch ( \Throwable $e ) {
            BoostBot_Polylang_Context::restore_currency( $currency_override );
            BoostBot_Logger::error(
                'Bundle swap failed',
                array(
                    'bundle_id'   => $bundle_id,
                    'swap_out_id' => $swap_out_id,
                    'error'       => $e->getMessage(),
                )
            );

            return $this->error(
                $language === 'en'
                    ? __( 'Could not swap bundle item right now. Please try again.', 'boostbot' )
                    : __( 'לא ניתן היה להחליף את פריט החבילה כרגע. נסה שוב.', 'boostbot' ),
                500
            );
        }
    }

    /**
     * Find the best coupon for a given bundle total.
     *
     * @since 1.0.0
     * @param float $bundle_total The bundle total price.
     * @return int|null The best coupon ID or null.
     */
    private function find_best_coupon_for_bundle( float $bundle_total, int $preset_id = 0 ): ?int {
        if ( ! class_exists( 'WooCommerce' ) ) {
            return null;
        }

        if ( $preset_id > 0 ) {
            $service_coupon_id = ( new BoostBot_Bundle_Builder_Service() )->find_best_bundle_coupon_id( $preset_id, $bundle_total );
            if ( $service_coupon_id ) {
                return $service_coupon_id;
            }
        }

        return null;
    }

    /**
     * Add bundle products to cart.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function add_bundle_to_cart( $request ) {
        // Check WooCommerce.
        if ( ! class_exists( 'WooCommerce' ) ) {
            return $this->error(
                __( 'נדרש WooCommerce להפעלת פיצ\'ר זה', 'boostbot' ),
                400
            );
        }

        $params      = $request->get_json_params();
        $bundle_id   = absint( $params['bundle_id'] ?? 0 );
        $coupon_code = sanitize_text_field( $params['coupon_code'] ?? '' );
        $language    = $this->get_request_language( $request );
        $bundle_items = $this->normalize_bundle_products_input( $params['bundle_items'] ?? array(), $language ?: 'he' );
        $discount_percent = isset( $params['discount_percent'] ) ? floatval( $params['discount_percent'] ) : null;

        if ( ! $bundle_id ) {
            return $this->error( __( 'מזהה חבילה חסר', 'boostbot' ), 400 );
        }

        // Get bundle.
        $bundle_model = new BoostBot_Bundle_Model();
        $bundle       = $bundle_model->get_with_products( $bundle_id );

        if ( ! $bundle ) {
            return $this->not_found( __( 'חבילה', 'boostbot' ) );
        }

        if ( empty( $bundle_items ) ) {
            $bundle_items = $this->normalize_bundle_products_input( $bundle->products ?? array(), $language ?: 'he' );
        }

        if ( empty( $bundle_items ) ) {
            return $this->error( __( 'אין מוצרים להוספה', 'boostbot' ), 400 );
        }

        $session_cookie_name = 'wp_woocommerce_session_' . COOKIEHASH;

        BoostBot_Logger::debug(
            '[Cart] bundle endpoint request',
            array(
                'bundle_id'                        => $bundle_id,
                'bundle_item_count'                => count( $bundle_items ),
                'coupon_code_present'              => '' !== $coupon_code,
                'language'                         => $language,
                'has_woocommerce_cart_hash_cookie' => isset( $_COOKIE['woocommerce_cart_hash'] ),
                'has_wp_session_cookie'            => isset( $_COOKIE[ $session_cookie_name ] ),
            )
        );

        if ( null === $discount_percent ) {
            $discount_percent = floatval( $bundle->discount_percent ?? 0 );
        }

        // Persist the shopper's current customized bundle so server-side cart actions
        // reflect prior remove/swap actions instead of the originally suggested items.
        $customized_total = array_sum(
            array_map(
                static function ( $product ) {
                    return (float) ( $product['price'] ?? 0 );
                },
                $bundle_items
            )
        );
        $customized_original = array_sum(
            array_map(
                static function ( $product ) {
                    return (float) ( $product['original_price'] ?? $product['price'] ?? 0 );
                },
                $bundle_items
            )
        );

        $bundle_model->update(
            $bundle_id,
            array(
                'products'         => $bundle_items,
                'total_price'      => round( $customized_total, 2 ),
                'original_price'   => round( $customized_original, 2 ),
                'discount_amount'  => round( $customized_original - $customized_total, 2 ),
                'discount_percent' => round( $discount_percent, 1 ),
                'status'           => 'customized',
            )
        );

        // Resolve the preset's bundle discount method (coupon vs line_item).
        $discount_method = 'coupon';
        $bot_id = $bundle->chatbot_id ?? 0;
        if ( $bot_id && class_exists( 'BoostBot_Bot_Model' ) && class_exists( 'BoostBot_WooCommerce_Preset_Model' ) ) {
            $bot_obj = ( new BoostBot_Bot_Model() )->get( (int) $bot_id );
            if ( $bot_obj && ! empty( $bot_obj->woocommerce_preset_id ) ) {
                $wc_preset = ( new BoostBot_WooCommerce_Preset_Model() )->get( (int) $bot_obj->woocommerce_preset_id );
                if ( $wc_preset ) {
                    $discount_method = $wc_preset->bundle_discount_method ?? 'coupon';
                }
            }
        }

        // Add to cart with bundle pricing info.
        $service = new BoostBot_Bundle_Builder_Service();
        $result  = $service->add_bundle_to_cart(
            array( 'products' => $bundle_items ),
            $discount_percent,
            $bundle_id,
            $language,
            $discount_method
        );

        BoostBot_Logger::debug(
            '[Cart] bundle endpoint result',
            array(
                'bundle_id'                        => $bundle_id,
                'success'                          => ! empty( $result['success'] ),
                'cart_count'                       => $result['cart_count'] ?? ( $result['cart']['count'] ?? null ),
                'has_cart_payload'                 => ! empty( $result['cart'] ),
                'coupon_code_present'              => '' !== $coupon_code,
                'coupon_applied'                   => $result['coupon_applied'] ?? null,
                'discount_method'                  => $discount_method,
                'has_woocommerce_cart_hash_cookie' => isset( $_COOKIE['woocommerce_cart_hash'] ),
                'has_wp_session_cookie'            => isset( $_COOKIE[ $session_cookie_name ] ),
            )
        );

        if ( $result['success'] ) {
            // Update bundle status.
            $bundle_model->update_status( $bundle_id, 'accepted' );
            $wc_service = new BoostBot_WooCommerce_Service();

            // Only apply coupon when using the coupon discount method (not line_item).
            if ( 'coupon' === $discount_method && ! empty( $coupon_code ) && WC()->cart ) {
                $coupon_result = WC()->cart->apply_coupon( $coupon_code );
                $result['coupon_applied'] = $coupon_result;
                $result['coupon_code']    = $coupon_code;
                $result['coupon_apply_source'] = 'bundle';
                
                if ( $coupon_result ) {
                    WC()->cart->calculate_totals();
                    $result['cart'] = $wc_service->get_cart_info( $language );
                    BoostBot_Logger::info(
                        'Coupon applied to bundle cart',
                        array( 
                            'bundle_id'   => $bundle_id,
                            'coupon_code' => $coupon_code,
                        )
                    );
                }
            } elseif ( 'line_item' === $discount_method ) {
                WC()->cart->calculate_totals();
                $result['cart'] = $wc_service->get_cart_info( $language );
                $result['discount_method'] = 'line_item';
            }

            BoostBot_Logger::info(
                'Bundle added to cart',
                array( 'bundle_id' => $bundle_id, 'discount_method' => $discount_method )
            );
        }

        return $this->success( $result );
    }

    /**
     * Get bundle builder configuration.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function get_bundle_builder_config( $request ) {
        $widget_id = absint( $request->get_param( 'widget_id' ) );

        // Get widget settings if provided.
        $settings = array();
        if ( $widget_id ) {
            $widget_model = new BoostBot_Widget_Model();
            $widget       = $widget_model->get( $widget_id );
            if ( $widget && ! empty( $widget->config ) ) {
                $config   = is_string( $widget->config ) ? json_decode( $widget->config, true ) : (array) $widget->config;
                $settings = $config['settings'] ?? array();
            }
        }

        $service = new BoostBot_Bundle_Builder_Service();

        // Check license.
        $license     = new BoostBot_License_Manager();
        $is_available = $license->has_bundles();

        return $this->success(
            array(
                'is_available'  => $is_available,
                'budget_ranges' => $service->get_budget_ranges( $settings ),
                'categories'    => $service->get_categories( $settings['categories'] ?? array() ),
                'settings'      => array(
                    'show_savings'        => $settings['show_savings'] ?? true,
                    'allow_customization' => $settings['allow_customization'] ?? true,
                    'intro_message'       => $settings['intro_message'] ?? __( 'בוא נבנה לך חבילה מושלמת!', 'boostbot' ),
                ),
            )
        );
    }

    /**
     * Resolve effective bundle runtime settings from preset + widget overrides.
     *
     * @param int $widget_id  Widget ID.
     * @param int $chatbot_id Chatbot ID.
     * @return array
     */
    private function get_bundle_runtime_settings( int $widget_id = 0, int $chatbot_id = 0 ): array {
        $service  = new BoostBot_Bundle_Builder_Service();
        $settings = array(
            'include_variable' => true,
            'include_on_sale'  => true,
            'product_settings' => array(),
            'pricing_settings' => array(
                'discount_type'    => 'percent',
                'discount_percent' => 10,
            ),
            'coupon_settings'  => array(),
        );

        $preset_settings = null;
        if ( $chatbot_id ) {
            $preset_settings = $service->get_settings_from_preset( $chatbot_id );
        }

        $widget_config = array();
        if ( $widget_id ) {
            $widget_model = new BoostBot_Widget_Model();
            $widget       = $widget_model->get( $widget_id );
            if ( $widget && ! empty( $widget->config ) ) {
                $widget_config = is_string( $widget->config ) ? json_decode( $widget->config, true ) : (array) $widget->config;

                if ( ! $preset_settings && ! empty( $widget_config['woocommerce_preset_id'] ) ) {
                    $preset_settings = $service->get_settings_from_preset_id( absint( $widget_config['woocommerce_preset_id'] ) );
                }
            }
        }

        if ( is_array( $preset_settings ) && ! empty( $preset_settings ) ) {
            $settings = array_merge( $settings, $preset_settings );
        }

        if ( ! empty( $widget_config ) ) {
            $settings = array_merge( $settings, $widget_config['settings'] ?? array() );

            if ( ! empty( $widget_config['product_settings'] ) ) {
                $settings['product_settings'] = array_merge(
                    $settings['product_settings'],
                    $widget_config['product_settings']
                );
            }
            if ( ! empty( $widget_config['pricing_settings'] ) ) {
                $settings['pricing_settings'] = array_merge(
                    $settings['pricing_settings'],
                    $widget_config['pricing_settings']
                );
            }
            if ( ! empty( $widget_config['coupon_settings'] ) ) {
                $settings['coupon_settings'] = array_merge(
                    $settings['coupon_settings'],
                    $widget_config['coupon_settings']
                );
            }

            $settings = array_merge( $settings, $widget_config['product_settings'] ?? array() );
        }

        return $settings;
    }

    /**
     * Normalize bundle product payloads received from the browser.
     *
     * Accepts either full product objects or a plain list of product IDs.
     *
     * @param mixed  $items    Incoming payload.
     * @param string $language Target language slug.
     * @return array
     */
    private function normalize_bundle_products_input( $items, string $language = 'he' ): array {
        if ( ! is_array( $items ) ) {
            return array();
        }

        $normalized  = array();
        $ids_to_load = array();

        foreach ( $items as $item ) {
            if ( is_array( $item ) && isset( $item['id'] ) ) {
                $product_id = absint( $item['id'] );
                if ( ! $product_id ) {
                    continue;
                }

                $price         = floatval( $item['price'] ?? 0 );
                $regular_price = floatval( $item['regular_price'] ?? 0 );
                $original      = floatval( $item['original_price'] ?? 0 );

                if ( $original <= 0 ) {
                    $original = $regular_price > 0 ? $regular_price : $price;
                }

                $normalized[] = array(
                    'id'             => $product_id,
                    'name'           => sanitize_text_field( $item['name'] ?? '' ),
                    'price'          => round( $price, 2 ),
                    'regular_price'  => round( $regular_price > 0 ? $regular_price : $original, 2 ),
                    'original_price' => round( $original, 2 ),
                    'image'          => esc_url_raw( $item['image'] ?? '' ),
                    'permalink'      => esc_url_raw( $item['permalink'] ?? '' ),
                    'rating'         => floatval( $item['rating'] ?? 0 ),
                    'on_sale'        => ! empty( $item['on_sale'] ),
                    'score'          => floatval( $item['score'] ?? 0 ),
                    'savings'        => floatval( $item['savings'] ?? max( 0, $original - $price ) ),
                );
            } elseif ( is_numeric( $item ) ) {
                $ids_to_load[] = absint( $item );
            }
        }

        foreach ( $ids_to_load as $product_id ) {
            $product = $this->load_bundle_product_by_id( $product_id, $language );
            if ( $product ) {
                $normalized[] = $product;
            }
        }

        return array_values( $normalized );
    }

    /**
     * Load a WooCommerce product in the requested language and format it for bundles.
     *
     * @param int    $product_id Product ID.
     * @param string $language   Target language slug.
     * @return array|null
     */
    private function load_bundle_product_by_id( int $product_id, string $language = 'he' ): ?array {
        if ( $product_id <= 0 ) {
            return null;
        }

        if ( $language && function_exists( 'pll_get_post_language' ) && function_exists( 'pll_get_post' ) ) {
            $product_lang = pll_get_post_language( $product_id );
            if ( $product_lang && $product_lang !== $language ) {
                $translated_id = pll_get_post( $product_id, $language );
                if ( $translated_id ) {
                    $product_id = (int) $translated_id;
                }
            }
        }

        $product = wc_get_product( $product_id );
        if ( ! $product ) {
            return null;
        }

        $price         = (float) $product->get_price();
        $regular_price = (float) $product->get_regular_price();
        $original      = $regular_price > 0 ? $regular_price : $price;

        return array(
            'id'             => (int) $product->get_id(),
            'name'           => $product->get_name(),
            'price'          => $price,
            'regular_price'  => $regular_price,
            'original_price' => $original,
            'image'          => wp_get_attachment_url( $product->get_image_id() ) ?: '',
            'permalink'      => urldecode( $product->get_permalink() ),
            'rating'         => (float) $product->get_average_rating(),
            'on_sale'        => (bool) $product->is_on_sale(),
            'score'          => 0,
            'savings'        => 0,
        );
    }

    /**
     * Recalculate bundle pricing for a customized set of products.
     *
     * @param array      $products                  Bundle products.
     * @param array      $settings                  Effective runtime settings.
     * @param float|null $fallback_discount_percent Previously applied percent, used when settings are incomplete.
     * @return array
     */
    private function price_bundle_products( array $products, array $settings, ?float $fallback_discount_percent = null ): array {
        $products = array_values(
            array_filter(
                array_map(
                    static function ( $product ) {
                        if ( ! is_array( $product ) || empty( $product['id'] ) ) {
                            return null;
                        }

                        $price         = floatval( $product['price'] ?? 0 );
                        $regular_price = floatval( $product['regular_price'] ?? 0 );
                        $original      = floatval( $product['original_price'] ?? 0 );
                        if ( $original <= 0 ) {
                            $original = $regular_price > 0 ? $regular_price : $price;
                        }

                        return array_merge(
                            $product,
                            array(
                                'price'          => round( $original, 2 ),
                                'regular_price'  => round( $regular_price > 0 ? $regular_price : $original, 2 ),
                                'original_price' => round( $original, 2 ),
                            )
                        );
                    },
                    $products
                )
            )
        );

        $subtotal = array_sum(
            array_map(
                static function ( $product ) {
                    return (float) ( $product['original_price'] ?? 0 );
                },
                $products
            )
        );

        $pricing_settings = is_array( $settings['pricing_settings'] ?? null ) ? $settings['pricing_settings'] : array();
        if ( empty( $pricing_settings ) ) {
            $pricing_settings = array(
                'discount_type'    => 'percent',
                'discount_percent' => $fallback_discount_percent ?? floatval( $settings['bundle_discount_percent'] ?? $settings['discount_percent'] ?? 0 ),
            );
        } elseif ( ! isset( $pricing_settings['discount_type'] ) ) {
            $pricing_settings['discount_type'] = 'percent';
        }

        if ( ! isset( $pricing_settings['discount_percent'] ) && null !== $fallback_discount_percent ) {
            $pricing_settings['discount_percent'] = $fallback_discount_percent;
        }

        $calculator = new BoostBot_Bundle_Discount_Calculator( $pricing_settings );
        $pricing    = $calculator->calculate(
            array(
                'products'        => $products,
                'total_price'     => $subtotal,
                'original_price'  => $subtotal,
            )
        );

        $discount_percent = isset( $pricing['discount_percent'] )
            ? floatval( $pricing['discount_percent'] )
            : floatval( $fallback_discount_percent ?? 0 );
        $discount_percent   = max( 0, min( 100, $discount_percent ) );
        $discount_multiplier = 1 - ( $discount_percent / 100 );

        $total_original   = 0.0;
        $total_discounted = 0.0;

        foreach ( $products as &$product ) {
            $original   = floatval( $product['original_price'] ?? 0 );
            $discounted = round( $original * $discount_multiplier, 2 );

            $product['price']        = $discounted;
            $product['bundle_price'] = $discounted;
            $product['savings']      = round( $original - $discounted, 2 );

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

        return array(
            'products'        => $products,
            'total_price'     => round( $total_discounted, 2 ),
            'original_price'  => round( $total_original, 2 ),
            'savings'         => round( $total_original - $total_discounted, 2 ),
            'savings_percent' => round( $discount_percent, 1 ),
        );
    }

    /**
     * Build the coupon payload for a bundle response when coupon rules allow it.
     *
     * @param array $settings Effective bundle settings.
     * @param array $bundle   Current bundle payload.
     * @return array|null
     */
    private function build_bundle_coupon_payload( array $settings, array $bundle ): ?array {
        $coupon_settings = $settings['coupon_settings'] ?? array();
        $preset_id       = (int) ( $settings['preset_id'] ?? 0 );

        if ( empty( $coupon_settings['enabled'] ) ) {
            return null;
        }

        $coupon_id = $coupon_settings['auto_coupon_id'] ?? null;
        if ( ! empty( $coupon_settings['auto_select_best'] ) ) {
            $coupon_id = $this->find_best_coupon_for_bundle( floatval( $bundle['total_price'] ?? 0 ), $preset_id );
        }

        if ( ! $coupon_id ) {
            return null;
        }

        $coupon = new WC_Coupon( $coupon_id );
        if ( ! $coupon->get_id() ) {
            return null;
        }

        return array(
            'id'            => $coupon->get_id(),
            'code'          => $coupon->get_code(),
            'discount_type' => $coupon->get_discount_type(),
            'amount'        => $coupon->get_amount(),
        );
    }

    /**
     * Get bundle analytics.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function get_bundle_analytics( $request ) {
        $days = absint( $request->get_param( 'days' ) ) ?: 30;

        $bundle_model = new BoostBot_Bundle_Model();
        $analytics    = $bundle_model->get_analytics( $days );
        $recent       = $bundle_model->get_recent( 10 );

        return $this->success(
            array(
                'analytics' => $analytics,
                'recent'    => $recent,
            )
        );
    }
    
    /**
     * Extract keywords from customer message for product matching.
     *
     * Parses customer's natural language request and extracts
     * relevant keywords to match against products.
     *
     * @since 1.0.0
     * @param string $message Customer's message.
     * @return array Array of keywords.
     */
    /**
     * Extract keywords from customer message for product matching.
     *
     * IMPORTANT: This function is DYNAMIC - it does NOT use hardcoded product keywords!
     * Instead, it extracts all meaningful words from the message and lets the
     * product matching in Bundle Builder Service compare them against actual
     * WooCommerce products (names, descriptions, categories).
     *
     * This approach works for ANY business type - the keywords come from the
     * customer's message and are matched against whatever products exist in WooCommerce.
     *
     * @since 1.0.0
     * @param string $message Customer's message.
     * @return array Array of keywords extracted from message.
     */
    private function extract_keywords_from_message( string $message, string $language = 'he' ): array {
        $keywords = array();
        $message  = mb_strtolower( trim( $message ) );

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

        if ( $language === 'en' ) {
            $stop_words = array(
                'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
                'of', 'with', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
                'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
                'should', 'may', 'might', 'shall', 'can', 'not', 'no', 'nor',
                'this', 'that', 'these', 'those', 'it', 'its', 'my', 'your', 'his',
                'her', 'our', 'their', 'what', 'which', 'who', 'whom', 'how',
                'when', 'where', 'why', 'all', 'each', 'every', 'both', 'few',
                'more', 'most', 'some', 'any', 'other', 'than', 'too', 'very',
                'just', 'also', 'about', 'want', 'need', 'like', 'get', 'make',
                'know', 'think', 'take', 'come', 'go', 'see', 'look', 'find',
                'give', 'tell', 'ask', 'seem', 'feel', 'try', 'leave', 'call',
                'i', 'me', 'we', 'us', 'you', 'he', 'she', 'they', 'them',
                'so', 'if', 'then', 'there', 'here', 'up', 'out', 'down',
                'from', 'by', 'been', 'into', 'only', 'own', 'same', 'such',
                'really', 'much', 'well', 'back', 'still', 'even', 'way',
                'new', 'now', 'old', 'great', 'good', 'best', 'better',
                'big', 'small', 'long', 'right', 'please', 'thanks', 'thank',
                'bundle', 'bundles', 'package', 'packages', 'product', 'products',
                'service', 'services', 'thing', 'things', 'something', 'anything',
                'help', 'build', 'create', 'put', 'together', 'suggest', 'recommend',
            );
        } else {
            $stop_words = array(
                'אני', 'את', 'אתה', 'אתם', 'אנחנו', 'הוא', 'היא', 'הם', 'הן',
                'של', 'על', 'עם', 'לי', 'לך', 'לנו', 'להם', 'שלי', 'שלך', 'שלנו',
                'זה', 'זאת', 'זו', 'אלה', 'אלו', 'כזה', 'כזאת',
                'יש', 'אין', 'היה', 'היתה', 'להיות', 'שיש', 'שאין',
                'רוצה', 'רוצים', 'מחפש', 'מחפשים', 'צריך', 'צריכים', 'צריכה',
                'תעשה', 'תעשי', 'עושה', 'עושים', 'לעשות',
                'תכין', 'תכיני', 'מכין', 'להכין',
                'תרכיב', 'לרכב', 'מרכיב',
                'תבנה', 'לבנות', 'בונה',
                'תציע', 'מציע', 'להציע',
                'יכול', 'יכולה', 'יכולים', 'אפשר',
                'טוב', 'טובה', 'טובים', 'יפה', 'יפים',
                'מצוין', 'מצוינת', 'מעולה', 'נהדר',
                'הכי', 'יותר', 'פחות', 'מאוד', 'קצת',
                'מה', 'מי', 'איך', 'למה', 'מתי', 'איפה', 'כמה', 'איזה', 'אילו',
                'ו', 'או', 'אם', 'כי', 'ש', 'אבל', 'גם', 'רק', 'עוד',
                'דבר', 'דברים', 'משהו', 'כמה', 'הרבה',
                'חבילה', 'חבילות', 'מוצר', 'מוצרים', 'שירות', 'שירותים',
                'בבקשה', 'תודה', 'סליחה',
            );
        }

        // Split message into words
        $words = preg_split( '/[\s,.\-!?()\"\']+/u', $message );

        foreach ( $words as $word ) {
            $word = trim( $word );

            if ( empty( $word ) || mb_strlen( $word ) < 2 ) {
                continue;
            }

            if ( in_array( $word, $stop_words, true ) ) {
                continue;
            }

            $clean_word = $word;

            // Hebrew prefix stripping — only for Hebrew
            if ( $language !== 'en' ) {
                $prefixes = array( 'ל', 'ב', 'ה', 'ו', 'מ', 'כ', 'ש' );

                foreach ( $prefixes as $prefix ) {
                    if ( mb_strpos( $word, $prefix ) === 0 && mb_strlen( $word ) > 2 ) {
                        $without_prefix = mb_substr( $word, 1 );
                        if ( ! in_array( $without_prefix, $stop_words, true ) && mb_strlen( $without_prefix ) >= 2 ) {
                            $clean_word = $without_prefix;
                            break;
                        }
                    }
                }
            }

            if ( in_array( $clean_word, $stop_words, true ) ) {
                continue;
            }

            $keywords[] = $clean_word;
            if ( $clean_word !== $word ) {
                $keywords[] = $word;
            }
        }

        $keywords = array_unique( $keywords );

        BoostBot_Logger::debug( 'Extracted keywords from message', array(
            'language' => $language,
            'keywords' => $keywords,
        ) );

        return $keywords;
    }

    /**
     * Get WooCommerce coupons for bundle builder settings.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function get_wc_coupons( $request ) {
        // Check if WooCommerce is active
        if ( ! class_exists( 'WooCommerce' ) ) {
            return $this->success( array() );
        }

        $coupons = array();

        // Get all published coupons
        $coupon_posts = get_posts(
            array(
                'post_type'      => 'shop_coupon',
                'post_status'    => 'publish',
                'posts_per_page' => -1,
                'orderby'        => 'title',
                'order'          => 'ASC',
            )
        );

        foreach ( $coupon_posts as $coupon_post ) {
            $coupon = new WC_Coupon( $coupon_post->ID );

            // Get expiry date
            $expiry_date = $coupon->get_date_expires();
            
            // Skip expired coupons
            if ( $expiry_date && $expiry_date->getTimestamp() < time() ) {
                continue;
            }

            $coupons[] = array(
                'id'            => $coupon_post->ID,
                'code'          => $coupon->get_code(),
                'discount_type' => $coupon->get_discount_type(),
                'amount'        => $coupon->get_amount(),
                'description'   => $coupon_post->post_excerpt,
                'usage_limit'   => $coupon->get_usage_limit(),
                'usage_count'   => $coupon->get_usage_count(),
                'expiry_date'   => $expiry_date ? $expiry_date->format( 'Y-m-d' ) : null,
            );
        }

        return $this->success( $coupons );
    }

    /**
     * Duplicate a widget (including form fields for form-type widgets).
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error Response object.
     */
    public function duplicate_widget( $request ) {
        $id    = absint( $request->get_param( 'id' ) );
        $model = new BoostBot_Widget_Model();

        $original = $model->get_with_fields( $id );
        if ( ! $original ) {
            return $this->not_found( __( 'וידג\'ט', 'boostbot' ) );
        }

        // Build copy data.
        $config = $original->config;
        if ( is_array( $config ) ) {
            $config = wp_json_encode( $config );
        }

        $data = array(
            'name'        => $original->name . __( ' (העתק)', 'boostbot' ),
            'type'        => $original->type,
            'description' => $original->description,
            'is_active'   => 0, // Duplicates start inactive.
            'config'      => $config,
        );

        // For form widgets, also duplicate the fields.
        if ( BoostBot_Widget_Model::TYPE_FORM === $original->type && ! empty( $original->fields ) ) {
            $fields = array();
            foreach ( $original->fields as $field ) {
                $fields[] = array(
                    'field_type'       => $field->field_type,
                    'label'            => $field->label,
                    'placeholder'      => $field->placeholder ?? '',
                    'name'             => $field->name,
                    'is_required'      => $field->is_required,
                    'validation_rules' => $field->validation_rules ?? null,
                    'options'          => $field->options ?? null,
                    'sort_order'       => $field->sort_order,
                );
            }
            $data['fields'] = $fields;
            $new_id = $model->create_with_fields( $data );
        } else {
            $new_id = $model->create( $data );
        }

        if ( ! $new_id ) {
            return $this->error( __( 'שגיאה בשכפול הווידג\'ט', 'boostbot' ), 500 );
        }

        $widget = $model->get_with_fields( $new_id );

        BoostBot_Logger::info(
            'Widget duplicated',
            array(
                'original_id' => $id,
                'new_id'      => $new_id,
                'type'        => $original->type,
            )
        );

        return $this->success( array( 'widget' => $widget ), 201 );
    }

    /**
     * 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 );
    }
}

