## BossManager — Boss 阶段状态机 + 攻击模式调度（Autoload）
## Boss 实体仍在 EnemyManager SoA（渲染/受击/HP）；本管理器侧挂运行态、接管移动、驱动招式。
## 数据驱动：data/bosses.json（每 boss_type 的 move_speed / phases / attack_patterns），自载。
extends Node

const BOSSES_JSON: String = "res://data/bosses.json"

# boss_type(int) → { move_speed:float, phases:Array[Dictionary] }
var _defs: Dictionary = {}
# boss_id(int) → 运行态 Dictionary
var _active: Dictionary = {}

func _ready() -> void:
	_load_defs()
	EventBus.subscribe(EventID.ENEMY_KILLED, _on_enemy_killed)

func _load_defs() -> void:
	if not FileAccess.file_exists(BOSSES_JSON):
		push_error("BossManager: 缺失 res://data/bosses.json")
		return
	var data = JSON.parse_string(FileAccess.get_file_as_string(BOSSES_JSON))
	if not (data is Dictionary):
		push_error("BossManager: bosses.json 格式错误")
		return
	_defs.clear()
	for k in data:
		_defs[int(k)] = data[k]

## wave_manager._spawn_boss 生成 SoA Boss 后调用
func register_boss(boss_id: int, boss_type: int, max_hp: float, wave: int) -> void:
	if not _defs.has(boss_type):
		push_error("BossManager: bosses.json 缺少 boss_type=%d 的配置（该 Boss 将无阶段/招式）" % boss_type)
	var def: Dictionary = _defs.get(boss_type, {})
	var phases: Array = def.get("phases", [])
	_active[boss_id] = {
		"boss_type": boss_type,
		"max_hp": max_hp,
		"wave": wave,
		"move_speed": float(def.get("move_speed", 100.0)),
		"phase": 0,
		"phases": phases,
		"pattern_index": 0,
		"cooldown": 1.0,        # 入场后短暂延迟再起手
		"action": {},           # 进行中招式（空=无）
		"spiral_phase": 0.0,    # spiral 招式的累积旋转角
	}
	EnemyManager.register_external_mover(boss_id)
	if not phases.is_empty():
		_emit_phase(boss_id, 0)

func _physics_process(delta: float) -> void:
	if _active.is_empty():
		return
	for boss_id in _active.keys():
		if not _active.has(boss_id):
			continue  # 防重入：本帧内被 reset()/erase（如玩家死亡致命一击）→ 跳过已失效键
		if not EnemyManager.has_entity(boss_id):
			continue
		var st: Dictionary = _active[boss_id]
		_check_phase(boss_id, st)
		_tick_movement(boss_id, st, delta)
		_tick_action(boss_id, st, delta)

# 阶段切换：HP% 跨入更深阶段（phases 按 hp_threshold 降序，取满足阈值的最深项）
func _check_phase(boss_id: int, st: Dictionary) -> void:
	var hp_pct: float = EnemyManager.get_hp_percent(boss_id)
	var phases: Array = st["phases"]
	var target: int = int(st["phase"])
	for i in phases.size():
		if hp_pct <= float(phases[i].get("hp_threshold", 1.0)):
			target = i
	if target > int(st["phase"]):
		st["phase"] = target
		st["pattern_index"] = 0
		st["cooldown"] = 0.5
		st["action"] = {}
		_summon_adds(boss_id, int(phases[target].get("summon_adds", 0)))
		_emit_phase(boss_id, target)

func _emit_phase(boss_id: int, phase: int) -> void:
	EventBus.emit(EventID.BOSS_PHASE_CHANGED, {
		"boss_id": boss_id, "boss_type": int(_active[boss_id]["boss_type"]), "phase": phase})

func _summon_adds(boss_id: int, n: int) -> void:
	if n <= 0:
		return
	var boss_pos: Vector2 = EnemyManager.get_pos_by_id(boss_id)
	for i in n:
		var ang: float = TAU * float(i) / float(n)
		var off: Vector2 = Vector2(cos(ang), sin(ang)) * 80.0
		EnemyManager.spawn_enemy(boss_pos + off, 40.0, EnemyManager.Type.FAST)

