-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                          https://tradeskillmaster.com                          --
--    All Rights Reserved - Detailed license information included with addon.     --
-- ------------------------------------------------------------------------------ --

local _, TSM = ...
local CancelScan = TSM.Auctioning:NewPackage("CancelScan")
local L = TSM.Include("Locale").GetTable()
local Database = TSM.Include("Util.Database")
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local AuctionTracking = TSM.Include("Service.AuctionTracking")
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
local private = {
	scanThreadId = nil,
	queueDB = nil,
	itemList = {},
	usedAuctionIndex = {},
	subRowsTemp = {},
}



-- ============================================================================
-- Module Functions
-- ============================================================================

function CancelScan.OnInitialize()
	-- initialize thread
	private.scanThreadId = Threading.New("CANCEL_SCAN", private.ScanThread)
	private.queueDB = Database.NewSchema("AUCTIONING_CANCEL_QUEUE")
		:AddNumberField("auctionId")
		:AddStringField("itemString")
		:AddStringField("operationName")
		:AddNumberField("bid")
		:AddNumberField("buyout")
		:AddNumberField("itemBid")
		:AddNumberField("itemBuyout")
		:AddNumberField("stackSize")
		:AddNumberField("duration")
		:AddNumberField("numStacks")
		:AddNumberField("numProcessed")
		:AddNumberField("numConfirmed")
		:AddNumberField("numFailed")
		:AddIndex("auctionId")
		:AddIndex("itemString")
		:Commit()
end

function CancelScan.Prepare()
	return private.scanThreadId
end

function CancelScan.GetCurrentRow()
	return private.queueDB:NewQuery()
		:Custom(private.NextProcessRowQueryHelper)
		:OrderBy("auctionId", false)
		:GetFirstResultAndRelease()
end

function CancelScan.GetStatus()
	return TSM.Auctioning.Util.GetQueueStatus(private.queueDB:NewQuery())
end

function CancelScan.DoProcess()
	local cancelRow = CancelScan.GetCurrentRow()
	local cancelItemString = cancelRow:GetField("itemString")
	local query = AuctionTracking.CreateQueryUnsoldItem(cancelItemString)
		:Equal("stackSize", cancelRow:GetField("stackSize"))
		:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
		:Equal("autoBaseItemString", cancelItemString)
		:Custom(private.ProcessQueryHelper, cancelRow)
		:OrderBy("auctionId", false)
		:Select("auctionId", "autoBaseItemString", "currentBid", "buyout")
	if not TSM.db.global.auctioningOptions.cancelWithBid then
		query:Equal("highBidder", "")
	end
	local auctionId, itemString, currentBid, buyout = query:GetFirstResultAndRelease()
	if auctionId then
		local result = nil
		if TSM.IsWowClassic() then
			private.usedAuctionIndex[itemString..buyout..currentBid..auctionId] = true
			CancelAuction(auctionId)
			result = true
		else
			private.usedAuctionIndex[auctionId] = true
			result = AuctionHouseWrapper.CancelAuction(auctionId)
		end
		local isRowDone = cancelRow:GetField("numProcessed") + 1 == cancelRow:GetField("numStacks")
		cancelRow:SetField("numProcessed", cancelRow:GetField("numProcessed") + 1)
			:Update()
		cancelRow:Release()
		if result and isRowDone then
			-- update the log
			TSM.Auctioning.Log.UpdateRowByIndex(auctionId, "state", "CANCELLED")
		end
		return result, false
	end

	-- we couldn't find this item, so mark this cancel as failed and we'll try again later
	cancelRow:SetField("numProcessed", cancelRow:GetField("numProcessed") + 1)
		:Update()
	cancelRow:Release()
	return false, false
end

function CancelScan.DoSkip()
	local cancelRow = CancelScan.GetCurrentRow()
	local auctionId = cancelRow:GetField("auctionId")
	cancelRow:SetField("numProcessed", cancelRow:GetField("numProcessed") + 1)
		:SetField("numConfirmed", cancelRow:GetField("numConfirmed") + 1)
		:Update()
	cancelRow:Release()
	-- update the log
	TSM.Auctioning.Log.UpdateRowByIndex(auctionId, "state", "SKIPPED")
