class_name StudentProgression
extends Resource

signal progression_changed()

enum Status{
	Locked,
	Unlocked,
	Completed,
}

static var cached_boss_gate_lessons: Array[int] = []
static var cached_boss_gate_lessons_total: int = -1

@export var version: String = ProjectSettings.get_setting("application/config/version")
@export var unlocks: Dictionary[int, Dictionary] = {}:
	set(value):
		unlocks = ensure_data_integrity(value)
@export var highest_boss_defeated: int = 0:
	set(value):
		highest_boss_defeated = _sanitize_highest_boss(value)
@export var boss_failure_streak: int = 0
@export var boss_blocked: bool = false
@export var last_modified: String


func _init() -> void:
	init_unlocks()


# Make sure the unlocks are correct
func init_unlocks() -> void:
	if not unlocks:
		unlocks = {} # Triggers ensure_data_integrity(), that will fill the default values
	_sanitize_boss_progression()
	
	# Verify the lessons
	var number_of_lessons: int = Database.get_lessons_count()
	if unlocks.size() != number_of_lessons:
		for index: int in range(number_of_lessons):
			if not unlocks.has(index+1):
				unlocks[index + 1] = {
					"look_and_learn": Status.Locked,
					"games": [
						Status.Locked,
						Status.Locked,
						Status.Locked,
					],
					"last_duration": PackedInt32Array([0, 0, 0]),
					"total_duration": PackedInt32Array([0, 0, 0])
				}
		
	# Make sure that the first garden is always accessible
	if unlocks.has(1):
		if unlocks[1]["look_and_learn"] == Status.Locked:
			unlocks[1]["look_and_learn"] = Status.Unlocked
	_sanitize_boss_progression()


func ensure_data_integrity(data: Dictionary[int, Dictionary]) -> Dictionary:
	var is_init: bool = data.is_empty()
	var result: Dictionary[int, Dictionary] = data.duplicate(true)
	var number_of_lessons: int = Database.get_lessons_count()
	# Check for extra keys
	for key: int in result.keys():
		if key > number_of_lessons:
			result.erase(key)
	# Check missing keys
	var min_key: int = 1
	var max_key: int = number_of_lessons
	for index: int in range(min_key, max_key + 1):
		if not result.has(index):
			if not is_init:
				Log.warn("StudentProgression: Garden %d missing → added with default values." % index)
			result[index] = {
				"games": [Status.Locked, Status.Locked, Status.Locked],
				"look_and_learn": Status.Locked,
				"last_duration": PackedInt32Array([0, 0, 0]),
				"total_duration": PackedInt32Array([0, 0, 0])
			}
	# Check internal structure
	for index: int in result.keys():
		var garden: Dictionary = result[index]
		# Check missing keys
		if not garden.has("games"):
			if not is_init:
				Log.warn("StudentProgression: Garden %d: Add missing key 'games'." % index)
			garden["games"] = [Status.Locked, Status.Locked, Status.Locked]
		if not garden.has("look_and_learn"):
			if not is_init:
				Log.warn("StudentProgression: Garden %d: Add missing key 'look_and_learn'." % index)
			garden["look_and_learn"] = Status.Locked
		if not garden.has("last_duration"):
			garden["last_duration"] = PackedInt32Array([0, 0, 0])
		if not garden.has("total_duration"):
			garden["total_duration"] = PackedInt32Array([0, 0, 0])

		# Check array "games"
		if typeof(garden["games"]) != TYPE_ARRAY or (garden["games"] as Array).size() != 3:
			if not is_init:
				Log.warn("StudentProgression: Garden %d: invalid format for 'games' → reset." % index)
			garden["games"] = [Status.Locked, Status.Locked, Status.Locked]

		# Check value outside of possible enum values
		for game_index: int in range(3):
			if garden["games"][game_index] not in [Status.Locked, Status.Unlocked, Status.Completed]:
				garden["games"][game_index] = Status.Locked
		if garden["look_and_learn"] not in [Status.Locked, Status.Unlocked, Status.Completed]:
			garden["look_and_learn"] = Status.Locked

	# Check progression rules
	for index: int in range(min_key, max_key + 1):
		var garden: Dictionary = result[index]
		var prev_completed: bool = false

		# Check previous garden is completed
		if result.has(index - 1):
			var prev: Dictionary = result[index - 1]
			prev_completed = (
				prev["look_and_learn"] == Status.Completed and
				(prev["games"] as Array).all(func(x: int) -> bool: return x == Status.Completed)
			)
		else:
			# First garden (key 1) is always unlocked
			prev_completed = true

		# Case: previous garden not completed
		if not prev_completed:
			for game_index: int in range(3):
				if garden["games"][game_index] != Status.Locked or garden["look_and_learn"] != Status.Locked:
					if not is_init:
						Log.warn("StudentProgression: Garden %d: invalid progression (previous not finished) → reset." % index)
					garden["games"] = [Status.Locked, Status.Locked, Status.Locked]
					garden["look_and_learn"] = Status.Locked
					break
			continue

		# Case: lesson completed → unlock games if needed
		if garden["look_and_learn"] == Status.Completed:
			for game_index: int in range(3):
				if garden["games"][game_index] == Status.Locked:
					garden["games"][game_index] = Status.Unlocked
					if not is_init:
						Log.warn("StudentProgression: Garden %d: game %d unlocked because lesson is completed" % [index, game_index + 1])

		# Case: previous garden completed → unlock lesson if needed
		elif garden["look_and_learn"] == Status.Locked:
			garden["look_and_learn"] = Status.Unlocked
			if not is_init:
				Log.warn("StudentProgression: Garden %d: lesson unlocked because previous garden is completed" % index)

	_sanitize_boss_progression()
	return result


