-- Portal Pro :: Modules/Group.lua
-- Invites, roster tracking, and post-portal cleanup.
-- C_PartyInfo.InviteUnit is not hardware-gated, so invites can be automated freely.

local ADDON_NAME, PP = ...
local U, C = PP.U, PP.C

local G = PP:NewModule("Group")
PP.Group = G

local pending = {}   -- [name] = { ts, leadID }
local inGroup = {}   -- [name] = true

local PENDING_TIMEOUT = 120

--------------------------------------------------------------------------------
-- Inviting
--------------------------------------------------------------------------------

function G:Invite(name, lead)
	if not name or name == "" then return false end

	if UnitInParty(name) or UnitInRaid(name) then
		PP:Debug(name, "already grouped")
		return false
	end

	C.InviteUnit(name)
	pending[name] = { ts = time(), leadID = lead and lead.id or nil }
	PP.global.counters.invites = (PP.global.counters.invites or 0) + 1
	PP:Fire("INVITE_SENT", name)

	if PP.profile.whispers.invited and PP.profile.whispers.invited ~= "" then
		PP.Whispers:SendTemplate("invited", name, {
			name = name,
			destKey = lead and lead.destKey or nil,
		})
	end
	return true
end

function G:IsPending(name)
	return pending[name] ~= nil
end

function G:PendingCount()
	return U.Count(pending)
end

--------------------------------------------------------------------------------
-- Roster
--------------------------------------------------------------------------------

local function rosterNames()
	local names = {}
	local n = GetNumGroupMembers() or 0
	if n == 0 then return names end
	local prefix = IsInRaid() and "raid" or "party"
	local count = IsInRaid() and n or (n - 1)
	for i = 1, count do
		local unit = prefix .. i
		local name = UnitName(unit)
		if name then names[C.ShortName(name)] = true end
	end
	return names
end

function G:OnEnable()
	PP:RegisterEvent("GROUP_ROSTER_UPDATE", function()
		local now = rosterNames()

		-- Joins
		for name in pairs(now) do
			if not inGroup[name] then
				inGroup[name] = true
				if pending[name] then
					pending[name] = nil
					PP:Fire("CUSTOMER_JOINED", name)
					local lead = PP.Leads:FindByName(name)
					if lead then
						lead.joined = true
						PP.Leads:MarkEngaged(lead)
					end
				end
			end
		end

		-- Leaves
		for name in pairs(inGroup) do
			if not now[name] then
				inGroup[name] = nil
				PP:Fire("CUSTOMER_LEFT", name)
			end
		end

		-- Expire stale invites
		local cutoff = time() - PENDING_TIMEOUT
		for name, info in pairs(pending) do
			if info.ts < cutoff then pending[name] = nil end
		end
	end)

	-- Announce the portal to the party. PARTY is not hardware-gated, so this one can
	-- genuinely fire on its own the instant the cast lands.
	PP:On("PORTAL_CAST", function(destKey, kind)
		if not PP.profile.group.announceToParty then return end
		if not IsInGroup() then return end
		-- Teleport only moves the caster. Telling the party to "hop in" after one is
		-- nonsense: there is nothing for them to hop into.
		if kind ~= "portal" then return end
		local msg = PP.Whispers:Expand(PP.profile.whispers.portalUp, { destKey = destKey })
		if msg ~= "" then
			SendChatMessage(msg, "PARTY")
		end
	end)

	-- Optional cleanup after a portal so the next customer can be invited.
	PP:On("PORTAL_CAST", function(destKey, kind, spellID, tracked)
		local cfg = PP.profile.group
		if not cfg.autoUninvite then return end
		-- Same reasoning as the announcement: a self-teleport served no customer, so
		-- there's no completed sale to clear the group after.
		if kind ~= "portal" then return end
		-- Never clear the group on an untracked cast. That's a dungeon party or a favour
		-- for friends, and kicking them 20 seconds after opening their portal would be a
		-- spectacular way to lose them.
		if tracked == false then
			PP:Debug("skipping auto-uninvite: cast wasn't tracked")
			return
		end
		C_Timer.After(cfg.uninviteDelay or 20, function()
			G:ClearGroup()
		end)
	end)
end

--------------------------------------------------------------------------------
-- Cleanup
--------------------------------------------------------------------------------

--- Remove everyone else from the party. Requires leadership; silently does nothing otherwise.
function G:ClearGroup()
	if not IsInGroup() then return end
	local isLeader = UnitIsGroupLeader and UnitIsGroupLeader("player")
	local isAssist = UnitIsGroupAssistant and UnitIsGroupAssistant("player")
	if not (isLeader or isAssist) then
		PP:Debug("not group leader, skipping auto-uninvite")
		return
	end
	for name in pairs(rosterNames()) do
		if name ~= PP.playerName then
			C.UninviteUnit(name)
		end
	end
	PP:Fire("GROUP_CLEARED")
end

function G:Leave()
	if IsInGroup() then C.LeaveParty() end
end
