extends Node

# Matchbox Client - Handles WebSocket signaling for WebRTC via Matchbox server
# Protocol: https://github.com/johanhelsing/matchbox

signal peer_joined(peer_id: int)
signal peer_left(peer_id: int)
signal peer_connected(peer_id: int)
signal connection_failed()
signal connection_succeeded(was_reconnecting: bool)
signal webrtc_ready()  # Emitted when WebRTC mesh is set up after Welcome message

const MATCHBOX_SERVER = "wss://matchbox.thefirstboss.com"
# STUN/TURN server configuration
# COTURN server on ports 3478/3479 with relay ports 49150:49500
const STUN_SERVER = "stun:ruinborn.thefirstboss.com:3478"
# TURN server configuration (Coturn with long-term credentials)
# Using standard TURN port 3478 (UDP/TCP) and 3479 (TLS)
const TURN_SERVER = "turn:ruinborn.thefirstboss.com:3478"  # Standard TURN port
# Note: Relay ports 49160:49500 are configured on the COTURN server side
# Realm: ruinborn.thefirstboss.com
# Credentials: myuser:mypassword (long-term credentials with lt-cred-mech)
const TURN_USERNAME = "myuser"  # TURN username (long-term credentials)
const TURN_PASSWORD = "mypassword"  # TURN password (long-term credentials)

var websocket: WebSocketPeer = null
var webrtc_peer: WebRTCMultiplayerPeer = null
var room_name: String = ""
var is_network_connected: bool = false
var is_hosting: bool = false  # Set by NetworkManager to determine if we're host (ID 1) or client (ID 2+)
var my_uuid: String = ""  # Our UUID assigned by Matchbox
var my_peer_id: int = 0  # Our integer peer ID (1 = host, 2+ = clients) - assigned by host for clients
var peer_uuid_to_id: Dictionary = {}  # UUID -> integer peer ID
var peer_id_counter: int = 1  # Counter for assigning integer peer IDs (host only)
var peer_connections: Dictionary = {}  # peer_id (int) -> WebRTCPeerConnection
var pending_offers: Dictionary = {}  # peer_id -> offer data
var waiting_for_peer_id: bool = false  # Client flag: waiting for host to assign peer ID
var queued_signaling_messages: Array = []  # Queue for signaling messages received before peer ID assignment
var connection_failed_emitted: bool = false  # Prevent multiple connection_failed emissions
var ice_candidates_sent: Dictionary = {}  # peer_id -> count of ICE candidates sent
var ice_candidates_received: Dictionary = {}  # peer_id -> count of ICE candidates received
var retry_count: int = 0  # Number of retry attempts
var max_retries: int = 3  # Maximum retry attempts (for initial connection)
var retry_timer: float = 0.0  # Timer for retry backoff
var retry_delay: float = 5.0  # Initial retry delay in seconds
var is_retrying: bool = false  # Whether we're currently retrying
var host_reconnect_timer: float = 0.0  # Timer for host reconnection
var host_reconnect_delay: float = 5.0  # Host reconnection delay: 5 seconds (reduced from 60.0)
var host_reconnect_count: int = 0  # Number of host reconnection attempts
var max_host_retries: int = 10  # Maximum host reconnection attempts
var is_host_reconnecting: bool = false  # Whether host is in reconnection mode

# Logging - use LogManager for categorized logging
func log_print(message: String):
	LogManager.log(message, LogManager.CATEGORY_NETWORK)

func log_error(message: String):
	LogManager.log_error(message, LogManager.CATEGORY_NETWORK)

func _ready():
	pass

func connect_to_room(room: String, hosting: bool = false) -> bool:
	if websocket:
		disconnect_from_room()
	
	room_name = room
	is_hosting = hosting
	retry_count = 0
	connection_failed_emitted = false
	is_retrying = false
	retry_timer = 0.0
	log_print("MatchboxClient: Connecting to room: " + room + " (hosting: " + str(hosting) + ")")
	
	# Create WebSocket connection to Matchbox
	websocket = WebSocketPeer.new()
	var url = MATCHBOX_SERVER + "/" + room_name
	var error = websocket.connect_to_url(url)
	
	if error != OK:
		log_error("MatchboxClient: Failed to connect to Matchbox: " + str(error))
		return false
	
	log_print("MatchboxClient: Connecting to: " + url)
	return true

func _check_and_emit_peer_connected(peer_id: int):
	"""Check if peer connection is established and emit peer_connected if so"""
	var pc = peer_connections.get(peer_id)
	if not pc:
		return
	
	# Check connection state - if it's CONNECTED (2) or CONNECTING (1), emit peer_connected
	var connection_state = pc.get_connection_state()
	var signaling_state = pc.get_signaling_state()
	
	# If signaling state is STABLE (0) or connection is CONNECTING/CONNECTED, emit peer_connected
	# This allows the joiner to proceed even if the connection isn't fully established yet
	# The connection will complete during gameplay
	if signaling_state == 0 or connection_state >= 1:  # STABLE or CONNECTING/CONNECTED
		log_print("MatchboxClient: Connection ready for peer " + str(peer_id) + " - emitting peer_connected")
		peer_connected.emit(peer_id)
		
		# On web, the multiplayer.peer_connected signal might not fire automatically
		# So we need to manually check and wait for the peer to be available
		if not is_hosting:
			# For joiners, check if multiplayer system sees the peer
			# If the connection is CONNECTED (2), the peer should be available soon
			if connection_state == 2:  # CONNECTED
				call_deferred("_check_multiplayer_peer_available", peer_id)
	else:
		# Connection not ready yet, try again after a short delay (silently retry)
		get_tree().create_timer(0.5).timeout.connect(func(): _check_and_emit_peer_connected(peer_id))

func _check_multiplayer_peer_available(peer_id: int):
	"""Check if the multiplayer system recognizes the peer and manually trigger connection_succeeded if needed"""
	if not webrtc_peer:
		return
	
	# Wait a frame for the multiplayer system to update
	await get_tree().process_frame
	
	# Check if the peer is in the multiplayer peer list
	var peers = multiplayer.get_peers()
	if peer_id in peers:
		# The multiplayer.peer_connected signal should fire automatically
		# But if it doesn't, we might need to manually trigger it
		# For now, just proceed - NetworkManager will handle connection_succeeded
		pass
	else:
		# Retry after a short delay (max 5 seconds)
		var peer_check_retry_count = get_meta("peer_check_retry_count", 0)
		if peer_check_retry_count < 10:  # Max 10 retries (5 seconds)
			set_meta("peer_check_retry_count", peer_check_retry_count + 1)
			get_tree().create_timer(0.5).timeout.connect(func(): _check_multiplayer_peer_available(peer_id))
		else:
			# After max retries, assume connection is ready even if multiplayer doesn't recognize it
			# This can happen on web where the multiplayer system might be delayed
			log_print("MatchboxClient: Max retries reached - assuming peer " + str(peer_id) + " is available")
			# Manually trigger the peer_connected signal for NetworkManager
			# This will cause _on_peer_connected to fire, which will emit connection_succeeded
			# We'll do this by directly calling the NetworkManager's handler
			var network_manager = get_node_or_null("/root/NetworkManager")
			if network_manager and network_manager.has_method("_on_peer_connected"):
				network_manager._on_peer_connected(peer_id)