static func get_boss_gate_lessons() -> Array[int]:
	var total_lessons: int = Database.get_lessons_count()
	if cached_boss_gate_lessons_total == total_lessons:
		return cached_boss_gate_lessons.duplicate()
	var gates: Array[int] = []
	if total_lessons <= 0:
		return gates
	var boundary_lessons: Array[int] = _get_garden_boundary_lessons(total_lessons)
	var last_boss_pseudowords: int = 0
	for lesson_number: int in range(1, total_lessons + 1):
		var total_pseudowords: int = Database.get_pseudowords_for_lesson(lesson_number).size()
		var new_pseudowords: int = total_pseudowords - last_boss_pseudowords
		if new_pseudowords >= 30 and boundary_lessons.has(lesson_number):
			gates.append(lesson_number)
			last_boss_pseudowords = total_pseudowords
	cached_boss_gate_lessons_total = total_lessons
	cached_boss_gate_lessons = gates.duplicate()
	return gates


static func _get_garden_boundary_lessons(total_lessons: int) -> Array[int]:
	var boundaries: Array[int] = []
	var layout: GardensLayout = Gardens.get_session_layout(total_lessons)
	var distribution: Array[int] = Gardens.get_lessons_distribution(total_lessons, layout.gardens)
	var lesson_index: int = 0
	for count: int in distribution:
		if count <= 0:
			continue
		lesson_index += count
		if lesson_index < total_lessons:
			boundaries.append(lesson_index)
	return boundaries


func _sanitize_boss_progression() -> void:
	highest_boss_defeated = _sanitize_highest_boss(highest_boss_defeated)


func _sanitize_highest_boss(value: int) -> int:
	var gate_lessons: Array[int] = get_boss_gate_lessons()
	if gate_lessons.is_empty():
		return 0
	gate_lessons.sort()
	var sanitized: int = 0
	for gate_lesson: int in gate_lessons:
		if gate_lesson <= value:
			sanitized = gate_lesson
		else:
			break
	return sanitized


func is_boss_completed(gate_lesson: int) -> bool:
	if not get_boss_gate_lessons().has(gate_lesson):
		return false
	return gate_lesson <= highest_boss_defeated


func is_lesson_blocked_by_boss(lesson_number: int) -> bool:
	var gate_lessons: Array[int] = get_boss_gate_lessons()
	gate_lessons.sort()
	for gate: int in gate_lessons:
		if gate >= lesson_number:
			break
		if gate > highest_boss_defeated:
			return true
	return false


func get_max_unlocked_lesson_index() -> int:
	var max_unlocked_level: int = 1
	for index: int in range(unlocks.size()):
		if is_lesson_blocked_by_boss(index + 1):
			break
		if unlocks[index + 1]["look_and_learn"] >= Status.Unlocked:
			max_unlocked_level = index
		else:
			break
	
	return max_unlocked_level


