<?php
/**
 * Public API functions.
 *
 * @package TrustLens
 * @since   1.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Normalize an email address before hashing or comparison.
 *
 * @since 1.1.5
 * @param string $email Email address.
 * @return string
 */
function wstl_normalize_email( string $email ): string {
	return strtolower( trim( $email ) );
}

/**
 * Normalize a stored customer hash.
 *
 * @since 1.1.5
 * @param string $hash Customer hash.
 * @return string
 */
function wstl_normalize_hash( string $hash ): string {
	return strtolower( trim( $hash ) );
}

/**
 * Check whether a value matches the current customer hash format.
 *
 * @since 1.1.5
 * @param string $hash Customer hash.
 * @return bool
 */
function wstl_is_email_hash( string $hash ): bool {
	// Accept both current 64-char SHA-256 hashes and legacy 32-char MD5 hashes.
	return 1 === preg_match( '/^[a-f0-9]{32}(?:[a-f0-9]{32})?$/', wstl_normalize_hash( $hash ) );
}

/**
 * Hash personal data using a keyed HMAC so stored identifiers are not reversible
 * via plain dictionary matching across sites.
 *
 * @since 1.1.5
 * @param string $value   Normalized value to hash.
 * @param string $context Stable hash context.
 * @return string
 */
function wstl_hash_personal_value( string $value, string $context ): string {
	$value = trim( $value );
	if ( '' === $value ) {
		return '';
	}

	return hash_hmac( 'sha256', $context . '|' . $value, wp_salt( 'auth' ) );
}

/**
 * Get the client IP address for the current request.
 *
 * Defaults to REMOTE_ADDR. Forwarding headers (X-Forwarded-For, Client-IP) are NOT
 * trusted by default — a remote attacker can send any value, which would otherwise
 * let them spoof their identity and bypass per-IP velocity / blocklist rules.
 *
 * Sites behind a known trusted reverse proxy (Cloudflare, load balancer, Sucuri,
 * etc.) can opt in by returning true from the `trustlens/trust_proxy_headers` filter.
 * When trusted, the last entry in X-Forwarded-For is returned — that is the IP the
 * closest trusted hop observed.
 *
 * @since 1.2.3
 * @return string IP address, or empty string if unknown.
 */
function wstl_get_client_ip(): string {
	$remote_addr = isset( $_SERVER['REMOTE_ADDR'] )
		? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) )
		: '';

	/**
	 * Whether to trust X-Forwarded-For for client IP resolution.
	 *
	 * Only return true when the site sits behind a verified trusted proxy.
	 *
	 * @since 1.2.3
	 * @param bool   $trust       Default false.
	 * @param string $remote_addr REMOTE_ADDR for the current request.
	 */
	if ( ! apply_filters( 'trustlens/trust_proxy_headers', false, $remote_addr ) ) {
		return $remote_addr;
	}

	if ( empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
		return $remote_addr;
	}

	$xff  = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) );
	$list = array_filter( array_map( 'trim', explode( ',', $xff ) ) );

	if ( empty( $list ) ) {
		return $remote_addr;
	}

	return (string) end( $list );
}

/**
 * Get email hash from email address.
 *
 * @since 1.0.0
 * @param string $email Email address.
 * @return string HMAC-SHA256 hash of the normalized email.
 */
function wstl_get_email_hash( string $email ): string {
	$normalized = wstl_normalize_email( $email );
	return '' === $normalized ? '' : wstl_hash_personal_value( $normalized, 'email' );
}

/**
 * Get customer key from order.
 *
 * @since 1.0.0
 * @param WC_Order $order WooCommerce order.
 * @return array Customer key data.
 */
function wstl_get_customer_key( WC_Order $order ): array {
	$email = $order->get_billing_email();
	$email_hash = wstl_get_email_hash( $email );
	$customer_id = $order->get_customer_id();

	return array(
		'email_hash'    => $email_hash,
		'customer_id'   => $customer_id > 0 ? $customer_id : null,
		'customer_type' => $customer_id > 0 ? 'user' : 'guest',
		'email'         => $email,
	);
}

/**
 * Get customer by email hash.
 *
 * @since 1.0.0
 * @param string $email_hash Customer email hash.
 * @return object|null Customer object or null.
 */
function wstl_get_customer( string $email_hash ): ?object {
	$repo = new TrustLens_Customer_Repository();
	return $repo->get_by_hash( $email_hash );
}

/**
 * Get customer by email address.
 *
 * @since 1.0.0
 * @param string $email Email address.
 * @return object|null Customer object or null.
 */
function wstl_get_customer_by_email( string $email ): ?object {
	return wstl_get_customer( wstl_get_email_hash( $email ) );
}

/**
 * Update customer data.
 *
 * @since 1.0.0
 * @param string $email_hash Customer email hash.
 * @param array  $data       Data to update.
 * @return bool True on success.
 */
function wstl_update_customer( string $email_hash, array $data ): bool {
	$repo = new TrustLens_Customer_Repository();
	return $repo->update( $email_hash, $data );
}

/**
 * Create or update customer record.
 *
 * @since 1.0.0
 * @param array $data Customer data.
 * @return bool True on success.
 */
function wstl_upsert_customer( array $data ): bool {
	$repo = new TrustLens_Customer_Repository();
	return $repo->upsert( $data );
}

/**
 * Ensure customer record exists.
 *
 * @since 1.0.0
 * @param array    $customer_key Customer key from wstl_get_customer_key().
 * @param WC_Order $order        Order object.
 * @return bool True on success.
 */
function wstl_ensure_customer_record( array $customer_key, WC_Order $order ): bool {
	$existing = wstl_get_customer( $customer_key['email_hash'] );

	if ( $existing ) {
		return true;
	}

	return wstl_upsert_customer( array(
		'email_hash'     => $customer_key['email_hash'],
		'customer_id'    => $customer_key['customer_id'],
		'customer_email' => $customer_key['email'],
		'customer_type'  => $customer_key['customer_type'],
	) );
}

/**
 * Get trust score for customer.
 *
 * @since 1.0.0
 * @param string $email_or_hash Email address or hash.
 * @return int Trust score (0-100).
 */
function wstl_get_trust_score( string $email_or_hash ): int {
	$hash = wstl_is_email_hash( $email_or_hash ) ? wstl_normalize_hash( $email_or_hash ) : wstl_get_email_hash( $email_or_hash );
	$customer = wstl_get_customer( $hash );

	return $customer ? (int) $customer->trust_score : 50;
}

/**
 * Get segment for customer.
 *
 * @since 1.0.0
 * @param string $email_or_hash Email address or hash.
 * @return string Segment name.
 */
function wstl_get_segment( string $email_or_hash ): string {
	$hash = wstl_is_email_hash( $email_or_hash ) ? wstl_normalize_hash( $email_or_hash ) : wstl_get_email_hash( $email_or_hash );
	$customer = wstl_get_customer( $hash );

	return $customer ? $customer->segment : 'normal';
}

/**
 * Check if customer is blocked.
 *
 * @since 1.0.0
 * @param string $email_or_hash Email address or hash.
 * @return bool True if blocked.
 */
function wstl_is_blocked( string $email_or_hash ): bool {
	$hash = wstl_is_email_hash( $email_or_hash ) ? wstl_normalize_hash( $email_or_hash ) : wstl_get_email_hash( $email_or_hash );
	$customer = wstl_get_customer( $hash );

	return $customer && (bool) $customer->is_blocked;
}

/**
 * Check if customer is allowlisted.
 *
 * @since 1.0.0
 * @param string $email_or_hash Email address or hash.
 * @return bool True if allowlisted.
 */
function wstl_is_allowlisted( string $email_or_hash ): bool {
	$hash = wstl_is_email_hash( $email_or_hash ) ? wstl_normalize_hash( $email_or_hash ) : wstl_get_email_hash( $email_or_hash );
	$customer = wstl_get_customer( $hash );

	return $customer && (bool) $customer->is_allowlisted;
}