func disconnect_from_room():
	if websocket:
		websocket.close()
		websocket = null
	
	# Close all peer connections
	for peer_id in peer_connections:
		var pc = peer_connections[peer_id]
		if pc:
			pc.close()
	peer_connections.clear()
	pending_offers.clear()
	queued_signaling_messages.clear()
	is_network_connected = false
	room_name = ""
	connection_failed_emitted = false
	retry_count = 0
	is_retrying = false
	retry_timer = 0.0
	is_host_reconnecting = false
	host_reconnect_count = 0
	host_reconnect_timer = 0.0
	waiting_for_peer_id = false
	my_peer_id = 0
	peer_uuid_to_id.clear()

func _process(_delta):
	# Handle retry timer
	if is_retrying:
		retry_timer -= _delta
		if retry_timer <= 0.0:
			# Time to retry
			is_retrying = false
			_attempt_reconnect()
		return
	
	if not websocket:
		# Handle host reconnection even if websocket is null
		if is_hosting and is_host_reconnecting:
			host_reconnect_timer -= _delta
			if host_reconnect_timer <= 0.0:
				# Time to retry host connection
				host_reconnect_timer = host_reconnect_delay
				_attempt_host_reconnect()
		return
	
	# Handle host reconnection timer (when websocket exists but is closed)
	if is_hosting and is_host_reconnecting:
		host_reconnect_timer -= _delta
		if host_reconnect_timer <= 0.0:
			# Time to retry host connection
			host_reconnect_timer = host_reconnect_delay
			_attempt_host_reconnect()
	
	websocket.poll()
	
	# Poll all peer connections to process WebRTC events
	for peer_id in peer_connections:
		var pc = peer_connections[peer_id]
		if pc:
			pc.poll()
	
	var state = websocket.get_ready_state()
	
	if state == WebSocketPeer.STATE_OPEN:
		if not is_network_connected:
			is_network_connected = true
			connection_failed_emitted = false
			retry_count = 0
			is_retrying = false
			# Store if host was reconnecting before clearing the flag
			var was_host_reconnecting = is_host_reconnecting
			is_host_reconnecting = false  # Clear host reconnection mode on successful connection
			host_reconnect_timer = 0.0
			host_reconnect_count = 0  # Reset retry counter on successful connection
			connection_succeeded.emit(was_host_reconnecting)  # Pass reconnection status to signal
			log_print("MatchboxClient: Connected to Matchbox server")
		
		# Process incoming messages
		while websocket.get_available_packet_count() > 0:
			var packet = websocket.get_packet()
			_handle_message(packet.get_string_from_utf8())
	
	elif state == WebSocketPeer.STATE_CLOSED:
		if is_network_connected:
			# Was connected, now disconnected
			is_network_connected = false
			if not connection_failed_emitted:
				connection_failed.emit()
				connection_failed_emitted = true
				log_print("MatchboxClient: Disconnected from Matchbox server")
				
				# If hosting, start reconnection mode (max 10 retries)
				if is_hosting:
					is_host_reconnecting = true
					host_reconnect_count = 0  # Reset counter when starting reconnection
					host_reconnect_timer = host_reconnect_delay
					log_print("MatchboxClient: Host disconnected, will retry every " + str(host_reconnect_delay) + " seconds (max " + str(max_host_retries) + " retries)")
				else:
					# Joiner: attempt quick reconnect with exponential backoff
					_handle_connection_failure()
		else:
			# Connection failed before being established
			if not connection_failed_emitted and not is_retrying and not is_host_reconnecting:
				# Only emit once and start retry logic
				connection_failed_emitted = true
				log_print("MatchboxClient: Connection failed (state: CLOSED)")
				
				# For hosts, use host reconnection logic (1 minute, max 10 retries)
				if is_hosting:
					is_host_reconnecting = true
					host_reconnect_count = 0  # Reset counter when starting reconnection
					host_reconnect_timer = host_reconnect_delay
					connection_failed.emit()  # Emit signal so NetworkManager can show chat message
					log_print("MatchboxClient: Host connection failed, will retry every " + str(host_reconnect_delay) + " seconds (max " + str(max_host_retries) + " retries)")
				else:
					# For joiners, use joiner retry logic
					_handle_connection_failure()
	
	elif state == WebSocketPeer.STATE_CONNECTING:
		# Still connecting, wait for next poll
		pass
	
	elif state == WebSocketPeer.STATE_CLOSING:
		# Connection is closing
		log_print("MatchboxClient: Connection closing...")

func _handle_message(message: String):
	# Only log message content if there's an error - reduce console spam
	var json = JSON.new()
	var error = json.parse(message)
	if error != OK:
		log_error("MatchboxClient: Failed to parse message: " + message)
		return
	
	var data = json.data
	
	# Matchbox protocol uses direct keys: IdAssigned, NewPeer, PeerLeft, Signal
	if data.has("IdAssigned"):
		_handle_id_assigned(data.get("IdAssigned", ""))
	elif data.has("NewPeer"):
		_handle_new_peer(data.get("NewPeer", ""))
	elif data.has("PeerLeft"):
		_handle_peer_left_uuid(data.get("PeerLeft", ""))
	elif data.has("Signal"):
		_handle_signal_message_dict(data.get("Signal", {}))
	else:
		log_print("MatchboxClient: Unknown message format: " + str(data))

func _handle_id_assigned(uuid: String):
	log_print("MatchboxClient: IdAssigned message received: " + uuid)
	my_uuid = uuid
	
	# Assign peer ID based on whether we're hosting or joining
	# Host gets ID 1 immediately, clients wait for host to assign their ID
	if is_hosting:
		my_peer_id = 1
		peer_uuid_to_id[my_uuid] = my_peer_id
		peer_id_counter = 2  # Next peer will get ID 2
		
		# Host can create mesh immediately
		if setup_webrtc_peer():
			webrtc_ready.emit()
			log_print("MatchboxClient: WebRTC mesh ready, signal emitted")
	else:
		# Client: wait for host to assign our peer ID via Signal message
		# Don't create mesh yet - we'll create it when we receive our peer ID from the host
		waiting_for_peer_id = true
		log_print("MatchboxClient: Client waiting for host to assign peer ID...")
	
	log_print("MatchboxClient: Assigned UUID: " + my_uuid + " -> Peer ID: " + str(my_peer_id) + " (hosting: " + str(is_hosting) + ")")
	
	# Note: Matchbox protocol behavior:
	# - When you join a room, you get IdAssigned
	# - Existing peers in the room should receive NewPeer for you
	# - But you don't get NewPeer for existing peers
	# - We'll discover existing peers when they send Signal messages
	# - The host should always create offers, so when host discovers a joiner via Signal, it creates an offer

