class_name NetworkItemUseService
extends Node

const ArtShopStockType = preload("res://economy/art_shop_stock.gd")

const MAX_LEDGER_ENTRIES: int = 64

signal local_item_use_pending(request_id: String)
signal local_item_use_finished(accepted: bool, message: String)
signal equipped_state_changed(peer_id: int, item_id: StringName, category: int)

var _session: NetworkSession
var _spawn_service: PlayerSpawnService
var _catalog: ItemCatalog
var _local_bag: PlayerBag
var _local_effects: PlayerItemEffects
var _save_manager: PlayerSaveManager
var _requests: Dictionary[int, Dictionary] = {}
var _pending_by_peer: Dictionary[int, String] = {}
var _result_owners: Dictionary[String, int] = {}
var _received_results: Dictionary[String, bool] = {}
var _pending_local: Dictionary = {}
var _equipped_states: Dictionary[int, Dictionary] = {}
var _local_equipped_revision: int = 0
var _reservations: PlayerAssetReservationService


func setup(
	session: NetworkSession,
	spawn_service: PlayerSpawnService,
	catalog: ItemCatalog,
	local_bag: PlayerBag,
	local_effects: PlayerItemEffects,
	save_manager: PlayerSaveManager,
	reservations: PlayerAssetReservationService,
) -> void:
	_session = session
	_spawn_service = spawn_service
	_catalog = catalog
	_local_bag = local_bag
	_local_effects = local_effects
	_save_manager = save_manager
	_reservations = reservations
	_session.peer_removed.connect(_on_peer_removed)
	_session.state_changed.connect(_on_session_state_changed)
	_session.peer_authenticated.connect(_on_peer_authenticated)


func request_use(item_id: StringName) -> String:
	if not _pending_local.is_empty():
		local_item_use_finished.emit(false, "An item use is already pending.")
		return ""
	var consumed_item_id: StringName = _consumed_item_id(item_id)
	if item_id == PlayerItemEffects.FISH_FINDER_ID and (
		_local_bag == null
		or not _local_bag.owns_item(item_id)
		or _local_bag.get_quantity(consumed_item_id) < 1
	):
		local_item_use_finished.emit(
			false, PlayerItemEffects.FISH_FINDER_DEAD_MESSAGE
		)
		return ""
	if (
		_reservations != null
		and _reservations.get_available_item_quantity(consumed_item_id) < 1
	):
		local_item_use_finished.emit(false, "Reserved in a letter.")
		return ""
	if (
		_session == null
		or not _session.is_gameplay_session_active()
		or (
			not _session.is_host()
			and not _session.supports_server_capability(
				NetworkItemProtocol.ITEM_USE_CAPABILITY
			)
		)
	):
		local_item_use_finished.emit(false, "Item use is unavailable.")
		return ""
	var request_id := _new_id("item")
	var data := {
		"request_id": request_id,
		"session_id": _session.get_session_id(),
		"item_id": str(item_id),
		"quantity": _local_bag.get_quantity(consumed_item_id),
	}
	_pending_local = data.duplicate(true)
	local_item_use_pending.emit(request_id)
	if _session.is_host():
		_handle_use_request(_session.get_local_peer_id(), data)
	else:
		submit_item_use.rpc_id(1, data)
	return request_id


