<?php
/**
 * Store Index Profile Manager
 *
 * Handles CRUD operations for Store Index profiles.
 *
 * @package AIPT\Api\StoreIndex
 * @since 2.5.0
 */

namespace AIPT\Api\StoreIndex;

/**
 * ProfileManager class
 *
 * Manages Store Index profiles including creation, retrieval,
 * updates, deletion, and active profile selection.
 */
class ProfileManager {

	/**
	 * Table name for profiles.
	 *
	 * @var string
	 */
	private $table_name;

	/**
	 * Mandatory fields that are always included and cannot be disabled.
	 *
	 * These fields are essential for AI index functionality:
	 * - title: Required for product identification
	 * - categories: Required for category-based filtering and navigation
	 * - price: Required for product context and recommendations
	 *
	 * @var array
	 */
	public const MANDATORY_FIELDS = array( 'title', 'categories', 'price' );

	/**
	 * Default included fields for new profiles.
	 * Includes mandatory fields plus commonly useful optional fields.
	 * short_description is included by default but can be removed by user.
	 * SKU is included by default because 7,900+ catalog stores routinely
	 * surface products via SKU/model lookups in chat ("do you have
	 * SKU-2024-AB?") and IndexConsumer's SKU scoring branch is dead
	 * weight without the field actually living in the indexed profile.
	 *
	 * @var array
	 */
	private const DEFAULT_INCLUDED_FIELDS = array( 'sku', 'title', 'short_description', 'categories', 'price', 'stock_status' );

	/**
	 * Fields available to free users.
	 * Premium users get access to all AVAILABLE_FIELDS via filter hook.
	 * SKU stays in the free tier because it is a fundamental product
	 * identifier, not a premium-only enrichment field; restricting it
	 * would break SKU search for free installs.
	 *
	 * @var array
	 */
	public const FREE_TIER_FIELDS = array( 'sku', 'title', 'short_description', 'categories', 'price', 'stock_status' );

	/**
	 * All available fields that can be included in a profile.
	 *
	 * Note: Media/images are intentionally excluded to keep indexes lightweight.
	 *
	 * @var array
	 */
	public const AVAILABLE_FIELDS = array(
		'sku',
		'title',
		'short_description',
		'description',
		'price',
		'stock_status',
		'stock_quantity',
		'categories',
		'tags',
		'attributes',
	);

	/**
	 * Maximum length for profile names.
	 *
	 * @var int
	 */
	private const MAX_NAME_LENGTH = 50;

	/**
	 * Option flag set after the one-time SKU back-fill has completed so the
	 * hook is a no-op on every subsequent admin_init.
	 *
	 * @var string
	 */
	private const SKU_BACKFILL_OPTION = 'aipt_storeindex_sku_default_migrated_v1';

	/**
	 * Constructor.
	 */
	public function __construct() {
		global $wpdb;
		$this->table_name = $wpdb->prefix . 'aipt_store_index_profiles';
	}

	/**
	 * Back-fill `'sku'` into every existing profile's `included_fields`.
	 *
	 * Adding `'sku'` to DEFAULT_INCLUDED_FIELDS / FREE_TIER_FIELDS only
	 * affects NEW profiles; pre-existing rows in `aipt_store_index_profiles`
	 * carry the persisted `included_fields` array from before the SKU
	 * default landed, so SKU search stays broken on those installs even
	 * after a fresh index build until an admin manually re-checks SKU in
	 * every profile. Run once, gated by self::SKU_BACKFILL_OPTION so the
	 * admin_init hook is a no-op afterwards. Safe to re-run: rows that
	 * already include `'sku'` are skipped.
	 *
	 * @return int Number of profile rows mutated.
	 */
	public static function migrateExistingProfilesToIncludeSku(): int {
		if ( \get_option( self::SKU_BACKFILL_OPTION ) ) {
			return 0;
		}

		global $wpdb;
		$table = $wpdb->prefix . 'aipt_store_index_profiles';

		$rows = $wpdb->get_results( "SELECT id, settings FROM {$table}", ARRAY_A );

		if ( ! is_array( $rows ) ) {
			return 0;
		}

		$mutated  = 0;
		$expected = 0;
		$failed   = false;

		foreach ( $rows as $row ) {
			$id       = isset( $row['id'] ) ? (int) $row['id'] : 0;
			$settings = isset( $row['settings'] ) ? json_decode( (string) $row['settings'], true ) : null;

			if ( $id <= 0 || ! is_array( $settings ) ) {
				continue;
			}

			$included = $settings['included_fields'] ?? null;
			if ( ! is_array( $included ) ) {
				continue;
			}

			if ( in_array( 'sku', $included, true ) ) {
				continue;
			}

			$expected++;

			$settings['included_fields'] = array_values( array_merge( [ 'sku' ], $included ) );

			$updated = $wpdb->update(
				$table,
				[ 'settings' => \wp_json_encode( $settings ) ],
				[ 'id' => $id ],
				[ '%s' ],
				[ '%d' ]
			);

			if ( false === $updated ) {
				$failed = true;
				continue;
			}

			if ( $updated > 0 ) {
				$mutated++;
			}
		}

		if ( ! $failed && $mutated === $expected ) {
			\update_option( self::SKU_BACKFILL_OPTION, 1, false );
		}

		return $mutated;
	}

