local addonName, privateDB = ...

-- ============================================================================
-- HDG_LumberHUD MODULE
-- Temporal Radar style HUD showing recent lumber gathering locations
-- Based on architecture doc: "Architectural Synthesis of Spatiotemporal HUDs"
-- Section 4.1: Concept A - Circular radar with player at center
-- ============================================================================

local HDG_LumberHUD = {}
privateDB.HDG_LumberHUD = HDG_LumberHUD

-- ============================================================================
-- CONFIGURATION
-- ============================================================================
local CONFIG = {
    RADAR_SIZE = 200,           -- Base size of the radar frame in pixels
    RANGE_YARDS = 450,          -- Radius of the radar in game yards
    RESPAWN_TIME = 600,         -- Time in seconds until a node "respawns" (10 mins)
    MAX_BLIPS = 50,             -- Maximum history to track
    UPDATE_FREQUENCY = 0.05,    -- Default 20fps (configurable via settings)
    RING_DISTANCES = {150, 300}, -- Distance rings in yards
}

-- ============================================================================
-- STORE HELPERS
-- ============================================================================

local function GetStore()
    local HDG = _G.HousingDecorGuide
    return HDG and HDG.Store
end

local function GetState()
    local store = GetStore()
    return store and store:GetState()
end

local function Dispatch(action, payload)
    local store = GetStore()
    if store then
        store:Dispatch(action, payload)
    end
end

local function GetConfig(key)
    local state = GetState()
    return state and state.config and state.config[key]
end

local HDG = _G.HousingDecorGuide
local function GetScheme() return HDG.Theme.GetCurrentScheme() end
local ApplyFont = function(fs, s, ft) HDG.Theme.ApplyFont(fs, s, ft) end

local EMPTY = {}
local function GetLumberCoordinates()
    local state = GetState()
    return state and state.lumberCoordinates or EMPTY
end

-- Get configurable values from saved settings
local cachedUpdateInterval = 1 / 20
local function GetUpdateFrequency()
    return cachedUpdateInterval
end

local function GetRadarScale()
    return GetConfig("lumberHUDScale") or 1.0
end

-- ============================================================================
-- DISTANCE CALCULATION (matches TomTom/HereBeDragons approach)
-- ============================================================================
-- Convert map coordinates (0-1) to world coordinates (yards)
-- Reuses a single Vector2D to avoid per-frame allocations
local reusableVector = CreateVector2D(0, 0)
local function GetWorldCoordsFromMap(mapX, mapY, mapID)
    reusableVector.x, reusableVector.y = mapX, mapY
    local instance, worldPos = C_Map.GetWorldPosFromMapPos(mapID, reusableVector)
    if not worldPos then return nil, nil, nil end
    local x, y = worldPos:GetXY()
    return x, y, instance
end

-- Time-based decay thresholds (Section 6.1)
-- Red = Dead (just cut), Yellow = Warming up, Green = Ready to return, Grey = Old
local DECAY_THRESHOLDS = {
    FRESH = 0.5,      -- 0-50% of respawn: Red
    RESPAWNING = 0.9, -- 50-90% of respawn: Yellow
    -- 90%+ to 1 hour: Green (ready)
    OLD = 3600,       -- After 1 hour: Grey
}

-- Localize hot-path math (avoids table lookups at 20fps)
local sin, cos, sqrt, atan2, pi = math.sin, math.cos, math.sqrt, math.atan2, math.pi
local TWO_PI = 2 * pi

-- ============================================================================
-- STATE
-- ============================================================================
local radarFrame = nil
local isVisible = false
local blipsByNodeKey = {}  -- Persistent blips keyed by node identifier (mapID_x_y)
local blipKeyOrder = {}    -- Insertion order for recycling oldest blips
local MAX_BLIP_ALLOC = CONFIG.MAX_BLIPS * 2 -- Cap total allocations to prevent frame leak
local updateTimer = 0
local cardinalLabels = {} -- Store references for rotation

