local mod = get_mod("claim_all_penances")
local TAG = "[claim_all_penances]"

-- Cache the active view instance
mod._penances_view = nil

-- Ensure we run A) once per view instance (weak keys prevents leaks)
local ran_for_view = setmetatable({}, { __mode = "k" })

-- Commendation claiming state per view instance (weak keys prevents leaks)
local claim_state_for_view = setmetatable({}, { __mode = "k" })

-- Wintrack claim-all state per view instance (weak keys prevents leaks)
local wintrack_claim_state_for_view = setmetatable({}, { __mode = "k" })

local CLAIM_REFRESH_INTERVAL_S = 0.35
local CLAIM_IDLE_REFRESH_INTERVAL_S = 3.0
local CLAIM_RETRY_INTERVAL_S = 0.35
local CLAIM_TIMEOUT_S = 1.5

mod._auto_close_claim_inflight = false
mod._auto_close_claim_inflight_count = mod._auto_close_claim_inflight_count or 0
mod._pending_auto_close_views = mod._pending_auto_close_views or {}
mod._confirm_spam = mod._confirm_spam or { remaining = 0, next_t = 0, down = false, down_action = nil }
mod._last_penances_input_service = mod._last_penances_input_service or nil

local AUTO_CLOSE_VIEW_NAME_SUBSTRINGS = {
	"reward",
	"rewards",
	"loot",
	"popup",
}

local function _string_contains_any(haystack, needles)
	if type(haystack) ~= "string" then
		return false
	end

	local lower = string.lower(haystack)
	for i = 1, #needles do
		if string.find(lower, needles[i], 1, true) then
			return true
		end
	end

	return false
end

local function _should_auto_dismiss_view(view_name)
	-- Only dismiss popups triggered by our own claim call (keeps this from closing unrelated views).
	if mod._auto_close_claim_inflight ~= true then
		return false
	end

	return _string_contains_any(view_name, AUTO_CLOSE_VIEW_NAME_SUBSTRINGS)
end

local CONFIRM_ACTION_CANDIDATES = {
	"confirm",
	"continue",
	"ui_confirm",
	"next",
}

local function _get_input_service_for_confirm(passed_input_service)
	if type(passed_input_service) == "table" and type(passed_input_service.start_simulate_action) == "function" then
		return passed_input_service
	end

	local cached = mod._last_penances_input_service
	if type(cached) == "table" and type(cached.start_simulate_action) == "function" then
		return cached
	end

	local input_manager = Managers.input
	local services = input_manager and input_manager._input_services
	if type(services) == "table" then
		for _, svc in pairs(services) do
			if type(svc) == "table" and type(svc.start_simulate_action) == "function" then
				return svc
			end
		end
	end

	return nil
end

local function _cancel_confirm_spam()
	local spam = mod._confirm_spam
	if not spam then
		return
	end

	if spam.down and spam.down_action then
		local input_service = _get_input_service_for_confirm(nil)
		if input_service then
			pcall(function()
				input_service:stop_simulate_action(spam.down_action)
			end)
		end
	end

	spam.remaining = 0
	spam.next_t = 0
	spam.down = false
	spam.down_action = nil
end

