-- MarketWatch Detection.lua
-- Normalization + Tier1/Tier2 trigger checks + categorization
-- Written for WotLK 3.3.5a Lua (5.1), no modern API.

MW = MW or {}

--------------------------------------------------------------------
-- Normalization (adapted from the RaidBrowser approach)
--------------------------------------------------------------------
function MW.NormalizeMsg(raw)
	if not raw or raw == "" then return "" end
	local s = raw:lower()

	-- strip hyperlink color/hash codes: |cffxxxxxx ... |r  and |Hitem:...|h[Name]|h
	s = s:gsub("|c%x%x%x%x%x%x%x%x", "")
	s = s:gsub("|r", "")
	s = s:gsub("|h%[", " ")
	s = s:gsub("%]|h", " ")
	s = s:gsub("|h", " ")
	s = s:gsub("|H.-|h", " ")

	-- expand [bracketed] text with surrounding spaces
	s = s:gsub("%[", " ")
	s = s:gsub("%]", " ")

	-- strip punctuation (keep letters, numbers, spaces, and %)
	-- apostrophes are DELETED (not space-replaced) so "wodin's" -> "wodins",
	-- matching the no-apostrophe dictionary variants used throughout Dictionaries.lua
	s = s:gsub("'", "")
	s = s:gsub("[!%?:%(%){}%+@#&\"~`]", " ")
	s = s:gsub("%-", " ")
	s = s:gsub("/", " ")

	-- collapse whitespace
	s = s:gsub("%s+", " ")
	s = s:gsub("^%s+", "")
	s = s:gsub("%s+$", "")

	return s
end

--------------------------------------------------------------------
-- Tier 1: transaction keyword present (word-boundary safe)
--------------------------------------------------------------------
-- returns matched keyword + type ("WTB"/"WTS"/"WTT"/"CHARTRADE") + start index in normalized string, or nil
--
-- tier 1 = deliberate trade-chat shorthand/typos that essentially never
-- mean anything else ("wts", "seel"). tier 2 = ordinary English words
-- that legitimately indicate a listing most of the time, but can also
-- show up as part of unrelated phrases or proper nouns (e.g. "trade" in
-- "### WARMANE TRADE ###", which is naming Warmane's own character-trade
-- system, not saying "I am trading"). A tier 1 match always wins over a
-- tier 2 one regardless of which appears earlier in the message - see
-- FindTier1 below.
MW.tier1Keywords = {
	{ pat = "wts%f[%A]",       kind = "WTS", tier = 1 },
	{ pat = "wtb%f[%A]",       kind = "WTB", tier = 1 },
	{ pat = "wtt%f[%A]",       kind = "WTT", tier = 1 },
	-- common misspellings/typos of "sell" seen in real trade chat
	{ pat = "seel%f[%A]",      kind = "WTS", tier = 1 },
	{ pat = "sel%f[%A]",       kind = "WTS", tier = 1 },
	{ pat = "sellin%f[%A]",    kind = "WTS", tier = 1 },

	{ pat = "selling%f[%A]",   kind = "WTS", tier = 2 },
	{ pat = "sell%f[%A]",      kind = "WTS", tier = 2 },
	{ pat = "buying%f[%A]",    kind = "WTB", tier = 2 },
	{ pat = "buy%f[%A]",       kind = "WTB", tier = 2 },
	{ pat = "trading%f[%A]",   kind = "WTT", tier = 2 },
	{ pat = "trade%f[%A]",     kind = "WTT", tier = 2 },
	-- "for sale" phrasing never contains wts/sell/etc as its own word
	{ pat = "sale%f[%A]",      kind = "WTS", tier = 2 },
}

function MW.FindTier1(normMsg)
	local best = nil
	for _, entry in ipairs(MW.tier1Keywords) do
		local s, e = normMsg:find("%f[%a]" .. entry.pat)
		if s then
			if not best or entry.tier < best.tier or (entry.tier == best.tier and s < best.startIdx) then
				best = { startIdx = s, endIdx = e, kind = entry.kind, word = normMsg:sub(s, e), tier = entry.tier }
			end
		end
	end
	return best
end

--------------------------------------------------------------------
-- Tier 2: positional check
-- Accept if keyword is within first ~4 words, OR immediately followed by
-- a bracketed-style item name (already stripped of brackets, so we just look
-- for content shortly after) or a price/quantity pattern.
--------------------------------------------------------------------
local function wordIndexOf(normMsg, charIdx)
	local sub = normMsg:sub(1, charIdx)
	local count = 0
	for _ in sub:gmatch("%S+") do count = count + 1 end
	return count