-- ============================================================================
-- BLIP MANAGEMENT (Persistent - no recycling to avoid flickering)
-- ============================================================================
local function GetNodeKey(node)
    if not node._key then
        node._key = string.format("%d_%.6f_%.6f", node.mapID, node.x, node.y)
    end
    return node._key
end

local function GetOrCreateBlip(nodeKey)
    if blipsByNodeKey[nodeKey] then
        return blipsByNodeKey[nodeKey]
    end

    local blip
    -- Recycle oldest hidden blip if at allocation cap (free-list scan)
    if #blipKeyOrder >= MAX_BLIP_ALLOC then
        local writeIdx = 1
        for readIdx = 1, #blipKeyOrder do
            local oldKey = blipKeyOrder[readIdx]
            local oldBlip = blipsByNodeKey[oldKey]
            if not blip and oldBlip and not oldBlip:IsShown() then
                blipsByNodeKey[oldKey] = nil
                blip = oldBlip
                blip:Hide()
                -- skip writing this entry (effectively removes it)
            else
                blipKeyOrder[writeIdx] = oldKey
                writeIdx = writeIdx + 1
            end
        end
        -- trim trailing entries
        for i = writeIdx, #blipKeyOrder do blipKeyOrder[i] = nil end
    end

    if not blip then
        blip = CreateFrame("Frame", nil, radarFrame)
        blip:SetSize(4, 4)
        blip.dot = blip:CreateTexture(nil, "ARTWORK")
        blip.dot:SetAllPoints()
        blip.dot:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
    end

    blipsByNodeKey[nodeKey] = blip
    table.insert(blipKeyOrder, nodeKey)
    return blip
end