end

function CancelScan.HandleConfirm(success, canRetry)
	local confirmRow = private.queueDB:NewQuery()
		:Custom(private.ConfirmRowQueryHelper)
		:OrderBy("auctionId", true)
		:GetFirstResultAndRelease()
	if not confirmRow then
		-- we may have cancelled something outside of TSM
		return
	end

	if canRetry then
		assert(not success)
		confirmRow:SetField("numFailed", confirmRow:GetField("numFailed") + 1)
	end
	confirmRow:SetField("numConfirmed", confirmRow:GetField("numConfirmed") + 1)
		:Update()
	confirmRow:Release()
end

function CancelScan.PrepareFailedCancels()
	wipe(private.usedAuctionIndex)
	private.queueDB:SetQueryUpdatesPaused(true)
	local query = private.queueDB:NewQuery()
		:GreaterThan("numFailed", 0)
	for _, row in query:Iterator() do
		local numFailed, numProcessed, numConfirmed = row:GetFields("numFailed", "numProcessed", "numConfirmed")
		assert(numProcessed >= numFailed and numConfirmed >= numFailed)
		row:SetField("numFailed", 0)
			:SetField("numProcessed", numProcessed - numFailed)
			:SetField("numConfirmed", numConfirmed - numFailed)
			:Update()
	end
	query:Release()
	private.queueDB:SetQueryUpdatesPaused(false)
end

function CancelScan.Reset()
	private.queueDB:Truncate()
	wipe(private.usedAuctionIndex)
end



-- ============================================================================
-- Scan Thread
-- ============================================================================

function private.ScanThread(auctionScan, groupList)
	auctionScan:SetScript("OnQueryDone", private.AuctionScanOnQueryDone)

	-- generate the list of items we want to scan for
	wipe(private.itemList)
	local processedItems = TempTable.Acquire()
	local query = AuctionTracking.CreateQueryUnsold()
		:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
		:Select("autoBaseItemString")
	if not TSM.db.global.auctioningOptions.cancelWithBid then
		query:Equal("highBidder", "")
	end
	for _, itemString in query:Iterator() do
		if not processedItems[itemString] and private.CanCancelItem(itemString, groupList) then
			tinsert(private.itemList, itemString)
		end
		processedItems[itemString] = true
	end
	query:Release()
	TempTable.Release(processedItems)

	if #private.itemList == 0 then
		return
	end
	TSM.Auctioning.SavedSearches.RecordSearch(groupList, "cancelGroups")

	-- run the scan
	auctionScan:AddItemListQueriesThreaded(private.itemList)
	for _, query2 in auctionScan:QueryIterator() do
		query2:AddCustomFilter(private.QueryBuyoutFilter)
	end
	if not auctionScan:ScanQueriesThreaded() then
		Log.PrintUser(L["TSM failed to scan some auctions. Please rerun the scan."])
	end
end



-- ============================================================================
-- Private Helper Functions
-- ============================================================================

function private.CanCancelItem(itemString, groupList)
	local groupPath = TSM.Groups.GetPathByItem(itemString)
	if not groupPath or not tContains(groupList, groupPath) then
		return false
	end

	local hasValidOperation, hasInvalidOperation = false, false
	for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do
		local isValid = private.IsOperationValid(itemString, operationName, operationSettings)
		if isValid == true then
			hasValidOperation = true
		elseif isValid == false then
			hasInvalidOperation = true
		else
			-- we are ignoring this operation
			assert(isValid == nil, "Invalid return value")
		end
	end
	return hasValidOperation and not hasInvalidOperation, itemString
end

