<?php

namespace Limb_Chatbot\Includes\Services\Notifications;

use Exception;
use Limb_Chatbot\Includes\Data_Objects\Chatbot;
use Limb_Chatbot\Includes\Data_Objects\Chatbot_User;
use Limb_Chatbot\Includes\Data_Objects\Setting;
use Limb_Chatbot\Includes\Interfaces\Notification_Type_Interface;
use Limb_Chatbot\Includes\Services\Helper;
use Limb_Chatbot\Includes\Services\User_Sync_Tracker;
use WP_User;

/**
 * Main entry point for the notification system.
 *
 * Queues notifications (via Action Scheduler), checks whether a notification should be sent
 * (type + role + user/global opt-in), and processes queued items. Notifications are always sent to Chatbot_User.
 * Supports instant (send immediately) vs queued delivery per type.
 *
 * @package Limb_Chatbot\Includes\Services\Notifications
 * @since 1.0.15
 */
class Notification_Service {

	/**
	 * Canonical default value for `lbaic.plugin.notifications`.
	 *
	 * @return array
	 * @since 2.0.1
	 */
	public static function get_defaults(): array {
		return [
			'chat_created' => [
				'status' => 0,
				'visibility' => [
					'roles'          => [ 'administrator' ],
					'custom_emails' => [],
				],
				'email_template' => [
					'subject' => '[{{site_name}}] New chat created',
					'body'    => implode( '<br><br>', [
						__( 'A new chat was created on your site.', 'limb-chatbot' ),
						__( 'Visitor IP:', 'limb-chatbot' ) . ' {{visitor_ip}}',
						__( 'Chat ID:', 'limb-chatbot' ) . ' {{chat_id}}',
						__( 'Timestamp:', 'limb-chatbot' ) . ' {{timestamp}}',
						__( 'Conversation preview:', 'limb-chatbot' ),
						'{{chat_summary}}',
					] ),
				],
			],
			'lead_captured' => [
				'status' => 0,
				'visibility' => [
					'roles'          => [ 'administrator' ],
					'custom_emails' => [],
				],
				'email_template' => [
					'subject' => '[{{site_name}}] New lead captured',
					'body'    => implode( '<br><br>', [
						__( 'A new lead was captured on your site.', 'limb-chatbot' ),
						__( 'Visitor IP:', 'limb-chatbot' ) . ' {{visitor_ip}}',
						__( 'Chat ID:', 'limb-chatbot' ) . ' {{chat_id}}',
						__( 'Timestamp:', 'limb-chatbot' ) . ' {{timestamp}}',
						__( 'Conversation preview:', 'limb-chatbot' ),
						'{{chat_summary}}',
					] ),
				],
			],
			'action_entry' => [
				'status' => 0,
				'visibility' => [
					'roles'          => [ 'administrator' ],
					'custom_emails' => [],
				],
				'email_template' => [
					'subject' => '[{{site_name}}] New action entry',
					'body'    => implode( '<br><br>', [
						__( 'A new action entry was submitted on your site.', 'limb-chatbot' ),
						__( 'Visitor IP:', 'limb-chatbot' ) . ' {{visitor_ip}}',
						__( 'Chat ID:', 'limb-chatbot' ) . ' {{chat_id}}',
						__( 'Timestamp:', 'limb-chatbot' ) . ' {{timestamp}}',
						__( 'Conversation preview:', 'limb-chatbot' ),
						'{{chat_summary}}',
					] ),
				],
			],
		];
	}

	/**
	 * Get the configured status (0/1) for a notification type.
	 * Missing/invalid status defaults to enabled (1).
	 *
	 * @param  string  $type_key
	 * @return int 0 or 1
	 * @since 2.0.1
	 */
	private function get_type_status( string $type_key ): int {
		$all = $this->get_plugin_notifications();
		$type_config = is_array( $all ) && isset( $all[ $type_key ] ) ? $all[ $type_key ] : [];

		$status = is_array( $type_config ) && array_key_exists( 'status', $type_config ) ? $type_config['status'] : 1;
		$status = is_numeric( $status ) ? (int) $status : 1;

		return $status === 0 ? 0 : 1;
	}