func _handle_new_peer(uuid: String):
	if uuid.is_empty():
		return
	
	log_print("MatchboxClient: NewPeer message received: " + uuid)
	
	# Only host assigns peer IDs to new peers
	if is_hosting:
		# Host assigns sequential integer peer ID
		var peer_id = peer_id_counter
		peer_uuid_to_id[uuid] = peer_id
		peer_id_counter += 1
		
		log_print("MatchboxClient: Host assigned peer ID " + str(peer_id) + " to UUID: " + uuid)
		
		# Send peer ID assignment to the client via Matchbox Signal
		_send_peer_id_assignment(uuid, peer_id)
		
		# Broadcast the new peer's ID to all existing clients so they can discover each other
		_broadcast_peer_id_to_all_clients(uuid, peer_id)
		
		# Also send all existing peer IDs to the new client so they can discover existing peers
		_send_all_existing_peers_to_new_client(uuid)
		
		peer_joined.emit(peer_id)
		_create_peer_connection(peer_id)
	else:
		# Client: we don't assign peer IDs, the host does
		# Just store the UUID for now, peer ID will come via Signal message
		log_print("MatchboxClient: Client received NewPeer for UUID: " + uuid + " (waiting for host to assign peer ID)")
		
		# If we're in reconnection mode and don't have a host connection yet, this might be the host reconnecting
		# Check if we previously had a host (peer ID 1) but lost it
		var had_host = false
		for stored_uuid in peer_uuid_to_id:
			if peer_uuid_to_id[stored_uuid] == 1:
				had_host = true
				break
		
		# If we don't have a host connection and we're waiting for reconnection, this could be the host
		if not had_host and not peer_connections.has(1):
			log_print("MatchboxClient: Client received NewPeer while waiting for host reconnection - this might be the host reconnecting")
			# Don't do anything yet - wait for host to assign peer ID via Signal
			# But we can clear any reconnection flags in NetworkManager if needed

func _handle_peer_left_uuid(uuid: String):
	if uuid.is_empty():
		return
	
	var peer_id = peer_uuid_to_id.get(uuid, 0)
	if peer_id == 0:
		return
	
	log_print("MatchboxClient: Peer left UUID: " + uuid + " -> Peer ID: " + str(peer_id))
	peer_uuid_to_id.erase(uuid)
	peer_left.emit(peer_id)
	_close_peer_connection(peer_id)

func _handle_signal_message_dict(signal_data: Dictionary):
	# Matchbox Signal format when receiving: The server routes messages, so we need to identify the sender
	# Log all fields to see what Matchbox actually sends
	log_print("MatchboxClient: Received Signal message with keys: " + str(signal_data.keys()))
	
	# Extract signal data first to check if it's a peer-id-assigned message
	# This needs to be checked BEFORE peer discovery/connection logic
	var sig = signal_data.get("data", {})
	if sig.is_empty():
		sig = signal_data.get("signal", {})
	
	# Handle peer ID assignment FIRST (clients only, before mesh is created)
	# This must happen before we try to discover/create peer connections
	if not is_hosting and sig.has("type") and sig.get("type") == "peer-id-assigned":
		# Client receiving peer ID assignment from host
		# Extract host UUID from sender field and store it
		var host_uuid = signal_data.get("sender", "")
		if not host_uuid.is_empty():
			peer_uuid_to_id[host_uuid] = 1  # Store host UUID -> peer ID 1 mapping
			log_print("MatchboxClient: Stored host UUID: " + host_uuid + " -> Peer ID 1")
		# Process this message directly - don't need peer_id yet
		log_print("MatchboxClient: Client received peer ID assignment from host (processing immediately)")
		_handle_signal_message(1, sig)  # Use peer_id = 1 (host) as the sender
		return
	
	# Handle peer discovery broadcast from host (clients only)
	if not is_hosting and sig.has("type") and sig.get("type") == "peer-discovered":
		var discovered_peer_uuid = sig.get("peer_uuid", "")
		var discovered_peer_id = sig.get("peer_id", 0)
		if not discovered_peer_uuid.is_empty() and discovered_peer_id > 0:
			log_print("MatchboxClient: Client received peer discovery broadcast: UUID " + discovered_peer_uuid + " -> Peer ID " + str(discovered_peer_id))
			# Store the peer ID mapping
			if not peer_uuid_to_id.has(discovered_peer_uuid):
				peer_uuid_to_id[discovered_peer_uuid] = discovered_peer_id
				# Create peer connection if we have our own peer ID
				if my_peer_id > 0 and not waiting_for_peer_id:
					_create_peer_connection(discovered_peer_id)
		return # Handled this signal, no further processing
	
	# Try to get sender UUID - Matchbox might include it in different fields
	var from_uuid = signal_data.get("From", "")
	if from_uuid.is_empty():
		from_uuid = signal_data.get("sender", "")
	if from_uuid.is_empty():
		from_uuid = signal_data.get("peer", "")
	
	# If still empty, try to determine from context
	if from_uuid.is_empty():
		# If we're a client and don't know the sender, assume it's the host (peer ID 1)
		if not is_hosting:
			# We're a client, so the sender must be the host
			var host_uuid = ""
			for uuid in peer_uuid_to_id:
				if peer_uuid_to_id[uuid] == 1:
					host_uuid = uuid
					break
			if not host_uuid.is_empty():
				from_uuid = host_uuid
				log_print("MatchboxClient: Assuming Signal is from host UUID: " + host_uuid)
			else:
				# Host not discovered yet - this might be the first Signal from host
				# We'll discover the host when we process this Signal
				log_print("MatchboxClient: Received Signal but host UUID not known yet - will discover from Signal")
				# We'll handle this in the discovery logic below
		else:
			# We're host, so sender must be a client - we'll discover it below
			log_print("MatchboxClient: Received Signal as host - will discover sender from Signal")
	
	# If we still don't have from_uuid, we need to discover it
	if from_uuid.is_empty():
		# If we're a client, the sender must be the host
		if not is_hosting:
			# Client receiving Signal from unknown sender - assume it's from the host
			# Check if this is a peer-id-assigned message (which comes from host)
			var sig_data = signal_data.get("data", {})
			if sig_data.is_empty():
				sig_data = signal_data.get("signal", {})
			
			if sig_data.has("type") and sig_data.get("type") == "peer-id-assigned":
				# This is a peer ID assignment from the host - process it directly
				log_print("MatchboxClient: Client received peer ID assignment from host")
				_handle_signal_message(1, sig_data)  # Use peer_id = 1 (host) as sender
				return
			
			# For other signals, assume from host
			var client_peer_id = 1
			log_print("MatchboxClient: Client received Signal - assuming from host (peer ID 1)")
			if not sig_data.is_empty():
				_handle_signal_message(client_peer_id, sig_data)
			return
		else:
			# We're host, so sender must be a client - we'll discover it
			log_print("MatchboxClient: Host received Signal - will discover sender from signal content")
			# Process signal and discover sender
			var host_sig = signal_data.get("data", {})
			if host_sig.is_empty():
				host_sig = signal_data.get("signal", {})
			if not host_sig.is_empty():
				# For now, assign to next available peer ID
				var host_peer_id = peer_id_counter
				peer_id_counter += 1
				log_print("MatchboxClient: Host assigning Signal to new peer ID: " + str(host_peer_id))
				_handle_signal_message(host_peer_id, host_sig)
			return
	
	var peer_id = peer_uuid_to_id.get(from_uuid, 0)
	if peer_id == 0:
		# Unknown peer - this is an existing peer we haven't seen yet
		# Matchbox doesn't send NewPeer for existing peers, so we discover them via Signal messages
		# Assign ID based on whether we're host or client
		if is_hosting:
			# We're host, so this must be a client (peer ID 2+)
			peer_id = peer_id_counter
			peer_id_counter += 1
			peer_uuid_to_id[from_uuid] = peer_id
			log_print("MatchboxClient: Host discovered existing peer via Signal message - UUID: " + from_uuid + " -> Peer ID: " + str(peer_id))
		else:
			# We're client - check if we already know the host
			# If we know the host and this isn't the host, it must be another client
			# But we don't know its peer ID yet - wait for host to broadcast it
			var host_uuid = ""
			for uuid in peer_uuid_to_id:
				if peer_uuid_to_id[uuid] == 1:
					host_uuid = uuid
					break
			
			if host_uuid == from_uuid:
				# This is the host
				peer_id = 1
				peer_uuid_to_id[from_uuid] = peer_id
				log_print("MatchboxClient: Client discovered host via Signal message - UUID: " + from_uuid + " -> Peer ID: 1")
			elif host_uuid.is_empty() and not from_uuid.is_empty():
				# We don't know the host yet, but this might be the host sending us an offer
				# If this is an offer or answer, it's likely from the host (host creates offers to clients)
				var sig_type = sig.get("type", "")
				if sig_type == "offer" or sig_type == "answer":
					# Assume this is from the host
					peer_id = 1
					peer_uuid_to_id[from_uuid] = peer_id
					log_print("MatchboxClient: Client assuming Signal is from host (offer/answer) - UUID: " + from_uuid + " -> Peer ID: 1")
				else:
					# This is another client, but we don't know its peer ID yet
					# Store the UUID and wait for host to broadcast the peer ID
					log_print("MatchboxClient: Client received Signal from unknown peer UUID: " + from_uuid + " (waiting for host to broadcast peer ID)")
					# Don't assign a peer ID yet - wait for host broadcast
					return
			else:
				# This is another client, but we don't know its peer ID yet
				# Store the UUID and wait for host to broadcast the peer ID
				log_print("MatchboxClient: Client received Signal from unknown peer UUID: " + from_uuid + " (waiting for host to broadcast peer ID)")
				# Don't assign a peer ID yet - wait for host broadcast
				return
		
		# Create peer connection for this newly discovered peer
		# Don't emit peer_joined signal here since this is discovery, not a new join
		# But clients must have their peer ID before creating connections
		if not is_hosting and (my_peer_id == 0 or waiting_for_peer_id):
			log_print("MatchboxClient: Client discovered peer but waiting for peer ID - will create connection after peer ID assignment")
			return
		_create_peer_connection(peer_id)
		
		# If we're the host and discovered a client via Signal (not NewPeer),
		# we should create an offer (the host always initiates)
		# _create_peer_connection already handles this if my_peer_id == 1,
		# but let's make sure we handle it here too if needed
		if is_hosting and my_peer_id == 1 and peer_id != 1:
			# This is discovery of an existing client - ensure we create an offer
			# But only if we haven't already created one (check signal type first)
			pass  # Will be handled by _create_peer_connection or in _handle_signal_message
	
	# Matchbox uses "data" field, not "signal" (already extracted above)
	if sig.is_empty():
		# Empty signal - this might be a discovery-only message
		# If we're host and just discovered a client, create an offer
		if is_hosting and my_peer_id == 1 and peer_id != 1:
			# We discovered a client but got an empty signal - create offer proactively
			call_deferred("_create_offer_for_peer", peer_id)
		return
	
	# If client is waiting for peer ID, queue this signal message
	if not is_hosting and waiting_for_peer_id:
		log_print("MatchboxClient: Queueing signaling message (waiting for peer ID)")
		queued_signaling_messages.append(signal_data)  # Queue the entire signal_data dict
		return
	
	_handle_signal_message(peer_id, sig)