/**
 * Block a customer.
 *
 * @since 1.0.0
 * @param string $email_hash Customer email hash.
 * @param string $reason     Optional reason.
 * @return bool True on success.
 */
function wstl_block_customer( string $email_hash, string $reason = '' ): bool {
	$email_hash = wstl_normalize_hash( $email_hash );
	if ( ! wstl_is_email_hash( $email_hash ) ) {
		return false;
	}

	$customer = wstl_get_customer( $email_hash );
	$data     = array( 'is_blocked' => 1 );

	if ( $reason ) {
		$notes  = $customer ? (string) $customer->admin_notes : '';
		$notes .= "\n[" . current_time( 'mysql' ) . "] Blocked: " . $reason;
		$data['admin_notes'] = trim( $notes );
	}

	if ( $customer ) {
		$result = wstl_update_customer( $email_hash, $data );
	} else {
		// No record yet — e.g. a card-testing attacker blocked at the request
		// gate before any order exists. Create a minimal blocklist row so the
		// block actually persists and is enforced on the next checkout, instead
		// of silently no-opping against a non-existent customer.
		$data['email_hash'] = $email_hash;
		$result             = wstl_upsert_customer( $data );
	}

	if ( $result ) {
		wstl_log_event(
			$email_hash,
			'customer_blocked',
			array(
				'reason' => $reason,
			)
		);
		do_action( 'trustlens/customer_blocked', $email_hash, $reason );
	}

	return $result;
}

/**
 * Unblock a customer.
 *
 * @since 1.0.0
 * @param string $email_hash Customer email hash.
 * @return bool True on success.
 */
function wstl_unblock_customer( string $email_hash ): bool {
	$result = wstl_update_customer( $email_hash, array( 'is_blocked' => 0 ) );

	if ( $result ) {
		wstl_log_event( $email_hash, 'customer_unblocked' );
		do_action( 'trustlens/customer_unblocked', $email_hash );
	}

	return $result;
}

/**
 * Add customer to allowlist.
 *
 * @since 1.0.0
 * @param string $email_hash Customer email hash.
 * @return bool True on success.
 */
function wstl_allowlist_customer( string $email_hash ): bool {
	$email_hash = wstl_normalize_hash( $email_hash );
	if ( ! wstl_is_email_hash( $email_hash ) ) {
		return false;
	}

	$data = array(
		'is_allowlisted' => 1,
		'is_blocked'     => 0,
		// Allowlisting is the "reviewed — they're fine" action, so it clears the
		// review flag too, mirroring how it clears is_blocked. Leaving a VIP /
		// score-100 customer "Flagged for review" would be contradictory.
		'is_flagged'     => 0,
		'trust_score'    => 100,
		'segment'        => 'vip',
	);

	if ( wstl_get_customer( $email_hash ) ) {
		$result = wstl_update_customer( $email_hash, $data );
	} else {
		// Mirror wstl_block_customer: allowlisting an as-yet-unseen email
		// creates a minimal record so the allowlist entry persists.
		$data['email_hash'] = $email_hash;
		$result             = wstl_upsert_customer( $data );
	}

	if ( $result ) {
		wstl_log_event( $email_hash, 'customer_allowlisted' );
		do_action( 'trustlens/customer_allowlisted', $email_hash );
		// Allowlisting flips is_allowlisted (and pins segment to vip), which
		// changes the high-risk list and segment counts — refresh those caches.
		wstl_invalidate_customer_caches();
	}

	return $result;
}

/**
 * Remove customer from allowlist.
 *
 * @since 1.0.0
 * @param string $email_hash Customer email hash.
 * @return bool True on success.
 */
function wstl_remove_from_allowlist( string $email_hash ): bool {
	$result = wstl_update_customer( $email_hash, array( 'is_allowlisted' => 0 ) );

	if ( $result ) {
		wstl_log_event( $email_hash, 'customer_allowlist_removed' );
		do_action( 'trustlens/customer_allowlist_removed', $email_hash );
		// Removing the allowlist flag re-admits the customer to the high-risk
		// query — refresh the caches now; the queued recalc may change segment too.
		wstl_invalidate_customer_caches();
		// Recalculate score.
		wstl_queue_score_update( $email_hash, 0 );
	}

	return $result;
}

/**
 * Flag a customer for review (first-class `is_flagged` state). Mirrors
 * wstl_block_customer: creates a minimal record when none exists so flagging a
 * never-seen email (e.g. from a card-testing rule) still persists, and records
 * the reason as a timestamped admin note.
 *
 * @since 1.5.1
 * @param string $email_hash Customer email hash.
 * @param string $reason     Optional reason (e.g. rule name).
 * @return bool True on success.
 */
function wstl_flag_customer( string $email_hash, string $reason = '' ): bool {
	$email_hash = wstl_normalize_hash( $email_hash );
	if ( ! wstl_is_email_hash( $email_hash ) ) {
		return false;
	}

	$customer = wstl_get_customer( $email_hash );
	$data     = array( 'is_flagged' => 1 );

	if ( $reason ) {
		$notes  = $customer ? (string) $customer->admin_notes : '';
		$notes .= "\n[" . current_time( 'mysql' ) . '] Flagged for review: ' . $reason;
		$data['admin_notes'] = trim( $notes );
	}

	if ( $customer ) {
		$result = wstl_update_customer( $email_hash, $data );
	} else {
		$data['email_hash'] = $email_hash;
		$result             = wstl_upsert_customer( $data );
	}

	if ( $result ) {
		wstl_log_event( $email_hash, 'customer_flagged', array( 'reason' => $reason ) );
	}

	return $result;
}

/**
 * Clear a customer's flagged-for-review state.
 *
 * @since 1.5.1
 * @param string $email_hash Customer email hash.
 * @return bool True on success.
 */
function wstl_unflag_customer( string $email_hash ): bool {
	$result = wstl_update_customer( $email_hash, array( 'is_flagged' => 0 ) );
	if ( $result ) {
		wstl_log_event( $email_hash, 'customer_unflagged' );
	}
	return $result;
}

/**
 * Whether a customer is flagged for review.
 *
 * @since 1.5.1
 * @param string $email_or_hash Email address or hash.
 * @return bool
 */
function wstl_is_flagged( string $email_or_hash ): bool {
	$hash     = wstl_is_email_hash( $email_or_hash ) ? wstl_normalize_hash( $email_or_hash ) : wstl_get_email_hash( $email_or_hash );
	$customer = wstl_get_customer( $hash );
	return $customer && (bool) ( $customer->is_flagged ?? 0 );
}

/**
 * Add a tag to a customer (canonical JSON tags column, deduplicated).
 *
 * @since 1.5.1
 * @param string $email_hash Customer email hash.
 * @param string $tag        Tag to add.
 * @return bool True on success.
 */
function wstl_add_customer_tag( string $email_hash, string $tag ): bool {
	$repo = new TrustLens_Customer_Repository();
	return $repo->add_tag( $email_hash, $tag );
}

/**
 * Remove a tag from a customer.
 *
 * @since 1.5.1
 * @param string $email_hash Customer email hash.
 * @param string $tag        Tag to remove.
 * @return bool True on success.
 */
function wstl_remove_customer_tag( string $email_hash, string $tag ): bool {
	$repo = new TrustLens_Customer_Repository();
	return $repo->remove_tag( $email_hash, $tag );
}

/**
 * Schedule exactly one pending Action Scheduler action for the given hook+args+group.
 *
 * Race-free replacement for the classic "check as_has_scheduled_action then enqueue"
 * pattern, which is non-atomic and misses in-progress actions. Works by unscheduling
 * any existing pending rows for the same hook+args+group, then scheduling a fresh one.
 *
 * This prevents the runaway/fan-out failure mode where concurrent admin requests or
 * a self-continuing batch both enqueue a duplicate pending row.
 *
 * @since 1.1.9
 * @param string $hook      Action Scheduler hook.
 * @param array  $args      Action arguments (must match exactly for dedup).
 * @param string $group     Action group (defaults to 'trustlens').
 * @param int    $timestamp Unix timestamp to run at (defaults to now).
 * @return bool True when a single action is scheduled; false when AS unavailable or failed.
 */
