-- Proficiency Data Handler
-- Handles loading and processing of proficiency JSON data
if not ProficiencyData then
    ProficiencyData = {}
    ProficiencyData.__index = ProficiencyData
    ProficiencyData.content = {}
    ProficiencyData.nameIndex = {} -- Index by normalized name for quick lookup
end
-- LuaFormatter off
-- Item tier patterns for matching item names to proficiency entries
-- Order matters - more specific patterns first
local TIER_PATTERNS = {
    -- Inferniarch variants (most specific first)
    { pattern = "siphoning inferniarch", tier = "Siphoning Inferniarch" },
    { pattern = "draining inferniarch", tier = "Draining Inferniarch" },
    { pattern = "rending inferniarch", tier = "Rending Inferniarch" },
    { pattern = "inferniarch", tier = "Inferniarch" },
    -- Eldritch variants
    { pattern = "gilded eldritch", tier = "Gilded Eldritch" },
    { pattern = "eldritch", tier = "Eldritch" },
    -- Umbral variants
    { pattern = "master umbral", tier = "Master Umbral" },
    { pattern = "crude umbral", tier = "Crude Umbral" },
    { pattern = "umbral", tier = "Umbral" },
    -- Sanguine variants
    { pattern = "grand sanguine", tier = "Grand Sanguine" },
    { pattern = "sanguine", tier = "Sanguine" },
    -- Other tiers
    { pattern = "amber", tier = "Amber" },
    { pattern = "soul", tier = "Soul" },
    { pattern = "cobra", tier = "Cobra" },
    { pattern = "falcon", tier = "Falcon" },
    { pattern = "naga", tier = "Naga" },
    { pattern = "lion", tier = "Lion" },
    { pattern = "jungle", tier = "Jungle" },
    { pattern = "glooth", tier = "Glooth" },
    { pattern = "destruction", tier = "Destruction" },
}
-- LuaFormatter on
-- Weapon type keywords
local WEAPON_KEYWORDS = {
    sword = {"sword", "blade", "sabre", "dagger", "knife", "slayer", "chopper"},
    axe = {"axe", "hatchet", "cleaver"},
    club = {"club", "hammer", "mace", "staff", "cudgel", "flail", "morningstar", "sceptre"},
    bow = {"bow", "crossbow", "arbalest"},
    throw = {"star", "spear", "javelin", "throwing"},
    fist = {"fist", "brass knuckles", "gloves"},
    wand = {"wand"},
    rod = {"rod"}
}

local function resolveProficienciesFile()
    local file = g_things.getProficienciesFile()
    if file and file ~= "" then
        return file
    end
    return nil
end

-- Load proficiency data from JSON file
function ProficiencyData:loadProficiencyJson(skipItemCache)
    self.content = {}
    self.nameIndex = {}

    local file = resolveProficienciesFile()
    if not file then
        return false
    end

    local status, result = pcall(function()
        return json.decode(g_resources.readFileContents(file))
    end)

    if not status or type(result) ~= "table" then
        return false
    end

    self.loadedFile = file

    for _, data in pairs(result) do
        if type(data) == "table" then
            local ProficiencyId = data["ProficiencyId"]
            local name = data["Name"]
            if ProficiencyId then
                self.content[ProficiencyId] = data
            end

            -- Build name-based index for quick lookup
            if ProficiencyId and name then
                local lowerName = string.lower(name)
                self.nameIndex[lowerName] = ProficiencyId
            end
        end
    end

    if WeaponProficiency and not skipItemCache then
        WeaponProficiency:createItemCache()
    end

    return true
end