	/**
	 * Action Scheduler hook name for processing a single notification.
	 */
	const ACTION_PROCESS_NOTIFICATION = 'lbaic_process_notification';

	/**
	 * Action Scheduler group for notification jobs.
	 */
	const ACTION_GROUP = 'lbaic_notifications';

	/**
	 * Meta key prefix for per-user opt-in: notification_opted_in_{type_key}.
	 * If meta exists and value is false/0, do not send. If meta does not exist, use global setting.
	 */
	const USER_META_OPTED_IN_PREFIX = 'notification_opted_in_';

	/**
	 * Setting key prefix for global default per type: lbaic.notifications.notification_type.{type_key}.
	 * When user meta is not set, send based on this setting (1/enabled = send, 0/disabled = do not send).
	 * If setting does not exist, default is enabled (send).
	 */
	const SETTING_NOTIFICATION_TYPE_PREFIX = 'notifications.notification_type.';
	/**
	 * Key in payload for log row IDs per channel (set during queueing, used during processing).
	 */
	const PAYLOAD_LOG_IDS = 'log_ids';
	/**
	 * @var Notification_Type_Registry
	 * @since 1.0.15
	 */
	private Notification_Type_Registry $type_registry;
	/**
	 * @var Notification_Channel_Registry
	 * @since 1.0.15
	 */
	private Notification_Channel_Registry $channel_registry;

	/**
	 * @var Notification_Log_Repository|null
	 * @since 1.0.15
	 */
	private ?Notification_Log_Repository $log_repository;

	/**
	 * Cached `lbaic.plugin.notifications` decoded value.
	 *
	 * @var array|null
	 * @since 2.0.0
	 */
	private ?array $plugin_notifications_cache = null;

	/**
	 * Constructor.
	 *
	 * @param  Notification_Type_Registry  $type_registry  Type registry.
	 * @param  Notification_Channel_Registry  $channel_registry  Channel registry.
	 * @param  Notification_Log_Repository|null  $log_repository  Optional log repository.
	 *
	 * @since 1.0.15
	 */
	public function __construct(
		Notification_Type_Registry $type_registry,
		Notification_Channel_Registry $channel_registry,
		?Notification_Log_Repository $log_repository = null
	) {
		$this->type_registry    = $type_registry;
		$this->channel_registry = $channel_registry;
		$this->log_repository   = $log_repository ?? new Notification_Log_Repository();
	}

	/**
	 * Queue or send a notification.
	 *
	 * `Notification_Service` resolves actual email recipients from
	 * `lbaic.plugin.notifications` (native WP roles + custom emails).
	 * The provided `$sender` is only used for contextual purposes (e.g. avoid
	 * sending to the sender when their email is also in recipient list).
	 *
	 * @param  string  $type_key  Registered notification type key.
	 * @param  array  $payload  Context data (e.g. ['chat_uuid' => '...']). Must be serializable.
	 *
	 * @return bool True if at least one notification was sent or queued, false if all skipped.
	 * @since 1.0.15
	 */
	public function queue( $sender, string $type_key, array $payload = [] ): bool {
		// Disable entirely when the notification type is configured with status=0.
		if ( $this->get_type_status( $type_key ) === 0 ) {
			return false;
		}

		// `$sender` is context only (used for job association + excluding their email).
		// If caller doesn't provide a valid Chatbot_User (e.g. some widget paths),
		// choose a context row based on notification settings.
		if ( ! $sender instanceof Chatbot_User ) {
			$sender = $this->resolve_context_sender_from_settings( $type_key );
		}
		if ( ! $sender instanceof Chatbot_User ) {
			return false;
		}
		$exclude = $sender->email() ?? null;
		$to_emails = $this->get_type_to_emails( $type_key, $exclude );
		if ( empty( $to_emails ) ) {
			return false;
		}

		return $this->queue_one( $sender, $type_key, $payload, $to_emails );
	}

