## CombatManager — 游戏循环状态机（属于 CombatScene 节点）
## 状态：BATTLE → SETTLEMENT → SHOP → BATTLE…
## 权威来源：development_plan.md S2 CombatManager
extends Node

enum GameState {
	INIT       = 0,
	BATTLE     = 1,
	SETTLEMENT = 2,
	SHOP       = 3,
	GAME_OVER  = 4,
}

var state: int = GameState.INIT

## 玩家法杯法术列表（已安装到 Core 的 ID；"" = 空槽，供 MATRIX/CIRCUIT 位置化布局）
var _deck_spell_ids: Array = ["action_spark_bolt"]
var _equipped_core: CoreDefinition = null
var _equipped_compiled: CompiledDeck = null

## 可切换的 Core 名册（商店内 Tab 循环切换；S5 高级 Core 实际可玩入口）
const _CORE_ROSTER: Array = ["wand_basic", "wand_fast", "wand_memory", "matrix_board", "circuit_fork"]
var _core_idx: int = 0

## 备牌区：拥有但未装入插槽的法术 id（背包 UI 在此与插槽间移动）
var _bench: Array = []

## 本局开始时刻（秒），用于排行榜评分的 elapsed_sec
var _run_start_time: float = 0.0

## 商店 UI 回调（由场景设置）
var _on_shop_closed_cb: Callable = Callable()

func _ready() -> void:
	EventBus.subscribe(EventID.WAVE_COMPLETE, _on_wave_complete)
	EventBus.subscribe(EventID.PLAYER_DIED, _on_player_died)
	ProfileManager.set_wand_provider(self)  # 法杖状态纳入 A/B 存档（ADR-A2）

## 开始新局
func start_game() -> void:
	PlayerStats.reset_for_run()
	WaveManager.reset()
	BulletManager.reset()
	EnemyManager.reset()
	StatusManager.reset()
	DpsTracker.reset()
	VFXManager.reset()
	ZoneManager.reset()
	MinionManager.reset()
	EnemyBulletManager.reset()
	BossManager.reset()
	SpatialGrid.clear()
	_core_idx = 0
	_equipped_core = WandPreset.make_core_by_id("wand_basic")
	_deck_spell_ids = ["action_spark_bolt"]
	_bench.clear()
	_run_start_time = Time.get_ticks_msec() / 1000.0
	_rebuild_wand()
	_transition_to_battle(1)

## 继续现有进度恢复
func resume_game() -> void:
	var saved: Dictionary = ProfileManager.load_run()
	if saved.is_empty():
		start_game()
		return
	ProfileManager.apply_run(saved)
	_rebuild_wand()
	var wave: int = max(1, WaveManager.current_wave)
	_transition_to_battle(wave)

func _transition_to_battle(wave_num: int) -> void:
	var prev: String = GameState.keys()[state] if state < GameState.size() else "UNKNOWN"
	state = GameState.BATTLE
	SubPayloadRegistry.lock_for_battle()  # P6-N10: 战斗期间锁定，防止竞争条件
	PlayerManager.equip_wand(_equipped_core, _equipped_compiled)
	WaveManager.start_wave(wave_num)
	EventBus.emit(EventID.GAME_STATE_CHANGED, {"from": prev, "to": "BATTLE"})

func _on_wave_complete(payload: Dictionary) -> void:
	var wave: int = int(payload.get("wave", WaveManager.current_wave))
	state = GameState.SETTLEMENT
	ProfileManager.save_run()
	EventBus.emit(EventID.GAME_STATE_CHANGED, {"from": "BATTLE", "to": "SETTLEMENT"})
	# 厂他延迟进入商店（等待结算动画， S2 直接进入）
	call_deferred("_open_shop", wave)

func _open_shop(wave: int) -> void:
	state = GameState.SHOP
	ShopManager.open_shop()
	EventBus.emit(EventID.GAME_STATE_CHANGED, {"from": "SETTLEMENT", "to": "SHOP"})
	if not _on_shop_closed_cb.is_null():
		_on_shop_closed_cb.call(wave)