-- Try to find an item-specific proficiency entry
-- JSON has entries like "Throw - Assassin Star", "Sword 1H Assassin Dagger", etc.
function ProficiencyData:findItemSpecificProficiency(itemName, weaponType, isTwoHanded)
    if not itemName then
        return nil
    end
    local lowerName = string.lower(itemName)

    -- Build possible JSON entry names based on weapon type
    local handedness = isTwoHanded and "2H" or "1H"
    local possibleNames = {}

    -- Determine weapon category prefix
    local prefix = nil
    if weaponType == 8 or weaponType == "throw" then
        -- Throwing weapons use "Throw - {ItemName}" format
        table.insert(possibleNames, "throw - " .. lowerName)
    else
        -- Map weapon types to prefixes used in JSON
        local prefixMap = {
            [3] = "Sword", -- WEAPON_SWORD
            [2] = "Axe", -- WEAPON_AXE  
            [1] = "Club", -- WEAPON_CLUB
            [6] = "Distance", -- WEAPON_BOW
            [7] = "Distance", -- WEAPON_BOW (alt)
            [9] = "Distance", -- WEAPON_CROSSBOW
            [0] = "Fist", -- WEAPON_FIST
            [4] = nil -- WEAPON_WANDROD - handled separately
        }
        prefix = prefixMap[weaponType]

        -- For wands/rods, check item name
        if weaponType == 4 then
            if string.find(lowerName, "rod", 1, true) then
                prefix = "Rod"
            else
                prefix = "Wand"
            end
        end

        if prefix then
            -- Try "{WeaponType} {1H/2H} {ItemName}" format
            table.insert(possibleNames, string.lower(prefix .. " " .. handedness .. " " .. itemName))
            -- Also try without handedness for some items
            table.insert(possibleNames, string.lower(prefix .. " " .. itemName))
        end

        -- Also try "Caster" prefix for wands/rods/staffs
        if prefix == "Wand" or prefix == "Rod" or string.find(lowerName, "staff", 1, true) then
            table.insert(possibleNames, string.lower("Caster " .. handedness .. " " .. itemName))
            table.insert(possibleNames, string.lower("Caster " .. itemName))
        end
    end

    -- Check each possible name in the index
    for _, searchName in ipairs(possibleNames) do
        local profId = self.nameIndex[searchName]
        if profId and self:isValidProficiencyId(profId) then
            return profId
        end
    end

    -- Also do a partial match search for item name in all entries
    -- Only match if the JSON name contains the item name as a suffix (after " - " or at the end)
    -- AND the weapon type matches (to avoid Club matching for Wand items)
    local weaponPrefix = nil
    if weaponType then
        if weaponType == 3 then
            weaponPrefix = "sword"
        elseif weaponType == 2 then
            weaponPrefix = "axe"
        elseif weaponType == 1 then
            weaponPrefix = "club"
        elseif weaponType == 4 then
            weaponPrefix = "wand" -- Also matches "rod"
        elseif weaponType == 6 or weaponType == 7 or weaponType == 8 or weaponType == 9 then
            weaponPrefix = "bow"
        elseif weaponType == 0 then
            weaponPrefix = "fist"
        end
    end

    for jsonName, profId in pairs(self.nameIndex) do
        -- Check formats like "Throw - Assassin Star" or "Sword 1H Assassin Dagger"
        -- The item name should appear after " - " or as the last words

        -- Check for " - itemname" at the end
        local dashSuffix = " - " .. lowerName
        if #jsonName >= #dashSuffix and string.sub(jsonName, -#dashSuffix) == dashSuffix then
            -- Verify weapon type matches if we have one
            if weaponPrefix then
                local firstWord = string.match(jsonName, "^(%w+)")
                if firstWord and string.lower(firstWord) == weaponPrefix then
                    return profId
                elseif weaponPrefix == "wand" and firstWord and
                    (string.lower(firstWord) == "wand" or string.lower(firstWord) == "rod" or string.lower(firstWord) ==
                        "caster") then
                    return profId
                end
                -- If weapon type doesn't match, skip this entry
            else
                return profId
            end
        end

        -- Check for " itemname" at the end (space + item name)
        local spaceSuffix = " " .. lowerName
        if #jsonName >= #spaceSuffix and string.sub(jsonName, -#spaceSuffix) == spaceSuffix then
            -- Verify weapon type matches if we have one
            if weaponPrefix then
                local firstWord = string.match(jsonName, "^(%w+)")
                if firstWord and string.lower(firstWord) == weaponPrefix then
                    return profId
                elseif weaponPrefix == "wand" and firstWord and
                    (string.lower(firstWord) == "wand" or string.lower(firstWord) == "rod" or string.lower(firstWord) ==
                        "caster") then
                    return profId
                end
                -- If weapon type doesn't match, skip this entry
            else
                return profId
            end
        end
    end

    return nil
end

-- Detect item tier from name
function ProficiencyData:detectItemTier(itemName)
    if not itemName then
        return "Sanguine"
    end
    local lowerName = string.lower(itemName)

    for _, tierInfo in ipairs(TIER_PATTERNS) do
        if string.find(lowerName, tierInfo.pattern, 1, true) then
            return tierInfo.tier
        end
    end

    return "Sanguine" -- Default tier
