--[[
============================================================================
ETB v10.0 - Filtering.lua
============================================================================
Advanced filtering, sorting, and search system.
============================================================================
]]--

local ADDON_NAME, NS = ...
local ETB = NS.ETB

ETB.Filtering = {
    filters = {
        category = 1,
        collected = "all",      -- "all", "collected", "uncollected"
        searchText = "",
        sourceType = "all",
        quality = "all",
        expansion = "all",
        myClassOnly = false,    -- deprecated, kept for compat
        weaponType = "all",     -- weapon type filter: "all", "AXE", "SWORD", etc.
        showWeaponsOnly = false, -- show only weapon categories (12, 13)
        armorType = "all",      -- armor type filter: "all", "PLATE", "MAIL", "LEATHER", "CLOTH"
        groupByAppearance = true, -- when true, show one item per visual; when false, show all sources
        colorFilter = nil,      -- color category: nil (all) - UI removed, kept for later
        classFilter = nil,      -- classID to filter by, or nil for all classes (default: player class)
    },
    sortBy = "name",
    sortAscending = true,
}

local Filter = ETB.Filtering

-- =============================================================================
-- FILTER SETTERS
-- =============================================================================

function Filter:SetCategory(categoryID)
    self.filters.category = categoryID
    ETB.EventBus:Fire("ETB_FILTER_CHANGED", "category", categoryID)
end

function Filter:SetCollectedFilter(value)
    self.filters.collected = value
    ETB.EventBus:Fire("ETB_FILTER_CHANGED", "collected", value)
end

function Filter:SetSearchText(text)
    self.filters.searchText = text or ""
    ETB.EventBus:Fire("ETB_FILTER_CHANGED", "search", text)
end

function Filter:SetSourceType(sourceType)
    self.filters.sourceType = sourceType
    ETB.EventBus:Fire("ETB_FILTER_CHANGED", "sourceType", sourceType)
end

function Filter:SetQuality(quality)
    self.filters.quality = quality
    ETB.EventBus:Fire("ETB_FILTER_CHANGED", "quality", quality)
end

function Filter:SetExpansion(expansion)
    self.filters.expansion = expansion
    ETB.EventBus:Fire("ETB_FILTER_CHANGED", "expansion", expansion)
end

function Filter:SetMyClassOnly(value)
    -- Deprecated: use SetClassFilter instead
    if value then
        local _, _, cid = UnitClass("player")
        self:SetClassFilter(cid)
    else
        self:SetClassFilter(nil)
    end
end

function Filter:SetClassFilter(classID)
    self.filters.classFilter = classID  -- nil = all classes, number = specific classID
    self.filters.myClassOnly = (classID ~= nil)  -- backward compat
    ETB:Debug("Filtering", string.format("Class filter changed to: %s", tostring(classID) or "all"))
    ETB.EventBus:Fire("ETB_FILTER_CHANGED", "classFilter", classID)
end

function Filter:SetArmorType(armorType)
    self.filters.armorType = armorType or "all"
    ETB:Debug("Filtering", string.format("Armor type filter changed to: %s", tostring(self.filters.armorType)))
    ETB.EventBus:Fire("ETB_FILTER_CHANGED", "armorType", armorType)
end

function Filter:SetWeaponType(weaponType)
    self.filters.weaponType = weaponType or "all"
    ETB:Debug("Filtering", string.format("Weapon type filter changed to: %s", tostring(self.filters.weaponType)))
    ETB.EventBus:Fire("ETB_FILTER_CHANGED", "weaponType", weaponType)
end

function Filter:SetShowWeaponsOnly(value)
    self.filters.showWeaponsOnly = value
    ETB.EventBus:Fire("ETB_FILTER_CHANGED", "showWeaponsOnly", value)
end

function Filter:SetColorFilter(colorCategory)
    self.filters.colorFilter = colorCategory  -- nil for "all", or "RED", "BLUE", etc.
    ETB:Debug("Filtering", string.format("Color filter changed to: %s", tostring(colorCategory) or "all"))
    ETB.EventBus:Fire("ETB_FILTER_CHANGED", "colorFilter", colorCategory)
end