function wstl_ensure_single_action( string $hook, array $args = array(), string $group = 'trustlens', int $timestamp = 0 ): bool {
	if ( ! function_exists( 'as_schedule_single_action' ) || ! function_exists( 'as_unschedule_all_actions' ) ) {
		return false;
	}

	if ( 0 === $timestamp ) {
		$timestamp = time();
	}

	try {
		as_unschedule_all_actions( $hook, $args, $group );
		as_schedule_single_action( $timestamp, $hook, $args, $group );
		return true;
	} catch ( \Throwable $e ) {
		// Avoid blowing up a running request because the AS store is temporarily unhappy.
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Operational log, not debug.
		error_log( sprintf( '[TrustLens] wstl_ensure_single_action failed for %s: %s', $hook, $e->getMessage() ) );
		return false;
	}
}

/**
 * Queue customer for score recalculation.
 *
 * @since 1.0.0
 * @param string $email_hash Customer email hash.
 * @param int    $delay      Delay in seconds.
 */
function wstl_queue_score_update( string $email_hash, int $delay = 60 ): void {
	// wstl_ensure_single_action is race-free (unschedule-then-schedule) — replaces
	// the older check-then-write dedup that could double-schedule under concurrent
	// events for the same customer.
	if ( wstl_ensure_single_action( 'trustlens/calculate_score', array( $email_hash ), 'trustlens', time() + $delay ) ) {
		return;
	}

	// No Action Scheduler — calculate immediately so the score still updates.
	do_action( 'trustlens/calculate_score', $email_hash );
}

/**
 * Log an event.
 *
 * @since 1.0.0
 * @param string   $email_hash Customer email hash.
 * @param string   $event_type Event type.
 * @param array    $event_data Event data.
 * @param int|null $order_id   Optional order ID.
 * @return bool True on success.
 */
function wstl_log_event( string $email_hash, string $event_type, array $event_data = array(), ?int $order_id = null ): bool {
	global $wpdb;

	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from $wpdb->prefix, safe.
	$result = $wpdb->insert(
		$wpdb->prefix . 'trustlens_events',
		array(
			'email_hash' => $email_hash,
			'event_type' => $event_type,
			'event_data' => wp_json_encode( $event_data ),
			'order_id'   => $order_id,
			'created_at' => current_time( 'mysql' ),
		),
		array( '%s', '%s', '%s', '%d', '%s' )
	);

	if ( $result ) {
		do_action( 'trustlens/event_logged', $email_hash, $event_type, $event_data );
	}

	return (bool) $result;
}

/**
 * Parse a list of email addresses from text input.
 *
 * @since 1.1.5
 * @param string $raw_emails Raw email string.
 * @return array Valid unique email addresses.
 */
function wstl_parse_email_list( string $raw_emails ): array {
	$emails = preg_split( '/[\s,;]+/', $raw_emails );
	if ( ! is_array( $emails ) ) {
		return array();
	}

	$emails = array_map( 'trim', $emails );
	$emails = array_filter( $emails, 'is_email' );

	return array_values( array_unique( $emails ) );
}

/**
 * Sanitize and normalize a comma-separated email list for option storage.
 *
 * @since 1.1.5
 * @param mixed $value Raw submitted value.
 * @return string Normalized comma-separated list.
 */
function wstl_sanitize_email_list( $value ): string {
	if ( ! is_string( $value ) ) {
		return '';
	}

	return implode( ', ', wstl_parse_email_list( $value ) );
}

/**
 * Get a safe sender address for plugin emails.
 *
 * @since 1.1.5
 * @return string Sender email address.
 */
function wstl_get_mail_from_address(): string {
	$admin_email = get_option( 'admin_email' );
	if ( is_email( $admin_email ) ) {
		return $admin_email;
	}

	$host = wp_parse_url( home_url(), PHP_URL_HOST );
	$host = is_string( $host ) ? preg_replace( '/^www\./', '', strtolower( $host ) ) : '';

	if ( $host ) {
		return 'wordpress@' . $host;
	}

	return 'wordpress@example.com';
}

/**
 * Get default headers for plugin emails.
 *
 * @since 1.1.5
 * @return array Default wp_mail() headers.
 */
function wstl_get_default_mail_headers(): array {
	$site_name = sanitize_text_field( wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) );

	return array(
		'Content-Type: text/html; charset=UTF-8',
		'From: ' . $site_name . ' <' . wstl_get_mail_from_address() . '>',
	);
}

/**
 * Get a human-readable mail error message.
 *
 * @since 1.1.5
 * @param mixed $error WP_Error or raw error message.
 * @return string Human-readable error text.
 */
function wstl_get_mail_error_message( $error ): string {
	if ( is_wp_error( $error ) ) {
		$message = $error->get_error_message();
		return is_string( $message ) && '' !== $message ? $message : __( 'Unknown mail error.', 'trustlens' );
	}

	if ( is_string( $error ) && '' !== $error ) {
		return $error;
	}

	return __( 'Unknown mail error.', 'trustlens' );
}

/**
 * Log a mail send attempt for debugging and support.
 *
 * @since 1.1.5
 * @param array $entry Mail log entry.
 * @return void
 */
function wstl_log_mail_result( array $entry ): void {
	$log = get_option( 'trustlens_email_log', array() );
	if ( ! is_array( $log ) ) {
		$log = array();
	}

	$log[] = array(
		'timestamp'  => current_time( 'mysql' ),
		'success'    => ! empty( $entry['success'] ),
		'context'    => sanitize_key( (string) ( $entry['context'] ?? 'general' ) ),
		'recipients' => array_values( array_map( 'sanitize_email', $entry['recipients'] ?? array() ) ),
		'subject'    => sanitize_text_field( (string) ( $entry['subject'] ?? '' ) ),
		'error'      => sanitize_text_field( (string) ( $entry['error'] ?? '' ) ),
	);

	$log = array_slice( $log, -50 );
	update_option( 'trustlens_email_log', $log, false );
}

/**
 * Send email with TrustLens defaults and structured error reporting.
 *
 * @since 1.1.5
 * @param string|array $to      Recipient email or array of emails.
 * @param string       $subject Email subject.
 * @param string       $message Email body.
 * @param array        $headers Optional headers.
 * @param array        $context Optional metadata for logging.
 * @return array Structured send result.
 */
function wstl_send_mail( $to, string $subject, string $message, array $headers = array(), array $context = array() ): array {
	$recipients = is_array( $to ) ? $to : array( $to );
	$recipients = array_values( array_filter( array_map( 'sanitize_email', $recipients ), 'is_email' ) );

	if ( empty( $recipients ) ) {
		$result = array(
			'success'    => false,
			'recipients' => array(),
			'error'      => __( 'No valid recipient email address was provided.', 'trustlens' ),
			'context'    => sanitize_key( (string) ( $context['context'] ?? 'general' ) ),
		);
		wstl_log_mail_result(
			array(
				'success'    => false,
				'context'    => $result['context'],
				'recipients' => array(),
				'subject'    => $subject,
				'error'      => $result['error'],
			)
		);
		do_action( 'trustlens/mail_result', $result, $context );
		return $result;
	}

	$headers = array_values( array_unique( array_merge( wstl_get_default_mail_headers(), $headers ) ) );

	$mail_error = null;
	$error_hook = static function( WP_Error $error ) use ( &$mail_error ): void {
		$mail_error = $error;
	};

	add_action( 'wp_mail_failed', $error_hook );
	$sent = wp_mail( $recipients, $subject, $message, $headers );
	remove_action( 'wp_mail_failed', $error_hook );

	$result = array(
		'success'    => (bool) $sent,
		'recipients' => $recipients,
		'error'      => $sent ? '' : wstl_get_mail_error_message( $mail_error ),
		'context'    => sanitize_key( (string) ( $context['context'] ?? 'general' ) ),
	);

	wstl_log_mail_result(
		array(
			'success'    => $result['success'],
			'context'    => $result['context'],
			'recipients' => $recipients,
			'subject'    => $subject,
			'error'      => $result['error'],
		)
	);

	do_action( 'trustlens/mail_result', $result, $context );

	return $result;
}