func is_lesson_completed(lesson_number: int) -> bool:
	return unlocks[lesson_number]["look_and_learn"] == Status.Completed and unlocks[lesson_number]["games"][0] == Status.Completed and unlocks[lesson_number]["games"][1] == Status.Completed and unlocks[lesson_number]["games"][2] == Status.Completed


# Return true if the progression is saved or false if the look and learn was already completed
func look_and_learn_completed(lesson_number: int) -> bool:
	if unlocks[lesson_number]["look_and_learn"] == Status.Completed:
		return false
	
	unlocks[lesson_number]["look_and_learn"] = Status.Completed
	
	for index: int in range(3):
		unlocks[lesson_number]["games"][index] = Status.Unlocked
	
	last_modified = Time.get_datetime_string_from_system(true)
	progression_changed.emit()
	return true


# Return true if the progression is saved or false if the game was already completed
func game_completed(lesson_number: int, game_number: int) -> bool:
	# If the game is already completed, do nothing
	if unlocks[lesson_number]["games"][game_number] == Status.Completed:
		return false
	
	unlocks[lesson_number]["games"][game_number] = Status.Completed
	
	var all_completed: bool = true
	for index: int in range(3):
		all_completed = all_completed and unlocks[lesson_number]["games"][index] == Status.Completed
	
	if all_completed:
		if unlocks.has(lesson_number + 1):
			unlocks[lesson_number + 1]["look_and_learn"] = Status.Unlocked
	
	last_modified = Time.get_datetime_string_from_system(true)
	progression_changed.emit()
	return true


func boss_completed(lesson_number: int) -> bool:
	if not get_boss_gate_lessons().has(lesson_number):
		return false
	if lesson_number <= highest_boss_defeated:
		return false
	highest_boss_defeated = lesson_number
	if boss_failure_streak != 0:
		boss_failure_streak = 0
	last_modified = Time.get_datetime_string_from_system(true)
	progression_changed.emit()
	return true


func register_boss_failure() -> bool:
	if boss_blocked:
		return true
	boss_failure_streak = max(0, boss_failure_streak) + 1
	if boss_failure_streak >= 2:
		boss_blocked = true
	last_modified = Time.get_datetime_string_from_system(true)
	progression_changed.emit()
	return boss_blocked


func reset_boss_failure_streak() -> void:
	if boss_failure_streak == 0:
		return
	boss_failure_streak = 0
	last_modified = Time.get_datetime_string_from_system(true)
	progression_changed.emit()


func clear_boss_block() -> void:
	if not boss_blocked and boss_failure_streak == 0:
		return
	boss_blocked = false
	boss_failure_streak = 0
	last_modified = Time.get_datetime_string_from_system(true)
	progression_changed.emit()


func add_level_time(lesson_number: int, game_number: int, time_spent: int) -> void:
	Log.trace("StudentProgression: Add time to level %d, minigame %d. Time added: %s" % [lesson_number, game_number, time_spent])
	if game_number > 2:
		Log.error("StudentProgression: Cannot log a level time for a minigame number superior to 2")
		return
	
	if not unlocks.has(lesson_number):
		Log.error("StudentProgression: Cannot log a level time for lesson %d because it does not exists in progression data" % lesson_number)
		return
	
	if not (unlocks[lesson_number] as Dictionary).has("last_duration") or not (unlocks[lesson_number]["last_duration"] as PackedInt32Array).size() > game_number:
		Log.error("StudentProgression: Cannot log a last_duration for lesson %d, game %d, because it does not exists" % [lesson_number, game_number])
	else:
		unlocks[lesson_number]["last_duration"][game_number] = time_spent
	
	if not (unlocks[lesson_number] as Dictionary).has("total_duration") or not (unlocks[lesson_number]["total_duration"] as PackedInt32Array).size() > game_number:
		Log.error("StudentProgression: Cannot log a total_duration for lesson %d, game %d, because it does not exists" % [lesson_number, game_number])
	else:
		unlocks[lesson_number]["total_duration"][game_number] += time_spent
	
	last_modified = Time.get_datetime_string_from_system(true)
	progression_changed.emit()
