-- Portal Pro :: Modules/TradeWatch.lua
--
-- TRADE_CLOSED fires on success AND on cancel, and carries no result — treating it as
-- "paid" books every cancelled trade as income. The only reliable signal is the actual
-- change in GetMoney(), so we register a claim with MoneyWatch the moment both parties
-- have accepted and let the money delta be the truth.

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

local TW = PP:NewModule("TradeWatch")
PP.TradeWatch = TW

local session = nil

local function newSession()
	return {
		partner    = C.TradePartner(),
		theirMoney = 0,
		myMoney    = 0,
		claim      = nil,   -- the live MoneyWatch expectation, once money is on the table
		completed  = false, -- set by ERR_TRADE_COMPLETE
	}
end

--- Best guess at what the payment was for: an open lead, else the last portal we cast.
local function inferDestination(partner)
	local lead = PP.Leads:FindByName(partner or "")
	if lead and lead.destKey then return lead.destKey end

	local recent = PP.CastTracker and PP.CastTracker:LastCast()
	if recent and (time() - recent.ts) < 600 then return recent.destKey end

	return nil
end

local function onDelta(delta, partner, source)
	partner = partner or "Unknown"

	if delta > 0 then
		local destKey = inferDestination(partner)
		local expected = destKey and PP.Dest:Price(destKey) or (PP.profile.prices.default or 0)
		local note
		if expected > 0 then
			local diff = delta - expected
			if diff > 0 then
				note = "tip " .. U.MoneyPlain(diff)
			elseif diff < 0 then
				note = "under asking by " .. U.MoneyPlain(-diff)
			end
		end

		PP.Ledger:RecordPayment(partner, delta, destKey, "trade", note)
		PP:Print(("Received %s from %s%s"):format(
			U.Money(delta), partner, note and (" (" .. note .. ")") or ""))

		-- The lead's fate is decided by the Leads module's PAYMENT_RECEIVED subscriber, so
		-- trade and mail payments clear the queue identically.

		if PP.profile.group.autoThanks then
			PP.Whispers:SendTemplate("thanks", partner, { name = partner, destKey = destKey })
		end

		PP:Fire("PAYMENT_RECEIVED", partner, delta, destKey)

	elseif delta < 0 then
		-- Gold going out to someone we've done business with is a refund. To anyone else
		-- it's just a trade, and we don't book it.
		local amount = -delta
		local customer = PP.Ledger:GetCustomer(partner)
		if customer and (customer.count or 0) > 0 then
			local linked
			for _, e in ipairs(PP.Ledger:Query({ customer = partner, kind = "payment" })) do
				if PP.Ledger:RefundedAmount(e.id) < e.copper then linked = e.id end
			end
			PP.Ledger:RecordRefund(partner, amount, linked, "trade", "Refunded via trade")
			PP:Print(("Refunded %s to %s"):format(U.Money(amount), partner))
			PP:Fire("REFUND_ISSUED", partner, amount)
		else
			PP:Debug("outgoing trade gold to non-customer, not booked:", partner)
		end
	end
end

function TW:OnEnable()
	PP:RegisterEvent("TRADE_SHOW", function()
		session = newSession()
		PP:Debug("trade opened with", session.partner or "?")
	end)

	-- Arm the claim as soon as there is money on the table.
	--
	-- The previous version waited for TRADE_ACCEPT_UPDATE to report BOTH parties accepted,
	-- which turns out not to be reliable: when the second party accepts, the server settles
	-- the trade immediately and the client usually goes straight to TRADE_CLOSED without
	-- ever firing that both-accepted update. Real payments were silently dropped.
	--
	-- Money appearing in the window is the honest trigger. It can't produce a false claim
	-- on an empty trade, and it's armed well before the delta can possibly arrive.
	local function refresh()
		if not session then return end

		session.partner    = C.TradePartner() or session.partner
		session.theirMoney = GetTargetTradeMoney() or 0
		session.myMoney    = GetPlayerTradeMoney() or 0

		local hasMoney = session.theirMoney > 0 or session.myMoney > 0
		if hasMoney and not session.claim then
			session.claim = PP.MoneyWatch:Expect("trade", session.partner, 60, onDelta)
			PP:Debug(("trade claim armed: theirs %s, mine %s"):format(
				U.MoneyPlain(session.theirMoney), U.MoneyPlain(session.myMoney)))
		elseif session.claim then
			-- Keep the partner and the deadline fresh while the window is open.
			session.claim.partner = session.partner or session.claim.partner
			session.claim.expires = GetTime() + 60
		end
	end

	PP:RegisterEvent("TRADE_MONEY_CHANGED", refresh)
	PP:RegisterEvent("TRADE_PLAYER_ITEM_CHANGED", refresh)
	PP:RegisterEvent("TRADE_TARGET_ITEM_CHANGED", refresh)
	PP:RegisterEvent("TRADE_ACCEPT_UPDATE", function(_, playerAccepted, targetAccepted)
		PP:Debug("TRADE_ACCEPT_UPDATE", tostring(playerAccepted), tostring(targetAccepted))
		refresh()
	end)

	-- Positive confirmation that the trade actually went through. Not required — the money
	-- delta is still the source of truth — but when it does fire we can hold the claim open
	-- with confidence instead of guessing.
	PP:RegisterEvent("UI_INFO_MESSAGE", function(_, _, message)
		if not session then return end
		if message and ERR_TRADE_COMPLETE and message == ERR_TRADE_COMPLETE then
			session.completed = true
			if session.claim then session.claim.expires = GetTime() + 10 end
			PP:Debug("trade confirmed complete")
		end
	end)

	PP:RegisterEvent("TRADE_CLOSED", function()
		-- Fires on success AND on cancel, identically, with no result to inspect. So rather
		-- than deciding here, leave the claim armed for a grace period: a completed trade's
		-- PLAYER_MONEY lands inside it, a cancelled one simply lets it lapse.
		if session and session.claim then
			session.claim.expires = GetTime() + (session.completed and 10 or 5)
			PP:Debug("trade closed; claim grace", session.completed and "10s" or "5s")
		end
		session = nil
	end)

	-- Deliberately NOT cancelling the claim on TRADE_REQUEST_CANCEL. That event isn't
	-- dependable enough to risk discarding a real payment on, and the grace window above
	-- expires harmlessly on its own. The failure mode we care about is losing a sale.
end

function TW:CurrentPartner()
	return session and session.partner
end