func _handle_signal_message(peer_id: int, signal_data: Dictionary):
	var type = signal_data.get("type", "")
	
	# Handle peer ID assignment from host (clients only)
	if type == "peer-id-assigned" and not is_hosting:
		var assigned_peer_id = signal_data.get("peer_id", 0)
		if assigned_peer_id > 0:
			log_print("MatchboxClient: Received peer ID assignment from host: " + str(assigned_peer_id))
			my_peer_id = assigned_peer_id
			peer_uuid_to_id[my_uuid] = my_peer_id
			waiting_for_peer_id = false
			
			# Now create the WebRTC mesh with the assigned peer ID
			if setup_webrtc_peer():
				webrtc_ready.emit()
				log_print("MatchboxClient: WebRTC mesh ready with assigned peer ID: " + str(my_peer_id))
			
			# Create peer connection for host if we discovered it earlier
			# Check if we know the host's UUID but don't have a connection yet
			for uuid in peer_uuid_to_id:
				if peer_uuid_to_id[uuid] == 1 and not peer_connections.has(1):
					log_print("MatchboxClient: Creating peer connection for host (peer ID 1) after peer ID assignment")
					_create_peer_connection(1)
					break
			
			# Process all queued signaling messages now that we have our peer ID
			log_print("MatchboxClient: Processing " + str(queued_signaling_messages.size()) + " queued signaling messages")
			for queued_msg in queued_signaling_messages:
				_handle_signal_message_dict(queued_msg)
			queued_signaling_messages.clear()
			
			# If we're in reconnection mode, clear it since we've successfully reconnected
			var network_manager = get_parent()
			if network_manager and "reconnection_attempting" in network_manager:
				if network_manager.reconnection_attempting:
					log_print("MatchboxClient: Host reconnected and assigned peer ID - clearing reconnection state")
					network_manager.reconnection_attempting = false
					network_manager.reconnection_timer = 0.0
			
			return
	
	# Handle WebRTC signaling messages (offer, answer, ice-candidate)
	# Clients must have their peer ID assigned before processing signaling messages
	if not is_hosting and waiting_for_peer_id:
		log_print("MatchboxClient: Client received signaling message but waiting for peer ID assignment - queuing for later")
		# Find the original signal message dict to queue it
		# We need to reconstruct it from the current context
		# This is a bit of a hack - we queue at the dict level in _handle_signal_message_dict
		return
	
	var peer_conn = peer_connections.get(peer_id)
	if not peer_conn:
		# Create connection if it doesn't exist
		_create_peer_connection(peer_id)
		peer_conn = peer_connections.get(peer_id)
		if not peer_conn:
			return
	
	match type:
		"offer":
			_handle_offer(peer_id, signal_data)
		"answer":
			_handle_answer(peer_id, signal_data)
		"ice-candidate":
			_handle_ice_candidate(peer_id, signal_data)