	/**
	 * Picks a Chatbot_User row to attach the Action Scheduler job to.
	 *
	 * This must not affect which emails get sent; email recipients are resolved
	 * separately from settings into `to_emails`.
	 *
	 * @param string $type_key
	 * @return Chatbot_User|null
	 * @since 2.0.1
	 */
	private function resolve_context_sender_from_settings( string $type_key ): ?Chatbot_User {
		$cfg = $this->get_type_receivers_config( $type_key );
		$roles = is_array( $cfg['roles'] ?? null ) ? $cfg['roles'] : [];
		$custom_emails = is_array( $cfg['custom_emails'] ?? null ) ? $cfg['custom_emails'] : [];

		// Try role-derived WP users first.
		if ( ! empty( $roles ) ) {
			$wp_users = get_users(
				[
					'role__in' => $roles,
					'fields'   => 'all',
				]
			);
			foreach ( (array) $wp_users as $wp_user ) {
				if ( ! $wp_user instanceof \WP_User ) {
					continue;
				}
				$chatbot_user = Chatbot_User::where( [ 'wp_user_id' => (int) $wp_user->ID ] )->first();
				if ( $chatbot_user instanceof Chatbot_User ) {
					return $chatbot_user;
				}
			}
		}

		// Try custom emails next.
		foreach ( (array) $custom_emails as $email ) {
			if ( ! is_string( $email ) || ! is_email( $email ) ) {
				continue;
			}
			$wp_user = get_user_by( 'email', strtolower( trim( $email ) ) );
			if ( ! $wp_user instanceof \WP_User ) {
				continue;
			}

			$chatbot_user = Chatbot_User::where( [ 'wp_user_id' => (int) $wp_user->ID ] )->first();
			if ( $chatbot_user instanceof Chatbot_User ) {
				return $chatbot_user;
			}
		}

		// Final fallback: attach the job to any existing Chatbot_User row so
		// custom email recipients can still be delivered even if none of the
		// settings-derived emails belong to WP users.
		$fallback = Chatbot_User::where( [] )->first();
		return $fallback instanceof Chatbot_User ? $fallback : null;
	}

	/**
	 * Compute all notification email recipients for a given type.
	 *
	 * Recipients are resolved from:
	 * - WP users matching configured native WP roles (e.g. administrator, editor)
	 *   with a stale check for manage_options users.
	 * - `custom_emails` from the same notification type config,
	 *   also applying the stale check when the email belongs to a manage_options user.
	 *
	 * @param  string  $type_key
	 *
	 * @return string[] Valid email addresses.
	 * @since 2.0.0
	 */
	private function get_type_to_emails( string $type_key, ?string $exclude_email = null ): array {
		$cfg           = $this->get_type_receivers_config( $type_key );
		$user_roles    = $cfg['roles'] ?? [];
		$custom_emails = $cfg['custom_emails'] ?? [];

		$to_emails = [];
		if ( ! is_array( $user_roles ) ) {
			$user_roles = [];
		}
		if ( ! is_array( $custom_emails ) ) {
			$custom_emails = [];
		}

		$tracker = new User_Sync_Tracker();

		// 1) Role-based recipients.
		if ( ! empty( $user_roles ) ) {
			$wp_users = get_users(
				[
					'role__in' => $user_roles,
					'fields'   => 'all',
				]
			);

			foreach ( (array) $wp_users as $wp_user ) {
				if ( ! $wp_user instanceof WP_User ) {
					continue;
				}

				$email = (string) ( $wp_user->user_email ?? '' );
				if ( $email === '' || ! is_email( $email ) ) {
					continue;
				}

				// Apply stale rule only for manage_options users.
				if ( user_can( $wp_user, 'manage_options' ) ) {
					if ( ! $tracker->is_admin_settings_stale( (int) $wp_user->ID ) ) {
						continue;
					}
				}

				$to_emails[] = strtolower( trim( $email ) );
			}
		}

		// 2) Custom email recipients.
		foreach ( $custom_emails as $email ) {
			if ( ! is_string( $email ) || $email === '' || ! is_email( $email ) ) {
				continue;
			}

			$email_lc = strtolower( trim( $email ) );
			$to_emails[] = $email_lc;
		}

		$to_emails = array_values( array_unique( array_filter( $to_emails ) ) );
		if ( $exclude_email !== null ) {
			$to_emails = array_values( array_filter( $to_emails, function ( $email ) use ( $exclude_email ) {
				return strtolower( (string) $email ) !== $exclude_email;
			} ) );
		}

		return $to_emails;
	}

