local _, ns = ...

ns.Nearby = {}
local Nearby = ns.Nearby

local PREFIX = "AZANEARBY"
local CMD_PING = "PING"

local function getPlayerNameShort(name)
    local raw = tostring(name or "")
    if raw == "" then return "" end
    if Ambiguate then
        return Ambiguate(raw, "short")
    end
    return raw:match("^([^%-]+)") or raw
end

local function getMyNameShort()
    return getPlayerNameShort(UnitName and UnitName("player") or "")
end

local function cleanZoneText(value)
    local zone = tostring(value or "")
    zone = zone:gsub("[\r\n|]", " ")
    zone = zone:gsub("%s+", " ")
    zone = zone:gsub("^%s+", "")
    zone = zone:gsub("%s+$", "")
    return zone
end

local function getPlayerPosition()
    if not (C_Map and C_Map.GetBestMapForUnit and C_Map.GetPlayerMapPosition) then
        return nil
    end

    local mapID = tonumber(C_Map.GetBestMapForUnit("player"))
    if not mapID then return nil end

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

    local x, y
    if pos.x and pos.y then
        x, y = tonumber(pos.x), tonumber(pos.y)
    elseif pos.GetXY then
        local px, py = pos:GetXY()
        x, y = tonumber(px), tonumber(py)
    end

    if not (x and y) then return nil end
    if x < 0 or x > 1 or y < 0 or y > 1 then return nil end

    return {
        mapID = mapID,
        x = x,
        y = y,
        zone = cleanZoneText(ns.Utils and ns.Utils.GetZoneName and ns.Utils:GetZoneName() or ""),
    }
end

local function ensureProfile()
    if not (ns.DB and ns.DB.profile) then return nil end
    ns.DB.profile.nearby = ns.DB.profile.nearby or {}

    local p = ns.DB.profile.nearby
    if p.enabled == nil then p.enabled = true end
    if p.showOnMinimap == nil then p.showOnMinimap = true end
    if p.autoPing == nil then p.autoPing = true end
    if p.pingInterval == nil then p.pingInterval = 12 end
    if p.peerTimeout == nil then p.peerTimeout = 45 end
    if p.maxMarkers == nil then p.maxMarkers = 8 end
    if p.mapRangeNormalized == nil then p.mapRangeNormalized = 0.04 end

    p.pingInterval = math.max(5, tonumber(p.pingInterval) or 12)
    p.peerTimeout = math.max(10, tonumber(p.peerTimeout) or 45)
    p.maxMarkers = math.max(1, math.min(20, tonumber(p.maxMarkers) or 8))
    p.mapRangeNormalized = math.max(0.01, math.min(0.20, tonumber(p.mapRangeNormalized) or 0.04))

    return p
end

local function ensureState()
    ns.state.nearby = ns.state.nearby or {}
    local st = ns.state.nearby
    st.peers = st.peers or {}
    st.pingElapsed = tonumber(st.pingElapsed) or 0
    st.cleanupElapsed = tonumber(st.cleanupElapsed) or 0
    st.markerElapsed = tonumber(st.markerElapsed) or 0
    st.minimapLayer = st.minimapLayer or nil
    st.markerPool = st.markerPool or {}
    st.visibleCount = tonumber(st.visibleCount) or 0
    return st
end

local function acquireMarker(index)
    local st = ensureState()
    local layer = st.minimapLayer
    if not layer then return nil end

    if st.markerPool[index] then
        return st.markerPool[index]
    end

    local dot = CreateFrame("Frame", nil, layer)
    dot:SetSize(8, 8)

    local core = dot:CreateTexture(nil, "ARTWORK")
    core:SetAllPoints()
    core:SetColorTexture(0.26, 0.86, 1.0, 0.95)
    dot.core = core

    local ring = dot:CreateTexture(nil, "OVERLAY")
    ring:SetPoint("CENTER")
    ring:SetSize(12, 12)
    ring:SetTexture("Interface\\COMMON\\Indicator-Yellow")
    ring:SetBlendMode("ADD")
    ring:SetVertexColor(0.18, 0.80, 1.00, 0.55)
    dot.ring = ring

    dot:Hide()
    st.markerPool[index] = dot
    return dot
end

local function hideUnusedMarkers(startIndex)
    local st = ensureState()
    for i = startIndex, #st.markerPool do
        local dot = st.markerPool[i]
        if dot then dot:Hide() end
    end
end

function Nearby:EnsureMinimapLayer()
    local st = ensureState()
    if st.minimapLayer and st.minimapLayer:IsObjectType("Frame") then
        return st.minimapLayer
    end

    local minimap = _G.Minimap
    if not minimap then return nil end

    local layer = CreateFrame("Frame", nil, minimap)
    layer:SetAllPoints(minimap)
    layer:SetFrameStrata("MEDIUM")
    layer:SetFrameLevel((minimap:GetFrameLevel() or 5) + 8)
    layer:EnableMouse(false)
    st.minimapLayer = layer
    return layer
end

function Nearby:ClearPeers()
    local st = ensureState()
    st.peers = {}
    st.visibleCount = 0
    hideUnusedMarkers(1)
end

function Nearby:BuildPingPayload()
    local pos = getPlayerPosition()
    if not pos then return nil end
    return string.format("%s|%d|%.4f|%.4f|%s", CMD_PING, pos.mapID, pos.x, pos.y, pos.zone)
end

function Nearby:BroadcastPing()
    local profile = ensureProfile()
    if not profile then return false end
    if not profile.enabled then return false end

    local payload = self:BuildPingPayload()
    if not payload then return false end

    ForgeComm:Send(PREFIX, payload, "SAY")
    return true
