<?php
/**
 * Widget Model for BoostBot.
 *
 * Database model for managing widget configurations.
 *
 * @package BoostBot
 * @since   1.0.0
 */

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

/**
 * Class BoostBot_Widget_Model
 *
 * Handles CRUD operations for widgets stored in database.
 *
 * @since 1.0.0
 */
class BoostBot_Widget_Model extends BoostBot_Base_Model {

	use BoostBot_Localizable;

	/**
	 * Widget type constants.
	 */
	const TYPE_FORM             = 'form';
	const TYPE_WHATSAPP         = 'whatsapp';
	/** @deprecated Use TYPE_CALENDAR_MEETING instead. Kept for backward compatibility with existing widgets. */
	const TYPE_CALENDAR_CALL    = 'calendar_call';
	const TYPE_CALENDAR_MEETING = 'calendar_meeting';
	const TYPE_PRODUCT_CARD     = 'product_card';
	const TYPE_COUPON           = 'coupon';
	const TYPE_BUNDLE_BUILDER   = 'bundle_builder';
	const TYPE_GALLERY          = 'gallery';
	const TYPE_YOUTUBE          = 'youtube';
	const TYPE_OPENING_SCREEN   = 'opening_screen';
	const TYPE_CONTACT_CARD     = 'contact_card';
	const TYPE_SURVEY           = 'survey';

	/**
	 * Constructor.
	 */
	public function __construct() {
		parent::__construct();
		$this->table_name   = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_widgets' );
		$this->fillable     = array(
			'name',
			'type',
			'description',
			'config',
			'is_active',
			'is_system_managed',
			'language',
		);
		$this->column_types = array(
			'id'                => 'bigint',
			'is_active'         => 'tinyint',
			'is_system_managed' => 'tinyint',
		);
	}

	/**
	 * Get all valid widget types.
	 *
	 * @return array Widget types with labels.
	 */
	public static function get_widget_types() {
		$types = array(
			self::TYPE_OPENING_SCREEN   => __( 'מסך פתיחה', 'boostbot' ),
			self::TYPE_FORM             => __( 'טופס דינמי', 'boostbot' ),
			self::TYPE_WHATSAPP         => __( 'וואטסאפ', 'boostbot' ),
			self::TYPE_CALENDAR_CALL    => __( 'תאריכון (שיחות)', 'boostbot' ), // backward compat
			self::TYPE_CALENDAR_MEETING => __( 'תאריכון', 'boostbot' ),
			self::TYPE_GALLERY          => __( 'גלריה', 'boostbot' ),
			self::TYPE_YOUTUBE          => __( 'יוטיוב', 'boostbot' ),
			self::TYPE_CONTACT_CARD     => __( 'כרטיס איש קשר', 'boostbot' ),
		);

		if ( class_exists( 'WooCommerce' ) ) {
			$types[ self::TYPE_PRODUCT_CARD ]   = __( 'כרטיס מוצר', 'boostbot' );
			$types[ self::TYPE_COUPON ]         = __( 'קופון', 'boostbot' );
			$types[ self::TYPE_BUNDLE_BUILDER ] = __( 'בונה חבילות AI', 'boostbot' );
		}

		return $types;
	}

