extends RefCounted
class_name DataCatalog

const OFFICERS_PATH := "res://data/defs/officers.json"
const CLASSES_PATH := "res://data/defs/classes.json"
const TERRAIN_PATH := "res://data/defs/terrain.json"
const ITEMS_PATH := "res://data/defs/items.json"
const SKILLS_PATH := "res://data/defs/skills.json"

var officers: Dictionary = {}
var classes: Dictionary = {}
var terrain: Dictionary = {}
var items: Dictionary = {}
var skills: Dictionary = {}


func load_defaults() -> bool:
	officers = _load_json_object(OFFICERS_PATH)
	classes = _load_json_object(CLASSES_PATH)
	terrain = _load_json_object(TERRAIN_PATH)
	items = _load_json_object(ITEMS_PATH)
	skills = _load_json_object(SKILLS_PATH)
	return not officers.is_empty() and not classes.is_empty() and not terrain.is_empty()


func hydrate_deployment(deployment: Dictionary) -> Dictionary:
	var officer := _get_dict(officers, deployment.get("officer_id", ""))
	var class_id := str(deployment.get("class_id", officer.get("class_id", "infantry")))
	var class_def := _get_dict(classes, class_id)
	var equipment := _merged_equipment(officer.get("equipment", {}), deployment.get("equipment", {}))
	var range_pair := _resolve_attack_range(class_def, equipment)

	var stats := {}
	_overlay_stats(stats, officer.get("base", {}))
	_overlay_stats(stats, deployment.get("base", {}))
	_add_stats(stats, class_def.get("base_bonus", {}))
	_add_equipment_bonuses(stats, equipment)

	var unit := {}
	unit["id"] = str(deployment.get("unit_id", deployment.get("id", deployment.get("officer_id", ""))))
	unit["officer_id"] = str(deployment.get("officer_id", ""))
	unit["name"] = str(deployment.get("name", officer.get("name", unit["id"])))
	unit["portrait"] = str(deployment.get("portrait", officer.get("portrait", "")))
	unit["sprite"] = str(deployment.get("sprite", officer.get("sprite", class_def.get("sprite", ""))))
	unit["class_id"] = class_id
	unit["class"] = str(class_def.get("name", class_id.capitalize()))
	unit["team"] = str(deployment.get("team", "enemy"))
	unit["pos"] = deployment.get("pos", [0, 0])
	unit["requires_joined"] = bool(deployment.get("requires_joined", false))
	unit["controllable"] = bool(deployment.get("controllable", true))
	unit["persist_progression"] = bool(deployment.get("persist_progression", true))
	unit["ai_target_priority"] = int(deployment.get("ai_target_priority", 0))
	unit["level"] = int(deployment.get("level", officer.get("level", 1)))
	unit["exp"] = int(deployment.get("exp", officer.get("exp", 0)))
	unit["max_hp"] = int(stats.get("hp", stats.get("max_hp", 1)))
	unit["hp"] = int(deployment.get("hp", unit["max_hp"]))
	unit["max_mp"] = int(stats.get("mp", 0))
	unit["mp"] = int(deployment.get("mp", unit["max_mp"]))
	unit["atk"] = int(stats.get("atk", 1))
	unit["def"] = int(stats.get("def", 0))
	unit["int"] = int(stats.get("int", 0))
	unit["agi"] = int(stats.get("agi", 0))
	unit["move"] = int(deployment.get("move", class_def.get("move", 3)))
	unit["move_type"] = str(deployment.get("move_type", class_def.get("move_type", "foot")))
	if deployment.has("range"):
		range_pair = _range_pair_from_value(deployment["range"])
	unit["min_range"] = int(deployment.get("min_range", range_pair["min"]))
	unit["range"] = int(range_pair["max"])
	unit["growth"] = class_def.get("growth", {}).duplicate(true)
	unit["growth_bonus"] = officer.get("growth_bonus", {}).duplicate(true)
	if deployment.has("growth_bonus") and typeof(deployment["growth_bonus"]) == TYPE_DICTIONARY:
		for stat in deployment["growth_bonus"].keys():
			unit["growth_bonus"][stat] = deployment["growth_bonus"][stat]
	unit["equipment"] = equipment
	unit["skills"] = _merged_skill_list(class_def.get("skills", []), officer.get("skills", []), deployment.get("skills", []))
	return unit


func get_runtime_terrain_defs(default_defs: Dictionary) -> Dictionary:
	var result := default_defs.duplicate(true)
	for key in terrain.keys():
		var source := _get_dict(terrain, key)
		var fallback: Dictionary = result.get(key, {})
		var runtime := fallback.duplicate(true)
		runtime["id"] = str(source.get("id", runtime.get("id", key)))
		runtime["name"] = str(source.get("name", runtime.get("name", key)))
		runtime["defense"] = int(source.get("defense", runtime.get("defense", 0)))
		runtime["avoid"] = int(source.get("avoid", runtime.get("avoid", 0)))
		runtime["move_cost"] = source.get("move_cost", runtime.get("move_cost", 1))
		runtime["color"] = _parse_color(source.get("color", runtime.get("color", Color.WHITE)), runtime.get("color", Color.WHITE))
		result[key] = runtime
	return result