	/**
	 * Get all profiles.
	 *
	 * @return array List of all profiles.
	 */
	public function getAll(): array {
		global $wpdb;

		$sql = "SELECT * FROM {$this->table_name} ORDER BY is_default DESC, name ASC";

		$profiles = $wpdb->get_results($sql, ARRAY_A);

		if ( ! is_array( $profiles ) ) {
			if ( $wpdb->last_error ) {
			}
			return array();
		}

		return array_map( array( $this, 'formatProfile' ), $profiles );
	}

	/**
	 * Get a profile by ID.
	 *
	 * @param int $id Profile ID.
	 * @return array|null Profile data or null if not found.
	 */
	public function getById( int $id ): ?array {
		global $wpdb;

		$sql = "SELECT * FROM {$this->table_name} WHERE id = %d";

		$profile = $wpdb->get_row($wpdb->prepare($sql, $id), ARRAY_A);

		if ( ! $profile ) {
			return null;
		}

		return $this->formatProfile( $profile );
	}

	/**
	 * Get the default profile.
	 *
	 * @return array|null Default profile data or null if not found.
	 */
	public function getDefault(): ?array {
		global $wpdb;

		$sql = "SELECT * FROM {$this->table_name} WHERE is_default = %d";

		$profile = $wpdb->get_row($wpdb->prepare($sql, 1), ARRAY_A);

		if ( ! $profile ) {
			return null;
		}

		return $this->formatProfile( $profile );
	}

	/**
	 * Get the currently active profile.
	 *
	 * @return array|null Active profile data or null if not found.
	 */
	public function getActive(): ?array {
		global $wpdb;

		$sql = "SELECT * FROM {$this->table_name} WHERE is_active = %d";

		$profile = $wpdb->get_row($wpdb->prepare($sql, 1), ARRAY_A);

		if ( ! $profile ) {
			return $this->getDefault();
		}

		return $this->formatProfile( $profile );
	}

	/**
	 * Get the active profile ID.
	 *
	 * @return int Active profile ID or 0 if not found.
	 */
	public function getActiveId(): int {
		$active = $this->getActive();
		return $active ? (int) $active['id'] : 0;
	}

