local _, GF = ...

local FavoriteInstances = {}
GF.FavoriteInstances = FavoriteInstances

local ROOT_KEY = "favorite_instances"
local ACTIVITY_IDENTITY_PREFIX = "activity:"
local PRESET_IDENTITY_PREFIX = "favorite:"

local function currentTabID()
	local mainFrame = GF.MainFrame
	return mainFrame and mainFrame.GetCurrentTabID
		and mainFrame:GetCurrentTabID() or GF.TAB_BROWSE
end

local function records()
	local db = GF.GetDB and GF.GetDB() or nil
	if not db then
		return {}
	end
	if type(db.favoriteInstances) ~= "table" then
		db.favoriteInstances = {}
	end
	return db.favoriteInstances
end

local function findRecord(identity)
	for index, entry in ipairs(records()) do
		if entry.identity == identity then
			return entry, index
		end
	end
	return nil, nil
end

local function recordKind(record)
	if record and record.kind == "preset" then
		return "preset"
	end
	if record and record.kind == "activity" then
		return "activity"
	end
	local identity = record and record.identity
	if type(identity) == "string"
		and identity:match("^" .. ACTIVITY_IDENTITY_PREFIX .. "%d+:%d+$")
	then
		return "activity"
	end
	return "instance"
end

local function cleanText(value)
	if type(value) ~= "string" then
		return nil
	end
	value = value:gsub("^%s+", ""):gsub("%s+$", "")
	return value ~= "" and value or nil
end

local function activityIDsForRecord(record)
	local categoryID = tonumber(record and record.categoryID)
	local activityID = tonumber(record and record.activityID)
	if not (categoryID and activityID) and record and type(record.identity) == "string" then
		local categoryText, activityText = record.identity:match(
			"^" .. ACTIVITY_IDENTITY_PREFIX .. "(%d+):(%d+)$")
		categoryID = categoryID or tonumber(categoryText)
		activityID = activityID or tonumber(activityText)
	end
	return categoryID, activityID
end

local function nodeLabel(node)
	local label = node and node.label
	if type(label) == "string" and label ~= "" then
		return label
	end
	local info = node and node.activityInfo
	local fullName = info and info.fullName
	if type(fullName) == "string" and fullName ~= "" then
		return fullName
	end
	local shortName = info and info.shortName
	if type(shortName) == "string" and shortName ~= "" then
		return shortName
	end
	return nil
end

local function activityFullName(node, activityID)
	local info = node and node.activityInfo
	local reader = C_LFGList and C_LFGList.GetActivityInfoTable
	if info == nil and activityID ~= nil and type(reader) == "function" then
		local ok, value = pcall(reader, activityID)
		info = ok and value or nil
	end
	return cleanText(info and info.fullName)
		or cleanText(info and info.shortName)
		or cleanText(nodeLabel(node)), info
end