/**
 * Get segment from trust score.
 *
 * @since 1.0.0
 * @param int $score Trust score.
 * @return string Segment name.
 */
function wstl_get_segment_from_score( int $score ): string {
	$thresholds = get_option( 'trustlens_segment_thresholds', array() );

	$defaults = array(
		'vip'      => 90,
		'trusted'  => 70,
		'normal'   => 50,
		'caution'  => 30,
		'risk'     => 10,
		'critical' => 0,
	);

	$thresholds = wp_parse_args( $thresholds, $defaults );

	/**
	 * Filter the per-segment trust-score thresholds.
	 *
	 * @since 1.0.0
	 * @param array $thresholds Map of segment => minimum score.
	 */
	$thresholds = apply_filters( 'trustlens/segment_thresholds', $thresholds );

	// Re-apply defaults after the filter so a partial (or non-array) return
	// value can never produce undefined-index access / mis-segmentation below.
	$thresholds = wp_parse_args( is_array( $thresholds ) ? $thresholds : array(), $defaults );

	if ( $score >= $thresholds['vip'] ) {
		return 'vip';
	}
	if ( $score >= $thresholds['trusted'] ) {
		return 'trusted';
	}
	if ( $score >= $thresholds['normal'] ) {
		return 'normal';
	}
	if ( $score >= $thresholds['caution'] ) {
		return 'caution';
	}
	if ( $score >= $thresholds['risk'] ) {
		return 'risk';
	}

	return 'critical';
}

/**
 * Get segment display label.
 *
 * @since 1.0.0
 * @param string $segment Segment key.
 * @return string Display label.
 */
function wstl_get_segment_label( string $segment ): string {
	$labels = array(
		'vip'      => __( 'VIP', 'trustlens' ),
		'trusted'  => __( 'Trusted', 'trustlens' ),
		'normal'   => __( 'Normal', 'trustlens' ),
		'caution'  => __( 'Caution', 'trustlens' ),
		'risk'     => __( 'Risk', 'trustlens' ),
		'critical' => __( 'Critical', 'trustlens' ),
	);

	return $labels[ $segment ] ?? ucfirst( $segment );
}

/**
 * Get segment color.
 *
 * @since 1.0.0
 * @param string $segment Segment key.
 * @return string Hex color.
 */
function wstl_get_segment_color( string $segment ): string {
	$colors = array(
		'vip'      => '#28a745',
		'trusted'  => '#17a2b8',
		'normal'   => '#6c757d',
		'caution'  => '#ffc107',
		'risk'     => '#fd7e14',
		'critical' => '#dc3545',
	);

	return $colors[ $segment ] ?? '#6c757d';
}

/**
 * Render an accessible inline help tooltip (a small "?" bubble).
 *
 * The returned markup is already escaped; echo it directly. The explanation
 * shows on hover and on keyboard focus, and is exposed to assistive tech via
 * aria-label.
 *
 * @since 1.2.6
 * @param string $text Plain-text explanation.
 * @return string Escaped HTML for the tooltip trigger.
 */
function wstl_help_tip( string $text ): string {
	$text = wp_strip_all_tags( $text );
	if ( '' === $text ) {
		return '';
	}
	return sprintf(
		'<span class="wstl-help" tabindex="0" role="img" aria-label="%1$s" data-wstl-tip="%1$s">?</span>',
		esc_attr( $text )
	);
}

/**
 * Get active modules.
 *
 * @since 1.0.0
 * @return array Active module instances.
 */
function wstl_get_active_modules(): array {
	return TrustLens::instance()->get_active_modules();
}

/**
 * Calculate days between two dates.
 *
 * @since 1.0.0
 * @param mixed $start Start date.
 * @param mixed $end   End date.
 * @return int Number of days.
 */
function wstl_days_between( $start, $end ): int {
	$start_time = is_numeric( $start ) ? $start : strtotime( $start );
	$end_time = is_numeric( $end ) ? $end : strtotime( $end );

	return (int) floor( abs( $end_time - $start_time ) / DAY_IN_SECONDS );
}

/**
 * Delete a customer and all related data.
 *
 * @since 1.1.0
 * @param string $email_hash Customer email hash.
 * @return bool True on success.
 */
function wstl_delete_customer( string $email_hash ): bool {
	global $wpdb;

	$customers_table = $wpdb->prefix . 'trustlens_customers';
	$events_table = $wpdb->prefix . 'trustlens_events';
	$signals_table = $wpdb->prefix . 'trustlens_signals';
	$fingerprints_table = $wpdb->prefix . 'trustlens_fingerprints';
	$category_stats_table = $wpdb->prefix . 'trustlens_category_stats';
	$automation_logs_table = $wpdb->prefix . 'trustlens_automation_logs';

	// Delete customer record.
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from $wpdb->prefix, safe.
	$deleted = $wpdb->delete(
		$customers_table,
		array( 'email_hash' => $email_hash ),
		array( '%s' )
	);

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

	// Delete related events.
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from $wpdb->prefix, safe.
	$wpdb->delete( $events_table, array( 'email_hash' => $email_hash ), array( '%s' ) );

	// Delete related signals.
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from $wpdb->prefix, safe.
	$wpdb->delete( $signals_table, array( 'email_hash' => $email_hash ), array( '%s' ) );

	// Delete fingerprints.
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from $wpdb->prefix, safe.
	$wpdb->delete( $fingerprints_table, array( 'email_hash' => $email_hash ), array( '%s' ) );

	// Delete category stats.
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from $wpdb->prefix, safe.
	$wpdb->delete( $category_stats_table, array( 'email_hash' => $email_hash ), array( '%s' ) );

	// Delete automation logs.
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from $wpdb->prefix, safe.
	$wpdb->delete( $automation_logs_table, array( 'email_hash' => $email_hash ), array( '%s' ) );

	/**
	 * Fires after a customer is deleted.
	 *
	 * @since 1.1.0
	 * @param string $email_hash Deleted customer's email hash.
	 */
	do_action( 'trustlens/customer_deleted', $email_hash );

	return true;
}

/**
 * Reset all operational TrustLens data while keeping plugin configuration intact.
 *
 * This clears customer intelligence data, derived analytics tables, runtime logs,
 * and plugin-owned transients so the plugin can rebuild from a clean state.
 *
 * @since 1.1.5
 * @return bool True when all reset queries completed successfully.
 */
function wstl_reset_plugin_data(): bool {
	global $wpdb;

	$tables = array(
		$wpdb->prefix . 'trustlens_customers',
		$wpdb->prefix . 'trustlens_events',
		$wpdb->prefix . 'trustlens_signals',
		$wpdb->prefix . 'trustlens_category_stats',
		$wpdb->prefix . 'trustlens_fingerprints',
		$wpdb->prefix . 'trustlens_webhook_logs',
		$wpdb->prefix . 'trustlens_automation_logs',
		$wpdb->prefix . 'trustlens_network_flags',
	);

	foreach ( $tables as $table ) {
		// Skip tables that don't exist (Pro-only tables on free installs).
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SHOW TABLES check, table name from $wpdb->prefix.
		$exists = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table ) );
		if ( ! $exists ) {
			continue;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- TRUNCATE requires direct query, table names are plugin-owned and built from $wpdb->prefix.
		$result = $wpdb->query( "TRUNCATE TABLE {$table}" );
		if ( false === $result ) {
			return false;
		}
	}

	if ( ! wstl_clear_plugin_transients() ) {
		return false;
	}

	delete_option( 'trustlens_sync_status' );
	delete_option( 'trustlens_welcome_summary_sent' );
	delete_option( '_trustlens_test_data' );
	update_option( 'trustlens_report_log', array() );
	update_option( 'trustlens_email_log', array() );

	// Clear Store Trust Network operational sync state (per-peer high-water marks +
	// the status panel snapshot) so a post-reset sync re-pulls peers in full. The
	// group key and peer list are configuration and are intentionally preserved.
	delete_option( 'trustlens_network_sync_status' );
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Prefix delete of plugin-owned options.
	$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( 'trustlens_network_peer_hwm_' ) . '%' ) );

	return true;
}