	/**
	 * Create a new profile.
	 *
	 * Uses DB-level UNIQUE constraint on name column to prevent race conditions.
	 * The pre-check is kept for better UX (immediate feedback) but the real
	 * protection comes from catching the duplicate key error from the DB.
	 *
	 * @param string $name Profile name.
	 * @return int New profile ID.
	 * @throws \InvalidArgumentException If name is invalid or already exists.
	 * @throws \RuntimeException If database insert fails for other reasons.
	 */
	public function create( string $name ): int {
		global $wpdb;

		$can_create = apply_filters( 'aipt_index_can_create_profile', false );
		if ( ! $can_create ) {

			throw new \InvalidArgumentException( __( 'Creating new profiles requires a premium plan.', 'ai-product-tools' ) );
		}

		$this->validateName( $name );

		$sql = "SELECT id FROM {$this->table_name} WHERE name = %s";

		$exists = $wpdb->get_var($wpdb->prepare($sql, $name));

		if ( $exists ) {

			throw new \InvalidArgumentException( __( 'A profile with this name already exists.', 'ai-product-tools' ) );
		}

		$settings = array(
			'enabled'            => false,
			'max_products'       => 10000,
			'batch_size'         => 100,
			'categories'         => array(),
			'tags'               => array(),
			'stock_filter'       => 'all',
			'date_filter_days'   => 0,
			'filter_mode'        => 'all',
			'scheduled_enabled'  => false,
			'schedule_frequency' => 'daily',
			'included_fields'    => self::DEFAULT_INCLUDED_FIELDS,
		);

		$status = array(
			'status'                   => 'idle',
			'last_build'               => null,
			'product_count'            => 0,
			'category_count'           => 0,
			'file_size'                => 0,
			'trimmed_count'            => 0,

			'shard_count'              => 0,
			'error'                    => null,
			'build_progress'           => 0,
			'progress_percent'         => 0,
			'processed_product_count'  => 0,
			'estimated_total_products' => 0,
			'build_job_id'             => 0,
		);

		$suppress = $wpdb->suppress_errors( true );

		$result = $wpdb->insert(
			$this->table_name,
			array(
				'name'       => $name,
				'is_default' => 0,
				'is_active'  => 0,
				'settings'   => wp_json_encode( $settings ),
				'status'     => wp_json_encode( $status ),
				'created_at' => current_time( 'mysql' ),
			),
			array( '%s', '%d', '%d', '%s', '%s', '%s' )
		);

		$wpdb->suppress_errors( $suppress );

		if ( false === $result ) {
			$last_error = $wpdb->last_error;

			if ( strpos( $last_error, 'Duplicate entry' ) !== false || strpos( $last_error, '1062' ) !== false ) {

				throw new \InvalidArgumentException( __( 'A profile with this name already exists.', 'ai-product-tools' ) );
			}

			throw new \RuntimeException(
				sprintf(
					/* translators: %s: database error message */
					__( 'Failed to create profile: %s', 'ai-product-tools' ), 
					$last_error ?: __( 'Unknown database error', 'ai-product-tools' ) 
				)
			);
		}

		$new_id = (int) $wpdb->insert_id;

		$this->setActive( $new_id );

		return $new_id;
	}

	/**
	 * Update a profile's settings.
	 *
	 * @param int   $id   Profile ID.
	 * @param array $data Data to update (settings and/or name).
	 * @return bool True on success.
	 * @throws \InvalidArgumentException If profile not found or name invalid.
	 */
	public function update( int $id, array $data ): bool {
		global $wpdb;

		$profile = $this->getById( $id );
		if ( ! $profile ) {

			throw new \InvalidArgumentException( __( 'Profile not found.', 'ai-product-tools' ) );
		}

		$update_data   = array();
		$update_format = array();

		if ( isset( $data['name'] ) ) {
			$name = $data['name'];
			$this->validateName( $name );

			$sql = "SELECT id FROM {$this->table_name} WHERE name = %s AND id != %d";

			$exists = $wpdb->get_var($wpdb->prepare($sql, $name, $id));

			if ( $exists ) {

				throw new \InvalidArgumentException( __( 'A profile with this name already exists.', 'ai-product-tools' ) );
			}

			$update_data['name'] = $name;
			$update_format[]     = '%s';
		}

		if ( isset( $data['settings'] ) && is_array( $data['settings'] ) ) {
			$current_settings = $profile['settings'];
			$new_settings     = array_merge( $current_settings, $data['settings'] );

			if ( isset( $new_settings['included_fields'] ) ) {

				$new_settings['included_fields'] = array_values(
					array_intersect(
						$new_settings['included_fields'],
						self::AVAILABLE_FIELDS
					)
				);

				$new_settings['included_fields'] = self::ensureMandatoryFields(
					$new_settings['included_fields']
				);
			}

			$update_data['settings'] = wp_json_encode( $new_settings );
			$update_format[]         = '%s';
		}

		if ( empty( $update_data ) ) {
			return true; 
		}

		$update_data['updated_at'] = current_time( 'mysql' );
		$update_format[]           = '%s';

		$suppress = $wpdb->suppress_errors( true );

		$result = $wpdb->update(
			$this->table_name,
			$update_data,
			array( 'id' => $id ),
			$update_format,
			array( '%d' )
		);

		$wpdb->suppress_errors( $suppress );

		if ( false === $result ) {
			$last_error = $wpdb->last_error;

			if ( strpos( $last_error, 'Duplicate entry' ) !== false || strpos( $last_error, '1062' ) !== false ) {

				throw new \InvalidArgumentException( __( 'A profile with this name already exists.', 'ai-product-tools' ) );
			}

			throw new \RuntimeException(
				sprintf(
					/* translators: %1$d: profile ID, %2$s: database error message */
					__( 'Failed to update profile %1$d: %2$s', 'ai-product-tools' ), 
					$id, 
					$last_error ?: __( 'Unknown database error', 'ai-product-tools' ) 
				)
			);
		}

		return true;
	}