	/**
	 * Get AI trigger keywords for each widget type.
	 * These keywords tell Gemini when to recommend each widget type.
	 *
	 * @param string $type Widget type.
	 * @return array Trigger configuration with keywords and instruction.
	 */
	public static function get_widget_ai_triggers( string $type, string $language = 'he' ): array {
		$language = 'en' === strtolower( $language ) ? 'en' : 'he';

		$triggers = array(
			self::TYPE_FORM             => array(
				'he' => array(
					'keywords'    => array( 'טופס', 'פרטים', 'להשאיר פרטים', 'יצירת קשר', 'ליצור קשר', 'שיחזרו אלי', 'להתקשר אלי' ),
					'instruction' => 'כשהמשתמש מבקש להשאיר פרטים, ליצור קשר, או למלא טופס - הצג את הטופס',
				),
				'en' => array(
					'keywords'    => array( 'form', 'contact', 'details', 'leave my details', 'get in touch', 'callback' ),
					'instruction' => 'When the user wants to leave details, get in touch, or fill a form, show the form widget.',
				),
			),
			self::TYPE_GALLERY          => array(
				'he' => array(
					'keywords'    => array( 'גלריה', 'תמונות', 'עבודות', 'דוגמאות', 'פורטפוליו', 'לראות' ),
					'instruction' => 'כשהמשתמש מבקש לראות גלריה, עבודות, דוגמאות או תמונות - הצג את הגלריה',
				),
				'en' => array(
					'keywords'    => array( 'gallery', 'photos', 'images', 'portfolio', 'examples', 'show me' ),
					'instruction' => 'When the user asks to see a gallery, examples, images, or portfolio items, show the gallery widget.',
				),
			),
			self::TYPE_CALENDAR_MEETING => array(
				'he' => array(
					'keywords'    => array( 'פגישה', 'לקבוע פגישה', 'תיאום פגישה', 'להיפגש', 'שיחה', 'טלפון', 'להתקשר', 'שיחת טלפון', 'לתאם שיחה' ),
					'instruction' => 'כשהמשתמש מבקש לקבוע או לתאם פגישה או שיחה - הצג את התאריכון',
				),
				'en' => array(
					'keywords'    => array( 'meeting', 'schedule', 'appointment', 'book a meeting', 'call', 'phone', 'callback' ),
					'instruction' => 'When the user wants to schedule a meeting or call, show the calendar widget.',
				),
			),
			self::TYPE_CALENDAR_CALL    => array(
				'he' => array(
					'keywords'    => array( 'שיחה', 'טלפון', 'להתקשר', 'שיחת טלפון', 'לתאם שיחה', 'פגישה' ),
					'instruction' => 'כשהמשתמש מבקש לתאם שיחה או פגישה - הצג את התאריכון',
				),
				'en' => array(
					'keywords'    => array( 'call', 'phone', 'schedule a call', 'meeting', 'appointment' ),
					'instruction' => 'When the user wants to schedule a call or meeting, show the calendar widget.',
				),
			),
			self::TYPE_WHATSAPP         => array(
				'he' => array(
					'keywords'    => array( 'וואטסאפ', 'ווטסאפ' ),
					'instruction' => 'כשהמשתמש מבקש ליצור קשר בוואטסאפ - הצג את כפתור הוואטסאפ',
				),
				'en' => array(
					'keywords'    => array( 'whatsapp', 'message on whatsapp' ),
					'instruction' => 'When the user wants to contact the business on WhatsApp, show the WhatsApp widget.',
				),
			),
			self::TYPE_YOUTUBE          => array(
				'he' => array(
					'keywords'    => array( 'סרטון', 'וידאו', 'יוטיוב', 'לצפות' ),
					'instruction' => 'כשהמשתמש מבקש לצפות בסרטון או וידאו - הצג את הסרטון',
				),
				'en' => array(
					'keywords'    => array( 'video', 'youtube', 'watch', 'demo video' ),
					'instruction' => 'When the user asks to watch a video, show the video widget.',
				),
			),
			self::TYPE_OPENING_SCREEN   => array(
				'he' => array(
					'keywords'    => array(),
					'instruction' => 'מסך פתיחה - מוצג אוטומטית, לא מופעל על ידי המשתמש',
				),
				'en' => array(
					'keywords'    => array(),
					'instruction' => 'Opening screen - shown automatically, not triggered by the user.',
				),
			),
			self::TYPE_CONTACT_CARD     => array(
				'he' => array(
					'keywords'    => array( 'איש קשר', 'פרטי קשר', 'עם מי לדבר', 'נציג', 'מי אתם', 'מי עובד', 'צוות' ),
					'instruction' => 'כשהמשתמש מבקש לראות פרטי קשר, לדבר עם נציג, או לדעת עם מי לדבר - הצג כרטיס איש קשר',
				),
				'en' => array(
					'keywords'    => array( 'contact card', 'contact info', 'representative', 'team', 'who should i talk to' ),
					'instruction' => 'When the user asks for contact details or wants to speak with a representative, show the contact card widget.',
				),
			),
		);

		if ( class_exists( 'WooCommerce' ) ) {
			$triggers[ self::TYPE_PRODUCT_CARD ]   = array(
				'he' => array(
					'keywords'    => array( 'מוצר', 'מוצרים', 'לקנות', 'חנות', 'תכשיט', 'תכשיטים', 'טבעת', 'טבעות', 'עגיל', 'עגילים', 'שרשרת', 'שרשראות', 'צמיד', 'צמידים', 'שעון', 'שעונים' ),
					'instruction' => 'כשהמשתמש שואל על סוג מוצר מסוים, אפשרויות קנייה, או מביע כוונת רכישה - הצג את כרטיס המוצר.',
				),
				'en' => array(
					'keywords'    => array( 'product', 'products', 'buy', 'purchase', 'shop', 'shopping', 'catalog', 'store', 'jewelry', 'ring', 'rings', 'bracelet', 'bracelets', 'necklace', 'necklaces', 'earring', 'earrings', 'watch', 'watches', 'anklet', 'anklets', 'pendant', 'pendants', 'chain', 'chains' ),
					'instruction' => 'When the user asks about a specific product type, shopping options, or buying intent, trigger the product card widget.',
				),
			);
			$triggers[ self::TYPE_COUPON ]         = array(
				'he' => array(
					'keywords'    => array( 'קופון', 'הנחה', 'מבצע', 'הטבה' ),
					'instruction' => 'כשהמשתמש שואל על הנחות, מבצעים או קופונים - הצג את הקופון',
				),
				'en' => array(
					'keywords'    => array( 'coupon', 'discount', 'deal', 'promo code', 'offer' ),
					'instruction' => 'When the user asks about discounts, deals, or coupons, show the coupon widget.',
				),
			);
			$triggers[ self::TYPE_BUNDLE_BUILDER ] = array(
				'he' => array(
					'keywords'    => array( 'חבילה', 'להרכיב חבילה', 'בונה חבילות' ),
					'instruction' => 'כשהמשתמש מעוניין בחבילה מותאמת אישית, התחל בשאלות קצרות וטבעיות כדי להבין תקציב והעדפות. אל תציג מיד את בונה החבילות אם הבקשה עדיין כללית. הצג את בונה החבילות רק אחרי שיש מספיק מידע כדי להכין חבילה רלוונטית.',
				),
				'en' => array(
					'keywords'    => array( 'bundle', 'package', 'customize', 'gift set' ),
					'instruction' => 'When the user wants a personalized bundle or package, start with a short natural conversation to understand their budget and preferences. Do not show the bundle builder widget immediately if the request is still vague. Show it only after you have enough context to build a relevant bundle.',
				),
			);
		}

		if ( isset( $triggers[ $type ][ $language ] ) ) {
			return $triggers[ $type ][ $language ];
		}

		return $triggers[ $type ]['en'] ?? array(
			'keywords'    => array(),
			'instruction' => '',
		);
	}

