local _, addonTable = ...

local EventHandlers = {}

-- Upvalues
local R = Rarity
local CONSTANTS = addonTable.constants

-- Locals
local coinamounts = {}
local initialBagSyncTimer = nil

-- Externals
local L = LibStub("AceLocale-3.0"):GetLocale("Rarity")
local lbz = LibStub("LibBabble-Zone-3.0"):GetUnstrictLookupTable()
local lbsz = LibStub("LibBabble-SubZone-3.0"):GetUnstrictLookupTable()
local lbb = LibStub("LibBabble-Boss-3.0"):GetUnstrictLookupTable()

-- Lua APIs
local bit_band = _G.bit.band
local strlower = _G.strlower
local format = _G.format

-- WOW APIs
local GetCurrencyInfo = _G.C_CurrencyInfo.GetCurrencyInfo
local CombatLogGetCurrentEventInfo = _G.CombatLogGetCurrentEventInfo
local LoadAddOn = _G.C_AddOns.LoadAddOn
local GetBestMapForUnit = _G.C_Map.GetBestMapForUnit
local GetMapInfo = _G.C_Map.GetMapInfo
local IsQuestFlaggedCompleted = C_QuestLog.IsQuestFlaggedCompleted
local UnitCanAttack = _G.UnitCanAttack
local UnitIsPlayer = _G.UnitIsPlayer
local UnitIsDead = _G.UnitIsDead
local GetNumLootItems = _G.GetNumLootItems
local GetLootSlotInfo = _G.GetLootSlotInfo
local GetLootSlotLink = _G.GetLootSlotLink
local GetItemInfo_Blizzard = _G.C_Item.GetItemInfo
local GetItemInfo = function(id)
	return R:GetItemInfo(id)
end
local GetRealZoneText = _G.GetRealZoneText
local GetContainerNumSlots = _G.C_Container.GetContainerNumSlots
local GetContainerItemID = _G.C_Container.GetContainerItemID
local GetContainerItemInfo = _G.C_Container.GetContainerItemInfo
local GetNumArchaeologyRaces = _G.GetNumArchaeologyRaces or function()
	return 0
end
local GetArchaeologyRaceInfo = _G.GetArchaeologyRaceInfo
local GetStatistic = _G.GetStatistic
local GetLootSourceInfo = _G.GetLootSourceInfo
local C_Timer = _G.C_Timer
local IsSpellKnown = _G.IsSpellKnown
local GetCurrentRenownLevel = C_MajorFactions and C_MajorFactions.GetCurrentRenownLevel

-- Addon APIs
local DebugCache = Rarity.Utils.DebugCache

function EventHandlers:Register()
	self = Rarity
	local WOW_INTERFACE_VER = select(4, GetBuildInfo())

	self:UnregisterAllEvents()
	self:RegisterBucketEvent("BAG_UPDATE", 0.5, "OnBagUpdate")
	self:RegisterEvent("LOOT_READY", "OnLootReady")
	self:RegisterEvent("CURRENCY_DISPLAY_UPDATE", "OnCurrencyUpdate")
	self:RegisterEvent("RESEARCH_ARTIFACT_COMPLETE", "OnResearchArtifactComplete")
	-- self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED", "OnCombat") -- Used to detect boss kills that we didn't solo
	self:RegisterEvent("CURSOR_CHANGED", "OnCursorChanged") -- Fishing detection
	self:RegisterEvent("UNIT_SPELLCAST_SENT", "OnSpellcastSent") -- Fishing detection
	self:RegisterEvent("UNIT_SPELLCAST_STOP", "OnSpellcastStopped") -- Fishing detection
	self:RegisterEvent("UNIT_SPELLCAST_FAILED", "OnSpellcastFailed") -- Fishing detection
	self:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED", "OnSpellcastFailed") -- Fishing detection
	self:RegisterEvent("LOOT_CLOSED", "OnLootFrameClosed") -- Fishing detection
	self:RegisterEvent("PLAYER_LOGOUT", "OnEvent")
	self:RegisterEvent("TRADE_SKILL_SHOW", "OnEvent")
	self:RegisterEvent("TRADE_SKILL_CLOSE", "OnEvent")
	self:RegisterEvent("UPDATE_MOUSEOVER_UNIT", "OnMouseOver")
	self:RegisterEvent("CRITERIA_COMPLETE", "OnCriteriaComplete")
	self:RegisterEvent("ENCOUNTER_END", "OnEncounterEnd")
	self:RegisterEvent("PLAYER_REGEN_ENABLED", "OnCombatEnded")
	self:RegisterEvent("PET_BATTLE_OPENING_START", "OnPetBattleStart")
	self:RegisterEvent("PET_BATTLE_CLOSE", "OnPetBattleEnd")
	self:RegisterEvent("ISLAND_COMPLETED", "OnIslandCompleted")
	self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED", "OnSpellcastSucceeded")
	self:RegisterEvent("QUEST_TURNED_IN", "OnQuestTurnedIn")

	if LE_EXPANSION_LEVEL_CURRENT >= LE_EXPANSION_MISTS_OF_PANDARIA then
		self:RegisterEvent("SHOW_LOOT_TOAST", "OnShowLootToast")
	end

	self:RegisterBucketEvent("UPDATE_INSTANCE_INFO", 1, "OnEvent")
	self:RegisterBucketEvent("LFG_UPDATE_RANDOM_INFO", 1, "OnEvent")
	self:RegisterBucketEvent("CALENDAR_UPDATE_EVENT_LIST", 1, "OnEvent")
	self:RegisterBucketEvent("TOYS_UPDATED", 1, "OnEvent")
	self:RegisterBucketEvent("COMPANION_UPDATE", 1, "OnEvent")

	self:RegisterEvent("PLAYER_INTERACTION_MANAGER_FRAME_SHOW", "OnPlayerInteractionFrameShow")
	self:RegisterEvent("PLAYER_INTERACTION_MANAGER_FRAME_HIDE", "OnPlayerInteractionFrameHide")
end

function R:StartInitialBagSync(delay)
	Rarity.isInitialBagSync = true

	if initialBagSyncTimer then
		self:CancelTimer(initialBagSyncTimer, true)
	end

	initialBagSyncTimer = self:ScheduleTimer(function()
		self:ScanBags()
		self:BackUpInventoryItemAmounts()
		Rarity.isInitialBagSync = false
		initialBagSyncTimer = nil
	end, delay or 10)
end

-- TODO: Move elsewhere/refactor
local function addAttemptForItem(itemName, categoryName)
	if not itemName or not categoryName then
		return
	end

	local Rarity = Rarity

	local group = Rarity.db.profile.groups[categoryName]
	if not group then
		return
	end

	local item = group[itemName]
	if not item then
		return
	end

	if item and type(item) == "table" and item.enabled ~= false and Rarity:IsAttemptAllowed(item) then -- Add one attempt for this item
		if item.attempts == nil then
			item.attempts = 1
		else
			item.attempts = item.attempts + 1
		end
		Rarity:OutputAttempts(item)
	end
end

local function table_contains(haystack, needle)
	if type(haystack) ~= "table" then
		return
	end

	for _, value in pairs(haystack) do
		if value == needle then
			return true
		end
	end
end

local function IsPlayerInHorrificVision()
	return (GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.HORRIFIC_VISION_OF_STORMWIND)
		or (GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.HORRIFIC_VISION_OF_ORGRIMMAR)
		or (GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.HORRIFIC_REVISION_OF_STORMWIND)
		or (GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.HORRIFIC_REVISION_OF_ORGRIMMAR)
end

function R:OnSpellcastSucceeded(event, unitID, castGUID, spellID)
	if unitID ~= "player" then
		return
	end

	if not Rarity.relevantSpells[spellID] then
		return
	end

	R:Debug("OnSpellcastSucceeded triggered with relevant spell " .. spellID)

	if IsPlayerInHorrificVision() and spellID == 312881 then
		self:Debug("Finished searching mailbox in a Horrific Vision")
		addAttemptForItem("Mail Muncher", "mounts")
	end

	if IsPlayerInHorrificVision() and spellID == 1223438 then
		self:Debug("Finished searching trash pile in a Horrific Vision")
		addAttemptForItem("Nesting Swarmite", "mounts")
	end

	if spellID == 430315 then
		addAttemptForItem("Writhing Transmutagen", "pets")
	end

	-- Detects opening on Dirty Glinting Object which may contain Lucy's Lost Collar
	if spellID == 345071 and Rarity.lastNode and Rarity.lastNode == L["Dirty Glinting Object"] then
		Rarity:Debug("Detected Opening on " .. L["Dirty Glinting Object"] .. " (method = SPECIAL)")
		addAttemptForItem("Lucy's Lost Collar", "pets")
	end
end

-- TBD: Move to shared constants?
local TYPE_IDENTIFIER_ITEM = "item" -- What others do they have? currency? gold? No idea.

-- Upvalues
--- WOW API
local GetItemInfoInstant = _G.C_Item.GetItemInfoInstant