end

-- Detect weapon type from item name
function ProficiencyData:detectWeaponTypeFromName(itemName)
    if not itemName then
        return nil
    end
    local lowerName = string.lower(itemName)

    -- Check each weapon type's keywords
    for weaponType, keywords in pairs(WEAPON_KEYWORDS) do
        for _, keyword in ipairs(keywords) do
            if string.find(lowerName, keyword, 1, true) then
                return weaponType
            end
        end
    end

    return nil
end

-- Get proficiency ID by matching item name to JSON entry
function ProficiencyData:getProficiencyIdByItemName(itemName, weaponType, isTwoHanded)
    if not itemName then
        return nil
    end

    local tier = self:detectItemTier(itemName)
    local handedness = isTwoHanded and "2H" or "1H"

    -- Determine weapon category name for JSON lookup
    local weaponCategoryName = nil
    if weaponType then
        if weaponType == 3 or weaponType == "sword" then -- WEAPON_SWORD
            weaponCategoryName = "Sword"
        elseif weaponType == 2 or weaponType == "axe" then -- WEAPON_AXE
            weaponCategoryName = "Axe"
        elseif weaponType == 1 or weaponType == "club" then -- WEAPON_CLUB
            weaponCategoryName = "Club"
        elseif weaponType == 6 or weaponType == 7 or weaponType == 8 or weaponType == 9 or weaponType == "bow" or
            weaponType == "throw" then
            weaponCategoryName = "Bow"
            handedness = "2H" -- Distance is always 2H in JSON
        elseif weaponType == 0 or weaponType == "fist" then -- WEAPON_FIST
            weaponCategoryName = "Fist"
            handedness = "2H" -- Fist is 2H in JSON
        elseif weaponType == 4 or weaponType == "wand" then -- WEAPON_WANDROD
            -- Detect if it's a wand or rod from name
            local lowerName = string.lower(itemName)
            if string.find(lowerName, "rod", 1, true) then
                weaponCategoryName = "Rod"
            else
                weaponCategoryName = "Wand"
            end
            handedness = "1H"
        elseif weaponType == "rod" then
            weaponCategoryName = "Rod"
            handedness = "1H"
        end
    else
        -- Try to detect from item name
        local detectedType = self:detectWeaponTypeFromName(itemName)
        if detectedType == "sword" then
            weaponCategoryName = "Sword"
        elseif detectedType == "axe" then
            weaponCategoryName = "Axe"
        elseif detectedType == "club" then
            weaponCategoryName = "Club"
        elseif detectedType == "bow" or detectedType == "throw" then
            weaponCategoryName = "Bow"
            handedness = "2H"
        elseif detectedType == "fist" then
            weaponCategoryName = "Fist"
            handedness = "2H"
        elseif detectedType == "wand" then
            weaponCategoryName = "Wand"
            handedness = "1H"
        elseif detectedType == "rod" then
            weaponCategoryName = "Rod"
            handedness = "1H"
        end
    end

    if not weaponCategoryName then
        return nil
    end

    -- Build the expected JSON entry name
    -- Format: "Tier HandH WeaponType" e.g. "Grand Sanguine 2H Bow"
    -- Special case for Inferniarch Distance
    local jsonName
    if tier == "Inferniarch" and weaponCategoryName == "Bow" then
        jsonName = string.format("%s %s Distance", tier, handedness)
    else
        jsonName = string.format("%s %s %s", tier, handedness, weaponCategoryName)
    end

    local lowerJsonName = string.lower(jsonName)
    local profId = self.nameIndex[lowerJsonName]

    if profId then
        return profId
    end

    return nil
end

-- Check if proficiency ID is valid
function ProficiencyData:isValidProficiencyId(id)
    return self.content[id] ~= nil
end

-- Get proficiency content by ID
function ProficiencyData:getContentById(id)
    local content = self.content[id]
    return content and content or nil
end