	/**
	 * Update a profile's status.
	 *
	 * @param int   $id     Profile ID.
	 * @param array $status Status data to update.
	 * @return bool True on success.
	 */
	public function updateStatus( int $id, array $status ): bool {
		global $wpdb;

		$profile = $this->getById( $id );
		if ( ! $profile ) {
			return false;
		}

		$current_status = $profile['status'];
		$new_status     = array_merge( $current_status, $status );

		$result = $wpdb->update(
			$this->table_name,
			array(
				'status'     => wp_json_encode( $new_status ),
				'updated_at' => current_time( 'mysql' ),
			),
			array( 'id' => $id ),
			array( '%s', '%s' ),
			array( '%d' )
		);

		return $result !== false;
	}

	/**
	 * Delete a profile.
	 *
	 * Operation order is important for transactional integrity:
	 * 1. Validate profile exists and is not default
	 * 2. Delete from database first (critical operation)
	 * 3. Switch active profile if needed (after successful DB deletion)
	 * 4. Delete index file last (non-critical, logged if fails)
	 *
	 * @param int $id Profile ID.
	 * @return bool True on success.
	 * @throws \InvalidArgumentException If profile is default or not found.
	 */
	public function delete( int $id ): bool {
		global $wpdb;

		$profile = $this->getById( $id );
		if ( ! $profile ) {

			throw new \InvalidArgumentException( __( 'Profile not found.', 'ai-product-tools' ) );
		}

		if ( $profile['is_default'] ) {

			throw new \InvalidArgumentException( __( 'Cannot delete the default profile.', 'ai-product-tools' ) );
		}

		$was_active = $profile['is_active'];

		$result = $wpdb->delete(
			$this->table_name,
			array( 'id' => $id ),
			array( '%d' )
		);

		if ( false === $result ) {
			return false;
		}

		if ( $was_active ) {
			$default = $this->getDefault();
			if ( $default ) {
				try {
					$this->setActive( (int) $default['id'] );
				} catch ( \InvalidArgumentException $e ) {

					error_log(
						sprintf(
							'[AIPT] ProfileManager: Failed to activate default profile after deleting profile %d: %s',
							$id,
							$e->getMessage()
						)
					);
				}
			}
		}

		$this->cleanupChatbotProfileReference( $id );

		$storage = new IndexStorage( $id );
		$storage->delete();

		return true;
	}

	/**
	 * Set a profile as the active profile.
	 *
	 * Only one profile can be active at a time. Uses a single atomic UPDATE
	 * statement to prevent race conditions that could leave no profile active.
	 *
	 * Note: No pre-check for profile existence to avoid TOCTOU race conditions.
	 * Instead, we verify the target profile was actually updated by checking
	 * that the target row's is_active value changed.
	 *
	 * @param int $id Profile ID.
	 * @return bool True on success.
	 * @throws \InvalidArgumentException If profile not found (detected after UPDATE).
	 */
	public function setActive( int $id ): bool {
		global $wpdb;

		$current_time = current_time( 'mysql' );

		$sql = "UPDATE {$this->table_name} SET is_active = CASE WHEN id = %d THEN 1 ELSE 0 END, updated_at = CASE WHEN id = %d THEN %s ELSE updated_at END";

		$result = $wpdb->query($wpdb->prepare($sql, $id, $id, $current_time));

		if ( false === $result ) {
			return false;
		}

		$verify_sql = "SELECT id FROM {$this->table_name} WHERE id = %d AND is_active = 1";

		$activated = $wpdb->get_var($wpdb->prepare($verify_sql, $id));

		if ( ! $activated ) {

			throw new \InvalidArgumentException( __( 'Profile not found.', 'ai-product-tools' ) );
		}

		return true;
	}

