extends Node
## Autoload singleton holding the gladiator roguelike economy + meta-progression.
## Accessible globally as `GameState`.
##
## - banked_gold persists between sessions (saved to user://save.cfg).
## - owned_items are resource paths of weapons the player has bought.
## - run_gold is gold earned during the current arena run (NOT persisted).
##
## Death settlement: on run end you keep 10% of your total value (banked + run
## gold + owned gear value), floored at the DEFAULT_GOLD starting amount so you
## can never fall below a fresh start. This punishes hoarding and rewards spending
## before you enter the arena.

const SAVE_PATH: String = "user://save.cfg"
const DEFAULT_GOLD: int = 500
## Fraction of total value kept on death.
const SURVIVAL_KEEP_FRACTION: float = 0.10

# Persisted progression
var banked_gold: int = DEFAULT_GOLD
var owned_items: Array[String] = []
# Loadout chosen in the outfitting screen: slot -> WeaponData resource path ("" = empty)
var selected_loadout: Dictionary = {"main": "", "offhand": ""}

# Per-run state (reset each run, not saved)
var run_gold: int = 0
var run_active: bool = false
var current_wave: int = 0
var kills_this_run: int = 0
var last_run_stats: Dictionary = {}
var _run_start_msec: int = 0

signal gold_changed(banked_gold: int)
signal run_gold_changed(run_gold: int)
signal wave_changed(wave: int)
signal run_ended(stats: Dictionary)
signal loadout_changed(loadout: Dictionary)

func _ready() -> void:
	load_save()

func _notification(what: int) -> void:
	# Closing the window mid-run counts as walking out alive - bank the winnings
	if what == NOTIFICATION_WM_CLOSE_REQUEST and run_active:
		retire_run()

# --- Persistence ---------------------------------------------------------

func load_save() -> void:
	var cfg := ConfigFile.new()
	var err := cfg.load(SAVE_PATH)
	if err != OK:
		# No save yet (or unreadable): start fresh.
		banked_gold = DEFAULT_GOLD
		owned_items = []
		save_game()
		return
	banked_gold = int(cfg.get_value("progress", "banked_gold", DEFAULT_GOLD))
	# ConfigFile returns an untyped Array; copy into the typed array.
	owned_items.clear()
	for path in cfg.get_value("progress", "owned_items", []):
		owned_items.append(str(path))
	# Restore loadout, dropping anything no longer owned
	var saved_loadout = cfg.get_value("progress", "selected_loadout", {"main": "", "offhand": ""})
	for slot in ["main", "offhand"]:
		var path := str(saved_loadout.get(slot, ""))
		selected_loadout[slot] = path if owns_item(path) else ""

func save_game() -> void:
	var cfg := ConfigFile.new()
	cfg.set_value("progress", "banked_gold", banked_gold)
	cfg.set_value("progress", "owned_items", owned_items)
	cfg.set_value("progress", "selected_loadout", selected_loadout)
	cfg.save(SAVE_PATH)

# --- Shop economy --------------------------------------------------------

func can_afford(cost: int) -> bool:
	return banked_gold >= cost

## Buy an item by its WeaponData resource path. Returns true on success.
func buy_item(resource_path: String, cost: int) -> bool:
	if not can_afford(cost):
		return false
	banked_gold -= cost
	if not owned_items.has(resource_path):
		owned_items.append(resource_path)
	gold_changed.emit(banked_gold)
	save_game()
	return true

func owns_item(resource_path: String) -> bool:
	return owned_items.has(resource_path)

# --- Loadout (what you walk into the arena with) --------------------------

## Equip an owned item into the slot its hand_type dictates.
## Two-handed weapons take the main slot and clear the off-hand.
func equip_loadout_item(resource_path: String) -> bool:
	if not owns_item(resource_path):
		return false
	var data = load(resource_path)
	if not data is WeaponData:
		return false

	match data.hand_type:
		WeaponData.Hand.OFF_HAND:
			selected_loadout["offhand"] = resource_path
			# An off-hand displaces a two-handed main - you only have two hands
			if _is_two_hand(selected_loadout.get("main", "")):
				selected_loadout["main"] = ""
		WeaponData.Hand.TWO_HAND:
			selected_loadout["main"] = resource_path
			selected_loadout["offhand"] = ""
		_:  # MAIN_HAND
			selected_loadout["main"] = resource_path
			# If the previous main was two-handed it's gone now; offhand stays as-is
	loadout_changed.emit(selected_loadout)
	save_game()
	return true

func unequip_loadout_slot(slot: String) -> void:
	if selected_loadout.get(slot, "") != "":
		selected_loadout[slot] = ""
		loadout_changed.emit(selected_loadout)
		save_game()

func is_loadout_item_selected(resource_path: String) -> bool:
	return selected_loadout.get("main", "") == resource_path \
		or selected_loadout.get("offhand", "") == resource_path

func get_selected_loadout() -> Dictionary:
	return selected_loadout