func _create_peer_connection(peer_id: int) -> WebRTCPeerConnection:
	# Note: This function cannot be async because it's called from signal handlers
	# We'll use call_deferred for async operations
	
	# Clients must have their peer ID assigned before creating peer connections
	if not is_hosting and (my_peer_id == 0 or waiting_for_peer_id):
		log_error("MatchboxClient: Cannot create peer connection - client waiting for peer ID assignment (my_peer_id=" + str(my_peer_id) + ")")
		return null
	
	if peer_connections.has(peer_id):
		return peer_connections[peer_id]
	
	log_print("MatchboxClient: Creating peer connection for peer " + str(peer_id))
	
	var pc = WebRTCPeerConnection.new()
	
	# Configure STUN and TURN servers according to Godot documentation format
	# Each server should be a separate entry in the iceServers array
	var ice_servers = []
	
	# Add STUN server as separate entry
	if not STUN_SERVER.is_empty():
		var stun_config = {
			"urls": [STUN_SERVER]  # urls must be an array
		}
		ice_servers.append(stun_config)
	
	# Add TURN server as separate entry with credentials
	if not TURN_SERVER.is_empty():
		# For TURN, create URLs array with appropriate transports
		# Note: libjuice (native builds) only supports UDP transport
		# WebAssembly (browser) supports both UDP and TCP/TLS
		var turn_urls = []
		var is_web = OS.get_name() == "Web"
		
		if TURN_SERVER.begins_with("turns:"):
			# TURNS over TLS: TCP transport (browser only, libjuice doesn't support it)
			if is_web:
				turn_urls.append(TURN_SERVER + "?transport=tcp")
			else:
				# Native builds: skip TLS (libjuice limitation), use UDP fallback if available
				log_print("MatchboxClient: Skipping TLS transport for native build (libjuice limitation)")
				# Convert turns: to turn: and use UDP transport for native builds
				var udp_turn_server = TURN_SERVER.replace("turns:", "turn:")
				turn_urls.append(udp_turn_server + "?transport=udp")
		else:
			# Standard TURN: UDP works everywhere, TCP only on browser
			turn_urls.append(TURN_SERVER + "?transport=udp")
			if is_web:
				# Add TCP transport for browsers (can help with restrictive firewalls)
				turn_urls.append(TURN_SERVER + "?transport=tcp")
		
		# Create TURN server configuration
		var turn_config = {
			"urls": turn_urls  # urls must be an array
		}
		
		# Add credentials if configured (for long-term credentials)
		if not TURN_USERNAME.is_empty() and not TURN_PASSWORD.is_empty():
			turn_config["username"] = TURN_USERNAME
			turn_config["credential"] = TURN_PASSWORD
		
		ice_servers.append(turn_config)
	
	var config = {
		"iceServers": ice_servers
	}
	
	# Log ICE server configuration
	log_print("MatchboxClient: Configuring ICE servers for peer " + str(peer_id) + ":")
	for i in range(ice_servers.size()):
		var server = ice_servers[i]
		var urls = server.get("urls", [])
		var username = server.get("username", "")
		var credential = server.get("credential", "")
		var has_credential = not credential.is_empty()
		log_print("  ICE Server " + str(i) + ": " + str(urls) + (", username: " + username if not username.is_empty() else "") + (", has credential: " + str(has_credential)))
	
	var error = pc.initialize(config)
	if error != OK:
		log_error("MatchboxClient: Failed to initialize peer connection: " + str(error))
		return null
	
	# Log the actual config that was used (for debugging)
	log_print("MatchboxClient: Peer connection initialized for peer " + str(peer_id) + " with config: " + str(config))
	
	# Connect signals
	# Note: session_description_created signal has signature (type: String, sdp: String)
	# We need to wrap it in a lambda to pass peer_id
	var signal_connected = pc.session_description_created.connect(func(type: String, sdp: String): _on_session_description_created(peer_id, type, sdp))
	if signal_connected != OK:
		push_error("MatchboxClient: Failed to connect session_description_created signal: " + str(signal_connected))
	# Note: ice_candidate_created signal has signature (media: String, index: int, name: String)
	# We need to wrap it in a lambda to pass peer_id
	var ice_connected = pc.ice_candidate_created.connect(func(media: String, index: int, candidate_name: String): _on_ice_candidate_created(peer_id, media, index, candidate_name))
	if ice_connected != OK:
		log_error("MatchboxClient: Failed to connect ice_candidate_created signal: " + str(ice_connected))
	log_print("MatchboxClient: Signals connected for peer " + str(peer_id) + " (session: " + str(signal_connected) + ", ice: " + str(ice_connected) + ")")
	
	peer_connections[peer_id] = pc
	
	# IMPORTANT: Add peer connection to WebRTC mesh BEFORE starting offer/answer exchange
	# The peer connection must be in STATE_NEW when added to the mesh
	# Note: Do NOT create data channels manually - WebRTCMultiplayerPeer.add_peer() 
	# automatically creates the required channels (reliable, unreliable, ordered)
	add_peer_to_mesh(peer_id)
	
	# In a full mesh, the peer with the lower ID creates the offer to the peer with the higher ID
	# This ensures we don't have both peers trying to create offers simultaneously
	if my_peer_id < peer_id:
		# We have a lower peer ID, so we create the offer
		# Use call_deferred to wait a frame for DataChannel to be ready
		call_deferred("_create_offer_for_peer", peer_id)
	# Otherwise (my_peer_id > peer_id), we wait for the other peer to create the offer
	
	return pc

func _create_offer_for_peer(peer_id: int):
	var pc = peer_connections.get(peer_id)
	if not pc:
		log_error("MatchboxClient: No peer connection found for peer " + str(peer_id))
		return
	
	# Ensure we poll the connection to process any pending operations
	pc.poll()
	
	log_print("MatchboxClient: Creating offer for peer " + str(peer_id) + " (my_peer_id: " + str(my_peer_id) + ")")
	log_print("MatchboxClient: Peer connection state before create_offer: " + str(pc.get_connection_state()))
	
	var offer_error = pc.create_offer()
	if offer_error != OK:
		log_error("MatchboxClient: Failed to create offer: " + str(offer_error))
	else:
		log_print("MatchboxClient: Offer creation initiated for peer " + str(peer_id) + " (error code: " + str(offer_error) + ")")
		# Poll immediately to process the offer creation
		pc.poll()
		log_print("MatchboxClient: Peer connection state after create_offer: " + str(pc.get_connection_state()))
		
		# Check ICE gathering state (if available)
		# Note: Godot's WebRTC might not expose ICE gathering state directly
		log_print("MatchboxClient: Waiting for ICE candidates to be generated...")
		
		# Signal should be connected from _create_peer_connection
		log_print("MatchboxClient: Signal is connected, waiting for callback...")
		# Force another poll to ensure the signal fires
		call_deferred("_poll_and_check_offer", peer_id)

func _close_peer_connection(peer_id: int):
	var pc = peer_connections.get(peer_id)
	if pc:
		pc.close()
		peer_connections.erase(peer_id)
	pending_offers.erase(peer_id)

func _handle_offer(peer_id: int, signal_data: Dictionary):
	var pc = peer_connections.get(peer_id)
	if not pc:
		# This shouldn't happen - _handle_signal_message should create it
		pc = _create_peer_connection(peer_id)
		if not pc:
			return
	
	var sdp = signal_data.get("sdp", "")
	if sdp.is_empty():
		return
	
	log_print("MatchboxClient: Received offer from peer " + str(peer_id))
	var set_error = pc.set_remote_description("offer", sdp)
	if set_error != OK:
		log_error("MatchboxClient: Failed to set remote offer: " + str(set_error))
		return
	
	# Don't create a new data channel - the offer already includes one from the host
	# The data channel is negotiated as part of the offer/answer exchange
	
	# Note: In Godot, set_remote_description with type "offer" automatically creates the answer
	# No need to call create_answer() - it doesn't exist in WebRTCPeerConnection
	log_print("MatchboxClient: Answer will be created automatically by set_remote_description for peer " + str(peer_id))