	/**
	 * Get global building status.
	 *
	 * Checks all profiles for any ongoing builds.
	 *
	 * @return array Building status information.
	 */
	public function getGlobalBuildingStatus(): array {
		$profiles          = $this->getAll();
		$building_profiles = array();

		foreach ( $profiles as $profile ) {
			$status = $profile['status'];
			if ( isset( $status['status'] ) && 'building' === $status['status'] ) {
				$building_profiles[] = array(
					'id'               => $profile['id'],
					'name'             => $profile['name'],
					'progress_percent' => $status['progress_percent'] ?? 0,
				);
			}
		}

		return array(
			'any_building'      => ! empty( $building_profiles ),
			'building_profiles' => $building_profiles,
		);
	}

	/**
	 * Ensure default profile exists.
	 *
	 * Creates a default profile if none exists.
	 *
	 * @return void
	 * @throws \RuntimeException If database insert fails.
	 */
	public function ensureDefaultExists(): void {
		$default = $this->getDefault();
		if ( $default ) {
			return;
		}

		global $wpdb;

		$settings = array(
			'enabled'            => false,
			'max_products'       => 10000,
			'batch_size'         => 100,
			'categories'         => array(),
			'tags'               => array(),
			'stock_filter'       => 'all',
			'date_filter_days'   => 0,
			'filter_mode'        => 'all',
			'scheduled_enabled'  => false,
			'schedule_frequency' => 'daily',
			'included_fields'    => self::DEFAULT_INCLUDED_FIELDS,
		);

		$status = array(
			'status'                   => 'idle',
			'last_build'               => null,
			'product_count'            => 0,
			'category_count'           => 0,
			'file_size'                => 0,
			'trimmed_count'            => 0,

			'shard_count'              => 0,
			'error'                    => null,
			'build_progress'           => 0,
			'progress_percent'         => 0,
			'processed_product_count'  => 0,
			'estimated_total_products' => 0,
			'build_job_id'             => 0,
		);

		$suppress = $wpdb->suppress_errors( true );

		$result = $wpdb->insert(
			$this->table_name,
			array(
				'name'       => 'Default',
				'is_default' => 1,
				'is_active'  => 1,
				'settings'   => wp_json_encode( $settings ),
				'status'     => wp_json_encode( $status ),
				'created_at' => current_time( 'mysql' ),
			),
			array( '%s', '%d', '%d', '%s', '%s', '%s' )
		);

		$wpdb->suppress_errors( $suppress );

		if ( false === $result ) {
			$last_error = $wpdb->last_error;

			if ( strpos( $last_error, 'Duplicate entry' ) !== false || strpos( $last_error, '1062' ) !== false ) {
				return; 
			}

			error_log(
				sprintf(
					'[AIPT] Failed to create default Store Index profile: %s',
					$last_error ?: 'Unknown database error'
				)
			);
		}
	}

	/**
	 * Validate a profile name.
	 *
	 * @param string $name Profile name.
	 * @return void
	 * @throws \InvalidArgumentException If name is invalid.
	 */
	private function validateName( string $name ): void {
		if ( empty( $name ) ) {

			throw new \InvalidArgumentException( __( 'Profile name cannot be empty.', 'ai-product-tools' ) );
		}

		if ( strlen( $name ) > self::MAX_NAME_LENGTH ) {
			throw new \InvalidArgumentException(
				sprintf(
					/* translators: %d: maximum name length */
					__( 'Profile name cannot exceed %d characters.', 'ai-product-tools' ), 
					self::MAX_NAME_LENGTH 
				)
			);
		}

		if ( ! preg_match( '/^[a-zA-Z0-9 \-]+$/', $name ) ) {
			throw new \InvalidArgumentException(
				__( 'Profile name can only contain letters, numbers, spaces, and dashes.', 'ai-product-tools' ) 
			);
		}
	}
	/**
	 * Format a profile row from database.
	 *
	 * @param array $row Database row.
	 * @return array Formatted profile data.
	 */
	private function formatProfile( array $row ): array {
		return array(
			'id'         => (int) $row['id'],
			'name'       => $row['name'],
			'is_default' => (bool) $row['is_default'],
			'is_active'  => (bool) $row['is_active'],
			'settings'   => json_decode( $row['settings'], true ) ?? array(),
			'status'     => json_decode( $row['status'], true ) ?? array(),
			'created_at' => $row['created_at'],
			'updated_at' => $row['updated_at'],
		);
	}

