---@class Townsfolk
local Townsfolk = QuestieLoader:CreateModule("Townsfolk")

---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type QuestieProfessions
local QuestieProfessions = QuestieLoader:ImportModule("QuestieProfessions")

local _, playerClass = UnitClassBase("player")
local playerFaction = UnitFactionGroup("player")

local tinsert = tinsert
local sub, bitband, strlen = string.sub, bit.band, string.len

local professionKeys = QuestieProfessions.professionKeys
local playerVendorKeys = {
    ["Ammo"] = true,
    ["Bags"] = true,
    ["Drink"] = true,
    ["Food"] = true,
    ["Pet Food"] = true,
    ["Poisons"] = true,
    ["Potions"] = true,
    ["Reagents"] = true,
    ["Trade Goods"] = true,
}


local function _reformatVendors(lst, existingTable)
    local newList = existingTable or {}
    for k in pairs(lst) do
        tinsert(newList, k)
    end
    return newList
end

---Uses a table to fetch multiple townfolk types at the same time.
---@param folkTypes table<string, {mask: NpcFlags|integer, requireSubname: boolean, data: NpcId[]}>
local function _PopulateTownsfolkTypes(folkTypes) -- populate the table with all npc ids based on the given bitmask
    local count = 0
    for id, npcData in pairs(QuestieDB.npcData) do
        local flags = npcData[QuestieDB.npcKeys.npcFlags]
        for name, folkType in pairs(folkTypes) do
            if flags and folkType.mask and bitband(flags, folkType.mask) == folkType.mask then
                local npcName = npcData[QuestieDB.npcKeys.name]
                local subName = npcData[QuestieDB.npcKeys.subName]
                if npcName and sub(npcName, 1, 5) ~= "[DND]" then
                    if (not folkType.requireSubname) or (subName and strlen(subName) > 1) then
                        folkType.data[#folkType.data+1] = id
                    end
                end
            end
        end
        if count > 700 then -- 700 seems like a good number
            count = 0
            coroutine.yield()
        end
        count = count + 1
    end
    return folkTypes
end


function Townsfolk.Initialize()

    --? This datastructure is used in PopulateTownsfolkTypes to fetch multiple townfolk data in the same npc loop cycle
    ---@type table<string, {mask: NpcFlags|integer, requireSubname: boolean, data: NpcId[]}>
    local townsfolkData = {
        ["Repair"] = {
            mask = QuestieDB.npcFlags.REPAIR,
            requireSubname = false,
            data = {}
        },
        ["Auctioneer"] = {
            mask = QuestieDB.npcFlags.AUCTIONEER,
            requireSubname = false,
            data = {}
        },
        ["Banker"] = {
            mask = QuestieDB.npcFlags.BANKER,
            requireSubname = true,
            data = {}
        },
        ["Battlemaster"] = {
            mask = QuestieDB.npcFlags.BATTLEMASTER,
            requireSubname = true,
            data = {}
        },
        ["Flight Master"] = {
            mask = QuestieDB.npcFlags.FLIGHT_MASTER,
            requireSubname = false,
            data = {}
        },
        ["Innkeeper"] = {
            mask = QuestieDB.npcFlags.INNKEEPER,
            requireSubname = false,
            data = {}
        },
        ["Stable Master"] = { -- Used further down by hunters.
            mask = QuestieDB.npcFlags.STABLEMASTER,
            requireSubname = false,
            data = {}
        },
        ["Spirit Healer"] = {-- Used further down
            mask = QuestieDB.npcFlags.SPIRIT_HEALER,
            requireSubname = false,
            data = {}
        }
    }
    _PopulateTownsfolkTypes(townsfolkData)

    local townfolk = {
        ["Repair"] = townsfolkData["Repair"].data,
        ["Auctioneer"] = townsfolkData["Auctioneer"].data,
        ["Banker"] = townsfolkData["Banker"].data,
        ["Battlemaster"] = townsfolkData["Battlemaster"].data,
        ["Barber"] = {},
        ["Flight Master"] = townsfolkData["Flight Master"].data,
        ["Innkeeper"] = townsfolkData["Innkeeper"].data,
        ["Riding Trainer"] = {},
        ["Weapon Master"] = {}, -- populated below
    }

    ---@type table<ProfessionEnum, NpcId[]>
    local professionTrainers = {
        [professionKeys.FIRST_AID] = {},
        [professionKeys.BLACKSMITHING] = {},
        [professionKeys.LEATHERWORKING] = {},
        [professionKeys.ALCHEMY] = {},
        [professionKeys.HERBALISM] = {},
        [professionKeys.COOKING] = {
            19186, -- Kylene <Barmaid> (this is an edge case)
        },
        [professionKeys.MINING] = {},
        [professionKeys.TAILORING] = {},
        [professionKeys.ENGINEERING] = {},
        [professionKeys.ENCHANTING] = {},
        [professionKeys.FISHING] = {},
        [professionKeys.SKINNING] = {}
    }

    if Questie.IsTBC or Questie.IsWotlk then
        professionTrainers[professionKeys.JEWELCRAFTING] = {}
    end

    if Questie.IsWotlk then
        professionTrainers[professionKeys.INSCRIPTION] = {}
    end

    local count = 0
    local validProfessionTrainers = Townsfolk.GetProfessionTrainers()
    for i=1, #validProfessionTrainers do
        local id = validProfessionTrainers[i]
        if QuestieDB.npcData[id] then
            local subName = QuestieDB.npcData[id][QuestieDB.npcKeys.subName]
            if subName then
                if townfolk[subName] then -- weapon master,
                    tinsert(townfolk[subName], id)
                else
                    for k, professionId in pairs(QuestieProfessions.professionTable) do
                        if string.match(subName, k) then
                            tinsert(professionTrainers[professionId], id)
                        end
                    end
                end
            end
        end

        if count > 10 then -- Yield every 10 iterations, 10 is just a madeup number, is pretty fast.
            count = 0
            coroutine.yield()
        end
        count = count + 1
    end

    local subNameTownsfolk = {
        ["Barber"] = "Barber",
        ["Riding Trainer"] = "Riding Trainer",
        ["Cold Weather Flying Trainer"] = "Riding Trainer",
    }
    local subNameTownsfolkIds = {}
    for _, key in pairs(subNameTownsfolk) do
        subNameTownsfolkIds[key] = {}
        for _, id in pairs(townfolk[key]) do
            subNameTownsfolkIds[key][id] = true
        end
    end

    count = 0
    for id, npcData in pairs(QuestieDB.npcData) do
        local subName = npcData[QuestieDB.npcKeys.subName]
        local key = subNameTownsfolk[subName]
        if key and npcData[QuestieDB.npcKeys.spawns] and not subNameTownsfolkIds[key][id] then
            subNameTownsfolkIds[key][id] = true
            tinsert(townfolk[key], id)
        end

        if count > 700 then
            count = 0
            coroutine.yield()
        end
        count = count + 1
    end

    -- Fix NPC Gubber Blump (10216) can train fishing profession
    tinsert(professionTrainers[professionKeys.FISHING], 10216)
    -- Fix NPC Aresella (18991) can train first aid profession
    if Questie.IsTBC or Questie.IsWotlk then
        tinsert(professionTrainers[professionKeys.FIRST_AID], 18991)
    end

    if Questie.IsClassic then
        -- Vendors selling "Expert First Aid - Under Wraps"
        tinsert(professionTrainers[professionKeys.FIRST_AID], 2805)
        tinsert(professionTrainers[professionKeys.FIRST_AID], 13476)
    end

    if Questie.IsWotlk or Questie.IsTBC then
        local meetingStones = Townsfolk.GetMeetingStones()

        townfolk["Meeting Stones"] = {}
        for _, id in pairs(meetingStones) do
            tinsert(townfolk["Meeting Stones"], id)
        end
    end

    -- todo: specialized trainer types (leatherworkers, engineers, etc)

    local classSpecificTownsfolk = {}
    local factionSpecificTownsfolk = {["Horde"] = {}, ["Alliance"] = {}}

    local classTrainers = Townsfolk.GetClassTrainers()
    for class, trainers in pairs(classTrainers) do
        local newTrainers = {}
        for _, trainer in pairs(trainers) do
            if QuestieDB.npcData[trainer] then
                tinsert(newTrainers, trainer)
            end
        end
        classSpecificTownsfolk[class] = {}
        classSpecificTownsfolk[class]["Class Trainer"] = newTrainers
    end

    -- These are filtered later, when the player class does not match
    classSpecificTownsfolk["HUNTER"]["Stable Master"] = townsfolkData["Stable Master"].data
    classSpecificTownsfolk["MAGE"]["Portal Trainer"] = {4165,2485,2489,5958,5957,2492,16654,16755,19340,20791,27703,27705}

    factionSpecificTownsfolk["Horde"]["Spirit Healer"]  = townsfolkData["Spirit Healer"].data
    factionSpecificTownsfolk["Alliance"]["Spirit Healer"]  = townsfolkData["Spirit Healer"].data

    local allianceMailBoxes, hordeMailBoxes = Townsfolk.GetFactionSpecificMailboxes()
    factionSpecificTownsfolk["Horde"]["Mailbox"] = hordeMailBoxes
    factionSpecificTownsfolk["Alliance"]["Mailbox"] = allianceMailBoxes

    local petFoodVendorTypes = {["Meat"] = {},["Fish"]={},["Cheese"]={},["Bread"]={},["Fungus"]={},["Fruit"]={},["Raw Meat"]={},["Raw Fish"]={}}
    local petFoodIndexes = {"Meat","Fish","Cheese","Bread","Fungus","Fruit","Raw Meat","Raw Fish"}

    count = 0
    for id, data in pairs(QuestieDB.itemData) do
        local foodType = data[QuestieDB.itemKeys.foodType]
        if foodType then
            tinsert(petFoodVendorTypes[petFoodIndexes[foodType]], id)
        end
        if count > 300 then -- Yield every 300 iterations, 300 is just a madeup number, is pretty fast.
            count = 0
            coroutine.yield()
        end
        count = count + 1
    end

    coroutine.yield()

    --- Set the globals
    Questie.db.global.townsfolk = townfolk
    Questie.db.global.townsfolkNeedsUpdatedGlobalVendors = true

    Questie.db.global.professionTrainers = professionTrainers
    Questie.db.global.classSpecificTownsfolk = classSpecificTownsfolk
    Questie.db.global.factionSpecificTownsfolk = factionSpecificTownsfolk
    Questie.db.global.petFoodVendorTypes = petFoodVendorTypes
end

function Townsfolk.PostBoot() -- post DB boot (use queries here)
    if Questie.db.global.townsfolkNeedsUpdatedGlobalVendors then
        Questie.db.global.townsfolkNeedsUpdatedGlobalVendors = nil
        -- insert item-based profession vendors
        _reformatVendors(Townsfolk:PopulateVendors({22012, 21992, 21993, 16084, 16112, 16113, 16085, 19442, 6454, 8547, 23689}), Questie.db.global.professionTrainers[professionKeys.FIRST_AID])
        _reformatVendors(Townsfolk:PopulateVendors({27532, 16082, 16083}), Questie.db.global.professionTrainers[professionKeys.FISHING])
        _reformatVendors(Townsfolk:PopulateVendors({27736, 16072, 16073}), Questie.db.global.professionTrainers[professionKeys.COOKING])
    end

    local reagents = {
        17031, 17032, 17020, -- MAGE
        17030, -- SHAMAN
        17029, 17028, -- PRIEST
        21177, 17033, -- PALADIN
        37201, -- DEATHKNIGHT
        5565, 16583, -- WARLOCK
        17034, 17026, 17035, 17021, 17038, 17036, 17037, -- DRUID
    }

    -- populate vendor IDs from db
    if #reagents > 0 then
        Questie.db.char.vendorList["Reagents"] = _reformatVendors(Townsfolk:PopulateVendors(reagents))
    end

    -- Beginning with WotLK, all poison vendors sell all ranks of poison, so Rank 1 of one poison is enough here
    local poisons = (Questie.IsWotlk or QuestieCompat.Is335) and {2892} or {5140,2928,8924,5173,2930,8923}
    Questie.db.char.vendorList["Poisons"] = _reformatVendors(Townsfolk:PopulateVendors(poisons))

    Questie.db.char.vendorList["Trade Goods"] = _reformatVendors(Townsfolk:PopulateVendors({ -- item ids from wowhead for trade goods   (temporarily disabled)
        14256,12810,13463,8845,8846,4234,3713,8170,14341,4389,3357,2453,13464,
        3355,3356,3358,4371,4304,5060,2319,18256,8925,3857,10940,2321,785,4404,2692,
        2605,3372,2320,6217,2449,4399,4364,10938,18567,4382,4289,765,3466,3371,2447,2880,
        2928,4361,10647,10648,4291,4357,8924,8343,4363,2678,5173,4400,2930,4342,2325,4340,
        6261,8923,2324,2604,6260,4378,10290,17194,4341
    }))
    Questie.db.char.vendorList["Bags"] = _reformatVendors(Townsfolk:PopulateVendors({4496, 4497, 4498, 4499, (Questie.IsTBC or Questie.IsWotlk) and 30744 or nil}))
    Questie.db.char.vendorList["Potions"] = _reformatVendors(Townsfolk:PopulateVendors({
        118, 858, 929, 1710, 3928, 13446, 18839, (Questie.IsTBC or Questie.IsWotlk) and 22829 or nil, (Questie.IsTBC or Questie.IsWotlk) and 32947 or nil, (Questie.IsWotlk) and 33447 or nil, -- Healing Potions
        2455, 3385, 3827, 6149, 13443, 13444, 18841, (Questie.IsTBC or Questie.IsWotlk) and 22832 or nil, (Questie.IsTBC or Questie.IsWotlk) and 32948 or nil, (Questie.IsWotlk) and 33448 or nil, -- Mana Potions
    }))
    Questie.db.char.vendorListInitialized = true
    Townsfolk:UpdatePlayerVendors()
end

function Townsfolk:BuildCharacterTownsfolk()
    Questie.db.char.townsfolk = {}
    Questie.db.char.vendorList = {}
    Questie.db.char.vendorListInitialized = nil
    Questie.db.char.townsfolkClass = select(2, UnitClassBase("player"))

    for key, npcs in pairs(Questie.db.global.factionSpecificTownsfolk[playerFaction]) do
        Questie.db.char.townsfolk[key] = npcs
    end

    for key, npcs in pairs(Questie.db.global.classSpecificTownsfolk[playerClass]) do
        Questie.db.char.townsfolk[key] = npcs
    end
end

local function _UpdatePetFood() -- call on change pet
    Questie.db.char.vendorList["Pet Food"] = {}
    -- detect petfood vendors for player's pet
    for _, key in pairs({GetStablePetFoodTypes(0)}) do
        if Questie.db.global.petFoodVendorTypes[key] then
            Townsfolk:PopulateVendors(Questie.db.global.petFoodVendorTypes[key], Questie.db.char.vendorList["Pet Food"], true)
        end
    end
    Questie.db.char.vendorList["Pet Food"] = _reformatVendors(Questie.db.char.vendorList["Pet Food"])
end

local function _UpdateAmmoVendors() -- call on change weapon
    Questie.db.char.vendorList["Ammo"] = _reformatVendors(Townsfolk:PopulateVendors({11285,3030,19316,2515,2512,11284,19317,2519,2516,3033,28056,28053,28061,28060}, {}, true))
end

local function _UpdateFoodDrink()
    local drink = {159,8766,1179,1708,1645,1205,17404,19300,19299,27860,28399,29395,29454,33042,32453,32455} -- water item ids
    local food = { -- food item ids (from wowhead)
        8932,4536,8952,19301,13724,8953,3927,11109,8957,4608,4599,4593,4592,117,3770,3771,4539,8950,8948,7228,
        2287,4601,422,16166,4537,4602,4542,4594,1707,4540,414,4538,4607,17119,19225,2070,21552,787,4544,18632,16167,4606,16170,
        4541,4605,17408,17406,11444,21033,22324,18635,21030,17407,19305,18633,4604,21031,16168,19306,16169,19304,17344,19224,19223,
        27857,27854,20857,27858,27856,29448,27855,29451,30355,28486,29450,29393,29394,29449,29452
    }

    Questie.db.char.vendorList["Food"] = _reformatVendors(Townsfolk:PopulateVendors(food, {}, true))
    Questie.db.char.vendorList["Drink"] = _reformatVendors(Townsfolk:PopulateVendors(drink, {}, true))
end

function Townsfolk:UpdatePlayerVendors() -- call on levelup
    if not Questie.db.char.vendorListInitialized then
        return
    end

    _UpdateFoodDrink()
    _UpdateAmmoVendors()

    if playerClass == "HUNTER" then
        _UpdatePetFood()
    end
end

function Townsfolk:IsVendorCategory(key)
    return playerVendorKeys[key] == true
end

function Townsfolk:EnsureVendorDataInitialized()
    if Questie.db.char.vendorListInitialized then
        if not Questie.db.char.vendorList["Ammo"] then
            Townsfolk:UpdatePlayerVendors()
        end
        return
    end

    Townsfolk.PostBoot()
end

function Townsfolk:PopulateVendors(itemList, existingTable, restrictLevel)
    local factionKey = playerFaction == "Alliance" and "A" or "H"
    local tbl = existingTable or {}
    -- Create a cache to minimize db calls
    local factionCache = {}
    local flagCache = {}

    local playerLevel = restrictLevel and UnitLevel("Player") or 0
    for _, id in pairs(itemList) do
        --print(id)
        local valid = true
        if restrictLevel then
            local requiredLevel = QuestieDB.QueryItemSingle(id, "requiredLevel")
            valid = (not requiredLevel) or (requiredLevel and requiredLevel <= playerLevel and requiredLevel >= playerLevel - 20)
        end
        if valid then
            local vendors = QuestieDB.QueryItemSingle(id, "vendors")
            if vendors then
                for i=1, #vendors do
                    local vendorId = vendors[i]
                    --print(vendorId)
                    local friendlyToFaction = factionCache[vendorId] or QuestieDB.QueryNPCSingle(vendorId, "friendlyToFaction")
                    if not factionCache[vendorId] then
                        factionCache[vendorId] = friendlyToFaction
                    end
                    if (not friendlyToFaction) or friendlyToFaction == "AH" or friendlyToFaction == factionKey then
                        local flags = flagCache[vendorId] or QuestieDB.QueryNPCSingle(vendorId, "npcFlags")
                        if not flagCache[vendorId] then
                            flagCache[vendorId] = flags
                        end
                        if bitband(flags, QuestieDB.npcFlags.VENDOR) == QuestieDB.npcFlags.VENDOR then
                            tbl[vendorId] = true
                        end
                    end
                end
            end
        end
    end
    return tbl
end

---@return number[], number[] -- Returns two lists of mailbox IDs, one for Alliance and one for Horde
function Townsfolk.GetFactionSpecificMailboxes()
    local allianceMailBoxes = {}
    local hordeMailBoxes = {}

    local mailboxes = Townsfolk.GetMailboxes()
    for i=1, #mailboxes do
        local id = mailboxes[i]
        if QuestieDB.objectData[id] then
            local factionID = QuestieDB.objectData[id][QuestieDB.objectKeys.factionID]

            if factionID == 0 then
                tinsert(hordeMailBoxes, id)
                tinsert(allianceMailBoxes, id)
            elseif QuestieDB.factionTemplate[factionID] and bitband(QuestieDB.factionTemplate[factionID], 12) == 0 and bitband(QuestieDB.factionTemplate[factionID], 10) == 0 then
                tinsert(hordeMailBoxes, id)
                tinsert(allianceMailBoxes, id)
            elseif QuestieDB.factionTemplate[factionID] and bitband(QuestieDB.factionTemplate[factionID], 12) == 0 then
                tinsert(hordeMailBoxes, id)
            elseif QuestieDB.factionTemplate[factionID] and bitband(QuestieDB.factionTemplate[factionID], 10) == 0 then
                tinsert(allianceMailBoxes, id)
            else
                tinsert(hordeMailBoxes, id)
                tinsert(allianceMailBoxes, id)
            end
        else
            Questie:Debug(Questie.DEBUG_DEVELOP, "Missing mailbox:", tostring(id))
        end
    end

    return allianceMailBoxes, hordeMailBoxes
end