	/**
	 * Get decoded receiver config for a notification type.
	 *
	 * Supported structures:
	 * - New: `visibility => ['roles'=>[], 'custom_emails'=>[]]`
	 * - Old: `receivers => ['user_roles'=>[], 'custom_emails'=>[]]` (back-compat)
	 *
	 * @param  string  $type_key
	 *
	 * @return array{roles:array,custom_emails:array}
	 * @since 2.0.0
	 */
	private function get_type_receivers_config( string $type_key ): array {
		$all         = $this->get_plugin_notifications();
		$type_config = is_array( $all ) && isset( $all[ $type_key ] ) ? $all[ $type_key ] : null;
		if ( ! is_array( $type_config ) ) {
			return [
				'roles'         => [],
				'custom_emails' => [],
			];
		}

		// New structure
		$visibility = $type_config['visibility'] ?? null;
		if ( is_array( $visibility ) ) {
			return [
				'roles'         => is_array( $visibility['roles'] ?? null ) ? array_values( $visibility['roles'] ) : [],
				'custom_emails' => is_array( $visibility['custom_emails'] ?? null ) ? array_values( $visibility['custom_emails'] ) : [],
			];
		}

		// Back-compat old structure
		$receivers = $type_config['receivers'] ?? [];
		if ( ! is_array( $receivers ) ) {
			$receivers = [];
		}

		return [
			'roles'         => is_array( $receivers['user_roles'] ?? null ) ? array_values( $receivers['user_roles'] ) : [],
			'custom_emails' => is_array( $receivers['custom_emails'] ?? null ) ? array_values( $receivers['custom_emails'] ) : [],
		];
	}

	/**
	 * Decode `lbaic.plugin.notifications` setting value.
	 *
	 * @return array
	 * @since 2.0.0
	 */
	private function get_plugin_notifications(): array {
		if ( $this->plugin_notifications_cache !== null ) {
			return $this->plugin_notifications_cache;
		}

		// Back-compat + your naming example support:
		// - primary key: `lbaic.plugin.notifications`
		// - alias key:   `lbaic.plugin.notification_types`
		$setting = Setting::find( 'lbaic.plugin.notification_types' );
		$value   = $setting instanceof Setting ? $setting->get_value() : null;

		if ( ! is_array( $value ) ) {
			$setting = Setting::find( 'lbaic.plugin.notifications' );
			$value   = $setting instanceof Setting ? $setting->get_value() : null;
		}

		if ( ! is_array( $value ) ) {
			$value = self::get_defaults();
		}

		$this->plugin_notifications_cache = $value;

		return $this->plugin_notifications_cache;
	}