/**
 * Bump the customer-stats cache generation so the dashboard High-Risk list and
 * segment counts refresh on the next read.
 *
 * Call after a change that affects those generation-scoped caches — a segment
 * change, or an allowlist add/remove (which moves a customer in or out of the
 * high-risk query). Mirrors the direct invalidation the orders and returns
 * modules already perform when they update customer stats.
 *
 * @since 1.3.0
 * @return void
 */
function wstl_invalidate_customer_caches(): void {
	if ( class_exists( 'TrustLens_Customer_Repository' ) ) {
		( new TrustLens_Customer_Repository() )->invalidate_caches();
	}
}

/**
 * Clear plugin-owned transients and cached query results.
 *
 * @since 1.1.5
 * @return bool True on success.
 */
function wstl_clear_plugin_transients(): bool {
	global $wpdb;

	// Bump the cache generation so generation-scoped transients (high-risk
	// lists under any limit, segment counts) are invalidated even on sites
	// using an external object cache that the bulk DELETE below can't reach.
	if ( class_exists( 'TrustLens_Customer_Repository' ) ) {
		( new TrustLens_Customer_Repository() )->invalidate_caches();
	}

	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk delete plugin-owned transients by fixed prefix.
	$deleted = $wpdb->query(
		"DELETE FROM {$wpdb->options}
		 WHERE option_name LIKE '_transient_wstl_%'
		    OR option_name LIKE '_transient_timeout_wstl_%'
		    OR option_name LIKE '_transient_trustlens_%'
		    OR option_name LIKE '_transient_timeout_trustlens_%'"
	);

	return false !== $deleted;
}

/**
 * Check if Pro license is active.
 *
 * Uses Freemius __premium_only method to strip from free version.
 *
 * @since 1.1.5
 * @return bool True if Pro is active.
 */
function wstl_is_pro(): bool {
	if ( function_exists( 'wstl_fs' ) ) {
		$fs = wstl_fs();
		if ( is_object( $fs ) ) {
			if ( method_exists( $fs, 'can_use_premium_code__premium_only' ) && $fs->can_use_premium_code__premium_only() ) {
				return true;
			}
			if ( method_exists( $fs, 'can_use_premium_code' ) && $fs->can_use_premium_code() ) {
				return true;
			}
		}
	}

	return (bool) apply_filters( 'trustlens/is_pro_active', false );
}

/**
 * Check if Pro features can run on this site.
 *
 * Requires both:
 * - Pro code present in current build.
 * - Active paid license.
 *
 * @since 1.1.5
 * @return bool True if Pro features are available and licensed.
 */
function wstl_can_use_pro(): bool {
	return wstl_has_pro_features() && wstl_is_pro();
}

/**
 * Canonical default value for every TrustLens option.
 *
 * Single source of truth so the install seeder, the settings forms, and the
 * runtime consumers all agree on the same default — preventing "shown default
 * != applied default" drift. Read a value via wstl_get_setting().
 *
 * @since 1.3.2
 * @return array<string,mixed> Map of option name => default value.
 */
function wstl_setting_defaults(): array {
	$can_use_pro = function_exists( 'wstl_can_use_pro' ) ? wstl_can_use_pro() : false;
	$admin_email = get_option( 'admin_email' );

	return array(
		// General.
		'trustlens_min_orders'                                 => 3,
		'trustlens_enable_blocking'                            => false,
		'trustlens_enable_dashboard'                           => true,
		'trustlens_enable_order_warning'                       => true,
		'trustlens_hold_segments'                              => array( 'risk', 'critical' ),
		'trustlens_block_message'                              => __( 'We are unable to process your order at this time. Please contact support for assistance.', 'trustlens' ),
		'trustlens_segment_thresholds'                         => array(
			'vip'      => 90,
			'trusted'  => 70,
			'normal'   => 50,
			'caution'  => 30,
			'risk'     => 10,
			'critical' => 0,
		),
		'trustlens_returns_high_threshold'                     => 40,
		'trustlens_returns_critical_threshold'                 => 60,
		// Modules.
		'trustlens_module_returns_enabled'                     => true,
		'trustlens_module_orders_enabled'                      => true,
		'trustlens_module_coupons_enabled'                     => true,
		'trustlens_module_categories_enabled'                  => true,
		'trustlens_module_chargebacks_enabled'                 => true,
		'trustlens_module_linked_accounts_enabled'             => true,
		'trustlens_linked_accounts_penalty'                    => 5,
		'trustlens_module_shipping_anomalies_enabled'          => true,
		'trustlens_shipping_anomalies_velocity_days'           => 30,
		'trustlens_shipping_anomalies_trend_enabled'           => true,
		'trustlens_coupons_block_linked_abuse'                 => false,
		'trustlens_coupons_max_first_order'                    => 2,
		'trustlens_module_card_testing_enabled'                => true,
		'trustlens_card_testing_bypass_vip_customers'          => true,
		// Chargebacks (Pro).
		'trustlens_chargebacks_auto_block'                     => 0, // Off by default; auto-blocking real customers is opt-in.
		'trustlens_chargeback_alert_threshold_pct'             => 70,
		'trustlens_report_verification'                        => true,
		// Store Trust Network (Pro, multi-site). Opt-in, off by default.
		'trustlens_module_store_network_enabled'               => false,
		'trustlens_network_group_key'                          => '',
		'trustlens_network_peers'                              => '',
		'trustlens_network_share_flags'                        => true,
		'trustlens_network_caution_penalty'                    => 15,
		'trustlens_network_high_penalty'                       => 35,
		// Notifications.
		'trustlens_enable_notifications'                       => false,
		'trustlens_notification_email'                         => $admin_email,
		'trustlens_notify_blocked_checkout'                    => true,
		'trustlens_notify_welcome_summary'                     => true,
		'trustlens_notify_weekly_summary'                      => true,
		'trustlens_notify_high_risk_order'                     => $can_use_pro,
		'trustlens_notify_segment_critical'                    => $can_use_pro,
		'trustlens_enable_daily_digest'                        => false,
		'trustlens_notify_high_value_order'                    => $can_use_pro,
		'trustlens_notify_high_value_threshold'                => 500,
		'trustlens_notify_high_value_score'                    => 30,
		'trustlens_notify_repeat_refunder'                     => $can_use_pro,
		'trustlens_notify_repeat_refunder_count'               => 3,
		'trustlens_notify_repeat_refunder_days'                => 30,
		'trustlens_notify_velocity'                            => $can_use_pro,
		'trustlens_notify_velocity_count'                      => 3,
		'trustlens_notify_velocity_hours'                      => 24,
		'trustlens_notify_score_recovery'                      => $can_use_pro,
		'trustlens_notify_new_customer_risk'                   => $can_use_pro,
		'trustlens_notify_new_customer_risk_score'             => 25,
		'trustlens_notify_monthly_report'                      => false,
		'trustlens_notify_chargeback_filed'                    => $can_use_pro,
		// Automation (Pro).
		'trustlens_enable_automation'                          => false,
		'trustlens_automation_rules'                           => array(),
		// Webhooks (Pro).
		'trustlens_enable_webhooks'                            => false,
		'trustlens_webhook_endpoints'                          => array(),
		'trustlens_webhook_events'                             => array( 'score_updated', 'customer_blocked', 'checkout_blocked', 'high_risk_order' ),
		// Payment method controls (Pro).
		'trustlens_enable_payment_method_controls'            => false,
		'trustlens_payment_method_control_segments'            => array( 'risk', 'critical' ),
		'trustlens_payment_method_control_gateways'            => array(),
		'trustlens_payment_method_control_min_total'           => 0,
		'trustlens_payment_method_control_message'             => __( 'Some payment methods are unavailable for this order. Please choose another payment option.', 'trustlens' ),
		'trustlens_payment_method_control_velocity_enabled'    => false,
		'trustlens_payment_method_control_velocity_count'      => 3,
		'trustlens_payment_method_control_velocity_hours'      => 24,
		'trustlens_payment_method_control_linked_risk_enabled' => false,
		// Scheduled reports (Pro).
		'trustlens_enable_scheduled_reports'                   => false,
		'trustlens_weekly_report_recipients'                   => $admin_email,
		'trustlens_monthly_report_recipients'                  => $admin_email,
		'trustlens_report_schedules'                           => array(),
		'trustlens_report_log'                                 => array(),
	);
}