## 外部调用：购买法术并安装到法杯
## LINEAR：MODIFIER 插首个 ACTION 前、ACTION 追加尾部（Noita 风格）
## MATRIX/CIRCUIT：位置化——填入首个空槽（"") 以保持拓扑布局
func install_spell(spell: SpellNode) -> void:
	if spell == null:
		return
	var max_slots: int = _equipped_core.slot_count if _equipped_core else 5
	if _is_positional_core():
		var placed: bool = false
		for i in _deck_spell_ids.size():
			if String(_deck_spell_ids[i]) == "":
				_deck_spell_ids[i] = spell.id
				placed = true
				break
		if not placed:
			if _deck_spell_ids.size() < max_slots:
				_deck_spell_ids.append(spell.id)
			else:
				_bench.append(spell.id)  # 满了进备牌区，玩家可在背包内重排
	elif _deck_spell_ids.size() >= max_slots:
		_bench.append(spell.id)          # 满了进备牌区
	elif spell.type == SpellNode.SpellType.MODIFIER:
		var insert_pos: int = 0
		for i in _deck_spell_ids.size():
			var sn: SpellNode = SpellRegistry.get_spell(_deck_spell_ids[i])
			if sn and sn.type == SpellNode.SpellType.ACTION:
				insert_pos = i
				break
			insert_pos = i + 1
		_deck_spell_ids.insert(insert_pos, spell.id)
	else:
		_deck_spell_ids.append(spell.id)
	_rebuild_wand()
	ProfileManager.mark_dirty()

func _is_positional_core() -> bool:
	return _equipped_core != null and _equipped_core.topology != CoreDefinition.Topology.LINEAR

## 切换装备的 Core（商店内调用）；载入该 Core 的演示默认 Deck 并重编译
func switch_core() -> void:
	_core_idx = (_core_idx + 1) % _CORE_ROSTER.size()
	var cid: String = _CORE_ROSTER[_core_idx]
	_equipped_core = _make_core_by_id(cid)
	_deck_spell_ids = _default_deck_for(cid)
	_rebuild_wand()
	PlayerManager.equip_wand(_equipped_core, _equipped_compiled)  # 即时换装
	ProfileManager.mark_dirty()

func get_core_display() -> String:
	return _equipped_core.display_name if _equipped_core else "—"

func _make_core_by_id(cid: String) -> CoreDefinition:
	return WandPreset.make_core_by_id(cid)  # 数据驱动（cores.json）+ 硬编码回退

## 每个 Core 的演示默认 Deck（"" = 空槽）：让玩家切换即见到该拓扑机制
func _default_deck_for(cid: String) -> Array:
	match cid:
		"wand_memory":   return ["logic_every_n_shots", "action_spark_bolt"]
		"matrix_board":  return ["action_spark_bolt", "", "", "", "modifier_damage_plus", "", "", ""]
		"circuit_fork":  return ["", "action_spark_bolt", "action_fire_bolt"]
		_:               return ["action_spark_bolt"]

# ── 背包 / 插槽编辑（inventory UI 调用）──────────────────────────
func get_slot_count() -> int:
	return _equipped_core.slot_count if _equipped_core else 5

## 返回补齐到 slot_count 长度的插槽视图（"" = 空槽）
func get_padded_deck() -> Array:
	var n: int = get_slot_count()
	var out: Array = _deck_spell_ids.duplicate()
	while out.size() < n:
		out.append("")
	if out.size() > n:
		out.resize(n)
	return out

func get_bench() -> Array:
	return _bench

## 写回插槽视图：LINEAR 压实去空槽，MATRIX/CIRCUIT 保留位置
func _apply_padded(arr: Array) -> void:
	if _is_positional_core():
		_deck_spell_ids = arr.duplicate()
	else:
		var compact: Array = []
		for s in arr:
			if String(s) != "":
				compact.append(s)
		_deck_spell_ids = compact
	_rebuild_wand()
	PlayerManager.equip_wand(_equipped_core, _equipped_compiled)
	ProfileManager.mark_dirty()

## 交换两个插槽内容
func swap_slots(a: int, b: int) -> void:
	var d: Array = get_padded_deck()
	if a < 0 or b < 0 or a >= d.size() or b >= d.size():
		return
	var t = d[a]; d[a] = d[b]; d[b] = t
	_apply_padded(d)

## 清空插槽 → 内容移入备牌区
func slot_to_bench(idx: int) -> void:
	var d: Array = get_padded_deck()
	if idx < 0 or idx >= d.size():
		return
	var sid: String = String(d[idx])
	if sid == "":
		return
	_bench.append(sid)
	d[idx] = ""
	_apply_padded(d)