	/**
	 * Get default included fields for new profiles.
	 *
	 * @return array Default fields.
	 */
	public static function getDefaultIncludedFields(): array {
		return self::DEFAULT_INCLUDED_FIELDS;
	}

	/**
	 * Get all available fields.
	 *
	 * @return array Available fields.
	 */
	public static function getAvailableFields(): array {
		return self::AVAILABLE_FIELDS;
	}

	/**
	 * Get available fields for the current user's plan.
	 *
	 * Uses filter hook to allow premium code to expand available fields.
	 * Free users: limited fields (default), Premium users: all fields (via filter).
	 *
	 * SECURITY NOTE: Default MUST be restrictive (free tier fields) because
	 * when premium code is stripped from free build, filters return their defaults.
	 *
	 * @return array Available fields for current plan.
	 */
	public static function getAvailableFieldsForPlan(): array {
		return apply_filters( 'aipt_index_available_fields_for_plan', self::FREE_TIER_FIELDS );
	}

	/**
	 * Get premium-only fields list.
	 *
	 * Uses filter hook to get premium-only fields from premium code.
	 *
	 * @return array Premium-only fields.
	 */
	public static function getPremiumOnlyFields(): array {
		return apply_filters( 'aipt_index_premium_only_fields', array() );
	}

	/**
	 * Check if user can filter by categories.
	 *
	 * Uses filter hook to check premium capability.
	 * Free users: false (default), Premium users: true (via filter).
	 *
	 * SECURITY NOTE: Default MUST be false because when premium code is
	 * stripped from free build, filters return their defaults.
	 *
	 * @return bool True if category filtering allowed.
	 */
	public static function canFilterCategories(): bool {
		return apply_filters( 'aipt_index_can_filter_categories', false );
	}

	/**
	 * Check if user can filter by tags.
	 *
	 * Uses filter hook to check premium capability.
	 * Free users: false (default), Premium users: true (via filter).
	 *
	 * SECURITY NOTE: Default MUST be false because when premium code is
	 * stripped from free build, filters return their defaults.
	 *
	 * @return bool True if tag filtering allowed.
	 */
	public static function canFilterTags(): bool {
		return apply_filters( 'aipt_index_can_filter_tags', false );
	}

	/**
	 * Check if user can create new profiles.
	 *
	 * Uses filter hook to check premium capability.
	 * Free users: false (default), Premium users: true (via filter).
	 *
	 * SECURITY NOTE: Default MUST be false because when premium code is
	 * stripped from free build, filters return their defaults.
	 *
	 * @return bool True if profile creation allowed.
	 */
	public static function canCreateProfiles(): bool {
		return apply_filters( 'aipt_index_can_create_profile', false );
	}

	/**
	 * Get mandatory fields that cannot be disabled.
	 *
	 * @return array Mandatory fields.
	 */
	public static function getMandatoryFields(): array {
		return self::MANDATORY_FIELDS;
	}

	/**
	 * Ensure mandatory fields are included in a field list.
	 *
	 * @param array $fields Field list to check.
	 * @return array Field list with mandatory fields included.
	 */
	public static function ensureMandatoryFields( array $fields ): array {
		return array_values( array_unique( array_merge( self::MANDATORY_FIELDS, $fields ) ) );
	}

	/**
	 * Global settings keys (shared across all profiles).
	 *
	 * @var array
	 */
	private const GLOBAL_SETTINGS_KEYS = array(
		'enabled',
		'max_products',
		'batch_size',
		'scheduled_enabled',
		'schedule_frequency',
	);