func get_skill(skill_id: String) -> Dictionary:
	return _get_dict(skills, skill_id).duplicate(true)


func get_item(item_id: String) -> Dictionary:
	return _get_dict(items, item_id).duplicate(true)


func get_item_ids() -> Array:
	var result := []
	for item_id in items.keys():
		result.append(str(item_id))
	result.sort()
	return result


func get_class_def(class_id: String) -> Dictionary:
	return _get_dict(classes, class_id).duplicate(true)


func get_portrait_for_speaker(speaker_name: String) -> String:
	var normalized_speaker := _normalized_speaker_name(speaker_name)
	if normalized_speaker.is_empty():
		return ""
	for officer_id in officers.keys():
		var officer := _get_dict(officers, officer_id)
		if _normalized_speaker_name(str(officer.get("name", ""))) == normalized_speaker:
			return str(officer.get("portrait", ""))
	return ""


func get_portrait_for_officer(officer_id: String) -> String:
	if officer_id.is_empty():
		return ""
	var officer := _get_dict(officers, officer_id)
	return str(officer.get("portrait", ""))


func _load_json_object(path: String) -> Dictionary:
	if not FileAccess.file_exists(path):
		push_error("Missing data file: %s" % path)
		return {}

	var text := FileAccess.get_file_as_string(path)
	var parsed = JSON.parse_string(text)
	if typeof(parsed) != TYPE_DICTIONARY:
		push_error("Data file is not a JSON object: %s" % path)
		return {}
	return parsed


func _get_dict(source: Dictionary, key) -> Dictionary:
	if key == null:
		return {}
	var value = source.get(str(key), {})
	if typeof(value) == TYPE_DICTIONARY:
		return value
	return {}


func _normalized_speaker_name(value: String) -> String:
	return value.strip_edges().to_lower()


func _merged_equipment(base_equipment, override_equipment) -> Dictionary:
	var result := {}
	if typeof(base_equipment) == TYPE_DICTIONARY:
		result = base_equipment.duplicate(true)
	if typeof(override_equipment) == TYPE_DICTIONARY:
		for slot in override_equipment.keys():
			result[slot] = override_equipment[slot]
	return result


func _overlay_stats(target: Dictionary, source) -> void:
	if typeof(source) != TYPE_DICTIONARY:
		return
	for key in source.keys():
		target[key] = int(source[key])


func _add_stats(target: Dictionary, source) -> void:
	if typeof(source) != TYPE_DICTIONARY:
		return
	for key in source.keys():
		target[key] = int(target.get(key, 0)) + int(source[key])


func _add_equipment_bonuses(target: Dictionary, equipment: Dictionary) -> void:
	for slot in equipment.keys():
		var item_id = equipment[slot]
		if item_id == null:
			continue
		var item := _get_dict(items, item_id)
		_add_stats(target, item.get("bonuses", {}))


func _merged_skill_list(class_skills, officer_skills, deployment_skills) -> Array:
	var result := []
	_append_unique_skills(result, class_skills)
	_append_unique_skills(result, officer_skills)
	_append_unique_skills(result, deployment_skills)
	return result


func _append_unique_skills(target: Array, source) -> void:
	if typeof(source) != TYPE_ARRAY:
		return
	for skill_id in source:
		var normalized := str(skill_id)
		if normalized.is_empty() or target.has(normalized):
			continue
		target.append(normalized)


func _resolve_attack_range(class_def: Dictionary, equipment: Dictionary) -> Dictionary:
	var attack_range := _range_pair_from_value(class_def.get("attack_range", 1))
	for slot in equipment.keys():
		var item_id = equipment[slot]
		if item_id == null:
			continue
		var item := _get_dict(items, item_id)
		if item.has("range"):
			var item_range := _range_pair_from_value(item["range"])
			attack_range["min"] = min(int(attack_range["min"]), int(item_range["min"]))
			attack_range["max"] = max(int(attack_range["max"]), int(item_range["max"]))
	return attack_range


func _range_pair_from_value(value) -> Dictionary:
	if typeof(value) == TYPE_ARRAY:
		if value.size() >= 2:
			return _normalized_range_pair(int(value[0]), int(value[1]))
		if value.size() == 1:
			return _normalized_range_pair(int(value[0]), int(value[0]))
		return _normalized_range_pair(1, 1)
	if typeof(value) != TYPE_INT and typeof(value) != TYPE_FLOAT:
		return _normalized_range_pair(1, 1)
	var scalar: int = maxi(1, int(value))
	return _normalized_range_pair(scalar, scalar)


func _normalized_range_pair(min_range: int, max_range: int) -> Dictionary:
	var safe_min: int = maxi(1, min_range)
	var safe_max: int = maxi(safe_min, max_range)
	return {
		"min": safe_min,
		"max": safe_max
	}


func _parse_color(value, fallback: Color) -> Color:
	if typeof(value) == TYPE_COLOR:
		return value
	if typeof(value) == TYPE_ARRAY and value.size() >= 3:
		var alpha := 1.0
		if value.size() >= 4:
			alpha = float(value[3])
		return Color(float(value[0]), float(value[1]), float(value[2]), alpha)

	var text := str(value)
	if Color.html_is_valid(text):
		return Color.html(text)
	return fallback
