---@class AddonPrivate
local Private = select(2, ...)

---@class WaypointUtil
local waypointsUtil = {}
Private.WaypointsUtil = waypointsUtil

local const = Private.constants

function waypointsUtil.FilterFunction(_, search, data, filter)
    if not data or type(data) ~= "table" then
        return
    end
    search = search:lower()
    local filtered = {}
    for _, node in ipairs(data) do
        local matched = node.text:lower():match(search)
        if not matched and node.children then
            for _, childNode in ipairs(node.children) do
                if not matched then
                    matched = childNode.text:lower():match(search)
                end
            end
        end
        local filteredOut = false
        if filter then
            filteredOut = (filter == 2 and not node.complete) or (filter == 3 and node.complete)
            -- "Almost done" modes: values 4/5/6 mean "<= 1/2/3 criteria remaining".
            -- Keep only incomplete achievements within that many criteria of finishing.
            if filter >= 4 and filter <= 6 then
                local threshold = filter - 3
                local rem = node.remaining
                filteredOut = node.complete or not (rem and rem >= 1 and rem <= threshold)
            end
        end

        if matched and not filteredOut then
            tinsert(filtered, node)
        end
    end
    return filtered
end

function waypointsUtil:BuildTree(expansion)
    if not expansion or type(expansion) ~= "table" then
        Private.Addon:Print("No Expansion found! Please activate at least 1 Expansion for this Addon!")
        return
    end
    local points = expansion
    local byID = {}
    for _, ach in ipairs(points) do
        byID[ach.achievementID] = ach
    end
    local tree = {}
    for _, achievement in ipairs(points) do
            -- Determine text color based on faction
            local nameColor = const.COLORS.WHITE -- Default white
            if achievement.faction == "Horde" then
                nameColor = const.COLORS.HORDE -- Red for Horde
            elseif achievement.faction == "Alliance" then
                nameColor = const.COLORS.ALLIANCE -- Blue for Alliance
            end

                        -- Calculate Achievement Progress
            local totalCriteria = GetAchievementNumCriteria(achievement.achievementID)
            local completedCriteria = 0
            local criteriaAvailable = totalCriteria > 0

            if criteriaAvailable then
                for i = 1, totalCriteria do
                    if select(3, GetAchievementCriteriaInfo(achievement.achievementID, i)) then
                        completedCriteria = completedCriteria + 1
                    end
                end
            else
                -- Handle achievements without criteria
                if select(4, GetAchievementInfo(achievement.achievementID)) then
                    completedCriteria = 1
                    totalCriteria = 1
                else
                    -- For incomplete achievements with no criteria, display 0/1
                    completedCriteria = 0
                    totalCriteria = 1
                end
            end
            local stillCompleted = select(4, GetAchievementInfo(achievement.achievementID))

            -- Apply special coloring for 0/1 case
            local progressColor
            if completedCriteria == 0 and totalCriteria == 1 then
                progressColor = const.COLORS.NEGATIVE -- Red for incomplete achievements
            else
                progressColor = (completedCriteria == totalCriteria) and const.COLORS.POSITIVE or const.COLORS.NEGATIVE
                if stillCompleted then
                    progressColor = const.COLORS.POSITIVE
                end
            end
            local progressText = string.format(progressColor:WrapTextInColorCode(" (%d/%d)|r"), completedCriteria, totalCriteria)
            local text = nameColor:WrapTextInColorCode(achievement.name) .. progressText
            local tooltip = const.COLORS.NEUTRAL:WrapTextInColorCode(achievement.name) ..
            "\n\n" .. (achievement.description or "") ..
            "\n\n" .. const.COLORS.POSITIVE:WrapTextInColorCode("Left Click to expand or collapse") ..
            "\n\n" .. const.COLORS.POSITIVE:WrapTextInColorCode("Shift-Left Click to open WoW Achievement UI")
        local treeData = {
            text = text,
            tooltip = tooltip,
            value = achievement.achievementID,
            height = 90,
            complete = progressColor == const.COLORS.POSITIVE,
            remaining = math.max(0, totalCriteria - completedCriteria), -- criteria left, for the "almost done" filter
            prerequisites = achievement.prerequisites
        }
        local childs = nil
        if achievement.parentAchievementID then
            local parentName = nil
            for _, parentAchievement in ipairs(points) do
                if parentAchievement.achievementID == achievement.parentAchievementID then
                    parentName = parentAchievement.name
                    break
                end
            end

            parentName = parentName or "Unknown"
            childs = {}
            tinsert(childs, {
                icon = "Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Up",
                text = "Go Back (" .. parentName .. ")",
                jump = achievement.parentAchievementID,
                value = achievement.achievementID .. "\001" .. achievement.parentAchievementID,
                height = 35
            })
        end
        if achievement.criteria then
            childs = childs or {}
            for fallbackIndex, childAchievement in ipairs(achievement.criteria) do
                local success, name, _, isComplete, quantity, reqQuantity = pcall(GetAchievementCriteriaInfo, achievement.achievementID, childAchievement.criteriaIndex or 0)
                local critColor = isComplete and "|cff00ff00" or "|cffff0000"
                local cProgressText = success and name and string.format(critColor .. " (%d/%d)|r", quantity or 0, reqQuantity or 1) or "|cffff0000 (API Error)|r"

                local cTooltip =
                "|cffffff00" .. (childAchievement.description or "No Description Available") .. "|r\n\n" ..
                ((childAchievement.waypoints and #childAchievement.waypoints > 0) and "|cff00ff00Right-click to set waypoints.|r" or "|cffff0000No Waypoints Available.|r") .. "\n\n" ..
                ((childAchievement.guide and childAchievement.guide ~= "") and "|cffffd700Guide:|r\n|cffffffff" .. childAchievement.guide .. "|r" or "|cffff0000No Guide Available.|r")

                if not childAchievement.criteriaIndex then
                    childAchievement.criteriaIndex = fallbackIndex
                end
                local critPrereqs = childAchievement.prerequisites
                if not critPrereqs and childAchievement.childAchievementID then
                    local linked = byID[childAchievement.childAchievementID]
                    critPrereqs = linked and linked.prerequisites
                end
                tinsert(childs, {
                    text = "|cffffffff" .. (childAchievement.description or "Unknown") .. "|r " .. cProgressText,
                    jump = childAchievement.childAchievementID,
                    icon = childAchievement.childAchievementID and "Interface\\Icons\\Achievement_Quests_Completed_01" or nil,
                    tooltip = cTooltip,
                    value = achievement.achievementID .. "\001" .. childAchievement.criteriaIndex,
                    waypointData = {achievement, childAchievement},
                    height = 35,
                    complete = isComplete,
                    prerequisites = critPrereqs
                })
            end
        end
        treeData.children = childs
        tinsert(tree, treeData)
    end
    return tree
end

function waypointsUtil:GetProgress(expansion)
    if not expansion or type(expansion) ~= "table" then
        return
    end
    local totalCriteria, completedCriteria = 0, 0

    for _, achievement in ipairs(expansion) do
        for _, criterion in ipairs(achievement.criteria or {}) do
            local criteriaIndex = criterion.criteriaIndex or 0
            local success, _, _, isComplete = pcall(GetAchievementCriteriaInfo, achievement.achievementID, criteriaIndex)

            if success then
                totalCriteria = totalCriteria + 1
                if isComplete then
                    completedCriteria = completedCriteria + 1
                end
            end
        end
    end

    return string.format("Progress: %.2f%% (%d/%d)", (completedCriteria / math.max(totalCriteria, 1)) * 100, completedCriteria, totalCriteria)
end

-- ---------------------------------------------------------------------------
-- Active tracked waypoint (drives the Nearby HUD "travel hint")
-- ---------------------------------------------------------------------------
-- The single objective the player is currently routing to. Set when they click
-- a Nearby-HUD row (or, later, a Do-it-now step). x/y are stored as 0-100.
local activeTarget = nil

function waypointsUtil:GetActiveTarget()
    return activeTarget
end

-- Apply the current activeTarget as a live waypoint (TomTom arrow if installed,
-- otherwise the built-in super-tracked user waypoint).
local function applyActiveWaypoint()
    if not activeTarget then return end
    local mapID, x, y = activeTarget.mapID, activeTarget.x, activeTarget.y
    -- Is the target in a different zone than the player? TomTom's crazy arrow
    -- auto-locks onto the CLOSEST waypoint, so a cross-zone target (e.g. a delve
    -- in another zone) often never gets arrowed. In that case we also drop the
    -- game's native supertracked waypoint, which navigates across zones reliably.
    local playerMap = C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player")
    local crossZone = playerMap and mapID and playerMap ~= mapID
    if TomTom then
        local uid = TomTom:AddWaypoint(mapID, x / 100, y / 100, {
            title = activeTarget.tomtomTitle,
            persistent = false,
            silent = true,
            crazy = true, -- request the crazy arrow on add as well
        })
        activeTarget.tomtomUID = uid
        if uid and TomTom.SetCrazyArrow then
            TomTom:SetCrazyArrow(uid, 15, activeTarget.name)
        end
    end
    -- Native supertracked user waypoint. Used when TomTom is absent, OR alongside
    -- TomTom for cross-zone targets so there's always a map pin + compass heading
    -- pointing the right way. Skipped for TomTom in-zone targets (its crazy arrow
    -- already covers those) to avoid a duplicate pin.
    if (not TomTom or crossZone) and C_Map.SetUserWaypoint and UiMapPoint and UiMapPoint.CreateFromCoordinates then
        pcall(C_Map.SetUserWaypoint, UiMapPoint.CreateFromCoordinates(mapID, x / 100, y / 100))
        activeTarget.usedNative = true
        if C_SuperTrack and C_SuperTrack.SetSuperTrackedUserWaypoint then
            pcall(C_SuperTrack.SetSuperTrackedUserWaypoint, true)
        end
    end
    if Private.NearbyHud then Private.NearbyHud:Refresh() end
end

-- Set a live tracked waypoint from an achievement + criterion + waypoint. Records
-- the achievement/criterion so the target can be auto-cleared once done.
function waypointsUtil:TrackWaypoint(achievement, criterion, waypoint)
    if not achievement or not criterion or not waypoint then return end
    local mapID, x, y = waypoint.mapID, waypoint.x, waypoint.y
    if not mapID or not x or not y then return end
    -- Overlay a player-recorded coordinate fix (Alt-right-click in the planner), so
    -- once corrected the arrow points at the right spot while still tracking this
    -- objective (auto-clears on completion like normal).
    local fix = Private.Objectives and Private.Objectives:GetWaypointFix(achievement, criterion, waypoint)
    if fix then x, y = fix.x, fix.y end
    self:ClearActiveTarget()
    local info = C_Map.GetMapInfo(mapID)
    activeTarget = {
        achievement = achievement,
        criterion = criterion,
        mapID = mapID, x = x, y = y,
        name = criterion.description or achievement.name or "Objective",
        achievementName = achievement.name,
        guide = criterion.guide,
        zoneName = (info and info.name) or "Unknown",
        tomtomTitle = string.format("|cff00ff00Meta Tracker|r\n%s\n%s", achievement.name or "", criterion.description or ""),
    }
    applyActiveWaypoint()
end

-- Point a waypoint at a whole zone (its centre) as a general "travel here" target
-- (e.g. clicking an "other zone" row). No achievement/criterion, so it isn't
-- auto-cleared on completion; the external-clear check still applies.
function waypointsUtil:TrackZone(mapID, zoneName)
    if not mapID then return end
    self:ClearActiveTarget()
    local info = C_Map.GetMapInfo(mapID)
    local name = (info and info.name) or zoneName or "Zone"
    activeTarget = {
        mapID = mapID, x = 50, y = 50, name = name, zoneName = name,
        tomtomTitle = "|cff00ff00Meta Tracker|r\nTravel to " .. name,
    }
    applyActiveWaypoint()
end

-- Point a waypoint at arbitrary coords (x/y are 0-100), e.g. a World Quest.
function waypointsUtil:TrackPoint(mapID, x, y, name)
    if not mapID or not x or not y then return end
    self:ClearActiveTarget()
    local info = C_Map.GetMapInfo(mapID)
    activeTarget = {
        mapID = mapID, x = x, y = y,
        name = name or "Objective",
        zoneName = (info and info.name) or "Unknown",
        tomtomTitle = "|cff00ff00Meta Tracker|r\n" .. (name or ""),
    }
    applyActiveWaypoint()
end

-- Clear the tracked waypoint (and its map arrow), e.g. once the objective is done.
function waypointsUtil:ClearActiveTarget()
    if not activeTarget then return end
    if activeTarget.tomtomUID and TomTom and TomTom.RemoveWaypoint then
        pcall(function() TomTom:RemoveWaypoint(activeTarget.tomtomUID) end)
    end
    if activeTarget.usedNative then
        if C_Map.ClearUserWaypoint then C_Map.ClearUserWaypoint() end
        if C_SuperTrack and C_SuperTrack.SetSuperTrackedUserWaypoint then
            C_SuperTrack.SetSuperTrackedUserWaypoint(false)
        end
    end
    activeTarget = nil
end

local activePins = {}
local function ClearCustomPins()
    for _, pin in ipairs(activePins) do
        pin:Hide()
        pin:SetParent(nil)
    end
    wipe(activePins)
end
local function NormalizeCoordinates(x, y)
    return x / 100, y / 100
end

function waypointsUtil:AddCustomPins(achievement, criterion)
    ClearCustomPins()

	local waypoints = criterion.waypoints

	if not achievement or not waypoints or #waypoints == 0 then
		print("No achievement or waypoints available")
		return
	end


    if not waypoints or #waypoints == 0 then
        print("No waypoints available for this criteria.")
        return
    end

    -- Pre-defined MapID-to-ZoneID and ZoneID-to-MapID mappings
    local mapIDToZoneID = {
        [826] = 8499, -- Example: MapID 826 maps to ZoneID 8499 (Zuldazar)
        -- Add more mappings as needed
    }

    local zoneIDToMapID = {
        [8499] = 862, -- Example: ZoneID 8499 resolves back to Zuldazar's correct MapID
        -- Add more mappings as needed
    }

    -- Helper function to get ZoneID from MapID
    local function GetCorrectedZoneID(mapID)
        return mapIDToZoneID[mapID] or mapID
    end

    -- Helper function to get MapID from ZoneID
    local function GetCorrectedMapID(zoneID)
        return zoneIDToMapID[zoneID] or zoneID
    end

    -- Resolve the target ZoneID and MapID
    --local targetZoneID = GetCorrectedZoneID(waypoints[1].mapID)
	local targetZoneID = GetCorrectedZoneID(waypoints[1].mapID)
    local targetMapID = GetCorrectedMapID(targetZoneID)

    if not targetZoneID or not targetMapID then
        print("Error: Invalid or missing ZoneID or MapID.")
        return
    end

    -- Debugging resolved zone and map
    local zoneInfo = C_Map.GetMapInfo(targetMapID)
    if not zoneInfo then
        print(string.format("Error: MapID %d does not exist or is invalid.", targetMapID))
        return
    end
    --print(string.format("Resolved Zone: %s (ZoneID: %d, MapID: %d)", zoneInfo.name, targetZoneID, targetMapID))

    local function AddPinsToMap()
        for _, waypoint in ipairs(waypoints) do
            local correctedZoneID = GetCorrectedZoneID(waypoint.mapID)
            local correctedMapID = GetCorrectedMapID(correctedZoneID)

            if correctedMapID == targetMapID then
                local normalizedX, normalizedY = NormalizeCoordinates(waypoint.x, waypoint.y)

                -- Create a pin
                local pin = CreateFrame("Frame", nil, WorldMapFrame.ScrollContainer.Child, "BackdropTemplate")
                pin:SetSize(30, 30)

                -- Set appearance
                pin.texture = pin:CreateTexture(nil, "OVERLAY")
                pin.texture:SetAllPoints()
                pin.texture:SetTexture("Interface\\AchievementFrame\\UI-Achievement-Progressive-Shield")

                -- Position the pin
                pin:SetPoint("CENTER", WorldMapFrame.ScrollContainer.Child, "TOPLEFT",
                    normalizedX * WorldMapFrame.ScrollContainer.Child:GetWidth(),
                    -normalizedY * WorldMapFrame.ScrollContainer.Child:GetHeight())

                pin:Show()

                -- Debug pin placement
                --print(string.format("Pin added at Zone: %s (MapID: %d, ZoneID: %d), Coordinates: %.2f, %.2f (Normalized: %.4f, %.4f)",zoneInfo.name, correctedMapID, correctedZoneID, waypoint.x, waypoint.y, normalizedX, normalizedY))

                -- Store pin details for tooltip
                pin.waypoint = waypoint

						if TomTom and waypoint.x and waypoint.y then
                            -- Use the corrected MapID for TomTom Waypoint

							local achievementName = achievement.name or "Unknown Achievement"
							local criteriaName = criterion.description or "Unknown Objective"
							local source = "Meta Achievement Tracker"
							local achDesc = achievement.description or "No Achievment Description"
							local guideText = criterion.guide and "|cff00ff00Objective Guide Available (in addon UI)|r" or "|cffff0000No Objective Guide|r"

							TomTom:AddWaypoint(correctedMapID, waypoint.x / 100, waypoint.y / 100, {
								title = string.format("|cff00ff00%s|r\n\n|cff00ff00Achievement: |r%s\n%s\n\n|cff00ff00Objective: |r%s\n%s\n\n",
													  source, achievementName, achDesc, criteriaName, guideText),
								persistent = true,   -- Keep the waypoint until manually removed
								crazy = true,        -- Don't enable "crazy arrow" by default
								silent = true        -- Suppress messages in chat
							})


                            --print(string.format("TomTom Waypoint added: %.2f, %.2f in Zone %s (MapID %d)", waypoint.x, waypoint.y, zoneInfo.name, correctedMapID))
                        else
                            print("TomTom is not available or waypoint data is missing.")
                        end

                -- Handle interactions
                pin:SetScript("OnMouseDown", function(_, button)
                    if button == "RightButton" then
                        if TomTom and waypoint.x and waypoint.y then
                            -- Use the corrected MapID for TomTom Waypoint
                            TomTom:AddWaypoint(correctedMapID, waypoint.x / 100, waypoint.y / 100, {
                                title = string.format("%s (from: %s)", criterion.description, source)
                            })
                            --print(string.format("TomTom Waypoint added: %.2f, %.2f in Zone %s (MapID %d)", waypoint.x, waypoint.y, zoneInfo.name, correctedMapID))
                        else
                            print("TomTom is not available or waypoint data is missing.")
                        end
                    elseif button == "LeftButton" then
                        GameTooltip:SetOwner(pin, "ANCHOR_RIGHT")

                        -- Safely retrieve description and guide
                        local description = criterion.description or "No Description Available"
                        local guide = achievement.guide or "No additional guidance available."

                        -- Display the description
                        GameTooltip:SetText(description, 1, 1, 1)

                        -- Display the guide
                        GameTooltip:AddLine(string.format("Guide: %s", guide), 0.5, 0.8, 0.5) -- Greenish text for guides

                        -- Always show the TomTom interaction hint
                        GameTooltip:AddLine("|cff00ff00Right Click to Add TomTom Waypoint.|r", 0, 1, 0)

                        GameTooltip:Show()
                    end
                end)

                pin:SetScript("OnLeave", function()
                    GameTooltip:Hide()
                end)

                -- Add pin to activePins for tracking
                table.insert(activePins, pin)
            end
        end
    end

    local function UpdatePinsVisibility()
        local currentMapID = WorldMapFrame:GetMapID()
        for _, pin in ipairs(activePins) do
            if pin.waypoint and GetCorrectedMapID(GetCorrectedZoneID(pin.waypoint.mapID)) == currentMapID then
                pin:Show()
            else
                pin:Hide()
            end
        end
    end

    -- Hook into the ScrollContainer to monitor map changes
    WorldMapFrame.ScrollContainer:HookScript("OnUpdate", function()
        UpdatePinsVisibility()
    end)

    -- Ensure the correct map is opened
    C_Timer.NewTicker(0.1, function(ticker)
        if WorldMapFrame:GetMapID() ~= targetMapID then
            WorldMapFrame:SetMapID(targetMapID)
        else
            ticker:Cancel()
            AddPinsToMap()
            UpdatePinsVisibility()
        end
    end, 20)
end