-- Get proficiency ID based on market category
-- Market categories from RTC: Axes=17, Clubs=18, Distance=19, Swords=20, Wands=21, Fist=27
-- Proficiency IDs from JSON (verified):
--   6=Sanguine 1H Sword, 8=Sanguine 1H Axe, 9=Sanguine 1H Club, 10=Sanguine 2H Sword
--   11=Sanguine 2H Axe, 12=Sanguine 2H Club, 13=Sanguine 2H Bow, 14=Sanguine 2H Fist, 15=Sanguine 1H Wand
function ProficiencyData:getProficiencyIdFromCategory(marketCategory, itemName)
    -- MarketCategory enum values from RTC -> Correct Proficiency IDs
    local categoryMap = {
        [17] = 8, -- MarketCategory.Axes -> Proficiency 8 (Axe)
        [18] = 9, -- MarketCategory.Clubs -> Proficiency 9 (Club)
        [20] = 6, -- MarketCategory.Swords -> Proficiency 6 (Sword)
        [21] = 15, -- MarketCategory.WandsRods -> Proficiency 15 (Wand)
        [27] = 14 -- MarketCategory.FistWeapons -> Proficiency 14 (Fist)
    }

    -- For distance weapons (category 19), all use Bow proficiency (13)
    if marketCategory == 19 then
        return 13 -- Bow proficiency for all distance weapons
    end

    return categoryMap[marketCategory]
end

-- Get a default proficiency ID based on weapon type
-- Weapon types: 0=None, 1=Club, 2=Axe, 3=Sword, 4=Wand/Rod, 5=Shield, 6=Bow, 7=Bow, 8=Throwing, 9=Crossbow
-- Proficiency IDs from JSON (verified):
--   6=Sword, 8=Axe, 9=Club, 13=Bow, 14=Fist, 15=Wand
function ProficiencyData:getDefaultProficiencyId(weaponType)
    local defaultMap = {
        [0] = 14, -- Fist/None -> ID 14 (Fist)
        [1] = 9, -- WEAPON_CLUB -> Proficiency ID 9 (Club)
        [2] = 8, -- WEAPON_AXE -> Proficiency ID 8 (Axe)
        [3] = 6, -- WEAPON_SWORD -> Proficiency ID 6 (Sword)
        [4] = 15, -- WEAPON_WANDROD -> Proficiency ID 15 (Wand)
        [6] = 13, -- WEAPON_BOW -> Proficiency ID 13 (Bow)
        [7] = 13, -- WEAPON_BOW (alt) -> Proficiency ID 13 (Bow)
        [8] = 13, -- WEAPON_THROW -> Proficiency ID 13 (Bow)
        [9] = 13, -- WEAPON_CROSSBOW -> Proficiency ID 13 (Bow - same base perks)
        [17] = 8, -- MarketCategory.Axes
        [18] = 9, -- MarketCategory.Clubs
        [19] = 13, -- MarketCategory.DistanceWeapons
        [20] = 6, -- MarketCategory.Swords
        [21] = 15, -- MarketCategory.WandsRods
        [27] = 14 -- MarketCategory.FistWeapons
    }
    return defaultMap[weaponType] or 6
end