/**
 * Read a TrustLens option, falling back to its canonical default.
 *
 * Use this instead of get_option( 'trustlens_*', <inline default> ) so the
 * default comes from the single source (wstl_setting_defaults()) and can't drift.
 *
 * @since 1.3.2
 * @param string $key Option name.
 * @return mixed Stored value, or the canonical default when the option is unset.
 */
function wstl_get_setting( string $key ) {
	$defaults = wstl_setting_defaults();
	return get_option( $key, array_key_exists( $key, $defaults ) ? $defaults[ $key ] : false );
}

/**
 * Check if Webhooks feature is present in this build (no license check).
 * Used for WordPress.org: free build has no Pro code; show only promotional upsell when false.
 *
 * @since 1.0.0
 * @return bool True if TrustLens_Webhooks class exists.
 */
function wstl_has_webhooks(): bool {
	return class_exists( 'TrustLens_Webhooks' );
}

/**
 * Check if Automation feature is present in this build (no license check).
 *
 * @since 1.0.0
 * @return bool True if TrustLens_Automation class exists.
 */
function wstl_has_automation(): bool {
	return class_exists( 'TrustLens_Automation' );
}

/**
 * Check if Scheduled Reports feature is present in this build (no license check).
 *
 * @since 1.0.0
 * @return bool True if TrustLens_Scheduled_Reports class exists.
 */
function wstl_has_scheduled_reports(): bool {
	return class_exists( 'TrustLens_Scheduled_Reports' );
}

/**
 * Check if Chargebacks module is present in this build (no license check).
 *
 * @since 1.0.0
 * @return bool True if TrustLens_Module_Chargebacks class exists.
 */
function wstl_has_chargebacks(): bool {
	return class_exists( 'TrustLens_Module_Chargebacks' );
}

/**
 * Check if Payment Method Risk Controls feature is present in this build (no license check).
 *
 * @since 1.0.6
 * @return bool True if TrustLens_Payment_Method_Controls class exists.
 */
function wstl_has_payment_method_controls(): bool {
	return class_exists( 'TrustLens_Payment_Method_Controls' );
}

/**
 * Check if this build contains Pro code (no license check).
 * Used to show Pro UI/options only when Pro code is in the build (WordPress.org compliant).
 *
 * Driven by the single premium-only marker (includes/pro-marker.php), which is
 * stripped from the free build alongside the rest of the @fs_premium_only files.
 * This replaces a hand-maintained class_exists() OR-chain that drifted whenever a
 * feature changed tier — e.g. the chargebacks module moving to Free wrongly made
 * the chain report "has Pro code" in free builds. The per-feature wstl_has_*()
 * helpers remain for deciding individual feature UI; only this aggregate is
 * marker-driven so it can never misclassify a free feature as Pro.
 *
 * @since 1.0.0
 * @return bool True if Pro code is present in the current build.
 */
function wstl_has_pro_features(): bool {
	return defined( 'TRUSTLENS_IS_PRO_BUILD' );
}

/**
 * Whether to show Pro upsell content (cards, strip, locked lists).
 * True when the build has no Pro code (WordPress.org) OR when the user has a free license.
 * Ensures full-build + free-license (e.g. local dev) sees the same promotions as wp.org users.
 *
 * @since 1.1.5
 * @return bool True if upsells should be shown.
 */
function wstl_show_pro_upsells(): bool {
	return ! wstl_has_pro_features() || wstl_is_free_user();
}

/**
 * Check if user is not paying (for showing upsells).
 *
 * @since 1.1.5
 * @return bool True if user is on free plan.
 */
function wstl_is_free_user(): bool {
	if ( ! function_exists( 'wstl_fs' ) ) {
		return true;
	}

	return wstl_fs()->is_not_paying();
}

/**
 * Get upgrade URL for upsell links.
 *
 * @since 1.1.5
 * @return string Upgrade URL.
 */
function wstl_get_upgrade_url(): string {
	if ( ! function_exists( 'wstl_fs' ) ) {
		return '';
	}

	return wstl_fs()->get_upgrade_url();
}

/**
 * Get fallback URL when Freemius upgrade URL is not available.
 * Used so CTAs are never dead (e.g. on WordPress.org build).
 *
 * @since 1.1.5
 * @return string Fallback URL (plugin/pricing page).
 */
function wstl_get_upgrade_fallback_url(): string {
	return apply_filters( 'trustlens_upgrade_fallback_url', 'https://webstepper.io/wordpress/plugins/trustlens' );
}

/**
 * Get upgrade URL or fallback for use in upsell links.
 * Ensures free users always have a clickable CTA.
 *
 * @since 1.1.5
 * @return string Upgrade or fallback URL.
 */
function wstl_get_upgrade_or_fallback_url(): string {
	$url = wstl_get_upgrade_url();
	return $url ? $url : wstl_get_upgrade_fallback_url();
}

/**
 * Get the canonical documentation URL for a TrustLens admin page.
 *
 * Maps each feature page (and each Settings sub-tab) to a specific, live
 * BetterDocs article or section landing under the TrustLens docs hub
 * (https://webstepper.io/docs/trustlens/). Every target below was verified to
 * resolve (HTTP 200, real page) against the live site. Unknown pages/tabs fall
 * back to the docs hub, which also resolves.
 *
 * @since 1.3.0
 * @param string $page Admin page slug (e.g. 'trustlens-card-testing').
 * @param string $tab  Optional Settings sub-tab slug for per-tab docs.
 * @return string Full documentation URL.
 */
function wstl_get_docs_url( string $page = '', string $tab = '' ): string {
	$base = 'https://webstepper.io/docs/trustlens/';

	// Settings is handled per sub-tab. Each tab maps to its dedicated
	// "setting-configuration" doc; an unknown tab falls back to the docs hub.
	if ( 'trustlens-settings' === $page ) {
		$tab_map = array(
			'general'          => 'setting-configuration/trustlens-general-settings/',
			'modules'          => 'setting-configuration/modules-settings/',
			'payment-controls' => 'setting-configuration/payment-controls/',
			'webhooks'         => 'setting-configuration/api-and-webhooks/',
			'reports'          => 'setting-configuration/reports-settings/',
			'api'              => 'setting-configuration/api-and-webhooks/',
			'data'             => 'setting-configuration/data-and-privacy/',
		);
		return $base . ( isset( $tab_map[ $tab ] ) ? $tab_map[ $tab ] : '' );
	}

	$map = array(
		'trustlens'                    => 'getting-started/dashboard-overview/',
		'trustlens-customers'          => 'customer-management/',
		'trustlens-notifications'      => 'notifications/',
		'trustlens-automation'         => 'automation/',
		'trustlens-card-testing'       => 'card-testing-defense/',
		'trustlens-chargeback-monitor' => 'chargeback-monitor/',
	);
	return $base . ( isset( $map[ $page ] ) ? $map[ $page ] : '' );
}

/**
 * Format a dispute response deadline into a label + severity band.
 *
 * Pure function (no DB) so it is unit-testable. Severity drives the worklist
 * countdown colour.
 *
 * @since 1.3.0
 * @param string|null $due_by MySQL datetime (UTC) or null.
 * @param int|null    $now    Reference timestamp; defaults to time().
 * @return array{label:string,severity:string,days:?int}
 */