func _cur_phase(st: Dictionary) -> Dictionary:
	var phases: Array = st["phases"]
	if phases.is_empty():
		return {}
	return phases[clampi(int(st["phase"]), 0, phases.size() - 1)]

# 移动：无「接管移动」的招式在进行时，朝玩家缓慢漂移
func _tick_movement(boss_id: int, st: Dictionary, delta: float) -> void:
	var action: Dictionary = st["action"]
	if not action.is_empty() and bool(action.get("controls_move", false)):
		return
	var pos: Vector2 = EnemyManager.get_pos_by_id(boss_id)
	var player: Vector2 = PlayerManager.get_position()
	var spd: float = float(st["move_speed"]) * float(_cur_phase(st).get("move_speed_mult", 1.0))
	var to: Vector2 = player - pos
	if to.length() > 1.0:
		pos += to.normalized() * spd * delta
		EnemyManager.set_entity_pos(boss_id, pos)

# 招式推进：有进行中招式 → 推进；否则冷却到点 → 起手下一招式
func _tick_action(boss_id: int, st: Dictionary, delta: float) -> void:
	var action: Dictionary = st["action"]
	if not action.is_empty():
		_advance_action(boss_id, st, action, delta)
		return
	st["cooldown"] = float(st["cooldown"]) - delta
	if float(st["cooldown"]) > 0.0:
		return
	var patterns: Array = _cur_phase(st).get("attack_patterns", [])
	if patterns.is_empty():
		st["cooldown"] = 1.0
		return
	var idx: int = int(st["pattern_index"]) % patterns.size()
	st["pattern_index"] = int(st["pattern_index"]) + 1
	_start_pattern(boss_id, st, patterns[idx])

# 招式起手（Task 8 再补 timed 招式分支）
func _start_pattern(boss_id: int, st: Dictionary, p: Dictionary) -> void:
	var kind: String = String(p.get("kind", "ring"))
	var pos: Vector2 = EnemyManager.get_pos_by_id(boss_id)
	match kind:
		"ring", "spread", "aimed", "spiral":
			_fire_ranged(boss_id, st, kind, p, pos)
			st["cooldown"] = float(p.get("cooldown", 2.0))
		"dash":
			_telegraph(p, pos)
			st["action"] = {
				"kind": "dash", "stage": "windup", "timer": float(p.get("windup", 0.8)),
				"controls_move": true, "p": p,
				"target": PlayerManager.get_position(), "dir": Vector2.RIGHT, "hit_cd": 0.0}
		"charge_up":
			_telegraph(p, pos)
			st["action"] = {
				"kind": "charge_up", "stage": "windup", "timer": float(p.get("windup", 1.0)),
				"controls_move": true, "p": p}
		"melee":
			_telegraph(p, pos)
			st["action"] = {
				"kind": "melee", "stage": "windup", "timer": float(p.get("windup", 0.6)),
				"controls_move": true, "p": p}
		_:
			st["cooldown"] = float(p.get("cooldown", 2.0))

## 瞬时弹幕发射（ring/spread/aimed/spiral）
func _fire_ranged(boss_id: int, st: Dictionary, kind: String, p: Dictionary, pos: Vector2) -> void:
	var count: int = maxi(1, int(p.get("count", 12)))
	var speed: float = float(p.get("speed", 200.0))
	var dmg: float = float(p.get("bullet_damage", 8.0))
	var r: float = float(p.get("bullet_radius", 8.0))
	var life: float = float(p.get("lifetime", 4.0))
	var to_player: Vector2 = PlayerManager.get_position() - pos
	var aim: float = to_player.angle() if to_player.length() > 0.01 else 0.0
	match kind:
		"ring":
			for i in count:
				_spawn_dir(pos, TAU * float(i) / float(count), speed, life, r, dmg)
		"spread":
			var half: float = deg_to_rad(float(p.get("half_angle", 30.0)))
			for i in count:
				var t: float = 0.0 if count <= 1 else (float(i) / float(count - 1)) * 2.0 - 1.0
				_spawn_dir(pos, aim + half * t, speed, life, r, dmg)
		"aimed":
			for i in count:
				_spawn_dir(pos, aim, speed, life, r, dmg)
		"spiral":
			var step: float = deg_to_rad(float(p.get("angle_step", 15.0)))
			var base_a: float = float(st.get("spiral_phase", 0.0))
			for i in count:
				_spawn_dir(pos, base_a + TAU * float(i) / float(count), speed, life, r, dmg)
			st["spiral_phase"] = base_a + step