	/**
	 * Queue or send a notification for a single Chatbot_User.
	 *
	 * is_notification_enabled_for_user is checked only here (queue time), so we never insert
	 * a pending row for opted-out users. Pending rows are created only when we will process/send.
	 *
	 * @param  Chatbot_User  $recipient  Target chatbot user.
	 * @param  string  $type_key  Notification type key.
	 * @param  array  $payload  Context payload (serializable).
	 *
	 * @return bool True if sent or queued, false if skipped.
	 * @since 1.0.15
	 */
	private function queue_one( Chatbot_User $recipient, string $type_key, array $payload, array $to_emails ): bool {
		$type = $this->type_registry->get( $type_key );
		if ( ! $type instanceof Notification_Type_Interface ) {
			return false;
		}

		$payload = array_merge(
			$payload,
			[
				'send_recipient_email' => false,
				'to_emails'            => $to_emails,
			]
		);

		$channels = $type->get_channels();
		if ( empty( $channels ) ) {
			return false;
		}

		$chatbot_user_id = $recipient->get_id();
		if ( empty( $chatbot_user_id ) ) {
			return false;
		}

		$log_ids = [];
		foreach ( $channels as $channel_key ) {
			$channel = $this->channel_registry->get( $channel_key );
			if ( ! $channel ) {
				continue;
			}

			// Email delivery is scoped via envelope payload, not via the context recipient.
			if ( $channel_key !== 'email' && ! $channel->can_deliver_to( $recipient ) ) {
				continue;
			}

			$id = $this->log_repository->insert_pending( $chatbot_user_id, $type_key, $channel_key );
			if ( $id !== null ) {
				$log_ids[ $channel_key ] = $id;
			}
		}
		if ( empty( $log_ids ) ) {
			return false;
		}
		$payload = array_merge( $payload, [ self::PAYLOAD_LOG_IDS => $log_ids ] );

		if ( $type->is_instant() ) {
			$this->process( $chatbot_user_id, $type_key, $payload );

			return true;
		}

		return $this->schedule_action( $chatbot_user_id, $type_key, $payload );
	}

	/**
	 * Process a single queued notification (called by Action Scheduler).
	 *
	 * Opt-in is not re-checked here so pending rows are always updated to sent/failed (no stuck pending).
	 * Payload may contain log_ids (channel => id) from queue time; those rows are updated to completed.
	 *
	 * @param  int  $chatbot_user_id  Chatbot user ID.
	 * @param  string  $type_key  Notification type key.
	 * @param  array  $payload  Context payload (serializable); may include 'log_ids' from queue.
	 *
	 * @return void
	 * @since 1.0.15
	 */
	public function process( int $chatbot_user_id, string $type_key, array $payload = [] ): void {
		// Hard stop: if the type is disabled in settings, do not deliver.
		if ( $this->get_type_status( $type_key ) === 0 ) {
			$log_ids = $payload[ self::PAYLOAD_LOG_IDS ] ?? [];
			$type = $this->type_registry->get( $type_key );
			if ( $type instanceof Notification_Type_Interface ) {
				foreach ( $type->get_channels() as $channel_key ) {
					$this->mark_log_failed_if_pending(
						$channel_key,
						$log_ids,
						__( 'Notification type disabled (status=0).', 'limb-chatbot' )
					);
				}
			}

			return;
		}

		$recipient = Chatbot_User::find( $chatbot_user_id );
		if ( ! $recipient instanceof Chatbot_User ) {
			return;
		}

		$log_ids         = $payload[ self::PAYLOAD_LOG_IDS ] ?? [];
		$context_payload = $payload;
		unset( $context_payload[ self::PAYLOAD_LOG_IDS ] );

		$type = $this->type_registry->get( $type_key );
		if ( ! $type instanceof Notification_Type_Interface ) {
			return;
		}

		$channels = $type->get_channels();
		foreach ( $channels as $channel_key ) {
			$channel = $this->channel_registry->get( $channel_key );
			if ( ! $channel || ! $channel->can_deliver_to( $recipient ) ) {
				$this->mark_log_failed_if_pending( $channel_key,
					$log_ids,
					__( 'Channel cannot deliver to recipient.', 'limb-chatbot' ) );

				continue;
			}

			$envelope = $type->build_envelope( $recipient, $context_payload );
			if ( ! $envelope instanceof Notification_Envelope ) {
				$this->mark_log_failed_if_pending( $channel_key,
					$log_ids,
					__( 'Envelope could not be built.', 'limb-chatbot' ) );

				continue;
			}
			$reason = __( 'Delivery failed.', 'limb-chatbot' );
			try {
				$sent = $channel->send( $recipient, $envelope );
			} catch ( Exception $e ) {
				$sent   = false;
				$reason .= ': ' . Helper::get_wp_error( $e )->get_error_message();
			}

			if ( $this->log_repository ) {
				$log_id       = $log_ids[ $channel_key ] ?? null;
				$content_type = $envelope->get_content_type();
				$content      = $envelope->get_body();
				if ( $log_id !== null ) {
					$this->log_repository->update_status(
						$log_id,
						$sent ? 'sent' : 'failed',
						null,
						$sent ? null : $reason,
						$content_type,
						$content
					);
				} else {
					$this->log_repository->log(
						$chatbot_user_id,
						$type_key,
						$channel_key,
						$sent ? 'sent' : 'failed',
						$sent ? null : $reason,
						$content_type,
						$content
					);
				}
			}
		}
	}