local function nodeBreadcrumb(node)
	local navTree = GF.NavTree
	local finder = navTree and navTree.FindNodePathByKey
	if type(finder) ~= "function" or not (node and node.key) then
		return nil
	end
	local _, path = navTree:FindNodePathByKey(node.key)
	local labels = {}
	for index = 1, math.max(0, #(path or {}) - 1) do
		local label = nodeLabel(path[index])
		if label then
			labels[#labels + 1] = label
		end
	end
	return #labels > 0 and table.concat(labels, " · ") or nil
end

local function nodeAllowedForTab(node, tabID)
	if not node or node.disabled then
		return false
	end
	local workspace = GF.LFGWorkspaceView
	if workspace and workspace.IsNodeAllowed
		and not workspace:IsNodeAllowed(node)
	then
		return false
	end
	if tabID == GF.TAB_CREATE then
		if node.browseOnly then
			return false
		end
		if workspace and workspace.CanCreateSelection then
			return workspace:CanCreateSelection(node) == true
		end
		local canCreate = GF.NavData and GF.NavData.CanCreateFromNode
		return type(canCreate) == "function" and canCreate(node) == true
	end
	local accepts = GF.NavData and GF.NavData.AcceptsBrowseSelection
	return type(accepts) == "function" and accepts(node) == true
end

local function directActivityNode(record)
	local categoryID, activityID = activityIDsForRecord(record)
	local finder = GF.NavData and GF.NavData.FindNodeByActivityID
	local node = type(finder) == "function" and finder(activityID) or nil
	if not node then
		return nil
	end
	local info = node.activityInfo
	local currentCategoryID = tonumber(node.categoryID)
		or tonumber(info and info.categoryID)
	if currentCategoryID ~= categoryID then
		return nil
	end
	return node
end

local function difficultyMergeKey(node)
	local info = node and node.activityInfo
	local reader = C_LFGList and C_LFGList.GetActivityInfoTable
	if info == nil and node and node.activityID and type(reader) == "function" then
		local ok, value = pcall(reader, node.activityID)
		info = ok and value or nil
	end
	local merge = GF.ActivityInfo
		and GF.ActivityInfo.GetActivityDifficultyMergeKey
	return type(merge) == "function" and merge(info) or nil
end

local function resolvePresetFallback(record, tabID)
	local instanceIdentity = cleanText(record and record.instanceIdentity)
	local expectedDifficulty = cleanText(record and record.difficultyKey)
	if not (instanceIdentity and expectedDifficulty) then
		return nil
	end
	local search = GF.QuickSearch
	local entry = search and search.GetEntryByIdentity
		and search:GetEntryByIdentity(instanceIdentity) or nil
	local children = entry and search.BuildDifficultyNodes
		and search:BuildDifficultyNodes(entry, tabID) or {}
	local expectedCategory = tonumber(record.categoryID)
	for _, proxy in ipairs(children) do
		local target = proxy.quickSearchTarget
		local targetCategory = tonumber(target and target.categoryID)
			or tonumber(target and target.activityInfo
				and target.activityInfo.categoryID)
		if targetCategory == expectedCategory
			and difficultyMergeKey(target) == expectedDifficulty
		then
			return target
		end
	end
	return nil
end

local function resolveDirectRecord(record, tabID)
	local node = directActivityNode(record)
	if not node and recordKind(record) == "preset" then
		node = resolvePresetFallback(record, tabID)
	end
	return node
end

local function favoriteRoot()
	for _, root in ipairs(GF.navTree or {}) do
		if root and root.key == ROOT_KEY then
			return root
		end
	end
	return nil
end

local function refreshOpenFavoritePanel(root)
	local flyout = GF.NavFlyout
	if root and flyout and flyout.IsNodeOpen and flyout:IsNodeOpen(root)
		and flyout.ReanchorOpenPanels
	then
		flyout:ReanchorOpenPanels()
	end
end

local function notifyChanged()
	local root = FavoriteInstances:RefreshRoot(currentTabID())
	if GF.NavTree and GF.NavTree.Refresh then
		GF.NavTree:Refresh()
	end
	refreshOpenFavoritePanel(root)
end

local function nextPresetIdentity()
	local maximum = 0
	for _, record in ipairs(records()) do
		local numeric = type(record.identity) == "string"
			and tonumber(record.identity:match(
				"^" .. PRESET_IDENTITY_PREFIX .. "(%d+)$")) or nil
		maximum = math.max(maximum, numeric or 0)
	end
	local identity
	repeat
		maximum = maximum + 1
		identity = PRESET_IDENTITY_PREFIX .. tostring(maximum)
	until findRecord(identity) == nil
	return identity
end

local function normalizeNumber(value)
	value = tonumber(value)
	return value and value >= 0 and value or 0
end

local function capturePreset(draft)
	return {
		generalPlaystyle = tonumber(draft and draft.generalPlaystyle) or 0,
		requiredItemLevel = normalizeNumber(draft and draft.requiredItemLevel),
		requiredDungeonScore = normalizeNumber(draft and draft.requiredDungeonScore),
		requiredPvpRating = normalizeNumber(draft and draft.requiredPvpRating),
		privateGroup = draft and draft.privateGroup == true or false,
		factionRestricted = draft and draft.factionRestricted == true or false,
	}
end

local function buildPresetRecord(identity, node, draft, customName)
	local resolver = GF.NavData and GF.NavData.ResolveCreateActivityID
	local activityID = type(resolver) == "function"
		and resolver(node) or tonumber(node and node.activityID)
	local label, info = activityFullName(node, activityID)
	local categoryID = tonumber(node and node.categoryID)
		or tonumber(info and info.categoryID)
	if not (activityID and categoryID and label) then
		return nil
	end
	local entry = GF.QuickSearch and GF.QuickSearch.GetEntryForNode
		and GF.QuickSearch:GetEntryForNode(node) or nil
	return {
		identity = identity,
		kind = "preset",
		categoryID = categoryID,
		activityID = activityID,
		groupID = tonumber(node and node.groupID)
			or tonumber(info and info.groupFinderActivityGroupID),
		instanceIdentity = entry and cleanText(entry.identity) or nil,
		difficultyKey = difficultyMergeKey(node),
		label = label,
		breadcrumb = nodeBreadcrumb(node),
		customName = cleanText(customName),
		preset = capturePreset(draft),
	}
end

function FavoriteInstances:GetRecords()
	return records()
end

function FavoriteInstances:GetCount()
	return #records()
end

function FavoriteInstances:GetRecord(identity)
	return findRecord(identity)
end

function FavoriteInstances:IsFavorite(identity)
	return findRecord(identity) ~= nil
end

function FavoriteInstances:HasPreset(identity)
	local record = findRecord(identity)
	return recordKind(record) == "preset"
		and type(record.preset) == "table"
end

function FavoriteInstances:SavePreset(recordIdentity, node, draft, customName)
	if type(node) ~= "table" then
		return false, nil, "invalid"
	end
	local existing, index = findRecord(recordIdentity)
	local identity = existing and existing.identity or nextPresetIdentity()
	local replacement = buildPresetRecord(identity, node, draft, customName)
	if not replacement then
		return false, nil, "invalid"
	end
	local list = records()
	if index then
		list[index] = replacement
	else
		list[#list + 1] = replacement
	end
	notifyChanged()
	return true, identity, index and "updated" or "added"
end

function FavoriteInstances:Remove(identity)
	local _, index = findRecord(identity)
	if not index then
		return false
	end
	table.remove(records(), index)
	notifyChanged()
	return true
end

function FavoriteInstances:GetIdentityOrder()
	local order = {}
	for index, entry in ipairs(records()) do
		order[index] = entry.identity
	end
	return order
end

function FavoriteInstances:CommitOrder(identityOrder, suppressRefresh)
	if type(identityOrder) ~= "table" then
		return false
	end
	local list = records()
	if #identityOrder ~= #list then
		return false
	end
	local byIdentity = {}
	for _, entry in ipairs(list) do
		if type(entry.identity) ~= "string" or byIdentity[entry.identity] then
			return false
		end
		byIdentity[entry.identity] = entry
	end
	local ordered, seen = {}, {}
	for index, identity in ipairs(identityOrder) do
		local entry = type(identity) == "string" and byIdentity[identity] or nil
		if not entry or seen[identity] then
			return false
		end
		seen[identity] = true
		ordered[index] = entry
	end
	for index, entry in ipairs(ordered) do
		list[index] = entry
	end
	if not suppressRefresh then
		notifyChanged()
	end
	return true
end

local function orderedRecords(identityOrder)
	local list = records()
	if type(identityOrder) ~= "table" or #identityOrder ~= #list then
		return list
	end
	local byIdentity, ordered, seen = {}, {}, {}
	for _, entry in ipairs(list) do
		byIdentity[entry.identity] = entry
	end
	for index, identity in ipairs(identityOrder) do
		local entry = byIdentity[identity]
		if not entry or seen[identity] then
			return list
		end
		seen[identity] = true
		ordered[index] = entry
	end
	return ordered
end

function FavoriteInstances:BuildNodes(tabID, identityOrder)
	local list = orderedRecords(identityOrder)
	local L = GF.L or {}
	if #list == 0 then
		return {
			{
				key = ROOT_KEY .. "::empty",
				level = 1,
				label = L.FAVORITE_INSTANCE_EMPTY
					or "Add favorites from the Create Listing form",
				isLeaf = false,
				disabled = true,
				navKind = ROOT_KEY,
				favoriteEmpty = true,
			},
		}
	end

	local search = GF.QuickSearch
	local activeTabID = tabID or currentTabID()
	local resolved, labelCounts = {}, {}
	for index, record in ipairs(list) do
		local kind = recordKind(record)
		local entry = kind == "instance" and search
			and search.GetEntryByIdentity
			and search:GetEntryByIdentity(record.identity) or nil
		local activityNode = kind ~= "instance"
			and resolveDirectRecord(record, activeTabID) or nil
		if entry then
			record.label = entry.label or record.label
			record.breadcrumb = entry.breadcrumb or record.breadcrumb
		elseif activityNode then
			record.label = activityFullName(activityNode, activityNode.activityID)
				or record.label
			record.breadcrumb = nodeBreadcrumb(activityNode) or record.breadcrumb
		end
		local actualLabel = entry and entry.label
			or activityNode and activityFullName(activityNode, activityNode.activityID)
			or record.label or record.identity
		local displayLabel = cleanText(record.customName) or actualLabel
		resolved[index] = {
			record = record,
			kind = kind,
			entry = entry,
			activityNode = activityNode,
			actualLabel = actualLabel,
			displayLabel = displayLabel,
		}
		labelCounts[displayLabel] = (labelCounts[displayLabel] or 0) + 1
	end

	local nodes = {}
	for index, item in ipairs(resolved) do
		local record, entry = item.record, item.entry
		local isDirect = item.kind ~= "instance"
		local children = not isDirect and search and search.BuildDifficultyNodes
			and search:BuildDifficultyNodes(entry, activeTabID) or {}
		for _, child in ipairs(children) do
			child.favoriteSourceIdentity = record.identity
		end
		local label = item.displayLabel
		if labelCounts[item.displayLabel] > 1 and record.breadcrumb then
			label = string.format("%s |cff888888· %s|r", label, record.breadcrumb)
		end
		local disabled = isDirect
			and not nodeAllowedForTab(item.activityNode, activeTabID)
			or not isDirect and (entry == nil or #children == 0)
		if disabled then
			label = string.format(
				L.FAVORITE_INSTANCE_UNAVAILABLE_FMT or "%s · Unavailable",
				label)
		end
		nodes[#nodes + 1] = {
			key = ROOT_KEY .. "::result::" .. record.identity,
			level = 1,
			label = label,
			isLeaf = false,
			disabled = disabled,
			disabledTooltip = disabled,
			disabledReason = disabled and (L.NAV_ACTIVITY_UNAVAILABLE
				or "This activity is not currently available.") or nil,
			navKind = ROOT_KEY,
			children = children,
			childrenLoaded = true,
			favoriteResult = true,
			favoriteIdentity = record.identity,
			favoriteOrder = index,
			favoriteActivity = isDirect,
			favoriteHasPreset = item.kind == "preset",
			favoriteActualLabel = item.actualLabel,
			favoriteActivityID = isDirect and record.activityID or nil,
			favoriteCategoryID = isDirect and record.categoryID or nil,
			quickSearchIdentity = not isDirect and record.identity or nil,
			quickSearchResult = not isDirect,
			_gfQuickSearchProxy = not isDirect,
			_gfQuickSearchGeneration = not isDirect and search
				and search.GetGeneration and search:GetGeneration() or 0,
		}
	end
	return nodes
end

function FavoriteInstances:RefreshRoot(tabID, identityOrder)
	local root = favoriteRoot()
	if not root then
		return nil
	end
	root.children = self:BuildNodes(tabID or currentTabID(), identityOrder)
	root.childrenLoaded = true
	return root
end

function FavoriteInstances:Invalidate()
	local root = favoriteRoot()
	if root then
		root.children = nil
		root.childrenLoaded = false
	end
end

function FavoriteInstances:ResolveTarget(proxy, tabID)
	if not (proxy and proxy.favoriteIdentity) then
		return nil
	end
	local record = findRecord(proxy.favoriteIdentity)
	if recordKind(record) ~= "instance" then
		local node = resolveDirectRecord(record, tabID or currentTabID())
		return nodeAllowedForTab(node, tabID or currentTabID()) and node or nil
	end
	local search = GF.QuickSearch
	local entry = search and search.GetEntryByIdentity
		and search:GetEntryByIdentity(proxy.favoriteIdentity) or nil
	return entry and search.ResolveEntrySource
		and search:ResolveEntrySource(entry, tabID or currentTabID()) or nil
end

function FavoriteInstances:ResolveBrowseSource(proxy)
	return self:ResolveTarget(proxy, GF.TAB_BROWSE)
end