## 备牌 → 插槽（插槽已占用则原内容退回备牌区）
func bench_to_slot(bench_idx: int, slot_idx: int) -> void:
	var d: Array = get_padded_deck()
	if bench_idx < 0 or bench_idx >= _bench.size() or slot_idx < 0 or slot_idx >= d.size():
		return
	var sid: String = String(_bench[bench_idx])
	var existing: String = String(d[slot_idx])
	_bench.remove_at(bench_idx)
	if existing != "":
		_bench.append(existing)
	d[slot_idx] = sid
	_apply_padded(d)

## 关闭商店进入下一波
func close_shop_and_next_wave() -> void:
	ShopManager.close_shop()
	BulletManager.reset()
	EnemyManager.reset()
	ZoneManager.reset()
	MinionManager.reset()
	EnemyBulletManager.reset()
	BossManager.reset()
	SpatialGrid.clear()
	var next_wave: int = WaveManager.current_wave + 1
	_transition_to_battle(next_wave)

func _on_player_died(_payload: Dictionary) -> void:
	if state == GameState.GAME_OVER:
		return  # 防重入：同帧内多路死亡（子弹命中 + 卖血施法）不重复结算战绩/发以太
	state = GameState.GAME_OVER
	# 先采集本局战绩（reset 前），写入排行榜
	var wave_reached: int = max(1, WaveManager.current_wave)
	var kills: int = PlayerManager.kill_count
	var elapsed: int = int(Time.get_ticks_msec() / 1000.0 - _run_start_time)
	var score: int = EndlessRecords.compute_score(wave_reached, elapsed)
	EndlessRecords.add_record(wave_reached, elapsed, kills)
	MetaProgress.add_aether(SettingsManager.aether_for_wave(wave_reached))
	PlayerManager.reset()
	BulletManager.reset()
	EnemyManager.reset()
	ZoneManager.reset()
	MinionManager.reset()
	EnemyBulletManager.reset()
	BossManager.reset()
	SpatialGrid.clear()
	ProfileManager.clear_run()
	EventBus.emit(EventID.GAME_OVER, {
		"wave": wave_reached, "elapsed_sec": elapsed, "kills": kills, "score": score})

func _rebuild_wand() -> void:
	if _equipped_core == null:
		_equipped_core = WandPreset.make_core_by_id("wand_basic")
	var nodes: Array = []
	for sid in _deck_spell_ids:
		if String(sid) == "":
			nodes.append(null)  # 空槽：保持 MATRIX/CIRCUIT 位置（LINEAR 扁平化会跳过 null）
			continue
		nodes.append(SpellRegistry.get_spell(sid))  # 纯数据驱动；未知 id → null（下游按位置跳过）
	_equipped_compiled = SpellEvaluator.compile_wand(_equipped_core, nodes)
	var leech: float = _equipped_core.base_mana_leech if _equipped_core else 0.0
	for n in nodes:
		if n != null:
			leech += float(n.meta.get("mana_leech", 0.0))
	PlayerStats.mana_leech = leech

# ── 法杖存档（ProfileManager A/B 槽，ADR-A2）─────────────────
func get_wand_save_data() -> Dictionary:
	return {
		"core_id": _equipped_core.id if _equipped_core else "wand_basic",
		"deck":    _deck_spell_ids.duplicate(),
		"bench":   _bench.duplicate(),
	}

func apply_wand_save_data(d: Dictionary) -> void:
	var cid: String = String(d.get("core_id", "wand_basic"))
	_equipped_core = _make_core_by_id(cid)
	_core_idx = _CORE_ROSTER.find(cid)
	if _core_idx < 0:
		_core_idx = 0
	# JSON 反序列化为 Array；逐项转 String 保证类型一致
	_deck_spell_ids = []
	for s in d.get("deck", ["action_spark_bolt"]):
		_deck_spell_ids.append(String(s))
	if _deck_spell_ids.is_empty():
		_deck_spell_ids = ["action_spark_bolt"]
	_bench = []
	for s in d.get("bench", []):
		_bench.append(String(s))
	_rebuild_wand()

func get_deck_spell_ids() -> Array:
	return _deck_spell_ids

## 防呆校验：Deck 是否含至少一个 ACTION 法术（否则法术塔无法开火，见 game_design §5.1 G5 防呆机制）
func has_castable_action() -> bool:
	for sid in _deck_spell_ids:
		if String(sid) == "":
			continue
		var sn: SpellNode = SpellRegistry.get_spell(sid)
		if sn != null and sn.type == SpellNode.SpellType.ACTION:
			return true
	return false

func get_equipped_compiled() -> CompiledDeck:
	return _equipped_compiled
