<?php
/**
 * Admin AJAX handlers.
 *
 * @package TrustLens\Admin
 * @since   1.1.5
 */

defined( 'ABSPATH' ) || exit;

/**
 * Handles TrustLens admin AJAX endpoints.
 *
 * @since 1.1.5
 */
class TrustLens_Admin_Ajax {

	/**
	 * Register hooks.
	 *
	 * @since 1.1.5
	 * @return void
	 */
	public function register_hooks(): void {
		add_action( 'wp_ajax_wstl_block_customer', array( $this, 'ajax_block_customer' ) );
		add_action( 'wp_ajax_wstl_unblock_customer', array( $this, 'ajax_unblock_customer' ) );
		add_action( 'wp_ajax_wstl_allowlist_customer', array( $this, 'ajax_allowlist_customer' ) );
		add_action( 'wp_ajax_wstl_remove_allowlist', array( $this, 'ajax_remove_allowlist' ) );
		add_action( 'wp_ajax_wstl_unflag_customer', array( $this, 'ajax_unflag_customer' ) );
		add_action( 'wp_ajax_wstl_start_sync', array( $this, 'ajax_start_sync' ) );
		add_action( 'wp_ajax_wstl_process_sync_batch', array( $this, 'ajax_process_sync_batch' ) );
		add_action( 'wp_ajax_wstl_stop_sync', array( $this, 'ajax_stop_sync' ) );
		add_action( 'wp_ajax_wstl_reset_sync', array( $this, 'ajax_reset_sync' ) );
		add_action( 'wp_ajax_wstl_generate_api_key', array( $this, 'ajax_generate_api_key' ) );
		add_action( 'wp_ajax_wstl_bulk_action', array( $this, 'ajax_bulk_action' ) );
		add_action( 'wp_ajax_wstl_recalculate_score', array( $this, 'ajax_recalculate_score' ) );
		add_action( 'wp_ajax_wstl_chargeback_backfill_step', array( $this, 'ajax_chargeback_backfill_step' ) );
		add_action( 'wp_ajax_wstl_save_note', array( $this, 'ajax_save_note' ) );
		add_action( 'wp_ajax_wstl_quickstart_toggle', array( $this, 'ajax_quickstart_toggle' ) );
		add_action( 'wp_ajax_wstl_quickstart_dismiss', array( $this, 'ajax_quickstart_dismiss' ) );
		add_action( 'wp_ajax_wstl_export_csv', array( $this, 'ajax_export_csv' ) );
		add_action( 'wp_ajax_wstl_send_test_notification', array( $this, 'ajax_send_test_notification' ) );
		add_action( 'wp_ajax_wstl_customer_search', array( $this, 'ajax_customer_search' ) );

		if ( wstl_has_automation() && wstl_can_use_pro() ) {
			add_action( 'wp_ajax_wstl_save_automation_rule', array( $this, 'ajax_save_automation_rule' ) );
			add_action( 'wp_ajax_wstl_delete_automation_rule', array( $this, 'ajax_delete_automation_rule' ) );
		}

		if ( wstl_has_webhooks() && wstl_can_use_pro() ) {
			add_action( 'wp_ajax_wstl_add_webhook', array( $this, 'ajax_add_webhook' ) );
			add_action( 'wp_ajax_wstl_delete_webhook', array( $this, 'ajax_delete_webhook' ) );
			add_action( 'wp_ajax_wstl_test_webhook', array( $this, 'ajax_test_webhook' ) );
		}

		if ( wstl_has_scheduled_reports() && wstl_can_use_pro() ) {
			add_action( 'wp_ajax_wstl_add_report_schedule', array( $this, 'ajax_add_report_schedule' ) );
			add_action( 'wp_ajax_wstl_delete_report_schedule', array( $this, 'ajax_delete_report_schedule' ) );
			add_action( 'wp_ajax_wstl_send_report_now', array( $this, 'ajax_send_report_now' ) );
			add_action( 'wp_ajax_wstl_update_report_schedule_enabled', array( $this, 'ajax_update_report_schedule_enabled' ) );
		}
	}