function private.IsOperationValid(itemString, operationName, operationSettings)
	if not operationSettings.cancelUndercut and not operationSettings.cancelRepost then
		-- canceling is disabled, so ignore this operation
		TSM.Auctioning.Log.AddEntry(itemString, operationName, "cancelDisabled", "", 0, 0)
		return nil
	end

	local errMsg = nil
	local minPrice = TSM.Auctioning.Util.GetPrice("minPrice", operationSettings, itemString)
	local normalPrice = TSM.Auctioning.Util.GetPrice("normalPrice", operationSettings, itemString)
	local maxPrice = TSM.Auctioning.Util.GetPrice("maxPrice", operationSettings, itemString)
	local undercut = TSM.Auctioning.Util.GetPrice("undercut", operationSettings, itemString)
	local cancelRepostThreshold = TSM.Auctioning.Util.GetPrice("cancelRepostThreshold", operationSettings, itemString)
	if not minPrice then
		errMsg = format(L["Did not cancel %s because your minimum price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.minPrice)
	elseif not maxPrice then
		errMsg = format(L["Did not cancel %s because your maximum price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.maxPrice)
	elseif not normalPrice then
		errMsg = format(L["Did not cancel %s because your normal price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.normalPrice)
	elseif operationSettings.cancelRepost and not cancelRepostThreshold then
		errMsg = format(L["Did not cancel %s because your cancel to repost threshold (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.cancelRepostThreshold)
	elseif not undercut then
		errMsg = format(L["Did not cancel %s because your undercut (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.undercut)
	elseif maxPrice < minPrice then
		errMsg = format(L["Did not cancel %s because your maximum price (%s) is lower than your minimum price (%s). Check your settings."], ItemInfo.GetLink(itemString), operationSettings.maxPrice, operationSettings.minPrice)
	elseif normalPrice < minPrice then
		errMsg = format(L["Did not cancel %s because your normal price (%s) is lower than your minimum price (%s). Check your settings."], ItemInfo.GetLink(itemString), operationSettings.normalPrice, operationSettings.minPrice)
	end

	if errMsg then
		if not TSM.db.global.auctioningOptions.disableInvalidMsg then
			Log.PrintUser(errMsg)
		end
		TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, 0)
		return false
	else
		return true
	end
end

function private.QueryBuyoutFilter(_, row)
	local _, itemBuyout, minItemBuyout = row:GetBuyouts()
	return (itemBuyout and itemBuyout == 0) or (minItemBuyout and minItemBuyout == 0)
end

function private.AuctionScanOnQueryDone(_, query)
	TSM.Auctioning.Log.SetQueryUpdatesPaused(true)
	for itemString in query:ItemIterator() do
		local groupPath = TSM.Groups.GetPathByItem(itemString)
		if groupPath then
			local auctionsDBQuery = AuctionTracking.CreateQueryUnsoldItem(itemString)
				:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
				:Equal("autoBaseItemString", itemString)
				:OrderBy("auctionId", false)
			for _, auctionsDBRow in auctionsDBQuery:IteratorAndRelease() do
				private.GenerateCancels(auctionsDBRow, itemString, groupPath, query)
			end
		else
			Log.Warn("Item removed from group since start of scan: %s", itemString)
		end
	end
	TSM.Auctioning.Log.SetQueryUpdatesPaused(false)
end

function private.GenerateCancels(auctionsDBRow, itemString, groupPath, query)
	local isHandled = false
	for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do
		if not isHandled and private.IsOperationValid(itemString, operationName, operationSettings) then
			assert(not next(private.subRowsTemp))
			TSM.Auctioning.Util.GetFilteredSubRows(query, itemString, operationSettings, private.subRowsTemp)
			local handled, logReason, itemBuyout, seller, auctionId = private.GenerateCancel(auctionsDBRow, itemString, operationName, operationSettings, private.subRowsTemp)
			wipe(private.subRowsTemp)
			if logReason then
				seller = seller or ""
				auctionId = auctionId or 0
				TSM.Auctioning.Log.AddEntry(itemString, operationName, logReason, seller, itemBuyout, auctionId)
			end
			isHandled = isHandled or handled
		end
	end
end

function private.GenerateCancel(auctionsDBRow, itemString, operationName, operationSettings, subRows)
	local auctionId, stackSize, currentBid, buyout, highBidder, duration = auctionsDBRow:GetFields("auctionId", "stackSize", "currentBid", "buyout", "highBidder", "duration")
	local itemBuyout = TSM.IsWowClassic() and floor(buyout / stackSize) or buyout
	local itemBid = TSM.IsWowClassic() and floor(currentBid / stackSize) or currentBid
	if TSM.IsWowClassic() and operationSettings.matchStackSize and stackSize ~= TSM.Auctioning.Util.GetPrice("stackSize", operationSettings, itemString) then
		return false
	elseif not TSM.db.global.auctioningOptions.cancelWithBid and highBidder ~= "" then
		-- Don't cancel an auction if it has a bid and we're set to not cancel those
		return true, "cancelBid", itemBuyout, nil, auctionId
	elseif not TSM.IsWowClassic() and C_AuctionHouse.GetCancelCost(auctionId) > GetMoney() then
		return true, "cancelNoMoney", itemBuyout, nil, auctionId
	end

	local lowestAuction = TempTable.Acquire()
	if not TSM.Auctioning.Util.GetLowestAuction(subRows, itemString, operationSettings, lowestAuction) then
		TempTable.Release(lowestAuction)
		lowestAuction = nil
	end
	local minPrice = TSM.Auctioning.Util.GetPrice("minPrice", operationSettings, itemString)
	local normalPrice = TSM.Auctioning.Util.GetPrice("normalPrice", operationSettings, itemString)
	local maxPrice = TSM.Auctioning.Util.GetPrice("maxPrice", operationSettings, itemString)
	local resetPrice = TSM.Auctioning.Util.GetPrice("priceReset", operationSettings, itemString)
	local cancelRepostThreshold = TSM.Auctioning.Util.GetPrice("cancelRepostThreshold", operationSettings, itemString)
	local undercut = TSM.Auctioning.Util.GetPrice("undercut", operationSettings, itemString)
	local aboveMax = TSM.Auctioning.Util.GetPrice("aboveMax", operationSettings, itemString)

	if not lowestAuction then
		-- all auctions which are posted (including ours) have been ignored, so check if we should cancel to repost higher
		if operationSettings.cancelRepost and normalPrice - itemBuyout > cancelRepostThreshold then
			private.AddToQueue(itemString, operationName, itemBid, itemBuyout, stackSize, duration, auctionId)
			return true, "cancelRepost", itemBuyout, nil, auctionId
		else
			return false, "cancelNotUndercut", itemBuyout
		end
	elseif lowestAuction.hasInvalidSeller then
		Log.PrintfUser(L["The seller name of the lowest auction for %s was not given by the server. Skipping this item."], ItemInfo.GetLink(itemString))
		TempTable.Release(lowestAuction)
		return false, "invalidSeller", itemBuyout
	end

	local shouldCancel, logReason = false, nil
	local playerLowestItemBuyout, playerLowestAuctionId = TSM.Auctioning.Util.GetPlayerLowestBuyout(subRows, itemString, operationSettings)
	local secondLowestBuyout = TSM.Auctioning.Util.GetNextLowestItemBuyout(subRows, itemString, lowestAuction, operationSettings)
	local nonPlayerLowestAuctionId = not TSM.IsWowClassic() and playerLowestItemBuyout and TSM.Auctioning.Util.GetLowestNonPlayerAuctionId(subRows, itemString, operationSettings, playerLowestItemBuyout)
	if itemBuyout < minPrice and not lowestAuction.isBlacklist then
		-- this auction is below the min price
		if operationSettings.cancelRepost and resetPrice and itemBuyout < (resetPrice - cancelRepostThreshold) then
			-- canceling to post at reset price
			shouldCancel = true
			logReason = "cancelReset"
		else
			logReason = "cancelBelowMin"
		end
	elseif lowestAuction.buyout < minPrice and not lowestAuction.isBlacklist then
		-- lowest buyout is below min price, so do nothing
		logReason = "cancelBelowMin"
	elseif operationSettings.cancelUndercut and playerLowestItemBuyout and ((itemBuyout - undercut) > playerLowestItemBuyout or (not TSM.IsWowClassic() and (itemBuyout - undercut) == playerLowestItemBuyout and auctionId ~= playerLowestAuctionId and auctionId < (nonPlayerLowestAuctionId or 0))) then
		-- we've undercut this auction
		shouldCancel = true
		logReason = "cancelPlayerUndercut"
	elseif TSM.Auctioning.Util.IsPlayerOnlySeller(subRows, itemString, operationSettings) then
		-- we are the only auction
		if operationSettings.cancelRepost and (normalPrice - itemBuyout) > cancelRepostThreshold then
			-- we can repost higher
			shouldCancel = true
			logReason = "cancelRepost"
		else
			logReason = "cancelAtNormal"
		end
	elseif lowestAuction.isPlayer and secondLowestBuyout and secondLowestBuyout > maxPrice then
		-- we are posted at the aboveMax price with no competition under our max price
		if operationSettings.cancelRepost and operationSettings.aboveMax ~= "none" and (aboveMax - itemBuyout) > cancelRepostThreshold then
			-- we can repost higher
			shouldCancel = true
			logReason = "cancelRepost"
		else
			logReason = "cancelAtAboveMax"
		end
	elseif lowestAuction.isPlayer then
		-- we are the loewst auction
		if operationSettings.cancelRepost and secondLowestBuyout and ((secondLowestBuyout - undercut) - lowestAuction.buyout) > cancelRepostThreshold then
			-- we can repost higher
			shouldCancel = true
			logReason = "cancelRepost"
		else
			logReason = "cancelNotUndercut"
		end
	elseif not operationSettings.cancelUndercut then
		-- we're undercut but not canceling undercut auctions
	elseif lowestAuction.isWhitelist and itemBuyout == lowestAuction.buyout then
		-- at whitelisted player price
		logReason = "cancelAtWhitelist"
	elseif not lowestAuction.isWhitelist then
		-- we've been undercut by somebody not on our whitelist
		shouldCancel = true
		logReason = "cancelUndercut"
	elseif itemBuyout ~= lowestAuction.buyout or itemBid ~= lowestAuction.bid then
		-- somebody on our whitelist undercut us (or their bid is lower)
		shouldCancel = true
		logReason = "cancelWhitelistUndercut"
	else
		error("Should not get here")
	end

	local seller = lowestAuction.seller
	TempTable.Release(lowestAuction)
	if shouldCancel then
		private.AddToQueue(itemString, operationName, itemBid, itemBuyout, stackSize, duration, auctionId)
	end
	return shouldCancel, logReason, itemBuyout, seller, shouldCancel and auctionId or nil
end

function private.AddToQueue(itemString, operationName, itemBid, itemBuyout, stackSize, duration, auctionId)
	private.queueDB:NewRow()
		:SetField("auctionId", auctionId)
		:SetField("itemString", itemString)
		:SetField("operationName", operationName)
		:SetField("bid", itemBid * stackSize)
		:SetField("buyout", itemBuyout * stackSize)
		:SetField("itemBid", itemBid)
		:SetField("itemBuyout", itemBuyout)
		:SetField("stackSize", stackSize)
		:SetField("duration", duration)
		:SetField("numStacks", 1)
		:SetField("numProcessed", 0)
		:SetField("numConfirmed", 0)
		:SetField("numFailed", 0)
		:Create()
end

function private.ProcessQueryHelper(row, cancelRow)
	if TSM.IsWowClassic() then
		local auctionId, itemString, stackSize, currentBid, buyout = row:GetFields("auctionId", "autoBaseItemString", "stackSize", "currentBid", "buyout")
		local itemBid = floor(currentBid / stackSize)
		local itemBuyout = floor(buyout / stackSize)
		return not private.usedAuctionIndex[itemString..buyout..currentBid..auctionId] and cancelRow:GetField("itemBid") == itemBid and cancelRow:GetField("itemBuyout") == itemBuyout
	else
		local auctionId = row:GetField("auctionId")
		return not private.usedAuctionIndex[auctionId] and cancelRow:GetField("auctionId") == auctionId
	end
end

function private.ConfirmRowQueryHelper(row)
	return row:GetField("numConfirmed") < row:GetField("numProcessed")
end

function private.NextProcessRowQueryHelper(row)
	return row:GetField("numProcessed") < row:GetField("numStacks")
end