function wstl_format_due_countdown( ?string $due_by, ?int $now = null ): array {
	if ( empty( $due_by ) ) {
		return array(
			'label'    => __( 'No deadline', 'trustlens' ),
			'severity' => 'none',
			'days'     => null,
		);
	}

	$now      = null === $now ? time() : $now;
	$deadline = strtotime( $due_by . ' UTC' );
	if ( false === $deadline ) {
		return array(
			'label'    => __( 'No deadline', 'trustlens' ),
			'severity' => 'none',
			'days'     => null,
		);
	}

	$diff = $deadline - $now;
	$days = (int) floor( abs( $diff ) / DAY_IN_SECONDS );

	if ( $diff < 0 ) {
		return array(
			/* translators: %d: days overdue */
			'label'    => sprintf( _n( 'Overdue by %d day', 'Overdue by %d days', max( 1, $days ), 'trustlens' ), max( 1, $days ) ),
			'severity' => 'overdue',
			'days'     => $days,
		);
	}

	if ( $diff <= 2 * DAY_IN_SECONDS ) {
		$severity = 'urgent';
	} elseif ( $diff <= 5 * DAY_IN_SECONDS ) {
		$severity = 'soon';
	} else {
		$severity = 'normal';
	}

	return array(
		/* translators: %d: days remaining */
		'label'    => 0 === $days ? __( 'Due today', 'trustlens' ) : sprintf( _n( 'Due in %d day', 'Due in %d days', $days, 'trustlens' ), $days ),
		'severity' => $severity,
		'days'     => $days,
	);
}

/**
 * Get open disputes for the worklist (Pro only).
 *
 * @since 1.3.0
 * @param int $limit Max rows.
 * @return array Row objects, or empty array when Pro is inactive.
 */
function wstl_get_open_disputes( int $limit = 100 ): array {
	if ( ! function_exists( 'wstl_can_use_pro' ) || ! wstl_can_use_pro() || ! class_exists( 'TrustLens_Dispute_Repository' ) ) {
		return array();
	}

	return ( new TrustLens_Dispute_Repository() )->get_open_disputes( $limit );
}

/**
 * Get checkout URL for direct purchase links.
 *
 * @since 1.1.5
 * @param array $params Optional checkout parameters.
 * @return string Checkout URL.
 */
function wstl_get_checkout_url( array $params = array() ): string {
	if ( ! function_exists( 'wstl_fs' ) ) {
		return '';
	}

	return wstl_fs()->checkout_url( '', $params );
}

/**
 * Render Pro badge with optional upsell link.
 *
 * @since 1.1.5
 * @param bool $with_link Include upgrade link for free users.
 * @return string HTML badge.
 */
function wstl_pro_badge( bool $with_link = true ): string {
	$html = '<span class="wstl-badge-pro">Pro</span>';

	if ( $with_link && wstl_is_free_user() ) {
		$url = wstl_get_upgrade_or_fallback_url();
		if ( $url ) {
			$html = sprintf( '<a href="%s" class="wstl-pro-link">%s</a>', esc_url( $url ), $html );
		}
	}

	return $html;
}

/**
 * Get trigger label for automation rules.
 *
 * @since 1.1.0
 * @param string $trigger Trigger key.
 * @return string Label.
 */
function wstl_get_trigger_label( string $trigger ): string {
	$triggers = array(
		'score_updated'              => __( 'Score Updated', 'trustlens' ),
		'segment_changed'            => __( 'Segment Changed', 'trustlens' ),
		'checkout_blocked'           => __( 'Checkout Blocked', 'trustlens' ),
		'shipping_anomaly'           => __( 'Shipping Anomaly', 'trustlens' ),
		'order_placed'               => __( 'Order Placed', 'trustlens' ),
		'order_completed'            => __( 'Order Completed', 'trustlens' ),
		'refund_processed'           => __( 'Refund Processed', 'trustlens' ),
		'dispute_recorded'           => __( 'Dispute Recorded', 'trustlens' ),
		'chargeback_filed'           => __( 'Chargeback Filed', 'trustlens' ),
		'linked_accounts_detected'   => __( 'Linked Accounts Detected', 'trustlens' ),
		'card_testing_attack'        => __( 'Card Testing Attack', 'trustlens' ),
		'customer_blocked'           => __( 'Customer Blocked', 'trustlens' ),
		'customer_unblocked'         => __( 'Customer Unblocked', 'trustlens' ),
		'customer_allowlisted'       => __( 'Customer Allowlisted', 'trustlens' ),
		'customer_allowlist_removed' => __( 'Customer Allowlist Removed', 'trustlens' ),
	);
	return $triggers[ $trigger ] ?? ucfirst( str_replace( '_', ' ', $trigger ) );
}

/**
 * Get action label for automation rules.
 *
 * @since 1.1.0
 * @param string $action Action key.
 * @return string Label.
 */
function wstl_get_action_label( string $action ): string {
	$actions = array(
		'send_email'           => __( 'Send Email', 'trustlens' ),
		'add_note'             => __( 'Add Note', 'trustlens' ),
		'add_tag'              => __( 'Add Tag', 'trustlens' ),
		'block_customer'       => __( 'Block Customer', 'trustlens' ),
		'allowlist_customer'   => __( 'Allowlist Customer', 'trustlens' ),
		'flag_review'          => __( 'Flag for Review', 'trustlens' ),
		'flag_order'           => __( 'Flag for Review', 'trustlens' ),
		'send_webhook'         => __( 'Send Webhook', 'trustlens' ),
		'hold_order'           => __( 'Hold Order', 'trustlens' ),
		'cancel_order'         => __( 'Cancel Order', 'trustlens' ),
		'require_verification' => __( 'Require Verification', 'trustlens' ),
	);
	return $actions[ $action ] ?? ucfirst( str_replace( '_', ' ', $action ) );
}

/**
 * Get report type label for scheduled reports.
 *
 * @since 1.1.0
 * @param string $type Report type.
 * @return string Label.
 */
function wstl_get_report_type_label( string $type ): string {
	$types = array(
		'summary'          => __( 'Summary Report', 'trustlens' ),
		'high_risk'        => __( 'High-Risk Customers', 'trustlens' ),
		'segment_changes'  => __( 'Segment Changes', 'trustlens' ),
		'blocked_attempts' => __( 'Blocked Attempts', 'trustlens' ),
		'trends'           => __( 'Trust Trends', 'trustlens' ),
	);
	return $types[ $type ] ?? ucfirst( str_replace( '_', ' ', $type ) );
}

/**
 * Render a Pro upsell panel for gated features.
 *
 * Provides a consistent, in-context upsell pattern with:
 * - Value highlights.
 * - Optional Free vs Pro comparison rows.
 * - Primary and secondary CTAs.
 *
 * @since 1.1.5
 * @param string      $feature_name    Feature name to display.
 * @param string      $description     Feature description.
 * @param array       $features        Value bullets (benefits in Pro).
 * @param array       $comparison_rows Optional comparison rows.
 *                                     Each row format: [ label, free_value, pro_value ].
 * @param string      $kicker          Optional eyebrow text above title.
 * @param string|null $trust_line      Optional trust line under CTA.
 */