func _handle_answer(peer_id: int, signal_data: Dictionary):
	var pc = peer_connections.get(peer_id)
	if not pc:
		return
	
	var sdp = signal_data.get("sdp", "")
	if sdp.is_empty():
		return
	
	# Check signaling state before setting remote description
	# On web, if the signaling state is already "stable", setting the answer will fail
	# We should only set the answer if we're in HAVE_LOCAL_OFFER state
	var signaling_state = pc.get_signaling_state()
	log_print("MatchboxClient: Answer received for peer " + str(peer_id) + " (signaling state: " + str(signaling_state) + ")")
	
	# WebRTCPeerConnection.SignalingState constants:
	# 0 = STATE_STABLE (offer/answer exchange complete)
	# 1 = STATE_HAVE_LOCAL_OFFER (we created offer, waiting for answer)
	# 2 = STATE_HAVE_REMOTE_OFFER (we received offer, need to create answer)
	# 3 = STATE_HAVE_LOCAL_PRANSWER (provisional answer)
	# 4 = STATE_HAVE_REMOTE_PRANSWER (provisional answer received)
	# 5 = STATE_CLOSED
	
	# Only set remote answer if we're in HAVE_LOCAL_OFFER state (1)
	# If we're already in STABLE (0), the answer was already set or the connection is established
	if signaling_state == 0:  # STATE_STABLE
		log_print("MatchboxClient: Signaling state is already STABLE for peer " + str(peer_id) + " - answer already set, connection established")
		# Connection is already established, emit peer_connected
		peer_connected.emit(peer_id)
		return
	elif signaling_state != 1:  # Not HAVE_LOCAL_OFFER
		log_print("MatchboxClient: Signaling state is " + str(signaling_state) + " (not HAVE_LOCAL_OFFER) for peer " + str(peer_id) + " - skipping answer")
		# Still emit peer_connected in case connection is working
		peer_connected.emit(peer_id)
		return
	
	# We're in HAVE_LOCAL_OFFER state, safe to set the remote answer
	var error = pc.set_remote_description("answer", sdp)
	if error != OK:
		log_error("MatchboxClient: Failed to set remote answer: " + str(error) + " (signaling state: " + str(signaling_state) + ")")
		# Still emit peer_connected - connection might still work
		peer_connected.emit(peer_id)
		return
	
	log_print("MatchboxClient: Answer set successfully for peer " + str(peer_id))
	# Note: Peer connection should already be added to mesh in _create_peer_connection()
	# Just emit the connected signal for NetworkManager to handle player spawning
	peer_connected.emit(peer_id)

func _handle_ice_candidate(peer_id: int, signal_data: Dictionary):
	var pc = peer_connections.get(peer_id)
	if not pc:
		return
	
	var candidate = signal_data.get("candidate", "")
	var sdp_mid = signal_data.get("sdpMid", "")
	var sdp_mline_index = signal_data.get("sdpMLineIndex", 0)
	
	if candidate.is_empty():
		return
	
	# Log ICE candidate type to help debug STUN/TURN connectivity
	var candidate_type = "unknown"
	if candidate.contains("typ host"):
		candidate_type = "host (local)"
	elif candidate.contains("typ srflx"):
		candidate_type = "srflx (STUN)"
	elif candidate.contains("typ relay"):
		candidate_type = "relay (TURN)"
	elif candidate.contains("typ prflx"):
		candidate_type = "prflx (peer-reflexive)"
	
	# Log non-host candidates to see if STUN/TURN is working
	if candidate_type != "host (local)":
		log_print("MatchboxClient: Received ICE candidate for peer " + str(peer_id) + " - Type: " + candidate_type)
	
	# Track ICE candidates received
	if not ice_candidates_received.has(peer_id):
		ice_candidates_received[peer_id] = 0
	ice_candidates_received[peer_id] += 1
	
	var error = pc.add_ice_candidate(sdp_mid, sdp_mline_index, candidate)
	if error != OK:
		log_error("MatchboxClient: Failed to add ICE candidate: " + str(error))
		return

func _on_session_description_created(peer_id: int, type: String, sdp: String):
	# According to Godot documentation, we MUST call set_local_description() 
	# after session_description_created, otherwise ice_candidate_created won't be emitted!
	var pc = peer_connections.get(peer_id)
	if not pc:
		log_error("MatchboxClient: No peer connection found for peer " + str(peer_id) + " when setting local description")
		return
	
	# Set the local description - this is REQUIRED for ICE candidates to be generated
	var set_error = pc.set_local_description(type, sdp)
	if set_error != OK:
		log_error("MatchboxClient: Failed to set local description for peer " + str(peer_id) + ": " + str(set_error))
		return
	
	log_print("MatchboxClient: Set local " + type + " for peer " + str(peer_id) + " - ICE candidates should now be generated")
	
	# Only log SDP summary, not full details - reduces console spam
	# Check for ICE candidates in SDP
	var candidate_count = sdp.split("a=candidate:").size() - 1
	log_print("MatchboxClient: Created " + type + " for peer " + str(peer_id) + " with " + str(candidate_count) + " ICE candidate(s) in SDP")
	
	# Matchbox protocol uses "From" with UUID, not "id" with peer ID
	# Find the UUID for this peer_id
	var target_uuid = ""
	for uuid in peer_uuid_to_id:
		if peer_uuid_to_id[uuid] == peer_id:
			target_uuid = uuid
			break
	
	if target_uuid.is_empty():
		log_error("MatchboxClient: No UUID found for peer ID " + str(peer_id))
		return
	
	var message = {
		"Signal": {
			"receiver": target_uuid,
			"data": {
				"type": type,
				"sdp": sdp
			}
		}
	}
	
	# If we're a joiner (not hosting) and we just created an answer,
	# emit peer_connected after sending it (deferred to ensure answer is sent first)
	if not is_hosting and type == "answer":
		# Use call_deferred to ensure the answer is sent first, then check connection state
		call_deferred("_check_and_emit_peer_connected", peer_id)
	_send_message(message)
	
	# If we're a joiner and just sent an answer, check connection and emit peer_connected
	# (This is in addition to the deferred call above, as a backup)
	if not is_hosting and type == "answer":
		# Also check immediately after sending - the connection might be ready
		call_deferred("_check_and_emit_peer_connected", peer_id)
	
	# If we're a joiner and just sent an answer, check connection and emit peer_connected
	# (This is in addition to the deferred call above, as a backup)
	if not is_hosting and type == "answer":
		# Also check immediately - the connection might be ready
		call_deferred("_check_and_emit_peer_connected", peer_id)

# Connection status methods for HUD display
func is_matchbox_connected() -> bool:
	"""Check if Matchbox WebSocket is connected"""
	return is_network_connected and websocket != null and websocket.get_ready_state() == WebSocketPeer.STATE_OPEN

func get_ice_connection_status() -> Dictionary:
	"""Get ICE connection status for all peers
	Returns: Dictionary with peer_id -> {"connected": bool, "connection_state": int, "signaling_state": int, "candidates_sent": int, "candidates_received": int}
	"""
	var status = {}
	for peer_id in peer_connections.keys():
		var pc = peer_connections[peer_id]
		if pc:
			var connection_state = pc.get_connection_state() if pc.has_method("get_connection_state") else -1
			var signaling_state = pc.get_signaling_state() if pc.has_method("get_signaling_state") else -1
			# Connection state: 0=NEW, 1=CONNECTING, 2=CONNECTED, 3=DISCONNECTED, 4=FAILED
			status[peer_id] = {
				"connected": connection_state == 2,  # CONNECTED
				"connection_state": connection_state,
				"signaling_state": signaling_state,
				"candidates_sent": ice_candidates_sent.get(peer_id, 0),
				"candidates_received": ice_candidates_received.get(peer_id, 0)
			}
	return status