@rpc("any_peer", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
func submit_item_use(data: Dictionary) -> void:
	var sender_id := multiplayer.get_remote_sender_id()
	if _session.is_host() and _session.is_authenticated_peer(sender_id):
		_handle_use_request(sender_id, data)


func _handle_use_request(peer_id: int, data: Dictionary) -> void:
	var request_id := str(data.get("request_id", ""))
	var ledger: Dictionary = _requests.get(peer_id, {})
	if ledger.has(request_id):
		_send_result(peer_id, ledger[request_id])
		return
	var error := NetworkItemProtocol.validate_use_request(data)
	if (
		error.is_empty()
		and str(data["session_id"]) != _session.get_session_id()
	):
		error = "Item use could not be completed."
	if error.is_empty() and _pending_by_peer.has(peer_id):
		error = "An item use is already pending."
	var item_id := StringName(str(data.get("item_id", "")))
	var item: ItemData = _catalog.get_item_by_id(item_id)
	var avatar: Player = _spawn_service.get_avatar(peer_id)
	var duration: float = 0.0
	var is_fish_finder: bool = (
		item_id == PlayerItemEffects.FISH_FINDER_ID
		and item != null
		and item.category == ItemData.Category.TOOL
		and item.usable
		and item.equippable
	)
	if error.is_empty() and (
		item == null
		or (
			item.category != ItemData.Category.CONSUMABLE
			and not is_fish_finder
		)
		or not item.usable
	):
		error = "That item cannot be used now."
	if error.is_empty() and int(data["quantity"]) < 1:
		error = (
			PlayerItemEffects.FISH_FINDER_DEAD_MESSAGE
			if is_fish_finder
			else "You do not have that item."
		)
	if error.is_empty() and (
		avatar == null
		or avatar.is_water_recovery_active()
	):
		error = "That item cannot be used now."
	if error.is_empty():
		duration = avatar.item_effects.get_effect_duration(item_id)
		if duration <= 0.0:
			error = "That item cannot be used now."
	var result := {
		"result_id": _new_id("item_result"),
		"request_id": request_id if not request_id.is_empty() else "invalid",
		"session_id": _session.get_session_id(),
		"target_peer_id": peer_id,
		"accepted": error.is_empty(),
		"item_id": str(item_id) if not item_id.is_empty() else "invalid",
		"quantity": 1 if error.is_empty() else 0,
		"duration": duration if error.is_empty() else 0.0,
		"message": "Used %s." % item.display_name if error.is_empty() else error,
	}
	ledger[request_id] = result.duplicate(true)
	_bound(ledger)
	_requests[peer_id] = ledger
	_result_owners[result["result_id"]] = peer_id
	_pending_by_peer[peer_id] = request_id
	_send_result(peer_id, result)


func _send_result(peer_id: int, result: Dictionary) -> void:
	if peer_id == _session.get_local_peer_id():
		_apply_result(result)
	else:
		receive_item_use_result.rpc_id(peer_id, result)


@rpc("authority", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
func receive_item_use_result(data: Dictionary) -> void:
	_apply_result(data)


func _apply_result(data: Dictionary) -> void:
	if (
		not NetworkItemProtocol.validate_use_result(data)
		or str(data["session_id"]) != _session.get_session_id()
		or int(data["target_peer_id"]) != _session.get_local_peer_id()
	):
		return
	var result_id: String = data["result_id"]
	if _received_results.has(result_id):
		_acknowledge(data, true, "")
		return
	if (
		_pending_local.is_empty()
		or str(data["request_id"]) != str(_pending_local.get("request_id", ""))
	):
		return
	if not bool(data["accepted"]):
		_received_results[result_id] = true
		_pending_local.clear()
		local_item_use_finished.emit(false, str(data["message"]))
		_acknowledge(data, false, str(data["message"]))
		return
	var item_id := StringName(str(data["item_id"]))
	var consumed_item_id: StringName = _consumed_item_id(item_id)
	if (
		_reservations != null
		and _reservations.get_available_item_quantity(consumed_item_id) < 1
	):
		_pending_local.clear()
		local_item_use_finished.emit(false, "Reserved in a letter.")
		_acknowledge(data, false, "Reserved in a letter.")
		return
	var bag_snapshot := _local_bag.get_all_items()
	if (
		_local_bag.get_quantity(consumed_item_id) < 1
		or not _local_bag.remove_item(consumed_item_id, 1)
		or not _save_manager.save_if_dirty()
	):
		_local_bag.replace_all_items(bag_snapshot)
		_save_manager.save_if_dirty()
		_pending_local.clear()
		local_item_use_finished.emit(false, "Item use could not be completed.")
		_acknowledge(data, false, "Item use could not be completed.")
		return
	_received_results[result_id] = true
	_bound(_received_results)
	_pending_local.clear()
	local_item_use_finished.emit(true, str(data["message"]))
	_acknowledge(data, true, "")


func _acknowledge(data: Dictionary, applied: bool, message: String) -> void:
	if _session.is_host():
		_handle_ack(
			_session.get_local_peer_id(),
			str(data["result_id"]),
			str(data["request_id"]),
			applied,
			message
		)
	else:
		acknowledge_item_use.rpc_id(
			1, str(data["result_id"]), str(data["request_id"]), applied,
			message.left(NetworkItemProtocol.MAX_MESSAGE_LENGTH)
		)


@rpc("any_peer", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
func acknowledge_item_use(
	result_id: String,
	request_id: String,
	applied: bool,
	message: String,
) -> void:
	var sender_id := multiplayer.get_remote_sender_id()
	if _session.is_host() and _session.is_authenticated_peer(sender_id):
		_handle_ack(sender_id, result_id, request_id, applied, message)


func _handle_ack(
	peer_id: int,
	result_id: String,
	request_id: String,
	applied: bool,
	_message: String,
) -> void:
	if (
		_result_owners.get(result_id, 0) != peer_id
		or _pending_by_peer.get(peer_id, "") != request_id
	):
		return
	_pending_by_peer.erase(peer_id)
	if not applied:
		return
	var result: Dictionary = _requests.get(peer_id, {}).get(request_id, {})
	var avatar := _spawn_service.get_avatar(peer_id)
	if avatar == null or result.is_empty():
		return
	var item_id := StringName(str(result["item_id"]))
	avatar.item_effects.activate_authoritative(item_id, float(result["duration"]))
	_broadcast_effect(peer_id, item_id, float(result["duration"]))


func get_effects_for_peer(peer_id: int) -> PlayerItemEffects:
	var avatar := _spawn_service.get_avatar(peer_id)
	return avatar.item_effects if avatar != null else null


func submit_local_equipped(item_id: StringName, owns_item: bool) -> void:
	if _session == null or not _session.is_gameplay_session_active():
		return
	_local_equipped_revision += 1
	var item: ItemData = _catalog.get_item_by_id(item_id)
	var data := {
		"session_id": _session.get_session_id(),
		"owner_peer_id": _session.get_local_peer_id(),
		"item_id": str(item_id) if item != null and owns_item else "",
		"category": int(item.category) if item != null and owns_item else -1,
		"revision": _local_equipped_revision,
		"owns_item": owns_item and item != null,
	}
	if _session.is_host():
		_handle_equipped_state(_session.get_local_peer_id(), data)
	elif _session.supports_server_capability(
		NetworkItemProtocol.EQUIPMENT_CAPABILITY
	):
		submit_equipped_state.rpc_id(1, data)


@rpc("any_peer", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
func submit_equipped_state(data: Dictionary) -> void:
	var sender_id := multiplayer.get_remote_sender_id()
	if _session.is_host() and _session.is_authenticated_peer(sender_id):
		_handle_equipped_state(sender_id, data)


func _handle_equipped_state(peer_id: int, data: Dictionary) -> void:
	if (
		not NetworkItemProtocol.validate_equipped_state(data)
		or str(data["session_id"]) != _session.get_session_id()
		or int(data["owner_peer_id"]) != peer_id
	):
		return
	var previous: Dictionary = _equipped_states.get(peer_id, {})
	if int(data["revision"]) <= int(previous.get("revision", -1)):
		return
	var item_id := StringName(str(data["item_id"]))
	var item: ItemData = _catalog.get_item_by_id(item_id)
	if not item_id.is_empty() and (
		item == null
		or not bool(data["owns_item"])
		or int(item.category) != int(data["category"])
	):
		return
	_equipped_states[peer_id] = data.duplicate(true)
	_apply_equipped(data)
	receive_equipped_state.rpc(data)


@rpc("authority", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
func receive_equipped_state(data: Dictionary) -> void:
	if NetworkItemProtocol.validate_equipped_state(data):
		_equipped_states[int(data["owner_peer_id"])] = data.duplicate(true)
		_apply_equipped(data)


func _apply_equipped(data: Dictionary) -> void:
	var peer_id: int = data["owner_peer_id"]
	var avatar := _spawn_service.get_avatar(peer_id)
	if avatar != null:
		var item_id := StringName(str(data["item_id"]))
		var item: ItemData = _catalog.get_item_by_id(item_id)
		avatar.set_active_item_is_rod(
			int(data["category"]) == ItemData.Category.ROD,
			true,
		)
		avatar.set_active_art_kit(
			item.icon if item != null else null,
			item_id == ArtShopStockType.ART_KIT_ITEM_ID and bool(data["owns_item"]),
		)
	equipped_state_changed.emit(
		peer_id, StringName(str(data["item_id"])), int(data["category"])
	)


func _broadcast_effect(peer_id: int, item_id: StringName, duration: float) -> void:
	var data := {
		"session_id": _session.get_session_id(),
		"owner_peer_id": peer_id,
		"item_id": str(item_id),
		"remaining": duration,
	}
	_apply_effect_snapshot(data)
	receive_effect_snapshot.rpc(data)


@rpc("authority", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
func receive_effect_snapshot(data: Dictionary) -> void:
	_apply_effect_snapshot(data)


func _apply_effect_snapshot(data: Dictionary) -> void:
	if (
		typeof(data.get("owner_peer_id")) != TYPE_INT
		or typeof(data.get("item_id")) != TYPE_STRING
		or typeof(data.get("remaining")) not in [TYPE_FLOAT, TYPE_INT]
		or str(data.get("session_id", "")) != _session.get_session_id()
	):
		return
	var avatar := _spawn_service.get_avatar(int(data["owner_peer_id"]))
	if avatar != null:
		avatar.item_effects.activate_authoritative(
			StringName(data["item_id"]), float(data["remaining"])
		)


func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
	if not _session.is_host():
		return
	for state: Dictionary in _equipped_states.values():
		receive_equipped_state.rpc_id(peer_id, state)


func _on_peer_removed(peer_id: int) -> void:
	_requests.erase(peer_id)
	_pending_by_peer.erase(peer_id)
	_equipped_states.erase(peer_id)


func _on_session_state_changed(state: NetworkSession.State) -> void:
	if state in [
		NetworkSession.State.INACTIVE,
		NetworkSession.State.DISCONNECTING,
		NetworkSession.State.CONNECTION_FAILED,
		NetworkSession.State.SERVER_LOST,
	]:
		var pending := not _pending_local.is_empty()
		_requests.clear()
		_pending_by_peer.clear()
		_result_owners.clear()
		_received_results.clear()
		_equipped_states.clear()
		_pending_local.clear()
		if pending:
			local_item_use_finished.emit(false, "Connection lost.")


func _bound(values: Dictionary) -> void:
	while values.size() > MAX_LEDGER_ENTRIES:
		values.erase(values.keys().front())


func _new_id(prefix: String) -> String:
	return "%s:%s" % [
		prefix, Crypto.new().generate_random_bytes(16).hex_encode(),
	]


func _consumed_item_id(item_id: StringName) -> StringName:
	return (
		PlayerItemEffects.BATTERIES_ID
		if item_id == PlayerItemEffects.FISH_FINDER_ID
		else item_id
	)