	private function ensure_manage_wc(): void {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'trustlens' ) ) );
		}
	}

	/**
	 * Command-palette customer lookup — returns customers matching an email term,
	 * each with a link to their detail page. Admin-only, nonce-protected.
	 *
	 * @since 1.2.7
	 */
	public function ajax_customer_search(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();

		$q = sanitize_text_field( wp_unslash( $_POST['q'] ?? '' ) );
		if ( strlen( $q ) < 2 ) {
			wp_send_json_success( array( 'results' => array() ) );
		}

		$repo    = new TrustLens_Customer_Repository();
		$rows    = $repo->search( $q, 6 );
		$results = array();
		foreach ( $rows as $row ) {
			$results[] = array(
				'email'   => (string) $row->customer_email,
				'segment' => wstl_get_segment_label( (string) $row->segment ),
				'score'   => (int) $row->trust_score,
				'href'    => admin_url( 'admin.php?page=trustlens-customers&customer=' . rawurlencode( (string) $row->email_hash ) ),
			);
		}

		wp_send_json_success( array( 'results' => $results ) );
	}

	public function ajax_block_customer(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$email_hash = sanitize_text_field( wp_unslash( $_POST['email_hash'] ?? '' ) );
		if ( '' === $email_hash ) {
			wp_send_json_error( array( 'message' => __( 'Invalid customer.', 'trustlens' ) ) );
		}
		$reason = sanitize_textarea_field( wp_unslash( $_POST['reason'] ?? '' ) );
		if ( '' === $reason ) {
			$reason = __( 'Blocked from admin', 'trustlens' );
		}
		$result = wstl_block_customer( $email_hash, $reason );
		$result ? wp_send_json_success( array( 'message' => __( 'Customer blocked.', 'trustlens' ) ) ) : wp_send_json_error( array( 'message' => __( 'Failed to block customer.', 'trustlens' ) ) );
	}

	public function ajax_unblock_customer(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$email_hash = sanitize_text_field( wp_unslash( $_POST['email_hash'] ?? '' ) );
		if ( '' === $email_hash ) {
			wp_send_json_error( array( 'message' => __( 'Invalid customer.', 'trustlens' ) ) );
		}
		$result = wstl_unblock_customer( $email_hash );
		$result ? wp_send_json_success( array( 'message' => __( 'Customer unblocked.', 'trustlens' ) ) ) : wp_send_json_error( array( 'message' => __( 'Failed to unblock customer.', 'trustlens' ) ) );
	}

	public function ajax_allowlist_customer(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$email_hash = sanitize_text_field( wp_unslash( $_POST['email_hash'] ?? '' ) );
		if ( '' === $email_hash ) {
			wp_send_json_error( array( 'message' => __( 'Invalid customer.', 'trustlens' ) ) );
		}
		$result = wstl_allowlist_customer( $email_hash );
		$result ? wp_send_json_success( array( 'message' => __( 'Customer added to allowlist.', 'trustlens' ) ) ) : wp_send_json_error( array( 'message' => __( 'Failed to update customer.', 'trustlens' ) ) );
	}

	public function ajax_remove_allowlist(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$email_hash = sanitize_text_field( wp_unslash( $_POST['email_hash'] ?? '' ) );
		if ( '' === $email_hash ) {
			wp_send_json_error( array( 'message' => __( 'Invalid customer.', 'trustlens' ) ) );
		}
		$result = wstl_remove_from_allowlist( $email_hash );
		$result ? wp_send_json_success( array( 'message' => __( 'Customer removed from allowlist.', 'trustlens' ) ) ) : wp_send_json_error( array( 'message' => __( 'Failed to update customer.', 'trustlens' ) ) );
	}

	public function ajax_unflag_customer(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$email_hash = sanitize_text_field( wp_unslash( $_POST['email_hash'] ?? '' ) );
		if ( '' === $email_hash ) {
			wp_send_json_error( array( 'message' => __( 'Invalid customer.', 'trustlens' ) ) );
		}
		$result = wstl_unflag_customer( $email_hash );
		$result ? wp_send_json_success( array( 'message' => __( 'Flag cleared.', 'trustlens' ) ) ) : wp_send_json_error( array( 'message' => __( 'Failed to clear flag.', 'trustlens' ) ) );
	}

	/**
	 * Quick Start: toggle a top-level setting (blocking / notifications).
	 *
	 * @since 1.2.6
	 */
	public function ajax_quickstart_toggle(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$setting = sanitize_key( wp_unslash( $_POST['setting'] ?? '' ) );
		$enabled = ! empty( $_POST['enabled'] ) && '0' !== sanitize_text_field( wp_unslash( $_POST['enabled'] ) );
		$map     = array(
			'blocking'      => 'trustlens_enable_blocking',
			'notifications' => 'trustlens_enable_notifications',
		);
		if ( ! isset( $map[ $setting ] ) ) {
			wp_send_json_error( array( 'message' => __( 'Unknown setting.', 'trustlens' ) ) );
		}
		update_option( $map[ $setting ], $enabled );
		wp_send_json_success( array( 'message' => $enabled ? __( 'Turned on.', 'trustlens' ) : __( 'Turned off.', 'trustlens' ) ) );
	}

	/**
	 * Quick Start: dismiss the setup card so it stays hidden.
	 *
	 * @since 1.2.6
	 */
	public function ajax_quickstart_dismiss(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		update_option( 'trustlens_quickstart_dismissed', true );
		wp_send_json_success( array( 'message' => __( 'Setup hidden. You can change these anytime in Settings.', 'trustlens' ) ) );
	}

	public function ajax_start_sync(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();

		try {
			$sync    = TrustLens_Historical_Sync::instance();
			$started = $sync->start( false );
			if ( $started ) {
				wp_send_json_success( array( 'message' => __( 'Sync started.', 'trustlens' ), 'status' => $sync->get_status() ) );
			}

			$status = $sync->get_status();
			wp_send_json_error( array( 'message' => 'running' === $status['status'] ? __( 'Sync is already running.', 'trustlens' ) : ( $status['error'] ?? __( 'Failed to start sync.', 'trustlens' ) ) ) );
		} catch ( \Throwable $e ) {
			wp_send_json_error( array( 'message' => sprintf( /* translators: %s: error message. */ __( 'Failed to start sync: %s', 'trustlens' ), $e->getMessage() ) ), 500 );
		}
	}

	public function ajax_process_sync_batch(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();

		try {
			$result = TrustLens_Historical_Sync::instance()->process_ajax_batch();
			wp_send_json_success( array( 'status' => $result, 'progress' => $result['progress'] ?? 0 ) );
		} catch ( \Throwable $e ) {
			wp_send_json_error( array( 'message' => sprintf( /* translators: %s: error message. */ __( 'Sync batch failed: %s', 'trustlens' ), $e->getMessage() ) ), 500 );
		}
	}

	public function ajax_stop_sync(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$sync = TrustLens_Historical_Sync::instance();
		$sync->stop();
		wp_send_json_success( array( 'message' => __( 'Sync stopped.', 'trustlens' ), 'status' => $sync->get_status() ) );
	}

	public function ajax_reset_sync(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$sync = TrustLens_Historical_Sync::instance();
		$sync->reset();
		wp_send_json_success( array( 'message' => __( 'All data has been reset.', 'trustlens' ), 'status' => $sync->get_status() ) );
	}

	public function ajax_generate_api_key(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$api_key    = wp_generate_password( 64, false, false );
		$hashed_key = hash( 'sha256', $api_key );
		update_option( 'trustlens_api_key', $hashed_key );
		wp_send_json_success( array( 'message' => __( 'API key generated. Copy it now - it will not be shown again.', 'trustlens' ), 'api_key' => $api_key ) );
	}

	public function ajax_bulk_action(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$action    = sanitize_text_field( wp_unslash( $_POST['bulk_action'] ?? '' ) );
		$customers = isset( $_POST['customers'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['customers'] ) ) : array();
		if ( '' === $action || empty( $customers ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid request.', 'trustlens' ) ) );
		}

		$reason = sanitize_textarea_field( wp_unslash( $_POST['reason'] ?? '' ) );

		switch ( $action ) {
			case 'block':
				$result = TrustLens_Bulk_Operations::bulk_block( $customers, '' !== $reason ? $reason : __( 'Blocked from admin (bulk)', 'trustlens' ) );
				break;
			case 'unblock':
				$result = TrustLens_Bulk_Operations::bulk_unblock( $customers );
				break;
			case 'allowlist':
				$result = TrustLens_Bulk_Operations::bulk_allowlist( $customers );
				break;
			case 'remove_allowlist':
				$result = TrustLens_Bulk_Operations::bulk_remove_allowlist( $customers );
				break;
			case 'recalculate':
				$result = TrustLens_Bulk_Operations::bulk_recalculate_scores( $customers );
				break;
			case 'delete':
				$result = TrustLens_Bulk_Operations::bulk_delete( $customers );
				break;
			default:
				wp_send_json_error( array( 'message' => __( 'Invalid action.', 'trustlens' ) ) );
		}

		$processed = (int) $result['success'];
		$failed    = (int) $result['failed'];
		$message   = sprintf(
			/* translators: 1: number processed, 2: number failed */
			_n( '%1$d customer processed, %2$d failed.', '%1$d customers processed, %2$d failed.', $processed, 'trustlens' ),
			$processed,
			$failed
		);

		if ( $processed > 0 ) {
			wp_send_json_success( array( 'message' => $message, 'processed' => $processed, 'failed' => $failed ) );
		}
		wp_send_json_error( array( 'message' => $message, 'processed' => $processed, 'failed' => $failed ) );
	}

	public function ajax_recalculate_score(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$email_hash = sanitize_text_field( wp_unslash( $_POST['email_hash'] ?? '' ) );
		if ( '' === $email_hash ) {
			wp_send_json_error( array( 'message' => __( 'Invalid customer.', 'trustlens' ) ) );
		}
		$result = TrustLens_Score_Calculator::instance()->recalculate_score( $email_hash );
		$result ? wp_send_json_success( array( 'message' => __( 'Score recalculated.', 'trustlens' ), 'score' => $result['score'], 'segment' => $result['segment'] ) ) : wp_send_json_error( array( 'message' => __( 'Failed to recalculate score.', 'trustlens' ) ) );
	}

	public function ajax_chargeback_backfill_step(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();

		if ( ! class_exists( 'TrustLens_Card_Brand_Detector' ) ) {
			wp_send_json_error( array( 'message' => __( 'Card brand detector unavailable.', 'trustlens' ) ) );
		}

		$detector = new TrustLens_Card_Brand_Detector();
		$result   = $detector->backfill_batch( 50 );
		wp_send_json_success( $result );
	}

	public function ajax_save_note(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$email_hash = sanitize_text_field( wp_unslash( $_POST['email_hash'] ?? '' ) );
		$note       = sanitize_textarea_field( wp_unslash( $_POST['note'] ?? '' ) );
		if ( '' === $email_hash ) {
			wp_send_json_error( array( 'message' => __( 'Invalid customer.', 'trustlens' ) ) );
		}
		$customer       = wstl_get_customer( $email_hash );
		$existing_notes = $customer ? $customer->admin_notes : '';
		$timestamp      = current_time( 'mysql' );
		$new_notes      = $existing_notes . "\n[{$timestamp}] " . $note;
		$result         = wstl_update_customer( $email_hash, array( 'admin_notes' => trim( $new_notes ) ) );
		$result ? wp_send_json_success( array( 'message' => __( 'Note saved.', 'trustlens' ) ) ) : wp_send_json_error( array( 'message' => __( 'Failed to save note.', 'trustlens' ) ) );
	}

	public function ajax_export_csv(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$segment   = sanitize_text_field( wp_unslash( $_POST['segment'] ?? '' ) );
		$search    = sanitize_text_field( wp_unslash( $_POST['search'] ?? '' ) );
		$customers = ( new TrustLens_Customer_Repository() )->get_customers( array( 'per_page' => 10000, 'segment' => $segment, 'search' => $search ) );
		$csv_rows  = array();
		$csv_rows[] = TrustLens_Data_Export::to_csv_row( array( 'Email', 'Score', 'Segment', 'Orders', 'Refunds', 'Return Rate', 'Refund Value', 'Blocked', 'Allowlisted', 'Flagged' ) );
		foreach ( $customers as $customer ) {
			$csv_rows[] = TrustLens_Data_Export::to_csv_row(
				array(
					$customer->customer_email ?: 'Guest',
					$customer->trust_score,
					$customer->segment,
					$customer->total_orders,
					$customer->total_refunds,
					number_format( $customer->return_rate, 1 ) . '%',
					$customer->total_refund_value,
					$customer->is_blocked ? 'Yes' : 'No',
					$customer->is_allowlisted ? 'Yes' : 'No',
					! empty( $customer->is_flagged ) ? 'Yes' : 'No',
				)
			);
		}
		wp_send_json_success( array( 'csv' => implode( "\n", $csv_rows ), 'filename' => 'trustlens-customers-' . gmdate( 'Y-m-d' ) . '.csv' ) );
	}

	public function ajax_save_automation_rule(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		if ( ! wstl_can_use_pro() || ! class_exists( 'TrustLens_Automation' ) ) {
			wp_send_json_error( array( 'message' => __( 'Automation rules are available in the Pro version.', 'trustlens' ) ) );
		}

		$rule_id        = sanitize_text_field( wp_unslash( $_POST['rule_id'] ?? '' ) );
		$trigger        = sanitize_text_field( wp_unslash( $_POST['trigger'] ?? '' ) );
		// `rule_action` (not `action`) — the WP AJAX router consumes `$_POST['action']`,
		// so the rule-action field has to use a distinct key on the wire.
		$action         = sanitize_text_field( wp_unslash( $_POST['rule_action'] ?? '' ) );
		$raw_conditions = isset( $_POST['conditions'] ) && is_array( $_POST['conditions'] ) ? wp_unslash( $_POST['conditions'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		// Whitelist enforcement — rejects typos, POST tampering, and stale
		// UI values. Unknown keys would otherwise be stored as-is and turn
		// into silent "never-fires" at runtime (trigger, action) or — worse,
		// before the evaluator was hardened — "always matches" (field).
		if ( ! in_array( $trigger, TrustLens_Automation::valid_triggers(), true ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid trigger.', 'trustlens' ) ) );
		}
		if ( ! in_array( $action, TrustLens_Automation::valid_actions(), true ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid action.', 'trustlens' ) ) );
		}
		// Order-only actions paired with a customer-only trigger would never
		// fire; reject at save rather than letting the runtime log an error
		// row on every event.
		if ( TrustLens_Automation::is_order_only_action( $action ) && ! TrustLens_Automation::is_order_bearing_trigger( $trigger ) ) {
			wp_send_json_error( array(
				'message' => __( 'This action is only valid for order triggers (Order Placed, Order Completed, Refund Processed).', 'trustlens' ),
			) );
		}

		$conditions = $this->sanitize_conditions( $raw_conditions, $trigger );

		$rule = array(
			'id'           => $rule_id ?: wp_generate_uuid4(),
			'name'         => sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) ),
			'trigger'      => $trigger,
			'conditions'   => $conditions,
			'action'       => $action,
			'action_value' => sanitize_text_field( wp_unslash( $_POST['action_value'] ?? '' ) ),
			'enabled'      => ! empty( $_POST['enabled'] ),
		);

		// Rule-quality gate: block the save when the rule can never do anything
		// useful. Two categories, both reported to the user the same way:
		//   - Unsatisfiable conditions (e.g. `trust_score > 50 AND < 30`).
		//   - Incomplete action configuration (e.g. `Send Webhook` with no URL).
		$warnings = array_merge(
			TrustLens_Automation::unsatisfiable_reasons( $conditions, $trigger ),
			TrustLens_Automation::incomplete_action_reasons( $action, $rule['action_value'] )
		);
		if ( ! empty( $warnings ) ) {
			wp_send_json_error( array(
				'message'  => __( 'This rule cannot be saved as-is — it would never do anything.', 'trustlens' ),
				'warnings' => $warnings,
			) );
		}

		// The callback returns false when an edit targets a rule_id that no
		// longer exists; we respond AFTER the lock is released (never die() while
		// holding GET_LOCK, or the finally that releases it is skipped).
		$saved = $this->with_rules_lock( function () use ( $rule, $rule_id ) {
			$rules = (array) get_option( 'trustlens_automation_rules', array() );
			if ( $rule_id ) {
				$replaced = false;
				foreach ( $rules as $i => $existing ) {
					if ( ( $existing['id'] ?? '' ) === $rule_id ) {
						$rules[ $i ] = $rule;
						$replaced    = true;
						break;
					}
				}
				if ( ! $replaced ) {
					return false;
				}
			} else {
				$rules[] = $rule;
			}
			update_option( 'trustlens_automation_rules', $rules, false );
			return true;
		} );

		if ( false === $saved ) {
			wp_send_json_error( array( 'message' => __( 'Rule not found. It may have been deleted.', 'trustlens' ) ) );
		}

		wp_send_json_success( array(
			'message' => __( 'Rule saved.', 'trustlens' ),
			'rule'    => $rule,
			// Pre-resolved labels so the client can update the list item
			// without shipping its own trigger/action label map.
			'labels'  => array(
				'trigger' => wstl_get_trigger_label( $rule['trigger'] ),
				'action'  => wstl_get_action_label( $rule['action'] ),
			),
			// Was this an edit of an existing rule, or a brand-new one?
			// The client uses this to decide between partial DOM update vs
			// navigation to the new rule's URL for a full inspector render.
			'is_edit' => (bool) $rule_id,
		) );
	}

	/**
	 * Validate + normalize condition rows. Silently drops rows whose field or
	 * operator isn't in the engine's whitelist, and rejects order-only fields
	 * paired with a non-order trigger. Empty-field rows are dropped (the UI
	 * may leave an unused blank row).
	 *
	 * @since 1.2.2
	 * @param array  $conditions Raw $_POST conditions array.
	 * @param string $trigger    Already-validated trigger key.
	 * @return array
	 */
	private function sanitize_conditions( array $conditions, string $trigger = '' ): array {
		$valid_fields    = TrustLens_Automation::valid_fields();
		$valid_operators = TrustLens_Automation::valid_operators();
		$is_order_trig   = TrustLens_Automation::is_order_bearing_trigger( $trigger );

		$sanitized = array();
		foreach ( $conditions as $condition ) {
			$field = sanitize_text_field( $condition['field'] ?? '' );
			// Operator is NOT run through sanitize_text_field: that invokes
			// strip_tags('<') and would turn our `<`, `<=`, `<>` operators into
			// empty strings. The whitelist check below is the sanitizer — any
			// value not in VALID_OPERATORS is rejected outright.
			$operator = isset( $condition['operator'] ) ? (string) $condition['operator'] : '';
			$value    = sanitize_text_field( $condition['value'] ?? '' );

			if ( '' === $field ) {
				continue;
			}
			if ( ! in_array( $field, $valid_fields, true ) ) {
				wp_send_json_error( array(
					/* translators: %s: rejected field name */
					'message' => sprintf( __( 'Unknown condition field: %s', 'trustlens' ), $field ),
				) );
			}
			if ( ! in_array( $operator, $valid_operators, true ) ) {
				wp_send_json_error( array(
					/* translators: %s: rejected operator */
					'message' => sprintf( __( 'Invalid operator: %s', 'trustlens' ), $operator ),
				) );
			}
			if ( TrustLens_Automation::is_order_only_field( $field ) && ! $is_order_trig ) {
				wp_send_json_error( array(
					/* translators: %s: field name */
					'message' => sprintf( __( '"%s" can only be used with order triggers.', 'trustlens' ), $field ),
				) );
			}

			$sanitized[] = array(
				'field'    => $field,
				'operator' => $operator,
				'value'    => $value,
			);
		}
		return $sanitized;
	}

	public function ajax_delete_automation_rule(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		if ( ! wstl_can_use_pro() || ! class_exists( 'TrustLens_Automation' ) ) {
			wp_send_json_error( array( 'message' => __( 'Automation rules are available in the Pro version.', 'trustlens' ) ) );
		}
		$rule_id = sanitize_text_field( wp_unslash( $_POST['rule_id'] ?? '' ) );

		$this->with_rules_lock( function () use ( $rule_id ) {
			$rules = array_filter(
				(array) get_option( 'trustlens_automation_rules', array() ),
				static function( $rule ) use ( $rule_id ) {
					return ( $rule['id'] ?? '' ) !== $rule_id;
				}
			);
			update_option( 'trustlens_automation_rules', array_values( $rules ), false );
		} );

		wp_send_json_success( array( 'message' => __( 'Rule deleted.', 'trustlens' ) ) );
	}

	/**
	 * Serialize concurrent writes to the `trustlens_automation_rules` option.
	 *
	 * Two parallel AJAX saves both doing get_option → mutate → update_option
	 * used to clobber each other (last-write-wins). A MySQL session-level
	 * advisory lock (GET_LOCK) fences the whole read-modify-write sequence
	 * across processes.
	 *
	 * The callback MUST NOT terminate the request (no wp_send_json or wp_die)
	 * while the lock is held — a die() would skip the finally and leak the lock
	 * onto the connection. Return a value from the callback and respond AFTER
	 * with_rules_lock() returns instead.
	 *
	 * @since 1.2.2
	 * @param callable $cb Callback to run while holding the lock.
	 * @return mixed The callback's return value.
	 */
	private function with_rules_lock( callable $cb ) {
		global $wpdb;
		// Scope the lock to this install (DB name + table prefix) so a shared
		// MySQL instance serving multiple WP installs doesn't collide, then hash
		// it to a fixed 32-char digest. A raw DB_NAME could push the name past
		// MySQL's 64-char GET_LOCK limit, making GET_LOCK return NULL and break
		// every rule save on that install regardless of contention.
		$name = 'wstl_automation_rules_' . md5( DB_NAME . '|' . $wpdb->prefix );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Advisory lock, not a data query.
		$got = $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, %d)', $name, 5 ) );
		if ( '1' !== (string) $got ) {
			// No lock acquired, so there is nothing to release — safe to die here.
			wp_send_json_error( array(
				'message' => __( 'Could not acquire rules lock. Another save is in progress — please retry.', 'trustlens' ),
			) );
		}

		try {
			return $cb();
		} finally {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Advisory lock release.
			$wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $name ) );
		}
	}

	public function ajax_add_webhook(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		if ( ! wstl_can_use_pro() || ! class_exists( 'TrustLens_Webhooks' ) ) {
			wp_send_json_error( array( 'message' => __( 'Webhooks are available in the Pro version.', 'trustlens' ) ) );
		}
		$url     = esc_url_raw( wp_unslash( $_POST['url'] ?? '' ) );
		$secret  = sanitize_text_field( wp_unslash( $_POST['secret'] ?? '' ) );
		$enabled = ! empty( $_POST['enabled'] );
		if ( '' === $url ) {
			wp_send_json_error( array( 'message' => __( 'URL is required.', 'trustlens' ) ) );
		}
		$id = TrustLens_Webhooks::add_endpoint( array( 'url' => $url, 'secret' => $secret, 'enabled' => $enabled ) );
		wp_send_json_success( array( 'message' => __( 'Webhook added.', 'trustlens' ), 'id' => $id ) );
	}

	public function ajax_delete_webhook(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		if ( ! wstl_can_use_pro() || ! class_exists( 'TrustLens_Webhooks' ) ) {
			wp_send_json_error( array( 'message' => __( 'Webhooks are available in the Pro version.', 'trustlens' ) ) );
		}
		$id = sanitize_text_field( wp_unslash( $_POST['endpoint_id'] ?? '' ) );
		TrustLens_Webhooks::remove_endpoint( $id );
		wp_send_json_success( array( 'message' => __( 'Webhook deleted.', 'trustlens' ) ) );
	}

	public function ajax_test_webhook(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		if ( ! wstl_can_use_pro() || ! class_exists( 'TrustLens_Webhooks' ) ) {
			wp_send_json_error( array( 'message' => __( 'Webhooks are available in the Pro version.', 'trustlens' ) ) );
		}

		// Look up the endpoint server-side from the stored config so the signing
		// secret never has to round-trip through the browser. The previous
		// implementation rendered `data-secret` on the test button, which exposed
		// the HMAC secret to any DOM-reading script (browser extensions, XSS).
		$endpoint_id = sanitize_text_field( wp_unslash( $_POST['endpoint_id'] ?? '' ) );
		$endpoint    = TrustLens_Webhooks::get_endpoint( $endpoint_id );

		if ( null === $endpoint ) {
			wp_send_json_error( array( 'message' => __( 'Webhook endpoint not found.', 'trustlens' ) ) );
		}

		$result = TrustLens_Webhooks::test_endpoint( (string) ( $endpoint['url'] ?? '' ), (string) ( $endpoint['secret'] ?? '' ) );
		! empty( $result['success'] )
			? wp_send_json_success( array( 'message' => __( 'Webhook test successful.', 'trustlens' ) ) )
			: wp_send_json_error( array( 'message' => $result['error'] ?? __( 'Webhook test failed.', 'trustlens' ) ) );
	}

	public function ajax_add_report_schedule(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		if ( ! wstl_can_use_pro() || ! class_exists( 'TrustLens_Scheduled_Reports' ) ) {
			wp_send_json_error( array( 'message' => __( 'Scheduled reports are available in the Pro version.', 'trustlens' ) ) );
		}
		$frequency = sanitize_text_field( wp_unslash( $_POST['frequency'] ?? 'weekly' ) );
		$day       = absint( wp_unslash( $_POST['day'] ?? 1 ) );
		$time      = sanitize_text_field( wp_unslash( $_POST['time'] ?? '09:00' ) );
		$enabled   = ! empty( $_POST['enabled'] );
		$schedule  = array(
			'id'        => wp_generate_uuid4(),
			'type'      => sanitize_text_field( wp_unslash( $_POST['type'] ?? 'summary' ) ),
			'frequency' => $frequency,
			'day'       => $day,
			'time'      => $time,
			'enabled'   => $enabled,
			'next_run'  => $enabled ? TrustLens_Scheduled_Reports::instance()->calculate_next_run( array( 'frequency' => $frequency, 'day' => $day, 'time' => $time ) ) : 0,
		);
		$schedules   = get_option( 'trustlens_report_schedules', array() );
		$schedules[] = $schedule;
		update_option( 'trustlens_report_schedules', $schedules, false );
		TrustLens_Scheduled_Reports::schedule_reports();
		wp_send_json_success( array( 'message' => __( 'Schedule added.', 'trustlens' ), 'schedule' => $schedule ) );
	}

	public function ajax_delete_report_schedule(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		if ( ! wstl_can_use_pro() || ! class_exists( 'TrustLens_Scheduled_Reports' ) ) {
			wp_send_json_error( array( 'message' => __( 'Scheduled reports are available in the Pro version.', 'trustlens' ) ) );
		}
		$schedule_id = sanitize_text_field( wp_unslash( $_POST['schedule_id'] ?? '' ) );
		$schedules   = array_filter(
			get_option( 'trustlens_report_schedules', array() ),
			static function( $schedule ) use ( $schedule_id ) {
				return ( $schedule['id'] ?? '' ) !== $schedule_id;
			}
		);
		update_option( 'trustlens_report_schedules', array_values( $schedules ), false );
		TrustLens_Scheduled_Reports::schedule_reports();
		wp_send_json_success( array( 'message' => __( 'Schedule deleted.', 'trustlens' ) ) );
	}

	public function ajax_update_report_schedule_enabled(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		if ( ! wstl_can_use_pro() || ! class_exists( 'TrustLens_Scheduled_Reports' ) ) {
			wp_send_json_error( array( 'message' => __( 'Scheduled reports are available in the Pro version.', 'trustlens' ) ) );
		}

		$schedule_id = sanitize_text_field( wp_unslash( $_POST['schedule_id'] ?? '' ) );
		$enabled     = ! empty( $_POST['enabled'] );
		$schedules   = get_option( 'trustlens_report_schedules', array() );
		$updated     = false;

		foreach ( $schedules as $index => $schedule ) {
			if ( ( $schedule['id'] ?? '' ) !== $schedule_id ) {
				continue;
			}

			$schedules[ $index ]['enabled'] = $enabled;
			$schedules[ $index ]['next_run'] = $enabled
				? TrustLens_Scheduled_Reports::instance()->calculate_next_run(
					array(
						'frequency' => $schedule['frequency'] ?? 'weekly',
						'day'       => $schedule['day'] ?? 1,
						'time'      => $schedule['time'] ?? '09:00',
					)
				)
				: 0;
			$updated = true;
			break;
		}

		if ( ! $updated ) {
			wp_send_json_error( array( 'message' => __( 'Report schedule not found.', 'trustlens' ) ) );
		}

		update_option( 'trustlens_report_schedules', array_values( $schedules ), false );
		TrustLens_Scheduled_Reports::schedule_reports();

		wp_send_json_success( array( 'message' => __( 'Schedule updated.', 'trustlens' ) ) );
	}

	public function ajax_send_report_now(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		if ( ! wstl_can_use_pro() || ! class_exists( 'TrustLens_Scheduled_Reports' ) ) {
			wp_send_json_error( array( 'message' => __( 'Scheduled reports are available in the Pro version.', 'trustlens' ) ) );
		}
		$result = TrustLens_Scheduled_Reports::instance()->send_report_by_id( sanitize_text_field( wp_unslash( $_POST['schedule_id'] ?? '' ) ) );
		if ( ! empty( $result['success'] ) ) {
			$message = __( 'Report sent.', 'trustlens' );
			if ( isset( $result['sent_count'] ) ) {
				$message = sprintf( /* translators: 1: number of recipients reached, 2: number that failed */ __( 'Report sent to %1$d recipient(s); %2$d failed.', 'trustlens' ), (int) $result['sent_count'], (int) ( $result['failure_count'] ?? 0 ) );
			}
			wp_send_json_success( array( 'message' => $message ) );
		}
		wp_send_json_error( array( 'message' => ! empty( $result['error'] ) ? $result['error'] : __( 'Failed to send report.', 'trustlens' ) ) );
	}

	public function ajax_send_test_notification(): void {
		check_ajax_referer( 'trustlens_admin', 'nonce' );
		$this->ensure_manage_wc();
		$notifications = TrustLens_Notifications::instance();
		$to            = $notifications->get_notification_email();
		$subject       = sprintf( /* translators: %s: site name */ __( '[TrustLens] Test Notification from %s', 'trustlens' ), get_bloginfo( 'name' ) );

		$message  = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head>';
		$message .= '<body style="font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif; font-size: 14px; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">';
		$message .= '<div style="background: linear-gradient(135deg, #2271b1, #135e96); padding: 20px; border-radius: 5px 5px 0 0;">';
		$message .= '<h1 style="margin: 0; color: #fff; font-size: 24px;">' . esc_html__( 'Test Notification', 'trustlens' ) . '</h1>';
		$message .= '</div><div style="background: #fff; padding: 20px; border: 1px solid #ddd; border-top: none; border-radius: 0 0 5px 5px;">';
		$message .= '<p>' . esc_html__( 'This is a test notification from TrustLens. If you received this email, your notification settings are working correctly.', 'trustlens' ) . '</p>';
		$message .= '<p style="margin-top: 20px;"><a href="' . esc_url( admin_url( 'admin.php?page=trustlens-notifications' ) ) . '" style="background: #2271b1; color: #fff; padding: 10px 20px; text-decoration: none; border-radius: 3px;">' . esc_html__( 'Go to Notification Settings', 'trustlens' ) . '</a></p>';
		$message .= '</div><p style="margin-top: 20px; font-size: 12px; color: #666; text-align: center;">';
		$message .= sprintf( /* translators: %s: site name */ esc_html__( 'This email was sent by TrustLens on %s', 'trustlens' ), get_bloginfo( 'name' ) );
		$message .= '</p></body></html>';

		$result = wstl_send_mail( $to, $subject, $message, array(), array( 'context' => 'test_notification' ) );
		if ( ! empty( $result['success'] ) ) {
			wp_send_json_success( array( 'message' => sprintf( /* translators: %s: recipient email address */ __( 'Test email sent to %s', 'trustlens' ), $to ) ) );
		}
		wp_send_json_error( array( 'message' => sprintf( /* translators: %s: error detail */ __( 'Failed to send test email. %s', 'trustlens' ), ! empty( $result['error'] ) ? $result['error'] : __( 'Check your server mail settings.', 'trustlens' ) ) ) );
	}
}