-- Get proficiency ID for an item, with fallback to default
-- Can pass either an Item, ThingType, or both via a table {item=..., thingType=...}
-- Also accepts marketData for category-based lookup
function ProficiencyData:getProficiencyIdForItem(displayItem, thingType, marketData)
    if not displayItem and not thingType and not marketData then
        return 6 -- Default fallback
    end

    -- Prefer the original ThingType over the visual Item, which may use marketData.showAs.
    if thingType and thingType.getProficiencyId then
        local id = thingType:getProficiencyId()
        if id and id > 0 and self:isValidProficiencyId(id) then
            return id
        end
    end

    -- Try to get proficiencyId from item if method exists
    if displayItem and displayItem.getProficiencyId then
        local id = displayItem:getProficiencyId()
        if id and id > 0 and self:isValidProficiencyId(id) then
            return id
        end
    end

    -- Get item name and weapon type for name-based lookup
    local itemName = nil
    local weaponType = nil
    local isTwoHanded = false

    if marketData then
        itemName = marketData.name
    end

    if thingType then
        if not itemName and thingType.getMarketData then
            local md = thingType:getMarketData()
            if md then
                itemName = md.name
            end
        end
        if thingType.getWeaponType then
            weaponType = thingType:getWeaponType()
        end
        if thingType.isTwoHanded then
            isTwoHanded = thingType:isTwoHanded()
        end
    end

    -- FORCE FIX: If item is in WandsRods category (21), override weaponType to 4 (Wand)
    -- This fixes items like Ferumbras' staff that have incorrect weaponType in .dat
    local marketCat = nil
    if marketData and marketData.category then
        marketCat = marketData.category
    elseif thingType and thingType.getMarketData then
        local md = thingType:getMarketData()
        if md then
            marketCat = md.category
        end
    end

    if marketCat == 21 then -- MarketCategory.WandsRods
        weaponType = 4 -- Force to WEAPON_WANDROD
    end

    if displayItem and not itemName then
        if displayItem.getName then
            itemName = displayItem:getName()
        end
        if not weaponType and displayItem.getWeaponType then
            weaponType = displayItem:getWeaponType()
        end
    end

    -- Get market category for debugging
    local marketCategory = nil
    if marketData then
        marketCategory = marketData.category
    elseif thingType and thingType.getMarketData then
        local md = thingType:getMarketData()
        if md then
            marketCategory = md.category
        end
    end

    -- FIRST: Try to find item-specific proficiency entry (e.g., "Throw - Assassin Star")
    if itemName then
        local profId = self:findItemSpecificProficiency(itemName, weaponType, isTwoHanded)
        if profId and self:isValidProficiencyId(profId) then
            return profId
        end
    end

    -- SECOND: Try tier-based lookup (for tiered items like "grand sanguine bow")
    if itemName then
        local profId = self:getProficiencyIdByItemName(itemName, weaponType, isTwoHanded)
        if profId and self:isValidProficiencyId(profId) then
            return profId
        end
    end

    -- Fallback to category-based lookup
    if marketData and marketData.category then
        local profId = self:getProficiencyIdFromCategory(marketData.category, itemName)
        if profId and self:isValidProficiencyId(profId) then
            return profId
        end
    end

    if thingType and thingType.getMarketData then
        local md = thingType:getMarketData()
        if md and md.category then
            local profId = self:getProficiencyIdFromCategory(md.category, md.name)
            if profId and self:isValidProficiencyId(profId) then
                return profId
            end
        end
    end

    -- Final fallback: weapon type default
    weaponType = weaponType or 0

    if weaponType > 0 then
        local defaultId = self:getDefaultProficiencyId(weaponType)
        if self:isValidProficiencyId(defaultId) then
            return defaultId
        end
    end

    -- Last resort: return first available proficiency ID
    for id, _ in pairs(self.content) do
        return id
    end

    return 6
end

-- Get number of perk lanes for a proficiency
function ProficiencyData:getPerkLaneCount(id)
    local content = self.content[id]
    if not content then
        return 0
    end
    return table.size(content.Levels or {})
end

-- Format float value for display
function ProficiencyData:formatFloatValue(value, roundFloat, perkType)
    local function isPercentageType(perkType)
        for _, v in ipairs(PercentageTypes) do
            if v == perkType then
                return true
            end
        end
        return false
    end

    local isInteger = math.floor(value) == value
    if not isInteger or (isInteger and isPercentageType(perkType)) then
        local percentage = value * 100
        if roundFloat then
            local intPart = math.floor(percentage)
            local decimal1 = math.floor(percentage * 10 + 0.5) / 10
            if percentage == intPart then
                return tostring(intPart)
            elseif percentage == decimal1 then
                return string.format("%.1f", percentage)
            else
                return string.format("%.2f", percentage)
            end
        else
            return string.format("%.2f", percentage)
        end
    else
        return tostring(value)
    end
end

-- Get image source and clip for a perk
function ProficiencyData:getImageSourceAndClip(perkData)
    local perkType = perkData.Type
    local data = PerkVisualData[perkType]
    local source = (data and data.source) or "icons-0"
    local imagePath = proficiencyImage(source)

    if not data then
        return imagePath, "0 0"
    end

    if perkType == PERK_SPELL_AUGMENT then
        local spellData = SpellAugmentIcons[perkData.SpellId]
        if spellData then
            return imagePath, spellData.imageOffset
        end
        return imagePath, "0 0"
    end

    if perkType == PERK_BESTIARY_DAMAGE then
        local bestiaryType = BestiaryCategories[perkData.BestiaryName]
        return imagePath, (bestiaryType and bestiaryType.imageOffset) or "0 0"
    end

    if perkType == PERK_MAGIC_BONUS then
        local elementData = MagicBoostMask[perkData.DamageType]
        return imagePath, elementData and elementData.imageOffset or "0 0"
    end

    if ElementalCritical_t[perkType] then
        local elementData = ElementalMask[perkData.ElementId]
        return imagePath, (elementData and elementData.imageOffset) or "0 0"
    end

    if FlatDamageBonus_t[perkType] then
        local skillData = SkillTypes[perkData.SkillId]
        return imagePath, skillData and skillData.imageOffset or "0 0"
    end

    if perkType == PERK_PIERCE then
        local pierceData = PierceElementMask[perkData.DamageType]
        return imagePath, pierceData and pierceData.imageOffset or "0 0"
    end

    return imagePath, data.offset or "0 0"