function wstl_render_pro_upsell(
	string $feature_name,
	string $description,
	array $features = array(),
	array $comparison_rows = array(),
	string $kicker = '',
	?string $trust_line = null,
	array $opts = array()
): void {
	$upgrade_url = isset( $opts['upgrade_url'] ) ? (string) $opts['upgrade_url'] : wstl_get_upgrade_or_fallback_url();
	$kicker      = $kicker ? $kicker : __( 'Available in Pro', 'trustlens' );
	if ( null === $trust_line ) {
		$trust_line = __( 'Secure checkout powered by Freemius. Cancel anytime.', 'trustlens' );
	}
	// Overridable labels. Defaults preserve the free-to-Pro wording; multi-site
	// (Professional/Business) tabs pass a "you already have Pro, upgrade your
	// plan" variant by supplying $opts.
	$badge_text    = array_key_exists( 'badge', $opts ) ? (string) $opts['badge'] : __( 'Pro', 'trustlens' );
	$compare_free  = isset( $opts['compare_free_label'] ) ? (string) $opts['compare_free_label'] : __( 'Free', 'trustlens' );
	$compare_pro   = isset( $opts['compare_pro_label'] ) ? (string) $opts['compare_pro_label'] : __( 'Pro', 'trustlens' );
	$primary_cta   = isset( $opts['primary_cta'] ) ? (string) $opts['primary_cta'] : __( 'Upgrade to Pro', 'trustlens' );
	$secondary_cta = isset( $opts['secondary_cta'] ) ? (string) $opts['secondary_cta'] : __( 'See Pro features', 'trustlens' );
	?>
	<div class="wstl-card wstl-pro-upsell">
		<div class="wstl-pro-upsell-content">
			<p class="wstl-pro-kicker">
				<?php echo esc_html( $kicker ); ?>
				<?php if ( '' !== $badge_text ) : ?>
					<span class="wstl-badge-pro"><?php echo esc_html( $badge_text ); ?></span>
				<?php endif; ?>
			</p>
			<h2><?php echo esc_html( $feature_name ); ?></h2>
			<p class="wstl-pro-upsell-description"><?php echo esc_html( $description ); ?></p>

			<?php if ( ! empty( $features ) ) : ?>
				<ul class="wstl-pro-upsell-features" role="list">
					<?php foreach ( $features as $f ) : ?>
						<li>
							<span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>
							<span><?php echo esc_html( $f ); ?></span>
						</li>
					<?php endforeach; ?>
				</ul>
			<?php endif; ?>

			<?php if ( ! empty( $comparison_rows ) ) : ?>
				<div class="wstl-pro-compare" role="table" aria-label="<?php esc_attr_e( 'Free vs Pro comparison', 'trustlens' ); ?>">
					<div class="wstl-pro-compare-head" role="row">
						<div role="columnheader"></div>
						<div role="columnheader"><?php echo esc_html( $compare_free ); ?></div>
						<div role="columnheader"><?php echo esc_html( $compare_pro ); ?></div>
					</div>
					<?php foreach ( $comparison_rows as $row ) : ?>
						<?php
						$label = isset( $row[0] ) ? (string) $row[0] : '';
						$free  = isset( $row[1] ) ? (string) $row[1] : '';
						$pro   = isset( $row[2] ) ? (string) $row[2] : '';
						?>
						<div class="wstl-pro-compare-row" role="row">
							<div role="cell"><?php echo esc_html( $label ); ?></div>
							<div role="cell"><?php echo esc_html( $free ); ?></div>
							<div role="cell"><?php echo esc_html( $pro ); ?></div>
						</div>
					<?php endforeach; ?>
				</div>
			<?php endif; ?>

			<?php if ( $upgrade_url ) : ?>
				<div class="wstl-pro-cta-group">
					<a href="<?php echo esc_url( $upgrade_url ); ?>" class="button button-primary button-hero wstl-pro-upsell-button">
						<?php echo esc_html( $primary_cta ); ?>
					</a>
					<a href="<?php echo esc_url( $upgrade_url ); ?>" class="button wstl-pro-upsell-secondary">
						<?php echo esc_html( $secondary_cta ); ?>
					</a>
				</div>
				<p class="wstl-pro-upsell-trust"><?php echo esc_html( $trust_line ); ?></p>
			<?php endif; ?>
		</div>
	</div>
	<?php
}

/**
 * Render the status chips for the unified page bar.
 *
 * Emits a date chip, a colored state pill, and/or free-text description — any
 * combination of the three. Shared by wstl_render_page_header() to surface a
 * short "what you're looking at" line of vital signs.
 *
 * Item shape:
 *   array( 'type' => 'date',  'value' => 'April 17, 2026' )
 *   array( 'type' => 'state', 'value' => 'Idle', 'level' => 'good' )  // good|warn|danger|info
 *   array( 'type' => 'text',  'value' => 'No active attack.' )
 *
 * @since 1.2.2
 * @param array<int,array<string,string>> $items Ordered meta items.
 * @return void
 */
function wstl_render_page_meta_chips( array $items ): void {
	foreach ( $items as $item ) {
		$type  = isset( $item['type'] ) ? (string) $item['type'] : 'text';
		$value = isset( $item['value'] ) ? (string) $item['value'] : '';
		if ( '' === $value ) {
			continue;
		}

		// Optional stable identifier so scripts can target a specific chip
		// (e.g. toggle the "Blocked" pill in place) regardless of locale.
		$key       = isset( $item['key'] ) ? sanitize_html_class( (string) $item['key'] ) : '';
		$data_attr = '' !== $key ? sprintf( ' data-chip="%s"', esc_attr( $key ) ) : '';

		if ( 'date' === $type ) {
			$chip = '<span class="wstl-page-meta-date"' . $data_attr . '>'
				. '<svg class="wstl-page-meta-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>'
				. esc_html( $value )
				. '</span>';
		} elseif ( 'state' === $type ) {
			$level = isset( $item['level'] ) ? sanitize_html_class( (string) $item['level'] ) : 'good';
			$chip  = sprintf(
				'<span class="wstl-page-meta-pill wstl-page-meta-pill-%1$s"%3$s>%2$s</span>',
				esc_attr( $level ),
				esc_html( $value ),
				$data_attr // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Pre-escaped attribute string.
			);
		} else {
			$chip = sprintf( '<span class="wstl-page-meta-text"%2$s>%1$s</span>', esc_html( $value ), $data_attr ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $data_attr pre-escaped; value escaped.
		}

		// Optional deep-link: a chip that names a state can take you to it.
		$href = isset( $item['href'] ) ? (string) $item['href'] : '';
		if ( '' !== $href ) {
			printf( '<a class="wstl-chip-link" href="%s">%s</a>', esc_url( $href ), $chip ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $chip is built from escaped parts above.
		} else {
			echo $chip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built from escaped parts above.
		}
	}
}

/**
 * Render the unified TrustLens page bar: the page name, live status chips, and
 * an optional primary action. A light card that aligns to the same edges as the
 * dark plugin header and sits directly beneath it, so every screen reads as one
 * continuous unit — name on the left, vital signs in the middle, action on the
 * right.
 *
 * @since 1.3.0
 * @param array $args {
 *     @type string $title   Page name (required).
 *     @type array  $meta    Optional status chips (same shape as wstl_render_page_meta_chips()).
 *     @type string $actions Optional pre-escaped HTML for the primary action(s).
 * }
 * @return void
 */
function wstl_render_page_header( array $args ): void {
	$title    = isset( $args['title'] ) ? (string) $args['title'] : '';
	$subtitle = isset( $args['subtitle'] ) ? (string) $args['subtitle'] : '';
	$meta     = ( isset( $args['meta'] ) && is_array( $args['meta'] ) ) ? $args['meta'] : array();
	$actions  = isset( $args['actions'] ) ? (string) $args['actions'] : '';

	echo '<div class="wstl-pagebar">';
	echo '<div class="wstl-pagebar-id">';
	if ( '' !== $title ) {
		echo '<h1 class="wstl-pagebar-title">' . esc_html( $title ) . '</h1>';
	}
	if ( '' !== $subtitle ) {
		echo '<p class="wstl-pagebar-sub">' . esc_html( $subtitle ) . '</p>';
	}
	echo '</div>';
	if ( ! empty( $meta ) ) {
		echo '<div class="wstl-pagebar-meta">';
		wstl_render_page_meta_chips( $meta );
		echo '</div>';
	}
	if ( '' !== $actions ) {
		// Action markup is composed and escaped by the calling template.
		echo '<div class="wstl-pagebar-actions">' . $actions . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}
	echo '</div>';
}