	/**
	 * Check whether a widget type is one of the WooCommerce-driven widgets.
	 *
	 * @param string $type Widget type.
	 * @return bool
	 */
	public static function is_woocommerce_widget_type( string $type ): bool {
		return in_array(
			$type,
			array(
				self::TYPE_PRODUCT_CARD,
				self::TYPE_COUPON,
				self::TYPE_BUNDLE_BUILDER,
			),
			true
		);
	}

	/**
	 * Get a default bot description for a widget type.
	 * Used as fallback when the admin hasn't set a custom description.
	 * These are neutral, purpose-focused descriptions that fit any context.
	 *
	 * @param string $type Widget type.
	 * @return string Default description, or empty string for non-AI-triggered types.
	 */
	public static function get_default_bot_description( string $type ): string {
		$defaults = array(
			self::TYPE_FORM             => __( 'כשהלקוח מעוניין להשאיר פרטים ליצירת קשר או שיחזרו אליו', 'boostbot' ),
			self::TYPE_CALENDAR_MEETING => __( 'כשהלקוח מעוניין לקבוע או לתאם פגישה', 'boostbot' ),
			self::TYPE_CALENDAR_CALL    => __( 'כשהלקוח מעוניין לתאם שיחת טלפון', 'boostbot' ),
			self::TYPE_GALLERY          => __( 'כשהלקוח מעוניין לראות גלריית תמונות', 'boostbot' ),
			self::TYPE_WHATSAPP         => __( 'כשהלקוח מעוניין ליצור קשר בוואטסאפ', 'boostbot' ),
			self::TYPE_YOUTUBE          => __( 'כשהלקוח מעוניין לצפות בסרטון או וידאו', 'boostbot' ),
			self::TYPE_CONTACT_CARD     => __( 'כשהלקוח מעוניין לראות פרטי קשר או לדבר עם נציג', 'boostbot' ),
			self::TYPE_OPENING_SCREEN   => '', // Not AI-triggered
		);

		return $defaults[ $type ] ?? '';
	}

