extends Node

signal prestige_state_changed(total_prestige: BigNumber, pending_gain: BigNumber)
signal prestige_performed(gain: BigNumber, total_prestige: BigNumber)
signal prestige_threshold_changed(next_threshold: BigNumber)

const SAVE_KEY: String = "prestige_state"
const TOTAL_PRESTIGE_KEY: String = "total_prestige_earned"
const CURRENT_PRESTIGE_UNSPENT_KEY: String = "current_prestige_unspent"
const PRESTIGE_RESETS_COUNT_KEY: String = "prestige_resets_count"
const RUN_START_TOTAL_SOURCE_KEY: String = "run_start_total_source_currency"
const RUN_PEAK_SOURCE_KEY: String = "run_peak_source_currency"
const LAST_RESET_TIME_KEY: String = "last_reset_time"

const BASIS_LIFETIME_TOTAL: int = 0
const BASIS_RUN_TOTAL: int = 1
const BASIS_RUN_MAX: int = 2

const FORMULA_POWER: int = 0
const FORMULA_TRIANGULAR_INVERSE: int = 1

const MULTIPLIER_MODE_ADDITIVE: int = 0
const MULTIPLIER_MODE_MULTIPLICATIVE_POWER: int = 1

@export var config: Resource

var total_prestige_earned: BigNumber = BigNumber.from_float(0.0)
var current_prestige_unspent: BigNumber = BigNumber.from_float(0.0)
var prestige_resets_count: int = 0
var run_start_total_source_currency: BigNumber = BigNumber.from_float(0.0)
var run_peak_source_currency: BigNumber = BigNumber.from_float(0.0)
var last_reset_time: int = 0

func _ready() -> void:
	if config == null:
		config = load("res://docs/gyms/prestige/primary_prestige.tres") as Resource

	if not _is_config_valid():
		push_warning("PrestigeManager has invalid or missing config; prestige is disabled.")
		return

	var loaded_state: Dictionary = GameState.get_external_save_data(SAVE_KEY)
	if loaded_state.is_empty():
		_initialize_run_tracking_from_current_state()
	else:
		_deserialize_state(loaded_state)
		_validate_loaded_run_tracking()
		_save_state_to_game_state()

	GameState.currency_changed.connect(_on_currency_changed)
	prestige_state_changed.emit(get_total_prestige(), calculate_pending_gain())
	prestige_threshold_changed.emit(get_next_prestige_threshold())

func get_config() -> Resource:
	return config

func get_source_currency_id() -> StringName:
	if not _is_config_valid():
		return &""

	return _normalize_currency_id(_get_config_string_name("source_currency_id", &""))

func get_total_prestige() -> BigNumber:
	return _copy_big_number(total_prestige_earned)

func get_current_prestige_unspent() -> BigNumber:
	return _copy_big_number(current_prestige_unspent)

func get_total_multiplier() -> float:
	if not _is_config_valid():
		return 1.0

	var base: float = maxf(_get_config_float("base_multiplier", 1.0), 0.0)
	var prestige_value: float = _big_number_to_float(total_prestige_earned)
	if prestige_value <= 0.0:
		return base

	match _get_config_int("multiplier_mode", MULTIPLIER_MODE_ADDITIVE):
		MULTIPLIER_MODE_MULTIPLICATIVE_POWER:
			var growth_base: float = 1.0 + maxf(_get_config_float("multiplier_per_prestige", 0.0), 0.0)
			if growth_base <= 0.0:
				return base

			var effective_power: float = pow(prestige_value, maxf(_get_config_float("multiplier_exponent", 1.0), 0.0))
			var mult: float = base * pow(growth_base, effective_power)
			if not is_finite(mult):
				return INF
			return maxf(mult, 0.0)
		_:
			return maxf(base + prestige_value * maxf(_get_config_float("multiplier_per_prestige", 0.0), 0.0), 0.0)

func calculate_pending_gain() -> BigNumber:
	if not _is_config_valid():
		return BigNumber.from_float(0.0)

	var basis_value: BigNumber = _get_basis_value()
	if basis_value.mantissa <= 0.0:
		return BigNumber.from_float(0.0)

	var raw_gain: float = _calculate_raw_gain_float(basis_value)
	if not is_finite(raw_gain):
		raw_gain = 0.0

	raw_gain = maxf(raw_gain, 0.0)
	var rounded_gain: float = _round_gain(raw_gain)
	if not is_finite(rounded_gain):
		rounded_gain = 0.0

	rounded_gain = maxf(rounded_gain, 0.0)
	if rounded_gain > 0.0:
		rounded_gain = maxf(rounded_gain, float(maxi(_get_config_int("minimum_gain", 0), 0)))

	return BigNumber.from_float(rounded_gain)