func are_data_channels_connected() -> bool:
	"""Check if data channels are connected for at least one peer
	Returns true if any peer has data channels open (connected flag in WebRTCMultiplayerPeer)
	Also checks peer_connections directly and actual channel ready states (following Godot demo pattern)
	"""
	if not webrtc_peer:
		return false
	
	# First, check if we have any peers with data channels open via multiplayer.get_peers()
	if multiplayer and multiplayer.has_multiplayer_peer():
		var peers = multiplayer.get_peers()
		if not peers.is_empty():
			# Check if at least one peer has connected data channels
			for peer_id in peers:
				if webrtc_peer.has_peer(peer_id):
					var peer_info = webrtc_peer.get_peer(peer_id)
					if peer_info:
						# Check connected flag first (fast path)
						if peer_info.get("connected", false):
							# Also verify channels are actually open (following demo pattern)
							var channels = peer_info.get("channels", [])
							if channels is Array and channels.size() > 0:
								var all_channels_open = true
								for channel in channels:
									if channel != null:
										# Check if channel has ready_state method
										if channel.has_method("get_ready_state"):
											var ready_state = channel.get_ready_state()
											# WebRTCDataChannel.State: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
											if ready_state != 1:  # Not OPEN
												all_channels_open = false
												break
										elif "ready_state" in channel:
											var ready_state = channel.get("ready_state")
											if ready_state != 1:  # Not OPEN
												all_channels_open = false
												break
								if all_channels_open:
									return true
							else:
								# No channels array, but connected flag is true - assume connected
								return true
	
	# Fallback: Check peer_connections directly (useful for joiner where host might not be in get_peers() yet)
	# If we have peer connections with CONNECTED state, check data channels
	for peer_id in peer_connections.keys():
		var pc = peer_connections[peer_id]
		if pc and pc.has_method("get_connection_state"):
			var connection_state = pc.get_connection_state()
			var signaling_state = pc.get_signaling_state() if pc.has_method("get_signaling_state") else -1
			# Connection state: 0=NEW, 1=CONNECTING, 2=CONNECTED, 3=DISCONNECTED, 4=FAILED
			# Signaling state: 0=STABLE, 1=HAVE_LOCAL_OFFER, 2=HAVE_REMOTE_OFFER, etc.
			# If connection is CONNECTED (2) OR signaling is STABLE (0) with connection >= CONNECTING (1),
			# data channels should be working (following same logic as ICE status check)
			if connection_state == 2:  # CONNECTED
				# Verify via webrtc_peer if available
				if webrtc_peer.has_peer(peer_id):
					var peer_info = webrtc_peer.get_peer(peer_id)
					if peer_info:
						# Check connected flag
						if peer_info.get("connected", false):
							# Also verify channels are actually open
							var channels = peer_info.get("channels", [])
							if channels is Array and channels.size() > 0:
								var all_channels_open = true
								for channel in channels:
									if channel != null:
										if channel.has_method("get_ready_state"):
											var ready_state = channel.get_ready_state()
											if ready_state != 1:  # Not OPEN
												all_channels_open = false
												break
										elif "ready_state" in channel:
											var ready_state = channel.get("ready_state")
											if ready_state != 1:  # Not OPEN
												all_channels_open = false
												break
								if all_channels_open:
									return true
							else:
								# Connection state is CONNECTED and connected flag is true
								# Data channels should be working even if channels array isn't available
								return true
				# Connection state is CONNECTED - even if peer not in webrtc_peer yet or connected flag not set,
				# if the WebRTC connection state is CONNECTED, data channels should be working
				# This is especially important for joiner where timing might be different
				return true
			elif signaling_state == 0 and connection_state >= 1:  # STABLE signaling and at least CONNECTING
				# Signaling is STABLE (offer/answer exchange complete) and connection is at least CONNECTING
				# This means data channels should be working (same logic as ICE status)
				# This helps catch cases where connection_state hasn't reached CONNECTED yet but channels are open
				return true
	
	return false

func _on_ice_candidate_created(peer_id: int, media: String, index: int, candidate_name: String):
	# Log all ICE candidates to help debug connection issues
	var candidate_type = "unknown"
	if candidate_name.contains("typ host"):
		candidate_type = "host (local)"
	elif candidate_name.contains("typ srflx"):
		candidate_type = "srflx (STUN)"
	elif candidate_name.contains("typ relay"):
		candidate_type = "relay (TURN)"
	elif candidate_name.contains("typ prflx"):
		candidate_type = "prflx (peer-reflexive)"
	
	# Log all candidates (including host) to debug connection issues
	log_print("MatchboxClient: ICE candidate created for peer " + str(peer_id) + " - Type: " + candidate_type + ", Media: " + media)
	# Log full candidate string for TURN debugging (first 200 chars to avoid spam)
	if candidate_type == "relay (TURN)":
		var candidate_preview = candidate_name.substr(0, 200) if candidate_name.length() > 200 else candidate_name
		log_print("MatchboxClient: TURN candidate details: " + candidate_preview)
	
	# Track ICE candidates sent
	if not ice_candidates_sent.has(peer_id):
		ice_candidates_sent[peer_id] = 0
	ice_candidates_sent[peer_id] += 1
	
	# Matchbox protocol uses "From" with UUID, not "id" with peer ID
	# Find the UUID for this peer_id
	var target_uuid = ""
	for uuid in peer_uuid_to_id:
		if peer_uuid_to_id[uuid] == peer_id:
			target_uuid = uuid
			break
	
	if target_uuid.is_empty():
		log_error("MatchboxClient: No UUID found for peer ID " + str(peer_id))
		return
	
	if my_uuid.is_empty():
		log_error("MatchboxClient: my_uuid is empty, cannot send ICE candidate")
		return
	
	var message = {
		"Signal": {
			"receiver": target_uuid,
			"data": {
				"type": "ice-candidate",
				"candidate": candidate_name,
				"sdpMid": media,
				"sdpMLineIndex": index
			}
		}
	}
	_send_message(message)

func _send_peer_id_assignment(target_uuid: String, assigned_peer_id: int):
	# Host sends peer ID assignment to a client via Matchbox Signal
	if not is_hosting:
		log_error("MatchboxClient: Only host can send peer ID assignments")
		return
	
	if my_uuid.is_empty():
		log_error("MatchboxClient: my_uuid is empty, cannot send peer ID assignment")
		return
	
	var message = {
		"Signal": {
			"receiver": target_uuid,
			"data": {
				"type": "peer-id-assigned",
				"peer_id": assigned_peer_id
			}
		}
	}
	log_print("MatchboxClient: Sending peer ID assignment: UUID " + target_uuid + " -> Peer ID " + str(assigned_peer_id))
	_send_message(message)

func _broadcast_peer_id_to_all_clients(new_peer_uuid: String, new_peer_id: int):
	# Host broadcasts a new peer's ID to all existing clients so they can discover each other
	if not is_hosting:
		return
	
	if my_uuid.is_empty():
		return
	
	# Send to all known clients (all UUIDs except the new peer and ourselves)
	for uuid in peer_uuid_to_id:
		if uuid != new_peer_uuid and uuid != my_uuid:
			var message = {
				"Signal": {
					"receiver": uuid,
					"data": {
						"type": "peer-discovered",
						"peer_uuid": new_peer_uuid,
						"peer_id": new_peer_id
					}
				}
			}
			log_print("MatchboxClient: Broadcasting peer discovery: UUID " + new_peer_uuid + " -> Peer ID " + str(new_peer_id) + " to " + uuid)
			_send_message(message)