function R:OnShowLootToast(
	event,
	typeIdentifier,
	itemLink,
	quantity,
	specID,
	sex,
	personalLootToast,
	toastMethod,
	lessAwesome,
	upgraded,
	corrupted
)
	if typeIdentifier ~= TYPE_IDENTIFIER_ITEM then
		return R:Debug(format("Ignoring loot toast of type %s (not an item)", typeIdentifier))
	end

	-- From wowhead: "\124cff0070dd\124Hitem:187278::::::::60:::::\124h[Talon-Pierced Mawsworn Lockbox]\124h\124r"
	local itemID = GetItemInfoInstant(itemLink)

	-- TBD: Should we generalize this to a LOOT_TOAST detection method? Not sure if there are many other items people would care about
	-- Seems a bit too specialized, since we're detecting the loot toast for one item and then add attempts for another (hacky)
	local TALONPIERCED_MAWSWORN_LOCKBOX = 187278 -- TBD: Move to shared enum for ITEM_IDS? If we ever get to that point, that is...
	local lootToastItems = { [TALONPIERCED_MAWSWORN_LOCKBOX] = "Wilderling Saddle" }

	local linkedItemName = lootToastItems[itemID]
	if not linkedItemName then
		return R:Debug(format("Ignoring loot toast item %s (not relevant)", itemID))
	end

	-- There's only one item, so hardcoding the mounts group isn't an issue (but if we do want to generalize this later, it'll be easy)
	addAttemptForItem(linkedItemName, "items") -- Should take care of the covenant restriction by itself (and not add them if it doesn't match)
end

-------------------------------------------------------------------------------------
-- Pet battles: we want to hide the progress bar(s) during them
-------------------------------------------------------------------------------------

local wasBarVisibleBeforePetBattle = false

function R:OnPetBattleStart(event)
	R:Debug("Pet battle started")
	wasBarVisibleBeforePetBattle = R.db.profile.bar.visible
	R.db.profile.bar.visible = false
	Rarity.GUI:UpdateBar()
	Rarity.GUI:UpdateText()
end

function R:OnPetBattleEnd(event)
	R:Debug("Pet battle ended")
	R.db.profile.bar.visible = wasBarVisibleBeforePetBattle
	Rarity.GUI:UpdateBar()
	Rarity.GUI:UpdateText()
end

-------------------------------------------------------------------------------------
-- Currency updates. Used for coin roll and archaeology solve detection.
-------------------------------------------------------------------------------------

function R:OnCurrencyUpdate(event)
	self:Debug("Currency updated (" .. event .. ")")

	-- Check if any coins were used
	for k, v in pairs(self.coins) do
		local currency = GetCurrencyInfo(k)
		if currency == nil then
			return
		end
		local name, currencyAmount = currency.name, currency.quantity
		local diff = currencyAmount - (coinamounts[k] or 0)
		coinamounts[k] = currencyAmount
		if diff < 0 then
			self:Debug("Used coin: " .. name)
			R:CheckForCoinItem()
			self:ScheduleTimer(function()
				R:CheckForCoinItem()
			end, 2)
			self:ScheduleTimer(function()
				R:CheckForCoinItem()
			end, 5)
			self:ScheduleTimer(function()
				R:CheckForCoinItem()
			end, 10)
			self:ScheduleTimer(function()
				R:CheckForCoinItem()
			end, 15)
			self:ScheduleTimer(function()
				R:CheckForCoinItem()
			end, 20)
			self:ScheduleTimer(function()
				R:CheckForCoinItem()
			end, 25)
		end
	end
end

function R:CheckForCoinItem()
	if self.lastCoinItem and self.lastCoinItem.enableCoin then
		self:Debug("COIN USE DETECTED FOR AN ITEM")
		if self.lastCoinItem.attempts == nil then
			self.lastCoinItem.attempts = 1
		else
			self.lastCoinItem.attempts = self.lastCoinItem.attempts + 1
		end
		self:OutputAttempts(self.lastCoinItem)
		self.lastCoinItem = nil
	end
end

function R:OnEncounterEnd(event, encounterID, encounterName, difficultyID, raidSize, endStatus)
	R:Debug(
		"ENCOUNTER_END with encounterID = "
			.. tonumber(encounterID or "0")
			.. ", name = "
			.. tostring(encounterName)
			.. ", endStatus = "
			.. tostring(endStatus)
	)

	local items = Rarity.encounters[encounterID] -- TODO add the others to the actual item DB
	if type(items) ~= "table" then
		-- Not a relevant encounter
		return
	end

	if endStatus ~= 1 then
		-- Not a victory = no loot (presumably)
		return
	end

	R:Debug(format("Found %d item(s) of interest for this encounter", #items))
	R.encitems = items -- TODO remove
	for _, item in pairs(items) do
		R:Debug("Found item of interest for this encounter: " .. tostring(item.name))
		-- TODO DRY
		if item and type(item) == "table" and item.enabled ~= false and R:IsAttemptAllowed(item) then -- Add one attempt for this item
			if item.attempts == nil then
				item.attempts = 1
			else
				item.attempts = item.attempts + 1
			end
			R:OutputAttempts(item)
		end
	end
end

-------------------------------------------------------------------------------------
-- When combat ends, we scan your statistics a few times.
-- This helps catch some items that can't be tracked by normal means (i.e. Ragnaros),
-- as well as acting as another backup to detect attempts if we missed one.
-- WoW can take a few seconds to update statistics, thus the repeated scans.
-- This is also where we detect if an achievement criteria has been met.
-------------------------------------------------------------------------------------
do
	local timer1, timer2, timer3, timer4, timer5, timer6
	function R:OnCombatEnded(event)
		-- if R:InTooltip() then Rarity:ShowTooltip() end

		self:CancelTimer(timer1, true)
		self:CancelTimer(timer2, true)
		self:CancelTimer(timer3, true)
		self:CancelTimer(timer4, true)
		self:CancelTimer(timer5, true)
		self:CancelTimer(timer6, true)

		self:ScanStatistics(event)

		timer1 = self:ScheduleTimer(function()
			Rarity:ScanStatistics(event .. " 1")
		end, 2)
		timer2 = self:ScheduleTimer(function()
			Rarity:ScanStatistics(event .. " 2")
		end, 5)
		timer3 = self:ScheduleTimer(function()
			Rarity:ScanStatistics(event .. " 3")
		end, 8)
		timer4 = self:ScheduleTimer(function()
			Rarity:ScanStatistics(event .. " 4")
		end, 10)
		timer5 = self:ScheduleTimer(function()
			Rarity:ScanStatistics(event .. " 5")
		end, 15)
		timer6 = self:ScheduleTimer(function()
			Rarity:ScanStatistics(event .. " 6")
		end, 20)
	end
end

-------------------------------------------------------------------------------------
-- Handle boss kills. You may not ever open a loot window on a boss, so we need to watch the combat log for its death.
-- This event also handles some special cases.
-------------------------------------------------------------------------------------
-- function R:OnCombat()
-- 	self.Profiling:StartTimer("EventHandlers.OnCombat")

-- 	-- Extract event payload (it's no longer being passed by the event iself as of 8.0.1)
-- 	local timestamp, eventType, hideCaster, srcGuid, srcName, srcFlags, srcRaidFlags, dstGuid, dstName, dstFlags, dstRaidFlags, spellId, spellName, spellSchool, auraType =
-- 		CombatLogGetCurrentEventInfo()

-- 	if eventType == "UNIT_DIED" then -- A unit died near you
-- 		local npcid = self:GetNPCIDFromGUID(dstGuid)
-- 		if Rarity.bosses[npcid] then -- It's a boss we're interested in
-- 			R:Debug("Detected UNIT_DIED for relevant NPC with ID = " .. tostring(npcid))
-- 			if
-- 				bit_band(srcFlags, COMBATLOG_OBJECT_AFFILIATION_MINE)
-- 				or bit_band(srcFlags, COMBATLOG_OBJECT_AFFILIATION_PARTY)
-- 				or bit_band(srcFlags, COMBATLOG_OBJECT_AFFILIATION_RAID)
-- 			then -- You, a party member, or a raid member killed it
-- 				if not Rarity.guids[dstGuid] then
-- 					if not UnitAffectingCombat("player") and not UnitIsDead("player") then
-- 						Rarity:Debug("Ignoring this UNIT_DIED event because the player is alive, but not in combat")
-- 						self.Profiling:EndTimer("EventHandlers.OnCombat")
-- 						return
-- 					end

-- 					-- Increment attempts counter(s). One NPC might drop multiple things we want, so scan for them all.
-- 					if Rarity.npcs_to_items[npcid] and type(Rarity.npcs_to_items[npcid]) == "table" then
-- 						for k, v in pairs(Rarity.npcs_to_items[npcid]) do
-- 							local isBossDrop = (v.method == CONSTANTS.DETECTION_METHODS.BOSS)
-- 							local hasKillStatistics = type(v.statisticId) ~= "nil"
-- 							if v.enabled ~= false and isBossDrop and not hasKillStatistics then
-- 								if self:IsAttemptAllowed(v) then
-- 									Rarity.guids[dstGuid] = true
-- 									if v.attempts == nil then
-- 										v.attempts = 1
-- 									else
-- 										v.attempts = v.attempts + 1
-- 									end
-- 									self:OutputAttempts(v)
-- 								end
-- 							end
-- 						end
-- 					end
-- 				end
-- 			end
-- 		end
-- 	end
-- 	self.Profiling:EndTimer("EventHandlers.OnCombat")
-- end

local worldEventQuests = {
	[52196] = "Slightly Damp Pile of Fur", -- Dunegorger Kraulok (TODO: Use encounter also?)
	[70867] = "Everlasting Horn of Lavaswimming", -- Scalebane Keep (scenario completion)
	[52847] = { alliance = "Toy Siege Tower", horde = "Toy War Machine" }, -- Doom's Howl / The Lion's Roar (Arathi Warfront world boss completion)
	[52848] = { alliance = "Toy Siege Tower", horde = "Toy War Machine" }, -- Doom's Howl / The Lion's Roar (Arathi Warfront world boss completion)
	-- Not actually from a world quest/event
	[85830] = "Parrot Cage (Void-Scarred Parrot)", -- More accurately detected via object GUID
	-- TBD: Are object GUIDs also secret now? Sigh.
}

function R:OnQuestTurnedIn(event, questID, experience, money)
	self:Debug(
		"OnQuestTurnedIn triggered with ID = " .. questID .. ", experience = " .. experience .. ", money = " .. money
	)

	local relevantItem = worldEventQuests[questID]
	if not relevantItem then
		return
	end
	self:Debug(format("Relevant quest turnin detected for item %s (questID = %d)", questID, relevantItem))

	local v = self.db.profile.groups.items[relevantItem]
		or self.db.profile.groups.pets[relevantItem]
		or self.db.profile.groups.mounts[relevantItem]
	if v and type(v) == "table" and v.enabled ~= false then
		if v.attempts == nil then
			v.attempts = 1
		else
			v.attempts = v.attempts + 1
		end
		self:OutputAttempts(v)
	end
end

-------------------------------------------------------------------------------------
-- ISLAND_COMPLETED handling: Used for detecting Island Expeditions in Battle for Azeroth
-- The collectibles are awarded randomly once the scenario has ended and appear in the "Pose" screen, right after this event is fired (indicated by a subsequent LFG_COMPLETION_REWARD event)
-------------------------------------------------------------------------------------

local islandMapIDs = {
	-- These IDs can be found in DBFilesClient\Map.db2
	[1813] = "Un'gol Ruins",
	[1897] = "Molten Cay",
	[1883] = "Whispering Reef",
	[1882] = "Verdant Wilds",
	[1892] = "Rotting Mire",
	[1893] = "Dread Chain",
	[1898] = "Skittering Hollow",
	[1814] = "Havenswood",
	[1879] = "Jorundall",
	[1907] = "Snowblossom",
	[2124] = "Crestfall",
}

local islandExpeditionCollectibles = {
	-- List of collectibles (so we don't have to search the item DB for them)

	-- Pets
	---- 8.0
	"Scuttle",
	"Captain Nibs",
	"Barnaby",
	"Poro",
	"Octopode Fry",
	"Inky",
	"Sparkleshell Sandcrawler",
	"Kindleweb Spiderling",
	"Mischievous Zephyr",
	"Littlehoof",
	"Snapper",
	"Sunscale Hatchling",
	"Bloodstone Tunneler",
	"Snort",
	"Muskflank Calfling",
	"Juvenile Brineshell",
	"Kunchong Hatchling",
	"Coldlight Surfrunner",
	"Voru'kar Leecher",
	"Tinder Pup",
	"Sandshell Chitterer",
	"Deathsting Scorpid",
	"Thistlebrush Bud",
	"Giggling Flame",
	"Laughing Stonekin",
	"Playful Frostkin",
	"False Knucklebump",
	"Craghoof Kid",
	---- 8.1
	"Baby Stonehide",
	"Leatherwing Screecher",
	"Rotting Ghoul",
	"Thunderscale Whelpling",
	"Scritches",
	"Tonguelasher",
	"Lord Woofington",
	"Firesting Buzzer",
	"Needleback Pup",
	"Shadefeather Hatchling",
	---- 8.2
	"Adventurous Hopling Pack",
	"Ghostly Whelpling",
	-- Toys
	---- 8.0
	"Oomgut Ritual Drum",
	"Whiskerwax Candle",
	"Enchanted Soup Stone",
	"Magic Monkey Banana",
	"Bad Mojo Banana",

	-- Mounts
	---- 8.0
	"Surf Jelly",
	"Squawks",
	"Qinsho's Eternal Hound",
	"Craghorn Chasm-Leaper",
	"Twilight Avenger",
	---- 8.1
	"Risen Mare",
	"Island Thunderscale",
	"Bloodgorged Hunter",
	"Stonehide Elderhorn",
}

function R:OnIslandCompleted(event, mapID, winner)
	R:Debug(
		"Detected completion for Island Expedition: "
			.. (islandMapIDs[mapID] or "Unknown Map")
			.. " (mapID = "
			.. tostring(mapID)
			.. ")"
	)

	if islandMapIDs[mapID] then -- Is a relevant map -> Add attempts for all collectibles
		-- (for now, I'm assuming they just drop from everything at the same rate.
		-- This may have to be revised once more data is available...)
		-- Update: Proper tracking seems night on impossible so this'll have to do for the time being
		-- See https://github.com/SacredDuckwhale/Rarity/issues/61 for more details
		R:Debug("Found this Island Expedition to be relevant -> Adding attempts for all known collectibles...")

		for index, name in pairs(islandExpeditionCollectibles) do -- Add an attempt for each item
			local v = self.db.profile.groups.items[name]
				or self.db.profile.groups.pets[name]
				or self.db.profile.groups.mounts[name]
			if v and type(v) == "table" and v.enabled ~= false then
				if v.attempts == nil then
					v.attempts = 1
				else
					v.attempts = v.attempts + 1
				end
				self:OutputAttempts(v)
			end
		end
	end
end

local timeRiftCriteriaLUT = { -- TBD: Can't remove this... I think? Would need to check ingame (etrace log)
	[60685] = "Gill'dan (Azmerloth)",
	[60688] = "Freya (Ulderoth)",
	[60689] = "The Lich King (Azmourne)",
	[60690] = "Illidan Stormrage (Azewrath)",
	[60691] = "Fury of N'zoth (Azq'roth)",
	[60692] = "Varian Wrynn (The Warlands)",
	[60693] = "Overlord Mechagon (A.Z.E.R.O.T.H.)",
}

local timeRiftMounts = {
	"Felstorm Dragon",
	"Gold-Toed Albatross",
	"Perfected Juggernaut",
	"Reins of the Scourgebound Vanquisher",
	"Sulfur Hound's Leash",
}

local timeRiftPets = {
	"Briarhorn Hatchling",
	"Doomrubble",
	"Gill'dan",
	"Jeepers",
	"Killbot 9000",
	"N'Ruby",
	"Obsidian Warwhelp",
}

function R:OnCriteriaComplete(event, id)
	local timeRiftEncounterName = timeRiftCriteriaLUT[id]
	R:Debug("Detected achievement criteria completion: " .. tostring(id))

	if timeRiftEncounterName then
		R:Debug("Completed criteria for Time Rift encounter: " .. timeRiftEncounterName)
		self:OnTimeRiftCompleted()
	end
end

function R:OnTimeRiftCompleted()
	if self.Caching:IsAlliance() then
		local itemName = "Reins of the Ravenous Black Gryphon"
		addAttemptForItem(itemName, "mounts")
	else
		local itemName = "Horn of the White War Wolf"
		addAttemptForItem(itemName, "mounts")
	end

	for _, itemName in ipairs(timeRiftMounts) do
		addAttemptForItem(itemName, "mounts")
	end

	for _, itemName in ipairs(timeRiftPets) do
		addAttemptForItem(itemName, "pets")
	end
end

-------------------------------------------------------------------------------------
-- Mouseover detection, currently used for Mysterious Camel Figurine as a special case
-------------------------------------------------------------------------------------

function R:OnMouseOver(event)
	local guid = Rarity:GetUnitGUID("mouseover")
	if not guid then
		return
	end

	local npcid = self:GetNPCIDFromGUID(guid)
	if not npcid then
		return
	end

	self.Profiling:StartTimer("EventHandlers.OnMouseOver")

	if npcid == 50409 or npcid == 50410 then
		if not Rarity.guids[guid] then
			Rarity.guids[guid] = true
			local v = self.db.profile.groups.mounts["Reins of the Grey Riding Camel"]
			if v and type(v) == "table" and v.enabled ~= false then
				if v.attempts == nil then
					v.attempts = 1
				else
					v.attempts = v.attempts + 1
				end
				self:OutputAttempts(v)
			end
		end
	end

	self.Profiling:EndTimer("EventHandlers.OnMouseOver")
end

function R:OnProfileChanged(event, database, newProfileKey)
	self:Debug("Profile changed. Reinitializing.")
	Rarity.Session:Cancel()

	local sessionTimer = Rarity.Session:GetTimer()
	if sessionTimer then
		self:CancelTimer(sessionTimer, true)
	end
	Rarity.Session:SetTimer(nil)
	self.db:RegisterDefaults(self.defaults)
	self:UpdateInterestingThings()
	self:OnCurrencyUpdate(event)
	self:ScanAllArch(event)
	Rarity.Collections:ScanExistingItems(event)
	self:ScanBags()
	Rarity.Tracking:FindTrackedItem()
	Rarity.GUI:UpdateText()
	self.db.profile.lastRevision = R.MINOR_VERSION
end

function R:OnChatCommand(input)
	if strlower(input) == "debug" then
		if self.db.profile.debugMode then
			self.db.profile.debugMode = false
			self:Print(L["Debug mode OFF"])
		else
			self.db.profile.debugMode = true
			self:Print(L["Debug mode ON"])
		end
	elseif strlower(input) == "dump" then
		self.ScrollingDebugMessageFrame:Toggle()
	elseif strlower(input) == "validate" then -- Verify the ItemDB
		self.Validation:ValidateItemDB()
	elseif strlower(input) == "mapinfo" then
		local mapID = C_Map.GetBestMapForUnit("player")
		local mapInfo = C_Map.GetMapInfo(mapID)
		local mapName = mapInfo and mapInfo.name or "Unknown"
		self:Print("Current map: " .. mapID .. " ~ " .. mapName)
	elseif strlower(input) == "purge" then -- TODO: This should be done automatically, no?
		self.Database:PurgeObsoleteEntries()
	elseif strlower(input) == "progress" then
		self.GUI:ToggleProgressBar()
	elseif strlower(input) == "test" then
		self.Testing:RunIntegrationTests()
	elseif strlower(input) == "profiling" then
		if self.db.profile.enableProfiling then
			self.db.profile.enableProfiling = false
			self:Print(L["Profiling OFF"])
		else
			self.db.profile.enableProfiling = true
			self:Print(L["Profiling ON"])
		end
	elseif strlower(input) == "tinspect" then --  TODO Document it?
		Rarity.Profiling:InspectAccumulatedTimes()
	else
		Rarity:TryShowOptionsUI()
	end
end

function R:OnItemFound(itemId, item, suppressAlert)
	if suppressAlert == nil then
		suppressAlert = false
	end

	if item.found and not item.repeatable then
		return
	end

	local playerClass = select(2, UnitClass("player"))
	if item.disableForClass and item.disableForClass[playerClass] then
		Rarity:Debug(format("Ignoring OnItemFound trigger for item %s (disabled for class %s)", item.name, playerClass))
		return
	end

	self:Debug("FOUND ITEM %d!", itemId)
	if item.attempts == nil then
		item.attempts = 1
	end
	if item.lastAttempts == nil then
		item.lastAttempts = 0
	end

	-- Hacky: If the item is unique and has 0 attempts, don't do this (if you really find a unique item on your first attempt, sorry)
	if item.unique and item.attempts - item.lastAttempts <= 1 then
		return
	end

	if not suppressAlert then
		self:ShowFoundAlert(itemId, item.attempts - item.lastAttempts, item, item)
	end
	self:ShowFoundAlert(itemId, item.attempts - item.lastAttempts, item, item)
	if Rarity.Session:IsActive() then
		Rarity.Session:End()
	end
	item.realAttempts = item.attempts - item.lastAttempts
	item.lastAttempts = item.attempts
	item.enabled = false
	item.found = true
	item.totalFinds = (item.totalFinds or 0) + 1
	if not item.finds then
		item.finds = {}
	end
	local count = 0
	for k, v in pairs(item.finds) do
		count = count + 1
	end
	table.insert(item.finds, {
		num = count + 1,
		totalAttempts = item.attempts,
		totalTime = item.time,
		attempts = item.realAttempts,
		time = (item.time or 0) - (item.lastTime or 0),
	})
	item.lastTime = item.time
	Rarity.Tracking:Update(item)
	self:UpdateInterestingThings()
	if item.repeatable then
		self:ScheduleTimer(function()
			-- If this is a repeatable item, turn it back on in a few seconds.
			-- OnItemFound() gets called repeatedly when we get an item, so we need to lock it out for a few seconds.
			item.enabled = nil
			item.found = nil
			self:UpdateInterestingThings()
			Rarity.GUI:UpdateText()
		end, 5)
	end
end

local FISHING_DELAY = 22

function R:OnSpellcastSent(event, unit, target, castGUID, spellID)
	if unit ~= "player" then
		return
	end
	self.Profiling:StartTimer("EventHandlers.OnSpellcastSent")

	Rarity.foundTarget = false
	-- ga = "No" -- WTF is this?

	if Rarity.relevantSpells[spellID] then -- An entry exists for this spell in the LUT -> It's one that needs to be tracked
		Rarity:Debug(
			"Detected relevant spell: " .. tostring(spellID) .. " ~ " .. tostring(Rarity.relevantSpells[spellID])
		)
		Rarity.currentSpell = spellID
		Rarity.previousSpell = spellID
		if Rarity.relevantSpells[spellID] == "Fishing" or Rarity.relevantSpells[spellID] == "Opening" then
			self:Debug("Fishing or opening something")
			if Rarity.relevantSpells[spellID] == "Opening" then
				self:Debug("Opening detected")
				Rarity.isOpening = true
			else
				Rarity.isOpening = false
			end
			Rarity.isFishing = true
			if Rarity.fishingTimer then
				self:CancelTimer(Rarity.fishingTimer, true)
			end
			Rarity.fishingTimer = self:ScheduleTimer(Rarity.OnFishingEnded, FISHING_DELAY)
			self:GetWorldTarget()
		end
	else
		Rarity.previousSpell, Rarity.currentSpell = nil, nil
	end

	self.Profiling:EndTimer("EventHandlers.OnSpellcastSent")
end

function R:OnFishingEnded()
	R:Debug("You didn't loot anything from that fishing. Giving up.")
	Rarity.fishingTimer = nil
	Rarity.isFishing = false
	Rarity.isPool = false
	Rarity.isOpening = false
end

function R:OnLootFrameClosed(event)
	Rarity.previousSpell, Rarity.currentSpell = nil, nil
	Rarity.foundTarget = false
	self:ScheduleTimer(function()
		Rarity.lastNode = nil
	end, 1)
end

local tooltipLeftText1 = _G["GameTooltipTextLeft1"]

local function stripColorCode(input)
	local output = input or ""
	output = gsub(output, "|c%x%x%x%x%x%x%x%x", "")
	output = gsub(output, "|r", "")
	return output
end

function R:OnCursorChanged(event)
	if Rarity.foundTarget then
		return
	end

	if MinimapCluster:IsMouseOver() then
		return
	end

	local tooltipText = tooltipLeftText1:GetText()
	if issecretvalue and issecretvalue(tooltipText) then
		return
	end

	self.Profiling:StartTimer("EventHandlers.OnCursorChanged")
	local text = stripColorCode(tooltipText)
	if self.miningnodes[text] or self.fishnodes[text] or self.opennodes[text] then
		Rarity.lastNode = text
		Rarity:Debug("OnCursorChanged found lastNode = " .. tostring(text))
	end
	if Rarity.relevantSpells[Rarity.previousSpell] then
		self:GetWorldTarget()
	end
	self.Profiling:EndTimer("EventHandlers.OnCursorChanged")
end

-- Doesn't really belong here, but no idea where to put it right now. Later...
function R:GetWorldTarget()
	if Rarity.foundTarget or not Rarity.relevantSpells[Rarity.currentSpell] then
		return
	end

	if MinimapCluster:IsMouseOver() then
		return
	end

	local text = tooltipLeftText1:GetText()
	if issecretvalue and issecretvalue(text) then
		return
	end

	self.Profiling:StartTimer("EventHandlers.GetWorldTarget")
	Rarity:Debug("Getting world target " .. tostring(text))
	if text and Rarity.previousSpell and text ~= Rarity.previousSpell and R.fishnodes[text] then
		self:Debug("------YOU HAVE STARTED FISHING A NODE ------")
		Rarity.isFishing = true
		Rarity.isPool = true
		if Rarity.fishingTimer then
			self:CancelTimer(Rarity.fishingTimer, true)
		end
		Rarity.fishingTimer = self:ScheduleTimer(Rarity.OnFishingEnded, FISHING_DELAY)
		Rarity.foundTarget = true
	end

	self.Profiling:EndTimer("EventHandlers.GetWorldTarget")
end

function R:OnSpellcastStopped(event, unit)
	if unit ~= "player" then
		return
	end

	self.Profiling:StartTimer("EventHandlers.OnSpellcastStopped")

	if Rarity.relevantSpells[Rarity.previousSpell] then
		self:GetWorldTarget()
	end
	Rarity.previousSpell, Rarity.currentSpell = Rarity.currentSpell, Rarity.currentSpell
	self.Profiling:EndTimer("EventHandlers.OnSpellcastStopped")
end

function R:OnSpellcastFailed(event, unit)
	if unit ~= "player" then
		return
	end
	Rarity.previousSpell, Rarity.currentSpell = nil, nil
end

-------------------------------------------------------------------------------------
-- Something in your bags changed.
--
-- This is used for a couple things. First, for boss drops that require a group, you may not have obtained the item even if it dropped from the boss.
-- Therefore, we only say you obtained it when it appears in your inventory. Secondly, this is useful as a second line of defense in case
-- you somehow obtain an item without us noticing it. This event fires a lot, so we need to be fast.
--
-- We also store how many of every item you have on you at the moment. If we notice an item decreasing in quantity, and it's something we care
-- about, you just used an item or opened a container.
--
-- This event is bucketed because it tends to fire tons of times in a row rapidly, leading to innaccurate results.
-------------------------------------------------------------------------------------
local table_wipe = table.wipe -- Nonstandard, but always exists in WOW's Lua environment

function R:BackUpInventoryItemAmounts()
	table_wipe(Rarity.tempbagitems)
	for itemID, inventoryAmount in pairs(Rarity.bagitems) do
		Rarity.tempbagitems[itemID] = inventoryAmount
	end
end

function R:ProcessContainerItems()
	for k, v in pairs(Rarity.tempbagitems) do
		if (Rarity.bagitems[k] or 0) < (Rarity.tempbagitems[k] or 0) then -- An inventory item went down in count or disappeared
			if Rarity.used[k] then -- It's an item we care about
				-- Scan through the whole item database now to find all items that could want this
				for _k, _v in pairs(self.db.profile.groups) do
					if type(_v) == "table" then
						for kk, vv in pairs(_v) do
							if type(vv) == "table" then
								if vv.enabled ~= false then
									if
										vv.method == CONSTANTS.DETECTION_METHODS.USE
										and vv.items ~= nil
										and type(vv.items) == "table"
									then
										local isHordePlayer = R.Caching:IsHorde()
										local canPlayerObtainFactionSpecificItem = not (
												vv.requiresHorde and not isHordePlayer
											) or (vv.requiresAlliance and isHordePlayer)
										if canPlayerObtainFactionSpecificItem then
											for kkk, vvv in pairs(vv.items) do
												if vvv == k then
													local i = vv
													if i.attempts == nil then
														i.attempts = 1
													else
														i.attempts = i.attempts + 1
													end
													self:OutputAttempts(i)
												end
											end
										end
									end
								end
							end
						end
					end
				end
				-- End scan through all items
			end
		end
	end
end

function R:ProcessInventoryItems(suppressAlert)
	self.Profiling:StartTimer("EventHandlers.ProcessInventoryItems")

	for itemID, _ in pairs(Rarity.bagitems) do
		self:ProcessCollectionItem(itemID, suppressAlert)
		self:ProcessOtherItem(itemID, suppressAlert)
	end

	self.Profiling:EndTimer("EventHandlers.ProcessInventoryItems")
end

function R:ProcessCollectionItem(itemID, suppressAlert)
	if not itemID then
		return
	end

	local item = Rarity.items[itemID]
	if not item then
		return
	end

	-- Handle collection items
	if not self:IsCollectionItem(item) then
		return
	end

	local inventoryItemCount = R:GetInventoryItemCount(itemID)

	-- Our items hashtable only saves one item for this collected item, so we have to scan to find them all now.
	-- Earlier, we pre-built a list of just the items that are COLLECTION items to save some time here.
	for collectionItemID, collectionItem in pairs(Rarity.collection_items) do
		-- This item is a collection of several items; add them all up and check for attempts
		if self:HasMultipleCollectionItems(collectionItem) then
			self:ProcessCollectionItemAggregate(collectionItem, suppressAlert)
		else
			self:ProcessCollectionItemSingle(collectionItem, itemID, suppressAlert)
		end
	end
end

function R:HasMultipleCollectionItems(item)
	return type(item.collectedItemId) == "table"
end

-- TODO: DRY
function R:IsCollectionItem(item)
	return item.method == CONSTANTS.DETECTION_METHODS.COLLECTION
end

function R:ProcessOtherItem(itemID, suppressAlert)
	if not itemID then
		return
	end

	local item = Rarity.items[itemID]
	if not item then
		return
	end

	local amountIncreasedSinceLastScan = (Rarity.bagitems[itemID] or 0) > (Rarity.tempbagitems[itemID] or 0)
	if amountIncreasedSinceLastScan then -- An inventory item went up in count
		if item and item.enabled ~= false and not self:IsCollectionItem(item) then
			self:OnItemFound(itemID, item, suppressAlert)
		end
	end
end

function R:GetInventoryItemCount(itemID)
	local inventoryItemCount = (Rarity.bagitems[itemID] or 0)
	return inventoryItemCount
end

-- Still incomprehensible, but I'll leave it for now
function R:ProcessCollectionItemSingle(collectionItem, itemID, suppressAlert)
	local inventoryItemCount = self:GetInventoryItemCount(itemID)
	local item = Rarity.items[itemID]

	if
		collectionItem.enabled
		and (
			collectionItem.collectedItemId == item.collectedItemId
			or table_contains(item.collectedItemId, collectionItem.collectedItemId)
		)
	then
		local originalCount = (collectionItem.attempts or 0)
		local goal = (collectionItem.chance or 100)
		collectionItem.lastAttempts = 0
		if collectionItem.attempts ~= inventoryItemCount then
			collectionItem.attempts = inventoryItemCount
		end
		if originalCount < inventoryItemCount and originalCount < goal and inventoryItemCount >= goal then
			self:OnItemFound(collectionItem.itemId, collectionItem, suppressAlert)
		elseif originalCount < inventoryItemCount then
			self:OutputAttempts(collectionItem)
		end
	end
end

-- Still incomprehensible, but I'll leave it for now
function R:ProcessCollectionItemAggregate(collectionItem, suppressAlert)
	if collectionItem.enabled ~= false then
		local total = 0
		local originalCount = (collectionItem.attempts or 0)
		local goal = (collectionItem.chance or 100)
		for kkk, vvv in pairs(collectionItem.collectedItemId) do
			vvv = tonumber(vvv) -- It's stored as string, but we expect numbers...
			if (Rarity.bagitems[vvv] or 0) > 0 then
				total = total + Rarity.bagitems[vvv]
			end
		end
		if total > originalCount then
			self:Debug("Total is > original count, overriding current attempts")
			collectionItem.attempts = total
			if originalCount < goal and total >= goal then
				self:Debug("Triggering OnItemFound since we just reached the goal")
				self:OnItemFound(collectionItem.itemId, collectionItem, suppressAlert)
			elseif total > originalCount then
				self:Debug("Triggering OutputAttempts since we gained one item, but didn't reach the goal")
				self:OutputAttempts(collectionItem)
			end
		end
	end
end

-- It's an abomination, but without tests I'm not refactoring this any further
function R:OnBagUpdate()
	-- Save a copy of your bags before this event
	R:BackUpInventoryItemAmounts()

	-- Get a list of the items you have now, alerting if we find anything we're looking for
	self:ScanBags()

	-- I assume that if there's even the feintest possibility of items being removed, we don't want to risk it?
	local shouldSkipBagUpdate = (
		Rarity.isBankOpen
		or Rarity.isGuildBankOpen
		or Rarity.isAuctionHouseOpen
		or Rarity.isTradeWindowOpen
		or Rarity.isTradeskillOpen
		or Rarity.isMailboxOpen
	)

	if shouldSkipBagUpdate then
		return
	end

	-- Check for a decrease in quantity of any items we're watching for
	R:ProcessContainerItems()

	-- Check for an increase in quantity of any items we're watching for
	R:ProcessInventoryItems(Rarity.isInitialBagSync)
end

function R:OnResearchArtifactComplete(event, _)
	self:ScanArchFragments(event)
end

function R:OnEvent(event, ...)
	self.Profiling:StartTimer("EventHandlers.OnEvent")

	if event == "TRADE_SKILL_SHOW" then
		Rarity.isTradeskillOpen = true
	elseif event == "TRADE_SKILL_CLOSE" then
		Rarity.isTradeskillOpen = false
	elseif event == "UPDATE_INSTANCE_INFO" then
		-- Instance lock info updated
		self:ScanInstanceLocks(event)
	elseif event == "LFG_UPDATE_RANDOM_INFO" then
		self:ScanInstanceLocks(event)
	elseif event == "CALENDAR_UPDATE_EVENT_LIST" then
		-- Calendar updated
		self:ScanCalendar(event)
	elseif event == "TOYS_UPDATED" then
		-- Toy box updated
		Rarity.Collections:ScanExistingItems(event)
	elseif event == "COMPANION_UPDATE" then
		-- Pets updated
		Rarity.Collections:ScanExistingItems(event)
	elseif event == "PLAYER_LOGOUT" then
		-- Logging out; end any open session
		if Rarity.Session:IsActive() then
			Rarity.Session:End()
		end
	end

	self.Profiling:EndTimer("EventHandlers.OnEvent")
end

-------------------------------------------------------------------------------------
-- You opened a loot window on a corpse or fishing node
-------------------------------------------------------------------------------------
function R:OnLootReady(event, ...)
	do
		local targetGUID = Rarity:GetUnitGUID("target") or "N/A"
		self:Debug("LOOT_READY with target: " .. targetGUID)

		-- Two LOOT_READY events may trigger when the loot window opens, in which case this prevents double counting
		if self.Session:IsLocked() then
			self:Debug("Session is locked; ignoring this LOOT_READY event")
			return
		end

		self.Session:Lock()

		if Rarity.isBankOpen then
			Rarity:Debug("Ignoring this LOOT_READY event (bank is open)")
			return
		end

		if Rarity.isGuildBankOpen then
			Rarity:Debug("Ignoring this LOOT_READY event (guild bank is open)")
			return
		end

		if Rarity.isMailboxOpen then
			Rarity:Debug("Ignoring this LOOT_READY event (mailbox is open)")
			return
		end

		if Rarity.isAuctionHouseOpen then
			Rarity:Debug("Ignoring this LOOT_READY event (auction house is open)")
			return
		end

		if Rarity.isTradeWindowOpen then
			Rarity:Debug("Ignoring this LOOT_READY event (trade window is open)")
			return
		end

		local zone = GetRealZoneText()
		local subzone = GetSubZoneText()
		local zone_t = LibStub("LibBabble-Zone-3.0"):GetReverseLookupTable()[zone]
		local subzone_t = LibStub("LibBabble-SubZone-3.0"):GetReverseLookupTable()[subzone]

		if Rarity.isFishing and Rarity.isOpening then
			self:Debug("Opened something")
		end

		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode then
			self:Debug("Opened a node: " .. Rarity.lastNode)
		end

		-- Handle opening Crane Nest
		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Crane Nest"]) then
			Rarity:Debug("Detected Opening on " .. L["Crane Nest"] .. " (method = SPECIAL)")
			local v = self.db.profile.groups.pets["Azure Crane Chick"]
			if v and type(v) == "table" and v.enabled ~= false then
				if v.attempts == nil then
					v.attempts = 1
				else
					v.attempts = v.attempts + 1
				end
				self:OutputAttempts(v)
			end
		end

		-- Handle opening Timeless Chest
		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Timeless Chest"]) then
			Rarity:Debug("Detected Opening on " .. L["Timeless Chest"] .. " (method = SPECIAL)")
			local v = self.db.profile.groups.pets["Bonkers"]
			if v and type(v) == "table" and v.enabled ~= false then
				if v.attempts == nil then
					v.attempts = 1
				else
					v.attempts = v.attempts + 1
				end
				self:OutputAttempts(v)
			end
		end

		-- Handle opening Snow Mound
		if
			Rarity.isFishing
			and Rarity.isOpening
			and Rarity.lastNode
			and (Rarity.lastNode == L["Snow Mound"])
			and GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.FROSTFIRE_RIDGE
		then -- Make sure we're in Frostfire Ridge (there are Snow Mounds in other zones, particularly Ulduar in the Hodir room)
			Rarity:Debug("Detected Opening on " .. L["Snow Mound"] .. " (method = SPECIAL)")
			local v = self.db.profile.groups.pets["Grumpling"]
			if v and type(v) == "table" and v.enabled ~= false then
				if v.attempts == nil then
					v.attempts = 1
				else
					v.attempts = v.attempts + 1
				end
				self:OutputAttempts(v)
			end
		end

		-- Handle opening Curious Wyrmtongue Cache
		if
			Rarity.isFishing
			and Rarity.isOpening
			and Rarity.lastNode
			and (Rarity.lastNode == L["Curious Wyrmtongue Cache"])
		then
			local names = { "Scraps", "Pilfered Sweeper" }
			Rarity:Debug("Detected Opening on " .. L["Curious Wyrmtongue Cache"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Glimmering Chest
		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Glimmering Chest"]) then
			local names = { "Sandclaw Nestseeker" }
			Rarity:Debug("Detected Opening on " .. L["Glimmering Chest"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Penitence of Purity (Shadowlands Kyrian only chest)
		if
			Rarity.isFishing
			and Rarity.isOpening
			and Rarity.lastNode
			and (Rarity.lastNode == L["Penitence of Purity"])
		then
			local names = { "Phalynx of Humility" }
			Rarity:Debug("Detected Opening on " .. L["Penitence of Purity"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.mounts[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Silver Strongbox & Gilded Chest (Bastion, Shadowlands nodes for Acrobatic Steward (toy) and Gilded Wader (pet))
		if
			Rarity.isFishing
			and Rarity.isOpening
			and Rarity.lastNode
			and (Rarity.lastNode == L["Silver Strongbox"] or Rarity.lastNode == L["Gilded Chest"])
		then
			local names = { "Acrobatic Steward", "Gilded Wader" }
			Rarity:Debug("Detected Opening on " .. Rarity.lastNode .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Broken Bell & Skyward Bell (Shadowlands, Bastion nodes for Soothing Vesper (toy) & Gilded Wader (pet))
		if
			Rarity.isFishing
			and Rarity.isOpening
			and Rarity.lastNode
			and (Rarity.lastNode == L["Broken Bell"] or Rarity.lastNode == L["Skyward Bell"])
		then
			local names = { "Soothing Vesper", "Gilded Wader" }
			Rarity:Debug("Detected Opening on " .. Rarity.lastNode .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Cache of the Ascended (Shadowlands, Bastion mount cache)
		if
			Rarity.isFishing
			and Rarity.isOpening
			and Rarity.lastNode
			and (Rarity.lastNode == L["Cache of the Ascended"])
		then
			local names = { "Ascended Skymane" }
			Rarity:Debug("Detected Opening on " .. L["Cache of the Ascended"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.mounts[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Slime-Coated Crate (Shadowlands, Maldraxxus crate for Kevin's Party Supplies (toy) & Bubbling Pustule (pet))
		if
			Rarity.isFishing
			and Rarity.isOpening
			and Rarity.lastNode
			and (Rarity.lastNode == L["Slime-Coated Crate"])
		then
			local names = { "Kevin's Party Supplies", "Bubbling Pustule" }
			Rarity:Debug("Detected Opening on " .. L["Slime-Coated Crate"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Sprouting Growth (Shadowlands, Maldraxxus crate for Skittering Venomspitter pet)
		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Sprouting Growth"]) then
			local names = { "Skittering Venomspitter" }
			Rarity:Debug("Detected Opening on " .. L["Sprouting Growth"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Stewart's Stewpendous Stew (Shadowlands, Bastion crate for Silvershell Snapper pet)
		if
			Rarity.isFishing
			and Rarity.isOpening
			and Rarity.lastNode
			and (Rarity.lastNode == L["Stewart's Stewpendous Stew"])
		then
			local names = { "Silvershell Snapper" }
			Rarity:Debug("Detected Opening on " .. L["Stewart's Stewpendous Stew"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Bleakwood Chest (Shadowlands, Revendreth chest for Trapped Stonefiend pet)
		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Bleakwood Chest"]) then
			local names = { "Trapped Stonefiend" }
			Rarity:Debug("Detected Opening on " .. L["Bleakwood Chest"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Blackhound Cache (Shadowlands, Maldraxxus cache for Battlecry of Krexus - Necrolord toy)
		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Blackhound Cache"]) then
			local names = { "Battlecry of Krexus" }
			Rarity:Debug("Detected Opening on " .. L["Blackhound Cache"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Secret Treasure (Shadowlands, Revendreth chest for Soullocked Sinstone pet)
		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Secret Treasure"]) then
			local names = { "Soullocked Sinstone" }
			Rarity:Debug("Detected Opening on " .. L["Secret Treasure"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Forgotten Chest (Shadowlands, Revendreth chest for Stony's Infused Ruby pet and Silessa's Battle Harness mount)
		if
			Rarity.isFishing
			and Rarity.isOpening
			and Rarity.lastNode
			and (Rarity.lastNode == L["Forgotten Chest"])
			and GetBestMapForUnit("player") ~= CONSTANTS.UIMAPIDS.STORMSONG_VALLEY -- Chest with the same name in Stormsong Valley
		then
			local names = { "Stony's Infused Ruby", "Silessa's Battle Harness" }
			Rarity:Debug("Detected Opening on " .. L["Forgotten Chest"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.pets[name] or self.db.profile.groups.mounts[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Cache of Eyes (Shadowlands, Maldraxxus chest for Luminous Webspinner pet)
		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Cache of Eyes"]) then
			local names = { "Luminous Webspinner" }
			Rarity:Debug("Detected Opening on " .. L["Cache of Eyes"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening lots of various chests for Gilded Wader (pet).
		local nodesGildedWader = {
			[L["Gift of Thenios"]] = true,
			[L["Hidden Hoard"]] = true,
			[L["Memorial Offerings"]] = true,
			[L["Treasure of Courage"]] = true,
		}
		local isRelevantNode = false
		if Rarity.lastNode then
			isRelevantNode = nodesGildedWader[Rarity.lastNode]
		end
		if Rarity.isFishing and Rarity.isOpening and isRelevantNode then
			local names = { "Gilded Wader" }
			Rarity:Debug("Detected Opening on " .. Rarity.lastNode .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.pets[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Zovaal's Vault (The Maw, Shadowlands treasure for Personal Ball and Chain & Jailer's Cage
		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Zovaal's Vault"]) then
			local names = { "Personal Ball and Chain", "Jailer's Cage" }
			Rarity:Debug("Detected Opening on " .. L["Zovaal's Vault"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Pile of Coins
		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Pile of Coins"]) then
			local names = { "Armored Vaultbot" }
			Rarity:Debug("Detected Opening on " .. L["Pile of Coins"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Glimmering Treasure Chest
		if
			Rarity.isFishing
			and Rarity.isOpening
			and Rarity.lastNode
			and (Rarity.lastNode == L["Glimmering Treasure Chest"])
			and select(8, GetInstanceInfo()) == 1626
		then -- Player is in Withered Army scenario and looted the reward chest
			local bigChest = false
			for _, slot in pairs(GetLootInfo()) do
				if slot.item == L["Ancient Mana"] and slot.quantity == 100 then
					bigChest = true
				end
			end

			if bigChest == true then
				self:Debug("Detected " .. Rarity.lastNode .. ": Adding toy drop attempts")
				local names = {
					"Arcano-Shower",
					"Displacer Meditation Stone",
					"Kaldorei Light Globe",
					"Unstable Powder Box",
					"Wisp in a Bottle",
					"Ley Spider Eggs",
				}
				for _, name in pairs(names) do
					local v = self.db.profile.groups.items[name]
					if v and type(v) == "table" and v.enabled ~= false then
						if v.attempts == nil then
							v.attempts = 1
						else
							v.attempts = v.attempts + 1
						end
						self:OutputAttempts(v)
					end
				end

				local v = self.db.profile.groups.mounts["Torn Invitation"]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		if
			Rarity.isFishing
			and Rarity.isOpening
			and Rarity.lastNode
			and (Rarity.lastNode == L["Mawsworn Supply Chest"])
		then
			local names = { "Spectral Mawrat's Tail" }
			Rarity:Debug("Detected Opening on " .. L["Mawsworn Supply Chest"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name] or self.db.profile.groups.mounts[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Sandworn Chest"]) then
			local names = { "Makaris's Satchel of Mines" }
			Rarity:Debug("Detected Opening on " .. L["Sandworn Chest"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening Expedition Scout's Pack (Verdant Skitterfly mount in Dragonflight)
		if
			Rarity.isFishing
			and Rarity.isOpening
			and Rarity.lastNode
			and (Rarity.lastNode == L["Expedition Scout's Pack"])
		then
			local names = { "Verdant Skitterfly" }
			Rarity:Debug("Detected Opening on " .. L["Expedition Scout's Pack"] .. " (method = SPECIAL)")
			-- This mount has a prerequisite to drop. Renown 25 with Dragonscale Expedition
			if GetCurrentRenownLevel(CONSTANTS.FACTION_IDS.DRAGONSCALE_EXPEDITION) >= 25 then
				for _, name in pairs(names) do
					local v = self.db.profile.groups.mounts[name]
					if v and type(v) == "table" and v.enabled ~= false then
						if v.attempts == nil then
							v.attempts = 1
						else
							v.attempts = v.attempts + 1
						end
						self:OutputAttempts(v)
					end
				end
			end
		end

		if Rarity.isOpening and Rarity.lastNode and Rarity.lastNode == L["Awakened Cache"] then
			Rarity:Debug("Detected Opening on " .. Rarity.lastNode .. " (method = SPECIAL)")
			addAttemptForItem("Machine Defense Unit 1-11", "mounts")
		end

		-- Handle opening Opera Chest (Holoviewers)
		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Opera Chest"]) then
			local names = { "Holoviewer: The Timeless One", "Holoviewer: The Lady of Dreams" }
			Rarity:Debug("Detected Opening on " .. L["Opera Chest"] .. " (method = SPECIAL)")
			for _, name in pairs(names) do
				local v = self.db.profile.groups.items[name]
				if v and type(v) == "table" and v.enabled ~= false then
					if v.attempts == nil then
						v.attempts = 1
					else
						v.attempts = v.attempts + 1
					end
					self:OutputAttempts(v)
				end
			end
		end

		-- Handle opening various chests in Zskera Vaults
		local vaultPets = {
			"Bunbo",
			"Berylmane",
			"Brightfeather",
		}
		local vaultToys = {
			"Obsidian Battle Horn",
		}
		local vaultChests = {
			[L["Chain-Bound Strongbox"]] = true,
			[L["Crystal Basket"]] = true,
			[L["Hardened Chest"]] = true,
			[L["Mindless Slime"]] = true,
			[L["Searing Chest"]] = true,
			[L["Hardened Strongbox"]] = true,
			[L["Cart of Crushed Stone"]] = true,
			[L["Hardshell Chest"]] = true,
			[L["Titan Coffer"]] = true,
			[L["Void-Bound Strongbox"]] = true,
			[L["Inert Goo"]] = true,
			[L["Mysterious Chest"]] = true,
			[L["Supply Trunk"]] = true,
			[L["Shattered Crystals"]] = true,
			[L["Chest of Ice"]] = true,
			[L["Forgotten Lockbox"]] = true,
			[L["Slimy Goo"]] = true,
			[L["Wind-Bound Strongbox"]] = true,
			[L["Spun Webs"]] = true,
			[L["Obsidian Grand Cache"]] = true,
			[L["Frozen Coffer"]] = true,
		}
		if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and vaultChests[Rarity.lastNode] then
			Rarity:Debug("Detected Opening on " .. Rarity.lastNode .. " (method = SPECIAL)")
			for _, itemName in ipairs(vaultToys) do
				addAttemptForItem(itemName, "items")
			end
			for _, itemName in ipairs(vaultPets) do
				addAttemptForItem(itemName, "pets")
			end
		end

		-- HANDLE FISHING
		if Rarity.isFishing and Rarity.isOpening == false then
			if Rarity.isPool then
				self:Debug("Successfully fished from a pool")
			else
				self:Debug("Successfully fished")
			end
			if
				Rarity.fishzones[tostring(GetBestMapForUnit("player"))]
				or Rarity.fishzones[zone]
				or Rarity.fishzones[subzone]
				or Rarity.fishzones[zone_t]
				or Rarity.fishzones[subzone_t]
			then
				-- We're interested in fishing in this zone; let's find the item(s) involved
				Rarity:Debug("We're interested in fishing in this zone; let's find the item(s) involved")
				for k, v in pairs(self.db.profile.groups) do
					if type(v) == "table" then
						for kk, vv in pairs(v) do
							if type(vv) == "table" then
								if vv.enabled ~= false then
									local found = false
									if
										vv.method == CONSTANTS.DETECTION_METHODS.FISHING
										and vv.zones ~= nil
										and type(vv.zones) == "table"
									then
										for kkk, vvv in pairs(vv.zones) do
											if
												vvv == tostring(GetBestMapForUnit("player"))
												or vvv == zone
												or vvv == lbz[zone]
												or vvv == subzone
												or vvv == lbsz[subzone]
												or vvv == zone_t
												or vvv == subzone_t
												or vvv == lbz[zone_t]
												or vvv == subzone
												or vvv == lbsz[subzone_t]
											then
												if (vv.requiresPool and Rarity.isPool) or not vv.requiresPool then
													Rarity:Debug(
														"Found interesting item for this zone: " .. tostring(vv.name)
													)
													found = true
												end
											end
										end
									end

									if
										vv.excludedMaps
										and type(vv.excludedMaps) == "table"
										and vv.excludedMaps[GetBestMapForUnit("player")]
									then
										Rarity:Debug(
											"The current map is excluded for item: "
												.. tostring(vv.name)
												.. ". Attempts will not be counted"
										)
										found = false
									end

									if found then
										if self:IsAttemptAllowed(vv) then
											if vv.attempts == nil then
												vv.attempts = 1
											else
												vv.attempts = vv.attempts + 1
											end
											self:OutputAttempts(vv)
										end
									end
								end
							end
						end
					end
				end
			end
		end
		if Rarity.fishingTimer then
			self:CancelTimer(Rarity.fishingTimer, true)
		end
		Rarity.fishingTimer = nil
		Rarity.isFishing = false
		Rarity.isPool = false

		-- Handle Disgusting Vat Fishing
		if -- There's no UNIT_SPELLCAST_SENT, so this will have to do
			Rarity.lastNode == L["Disgusting Vat"]
		then
			Rarity:OnDisgustingVatFished()
		end

		if Rarity.isOpening and Rarity.lastNode == L["Chest of Massive Gains"] then
			Rarity:OnChestOfMassiveGainsOpened()
		end

		-- Handle mining Elementium
		if
			Rarity.relevantSpells[Rarity.previousSpell] == "Mining"
			and (Rarity.lastNode == L["Elementium Vein"] or Rarity.lastNode == L["Rich Elementium Vein"])
		then
			Rarity:Debug("Detected Mining on " .. Rarity.lastNode .. " (method = SPECIAL)")
			local v = self.db.profile.groups.pets["Elementium Geode"]
			if v and type(v) == "table" and v.enabled ~= false then
				if v.attempts == nil then
					v.attempts = 1
				else
					v.attempts = v.attempts + 1
				end
				self:OutputAttempts(v)
			end
		end

		-- Handle skinning on Argus (Fossorial Bile Larva)
		if
			(
				Rarity.relevantSpells[Rarity.previousSpell] == "Skinning"
				or Rarity.relevantSpells[Rarity.previousSpell] == "Mother's Skinning Knife"
			) -- Skinned something
			and (
				GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.KROKUUN
				or GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.MACAREE
				or GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.ANTORAN_WASTES
			)
		then -- Player is on Argus -> Can obtain the pet from skinning creatures
			Rarity:Debug(
				"Detected skinning on Argus - Can obtain " .. L["Fossorial Bile Larva"] .. " (method = SPECIAL)"
			)
			local v = self.db.profile.groups.pets["Fossorial Bile Larva"]
			if v and type(v) == "table" and v.enabled ~= false then -- Add an attempt
				v.attempts = v.attempts ~= nil and v.attempts + 1 or 1 -- Defaults to 1 if this is the first attempt
				self:OutputAttempts(v)
			end
		end

		if Rarity.isOpening and Rarity.lastNode == L["Dreamseed Cache"] then
			Rarity:OnDreamseedCacheOpened()
		end

		-- Handle herb gathering on Argus (Fel Lasher)
		if
			Rarity.relevantSpells[Rarity.previousSpell] == "Herb Gathering" -- Gathered a herbalism node
			and (
				GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.KROKUUN
				or GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.MACAREE
				or GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.ANTORAN_WASTES
			)
		then -- Player is on Argus -> Can obtain the pet from gathering herbalism nodes
			Rarity:Debug("Detected herb gathering on Argus - Can obtain " .. L["Fel Lasher"] .. " (method = SPECIAL)")
			local v = self.db.profile.groups.pets["Fel Lasher"]
			if v and type(v) == "table" and v.enabled ~= false then -- Add an attempt
				v.attempts = v.attempts ~= nil and v.attempts + 1 or 1 -- Defaults to 1 if this is the first attempt
				self:OutputAttempts(v)
			end
		end

		-- HANDLE NORMAL NPC LOOTING
		local numItems = GetNumLootItems()

		-- Legacy support for pre-5.0 single-target looting
		local guid = Rarity:GetUnitGUID("target")
		local name = Rarity:GetUnitName("target")
		if not name or not guid then
			return
		end -- No target when looting
		if not UnitCanAttack("player", "target") then
			return
		end -- You targeted something you can't attack
		if UnitIsPlayer("target") then
			return
		end -- You targetted a player

		-- You're looting something that's alive -- this is only done for pickpocketing
		local requiresPickpocket = false
		if not UnitIsDead("target") then
			requiresPickpocket = true
		end

		-- Disallow "minus" NPCs; nothing good drops from them
		if UnitClassification(guid) == "minus" then
			return
		end -- (This doesn't actually work currently; UnitClassification needs a unit, not a GUID)

		local numChecked = 0
		self:Debug(numItems .. " slot(s) to loot")
		for slotID = 1, numItems, 1 do -- Loop through all loot slots (for AoE looting)
			local guidlist
			if GetLootSourceInfo then
				guidlist = { GetLootSourceInfo(slotID) }
			else
				guidlist = { guid }
			end
			local guidIndex
			for k, v in pairs(guidlist) do -- Loop through all NPC Rarity.guids being looted (will be 1 for single-target looting pre-5.0)
				guid = v
				if guid and type(guid) == "string" then
					self:Debug("Checking NPC guid (" .. (numChecked + 1) .. "): " .. guid)
					self:CheckNpcInterest(
						guid,
						zone,
						subzone,
						zone_t,
						subzone_t,
						Rarity.currentSpell,
						requiresPickpocket
					) -- Decide if we should increment an attempt count for this NPC
					numChecked = numChecked + 1
					-- else
					-- 	--self:Debug("Didn't check guid: "..guid or "nil")
				end -- Loop through all NPC GUIDs being looted (will be 1 for single-target looting pre-5.0)
			end -- Haven't seen this corpse yet
		end -- Loop through all loot slots (for AoE looting)

		-- If we failed to scan anything, scan the current target
		if numChecked <= 0 then
			self:CheckNpcInterest(Rarity:GetUnitGUID("target"), zone, subzone, zone_t, subzone_t, Rarity.currentSpell)
		end

		-- Scan the loot to see if we found something we're looking for
		for slotID = 1, numItems, 1 do
			local _, _, qty = GetLootSlotInfo(slotID)
			if (qty or 0) > 0 then -- Coins have quantity of 0, so skip those
				local itemLink = GetLootSlotLink(slotID)
				if itemLink then
					local _, itemId = strsplit(":", itemLink)
					itemId = tonumber(itemId)
					if
						Rarity.items[itemId] ~= nil
						and Rarity.items[itemId].method ~= CONSTANTS.DETECTION_METHODS.COLLECTION
					then
						self:OnItemFound(itemId, Rarity.items[itemId])
					end
				end
			end
		end

		if requiresPickpocket then
			-- Pick Pocket triggers the same loot events, but it shouldn't prevent kills from counting afterwards
			Rarity.guids[guid] = false
		end
	end
end

function Rarity:OnChestOfMassiveGainsOpened()
	Rarity:Debug("Detected Opening on Chest of Massive Gains")

	local hasOpenedChestToday = IsQuestFlaggedCompleted(75325)
	if hasOpenedChestToday then
		Rarity:Debug("Skipping this attempt (loot lockout for Chest of Massive Gains is active)")
		return
	end

	local wasRequiredAuraFoundOnPlayer = false
	AuraUtil.ForEachAura("player", "HELPFUL", nil, function(_, _, _, _, _, _, _, _, _, spellID)
		if spellID == CONSTANTS.AURAS.ROCKS_ON_THE_ROCKS then
			wasRequiredAuraFoundOnPlayer = true
		end
	end)

	if not wasRequiredAuraFoundOnPlayer then
		Rarity:Debug(format("Required aura %s NOT found on player", L["Rocks on the Rocks"]))
		return
	end

	addAttemptForItem("Brul", "pets")
end

function Rarity:OnDisgustingVatFished()
	local hasFishedEmmahThisWeek = IsQuestFlaggedCompleted(75488)
	if hasFishedEmmahThisWeek then
		self:Debug("Skipping this fishing attempt (loot lockout for Emmah is active)")
		return
	end

	self:Debug("Detected fishing on Disgusting Vat (method = SPECIAL)")
	addAttemptForItem("Emmah", "pets")
end

local dreamseedMounts = {
	"Reins of the Winter Night Dreamsaber",
	"Reins of the Snowfluff Dreamtalon",
	"Reins of the Evening Sun Dreamsaber",
	"Reins of the Blossoming Dreamstag",
	"Reins of the Springtide Dreamtalon",
	"Reins of the Morning Flourish Dreamsaber",
	"Reins of the Rekindled Dreamstag",
}

function Rarity:OnDreamseedCacheOpened()
	Rarity:Debug("Detected Opening on Dreamseed Cache")
	for _, mount in ipairs(dreamseedMounts) do
		addAttemptForItem(mount, "mounts")
	end
end

function R:OnPlayerInteractionFrameShow(event, playerInteractionTypeID)
	if playerInteractionTypeID == Enum.PlayerInteractionType.Banker then
		Rarity.isBankOpen = true
	elseif playerInteractionTypeID == Enum.PlayerInteractionType.GuildBanker then
		Rarity.isGuildBankOpen = true
	elseif playerInteractionTypeID == Enum.PlayerInteractionType.Auctioneer then
		Rarity.isAuctionHouseOpen = true
	elseif playerInteractionTypeID == Enum.PlayerInteractionType.TradePartner then
		Rarity.isTradeWindowOpen = true
	elseif playerInteractionTypeID == Enum.PlayerInteractionType.MailInfo then
		Rarity.isMailboxOpen = true
	end
end

function R:OnPlayerInteractionFrameHide(event, playerInteractionTypeID)
	if playerInteractionTypeID == Enum.PlayerInteractionType.Banker then
		Rarity.isBankOpen = false
	elseif playerInteractionTypeID == Enum.PlayerInteractionType.GuildBanker then
		Rarity.isGuildBankOpen = false
	elseif playerInteractionTypeID == Enum.PlayerInteractionType.Auctioneer then
		Rarity.isAuctionHouseOpen = false
	elseif playerInteractionTypeID == Enum.PlayerInteractionType.TradePartner then
		Rarity.isTradeWindowOpen = false
	elseif playerInteractionTypeID == Enum.PlayerInteractionType.MailInfo then
		Rarity.isMailboxOpen = false
	end
end

Rarity.EventHandlers = EventHandlers
return EventHandlers