	/**
	 * Returns a hardcoded core description of what a widget type provides to the end user.
	 * This helps the AI understand widget capabilities beyond the admin's custom description.
	 *
	 * @param string $widget_type The widget type.
	 * @param string $language    Widget language (`he` or `en`).
	 * @return string Core description text.
	 */
	public static function get_widget_core_description( string $widget_type, string $language = 'he' ): string {
		$descriptions = array(
			self::TYPE_FORM             => array(
				'he' => 'ווידג׳ט זה מציג טופס אינטראקטיבי עם שדות כמו שם, אימייל, טלפון והודעה. כשהמשתמש ממלא ושולח את הטופס, נוצר ליד חדש במערכת ומנהל האתר מקבל התראה. השתמש בו כשהמשתמש רוצה להשאיר פרטים, ליצור קשר, לקבל הצעת מחיר, לתאם שיחה, או בכל מצב שדורש איסוף פרטי קשר.',
				'en' => 'This widget displays an interactive form with fields like name, email, phone, and message. When the user fills and submits the form, a new lead is created in the system and the site admin is notified. Use it when the user wants to leave their details, get in touch, request a quote, schedule a call, or any situation requiring contact information collection.',
			),
			self::TYPE_GALLERY          => array(
				'he' => 'ווידג׳ט זה מציג גלריית תמונות אינטראקטיבית שהמשתמש יכול לדפדף בה. השתמש בו כשהמשתמש רוצה לראות תמונות, דוגמאות ויזואליות, פורטפוליו, או תוצאות עבודה.',
				'en' => 'This widget displays an interactive image gallery that the user can browse and swipe through. Use it when the user wants to see images, visual examples, a portfolio, or work results.',
			),
			self::TYPE_CALENDAR_MEETING => array(
				'he' => 'ווידג׳ט זה מציג לוח שנה אינטראקטיבי שמאפשר למשתמש לבחור תאריך ושעה ולקבוע פגישה או שיחה. השתמש בו כשהמשתמש רוצה לתאם פגישה, לקבוע שיחת ייעוץ, או לבחור מועד.',
				'en' => 'This widget displays an interactive calendar that lets the user pick a date and time to book a meeting or call. Use it when the user wants to schedule an appointment, book a consultation, or choose a time slot.',
			),
			self::TYPE_CALENDAR_CALL    => array(
				'he' => 'ווידג׳ט זה מציג לוח שנה אינטראקטיבי שמאפשר למשתמש לבחור תאריך ושעה לתיאום שיחה. השתמש בו כשהמשתמש רוצה לקבוע שיחה, לבחור מועד לשיחת טלפון, או לתאם המשך שיחה עם העסק.',
				'en' => 'This widget displays an interactive calendar that lets the user choose a date and time for a phone call. Use it when the user wants to book a call, pick a callback time, or schedule a follow-up conversation with the business.',
			),
			self::TYPE_CONTACT_CARD     => array(
				'he' => 'ווידג׳ט זה מציג כרטיס איש קשר עם פרטים כמו שם, תפקיד, טלפון ואימייל, עם כפתורי חיוג ושליחת מייל ישירים. השתמש בו כשהמשתמש שואל מי אנשי הקשר, מבקש לדבר עם נציג, או רוצה פרטי צוות.',
				'en' => 'This widget displays a contact card with details like name, role, phone, and email, with direct call and email buttons. Use it when the user asks about contact persons, wants to speak with a representative, or requests staff details.',
			),
			self::TYPE_WHATSAPP         => array(
				'he' => 'ווידג׳ט זה מציג כפתור וואטסאפ שפותח שיחת וואטסאפ ישירה עם העסק. השתמש בו כשהמשתמש רוצה לדבר בוואטסאפ, מעדיף הודעות על שיחה, או מבקש ערוץ תקשורת ישיר.',
				'en' => 'This widget displays a WhatsApp button that opens a direct WhatsApp chat with the business. Use it when the user wants to chat on WhatsApp, prefers messaging over calling, or requests a direct communication channel.',
			),
			self::TYPE_YOUTUBE          => array(
				'he' => 'ווידג׳ט זה מציג סרטון יוטיוב מוטמע שהמשתמש יכול לצפות בו ישירות בצ׳אט. השתמש בו כשהמשתמש שואל על סרטונים, הדגמות, הדרכות, או תוכן ויזואלי.',
				'en' => 'This widget displays an embedded YouTube video that the user can watch directly in the chat. Use it when the user asks about videos, demos, tutorials, or visual content.',
			),
			self::TYPE_SURVEY           => array(
				'he' => 'ווידג׳ט זה מציג סקר אינטראקטיבי עם כמה שאלות ואפשרויות בחירה. השתמש בו כשצריך לאסוף משוב, לבצע סקר שביעות רצון, או לשאול סדרת שאלות בצורה מסודרת.',
				'en' => 'This widget displays an interactive survey with multiple questions and selectable options. Use it when you need to collect feedback, run a satisfaction survey, or ask a structured series of questions.',
			),
			self::TYPE_PRODUCT_CARD     => array(
				'he' => 'ווידג׳ט זה מציג כרטיס מוצר עם תמונה, שם, מחיר, תיאור וכפתור הוספה לעגלה. השתמש בו כשהמשתמש מחפש מוצר, רוצה לקנות, או שואל על פריט ספציפי.',
				'en' => 'This widget displays a product card with image, name, price, description, and an add-to-cart button. Use it when the user is looking for a product, wants to buy, or asks about a specific item.',
			),
			self::TYPE_COUPON           => array(
				'he' => 'ווידג׳ט זה מציג כרטיסי קופון עם קוד הנחה, סוג ההנחה, הסכום, ותאריך תפוגה, עם כפתור להעתקת הקוד. השתמש בו כשהמשתמש שואל על הנחות, מבצעים, קופונים, או מחירים מיוחדים.',
				'en' => 'This widget displays coupon cards with a discount code, discount type, amount, and expiry date, with a copy-code button. Use it when the user asks about discounts, deals, coupons, or special pricing.',
			),
			self::TYPE_BUNDLE_BUILDER   => array(
				'he' => 'ווידג׳ט זה מאפשר למשתמש ליצור חבילת מוצרים מותאמת אישית. הוא מציג תהליך שלבים לבחירת תקציב, קטגוריה והעדפות, ואז בונה חבילה מותאמת עם מוצרים והנחה. השתמש בו כשהמשתמש רוצה ליצור חבילה, מתנה, סט מוצרים, או הצעה מותאמת.',
				'en' => 'This widget lets the user create a personalized product bundle. It presents a step-by-step flow for selecting budget, category, and preferences, then builds a custom bundle with products and a discount. Use it when the user wants to create a bundle, gift, product set, or personalized offer.',
			),
		);

		if ( isset( $descriptions[ $widget_type ][ $language ] ) ) {
			return $descriptions[ $widget_type ][ $language ];
		}

		if ( isset( $descriptions[ $widget_type ]['en'] ) ) {
			return $descriptions[ $widget_type ]['en'];
		}

		return '';
	}