-- ============================================================================
-- UI CREATION
-- ============================================================================
local function CreateRadar()
    if radarFrame then return end

    radarFrame = CreateFrame("Frame", "HDG_LumberRadarFrame", UIParent)
    radarFrame:SetSize(CONFIG.RADAR_SIZE, CONFIG.RADAR_SIZE)
    radarFrame:SetFrameStrata("HIGH")  -- Above normal UI, below dialogs
    radarFrame:SetToplevel(true)  -- Raise to top when clicked
    radarFrame:EnableMouse(true)
    radarFrame:SetMovable(true)
    radarFrame:RegisterForDrag("LeftButton")

    -- Load saved position or default (center, slightly down)
    local pos = GetConfig("lumberHUDPosition")
    if pos then
        radarFrame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.x, pos.y)
    else
        radarFrame:SetPoint("CENTER", UIParent, "CENTER", 0, -100)
    end

    -- Dragging
    radarFrame:SetScript("OnDragStart", function(self)
        if GetConfig("lumberRadarPinToTracker") then return end
        self:StartMoving()
    end)
    radarFrame:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local point, _, relativePoint, x, y = self:GetPoint()
        Dispatch("SET_CONFIG", {
            key = "lumberHUDPosition",
            value = {
                point = point,
                relativePoint = relativePoint,
                x = x,
                y = y
            }
        })
    end)

    -- Circular background with subtle green tint
    local bg = radarFrame:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
    bg:SetVertexColor(0, 0.02, 0, 0.65)  -- Very subtle green tint

    -- Outer edge glow/ring for a more "radar" look
    local outerGlow = radarFrame:CreateTexture(nil, "BACKGROUND", nil, 1)
    outerGlow:SetSize(CONFIG.RADAR_SIZE + 4, CONFIG.RADAR_SIZE + 4)
    outerGlow:SetPoint("CENTER")
    outerGlow:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
    outerGlow:SetVertexColor(0, 0.25, 0, 0.35)  -- Subtle green outer ring

    -- Distance Rings (using stacked circles to create ring outlines - no filled textures)
    local scheme = GetScheme()
    for _, radiusYards in ipairs(CONFIG.RING_DISTANCES) do
        local size = (radiusYards / CONFIG.RANGE_YARDS) * CONFIG.RADAR_SIZE
        local ringThickness = 2  -- pixels

        -- Outer circle (green)
        local outerRing = radarFrame:CreateTexture(nil, "BORDER")
        outerRing:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
        outerRing:SetVertexColor(0, 0.4, 0, 0.25)  -- Dimmer green
        outerRing:SetSize(size, size)
        outerRing:SetPoint("CENTER")

        -- Inner circle (same as background) to "cut out" the center, leaving just the ring
        local innerCutout = radarFrame:CreateTexture(nil, "BORDER", nil, 1)  -- sublevel 1 = on top of sublevel 0
        innerCutout:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
        innerCutout:SetVertexColor(0, 0.02, 0, 0.65)  -- Match background color
        innerCutout:SetSize(size - ringThickness * 2, size - ringThickness * 2)
        innerCutout:SetPoint("CENTER")

        -- Distance label (positioned on the ring line)
        local label = radarFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        label:SetPoint("CENTER", radarFrame, "CENTER", 0, -(size / 2))
        ApplyFont(label, scheme, "small")
        label:SetText(radiusYards .. "y")
        label:SetTextColor(0, 1, 0, 0.5)
    end

    -- Player dot at center
    local playerDot = radarFrame:CreateTexture(nil, "OVERLAY")
    playerDot:SetSize(6, 6)
    playerDot:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
    playerDot:SetVertexColor(0, 1, 0, 1)
    playerDot:SetPoint("CENTER")

    -- Direction line
    local directionLine = radarFrame:CreateTexture(nil, "ARTWORK")
    directionLine:SetSize(2, 20)
    directionLine:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
    directionLine:SetVertexColor(0, 1, 0, 0.8)
    directionLine:SetPoint("BOTTOM", radarFrame, "CENTER", 0, 3)

    -- Cardinal direction labels (will be repositioned each frame based on facing)
    local function CreateDirectionLabel(text, angle)
        local label = radarFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        ApplyFont(label, scheme, "small")
        label:SetText(text)
        label:SetTextColor(0.7, 0.7, 0.7, 0.8)
        return { label = label, baseAngle = angle }
    end

    local labelOffset = (CONFIG.RADAR_SIZE / 2) - 12
    -- Store with their "world" angles (0 = North, pi/2 = left on screen, -pi/2 = right on screen)
    cardinalLabels.N = CreateDirectionLabel("N", 0)
    cardinalLabels.W = CreateDirectionLabel("W", math.pi / 2)
    cardinalLabels.S = CreateDirectionLabel("S", math.pi)
    cardinalLabels.E = CreateDirectionLabel("E", -math.pi / 2)
    cardinalLabels.offset = labelOffset

    -- Apply initial scale from settings
    local initialScale = GetRadarScale()
    radarFrame:SetScale(initialScale)

    -- OnUpdate for rendering
    radarFrame:SetScript("OnUpdate", function(self, elapsed)
        if not isVisible then return end

        updateTimer = updateTimer + elapsed
        if updateTimer < GetUpdateFrequency() then return end
        updateTimer = 0

        HDG_LumberHUD.UpdateRadar()
    end)

    radarFrame:Hide()
end

-- ============================================================================
-- RADAR RENDERING (Section 4.1: Polar to Cartesian)
-- ============================================================================
local visibleBlipKeys = {} -- Track which blips should be visible this frame

local function UpdateCardinalLabel(data, facing)
    local relAngle = data.baseAngle - facing
    local x = -sin(relAngle) * cardinalLabels.offset
    local y = cos(relAngle) * cardinalLabels.offset
    data.label:ClearAllPoints()
    data.label:SetPoint("CENTER", radarFrame, "CENTER", x, y)
end