function Filter:SetGroupByAppearance(value)
    -- Always group by appearance (one item per visual) - not toggleable
    self.filters.groupByAppearance = true
    if ETB.ArmorData then
        ETB.ArmorData:InvalidateCache()
    end
end

function Filter:ClearFilters()
    self.filters.collected = "all"
    self.filters.searchText = ""
    self.filters.sourceType = "all"
    self.filters.quality = "all"
    self.filters.expansion = "all"
    self.filters.armorType = "all"
    self.filters.colorFilter = nil
    -- Reset classFilter to player's class (not nil, so we still see our class)
    local _, _, playerClassID = UnitClass("player")
    self.filters.classFilter = playerClassID
    self.filters.myClassOnly = true
    ETB.EventBus:Fire("ETB_FILTERS_CLEARED")
end

-- =============================================================================
-- APPLY FILTERS
-- =============================================================================

function Filter:ApplyFilters(items)
    if not items then return {} end
    
    local filtered = {}
    local searchLower = (self.filters.searchText or ""):lower()
    local filterStats = {
        total = #items,
        passed = 0,
        filteredByCollection = 0,
        filteredBySearch = 0,
        filteredByArmorType = 0,
        filteredByColor = 0,
        filteredByColorNoData = 0,
        filteredByClass = 0,
        colorPassedNoData = 0,
        foundInAlternative = 0,
    }
    
    local armorFilter = self.filters.armorType
    local sourceFilter = self.filters.sourceType
    local expansionFilter = self.filters.expansion
    local categoryID = self.filters.category
    local catInfo = categoryID and ETB.CATEGORY_INFO and ETB.CATEGORY_INFO[categoryID]
    local isWeaponCat = (catInfo and catInfo.isWeapon) or (categoryID and categoryID >= 12) or false
    local groupByAppearance = self.filters.groupByAppearance
    
    for i = 1, #items do
        local item = items[i]
        if item then
            local include = true
            local displayItem = item  -- The item to show (may be swapped from alternatives)
            
            -- Collection filter
            if include then
                if self.filters.collected == "collected" and not item.isCollected then
                    include = false
                    filterStats.filteredByCollection = filterStats.filteredByCollection + 1
                elseif self.filters.collected == "uncollected" and item.isCollected then
                    include = false
                    filterStats.filteredByCollection = filterStats.filteredByCollection + 1
                end
            end
            
            -- Source type filter
            if include and sourceFilter and sourceFilter ~= "all" then
                local itemSrc = item.sourceType
                if not itemSrc then
                    include = false
                elseif sourceFilter == "boss" then
                    include = (itemSrc == "BOSS")
                elseif sourceFilter == "raid" then
                    include = (itemSrc == "BOSS" and item.isRaid == true)
                elseif sourceFilter == "dungeon" then
                    include = (itemSrc == "BOSS" and not item.isRaid)
                elseif sourceFilter == "quest" then
                    include = (itemSrc == "QUEST")
                elseif sourceFilter == "vendor" then
                    include = (itemSrc == "VENDOR")
                elseif sourceFilter == "crafted" then
                    include = (itemSrc == "CRAFTED" or itemSrc == "PROFESSION")
                elseif sourceFilter == "pvp" then
                    include = (itemSrc == "PVP")
                elseif sourceFilter == "world_drop" then
                    include = (itemSrc == "WORLD_DROP")
                else
                    -- Generic match (case-insensitive)
                    include = (itemSrc:lower() == sourceFilter)
                end
            end
            
            -- Expansion filter
            if include and expansionFilter and expansionFilter ~= "all" then
                local itemExp = item.expansion
                if not itemExp then
                    include = false
                else
                    -- Normalize: data may be "Classic", "TBC", "Wrath", "Cata", "MoP" (mixed case)
                    include = (itemExp:lower() == expansionFilter:lower())
                end
            end
            
            -- Search filter (multi-word support)
            if include and searchLower ~= "" then
                local nameLower = (item.name or ""):lower()
                local sourceLower = (item.dropSource or ""):lower()
                
                -- Check if ALL search words are found
                local allWordsFound = true
                for word in searchLower:gmatch("%S+") do
                    if not nameLower:find(word, 1, true) and not sourceLower:find(word, 1, true) then
                        allWordsFound = false
                        break
                    end
                end
                
                if not allWordsFound then
                    include = false
                    filterStats.filteredBySearch = filterStats.filteredBySearch + 1
                end
            end
            
            -- Color filter (zero-allocation: ItemMatchesCategory uses direct arithmetic)
            if include and self.filters.colorFilter then
                if ETB.ColorSearch and ETB.ColorSearch:HasData() then
                    local vid = item.visualID
                    local hasMapData = vid and ETB.ColorMap and ETB.ColorMap[vid]
                    if hasMapData or (item.name and item.name ~= "Loading...") then
                        -- ItemMatchesCategory checks ColorMap first, then name fallback
                        -- Both paths are zero-allocation (no temporary tables)
                        if not ETB.ColorSearch:ItemMatchesCategory(vid, item.name, self.filters.colorFilter) then
                            include = false
                            filterStats.filteredByColor = filterStats.filteredByColor + 1
                        end
                    else
                        -- No data at all — pass through
                        filterStats.colorPassedNoData = filterStats.colorPassedNoData + 1
                    end
                end
            end
            
            -- Class restriction filter (classFilter)
            -- When set, hide items not usable by this class
            if include and self.filters.classFilter then
                -- Check 1: Armor type vs player's primary armor (most reliable)
                -- Skip for cosmetic categories: Back(3), Shirt(5), Tabard(6)
                local COSMETIC_CATS = { [3]=true, [5]=true, [6]=true }
                if item.armorType and not COSMETIC_CATS[categoryID] then
                    if ETB.BlizzardData and not ETB.BlizzardData:CanPlayerWearArmorType(item.armorType) then
                        -- Check alternatives for a wearable version first
                        local hasWearableAlt = false
                        if groupByAppearance and item.alternatives and #item.alternatives > 1 then
                            for _, alt in ipairs(item.alternatives) do
                                if not alt.armorType or ETB.BlizzardData:CanPlayerWearArmorType(alt.armorType) then
                                    hasWearableAlt = true
                                    displayItem = alt
                                    displayItem.alternatives = item.alternatives
                                    displayItem.hasAlternatives = item.hasAlternatives
                                    displayItem.alternativeCount = item.alternativeCount
                                    break
                                end
                            end
                        end
                        if not hasWearableAlt then
                            include = false
                            filterStats.filteredByClass = filterStats.filteredByClass + 1
                        end
                    end
                end
                -- Check 2: Blizzard API isUsable (catches class-locked items like tier sets)
                if include and item.isUsable == false then
                    local hasUsableAlt = false
                    if groupByAppearance and item.alternatives and #item.alternatives > 1 then
                        for _, alt in ipairs(item.alternatives) do
                            if alt.isUsable ~= false then
                                hasUsableAlt = true
                                displayItem = alt
                                displayItem.alternatives = item.alternatives
                                displayItem.hasAlternatives = item.hasAlternatives
                                displayItem.alternativeCount = item.alternativeCount
                                break
                            end
                        end
                    end
                    if not hasUsableAlt then
                        include = false
                        filterStats.filteredByClass = filterStats.filteredByClass + 1
                    end
                end
                -- Check 3: Explicit class restriction data (tier sets, class-locked items)
                if include and ETB.ClassRestrictions and item.itemID then
                    if not ETB.ClassRestrictions:IsItemUsableByClass(item.itemID, self.filters.classFilter) then
                        include = false
                        filterStats.filteredByClass = filterStats.filteredByClass + 1
                    end
                end
            end
            
            -- Armor type filter
            if include and armorFilter ~= "all" and not isWeaponCat then
                if item.armorType == armorFilter then
                    -- Item directly matches
                elseif groupByAppearance and item.alternatives and #item.alternatives > 1 then
                    -- In grouped mode: check alternatives for matching armor type
                    local foundMatch = false
                    for _, alt in ipairs(item.alternatives) do
                        if alt.armorType == armorFilter then
                            displayItem = alt  -- Swap to the matching alternative
                            displayItem.alternatives = item.alternatives
                            displayItem.hasAlternatives = item.hasAlternatives
                            displayItem.alternativeCount = item.alternativeCount
                            displayItem.availableArmorTypes = item.availableArmorTypes
                            foundMatch = true
                            filterStats.foundInAlternative = filterStats.foundInAlternative + 1
                            break
                        end
                    end
                    if not foundMatch then
                        include = false
                        filterStats.filteredByArmorType = filterStats.filteredByArmorType + 1
                    end
                else
                    -- Doesn't match
                    include = false
                    filterStats.filteredByArmorType = filterStats.filteredByArmorType + 1
                end
            end
            
            if include then
                filtered[#filtered + 1] = displayItem
                filterStats.passed = filterStats.passed + 1
            end
        end
    end
    
    -- Debug logging
    ETB:Debug("Filtering", string.format("ApplyFilters: %d total → %d passed", filterStats.total, filterStats.passed))
    if armorFilter ~= "all" then
        ETB:Debug("Filtering", string.format("  Armor filter '%s': %d filtered, %d found in alternatives", 
            armorFilter, filterStats.filteredByArmorType, filterStats.foundInAlternative))
    end
    if filterStats.filteredBySearch > 0 then
        ETB:Debug("Filtering", string.format("  Search filter: %d filtered out", filterStats.filteredBySearch))
    end
    if filterStats.filteredByCollection > 0 then
        ETB:Debug("Filtering", string.format("  Collection filter: %d filtered out", filterStats.filteredByCollection))
    end
    
    -- Color filter diagnostics (debug only)
    if self.filters.colorFilter then
        ETB:Debug("Filtering", string.format("Color '%s': %d items → %d passed | color: -%d | class: -%d | armor: -%d | nodata: %d",
            self.filters.colorFilter, filterStats.total, filterStats.passed,
            filterStats.filteredByColor, filterStats.filteredByClass, 
            filterStats.filteredByArmorType, filterStats.colorPassedNoData))
    end
    
    return filtered
end

-- =============================================================================
-- SORTING
-- =============================================================================

function Filter:SetSort(sortBy, ascending)
    self.sortBy = sortBy
    self.sortAscending = ascending ~= false
    ETB.EventBus:Fire("ETB_SORT_CHANGED", sortBy, ascending)
end

function Filter:SortItems(items)
    if not items or #items == 0 then return items end
    
    -- Compact in-place: remove nil values without allocating new table
    local writeIdx = 0
    for i = 1, #items do
        if items[i] then
            writeIdx = writeIdx + 1
            if writeIdx ~= i then
                items[writeIdx] = items[i]
                items[i] = nil
            end
        end
    end
    -- Trim trailing entries
    for i = writeIdx + 1, #items do
        items[i] = nil
    end
    
    if writeIdx == 0 then return items end
    
    local ascending = self.sortAscending
    local sortFunc
    
    if self.sortBy == "name" then
        sortFunc = function(a, b)
            -- Guard against nil values
            if not a then return false end
            if not b then return true end
            -- Convert to strings to handle both numbers and strings
            local na = tostring(a.name or "")
            local nb = tostring(b.name or "")
            if ascending then
                return na < nb
            else
                return na > nb
            end
        end
    elseif self.sortBy == "quality" then
        sortFunc = function(a, b)
            if not a then return false end
            if not b then return true end
            local qa, qb = a.quality or 0, b.quality or 0
            if qa == qb then 
                local na = tostring(a.name or "")
                local nb = tostring(b.name or "")
                return na < nb 
            end
            if ascending then
                return qa < qb
            else
                return qa > qb
            end
        end
    elseif self.sortBy == "collected" then
        sortFunc = function(a, b)
            if not a then return false end
            if not b then return true end
            local ca = a.isCollected and 1 or 0
            local cb = b.isCollected and 1 or 0
            if ca == cb then 
                local na = tostring(a.name or "")
                local nb = tostring(b.name or "")
                return na < nb 
            end
            if ascending then
                return ca < cb
            else
                return ca > cb
            end
        end
    elseif self.sortBy == "source" then
        sortFunc = function(a, b)
            if not a then return false end
            if not b then return true end
            local sa, sb = a.dropSource or "zzz", b.dropSource or "zzz"
            if ascending then
                return sa < sb
            else
                return sa > sb
            end
        end
    elseif self.sortBy == "weaponType" then
        sortFunc = function(a, b)
            if not a then return false end
            if not b then return true end
            local wa, wb = a.weaponType or "zzz", b.weaponType or "zzz"
            if wa == wb then return (a.name or "") < (b.name or "") end
            if ascending then
                return wa < wb
            else
                return wa > wb
            end
        end
    elseif self.sortBy == "weaponHand" then
        sortFunc = function(a, b)
            if not a then return false end
            if not b then return true end
            -- Two-handed first (1), then one-handed (0)
            local ha, hb = (a.isTwoHanded and 1 or 0), (b.isTwoHanded and 1 or 0)
            if ha == hb then
                -- If same hand type, sort by weapon type, then name
                local wa, wb = a.weaponType or "zzz", b.weaponType or "zzz"
                if wa == wb then return (a.name or "") < (b.name or "") end
                return wa < wb
            end
            if ascending then
                return ha < hb
            else
                return ha > hb
            end
        end
    elseif self.sortBy == "weaponTypeHand" then
        -- Sort by weapon type first, then by one/two-handed
        sortFunc = function(a, b)
            if not a then return false end
            if not b then return true end
            local wa, wb = a.weaponType or "zzz", b.weaponType or "zzz"
            if wa == wb then
                -- Same weapon type, sort by hand type
                local ha, hb = (a.isTwoHanded and 1 or 0), (b.isTwoHanded and 1 or 0)
                if ha == hb then return (a.name or "") < (b.name or "") end
                if ascending then
                    return ha < hb
                else
                    return ha > hb
                end
            end
            if ascending then
                return wa < wb
            else
                return wa > wb
            end
        end
    elseif self.sortBy == "color" and self.filters.colorFilter then
        -- Sort by color match: matched items first, unknown last, then alphabetical
        local colorID = self.filters.colorFilter
        local cat = ETB.ColorSearch and ETB.ColorSearch.CategoryByID[colorID]
        local targetIdx = cat and cat.idx
        -- Pre-compute scores for all items (one pass, zero allocation per item)
        -- This avoids creating tables inside sort comparisons (16,000+ calls)
        local scoreCache = {}
        sortFunc = function(a, b)
            if not a then return false end
            if not b then return true end
            local sa = scoreCache[a]
            if not sa then
                if not ETB.ColorSearch or not ETB.ColorMap then
                    sa = 2
                else
                    local packed = a.visualID and ETB.ColorMap[a.visualID]
                    if packed then
                        -- Inline PackedHasColor (zero alloc)
                        local _match = false
                        if packed <= 12 then _match = packed == targetIdx
                        elseif packed <= 1299 then _match = math.floor(packed / 100) == targetIdx or packed % 100 == targetIdx
                        else _match = math.floor(packed / 10000) == targetIdx; if not _match then local _r = packed % 10000; _match = math.floor(_r / 100) == targetIdx or _r % 100 == targetIdx end end
                        if _match then
                            sa = 0
                        else
                            sa = 1
                        end
                    else
                        sa = 2
                    end
                end
                scoreCache[a] = sa
            end
            local sb = scoreCache[b]
            if not sb then
                if not ETB.ColorSearch or not ETB.ColorMap then
                    sb = 2
                else
                    local packed = b.visualID and ETB.ColorMap[b.visualID]
                    if packed then
                        -- Inline PackedHasColor (zero alloc)
                        local _match = false
                        if packed <= 12 then _match = packed == targetIdx
                        elseif packed <= 1299 then _match = math.floor(packed / 100) == targetIdx or packed % 100 == targetIdx
                        else _match = math.floor(packed / 10000) == targetIdx; if not _match then local _r = packed % 10000; _match = math.floor(_r / 100) == targetIdx or _r % 100 == targetIdx end end
                        if _match then
                            sb = 0
                        else
                            sb = 1
                        end
                    else
                        sb = 2
                    end
                end
                scoreCache[b] = sb
            end
            if sa ~= sb then return sa < sb end
            return (a.name or "") < (b.name or "")
        end
    end
    
    if sortFunc then
        local success, err = pcall(table.sort, items, sortFunc)
        if not success then
            ETB:PrintError("Filtering: Sort error: " .. tostring(err))
            ETB:Debug("Filtering", "Sort error details - items count: " .. #items)
        else
            ETB:Debug("Filtering", string.format("SortItems: Sorted %d items by %s (%s)", 
                #items, self.sortBy or "default", self.sortAscending and "asc" or "desc"))
        end
    else
        ETB:Debug("Filtering", string.format("SortItems: No sort function, returning %d items as-is", #items))
    end
    
    return items
end

-- =============================================================================
-- GET FILTERED ITEMS
-- =============================================================================

function Filter:GetItems(options)
    options = options or {}
    
    local bdOptions = { 
        limit = options.limit or 100000
    }
    
    local function isWeaponCategory(catID)
        if not catID then return false end
        local catInfo = ETB.CATEGORY_INFO and ETB.CATEGORY_INFO[catID]
        if catInfo and catInfo.isWeapon then
            return true
        end
        return catID >= 12
    end
    
    local categoryID = self.filters.category
    bdOptions.onlyUsable = (self.filters.classFilter ~= nil)
    
    if self.filters.collected == "collected" then
        bdOptions.onlyCollected = true
    elseif self.filters.collected == "uncollected" then
        bdOptions.onlyUncollected = true
    end
    
    local items = {}
    
    if self.filters.showWeaponsOnly then
        for catID, catInfo in pairs(ETB.CATEGORY_INFO or {}) do
            if catInfo.isWeapon then
                local catItems = ETB.BlizzardData:GetItemsForCategory(catID, bdOptions)
                for _, item in ipairs(catItems) do
                    items[#items + 1] = item
                end
            end
        end
    else
        items = ETB.BlizzardData:GetItemsForCategory(self.filters.category, bdOptions)
    end
    
    -- Apply UI-level filters
    items = self:ApplyFilters(items)
    
    -- Sort items
    items = self:SortItems(items)
    
    return items
end

-- =============================================================================
-- FILTER SUMMARY
-- =============================================================================

function Filter:GetFilterSummary()
    local parts = {}
    
    if self.filters.showWeaponsOnly then
        parts[#parts + 1] = "Weapons Only"
    end
    if self.filters.classFilter then
        local CLASS_NAMES = {
            [1]="Warrior",[2]="Paladin",[3]="Hunter",[4]="Rogue",[5]="Priest",
            [6]="Death Knight",[7]="Shaman",[8]="Mage",[9]="Warlock",[10]="Monk",[11]="Druid",
        }
        parts[#parts + 1] = CLASS_NAMES[self.filters.classFilter] or "Class"
    end
    if self.filters.collected ~= "all" then
        parts[#parts + 1] = self.filters.collected
    end
    if self.filters.searchText ~= "" then
        parts[#parts + 1] = '"' .. self.filters.searchText .. '"'
    end
    if self.filters.sourceType ~= "all" then
        parts[#parts + 1] = self.filters.sourceType
    end
    if self.filters.expansion ~= "all" then
        parts[#parts + 1] = self.filters.expansion
    end
    if self.filters.quality ~= "all" then
        parts[#parts + 1] = ETB:GetQualityName(self.filters.quality)
    end
    if self.filters.weaponType ~= "all" then
        parts[#parts + 1] = self.filters.weaponType
    end
    
    return #parts > 0 and table.concat(parts, ", ") or "No filters"
end

function Filter:HasActiveFilters()
    return self.filters.collected ~= "all" or
           self.filters.searchText ~= "" or
           self.filters.sourceType ~= "all" or
           self.filters.quality ~= "all" or
           self.filters.expansion ~= "all"
    -- Note: myClassOnly is always on by default, not counted as "active filter"
end

ETB:Debug("Filtering", "Filtering.lua loaded")

-- Default classFilter to player's class (delayed to ensure UnitClass is available)
-- This runs at file load time; UnitClass("player") works during addon loading
do
    local _, _, playerClassID = UnitClass("player")
    if playerClassID then
        Filter.filters.classFilter = playerClassID
        Filter.filters.myClassOnly = true
    end
end