end

function Nearby:OnAddonMessage(prefix, message, channel, sender)
    local profile = ensureProfile()
    if not profile or not profile.enabled then return end
    if prefix ~= PREFIX then return end
    if channel ~= "SAY" and channel ~= "YELL" then return end

    local senderShort = getPlayerNameShort(sender)
    if senderShort == "" or senderShort == getMyNameShort() then return end

    local cmd, mapIDText, xText, yText, zoneText = tostring(message or ""):match("^(%u+)|([^|]+)|([^|]+)|([^|]+)|(.+)$")
    if cmd ~= CMD_PING then return end

    local mapID = tonumber(mapIDText)
    local x = tonumber(xText)
    local y = tonumber(yText)
    if not (mapID and x and y) then return end
    if x < 0 or x > 1 or y < 0 or y > 1 then return end

    local st = ensureState()
    st.peers[senderShort] = {
        name = senderShort,
        mapID = mapID,
        x = x,
        y = y,
        zone = cleanZoneText(zoneText),
        seenAt = GetTime and GetTime() or 0,
    }
end

function Nearby:GetVisibleCount()
    local st = ensureState()
    return tonumber(st.visibleCount) or 0
end

function Nearby:RefreshMinimapMarkers()
    local profile = ensureProfile()
    local st = ensureState()

    if not profile or not profile.enabled or not profile.showOnMinimap then
        st.visibleCount = 0
        hideUnusedMarkers(1)
        return
    end

    local layer = self:EnsureMinimapLayer()
    if not layer then return end

    local me = getPlayerPosition()
    if not me then
        st.visibleCount = 0
        hideUnusedMarkers(1)
        return
    end

    local now = GetTime and GetTime() or 0
    local peers = {}
    for name, peer in pairs(st.peers or {}) do
        if type(peer) == "table" then
            local age = now - (tonumber(peer.seenAt) or 0)
            if age <= profile.peerTimeout and peer.mapID == me.mapID then
                peers[#peers + 1] = peer
            elseif age > profile.peerTimeout then
                st.peers[name] = nil
            end
        end
    end

    table.sort(peers, function(a, b)
        return (a.seenAt or 0) > (b.seenAt or 0)
    end)

    local minimap = _G.Minimap
    local radius = math.max(30, ((minimap and minimap:GetWidth()) or 140) * 0.5 - 8)
    local rangeNorm = profile.mapRangeNormalized
    local maxMarkers = profile.maxMarkers

    local shown = 0
    for i = 1, math.min(#peers, maxMarkers) do
        local peer = peers[i]
        local dx = (peer.x or 0) - (me.x or 0)
        local dy = (peer.y or 0) - (me.y or 0)

        local distance = math.sqrt((dx * dx) + (dy * dy))
        local clamp = 1
        if distance > rangeNorm and distance > 0 then
            clamp = rangeNorm / distance
        end

        local xOffset = (dx * clamp / rangeNorm) * radius
        local yOffset = (-dy * clamp / rangeNorm) * radius

        local dot = acquireMarker(i)
        if dot then
            dot:ClearAllPoints()
            dot:SetPoint("CENTER", layer, "CENTER", xOffset, yOffset)
            dot:Show()
            shown = shown + 1
        end
    end

    st.visibleCount = shown
    hideUnusedMarkers(shown + 1)
end

function Nearby:OnZoneChanged()
    self:RefreshMinimapMarkers()
end

function Nearby:Initialize()
    ensureProfile()
    ensureState()
    -- Register with ForgeDMC comm layer instead of raw C_ChatInfo
    ForgeComm:Register(PREFIX, function(prefix, msg, channel, sender)
        Nearby:OnAddonMessage(prefix, msg, channel, sender)
    end)
    self:EnsureMinimapLayer()
end

function Nearby:OnUpdate(elapsed)
    local profile = ensureProfile()
    if not profile then return end

    local st = ensureState()

    if not profile.enabled then
        if st.visibleCount ~= 0 then
            st.visibleCount = 0
            hideUnusedMarkers(1)
        end
        return
    end

    if profile.autoPing then
        st.pingElapsed = st.pingElapsed + (elapsed or 0)
        if st.pingElapsed >= profile.pingInterval then
            st.pingElapsed = 0
            self:BroadcastPing()
        end
    end

    st.cleanupElapsed = st.cleanupElapsed + (elapsed or 0)
    st.markerElapsed = st.markerElapsed + (elapsed or 0)

    if st.cleanupElapsed >= 2 then
        st.cleanupElapsed = 0
        local now = GetTime and GetTime() or 0
        for name, peer in pairs(st.peers) do
            local age = now - (tonumber(peer.seenAt) or 0)
            if age > profile.peerTimeout then
                st.peers[name] = nil
            end
        end
    end

    if st.markerElapsed >= 0.35 then
        st.markerElapsed = 0
        self:RefreshMinimapMarkers()
    end
end

function Nearby:ToggleEnabled()
    local profile = ensureProfile()
    if not profile then return end
    profile.enabled = not profile.enabled
    if not profile.enabled then
        self:ClearPeers()
    end
    ns.Utils:Print("Nearby app ping " .. (profile.enabled and "enabled." or "disabled."))
end

function Nearby:ToggleMinimapMarkers()
    local profile = ensureProfile()
    if not profile then return end
    profile.showOnMinimap = not profile.showOnMinimap
    if not profile.showOnMinimap then
        hideUnusedMarkers(1)
        ensureState().visibleCount = 0
    end
    ns.Utils:Print("Nearby minimap markers " .. (profile.showOnMinimap and "shown." or "hidden."))
end