function HDG_LumberHUD.UpdateRadar()
    -- Pre-calculate pulse values once per frame
    local currentTime = GetTime()
    local sinVal = sin(currentTime * 3)
    local pulseAlpha = 0.5 + 0.5 * sinVal
    local pulseScale = 1.0 + 0.2 * sinVal

    -- Get Player Position (Section 7.1: Handle nil gracefully)
    local mapID = C_Map.GetBestMapForUnit("player")
    if not mapID then return end

    local pos = C_Map.GetPlayerMapPosition(mapID, "player")
    if not pos then return end

    local playerFacing = GetPlayerFacing()
    if not playerFacing then return end

    -- Update cardinal direction labels (rotate with player facing)
    UpdateCardinalLabel(cardinalLabels.N, playerFacing)
    UpdateCardinalLabel(cardinalLabels.E, playerFacing)
    UpdateCardinalLabel(cardinalLabels.S, playerFacing)
    UpdateCardinalLabel(cardinalLabels.W, playerFacing)

    -- Get player world coordinates (using HereBeDragons-style conversion)
    local pWorldX, pWorldY, pInstance = GetWorldCoordsFromMap(pos.x, pos.y, mapID)
    if not pWorldX then return end

    -- Get coordinate history from Store
    local history = GetLumberCoordinates()
    local now = GetServerTime()
    local rangeYards = CONFIG.RANGE_YARDS
    local halfRadar = CONFIG.RADAR_SIZE / 2

    -- Track which blips are visible this frame
    wipe(visibleBlipKeys)

    for i = 1, #history do
        local node = history[i]
        if node.mapID == mapID then
            local age = now - (node.timestamp or 0)

            -- Cache node world coordinates (static per node)
            if not node._wx then
                local wx, wy, wi = GetWorldCoordsFromMap(node.x, node.y, node.mapID)
                if wx then
                    node._wx, node._wy, node._wi = wx, wy, wi
                end
            end

            local nWorldX, nInstance = node._wx, node._wi
            if nWorldX and nInstance == pInstance then
                local dx = nWorldX - pWorldX
                local dy = node._wy - pWorldY
                local dist = sqrt(dx * dx + dy * dy)

                if dist < rangeYards then
                    local angleToNode = atan2(dy, dx)
                    local relativeAngle = angleToNode - playerFacing

                    -- Normalize to [-pi, pi]
                    if relativeAngle > pi then relativeAngle = relativeAngle - TWO_PI
                    elseif relativeAngle < -pi then relativeAngle = relativeAngle + TWO_PI end

                    -- Convert polar to screen coords (Section 4.1)
                    local screenRadius = (dist / rangeYards) * halfRadar
                    local blipX = -sin(relativeAngle) * screenRadius
                    local blipY = cos(relativeAngle) * screenRadius

                    -- Get or create persistent blip for this node
                    local nodeKey = GetNodeKey(node)
                    visibleBlipKeys[nodeKey] = true
                    local blip = GetOrCreateBlip(nodeKey)

                    -- Update position (reposition, don't recreate)
                    blip:ClearAllPoints()
                    blip:SetPoint("CENTER", radarFrame, "CENTER", blipX, blipY)
                    blip:Show()

                    -- Heatmap Coloring (Section 6.1: Time-Based Decay)
                    local ageRatio = age / CONFIG.RESPAWN_TIME
                    local isPulsing = false
                    if age > DECAY_THRESHOLDS.OLD then
                        blip.dot:SetVertexColor(0.5, 0.5, 0.5, 0.3)
                    elseif ageRatio < DECAY_THRESHOLDS.FRESH then
                        blip.dot:SetVertexColor(1, 0.2, 0.2, 1)
                    elseif ageRatio < DECAY_THRESHOLDS.RESPAWNING then
                        blip.dot:SetVertexColor(1, 1, 0, 0.9)
                    else
                        isPulsing = true
                        blip.dot:SetVertexColor(0, 1, 0, 0.7 + pulseAlpha * 0.3)
                    end

                    -- Scale blips: closer = larger
                    local baseScale = 1.0 + (1.0 - dist / rangeYards) * 0.3
                    local dotScale = baseScale
                    if isPulsing then
                        dotScale = baseScale * pulseScale
                    end
                    blip:SetSize(4 * baseScale, 4 * baseScale)
                    blip.dot:ClearAllPoints()
                    blip.dot:SetPoint("CENTER")
                    blip.dot:SetSize(4 * dotScale, 4 * dotScale)
                end
            end
        end
    end

    -- Hide blips that are no longer visible (out of range or different zone)
    for nodeKey, blip in pairs(blipsByNodeKey) do
        if not visibleBlipKeys[nodeKey] then
            blip:Hide()
        end
    end
end

-- ============================================================================
-- PUBLIC API
-- ============================================================================
function HDG_LumberHUD.Show()
    if not radarFrame then CreateRadar() end
    radarFrame:Show()
    isVisible = true
    Dispatch("SET_CONFIG", { key = "lumberHUDVisible", value = true })
    HDG_LumberHUD.UpdateAnchor()
end

function HDG_LumberHUD.Hide()
    if radarFrame then
        radarFrame:Hide()
    end
    isVisible = false
    Dispatch("SET_CONFIG", { key = "lumberHUDVisible", value = false })
end

function HDG_LumberHUD.Toggle()
    if isVisible then
        HDG_LumberHUD.Hide()
        print("|cFF14b8a6[Housing Decor Guide]|r Lumber radar HUD hidden")
    else
        HDG_LumberHUD.Show()
        print("|cFF14b8a6[Housing Decor Guide]|r Lumber radar HUD shown")
    end
end

function HDG_LumberHUD.IsVisible()
    return isVisible
end

function HDG_LumberHUD.IsEnabled()
    return GetConfig("lumberHUDEnabled") == true
end

-- Update radar scale (called from config)
function HDG_LumberHUD.UpdateScale(scale)
    if radarFrame then
        radarFrame:SetScale(scale)
    end
end

-- Update radar framerate (called from config) - no action needed, reads from settings dynamically
function HDG_LumberHUD.UpdateFramerate()
    local fps = GetConfig("lumberHUDFPS") or 20
    cachedUpdateInterval = 1 / fps
end

-- Anchor radar below tracker when pinned, or restore independent position
function HDG_LumberHUD.UpdateAnchor()
    if not radarFrame then return end
    radarFrame:ClearAllPoints()
    if GetConfig("lumberRadarPinToTracker") then
        local trackerFrame = HDG_LumberTracker and HDG_LumberTracker.GetPinnedFrame and HDG_LumberTracker.GetPinnedFrame()
        if trackerFrame and trackerFrame:IsShown() then
            radarFrame:SetPoint("TOP", trackerFrame, "BOTTOM", 0, -4)
            return
        end
    end
    -- Restore independent saved position
    local pos = GetConfig("lumberHUDPosition")
    if pos then
        radarFrame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.x, pos.y)
    else
        radarFrame:SetPoint("CENTER", UIParent, "CENTER", 0, -100)
    end
