local DR = select(2, ...)
local constants = DR.Modules["Core.Constants"]

local Comms = {
  initialized = false,
  listeners = {},
  pendingReliable = {},
  seenMessageIds = {},
  seenMessageOrder = {},
  seenCursor = 0,
}

LibStub("AceComm-3.0"):Embed(Comms)
LibStub("AceSerializer-3.0"):Embed(Comms)

local libDeflate = LibStub("LibDeflate", true)

local function getDistributionChannel()
  if IsPartyLFG and IsPartyLFG() then
    return "INSTANCE_CHAT"
  end
  if IsInRaid and IsInRaid() then
    return "RAID"
  end
  if IsInGroup and IsInGroup() then
    return "PARTY"
  end
  return nil
end

local function countStat(key)
  local stats = DR.db.global.commStats
  stats[key] = (stats[key] or 0) + 1
end

local function getSelfDisplay()
  local identity = DR:Require("Services.Identity")
  return identity:BuildDisplayName(UnitName("player"))
end

-- AceComm senders arrive as "Name" (same realm) or "Name-Realm". Normalize to
-- the same display form Identity uses so self-filtering and GUID lookup work.
local function resolveSender(sender)
  local identity = DR:Require("Services.Identity")
  local display = identity:BuildDisplayName(sender)
  if not display then
    return nil, nil
  end

  local guid = identity:GetGuidByDisplay(display)
  if not guid and UnitGUID and Ambiguate then
    local ok, unitGuid = pcall(UnitGUID, Ambiguate(sender, "none"))
    if ok and unitGuid then
      guid = unitGuid
      identity:Bind(guid, display)
    end
  end

  return display, guid
end