	/**
	 * Mark a pending log row as failed when we skip sending (e.g. can't deliver or no envelope).
	 *
	 * @param  string  $channel_key  Channel key.
	 * @param  array  $log_ids  Map of channel => log id.
	 * @param  string|null  $fail_reason  Reason for failure.
	 *
	 * @return void
	 * @since 1.0.15
	 */
	private function mark_log_failed_if_pending(
		string $channel_key,
		array $log_ids,
		?string $fail_reason = null
	): void {
		$log_id = $log_ids[ $channel_key ] ?? null;
		if ( $log_id !== null && $this->log_repository ) {
			$this->log_repository->update_status( $log_id, 'failed', null, $fail_reason );
		}
	}

	/**
	 * Schedule the notification via Action Scheduler.
	 *
	 * Uses a short delay (default 15 seconds) so actions are not all "due now". That lets
	 * WP-Cron process them in bulk (every minute) without triggering long async chains
	 * that sleep 5 seconds between batches and tie up PHP workers (which slows the site).
	 *
	 * @param  int  $chatbot_user_id  Chatbot user ID.
	 * @param  string  $type_key  Type key.
	 * @param  array  $payload  Payload.
	 *
	 * @return bool True if scheduled.
	 * @since 1.0.15
	 */
	private function schedule_action( int $chatbot_user_id, string $type_key, array $payload ): bool {
		if ( ! function_exists( 'as_schedule_single_action' ) ) {
			do_action( self::ACTION_PROCESS_NOTIFICATION, $chatbot_user_id, $type_key, $payload );

			return true;
		}

		$delay_seconds = (int) apply_filters( 'lbaic_notification_schedule_delay_seconds', 15 );
		$delay_seconds = max( 0, $delay_seconds );
		$when          = time() + $delay_seconds;

		$action_id = as_schedule_single_action(
			$when,
			self::ACTION_PROCESS_NOTIFICATION,
			[ $chatbot_user_id, $type_key, $payload ],
			self::ACTION_GROUP
		);

		return $action_id !== 0;
	}

	/**
	 * Check whether the notification should be sent to this user (type exists, role allowed).
	 *
	 * @param  Chatbot_User  $recipient  Target chatbot user.
	 * @param  string  $type_key  Notification type key.
	 *
	 * @return bool
	 * @since 1.0.15
	 */
	public function should_send( Chatbot_User $recipient, string $type_key ): bool {
		// Legacy entry point: prefer using `is_notification_enabled_for_user()`.
		// This method is still kept for backwards compatibility.
		$chatbot = Chatbot::make();

		return $this->is_notification_enabled_for_user( $chatbot, $recipient, $type_key );
	}