	/**
	 * Validate data before insert/update.
	 *
	 * @param array    $data The data to validate.
	 * @param int|null $id   Optional. The record ID for updates.
	 * @return true|WP_Error True if valid, WP_Error otherwise.
	 */
	public function validate( $data, $id = null ) {
		$errors = new WP_Error();

		// Validate name.
		if ( isset( $data['name'] ) && empty( trim( $data['name'] ) ) ) {
			$errors->add( 'name_required', __( 'שם הווידג\'ט הוא שדה חובה', 'boostbot' ) );
		}

		// Validate type on create.
		if ( null === $id && isset( $data['type'] ) ) {
			$valid_types = array_keys( self::get_widget_types() );
			if ( ! in_array( $data['type'], $valid_types, true ) ) {
				$errors->add( 'invalid_type', __( 'סוג ווידג\'ט לא תקין', 'boostbot' ) );
			}
		}

		if ( $errors->has_errors() ) {
			return $errors;
		}

		return true;
	}

	/**
	 * Sanitize data before insert/update.
	 *
	 * @param array $data The data to sanitize.
	 * @return array The sanitized data.
	 */
	public function sanitize( $data ) {
		$sanitized = array();

		if ( isset( $data['name'] ) ) {
			$sanitized['name'] = sanitize_text_field( $data['name'] );
		}

		if ( isset( $data['type'] ) ) {
			$sanitized['type'] = sanitize_key( $data['type'] );
		}

		if ( isset( $data['description'] ) ) {
			$sanitized['description'] = sanitize_textarea_field( $data['description'] );
		}

		if ( isset( $data['config'] ) ) {
			if ( is_array( $data['config'] ) ) {
				$sanitized['config'] = wp_json_encode( $data['config'] );
			} else {
				$sanitized['config'] = $data['config'];
			}
		}

		if ( isset( $data['is_active'] ) ) {
			// Handle boolean and numeric values
			$is_active_value = $data['is_active'];
			if ( is_bool( $is_active_value ) ) {
				$sanitized['is_active'] = $is_active_value ? 1 : 0;
			} else {
				$sanitized['is_active'] = absint( $is_active_value ) ? 1 : 0;
			}
		}

		if ( isset( $data['is_system_managed'] ) ) {
			$is_managed_value = $data['is_system_managed'];
			if ( is_bool( $is_managed_value ) ) {
				$sanitized['is_system_managed'] = $is_managed_value ? 1 : 0;
			} else {
				$sanitized['is_system_managed'] = absint( $is_managed_value ) ? 1 : 0;
			}
		}

		if ( isset( $data['language'] ) ) {
			$sanitized['language'] = sanitize_text_field( $data['language'] );
		}

		return $sanitized;
	}