end

-- Get bonus name and tooltip for a perk
function ProficiencyData:getBonusNameAndTooltip(perkData)
    local perkType = perkData.Type
    local data = PerkTextData[perkType]
    local value = self:formatFloatValue(perkData.Value, false, perkType)
    local bonusName = data and data.name or "Empty"

    if not data then
        return bonusName, "Empty"
    end

    if perkType == PERK_SPELL_AUGMENT then
        local spellData = SpellAugmentIcons[perkData.SpellId]
        local augmentData = AugmentPerkIcons[perkData.AugmentType]

        if spellData and augmentData then
            if perkData.AugmentType == AUGMENT_COOLDOWN then
                value = (perkData.Value or 0) / 1000
            else
                value = self:formatFloatValue(perkData.Value, true, perkType)
            end

            local description = string.format(augmentData.desc, value, spellData.name)
            return bonusName, description
        end
        return bonusName, "Unknown spell augment"
    end

    if perkType == PERK_BESTIARY_DAMAGE then
        local description = string.format(data.desc, value, perkData.BestiaryName or "Unknown")
        return bonusName, description
    end

    if perkType == PERK_MAGIC_BONUS then
        local elementData = MagicBoostMask[perkData.DamageType]
        local description = string.format(data.desc, value, elementData and elementData.name or "Unknown")
        return bonusName, description
    end

    if perkType == PERK_PERFECT_SHOT then
        local description = string.format(data.desc, value, perkData.Range or 1)
        return bonusName, description
    end

    if ElementalCritical_t[perkType] then
        local elementData = ElementalMask[perkData.ElementId]
        local description = string.format(data.desc, value, elementData and elementData.name or "Unknown")
        return bonusName, description
    end

    if FlatDamageBonus_t[perkType] then
        local skillData = SkillTypes[perkData.SkillId]
        local description = string.format(data.desc, value, skillData and skillData.name or "Unknown")
        return bonusName, description
    end

    if perkType == PERK_PIERCE then
        local pierceData = PierceElementMask[perkData.DamageType]
        local elementName = pierceData and pierceData.name or "Unknown"
        local description = string.format(data.desc, value, elementName)
        return bonusName, description
    end

    return bonusName, string.format(data.desc, value)
end

-- Get augment icon clip
function ProficiencyData:getAugmentIconClip(perkData)
    local augmentData = AugmentPerkIcons[perkData.AugmentType]
    if not augmentData then
        return "0 0"
    end
    return augmentData.imageOffset
end

-- Get current ceil experience for next level
function ProficiencyData:getCurrentCeilExperience(exp, displayItem, thingType, marketData)
    local best = nil
    local vocation = self:getWeaponProfessionType(displayItem, thingType, marketData)
    local lastExp = nil
    local proficiencyId = self:getProficiencyIdForItem(displayItem, thingType, marketData)
    local limitIndex = self:getPerkLaneCount(proficiencyId) + 2

    for index, stage in ipairs(ExperienceTable) do
        if index > limitIndex then
            break
        end

        local stageExp = stage[vocation]
        if stageExp then
            if stageExp > exp then
                if not best or stageExp < best then
                    best = stageExp
                end
            end
            lastExp = stageExp
        end
    end

    return best or lastExp or 0
end

-- Get max experience for a proficiency
function ProficiencyData:getMaxExperience(perkCount, displayItem, thingType, marketData)
    local vocation = self:getWeaponProfessionType(displayItem, thingType, marketData)
    local lastLevel = ExperienceTable[perkCount + 2]
    return (lastLevel and lastLevel[vocation]) or 0
end