	/**
	 * Check whether the notification is enabled for this chatbot user.
	 *
	 * First checks user meta: notification_opted_in_{type_key}. If meta exists and is false/0, do not send.
	 * If meta does not exist, falls back to Setting::find('lbaic.notifications.notification_type.{type_key}').
	 * If that setting exists and is false/0, do not send. If setting does not exist, default is enabled (send).
	 *
	 * @param  Chatbot_User  $recipient  Target chatbot user.
	 * @param  string  $type_key  Notification type key.
	 *
	 * @return bool True if sending is enabled for this user, false otherwise.
	 * @since 1.0.15
	 */
	public function is_notification_enabled_for_user(
		?Chatbot $chatbot,
		Chatbot_User $recipient,
		string $type_key
	): bool {
		$meta_key = self::USER_META_OPTED_IN_PREFIX . $type_key;
		$user_val = $recipient->get_meta( $meta_key );

		// User has an explicit preference: respect it.
		if ( $user_val !== null && $user_val !== '' ) {
			return $this->value_is_enabled( $user_val );
		}

		// No user preference: use global setting for this notification type.
		$plugin_config = $this->get_type_receivers_config( $type_key );
		$user_roles    = $plugin_config['roles'] ?? [];
		$custom_emails = $this->get_type_custom_emails( $type_key );
		$custom_emails = array_values( array_unique( array_map( function ( $email ) {
			return is_string( $email ) ? strtolower( trim( $email ) ) : $email;
		}, $custom_emails ) ) );

		$all           = $this->get_plugin_notifications();
		$type_config   = is_array( $all ) && isset( $all[ $type_key ] ) ? $all[ $type_key ] : null;
		$has_receivers = is_array( $type_config ) && ( array_key_exists( 'visibility',
					$type_config ) || array_key_exists( 'receivers', $type_config ) );

		// If the plugin setting exists for this type, it is authoritative.
		if ( $has_receivers ) {
			$recipient_email = $recipient->email();
			$recipient_email = is_string( $recipient_email ) ? $recipient_email : '';
			$recipient_email = strtolower( trim( $recipient_email ) );

			$tracker = new User_Sync_Tracker();

			// Custom emails can target a user even if their WP roles don't match.
			if ( $recipient_email !== '' && in_array( $recipient_email, $custom_emails, true ) ) {
				$wp_user = null;
				if ( $recipient->get_type() === Chatbot_User::TYPE_WP_USER && $recipient->get_wp_user_id() ) {
					$wp_user = get_userdata( (int) $recipient->get_wp_user_id() );
				}
				if ( $wp_user instanceof WP_User && user_can( $wp_user, 'manage_options' ) ) {
					return $tracker->is_admin_settings_stale( (int) $wp_user->ID );
				}

				return true;
			}

			// Role-based delivery.
			if ( empty( $user_roles ) ) {
				return false;
			}

			if ( $recipient->get_type() === Chatbot_User::TYPE_WP_USER && $recipient->get_wp_user_id() ) {
				$wp_user = get_userdata( (int) $recipient->get_wp_user_id() );
				if ( ! $wp_user instanceof WP_User ) {
					return false;
				}

				$role_intersection = array_intersect( (array) $wp_user->roles, $user_roles );
				if ( empty( $role_intersection ) ) {
					return false;
				}

				// Apply stale rule to manage_options admins.
				if ( user_can( $wp_user, 'manage_options' ) ) {
					return $tracker->is_admin_settings_stale( (int) $wp_user->ID );
				}

				return true;
			}

			return false;
		}

		// Backwards compatibility fallback (old per-chatbot notification booleans).
		if ( ! $chatbot instanceof Chatbot ) {
			return true;
		}

		$parameter = $chatbot->get_parameter( self::SETTING_NOTIFICATION_TYPE_PREFIX . $type_key );
		if ( $parameter === null ) {
			return true;
		}

		return $this->value_is_enabled( $parameter );
	}

	/**
	 * Interpret a stored value as enabled (true) or disabled (false).
	 *
	 * @param  mixed  $value  Stored value (e.g. 1, '1', true, 0, '0', false).
	 *
	 * @return bool
	 * @since 1.0.15
	 */
	private function value_is_enabled( $value ): bool {
		if ( $value === true || $value === 1 || $value === '1' ) {
			return true;
		}
		if ( $value === false || $value === 0 || $value === '0' ) {
			return false;
		}

		return (bool) $value;
	}

	/**
	 * @param  string  $type_key
	 *
	 * @return string[]
	 * @since 2.0.0
	 */
	private function get_type_custom_emails( string $type_key ): array {
		$cfg    = $this->get_type_receivers_config( $type_key );
		$emails = $cfg['custom_emails'] ?? [];
		if ( ! is_array( $emails ) ) {
			return [];
		}

		return array_values( array_filter( $emails, function ( $email ) {
			return is_string( $email ) && ! empty( $email ) && is_email( $email );
		} ) );
	}
}