func _send_all_existing_peers_to_new_client(new_client_uuid: String):
	# Host sends all existing peer IDs to a new client so they can discover existing peers
	if not is_hosting:
		return
	
	if my_uuid.is_empty():
		return
	
	# Send all existing peer IDs to the new client (except the new client itself and the host)
	for uuid in peer_uuid_to_id:
		if uuid != new_client_uuid and uuid != my_uuid:
			var existing_peer_id = peer_uuid_to_id[uuid]
			var message = {
				"Signal": {
					"receiver": new_client_uuid,
					"data": {
						"type": "peer-discovered",
						"peer_uuid": uuid,
						"peer_id": existing_peer_id
					}
				}
			}
			log_print("MatchboxClient: Sending existing peer to new client: UUID " + uuid + " -> Peer ID " + str(existing_peer_id) + " to " + new_client_uuid)
			_send_message(message)

func _send_message(message: Dictionary):
	if not websocket or websocket.get_ready_state() != WebSocketPeer.STATE_OPEN:
		return
	
	var json = JSON.stringify(message)
	var error = websocket.send_text(json)
	if error != OK:
		log_error("MatchboxClient: Failed to send message: " + str(error))

func get_my_peer_id() -> int:
	return my_peer_id

func _poll_and_check_offer(peer_id: int):
	"""Helper function to poll peer connection and check if offer was created"""
	var pc = peer_connections.get(peer_id)
	if not pc:
		return
	
	# Poll multiple times to ensure async operations complete and ICE candidates are gathered
	log_print("MatchboxClient: Performing additional polls for peer " + str(peer_id) + " offer creation (waiting for ICE candidates)")
	var candidate_count_before = ice_candidates_sent.get(peer_id, 0)
	for i in range(20):  # Increased from 10 to 20 to give more time for ICE gathering
		pc.poll()
		# Small delay between polls
		await get_tree().create_timer(0.05).timeout  # Increased from 0.01 to 0.05
		var candidate_count_after = ice_candidates_sent.get(peer_id, 0)
		if candidate_count_after > candidate_count_before:
			log_print("MatchboxClient: ICE candidates detected during polling (count: " + str(candidate_count_after) + ")")
			candidate_count_before = candidate_count_after
	
	var final_candidate_count = ice_candidates_sent.get(peer_id, 0)
	log_print("MatchboxClient: Finished additional polling for peer " + str(peer_id) + " (total ICE candidates sent: " + str(final_candidate_count) + ")")

func _handle_connection_failure():
	"""Handle connection failure and decide whether to retry"""
	if retry_count >= max_retries:
		# Max retries reached, give up
		log_print("MatchboxClient: Max retries (" + str(max_retries) + ") reached, giving up")
		connection_failed.emit()
		return
	
	# Calculate exponential backoff delay
	var delay = retry_delay * pow(2, retry_count)  # 5s, 10s, 20s
	retry_count += 1
	retry_timer = delay
	is_retrying = true
	log_print("MatchboxClient: Will retry connection in " + str(delay) + " seconds (attempt " + str(retry_count) + "/" + str(max_retries) + ")")

func _attempt_reconnect():
	"""Attempt to reconnect to the Matchbox server (for initial connection failures)"""
	if room_name.is_empty():
		return
	
	log_print("MatchboxClient: Retrying connection to room: " + room_name)
	connection_failed_emitted = false
	
	# Create new WebSocket connection
	websocket = WebSocketPeer.new()
	var url = MATCHBOX_SERVER + "/" + room_name
	var error = websocket.connect_to_url(url)
	
	if error != OK:
		log_error("MatchboxClient: Failed to reconnect to Matchbox: " + str(error))
		_handle_connection_failure()
		return

func _attempt_host_reconnect():
	"""Attempt to reconnect to the Matchbox server (for host reconnection, max 10 retries)"""
	if room_name.is_empty():
		return
	
	# Check if we've exceeded max retries
	if host_reconnect_count >= max_host_retries:
		log_print("MatchboxClient: Host max retries (" + str(max_host_retries) + ") reached, giving up")
		is_host_reconnecting = false
		return
	
	host_reconnect_count += 1
	log_print("MatchboxClient: Host retrying connection to room: " + room_name + " (attempt " + str(host_reconnect_count) + "/" + str(max_host_retries) + ")")
	
	# Show chat message for host reconnection attempt
	var network_manager = get_parent()
	if network_manager:
		var game_world = network_manager.get_tree().get_first_node_in_group("game_world")
		if game_world:
			var chat_ui = game_world.get_node_or_null("ChatUI")
			if chat_ui and chat_ui.has_method("add_local_message"):
				chat_ui.add_local_message("System", "Host reconnecting... (attempt " + str(host_reconnect_count) + "/" + str(max_host_retries) + ")")
	
	connection_failed_emitted = false
	
	# Create new WebSocket connection
	websocket = WebSocketPeer.new()
	var url = MATCHBOX_SERVER + "/" + room_name
	var error = websocket.connect_to_url(url)
	
	if error != OK:
		log_error("MatchboxClient: Host failed to reconnect: " + str(error))
		# Show chat message for failed attempt
		if network_manager:
			var game_world = network_manager.get_tree().get_first_node_in_group("game_world")
			if game_world:
				var chat_ui = game_world.get_node_or_null("ChatUI")
				if chat_ui and chat_ui.has_method("add_local_message"):
					if host_reconnect_count < max_host_retries:
						chat_ui.add_local_message("System", "Host reconnection failed, retrying in " + str(int(host_reconnect_delay)) + " seconds...")
					else:
						chat_ui.add_local_message("System", "Host reconnection failed, max retries reached")
		# Will retry again (if under max retries)
		if host_reconnect_count >= max_host_retries:
			is_host_reconnecting = false
		return

func setup_webrtc_peer() -> bool:
	if webrtc_peer:
		# Already set up
		return true
	
	webrtc_peer = WebRTCMultiplayerPeer.new()
	
	# Create mesh with our peer ID (assigned by Matchbox)
	# For host: peer_id = 1, for clients: peer_id = 2, 3, 4, etc.
	var error = webrtc_peer.create_mesh(my_peer_id)
	
	if error != OK:
		log_error("MatchboxClient: Failed to create WebRTC mesh: " + str(error))
		return false
	
	multiplayer.multiplayer_peer = webrtc_peer
	
	log_print("MatchboxClient: WebRTC mesh created with peer ID: " + str(my_peer_id))
	return true

func add_peer_to_mesh(peer_id: int):
	# Ensure we have a valid peer ID before creating mesh
	if my_peer_id == 0:
		log_error("MatchboxClient: Cannot add peer to mesh - my_peer_id is 0")
		return
	
	if not webrtc_peer:
		if not setup_webrtc_peer():
			return
	
	var pc = peer_connections.get(peer_id)
	if not pc:
		log_error("MatchboxClient: No peer connection for peer " + str(peer_id))
		return
	
	var error = webrtc_peer.add_peer(pc, peer_id)
	
	if error != OK:
		log_error("MatchboxClient: Failed to add peer to mesh: " + str(error))
		return
	
	log_print("MatchboxClient: Added peer " + str(peer_id) + " to WebRTC mesh")