function Comms:OnInitialize()
  self.listeners = {}
  self.pendingReliable = {}
  self.seenMessageIds = {}
  self.seenMessageOrder = {}
  self.seenCursor = 0
  self.prefix = constants.COMMS.PREFIX

  if not self.initialized then
    self:RegisterComm(self.prefix, "OnCommReceived")
    self.initialized = true
  end

  self:Subscribe(constants.COMMS.MESSAGE.PING, function(envelope)
    self:SendMessage(constants.COMMS.MESSAGE.PONG, {
      pingId = envelope.payload and envelope.payload.pingId,
      version = DR.version,
    }, { reliable = false })
  end)

  self:Subscribe(constants.COMMS.MESSAGE.PONG, function(envelope)
    local active = self.activePing
    local payload = envelope.payload or {}
    if not active or payload.pingId ~= active.id then
      return
    end
    local who = envelope.verifiedSenderName or "unknown"
    if not active.seen[who] then
      active.seen[who] = true
      active.responders[#active.responders + 1] = string.format("%s (v%s)", who, tostring(payload.version or "?"))
    end
  end)

  local log = DR.logService and DR.logService:Get("Comms")
  if log then
    log:i("Initialized", self.prefix)
  end
end

--- Broadcasts a ping and reports which raiders' addons answered. This is the
--- in-game way to verify the addon is talking to other players' addons.
function Comms:StartPing()
  local pingId = DR:Require("Services.Protocol"):NextMessageId()
  self.activePing = { id = pingId, responders = {}, seen = {} }

  local sent = self:SendMessage(constants.COMMS.MESSAGE.PING, { pingId = pingId }, { reliable = false })
  if not sent then
    self.activePing = nil
    DR:Print("Ping failed: not in a group (no addon channel available).")
    return false
  end

  DR:ScheduleTimer(function()
    local active = self.activePing
    self.activePing = nil
    if not active then
      return
    end
    if #active.responders == 0 then
      DR:Print("Ping: no responses. Nobody else in the group is running DetentionRolls.")
    else
      DR:Print(string.format("Ping: %d response(s): %s", #active.responders, table.concat(active.responders, ", ")))
    end
  end, 3)

  return true
end

function Comms:_rememberSeenMessage(messageId)
  if self.seenMessageIds[messageId] then
    return false
  end

  local maxSeen = (constants.LIMITS and constants.LIMITS.SEEN_MESSAGE_IDS) or 4000
  if maxSeen < 1 then
    maxSeen = 1
  end

  self.seenCursor = (self.seenCursor % maxSeen) + 1
  local evicted = self.seenMessageOrder[self.seenCursor]
  if evicted then
    self.seenMessageIds[evicted] = nil
    countStat("pruned")
  end

  self.seenMessageOrder[self.seenCursor] = messageId
  self.seenMessageIds[messageId] = true
  return true
end

function Comms:OnEnable()
  -- Version announces are owned by Modules.VersionCheck (login + roster changes).
end

function Comms:Subscribe(messageType, callback)
  if type(callback) ~= "function" then
    return
  end
  self.listeners[messageType] = self.listeners[messageType] or {}
  table.insert(self.listeners[messageType], callback)
end

function Comms:_encodeEnvelope(envelope)
  local ok, serialized = pcall(self.Serialize, self, envelope)
  if not ok or type(serialized) ~= "string" then
    local log = DR.logService and DR.logService:Get("Comms")
    if log then
      log:e("Serialize failed", envelope and envelope.messageType or "?")
    end
    return nil
  end
  if libDeflate then
    local compressed = libDeflate:CompressDeflate(serialized, { level = 3 })
    return libDeflate:EncodeForWoWAddonChannel(compressed)
  end
  return serialized
end

function Comms:_decodePayload(text)
  if libDeflate then
    local decoded = libDeflate:DecodeForWoWAddonChannel(text)
    if decoded then
      local decompressed = libDeflate:DecompressDeflate(decoded)
      if decompressed then
        return decompressed
      end
    end
  end
  return text
end

function Comms:_sendRaw(envelope, distribution, target)
  local payload = self:_encodeEnvelope(envelope)
  if not payload then
    return false
  end
  self:SendCommMessage(self.prefix, payload, distribution, target, "NORMAL")
  countStat("sent")
  return true
end

function Comms:_scheduleRetry(messageId)
  local entry = self.pendingReliable[messageId]
  if not entry then
    return
  end

  if entry.timerId then
    DR:CancelTimer(entry.timerId, true)
    entry.timerId = nil
  end

  local maxAttempts = constants.COMMS.RETRY_MAX
  if entry.attempts >= maxAttempts then
    local log = DR.logService and DR.logService:Get("Comms")
    if log then
      log:w("Retry exhausted", messageId)
    end
    countStat("dropped")
    self.pendingReliable[messageId] = nil
    return
  end

  local delay = constants.COMMS.RETRY_BASE_SECONDS * (2 ^ math.max(entry.attempts - 1, 0))
  entry.timerId = DR:ScheduleTimer(function()
    local current = self.pendingReliable[messageId]
    if not current then
      return
    end

    current.attempts = current.attempts + 1
    countStat("retried")
    self:_sendRaw(current.envelope, current.distribution, current.target)
    self:_scheduleRetry(messageId)
  end, delay)
end

function Comms:_trackReliable(envelope, distribution, target)
  self.pendingReliable[envelope.messageId] = {
    envelope = envelope,
    distribution = distribution,
    target = target,
    attempts = 1,
    timerId = nil,
  }
  self:_scheduleRetry(envelope.messageId)
end

function Comms:SendMessage(messageType, payload, opts)
  opts = opts or {}
  local protocol = DR:Require("Services.Protocol")

  local envelope = protocol:BuildEnvelope({
    messageType = messageType,
    sessionId = opts.sessionId,
    itemNonce = opts.itemNonce,
    revision = opts.revision,
    payload = payload,
  })

  local distribution = opts.distribution or getDistributionChannel()
  if not distribution then
    return false, "no_distribution"
  end

  if not self:_sendRaw(envelope, distribution, opts.target) then
    return false, "encode_failed"
  end

  local reliable = opts.reliable
  if reliable == nil then
    reliable = protocol:IsReliable(messageType)
  end

  if reliable then
    self:_trackReliable(envelope, distribution, opts.target)
  end

  return true, envelope.messageId
end

function Comms:SendAck(ackMessageId, distribution)
  local protocol = DR:Require("Services.Protocol")
  local ack = protocol:BuildAckEnvelope(ackMessageId)
  self:_sendRaw(ack, distribution or getDistributionChannel(), nil)
end

function Comms:OnCommReceived(_, rawText, distribution, sender)
  -- Broadcast channels echo our own messages back; ignore them so the sender
  -- never ACKs itself (which would silently defeat reliable delivery).
  local senderDisplay, senderGuid = resolveSender(sender)
  if not senderDisplay or senderDisplay == getSelfDisplay() then
    return
  end

  local decoded = self:_decodePayload(rawText)
  local ok, envelope = self:Deserialize(decoded)
  if not ok then
    return
  end

  local protocol = DR:Require("Services.Protocol")
  if not protocol:IsValidEnvelope(envelope) then
    return
  end

  -- envelope.sender is self-reported and untrusted; these fields are derived
  -- from the transport-level sender Blizzard hands us and cannot be spoofed.
  envelope.verifiedSenderName = senderDisplay
  envelope.verifiedSenderGuid = senderGuid

  if envelope.messageType == constants.COMMS.MESSAGE.ACK then
    local ackId = envelope.payload and envelope.payload.ackMessageId
    if ackId and self.pendingReliable[ackId] then
      local pending = self.pendingReliable[ackId]
      if pending.timerId then
        DR:CancelTimer(pending.timerId, true)
      end
      self.pendingReliable[ackId] = nil
      countStat("acked")
    end
    return
  end

  if not self:_rememberSeenMessage(envelope.messageId) then
    countStat("duplicates")
    self:SendAck(envelope.messageId, distribution)
    return
  end

  self:SendAck(envelope.messageId, distribution)

  local listeners = self.listeners[envelope.messageType]
  if not listeners then
    return
  end

  for _, fn in ipairs(listeners) do
    local okCall, err = pcall(fn, envelope, sender, distribution)
    if not okCall then
      local log = DR.logService and DR.logService:Get("Comms")
      if log then
        log:e("Listener error", envelope.messageType, err)
      end
    end
  end
end

function Comms:EmitSnapshotForTransition(itemKey, fromState, toState, reason)
  if not UnitIsGroupLeader("player") then
    return
  end

  local snapshot = nil
  local sessionManager = DR.Modules["Services.SessionManager"]
  if sessionManager and sessionManager.GetSessionSnapshot then
    snapshot = sessionManager:GetSessionSnapshot(itemKey)
  end

  -- The manager writes session.state only after Transition() returns, so the
  -- snapshot can still carry the PRE-transition state. The transition target
  -- is authoritative — without this, raiders get stuck one state behind
  -- (e.g. "OPEN" with no roll buttons while the leader is collecting).
  if snapshot then
    snapshot.state = toState
  end

  self:SendMessage(constants.COMMS.MESSAGE.STATE_SNAPSHOT, {
    itemKey = itemKey,
    fromState = fromState,
    toState = toState,
    reason = reason,
    snapshot = snapshot,
  }, {
    sessionId = itemKey,
    reliable = true,
  })
end

function Comms:SendVersionAnnounce(version)
  local identity = DR:Require("Services.Identity")
  local guid = UnitGUID("player")
  local name, realm = UnitName("player")
  local display = identity:BuildDisplayName(name, realm)

  if guid and display then
    identity:Bind(guid, display)
  end

  self:SendMessage(constants.COMMS.MESSAGE.VERSION_ANNOUNCE, {
    version = version,
    protocol = DR.db and DR.db.global.protocolVersion or constants.VERSION.PROTOCOL,
    displayName = display,
    guid = guid,
  }, {
    reliable = false,
  })
end

DR:RegisterModule("Services.Comms", Comms)