end

-- price/quantity pattern: numbers followed by g/gold/k/stack/each/ea, or vice versa
local PRICE_PATTERN = "%d+%s*[gk]%f[%A]"
local STACK_PATTERN = "stacks?%f[%A]"
local EACH_PATTERN = "%f[%a]eas?%f[%A]"

function MW.PassesTier2(normMsg, tier1)
	if not tier1 then return false end

	local wIdx = wordIndexOf(normMsg, tier1.startIdx)
	if wIdx <= 4 then
		return true
	end

	-- look at the remainder of the message after the keyword for price/stack signals
	local rest = normMsg:sub(tier1.endIdx + 1, tier1.endIdx + 40) -- next ~40 chars
	if rest:find(PRICE_PATTERN) or rest:find(STACK_PATTERN) or rest:find(EACH_PATTERN) then
		return true
	end

	return false
end

--------------------------------------------------------------------
-- Categorization
--------------------------------------------------------------------
-- returns: routeType ("ITEM" / "TRANSMOG" / "CHARTRADE"), category (string or nil for non-item routes)
-- kind is the WTS/WTB/WTT already determined by Tier 1 (see FindTier1) - passed in
-- rather than re-derived here, since Tier 1 also recognizes synonyms/typos like
-- "seel"/"sel" that a literal "wts"/"wtb"/"wtt" substring search would miss.
function MW.Categorize(normMsg, kind)
	-- Character trading phrase check (highest priority - very specific)
	for _, phrase in ipairs(MW.charTradeTriggers) do
		if normMsg:find(phrase, 1, true) then
			return "CHARTRADE", nil
		end
	end

	-- Boost/carry service ads (checked before the char-trade heuristic and item
	-- dictionaries, since these are service posts, not item or character sales)
	for _, trig in ipairs(MW.boostTriggers) do
		if normMsg:find("%f[%a]" .. trig .. "%f[%A]") then
			return "BOOST", nil
		end
	end

	-- Standalone strong words (account/toon/character/char) - specific enough
	-- on their own to flag a character sale even with no class word present.
	if kind then
		for _, w in ipairs(MW.charTradeStrongWords) do
			if normMsg:find("%f[%a]" .. w .. "%f[%A]") then
				return "CHARTRADE", nil
			end
		end
	end

	-- Character trading heuristic: wts/wtb/wtt + a class word + a sale-context word
	-- (catches posts like "WTS Human Lock Srdja 122 Coins - 5 BIS + 440 EOF..."
	--  or "WTB Rogue, Druid or Priest need good achivs and mounts" which mention
	--  no explicit "account"/"character" but are clearly char-sale posts)
	--
	-- Deliberately checked BEFORE the Transmog trigger words: a character-sale
	-- post that lists "14 tmog scrolls" among the character's assets should
	-- still route as CharTrade, not get hijacked into Transmog just because it
	-- mentions transmog scrolls as one line item among many.
	if kind then
		local hasClassWord = false
		for _, w in ipairs(MW.charTradeClassWords) do
			if normMsg:find("%f[%a]" .. w .. "%f[%A]") then
				hasClassWord = true
				break
			end
		end
		if hasClassWord then
			local hasContextWord = false
			for _, w in ipairs(MW.charTradeContextWords) do
				if normMsg:find("%f[%a]" .. w .. "%f[%A]") then
					hasContextWord = true
					break
				end
			end
			if hasContextWord then
				return "CHARTRADE", nil
			end

			-- Race word + class word together (e.g. "80 Human DK") is distinctive
			-- enough on its own, even with no context word present - see
			-- MW.charTradeRaceWords for reasoning.
			for _, w in ipairs(MW.charTradeRaceWords) do
				if normMsg:find("%f[%a]" .. w .. "%f[%A]") then
					return "CHARTRADE", nil
				end
			end
		end
	end

	-- Transmog trigger words (checked before item dictionaries)
	for _, trig in ipairs(MW.transmogTriggers) do
		if normMsg:find(trig, 1, true) then
			return "TRANSMOG", nil
		end
	end

	-- Item dictionaries, in fixed priority order. The matched keyword itself
	-- is returned as a 3rd value - Core.lua uses it as the item name to look
	-- up in Auctionator/TSM for deal detection on BoE/Mounts listings.
	for _, catKey in ipairs(MW.categoryOrder) do
		local list = MW.dictionaries[catKey]
		for _, keyword in ipairs(list) do
			if normMsg:find(keyword, 1, true) then
				return "ITEM", catKey, keyword
			end
		end
	end

	return "ITEM", "Uncategorized"
end