end

-- Called when lumber is collected (integration point)
function HDG_LumberHUD.OnLocationAdded(x, y, mapID)
    -- Auto-show HUD when farming starts (if enabled and not manually hidden)
    if HDG_LumberHUD.IsEnabled() and not isVisible and GetConfig("lumberHUDVisible") ~= false then
        HDG_LumberHUD.Show()
    end
end

-- Called when player changes zones (v2.2.2)
-- Clears cached blip positions so they get recalculated with new map context
function HDG_LumberHUD.OnZoneChanged()
    wipe(visibleBlipKeys)
    -- Invalidate cached world coords (map→world conversion is zone-dependent)
    local history = GetLumberCoordinates()
    for i = 1, #history do
        history[i]._wx = nil
    end
end

-- Initialize on addon load
function HDG_LumberHUD.Initialize()
    CreateRadar()

    -- Restore visibility state
    if GetConfig("lumberHUDVisible") then
        HDG_LumberHUD.Show()
    end
end

-- ============================================================================
-- DEBUG COMMANDS
-- ============================================================================
SLASH_LUMBERHUD1 = "/lumberhud"
SlashCmdList["LUMBERHUD"] = function(msg)
    if msg == "test" then
        -- Add a test node slightly offset from current location
        local mapID = C_Map.GetBestMapForUnit("player")
        if not mapID then
            print("|cFFFF0000[LumberHUD]|r Cannot get player map position")
            return
        end
        local pos = C_Map.GetPlayerMapPosition(mapID, "player")
        if not pos then
            print("|cFFFF0000[LumberHUD]|r Cannot get player position (instanced?)")
            return
        end

        -- Add to lumberCoordinates - offset by 0.005 (roughly 50 yards depending on zone)
        local testNode = {
            x = pos.x + 0.005,
            y = pos.y - 0.005, -- Offset to the north-east
            mapID = mapID,
            timestamp = GetServerTime(),
            lumberId = 0,
            zone = GetZoneText() or "Unknown"
        }
        Dispatch("ADD_LUMBER_COORDINATE", { node = testNode })

        print(string.format("|cFF14b8a6[LumberHUD]|r Added test node: x=%.4f y=%.4f mapID=%d", testNode.x, testNode.y, mapID))
        print(string.format("|cFF14b8a6[LumberHUD]|r Player pos: x=%.4f y=%.4f", pos.x, pos.y))
        print(string.format("|cFF14b8a6[LumberHUD]|r Total nodes in history: %d", #GetLumberCoordinates()))

        HDG_LumberHUD.OnLocationAdded(pos.x, pos.y, mapID)

    elseif msg == "show" then
        -- Just show the radar
        HDG_LumberHUD.Show()

    elseif msg == "testdist" then
        -- Add test nodes at exact ring distances using world coordinates
        local mapID = C_Map.GetBestMapForUnit("player")
        if not mapID then
            print("|cFFFF0000[LumberHUD]|r Cannot get player map position")
            return
        end
        local pos = C_Map.GetPlayerMapPosition(mapID, "player")
        if not pos then
            print("|cFFFF0000[LumberHUD]|r Cannot get player position (instanced?)")
            return
        end

        -- Get player world coordinates
        local pWorldX, pWorldY, pInstance = GetWorldCoordsFromMap(pos.x, pos.y, mapID)
        if not pWorldX then
            print("|cFFFF0000[LumberHUD]|r Cannot get world coordinates")
            return
        end

        -- Get map size to convert back from world to map coords
        local widthYards, heightYards = C_Map.GetMapWorldSize(mapID)
        if not widthYards or widthYards == 0 then
            print("|cFFFF0000[LumberHUD]|r Cannot get map world size")
            return
        end

        -- Place blips at ring distances (matching CONFIG.RING_DISTANCES)
        local testDistances = CONFIG.RING_DISTANCES
        local now = GetServerTime()

        print("|cFF14b8a6[LumberHUD]|r Testing distance calibration (world coord method)...")
        print(string.format("  Player world coords: X=%.1f Y=%.1f", pWorldX, pWorldY))

        for _, distYards in ipairs(testDistances) do
            -- In world coords, moving "north" typically increases Y
            -- We need to figure out which direction by testing
            -- For now, offset the world Y coordinate directly
            local targetWorldY = pWorldY + distYards

            -- Convert target world position back to map coordinates
            -- We need to find map coords that produce target world coords
            -- Since world = left - width*mapX and world = top - height*mapY
            -- We can approximate by using the ratio
            local offsetY = -distYards / heightYards -- Approximate

            local testNode = {
                x = pos.x,
                y = pos.y + offsetY,
                mapID = mapID,
                timestamp = now - 550, -- Make them green (ready) so they pulse
                lumberId = 0,
                zone = GetZoneText() or "Test"
            }
            Dispatch("ADD_LUMBER_COORDINATE", { node = testNode })

            -- Verify the distance
            local nWorldX, nWorldY = GetWorldCoordsFromMap(testNode.x, testNode.y, mapID)
            local actualDist = nWorldX and math.sqrt((nWorldX - pWorldX)^2 + (nWorldY - pWorldY)^2) or 0

            print(string.format("  Added %dy test node: actual distance=%.1fy", distYards, actualDist))
        end

        print("|cFF14b8a6[LumberHUD]|r Face north and blips should align with distance rings")
        print("|cFF14b8a6[LumberHUD]|r Commands: /lumberhud dist, /lumberhud clear")

        HDG_LumberHUD.Show()

    elseif msg == "clear" then
        -- Clear only test nodes (lumberId = 0), preserve real farming data
        local coords = GetLumberCoordinates()
        if coords and #coords > 0 then
            local newCoords = {}
            local removed = 0
            for _, node in ipairs(coords) do
                if node.lumberId and node.lumberId ~= 0 then
                    table.insert(newCoords, node)
                else
                    removed = removed + 1
                end
            end
            Dispatch("SET_LUMBER_COORDINATES", { coordinates = newCoords })
            print(string.format("|cFF14b8a6[LumberHUD]|r Cleared %d test nodes, kept %d real nodes", removed, #newCoords))
        else
            print("|cFF14b8a6[LumberHUD]|r No coordinates to clear")
        end

    elseif msg == "clearall" then
        -- Clear ALL lumber coordinates (including real farming data)
        local coords = GetLumberCoordinates()
        if coords and #coords > 0 then
            local count = #coords
            Dispatch("CLEAR_LUMBER_COORDINATES", {})
            print(string.format("|cFFFF9900[LumberHUD]|r Cleared ALL %d lumber coordinates (including saved farming data)", count))
        else
            print("|cFF14b8a6[LumberHUD]|r No coordinates to clear")
        end

    elseif msg == "dist" then
        -- Show live distance to all nodes (useful for verifying accuracy with TomTom)
        local mapID = C_Map.GetBestMapForUnit("player")
        if not mapID then
            print("|cFFFF0000[LumberHUD]|r Cannot get map position")
            return
        end
        local pos = C_Map.GetPlayerMapPosition(mapID, "player")
        if not pos then
            print("|cFFFF0000[LumberHUD]|r Cannot get player position")
            return
        end

        -- Get player world coordinates (same method as radar uses)
        local pWorldX, pWorldY, pInstance = GetWorldCoordsFromMap(pos.x, pos.y, mapID)
        if not pWorldX then
            print("|cFFFF0000[LumberHUD]|r Cannot get world coordinates")
            return
        end

        local history = GetLumberCoordinates()
        local now = GetServerTime()

        print("|cFF14b8a6[LumberHUD Distance Check]|r (World coord method - matches TomTom)")
        local count = 0
        for i, node in ipairs(history) do
            if node.mapID == mapID then
                local age = now - (node.timestamp or 0)
                if age < 660 then
                    -- Get node world coordinates
                    local nWorldX, nWorldY, nInstance = GetWorldCoordsFromMap(node.x, node.y, node.mapID)
                    if nWorldX and nInstance == pInstance then
                        local dx = nWorldX - pWorldX
                        local dy = nWorldY - pWorldY
                        local dist = math.sqrt(dx * dx + dy * dy)

                        local nodeType = (node.lumberId == 0) and "|cFFFFFF00TEST|r" or "|cFF00FF00REAL|r"
                        print(string.format("  %s Node %d: |cFFFFFFFF%.1f yards|r away", nodeType, i, dist))
                        count = count + 1
                        if count >= 5 then
                            print("  ... (showing first 5)")
                            break
                        end
                    end
                end
            end
        end
        if count == 0 then
            print("  No nodes in current zone")
        end

    elseif msg == "trace" then
        -- Detailed trace of the math for the nearest node
        local mapID = C_Map.GetBestMapForUnit("player")
        if not mapID then print("No mapID") return end
        local pos = C_Map.GetPlayerMapPosition(mapID, "player")
        if not pos then print("No position") return end
        local facing = GetPlayerFacing()
        if not facing then print("No facing") return end

        -- Get player world coordinates
        local pWorldX, pWorldY, pInstance = GetWorldCoordsFromMap(pos.x, pos.y, mapID)
        if not pWorldX then print("Cannot get world coordinates") return end

        local history = GetLumberCoordinates()
        local now = GetServerTime()

        print("|cFF14b8a6[LumberHUD TRACE]|r (World coord method)")
        print(string.format("  Player map: x=%.4f y=%.4f facing=%.2f rad (%.1f deg)", pos.x, pos.y, facing, math.deg(facing)))
        print(string.format("  Player world: X=%.1f Y=%.1f", pWorldX, pWorldY))

        local nodeCount = 0
        for i, node in ipairs(history) do
            if node.mapID == mapID then
                local age = now - (node.timestamp or 0)
                if age < 660 then
                    -- Get node world coordinates
                    local nWorldX, nWorldY, nInstance = GetWorldCoordsFromMap(node.x, node.y, node.mapID)
                    if nWorldX and nInstance == pInstance then
                        local dx = nWorldX - pWorldX
                        local dy = nWorldY - pWorldY
                        local dist = math.sqrt(dx * dx + dy * dy)

                        local angleToNode = math.atan2(-dx, -dy)
                        local relativeAngle = angleToNode - facing

                        while relativeAngle > math.pi do relativeAngle = relativeAngle - (2 * math.pi) end
                        while relativeAngle < -math.pi do relativeAngle = relativeAngle + (2 * math.pi) end

                        local screenRadius = (dist / CONFIG.RANGE_YARDS) * (CONFIG.RADAR_SIZE / 2)
                        local blipX = -math.sin(relativeAngle) * screenRadius
                        local blipY = math.cos(relativeAngle) * screenRadius

                        print(string.format("  Node %d:", i))
                        print(string.format("    Map: x=%.4f y=%.4f", node.x, node.y))
                        print(string.format("    World: X=%.1f Y=%.1f", nWorldX, nWorldY))
                        print(string.format("    Delta: dx=%.1f dy=%.1f dist=%.1f yards", dx, dy, dist))
                        print(string.format("    angleToNode=%.2f rad (%.1f deg)", angleToNode, math.deg(angleToNode)))
                        print(string.format("    relativeAngle=%.2f rad (%.1f deg)", relativeAngle, math.deg(relativeAngle)))
                        print(string.format("    Screen: blipX=%.1f blipY=%.1f screenRadius=%.1f", blipX, blipY, screenRadius))

                        nodeCount = nodeCount + 1
                        if nodeCount >= 3 then break end -- Only show first 3
                    end
                end
            end
        end

    elseif msg == "debug" then
        -- Show debug info
        local mapID = C_Map.GetBestMapForUnit("player")
        local pos = C_Map.GetPlayerMapPosition(mapID, "player")
        local facing = GetPlayerFacing()

        print("|cFF14b8a6[LumberHUD Debug]|r")
        print(string.format("  Player mapID: %s", tostring(mapID)))
        print(string.format("  Player pos: x=%.4f y=%.4f", pos and pos.x or 0, pos and pos.y or 0))
        print(string.format("  Player facing: %.2f rad (%.1f deg)", facing or 0, facing and math.deg(facing) or 0))
        print(string.format("  HUD visible: %s", tostring(isVisible)))
        print(string.format("  HUD enabled: %s", tostring(GetConfig("lumberHUDEnabled"))))

        -- Show world size for current map
        if mapID then
            local widthYards, heightYards = C_Map.GetMapWorldSize(mapID)
            if widthYards and heightYards then
                print(string.format("  Map world size: %.1f x %.1f yards", widthYards, heightYards))
            end
        end

        local history = GetLumberCoordinates()
        print(string.format("  Lumber coordinates: %d entries", #history))

        local now = GetServerTime()
        local inZone = 0
        for i, node in ipairs(history) do
            if node.mapID == mapID then
                inZone = inZone + 1
                local age = now - (node.timestamp or 0)
                print(string.format("    Node %d: x=%.4f y=%.4f age=%ds", i, node.x, node.y, age))
            end
        end
        print(string.format("  Nodes in current zone: %d", inZone))

    else
        HDG_LumberHUD.Toggle()
    end
end