-- Get level percent progress
function ProficiencyData:getLevelPercent(currentExperience, level, displayItem, thingType, marketData)
    local vocation = self:getWeaponProfessionType(displayItem, thingType, marketData)
    local prevLevel = math.max(level - 1, 0)
    local xpMin = prevLevel == 0 and 0 or (ExperienceTable[prevLevel] and ExperienceTable[prevLevel][vocation] or 0)
    local xpMax = (ExperienceTable[level] and ExperienceTable[level][vocation]) or xpMin + 1

    -- If xpMax is nil or invalid, return 0 to avoid showing 100%
    if not xpMax or xpMax <= xpMin then
        return 0
    end

    local progress = math.max(0, math.min(1, (currentExperience - xpMin) / (xpMax - xpMin)))
    local percent = math.floor(progress * 100)

    return percent
end

-- Get total progress percent
function ProficiencyData:getTotalPercent(currentExperience, perkCount, displayItem, thingType, marketData)
    local vocation = self:getWeaponProfessionType(displayItem, thingType, marketData)
    local maxExperience = (ExperienceTable[perkCount + 2] and ExperienceTable[perkCount + 2][vocation]) or 1
    local progress = math.max(0, math.min(1, currentExperience / maxExperience))
    return math.floor(progress * 100)
end

-- Get max experience by level
function ProficiencyData:getMaxExperienceByLevel(level, displayItem, thingType, marketData)
    local vocation = self:getWeaponProfessionType(displayItem, thingType, marketData)
    return (ExperienceTable[level] and ExperienceTable[level][vocation]) or 0
end

-- Get current level by experience
function ProficiencyData:getCurrentLevelByExp(displayItem, currentExperience, includeMastery, thingType, marketData)
    local vocation = self:getWeaponProfessionType(displayItem, thingType, marketData)
    local currentLevel = 0

    for level, data in pairs(ExperienceTable) do
        local requiredExp = data[vocation]
        if requiredExp and currentExperience >= requiredExp then
            if level > currentLevel then
                currentLevel = level
            end
        end
    end

    local level = math.min(7, currentLevel)
    if includeMastery then
        level = currentLevel
    end

    return level
end

-- Determine weapon profession type based on vocation and weapon type
function ProficiencyData:getWeaponProfessionType(displayItem, thingType, marketData)
    if not displayItem and not thingType and not marketData then
        return "regular"
    end

    marketData = marketData or (thingType and thingType.getMarketData and thingType:getMarketData()) or
                     (displayItem and displayItem.getMarketData and displayItem:getMarketData()) or {}

    local weaponType = 0
    if thingType and thingType.getWeaponType then
        weaponType = thingType:getWeaponType() or 0
    elseif displayItem and displayItem.getWeaponType then
        weaponType = displayItem:getWeaponType() or 0
    end

    local itemName = marketData.name
    if (not itemName or itemName == "") and thingType and thingType.getMarketData then
        local md = thingType:getMarketData()
        itemName = md and md.name
    end
    if (not itemName or itemName == "") and displayItem and displayItem.getName then
        itemName = displayItem:getName()
    end

    if weaponType == 9 or (itemName and string.find(string.lower(itemName), "crossbow", 1, true)) then
        return "crossbow"
    end

    local proficiencyId = self:getProficiencyIdForItem(displayItem, thingType, marketData)
    if proficiencyId and proficiencyId > 0 then
        local profEntry = self:getContentById(proficiencyId)
        if profEntry and profEntry.Category then
            return profEntry.Category
        end
    end

    -- Check for knight vocation restriction
    -- restrictVocation can be a number (bitmask) or a table
    if marketData.restrictVocation then
        local restrictVoc = marketData.restrictVocation
        if type(restrictVoc) == "table" then
            for _, vocationId in pairs(restrictVoc) do
                if vocationId == 1 then -- Knight
                    return "knight"
                end
            end
        elseif type(restrictVoc) == "number" then
            -- Check if knight bit is set (assuming knight = vocation 1)
            -- Bitmask: bit 0 = Knight (1), bit 1 = Paladin (2), etc.
            if bit32 then
                if bit32.band(restrictVoc, 1) ~= 0 then
                    return "knight"
                end
            else
                -- Fallback without bit32
                if restrictVoc % 2 == 1 then
                    return "knight"
                end
            end
        end
    end

    return "regular"
end