func can_prestige() -> bool:
	if not _is_config_valid():
		return false

	var pending: BigNumber = calculate_pending_gain()
	if pending.mantissa > 0.0:
		return true

	return _get_config_bool("allow_prestige_without_gain", false)

func perform_prestige() -> bool:
	if not can_prestige():
		return false

	var gain: BigNumber = calculate_pending_gain()
	if gain.mantissa > 0.0:
		total_prestige_earned.add_in_place(gain)
		current_prestige_unspent.add_in_place(gain)

	prestige_resets_count += 1
	last_reset_time = Time.get_unix_time_from_system()

	GameState.reset_for_prestige(true, false)
	_reset_all_buff_levels()
	_reinitialize_generators_after_prestige_reset()
	GameState.emit_currency_changed_for_all()
	_initialize_run_tracking_from_current_state()
	_save_state_to_game_state()
	GameState.save_game()

	var total: BigNumber = get_total_prestige()
	var next_threshold: BigNumber = get_next_prestige_threshold()
	prestige_performed.emit(gain, total)
	prestige_state_changed.emit(total, calculate_pending_gain())
	prestige_threshold_changed.emit(next_threshold)
	return true

func get_next_prestige_threshold() -> BigNumber:
	if not _is_config_valid():
		return BigNumber.from_float(0.0)
	
	match _get_config_int("basis", BASIS_LIFETIME_TOTAL):
		BASIS_RUN_TOTAL:
			return _get_config_threshold()
		_:
			var current_total: float = _big_number_to_float(total_prestige_earned)
			var next_target: float = _calculate_target_for_prestige(current_total + 1.0)
			return BigNumber.from_float(next_target)

func get_basis_value_for_display() -> BigNumber:
	return _get_basis_value()

func get_basis_label() -> String:
	if not _is_config_valid():
		return "-"

	match _get_config_int("basis", BASIS_LIFETIME_TOTAL):
		BASIS_RUN_TOTAL:
			return "Run Total"
		BASIS_RUN_MAX:
			return "Run Max"
		_:
			return "Lifetime Total"

func _on_currency_changed(changed_currency_id: StringName, new_amount: BigNumber) -> void:
	if config == null:
		return

	var source_currency_id: StringName = get_source_currency_id()
	if source_currency_id == &"":
		return
	if changed_currency_id != source_currency_id:
		return

	if new_amount.is_greater_than(run_peak_source_currency):
		run_peak_source_currency = _copy_big_number(new_amount)

	_save_state_to_game_state()
	var next_threshold: BigNumber = get_next_prestige_threshold()
	prestige_state_changed.emit(get_total_prestige(), calculate_pending_gain())
	prestige_threshold_changed.emit(next_threshold)

func _calculate_target_for_prestige(prestige_level: float) -> float:
	if config == null:
		return 0.0
	
	var threshold_value: float = _big_number_to_float(_get_config_threshold())
	if threshold_value <= 0.0:
		return 0.0
	
	match _get_config_int("formula", FORMULA_POWER):
		FORMULA_TRIANGULAR_INVERSE:
			var target_total: float = prestige_level
			var k: float = target_total * (target_total + 1.0) * 0.5
			return k * threshold_value
		_:
			var scale: float = maxf(_get_config_float("scale", 1.0), 0.0001)
			var exponent: float = maxf(_get_config_float("exponent", 1.0), 0.0001)
			var flat_bonus: float = _get_config_float("flat_bonus", 0.0)
			if scale <= 0.0:
				scale = 0.0001
			if exponent <= 0.0:
				exponent = 0.0001
			
			var ratio: float = pow((prestige_level - flat_bonus) / scale, 1.0 / exponent)
			if ratio <= 0.0:
				return threshold_value
			return ratio * threshold_value