	/**
	 * Get global settings from default profile.
	 *
	 * Global settings are stored in the default profile and apply to all profiles.
	 *
	 * @return array Global settings.
	 */
	public function getGlobalSettings(): array {
		$default = $this->getDefault();

		if ( ! $default ) {

			return array(
				'enabled'            => false,
				'max_products'       => 10000,
				'batch_size'         => 100,
				'scheduled_enabled'  => false,
				'schedule_frequency' => 'daily',
			);
		}

		$settings = $default['settings'];

		$global = array();
		foreach ( self::GLOBAL_SETTINGS_KEYS as $key ) {
			$global[ $key ] = $settings[ $key ] ?? $this->getGlobalDefault( $key );
		}

		return $global;
	}

	/**
	 * Update global settings in default profile.
	 *
	 * @param array $settings Global settings to update.
	 * @return bool True on success.
	 * @throws \RuntimeException If default profile not found.
	 */
	public function updateGlobalSettings( array $settings ): bool {
		$default = $this->getDefault();

		if ( ! $default ) {

			throw new \RuntimeException( __( 'Default profile not found.', 'ai-product-tools' ) );
		}

		$global_updates = array();
		foreach ( self::GLOBAL_SETTINGS_KEYS as $key ) {
			if ( array_key_exists( $key, $settings ) ) {
				$global_updates[ $key ] = $this->sanitizeGlobalSetting( $key, $settings[ $key ] );
			}
		}

		if ( empty( $global_updates ) ) {
			return true; 
		}

		$current_settings = $default['settings'];
		$new_settings     = array_merge( $current_settings, $global_updates );

		global $wpdb;

		$result = $wpdb->update(
			$this->table_name,
			array(
				'settings'   => wp_json_encode( $new_settings ),
				'updated_at' => current_time( 'mysql' ),
			),
			array( 'id' => $default['id'] ),
			array( '%s', '%s' ),
			array( '%d' )
		);

		return false !== $result;
	}

	/**
	 * Get default value for a global setting.
	 *
	 * @param string $key Setting key.
	 * @return mixed Default value.
	 */
	private function getGlobalDefault( string $key ) {
		$defaults = array(
			'enabled'            => false,
			'max_products'       => 10000,
			'batch_size'         => 100,
			'scheduled_enabled'  => false,
			'schedule_frequency' => 'daily',
		);

		return $defaults[ $key ] ?? null;
	}

	/**
	 * Sanitize a global setting value.
	 *
	 * @param string $key   Setting key.
	 * @param mixed  $value Setting value.
	 * @return mixed Sanitized value.
	 */
	private function sanitizeGlobalSetting( string $key, $value ) {
		switch ( $key ) {
			case 'enabled':
			case 'scheduled_enabled':
				return (bool) $value;

			case 'max_products':
				$value = (int) $value;
				return max( 1, min( 50000, $value ) );

			case 'batch_size':
				$value = (int) $value;
				return max( 50, min( 500, $value ) );

			case 'schedule_frequency':
				$valid = array( 'hourly', 'two_hourly', 'six_hourly', 'daily' );
				return in_array( $value, $valid, true ) ? $value : 'daily';

			default:
				return $value;
		}
	}

	/**
	 * Get global settings keys.
	 *
	 * @return array Global settings keys.
	 */
	public static function getGlobalSettingsKeys(): array {
		return self::GLOBAL_SETTINGS_KEYS;
	}
	/**
	 * Clean up chatbot settings if they reference a deleted profile.
	 *
	 * Resets the chatbot's index_profile setting to the default profile if it
	 * was pointing to the deleted profile, ensuring a valid profile is always set.
	 *
	 * @param int $deleted_profile_id The ID of the profile being deleted.
	 * @return void
	 */
	private function cleanupChatbotProfileReference( int $deleted_profile_id ): void {
		$option_key = 'aipt_chatbot_settings';
		$settings   = get_option( $option_key, array() );

		if (
			isset( $settings['behavior']['index_profile'] ) &&
			(int) $settings['behavior']['index_profile'] === $deleted_profile_id
		) {

			$default_profile = $this->getDefault();

			if ( $default_profile ) {
				$settings['behavior']['index_profile'] = (int) $default_profile['id'];
				update_option( $option_key, $settings );
			} else {

			}
		}
	}
}