local function _tick_confirm_spam(passed_input_service, t)
	if mod._auto_close_claim_inflight ~= true then
		return
	end

	local spam = mod._confirm_spam
	if not spam or (spam.remaining or 0) <= 0 then
		return
	end

	local input_service = _get_input_service_for_confirm(passed_input_service)
	if not input_service then
		return
	end

	-- Release after a short hold.
	if spam.down and t >= (spam.next_t or 0) then
		pcall(function()
			input_service:stop_simulate_action(spam.down_action)
		end)
		spam.down = false
		spam.down_action = nil
		spam.next_t = t + 0.06
		spam.remaining = math.max(0, (spam.remaining or 1) - 1)
		return
	end

	-- Press if we're ready and not currently held.
	if (not spam.down) and t >= (spam.next_t or 0) and (spam.remaining or 0) > 0 then
		local action = CONFIRM_ACTION_CANDIDATES[((spam.remaining - 1) % #CONFIRM_ACTION_CANDIDATES) + 1]
		pcall(function()
			input_service:start_simulate_action(action)
		end)
		spam.down = true
		spam.down_action = action
		spam.next_t = t + 0.04
	end
end

local function _auto_close_claim_begin(state)
	if state and state._auto_close_active then
		return
	end

	if state then
		state._auto_close_active = true
	end

	mod._auto_close_claim_inflight_count = (mod._auto_close_claim_inflight_count or 0) + 1
	mod._auto_close_claim_inflight = true
end

local function _auto_close_claim_end(state)
	if state and not state._auto_close_active then
		return
	end

	if state then
		state._auto_close_active = false
	end

	mod._auto_close_claim_inflight_count = math.max(0, (mod._auto_close_claim_inflight_count or 1) - 1)
	mod._auto_close_claim_inflight = (mod._auto_close_claim_inflight_count > 0)
	if mod._auto_close_claim_inflight ~= true then
		_cancel_confirm_spam()
	end
end

local function _tick_auto_close_views()
	local pending = mod._pending_auto_close_views
	if not pending or not next(pending) then
		return
	end

	local ui = Managers.ui
	if not ui then
		return
	end

	for view_name, _ in pairs(pending) do
		pending[view_name] = nil

		if ui:view_active(view_name) and not ui:is_view_closing(view_name) then
			pcall(function()
				ui:close_view(view_name, true)
			end)
		end
	end
end

local function _is_ready_for_wintrack(view)
	return view
		and view._wintrack_initialized == true
		and type(view._wintrack_rewards) == "table"
		and view._wintrack_element
		and type(view._wintrack_element.points) == "function"
end

local function _log_first_n_tiers(view, n)
	local rewards = view._wintrack_rewards
	local current_points = view._wintrack_element:points()

	mod:info(string.format("%s Wintrack points: %d", TAG, current_points))

	local max_n = math.min(n, #rewards)
	for i = 1, max_n do
		local required = rewards[i] and rewards[i].points_required
		local claimable = (type(required) == "number") and (required <= current_points) or false

		mod:info(string.format(
			"%s   Tier %d: required=%s current=%s claimable=%s",
			TAG, i, tostring(required), tostring(current_points), tostring(claimable)
		))
	end

	if #rewards > max_n then
		mod:info(string.format("%s   ... (%d more tiers not shown)", TAG, (#rewards - max_n)))
	end
end

local function _claim_key(entry)
	if type(entry) ~= "table" then
		return nil
	end

	local source_info = entry.sourceInfo
	if type(source_info) == "table" and source_info.sourceIdentifier ~= nil then
		return tostring(source_info.sourceIdentifier)
	end

	if entry.id ~= nil then
		return tostring(entry.id)
	end
	if entry.uuid ~= nil then
		return tostring(entry.uuid)
	end

	return nil
end

local _ITEM_ID_KEYS = {
	item = true,
	items = true,
	item_id = true,
	itemId = true,
	item_instance_id = true,
	itemInstanceId = true,
	item_master_id = true,
	itemMasterId = true,
	item_template = true,
	itemTemplate = true,
	gear_id = true,
	gearId = true,
	master_id = true,
	masterId = true,
}

local function _contains_item_identifiers(value, depth, visited)
	if depth <= 0 or type(value) ~= "table" then
		return false
	end

	visited = visited or {}
	if visited[value] then
		return false
	end
	visited[value] = true

	for k, v in pairs(value) do
		if _ITEM_ID_KEYS[k] and v ~= nil and v ~= false then
			return true
		end

		if type(v) == "table" and _contains_item_identifiers(v, depth - 1, visited) then
			return true
		end
	end

	return false
end

local function _reward_has_item(entry)
	if type(entry) ~= "table" then
		return false
	end

	if type(entry.items) == "table" and #entry.items > 0 then
		return true
	end
	if type(entry.item) == "table" then
		return true
	end

	local rewards = entry.rewards or entry.reward or entry.grants
	if type(rewards) == "table" then
		for _, r in pairs(rewards) do
			if type(r) == "table" then
				local rt = r.type or r.reward_type or r.kind
				if type(rt) == "string" then
					local lrt = string.lower(rt)
					if string.find(lrt, "item", 1, true) or string.find(lrt, "gear", 1, true) then
						return true
					end
				end

				if _contains_item_identifiers(r, 2) then
					return true
				end
			end
		end
	end

	return _contains_item_identifiers(entry, 3)
end

local function _tick_wintrack_claim_all(view, dt, t)
	local state = wintrack_claim_state_for_view[view]
	if not state or state.active ~= true then
		return
	end

	if not _is_ready_for_wintrack(view) then
		return
	end

	-- Don't start a new claim while another claim (commendation or wintrack) is in-flight.
	if mod._auto_close_claim_inflight == true and state.claiming ~= true then
		return
	end

	local el = view._wintrack_element
	local rewards = view._wintrack_rewards
	if not el or type(el.points) ~= "function" or type(rewards) ~= "table" then
		return
	end

	local current_points = el:points()
	local next_ui = (type(el._next_reward_to_claim) == "number") and el._next_reward_to_claim
		or (type(view._rewarded_tiers) == "number" and (view._rewarded_tiers + 1))
		or nil

	-- Resolve an in-flight claim if the pointer advanced.
	if state.claiming then
		if type(next_ui) == "number" and state.claim_tier and next_ui ~= state.claim_tier then
			state.claiming = false
			state.claim_tier = nil
			_auto_close_claim_end(state)
			state.next_try_t = t + 0.1
		elseif t - (state.claim_started_t or 0) > (state.timeout_s or 6.0) then
			state.claiming = false
			state.claim_tier = nil
			_auto_close_claim_end(state)
			state.next_try_t = t + CLAIM_RETRY_INTERVAL_S
		end
	end

	-- Stop if we can't determine the next tier, or it's not claimable.
	if type(next_ui) ~= "number" or next_ui < 1 or next_ui > #rewards then
		state.active = false
		if state.claiming then
			state.claiming = false
			_auto_close_claim_end(state)
		end
		mod:info("%s Wintrack claim-all stopped (no next tier).", TAG)
		return
	end

	local required = rewards[next_ui] and rewards[next_ui].points_required
	if type(required) ~= "number" or required > current_points then
		state.active = false
		if state.claiming then
			state.claiming = false
			_auto_close_claim_end(state)
		end
		mod:info("%s Wintrack claim-all finished (no claimable tiers).", TAG)
		return
	end

	if state.claiming then
		return
	end

	if t < (state.next_try_t or 0) then
		return
	end

	if type(view._claim_wintrack_reward) ~= "function" then
		state.active = false
		mod:warning("%s view._claim_wintrack_reward not available.", TAG)
		return
	end

	state.claiming = true
	state.claim_tier = next_ui
	state.claim_started_t = t
	state.next_try_t = t + CLAIM_RETRY_INTERVAL_S
	_auto_close_claim_begin(state)
	mod._confirm_spam.remaining = math.max(mod._confirm_spam.remaining or 0, 12)
	mod._confirm_spam.next_t = t

	mod:info("%s Claiming wintrack tier (queued): ui=%d required=%d current=%d", TAG, next_ui, required, current_points)
	view:_claim_wintrack_reward(next_ui)
end

local function _refresh_commendation_queue(view, state)
	local player_rewards = Managers.backend and Managers.backend.interfaces and Managers.backend.interfaces.player_rewards
	if not player_rewards then
		mod:warning("%s player_rewards backend interface not available.", TAG)
		return
	end
	if type(view._claim_penance) ~= "function" then
		mod:warning("%s view._claim_penance not available.", TAG)
		return
	end

	if state.refreshing then
		return
	end

	state.refreshing = true
	player_rewards:get_rewards_by_source_paged(100, "commendation"):next(function(data)
		state.refreshing = false

		local items = (data and data.items) or {}

		state.queue_no_item = {}
		state.queue_with_item = {}
		for i = 1, #items do
			local entry = items[i]
			local key = _claim_key(entry)
			if key then
				if _reward_has_item(entry) then
					state.queue_with_item[#state.queue_with_item + 1] = entry
				else
					state.queue_no_item[#state.queue_no_item + 1] = entry
				end
			end
		end
	end)
end

local function _queues_contain_claim(state, claim_key)
	if not claim_key or type(state) ~= "table" then
		return false
	end

	local q1 = state.queue_no_item
	if type(q1) == "table" then
		for i = 1, #q1 do
			if _claim_key(q1[i]) == claim_key then
				return true
			end
		end
	end

	local q2 = state.queue_with_item
	if type(q2) == "table" then
		for i = 1, #q2 do
			if _claim_key(q2[i]) == claim_key then
				return true
			end
		end
	end

	return false
end

local function _tick_commendation_claims(view, dt, t)
	local state = claim_state_for_view[view]
	if not state then
		state = {
			queue = nil,
			refreshing = false,
			next_refresh_t = 0,

			claiming = false,
			claim_key = nil,
			claim_started_t = 0,
			next_claim_try_t = 0,

			_last_logged_total = nil,
			_last_logged_no_item = nil,
			_last_logged_with_item = nil,
			_last_logged_claim_key = nil,
			_auto_close_active = false,
		}
		claim_state_for_view[view] = state
	end

	if type(view._claim_penance) ~= "function" then
		return
	end

	-- Refresh queue periodically (less often when idle).
	if t >= (state.next_refresh_t or 0) then
		_refresh_commendation_queue(view, state)
		local no_item = (type(state.queue_no_item) == "table") and #state.queue_no_item or 0
		local with_item = (type(state.queue_with_item) == "table") and #state.queue_with_item or 0
		local total = no_item + with_item
		state.next_refresh_t = t + ((total > 0) and CLAIM_REFRESH_INTERVAL_S or CLAIM_IDLE_REFRESH_INTERVAL_S)
	end

	-- If we attempted a claim, keep refreshing until it disappears (or time out and retry).
	if state.claiming then
		if t - (state.claim_started_t or 0) > CLAIM_TIMEOUT_S then
			state.claiming = false
			state.claim_key = nil
			_auto_close_claim_end(state)
			state.next_claim_try_t = t + CLAIM_RETRY_INTERVAL_S
		elseif state.claim_key and not _queues_contain_claim(state, state.claim_key) then
			state.claiming = false
			state.claim_key = nil
			_auto_close_claim_end(state)
			state.next_claim_try_t = t + 0.1
		end
	end

	-- Log queue size changes (helps confirm progress without spamming per-entry logs).
	local no_item = (type(state.queue_no_item) == "table") and #state.queue_no_item or 0
	local with_item = (type(state.queue_with_item) == "table") and #state.queue_with_item or 0
	local total = no_item + with_item
	if state.queue_no_item or state.queue_with_item then
		if state._last_logged_total ~= total or state._last_logged_no_item ~= no_item or state._last_logged_with_item ~= with_item then
			state._last_logged_total = total
			state._last_logged_no_item = no_item
			state._last_logged_with_item = with_item
			mod:info("%s Commendation rewards remaining: %d (no_item=%d item=%d)", TAG, total, no_item, with_item)
		end
	end

	-- Attempt next claim (one at a time); item rewards may open a popup and block further claims until dismissed.
	if total > 0 and not state.claiming and mod._auto_close_claim_inflight ~= true and t >= (state.next_claim_try_t or 0) then
		local active_queue = (no_item > 0) and state.queue_no_item or state.queue_with_item
		local entry = active_queue and active_queue[1]
		local key = _claim_key(entry)

		if not key then
			table.remove(active_queue, 1)
			state.next_claim_try_t = t + 0.1
			return
		end

		state.claiming = true
		state.claim_key = key
		state.claim_started_t = t
		state.next_claim_try_t = t + CLAIM_RETRY_INTERVAL_S
		state.next_refresh_t = math.min(state.next_refresh_t or (t + CLAIM_REFRESH_INTERVAL_S), t + 0.25)
		_auto_close_claim_begin(state)
		mod._confirm_spam.remaining = math.max(mod._confirm_spam.remaining or 0, 12)
		mod._confirm_spam.next_t = t

		if state._last_logged_claim_key ~= key then
			state._last_logged_claim_key = key
			mod:info("%s Claiming penance (queued): %s (has_item=%s)", TAG, key, tostring(_reward_has_item(entry)))
		end
		view:_claim_penance(entry)
	end
end

function mod.list_commendation_rewards()
	local player_rewards = Managers.backend and Managers.backend.interfaces and Managers.backend.interfaces.player_rewards
	if not player_rewards then
		mod:warning("%s player_rewards backend interface not available.", TAG)
		return
	end

	player_rewards:get_rewards_by_source_paged(100, "commendation"):next(function(data)
		local items = (data and data.items) or {}
		mod:info("%s Commendation reward entries: %d", TAG, #items)

		for i = 1, #items do
			local entry = items[i]
			local key = _claim_key(entry)
			mod:info("%s   [%d] %s has_item=%s", TAG, i, tostring(key), tostring(_reward_has_item(entry)))
		end
	end)
end

function mod.dump_commendation_reward(index, depth)
	local idx = tonumber(index) or 1
	local max_depth = tonumber(depth) or 3

	local player_rewards = Managers.backend and Managers.backend.interfaces and Managers.backend.interfaces.player_rewards
	if not player_rewards then
		mod:warning("%s player_rewards backend interface not available.", TAG)
		return
	end

	player_rewards:get_rewards_by_source_paged(100, "commendation"):next(function(data)
		local items = (data and data.items) or {}
		local entry = items[idx]
		if not entry then
			mod:warning("%s No commendation reward entry at index %d (1..%d).", TAG, idx, #items)
			return
		end

		local key = _claim_key(entry)
		mod:info("%s Dumping commendation reward [%d] %s (has_item=%s) depth=%d", TAG, idx, tostring(key), tostring(_reward_has_item(entry)), max_depth)

		if mod.dump then
			mod:dump(entry, "commendation_reward_" .. tostring(idx), max_depth)
		else
			mod:warning("%s mod:dump not available.", TAG)
		end
	end)
end

-- Attempt to auto-dismiss reward popups opened by claim calls.
if mod.hook_safe then
	mod:hook_safe(CLASS.UIManager, "open_view", function(self, view_name, ...)
		if not view_name then
			return
		end

		if _should_auto_dismiss_view(view_name) then
			mod._pending_auto_close_views[tostring(view_name)] = true
		end
		if mod._auto_close_claim_inflight == true then
			mod._confirm_spam.remaining = math.max(mod._confirm_spam.remaining or 0, 12)
			mod._confirm_spam.next_t = 0
		end
	end)

	mod:hook_safe(CLASS.UIManager, "update", function(self, ...)
		_tick_auto_close_views()
	end)
end

-- A) Cache view on enter (no claiming here)
mod:hook(CLASS.PenanceOverviewView, "on_enter", function(func, self, ...)
	func(self, ...)
	mod._penances_view = self
	claim_state_for_view[self] = nil
	wintrack_claim_state_for_view[self] = nil
	mod:info("%s PenanceOverviewView entered.", TAG)
end)

-- A) Clear cached view on exit (prevents keybinds acting outside the menu)
mod:hook(CLASS.PenanceOverviewView, "on_exit", function(func, self, ...)
	func(self, ...)
	if mod._penances_view == self then
		mod._penances_view = nil
	end

	local cs = claim_state_for_view[self]
	if cs and cs._auto_close_active then
		_auto_close_claim_end(cs)
	end
	local ws = wintrack_claim_state_for_view[self]
	if ws and ws._auto_close_active then
		_auto_close_claim_end(ws)
	end

	claim_state_for_view[self] = nil
	wintrack_claim_state_for_view[self] = nil
end)

-- A) Run once per view instance after wintrack is initialized
mod:hook(CLASS.PenanceOverviewView, "update", function(func, self, dt, t, input_service, ...)
	func(self, dt, t, input_service, ...)

	if mod._penances_view == self then
		mod._last_penances_input_service = input_service or mod._last_penances_input_service
		_tick_commendation_claims(self, dt, t)
		_tick_wintrack_claim_all(self, dt, t)
		_tick_confirm_spam(input_service, t)
	end

	if ran_for_view[self] then
		return
	end

	if _is_ready_for_wintrack(self) then
		ran_for_view[self] = true

		local n = (mod.get and mod:get("log_tier_count")) or 20
		if type(n) == "number" and n > 0 then
			_log_first_n_tiers(self, n)
		end
	end
end)

-- C) Debug dump: show the state fields you discovered
function mod.dump_wintrack_state()
	local view = mod._penances_view
	if not view then
		mod:warning("%s No cached PenanceOverviewView. Open Penances menu first.", TAG)
		return
	end

	mod:info("%s ---- Dumping likely wintrack state fields ----", TAG)

	mod:info("%s view._wintrack_initialized = %s", TAG, tostring(view._wintrack_initialized))
	mod:info("%s view._rewarded_tiers = %s", TAG, tostring(view._rewarded_tiers))
	mod:info("%s view._track_data = %s", TAG, tostring(view._track_data))

	local el = view._wintrack_element
	if type(el) == "table" then
		mod:info("%s element._next_reward_to_claim = %s", TAG, tostring(el._next_reward_to_claim))
		mod:info("%s element._can_claim_rewards = %s", TAG, tostring(el._can_claim_rewards))
	end
end

function mod.claim_all_available_wintrack_tiers()
    local view = mod._penances_view
    if not view then
        return -- not in penances menu
    end
    if view._wintrack_initialized ~= true then
        return -- penances menu open, but not ready yet
    end

	local state = wintrack_claim_state_for_view[view] or {
		active = false,
		claiming = false,
		claim_tier = nil,
		claim_started_t = 0,
		next_try_t = 0,
		timeout_s = 3.0,
		_auto_close_active = false,
	}
	wintrack_claim_state_for_view[view] = state

	state.active = true
	state.claiming = false
	state.claim_tier = nil
	state.claim_started_t = 0
	state.next_try_t = 0

	mod:info("%s Wintrack claim-all started (queued).", TAG)
end


-- Chat commands
if mod.command then
	mod:command("dump_wintrack", "Dump wintrack state fields.", function()
		mod.dump_wintrack_state()
	end)

    mod:command("claim_all_tiers", "Claim all currently claimable wintrack tiers.", function()
        mod.claim_all_available_wintrack_tiers()
    end)

	mod:command("list_commendations", "List commendation reward entries with best-effort item detection.", function()
		mod.list_commendation_rewards()
	end)

	mod:command("dump_commendation", "Dump a commendation reward entry: /dump_commendation [index] [depth]", function(index, depth)
		mod.dump_commendation_reward(index, depth)
	end)
end