# --- Run lifecycle -------------------------------------------------------

func start_run() -> void:
	run_gold = 0
	current_wave = 0
	kills_this_run = 0
	run_active = true
	_run_start_msec = Time.get_ticks_msec()
	run_gold_changed.emit(run_gold)
	wave_changed.emit(current_wave)

func add_run_gold(amount: int) -> void:
	run_gold += amount
	run_gold_changed.emit(run_gold)

## Seconds since the current run started.
func get_run_time() -> float:
	if _run_start_msec == 0:
		return 0.0
	return (Time.get_ticks_msec() - _run_start_msec) / 1000.0

## Total liquid value of the player: banked + earned this run + value of owned gear.
func get_total_value() -> int:
	var total := banked_gold + run_gold
	for path in owned_items:
		total += _item_cost(path)
	return total

## Settle a finished run (death). Liquidates gear, keeps 10% of total value
## floored at DEFAULT_GOLD, then persists. Returns a stats Dictionary.
## Guarded against double-calls (died can fire more than once on the host).
func end_run() -> Dictionary:
	if not run_active:
		return last_run_stats
	run_active = false

	var total := get_total_value()
	var kept: int = max(DEFAULT_GOLD, int(floor(total * SURVIVAL_KEEP_FRACTION)))

	last_run_stats = {
		"waves": current_wave,
		"kills": kills_this_run,
		"time": get_run_time(),
		"gold_earned": run_gold,
		"total_value": total,
		"kept": kept,
	}

	banked_gold = kept
	owned_items.clear()
	selected_loadout = {"main": "", "offhand": ""}
	run_gold = 0
	gold_changed.emit(banked_gold)
	run_gold_changed.emit(run_gold)
	loadout_changed.emit(selected_loadout)
	save_game()
	run_ended.emit(last_run_stats)
	return last_run_stats

## Walk out of the arena alive: bank 100% of run gold, keep all gear.
## The reward for retiring instead of dying (death only keeps 10%).
func retire_run() -> Dictionary:
	if not run_active:
		return last_run_stats
	run_active = false

	last_run_stats = {
		"waves": current_wave,
		"kills": kills_this_run,
		"time": get_run_time(),
		"gold_earned": run_gold,
		"total_value": get_total_value(),
		"kept": banked_gold + run_gold,
		"retired": true,
	}

	banked_gold += run_gold
	run_gold = 0
	gold_changed.emit(banked_gold)
	run_gold_changed.emit(run_gold)
	save_game()
	run_ended.emit(last_run_stats)
	return last_run_stats

# --- Multiplayer crediting (server -> owning peer) ------------------------
# GameState is an autoload, so it exists at the same path on every peer with
# the server (peer 1) as its multiplayer authority. The server decides who
# earned gold and credits exactly that peer's local GameState.

## Server-side entry point: credit kill gold (and a kill) to a specific peer.
func server_credit_kill(peer_id: int, amount: int) -> void:
	if not multiplayer.is_server():
		return
	if peer_id == 1:
		credit_kill(amount)  # Host credits itself directly
	elif _is_known_peer(peer_id):
		rpc_id(peer_id, "credit_kill", amount)

## Server-side entry point: credit plain gold (e.g. wave bonus) to a peer.
func server_credit_gold(peer_id: int, amount: int) -> void:
	if not multiplayer.is_server():
		return
	if peer_id == 1:
		credit_gold(amount)
	elif _is_known_peer(peer_id):
		rpc_id(peer_id, "credit_gold", amount)

## Runtime lookup so this singleton has no compile-time dependency on Network
## (keeps the economy testable standalone).
func _is_known_peer(peer_id: int) -> bool:
	var network = get_node_or_null("/root/Network")
	return network != null and network.players.has(peer_id)

## Server-side entry point: broadcast the current wave number to all peers.
func server_sync_wave(wave: int) -> void:
	if not multiplayer.is_server():
		return
	rpc("sync_wave", wave)

@rpc("authority", "reliable")
func credit_gold(amount: int) -> void:
	if not run_active:
		return
	add_run_gold(amount)

@rpc("authority", "reliable")
func credit_kill(amount: int) -> void:
	if not run_active:
		return
	kills_this_run += 1
	add_run_gold(amount)

@rpc("authority", "call_local", "reliable")
func sync_wave(wave: int) -> void:
	current_wave = wave
	wave_changed.emit(current_wave)

# --- Helpers -------------------------------------------------------------

func _is_two_hand(resource_path: String) -> bool:
	if resource_path.is_empty() or not ResourceLoader.exists(resource_path):
		return false
	var res = load(resource_path)
	return res is WeaponData and res.hand_type == WeaponData.Hand.TWO_HAND

func _item_cost(resource_path: String) -> int:
	if resource_path.is_empty() or not ResourceLoader.exists(resource_path):
		return 0
	var res := load(resource_path)
	if res is WeaponData:
		return res.cost
	return 0