func _spawn_dir(pos: Vector2, angle: float, speed: float, life: float, r: float, dmg: float) -> void:
	EnemyBulletManager.spawn_bullet(pos, Vector2(cos(angle), sin(angle)) * speed, life, r, dmg)

## 前摇预警 VFX
func _telegraph(p: Dictionary, pos: Vector2) -> void:
	var vfx: String = String(p.get("telegraph_vfx", ""))
	if vfx != "":
		VFXManager.play(vfx, pos, 1.5)

## 招式推进：三段式 windup → active → recover；结束还原并置冷却
func _advance_action(boss_id: int, st: Dictionary, action: Dictionary, delta: float) -> void:
	action["timer"] = float(action["timer"]) - delta
	var p: Dictionary = action["p"]
	var pos: Vector2 = EnemyManager.get_pos_by_id(boss_id)
	match String(action["kind"]):
		"dash":
			match String(action["stage"]):
				"windup":
					if float(action["timer"]) <= 0.0:
						var dir: Vector2 = Vector2(action["target"]) - pos
						action["dir"] = dir.normalized() if dir.length() > 0.01 else Vector2.RIGHT
						action["stage"] = "active"
						action["timer"] = float(p.get("dash_time", 0.5))
				"active":
					pos += Vector2(action["dir"]) * float(p.get("dash_speed", 600.0)) * delta
					EnemyManager.set_entity_pos(boss_id, pos)
					action["hit_cd"] = maxf(0.0, float(action["hit_cd"]) - delta)
					if pos.distance_to(PlayerManager.get_position()) <= float(p.get("contact_radius", 40.0)) and float(action["hit_cd"]) <= 0.0:
						PlayerStats.take_damage(float(p.get("contact_damage", 15.0)))
						action["hit_cd"] = float(p.get("contact_hit_cd", 0.4))
					if float(action["timer"]) <= 0.0:
						action["stage"] = "recover"
						action["timer"] = float(p.get("recover", 0.8))
				"recover":
					if float(action["timer"]) <= 0.0:
						_end_action(st, p)
		"charge_up":
			match String(action["stage"]):
				"windup":
					if float(action["timer"]) <= 0.0:
						_fire_ranged(boss_id, st, String(p.get("release_kind", "ring")), p, pos)
						action["stage"] = "recover"
						action["timer"] = float(p.get("recover", 0.8))
				"recover":
					if float(action["timer"]) <= 0.0:
						_end_action(st, p)
		"melee":
			match String(action["stage"]):
				"windup":
					if float(action["timer"]) <= 0.0:
						if pos.distance_to(PlayerManager.get_position()) <= float(p.get("melee_radius", 90.0)):
							PlayerStats.take_damage(float(p.get("melee_damage", 20.0)))
						VFXManager.play("hit_spark", pos, 2.0)
						action["stage"] = "recover"
						action["timer"] = float(p.get("recover", 0.5))
				"recover":
					if float(action["timer"]) <= 0.0:
						_end_action(st, p)
		_:
			st["action"] = {}

func _end_action(st: Dictionary, p: Dictionary) -> void:
	st["action"] = {}
	st["cooldown"] = float(p.get("cooldown", 2.0))

func _on_enemy_killed(payload: Dictionary) -> void:
	var id: int = int(payload.get("entity_id", -1))
	if not _active.has(id):
		return
	var st: Dictionary = _active[id]
	EnemyManager.unregister_external_mover(id)
	EventBus.emit(EventID.BOSS_KILLED, {
		"boss_id": id, "boss_type": int(st["boss_type"]), "wave": int(st["wave"])})
	_active.erase(id)

func get_active_boss_count() -> int:
	return _active.size()

func get_phase(boss_id: int) -> int:
	return int(_active.get(boss_id, {}).get("phase", -1))

func reset() -> void:
	for id in _active.keys():
		EnemyManager.unregister_external_mover(id)
	_active.clear()
