-- Portal Pro :: Modules/CastTracker.lua
-- UNIT_SPELLCAST_SUCCEEDED is the reliable "a portal actually happened" signal —
-- it fires once, after the cast completes, and carries the spellID.

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

local CT = PP:NewModule("CastTracker")
PP.CastTracker = CT

local lastCast = nil
local history = {}   -- recent casts, newest first, capped

local HISTORY_CAP = 50

-- Session-only, deliberately not persisted. A forgotten "stop tracking" flag that survived
-- logout would quietly stop recording income, and you'd only find out from a ledger that
-- doesn't add up.
CT.freeMode = false

--- Should this cast be booked as business?
--- @return boolean tracked
--- @return string|nil reason
function CT:ShouldTrack(dest, kind)
	local cfg = PP.profile and PP.profile.tracking
	if not cfg then return true end

	-- Teleport relocates the caster and nobody else. There is no customer and no revenue,
	-- so by default its rune isn't a business cost either.
	if kind == "teleport" and not cfg.trackTeleports then
		return false, "teleports only move you, so they aren't counted as sales"
	end
	if self.freeMode then return false, "free-cast mode is on" end
	if dest and cfg.excludedDests[dest.key] then
		return false, dest.name .. " is excluded from tracking"
	end
	if cfg.ignoreInInstances and IsInInstance and IsInInstance() then
		return false, "you're in a dungeon or raid"
	end
	return true
end

function CT:SetFreeMode(enabled)
	self.freeMode = enabled and true or false
	PP:Fire("FREEMODE_CHANGED", self.freeMode)
	if self.freeMode then
		PP:Warn("Free-cast mode ON - portals won't be logged. Clears on logout.")
	else
		PP:Print("Free-cast mode off. Portals are being logged again.")
	end
end

function CT:ToggleFreeMode()
	self:SetFreeMode(not self.freeMode)
end

function CT:OnEnable()
	PP:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED", function(_, unit, castGUID, spellID)
		if unit ~= "player" then return end
		if not spellID then return end

		local dest, kind = PP.Dest:BySpellID(spellID)
		if not dest then return end

		CT:Record(dest, kind, spellID)
	end)

	-- Surface the reason a portal failed rather than leaving the user staring at a
	-- red error they may have missed in the middle of a fight.
	PP:RegisterEvent("UNIT_SPELLCAST_FAILED", function(_, unit, _, spellID)
		if unit ~= "player" or not spellID then return end
		if not PP.Dest:IsPortalSpell(spellID) then return end
		local ok, reason = PP.Reagents:CanCast(select(2, PP.Dest:BySpellID(spellID)))
		if not ok then PP:Warn(reason) end
	end)
end

--- @param force boolean|nil bypass the tracking filters (used by the sandbox)
function CT:Record(dest, kind, spellID, force)
	local tracked, reason = true, nil
	if not force then
		tracked, reason = self:ShouldTrack(dest, kind)
	end

	local entry = {
		destKey = dest.key,
		destName = dest.name,
		kind    = kind,
		spellID = spellID,
		ts      = time(),
		tracked = tracked,
	}
	lastCast = entry
	table.insert(history, 1, entry)
	while #history > HISTORY_CAP do table.remove(history) end

	if tracked then
		PP.global.counters.casts = (PP.global.counters.casts or 0) + 1

		-- Cost basis. Without this the ledger reports gross, not profit.
		local cost = PP.Reagents:UnitCost(kind)
		if cost > 0 then
			PP.Ledger:RecordReagent(cost, dest.key, spellID)
		end

		PP:Debug("cast recorded:", dest.name, kind)
		PP:Fire("LEDGER_CHANGED")
	else
		-- Counted separately rather than dropped, so the number is still somewhere if you
		-- ever wonder where your runes went.
		PP.global.counters.personalCasts = (PP.global.counters.personalCasts or 0) + 1
		PP:Debug("cast not tracked:", dest.name, reason)
	end

	-- Fired either way: the party announcement is just as useful for a dungeon group.
	-- Consumers that change state (auto-uninvite) must check the `tracked` flag.
	PP:Fire("PORTAL_CAST", dest.key, kind, spellID, tracked)
end

--- Retroactively stop counting the most recent cast, for when you realise mid-run that
--- it shouldn't have been billed. Removes its reagent entry from the ledger.
--- @return boolean ok, string message
function CT:UntrackLast()
	if not lastCast then return false, "No casts recorded yet." end
	if not lastCast.tracked then return false, "The last cast wasn't tracked anyway." end

	-- Find the reagent entry this cast wrote and delete it.
	local removed = false
	for _, e in ipairs(PP.Ledger:All()) do
		if e.kind == "reagent" and e.spellID == lastCast.spellID
			and math.abs(e.ts - lastCast.ts) <= 2 then
			PP.Ledger:Delete(e.id)
			removed = true
			break
		end
	end

	lastCast.tracked = false
	PP.global.counters.casts = math.max(0, (PP.global.counters.casts or 1) - 1)
	PP.global.counters.personalCasts = (PP.global.counters.personalCasts or 0) + 1
	PP:Fire("LEDGER_CHANGED")

	return true, ("Untracked your %s portal%s."):format(
		lastCast.destName, removed and " and removed its rune cost" or "")
end

function CT:LastCast()
	return lastCast
end

function CT:History()
	return history
end

--- Casts since a timestamp, split by kind. Untracked (personal) casts are excluded so the
--- Portals figure lines up with what the ledger charged you for.
function CT:CountSince(since)
	local portals, teleports = 0, 0
	for _, e in ipairs(history) do
		if (not since or e.ts >= since) and e.tracked ~= false then
			if e.kind == "teleport" then teleports = teleports + 1 else portals = portals + 1 end
		end
	end
	return portals, teleports
end
