<?php
/**
 * WhatsApp Web/Baileys transport for the admin autonomy founder agent.
 *
 * @package Niwa_AI
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class Niwa_AI_Admin_Autonomy_WhatsApp {
	const OPTION_KEY         = Niwa_AI_Constants::OPT_AUTONOMY;
	const MESSAGE_TEXT_LIMIT = 4000;
	const STARTUP_STALL_SECONDS = 120;
	private $state_migration_checked = false;
	private $runtime_refresh_active = false;

	public function get_defaults() {
		return array(
			'enabled'        => 0,
			'admin_numbers'  => array(),
			'stock_manager_numbers' => array(),
			'lead_numbers'   => array(),
			'admin_group_jids' => array(),
			'stock_manager_group_jids' => array(),
			'lead_group_jids' => array(),
			'node_path'      => '',
			'gateway_mode'   => 'auto',
			'gateway_secret' => '',
		);
	}

	public function get_settings() {
		$settings = get_option( self::OPTION_KEY, array() );
		$settings = is_array( $settings ) ? $settings : array();
		$wa       = isset( $settings['whatsapp'] ) && is_array( $settings['whatsapp'] ) ? $settings['whatsapp'] : array();
		$defaults = $this->get_defaults();
		$changed  = false;

		foreach ( array_keys( $wa ) as $key ) {
			if ( ! array_key_exists( $key, $defaults ) ) {
				unset( $wa[ $key ] );
				$changed = true;
			}
		}

		$wa = array_intersect_key( wp_parse_args( $wa, $defaults ), $defaults );
		foreach ( $this->get_access_role_keys() as $role => $key ) {
			$before    = isset( $wa[ $key ] ) ? $wa[ $key ] : array();
			$sanitized = $this->sanitize_access_number_entries_for_role( $before, $role );
			if ( $sanitized !== $before ) {
				$changed = true;
			}
			$wa[ $key ] = $sanitized;
		}
		$deduplicated = $this->enforce_number_role_precedence( $wa );
		if ( $deduplicated !== $wa ) {
			$wa      = $deduplicated;
			$changed = true;
		}
		foreach ( $this->get_access_group_role_keys() as $role => $key ) {
			$before    = isset( $wa[ $key ] ) ? $wa[ $key ] : array();
			$sanitized = $this->sanitize_access_group_entries_for_role( $before, $role );
			if ( $sanitized !== $before ) {
				$changed = true;
			}
			$wa[ $key ] = $sanitized;
		}
		if ( $changed ) {
			$settings['whatsapp'] = $wa;
			update_option( self::OPTION_KEY, $settings, false );
			$this->sync_access_policy_file( $wa );
		}

		return $wa;
	}

	public function is_enabled() {
		$settings = $this->get_settings();
		return ! empty( $settings['enabled'] );
	}

	public function get_gateway_endpoint_url() {
		if ( ! isset( $GLOBALS['wp_rewrite'] ) || ! is_object( $GLOBALS['wp_rewrite'] ) ) {
			return set_url_scheme( home_url( '/wp-json/niwa-ai/v1/whatsapp/webhook' ), 'https' );
		}

		return set_url_scheme( rest_url( 'niwa-ai/v1/whatsapp/webhook' ), 'https' );
	}

	public function get_state_dir() {
		$base = class_exists( 'Niwa_AI_Admin_Autonomy_Workspace' )
			? Niwa_AI_Admin_Autonomy_Workspace::get_private_data_root()
			: '';
		if ( '' === $base ) {
			$base = rtrim( str_replace( '\\', '/', sys_get_temp_dir() ), '/' ) . '/niwa-ai-private-' . substr( hash( 'sha256', (string) ABSPATH ), 0, 12 );
		}
		return rtrim( $base, "/\\" ) . '/whatsapp';
	}

	protected function get_legacy_public_state_dir() {
		$uploads = function_exists( 'wp_upload_dir' ) ? wp_upload_dir( null, false ) : array();
		return ! empty( $uploads['basedir'] )
			? rtrim( (string) $uploads['basedir'], "/\\" ) . '/niwa-ai/whatsapp'
			: '';
	}

	public function resolve_local_media_path( $path ) {
		$this->ensure_state_dirs();
		$path      = (string) $path;
		$legacy    = rtrim( str_replace( '\\', '/', $this->get_legacy_public_state_dir() ), '/' );
		$normalized = str_replace( '\\', '/', $path );
		if ( ! is_file( $path ) && '' !== $legacy && 0 === strpos( $normalized, $legacy . '/' ) ) {
			$path = $this->get_state_dir() . substr( $normalized, strlen( $legacy ) );
		}
		$real_path = realpath( $path );
		$media_dir = realpath( $this->get_state_dir() . '/media' );
		if ( false === $real_path || false === $media_dir || ! is_file( $real_path ) || ! is_readable( $real_path ) ) {
			return '';
		}

		$real_path = str_replace( '\\', '/', $real_path );
		$media_dir = rtrim( str_replace( '\\', '/', $media_dir ), '/' ) . '/';
		return 0 === strpos( $real_path, $media_dir ) ? $real_path : '';
	}

	public function ensure_state_dirs() {
		$this->migrate_legacy_public_state();
		$dirs = array(
			$this->get_state_dir(),
			$this->get_state_dir() . '/auth',
			$this->get_state_dir() . '/media',
			$this->get_state_dir() . '/outbox',
			$this->get_state_dir() . '/sent',
		);
		$old_umask = umask( 0077 );
		try {
			foreach ( $dirs as $dir ) {
				if ( is_link( $dir ) ) {
					throw new RuntimeException( 'WhatsApp private state directory is unsafe.' );
				}
				if ( ! is_dir( $dir ) ) {
					if ( function_exists( 'wp_mkdir_p' ) ) {
						wp_mkdir_p( $dir );
					} else {
						@mkdir( $dir, 0700, true );
					}
				}
				$this->protect_state_dir( $dir );
			}
		} finally {
			umask( $old_umask );
		}
		if ( class_exists( 'Niwa_AI_Admin_Autonomy_Workspace' ) ) {
			Niwa_AI_Admin_Autonomy_Workspace::ensure_private_data_permissions();
		}
	}

	public function get_gateway_status( $allow_auto_restart = true ) {
		$this->ensure_state_dirs();
		$status = $this->read_json_file( $this->get_state_dir() . '/status.json' );
		$status = is_array( $status ) ? $status : array();
		$pid    = intval( isset( $status['pid'] ) ? $status['pid'] : 0 );
		if ( $pid <= 0 ) {
			$pid = $this->read_pid_file();
		}

		$status['configured'] = $this->is_enabled();
		$status['mode']       = $this->get_gateway_mode();
		$status['pid']        = $pid;
		$status['running']    = $pid > 0 && $this->process_is_alive( $pid );
		$status['state_dir']  = $this->get_state_dir();
		if ( empty( $status['status'] ) ) {
			$status['status'] = $status['running'] ? 'running' : 'not_started';
		}
		if ( ! empty( $status['running'] ) && $this->is_gateway_startup_stalled( $status ) ) {
			$active_status           = (string) $status['status'];
			$status['status']        = 'gateway_stalled';
			$status['lastError']     = 'gateway_startup_stalled';
			$status['qrDataUrl']     = '';
			$restart_status          = $status;
			$restart_status['status'] = $active_status;
			if ( $allow_auto_restart && $this->should_attempt_gateway_auto_restart( $restart_status, $pid ) ) {
				$this->terminate_gateway_process( $pid );
				@unlink( $this->get_state_dir() . '/gateway.pid' );
				$this->write_status_file(
					array(
						'status'    => 'restarting',
						'pid'       => 0,
						'qrDataUrl' => '',
						'lastError' => 'gateway_startup_stalled',
					)
				);
				$started = $this->start_gateway( false );
				if ( ! empty( $started['success'] ) && ! empty( $started['status'] ) && is_array( $started['status'] ) ) {
					$restarted                   = $started['status'];
					$restarted['autoRestarted'] = true;
					return $restarted;
				}

				$status['running']           = false;
				$status['pid']               = 0;
				$status['autoRestartFailed'] = isset( $started['error'] ) ? (string) $started['error'] : 'gateway_restart_failed';
				$status['lastError']         = $status['autoRestartFailed'];
			}
		}
		$stop_requested = is_readable( $this->get_state_dir() . '/supervisor.stop' );
		$recoverable_dead_states = $stop_requested
			? array( 'starting', 'qr', 'running', 'reconnecting', 'connected' )
			: array( 'starting', 'qr', 'running', 'reconnecting', 'connected', 'stopping' );
		if ( ! $status['running'] && in_array( (string) $status['status'], $recoverable_dead_states, true ) ) {
			$active_status = (string) $status['status'];
			$updated_at = isset( $status['updatedAt'] ) ? strtotime( (string) $status['updatedAt'] ) : 0;
			if ( $pid <= 0 && $updated_at > 0 && ( time() - $updated_at ) < 20 ) {
				return $status;
			}
			$status['status'] = $pid > 0 ? 'gateway_crashed' : 'not_started';
			if ( empty( $status['lastError'] ) ) {
				$status['lastError'] = $this->read_recent_gateway_error();
				if ( '' === trim( (string) $status['lastError'] ) ) {
					$status['lastError'] = 'gateway_process_not_running';
				}
			}
			$status['qrDataUrl'] = '';
			$restart_status           = $status;
			$restart_status['status'] = $active_status;
			if ( $allow_auto_restart && $this->should_attempt_gateway_auto_restart( $restart_status, $pid ) ) {
				$this->write_status_file(
					array(
						'status'    => 'restarting',
						'pid'       => 0,
						'qrDataUrl' => '',
						'lastError' => 'gateway_process_not_running',
					)
				);
				$started = $this->start_gateway( false );
				if ( ! empty( $started['success'] ) && ! empty( $started['status'] ) && is_array( $started['status'] ) ) {
					$restarted                   = $started['status'];
					$restarted['autoRestarted'] = true;
					return $restarted;
				}

				$status['autoRestartFailed'] = isset( $started['error'] ) ? (string) $started['error'] : 'gateway_restart_failed';
				$status['lastError']         = $status['autoRestartFailed'];
			}
		}
		if ( 'connected' === (string) $status['status'] ) {
			$status['lastError'] = '';
		}
		return $status;
	}

	public function start_gateway( $force = false ) {
		if ( ! $this->is_enabled() ) {
			return array(
				'success' => false,
				'error'   => 'whatsapp_disabled',
			);
		}

		$this->ensure_state_dirs();
		if ( ! $this->runtime_refresh_active && $this->is_runtime_refresh_guard_active() ) {
			return array(
				'success' => false,
				'error'   => 'gateway_runtime_refresh_in_progress',
			);
		}
		$status = $this->get_gateway_status( false );
		if ( 'logged_out' === (string) ( isset( $status['status'] ) ? $status['status'] : '' ) ) {
			$this->clear_gateway_auth_state();
			$this->write_status_file(
				array(
					'status'    => 'not_started',
					'pid'       => 0,
					'qrDataUrl' => '',
					'lastError' => '',
				)
			);
			$status = $this->get_gateway_status( false );
		}
		if ( empty( $force ) && ! empty( $status['running'] ) && in_array( (string) $status['status'], array( 'qr', 'connected', 'starting', 'reconnecting', 'running' ), true ) ) {
			return array(
				'success' => true,
				'status'  => $status,
				'note'    => 'already_running',
			);
		}
		if ( ! empty( $status['running'] ) && 'gateway_stalled' === (string) ( isset( $status['status'] ) ? $status['status'] : '' ) ) {
			$this->terminate_gateway_process( isset( $status['pid'] ) ? intval( $status['pid'] ) : 0 );
			@unlink( $this->get_state_dir() . '/gateway.pid' );
			$this->write_status_file(
				array(
					'status'    => 'restarting',
					'pid'       => 0,
					'qrDataUrl' => '',
					'lastError' => 'gateway_startup_stalled',
				)
			);
		}

		$mode = $this->get_gateway_mode();
		if ( 'external' === $mode ) {
			return array(
				'success' => false,
				'error'   => 'external_gateway_not_running',
				'status'  => $status,
			);
		}

		$secret = $this->ensure_gateway_secret();
		$this->sync_gateway_secret_file( $secret );
		$this->sync_access_policy_file( $this->get_settings() );
		$node   = $this->resolve_node_binary();
		if ( '' === $node ) {
			return array(
				'success' => false,
				'error'   => 'node_binary_unavailable',
			);
		}

		if ( ! class_exists( 'Niwa_AI_Admin_Autonomy_Process_Runtime' ) || ! Niwa_AI_Admin_Autonomy_Process_Runtime::is_proc_open_available() ) {
			return array(
				'success' => false,
				'error'   => 'proc_open_unavailable',
			);
		}

		$script = defined( 'NIWA_AI_DIR' ) ? NIWA_AI_DIR . 'bin/whatsapp-gateway.mjs' : dirname( __DIR__, 2 ) . '/bin/whatsapp-gateway.mjs';
		if ( ! is_readable( $script ) ) {
			return array(
				'success' => false,
				'error'   => 'gateway_script_missing',
			);
		}
		$supervisor = defined( 'NIWA_AI_DIR' ) ? NIWA_AI_DIR . 'bin/whatsapp-gateway-supervisor.sh' : dirname( __DIR__, 2 ) . '/bin/whatsapp-gateway-supervisor.sh';
		if ( ! is_readable( $supervisor ) ) {
			return array(
				'success' => false,
				'error'   => 'gateway_supervisor_missing',
			);
		}
		$prepared_supervisor = $this->prepare_gateway_supervisor_script( $supervisor );
		if ( empty( $prepared_supervisor['success'] ) ) {
			return array(
				'success' => false,
				'error'   => isset( $prepared_supervisor['error'] ) ? (string) $prepared_supervisor['error'] : 'gateway_supervisor_prepare_failed',
			);
		}
		$supervisor = (string) $prepared_supervisor['path'];

		$deps = $this->ensure_gateway_dependencies();
		if ( empty( $deps['success'] ) ) {
			return array(
				'success'         => false,
				'error'           => 'gateway_dependencies_missing',
				'missing'         => isset( $deps['missing'] ) ? $deps['missing'] : array(),
				'install_command' => 'npm ci --omit=dev --omit=optional',
				'install_error'   => isset( $deps['install_error'] ) ? (string) $deps['install_error'] : '',
			);
		}

		$log = $this->get_state_dir() . '/launcher.log';
		$this->write_private_state_file( $log, '[' . gmdate( 'c' ) . '] launching WhatsApp gateway' . "\n", FILE_APPEND );
		@unlink( $this->get_state_dir() . '/supervisor.stop' );
		$this->write_status_file(
			array(
				'status'    => 'starting',
				'pid'       => 0,
				'qrDataUrl' => '',
				'lastError' => '',
			)
		);

		$node_args = $this->node_supports_no_experimental_fetch( $node ) ? '--no-experimental-fetch' : '';
		$supervisor_pid = $this->read_supervisor_pid_file();
		if ( empty( $force ) && $supervisor_pid > 0 && $this->process_is_alive( $supervisor_pid ) ) {
			return array(
				'success' => true,
				'status'  => $status,
				'note'    => 'supervisor_already_running',
			);
		}

		$cmd = 'NIWA_WA_SECRET=' . escapeshellarg( $secret ) . ' /bin/sh ' . escapeshellarg( $supervisor ) . ' ' . escapeshellarg( $node ) . ' ' . escapeshellarg( $script ) . ' ' . escapeshellarg( $node_args )
			. ' --state-dir=' . escapeshellarg( $this->get_state_dir() )
			. ' --site-url=' . escapeshellarg( home_url( '/' ) )
			. ' --webhook-url=' . escapeshellarg( $this->get_gateway_endpoint_url() )
			. ' >> ' . escapeshellarg( $log ) . ' 2>&1 & echo $!';

		$process = @proc_open(
			'/bin/sh -c ' . escapeshellarg( $cmd ),
			array(
				0 => array( 'file', '/dev/null', 'r' ),
				1 => array( 'pipe', 'w' ),
				2 => array( 'file', $log, 'a' ),
			),
			$pipes,
			defined( 'ABSPATH' ) ? ABSPATH : null
		);

		if ( ! is_resource( $process ) ) {
			return array(
				'success' => false,
				'error'   => 'gateway_proc_open_failed',
			);
		}

		$pid_raw = isset( $pipes[1] ) && is_resource( $pipes[1] ) ? (string) stream_get_contents( $pipes[1] ) : '';
		foreach ( (array) $pipes as $pipe ) {
			if ( is_resource( $pipe ) ) {
				fclose( $pipe );
			}
		}
		$exit_code = @proc_close( $process );
		$pid       = preg_match( '/\d+/', $pid_raw, $m ) ? intval( $m[0] ) : 0;

		if ( 0 !== intval( $exit_code ) || $pid <= 0 ) {
			return array(
				'success'   => false,
				'error'     => 'gateway_spawn_failed',
				'exit_code' => intval( $exit_code ),
			);
		}

		$this->write_private_state_file( $this->get_state_dir() . '/gateway.pid', (string) $pid );
		usleep( 200000 );

		return array(
			'success' => true,
			'pid'     => $pid,
			'status'  => $this->get_gateway_status( false ),
		);
	}

	public function disconnect_gateway() {
		$this->ensure_state_dirs();
		$status = $this->get_gateway_status( false );
		$this->write_private_state_file( $this->get_state_dir() . '/supervisor.stop', (string) time() );
		$this->write_status_file(
			array(
				'status'    => ! empty( $status['running'] ) ? 'disconnecting' : 'logged_out',
				'pid'       => isset( $status['pid'] ) ? intval( $status['pid'] ) : 0,
				'qrDataUrl' => '',
				'lastError' => '',
			)
		);
		$this->write_private_state_file( $this->get_state_dir() . '/control.json', wp_json_encode( array( 'action' => 'logout', 'at' => time() ) ) );
		$this->terminate_gateway_process( $this->read_supervisor_pid_file() );
		return array(
			'success' => true,
			'status'  => $this->get_gateway_status( false ),
		);
	}

	public function refresh_runtime_after_plugin_update( $plugin_version ) {
		$plugin_version = trim( (string) $plugin_version );
		$settings       = $this->get_settings();
		if ( empty( $settings['enabled'] ) || 'auto' !== $this->get_gateway_mode() ) {
			return array( 'success' => true, 'skipped' => 'whatsapp_auto_runtime_disabled' );
		}
		if ( '' === $plugin_version ) {
			return array( 'success' => false, 'error' => 'plugin_version_required', 'retry' => true );
		}

		$this->ensure_state_dirs();
		$state_dir   = $this->get_state_dir();
		$lock_path   = $state_dir . '/.runtime-refresh.lock';
		$active_path = $state_dir . '/.runtime-refresh.active.json';
		$version_path = $state_dir . '/.runtime-package-version';
		$lock        = $this->open_private_state_file( $lock_path, 'c' );
		if ( ! is_resource( $lock ) || ! @flock( $lock, LOCK_EX | LOCK_NB ) ) {
			if ( is_resource( $lock ) ) {
				@fclose( $lock );
			}
			return array( 'success' => false, 'error' => 'gateway_runtime_refresh_locked', 'retry' => true );
		}

		try {
			$current_version = is_readable( $version_path ) ? trim( (string) @file_get_contents( $version_path ) ) : '';
			if ( hash_equals( $plugin_version, $current_version ) ) {
				return array( 'success' => true, 'skipped' => 'whatsapp_runtime_current' );
			}
			if ( ! $this->has_active_runtime_process() ) {
				if ( ! $this->refresh_private_permissions_after_runtime_launch() ) {
					return array( 'success' => false, 'error' => 'private_permissions_refresh_failed', 'retry' => true );
				}
				if ( false === $this->write_private_state_file( $version_path, $plugin_version . "\n", LOCK_EX ) ) {
					return array( 'success' => false, 'error' => 'runtime_version_marker_write_failed', 'retry' => true );
				}
				return array( 'success' => true, 'skipped' => 'whatsapp_runtime_inactive' );
			}

			if ( false === $this->write_private_state_file(
				$active_path,
				wp_json_encode( array( 'version' => $plugin_version, 'started_at' => time() ) ),
				LOCK_EX
			) ) {
				return array( 'success' => false, 'error' => 'runtime_refresh_guard_write_failed', 'retry' => true );
			}
			$this->runtime_refresh_active = true;
			if ( false === $this->write_private_state_file( $state_dir . '/supervisor.stop', (string) time(), LOCK_EX ) ) {
				return array( 'success' => false, 'error' => 'gateway_stop_signal_write_failed', 'retry' => true );
			}

			$gateway_pid    = $this->read_pid_file();
			$supervisor_pid = $this->read_supervisor_pid_file();
			if ( $gateway_pid > 0 && ! $this->terminate_gateway_process( $gateway_pid ) ) {
				return array( 'success' => false, 'error' => 'gateway_runtime_stop_failed', 'retry' => true );
			}
			if ( $supervisor_pid > 0 && ! $this->terminate_gateway_process( $supervisor_pid ) ) {
				return array( 'success' => false, 'error' => 'gateway_supervisor_stop_failed', 'retry' => true );
			}

			@unlink( $state_dir . '/gateway.pid' );
			@unlink( $state_dir . '/supervisor.pid' );
			@unlink( $state_dir . '/supervisor.stop' );
			$this->write_status_file(
				array(
					'status'    => 'restarting',
					'pid'       => 0,
					'qrDataUrl' => '',
					'lastError' => '',
				)
			);

			$started = $this->start_gateway( false );
			if ( empty( $started['success'] ) ) {
				return array(
					'success' => false,
					'error'   => isset( $started['error'] ) ? (string) $started['error'] : 'gateway_runtime_restart_failed',
					'retry'   => true,
				);
			}

			if ( ! $this->refresh_private_permissions_after_runtime_launch() ) {
				return array( 'success' => false, 'error' => 'private_permissions_refresh_failed', 'retry' => true );
			}
			if ( false === $this->write_private_state_file( $version_path, $plugin_version . "\n", LOCK_EX ) ) {
				return array( 'success' => false, 'error' => 'runtime_version_marker_write_failed', 'retry' => true );
			}

			return array( 'success' => true, 'restarted' => true, 'pid' => isset( $started['pid'] ) ? intval( $started['pid'] ) : 0 );
		} finally {
			$this->runtime_refresh_active = false;
			@unlink( $active_path );
			@flock( $lock, LOCK_UN );
			@fclose( $lock );
		}
	}

	protected function refresh_private_permissions_after_runtime_launch() {
		return ! class_exists( 'Niwa_AI_Admin_Autonomy_Workspace' )
			|| Niwa_AI_Admin_Autonomy_Workspace::refresh_private_data_permissions();
	}

	protected function has_active_runtime_process() {
		$status = $this->read_json_file( $this->get_state_dir() . '/status.json' );
		$current = isset( $status['status'] ) ? (string) $status['status'] : '';
		if (
			is_file( $this->get_state_dir() . '/supervisor.stop' )
			|| in_array( $current, array( 'logged_out', 'not_started', 'stopping', 'disconnecting' ), true )
		) {
			return false;
		}
		foreach ( array( $this->read_pid_file(), $this->read_supervisor_pid_file() ) as $pid ) {
			if ( $pid > 0 && $this->is_runtime_pid_alive( $pid ) ) {
				return true;
			}
		}
		return false;
	}

	protected function is_runtime_pid_alive( $pid ) {
		return $this->process_is_alive( $pid );
	}

	public function handle_gateway_webhook( WP_REST_Request $request ) {
		if ( ! $this->is_enabled() ) {
			return new WP_REST_Response( array( 'success' => false, 'error' => 'whatsapp_disabled' ), 403 );
		}

		$payload = $request->get_json_params();
		$payload = is_array( $payload ) ? $payload : array();
		$secret  = isset( $payload['secret'] ) ? (string) $payload['secret'] : '';
		$current = $this->ensure_gateway_secret();
		if ( '' === $secret || ! hash_equals( $current, $secret ) ) {
			$this->sync_gateway_secret_file( $current );
			return new WP_REST_Response( array( 'success' => false, 'error' => 'forbidden', 'reason' => 'gateway_secret_mismatch' ), 403 );
		}

		return $payload;
	}

	public function extract_messages_from_gateway_payload( $payload ) {
		$payload  = is_array( $payload ) ? $payload : array();
		$raw      = isset( $payload['messages'] ) && is_array( $payload['messages'] ) ? $payload['messages'] : array();
		$messages = array();
		foreach ( $raw as $message ) {
			$normalized = $this->normalize_gateway_message( $message );
			if ( ! empty( $normalized['success'] ) ) {
				$messages[] = $normalized;
			}
		}
		return $messages;
	}

	public function get_user_role( $chat_id ) {
		$settings = $this->get_settings();
		$chat_id  = trim( (string) $chat_id );
		$group    = $this->is_explicit_group_chat_id( $chat_id ) ? $this->normalize_group_jid( $chat_id ) : '';
		if ( '' !== $group ) {
			if ( $this->access_entries_contain_group( $settings['admin_group_jids'], $group ) ) {
				return 'admin';
			}
			if ( $this->access_entries_contain_group( $settings['stock_manager_group_jids'], $group ) ) {
				return 'stock_manager';
			}
			if ( $this->access_entries_contain_group( $settings['lead_group_jids'], $group ) ) {
				return 'lead';
			}
			return null;
		}

		$sender   = $this->normalize_sender_id( $chat_id );

		if ( '' === $sender ) {
			return null;
		}

		if ( $this->access_entries_contain_phone( $settings['admin_numbers'], $sender ) ) {
			return 'admin';
		}
		if ( $this->access_entries_contain_phone( $settings['stock_manager_numbers'], $sender ) ) {
			return 'stock_manager';
		}
		if ( $this->access_entries_contain_phone( $settings['lead_numbers'], $sender ) ) {
			return 'lead';
		}
		return null;
	}

	public function normalize_access_phone( $phone ) {
		$normalized = $this->normalize_sender_id( $phone );
		if ( '' !== $normalized && '0' === $normalized[0] ) {
			$normalized = class_exists( 'Niwa_AI_Phone_Normalizer' )
				? (string) Niwa_AI_Phone_Normalizer::normalize( $phone )
				: '';
		}
		return '' !== $normalized ? $this->normalize_sender_id( $normalized ) : '';
	}

	public function normalize_group_jid( $jid ) {
		return Niwa_AI_Admin_Autonomy_WhatsApp_Identity::normalize_group_jid( $jid );
	}

	public function parse_access_number_lines( $text ) {
		$lines   = preg_split( '/\r\n|\r|\n/', (string) $text );
		$entries = array();
		foreach ( is_array( $lines ) ? $lines : array() as $line ) {
			$line = trim( (string) $line );
			if ( '' === $line ) {
				continue;
			}

			$label = '';
			$phone = $line;
			if ( false !== strpos( $line, '|' ) ) {
				$parts = array_map( 'trim', explode( '|', $line, 2 ) );
				$label = isset( $parts[0] ) ? $parts[0] : '';
				$phone = isset( $parts[1] ) ? $parts[1] : '';
			}

			$entries[] = array(
				'phone' => $phone,
				'label' => $label,
			);
		}

		return $this->sanitize_access_number_entries( $entries );
	}

	public function sanitize_access_number_entries( $entries ) {
		$entries = Niwa_AI_Admin_Autonomy_WhatsApp_Access_Policy::sanitize_entries( $entries );
		foreach ( $entries as &$entry ) {
			if ( isset( $entry['phone'] ) ) {
				$entry['phone'] = '*' === trim( (string) $entry['phone'] )
					? '*'
					: $this->normalize_access_phone( $entry['phone'] );
			}
		}
		unset( $entry );
		return Niwa_AI_Admin_Autonomy_WhatsApp_Access_Policy::sanitize_entries( $entries );
	}

	/**
	 * Keep each canonical phone in only its highest access role.
	 *
	 * Admin settings submit all three lists independently, so moving a saved
	 * lead into Admins can otherwise leave the old lead row behind. Resolving
	 * that conflict here also covers phone/LID aliases through the canonical
	 * sender normalizer used by sanitize_access_number_entries().
	 */
	public function enforce_number_role_precedence( $settings ) {
		$settings = is_array( $settings ) ? $settings : array();
		$seen     = array();

		foreach ( $this->get_access_role_keys() as $role => $key ) {
			$entries = $this->sanitize_access_number_entries_for_role(
				isset( $settings[ $key ] ) ? $settings[ $key ] : array(),
				$role
			);
			$settings[ $key ] = array_values(
				array_filter(
					$entries,
					function ( $entry ) use ( &$seen ) {
						$phone = isset( $entry['phone'] ) ? (string) $entry['phone'] : '';
						if ( '' === $phone || '*' === $phone ) {
							return true;
						}
						if ( isset( $seen[ $phone ] ) ) {
							return false;
						}
						$seen[ $phone ] = true;
						return true;
					}
				)
			);
		}

		return $settings;
	}

	private function sanitize_access_number_entries_for_role( $entries, $role ) {
		$entries = $this->sanitize_access_number_entries( $entries );
		$role    = $this->normalize_access_role( $role );
		if ( 'lead' === $role ) {
			return $entries;
		}

		return array_values(
			array_filter(
				$entries,
				function ( $entry ) {
					return '*' !== trim( (string) ( isset( $entry['phone'] ) ? $entry['phone'] : '' ) );
				}
			)
		);
	}

	public function sanitize_access_group_entries( $entries ) {
		return Niwa_AI_Admin_Autonomy_WhatsApp_Access_Policy::sanitize_group_entries( $entries );
	}

	private function sanitize_access_group_entries_for_role( $entries, $role ) {
		unset( $role );
		return $this->sanitize_access_group_entries( $entries );
	}

	public function manage_access_numbers( $action, $role, $phone = '', $label = '' ) {
		$action = sanitize_key( (string) $action );
		$role   = $this->normalize_access_role( $role );
		if ( ! in_array( $action, array( 'add', 'remove', 'list' ), true ) ) {
			return array(
				'success' => false,
				'ok'      => false,
				'error'   => 'Nepodrzana WhatsApp access akcija.',
			);
		}

		$settings          = get_option( self::OPTION_KEY, array() );
		$settings          = is_array( $settings ) ? $settings : array();
		$original_settings = $settings;
		$wa       = isset( $settings['whatsapp'] ) && is_array( $settings['whatsapp'] ) ? $settings['whatsapp'] : array();
		$wa       = wp_parse_args( $wa, $this->get_defaults() );
		if ( '' === $role ) {
			return array(
				'success' => false,
				'ok'      => false,
				'error'   => 'Nepoznata WhatsApp access rola.',
			);
		}
		foreach ( $this->get_access_role_keys() as $entry_role => $entry_key ) {
			$wa[ $entry_key ] = $this->sanitize_access_number_entries_for_role( isset( $wa[ $entry_key ] ) ? $wa[ $entry_key ] : array(), $entry_role );
		}
		foreach ( $this->get_access_group_role_keys() as $entry_role => $entry_key ) {
			$wa[ $entry_key ] = $this->sanitize_access_group_entries_for_role( isset( $wa[ $entry_key ] ) ? $wa[ $entry_key ] : array(), $entry_role );
		}
		$changed = false;

		$key     = $this->role_to_access_number_key( $role );
		if ( 'list' !== $action ) {
			$is_public_lead_wildcard = '*' === trim( (string) $phone );
			if ( $is_public_lead_wildcard && 'lead' !== $role ) {
				return array(
					'success' => false,
					'ok'      => false,
					'error'   => 'whatsapp_public_wildcard_allowed_only_for_leads',
				);
			}
			$phone = $is_public_lead_wildcard ? '*' : $this->normalize_access_phone( $phone );
			if ( '' === $phone ) {
				return array(
					'success' => false,
					'ok'      => false,
					'error'   => 'WhatsApp broj je obavezan za add/remove.',
				);
			}
		}

		if ( 'add' === $action ) {
			$existing_role = '*' === $phone ? '' : $this->find_explicit_access_role_for_phone( $wa, $phone );
			if ( '' !== $existing_role && $existing_role !== $role && $this->access_role_rank( $existing_role ) >= $this->access_role_rank( $role ) ) {
				return array(
					'success'       => false,
					'ok'            => false,
					'error'         => 'whatsapp_role_demotion_refused',
					'existing_role' => $existing_role,
					'requested_role' => $role,
				);
			}

			foreach ( $this->get_access_number_keys() as $other_key ) {
				if ( $other_key === $key ) {
					continue;
				}
				$before_other = count( $wa[ $other_key ] );
				$wa[ $other_key ] = array_values(
					array_filter(
						$wa[ $other_key ],
						function ( $entry ) use ( $phone ) {
							$current = isset( $entry['phone'] ) ? (string) $entry['phone'] : '';
							return '' === $current || '*' === $current || ! hash_equals( $current, $phone );
						}
					)
				);
				if ( count( $wa[ $other_key ] ) !== $before_other ) {
					$changed = true;
				}
			}
			if ( ! $this->access_entries_contain_phone( $wa[ $key ], $phone ) ) {
				$wa[ $key ][] = array(
					'phone' => $phone,
					'label' => function_exists( 'sanitize_text_field' ) ? sanitize_text_field( mb_substr( (string) $label, 0, 100 ) ) : substr( (string) $label, 0, 100 ),
				);
				$changed = true;
			}
		} elseif ( 'remove' === $action ) {
			$before    = count( $wa[ $key ] );
			$wa[ $key ] = array_values(
				array_filter(
					$wa[ $key ],
					function ( $entry ) use ( $phone ) {
						$current = isset( $entry['phone'] ) ? (string) $entry['phone'] : '';
						return '' === $current || ! hash_equals( $current, $phone );
					}
				)
			);
			$changed = count( $wa[ $key ] ) !== $before;
		}

		$deduplicated = $this->enforce_number_role_precedence( $wa );
		if ( $deduplicated !== $wa ) {
			$wa      = $deduplicated;
			$changed = true;
		}
		$settings['whatsapp'] = $wa;
		if ( $changed ) {
			update_option( self::OPTION_KEY, $settings, false );
		}
		$policy_synced = 'list' === $action ? true : $this->sync_access_policy_file( $wa );
		if ( ! $policy_synced ) {
			if ( $changed ) {
				update_option( self::OPTION_KEY, $original_settings, false );
			}
			return array(
				'success' => false,
				'ok'      => false,
				'error'   => 'whatsapp_access_policy_sync_failed',
				'action'  => $action,
				'role'    => $role,
				'changed' => false,
			);
		}

		return array(
			'success' => true,
			'ok'      => true,
			'action'  => $action,
			'role'    => $role,
			'changed' => $changed,
			'policy_synced' => $policy_synced,
			'summary' => $this->build_access_numbers_summary( $wa ),
			'counts'  => array(
				'admin'         => count( $wa['admin_numbers'] ),
				'stock_manager' => count( $wa['stock_manager_numbers'] ),
				'lead'          => count( $wa['lead_numbers'] ),
			),
		);
	}

	public function send_message( $chat_id, $text, $context = array() ) {
		$this->ensure_state_dirs();
		$context = is_array( $context ) ? $context : array();
		$status = $this->get_gateway_status();
		if ( empty( $status['running'] ) ) {
			$started = $this->start_gateway( false );
			if ( empty( $started['success'] ) ) {
				return $started;
			}
		}

		if ( class_exists( 'Niwa_AI_Admin_Autonomy_Delivery_Envelope_Normalizer' ) ) {
			$envelope = Niwa_AI_Admin_Autonomy_Delivery_Envelope_Normalizer::normalize( $text );
			if ( empty( $envelope['send'] ) ) {
				return array(
					'success' => true,
					'queued'  => false,
					'skipped' => true,
					'reason'  => ! empty( $envelope['unwrapped'] ) ? 'delivery_envelope_send_false' : 'empty_message',
				);
			}
			$text = isset( $envelope['message'] ) ? (string) $envelope['message'] : '';
		}

		$route  = $this->resolve_outbound_target( $chat_id, $context );
		$text   = trim( (string) $text );
		if ( empty( $route['to'] ) || empty( $route['jid'] ) || '' === $text ) {
			return array(
				'success' => false,
				'error'   => 'invalid_outbound_message',
			);
		}

		$chunks       = $this->split_text( $text );
		$ids          = array();
		$workflow_key = isset( $context['workflow_key'] ) ? sanitize_key( (string) $context['workflow_key'] ) : '';
		foreach ( $chunks as $index => $chunk ) {
			$id = '' !== $workflow_key
				? 'wf-' . substr( hash( 'sha256', $route['jid'] . '|' . $workflow_key . '|' . (int) $index ), 0, 40 )
				: ( function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : md5( uniqid( 'wa_', true ) ) );
			$target = $this->get_state_dir() . '/outbox/' . $id . '.json';
			if ( '' !== $workflow_key && $this->workflow_outbox_entry_exists( $id ) ) {
				$ids[] = $id;
				continue;
			}

			$payload = wp_json_encode(
				array(
					'id'           => $id,
					'type'         => 'message',
					'to'           => $route['to'],
					'jid'          => $route['jid'],
					'text'         => $chunk,
					'at'           => time(),
					'workflow_key' => $workflow_key,
				)
			);
			$ok = '' !== $workflow_key
				? $this->write_new_outbox_file( $target, $payload )
				: false !== $this->write_private_state_file( $target, $payload );
			if ( false === $ok ) {
				if ( '' !== $workflow_key && $this->workflow_outbox_entry_exists( $id ) ) {
					$ids[] = $id;
					continue;
				}
				return array(
					'success' => false,
					'error'   => 'outbox_write_failed',
				);
			}
			$ids[] = $id;
		}

		return array(
			'success' => true,
			'queued'  => true,
			'ids'     => $ids,
		);
	}

	private function workflow_outbox_entry_exists( $id ) {
		$id    = preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $id );
		$state = $this->get_state_dir();
		return is_file( $state . '/outbox/' . $id . '.json' )
			|| is_file( $state . '/outbox/' . $id . '.json.sending' )
			|| is_file( $state . '/sent/' . $id . '.json.sent' );
	}

	private function write_new_outbox_file( $target, $payload ) {
		$target = (string) $target;
		$temp   = $target . '.' . getmypid() . '.' . substr( hash( 'sha256', uniqid( '', true ) ), 0, 12 ) . '.tmp';
		$bytes  = $this->write_private_state_file( $temp, (string) $payload, LOCK_EX );
		if ( false === $bytes || $bytes !== strlen( (string) $payload ) ) {
			@unlink( $temp );
			return false;
		}

		// A hard-link publish is atomic and fails when another worker already
		// created the deterministic target. The gateway can therefore never
		// claim a partially-written JSON file.
		$published = @link( $temp, $target );
		@unlink( $temp );
		return $published;
	}

	public function send_chat_action( $chat_id, $action = 'typing', $context = array() ) {
		$this->ensure_state_dirs();
		$context = is_array( $context ) ? $context : array();
		$status  = $this->get_gateway_status();
		if ( empty( $status['running'] ) ) {
			$started = $this->start_gateway( false );
			if ( empty( $started['success'] ) ) {
				return $started;
			}
		}

		$route    = $this->resolve_outbound_target( $chat_id, $context );
		$presence = $this->normalize_presence_action( $action );
		if ( empty( $route['to'] ) || empty( $route['jid'] ) || '' === $presence ) {
			return array(
				'success' => false,
				'error'   => 'invalid_presence_action',
			);
		}

		$id     = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : md5( uniqid( 'wa_presence_', true ) );
		$target = $this->get_state_dir() . '/outbox/' . $id . '.json';
		$ok     = $this->write_private_state_file(
			$target,
			wp_json_encode(
				array(
					'id'       => $id,
					'type'     => 'presence',
					'to'       => $route['to'],
					'jid'      => $route['jid'],
					'presence' => $presence,
					'at'       => time(),
				)
			)
		);
		if ( false === $ok ) {
			return array(
				'success' => false,
				'error'   => 'outbox_write_failed',
			);
		}

		return array(
			'success' => true,
			'queued'  => true,
			'id'      => $id,
		);
	}

	public function send_media_message( $chat_id, $file_path, $caption = '', $context = array() ) {
		$this->ensure_state_dirs();
		$context = is_array( $context ) ? $context : array();
		$status  = $this->get_gateway_status();
		if ( empty( $status['running'] ) ) {
			$started = $this->start_gateway( false );
			if ( empty( $started['success'] ) ) {
				return $started;
			}
		}

		$route     = $this->resolve_outbound_target( $chat_id, $context );
		$file_path = (string) $file_path;
		$real_path = realpath( $file_path );
		if ( empty( $route['to'] ) || empty( $route['jid'] ) || false === $real_path || ! is_file( $real_path ) || ! is_readable( $real_path ) ) {
			return array(
				'success' => false,
				'error'   => 'invalid_outbound_media',
			);
		}

		$mime = $this->detect_outbound_media_mime_type( $real_path );
		$kind = $this->detect_outbound_media_kind( $real_path, $mime );
		if ( '' === $kind ) {
			return array(
				'success' => false,
				'error'   => 'unsupported_outbound_media_type',
			);
		}

		$id     = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : md5( uniqid( 'wa_media_', true ) );
		$target = $this->get_state_dir() . '/outbox/' . $id . '.json';
		$ok     = $this->write_private_state_file(
			$target,
			wp_json_encode(
				array(
					'id'         => $id,
					'type'       => 'media',
					'to'         => $route['to'],
					'jid'        => $route['jid'],
					'file_path'  => $real_path,
					'file_name'  => basename( $real_path ),
					'media_kind' => $kind,
					'mime_type'  => $mime,
					'caption'    => function_exists( 'mb_substr' ) ? mb_substr( trim( (string) $caption ), 0, 1024 ) : substr( trim( (string) $caption ), 0, 1024 ),
					'at'         => time(),
				)
			)
		);
		if ( false === $ok ) {
			return array(
				'success' => false,
				'error'   => 'outbox_write_failed',
			);
		}

		return array(
			'success'    => true,
			'queued'     => true,
			'id'         => $id,
			'media_kind' => $kind,
			'mime_type'  => $mime,
		);
	}

	private function normalize_gateway_message( $raw ) {
		$raw     = is_array( $raw ) ? $raw : array();
		$from    = isset( $raw['from'] ) ? $this->normalize_sender_id( $raw['from'] ) : '';
		$chat_id = isset( $raw['chat_id'] ) ? (string) $raw['chat_id'] : ( '' !== $from ? 'whatsapp:' . $from : '' );
		$id      = isset( $raw['id'] ) ? (string) $raw['id'] : '';
		if ( '' === $chat_id || '' === $id ) {
			return array( 'success' => false, 'error' => 'missing_sender_or_message_id' );
		}

		$type = isset( $raw['type'] ) ? sanitize_key( (string) $raw['type'] ) : 'text';
		$text = isset( $raw['text'] ) ? (string) $raw['text'] : '';
		$media_refs = array();
		if ( ! empty( $raw['media'] ) && is_array( $raw['media'] ) ) {
			$media = $raw['media'];
			$kind  = isset( $media['kind'] ) ? sanitize_key( (string) $media['kind'] ) : $type;
			$media_ref = array(
				'kind'       => 'image' === $kind ? 'image' : ( 'audio' === $kind ? 'audio' : ( 'video' === $kind ? 'video' : ( 'sticker' === $kind ? 'sticker' : 'document' ) ) ),
				'file_id'    => isset( $media['local_path'] ) ? md5( (string) $media['local_path'] ) : $id,
				'local_path' => isset( $media['local_path'] ) ? (string) $media['local_path'] : '',
				'mime_hint'  => isset( $media['mime_hint'] ) ? (string) $media['mime_hint'] : '',
				'file_name'  => isset( $media['file_name'] ) ? (string) $media['file_name'] : '',
				'caption'    => isset( $media['caption'] ) ? (string) $media['caption'] : '',
				'channel'    => 'whatsapp',
			);
			if ( Niwa_AI_Admin_Autonomy_Audio_Utils::is_audio_media_ref( $media_ref ) ) {
				$media_ref['kind'] = 'audio';
			}
			$media_refs[] = $media_ref;
			if ( '' === trim( $text ) && ! empty( $media['caption'] ) ) {
				$text = (string) $media['caption'];
			}
			if ( '' === trim( $text ) && 'audio' !== $media_ref['kind'] ) {
				$text = '<whatsapp:' . $type . '>';
			}
		}

		return array(
			'success'           => true,
			'channel'           => 'whatsapp',
			'chat_id'           => $chat_id,
			'message_id'        => abs( crc32( $id ) ),
			'gateway_message_id' => isset( $raw['messageId'] ) ? (string) $raw['messageId'] : $id,
			'gateway_chat_id'   => isset( $raw['chatId'] ) ? $this->normalize_gateway_jid( $raw['chatId'] ) : '',
			'gateway_sender_id' => isset( $raw['senderId'] ) ? $this->normalize_gateway_jid( $raw['senderId'] ) : '',
			'update_id'         => 0,
			'text'              => $text,
			'text_only'         => $text,
			'caption'           => '',
			'date'              => isset( $raw['timestamp'] ) ? intval( $raw['timestamp'] ) : time(),
			'from_id'           => $from,
			'from_me'           => ! empty( $raw['from_me'] ),
			'body'              => isset( $raw['body'] ) ? (string) $raw['body'] : $text,
			'sender_name'       => isset( $raw['senderName'] ) ? (string) $raw['senderName'] : '',
			'chat_name'         => isset( $raw['chatName'] ) ? (string) $raw['chatName'] : '',
			'is_group'          => ! empty( $raw['isGroup'] ),
			'has_media'         => ! empty( $raw['hasMedia'] ),
			'media_type'        => isset( $raw['mediaType'] ) ? sanitize_key( (string) $raw['mediaType'] ) : '',
			'media_urls'        => isset( $raw['mediaUrls'] ) && is_array( $raw['mediaUrls'] ) ? array_values( array_map( 'strval', $raw['mediaUrls'] ) ) : array(),
			'mentioned_ids'     => isset( $raw['mentionedIds'] ) && is_array( $raw['mentionedIds'] ) ? array_values( array_map( array( $this, 'normalize_gateway_jid' ), $raw['mentionedIds'] ) ) : array(),
			'quoted_message_id' => isset( $raw['quotedMessageId'] ) ? (string) $raw['quotedMessageId'] : '',
			'quoted_participant' => isset( $raw['quotedParticipant'] ) ? $this->normalize_gateway_jid( $raw['quotedParticipant'] ) : '',
			'quoted_remote_jid' => isset( $raw['quotedRemoteJid'] ) ? $this->normalize_gateway_jid( $raw['quotedRemoteJid'] ) : '',
			'has_quoted_message' => ! empty( $raw['hasQuotedMessage'] ),
			'bot_ids'           => isset( $raw['botIds'] ) && is_array( $raw['botIds'] ) ? array_values( array_map( array( $this, 'normalize_gateway_jid' ), $raw['botIds'] ) ) : array(),
			'remote_jid'        => isset( $raw['remote_jid'] ) ? $this->normalize_gateway_jid( $raw['remote_jid'] ) : '',
			'phone_jid'         => isset( $raw['phone_jid'] ) ? $this->normalize_gateway_jid( $raw['phone_jid'] ) : '',
			'chat_jid'          => isset( $raw['chat_jid'] ) ? $this->normalize_gateway_jid( $raw['chat_jid'] ) : '',
			'sender_jid'        => isset( $raw['sender_jid'] ) ? $this->normalize_gateway_jid( $raw['sender_jid'] ) : '',
			'from_username'     => '',
			'from_first_name'   => isset( $raw['push_name'] ) ? (string) $raw['push_name'] : '',
			'language_code'     => '',
			'kind'              => $type,
			'placeholder'       => '',
			'media_refs'        => $media_refs,
			'reply_media_refs'  => array(),
			'has_user_text'     => '' !== trim( $text ),
			'is_album'          => false,
			'is_text_fragment'  => false,
			'is_edited'         => false,
			'media_group_id'    => '',
			'forward'           => array( 'is_forwarded' => false ),
			'location'          => null,
			'contact'           => null,
			'callback_query_id' => '',
			'callback_data'     => isset( $raw['callback_data'] ) ? (string) $raw['callback_data'] : '',
			'callback_origin_message_id' => 0,
			'raw_message_id'    => $id,
		);
	}

	private function ensure_gateway_secret() {
		$settings = get_option( self::OPTION_KEY, array() );
		$settings = is_array( $settings ) ? $settings : array();
		$wa       = isset( $settings['whatsapp'] ) && is_array( $settings['whatsapp'] ) ? $settings['whatsapp'] : array();
		$secret   = isset( $wa['gateway_secret'] ) ? trim( (string) $wa['gateway_secret'] ) : '';
		if ( '' !== $secret ) {
			$this->sync_gateway_secret_file( $secret );
			return $secret;
		}

		$secret = function_exists( 'wp_generate_password' ) ? wp_generate_password( 48, false, false ) : md5( uniqid( 'niwa_wa_', true ) );
		$wa['gateway_secret'] = $secret;
		$settings['whatsapp'] = $wa;
		update_option( self::OPTION_KEY, $settings, false );
		$this->sync_gateway_secret_file( $secret );
		return $secret;
	}

	public function sync_gateway_secret_file( $secret = '' ) {
		$secret = trim( (string) $secret );
		if ( '' === $secret ) {
			$settings = get_option( self::OPTION_KEY, array() );
			$settings = is_array( $settings ) ? $settings : array();
			$wa       = isset( $settings['whatsapp'] ) && is_array( $settings['whatsapp'] ) ? $settings['whatsapp'] : array();
			$secret   = isset( $wa['gateway_secret'] ) ? trim( (string) $wa['gateway_secret'] ) : '';
		}
		if ( '' === $secret ) {
			return false;
		}

		$this->ensure_state_dirs();
		$path    = $this->get_state_dir() . '/.gateway-secret.json';
		$tmp     = $path . '.tmp';
		$payload = wp_json_encode(
			array(
				'secret'    => $secret,
				'updatedAt' => gmdate( 'c' ),
			)
		);
		if ( false === $this->write_private_state_file( $tmp, $payload, LOCK_EX ) ) {
			return false;
		}
		@chmod( $tmp, 0600 );
		if ( ! @rename( $tmp, $path ) ) {
			@unlink( $tmp );
			return false;
		}
		@chmod( $path, 0600 );
		return true;
	}

	public function sync_access_policy_file( $settings = null ) {
		$wa = is_array( $settings ) ? $settings : $this->get_settings();
		foreach ( $this->get_access_role_keys() as $role => $key ) {
			$wa[ $key ] = $this->sanitize_access_number_entries_for_role( isset( $wa[ $key ] ) ? $wa[ $key ] : array(), $role );
		}
		foreach ( $this->get_access_group_role_keys() as $role => $key ) {
			$wa[ $key ] = $this->sanitize_access_group_entries_for_role( isset( $wa[ $key ] ) ? $wa[ $key ] : array(), $role );
		}

		$this->ensure_state_dirs();
		$payload = array(
			'admins'                   => $this->access_entries_to_numbers( isset( $wa['admin_numbers'] ) ? $wa['admin_numbers'] : array() ),
			'stock_managers'           => $this->access_entries_to_numbers( isset( $wa['stock_manager_numbers'] ) ? $wa['stock_manager_numbers'] : array() ),
			'leads'                    => $this->access_entries_to_numbers( isset( $wa['lead_numbers'] ) ? $wa['lead_numbers'] : array() ),
			'admin_groups'             => $this->access_entries_to_group_jids( isset( $wa['admin_group_jids'] ) ? $wa['admin_group_jids'] : array() ),
			'stock_manager_groups'     => $this->access_entries_to_group_jids( isset( $wa['stock_manager_group_jids'] ) ? $wa['stock_manager_group_jids'] : array() ),
			'lead_groups'              => $this->access_entries_to_group_jids( isset( $wa['lead_group_jids'] ) ? $wa['lead_group_jids'] : array() ),
			'updatedAt'                => gmdate( 'c' ),
		);
		$path = $this->get_state_dir() . '/access-policy.json';
		$tmp  = $path . '.tmp';
		if ( false === $this->write_private_state_file( $tmp, wp_json_encode( $payload ), LOCK_EX ) ) {
			return false;
		}
		@chmod( $tmp, 0600 );
		if ( ! @rename( $tmp, $path ) ) {
			@unlink( $tmp );
			return false;
		}
		@chmod( $path, 0600 );
		return true;
	}

	private function get_access_number_keys() {
		return Niwa_AI_Admin_Autonomy_WhatsApp_Access_Policy::number_keys();
	}

	private function get_access_role_keys() {
		return array(
			'admin'         => 'admin_numbers',
			'stock_manager' => 'stock_manager_numbers',
			'lead'          => 'lead_numbers',
		);
	}

	private function get_access_group_role_keys() {
		return array(
			'admin'         => 'admin_group_jids',
			'stock_manager' => 'stock_manager_group_jids',
			'lead'          => 'lead_group_jids',
		);
	}

	private function normalize_access_role( $role ) {
		return Niwa_AI_Admin_Autonomy_WhatsApp_Access_Policy::normalize_role( $role );
	}

	private function role_to_access_number_key( $role ) {
		return Niwa_AI_Admin_Autonomy_WhatsApp_Access_Policy::role_to_number_key( $role );
	}

	private function access_entries_contain_phone( $entries, $phone ) {
		return Niwa_AI_Admin_Autonomy_WhatsApp_Access_Policy::entries_contain_phone( $entries, $phone );
	}

	private function access_entries_contain_group( $entries, $jid ) {
		return Niwa_AI_Admin_Autonomy_WhatsApp_Access_Policy::entries_contain_group( $entries, $jid );
	}

	private function find_explicit_access_role_for_phone( $wa, $phone ) {
		$phone = $this->normalize_access_phone( $phone );
		if ( '' === $phone ) {
			return '';
		}

		foreach ( $this->get_access_role_keys() as $role => $key ) {
			$entries = isset( $wa[ $key ] ) ? $this->sanitize_access_number_entries_for_role( $wa[ $key ], $role ) : array();
			foreach ( $entries as $entry ) {
				$current = isset( $entry['phone'] ) ? (string) $entry['phone'] : '';
				if ( '' !== $current && '*' !== $current && hash_equals( $current, $phone ) ) {
					return $role;
				}
			}
		}

		return '';
	}

	private function access_role_rank( $role ) {
		$role = $this->normalize_access_role( $role );
		if ( 'admin' === $role ) {
			return 300;
		}
		if ( 'stock_manager' === $role ) {
			return 200;
		}
		if ( 'lead' === $role ) {
			return 100;
		}

		return 0;
	}

	private function build_access_numbers_summary( $wa ) {
		return Niwa_AI_Admin_Autonomy_WhatsApp_Access_Policy::build_summary( $wa );
	}

	private function detect_outbound_media_mime_type( $file_path ) {
		$mime = function_exists( 'mime_content_type' ) ? (string) @mime_content_type( $file_path ) : '';
		if ( '' !== $mime && 'application/octet-stream' !== $mime ) {
			return $mime;
		}

		$ext = strtolower( pathinfo( (string) $file_path, PATHINFO_EXTENSION ) );
		$map = array(
			'jpg'  => 'image/jpeg',
			'jpeg' => 'image/jpeg',
			'png'  => 'image/png',
			'webp' => 'image/webp',
			'gif'  => 'image/gif',
			'mp4'  => 'video/mp4',
			'mov'  => 'video/quicktime',
			'mp3'  => 'audio/mpeg',
			'ogg'  => 'audio/ogg',
			'opus' => 'audio/ogg',
			'pdf'  => 'application/pdf',
		);

		return isset( $map[ $ext ] ) ? $map[ $ext ] : 'application/octet-stream';
	}

	private function detect_outbound_media_kind( $file_path, $mime ) {
		$mime = strtolower( (string) $mime );
		if ( 0 === strpos( $mime, 'image/' ) ) {
			return 'image';
		}
		if ( 0 === strpos( $mime, 'video/' ) ) {
			return 'video';
		}
		if ( 0 === strpos( $mime, 'audio/' ) ) {
			return 'audio';
		}

		$ext = strtolower( pathinfo( (string) $file_path, PATHINFO_EXTENSION ) );
		if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'webp', 'gif' ), true ) ) {
			return 'image';
		}
		if ( in_array( $ext, array( 'mp4', 'mov' ), true ) ) {
			return 'video';
		}
		if ( in_array( $ext, array( 'mp3', 'ogg', 'opus' ), true ) ) {
			return 'audio';
		}
		if ( '' !== $ext ) {
			return 'document';
		}

		return '';
	}

	private function access_entries_to_numbers( $entries ) {
		$numbers = array();
		foreach ( $this->sanitize_access_number_entries( $entries ) as $entry ) {
			if ( ! empty( $entry['phone'] ) ) {
				$numbers[] = (string) $entry['phone'];
			}
		}
		return array_values( array_unique( $numbers ) );
	}

	private function access_entries_to_group_jids( $entries ) {
		return Niwa_AI_Admin_Autonomy_WhatsApp_Access_Policy::group_entries_to_jids( $entries );
	}

	private function resolve_node_binary() {
		$settings = $this->get_settings();
		$custom   = isset( $settings['node_path'] ) ? trim( (string) $settings['node_path'] ) : '';
		$candidates = array_filter(
			array(
				$custom,
				'/opt/alt/alt-nodejs20/root/usr/bin/node',
				'/opt/alt/alt-nodejs18/root/usr/bin/node',
				'/usr/local/bin/node',
				'/usr/bin/node',
				'node',
			)
		);

		foreach ( $candidates as $candidate ) {
			if ( 'node' === $candidate || ( is_file( $candidate ) && is_executable( $candidate ) ) ) {
				return $candidate;
			}
		}

		return '';
	}

	private function get_gateway_mode() {
		$settings = $this->get_settings();
		$mode     = isset( $settings['gateway_mode'] ) ? sanitize_key( (string) $settings['gateway_mode'] ) : 'auto';
		return 'external' === $mode ? 'external' : 'auto';
	}

	private function prepare_gateway_supervisor_script( $supervisor ) {
		$supervisor = (string) $supervisor;
		$contents   = @file_get_contents( $supervisor );
		if ( ! is_string( $contents ) || '' === $contents ) {
			return array( 'success' => false, 'error' => 'gateway_supervisor_unreadable' );
		}

		$normalized = preg_replace( "/\r\n?/", "\n", $contents );
		if ( ! is_string( $normalized ) ) {
			return array( 'success' => false, 'error' => 'gateway_supervisor_normalize_failed' );
		}
		if ( $normalized === $contents ) {
			return array( 'success' => true, 'path' => $supervisor );
		}

		$runtime_supervisor = $this->get_state_dir() . '/whatsapp-gateway-supervisor.runtime.sh';
		$current           = is_readable( $runtime_supervisor ) ? @file_get_contents( $runtime_supervisor ) : null;
		if ( $current !== $normalized && false === $this->write_private_state_file( $runtime_supervisor, $normalized, LOCK_EX ) ) {
			return array( 'success' => false, 'error' => 'gateway_supervisor_runtime_write_failed' );
		}
		@chmod( $runtime_supervisor, 0600 );

		$this->write_private_state_file(
			$this->get_state_dir() . '/launcher.log',
			'[' . gmdate( 'c' ) . '] normalized WhatsApp gateway supervisor line endings' . "\n",
			FILE_APPEND
		);

		return array( 'success' => true, 'path' => $runtime_supervisor );
	}

	private function should_attempt_gateway_auto_restart( $status, $pid ) {
		if ( ! $this->is_enabled() || 'auto' !== $this->get_gateway_mode() ) {
			return false;
		}

		$current = isset( $status['status'] ) ? (string) $status['status'] : '';
		if ( 'stopping' === $current && ! is_readable( $this->get_state_dir() . '/supervisor.stop' ) ) {
			$current = 'connected';
		}
		if ( in_array( $current, array( 'logged_out', 'stopping', 'not_started' ), true ) ) {
			return false;
		}

		$updated_at = isset( $status['updatedAt'] ) ? strtotime( (string) $status['updatedAt'] ) : 0;
		if ( intval( $pid ) <= 0 && $updated_at > 0 && ( time() - $updated_at ) < 20 ) {
			return false;
		}

		$key = 'niwa_ai_wa_gateway_restart_' . md5( $this->get_state_dir() );
		if ( get_transient( $key ) ) {
			return false;
		}

		set_transient( $key, '1', MINUTE_IN_SECONDS );
		return true;
	}

	private function is_runtime_refresh_guard_active() {
		$path = $this->get_state_dir() . '/.runtime-refresh.active.json';
		if ( ! is_file( $path ) ) {
			return false;
		}
		$payload    = $this->read_json_file( $path );
		$started_at = isset( $payload['started_at'] ) ? intval( $payload['started_at'] ) : intval( @filemtime( $path ) );
		if ( $started_at > 0 && ( time() - $started_at ) <= 300 ) {
			return true;
		}
		@unlink( $path );
		return false;
	}

	private function node_supports_no_experimental_fetch( $node ) {
		static $cache = array();
		$key = (string) $node;
		if ( array_key_exists( $key, $cache ) ) {
			return $cache[ $key ];
		}

		$cmd     = escapeshellarg( $node ) . ' --no-experimental-fetch -e ' . escapeshellarg( 'process.exit(typeof fetch === "undefined" ? 0 : 2)' );
		$process = @proc_open(
			'/bin/sh -c ' . escapeshellarg( $cmd ),
			array(
				0 => array( 'file', '/dev/null', 'r' ),
				1 => array( 'file', '/dev/null', 'w' ),
				2 => array( 'file', '/dev/null', 'w' ),
			),
			$pipes,
			defined( 'ABSPATH' ) ? ABSPATH : null
		);

		if ( ! is_resource( $process ) ) {
			$cache[ $key ] = false;
			return false;
		}

		foreach ( (array) $pipes as $pipe ) {
			if ( is_resource( $pipe ) ) {
				fclose( $pipe );
			}
		}
		$cache[ $key ] = 0 === intval( @proc_close( $process ) );
		return $cache[ $key ];
	}

	private function read_pid_file() {
		$pid_file = $this->get_state_dir() . '/gateway.pid';
		return is_readable( $pid_file ) ? intval( trim( (string) @file_get_contents( $pid_file ) ) ) : 0;
	}

	private function read_supervisor_pid_file() {
		$pid_file = $this->get_state_dir() . '/supervisor.pid';
		return is_readable( $pid_file ) ? intval( trim( (string) @file_get_contents( $pid_file ) ) ) : 0;
	}

	private function clear_gateway_auth_state() {
		$state_dir = $this->get_state_dir();
		$auth_dir  = $state_dir . '/auth';
		if ( ! is_dir( $auth_dir ) ) {
			$old_umask = umask( 0077 );
			try {
				wp_mkdir_p( $auth_dir );
			} finally {
				umask( $old_umask );
			}
			$this->protect_state_dir( $auth_dir );
			return;
		}

		$state_root = realpath( $state_dir );
		$auth_root  = realpath( $auth_dir );
		if ( false === $state_root || false === $auth_root || 0 !== strpos( $auth_root, $state_root . DIRECTORY_SEPARATOR ) ) {
			return;
		}

		$items = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator( $auth_root, FilesystemIterator::SKIP_DOTS ),
			RecursiveIteratorIterator::CHILD_FIRST
		);
		foreach ( $items as $item ) {
			$path = $item->getPathname();
			if ( $item->isDir() && ! $item->isLink() ) {
				@rmdir( $path );
				continue;
			}
			@unlink( $path );
		}
	}

	private function process_is_alive( $pid ) {
		$pid = intval( $pid );
		if ( $pid <= 0 ) {
			return false;
		}
		if ( is_readable( '/proc/' . $pid . '/stat' ) ) {
			$stat = @file_get_contents( '/proc/' . $pid . '/stat' );
			if ( false === $stat ) {
				return false;
			}
			$close = strrpos( $stat, ')' );
			if ( false === $close ) {
				return true;
			}
			$state = substr( ltrim( substr( $stat, $close + 1 ) ), 0, 1 );
			if ( in_array( $state, array( 'Z', 'X', 'x' ), true ) ) {
				return false;
			}
			$cmdline = '/proc/' . $pid . '/cmdline';
			if ( is_readable( $cmdline ) ) {
				$cmd = str_replace( "\0", ' ', (string) @file_get_contents( $cmdline ) );
				if ( false === strpos( $cmd, 'whatsapp-gateway.mjs' ) || false === strpos( $cmd, $this->get_state_dir() ) ) {
					return false;
				}
			}
			return true;
		}
		if ( is_dir( '/proc' ) ) {
			return false;
		}
		return function_exists( 'posix_kill' ) ? (bool) @posix_kill( $pid, 0 ) : true;
	}

	private function is_gateway_startup_stalled( $status ) {
		$status = is_array( $status ) ? $status : array();
		$current = isset( $status['status'] ) ? (string) $status['status'] : '';
		if ( ! in_array( $current, array( 'starting', 'reconnecting', 'running' ), true ) ) {
			return false;
		}

		$updated_at = isset( $status['updatedAt'] ) ? strtotime( (string) $status['updatedAt'] ) : 0;
		return $updated_at > 0 && ( time() - $updated_at ) >= self::STARTUP_STALL_SECONDS;
	}

	private function terminate_gateway_process( $pid ) {
		$pid = intval( $pid );
		if ( $pid <= 0 ) {
			return false;
		}
		if ( ! $this->process_is_alive( $pid ) ) {
			return true;
		}
		if ( function_exists( 'posix_kill' ) ) {
			@posix_kill( $pid, 15 );
			usleep( 250000 );
			if ( ! $this->process_is_alive( $pid ) ) {
				return true;
			}
			@posix_kill( $pid, 9 );
			usleep( 150000 );
			return ! $this->process_is_alive( $pid );
		}

		$cmd = 'kill -TERM ' . intval( $pid ) . ' 2>/dev/null; sleep 1; kill -KILL ' . intval( $pid ) . ' 2>/dev/null';
		@exec( $cmd );
		return true;
	}

	private function read_json_file( $path ) {
		if ( ! is_readable( $path ) ) {
			return array();
		}
		$data = json_decode( (string) @file_get_contents( $path ), true );
		return is_array( $data ) ? $data : array();
	}

	private function write_private_state_file( $path, $content, $flags = 0 ) {
		$old_umask = umask( 0077 );
		try {
			$written = @file_put_contents( $path, $content, $flags );
		} finally {
			umask( $old_umask );
		}
		if ( false !== $written ) {
			@chmod( $path, 0600 );
		}
		return $written;
	}

	private function open_private_state_file( $path, $mode ) {
		$old_umask = umask( 0077 );
		try {
			$handle = @fopen( $path, $mode );
		} finally {
			umask( $old_umask );
		}
		if ( is_resource( $handle ) ) {
			@chmod( $path, 0600 );
		}
		return $handle;
	}

	private function protect_state_dir( $dir ) {
		$dir = rtrim( (string) $dir, "/\\" );
		if ( '' === $dir || ! is_dir( $dir ) || is_link( $dir ) ) {
			return;
		}
		$index = $dir . '/index.php';
		if ( ! file_exists( $index ) ) {
			$this->write_private_state_file( $index, "<?php\n// Silence is golden.\n" );
		}
		$htaccess = $dir . '/.htaccess';
		if ( ! file_exists( $htaccess ) ) {
			$this->write_private_state_file( $htaccess, "Deny from all\n" );
		}
		@chmod( $dir, 0700 );
	}

	private function migrate_legacy_public_state() {
		if ( $this->state_migration_checked ) {
			return;
		}
		$this->state_migration_checked = true;

		$legacy = rtrim( str_replace( '\\', '/', $this->get_legacy_public_state_dir() ), '/' );
		$target = rtrim( str_replace( '\\', '/', $this->get_state_dir() ), '/' );
		if ( '' === $legacy || '' === $target || $legacy === $target || ! is_dir( $legacy ) ) {
			return;
		}

		$this->protect_state_dir( $legacy );
		$old_umask = umask( 0077 );
		try {
			wp_mkdir_p( dirname( $target ) );
		} finally {
			umask( $old_umask );
		}
		@chmod( dirname( $target ), 0700 );
		$lock_path = dirname( $target ) . '/.whatsapp-state-migration.lock';
		$lock      = $this->open_private_state_file( $lock_path, 'c' );
		if ( is_resource( $lock ) && ! @flock( $lock, LOCK_EX ) ) {
			@fclose( $lock );
			return;
		}

		try {
			if ( ! is_dir( $legacy ) ) {
				return;
			}
			if ( ! $this->secure_private_state_tree( $legacy ) ) {
				return;
			}
			if ( ! $this->stop_legacy_gateway_processes( $legacy ) ) {
				return;
			}

			if ( ! file_exists( $target ) ) {
				if ( @rename( $legacy, $target ) ) {
					$this->secure_private_state_tree( $target );
					return;
				}

				$staging = $target . '.migrating-' . substr( hash( 'sha256', microtime( true ) . $legacy ), 0, 10 );
				if ( $this->copy_state_directory_verified( $legacy, $staging ) && @rename( $staging, $target ) ) {
					$this->remove_state_directory_tree( $legacy );
					$this->secure_private_state_tree( $target );
					return;
				}
				$this->remove_state_directory_tree( $staging );
				return;
			}

			$archive = dirname( $target ) . '/legacy-whatsapp-' . gmdate( 'Ymd-His' ) . '-' . substr( hash( 'sha256', $legacy . microtime( true ) ), 0, 8 );
			$moved   = @rename( $legacy, $archive );
			if ( ! $moved && $this->copy_state_directory_verified( $legacy, $archive ) ) {
				$this->remove_state_directory_tree( $legacy );
				$moved = ! is_dir( $legacy );
			}
			if ( $moved ) {
				$this->merge_missing_state_files( $archive, $target );
				$this->secure_private_state_tree( $archive );
				$this->secure_private_state_tree( $target );
			}
		} finally {
			if ( is_resource( $lock ) ) {
				@flock( $lock, LOCK_UN );
				@fclose( $lock );
			}
			@chmod( $lock_path, 0600 );
		}
	}

	private function stop_legacy_gateway_processes( $legacy ) {
		@file_put_contents( rtrim( $legacy, "/\\" ) . '/supervisor.stop', (string) time(), LOCK_EX );
		$stopped = true;
		foreach ( array( 'supervisor.pid', 'gateway.pid' ) as $name ) {
			$path = rtrim( $legacy, "/\\" ) . '/' . $name;
			$pid  = is_readable( $path ) ? intval( trim( (string) @file_get_contents( $path ) ) ) : 0;
			if ( $pid > 0 ) {
				$stopped = $this->terminate_legacy_gateway_process( $pid, $legacy ) && $stopped;
			}
		}
		return $stopped;
	}

	private function terminate_legacy_gateway_process( $pid, $legacy ) {
		$pid      = intval( $pid );
		$cmd_path = '/proc/' . $pid . '/cmdline';
		if ( $pid <= 0 ) {
			return true;
		}
		if ( ! is_readable( $cmd_path ) ) {
			return is_dir( '/proc' );
		}
		$cmd = str_replace( "\0", ' ', (string) @file_get_contents( $cmd_path ) );
		if (
			false === strpos( $cmd, (string) $legacy )
			|| ( false === strpos( $cmd, 'whatsapp-gateway.mjs' ) && false === strpos( $cmd, 'whatsapp-gateway-supervisor' ) )
		) {
			return true;
		}
		if ( function_exists( 'posix_kill' ) ) {
			@posix_kill( $pid, 15 );
		} else {
			@exec( 'kill -TERM ' . $pid . ' 2>/dev/null' );
		}
		for ( $attempt = 0; $attempt < 10; $attempt++ ) {
			usleep( 100000 );
			if ( ! is_readable( $cmd_path ) ) {
				return true;
			}
			$current = str_replace( "\0", ' ', (string) @file_get_contents( $cmd_path ) );
			if ( false === strpos( $current, (string) $legacy ) ) {
				return true;
			}
		}
		if ( function_exists( 'posix_kill' ) ) {
			@posix_kill( $pid, 9 );
		} else {
			@exec( 'kill -KILL ' . $pid . ' 2>/dev/null' );
		}
		usleep( 100000 );
		return ! is_readable( $cmd_path );
	}

	private function copy_state_directory_verified( $source, $destination ) {
		$source      = rtrim( (string) $source, "/\\" );
		$destination = rtrim( (string) $destination, "/\\" );
		$old_umask   = umask( 0077 );
		try {
			if ( ! is_dir( $source ) || ! wp_mkdir_p( $destination ) ) {
				return false;
			}
			$iterator = new RecursiveIteratorIterator(
				new RecursiveDirectoryIterator( $source, FilesystemIterator::SKIP_DOTS ),
				RecursiveIteratorIterator::SELF_FIRST
			);
			foreach ( $iterator as $item ) {
				if ( $item->isLink() ) {
					return false;
				}
				$relative = substr( str_replace( '\\', '/', $item->getPathname() ), strlen( str_replace( '\\', '/', $source ) ) + 1 );
				$target   = $destination . '/' . $relative;
				if ( $item->isDir() ) {
					if ( ! wp_mkdir_p( $target ) ) {
						return false;
					}
					continue;
				}
				if ( ! @copy( $item->getPathname(), $target ) || hash_file( 'sha256', $item->getPathname() ) !== hash_file( 'sha256', $target ) ) {
					return false;
				}
			}
		} catch ( \Throwable $e ) {
			return false;
		} finally {
			umask( $old_umask );
		}
		return true;
	}

	private function merge_missing_state_files( $source, $destination ) {
		$old_umask = umask( 0077 );
		try {
			if ( ! is_dir( $source ) || ! wp_mkdir_p( $destination ) ) {
				return false;
			}
			$iterator = new RecursiveIteratorIterator(
				new RecursiveDirectoryIterator( $source, FilesystemIterator::SKIP_DOTS ),
				RecursiveIteratorIterator::SELF_FIRST
			);
			foreach ( $iterator as $item ) {
				if ( $item->isLink() ) {
					return false;
				}
				$relative = substr( str_replace( '\\', '/', $item->getPathname() ), strlen( str_replace( '\\', '/', rtrim( $source, "/\\" ) ) ) + 1 );
				$target   = rtrim( $destination, "/\\" ) . '/' . $relative;
				if ( $item->isDir() ) {
					wp_mkdir_p( $target );
					continue;
				}
				if ( file_exists( $target ) ) {
					continue;
				}
				wp_mkdir_p( dirname( $target ) );
				$tmp = $target . '.migrating';
				if ( ! @copy( $item->getPathname(), $tmp ) || hash_file( 'sha256', $item->getPathname() ) !== hash_file( 'sha256', $tmp ) || ! @rename( $tmp, $target ) ) {
					@unlink( $tmp );
					return false;
				}
			}
		} catch ( \Throwable $e ) {
			return false;
		} finally {
			umask( $old_umask );
		}
		return true;
	}

	private function secure_private_state_tree( $root ) {
		if ( ! is_dir( $root ) || is_link( $root ) ) {
			return false;
		}
		$protected = @chmod( $root, 0700 );
		try {
			$iterator = new RecursiveIteratorIterator(
				new RecursiveDirectoryIterator( $root, FilesystemIterator::SKIP_DOTS ),
				RecursiveIteratorIterator::SELF_FIRST
			);
			foreach ( $iterator as $item ) {
				if ( $item->isLink() ) {
					return false;
				}
				$protected = @chmod( $item->getPathname(), $item->isDir() ? 0700 : 0600 ) && $protected;
			}
		} catch ( \Throwable $e ) {
			return false;
		}
		return $protected;
	}

	private function remove_state_directory_tree( $root ) {
		if ( '' === (string) $root || ! is_dir( $root ) ) {
			return;
		}
		$iterator = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator( $root, FilesystemIterator::SKIP_DOTS ),
			RecursiveIteratorIterator::CHILD_FIRST
		);
		foreach ( $iterator as $item ) {
			$item->isDir() && ! $item->isLink() ? @rmdir( $item->getPathname() ) : @unlink( $item->getPathname() );
		}
		@rmdir( $root );
	}

	private function get_missing_gateway_dependencies() {
		$base = defined( 'NIWA_AI_DIR' ) ? NIWA_AI_DIR : dirname( __DIR__, 2 ) . '/';
		$deps = array(
			'@whiskeysockets/baileys' => $base . 'node_modules/@whiskeysockets/baileys',
			'pino'                   => $base . 'node_modules/pino',
			'qrcode'                 => $base . 'node_modules/qrcode',
		);
		$missing = array();
		foreach ( $deps as $name => $path ) {
			if ( ! is_dir( $path ) ) {
				$missing[] = $name;
			}
		}

		return $missing;
	}

	private function ensure_gateway_dependencies() {
		$missing = $this->get_missing_gateway_dependencies();
		if ( empty( $missing ) ) {
			return array( 'success' => true );
		}

		$install = $this->install_gateway_dependencies();
		$missing = $this->get_missing_gateway_dependencies();
		if ( empty( $missing ) ) {
			return array(
				'success'   => true,
				'installed' => true,
				'install'   => $install,
			);
		}

		return array(
			'success'       => false,
			'missing'       => $missing,
			'install_error' => isset( $install['error'] ) ? (string) $install['error'] : 'dependencies_still_missing',
		);
	}

	private function install_gateway_dependencies() {
		$base = defined( 'NIWA_AI_DIR' ) ? NIWA_AI_DIR : dirname( __DIR__, 2 ) . '/';
		if ( ! is_readable( $base . 'package-lock.json' ) || ! is_readable( $base . 'package.json' ) ) {
			return array( 'success' => false, 'error' => 'package_metadata_missing' );
		}
		if ( ! class_exists( 'Niwa_AI_Admin_Autonomy_Process_Runtime' ) || ! Niwa_AI_Admin_Autonomy_Process_Runtime::is_proc_open_available() ) {
			return array( 'success' => false, 'error' => 'proc_open_unavailable' );
		}

		$npm = $this->resolve_npm_binary();
		if ( '' === $npm ) {
			return array( 'success' => false, 'error' => 'npm_binary_unavailable' );
		}

		$log = $this->get_state_dir() . '/dependency-install.log';
		$this->write_private_state_file( $log, '[' . gmdate( 'c' ) . '] installing WhatsApp gateway dependencies' . "\n", FILE_APPEND );
		$inner_cmd = 'cd ' . escapeshellarg( $base ) . ' && ' . escapeshellarg( $npm ) . ' ci --omit=dev --omit=optional --ignore-scripts --no-audit --no-fund';
		$cmd       = $inner_cmd;
		if ( is_executable( '/usr/bin/timeout' ) ) {
			$cmd = '/usr/bin/timeout 180s /bin/sh -c ' . escapeshellarg( $inner_cmd );
		}
		$process = @proc_open(
			'/bin/sh -c ' . escapeshellarg( $cmd ),
			array(
				0 => array( 'file', '/dev/null', 'r' ),
				1 => array( 'pipe', 'w' ),
				2 => array( 'pipe', 'w' ),
			),
			$pipes,
			$base
		);
		if ( ! is_resource( $process ) ) {
			return array( 'success' => false, 'error' => 'npm_proc_open_failed' );
		}

		$output = '';
		foreach ( array( 1, 2 ) as $index ) {
			if ( isset( $pipes[ $index ] ) && is_resource( $pipes[ $index ] ) ) {
				$output .= (string) stream_get_contents( $pipes[ $index ] );
			}
		}
		foreach ( (array) $pipes as $pipe ) {
			if ( is_resource( $pipe ) ) {
				fclose( $pipe );
			}
		}
		$exit_code = @proc_close( $process );
		$this->write_private_state_file( $log, substr( $output, -4000 ) . "\n", FILE_APPEND );
		if ( 0 !== intval( $exit_code ) ) {
			return array(
				'success'   => false,
				'error'     => 'npm_install_failed',
				'exit_code' => intval( $exit_code ),
			);
		}

		return array( 'success' => true );
	}

	private function resolve_npm_binary() {
		$node = $this->resolve_node_binary();
		$candidates = array();
		if ( '' !== $node ) {
			$candidates[] = trailingslashit( dirname( $node ) ) . 'npm';
		}
		$candidates[] = '/opt/alt/alt-nodejs20/root/usr/bin/npm';
		$candidates[] = '/opt/alt/alt-nodejs18/root/usr/bin/npm';
		$candidates[] = '/usr/local/bin/npm';
		$candidates[] = '/usr/bin/npm';

		foreach ( $candidates as $candidate ) {
			if ( is_executable( $candidate ) ) {
				return $candidate;
			}
		}

		return '';
	}

	private function read_recent_gateway_error() {
		foreach ( array( $this->get_state_dir() . '/launcher.log', $this->get_state_dir() . '/gateway.log' ) as $path ) {
			if ( ! is_readable( $path ) ) {
				continue;
			}
			$lines = @file( $path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
			if ( ! is_array( $lines ) || empty( $lines ) ) {
				continue;
			}
			$tail = array_slice( $lines, -8 );
			for ( $i = count( $tail ) - 1; $i >= 0; $i-- ) {
				$line = trim( (string) $tail[ $i ] );
				if ( '' !== $line ) {
					$line = preg_replace( '/--secret=\\S+/', '--secret=***', $line );
					$line = preg_replace( '/NIWA_WA_SECRET=(\\S+)/', 'NIWA_WA_SECRET=***', $line );
					return substr( $line, 0, 240 );
				}
			}
		}

		return '';
	}

	private function resolve_reply_jid( $sender, $context = array() ) {
		return Niwa_AI_Admin_Autonomy_WhatsApp_Identity::resolve_reply_jid( $sender, $context, $this->get_state_dir() );
	}

	private function resolve_outbound_target( $chat_id, $context = array() ) {
		$context = is_array( $context ) ? $context : array();
		$group   = $this->is_explicit_group_chat_id( $chat_id ) ? $this->normalize_group_jid( $chat_id ) : '';
		if ( '' === $group ) {
			foreach ( array( 'remote_jid', 'gateway_chat_jid', 'chat_jid', 'whatsapp_jid' ) as $key ) {
				if ( empty( $context[ $key ] ) || ! $this->is_explicit_group_chat_id( $context[ $key ] ) ) {
					continue;
				}
				$group = $this->normalize_group_jid( $context[ $key ] );
				if ( '' !== $group ) {
					break;
				}
			}
		}
		if ( '' !== $group ) {
			return array(
				'to'  => $group,
				'jid' => $group,
			);
		}

		$to = $this->normalize_sender_id( $chat_id );
		if ( '' === $to ) {
			return array(
				'to'  => '',
				'jid' => '',
			);
		}

		return array(
			'to'  => $to,
			'jid' => $this->resolve_reply_jid( $to, $context ),
		);
	}

	private function is_explicit_group_chat_id( $chat_id ) {
		$chat_id = trim( (string) $chat_id );
		return 0 === strpos( $chat_id, 'whatsapp-group:' ) || (bool) preg_match( '/@g\.us$/', $chat_id );
	}

	private function write_status_file( $next ) {
		$this->ensure_state_dirs();
		$path = $this->get_state_dir() . '/status.json';
		$prev = $this->read_json_file( $path );
		$next = is_array( $next ) ? $next : array();
		$status = array_merge(
			is_array( $prev ) ? $prev : array(),
			$next,
			array(
				'updatedAt' => gmdate( 'c' ),
			)
		);
		$tmp = $path . '.' . getmypid() . '.tmp';
		if ( false === $this->write_private_state_file( $tmp, wp_json_encode( $status ), LOCK_EX ) ) {
			return;
		}
		if ( ! @rename( $tmp, $path ) ) {
			@unlink( $tmp );
		}
	}

	private function normalize_sender_id( $value ) {
		return Niwa_AI_Admin_Autonomy_WhatsApp_Identity::canonical_sender_id( $value, $this->get_state_dir() );
	}

	private function normalize_gateway_jid( $value ) {
		return Niwa_AI_Admin_Autonomy_WhatsApp_Identity::normalize_gateway_jid( $value );
	}

	private function normalize_presence_action( $action ) {
		$action = sanitize_key( (string) $action );
		if ( in_array( $action, array( 'typing', 'composing' ), true ) ) {
			return 'composing';
		}
		if ( in_array( $action, array( 'paused', 'available', 'unavailable', 'recording' ), true ) ) {
			return $action;
		}

		return '';
	}

	private function split_text( $text ) {
		$text = trim( (string) $text );
		if ( '' === $text ) {
			return array();
		}
		if ( strlen( $text ) <= self::MESSAGE_TEXT_LIMIT ) {
			return array( $text );
		}

		$chunks = array();
		while ( '' !== $text ) {
			$chunk = substr( $text, 0, self::MESSAGE_TEXT_LIMIT );
			$pos   = strrpos( $chunk, "\n" );
			if ( false === $pos || $pos < 1000 ) {
				$pos = strrpos( $chunk, ' ' );
			}
			if ( false === $pos || $pos < 1000 ) {
				$pos = self::MESSAGE_TEXT_LIMIT;
			}
			$chunks[] = trim( substr( $text, 0, $pos ) );
			$text     = trim( substr( $text, $pos ) );
		}

		return array_values( array_filter( $chunks, 'strlen' ) );
	}
}
