local SlickUI = _G.SlickUI
if not SlickUI or not SlickUI.Fishing then return end

local Ledger = { CharacterScope = "current", ZoneScope = "current", Period = "session", Rows = {} }
SlickUI.FishingLedger = Ledger

local function Settings() return SlickUI.DB.fishing end
local function CharacterKey() return (UnitName("player") or "Unknown") .. " - " .. (GetRealmName() or "Unknown Realm") end
local function CurrentMap() return C_Map.GetBestMapForUnit("player") or 0 end

-- Bobber toys whose names do not necessarily contain the word "Bobber".
-- The dynamic Toy Box scan below also picks up future owned toys with Bobber
-- in their localized name, so this list only needs to cover the Crate series.
local CRATE_BOBBER_TOYS = {
	[142528] = true, [142529] = true, [142530] = true, [142531] = true,
	[142532] = true, [143662] = true,
	[147307] = true, [147308] = true, [147309] = true,
	[147310] = true, [147311] = true, [147312] = true,
}

local KNOWN_BOBBER_TOYS = {
	142528, 142529, 142530, 142531, 142532, 143662,
	147307, 147308, 147309, 147310, 147311, 147312,
	180993, -- Bat Visage Bobber
	202207, -- Reusable Oversized Bobber
	237345, 237346, 237347, -- Undermine bobbers
}

local function ToyInfo(itemID)
	if not C_ToyBox or not C_ToyBox.GetToyInfo then return nil end
	local _, name, icon = C_ToyBox.GetToyInfo(itemID)
	return name, icon
end