	/**
	 * Get widgets by type.
	 *
	 * @param string $type Widget type.
	 * @return array Array of widget objects.
	 */
	public function get_by_type( $type ) {
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	return $this->wpdb->get_results(
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"SELECT * FROM {$this->table_name} WHERE type = %s AND is_active = 1 ORDER BY name ASC",
				$type
			)
		);
	}

	/**
	 * Get widgets for a specific chatbot.
	 *
	 * @param int $chatbot_id The chatbot ID.
	 * @return array Array of widget objects with custom_config.
	 */
	public function get_for_chatbot( $chatbot_id, $include_incomplete = false ) {
		$junction_table = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_chatbot_widgets' );

	// 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().
	$widgets = $this->wpdb->get_results(
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"SELECT w.*, cw.custom_config, cw.is_enabled
			 FROM {$this->table_name} w
			 INNER JOIN {$junction_table} cw ON w.id = cw.widget_id
			 WHERE cw.chatbot_id = %d AND cw.is_enabled = 1 AND w.is_active = 1
			 ORDER BY w.type, w.name",
			$chatbot_id
			)
		);

		if ( empty( $widgets ) || ! is_array( $widgets ) ) {
			return array();
		}

		// Decode configs and attach field counts for form widgets.
		foreach ( $widgets as $widget ) {
			if ( ! empty( $widget->config ) && is_string( $widget->config ) ) {
				$widget->config = json_decode( $widget->config, true );
			}
			if ( self::TYPE_FORM === $widget->type ) {
				$field_model         = new BoostBot_Form_Field_Model();
				$widget->field_count = $field_model->count( array( 'widget_id' => (int) $widget->id ) );
			}
		}

		if ( $include_incomplete ) {
			// Admin context: attach the is_fully_configured flag but keep all widgets
			// so the admin panel can show every assignment and indicate completeness.
			foreach ( $widgets as $widget ) {
				$widget->is_fully_configured = self::is_fully_configured( $widget );
			}
		} else {
			// AI / chat context: filter out widgets that are not fully configured —
			// they must not be sent to Gemini/GPT to prevent the AI from referencing
			// widgets that cannot render properly.
			$widgets = array_filter( $widgets, function ( $w ) {
				return self::is_fully_configured( $w );
			} );
		}

		return array_values( $widgets );
	}

	/**
	 * Get widgets assigned to a bot (alias for get_for_chatbot with widget_type field).
	 *
	 * @param int $bot_id The bot/chatbot ID.
	 * @return array Array of widget objects.
	 */
	public function get_by_bot( $bot_id ) {
		$junction_table = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_chatbot_widgets' );

	// 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().
	$widgets = $this->wpdb->get_results(
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"SELECT w.*, w.type as widget_type, w.name as widget_name, w.config as widget_config, cw.custom_config, cw.is_enabled
			 FROM {$this->table_name} w
			 INNER JOIN {$junction_table} cw ON w.id = cw.widget_id
			 WHERE cw.chatbot_id = %d AND cw.is_enabled = 1 AND w.is_active = 1
			 ORDER BY w.type, w.name",
			$bot_id
			)
		);

		if ( empty( $widgets ) || ! is_array( $widgets ) ) {
			return array();
		}

		// Decode configs and attach field counts for form widgets.
		foreach ( $widgets as $widget ) {
			if ( ! empty( $widget->config ) && is_string( $widget->config ) ) {
				$widget->config = json_decode( $widget->config, true );
			}
			if ( self::TYPE_FORM === ( $widget->type ?? $widget->widget_type ?? '' ) ) {
				$field_model         = new BoostBot_Form_Field_Model();
				$widget->field_count = $field_model->count( array( 'widget_id' => (int) $widget->id ) );
			}
		}

		// Filter out widgets that are not fully configured.
		$widgets = array_filter( $widgets, function ( $w ) {
			return self::is_fully_configured( $w );
		} );

		return array_values( $widgets );
	}

	/**
	 * Get widget with its form fields (for form type widgets).
	 *
	 * @param int $id Widget ID.
	 * @return object|null Widget with fields or null.
	 */
	public function get_with_fields( $id ) {
		$widget = $this->get( $id );
		if ( ! $widget ) {
			return null;
		}

		// Decode config JSON.
		if ( ! empty( $widget->config ) ) {
			$widget->config = json_decode( $widget->config, true );
		}

		// For form widgets, get fields.
		if ( self::TYPE_FORM === $widget->type ) {
			$field_model    = new BoostBot_Form_Field_Model();
			$widget->fields = $field_model->get_by_widget( $id );
		}

		// Add configuration completeness flag.
		$widget->is_fully_configured = self::is_fully_configured( $widget );

		return $widget;
	}

	/**
	 * Assign a widget to a chatbot.
	 *
	 * @param int   $widget_id     Widget ID.
	 * @param int   $chatbot_id    Chatbot ID.
	 * @param array $custom_config Optional custom config for this assignment.
	 * @return int|false Insert ID or false on failure.
	 */
	public function assign_to_chatbot( $widget_id, $chatbot_id, $custom_config = null ) {
		$table = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_chatbot_widgets' );

		// Check if assignment already exists.
		// 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().
		$exists = $this->wpdb->get_var(
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$this->wpdb->prepare(
				"SELECT id FROM {$table} WHERE chatbot_id = %d AND widget_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$chatbot_id,
				$widget_id
			)
		);

		if ( $exists ) {
			// Update existing.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$result = $this->wpdb->update(
				$table,
				array(
					'is_enabled'    => 1,
					'custom_config' => $custom_config ? wp_json_encode( $custom_config ) : null,
				),
				array( 'id' => $exists ),
				array( '%d', '%s' ),
				array( '%d' )
			);
			return false !== $result ? $exists : false;
		}

		// Create new assignment.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$result = $this->wpdb->insert(
			$table,
			array(
				'chatbot_id'    => $chatbot_id,
				'widget_id'     => $widget_id,
				'is_enabled'    => 1,
				'custom_config' => $custom_config ? wp_json_encode( $custom_config ) : null,
			),
			array( '%d', '%d', '%d', '%s' )
		);

		return false !== $result ? $this->wpdb->insert_id : false;
	}

	/**
	 * Unassign a widget from a chatbot.
	 *
	 * @param int $widget_id  Widget ID.
	 * @param int $chatbot_id Chatbot ID.
	 * @return bool True on success.
	 */
	public function unassign_from_chatbot( $widget_id, $chatbot_id ) {
		$table = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_chatbot_widgets' );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $this->wpdb->delete(
			$table,
			array(
				'chatbot_id' => $chatbot_id,
				'widget_id'  => $widget_id,
			),
			array( '%d', '%d' )
		);

		return false !== $result;
	}

	/**
	 * Get all widgets with decoded config.
	 *
	 * @param array $args Query arguments.
	 * @return array Array of widgets.
	 */
	public function get_all_with_config( $args = array() ) {
		$widgets = $this->get_all( $args );

		foreach ( $widgets as $widget ) {
			if ( ! empty( $widget->config ) ) {
				$widget->config = json_decode( $widget->config, true );
			}

			// For form widgets, get field count.
			if ( self::TYPE_FORM === $widget->type ) {
				$field_model         = new BoostBot_Form_Field_Model();
				$widget->field_count = $field_model->count( array( 'widget_id' => $widget->id ) );
			}

			// Add is_fully_configured flag.
			$widget->is_fully_configured = self::is_fully_configured( $widget );
		}

		return $widgets;
	}

	/**
	 * Create widget with fields (for form type).
	 *
	 * @param array $data Widget data with optional 'fields' array.
	 * @return int|false Widget ID or false on failure.
	 */
	public function create_with_fields( $data ) {
		$fields = isset( $data['fields'] ) ? $data['fields'] : array();
		unset( $data['fields'] );

		// Create widget.
		$widget_id = $this->create( $data );
		if ( ! $widget_id ) {
			return false;
		}

		// Create fields for form widgets.
		if ( self::TYPE_FORM === $data['type'] && ! empty( $fields ) ) {
			$field_model = new BoostBot_Form_Field_Model();
			foreach ( $fields as $index => $field_data ) {
				$field_data['widget_id']  = $widget_id;
				$field_data['sort_order'] = isset( $field_data['sort_order'] ) ? $field_data['sort_order'] : $index;
				$field_model->create( $field_data );
			}
		}

		return $widget_id;
	}

	/**
	 * Update widget with fields (for form type).
	 *
	 * @param int   $id   Widget ID.
	 * @param array $data Widget data with optional 'fields' array.
	 * @return bool True on success.
	 */
	public function update_with_fields( $id, $data ) {
		$fields = isset( $data['fields'] ) ? $data['fields'] : null;
		unset( $data['fields'] );

		// Update widget.
		$result = $this->update( $id, $data );
		if ( ! $result ) {
			return false;
		}

		// Update fields for form widgets if provided.
		$widget = $this->get( $id );
		if ( self::TYPE_FORM === $widget->type && null !== $fields ) {
			$field_model = new BoostBot_Form_Field_Model();

			// Get existing field IDs.
			$existing_fields = $field_model->get_by_widget( $id );
			$existing_ids    = array_map( function( $f ) {
				return (int) $f->id;
			}, $existing_fields );
			$updated_ids     = array();

			foreach ( $fields as $index => $field_data ) {
				$field_data['widget_id']  = $id;
				$field_data['sort_order'] = isset( $field_data['sort_order'] ) ? $field_data['sort_order'] : $index;

				if ( ! empty( $field_data['id'] ) ) {
					// Update existing field.
					$field_model->update( $field_data['id'], $field_data );
					$updated_ids[] = (int) $field_data['id'];
				} else {
					// Create new field.
					$new_id        = $field_model->create( $field_data );
					$updated_ids[] = (int) $new_id;
				}
			}

			// Delete removed fields.
			$to_delete = array_diff( $existing_ids, $updated_ids );
			foreach ( $to_delete as $field_id ) {
				$field_model->delete( $field_id );
			}
		}

		return true;
	}

	/**
	 * Delete widget and its fields.
	 *
	 * @param int $id Widget ID.
	 * @return bool True on success.
	 */
	public function delete_with_fields( $id ) {
		$widget = $this->get( $id );
		if ( ! $widget ) {
			return false;
		}

		// Delete fields first (for form widgets).
		if ( self::TYPE_FORM === $widget->type ) {
			$field_model = new BoostBot_Form_Field_Model();
			$fields      = $field_model->get_by_widget( $id );
			foreach ( $fields as $field ) {
				$field_model->delete( $field->id );
			}
		}

		// Delete chatbot assignments.
		$junction_table = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_chatbot_widgets' );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$this->wpdb->delete(
			$junction_table,
			array( 'widget_id' => $id ),
			array( '%d' )
		);

		// Delete widget.
		return $this->delete( $id );
	}

	/**
	 * Increment widget usage count (total).
	 *
	 * @param int $id Widget ID.
	 * @return bool True on success.
	 */
	public function increment_usage( $id ) {
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	return false !== $this->wpdb->query(
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"UPDATE {$this->table_name} SET usage_count = usage_count + 1 WHERE id = %d",
				$id
			)
		);
	}

	/**
	 * Track widget usage by a specific bot.
	 *
	 * @param int    $widget_id Widget ID.
	 * @param int    $bot_id    Bot ID.
	 * @param string $language  Language code (default 'he').
	 * @return bool True on success.
	 */
	public function track_usage( $widget_id, $bot_id, $language = '' ) {
		if ( '' === $language ) {
			$language = BoostBot_Language::get_primary();
		}

		$table = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_widget_usage' );

		// Try to update existing record first
	// 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().
	$updated = $this->wpdb->query(
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"UPDATE {$table} SET usage_count = usage_count + 1, last_used_at = NOW(), language = %s
			 WHERE widget_id = %d AND bot_id = %d",
				$language,
				$widget_id,
				$bot_id
			)
		);

		// If no rows updated, insert new record
		if ( 0 === $updated ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			$this->wpdb->insert(
				$table,
				array(
					'language'    => $language,
					'widget_id'   => $widget_id,
					'bot_id'      => $bot_id,
					'usage_count' => 1,
				),
				array( '%s', '%d', '%d', '%d' )
			);
		}

		// Also increment total usage
		$this->increment_usage( $widget_id );

		return true;
	}

	/**
	 * Get usage statistics for a widget.
	 *
	 * @param int $widget_id Widget ID.
	 * @return array Usage stats with total and per-bot breakdown.
	 */
	public function get_usage_stats( $widget_id ) {
		$usage_table = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_widget_usage' );
		$bots_table  = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_bots' );

		// Get widget total usage
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	$widget = $this->wpdb->get_row(
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"SELECT usage_count FROM {$this->table_name} WHERE id = %d",
				$widget_id
			)
		);

		// Get per-bot usage
		// 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().
		$per_bot = $this->wpdb->get_results(
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$this->wpdb->prepare(
				"SELECT u.bot_id, b.bot_name, u.usage_count, u.last_used_at
				 FROM {$usage_table} u
				 LEFT JOIN {$bots_table} b ON u.bot_id = b.id
				 WHERE u.widget_id = %d
				 ORDER BY u.usage_count DESC",
				$widget_id
			)
		);

		return array(
			'total_usage' => $widget ? (int) $widget->usage_count : 0,
			'per_bot'     => $per_bot,
		);
	}

	/**
	 * Check if a widget is fully configured (has all required data to render).
	 *
	 * A widget that is not fully configured should not be sent to the AI
	 * and should not be toggleable in the admin panel.
	 *
	 * @param object|array $widget Widget object (must include type, config, and optionally field_count/fields).
	 * @return bool True if the widget has all minimally required data.
	 */
	public static function is_fully_configured( $widget ): bool {
		$widget = (object) $widget;
		$type   = $widget->type ?? '';
		$config = $widget->config ?? null;

		// Decode config if it's a JSON string.
		if ( is_string( $config ) && '' !== $config ) {
			$config = json_decode( $config, true );
		}

		if ( ! is_array( $config ) ) {
			$config = array();
		}

		switch ( $type ) {
			case self::TYPE_FORM:
				// Form must have at least 1 field.
				$field_count = 0;
				if ( isset( $widget->field_count ) ) {
					$field_count = (int) $widget->field_count;
				} elseif ( ! empty( $widget->fields ) && is_array( $widget->fields ) ) {
					$field_count = count( $widget->fields );
				} else {
					// Query field count from DB if not pre-loaded.
					if ( ! empty( $widget->id ) && class_exists( 'BoostBot_Form_Field_Model' ) ) {
						$field_model = new BoostBot_Form_Field_Model();
						$field_count = $field_model->count( array( 'widget_id' => (int) $widget->id ) );
					}
				}
				return $field_count > 0;

			case self::TYPE_WHATSAPP:
				// Must have at least 1 contact with name and phone.
				$contacts = $config['contacts'] ?? array();
				if ( empty( $contacts ) || ! is_array( $contacts ) ) {
					return false;
				}
				foreach ( $contacts as $contact ) {
					if ( ! empty( $contact['name'] ) && ! empty( $contact['phone'] ) ) {
						return true;
					}
				}
				return false;

			case self::TYPE_CALENDAR_MEETING:
			case self::TYPE_CALENDAR_CALL:
				// Must have a title. Duration is now per-contact (via meeting_availability),
				// so duration_options is no longer required.
				return ! empty( $config['title'] );

			case self::TYPE_GALLERY:
				// Must have at least 1 image with a URL.
				$images = $config['images'] ?? array();
				if ( empty( $images ) || ! is_array( $images ) ) {
					return false;
				}
				foreach ( $images as $image ) {
					if ( ! empty( $image['url'] ) ) {
						return true;
					}
				}
				return false;

			case self::TYPE_YOUTUBE:
				// Must have a video_url.
				return ! empty( $config['video_url'] );

			case self::TYPE_CONTACT_CARD:
				// Must have at least 1 contact list ID.
				$ids = $config['contact_list_ids'] ?? array();
				return ! empty( $ids ) && is_array( $ids ) && count( $ids ) > 0;

			case self::TYPE_OPENING_SCREEN:
				// Opening screen is always considered configured if it exists.
				return true;

			case self::TYPE_PRODUCT_CARD:
			case self::TYPE_COUPON:
			case self::TYPE_BUNDLE_BUILDER:
				// WooCommerce widgets - assume handled by their respective models/logic
				// for now as they are being revived.
				return true;

			default:
				// Unknown type — assume configured.
				return true;
		}
	}

	/**
	 * Get linked bots count for a widget.
	 *
	 * @param int $widget_id Widget ID.
	 * @return int Number of linked bots.
	 */
	public function get_linked_bots_count( $widget_id ) {
		$junction_table = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_chatbot_widgets' );

	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Identifier sanitized via sanitize_identifier() at assignment site.
	return (int) $this->wpdb->get_var(
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"SELECT COUNT(DISTINCT chatbot_id) FROM {$junction_table} WHERE widget_id = %d",
			$widget_id
			)
		);
	}

	/**
	 * Get all widgets with linked bots count.
	 *
	 * @param array $args Query arguments.
	 * @return array Array of widgets with linked_bots_count.
	 */
	public function get_all_with_stats( $args = array() ) {
		$widgets = $this->get_all_with_config( $args );
		$junction_table = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_chatbot_widgets' );

		foreach ( $widgets as $widget ) {
		// 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().
		$widget->linked_bots_count = (int) $this->wpdb->get_var(
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$this->wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT COUNT(DISTINCT chatbot_id) FROM {$junction_table} WHERE widget_id = %d",
					$widget->id
				)
			);
		}

		return $widgets;
	}

    /**
     * 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.
     */
    protected static function sanitize_identifier( $identifier ) {
        return preg_replace( '/[^A-Za-z0-9_]/', '', (string) $identifier );
    }
}