func _calculate_raw_gain_float(basis_value: BigNumber) -> float:
	if config == null:
		return 0.0

	match _get_config_int("formula", FORMULA_POWER):
		FORMULA_TRIANGULAR_INVERSE:
			var threshold_value: float = _big_number_to_float(_get_config_threshold())
			if threshold_value <= 0.0:
				return 0.0

			var basis_float: float = _big_number_to_float(basis_value)
			if basis_float <= 0.0:
				return 0.0

			var k: float = basis_float / threshold_value
			if k <= 0.0:
				return 0.0

			var target_total: float = (sqrt(1.0 + 8.0 * k) - 1.0) * 0.5
			if _is_cumulative_basis():
				return target_total - _big_number_to_float(total_prestige_earned)
			return target_total
		_:
			var ratio: float = _big_number_ratio_to_float(basis_value, _get_config_threshold())
			if ratio <= 0.0:
				return 0.0

			var value: float = maxf(_get_config_float("scale", 0.0), 0.0) * pow(ratio, maxf(_get_config_float("exponent", 0.0001), 0.0001)) + _get_config_float("flat_bonus", 0.0)
			if _is_cumulative_basis():
				return value - _big_number_to_float(total_prestige_earned)
			return value

func _get_basis_value() -> BigNumber:
	if config == null:
		return BigNumber.from_float(0.0)

	var source_currency_id: StringName = get_source_currency_id()
	if source_currency_id == &"":
		return BigNumber.from_float(0.0)

	match _get_config_int("basis", BASIS_LIFETIME_TOTAL):
		BASIS_RUN_TOTAL:
			var lifetime_now: BigNumber = GameState.get_total_currency_acquired_by_id(source_currency_id)
			var run_total: BigNumber = lifetime_now.subtract(run_start_total_source_currency)
			if run_total.mantissa < 0.0:
				return BigNumber.from_float(0.0)
			return run_total
		BASIS_RUN_MAX:
			return _copy_big_number(run_peak_source_currency)
		_:
			return _copy_big_number(GameState.get_total_currency_acquired_by_id(source_currency_id))

func _initialize_run_tracking_from_current_state() -> void:
	if config == null:
		return

	var source_currency_id: StringName = get_source_currency_id()
	if source_currency_id == &"":
		run_start_total_source_currency = BigNumber.from_float(0.0)
		run_peak_source_currency = BigNumber.from_float(0.0)
		return

	run_start_total_source_currency = _copy_big_number(GameState.get_total_currency_acquired_by_id(source_currency_id))
	run_peak_source_currency = _copy_big_number(GameState.get_currency_amount_by_id(source_currency_id))
	_save_state_to_game_state()

func _validate_loaded_run_tracking() -> void:
	if config == null:
		return

	var source_currency_id: StringName = get_source_currency_id()
	if source_currency_id == &"":
		run_start_total_source_currency = BigNumber.from_float(0.0)
		run_peak_source_currency = BigNumber.from_float(0.0)
		return

	var current_currency: BigNumber = GameState.get_currency_amount_by_id(source_currency_id)
	if current_currency.is_greater_than(run_peak_source_currency):
		run_peak_source_currency = _copy_big_number(current_currency)

	var lifetime_now: BigNumber = GameState.get_total_currency_acquired_by_id(source_currency_id)
	if run_start_total_source_currency.is_greater_than(lifetime_now):
		run_start_total_source_currency = _copy_big_number(lifetime_now)

func _serialize_state() -> Dictionary:
	return {
		TOTAL_PRESTIGE_KEY: total_prestige_earned.serialize(),
		CURRENT_PRESTIGE_UNSPENT_KEY: current_prestige_unspent.serialize(),
		PRESTIGE_RESETS_COUNT_KEY: maxi(prestige_resets_count, 0),
		RUN_START_TOTAL_SOURCE_KEY: run_start_total_source_currency.serialize(),
		RUN_PEAK_SOURCE_KEY: run_peak_source_currency.serialize(),
		LAST_RESET_TIME_KEY: maxi(last_reset_time, 0),
	}

func _deserialize_state(raw_state: Dictionary) -> void:
	total_prestige_earned = _deserialize_big_number(raw_state.get(TOTAL_PRESTIGE_KEY, {}))
	current_prestige_unspent = _deserialize_big_number(raw_state.get(CURRENT_PRESTIGE_UNSPENT_KEY, {}))
	prestige_resets_count = _to_non_negative_int(raw_state.get(PRESTIGE_RESETS_COUNT_KEY, 0), 0)
	run_start_total_source_currency = _deserialize_big_number(raw_state.get(RUN_START_TOTAL_SOURCE_KEY, {}))
	run_peak_source_currency = _deserialize_big_number(raw_state.get(RUN_PEAK_SOURCE_KEY, {}))
	last_reset_time = _to_non_negative_int(raw_state.get(LAST_RESET_TIME_KEY, 0), 0)

	if current_prestige_unspent.is_greater_than(total_prestige_earned):
		current_prestige_unspent = _copy_big_number(total_prestige_earned)