function Ledger:GetOwnedBobbers()
	local candidates, seen = {}, {}
	local function Add(itemID)
		itemID = tonumber(itemID)
		if not itemID or seen[itemID] or not PlayerHasToy or not PlayerHasToy(itemID) then return end
		local name, icon = ToyInfo(itemID)
		if not name then return end
		if not CRATE_BOBBER_TOYS[itemID] and not name:lower():find("bobber", 1, true) then return end
		seen[itemID] = true
		candidates[#candidates + 1] = { itemID = itemID, name = name, icon = icon }
	end
	for _, itemID in ipairs(KNOWN_BOBBER_TOYS) do Add(itemID) end
	if C_ToyBox and C_ToyBox.GetToyIDs then
		local ok, toyIDs = pcall(C_ToyBox.GetToyIDs)
		if ok and type(toyIDs) == "table" then
			for _, itemID in ipairs(toyIDs) do Add(itemID) end
		end
	end
	table.sort(candidates, function(a, b) return a.name < b.name end)
	return candidates
end

function Ledger:GetSelectedBobber()
	local owned = self:GetOwnedBobbers()
	if #owned == 0 then return nil, owned end
	local selected = tonumber(Settings().bobberToyID) or 0
	if selected ~= 0 then
		for _, toy in ipairs(owned) do
			if toy.itemID == selected then return toy, owned end
		end
		Settings().bobberToyID = 0
	end
	local current
	for _, toy in ipairs(owned) do
		if toy.itemID == self.RandomBobberID then current = toy break end
	end
	if not current then
		current = owned[math.random(#owned)]
		self.RandomBobberID = current.itemID
	end
	return current, owned
end

function Ledger:RefreshBobber()
	if not self.Window or not self.Window.BobberButton then return end
	local toy = self:GetSelectedBobber()
	local button = self.Window.BobberButton
	button.ToyID = toy and toy.itemID or nil
	button.Icon:SetTexture(toy and toy.icon or 134400)
	button.Icon:SetDesaturated(toy == nil)
	-- The bobber is a secure item-use button. Protected button state and
	-- attributes may only be changed outside combat.
	if not InCombatLockdown() then
		button:SetEnabled(toy ~= nil)
		button:SetAttribute("type1", toy and "item" or nil)
		button:SetAttribute("item1", toy and ("item:" .. toy.itemID) or nil)
	end
	local selected = tonumber(Settings().bobberToyID) or 0
	UIDropDownMenu_SetText(self.Window.BobberDropdown,
		toy and (selected == 0 and "Random Bobber" or toy.name) or "No bobber toys known")
end

local function ItemDisplay(itemID)
	local name, _, quality = C_Item.GetItemInfo(itemID)
	if not name then C_Item.RequestLoadItemDataByID(itemID) end
	name = name or C_Item.GetItemNameByID(itemID) or ("Item " .. itemID)
	local color = quality and ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[quality]
	return name, color
end

local function FormatDuration(seconds)
	seconds = math.max(0, math.floor(seconds or 0))
	local hours = math.floor(seconds / 3600)
	local minutes = math.floor((seconds % 3600) / 60)
	return string.format("%02d:%02d:%02d", hours, minutes, seconds % 60)
end

function Ledger:UpdatePersistentSummary()
	if not self.Window then return end
	local allTime = SlickUI.Fishing:GetAllTimeTotal(false, false)
	self.Window.PersistentSummary:SetText(string.format(
		"Active Session: |cff55bfff%s|r    Lifetime Fishing: |cffb58cff%s|r    All-Time Catches: |cff42df72%d|r",
		FormatDuration(SlickUI.Fishing:GetSessionSeconds()), FormatDuration(SlickUI.Fishing:GetLifetimeSeconds(false)), allTime
	))
end

function Ledger:RefreshLure()
	if not self.Window or not self.Window.LureButton then return end
	local button = self.Window.LureButton
	local itemID, link, icon, count, bonus, bagID, slotID = SlickUI.Fishing:FindAvailableLure()
	local applied = SlickUI.Fishing:HasPoleLure()
	button.ItemLink = link
	button.Icon:SetTexture(icon or 134335)
	button.Count:SetText(itemID and count and count > 1 and count or "")
	self.Window.LureStatus:SetText(applied and "|cff42df72Lure applied|r" or (itemID and (bonus > 0 and ("|cffffb347Best lure: +" .. bonus .. "|r") or "|cffffb347Apply lure|r") or "|cff999999No lure available|r"))
	if not InCombatLockdown() then
		button:SetShown(itemID ~= nil)
		button:SetAttribute("type1", itemID and "item" or nil)
		button:SetAttribute("item1", itemID and (bagID .. " " .. slotID) or nil)
	end
end

function Ledger:Collect()
	local items, total = {}, 0
	local character, mapID = CharacterKey(), CurrentMap()
	local source = self.Period == "session" and SlickUI.Fishing.SessionEntries or Settings().entries
	for _, entry in ipairs(source) do
		if (self.CharacterScope == "all" or entry.character == character) and (self.ZoneScope == "all" or entry.mapID == mapID) then
			local item = items[entry.itemID]
			if not item then
				item = { itemID = entry.itemID, quantity = 0, zones = {} }
				items[entry.itemID] = item
			end
			item.quantity = item.quantity + entry.quantity
			item.zones[entry.zone or "Unknown Zone"] = true
			total = total + entry.quantity
		end
	end
	local sorted = {}
	for _, item in pairs(items) do
		local zones = {}
		for zone in pairs(item.zones) do zones[#zones + 1] = zone end
		table.sort(zones)
		item.zoneText = table.concat(zones, ", ")
		sorted[#sorted + 1] = item
	end
	table.sort(sorted, function(a, b) if a.quantity == b.quantity then return a.itemID < b.itemID end return a.quantity > b.quantity end)
	return sorted, total
end

function Ledger:Refresh()
	if not self.Window then return end
	local data, total = self:Collect()
	local elapsed = math.max(1, SlickUI.Fishing:GetSessionSeconds())
	local rate = SlickUI.Fishing.SessionTotal * 3600 / elapsed
	self:UpdatePersistentSummary()
	self:RefreshLure()
	self:RefreshBobber()
	self.Window.Summary:SetText(string.format("Current View: |cff42df72%d|r caught    Unique: |cff55bfff%d|r    Session Rate: %.1f/hour", total, #data, rate))
	self.Window.Character:SetText(self.CharacterScope == "all" and "All Characters" or "Current Character")
	self.Window.Zone:SetText(self.ZoneScope == "all" and "All Zones" or (GetZoneText() or "Current Zone"))
	self.Window.Period:SetText(self.Period == "session" and "This Session" or "All Time")
	for _, row in ipairs(self.Rows) do row:Hide() end
	for index, entry in ipairs(data) do
		local row = self.Rows[index]
		if not row then
			row = CreateFrame("Frame", nil, self.Window.List)
			row:SetHeight(34); row:SetPoint("TOPLEFT", 0, -((index - 1) * 35)); row:SetPoint("RIGHT")
			row.IconButton = CreateFrame("Button", nil, row)
			row.IconButton:SetSize(30, 30); row.IconButton:SetPoint("LEFT", 2, 0)
			row.Icon = row.IconButton:CreateTexture(nil, "ARTWORK"); row.Icon:SetAllPoints(); row.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
			row.IconButton:SetScript("OnEnter", function(self)
				if not self.ItemID then return end
				GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
				GameTooltip:SetHyperlink("item:" .. self.ItemID)
				GameTooltip:Show()
			end)
			row.IconButton:SetScript("OnLeave", GameTooltip_Hide)
			row.Name = row:CreateFontString(nil, "ARTWORK", "GameFontHighlight"); row.Name:SetPoint("LEFT", row.IconButton, "RIGHT", 9, 0); row.Name:SetWidth(250); row.Name:SetJustifyH("LEFT"); row.Name:SetWordWrap(false)
			row.ZoneName = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall"); row.ZoneName:SetPoint("LEFT", 330, 0); row.ZoneName:SetWidth(230); row.ZoneName:SetJustifyH("LEFT"); row.ZoneName:SetWordWrap(false); row.ZoneName:SetTextColor(0.72, 0.74, 0.80)
			row.Count = row:CreateFontString(nil, "ARTWORK", "GameFontNormal"); row.Count:SetPoint("RIGHT", -8, 0)
			self.Rows[index] = row
		end
		local name, color = ItemDisplay(entry.itemID)
		row.Icon:SetTexture(C_Item.GetItemIconByID(entry.itemID) or 134400)
		row.IconButton.ItemID = entry.itemID
		row.Name:SetText(name)
		if color then row.Name:SetTextColor(color.r, color.g, color.b) else row.Name:SetTextColor(1, 1, 1) end
		row.ZoneName:SetText(self.ZoneScope == "all" and entry.zoneText or "")
		row.ZoneName:SetShown(self.ZoneScope == "all")
		row.Count:SetText(entry.quantity)
		row:Show()
	end
	self.Window.List:SetHeight(math.max(1, #data * 35))
	self.Window.Empty:SetShown(#data == 0)
end

function Ledger:CreateWindow()
	if self.Window then return end
	local window = CreateFrame("Frame", "SlickUIFishingLedger", UIParent, "BackdropTemplate")
	window:SetSize(760, 650); window:SetPoint("CENTER"); window:SetFrameStrata("DIALOG"); window:SetClampedToScreen(true)
	window:SetMovable(true); window:EnableMouse(true); window:RegisterForDrag("LeftButton")
	window:SetScript("OnDragStart", window.StartMoving); window:SetScript("OnDragStop", window.StopMovingOrSizing)
	window:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 2 })
	window:SetBackdropColor(0.015, 0.018, 0.025, 0.97); window:SetBackdropBorderColor(1, 0.58, 0, 1)
	table.insert(UISpecialFrames, window:GetName())
	local title = window:CreateFontString(nil, "ARTWORK", "GameFontNormalHuge"); title:SetPoint("TOPLEFT", 20, -18); title:SetText("SlickUI Fishing Ledger")
	local close = CreateFrame("Button", nil, window, "UIPanelCloseButton"); close:SetPoint("TOPRIGHT", -4, -4)
	local journalButton = CreateFrame("Button", nil, window, "BackdropTemplate")
	journalButton:SetSize(34, 34); journalButton:SetPoint("TOPRIGHT", -48, -88)
	journalButton:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 }); journalButton:SetBackdropBorderColor(1, 0.58, 0, 1)
	journalButton.Icon = journalButton:CreateTexture(nil, "ARTWORK"); journalButton.Icon:SetAllPoints(); journalButton.Icon:SetTexture(136245); journalButton.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
	journalButton:SetScript("OnClick", function()
		if InCombatLockdown() then
			SlickUI:Print("The Fishing Journal cannot be opened during combat.")
			return
		end
		local profession = Enum.Profession and Enum.Profession.Fishing
		local skillLineID = profession and C_TradeSkillUI.GetProfessionSkillLineID(profession)
		if skillLineID then C_TradeSkillUI.OpenTradeSkill(skillLineID) else ToggleProfessionsBook() end
	end)
	journalButton:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_RIGHT"); GameTooltip:SetText("Open Fishing Journal")
		GameTooltip:AddLine("Click to show your equipped fishing pole.", 0.65, 0.68, 0.75); GameTooltip:Show()
	end)
	journalButton:SetScript("OnLeave", GameTooltip_Hide)
	window.JournalButton = journalButton

	local bobberButton = CreateFrame("Button", "SlickUIFishingBobberButton", window, "SecureActionButtonTemplate,BackdropTemplate")
	bobberButton:SetSize(34, 34); bobberButton:SetPoint("TOPRIGHT", -48, -42)
	bobberButton:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 })
	bobberButton:SetBackdropBorderColor(1, 0.58, 0, 1)
	bobberButton.Icon = bobberButton:CreateTexture(nil, "ARTWORK")
	bobberButton.Icon:SetAllPoints(); bobberButton.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
	bobberButton:SetAttribute("useOnKeyDown", false)
	bobberButton:RegisterForClicks("AnyUp", "AnyDown")
	bobberButton:SetScript("PostClick", function(self)
		if not self.ToyID or InCombatLockdown() then return end
		if (tonumber(Settings().bobberToyID) or 0) == 0 then Ledger.RandomBobberID = nil end
		C_Timer.After(0.2, function() Ledger:RefreshBobber() end)
	end)
	bobberButton:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
		if self.ToyID then
			GameTooltip:SetHyperlink("item:" .. self.ToyID)
			GameTooltip:AddLine("Click to use this fishing bobber toy.", 0.65, 0.68, 0.75)
		else
			GameTooltip:SetText("No fishing bobber toys learned")
		end
		GameTooltip:Show()
	end)
	bobberButton:SetScript("OnLeave", GameTooltip_Hide)
	window.BobberButton = bobberButton

	local bobberLabel = window:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
	local bobberDropdown = CreateFrame("Frame", "SlickUIFishingBobberDropdown", window, "UIDropDownMenuTemplate")
	UIDropDownMenu_SetWidth(bobberDropdown, 225)
	bobberDropdown:SetPoint("RIGHT", bobberButton, "LEFT", -3, 0)
	bobberLabel:SetPoint("BOTTOMRIGHT", bobberDropdown, "TOPRIGHT", -18, 3)
	bobberLabel:SetText("Bobber")
	UIDropDownMenu_Initialize(bobberDropdown, function()
		local info = UIDropDownMenu_CreateInfo()
		info.text = "Random Bobber"; info.value = 0
		info.minWidth = 250
		info.checked = (tonumber(Settings().bobberToyID) or 0) == 0
		info.func = function()
			Settings().bobberToyID = 0; Ledger.RandomBobberID = nil; Ledger:RefreshBobber()
		end
		UIDropDownMenu_AddButton(info)
		for _, toy in ipairs(Ledger:GetOwnedBobbers()) do
			local toyID, toyName, toyIcon = toy.itemID, toy.name, toy.icon
			info = UIDropDownMenu_CreateInfo()
			info.text = toyName; info.value = toyID; info.icon = toyIcon
			info.minWidth = 250
			info.checked = (tonumber(Settings().bobberToyID) or 0) == toyID
			info.func = function()
				Settings().bobberToyID = toyID; Ledger:RefreshBobber()
			end
			UIDropDownMenu_AddButton(info)
		end
	end)
	window.BobberDropdown = bobberDropdown

	local lureButton = CreateFrame("Button", "SlickUIFishingLureButton", window, "SecureActionButtonTemplate,BackdropTemplate")
	lureButton:SetSize(34, 34); lureButton:SetPoint("RIGHT", journalButton, "LEFT", -8, 0)
	lureButton:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 })
	lureButton:SetBackdropBorderColor(1, 0.58, 0, 1)
	lureButton.Icon = lureButton:CreateTexture(nil, "ARTWORK"); lureButton.Icon:SetAllPoints(); lureButton.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
	lureButton.Count = lureButton:CreateFontString(nil, "OVERLAY", "NumberFontNormal"); lureButton.Count:SetPoint("BOTTOMRIGHT", -2, 2)
	lureButton:SetAttribute("useOnKeyDown", false)
	lureButton:RegisterForClicks("AnyUp", "AnyDown")
	lureButton:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
		if self.ItemLink then GameTooltip:SetHyperlink(self.ItemLink) else GameTooltip:SetText("No fishing lure available") end
		GameTooltip:AddLine("Click to apply this lure.", 0.65, 0.68, 0.75)
		GameTooltip:Show()
	end)
	lureButton:SetScript("OnLeave", GameTooltip_Hide)
	window.LureButton = lureButton
	local lureStatus = window:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
	lureStatus:SetPoint("RIGHT", lureButton, "LEFT", -8, 0); window.LureStatus = lureStatus
	local lureHelp = window:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
	lureHelp:SetPoint("TOPRIGHT", journalButton, "BOTTOMRIGHT", 0, -6); lureHelp:SetWidth(360); lureHelp:SetJustifyH("RIGHT"); lureHelp:SetWordWrap(true)
	lureHelp:SetText("Apply lure: click lure icon, open the Fishing Journal, then click your fishing pole.")
	window.LureHelp = lureHelp
	local persistentSummary = window:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
	persistentSummary:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -10)
	window.PersistentSummary = persistentSummary
	local summary = window:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
	summary:SetPoint("TOPLEFT", persistentSummary, "BOTTOMLEFT", 0, -7)
	window.Summary = summary

	local function FilterButton(x, width, click)
		local button = CreateFrame("Button", nil, window, "UIPanelButtonTemplate")
		button:SetSize(width, 25); button:SetPoint("TOPLEFT", x, -142); button:SetScript("OnClick", click); return button
	end
	window.Character = FilterButton(20, 170, function() Ledger.CharacterScope = Ledger.CharacterScope == "all" and "current" or "all"; Ledger:Refresh() end)
	window.Zone = FilterButton(200, 150, function() Ledger.ZoneScope = Ledger.ZoneScope == "all" and "current" or "all"; Ledger:Refresh() end)
	window.Period = FilterButton(360, 140, function() Ledger.Period = Ledger.Period == "session" and "all" or "session"; Ledger:Refresh() end)
	local reset = FilterButton(510, 145, function()
		local wasActive = SlickUI.Fishing.ActiveStartedAt ~= nil
		SlickUI.Fishing:CommitActiveTime()
		wipe(SlickUI.Fishing.Session)
		wipe(SlickUI.Fishing.SessionEntries)
		SlickUI.Fishing.SessionTotal = 0
		SlickUI.Fishing.SessionSeconds = 0
		if wasActive then SlickUI.Fishing:StartActiveTime() end
		Ledger:Refresh()
	end); reset:SetText("Reset Session")

	local scroll = CreateFrame("ScrollFrame", nil, window, "UIPanelScrollFrameTemplate")
	scroll:SetPoint("TOPLEFT", 20, -182); scroll:SetPoint("BOTTOMRIGHT", -38, 20)
	local list = CreateFrame("Frame", nil, scroll); list:SetWidth(610); list:SetHeight(1); scroll:SetScrollChild(list); window.List = list
	local empty = list:CreateFontString(nil, "ARTWORK", "GameFontDisableLarge"); empty:SetPoint("TOP", 0, -60); empty:SetText("No fishing catches recorded for this view."); window.Empty = empty
	self.Window = window
	local events = CreateFrame("Frame")
	events:RegisterEvent("GET_ITEM_INFO_RECEIVED")
	events:RegisterEvent("BAG_UPDATE_DELAYED")
	events:RegisterEvent("PLAYER_REGEN_ENABLED")
	events:SetScript("OnEvent", function()
		if Ledger.Window and Ledger.Window:IsShown() then Ledger:Refresh() end
	end)
	self.ItemEvents = events
	window:SetScript("OnUpdate", function(_, elapsed)
		Ledger.TimerElapsed = (Ledger.TimerElapsed or 0) + elapsed
		if Ledger.TimerElapsed >= 1 then
			Ledger.TimerElapsed = 0
			Ledger:UpdatePersistentSummary()
		end
	end)
end

function Ledger:Show()
	self:CreateWindow(); self:Refresh(); self.Window:Show(); self.Window:Raise()
end

function Ledger:Toggle()
	if self.Window and self.Window:IsShown() then self.Window:Hide() else self:Show() end
end