func _save_state_to_game_state() -> void:
	GameState.set_external_save_data(SAVE_KEY, _serialize_state())

func _reset_all_buff_levels() -> void:
	for buff_id in GameState._buff_levels.keys():
		GameState.set_buff_level(buff_id, 0)
		GameState.set_buff_unlocked(buff_id, false)

func _reinitialize_generators_after_prestige_reset() -> void:
	var scene_root: Node = get_tree().current_scene
	if scene_root == null:
		return

	var generator_nodes: Array[Node] = scene_root.find_children("*", "CurrencyGenerator", true, false)
	for generator_node in generator_nodes:
		if generator_node == null:
			continue
		if not generator_node.has_method("reset_runtime_state_for_prestige"):
			continue

		generator_node.call("reset_runtime_state_for_prestige")

func _deserialize_big_number(raw_value: Variant) -> BigNumber:
	if raw_value is Dictionary:
		return BigNumber.deserialize(raw_value)
	return BigNumber.from_float(0.0)

func _copy_big_number(value: BigNumber) -> BigNumber:
	return BigNumber.new(value.mantissa, value.exponent)

func _big_number_ratio_to_float(a: BigNumber, b: BigNumber) -> float:
	if b.mantissa <= 0.0:
		return 0.0
	if a.mantissa <= 0.0:
		return 0.0

	var exponent_delta: int = a.exponent - b.exponent
	if exponent_delta > 308:
		return INF
	if exponent_delta < -308:
		return 0.0

	return (a.mantissa / b.mantissa) * pow(10.0, float(exponent_delta))

func _big_number_to_float(value: BigNumber) -> float:
	if value.mantissa <= 0.0:
		return 0.0
	if value.exponent > 308:
		return INF
	if value.exponent < -308:
		return 0.0
	return value.mantissa * pow(10.0, float(value.exponent))

func _normalize_currency_id(currency_id: StringName) -> StringName:
	var normalized: String = String(currency_id).to_lower().strip_edges()
	if normalized.is_empty():
		return &""
	return StringName(normalized)

func _to_non_negative_int(value: Variant, fallback: int = 0) -> int:
	if value is int:
		return maxi(value, 0)
	if value is float:
		return maxi(int(value), 0)
	return maxi(fallback, 0)

func _is_config_valid() -> bool:
	if config == null:
		return false
	if not config.has_method("is_valid"):
		return false

	return bool(config.call("is_valid"))

func _is_cumulative_basis() -> bool:
	if config != null and config.has_method("is_cumulative_basis"):
		return bool(config.call("is_cumulative_basis"))

	return _get_config_int("basis", BASIS_LIFETIME_TOTAL) != BASIS_RUN_TOTAL

func _round_gain(value: float) -> float:
	if config != null and config.has_method("round_value"):
		var rounded: Variant = config.call("round_value", value)
		if rounded is float:
			return rounded
		if rounded is int:
			return float(rounded)

	return floorf(value)

func _get_config_threshold() -> BigNumber:
	if config != null and config.has_method("get_threshold"):
		var threshold: Variant = config.call("get_threshold")
		if threshold is BigNumber:
			return threshold

	return BigNumber.from_float(1.0)

func _get_config_string_name(property_name: String, fallback: StringName = &"") -> StringName:
	if config == null:
		return fallback

	var value: Variant = config.get(property_name)
	if value is StringName:
		return value
	if value is String:
		return StringName(String(value))

	return fallback

func _get_config_int(property_name: String, fallback: int = 0) -> int:
	if config == null:
		return fallback

	var value: Variant = config.get(property_name)
	if value is int:
		return value
	if value is float:
		return int(value)

	return fallback

func _get_config_float(property_name: String, fallback: float = 0.0) -> float:
	if config == null:
		return fallback

	var value: Variant = config.get(property_name)
	if value is float:
		return value
	if value is int:
		return float(value)

	return fallback

func _get_config_bool(property_name: String, fallback: bool = false) -> bool:
	if config == null:
		return fallback

	var value: Variant = config.get(property_name)
	if value is bool:
		return value

	return fallback
